From 92de56e8c9cd0122f7c7703400911fbdace8482c Mon Sep 17 00:00:00 2001 From: Alexander John Lee <77119221+partylikeits1983@users.noreply.github.com> Date: Thu, 4 Jun 2026 17:50:53 -0400 Subject: [PATCH 1/2] feat: allow root-allowlisted tx scripts on network accounts (#3028) feat: allow root-allowlisted tx scripts on network accounts (#3028) AuthNetworkAccount previously banned every transaction script outright. This generalizes that ban into a tx-script-root allowlist that mirrors the existing note-script allowlist: a transaction that runs no tx script is allowed, and any tx script that does run must have its MAST root in an allowlist fixed at account creation. This lets network accounts run owner-approved scripts (e.g. setting the expiration delta, needed by the ntx-builder) while preserving the invariant that only root-pinned, pre-approved code can run against a signature-less account. Changes: - Add `TransactionScriptRoot` newtype mirroring `NoteScriptRoot`; `TransactionScript::root()` now returns it. - Add `tx_script_allowlist.masm` (`assert_tx_script_allowed`) and switch the network auth component to it; remove the now-unused `assert_no_tx_script`. - Add `NetworkAccountTxScriptAllowlist` storage (`allowed_tx_scripts` slot); an empty allowlist permits no tx scripts (the prior behavior). - Wire through `AuthNetworkAccount::with_allowed_tx_scripts` and `AuthMethod::NetworkAccount.allowed_tx_script_roots`. A root pins a script's code but not its `TX_SCRIPT_ARGS`/advice inputs, which an arbitrary submitter controls, so only scripts whose behavior is fixed regardless of those inputs should be allowlisted. Breaking: the component gains a second storage slot (existing network accounts must be recreated), and `TransactionScript::root()` returns `TransactionScriptRoot` instead of `Word`. Closes #3027. Co-authored-by: Philipp Gackstatter --- CHANGELOG.md | 7 + .../src/transaction/kernel/advice_inputs.rs | 4 +- crates/miden-protocol/src/transaction/mod.rs | 2 +- .../miden-protocol/src/transaction/tx_args.rs | 43 ++- .../auth/network_account.masm | 15 +- .../standards/auth/note_script_allowlist.masm | 25 +- .../standards/auth/tx_script_allowlist.masm | 66 ++++ .../miden-standards/src/account/auth/mod.rs | 2 + .../network_account/auth_network_account.rs | 102 +++++- .../src/account/auth/network_account/mod.rs | 6 + .../network_account/tx_script_allowlist.rs | 253 +++++++++++++ .../src/account/faucets/fungible/mod.rs | 6 +- .../src/account/faucets/fungible/tests.rs | 5 +- .../src/account/interface/component.rs | 6 +- crates/miden-standards/src/auth_method.rs | 7 +- .../src/kernel_tests/tx/test_prologue.rs | 2 +- crates/miden-testing/src/mock_chain/auth.rs | 11 +- .../src/mock_chain/chain_builder.rs | 12 +- .../agglayer/network_account_regression.rs | 10 +- .../tests/auth/network_account.rs | 343 ++++++++++++++---- crates/miden-testing/tests/scripts/faucet.rs | 1 + 21 files changed, 798 insertions(+), 130 deletions(-) create mode 100644 crates/miden-standards/asm/standards/auth/tx_script_allowlist.masm create mode 100644 crates/miden-standards/src/account/auth/network_account/tx_script_allowlist.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 2457f714b8..a4e6c96a15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,13 @@ - Fixed `pausable::assert_not_paused` to guard its storage read with `active_account::has_storage_slot`, making it a no-op on accounts without the `Pausable` component instead of panicking on the missing `is_paused` slot ([#3047](https://github.com/0xMiden/protocol/pull/3047)). - [BREAKING] Fixed batch ID being serialized/deserialized and potentially not matching the serialized transaction headers ([#3061](https://github.com/0xMiden/protocol/pull/3061)). +## v0.15.2 (TBD) + +### Changes + +- [BREAKING] `AuthNetworkAccount` now gates transaction scripts with a root allowlist instead of banning them outright, enabling network accounts to run approved tx scripts such as setting the expiration delta ([#3028](https://github.com/0xMiden/protocol/pull/3028)). +- [BREAKING] `TransactionScript::root()` now returns `TransactionScriptRoot` instead of `Word` ([#3028](https://github.com/0xMiden/protocol/pull/3028)). + ## v0.15.1 (TBD) ### Changes diff --git a/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs b/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs index cd0bdaeec0..6c692e1248 100644 --- a/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs +++ b/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs @@ -197,7 +197,9 @@ impl TransactionAdviceInputs { // --- number of notes, script root and args -------------------------- self.extend_stack([Felt::from(tx_inputs.input_notes().num_notes())]); let tx_args = tx_inputs.tx_args(); - self.extend_stack(tx_args.tx_script().map_or(Word::empty(), |script| script.root())); + self.extend_stack( + tx_args.tx_script().map_or(Word::empty(), |script| script.root().as_word()), + ); self.extend_stack(tx_args.tx_script_args()); // --- auth procedure args -------------------------------------------- diff --git a/crates/miden-protocol/src/transaction/mod.rs b/crates/miden-protocol/src/transaction/mod.rs index 864c90014b..8b036db3f2 100644 --- a/crates/miden-protocol/src/transaction/mod.rs +++ b/crates/miden-protocol/src/transaction/mod.rs @@ -33,7 +33,7 @@ pub use outputs::{ pub use partial_blockchain::PartialBlockchain; pub use proven_tx::{InputNoteCommitment, ProvenTransaction, TxAccountUpdate}; pub use transaction_id::TransactionId; -pub use tx_args::{TransactionArgs, TransactionScript}; +pub use tx_args::{TransactionArgs, TransactionScript, TransactionScriptRoot}; pub use tx_header::TransactionHeader; pub use tx_summary::TransactionSummary; pub use verifier::TransactionVerifier; diff --git a/crates/miden-protocol/src/transaction/tx_args.rs b/crates/miden-protocol/src/transaction/tx_args.rs index c18b2eb996..0ffc8eeec5 100644 --- a/crates/miden-protocol/src/transaction/tx_args.rs +++ b/crates/miden-protocol/src/transaction/tx_args.rs @@ -1,9 +1,12 @@ use alloc::collections::BTreeMap; +use alloc::string::String; use alloc::sync::Arc; use alloc::vec::Vec; +use core::fmt::Display; use miden_core::mast::MastNodeExt; use miden_crypto::merkle::InnerNodeInfo; +use miden_crypto_derive::WordWrapper; use miden_mast_package::Package; use super::{Felt, Hasher, Word}; @@ -276,6 +279,42 @@ impl Deserializable for TransactionArgs { } } +// TRANSACTION SCRIPT ROOT +// ================================================================================================ + +/// The MAST root of a [`TransactionScript`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, WordWrapper)] +pub struct TransactionScriptRoot(Word); + +impl From for Word { + fn from(root: TransactionScriptRoot) -> Self { + root.0 + } +} + +impl Display for TransactionScriptRoot { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + Display::fmt(&self.0, f) + } +} + +impl Serializable for TransactionScriptRoot { + fn write_into(&self, target: &mut W) { + target.write(self.0); + } + + fn get_size_hint(&self) -> usize { + self.0.get_size_hint() + } +} + +impl Deserializable for TransactionScriptRoot { + fn read_from(source: &mut R) -> Result { + let word: Word = source.read()?; + Ok(Self::from_raw(word)) + } +} + // TRANSACTION SCRIPT // ================================================================================================ @@ -334,8 +373,8 @@ impl TransactionScript { } /// Returns the commitment of this transaction script (i.e., the script's MAST root). - pub fn root(&self) -> Word { - self.mast[self.entrypoint].digest() + pub fn root(&self) -> TransactionScriptRoot { + TransactionScriptRoot::from_raw(self.mast[self.entrypoint].digest()) } /// Returns a new [TransactionScript] with the provided advice map entries merged into the diff --git a/crates/miden-standards/asm/account_components/auth/network_account.masm b/crates/miden-standards/asm/account_components/auth/network_account.masm index cd6b716a7f..0237e8996e 100644 --- a/crates/miden-standards/asm/account_components/auth/network_account.masm +++ b/crates/miden-standards/asm/account_components/auth/network_account.masm @@ -6,6 +6,7 @@ use miden::protocol::active_account use miden::protocol::native_account use miden::core::word use miden::standards::auth::note_script_allowlist +use miden::standards::auth::tx_script_allowlist # CONSTANTS # ================================================================================================= @@ -14,13 +15,18 @@ use miden::standards::auth::note_script_allowlist # (defined as Word); any non-empty value marks a root as allowed. const ALLOWED_NOTE_SCRIPTS_SLOT = word("miden::standards::auth::network_account::allowed_note_scripts") +# The slot holding the map of allowed tx script roots. Keys are tx script roots (defined as Word); +# any non-empty value marks a root as allowed. +const ALLOWED_TX_SCRIPTS_SLOT = word("miden::standards::auth::network_account::allowed_tx_scripts") + # AUTH PROCEDURE # ================================================================================================= #! Authenticates a transaction against an `AuthNetworkAccount` component. #! #! Enforces two invariants: -#! 1. No transaction script was executed in this transaction. +#! 1. The transaction script root, if any, must be present in the allowlist stored at +#! `ALLOWED_TX_SCRIPTS_SLOT` (a transaction that executed no tx script is always allowed). #! 2. Every consumed input note must have a script root present in the allowlist stored at #! `ALLOWED_NOTE_SCRIPTS_SLOT`. #! @@ -36,8 +42,11 @@ pub proc auth_network_transaction(auth_args: word) dropw # => [pad(16)] - # ---- Reject transactions that executed a tx script ---- - exec.note_script_allowlist::assert_no_tx_script + # ---- Reject any tx script whose root is not allowlisted ---- + push.ALLOWED_TX_SCRIPTS_SLOT[0..2] + # => [slot_id_suffix, slot_id_prefix, pad(16)] + + exec.tx_script_allowlist::assert_tx_script_allowed # => [pad(16)] # ---- Reject any input note whose script root is not allowlisted ---- diff --git a/crates/miden-standards/asm/standards/auth/note_script_allowlist.masm b/crates/miden-standards/asm/standards/auth/note_script_allowlist.masm index 32371667e1..3164c43324 100644 --- a/crates/miden-standards/asm/standards/auth/note_script_allowlist.masm +++ b/crates/miden-standards/asm/standards/auth/note_script_allowlist.masm @@ -1,11 +1,10 @@ -# Reusable note-script allowlist primitives. +# Reusable note-script allowlist primitive. # -# Provides two checks used to restrict what an account can do during a transaction: -# - `assert_no_tx_script` rejects transactions that executed a tx script. +# Provides one check used to restrict what an account can do during a transaction: # - `assert_all_input_notes_allowed` rejects transactions that consume an input note whose script # root is not present in a storage map at the given slot id. # -# These are designed to be composed into auth components. The caller owns the storage map and +# This is designed to be composed into auth components. The caller owns the storage map and # passes the slot id (suffix, prefix) so the same logic can back multiple components, each with # their own allowlist. @@ -17,29 +16,11 @@ use miden::core::word # ERRORS # ================================================================================================= -const ERR_NOTE_SCRIPT_ALLOWLIST_TX_SCRIPT_NOT_ALLOWED="a transaction script cannot be executed against an account guarded by a note script allowlist" const ERR_NOTE_SCRIPT_ALLOWLIST_NOTE_NOT_ALLOWED="input note script root is not in the note script allowlist" # PROCEDURES # ================================================================================================= -#! Asserts that no transaction script was executed in the current transaction. -#! -#! Inputs: [] -#! Outputs: [] -#! -#! Invocation: exec -pub proc assert_no_tx_script - exec.tx::get_tx_script_root - # => [TX_SCRIPT_ROOT] - - exec.word::eqz - # => [has_no_tx_script] - - assert.err=ERR_NOTE_SCRIPT_ALLOWLIST_TX_SCRIPT_NOT_ALLOWED - # => [] -end - #! Asserts that every input note consumed by this transaction has a script root present in the #! storage map at the given slot id. #! diff --git a/crates/miden-standards/asm/standards/auth/tx_script_allowlist.masm b/crates/miden-standards/asm/standards/auth/tx_script_allowlist.masm new file mode 100644 index 0000000000..e829d54ac0 --- /dev/null +++ b/crates/miden-standards/asm/standards/auth/tx_script_allowlist.masm @@ -0,0 +1,66 @@ +# Reusable tx-script allowlist primitive. +# +# Provides a single check used to restrict which transaction scripts an account will execute: +# - `assert_tx_script_allowed` accepts transactions that executed no tx script, and otherwise +# rejects transactions whose tx script root is not present in a storage map at the given slot id. +# +# This is designed to be composed into auth components. The caller owns the storage map and passes +# the slot id (suffix, prefix) so the same logic can back multiple components, each with their own +# allowlist. + +use miden::protocol::active_account +use miden::protocol::tx +use miden::core::word + +# ERRORS +# ================================================================================================= + +const ERR_TX_SCRIPT_ALLOWLIST_TX_SCRIPT_NOT_ALLOWED="transaction script root is not in the tx script allowlist" + +# PROCEDURES +# ================================================================================================= + +#! Asserts that the transaction script root is present in the storage map at the given slot id. +#! +#! A transaction that executed no tx script (empty root) is always allowed. Any other tx script must +#! have its root present in the allowlist. +#! +#! Map convention: keys are tx script roots (defined as Word), and any non-empty value marks a root +#! as allowed. Empty values (the default for absent keys) cause this procedure to fail. +#! +#! Inputs: [allowlist_slot_id_suffix, allowlist_slot_id_prefix] +#! Outputs: [] +#! +#! Where: +#! - allowlist_slot_id_{suffix, prefix} are the suffix and prefix felts of the slot identifier +#! pointing at the allowlist storage map. +#! +#! Invocation: exec +pub proc assert_tx_script_allowed + # => [slot_id_suffix, slot_id_prefix] + + exec.tx::get_tx_script_root + # => [TX_SCRIPT_ROOT, slot_id_suffix, slot_id_prefix] + + exec.word::testz + # => [no_tx_script, TX_SCRIPT_ROOT, slot_id_suffix, slot_id_prefix] + + if.true + # No tx script was executed, which is always allowed. + dropw drop drop + # => [] + else + movup.5 movup.5 + # => [slot_id_suffix, slot_id_prefix, TX_SCRIPT_ROOT] + + exec.active_account::get_map_item + # => [VALUE] + + exec.word::eqz not + # => [is_allowed] + + assert.err=ERR_TX_SCRIPT_ALLOWLIST_TX_SCRIPT_NOT_ALLOWED + # => [] + end + # => [] +end diff --git a/crates/miden-standards/src/account/auth/mod.rs b/crates/miden-standards/src/account/auth/mod.rs index e6caf01607..6bd23d4b63 100644 --- a/crates/miden-standards/src/account/auth/mod.rs +++ b/crates/miden-standards/src/account/auth/mod.rs @@ -22,4 +22,6 @@ pub use network_account::{ NetworkAccount, NetworkAccountNoteAllowlist, NetworkAccountNoteAllowlistError, + NetworkAccountTxScriptAllowlist, + NetworkAccountTxScriptAllowlistError, }; diff --git a/crates/miden-standards/src/account/auth/network_account/auth_network_account.rs b/crates/miden-standards/src/account/auth/network_account/auth_network_account.rs index 324c4efbbf..91a443f30f 100644 --- a/crates/miden-standards/src/account/auth/network_account/auth_network_account.rs +++ b/crates/miden-standards/src/account/auth/network_account/auth_network_account.rs @@ -9,8 +9,13 @@ use miden_protocol::account::component::{ }; use miden_protocol::account::{AccountComponent, AccountComponentName, StorageSlotName}; use miden_protocol::note::NoteScriptRoot; +use miden_protocol::transaction::TransactionScriptRoot; -use super::{NetworkAccountNoteAllowlist, NetworkAccountNoteAllowlistError}; +use super::{ + NetworkAccountNoteAllowlist, + NetworkAccountNoteAllowlistError, + NetworkAccountTxScriptAllowlist, +}; use crate::account::account_component_code; account_component_code!(NETWORK_ACCOUNT_AUTH_CODE, "auth/network_account.masl"); @@ -19,24 +24,44 @@ account_component_code!(NETWORK_ACCOUNT_AUTH_CODE, "auth/network_account.masl"); // ================================================================================================ /// An [`AccountComponent`] implementing an authentication scheme that restricts what notes an -/// account can consume to a fixed allowlist of note script roots, and forbids transaction scripts -/// from running against the account. +/// account can consume to a fixed allowlist of note script roots, and what transaction scripts may +/// run against the account to a fixed allowlist of tx script roots. /// /// This is intended for network-owned accounts (e.g. the AggLayer bridge or a network faucet) -/// whose only legitimate inputs are a known, finite set of system-issued notes. +/// whose only legitimate inputs are a known, finite set of system-issued notes and scripts. /// /// The component exports a single auth procedure, `auth_network_transaction`, that rejects the /// transaction unless: -/// - no transaction script was executed, and -/// - every consumed input note has a script root present in the component's allowlist. +/// - the transaction script root, if any, is present in the component's tx-script allowlist, and +/// - every consumed input note has a script root present in the component's note-script allowlist. +/// +/// Because a network account has no signature gate by default, a transaction script is an +/// unconstrained code path that could call the account's procedures directly. The tx-script +/// allowlist constrains this to a fixed set of owner-approved scripts; an empty tx-script allowlist +/// permits no transaction scripts at all. +/// +/// IMPORTANT: an allowlisted root pins a script's *code* (its MAST root), not the inputs it runs +/// on. A tx script still receives caller-controlled `TX_SCRIPT_ARGS` and advice-provider inputs, +/// and a note script receives caller-controlled `NOTE_ARGS`; on an open network account anyone can +/// supply those. A root should therefore only be allowlisted when the script's effect is safe for +/// *every* possible input. The canonical example is a tx script that sets the transaction +/// expiration delta to a hardcoded constant: its effect is fixed regardless of caller or inputs, +/// and the kernel only ever lets a script tighten the current transaction's expiration window +/// (never extend it), so the worst a caller can do is make their own transaction expire sooner. +/// Allowlisting a script whose effect depends on its inputs re-opens the very code path the +/// allowlist exists to constrain. /// -/// The allowlist is stored in the standardized [`NetworkAccountNoteAllowlist`] slot so off-chain -/// services can identify a network account by checking for this slot. +/// The note allowlist is stored in the standardized [`NetworkAccountNoteAllowlist`] slot so +/// off-chain services can identify a network account by checking for this slot. /// -/// The allowlist is fixed at account creation; there is intentionally no procedure to mutate it -/// after deployment. +/// Both allowlists are fixed at account creation: this component intentionally exports no procedure +/// to mutate them after deployment. That is a limitation of this component rather than a safety +/// requirement, and a user who needs a mutable allowlist can write their own component today. Note +/// that the node would likely not yet respect updates made to the list after deployment, but there +/// is in principle nothing preventing us from supporting mutation in the future. pub struct AuthNetworkAccount { allowlist: NetworkAccountNoteAllowlist, + tx_script_allowlist: NetworkAccountTxScriptAllowlist, } impl AuthNetworkAccount { @@ -65,28 +90,59 @@ impl AuthNetworkAccount { ) -> Result { Ok(Self { allowlist: NetworkAccountNoteAllowlist::new(allowed_script_roots)?, + tx_script_allowlist: NetworkAccountTxScriptAllowlist::default(), }) } + /// Sets the allowlist of transaction script roots this account will execute, replacing any + /// previously configured tx-script allowlist. + /// + /// An empty set (the default) means the account permits no transaction scripts. + /// + /// Only scripts whose effect is safe for every possible input should be allowlisted: a root + /// pins the script's code but not its `TX_SCRIPT_ARGS` or advice inputs, which the + /// (arbitrary) transaction submitter controls. See the [`AuthNetworkAccount`] type docs for + /// the full rationale. + pub fn with_allowed_tx_scripts( + mut self, + allowed_tx_script_roots: BTreeSet, + ) -> Self { + self.tx_script_allowlist = NetworkAccountTxScriptAllowlist::new(allowed_tx_script_roots); + self + } + /// Returns the storage slot holding the allowlist of allowed input-note script roots. pub fn allowed_note_scripts_slot() -> &'static StorageSlotName { NetworkAccountNoteAllowlist::slot_name() } - /// Returns the storage slot schema for the allowlist slot. + /// Returns the storage slot schema for the note-script allowlist slot. pub fn allowed_note_scripts_slot_schema() -> (StorageSlotName, StorageSlotSchema) { NetworkAccountNoteAllowlist::slot_schema() } + /// Returns the storage slot holding the allowlist of allowed transaction script roots. + pub fn allowed_tx_scripts_slot() -> &'static StorageSlotName { + NetworkAccountTxScriptAllowlist::slot_name() + } + + /// Returns the storage slot schema for the tx-script allowlist slot. + pub fn allowed_tx_scripts_slot_schema() -> (StorageSlotName, StorageSlotSchema) { + NetworkAccountTxScriptAllowlist::slot_schema() + } + /// Returns the [`AccountComponentMetadata`] for this component. pub fn component_metadata() -> AccountComponentMetadata { - let storage_schema = StorageSchema::new(vec![NetworkAccountNoteAllowlist::slot_schema()]) - .expect("storage schema should be valid"); + let storage_schema = StorageSchema::new(vec![ + NetworkAccountNoteAllowlist::slot_schema(), + NetworkAccountTxScriptAllowlist::slot_schema(), + ]) + .expect("storage schema should be valid"); AccountComponentMetadata::new(Self::NAME) .with_description( - "Authentication component that restricts input notes to a fixed allowlist of \ - note script roots and forbids tx scripts", + "Authentication component that restricts input notes and transaction scripts to \ + fixed allowlists of script roots", ) .with_storage_schema(storage_schema) } @@ -94,7 +150,10 @@ impl AuthNetworkAccount { impl From for AccountComponent { fn from(component: AuthNetworkAccount) -> Self { - let storage_slots = vec![component.allowlist.into_storage_slot()]; + let storage_slots = vec![ + component.allowlist.into_storage_slot(), + component.tx_script_allowlist.into_storage_slot(), + ]; let metadata = AuthNetworkAccount::component_metadata(); AccountComponent::new(AuthNetworkAccount::code().clone(), storage_slots, metadata).expect( @@ -144,11 +203,14 @@ mod tests { .into(); let storage_slots = component.storage_slots(); - assert_eq!(storage_slots.len(), 1); + assert_eq!(storage_slots.len(), 2); assert_eq!(storage_slots[0].name(), NetworkAccountNoteAllowlist::slot_name()); + assert_eq!(storage_slots[1].name(), NetworkAccountTxScriptAllowlist::slot_name()); - let StorageSlotContent::Map(_) = storage_slots[0].content() else { - panic!("allowlist slot must be a map"); - }; + for slot in storage_slots { + let StorageSlotContent::Map(_) = slot.content() else { + panic!("allowlist slots must be maps"); + }; + } } } diff --git a/crates/miden-standards/src/account/auth/network_account/mod.rs b/crates/miden-standards/src/account/auth/network_account/mod.rs index c3c773ca2a..d1362af9ab 100644 --- a/crates/miden-standards/src/account/auth/network_account/mod.rs +++ b/crates/miden-standards/src/account/auth/network_account/mod.rs @@ -7,3 +7,9 @@ pub use network_account::NetworkAccount; mod note_allowlist; pub use note_allowlist::{NetworkAccountNoteAllowlist, NetworkAccountNoteAllowlistError}; + +mod tx_script_allowlist; +pub use tx_script_allowlist::{ + NetworkAccountTxScriptAllowlist, + NetworkAccountTxScriptAllowlistError, +}; diff --git a/crates/miden-standards/src/account/auth/network_account/tx_script_allowlist.rs b/crates/miden-standards/src/account/auth/network_account/tx_script_allowlist.rs new file mode 100644 index 0000000000..ee2e325644 --- /dev/null +++ b/crates/miden-standards/src/account/auth/network_account/tx_script_allowlist.rs @@ -0,0 +1,253 @@ +use alloc::collections::BTreeSet; + +use miden_protocol::account::component::{SchemaType, StorageSlotSchema}; +use miden_protocol::account::{ + AccountStorage, + StorageMap, + StorageMapKey, + StorageSlot, + StorageSlotContent, + StorageSlotName, +}; +use miden_protocol::transaction::TransactionScriptRoot; +use miden_protocol::utils::sync::LazyLock; +use miden_protocol::{Felt, Word}; + +// CONSTANTS +// ================================================================================================ + +static SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::auth::network_account::allowed_tx_scripts") + .expect("storage slot name should be valid") +}); + +// A flag value used as the storage map entry for each allowed script root. Its only job is to be +// distinguishable from the storage map's default empty word, letting the MASM allowlist check +// detect "this key is present" without caring about its contents. Any non-empty word would serve; +// we pick `[1, 0, 0, 0]` for readability when inspecting storage. +const ALLOWED_FLAG: Word = Word::new([Felt::ONE, Felt::ZERO, Felt::ZERO, Felt::ZERO]); + +// NETWORK ACCOUNT TX SCRIPT ALLOWLIST +// ================================================================================================ + +/// A standardized storage slot holding the allowlist of transaction script roots that a network +/// account is willing to execute. +/// +/// A network account has no signature gate, so any transaction script that runs against it is a +/// code path the account owner must have pre-approved. This allowlist is that approval: a +/// transaction that executes no tx script is always allowed, but any other tx script must have its +/// root present here. An empty allowlist therefore reproduces the strictest behavior of permitting +/// no transaction scripts at all. +/// +/// A root pins the script's code but not its `TX_SCRIPT_ARGS` or advice inputs, which the +/// transaction submitter controls; only scripts whose effect is safe for every possible input +/// should be allowlisted (see the [`AuthNetworkAccount`](super::AuthNetworkAccount) docs). +/// +/// The slot is a [`StorageMap`] keyed by tx script root; any non-empty value marks a root as +/// allowed. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct NetworkAccountTxScriptAllowlist { + allowed_script_roots: BTreeSet, +} + +impl NetworkAccountTxScriptAllowlist { + /// Creates a new allowlist from the provided list of allowed transaction script roots. + /// + /// An empty set is permitted and means the account allows no transaction scripts. + pub fn new(allowed_script_roots: BTreeSet) -> Self { + Self { allowed_script_roots } + } + + /// Returns the [`StorageSlotName`] of the standardized allowlist slot. + pub fn slot_name() -> &'static StorageSlotName { + &SLOT_NAME + } + + /// Returns the allowed transaction script roots in this allowlist. + pub fn allowed_script_roots(&self) -> &BTreeSet { + &self.allowed_script_roots + } + + /// Consumes this allowlist and returns the allowed transaction script roots. + pub fn into_allowed_script_roots(self) -> BTreeSet { + self.allowed_script_roots + } + + /// Returns the schema entry for the allowlist slot. + pub fn slot_schema() -> (StorageSlotName, StorageSlotSchema) { + ( + Self::slot_name().clone(), + StorageSlotSchema::map( + "Allowed transaction script roots", + SchemaType::native_word(), + SchemaType::native_word(), + ), + ) + } + + /// Consumes this allowlist and returns the [`StorageSlot`] suitable for inclusion in an + /// [`AccountComponent`](miden_protocol::account::AccountComponent)'s storage layout. + pub fn into_storage_slot(self) -> StorageSlot { + let entries = self + .allowed_script_roots + .into_iter() + .map(|root| (StorageMapKey::new(root.as_word()), ALLOWED_FLAG)); + + let storage_map = StorageMap::with_entries(entries) + .expect("allowlist entries should produce a valid storage map"); + + StorageSlot::with_map(Self::slot_name().clone(), storage_map) + } +} + +// TRAIT IMPLEMENTATIONS +// ================================================================================================ + +impl TryFrom<&AccountStorage> for NetworkAccountTxScriptAllowlist { + type Error = NetworkAccountTxScriptAllowlistError; + + /// Reconstructs a [`NetworkAccountTxScriptAllowlist`] from account storage by reading the + /// allowlist slot and collecting its keys. + /// + /// # Errors + /// Returns an error if: + /// - The standardized allowlist slot is not present in storage. + /// - The slot is present but is not a [`StorageSlotContent::Map`]. + fn try_from(storage: &AccountStorage) -> Result { + let slot = storage + .get(Self::slot_name()) + .ok_or(NetworkAccountTxScriptAllowlistError::SlotNotFound)?; + + let StorageSlotContent::Map(map) = slot.content() else { + return Err(NetworkAccountTxScriptAllowlistError::UnexpectedSlotType); + }; + + // Only entries with a non-empty value mark a root as allowed, matching the MASM check + // (`word::eqz`), so the reconstructed view agrees with on-chain enforcement. + let allowed_script_roots = map + .entries() + .filter(|(_key, value)| **value != Word::empty()) + .map(|(key, _value)| TransactionScriptRoot::from_raw(key.as_word())) + .collect(); + + Ok(Self::new(allowed_script_roots)) + } +} + +// NETWORK ACCOUNT TX SCRIPT ALLOWLIST ERROR +// ================================================================================================ + +/// Errors that can occur when reconstructing a [`NetworkAccountTxScriptAllowlist`] from storage. +#[derive(Debug, thiserror::Error)] +pub enum NetworkAccountTxScriptAllowlistError { + #[error( + "network account tx script allowlist storage slot {} not found in account storage", + NetworkAccountTxScriptAllowlist::slot_name() + )] + SlotNotFound, + #[error( + "network account tx script allowlist storage slot {} must be a map", + NetworkAccountTxScriptAllowlist::slot_name() + )] + UnexpectedSlotType, +} + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + use miden_protocol::account::{AccountBuilder, StorageSlotContent}; + + use super::*; + use crate::account::auth::network_account::AuthNetworkAccount; + use crate::account::wallets::BasicWallet; + + #[test] + fn allowlist_storage_slot_contains_expected_entries() { + let root_a = TransactionScriptRoot::from_raw(Word::from([1, 2, 3, 4u32])); + let root_b = TransactionScriptRoot::from_raw(Word::from([5, 6, 7, 8u32])); + + let slot = NetworkAccountTxScriptAllowlist::new(BTreeSet::from_iter([root_a, root_b])) + .into_storage_slot(); + + assert_eq!(slot.name(), NetworkAccountTxScriptAllowlist::slot_name()); + + let StorageSlotContent::Map(map) = slot.content() else { + panic!("allowlist slot must be a map"); + }; + + assert_eq!( + map.get(&StorageMapKey::new(root_a.as_word())), + ALLOWED_FLAG, + "root_a should resolve to the flag value" + ); + assert_eq!( + map.get(&StorageMapKey::new(root_b.as_word())), + ALLOWED_FLAG, + "root_b should resolve to the flag value" + ); + } + + #[test] + fn empty_allowlist_is_allowed() { + let slot = NetworkAccountTxScriptAllowlist::new(BTreeSet::new()).into_storage_slot(); + let StorageSlotContent::Map(map) = slot.content() else { + panic!("allowlist slot must be a map"); + }; + assert_eq!(map.entries().count(), 0); + } + + #[test] + fn allowlist_round_trips_through_account_storage() { + let root_a = TransactionScriptRoot::from_raw(Word::from([1, 2, 3, 4u32])); + let root_b = TransactionScriptRoot::from_raw(Word::from([5, 6, 7, 8u32])); + let original_roots = BTreeSet::from_iter([root_a, root_b]); + + let account = AccountBuilder::new([0; 32]) + .with_auth_component( + AuthNetworkAccount::with_allowlist(BTreeSet::from_iter([ + miden_protocol::note::NoteScriptRoot::from_array([9, 9, 9, 9]), + ])) + .expect("non-empty note allowlist should construct") + .with_allowed_tx_scripts(original_roots.clone()), + ) + .with_component(BasicWallet) + .build() + .expect("account building with AuthNetworkAccount failed"); + + let allowlist = NetworkAccountTxScriptAllowlist::try_from(account.storage()) + .expect("allowlist should be reconstructable from account storage"); + + let actual: BTreeSet = + allowlist.allowed_script_roots().iter().copied().collect(); + + assert_eq!(actual, original_roots); + } + + #[test] + fn try_from_fails_when_slot_missing() { + // Storage that contains an unrelated slot but not the tx-script allowlist slot. + let other_slot = StorageSlot::with_value( + StorageSlotName::new("miden::standards::test::unrelated").expect("valid slot name"), + Word::empty(), + ); + let storage = AccountStorage::new(vec![other_slot]).expect("storage should be valid"); + + let result = NetworkAccountTxScriptAllowlist::try_from(&storage); + assert!(matches!(result, Err(NetworkAccountTxScriptAllowlistError::SlotNotFound))); + } + + #[test] + fn try_from_fails_when_slot_is_not_a_map() { + // The allowlist slot is present but is a value slot rather than the expected map. + let value_slot = StorageSlot::with_value( + NetworkAccountTxScriptAllowlist::slot_name().clone(), + Word::empty(), + ); + let storage = AccountStorage::new(vec![value_slot]).expect("storage should be valid"); + + let result = NetworkAccountTxScriptAllowlist::try_from(&storage); + assert!(matches!(result, Err(NetworkAccountTxScriptAllowlistError::UnexpectedSlotType))); + } +} diff --git a/crates/miden-standards/src/account/faucets/fungible/mod.rs b/crates/miden-standards/src/account/faucets/fungible/mod.rs index 9dd256bc2a..b540c05ce1 100644 --- a/crates/miden-standards/src/account/faucets/fungible/mod.rs +++ b/crates/miden-standards/src/account/faucets/fungible/mod.rs @@ -669,13 +669,17 @@ fn build_auth_component( // consumed against the faucet. ( AccessControl::Ownable2Step { .. } | AccessControl::Rbac { .. }, - AuthMethod::NetworkAccount { allowed_script_roots }, + AuthMethod::NetworkAccount { + allowed_script_roots, + allowed_tx_script_roots, + }, ) => Ok(AuthNetworkAccount::with_allowlist(allowed_script_roots) .map_err(|err| { FungibleFaucetError::UnsupportedAuthMethod(alloc::format!( "invalid network account allowlist: {err}" )) })? + .with_allowed_tx_scripts(allowed_tx_script_roots) .into()), // Ownable2Step / Rbac + NoAuth: valid; the setter gate is the in-procedure owner / diff --git a/crates/miden-standards/src/account/faucets/fungible/tests.rs b/crates/miden-standards/src/account/faucets/fungible/tests.rs index 7072e4003d..c7d1e155d6 100644 --- a/crates/miden-standards/src/account/faucets/fungible/tests.rs +++ b/crates/miden-standards/src/account/faucets/fungible/tests.rs @@ -196,7 +196,10 @@ fn auth_controlled_rejects_network_account() { [7u8; 32], sample_faucet(), AccountType::Private, - AuthMethod::NetworkAccount { allowed_script_roots }, + AuthMethod::NetworkAccount { + allowed_script_roots, + allowed_tx_script_roots: BTreeSet::new(), + }, AccessControl::AuthControlled, allow_all_policy_manager(), ) diff --git a/crates/miden-standards/src/account/interface/component.rs b/crates/miden-standards/src/account/interface/component.rs index a386fa0c0c..121e694274 100644 --- a/crates/miden-standards/src/account/interface/component.rs +++ b/crates/miden-standards/src/account/interface/component.rs @@ -14,6 +14,7 @@ use crate::account::auth::{ AuthSingleSig, AuthSingleSigAcl, NetworkAccountNoteAllowlist, + NetworkAccountTxScriptAllowlist, }; use crate::account::interface::AccountInterfaceError; @@ -411,9 +412,12 @@ fn extract_multisig_auth_method( /// Extracts authentication method from a network-account component. fn extract_network_account_auth_method(storage: &AccountStorage) -> AuthMethod { let allowlist = NetworkAccountNoteAllowlist::try_from(storage) - .expect("network account allowlist slot should be present and valid"); + .expect("network account note allowlist slot should be present and valid"); + let tx_script_allowlist = NetworkAccountTxScriptAllowlist::try_from(storage) + .expect("network account tx script allowlist slot should be present and valid"); AuthMethod::NetworkAccount { allowed_script_roots: allowlist.into_allowed_script_roots(), + allowed_tx_script_roots: tx_script_allowlist.into_allowed_script_roots(), } } diff --git a/crates/miden-standards/src/auth_method.rs b/crates/miden-standards/src/auth_method.rs index 1cb97350e7..86c0306e2a 100644 --- a/crates/miden-standards/src/auth_method.rs +++ b/crates/miden-standards/src/auth_method.rs @@ -3,6 +3,7 @@ use alloc::vec::Vec; use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment}; use miden_protocol::note::NoteScriptRoot; +use miden_protocol::transaction::TransactionScriptRoot; /// Defines standard authentication methods supported by account auth components. #[derive(Debug, Clone, PartialEq, Eq)] @@ -28,10 +29,12 @@ pub enum AuthMethod { /// An authentication method intended for network-owned accounts. /// /// It restricts the account to consuming only notes whose script roots are in - /// `allowed_script_roots`, and forbids transaction scripts from running against the account. - /// The allowlist must be non-empty. + /// `allowed_script_roots` (which must be non-empty), and to executing only transaction scripts + /// whose roots are in `allowed_tx_script_roots`. An empty `allowed_tx_script_roots` permits no + /// transaction scripts. NetworkAccount { allowed_script_roots: BTreeSet, + allowed_tx_script_roots: BTreeSet, }, /// A non-standard authentication method. Unknown, diff --git a/crates/miden-testing/src/kernel_tests/tx/test_prologue.rs b/crates/miden-testing/src/kernel_tests/tx/test_prologue.rs index 667eaea6fd..95ab4b236b 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_prologue.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_prologue.rs @@ -207,7 +207,7 @@ fn global_input_memory_assertions(exec_output: &ExecutionOutput, inputs: &Transa assert_eq!( exec_output.get_kernel_mem_word(TX_SCRIPT_ROOT_PTR), - inputs.tx_args().tx_script().as_ref().unwrap().root(), + inputs.tx_args().tx_script().as_ref().unwrap().root().as_word(), "The transaction script root should be stored at the TX_SCRIPT_ROOT_PTR" ); } diff --git a/crates/miden-testing/src/mock_chain/auth.rs b/crates/miden-testing/src/mock_chain/auth.rs index 7d08b5f890..032a0f59b5 100644 --- a/crates/miden-testing/src/mock_chain/auth.rs +++ b/crates/miden-testing/src/mock_chain/auth.rs @@ -8,6 +8,7 @@ use miden_protocol::account::auth::{AuthScheme, AuthSecretKey, PublicKeyCommitme use miden_protocol::account::{AccountComponent, AccountProcedureRoot}; use miden_protocol::note::NoteScriptRoot; use miden_protocol::testing::noop_auth_component::NoopAuthComponent; +use miden_protocol::transaction::TransactionScriptRoot; use miden_standards::account::auth::multisig_smart::ProcedurePolicy; use miden_standards::account::auth::{ AuthGuardedMultisig, @@ -83,9 +84,11 @@ pub enum Auth { Conditional, /// Network-account authentication that restricts the account to consuming only notes whose - /// script roots appear in `allowed_script_roots`. Must be non-empty. + /// script roots appear in `allowed_script_roots` (must be non-empty), and to executing only + /// transaction scripts whose roots appear in `allowed_tx_script_roots` (may be empty). NetworkAccount { allowed_script_roots: BTreeSet, + allowed_tx_script_roots: BTreeSet, }, } @@ -180,9 +183,13 @@ impl Auth { Auth::IncrNonce => (IncrNonceAuthComponent.into(), None), Auth::Noop => (NoopAuthComponent.into(), None), Auth::Conditional => (ConditionalAuthComponent.into(), None), - Auth::NetworkAccount { allowed_script_roots } => { + Auth::NetworkAccount { + allowed_script_roots, + allowed_tx_script_roots, + } => { let component = AuthNetworkAccount::with_allowlist(allowed_script_roots.clone()) .expect("network account allowlist must be non-empty") + .with_allowed_tx_scripts(allowed_tx_script_roots.clone()) .into(); (component, None) }, diff --git a/crates/miden-testing/src/mock_chain/chain_builder.rs b/crates/miden-testing/src/mock_chain/chain_builder.rs index ff146119c6..e3202a0b02 100644 --- a/crates/miden-testing/src/mock_chain/chain_builder.rs +++ b/crates/miden-testing/src/mock_chain/chain_builder.rs @@ -1,4 +1,4 @@ -use alloc::collections::BTreeMap; +use alloc::collections::{BTreeMap, BTreeSet}; use alloc::vec::Vec; use anyhow::Context; @@ -459,7 +459,10 @@ impl MockChainBuilder { .collect(); self.add_existing_fungible_faucet( - Auth::NetworkAccount { allowed_script_roots }, + Auth::NetworkAccount { + allowed_script_roots, + allowed_tx_script_roots: BTreeSet::new(), + }, faucet, AccountType::Public, AccessControl::Ownable2Step { owner: owner_account_id }, @@ -492,7 +495,10 @@ impl MockChainBuilder { .collect(); self.add_existing_fungible_faucet( - Auth::NetworkAccount { allowed_script_roots }, + Auth::NetworkAccount { + allowed_script_roots, + allowed_tx_script_roots: BTreeSet::new(), + }, faucet, AccountType::Public, AccessControl::Ownable2Step { owner: owner_account_id }, diff --git a/crates/miden-testing/tests/agglayer/network_account_regression.rs b/crates/miden-testing/tests/agglayer/network_account_regression.rs index a3bfa23ac0..6c97851245 100644 --- a/crates/miden-testing/tests/agglayer/network_account_regression.rs +++ b/crates/miden-testing/tests/agglayer/network_account_regression.rs @@ -24,7 +24,7 @@ use miden_protocol::transaction::RawOutputNote; use miden_standards::code_builder::CodeBuilder; use miden_standards::errors::standards::{ ERR_NOTE_SCRIPT_ALLOWLIST_NOTE_NOT_ALLOWED, - ERR_NOTE_SCRIPT_ALLOWLIST_TX_SCRIPT_NOT_ALLOWED, + ERR_TX_SCRIPT_ALLOWLIST_TX_SCRIPT_NOT_ALLOWED, }; use miden_standards::testing::note::NoteBuilder; use miden_testing::{Auth, MockChain, assert_transaction_executor_error}; @@ -38,7 +38,7 @@ end "; /// Asserts that a transaction submitting any tx script against a bridge account fails with -/// [`ERR_NOTE_SCRIPT_ALLOWLIST_TX_SCRIPT_NOT_ALLOWED`], even when the transaction also consumes +/// [`ERR_TX_SCRIPT_ALLOWLIST_TX_SCRIPT_NOT_ALLOWED`], even when the transaction also consumes /// an allowlisted input note (UPDATE_GER). This proves the tx-script check fires regardless of /// what allowlisted input notes accompany it — the two allowlist checks are independent. #[tokio::test] @@ -79,7 +79,7 @@ async fn bridge_rejects_tx_script() -> anyhow::Result<()> { .execute() .await; - assert_transaction_executor_error!(result, ERR_NOTE_SCRIPT_ALLOWLIST_TX_SCRIPT_NOT_ALLOWED); + assert_transaction_executor_error!(result, ERR_TX_SCRIPT_ALLOWLIST_TX_SCRIPT_NOT_ALLOWED); Ok(()) } @@ -126,7 +126,7 @@ async fn bridge_rejects_non_allowlisted_input_note() -> anyhow::Result<()> { } /// Asserts that a transaction submitting any tx script against an AggLayer faucet account fails -/// with [`ERR_NOTE_SCRIPT_ALLOWLIST_TX_SCRIPT_NOT_ALLOWED`]. Symmetric to +/// with [`ERR_TX_SCRIPT_ALLOWLIST_TX_SCRIPT_NOT_ALLOWED`]. Symmetric to /// [`bridge_rejects_tx_script`]: the faucet's [`AuthNetworkAccount`] allowlist (MINT, BURN) must /// reject every tx script, regardless of which input notes (if any) accompany it. /// @@ -162,7 +162,7 @@ async fn faucet_rejects_tx_script() -> anyhow::Result<()> { .execute() .await; - assert_transaction_executor_error!(result, ERR_NOTE_SCRIPT_ALLOWLIST_TX_SCRIPT_NOT_ALLOWED); + assert_transaction_executor_error!(result, ERR_TX_SCRIPT_ALLOWLIST_TX_SCRIPT_NOT_ALLOWED); Ok(()) } diff --git a/crates/miden-testing/tests/auth/network_account.rs b/crates/miden-testing/tests/auth/network_account.rs index e8f0fa8919..7106fb500d 100644 --- a/crates/miden-testing/tests/auth/network_account.rs +++ b/crates/miden-testing/tests/auth/network_account.rs @@ -1,36 +1,48 @@ use core::slice; +use std::collections::BTreeSet; -use miden_protocol::Word; use miden_protocol::account::{Account, AccountBuilder, AccountType}; -use miden_protocol::note::NoteScriptRoot; -use miden_protocol::transaction::RawOutputNote; +use miden_protocol::note::{Note, NoteScriptRoot}; +use miden_protocol::testing::account_id::ACCOUNT_ID_SENDER; +use miden_protocol::transaction::{RawOutputNote, TransactionScript, TransactionScriptRoot}; +use miden_protocol::{Felt, Word}; use miden_standards::account::auth::AuthNetworkAccount; use miden_standards::account::wallets::BasicWallet; use miden_standards::code_builder::CodeBuilder; use miden_standards::errors::standards::{ ERR_NOTE_SCRIPT_ALLOWLIST_NOTE_NOT_ALLOWED, - ERR_NOTE_SCRIPT_ALLOWLIST_TX_SCRIPT_NOT_ALLOWED, + ERR_TX_SCRIPT_ALLOWLIST_TX_SCRIPT_NOT_ALLOWED, }; use miden_standards::testing::note::NoteBuilder; use miden_testing::{MockChain, assert_transaction_executor_error}; +use rstest::rstest; // HELPER FUNCTIONS // ================================================================================================ -/// A placeholder script root used when a test needs an [`AuthNetworkAccount`] account whose -/// allowlist contents are not material to the test logic (e.g. for bootstrap accounts that only -/// exist to seed a [`NoteBuilder`]). The constructor rejects empty allowlists, so tests must -/// supply at least one root. +/// A placeholder note script root used when a test needs an [`AuthNetworkAccount`] account whose +/// allowlist contents are not material to the test logic (e.g. an account that never consumes a +/// note). The constructor rejects empty allowlists, so tests must supply at least one root. fn placeholder_script_root() -> Word { NoteScriptRoot::from_array([1, 0, 0, 0]).into() } /// Builds a minimal account that uses the [`AuthNetworkAccount`] auth component with the provided -/// allowlist of input-note script roots. +/// allowlist of input-note script roots and an empty tx-script allowlist. fn build_allowlist_account(allowed_script_roots: Vec) -> anyhow::Result { + build_account_with_allowlists(allowed_script_roots, Vec::new()) +} + +/// Builds a minimal account that uses the [`AuthNetworkAccount`] auth component with the provided +/// note-script and tx-script allowlists. +fn build_account_with_allowlists( + allowed_note_script_roots: Vec, + allowed_tx_script_roots: Vec, +) -> anyhow::Result { let auth_component = AuthNetworkAccount::with_allowlist( - allowed_script_roots.into_iter().map(NoteScriptRoot::from_raw).collect(), - )?; + allowed_note_script_roots.into_iter().map(NoteScriptRoot::from_raw).collect(), + )? + .with_allowed_tx_scripts(allowed_tx_script_roots.into_iter().collect::>()); Ok(AccountBuilder::new([0; 32]) .with_auth_component(auth_component) @@ -39,14 +51,59 @@ fn build_allowlist_account(allowed_script_roots: Vec) -> anyhow::Result anyhow::Result { + Ok(NoteBuilder::new(ACCOUNT_ID_SENDER.try_into()?, &mut rand::rng()).build()?) +} + +/// Compiles a transaction script that sets the transaction expiration delta to `delta`. This is the +/// canonical kind of tx script a network account would allowlist (see protocol issue #3027). +fn expiration_tx_script(delta: u16) -> TransactionScript { + let code = format!( + " + use miden::protocol::tx + + begin + push.{delta} + exec.tx::update_expiration_block_delta + end + " + ); + + CodeBuilder::default() + .compile_tx_script(code) + .expect("expiration tx script should compile") +} + +/// Compiles a transaction script that sets the expiration delta to the value the caller supplies in +/// the first element of `TX_SCRIPT_ARGS`, rather than baking it into the script. A single +/// allowlisted root therefore accepts any caller-chosen delta. At script entry the operand stack +/// holds `[TX_SCRIPT_ARGS, ..]`, so the top element is the delta; the remaining three arg elements +/// are dropped. +fn expiration_from_args_tx_script() -> TransactionScript { + let code = " + use miden::protocol::tx + + begin + exec.tx::update_expiration_block_delta + drop drop drop + end + "; + + CodeBuilder::default() + .compile_tx_script(code) + .expect("expiration-from-args tx script should compile") +} + // TESTS // ================================================================================================ -/// A transaction that executes a tx script must be rejected by `AuthNetworkAccount`, even if the -/// allowlist and input notes are otherwise valid. +/// A transaction that executes a tx script whose root is not in the tx-script allowlist must be +/// rejected by `AuthNetworkAccount`. An empty tx-script allowlist rejects every tx script. #[tokio::test] async fn test_auth_network_account_rejects_tx_script() -> anyhow::Result<()> { - // Allowlist contents don't matter — the tx-script check rejects before any allowlist lookup. + // Empty tx-script allowlist => no tx script is permitted. let account = build_allowlist_account(vec![placeholder_script_root()])?; let mut builder = MockChain::builder(); @@ -62,42 +119,220 @@ async fn test_auth_network_account_rejects_tx_script() -> anyhow::Result<()> { .execute() .await; - assert_transaction_executor_error!(result, ERR_NOTE_SCRIPT_ALLOWLIST_TX_SCRIPT_NOT_ALLOWED); + assert_transaction_executor_error!(result, ERR_TX_SCRIPT_ALLOWLIST_TX_SCRIPT_NOT_ALLOWED); Ok(()) } -/// A transaction that consumes a mix of allowed and disallowed input notes must be rejected: the -/// allowlist check must fail as soon as any single consumed note is not in the allowlist, even if -/// the others are. +/// A transaction that executes a tx script whose root IS in the tx-script allowlist must succeed, +/// and the script's effect (setting the expiration delta) must be reflected in the transaction. +/// +/// The transaction also consumes an allowlisted note, both because a network transaction does so in +/// practice and because the kernel rejects a transaction that neither changes the account state nor +/// consumes a note. #[tokio::test] -async fn test_auth_network_account_rejects_when_any_note_disallowed() -> anyhow::Result<()> { - // Build a template note with the default code to learn the "allowed" script root. The - // bootstrap account never executes a transaction, so its allowlist contents don't matter. - let bootstrap_account = build_allowlist_account(vec![placeholder_script_root()])?; - let template_allowed = NoteBuilder::new(bootstrap_account.id(), &mut rand::rng()) - .build() - .expect("failed to build template allowed note"); - let allowed_root = template_allowed.script().root(); +async fn test_auth_network_account_accepts_allowlisted_tx_script() -> anyhow::Result<()> { + const DELTA: u16 = 10; + let tx_script = expiration_tx_script(DELTA); + + let mut builder = MockChain::builder(); + let note = build_input_note()?; + builder.add_output_note(RawOutputNote::Full(note.clone())); + + // Allowlist the note script root and the expiration tx script's root. + let account = + build_account_with_allowlists(vec![note.script().root().into()], vec![tx_script.root()])?; + builder.add_account(account.clone())?; + + let mock_chain = builder.build()?; + + let executed = mock_chain + .build_tx_context(account.id(), &[], slice::from_ref(¬e))? + .tx_script(tx_script) + .build()? + .execute() + .await?; + + // The expiration delta script set the expiration to reference_block + DELTA. + let reference_block = executed.block_header().block_num(); + assert_eq!( + executed.expiration_block_num(), + reference_block + u32::from(DELTA), + "the allowlisted expiration script should have set the expiration block number", + ); + + Ok(()) +} + +/// A transaction that runs no tx script must be allowed regardless of the tx-script allowlist +/// contents: the empty-root case short-circuits before any allowlist lookup. +#[tokio::test] +async fn test_auth_network_account_allows_no_tx_script_with_non_empty_allowlist() +-> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let note = build_input_note()?; + builder.add_output_note(RawOutputNote::Full(note.clone())); + + // Non-empty tx-script allowlist, but the transaction below runs no tx script at all. + let account = build_account_with_allowlists( + vec![note.script().root().into()], + vec![expiration_tx_script(10).root()], + )?; + builder.add_account(account.clone())?; + + let mock_chain = builder.build()?; - // Build the real account with only that one root in the allowlist. - let account = build_allowlist_account(vec![allowed_root.into()])?; + mock_chain + .build_tx_context(account.id(), &[], slice::from_ref(¬e))? + .build()? + .execute() + .await?; + + Ok(()) +} + +/// A non-empty tx-script allowlist must still reject a tx script whose root is not in it. +#[tokio::test] +async fn test_auth_network_account_rejects_non_allowlisted_tx_script() -> anyhow::Result<()> { + // Allowlist the expiration script, then try to run a different (non-allowlisted) tx script. + let allowed_script = expiration_tx_script(10); + let account = build_account_with_allowlists( + vec![placeholder_script_root()], + vec![allowed_script.root()], + )?; let mut builder = MockChain::builder(); builder.add_account(account.clone())?; + let mock_chain = builder.build()?; + + let other_script = CodeBuilder::default().compile_tx_script("begin nop end")?; + assert_ne!( + other_script.root(), + allowed_script.root(), + "the other script must differ from the allowlisted one", + ); + + let result = mock_chain + .build_tx_context(account.id(), &[], &[])? + .tx_script(other_script) + .build()? + .execute() + .await; + + assert_transaction_executor_error!(result, ERR_TX_SCRIPT_ALLOWLIST_TX_SCRIPT_NOT_ALLOWED); + + Ok(()) +} + +/// Allowlisting several *distinct* tx-script roots must let a transaction run any one of them. Both +/// hardcoded expiration scripts (delta 10 and delta 30) are allowlisted, and running either one +/// end-to-end succeeds and produces the corresponding expiration block number. +#[rstest] +#[case(10)] +#[case(30)] +#[tokio::test] +async fn test_auth_network_account_accepts_any_of_multiple_allowlisted_roots( + #[case] delta: u16, +) -> anyhow::Result<()> { + let script_10 = expiration_tx_script(10); + let script_30 = expiration_tx_script(30); + let tx_script = expiration_tx_script(delta); + + let mut builder = MockChain::builder(); + let note = build_input_note()?; + builder.add_output_note(RawOutputNote::Full(note.clone())); + + // Allowlist the note root and both distinct expiration script roots. + let account = build_account_with_allowlists( + vec![note.script().root().into()], + vec![script_10.root(), script_30.root()], + )?; + builder.add_account(account.clone())?; + + let mock_chain = builder.build()?; + + let executed = mock_chain + .build_tx_context(account.id(), &[], slice::from_ref(¬e))? + .tx_script(tx_script) + .build()? + .execute() + .await?; - // Allowed note: uses the default note code so its script root matches `allowed_root`. - let note_allowed = NoteBuilder::new(account.id(), &mut rand::rng()) - .build() - .expect("failed to build allowed input note"); assert_eq!( - note_allowed.script().root(), - allowed_root, - "default-code NoteBuilder should reproduce the allowed script root", + executed.expiration_block_num(), + executed.block_header().block_num() + u32::from(delta), + "running one of several allowlisted scripts should set the expiration to reference + delta", ); - // Disallowed note: distinct code → distinct script root → not in the allowlist. - let note_disallowed = NoteBuilder::new(account.id(), &mut rand::rng()) + Ok(()) +} + +/// An allowlisted tx script may read caller-supplied `TX_SCRIPT_ARGS`: the allowlist pins the +/// script's *code* (its root), not its arguments. Here a single expiration-delta script that takes +/// the delta from `TX_SCRIPT_ARGS` is allowlisted, and transactions supplying different arbitrary +/// deltas all run end-to-end and produce the corresponding expiration block number. +/// +/// This is the input-dependent pattern the type docs caution against in general; it is acceptable +/// for the expiration delta specifically because the kernel only ever lets a script tighten the +/// expiration window, never extend it, so the worst an arbitrary caller can do is make their own +/// transaction expire sooner. Network accounts that want this (e.g. for the ntx-builder) can +/// allowlist such a script knowingly. +#[rstest] +#[case(10)] +#[case(30)] +#[case(u16::MAX)] +#[tokio::test] +async fn test_auth_network_account_accepts_allowlisted_tx_script_with_caller_args( + #[case] delta: u16, +) -> anyhow::Result<()> { + let tx_script = expiration_from_args_tx_script(); + + let mut builder = MockChain::builder(); + let note = build_input_note()?; + builder.add_output_note(RawOutputNote::Full(note.clone())); + + // Allowlist the note root and the single caller-parameterized expiration script. + let account = + build_account_with_allowlists(vec![note.script().root().into()], vec![tx_script.root()])?; + builder.add_account(account.clone())?; + + let mock_chain = builder.build()?; + + // The caller chooses the expiration delta via TX_SCRIPT_ARGS; the allowlist still permits it + // because the script's root is allowlisted regardless of its arguments. + let tx_script_args = Word::new([Felt::from(delta), Felt::ZERO, Felt::ZERO, Felt::ZERO]); + + let executed = mock_chain + .build_tx_context(account.id(), &[], slice::from_ref(¬e))? + .tx_script(tx_script) + .tx_script_args(tx_script_args) + .build()? + .execute() + .await?; + + assert_eq!( + executed.expiration_block_num(), + executed.block_header().block_num() + u32::from(delta), + "the caller-supplied expiration delta should be applied", + ); + + Ok(()) +} + +/// A transaction that consumes a mix of allowed and disallowed input notes must be rejected: the +/// allowlist check must fail as soon as any single consumed note is not in the allowlist, even if +/// the others are. +#[tokio::test] +async fn test_auth_network_account_rejects_when_any_note_disallowed() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + + // Allowed note: uses the default note code, so its script root is the one we allowlist. + let note_allowed = build_input_note()?; + let account = build_allowlist_account(vec![note_allowed.script().root().into()])?; + builder.add_account(account.clone())?; + + // Disallowed note: distinct code => distinct script root => not in the allowlist. + let note_disallowed = NoteBuilder::new(ACCOUNT_ID_SENDER.try_into()?, &mut rand::rng()) .code( "\ @note_script @@ -106,11 +341,10 @@ async fn test_auth_network_account_rejects_when_any_note_disallowed() -> anyhow: end ", ) - .build() - .expect("failed to build disallowed input note"); + .build()?; assert_ne!( note_disallowed.script().root(), - allowed_root, + note_allowed.script().root(), "disallowed note must have a different script root than the allowed one", ); @@ -134,31 +368,11 @@ async fn test_auth_network_account_rejects_when_any_note_disallowed() -> anyhow: /// Consuming an input note whose script root is in the allowlist must succeed. #[tokio::test] async fn test_auth_network_account_accepts_allowed_note() -> anyhow::Result<()> { - // First build a template note so we know its script root, then use that root to configure the - // account's allowlist. The bootstrap account never executes a transaction, so its allowlist - // contents don't matter. - let bootstrap_account = build_allowlist_account(vec![placeholder_script_root()])?; - let template_note = NoteBuilder::new(bootstrap_account.id(), &mut rand::rng()) - .build() - .expect("failed to build template note"); - let allowed_root = template_note.script().root(); - - // Now build the real account with the allowlist containing that root. - let account = build_allowlist_account(vec![allowed_root.into()])?; - let mut builder = MockChain::builder(); - builder.add_account(account.clone())?; - // Build a note that uses the same code but is sent from the real account so its script root - // matches `allowed_root`. - let note = NoteBuilder::new(account.id(), &mut rand::rng()) - .build() - .expect("failed to build input note"); - assert_eq!( - note.script().root(), - allowed_root, - "NoteBuilder with default code should produce a fixed script root" - ); + let note = build_input_note()?; + let account = build_allowlist_account(vec![note.script().root().into()])?; + builder.add_account(account.clone())?; builder.add_output_note(RawOutputNote::Full(note.clone())); let mock_chain = builder.build()?; @@ -167,8 +381,7 @@ async fn test_auth_network_account_accepts_allowed_note() -> anyhow::Result<()> .build_tx_context(account.id(), &[], slice::from_ref(¬e))? .build()? .execute() - .await - .expect("consuming an allowed note should succeed"); + .await?; Ok(()) } diff --git a/crates/miden-testing/tests/scripts/faucet.rs b/crates/miden-testing/tests/scripts/faucet.rs index 65b51b5875..06d0313ab1 100644 --- a/crates/miden-testing/tests/scripts/faucet.rs +++ b/crates/miden-testing/tests/scripts/faucet.rs @@ -2109,6 +2109,7 @@ fn build_network_faucet_with_blocklist_transfer( builder.add_account_from_builder( Auth::NetworkAccount { allowed_script_roots: BTreeSet::from([MintNote::script_root()]), + allowed_tx_script_roots: BTreeSet::new(), }, account_builder, AccountState::Exists, From 6f0a92263d77e98c6ba46063485f0ee69dfb0a8b Mon Sep 17 00:00:00 2001 From: Alexander John Lee <77119221+partylikeits1983@users.noreply.github.com> Date: Fri, 5 Jun 2026 01:14:41 -0400 Subject: [PATCH 2/2] refactor: rename network-account note allowlist API (#3049) * refactor: rename network-account note allowlist API --- CHANGELOG.md | 1 + crates/miden-agglayer/build.rs | 2 +- crates/miden-agglayer/src/lib.rs | 4 ++-- .../network_account/auth_network_account.rs | 22 +++++++++---------- .../auth/network_account/network_account.rs | 2 +- .../auth/network_account/note_allowlist.rs | 2 +- .../network_account/tx_script_allowlist.rs | 2 +- .../src/account/faucets/fungible/mod.rs | 2 +- crates/miden-testing/src/mock_chain/auth.rs | 9 ++++---- .../tests/auth/network_account.rs | 2 +- 10 files changed, 25 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4e6c96a15..a2bd91d89b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ - [BREAKING] `AuthNetworkAccount` now gates transaction scripts with a root allowlist instead of banning them outright, enabling network accounts to run approved tx scripts such as setting the expiration delta ([#3028](https://github.com/0xMiden/protocol/pull/3028)). - [BREAKING] `TransactionScript::root()` now returns `TransactionScriptRoot` instead of `Word` ([#3028](https://github.com/0xMiden/protocol/pull/3028)). +- Renamed `AuthNetworkAccount::with_allowlist` to `with_allowed_notes` and aligned the component's internal allowlist field names, for consistency with `with_allowed_tx_scripts` ([#3049](https://github.com/0xMiden/protocol/pull/3049)). ## v0.15.1 (TBD) diff --git a/crates/miden-agglayer/build.rs b/crates/miden-agglayer/build.rs index 7bced82a44..64a2334c4d 100644 --- a/crates/miden-agglayer/build.rs +++ b/crates/miden-agglayer/build.rs @@ -326,7 +326,7 @@ fn generate_agglayer_constants( // The allowlist lives in storage, not code, and here we only care about the code commitment // of the accounts, so we can init the allowlists with dummy values. let placeholder_allowlist = BTreeSet::from([NoteScriptRoot::from_raw(Word::default())]); - let auth_component = AuthNetworkAccount::with_allowlist(placeholder_allowlist) + let auth_component = AuthNetworkAccount::with_allowed_notes(placeholder_allowlist) .expect("placeholder allowlist is non-empty"); let mut components: Vec = vec![AccountComponent::from(auth_component), agglayer_component]; diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index 0c7ceed13d..19f35dd8ce 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -138,7 +138,7 @@ fn create_bridge_account_builder( .account_type(AccountType::Public) .with_component(AggLayerBridge::new(bridge_admin_id, ger_manager_id)) .with_auth_component( - AuthNetworkAccount::with_allowlist(AggLayerBridge::allowed_notes()) + AuthNetworkAccount::with_allowed_notes(AggLayerBridge::allowed_notes()) .expect("bridge note allowlist is non-empty"), ) } @@ -212,7 +212,7 @@ fn create_agglayer_faucet_builder( .with_components(token_policy_manager) .with_component(BurnAllowAll) .with_auth_component( - AuthNetworkAccount::with_allowlist(AggLayerFaucet::allowed_notes()) + AuthNetworkAccount::with_allowed_notes(AggLayerFaucet::allowed_notes()) .expect("faucet note allowlist is non-empty"), ) } diff --git a/crates/miden-standards/src/account/auth/network_account/auth_network_account.rs b/crates/miden-standards/src/account/auth/network_account/auth_network_account.rs index 91a443f30f..56322f66cc 100644 --- a/crates/miden-standards/src/account/auth/network_account/auth_network_account.rs +++ b/crates/miden-standards/src/account/auth/network_account/auth_network_account.rs @@ -60,8 +60,8 @@ account_component_code!(NETWORK_ACCOUNT_AUTH_CODE, "auth/network_account.masl"); /// that the node would likely not yet respect updates made to the list after deployment, but there /// is in principle nothing preventing us from supporting mutation in the future. pub struct AuthNetworkAccount { - allowlist: NetworkAccountNoteAllowlist, - tx_script_allowlist: NetworkAccountTxScriptAllowlist, + allowed_notes: NetworkAccountNoteAllowlist, + allowed_tx_scripts: NetworkAccountTxScriptAllowlist, } impl AuthNetworkAccount { @@ -85,12 +85,12 @@ impl AuthNetworkAccount { /// /// Returns an error if `allowed_script_roots` is empty since the account could not consume any /// notes. - pub fn with_allowlist( + pub fn with_allowed_notes( allowed_script_roots: BTreeSet, ) -> Result { Ok(Self { - allowlist: NetworkAccountNoteAllowlist::new(allowed_script_roots)?, - tx_script_allowlist: NetworkAccountTxScriptAllowlist::default(), + allowed_notes: NetworkAccountNoteAllowlist::new(allowed_script_roots)?, + allowed_tx_scripts: NetworkAccountTxScriptAllowlist::default(), }) } @@ -107,7 +107,7 @@ impl AuthNetworkAccount { mut self, allowed_tx_script_roots: BTreeSet, ) -> Self { - self.tx_script_allowlist = NetworkAccountTxScriptAllowlist::new(allowed_tx_script_roots); + self.allowed_tx_scripts = NetworkAccountTxScriptAllowlist::new(allowed_tx_script_roots); self } @@ -151,8 +151,8 @@ impl AuthNetworkAccount { impl From for AccountComponent { fn from(component: AuthNetworkAccount) -> Self { let storage_slots = vec![ - component.allowlist.into_storage_slot(), - component.tx_script_allowlist.into_storage_slot(), + component.allowed_notes.into_storage_slot(), + component.allowed_tx_scripts.into_storage_slot(), ]; let metadata = AuthNetworkAccount::component_metadata(); @@ -180,7 +180,7 @@ mod tests { let _account = AccountBuilder::new([0; 32]) .with_auth_component( - AuthNetworkAccount::with_allowlist(BTreeSet::from_iter([root_a, root_b])) + AuthNetworkAccount::with_allowed_notes(BTreeSet::from_iter([root_a, root_b])) .expect("non-empty allowlist should construct"), ) .with_component(BasicWallet) @@ -190,7 +190,7 @@ mod tests { #[test] fn auth_network_account_with_empty_allowlist_is_rejected() { - let result = AuthNetworkAccount::with_allowlist(BTreeSet::new()); + let result = AuthNetworkAccount::with_allowed_notes(BTreeSet::new()); assert!(matches!(result, Err(NetworkAccountNoteAllowlistError::EmptyAllowlist))); } @@ -198,7 +198,7 @@ mod tests { fn auth_network_account_uses_standardized_allowlist_slot() { let root_a = NoteScriptRoot::from_array([1, 2, 3, 4]); let component: AccountComponent = - AuthNetworkAccount::with_allowlist(BTreeSet::from_iter([root_a])) + AuthNetworkAccount::with_allowed_notes(BTreeSet::from_iter([root_a])) .expect("non-empty allowlist should construct") .into(); diff --git a/crates/miden-standards/src/account/auth/network_account/network_account.rs b/crates/miden-standards/src/account/auth/network_account/network_account.rs index dff81eae76..9f3e9aef7b 100644 --- a/crates/miden-standards/src/account/auth/network_account/network_account.rs +++ b/crates/miden-standards/src/account/auth/network_account/network_account.rs @@ -101,7 +101,7 @@ mod tests { AccountBuilder::new([0; 32]) .account_type(account_type) .with_auth_component( - AuthNetworkAccount::with_allowlist(roots).expect("non-empty allowlist"), + AuthNetworkAccount::with_allowed_notes(roots).expect("non-empty allowlist"), ) .with_component(BasicWallet) .build() diff --git a/crates/miden-standards/src/account/auth/network_account/note_allowlist.rs b/crates/miden-standards/src/account/auth/network_account/note_allowlist.rs index 68bb7519a2..5d458e39ff 100644 --- a/crates/miden-standards/src/account/auth/network_account/note_allowlist.rs +++ b/crates/miden-standards/src/account/auth/network_account/note_allowlist.rs @@ -217,7 +217,7 @@ mod tests { let account = AccountBuilder::new([0; 32]) .with_auth_component( - AuthNetworkAccount::with_allowlist(original_roots.clone()) + AuthNetworkAccount::with_allowed_notes(original_roots.clone()) .expect("non-empty allowlist should construct"), ) .with_component(BasicWallet) diff --git a/crates/miden-standards/src/account/auth/network_account/tx_script_allowlist.rs b/crates/miden-standards/src/account/auth/network_account/tx_script_allowlist.rs index ee2e325644..58d78b6340 100644 --- a/crates/miden-standards/src/account/auth/network_account/tx_script_allowlist.rs +++ b/crates/miden-standards/src/account/auth/network_account/tx_script_allowlist.rs @@ -206,7 +206,7 @@ mod tests { let account = AccountBuilder::new([0; 32]) .with_auth_component( - AuthNetworkAccount::with_allowlist(BTreeSet::from_iter([ + AuthNetworkAccount::with_allowed_notes(BTreeSet::from_iter([ miden_protocol::note::NoteScriptRoot::from_array([9, 9, 9, 9]), ])) .expect("non-empty note allowlist should construct") diff --git a/crates/miden-standards/src/account/faucets/fungible/mod.rs b/crates/miden-standards/src/account/faucets/fungible/mod.rs index b540c05ce1..7f1fcd42c9 100644 --- a/crates/miden-standards/src/account/faucets/fungible/mod.rs +++ b/crates/miden-standards/src/account/faucets/fungible/mod.rs @@ -673,7 +673,7 @@ fn build_auth_component( allowed_script_roots, allowed_tx_script_roots, }, - ) => Ok(AuthNetworkAccount::with_allowlist(allowed_script_roots) + ) => Ok(AuthNetworkAccount::with_allowed_notes(allowed_script_roots) .map_err(|err| { FungibleFaucetError::UnsupportedAuthMethod(alloc::format!( "invalid network account allowlist: {err}" diff --git a/crates/miden-testing/src/mock_chain/auth.rs b/crates/miden-testing/src/mock_chain/auth.rs index 032a0f59b5..3c79e25c1d 100644 --- a/crates/miden-testing/src/mock_chain/auth.rs +++ b/crates/miden-testing/src/mock_chain/auth.rs @@ -187,10 +187,11 @@ impl Auth { allowed_script_roots, allowed_tx_script_roots, } => { - let component = AuthNetworkAccount::with_allowlist(allowed_script_roots.clone()) - .expect("network account allowlist must be non-empty") - .with_allowed_tx_scripts(allowed_tx_script_roots.clone()) - .into(); + let component = + AuthNetworkAccount::with_allowed_notes(allowed_script_roots.clone()) + .expect("network account allowlist must be non-empty") + .with_allowed_tx_scripts(allowed_tx_script_roots.clone()) + .into(); (component, None) }, } diff --git a/crates/miden-testing/tests/auth/network_account.rs b/crates/miden-testing/tests/auth/network_account.rs index 7106fb500d..3a38009349 100644 --- a/crates/miden-testing/tests/auth/network_account.rs +++ b/crates/miden-testing/tests/auth/network_account.rs @@ -39,7 +39,7 @@ fn build_account_with_allowlists( allowed_note_script_roots: Vec, allowed_tx_script_roots: Vec, ) -> anyhow::Result { - let auth_component = AuthNetworkAccount::with_allowlist( + let auth_component = AuthNetworkAccount::with_allowed_notes( allowed_note_script_roots.into_iter().map(NoteScriptRoot::from_raw).collect(), )? .with_allowed_tx_scripts(allowed_tx_script_roots.into_iter().collect::>());