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
41 changes: 22 additions & 19 deletions crates/miden-protocol/asm/protocol/src/tx.masm
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ end
#! Returns the timestamp of the reference block for this transaction.
#!
#! WARNING: the returned timestamp is not guaranteed to be precise (i.e., could be several seconds
#! off) or recent, unless recency is separately enforced by setting transaction expiration delta.
#! off) or recent, unless an expiration delta bounds the age of the reference block.
#!
#! Specifically, the reference blocks (and therefore the corresponding block timestamp) can be
#! chosen somewhat arbitrarily by the transaction executor. While this does not allow executors to
Expand All @@ -92,8 +92,8 @@ end
#! to consume the note when referencing the latest block. This is not necessarily a problem in all
#! cases, but must be taken into consideration by script developers.
#!
#! If the above is undesired, then one possible countermeasure is to set a transaction expiration
#! delta. For example, with a delta of 3, the oldest block account X could reference is the one at
#! If the above is undesired, then one possible countermeasure is to set an expiration delta. For
#! example, with an expiration delta of 3, the oldest block account X could reference is the one at
#! time 8. This still allows for consumption by both accounts during a period of time, but shortens
#! that window.
#!
Expand Down Expand Up @@ -232,12 +232,16 @@ end
#! and is not revalidated against the foreign account's current on-chain state at inclusion, so the
#! returned values may be outdated.
#!
#! If a foreign account holds time-sensitive data, it is the responsibility of that account to set a
#! transaction expiration delta according to how time-sensitive the data is. The delta bounds how old
#! the reference block can be relative to the block the transaction is included in. For example, if an
#! oracle price is updated every 5 blocks, the oracle account should set an expiration delta of 5 (or
#! smaller): if the current block is 40 and the delta is 5, the reference block must be block 35 or
#! newer, so a value from block 20 could not be read.
#! Any FPI-callable procedure or asset callback that reads mutable, security-sensitive state must
#! call [`tx::update_expiration_block_delta`] in the execution path that reads that state. This is
#! the foreign account's responsibility because the caller chooses the transaction reference block.
#! No expiration delta is required when a procedure reads only immutable data or when stale data is
#! acceptable.
#!
#! The expiration delta bounds the age of the reference block relative to the block that includes
#! the transaction. For example, if an oracle price is updated every 5 blocks, the oracle account
#! should set an expiration delta of 5 or less. If the current block is 40 and the delta is 5, the
#! reference block must be block 35 or newer, so a value from block 20 cannot be read.
#!
#! Inputs: [foreign_account_id_suffix, foreign_account_id_prefix, FOREIGN_PROC_ROOT, foreign_procedure_inputs(16)]
#! Outputs: [foreign_procedure_outputs(16)]
Expand Down Expand Up @@ -299,21 +303,20 @@ pub proc execute_foreign_procedure(
# => [foreign_procedure_outputs(16)]
end

#! Updates the transaction expiration delta.
#! Updates the expiration delta.
#!
#! The transaction expiration delta specifies how close to the transaction's reference block the
#! transaction must be included into the chain. For example, if the transaction's reference block is
#! 100 and transaction expiration delta is 10, the transaction can be included into the chain by
#! block 110. If this does not happen, the transaction is considered expired and cannot be included
#! into the chain.
#! The expiration delta specifies how close to the transaction's reference block the transaction
#! must be included in the chain. For example, if the transaction's reference block is 100 and the
#! expiration delta is 10, the transaction can be included in the chain by block 110. If this does
#! not happen, the transaction is considered expired and cannot be included in the chain.
#!
#! Once set, transaction expiration delta can be decreased, but not increased.
#! Once set, the expiration delta can be decreased, but not increased.
#!
#! Inputs: [block_height_delta, ...]
#! Output: [...]
#!
#! Where:
#! - block_height_delta is the desired expiration time delta (1 to 0xFFFF).
#! - block_height_delta is the desired expiration delta (1 to 65535).
#!
#! Annotation hint: is not used anywhere
pub proc update_expiration_block_delta(block_height_delta: u16)
Expand All @@ -330,13 +333,13 @@ pub proc update_expiration_block_delta(block_height_delta: u16)
dropw dropw dropw dropw
end

#! Returns the transaction expiration delta, or 0 if the delta has not been set.
#! Returns the expiration delta, or 0 if the delta has not been set.
#!
#! Inputs: [...]
#! Output: [block_height_delta, ...]
#!
#! Where:
#! - block_height_delta is the stored expiration time delta (1 to 0xFFFF).
#! - block_height_delta is the stored expiration delta (1 to 65535).
#!
#! Annotation hint: is not used anywhere
pub proc get_expiration_block_delta() -> u16
Expand Down
18 changes: 10 additions & 8 deletions crates/miden-standards/asm/standards/expiration.masm
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
# miden::standards::expiration
#
# Helpers for applying transaction expiration limits.
# Helpers for applying expiration deltas.

use miden::protocol::tx

# CONSTANTS
# =================================================================================================

#! The default expiration limit for standards procedures that read mutable state through FPI.
#! The default expiration delta for standards procedures that read mutable, security-sensitive state
#! through FPI.
#! At the default three-second block interval, this corresponds to approximately one minute.
pub const DEFAULT_EXPIRATION_BLOCK_DELTA = 20

# PROCEDURES
# =================================================================================================

#! Applies the default expiration block delta to the transaction.
#! Applies the default expiration delta to the transaction.
#!
#! A procedure that reads mutable or time-sensitive state exposed through FPI should call this as
#! its first action; a procedure needing a custom limit can call
#! [`tx::update_expiration_block_delta`] directly. Policy dispatchers deliberately leave the limit
#! to the invoked policy. The transaction-wide expiration can only decrease, so several procedures
#! may safely apply different limits in the same transaction.
#! A procedure that reads mutable, security-sensitive state exposed through FPI must call
#! this in the execution path that reads the state; a procedure needing a custom expiration delta
#! can call [`tx::update_expiration_block_delta`] directly. Policy dispatchers deliberately leave
#! the expiration delta to the invoked policy. The transaction-wide expiration delta can only
#! decrease, so several procedures may safely apply different expiration deltas in the same
#! transaction.
#!
#! Inputs: []
#! Outputs: []
Expand Down
9 changes: 9 additions & 0 deletions crates/miden-standards/src/account/policies/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@
//! [`TransferAllowAll`]) install a specific policy procedure on the account so that the
//! manager's `dyncall` can dispatch to it.
//!
//! Policies that may run through FPI and read mutable, security-sensitive state must set an
//! expiration delta in the same execution path that reads the state. This includes transfer
//! policies reached through asset callbacks and policies that read blocklists, allowlists, pause
//! flags, active policy roots, oracle values, risk parameters, or similar mutable data. The
//! built-in mutable transfer policies apply `miden::standards::expiration::apply_default`; custom
//! policies should call that helper or `tx::update_expiration_block_delta` directly with a custom
//! expiration delta. No expiration delta is required when a policy reads only immutable data or
//! when stale data is acceptable.
//!
//! A faucet constructs the manager via [`TokenPolicyManager::builder`], setting the required
//! `active_*_policy` for each kind (and optionally any number of reserved `allowed_*_policy`
//! entries), then passes the built manager directly to
Expand Down
6 changes: 6 additions & 0 deletions crates/miden-standards/src/account/policies/transfer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ pub enum TransferPolicyError {
/// an ordinary send. The bundled blocklist and allowlist policies therefore exempt the issuer by
/// comparing the asset's faucet ID against the native account ID.
///
/// Transfer policies reached through asset callbacks run through FPI. Policies that read mutable,
/// security-sensitive state must set an expiration delta in the execution path that reads that
/// state. The built-in blocklist and allowlist policies apply the standards default; custom
/// policies should call `miden::standards::expiration::apply_default` or
/// `tx::update_expiration_block_delta` directly with a custom expiration delta.
///
/// The companion components carried by the descriptor are inlined into the account by the
/// [`super::TokenPolicyManager`] when it is converted into account components.
#[derive(Debug, Clone)]
Expand Down
8 changes: 8 additions & 0 deletions docs/src/account/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ merged to form the account's `Code` and `Storage`.

The component's code defines a library of functions that can perform arbitrary computations, as well as read and write to account storage.

### FPI-callable mutable reads

Account component procedures can become part of an account's public interface and can be called from note scripts, transaction scripts, and foreign accounts through FPI. If a procedure reads mutable, security-sensitive state, it must set an expiration delta in the execution path that reads that state.

The component procedure defines the expiration delta because callers can choose an old reference block, so callers cannot be trusted to set it. Standards components can use `miden::standards::expiration::apply_default` for the common expiration delta, or call `tx::update_expiration_block_delta` directly when they need a custom expiration delta.

For more information, see [Foreign procedure invocation (FPI) and expiration](../transaction#foreign-procedure-invocation-fpi-and-expiration).

## Component metadata

The component metadata describes the account component entirely: its name, description, version, and storage layout.
Expand Down
6 changes: 6 additions & 0 deletions docs/src/asset.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,10 @@ Outputs: [pad(16)]

Both callbacks are invoked via `dyncall`, so they must follow the convention of accepting and returning 16 stack elements (input + padding).

#### Expiration requirement

Asset callbacks execute against the issuing faucet through FPI. Any callback or callback-dispatched policy that reads mutable, security-sensitive state must set an expiration delta in the execution path that reads that state. Standards components can use `miden::standards::expiration::apply_default` for the common expiration delta, and custom callbacks can call `tx::update_expiration_block_delta` directly. For more information, see [Foreign procedure invocation (FPI) and expiration](transaction#foreign-procedure-invocation-fpi-and-expiration).

#### Callback skipping

A callback is not invoked in any of these cases:
Expand All @@ -207,3 +211,5 @@ All data structures not following the Miden asset model that can be exchanged.
:::

Miden is flexible enough to support other `Asset` models. For example, developers can replicate Ethereum’s ERC20 pattern, where fungible `Asset` ownership is recorded in a single account. To transact, users send a note to that account, triggering updates in the global hashmap state.

Alternative or programmable asset models that expose FPI-callable checks follow the same expiration delta rule as native asset callbacks; see [Foreign procedure invocation (FPI) and expiration](transaction#foreign-procedure-invocation-fpi-and-expiration).
12 changes: 8 additions & 4 deletions docs/src/protocol_library.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,13 +187,17 @@ Transaction procedures manage transaction-level operations including note creati
| `get_output_notes_commitment` | Returns the output notes commitment hash.<br/><br/>**Inputs:** `[]`<br/>**Outputs:** `[OUTPUT_NOTES_COMMITMENT]` | Any |
| `get_num_input_notes` | Returns the total number of input notes consumed by this transaction.<br/><br/>**Inputs:** `[]`<br/>**Outputs:** `[num_input_notes]` | Any |
| `get_num_output_notes` | Returns the current number of output notes created in this transaction.<br/><br/>**Inputs:** `[]`<br/>**Outputs:** `[num_output_notes]` | Any |
| `execute_foreign_procedure` | Executes the provided procedure against the foreign account.<br/><br/>**Inputs:** `[foreign_account_id_suffix, foreign_account_id_prefix, FOREIGN_PROC_ROOT, <inputs>, pad(n)]`<br/>**Outputs:** `[<outputs>]` | Any |
| `get_expiration_block_delta` | Returns the transaction expiration delta, or 0 if not set.<br/><br/>**Inputs:** `[]`<br/>**Outputs:** `[block_height_delta]` | Any |
| `update_expiration_block_delta` | Updates the transaction expiration delta.<br/><br/>**Inputs:** `[block_height_delta]`<br/>**Outputs:** `[]` | Any |
| `execute_foreign_procedure` | Executes the provided procedure against the foreign account. Values read from the foreign account are authenticated for the transaction reference block, not for the inclusion block.<br/><br/>**Inputs:** `[foreign_account_id_suffix, foreign_account_id_prefix, FOREIGN_PROC_ROOT, <inputs>, pad(n)]`<br/>**Outputs:** `[<outputs>]` | Any |
| `get_expiration_block_delta` | Returns the expiration delta, or 0 if not set.<br/><br/>**Inputs:** `[]`<br/>**Outputs:** `[block_height_delta]` | Any |
| `update_expiration_block_delta` | Updates the expiration delta.<br/><br/>**Inputs:** `[block_height_delta]`<br/>**Outputs:** `[]` | Any |
| `compute_fee` | Computes the fee required for the current transaction.<br/><br/>**Inputs:** `[num_extra_cycles, EXCLUDE_NOTES_COMMITMENT]`<br/>**Outputs:** `[fee_amount]` | Any |
| `get_fee_asset_id` | Returns the ID of the asset that fees are paid in.<br/><br/>**Inputs:** `[]`<br/>**Outputs:** `[FEE_ASSET_ID]` | Any |

Note on `execute_foreign_procedure`: the values it reads reflect the foreign account's state at the transaction reference block, which is chosen by the executor. The foreign account commitment is not a transaction public input and is not revalidated against the foreign account's current on-chain state at inclusion, so a foreign read may be outdated. If a foreign account holds time-sensitive data, it is the responsibility of that account to set the transaction expiration delta according to how time-sensitive the data is, so that the FPI interface cannot be used in unintended ways.
### Foreign procedure invocation and expiration

`execute_foreign_procedure` reads the foreign account's state at the transaction reference block, which is chosen by the executor. The foreign account commitment is not a transaction public input and is not revalidated against the foreign account's current on-chain state at inclusion, so a foreign read may be outdated.

Any FPI-callable procedure that reads mutable, security-sensitive state must set an expiration delta in the execution path that reads that state. For the full rule and rationale, see [Foreign procedure invocation (FPI) and expiration](transaction#foreign-procedure-invocation-fpi-and-expiration).

## Faucet Procedures (`miden::protocol::faucet`)

Expand Down
Loading
Loading