Skip to content

feat(protocol): tie the asset callback flag to the callback slots - #3658

Open
onurinanc wants to merge 20 commits into
nextfrom
refactor-callback-fix
Open

feat(protocol): tie the asset callback flag to the callback slots#3658
onurinanc wants to merge 20 commits into
nextfrom
refactor-callback-fix

Conversation

@onurinanc

Copy link
Copy Markdown
Collaborator

Summary

  • Derive an account's AssetCallbackFlag from the protocol-reserved asset callback slots installed by its components, replacing AccountBuilder::with_asset_callbacks with enable_asset_callbacks.
  • Reject new accounts in the transaction kernel prologue when their storage contains an asset callback slot while their account ID has callbacks disabled.
  • Apply the same rule in Account::new so 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.

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

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.
#!

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

Comment on lines +227 to +230
# 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.

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.

Thanks for adding this 👌

Comment on lines +107 to +114
/// 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 {

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

Comment on lines +618 to +633
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);

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.

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() {

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: This tests the behavior of Account::new so the test module in crates/miden-protocol/src/account/mod.rs seems like a better place.

Comment on lines +537 to +544
///
/// 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.
///

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'd remove this and point to AccountBuilder docs for details, so we don't duplicate this info.

Comment on lines +555 to +562
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(),
});
}
}

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 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().

Comment on lines +84 to +86
/// Returns `true` if `storage` contains at least one of the protocol-reserved asset callback
/// slots, `false` otherwise.
pub fn is_installed(storage: &AccountStorage) -> bool {

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.

As mentioned in another comment, I'd make this a method on AccountStorage.

Comment on lines 218 to 220
/// 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].

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.

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.

Comment on lines +813 to +823
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!(

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

Comment thread crates/miden-protocol/asm/kernels/transaction/lib/api.masm Outdated
Comment thread crates/miden-protocol/src/account/builder/mod.rs Outdated
Comment thread crates/miden-standards/src/account/policies/manager.rs Outdated
Comment thread crates/miden-agglayer/src/lib.rs Outdated

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

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

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 👍

Comment on lines +176 to +179
pub fn has_callbacks(&self) -> bool {
AssetCallbacks::slot_names()
.iter()
.any(|slot_name| self.get(slot_name).is_some())

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

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.

3 participants