Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
15 changes: 9 additions & 6 deletions crates/miden-protocol/asm/protocol/src/tx.masm
Original file line number Diff line number Diff line change
Expand Up @@ -232,12 +232,15 @@ 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 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.
#! Procedures that read immutable data, or for which stale data is acceptable, may omit it.
#!
#! 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.
#!
#! Inputs: [foreign_account_id_suffix, foreign_account_id_prefix, FOREIGN_PROC_ROOT, foreign_procedure_inputs(16)]
#! Outputs: [foreign_procedure_outputs(16)]
Expand Down
7 changes: 4 additions & 3 deletions crates/miden-standards/asm/standards/expiration.masm
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ use miden::protocol::tx
# CONSTANTS
# =================================================================================================

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

Expand All @@ -16,8 +17,8 @@ pub const DEFAULT_EXPIRATION_BLOCK_DELTA = 20

#! Applies the default expiration block 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
#! A procedure that reads mutable or time-sensitive security state exposed through FPI must call
#! this in the execution path that reads the state; a procedure needing a custom limit can call
Comment thread
partylikeits1983 marked this conversation as resolved.
Outdated
#! [`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.
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 state must set a transaction
//! 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 their own
//! staleness limit. Policies that only read immutable data, or for which stale data is acceptable,
//! do not need an expiration delta.
Comment thread
partylikeits1983 marked this conversation as resolved.
Outdated
//!
//! 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: 5 additions & 1 deletion crates/miden-standards/src/account/policies/transfer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,11 @@ 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.
///
/// Policies should apply a transaction expiration delta based on their staleness considerations.
/// Transfer policies reached through asset callbacks run through FPI. Policies that read mutable
/// security state must apply a transaction expiration delta in the execution path that reads that
Comment thread
partylikeits1983 marked this conversation as resolved.
Outdated
/// 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 their own staleness limit.
///
/// The companion components carried by the descriptor are inlined into the account by the
/// [`super::TokenPolicyManager`] when it is converted into account components.
Expand Down
18 changes: 18 additions & 0 deletions docs/src/account/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,24 @@ 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 such a procedure reads
mutable security state, it must call `tx::update_expiration_block_delta` in the execution path that
reads that state.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude has a tendency to introduce newlines to .md files, but IMO (and keeping in line with our current .md files) we should not artificially split lines here.
May be worth adding a skill that we don't need to respect the 100-char (or whatever) limit in .mds

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

true about claude, but this is not claude :)


This rule applies to asset callbacks and to procedures that read blocklists, allowlists, pause
flags, role maps, active policy roots, oracle values, risk parameters, or other mutable state where
stale reads can change an authorization or pricing decision. The component owns the recency bound:
callers can choose an old reference block, so callers cannot be trusted to set the expiration
policy for the component.
Comment thread
partylikeits1983 marked this conversation as resolved.
Outdated

Procedures that only read immutable data, or for which stale data is acceptable, may omit the
expiration delta. Standards components can use `miden::standards::expiration::apply_default` for
the common limit, or call `tx::update_expiration_block_delta` directly when they need a custom
limit.

## Component metadata

The component metadata describes the account component entirely: its name, description, version, and storage layout.
Expand Down
20 changes: 20 additions & 0 deletions docs/src/asset.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,22 @@ 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 state must call `tx::update_expiration_block_delta` in the
execution path that reads that state. This includes checks against blocklists, allowlists, pause
flags, active policy roots, oracle values, risk parameters, or similar state.

Without an expiration delta, a prover can choose an older reference block where the callback state
allowed the transfer. The expiration delta bounds how stale that reference block may be when the
transaction is included. Standards components that need the common limit can use
`miden::standards::expiration::apply_default`; custom callbacks can call
`tx::update_expiration_block_delta` directly with a tighter limit.

Callbacks that only inspect immutable data, or for which stale data is acceptable, do not need to
set an expiration delta.
Comment thread
partylikeits1983 marked this conversation as resolved.
Outdated

#### Callback skipping

A callback is not invoked in any of these cases:
Expand All @@ -207,3 +223,7 @@ 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 must follow the same
expiration rule as native asset callbacks: if a check reads mutable security state, it must set a
transaction expiration delta in that execution path.
19 changes: 17 additions & 2 deletions docs/src/protocol_library.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,13 +187,28 @@ 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 |
| `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 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 |
| `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 state must call
`tx::update_expiration_block_delta` in the execution path that reads that state. This includes
asset callbacks and procedures that read blocklists, allowlists, pause flags, role maps, active
policy roots, oracle values, risk parameters, or other mutable data where stale reads can change an
authorization or pricing decision.

The call is the foreign account's responsibility because the caller controls which valid reference
block is used for proving. Procedures may omit the call only when they read immutable data or stale
data is acceptable.
Comment thread
partylikeits1983 marked this conversation as resolved.
Outdated

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

Expand Down
37 changes: 34 additions & 3 deletions docs/src/transaction.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,39 @@ A `Transaction` requires several inputs:

The proof together with the corresponding data needed for verification and updates of the global state can then be submitted and processed by the network.

### Foreign procedure invocation (FPI) and expiration

Note scripts and transaction scripts can read state from foreign accounts by calling public
account procedures through foreign procedure invocation (FPI). FPI authenticates the foreign
account state against the transaction reference block. It does not prove that the foreign account
state is current when the transaction is included in a block.

The executor chooses the transaction reference block. If no expiration delta is set, a transaction
can be proved against an old canonical block where mutable foreign state still allowed the action.
For example, a prover could choose a block from before an account was added to a blocklist, before
an account was removed from an allowlist, or before an oracle value or active policy root changed.
Comment thread
partylikeits1983 marked this conversation as resolved.
Outdated

:::warning

Any FPI-callable procedure or asset callback that reads mutable security state must call
`tx::update_expiration_block_delta` in the execution path that reads that state.

:::

The expiration block is computed as:

```text
expiration_block = transaction_reference_block + expiration_delta
```

The expiration delta bounds how old the reference block may be 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 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 cannot be used.

This requirement belongs to the foreign account procedure, not to the caller. Procedures that only
read immutable data, or for which stale data is acceptable, do not need to set an expiration delta.

## Examples

To illustrate the `Transaction` protocol, we provide two examples for a basic `Transaction`. We will use references to the existing Miden `Transaction` kernel — the reference implementation of the protocol — and to the methods in Miden Assembly.
Expand Down Expand Up @@ -129,9 +162,7 @@ The ability to facilitate both, local and network transactions, **is one of the

- In Miden, executors can choose arbitrary reference blocks to execute against their state. Hence it is possible to set `Transaction` expiration heights and in doing so, to define a block height until a `Transaction` should be included into a block. If the `Transaction` is expired, the resulting account state change is not valid and the `Transaction` cannot be verified anymore.

- Note and `Transaction` scripts can read the state of foreign accounts during execution. This is called foreign procedure invocation (FPI). A transaction can load at most **63 distinct foreign accounts** in addition to its native account. This does not limit the total number of FPI calls: once a foreign account is loaded, subsequent calls to the same account reuse the loaded data and do not consume another foreign-account slot. For example, the price of an asset for the **Swap** script might depend on a certain value stored in the oracle account. The caller identifies the invoked procedure by its root, which the kernel requires to be part of the code of the foreign account it is called on, so the executed logic is always one the account committed to.

- Values read from a foreign account through foreign procedure invocation reflect that account's state at the transaction's reference block, which the executor chooses and which may be an older canonical block. Unlike the native account's initial state, a foreign read is bound only to the reference block: the foreign account commitment is not part of the transaction's public inputs and is never revalidated against the foreign account's current on-chain state when the `Transaction` is included in a block. Because the party proving the `Transaction` can anchor it to a past block in which a foreign value was outdated but favorable (for example to bypass cross-account authorization such as roles or allowlists, or to act on a stale oracle price), a foreign account holding time-sensitive data must protect itself: it is the responsibility of that account to set a transaction expiration delta according to how time-sensitive the data is, so the FPI interface cannot be used in unintended ways. 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.
- Note and `Transaction` scripts can read the state of foreign accounts during execution. This is called [foreign procedure invocation (FPI)](#foreign-procedure-invocation-fpi-and-expiration). A transaction can load at most **63 distinct foreign accounts** in addition to its native account. This does not limit the total number of FPI calls: once a foreign account is loaded, subsequent calls to the same account reuse the loaded data and do not consume another foreign-account slot. The caller identifies the invoked procedure by its root, which the kernel requires to be part of the code of the foreign account it is called on, so the executed logic is always one the account committed to.

- An example of the right usage of `Transaction` arguments is the consumption of a **Swap** note. Those notes allow asset exchange based on predefined conditions. Example:
- The note's consumption condition is defined as "anyone can consume this note to take `X` units of asset A if they simultaneously create a note sending Y units of asset B back to the creator." If an executor wants to buy only a fraction `(X-m)` of asset A, they provide this amount via transaction arguments. The executor would provide the value `m`. The note script then enforces the correct transfer:
Expand Down
Loading