Conversation
bobbinth
left a comment
There was a problem hiding this comment.
Looks good! Thank you! I left just a few questions/comments inline.
bobbinth
left a comment
There was a problem hiding this comment.
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.
bobbinth
left a comment
There was a problem hiding this comment.
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_slotprocedure to figure out if theis_pausedstorage 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).
| #! 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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
PhilippGackstatter
left a comment
There was a problem hiding this comment.
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.
| #! 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. |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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<()> { |
There was a problem hiding this comment.
| 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?
| let mut rng = | ||
| RandomCoin::new(Word::from([1u32, 2, 3, 4].map(|v| Felt::new_unchecked(v as u64)))); |
There was a problem hiding this comment.
| 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" |
There was a problem hiding this comment.
Nit: I'd rename this to ERR_PAUSABLE_IS_PAUSED.
| let seed: [u64; 4] = rand::random(); | ||
| let mut rng = RandomCoin::new(Word::from(seed.map(Felt::new_unchecked))); |
There was a problem hiding this comment.
| 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
left a comment
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
is this expected behavior? My intuition would have been that pausing an already-paused account would be a no-op
| /// role for both pause and unpause (no PAUSER / UNPAUSER separation — emergency pause is a | ||
| /// coarse-grained capability). |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| 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), |
There was a problem hiding this comment.
I think we can use WordSchema with a default value, WDYT?
| #[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)?; |
There was a problem hiding this comment.
nit: I wonder how easy would it be to parametrize pausable_{action}_fails_when_paused tests with rstest?
There was a problem hiding this comment.
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.
| /// setters can read it without panicking. Pause / unpause administration is exposed by the | ||
| /// optional [`crate::account::access::pausable::PausableManager`] component. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
Looks good! Thank you!
Let's create an issue to address the remaining comments in a follow-up PR.
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:
Related Issues: