Skip to content

feat(standards): Add DelayedExecution (Timelocked Accounts) to AuthMultisigSmart - #3044

Open
onurinanc wants to merge 218 commits into
nextfrom
onur-multisig-smart-delayed-execution
Open

feat(standards): Add DelayedExecution (Timelocked Accounts) to AuthMultisigSmart#3044
onurinanc wants to merge 218 commits into
nextfrom
onur-multisig-smart-delayed-execution

Conversation

@onurinanc

Copy link
Copy Markdown
Collaborator

Closes: #3043.

Previously opened a draft PR implementing all features regarding Smart Multisig here: #2973. That PR still relatively big, and we separate the delayed execution logic into this PR.

PhilippGackstatter and others added 19 commits May 29, 2026 17:27
* feat: implement Clone for TransactionContextBuilder

* chore: add unwrap_unauthorized_err
* feat(agglayer): reject duplicate GER insertions

Adds a duplicate-detection guard to `bridge_config::update_ger`: after
calling `set_map_item`, the OLD_VALUE returned is compared against
EMPTY_WORD; if it is not empty the GER was already registered and the
transaction panics with ERR_GER_ALREADY_REGISTERED.

Note on Solidity divergence: the Solidity GlobalExitRootManager treats
duplicate GER insertions as idempotent no-ops. Miden intentionally
rejects them because a permanently-unconsumable network note is a worse
failure mode than an explicit transaction revert. This divergence is
documented in SPEC.md.

Docs updated on `update_ger`, the `update_ger.masm` note script, and
SPEC.md (Panics row + Section 2.3 prose). A new integration test
(`update_ger_rejects_duplicate`) verifies that a second UPDATE_GER note
carrying the same GER value is rejected with the expected error code.

Closes #2708

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: add CHANGELOG entry for #2983

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* review: combine padw+assert_eqw and use assert_transaction_executor_error!

- bridge_config.masm: collapse the `padw` / `assert_eqw` pair onto a single
  line as suggested in review.
- tests: replace the manual `is_err()` + `error_msg.contains(...)` block
  with the standard `assert_transaction_executor_error!` macro.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* review: use word::eqz + assert instead of padw + assert_eqw

Slightly cheaper than the `padw`-then-`assert_eqw` pair: `word::eqz`
consumes the 4-felt `OLD_VALUE` and pushes a single 0/1 flag, then a
plain `assert.err=...` verifies it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The MASM doc-comment and formatting conventions previously captured in
masm_doc_comment_fmt.md are now covered by the .claude/skills/masm-*
skill files plus the MASM formatting tool. Maintaining a separate
hand-written guide is redundant and prone to drift, so remove it.

Closes #2021

Co-authored-by: Claude (Opus) <noreply@anthropic.com>
The protocol does not prevent adding multiple attachments with the same
scheme to a note (e.g. two NetworkAccountTarget attachments). Document
that doing so is discouraged as it brings no additional benefit and only
increases public on-chain data and fees, and clarify in the rustdoc that
the first matching attachment is treated as the canonical one.

Closes #2873

Co-authored-by: Claude (Opus) <noreply@anthropic.com>
* refactor: include leaves number in BURN note ID computation

* Apply suggestions from code review

Co-authored-by: Marti <marcin.gorny.94@protonmail.com>

---------

Co-authored-by: Marti <marti@miden.team>
Co-authored-by: Marti <marcin.gorny.94@protonmail.com>
* Extend execution-error assertion macros with patter and any arms

* Fix formating

* Apply PR review feedback
…k level (#2993)

* feat: reject consuming and creating same note in a tx

* feat: erase notes on the fly

* chore: remove unused `NoteCommitmentMismatch` error variants

* chore: add changelog

* chore: rename input/output note tracker functions

* feat: reintroduce `NoteTracker` for readability

* chore: improve clarity of comments

* chore: format changelog

* chore: add second cross-tx circular dep test
…2988) (#3005)

* feat(protocol): add active_note::is_public / is_private (#2988)

Cherry-pick the reusable protocol-level building blocks from #2988,
which was merged to the agglayer branch as an agglayer-specific
B2AGG bridge-out hardening:

  - active_note::is_public
  - active_note::is_private

Both procs read the active note's metadata and decode its note
type via the existing note::metadata_into_note_type primitive
(from #2738), then compare against NOTE_TYPE_PUBLIC /
NOTE_TYPE_PRIVATE from miden::protocol::util::note. A parameterized
kernel test covers the Public and Private cases.

The agglayer-only files from #2988 (bridge_out.masm, B2AGG.masm,
tests/agglayer/bridge_out.rs) and the constant-value flip the PR
applied on agglayer's NoteType encoding are intentionally not
brought along: next already encodes Private=0, Public=1, and the
existing metadata_into_note_type is tailored to next's metadata
bit layout.

Co-Authored-By: Claude (Opus) <noreply@anthropic.com>

* fix(agglayer): enforce NoteType::Public for B2AGG bridge-out notes (#2988)

Cherry-pick the bridge-side half of #2988 to next. The B2AGG note
type lives in NoteMetadata, not in the recipient commitment, so an
attacker could submit a note with an identical recipient,
attachment, and asset but NoteType::Private. consensus would accept
it, the leaf would be folded into the on-chain Local Exit Tree, but
aggkit could never recover the pre-image off-chain — the LET mirror
would permanently desync and the bridge-out path would brick.

Add an active_note::is_public assert at the top of bridge_out,
extend the bridge_out and b2agg.masm doc comments, and parameterize
the existing destination-network-is-Miden test into
test_bridge_out_rejects_invalid_b2agg_note covering the
recipient-identical private-note case too.

Co-Authored-By: Claude (Opus) <noreply@anthropic.com>

* Apply suggestions from code review

Co-authored-by: Philipp Gackstatter <PhilippGackstatter@users.noreply.github.com>

* chore: fmt files

---------

Co-authored-by: Claude (Opus) <noreply@anthropic.com>
Co-authored-by: Marti <marti@miden.team>
Co-authored-by: Philipp Gackstatter <PhilippGackstatter@users.noreply.github.com>
…ution' into onur-multisig-smart-delayed-execution
partylikeits1983 and others added 3 commits June 4, 2026 17:50
feat: allow root-allowlisted tx scripts on network accounts (#3028)

AuthNetworkAccount previously banned every transaction script outright. This generalizes that ban into a tx-script-root allowlist that mirrors the existing note-script allowlist: a transaction that runs no tx script is allowed, and any tx script that does run must have its MAST root in an allowlist fixed at account creation. This lets network accounts run owner-approved scripts (e.g. setting the expiration delta, needed by the ntx-builder) while preserving the invariant that only root-pinned, pre-approved code can run against a signature-less account.

Changes:
- Add `TransactionScriptRoot` newtype mirroring `NoteScriptRoot`; `TransactionScript::root()` now returns it.
- Add `tx_script_allowlist.masm` (`assert_tx_script_allowed`) and switch the network auth component to it; remove the now-unused `assert_no_tx_script`.
- Add `NetworkAccountTxScriptAllowlist` storage (`allowed_tx_scripts` slot); an empty allowlist permits no tx scripts (the prior behavior).
- Wire through `AuthNetworkAccount::with_allowed_tx_scripts` and `AuthMethod::NetworkAccount.allowed_tx_script_roots`.

A root pins a script's code but not its `TX_SCRIPT_ARGS`/advice inputs, which an arbitrary submitter controls, so only scripts whose behavior is fixed regardless of those inputs should be allowlisted.

Breaking: the component gains a second storage slot (existing network accounts must be recreated), and `TransactionScript::root()` returns `TransactionScriptRoot` instead of `Word`.

Closes #3027.

Co-authored-by: Philipp Gackstatter <PhilippGackstatter@users.noreply.github.com>
* refactor: rename network-account note allowlist API

@PhilippGackstatter PhilippGackstatter left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I left a few questions/comments/nits. Overall, I'm having a hard time wrapping my head around this PR. In particular:

  • it seems we have multiple names for the same concept - would be great to unify.
  • I find the use of the pending slots hard to follow, and I'm not sure I understand why we need these. I left a question/suggestion to use auth args instead.
  • I don't understand why we need the cancel_and_propose functionality when we already have the individual cancel and propose.

Comment on lines +12 to +13
# Map entries: [TX_HASH] -> [unlock_timestamp, proposal_timestamp, min_cancel_sigs, 1]
const TX_PROPOSALS_SLOT = word("miden::standards::auth::multisig_smart::tx_proposals")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: I'd call this TX_SUMMARY_COMMITMENT instead of TX_HASH for accuracy (everywhere it is used in this PR).

Comment on lines +88 to +91
#! Loads the delayed execution policy configuration from `DELAYED_EXECUTION_SLOT`.
#!
#! Inputs: []
#! Outputs: [min_delay, propose_expiration_delta, 0, 0]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: Returning the zeros is unnecessary.

Comment on lines +155 to +157
proc get_propose_expiration_delta
exec.get_delayed_execution
# => [min_delay, propose_expiration_delta, 0, 0]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
proc get_propose_expiration_delta
exec.get_delayed_execution
# => [min_delay, propose_expiration_delta, 0, 0]
proc get_propose_expiration_delta
exec.get_delayed_execution_config
# => [min_delay, propose_expiration_delta, 0, 0]

Nit: Maybe the current procedure name could be a bit clearer.

Comment on lines +178 to +193
proc apply_expiration_delta
dup neq.0 assert.err=ERR_EXPIRATION_DELTA_ZERO
# => [expiration_delta]

exec.tx::update_expiration_block_delta
# => []

exec.tx::get_expiration_block_delta
# => [tx_expiration_delta]

dup neq.0 assert.err=ERR_TX_EXPIRATION_DELTA_NOT_SET
# => [tx_expiration_delta]

drop
# => []
end

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't understand why we set and then get the delta - this seems redundant. The neq.0 check is already done in tx::update_expiration_block_delta (but it is not documented in a Panics if section - if you don't mind, we could add it in this PR).

So, I think the whole procedure can be removed in favor of calling tx::update_expiration_block_delta directly.

Comment on lines +892 to +895
#! Finalizes a pending cancel action for the current transaction.
#!
#! Inputs: [num_verified_signatures, TX_HASH]
#! Outputs: [num_verified_signatures, TX_HASH]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Does this procedure use TX_HASH? If not, can we remove it from the inputs/outputs? I'd also pass in num_verified_signatures but not return it. Caller should dup it.

Comment on lines +810 to +822
#! Marks that this tx intends to execute a proposed action.
#!
#! Inputs: []
#! Outputs: []
#!
#! Panics if:
#! - `PENDING_EXECUTE_SLOT` is already non-empty (`ERR_PENDING_ALREADY_SET`).
#!
#! Side effects:
#! - Writes `PENDING_EXECUTE_FLAG` to `PENDING_EXECUTE_SLOT`.
#!
#! Invocation: exec
pub proc execute_proposed_transaction

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is the use of this procedure to communicate to the auth procedure that the current transaction executed a proposed action? It seems like this is intended to be called from a tx script, which is arbitrarily provided by the tx executor.

If so, can we not achieve the same by passing a flag as AUTH_ARGS? And if so, can we get rid of all the pending slots by using this pattern?

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.

I think the same pattern needs to stay for propose/cancel, so it might be good keeping execute on the same pattern for symmetry. I'm not sure if removing all the pending slots by using the AUTH_ARGS pattern works.

Comment on lines +477 to +487
#! Returns the pending execute transaction summary hash, or `EMPTY_WORD` if none is set.
#!
#! Inputs: []
#! Outputs: [PENDING_EXECUTE_HASH]
#!
#! Where:
#! - PENDING_EXECUTE_HASH is the pending execute transaction summary hash, or
#! `EMPTY_WORD` if no execute action is pending.
#!
#! Invocation: exec
proc get_pending_execute

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this procedure description accurate? In set_pending_execute we write PENDING_EXECUTE_FLAG and not a HASH, so there is a mismatch.

#! - Writes `NEW_TX_HASH` to `PENDING_PROPOSE_SLOT`.
#!
#! Invocation: exec
pub proc cancel_and_propose_new_transaction

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is the functionality of this procedure not the same as calling cancel and then propose? Why do we need a single procedure for this?

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.

Commented here now: #3043 (comment)

Comment on lines +851 to +852
exec.delayed_execution::finalize_timelock_proposals
# => [TX_SUMMARY_COMMITMENT]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Given that we clear the pending slots as the last step, the tx summary we computed earlier contains the changes to the pending slots. This works in principle, but feels a bit unclean. Mentioning in case this was not intended.

@mmagician
mmagician requested a review from Fumuran June 8, 2026 16:18
onurinanc and others added 8 commits July 31, 2026 12:24
…ctions (#3453)

* add tests covering the fungible faucet metadata string setters

* changelog

* add  note for token metadata actions

* record the FUNGIBLE_FAUCET_CONFIG benchmark measurements

* remove redundandt tests

* use the config note in the faucet metadata tests

* changelog

* rename as to FaucetMetadataConfig
…andard (#3449)

* feat(agglayer): add emergency pause to the bridge via the Pausable standard

Installs the standards Pausable + PausableManager components on the bridge
account and gates bridge_out, claim, update_ger, register_faucet,
store_faucet_metadata_hash, and deregister_faucet with
pausable::assert_not_paused. remove_ger stays live while paused as the
emergency remediation tool. Pause/unpause is authorized via the Authority
ADMIN fallback (dedicated PAUSER/UNPAUSER roles are a planned follow-up) and
driven by the standards PAUSE_ACTION note, which is added to the bridge's
note allowlist.

The testing-side bridge account validator now also certifies the pausable
is_paused storage slot, SPEC.md documents the mechanism (replacing the #2696
TODOs), integration tests cover pause/unpause, authorization, every gated
entry point, and the remove_ger exemption, and the note cost tables are
regenerated for the guard's extra cycles.

Closes #2696.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(agglayer): bind the emergency pause to the bridge via PAUSE_AGG_BRIDGE note

Replaces the generic standards PAUSE_ACTION note in the bridge's allowlist
with an agglayer-specific PAUSE_AGG_BRIDGE note. Like every other bridge
admin note, its script asserts the NetworkAccountTarget attachment, so an
admin's pause/unpause note for one account cannot be redirected onto the
bridge, and the attachment makes the note routable as a network note.

Also documents the remove_ger pause exemption in MASM, clarifies that the
slot certification lives in the testing-side validator, notes the
ADMIN-key-loss consequence in the SPEC, and adds tests for target binding,
network-note routability, and non-admin unpause of a paused bridge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(agglayer): drive the bridge pause with the standards PAUSE_ACTION note

Removes the bridge-specific PAUSE_AGG_BRIDGE wrapper note and allowlists
the standards PAUSE_ACTION note instead. The wrapper existed to bind the
note to the bridge account, but that gap affects the whole standards
admin-note family and is tracked in #3433, which will be fixed in this
release - so the bridge accepts the interim risk and stays on the standard
note rather than carrying a throwaway wrapper.

Adds AggLayerBridge::pause_note, which builds the note with the
NetworkAccountTarget attachment the standards builder omits, so bridge
pause notes stay routable network notes and fee-sponsorship-eligible; the
script does not assert the target until #3433 lands. SPEC 2.5 and 4.10 and
the allowed_notes rustdoc document the interim exposure in both directions:
anyone can burn a pause or unpause note by consuming it into an account
naming the bridge ADMIN as owner, so the pause is neither guaranteed to
engage nor guaranteed to lift until then. A characterization test pins the
current behaviour so #3433's fix has to invert it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(agglayer): trim the emergency-pause documentation

Addresses review feedback: drops the SPEC cross-references from rustdoc and
MASM, removes the repeated per-call-site pause comments, and cuts the
prose that either restates protocol design (in-flight notes cannot be
retracted), duplicates the #3433 issue, or belongs only in build.rs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(agglayer): narrow the unbound-note claim after #3372

NETWORK_ACCOUNT_CONFIG now asserts its target attachment, so the
limitation no longer covers the whole standards admin-note family.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(agglayer): address pause review comments

Moves the duplicated bridge setup helper into test_utils so pause.rs and
remove_ger.rs share one BridgeSetup, drops the local consume_and_commit in
favour of the shared consume_note helper, gives the note-builder parameter
a named type, and lifts the DUMMY_ETH_ADDRESS const up under the imports.

On the error type, renames InvalidPauseNoteTarget to
NonPublicPauseNoteTarget - NetworkAccountTarget::new can only fail with
TargetNotPublic - reworks the messages, and switches both variants from
#[source] to #[from] so pause_note uses plain ?.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(agglayer): keep the pause note errors on #[source]

#[from] generates public blanket conversions, so any future ? on a
NoteError or NetworkAccountTargetError in a function returning
AgglayerBridgeError would silently inherit a pause-note-specific message.
Both variant names assert a cause, so keep the explicit map_err at the one
construction site. This also matches the repo's prevailing convention.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude (Opus) <noreply@anthropic.com>
* feat: enforce sponsorship policy in network account auth

* chore: address review comments

* chore: update changelog
* bind the remaining config notes to their target account

* apply to faucetmetadataconfig

* changelog

* remove redundant comments

* fix comments

* feat: ensure presence of network account target

* chore: rename `account` to `target` in config notes

* chore: remove burn notion from comments

* chore: refactor pause_note_targeting_another_account_is_rejected test

* chore: update note cost table

---------

Co-authored-by: Philipp Gackstatter <PhilippGackstatter@users.noreply.github.com>
Co-authored-by: Marti <marti@miden.team>
…notes (#3447)

* feat(agglayer): enable on-chain bridge role rotation via RBAC action notes

Wraps up the role-transfer mechanism for the bridge's administrative
roles. PR #3130 moved the bridge to the miden-standards RBAC stack but
left rotation unexercisable on-chain: the bridge's AuthNetworkAccount
allowlist rejected the role-management note.

- Add RbacActionNote::script_root() to AggLayerBridge::allowed_notes(),
  making grant_role / revoke_role / set_role_admin / renounce_role
  reachable on the bridge. The fee-schedule entry derives from the
  allowlist automatically; the allowlist is component storage, so
  BRIDGE_CODE_COMMITMENT is unaffected.
- Add a create_existing_bridge_account_with_admin_and_roles fixture and
  an agglayer rbac_rotation test suite covering: grant-then-use,
  revoke-then-fail, unauthorized grant, ADMIN rotation, the
  last-admin-renounce lockout hazard, and an allowlist pin test.
- Document role rotation in SPEC.md section 2.5 (with the rotation
  ordering, last-admin, and target-binding caveats), add the RBAC_ACTION
  note as section 4.7, and resolve the stale rotation TODOs.

Closes #2706

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(agglayer): single bridge test fixture with explicit admin

Merge create_existing_bridge_account_with_admin_and_roles into
create_existing_bridge_account_with_roles: the fixture now always takes
the ADMIN member explicitly, and callers that do not exercise rotation
pass the dummy admin themselves.

Addresses review feedback on the duplicate constructors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(agglayer): drop historical framing and fix the ADMIN-decommission recipe

Document only present behavior in SPEC section 2.5, per review feedback.
Also correct the recipe for bounding ADMIN authority: each delegate
admin role must be populated and made self-administering, in committed
steps, before ADMIN is emptied - otherwise the delegate roles freeze
under the then-empty ADMIN role. Pinned by the new
self_administered_delegate_survives_admin_removal test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: move generic RBAC rotation guidance from the agglayer SPEC to the component docs

SPEC section 2.5 now keeps only the bridge-specific consequences of
on-chain role management and points to the miden-standards docs for the
generic hazards: the RoleBasedAccessControl rustdoc gains the corrected,
strictly ordered ADMIN-decommission recipe (with its no-quorum residual
risk), and the RbacActionNote rustdoc gains the note-model security
considerations (ordering, unexpiring pending notes, no target binding).

Addresses review feedback that most of this content belongs in the RBAC
documentation rather than the agglayer spec.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: shorten the RBAC rotation guidance

Cut the RoleBasedAccessControl decommission paragraph, the
RbacConfigNote security considerations, and the SPEC 2.5 caveat list
down to their essentials. While shortening, correct two claims for the
post-#3434 reality: the builder now attaches NetworkAccountTarget by
default, and the auto-allowlisted NETWORK_ACCOUNT_CONFIG note reaches
ADMIN-defaulted procedures - so the bridge's ADMIN role must never be
emptied (it would forfeit the post-deployment configuration channel),
and the SPEC now says so instead of describing decommissioning as
viable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(agglayer): trim the bridge fixture doc comment

Addresses review feedback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(standards): drop the target-binding caveat from the RbacConfigNote docs

Target binding will be enforced by the note script itself (tracked
separately), so the caveat is not worth documenting.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(agglayer): drop the target-binding caveat from SPEC section 2.5

Note target binding is tracked in a separate issue; the section 4.7
permissions table keeps the factual consumer row.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(agglayer): drop rotation tests already covered by the generic RBAC suite

Keep only the bridge-specific wiring tests: grant-then-use and
revoke-then-fail (RBAC_CONFIG through the bridge allowlist, fee
schedule, and update_ger gating) plus the allowlist pin. Admin
rotation, delegation exclusivity, and last-admin removal are component
semantics covered in tests/scripts/rbac/.

Three properties the generic suite did not yet cover move there instead
of being dropped: an unauthorized-sender RBAC_CONFIG note is rejected,
a note targeting another account is consumable (the assertion that
inverts once target binding lands), and a self-administered role stays
manageable after ADMIN renounces (set_role_admin(X, X)).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(agglayer): share the bridge setup helper across test suites

Extract the duplicated setup_bridge helpers from remove_ger.rs and
rbac_rotation.rs into test_utils (BridgeSetup + setup_bridge), seeding
a chain-resident admin wallet so both suites - and future ones - can
use the same fixture.

Addresses review feedback on re-using test helpers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(agglayer): pin paused-bridge rotation and the effective note allowlist

Post-merge follow-ups: a test pinning that RBAC_CONFIG notes stay
consumable while the bridge is paused (the emergency-recovery path SPEC
section 2.5 documents), an extension of the allowlist pin to the
effective on-account set including the AuthNetworkAccount auto-added
entries, and two wording fixes (NETWORK_ACCOUNT_CONFIG also dispatches
fee-policy updates; the bench dummy admin covers all ADMIN-gated
operations).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(agglayer): assert the pause actually took effect during paused rotation

Promote is_bridge_paused into test_utils, assert the pause state before
and after the paused-bridge rotation, and pin the bridge's tx-script
allowlist alongside the note allowlist.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Apply suggestions from code review

Co-authored-by: Marti <marcin.gorny.94@protonmail.com>

---------

Co-authored-by: Claude (Opus) <noreply@anthropic.com>
* refactor(standards): remove the AuthSingleSigAcl auth component

Its exempt-procedure-list mechanism let fee-charging accounts be
drained: anyone could call an exempt procedure without a signature,
but the account still paid the transaction fee. The team decided to
remove the component rather than fix it now (see issue discussion).

The plain AuthSingleSig component (every call requires a signature)
remains available and now backs the "singlesig user faucet" factories.

Closes #3360

* fix: revert unnecessary comment change in BurnNote conversion

Per review feedback: the struct-level doc comment already covers the
network-execution caveat; the internal conversion comment didn't need
the same rewording.

---------

Co-authored-by: Claude (Opus) <noreply@anthropic.com>
* fix: restrict indexed input note asset removal

* refactor: share indexed input note removal guard

* test: make malicious input note theft regression explicit

* fix: require native account context for indexed input note asset removal

The account-origin check resolves the caller against the *active* account,
so the gate alone was bypassable: a malicious note script could route the
indexed removal through an attacker-controlled foreign account via FPI,
where that foreign account is active and vouches for its own procedures.
The full #3445 drain still worked end to end against a standard wallet.

Add exec.memory::assert_native_account to the guard, which closes that
path while leaving the one legitimate by-index caller intact (the fee
manager collects sponsorship note assets from the native account's auth
procedure).

Tests: a note-script-via-FPI regression test, and an explicit test that
transaction scripts are rejected too - the latter pins a behavior change
that was previously only implied by a deleted test.

---------

Co-authored-by: Claude (Opus) <noreply@anthropic.com>
Co-authored-by: Bobbin Threadbare <43513081+bobbinth@users.noreply.github.com>
TomasArrachea and others added 4 commits August 3, 2026 20:16
* feat: add miden-build-utils crate

* feat: remove copy_directory helper

* feat: more build-utils

* feat: remove TransactionScript and NoteScript constructor from Program

* feat: remove unused helpers

* feat: refactors

* revert: remove Program constructors

* chore: changelog

* chore: remove leftover changes

* refactor: assemble utils

* review: move regex into LazyLock

* fix: cargo shear

* review: address comments

* review: address remaining comments

* fix: changelog merge

* feat: add publish logic to read masp file from OUT_DIR

* refactor: inline code

---------

Co-authored-by: Bobbin Threadbare <43513081+bobbinth@users.noreply.github.com>
* refactor min burn amount comment

* changelog

* Update CHANGELOG.md

Co-authored-by: zeapoz <zeapo@pm.me>

---------

Co-authored-by: Philipp Gackstatter <PhilippGackstatter@users.noreply.github.com>
Co-authored-by: zeapoz <zeapo@pm.me>
* feat: assemble `ExpirationTransactionScript` on build-time

* feat: move script to library procedure inside the standards package

* chore: changelog

* feat: add `SendNotesTransactionScript` to standards package

* chore: changelog

* feat: add tx_script() accessor

* chore: comments

* chore: tests

* chore: fmt

* feat: factor out common masm code

* feat: comments; masm signatures

* feat: add tests

* review: address comments

* review: address nits and tests

* feat: address more comments

* review: address comments

* chore: update comments

* review: make common module private

* review: improve comments

* review: validate asset composition

* review: add `add_existing_non_fungible_faucet` to mockchain builder helper

* review: remove `validate_note_records` costly validation

* review: keep note idx and read ptr on stack

* review: add cross-check with asset composition to pick script

---------

Co-authored-by: Bobbin Threadbare <43513081+bobbinth@users.noreply.github.com>
* chore: fix fresh build

* change version to rc1

* regenerate cost tables

---------

Co-authored-by: Marti <marti@miden.team>
onurinanc and others added 8 commits August 4, 2026 12:34
…3471)

* fix(tx): gate only signature production outside the auth procedure

* changelog

* fix in_auth_procedure: bool comment

* remove auth request probe component
* chore: reject unbounded signatures in tx event

* chore: add changelog

* chore: use multiple assertions

* fix: changelog entry
… note (#3469)

* fix pswap.masm

* add regression tests

* test: cover a pswap note-fill payback created behind a remainder note

* rename tests

* changelog

* clean tests

* simplify tests

* Update crates/miden-testing/tests/scripts/pswap.rs

---------

Co-authored-by: Philipp Gackstatter <PhilippGackstatter@users.noreply.github.com>
The v0.16.0-rc.1 release aborted mid-publish with:

  403 Forbidden: Trusted Publishing tokens do not support creating new
  crates. Publish the crate manually, first

`miden-protocol-build-utils` was added to the workspace and had never been
published, and crates.io does not let an OIDC token create a new crate name.
`cargo publish --dry-run` never contacts the registry to reserve a name, so
nothing caught this until the tagged release had already been cut.

Add scripts/check-crates-published.sh, which enumerates publishable workspace
members via `cargo metadata` and checks each name against the crates.io sparse
index. It runs in the publish job, ahead of the token mint - the check needs no
credential, and a slow registry would otherwise eat into the token's short
lifetime. A release that would abort partway now stops before publishing
anything, with an error naming the manual-claim and Trusted Publishing setup
needed to fix it.

The remediation is explicit that the claim must happen at a version strictly
below the upcoming release, from a placeholder outside this workspace. Claiming
the name at the version about to be released is what turned this incident's
retry into "crate miden-protocol-build-utils@0.16.0-rc.1 already exists on
crates.io index", and publishing from the workspace would need --allow-dirty,
which bakes untracked files into a permanently public artifact.

Scope is narrow and stated in the header: a hit proves the name is registered
by someone, not that we own it, that Trusted Publishing is configured, or that
the release version is free. Those still surface only at publish time, and the
success message says so rather than reading as "the release will succeed".

`cargo metadata` and the jq filter are captured into a variable rather than
read through a process substitution, so a failed or truncated read cannot
yield a partial crate list that silently passes. An unverifiable crate refuses
to guess rather than passing.

All eight publishable members currently resolve on the index, so this is safe
to land as-is.

Co-authored-by: Claude (Opus) <noreply@anthropic.com>
Co-authored-by: Marti <marti@miden.team>
@onurinanc

Copy link
Copy Markdown
Collaborator Author

Update:

Now that #3383 binds the reference block commitment and the expiration block delta into the signed transaction summary, the delayed-execution flow needs a small design change. This comment explains why and proposes a change in the design.

Before #3383, the transaction summary commitment was:

TX_SUMMARY_COMMITMENT = hash(account_delta, input_notes, output_notes, user_params)

It was independent of the reference block. After #3383 it also binds the block and expiration:

TX_SUMMARY_COMMITMENT = hash(account_delta, input_notes, output_notes, BLOCK_COMMITMENT, expiration_delta, user_params)

A delayed transaction is proposed at one block and executed later, at a different block, and the delayed-execution flow currently keys a proposal on the transaction summary commitment and recomputes that same commitment at execute time to look the proposal.

With the change binding block into the commitment, the value computed at propose time, never equals the value computed at execute time, so the lookup fails and the timelock and consume step never runs.

Proposed design:

We can keep the TX_SUMMARY_COMMITMENT exactly as #3383 defines it for signature verification, and introduce a second, block-independent commitment that identifies what the transaction does, used only as the proposal key:

ACTION_COMMITMENT = hash(account_delta, input_notes, output_notes, user_params)
                    # i.e. the tx summary with BLOCK_COMMITMENT and expiration_delta are zero

The reference block and expiration are execution context, not part of the transaction's intent, so excluding them is both necessary so that propose and execute agree on the commitment. The account delta, input and output notes, and user parameters constitutes ACTION_COMMITMENT, so the action's identity is still works.

At execute time the auth procedure computes both commitments from the same summary: the full one for signature verification, and the action commitment to match the proposal. To make this happen:

  • Add a hash_action_commitment helper that hashes the six summary words with the block commitment and expiration delta zeroed out, so the result identifies what a transaction does, independent of when it runs.
  • Derive both commitments from the same summary: look up proposals by the action commitment (is_tx_proposed, assert_and_consume_proposal), and keep verifying signatures against the full tx-summary commitment.
  • multisig_smart/delayed_execution.masm: proposals are now stored under the action commitment, and that's what cosigners sign.
  • TransactionSummary::action_commitment() in miden-protocol as the Rust equivalent, so proposers and tests derive the same value as the MASM helper.

Note on signatures:

Because signature verification stays over the block bounded summary, the execute transaction verifies fresh signatures over its own summary, while the proposal is authorized by signatures over the action commitment. So the proposal and the execution are authorized independently: the proposal proves "this action was scheduled and the timelock started", the execution proves "the approvers authorize this specific run now". This is a deliberate change from the earlier "sign once, reuse the same signature at propose and execute" idea, which only worked while the summary was block-independent. It means the following:

  • At propose time → the action commitment is signed.
  • At execute time → the full transaction summary is signed.

Any feedback on this design? @bobbinth @PhilippGackstatter @mmagician

@PhilippGackstatter

PhilippGackstatter commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Thanks for the updated proposal!

A delayed transaction is proposed at one block and executed later, at a different block

Does this necessarily have to be the case, or is it even desirable?

As mentioned in #3210 (comment):

Contrary to intuition, committing to the block also doesn't force multiple in-flight multisig transactions to be executed in the same order in which they were created. That is, if tx A with ref block 5 is created before tx B with ref block 10, then it is actually allowed and possible to execute B first and then A.

So, I think there is no protocol reason why we'd need to propose at one block and execute at a different block.

Additionally, if the multisig tx involves FPI calls, then what the proposal tx against ref block X signs are the FPI calls against the account states in block X. If we later change the reference block, the foreign account states may have changed and return different values, and the signature would be stale. From this perspective, it seems desirable to use the same ref block during proposal and execution, because it guarantees the action/effects portion of the tx summary does not change.

I can also see the counterargument to this: Re-signing at execution time means we can use a newer ref block and execute against newer foreign account state. However, by its nature, delayed execution, due to the inherent delay, authorizes an action that shouldn't require the newest blockchain state, else it would be the wrong mechanism.

For completeness, there is also the fee-related concern mentioned in #3210 (comment), but I think we'll solve this separately in #3358.

  • At propose time → the action commitment is signed.
  • At execute time → the full transaction summary is signed.

In my mind, it's a UX feature that we can use the same signature for proposing and executing, as signing a proposal itself already signals the approval for its execution (at least for some timeframe - signing a block commitment + expiration delta does this directly at the signature level).

To summarize, I think the design space allows for the old and the new model. I think the main differences are:

  • old/curent model: uses the proposed, likely older foreign account state, but requires signing just once per approver.
  • new model: uses the newest foreign account state, but requires signing twice per approver.

For fee-computation purposes, a newer ref block is always preferable, but due to the signing delay for multisigs in general, I think we need a separate general mechanism here, and so I'm not sure that it should be the deciding factor.

Given the above, I still prefer the old model due to the UX benefits.

@onurinanc

Copy link
Copy Markdown
Collaborator Author

Does this necessarily have to be the case, or is it even desirable?

I do think it is necessary. In the same ref block model, how do we enforce that min_delay has elapsed when the execute tx references an old block? Since the timelock reads the timestamp of the ref block, execute necessarily references a newer block, this changes the full summary and makes an action commitment mandatory for matching.

Actually, a single signature per approver can be achieved without requiring the same ref block, we sign the action commitment at propose time, and at execute time we verify that same signature against the action commitment as well. The ref blocks would still differ, but since the signed message stays the same, the same signature can be reused.

The problem is that this brings #3210 back. If we only sign the action commitment, the signed message no longer binds the reference block commitment or the expiration delta .

Since the execute block isn't known at propose time, the only way to keep the block and expiration bound is to verify the full summary at execute time. For delayed txs, do you think reduced binding is acceptable?

@PhilippGackstatter

Copy link
Copy Markdown
Contributor

Since the timelock reads the timestamp of the ref block, execute necessarily references a newer block, this changes the full summary and makes an action commitment mandatory for matching.

Ah, true. We require a newer ref block to enforce the delay.

a single signature per approver can be achieved without requiring the same ref block, we sign the action commitment at propose time, and at execute time we verify that same signature against the action commitment as well.

&

The problem is that this brings #3210 back.

I think even when we sign the "action commitment" at propose time and the tx summary at execution time, we have the "reorganization and cross-network ambiguity" described in #3210, because an action commitment doesn't bind the ref block or the chain's identity.

So, I guess the most secure option is to avoid the single signature per approver and require signing at execution time from everyone involved. That way, actual execution is bound to a specific ref block and chain identity. That's the proposal you made originally.

I think reduced binding may be acceptable, but I'm not 100% convinced, and so I would err on the side of caution. We can always ship a "multisig smart 2.0" at a later point that improves such aspects.

One note on the naming: Since the "action commitment" commits to the outcome of a tx, but doesn't care how that outcome was achieved (e.g. it doesn't bind the tx script), I think transaction effects - focussing on the effects/outcome - would be a better term than "action", which focusses on the actions that were executed in a transaction. Transaction effects is also a standard term in other chains, so I think this fits well.

Two notes regarding the implementation:

@partylikeits1983 partylikeits1983 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It looks like a merge issue has caused this PR to show a much larger diff than the feature itself. @onurinanc could you please take a look and update the branch so the diff contains only the intended changes?

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.

Add DelayedExecution (Timelocked Accounts) to AuthMultisigSmart