Skip to content

Pausable#2793

Merged
bobbinth merged 42 commits into
nextfrom
pausable
May 22, 2026
Merged

Pausable#2793
bobbinth merged 42 commits into
nextfrom
pausable

Conversation

@onurinanc

Copy link
Copy Markdown
Collaborator

Closes issue including the design discussion provided here: #2241

This PR adds a Pausable component without Access Control mechanism included as we split this work into small PRs.

In the next PRs, we'll integrate the following features:

  • Pausing is controlled by the owner.
  • Pausing is controlled by some role in an RBAC setup.

Related Issues:

@bobbinth bobbinth 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.

Looks good! Thank you! I left just a few questions/comments inline.

Comment thread crates/miden-standards/asm/standards/utils/pausable.masm Outdated
Comment thread crates/miden-standards/asm/standards/access/pausable/mod.masm
Comment thread crates/miden-standards/asm/standards/utils/pausable.masm Outdated
Comment thread crates/miden-standards/src/account/pausable.rs Outdated
@mmagician mmagician added standards Related to standard note scripts or account components pr-from-maintainers PRs that come from internal contributors or integration partners. They should be given priority labels Apr 29, 2026

@bobbinth bobbinth 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.

Looks good! Thank you! I left a couple of comments inline - but overall, as you've outlined, I'd come back to this PR after we get the core PRs merged (i.e., the RBAC and mint/burn token policies).

I would maybe even come back to this after we've implemented transfer policies and merged fungible faucets into a single component. I think the overall structure would be much more clear then.

Comment thread crates/miden-standards/asm/account_components/utils/pausable.masm Outdated
Comment thread crates/miden-standards/asm/standards/utils/pausable.masm Outdated

@bobbinth bobbinth 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.

Looks good! Thank you! I reviewed mostly non-test code and left some comments inline. In the interest of getting this merged sooner, I think we should only address the simple comments here, and leave the bigger ones to a follow-up PR.

The follow-ups would include:

  • Using active_account::has_storage_slot procedure to figure out if the is_paused storage slot is installed in an account. This procedure first needs to be added to the transaction kernel.
  • Adding a level of indirection to the transfer policy handlers so that we could control them from the TokenPolicyManager (similar to how we control mint/burn policies).

Comment on lines +114 to +118
#! If the `IS_PAUSED_SLOT` is not installed on the account, `active_account:: get_item`
#! returns the zero word, which is treated as "unpaused" — so the assertion is a no-op
#! for accounts that did not install the Pausable component. This is the canonical way for
#! cross-cutting consumers (TokenPolicyManager dispatch, asset callbacks, metadata setters) to
#! gate their logic on pause state without making Pausable a hard dependency.

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 behavior described here correct? The docs for active_account::get_item say that it should panic if the requested storage slot does not exist. cc @PhilippGackstatter

If active_account::get_item does not panic on non-existent storage slot, that's a bug and we should fix it (let's open an issue for this).

If it does panic, the we should update the logic here, but AFACT, we actually don't have something like active_account::has_storage_slot. I think we should add it and then based this procedure on that. But I would do it in a separate PR.

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.

The documented behavior should be correct, get_item panics on unknown slots. test_account_get_item_fails_on_unknown_slot tests that.

The idea is that, because assert_not_paused is used so pervasively, we don't want to force every faucet to explicitly set that slot, if they don't use pause functionality? I guess that's fine, though being explicit about it also has its own value. I'd lean towards being explicit, but not a strong opinion.

The has_storage_slot functionality may be interesting independently, though I'd add it when we need it.

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 think this is more about implied dependencies. Without the extra check, TokenPolicyManager will require Pausable as a dependency. So, anyone deploying a faucet would need to also add the Pausable component to their account, even if they don't need this functionality.

Comment thread crates/miden-standards/src/account/faucets/fungible/mod.rs Outdated
Comment thread crates/miden-standards/src/account/access/pausable/mod.rs
Comment thread crates/miden-standards/src/account/pausable/mod.rs Outdated
Comment thread crates/miden-standards/src/account/pausable/mod.rs Outdated
Comment thread crates/miden-standards/src/account/pausable/mod.rs Outdated
@onurinanc onurinanc requested a review from bobbinth May 22, 2026 07:07

@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.

Looks good to me! Left a few comments, nothing blocking.

I agree with the follow-up of moving the assert_not_paused into the token policy manager. Pause functionality seems to be quite fundamental, so making the manager pause-aware makes sense to me.

Comment thread crates/miden-standards/asm/standards/access/pausable/mod.masm
Comment on lines +114 to +118
#! If the `IS_PAUSED_SLOT` is not installed on the account, `active_account:: get_item`
#! returns the zero word, which is treated as "unpaused" — so the assertion is a no-op
#! for accounts that did not install the Pausable component. This is the canonical way for
#! cross-cutting consumers (TokenPolicyManager dispatch, asset callbacks, metadata setters) to
#! gate their logic on pause state without making Pausable a hard dependency.

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.

The documented behavior should be correct, get_item panics on unknown slots. test_account_get_item_fails_on_unknown_slot tests that.

The idea is that, because assert_not_paused is used so pervasively, we don't want to force every faucet to explicitly set that slot, if they don't use pause functionality? I guess that's fine, though being explicit about it also has its own value. I'd lean towards being explicit, but not a strong opinion.

The has_storage_slot functionality may be interesting independently, though I'd add it when we need it.

/// - [`Self::is_paused_slot()`]: single word; the zero word means unpaused, `[1, 0, 0, 0]` means
/// paused. Any non-zero word is interpreted as paused by the MASM helpers.
#[derive(Debug, Clone, Copy, Default)]
pub struct PausableStorage {

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.

Iiuc, we only use this in Pausable, so why is this a separate type? I have the same question about BasicBlocklist and BlocklistStorage.

If it must be a separate type, I'd redefine Pausable(PausableStorage), unless that creates issues.

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 think it is a good pattern to have a separate storage type, and I'd propagate it to other components as well. But I do agree that Pausable(PausableStorage) is probably the right way to compose things.

// ================================================================================================

#[tokio::test]
async fn pausable_manager_pause_succeeds_when_owner_signs() -> anyhow::Result<()> {

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
async fn pausable_manager_pause_succeeds_when_owner_signs() -> anyhow::Result<()> {
async fn pausable_manager_pause_succeeds_when_sender_is_owner() -> anyhow::Result<()> {

I don't think the owner signs here?

Comment on lines +498 to +499
let mut rng =
RandomCoin::new(Word::from([1u32, 2, 3, 4].map(|v| Felt::new_unchecked(v as u64))));

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
let mut rng =
RandomCoin::new(Word::from([1u32, 2, 3, 4].map(|v| Felt::new_unchecked(v as u64))));
let mut rng = RandomCoin::new(Word::from([1u32, 2, 3, 4]));

# ERRORS
# ================================================================================================

const ERR_PAUSABLE_ENFORCED_PAUSE = "the contract is paused"

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 rename this to ERR_PAUSABLE_IS_PAUSED.

Comment on lines +82 to +83
let seed: [u64; 4] = rand::random();
let mut rng = RandomCoin::new(Word::from(seed.map(Felt::new_unchecked)));

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
let seed: [u64; 4] = rand::random();
let mut rng = RandomCoin::new(Word::from(seed.map(Felt::new_unchecked)));
let seed: [u32; 4] = rand::random();
let mut rng = RandomCoin::new(Word::from(seed));

@mmagician mmagician left a comment

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.

LGTM ✅
I'd like to still discuss whether bundling the PausableManager with the faucets by default would be a good idea

# => [is_paused, pad(15)]
end

#! Sets the paused flag. Fails if already paused.

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.

is this expected behavior? My intuition would have been that pausing an already-paused account would be a no-op

Comment on lines +37 to +38
/// role for both pause and unpause (no PAUSER / UNPAUSER separation — emergency pause is a
/// coarse-grained capability).

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.

just FYI, for AggLayer we might want to decide that we actually do want a separate role for Pauser & Unpauser roles, see #2701

but it's not a priority and can be done in a follow-up if we decide to proceed with the separation

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 have intentionally included this comment in this part. The reason is that before the design specified here by @bobbinth: #2862 (comment) applied to this PR, with the initial design, it was possible to separate PAUSER and UNPAUSER roles by having #2701 in mind. Additionally, I believe other projects intent to use this role separation since it's meaningful in the case of compromised pauser  / unpauser roles. I haven't discovered yet with the current design how it will be possible to include this feature yet, but I'm aiming to include this feature in the follow-up PR.

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 think we may be able to do this via the per-procedure role assignment of the RBAC component I mentioned in some other comment.

Comment on lines +91 to +94
FeltSchema::felt("w0").with_default(Felt::ZERO),
FeltSchema::felt("w1").with_default(Felt::ZERO),
FeltSchema::felt("w2").with_default(Felt::ZERO),
FeltSchema::felt("w3").with_default(Felt::ZERO),

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.

I think we can use WordSchema with a default value, WDYT?

Comment on lines +432 to +485
#[tokio::test]
async fn pausable_mint_fails_when_paused() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let _target = builder.add_existing_wallet(Auth::IncrNonce)?;
let faucet = add_faucet_with_pause_and_policies(&mut builder, *OWNER_ID)?;

let pause_note = build_pause_note(*OWNER_ID)?;
builder.add_output_note(RawOutputNote::Full(pause_note.clone()));

let mut mock_chain = builder.build()?;
mock_chain.prove_next_block()?;

// Pause the faucet first.
execute_note_on_faucet(&mut mock_chain, faucet.id(), &pause_note).await?;

// Build a mint tx-script targeting the now-paused faucet.
let recipient_word = Word::from([0u32, 1, 2, 3]);
let tx_script_code = format!(
r#"
begin
padw padw push.0
push.{recipient}
push.{note_type}
push.{tag}
push.{amount}
call.::miden::standards::faucets::fungible::mint_and_send
dropw dropw dropw dropw
end
"#,
recipient = recipient_word,
note_type = NoteType::Private as u8,
tag = u32::from(NoteTag::default()),
amount = 100u64,
);
let tx_script =
miden_standards::code_builder::CodeBuilder::default().compile_tx_script(&tx_script_code)?;

let result = mock_chain
.build_tx_context(faucet.id(), &[], &[])?
.tx_script(tx_script)
.build()?
.execute()
.await;

// execute_mint_policy calls exec.pausable::assert_not_paused before dispatch → panic.
assert_transaction_executor_error!(result, ERR_PAUSABLE_IS_PAUSED);

Ok(())
}

#[tokio::test]
async fn pausable_burn_fails_when_paused() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let faucet = add_faucet_with_pause_and_policies(&mut builder, *OWNER_ID)?;

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.

nit: I wonder how easy would it be to parametrize pausable_{action}_fails_when_paused tests with rstest?

@onurinanc onurinanc May 22, 2026

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 have considered to include this one but as different actions need different setups, which require a matching each case for different actions, this might overload one test function pausable_{action}_fails_when_paused.

Comment on lines +390 to +391
/// setters can read it without panicking. Pause / unpause administration is exposed by the
/// optional [`crate::account::access::pausable::PausableManager`] component.

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.

I would have expected that if we have is_paused slot, then automatically we would get PausableManager, otherwise is_paused doesn't serve much purpose.

And while users could still manually construct whichever combinations of components they wish, I think it the standard for the faucet should also bundle the manager.

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.

This is resulting from the design pattern I described in #2862 (comment). The basic idea is that there could be multiple implementations of PausableManager, and while we provide a stock implementation, users may have their own requirements. This separation will enable using standard Pausable component, while making the management aspect a bit more flexible.

@bobbinth bobbinth 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.

Looks good! Thank you!

Let's create an issue to address the remaining comments in a follow-up PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-from-maintainers PRs that come from internal contributors or integration partners. They should be given priority standards Related to standard note scripts or account components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants