feat(protocol): tie the asset callback flag to the callback slots - #3658
feat(protocol): tie the asset callback flag to the callback slots#3658onurinanc wants to merge 20 commits into
Conversation
…k slot is present
…AssetCallbackFlag
PhilippGackstatter
left a comment
There was a problem hiding this comment.
Looks good!
I think the main suggestion I have is adding the callback slots in TokenPolicyManager unconditionally.
| #! flag encoded in its account ID, and that flag is immutable once the ID is ground. A callback slot | ||
| #! installed on an account whose flag is disabled would therefore look correctly configured while | ||
| #! never being invoked, silently and permanently disabling whatever the callback enforces. | ||
| #! |
There was a problem hiding this comment.
I think not requiring slots when the account ID flag is enabled actually makes sense. This retains the ability to add new types of callback slots that are introduced in a later protocol version.
I'd add something like this here:
`has_callback_slot` must imply `has_callbacks`, but not vice versa. That is, the callback flag
can be enabled without callback slots present. This is allowed so that an account retains the
ability to add a callback slot via an account upgrade later, which is particularly useful if new
types of callbacks are introduced.
| # The same applies to the asset callback rule validated in | ||
| # prologue::validate_asset_callbacks: an upgrade must not add an asset callback slot to an | ||
| # account whose asset callback flag is disabled, since the flag is immutable and the callback | ||
| # could then never be invoked. |
There was a problem hiding this comment.
Thanks for adding this 👌
| /// The flag determines whether assets issued by the account (if any) trigger callbacks and is | ||
| /// encoded into the resulting [`AccountId`] at creation. It is normally derived from the | ||
| /// account's storage: it is [`AssetCallbackFlag::Enabled`] if any component installs one of the | ||
| /// protocol-reserved asset callback slots (see [`AssetCallbacks::is_installed`]) and | ||
| /// [`AssetCallbackFlag::Disabled`] otherwise. There is deliberately no way to disable the flag | ||
| /// for an account that does install such a slot, since the kernel gates callback invocation on | ||
| /// the flag alone and the flag cannot be changed after the ID is ground. | ||
| pub fn enable_asset_callbacks(mut self) -> Self { |
There was a problem hiding this comment.
This seems like general account builder information, so I would move this to the AccountBuilder type-level docs. These function docs can keep just the first sentence.
| let account = Account::builder([7; 32]) | ||
| .with_component(NoopAuthComponent) | ||
| .with_component(CustomComponent1 { slot0: 25 }) | ||
| .build() | ||
| .unwrap(); | ||
|
|
||
| assert_eq!(account.id().asset_callback_flag(), AssetCallbackFlag::Disabled); | ||
|
|
||
| let account = Account::builder([7; 32]) | ||
| .enable_asset_callbacks() | ||
| .with_component(NoopAuthComponent) | ||
| .with_component(CustomComponent1 { slot0: 25 }) | ||
| .build() | ||
| .unwrap(); | ||
|
|
||
| assert_eq!(account.id().asset_callback_flag(), AssetCallbackFlag::Enabled); |
There was a problem hiding this comment.
let builder = Account::builder([7; 32])
.with_component(NoopAuthComponent)
.with_component(CustomComponent1 { slot0: 25 });
let account = builder.clone().build().unwrap();
assert_eq!(account.id().asset_callback_flag(), AssetCallbackFlag::Disabled);
let account = builder.enable_asset_callbacks().build().unwrap();
assert_eq!(account.id().asset_callback_flag(), AssetCallbackFlag::Enabled);nit: conciseness
| /// Accounts constructed outside of the builder are rejected if they install a callback slot | ||
| /// without having callbacks enabled. | ||
| #[test] | ||
| fn account_new_rejects_callback_slot_with_disabled_flag() { |
There was a problem hiding this comment.
nit: This tests the behavior of Account::new so the test module in crates/miden-protocol/src/account/mod.rs seems like a better place.
| /// | ||
| /// The transaction kernel decides whether to invoke an account's asset callbacks solely from the | ||
| /// [`AssetCallbackFlag`] encoded in its [`AccountId`], and that flag is immutable once the ID is | ||
| /// ground. A callback slot installed on an account whose flag is disabled would therefore look | ||
| /// correctly configured while never being invoked, silently and permanently disabling whatever the | ||
| /// callback enforces. The transaction kernel rejects such accounts when they are created; this | ||
| /// mirrors that rule for accounts that are constructed or deserialized outside of a transaction. | ||
| /// |
There was a problem hiding this comment.
I'd remove this and point to AccountBuilder docs for details, so we don't duplicate this info.
| for slot_name in AssetCallbacks::slot_names() { | ||
| if storage.get(slot_name).is_some() { | ||
| return Err(AccountError::AssetCallbackSlotWithDisabledFlag { | ||
| account_id: id, | ||
| slot_name: slot_name.clone(), | ||
| }); | ||
| } | ||
| } |
There was a problem hiding this comment.
I think this could be written more concisely as:
if AssetCallbacks::is_installed(storage) {
return Err(...);
}But I think this would read more nicely if it was storage.has_callbacks().
| /// Returns `true` if `storage` contains at least one of the protocol-reserved asset callback | ||
| /// slots, `false` otherwise. | ||
| pub fn is_installed(storage: &AccountStorage) -> bool { |
There was a problem hiding this comment.
As mentioned in another comment, I'd make this a method on AccountStorage.
| /// switch. The slots are omitted only when no send or receive policy of any kind is registered, in | ||
| /// which case the faucet's account ID is created with | ||
| /// [`AssetCallbackFlag::Disabled`][miden_protocol::account::AssetCallbackFlag::Disabled]. |
There was a problem hiding this comment.
Independent of the ability to add callback slots later, I think it makes sense to let the TokenPolicyManager always add the callback slots, because otherwise the callback flag will be disabled for the lifetime of the account and the transfer policies of the policy manager become unusable permanently.
Technically users can of course enable the flag by themselves to override this, but I find this a bit too subtle.
| let code = " | ||
| use miden::tx_kernel_core::prologue | ||
|
|
||
| begin | ||
| exec.prologue::prepare_transaction | ||
| end | ||
| "; | ||
|
|
||
| let result = mock_tx.execute_code(code).await; | ||
|
|
||
| assert_execution_error!( |
There was a problem hiding this comment.
| let code = " | |
| use miden::tx_kernel_core::prologue | |
| begin | |
| exec.prologue::prepare_transaction | |
| end | |
| "; | |
| let result = mock_tx.execute_code(code).await; | |
| assert_execution_error!( | |
| let result = mock_tx.execute().await; | |
| assert_transaction_executor_error!( |
nit: bit more concise
PhilippGackstatter
left a comment
There was a problem hiding this comment.
Looks good to me!
Having TokenPolicyManager still only conditionally add the callback slots makes sense. I missed that this would mean that all faucets that add it, which is all of them, would always have to be FPI-ed into to check if callbacks are defined, and that would defeat the purpose of the callback flag. Good call 👍
| pub fn has_callbacks(&self) -> bool { | ||
| AssetCallbacks::slot_names() | ||
| .iter() | ||
| .any(|slot_name| self.get(slot_name).is_some()) |
There was a problem hiding this comment.
nit: I'd name this has_callback_slots because has_callbacks makes it sound like the storage could define a callback (function), so would be nice to disambiguate a bit.
Summary
AssetCallbackFlagfrom the protocol-reserved asset callback slots installed by its components, replacingAccountBuilder::with_asset_callbackswithenable_asset_callbacks.Account::newso accounts built or deserialized outside a transaction, including genesis accounts, cannot violate it either.We have discussed here (https://github.com/0xMiden/protocol/pull/3547/changes#r3804351573) to apply "Callback slots are present <-> callbacks are enabled" in the protocol level.
However, this PR only implements one direction: if a callback slot is present, callbacks must be enabled. We left the other direction out because a faucet is allowed to enable callbacks now and add the policy slots later, and since the flag is immutable and inserted into the account ID, forbidding that would take the option away permanently.