-
Notifications
You must be signed in to change notification settings - Fork 140
feat: canonical expiration transaction script in miden-standards #3051
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
partylikeits1983
merged 6 commits into
next
from
partylikeits1983-claude/canonical-expiration-tx-script
Jun 12, 2026
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
6b178af
feat: canonical expiration transaction script in miden-standards
partylikeits1983 683b3ae
Update crates/miden-standards/src/transaction.rs
partylikeits1983 e979bcf
refactor: move ExpirationTransactionScript into tx_script module
partylikeits1983 4996a5a
docs: clarify expiration delta range, tx wording, and allowlist safety
partylikeits1983 d1b4fc7
docs: document expiration-delta range failure behavior
partylikeits1983 a346dbc
Merge branch 'next' into partylikeits1983-claude/canonical-expiration…
partylikeits1983 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| # => [pad(16)] | ||
| end | ||
| "; | ||
|
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() | ||
| } | ||
|
partylikeits1983 marked this conversation as resolved.
|
||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(()) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.