-
Notifications
You must be signed in to change notification settings - Fork 164
feat(protocol): tie the asset callback flag to the callback slots #3658
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 11 commits
671eeea
172424b
ebca364
c78a467
a184449
396da34
6ed08a6
4806c4e
53e5f10
e4fa0b6
4ae9bbd
708a1b2
bcff8f8
912ef1d
434685a
83a7f69
e5db4dd
baf1ffc
78a1ce1
10e46b2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -224,6 +224,10 @@ end | |
| pub proc account_upgrade | ||
| # TODO(code_upgrades): Account upgrades must ensure the same conditions hold for an upgraded | ||
| # account as validated in account::{validate_storage, validate_procedures}. | ||
| # 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. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for adding this 👌
mmagician marked this conversation as resolved.
Outdated
|
||
| # check that this procedure was executed against the native account | ||
| exec.memory::assert_native_account | ||
| # => [CODE_UPGRADE_COMMITMENT, STORAGE_UPGRADE_COMMITMENT, pad(8)] | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -101,14 +101,18 @@ impl AccountBuilder { | |
| self | ||
| } | ||
|
|
||
| /// Sets the immutable [`AssetCallbackFlag`] of the account. | ||
| /// Enables the immutable [`AssetCallbackFlag`] of the account even if none of its components | ||
| /// install an asset callback slot. | ||
| /// | ||
| /// This determines whether assets issued by the account (if any) trigger callbacks. It must be | ||
| /// set to [`AssetCallbackFlag::Enabled`] for faucets that configure a transfer policy, and | ||
| /// is encoded into the resulting [`AccountId`] at creation. Defaults to | ||
| /// [`AssetCallbackFlag::Disabled`]. | ||
| pub fn with_asset_callbacks(mut self, asset_callbacks: AssetCallbackFlag) -> Self { | ||
| self.asset_callbacks = asset_callbacks; | ||
| /// 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 { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| self.asset_callbacks = AssetCallbackFlag::Enabled; | ||
| self | ||
| } | ||
|
|
||
|
|
@@ -169,57 +173,34 @@ impl AccountBuilder { | |
| ) | ||
| })?; | ||
|
|
||
| self.validate_asset_callbacks(&storage)?; | ||
|
|
||
| Ok((vault, code, storage)) | ||
| } | ||
|
|
||
| /// Validates that the configured [`AssetCallbackFlag`] is consistent with the asset callback | ||
| /// slots installed by the builder's components. | ||
| /// | ||
| /// The kernel decides whether to invoke a faucet's asset callbacks solely from the | ||
| /// [`AssetCallbackFlag`] encoded in its [`AccountId`], and that flag is immutable once the ID | ||
| /// is ground. A component that installs a callback slot while the flag is | ||
| /// [`AssetCallbackFlag::Disabled`] therefore looks correctly configured but can never have its | ||
| /// callbacks invoked, silently and permanently disabling whatever the callbacks enforce. This | ||
| /// is rejected at build time so the misconfiguration cannot reach a deployed account. | ||
| /// Derives the account's [`AssetCallbackFlag`] from the asset callback slots installed by its | ||
| /// components. | ||
| /// | ||
| /// The converse (the flag enabled without callback slots) is valid: the kernel skips the | ||
| /// callback when the slot is absent or holds the empty word. | ||
| fn validate_asset_callbacks(&self, storage: &AccountStorage) -> Result<(), AccountError> { | ||
| if self.asset_callbacks == AssetCallbackFlag::Enabled { | ||
| return Ok(()); | ||
| } | ||
|
|
||
| for slot_name in [ | ||
| AssetCallbacks::on_before_asset_added_to_account_slot(), | ||
| AssetCallbacks::on_before_asset_added_to_note_slot(), | ||
| ] { | ||
| if storage.get(slot_name).is_some_and(|slot| !slot.value().is_empty()) { | ||
| return Err(AccountError::BuildError( | ||
| format!( | ||
| "component installs the asset callback slot `{slot_name}` but the account's asset callback flag is disabled, so the callback would never be invoked" | ||
| ), | ||
| None, | ||
| )); | ||
| } | ||
| } | ||
|
|
||
| Ok(()) | ||
| /// The flag is [`AssetCallbackFlag::Enabled`] if any component installs one of the | ||
| /// protocol-reserved asset callback slots, or if [`Self::enable_asset_callbacks`] was called, | ||
| /// and [`AssetCallbackFlag::Disabled`] otherwise. | ||
|
mmagician marked this conversation as resolved.
Outdated
|
||
| fn derive_asset_callbacks(&self, storage: &AccountStorage) -> AssetCallbackFlag { | ||
| AssetCallbackFlag::from( | ||
| self.asset_callbacks.is_enabled() || AssetCallbacks::is_installed(storage), | ||
| ) | ||
| } | ||
|
|
||
| /// Grinds a new [`AccountId`] using the `init_seed` as a starting point. | ||
| fn grind_account_id( | ||
| &self, | ||
| init_seed: [u8; 32], | ||
| version: AccountIdVersion, | ||
| asset_callbacks: AssetCallbackFlag, | ||
| code_commitment: Word, | ||
| storage_commitment: Word, | ||
| ) -> Result<Word, AccountError> { | ||
| let seed = AccountIdV1::compute_account_seed( | ||
| init_seed, | ||
| self.account_type, | ||
| self.asset_callbacks, | ||
| asset_callbacks, | ||
| version, | ||
| code_commitment, | ||
| storage_commitment, | ||
|
|
@@ -245,8 +226,6 @@ impl AccountBuilder { | |
| /// - The number of [`StorageSlot`](crate::account::StorageSlot)s of all components exceeds 255. | ||
| /// - [`MastForest::merge`](miden_processor::mast::MastForest::merge) fails on the given | ||
| /// components. | ||
| /// - A component installs an asset callback slot while the configured [`AssetCallbackFlag`] is | ||
| /// [`AssetCallbackFlag::Disabled`], since the kernel would never invoke that callback. | ||
| /// - If duplicate assets were added to the builder (only under the `testing` feature). | ||
| /// - If the vault is not empty on new accounts (only under the `testing` feature). | ||
| pub fn build(mut self) -> Result<Account, AccountError> { | ||
|
|
@@ -260,9 +239,12 @@ impl AccountBuilder { | |
| )); | ||
| } | ||
|
|
||
| let asset_callbacks = self.derive_asset_callbacks(&storage); | ||
|
|
||
| let seed = self.grind_account_id( | ||
| self.init_seed, | ||
| self.id_version, | ||
| asset_callbacks, | ||
| code.commitment(), | ||
| storage.to_commitment(), | ||
| )?; | ||
|
|
@@ -276,7 +258,7 @@ impl AccountBuilder { | |
| .expect("get_account_seed should provide a suitable seed"); | ||
|
|
||
| debug_assert_eq!(account_id.account_type(), self.account_type); | ||
| debug_assert_eq!(account_id.asset_callback_flag(), self.asset_callbacks); | ||
| debug_assert_eq!(account_id.asset_callback_flag(), asset_callbacks); | ||
|
|
||
| // SAFETY: The account ID was derived from the seed and the seed is provided, so it is safe | ||
| // to bypass the checks of `Account::new`. | ||
|
|
@@ -322,7 +304,7 @@ impl AccountBuilder { | |
| bytes, | ||
| AccountIdVersion::Version1, | ||
| self.account_type, | ||
| self.asset_callbacks, | ||
| self.derive_asset_callbacks(&storage), | ||
| ) | ||
| }; | ||
|
|
||
|
|
@@ -596,12 +578,12 @@ mod tests { | |
| assert_matches!(build_error, AccountError::BuildError(msg, _) if msg == "account asset vault must be empty on new accounts") | ||
| } | ||
|
|
||
| /// A component that installs an asset callback slot must not be built into an account whose | ||
| /// [`AssetCallbackFlag`] is disabled: the kernel gates callback invocation on that flag alone | ||
| /// and the flag is immutable once the ID is ground, so whatever the callback enforces would | ||
| /// be silently and permanently bypassed. | ||
| /// The [`AssetCallbackFlag`] is derived from the installed asset callback slots: the kernel | ||
| /// gates callback invocation on that flag alone and the flag is immutable once the ID is | ||
| /// ground, so an account that installs a callback slot must have callbacks enabled or whatever | ||
| /// the callback enforces would be silently and permanently bypassed. | ||
| #[test] | ||
| fn account_builder_rejects_callback_slot_with_disabled_flag() { | ||
| fn account_builder_derives_asset_callback_flag_from_callback_slots() { | ||
| let callback_component = |slots| { | ||
| AccountComponent::new( | ||
| CUSTOM_PACKAGE1.clone(), | ||
|
|
@@ -619,23 +601,69 @@ mod tests { | |
| .on_before_asset_added_to_account(Word::from([1u32, 2, 3, 4])) | ||
| .into_storage_slots(), | ||
| ] { | ||
| let build_error = Account::builder([7; 32]) | ||
| .with_component(NoopAuthComponent) | ||
| .with_component(callback_component(slots.clone())) | ||
| .build() | ||
| .unwrap_err(); | ||
|
|
||
| assert_matches!(build_error, AccountError::BuildError(msg, _) if msg.contains("asset callback flag is disabled")); | ||
|
|
||
| // The same component is accepted once the flag is enabled. | ||
| Account::builder([7; 32]) | ||
| .with_asset_callbacks(AssetCallbackFlag::Enabled) | ||
| let account = Account::builder([7; 32]) | ||
| .with_component(NoopAuthComponent) | ||
| .with_component(callback_component(slots)) | ||
| .build() | ||
| .unwrap(); | ||
|
|
||
| assert_eq!(account.id().asset_callback_flag(), AssetCallbackFlag::Enabled); | ||
| } | ||
| } | ||
|
|
||
| /// Without an installed callback slot the flag is disabled, unless callbacks are explicitly | ||
| /// enabled to reserve the capability for the account's lifetime. | ||
| #[test] | ||
| fn account_builder_derives_disabled_asset_callback_flag_without_callback_slots() { | ||
| 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); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: This tests the behavior of |
||
| let account = Account::builder([7; 32]) | ||
| .with_component(NoopAuthComponent) | ||
| .with_component(CustomComponent1 { slot0: 25 }) | ||
| .build_existing() | ||
| .unwrap(); | ||
|
|
||
| assert_eq!(account.id().asset_callback_flag(), AssetCallbackFlag::Disabled); | ||
|
|
||
| let mut slots = account.storage().clone().into_slots(); | ||
| slots.push(StorageSlot::with_value( | ||
| AssetCallbacks::on_before_asset_added_to_account_slot().clone(), | ||
| Word::from([1u32, 2, 3, 4]), | ||
| )); | ||
| let storage = AccountStorage::new(slots).unwrap(); | ||
|
|
||
| let error = Account::new( | ||
| account.id(), | ||
| account.vault().clone(), | ||
| storage, | ||
| account.code().clone(), | ||
| account.nonce(), | ||
| None, | ||
| ) | ||
| .unwrap_err(); | ||
|
|
||
| assert_matches!(error, AccountError::AssetCallbackSlotWithDisabledFlag { .. }); | ||
| } | ||
|
|
||
| // TODO: Test that a BlockHeader with a number which is not a multiple of 2^16 returns an error. | ||
| } | ||
There was a problem hiding this comment.
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: