Skip to content

feat(standards): pay the transaction fee in guarded and smart multisig auth - #3798

Open
mmagician wants to merge 12 commits into
mmagician-claude/port-fee-assert-boundfrom
mmagician-claude/port-multisig-pays-fee
Open

feat(standards): pay the transaction fee in guarded and smart multisig auth#3798
mmagician wants to merge 12 commits into
mmagician-claude/port-fee-assert-boundfrom
mmagician-claude/port-multisig-pays-fee

Conversation

@mmagician

@mmagician mmagician commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

Port of #3786 to next.

On next the guarded and smart multisig components decode their auth args but drop the conversion info and pay no fee. Both paths can authenticate below the account's spending quorum (guardian key rotation, or a procedure with a reduced per-procedure threshold), and the conversion rate is host-supplied, so paying the fee unbounded would let such an authorization move arbitrary value out of the vault as a fee note. This wires in the bounded flow from #3797 instead.

Notes

Differs from the rc version in following next's auth-args scheme: the conversion info comes from resolve_auth_args rather than load_conversion_info, so the auth args no longer double as the summary salt, and the summary is bound to the block number as on next.

Comment on lines +81 to +83
# 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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Suggested change
# 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.

unnecessary impl. detail.
We should guard against such bloat in the future, add / modify the MASM skills (separate PR to do this)

Comment on lines +93 to +94
# 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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Suggested change
# 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
# settle the sponsorship obligation first

Comment on lines +57 to +59
pub proc auth_tx_multisig_smart(auth_args: word)
# read the output-note count before the fee payment so the notes it creates can be counted
exec.tx::get_num_output_notes movdn.4

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

apply the same doc changes as suggested in the other file

Comment on lines +69 to +104
exec.multisig::get_initial_threshold_and_num_approvers drop
# => [num_of_approvers, CONVERSION_INFO, block_number, SALT, num_output_notes_before_fee]

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

exec.fee::estimate_fee
# => [fee_amount, CONVERSION_INFO, block_number, SALT, 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.tx::get_fee_asset_id exec.fees::create_network_note_sponsorships drop
# => [fee_amount, CONVERSION_INFO, block_number, SALT, num_output_notes_before_fee]

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

exec.fee::resolve_payment_info
# => [payment_faucet_id_suffix, payment_faucet_id_prefix, payment_amount, fee_amount,
# block_number, SALT, 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, block_number, SALT, num_output_notes_before_fee]

exec.fee::assert_fee_bound
# => [payment_faucet_id_suffix, payment_faucet_id_prefix, payment_amount, block_number, SALT,
# num_output_notes_before_fee]

exec.fee::pay_estimated_fee
# => [block_number, SALT, num_output_notes_before_fee]

# 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.6 sub
# => [num_own_output_notes, block_number, SALT]

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

isn't this logic duplicated in crates/miden-standards/asm/components/auth/guarded_multisig/guarded_multisig.masm? If we can re-use it, then let's pull it out to a common helper procedure

…g auth

Port of #3786 to next. Both components run estimate_fee, the sponsorship
payment, resolve_payment_info, assert_fee_bound (2/1, native fee asset) and
pay_estimated_fee between multisig::resolve_auth_args and the summary, and
pass the number of notes the payment created to guardian::verify_signature
and multisig_smart::auth_tx, whose note restrictions exclude them.
tx_policy::assert_no_output_notes reads the verified count itself.

Adaptations to next: the conversion info comes from resolve_auth_args
(MultisigAuthArgs) rather than load_conversion_info, so the auth args no
longer double as the summary salt; multisig_smart::auth_tx takes
(num_own_output_notes, block_number, SALT); the fee asset is read via
tx::get_fee_asset_id; the sponsorship wrapper dropped in the fee-split port
is spelled out at the call sites; pay_fee reads the fee asset once via an
estimate_fee_for_asset helper. Tests build MultisigAuthArgs, share the
multisig fixture and signing helpers, and drop the salt-binding assertions.
Cost tables and bench-tx.json regenerated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KkQzFtRjyDwQ7iVtRbsHsn
…tisig auth

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KkQzFtRjyDwQ7iVtRbsHsn
…sig auth

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KkQzFtRjyDwQ7iVtRbsHsn
…g auth

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KkQzFtRjyDwQ7iVtRbsHsn
…ig components

Pull the fee-paying block of the guarded and smart multisig auth procedures
into multisig::pay_bounded_fee, which takes the number of signers and the
conversion info and returns the number of notes the payment created. The
fee bound constants and the drain rationale live there once; the components
keep only the signer count that differs. Written against fee::estimate_fee
taking the fee asset ID, so the fee asset is read once for the estimate and
the sponsorship notes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KkQzFtRjyDwQ7iVtRbsHsn
The estimate records every network note's sponsorship price in a local
table, which the sponsorship payment reads back instead of pricing the
notes through FPI a second time, mirroring fee::pay_fee.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KkQzFtRjyDwQ7iVtRbsHsn
AuthMultisig now pays its fee through multisig::pay_bounded_fee, so the
host-supplied conversion rate can move at most twice the computed fee out
of the vault, in the native fee asset, closing the drain of #3763. The
bound's rationale is reworded to cover every multisig component.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KkQzFtRjyDwQ7iVtRbsHsn
Composing pay_fee from resolve_payment_info and pay_estimated_fee cost
every plain fee payer 121 auth cycles and pushed the two-P2ID ECDSA
consumption into the next padded-trace bracket. Only the bounded multisig
payment composes the two procedures; pay_fee keeps its inline tail.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KkQzFtRjyDwQ7iVtRbsHsn
The sponsorship price table now holds one fee asset value word per output
note, so the local table grows to fees::SPONSORSHIP_PRICE_TABLE_NUM_ELEMENTS
as in fee::pay_fee. The pointer plumbing is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KkQzFtRjyDwQ7iVtRbsHsn
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KkQzFtRjyDwQ7iVtRbsHsn
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KkQzFtRjyDwQ7iVtRbsHsn
@mmagician
mmagician force-pushed the mmagician-claude/port-multisig-pays-fee branch from 0611809 to 78fe2df Compare September 3, 2026 15:10
@mmagician
mmagician requested a review from bobbinth September 3, 2026 15:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AuthMultisig: unbounded fee conversion lets approvers drain the vault via the fee note

2 participants