Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
- Clarified Miden's operational roles on the architecture overview page and linked them from the note lifecycle ([#3017](https://github.com/0xMiden/protocol/pull/3017)).
- [BREAKING] Unified the fungible and non-fungible asset vault deltas into a single asset delta, changing the on-chain account delta commitment layout ([#3038](https://github.com/0xMiden/protocol/pull/3038)).
- [BREAKING] Replaced `AccountInterface::build_send_notes_script` with a standalone `SendNotesTransactionScript` built against `AccountCodeInterface` ([#3055](https://github.com/0xMiden/protocol/pull/3055)).
- Added the canonical `ExpirationTransactionScript` to `miden-standards`, with a delta-independent script root that network accounts can allowlist ([#3051](https://github.com/0xMiden/protocol/pull/3051)).

### Fixes
- Fixed `update_ger` to explicitly reject duplicate GER insertions with `ERR_GER_ALREADY_REGISTERED` instead of silently accepting them ([#2983](https://github.com/0xMiden/protocol/pull/2983)).
Expand Down
97 changes: 97 additions & 0 deletions crates/miden-standards/src/tx_script/expiration_script.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
use core::num::NonZeroU16;

use miden_protocol::transaction::{TransactionScript, TransactionScriptRoot};
use miden_protocol::utils::sync::LazyLock;
use miden_protocol::{Felt, Word};

use crate::code_builder::CodeBuilder;

// EXPIRATION TRANSACTION SCRIPT
// ================================================================================================

/// Transaction script that sets the expiration delta.
const EXPIRATION_TX_SCRIPT_SOURCE: &str = "\
use miden::protocol::tx

#! Set the transaction's expiration delta.
#!
#! Inputs: [[delta, 0, 0, 0], pad(12)]
#! Outputs: [pad(16)]
#!
#! Panics if:
#! - delta is 0 or not a u32 in the range 1..=0xFFFF (ERR_TX_INVALID_EXPIRATION_DELTA).
#!
#! Invocation: call
begin
exec.tx::update_expiration_block_delta
Comment thread
partylikeits1983 marked this conversation as resolved.
# => [pad(16)]
end
";
Comment thread
partylikeits1983 marked this conversation as resolved.

static EXPIRATION_TX_SCRIPT: LazyLock<TransactionScript> = LazyLock::new(|| {
CodeBuilder::default()
.compile_tx_script(EXPIRATION_TX_SCRIPT_SOURCE)
.expect("canonical expiration tx script should compile")
});

/// The canonical transaction script that sets the transaction's expiration delta to the value
/// supplied in the first element of `TX_SCRIPT_ARGS`.
///
/// This is the standard tx script a network account allowlists so that the network transaction
/// builder can bound how long a submitted transaction stays valid. Because the delta is an
/// input rather than hardcoded, the single [`ExpirationTransactionScript::script_root`] covers
/// every delta. It is safe to allowlist on an open network account even though an arbitrary
/// submitter controls the argument: the delta only bounds the inclusion window of the submitter's
/// own transaction - it cannot touch the account's nonce, state, or assets - and the kernel
/// hard-caps it at `0xFFFF` blocks. So the only thing the submitter decides is how soon their own
/// transaction must be included before it expires, within that fixed bound.
///
/// The type pairs the script (via [`From<ExpirationTransactionScript>`]) with the matching
/// `TX_SCRIPT_ARGS` ([`ExpirationTransactionScript::tx_script_args`]), so callers do not assemble
/// the argument word by hand:
///
/// ```ignore
/// let script = ExpirationTransactionScript::new(delta);
/// let context = build_tx_context(/* .. */)
/// .tx_script(script.into())
/// .tx_script_args(script.tx_script_args());
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ExpirationTransactionScript {
delta: NonZeroU16,
}

impl ExpirationTransactionScript {
/// Creates an expiration script that sets the transaction's expiration block delta to `delta`.
///
/// `delta` is a [`NonZeroU16`] because the kernel's `update_expiration_block_delta` only
/// accepts a delta in `1..=0xFFFF` and otherwise fails the transaction with
/// `ERR_TX_INVALID_EXPIRATION_DELTA`. Encoding that bound in the type keeps this constructor
/// infallible and guarantees the delta produced by [`Self::tx_script_args`] is always in
/// range.
pub fn new(delta: NonZeroU16) -> Self {
Self { delta }
}

/// The `TX_SCRIPT_ARGS` word the script reads its delta from: `[delta, 0, 0, 0]`.
///
/// Since `delta` is a [`NonZeroU16`], this word always carries an in-range delta, so the
/// script never triggers the kernel's range check. A caller that bypasses this type and
/// hand-crafts an out-of-range first element makes the kernel reject the transaction with
/// `ERR_TX_INVALID_EXPIRATION_DELTA`; it does not panic the host.
pub fn tx_script_args(&self) -> Word {
Word::from([Felt::from(self.delta.get()), Felt::ZERO, Felt::ZERO, Felt::ZERO])
}

/// The [`TransactionScriptRoot`] of the canonical script, to be allowlisted on a network
/// account via `AuthNetworkAccount::with_allowed_tx_scripts`.
pub fn script_root() -> TransactionScriptRoot {
EXPIRATION_TX_SCRIPT.root()
}
}

impl From<ExpirationTransactionScript> for TransactionScript {
fn from(_script: ExpirationTransactionScript) -> Self {
EXPIRATION_TX_SCRIPT.clone()
}
Comment thread
partylikeits1983 marked this conversation as resolved.
}
3 changes: 3 additions & 0 deletions crates/miden-standards/src/tx_script/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
mod expiration_script;
pub use expiration_script::ExpirationTransactionScript;

mod send_notes_script;
pub use send_notes_script::{SendNotesTransactionScript, SendNotesTransactionScriptError};
50 changes: 17 additions & 33 deletions crates/miden-testing/tests/auth/network_account.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use core::num::NonZeroU16;
use core::slice;
use std::collections::BTreeSet;

use miden_protocol::Word;
use miden_protocol::account::{Account, AccountBuilder, AccountType};
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;
Expand All @@ -14,6 +15,7 @@ use miden_standards::errors::standards::{
ERR_TX_SCRIPT_ALLOWLIST_TX_SCRIPT_NOT_ALLOWED,
};
use miden_standards::testing::note::NoteBuilder;
use miden_standards::tx_script::ExpirationTransactionScript;
use miden_testing::{MockChain, assert_transaction_executor_error};
use rstest::rstest;

Expand Down Expand Up @@ -76,26 +78,6 @@ fn expiration_tx_script(delta: u16) -> TransactionScript {
.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
// ================================================================================================

Expand Down Expand Up @@ -273,9 +255,9 @@ async fn test_auth_network_account_accepts_any_of_multiple_allowlisted_roots(
/// 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
/// for the expiration delta specifically because the delta only bounds the inclusion window of the
/// caller's own transaction (it cannot touch account nonce, state, or assets) and the kernel
/// hard-caps it at `0xFFFF` blocks. Network accounts that want this (e.g. for the ntx-builder) can
/// allowlist such a script knowingly.
#[rstest]
#[case(10)]
Expand All @@ -285,27 +267,29 @@ async fn test_auth_network_account_accepts_any_of_multiple_allowlisted_roots(
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();
// Use the canonical, args-driven expiration script from miden-standards - the same script the
// node allowlists - so this exercises the standardized root rather than a local copy.
let script = ExpirationTransactionScript::new(
NonZeroU16::new(delta).expect("rstest delta cases are non-zero"),
);

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()])?;
let account = build_account_with_allowlists(
vec![note.script().root().into()],
vec![ExpirationTransactionScript::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(&note))?
.tx_script(tx_script)
.tx_script_args(tx_script_args)
.tx_script(script.into())
.tx_script_args(script.tx_script_args())
.build()?
.execute()
.await?;
Expand Down
40 changes: 40 additions & 0 deletions crates/miden-testing/tests/scripts/expiration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use core::num::NonZeroU16;

use miden_protocol::account::auth::AuthScheme;
use miden_standards::tx_script::ExpirationTransactionScript;
use miden_testing::{Auth, MockChain};

/// Example: attach the standardized expiration transaction script to a transaction and choose the
/// expiration delta at execution time via `TX_SCRIPT_ARGS`, rather than baking it into the script.
/// A single allowlistable script root therefore works for any delta.
#[tokio::test]
async fn expiration_tx_script_sets_expiration_from_tx_args() -> anyhow::Result<()> {
const DELTA: NonZeroU16 = NonZeroU16::new(42).unwrap();

let mut builder = MockChain::builder();
let account = builder.add_existing_wallet(Auth::BasicAuth {
auth_scheme: AuthScheme::Falcon512Poseidon2,
})?;
let mock_chain = builder.build()?;

// Mirror how a real caller (client / node) attaches the script: convert the typed script into a
// `TransactionScript` and read its matching `TX_SCRIPT_ARGS` off the same typed value, rather
// than assembling the argument word by hand.
let script = ExpirationTransactionScript::new(DELTA);

let executed = mock_chain
.build_tx_context(account.id(), &[], &[])?
.tx_script(script.into())
.tx_script_args(script.tx_script_args())
.build()?
.execute()
.await?;

assert_eq!(
executed.expiration_block_num(),
executed.block_header().block_num() + u32::from(DELTA.get()),
"the tx-args-supplied expiration delta should be applied",
);

Ok(())
}
1 change: 1 addition & 0 deletions crates/miden-testing/tests/scripts/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod allowlist;
mod blocklist;
mod expiration;
mod faucet;
mod fee;
mod ownable2step;
Expand Down
Loading