miden_standards/tx_script/expiration_script.rs
1use core::num::NonZeroU16;
2
3use miden_protocol::assembly::Path;
4use miden_protocol::transaction::{TransactionScript, TransactionScriptRoot};
5use miden_protocol::utils::sync::LazyLock;
6use miden_protocol::{Felt, Word};
7
8use crate::StandardsLib;
9
10// CONSTANTS
11// ================================================================================================
12
13/// Path to the expiration transaction script procedure in the standards library, assembled from
14/// `asm/standards/tx_scripts/expiration.masm`.
15const EXPIRATION_TX_SCRIPT_PATH: &str = "::miden::standards::tx_scripts::expiration::main";
16
17// EXPIRATION TRANSACTION SCRIPT
18// ================================================================================================
19
20static EXPIRATION_TX_SCRIPT: LazyLock<TransactionScript> = LazyLock::new(|| {
21 let standards_lib = StandardsLib::default();
22 let path = Path::new(EXPIRATION_TX_SCRIPT_PATH);
23 TransactionScript::from_library_reference(standards_lib.as_ref(), path)
24 .expect("standards library should contain the expiration tx script procedure")
25});
26
27/// The canonical transaction script that sets the transaction's expiration delta to the value
28/// supplied in the first element of `TX_SCRIPT_ARGS`.
29///
30/// This is the standard tx script a network account allowlists so that the network transaction
31/// builder can bound how long a submitted transaction stays valid. Because the delta is an
32/// input rather than hardcoded, the single [`ExpirationTransactionScript::script_root`] covers
33/// every delta. It is safe to allowlist on an open network account even though an arbitrary
34/// submitter controls the argument: the delta only bounds the inclusion window of the submitter's
35/// own transaction - it cannot touch the account's nonce, state, or assets - and the kernel
36/// hard-caps it at `0xFFFF` blocks. So the only thing the submitter decides is how soon their own
37/// transaction must be included before it expires, within that fixed bound.
38///
39/// The type pairs the script (via [`From<ExpirationTransactionScript>`]) with the matching
40/// `TX_SCRIPT_ARGS` ([`ExpirationTransactionScript::tx_script_args`]), so callers do not assemble
41/// the argument word by hand:
42///
43/// ```ignore
44/// let script = ExpirationTransactionScript::new(delta);
45/// let context = build_tx_context(/* .. */)
46/// .tx_script(script.into())
47/// .tx_script_args(script.tx_script_args());
48/// ```
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub struct ExpirationTransactionScript {
51 delta: NonZeroU16,
52}
53
54impl ExpirationTransactionScript {
55 /// Creates an expiration script that sets the transaction's expiration block delta to `delta`.
56 ///
57 /// `delta` is a [`NonZeroU16`] because the kernel's `update_expiration_block_delta` only
58 /// accepts a delta in `1..=0xFFFF` and otherwise fails the transaction with
59 /// `ERR_TX_INVALID_EXPIRATION_DELTA`. Encoding that bound in the type keeps this constructor
60 /// infallible and guarantees the delta produced by [`Self::tx_script_args`] is always in
61 /// range.
62 pub fn new(delta: NonZeroU16) -> Self {
63 Self { delta }
64 }
65
66 /// The `TX_SCRIPT_ARGS` word the script reads its delta from: `[delta, 0, 0, 0]`.
67 ///
68 /// Since `delta` is a [`NonZeroU16`], this word always carries an in-range delta, so the
69 /// script never triggers the kernel's range check. A caller that bypasses this type and
70 /// hand-crafts an out-of-range first element makes the kernel reject the transaction with
71 /// `ERR_TX_INVALID_EXPIRATION_DELTA`; it does not panic the host.
72 pub fn tx_script_args(&self) -> Word {
73 Word::from([Felt::from(self.delta.get()), Felt::ZERO, Felt::ZERO, Felt::ZERO])
74 }
75
76 /// The [`TransactionScriptRoot`] of the canonical script, to be allowlisted on a network
77 /// account via `AuthNetworkAccount::with_allowed_tx_scripts`.
78 pub fn script_root() -> TransactionScriptRoot {
79 EXPIRATION_TX_SCRIPT.root()
80 }
81}
82
83impl From<ExpirationTransactionScript> for TransactionScript {
84 fn from(_script: ExpirationTransactionScript) -> Self {
85 EXPIRATION_TX_SCRIPT.clone()
86 }
87}