Skip to content

Commit 92de56e

Browse files
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 <PhilippGackstatter@users.noreply.github.com>
1 parent 5bc1655 commit 92de56e

21 files changed

Lines changed: 798 additions & 130 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@
2626
- 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)).
2727
- [BREAKING] Fixed batch ID being serialized/deserialized and potentially not matching the serialized transaction headers ([#3061](https://github.com/0xMiden/protocol/pull/3061)).
2828

29+
## v0.15.2 (TBD)
30+
31+
### Changes
32+
33+
- [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)).
34+
- [BREAKING] `TransactionScript::root()` now returns `TransactionScriptRoot` instead of `Word` ([#3028](https://github.com/0xMiden/protocol/pull/3028)).
35+
2936
## v0.15.1 (TBD)
3037

3138
### Changes

crates/miden-protocol/src/transaction/kernel/advice_inputs.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,9 @@ impl TransactionAdviceInputs {
197197
// --- number of notes, script root and args --------------------------
198198
self.extend_stack([Felt::from(tx_inputs.input_notes().num_notes())]);
199199
let tx_args = tx_inputs.tx_args();
200-
self.extend_stack(tx_args.tx_script().map_or(Word::empty(), |script| script.root()));
200+
self.extend_stack(
201+
tx_args.tx_script().map_or(Word::empty(), |script| script.root().as_word()),
202+
);
201203
self.extend_stack(tx_args.tx_script_args());
202204

203205
// --- auth procedure args --------------------------------------------

crates/miden-protocol/src/transaction/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ pub use outputs::{
3333
pub use partial_blockchain::PartialBlockchain;
3434
pub use proven_tx::{InputNoteCommitment, ProvenTransaction, TxAccountUpdate};
3535
pub use transaction_id::TransactionId;
36-
pub use tx_args::{TransactionArgs, TransactionScript};
36+
pub use tx_args::{TransactionArgs, TransactionScript, TransactionScriptRoot};
3737
pub use tx_header::TransactionHeader;
3838
pub use tx_summary::TransactionSummary;
3939
pub use verifier::TransactionVerifier;

crates/miden-protocol/src/transaction/tx_args.rs

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
use alloc::collections::BTreeMap;
2+
use alloc::string::String;
23
use alloc::sync::Arc;
34
use alloc::vec::Vec;
5+
use core::fmt::Display;
46

57
use miden_core::mast::MastNodeExt;
68
use miden_crypto::merkle::InnerNodeInfo;
9+
use miden_crypto_derive::WordWrapper;
710
use miden_mast_package::Package;
811

912
use super::{Felt, Hasher, Word};
@@ -276,6 +279,42 @@ impl Deserializable for TransactionArgs {
276279
}
277280
}
278281

282+
// TRANSACTION SCRIPT ROOT
283+
// ================================================================================================
284+
285+
/// The MAST root of a [`TransactionScript`].
286+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, WordWrapper)]
287+
pub struct TransactionScriptRoot(Word);
288+
289+
impl From<TransactionScriptRoot> for Word {
290+
fn from(root: TransactionScriptRoot) -> Self {
291+
root.0
292+
}
293+
}
294+
295+
impl Display for TransactionScriptRoot {
296+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
297+
Display::fmt(&self.0, f)
298+
}
299+
}
300+
301+
impl Serializable for TransactionScriptRoot {
302+
fn write_into<W: ByteWriter>(&self, target: &mut W) {
303+
target.write(self.0);
304+
}
305+
306+
fn get_size_hint(&self) -> usize {
307+
self.0.get_size_hint()
308+
}
309+
}
310+
311+
impl Deserializable for TransactionScriptRoot {
312+
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
313+
let word: Word = source.read()?;
314+
Ok(Self::from_raw(word))
315+
}
316+
}
317+
279318
// TRANSACTION SCRIPT
280319
// ================================================================================================
281320

@@ -334,8 +373,8 @@ impl TransactionScript {
334373
}
335374

336375
/// Returns the commitment of this transaction script (i.e., the script's MAST root).
337-
pub fn root(&self) -> Word {
338-
self.mast[self.entrypoint].digest()
376+
pub fn root(&self) -> TransactionScriptRoot {
377+
TransactionScriptRoot::from_raw(self.mast[self.entrypoint].digest())
339378
}
340379

341380
/// Returns a new [TransactionScript] with the provided advice map entries merged into the

crates/miden-standards/asm/account_components/auth/network_account.masm

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use miden::protocol::active_account
66
use miden::protocol::native_account
77
use miden::core::word
88
use miden::standards::auth::note_script_allowlist
9+
use miden::standards::auth::tx_script_allowlist
910

1011
# CONSTANTS
1112
# =================================================================================================
@@ -14,13 +15,18 @@ use miden::standards::auth::note_script_allowlist
1415
# (defined as Word); any non-empty value marks a root as allowed.
1516
const ALLOWED_NOTE_SCRIPTS_SLOT = word("miden::standards::auth::network_account::allowed_note_scripts")
1617

18+
# The slot holding the map of allowed tx script roots. Keys are tx script roots (defined as Word);
19+
# any non-empty value marks a root as allowed.
20+
const ALLOWED_TX_SCRIPTS_SLOT = word("miden::standards::auth::network_account::allowed_tx_scripts")
21+
1722
# AUTH PROCEDURE
1823
# =================================================================================================
1924

2025
#! Authenticates a transaction against an `AuthNetworkAccount` component.
2126
#!
2227
#! Enforces two invariants:
23-
#! 1. No transaction script was executed in this transaction.
28+
#! 1. The transaction script root, if any, must be present in the allowlist stored at
29+
#! `ALLOWED_TX_SCRIPTS_SLOT` (a transaction that executed no tx script is always allowed).
2430
#! 2. Every consumed input note must have a script root present in the allowlist stored at
2531
#! `ALLOWED_NOTE_SCRIPTS_SLOT`.
2632
#!
@@ -36,8 +42,11 @@ pub proc auth_network_transaction(auth_args: word)
3642
dropw
3743
# => [pad(16)]
3844

39-
# ---- Reject transactions that executed a tx script ----
40-
exec.note_script_allowlist::assert_no_tx_script
45+
# ---- Reject any tx script whose root is not allowlisted ----
46+
push.ALLOWED_TX_SCRIPTS_SLOT[0..2]
47+
# => [slot_id_suffix, slot_id_prefix, pad(16)]
48+
49+
exec.tx_script_allowlist::assert_tx_script_allowed
4150
# => [pad(16)]
4251

4352
# ---- Reject any input note whose script root is not allowlisted ----

crates/miden-standards/asm/standards/auth/note_script_allowlist.masm

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
# Reusable note-script allowlist primitives.
1+
# Reusable note-script allowlist primitive.
22
#
3-
# Provides two checks used to restrict what an account can do during a transaction:
4-
# - `assert_no_tx_script` rejects transactions that executed a tx script.
3+
# Provides one check used to restrict what an account can do during a transaction:
54
# - `assert_all_input_notes_allowed` rejects transactions that consume an input note whose script
65
# root is not present in a storage map at the given slot id.
76
#
8-
# These are designed to be composed into auth components. The caller owns the storage map and
7+
# This is designed to be composed into auth components. The caller owns the storage map and
98
# passes the slot id (suffix, prefix) so the same logic can back multiple components, each with
109
# their own allowlist.
1110

@@ -17,29 +16,11 @@ use miden::core::word
1716
# ERRORS
1817
# =================================================================================================
1918

20-
const ERR_NOTE_SCRIPT_ALLOWLIST_TX_SCRIPT_NOT_ALLOWED="a transaction script cannot be executed against an account guarded by a note script allowlist"
2119
const ERR_NOTE_SCRIPT_ALLOWLIST_NOTE_NOT_ALLOWED="input note script root is not in the note script allowlist"
2220

2321
# PROCEDURES
2422
# =================================================================================================
2523

26-
#! Asserts that no transaction script was executed in the current transaction.
27-
#!
28-
#! Inputs: []
29-
#! Outputs: []
30-
#!
31-
#! Invocation: exec
32-
pub proc assert_no_tx_script
33-
exec.tx::get_tx_script_root
34-
# => [TX_SCRIPT_ROOT]
35-
36-
exec.word::eqz
37-
# => [has_no_tx_script]
38-
39-
assert.err=ERR_NOTE_SCRIPT_ALLOWLIST_TX_SCRIPT_NOT_ALLOWED
40-
# => []
41-
end
42-
4324
#! Asserts that every input note consumed by this transaction has a script root present in the
4425
#! storage map at the given slot id.
4526
#!
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Reusable tx-script allowlist primitive.
2+
#
3+
# Provides a single check used to restrict which transaction scripts an account will execute:
4+
# - `assert_tx_script_allowed` accepts transactions that executed no tx script, and otherwise
5+
# rejects transactions whose tx script root is not present in a storage map at the given slot id.
6+
#
7+
# This is designed to be composed into auth components. The caller owns the storage map and passes
8+
# the slot id (suffix, prefix) so the same logic can back multiple components, each with their own
9+
# allowlist.
10+
11+
use miden::protocol::active_account
12+
use miden::protocol::tx
13+
use miden::core::word
14+
15+
# ERRORS
16+
# =================================================================================================
17+
18+
const ERR_TX_SCRIPT_ALLOWLIST_TX_SCRIPT_NOT_ALLOWED="transaction script root is not in the tx script allowlist"
19+
20+
# PROCEDURES
21+
# =================================================================================================
22+
23+
#! Asserts that the transaction script root is present in the storage map at the given slot id.
24+
#!
25+
#! A transaction that executed no tx script (empty root) is always allowed. Any other tx script must
26+
#! have its root present in the allowlist.
27+
#!
28+
#! Map convention: keys are tx script roots (defined as Word), and any non-empty value marks a root
29+
#! as allowed. Empty values (the default for absent keys) cause this procedure to fail.
30+
#!
31+
#! Inputs: [allowlist_slot_id_suffix, allowlist_slot_id_prefix]
32+
#! Outputs: []
33+
#!
34+
#! Where:
35+
#! - allowlist_slot_id_{suffix, prefix} are the suffix and prefix felts of the slot identifier
36+
#! pointing at the allowlist storage map.
37+
#!
38+
#! Invocation: exec
39+
pub proc assert_tx_script_allowed
40+
# => [slot_id_suffix, slot_id_prefix]
41+
42+
exec.tx::get_tx_script_root
43+
# => [TX_SCRIPT_ROOT, slot_id_suffix, slot_id_prefix]
44+
45+
exec.word::testz
46+
# => [no_tx_script, TX_SCRIPT_ROOT, slot_id_suffix, slot_id_prefix]
47+
48+
if.true
49+
# No tx script was executed, which is always allowed.
50+
dropw drop drop
51+
# => []
52+
else
53+
movup.5 movup.5
54+
# => [slot_id_suffix, slot_id_prefix, TX_SCRIPT_ROOT]
55+
56+
exec.active_account::get_map_item
57+
# => [VALUE]
58+
59+
exec.word::eqz not
60+
# => [is_allowed]
61+
62+
assert.err=ERR_TX_SCRIPT_ALLOWLIST_TX_SCRIPT_NOT_ALLOWED
63+
# => []
64+
end
65+
# => []
66+
end

crates/miden-standards/src/account/auth/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,6 @@ pub use network_account::{
2222
NetworkAccount,
2323
NetworkAccountNoteAllowlist,
2424
NetworkAccountNoteAllowlistError,
25+
NetworkAccountTxScriptAllowlist,
26+
NetworkAccountTxScriptAllowlistError,
2527
};

0 commit comments

Comments
 (0)