Skip to content
Open
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 @@ -30,6 +30,7 @@

- [BREAKING] Split fee estimation out of fee payment: the new `fee::estimate_fee` only computes the fee, pricing the FEE_SPONSORSHIP notes the payment will need through the new `fees::estimate_network_note_sponsorships`, and the new `fee::pay_network_note_sponsorships` creates them; `fee::pay_fee` composes the two, with unchanged inputs, outputs and computed fee, but its MAST root and therefore the code commitment of every fee-paying component change ([#3332](https://github.com/0xMiden/protocol/issues/3332)).
- [BREAKING] The `AuthGuardedMultisig` and `AuthMultisigSmart` components now pay the transaction fee, bounding the payment to the native fee asset at at most twice the computed fee via `fee::assert_fee_bound`. Both required the transaction's auth args to commit to the fee conversion info, and both code commitments change. Without the bound, an authorization cheaper than an ordinary spend — guardian key rotation, or a procedure with a reduced per-procedure threshold — could drain the vault through the fee note ([#3757](https://github.com/0xMiden/protocol/issues/3757), [#3763](https://github.com/0xMiden/protocol/issues/3763)).
- [BREAKING] The `AuthMultisig` component now bounds its fee payment to the native fee asset at at most twice the computed fee via the new `multisig::pay_bounded_fee` shared by the three multisig components; its code commitment changes ([#3763](https://github.com/0xMiden/protocol/issues/3763)).

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.

Suggested change
- [BREAKING] The `AuthMultisig` component now bounds its fee payment to the native fee asset at at most twice the computed fee via the new `multisig::pay_bounded_fee` shared by the three multisig components; its code commitment changes ([#3763](https://github.com/0xMiden/protocol/issues/3763)).
- [BREAKING] The `AuthMultisig` component now bounds its fee payment to the native fee asset, capped at twice the computed fee via the new `multisig::pay_bounded_fee` shared by the three multisig components ([#3802](https://github.com/0xMiden/protocol/pull/3802)).

nit: we could probably drop the last part

- [BREAKING] Incremented the MSRV to 1.98.
- [BREAKING] The user fungible faucet factories now install `BasicWallet` so a faucet can hold the native fee asset ([#3766](https://github.com/0xMiden/protocol/pull/3766)).
- [BREAKING] Bind the standard config notes to their target account: `OwnerConfigNote`, `PauseConfigNote`, `RbacConfigNote`, `FaucetPolicyConfigNote`, `AllowlistConfigNote`, `BlocklistConfigNote` and `FaucetMetadataConfigNote` now carry a `NetworkAccountTarget` attachment for that account ([#3433](https://github.com/0xMiden/protocol/issues/3433), [#3455](https://github.com/0xMiden/protocol/pull/3455)).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@

use miden::standards::auth::multisig
use miden::standards::auth::guardian
use miden::standards::auth::signature
use miden::standards::fee
use miden::protocol::tx

pub use {update_signers_and_threshold} from miden::standards::auth::multisig
pub use {get_threshold_and_num_approvers} from miden::standards::auth::multisig
Expand All @@ -16,26 +14,16 @@ pub use {is_signer} from miden::standards::auth::multisig

pub use {update_guardian_public_key} from miden::standards::auth::guardian

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

# The largest fee payment this component accepts, as the fraction FEE_BOUND_NUM / FEE_BOUND_DEN of
# the computed fee. Guardian key rotation authenticates without a guardian signature and can be
# thresholded below the account's spending quorum, so an unbounded host-supplied rate would drain
# the vault through the fee note. The margin covers a fee rising while signatures are collected.
const FEE_BOUND_NUM = 2
const FEE_BOUND_DEN = 1

#! Authenticate a transaction with multi-signature support and guardian verification, paying the
#! transaction fee in the process.
#!
#! It first decodes the fee conversion info committed to by the AUTH_ARGS (see
#! miden::standards::fee::load_conversion_info) and pays the transaction fee by creating and
#! funding a public TX_FEE note. The payment is bounded to at most FEE_BOUND_NUM / FEE_BOUND_DEN of
#! the computed fee and pinned to the native fee asset (see fee::assert_fee_bound). On chains with
#! a zero verification base fee no note is created. Because the fee is paid before the transaction
#! summary is created, the fee note and the vault withdrawal funding it are covered by the approver
#! and guardian signatures.
#! funding a public TX_FEE note, bounded to the native fee asset at at most twice the computed fee

@zeapoz zeapoz Sep 4, 2026

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.

Suggested change
#! funding a public TX_FEE note, bounded to the native fee asset at at most twice the computed fee
#! funding a public TX_FEE note, bounded to the native fee asset, capped at twice the computed fee

nit: at at most reads a bit awkwardly, this might be better?

#! (see miden::standards::auth::multisig::pay_bounded_fee). On chains with a zero verification base
#! fee no note is created. Because the fee is paid before the transaction summary is created, the
#! fee note and the vault withdrawal funding it are covered by the approver and guardian
#! signatures.
#!
#! The AUTH_ARGS (= hash(CONVERSION_INFO || SALT)) then continue to serve as the transaction
#! summary salt. The uniqueness that replay protection relies on originates from the
Expand Down Expand Up @@ -64,62 +52,27 @@ const FEE_BOUND_DEN = 1
#! Invocation: call
@auth_script
pub proc auth_tx_guarded_multisig(auth_args: word)
# Pay the transaction fee before the summary is created so that the TX_FEE note and the vault
# withdrawal funding it are covered by the approver and guardian signatures.
# load_conversion_info consumes the AUTH_ARGS, so keep a copy to serve as the summary salt.
# Pay the transaction fee before the summary is created; load_conversion_info consumes the
# AUTH_ARGS, so keep a copy to serve as the summary salt.
# ---------------------------------------------------------------------------------------------

# read the output-note count so the notes the fee payment goes on to create can be counted
exec.tx::get_num_output_notes movdn.4
# => [AUTH_ARGS, num_output_notes_before_fee]

dupw
# => [AUTH_ARGS, AUTH_ARGS, num_output_notes_before_fee]
# => [AUTH_ARGS, AUTH_ARGS]

exec.fee::load_conversion_info
# => [CONVERSION_INFO, AUTH_ARGS, num_output_notes_before_fee]
# => [CONVERSION_INFO, AUTH_ARGS]

exec.multisig::get_initial_threshold_and_num_approvers drop
# => [num_of_approvers, CONVERSION_INFO, AUTH_ARGS, num_output_notes_before_fee]
# => [num_of_approvers, CONVERSION_INFO, AUTH_ARGS]

# one slot beyond the approvers, for the guardian signature. It is unconditional because the
# rotation path verifies no guardian signature but scans every account procedure instead, which
# the slot also covers.
# one slot beyond the approvers, for the guardian signature.
add.1
# => [num_of_signers, CONVERSION_INFO, AUTH_ARGS, num_output_notes_before_fee]

exec.signature::estimate_multisig_authentication_cycles
# => [num_extra_cycles, CONVERSION_INFO, AUTH_ARGS, num_output_notes_before_fee]

exec.fee::estimate_fee
# => [fee_amount, CONVERSION_INFO, AUTH_ARGS, num_output_notes_before_fee]

# settle the sponsorship obligation first, in pay_fee's order; the bound below guards the
# host-supplied rate, which the sponsorship amounts do not depend on
exec.fee::pay_network_note_sponsorships drop
# => [fee_amount, CONVERSION_INFO, AUTH_ARGS, num_output_notes_before_fee]

dup movdn.5
# => [fee_amount, CONVERSION_INFO, fee_amount, AUTH_ARGS, num_output_notes_before_fee]

exec.fee::resolve_payment_info
# => [payment_faucet_id_suffix, payment_faucet_id_prefix, payment_amount, fee_amount,
# AUTH_ARGS, num_output_notes_before_fee]

push.FEE_BOUND_DEN push.FEE_BOUND_NUM
# => [bound_num, bound_den, payment_faucet_id_suffix, payment_faucet_id_prefix, payment_amount,
# fee_amount, AUTH_ARGS, num_output_notes_before_fee]

exec.fee::assert_fee_bound
# => [payment_faucet_id_suffix, payment_faucet_id_prefix, payment_amount, AUTH_ARGS,
# num_output_notes_before_fee]
# => [num_of_signers, CONVERSION_INFO, AUTH_ARGS]

exec.fee::pay_estimated_fee
# => [AUTH_ARGS, num_output_notes_before_fee]
exec.multisig::pay_bounded_fee
# => [num_own_output_notes, AUTH_ARGS]

# the notes the fee payment created: the TX_FEE note and one FEE_SPONSORSHIP note per network
# output note. The rotation path excludes them from its no-output-notes check.
exec.tx::get_num_output_notes movup.5 sub movdn.4
movdn.4
# => [AUTH_ARGS, num_own_output_notes]

# Authenticate the transaction and record it for replay protection.
Expand Down
27 changes: 14 additions & 13 deletions crates/miden-standards/asm/components/auth/multisig/multisig.masm
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
# See the `AuthMultisig` Rust type's documentation for more details.

use miden::standards::auth::multisig
use miden::standards::auth::signature
use miden::standards::fee

pub use {update_signers_and_threshold} from miden::standards::auth::multisig
Expand All @@ -17,11 +16,10 @@ pub use {is_signer} from miden::standards::auth::multisig
#!
#! It first decodes the fee conversion info committed to by the AUTH_ARGS (see
#! miden::standards::fee::load_conversion_info) and pays the transaction fee by creating and
#! funding a public TX_FEE note (see miden::standards::fee::pay_fee). The payment asset and
#! conversion rate are committed to via the AUTH_ARGS (native fee asset at rate 1/1 for plain
#! native payment). On chains with a zero verification base fee no note is created. Because the
#! fee is paid before the transaction summary is created, the fee note and the vault withdrawal
#! funding it are covered by the approver signatures.
#! funding a public TX_FEE note, bounded to the native fee asset at at most twice the computed fee

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.

Suggested change
#! funding a public TX_FEE note, bounded to the native fee asset at at most twice the computed fee
#! funding a public TX_FEE note, bounded to the native fee asset, capped at twice the computed fee

#! (see miden::standards::auth::multisig::pay_bounded_fee). On chains with a zero verification base
#! fee no note is created. Because the fee is paid before the transaction summary is created, the
#! fee note and the vault withdrawal funding it are covered by the approver signatures.
#!
#! The AUTH_ARGS (= hash(CONVERSION_INFO || SALT)) then continue to serve as the transaction
#! summary salt. The uniqueness that replay protection relies on originates from the
Expand All @@ -33,12 +31,18 @@ pub use {is_signer} from miden::standards::auth::multisig
#! Outputs:
#! Operand stack: []
#!
#! Panics if:
#! - fee::load_conversion_info fails to verify.
#! - the fee payment is not in the native fee asset, or exceeds the bound.
#! - the fee cannot be paid.
#! - multisig::auth_tx fails to verify.
#! - the same transaction has already been executed.
#!
#! Invocation: call
@auth_script
pub proc auth_tx_multisig(auth_args: word)
# Pay the transaction fee before the summary is created so that the TX_FEE note and the
# vault withdrawal funding it are covered by the approver signatures. load_conversion_info
# consumes the AUTH_ARGS, so keep a copy around to serve as the summary salt afterwards.
# Pay the transaction fee before the summary is created; load_conversion_info consumes the
# AUTH_ARGS, so keep a copy to serve as the summary salt.
# ---------------------------------------------------------------------------------------------

dupw
Expand All @@ -50,10 +54,7 @@ pub proc auth_tx_multisig(auth_args: word)
exec.multisig::get_initial_threshold_and_num_approvers drop
# => [num_of_approvers, CONVERSION_INFO, AUTH_ARGS]

exec.signature::estimate_multisig_authentication_cycles
# => [num_extra_cycles, CONVERSION_INFO, AUTH_ARGS]

exec.fee::pay_fee drop
exec.multisig::pay_bounded_fee drop
# => [AUTH_ARGS]

# Authenticate the transaction and record it for replay protection.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,23 @@

use miden::standards::auth::multisig
use miden::standards::auth::multisig_smart
use miden::standards::auth::signature
use miden::standards::fee
use miden::protocol::tx

pub use {get_threshold_and_num_approvers} from miden::standards::auth::multisig
pub use {get_signer_at} from miden::standards::auth::multisig
pub use {is_signer} from miden::standards::auth::multisig
pub use {set_procedure_policy} from miden::standards::auth::multisig_smart
pub use {update_signers_and_threshold} from miden::standards::auth::multisig_smart

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

# The largest fee payment this component accepts, as the fraction FEE_BOUND_NUM / FEE_BOUND_DEN of
# the computed fee. A per-procedure policy can authorize a transaction below the account's default
# threshold, so an unbounded host-supplied rate would let such a transaction drain the vault
# through the fee note. The margin covers a fee rising while signatures are collected.
const FEE_BOUND_NUM = 2
const FEE_BOUND_DEN = 1

#! Authenticate a transaction using multisig smart-policy rules, paying the transaction fee in the
#! process.
#!
#! It first decodes the fee conversion info committed to by the AUTH_ARGS (see
#! miden::standards::fee::load_conversion_info) and pays the transaction fee by creating and
#! funding a public TX_FEE note. The payment is bounded to at most FEE_BOUND_NUM / FEE_BOUND_DEN of
#! the computed fee and pinned to the native fee asset (see fee::assert_fee_bound). On chains with
#! a zero verification base fee no note is created. Because the fee is paid before the transaction
#! summary is created, the fee note and the vault withdrawal funding it are covered by the approver
#! signatures.
#! funding a public TX_FEE note, bounded to the native fee asset at at most twice the computed fee

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.

Suggested change
#! funding a public TX_FEE note, bounded to the native fee asset at at most twice the computed fee
#! funding a public TX_FEE note, bounded to the native fee asset, capped at twice the computed fee

#! (see miden::standards::auth::multisig::pay_bounded_fee). On chains with a zero verification base
#! fee no note is created. Because the fee is paid before the transaction summary is created, the
#! fee note and the vault withdrawal funding it are covered by the approver signatures.
#!
#! The AUTH_ARGS (= hash(CONVERSION_INFO || SALT)) then continue to serve as the transaction
#! summary salt, exactly as in the plain multisig component.
Expand All @@ -57,56 +44,23 @@ const FEE_BOUND_DEN = 1
#! Invocation: call
@auth_script
pub proc auth_tx_multisig_smart(auth_args: word)
# Pay the transaction fee before the summary is created so that the TX_FEE note and the vault
# withdrawal funding it are covered by the approver signatures. load_conversion_info consumes
# the AUTH_ARGS, so keep a copy to serve as the summary salt.
# Pay the transaction fee before the summary is created; load_conversion_info consumes the
# AUTH_ARGS, so keep a copy to serve as the summary salt.
# ---------------------------------------------------------------------------------------------

# sample the output-note count so the notes the fee payment goes on to create can be counted
exec.tx::get_num_output_notes movdn.4
# => [AUTH_ARGS, num_output_notes_before_fee]

dupw
# => [AUTH_ARGS, AUTH_ARGS, num_output_notes_before_fee]
# => [AUTH_ARGS, AUTH_ARGS]

exec.fee::load_conversion_info
# => [CONVERSION_INFO, AUTH_ARGS, num_output_notes_before_fee]
# => [CONVERSION_INFO, AUTH_ARGS]

exec.multisig::get_initial_threshold_and_num_approvers drop
# => [num_of_approvers, CONVERSION_INFO, AUTH_ARGS, num_output_notes_before_fee]

exec.signature::estimate_multisig_authentication_cycles
# => [num_extra_cycles, CONVERSION_INFO, AUTH_ARGS, num_output_notes_before_fee]

exec.fee::estimate_fee
# => [fee_amount, CONVERSION_INFO, AUTH_ARGS, num_output_notes_before_fee]

# settle the sponsorship obligation first, in pay_fee's order; the bound below guards the
# host-supplied rate, which the sponsorship amounts do not depend on
exec.fee::pay_network_note_sponsorships drop
# => [fee_amount, CONVERSION_INFO, AUTH_ARGS, num_output_notes_before_fee]

dup movdn.5
# => [fee_amount, CONVERSION_INFO, fee_amount, AUTH_ARGS, num_output_notes_before_fee]

exec.fee::resolve_payment_info
# => [payment_faucet_id_suffix, payment_faucet_id_prefix, payment_amount, fee_amount,
# AUTH_ARGS, num_output_notes_before_fee]

push.FEE_BOUND_DEN push.FEE_BOUND_NUM
# => [bound_num, bound_den, payment_faucet_id_suffix, payment_faucet_id_prefix, payment_amount,
# fee_amount, AUTH_ARGS, num_output_notes_before_fee]

exec.fee::assert_fee_bound
# => [payment_faucet_id_suffix, payment_faucet_id_prefix, payment_amount, AUTH_ARGS,
# num_output_notes_before_fee]
# => [num_of_approvers, CONVERSION_INFO, AUTH_ARGS]

exec.fee::pay_estimated_fee
# => [AUTH_ARGS, num_output_notes_before_fee]
exec.multisig::pay_bounded_fee
# => [num_own_output_notes, AUTH_ARGS]

# the notes the fee payment created: the TX_FEE note and one FEE_SPONSORSHIP note per network
# output note. The procedure policies' note restrictions exclude them.
exec.tx::get_num_output_notes movup.5 sub movdn.4
movdn.4
# => [AUTH_ARGS, num_own_output_notes]

# Authenticate the transaction and record it for replay protection.
Expand Down
Loading
Loading