Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

### Changes

- [BREAKING] Changed asset callbacks into validation-only interfaces that return no asset value; the transaction kernel retains and uses the original value, preventing callbacks from modifying it. The kernel commitment changes ([#3505](https://github.com/0xMiden/protocol/issues/3505), [#3513](https://github.com/0xMiden/protocol/pull/3513)).
- Documented the RBAC freeze-only actor pattern on `Authority` and added test coverage pinning that a `FREEZER` can trip the emergency switch but can never unfreeze the account ([#3520](https://github.com/0xMiden/protocol/pull/3520)).

### Fixes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -644,24 +644,24 @@ end
#! added.
#! - the vault already contains the same non-fungible asset.
pub proc add_asset_to_vault
swapw dupw.1
# => [ASSET_ID, ASSET_VALUE, ASSET_ID]
# retain the original asset while the callback validates a copy
dupw.1 dupw.1
# => [ASSET_ID, ASSET_VALUE, ASSET_ID, ASSET_VALUE]

exec.callbacks::on_before_asset_added_to_account
swapw
# => [ASSET_ID, PROCESSED_ASSET_VALUE]
# => [ASSET_ID, ASSET_VALUE]

# duplicate the asset ID for the later event and delta update
swapw dupw.1
# => [ASSET_ID, PROCESSED_ASSET_VALUE, ASSET_ID]
# => [ASSET_ID, ASSET_VALUE, ASSET_ID]

# push the account vault root ptr
exec.memory::get_account_vault_root_ptr movdn.8
# => [ASSET_ID, PROCESSED_ASSET_VALUE, account_vault_root_ptr, ASSET_ID]
# => [ASSET_ID, ASSET_VALUE, account_vault_root_ptr, ASSET_ID]

# emit event to signal that an asset is going to be added to the account vault
emit.ACCOUNT_VAULT_BEFORE_ADD_ASSET_EVENT
# => [ASSET_ID, PROCESSED_ASSET_VALUE, account_vault_root_ptr, ASSET_ID]
# => [ASSET_ID, ASSET_VALUE, account_vault_root_ptr, ASSET_ID]

# add the asset to the account vault
exec.asset_vault::add_asset
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,9 @@ use miden::core::word
# CONSTANTS
# ==================================================================================================

const ERR_FAUCET_CALLBACK_ASSET_VALUE_MUST_MATCH_INPUT =
"asset callback output value must match its input value"

# The index of the local memory slot that contains the procedure root of the callback.
const CALLBACK_PROC_ROOT_LOC = 0

# The index of the local memory slot that contains the original asset value passed to the callback.
const CALLBACK_ASSET_VALUE_LOC = 4

# The name of the storage slot where the procedure root for the on_before_asset_added_to_account callback
# is stored.
pub const ON_BEFORE_ASSET_ADDED_TO_ACCOUNT_PROC_ROOT_SLOT =
Expand All @@ -42,17 +36,11 @@ pub const ON_BEFORE_ASSET_ADDED_TO_NOTE_PROC_ROOT_SLOT =
#! - If the callback storage slot contains the empty word.
#!
#! Inputs: [ASSET_ID, ASSET_VALUE]
#! Outputs: [PROCESSED_ASSET_VALUE]
#! Outputs: []
#!
#! Where:
#! - ASSET_ID is the asset ID of the asset being added.
#! - ASSET_VALUE is the value of the asset being added.
#! - PROCESSED_ASSET_VALUE is the asset value returned by the callback, or the original
#! ASSET_VALUE if callbacks are disabled. The callback is required to return the asset
#! value it received, so this is always equal to ASSET_VALUE.
#!
#! Panics if:
#! - the callback returns an asset value different from the one it received.
pub proc on_before_asset_added_to_account
# derive the callback flag from the asset metadata, carried in the asset ID
exec.asset::id_to_has_callbacks
Expand All @@ -65,13 +53,12 @@ pub proc on_before_asset_added_to_account

push.ON_BEFORE_ASSET_ADDED_TO_ACCOUNT_PROC_ROOT_SLOT[0..2]
exec.invoke_callback
# => [PROCESSED_ASSET_VALUE]
# => []
else
# drop asset ID
dropw
# => [ASSET_VALUE]
dropw dropw
# => []
end
# => [PROCESSED_ASSET_VALUE]
# => []
end

#! Invokes the `on_before_asset_added_to_note` callback on the faucet that issued the asset,
Expand All @@ -83,18 +70,12 @@ end
#! - If the callback storage slot contains the empty word.
#!
#! Inputs: [ASSET_ID, ASSET_VALUE, note_idx]
#! Outputs: [PROCESSED_ASSET_VALUE]
#! Outputs: []
#!
#! Where:
#! - ASSET_ID is the asset ID of the asset being added.
#! - ASSET_VALUE is the value of the asset being added.
#! - note_idx is the index of the output note the asset is being added to.
#! - PROCESSED_ASSET_VALUE is the asset value returned by the callback, or the original
#! ASSET_VALUE if callbacks are disabled. The callback is required to return the asset
#! value it received, so this is always equal to ASSET_VALUE.
#!
#! Panics if:
#! - the callback returns an asset value different from the one it received.
pub proc on_before_asset_added_to_note
# derive the callback flag from the asset metadata, carried in the asset ID
exec.asset::id_to_has_callbacks
Expand All @@ -103,37 +84,30 @@ pub proc on_before_asset_added_to_note
if.true
push.ON_BEFORE_ASSET_ADDED_TO_NOTE_PROC_ROOT_SLOT[0..2]
exec.invoke_callback
# => [PROCESSED_ASSET_VALUE]
# => []
else
# drop asset ID and note index
dropw movup.4 drop
# => [ASSET_VALUE]
dropw dropw drop
# => []
end
# => [PROCESSED_ASSET_VALUE]
# => []
end

#! Invokes a callback by starting a foreign context against the faucet, reading the callback
#! procedure root from the provided slot ID in the faucet's storage, and invoking it via `dyncall`.
#!
#! If the faucet does not have the callback storage slot, or if the slot contains the empty word,
#! the callback is skipped and the original ASSET_VALUE is returned.
#! the callback is skipped and its inputs are consumed.
#!
#! custom_data should be set to 0 for the account callback and to note_idx for the note callback.
#!
#! Inputs: [slot_id_suffix, slot_id_prefix, ASSET_ID, ASSET_VALUE, custom_data]
#! Outputs: [PROCESSED_ASSET_VALUE]
#! Outputs: []
#!
#! Where:
#! - slot_id* is the ID of the slot that contains the callback procedure root.
#! - ASSET_ID is the asset ID of the asset being added.
#! - ASSET_VALUE is the value of the asset being added.
#! - PROCESSED_ASSET_VALUE is the asset value returned by the callback, or the original
#! ASSET_VALUE if no callback is configured. The callback is required to return the asset
#! value it received, so this is always equal to ASSET_VALUE.
#!
#! Panics if:
#! - the callback returns an asset value different from the one it received.
@locals(8)
@locals(4)
proc invoke_callback
exec.maybe_start_faucet_callback_context
# => [was_foreign_context_started, should_invoke_callback, PROC_ROOT, ASSET_ID, ASSET_VALUE, custom_data]
Expand All @@ -148,10 +122,6 @@ proc invoke_callback
loc_storew_le.CALLBACK_PROC_ROOT_LOC dropw
# => [ASSET_ID, ASSET_VALUE, custom_data, was_foreign_context_started]

# save the original asset value for the post-callback equality check
swapw loc_storew_le.CALLBACK_ASSET_VALUE_LOC swapw
# => [ASSET_ID, ASSET_VALUE, custom_data, was_foreign_context_started]

# pad the stack to 16 for the call
repeat.7
push.0 movdn.9
Expand All @@ -161,28 +131,19 @@ proc invoke_callback
# invoke the callback
locaddr.CALLBACK_PROC_ROOT_LOC
dyncall
# => [PROCESSED_ASSET_VALUE, pad(12), was_foreign_context_started]

# truncate the stack after the call
swapdw dropw dropw swapw dropw
# => [PROCESSED_ASSET_VALUE, was_foreign_context_started]
# => [pad(16), was_foreign_context_started]

# assert that the callback returned the asset value unchanged
dupw padw loc_loadw_le.CALLBACK_ASSET_VALUE_LOC
assert_eqw.err=ERR_FAUCET_CALLBACK_ASSET_VALUE_MUST_MATCH_INPUT
# => [PROCESSED_ASSET_VALUE, was_foreign_context_started]
dropw dropw dropw dropw
# => [was_foreign_context_started]
else
# drop proc root, asset ID and custom_data
dropw dropw movup.4 drop
# => [ASSET_VALUE, was_foreign_context_started]
# drop proc root and callback inputs
dropw dropw dropw drop
# => [was_foreign_context_started]
end
# => [PROCESSED_ASSET_VALUE, was_foreign_context_started]

movup.4
# => [was_foreign_context_started, PROCESSED_ASSET_VALUE]
# => [was_foreign_context_started]

exec.maybe_end_faucet_callback_context
# => [PROCESSED_ASSET_VALUE]
# => []
end

#! Prepares the invocation of a faucet callback.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,23 +249,21 @@ pub proc add_asset
emit.NOTE_BEFORE_ADD_ASSET_EVENT
# => [ASSET_ID, ASSET_VALUE, note_idx]

# prepare the stack for the callback
swapw dupw.1
# => [ASSET_ID, ASSET_VALUE, ASSET_ID, note_idx]

dup.12 movdn.8
# => [ASSET_ID, ASSET_VALUE, note_idx, ASSET_ID, note_idx]
# retain the original asset and note index while the callback validates a copy
repeat.9
dup.8
end
# => [ASSET_ID, ASSET_VALUE, note_idx, ASSET_ID, ASSET_VALUE, note_idx]

# invoke the callback
exec.callbacks::on_before_asset_added_to_note
swapw
# => [ASSET_ID, PROCESSED_ASSET_VALUE, note_idx]
# => [ASSET_ID, ASSET_VALUE, note_idx]

movup.8 exec.memory::get_output_note_ptr dup
# => [note_ptr, note_ptr, ASSET_ID, PROCESSED_ASSET_VALUE]
# => [note_ptr, note_ptr, ASSET_ID, ASSET_VALUE]

movdn.9 movdn.9
# => [ASSET_ID, PROCESSED_ASSET_VALUE, note_ptr, note_ptr]
# => [ASSET_ID, ASSET_VALUE, note_ptr, note_ptr]

# add the asset to the note
exec.add_asset_raw
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,16 +150,11 @@ end
#! applies the pause check and invokes the active send policy.
#!
#! Inputs: [ASSET_ID, ASSET_VALUE, note_idx, pad(7)]
#! Outputs: [PROCESSED_ASSET_VALUE, pad(12)]
#!
#! Where:
#! - PROCESSED_ASSET_VALUE is the asset value returned by the policy, or the original ASSET_VALUE
#! if no send policy is configured.
#! Outputs: [pad(16)]
#!
#! Panics if:
#! - the account is paused.
#! - the active send policy predicate fails.
#! - the active send policy returns an asset value different from the one it received.
#!
#! Invocation: call
@account_procedure
Expand All @@ -168,7 +163,7 @@ pub proc invoke_send_policy
# => [slot_id_suffix, slot_id_prefix, ASSET_ID, ASSET_VALUE, note_idx, pad(7)]

exec.invoke_transfer_policy
# => [PROCESSED_ASSET_VALUE, pad(12)]
# => [pad(16)]
end

#! Returns active send policy root.
Expand Down Expand Up @@ -221,17 +216,14 @@ end
#! receive policy is configured).
#!
#! Inputs: [ASSET_ID, ASSET_VALUE, custom_data, pad(7)]
#! Outputs: [PROCESSED_ASSET_VALUE, pad(12)]
#! Outputs: [pad(16)]
#!
#! Where:
#! - custom_data is `0` for the receive callback.
#! - PROCESSED_ASSET_VALUE is the asset value returned by the policy, or the original ASSET_VALUE
#! if no receive policy is configured.
#!
#! Panics if:
#! - the account is paused.
#! - the active receive policy predicate fails.
#! - the active receive policy returns an asset value different from the one it received.
#!
#! Invocation: call
@account_procedure
Expand All @@ -240,7 +232,7 @@ pub proc invoke_receive_policy
# => [slot_id_suffix, slot_id_prefix, ASSET_ID, ASSET_VALUE, custom_data, pad(7)]

exec.invoke_transfer_policy
# => [PROCESSED_ASSET_VALUE, pad(12)]
# => [pad(16)]
end

#! Returns active receive policy root.
Expand Down Expand Up @@ -536,23 +528,20 @@ end
#! which differ only in which slot they bind.
#!
#! If the active root is the empty word (no policy configured for this kind), the transfer is
#! accepted unchanged and the pause check is skipped — mirroring the kernel's behavior when a
#! callback slot holds the empty word. Otherwise the account-wide pause flag is asserted (a no-op
#! when the [`Pausable`] component is not installed) and the policy is invoked via `dyncall`.
#! accepted and the pause check is skipped — mirroring the kernel's behavior when a callback slot
#! holds the empty word. Otherwise the account-wide pause flag is asserted (a no-op when the
#! [`Pausable`] component is not installed) and the policy is invoked via `dyncall`.
#!
#! Inputs: [slot_id_suffix, slot_id_prefix, ASSET_ID, ASSET_VALUE, custom_data, pad(7)]
#! Outputs: [PROCESSED_ASSET_VALUE, pad(12)]
#! Outputs: [pad(16)]
#!
#! Where:
#! - slot_id_{suffix, prefix} identify the storage slot holding the active policy root.
#! - custom_data is `0` for the receive callback and the output note index for the send callback.
#! - PROCESSED_ASSET_VALUE is the asset value returned by the policy, or the original ASSET_VALUE
#! if no policy is configured.
#!
#! Panics if:
#! - the account is paused.
#! - the invoked policy predicate fails.
#! - the invoked policy returns an asset value different from the one it received.
#!
#! Invocation: exec
@locals(4)
Expand All @@ -564,10 +553,9 @@ proc invoke_transfer_policy
# => [is_empty, POLICY_ROOT, ASSET_ID, ASSET_VALUE, custom_data, pad(7)]

if.true
# No policy configured: drop the empty root and asset ID, return the asset value
# unchanged. `movup.4 drop` removes custom_data so only ASSET_VALUE is returned.
dropw dropw movup.4 drop
# => [ASSET_VALUE, pad(7)]
# No policy configured: consume the callback inputs.
dropw dropw dropw drop
# => [pad(16)]
Comment on lines +556 to +558

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 happens to work because this procedure is always invoked as a last thing in a call-ed procedure, but I don't think we should be doing this - at least not without being super explicit about the invocation context in the comments.

Better yet, we should change the signature to something like:

#! Inputs:  [slot_id_suffix, slot_id_prefix, ASSET_ID, ASSET_VALUE, custom_data]
#! Outputs: []

And handle padding adjustments at the callsites.

else
exec.pausable::assert_not_paused
# => [POLICY_ROOT, ASSET_ID, ASSET_VALUE, custom_data, pad(7)]
Expand All @@ -579,6 +567,6 @@ proc invoke_transfer_policy
# => [policy_root_ptr, ASSET_ID, ASSET_VALUE, custom_data, pad(7)]

dyncall
# => [PROCESSED_ASSET_VALUE, pad(12)]
# => [pad(16)]
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@
#! account-wide pause check.
#!
#! Inputs: [ASSET_ID, ASSET_VALUE, custom_data, pad(7)]
#! Outputs: [ASSET_VALUE, pad(12)]
#! Outputs: [pad(16)]
#!
#! Where:
#! - custom_data is `0` for the receive (account) callback and the output note index for the
#! send (note) callback. This policy does not inspect it; it passes through unchanged.
#! send (note) callback. This policy does not inspect it.
#!
#! Invocation: call
@account_procedure
pub proc check_policy
dropw
# => [ASSET_VALUE, pad(12)]
dropw dropw drop
# => [pad(16)]
end
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,15 @@ use miden::standards::faucets::policies::transfer::allowlist
#! Transfer policy that rejects transfers whose native account is not allowed on the issuing
#! faucet.
#!
#! The same procedure root is reusable as both send and receive policy because it only
#! consumes the top eight felts (`ASSET_ID`, `ASSET_VALUE`) and leaves the rest of the call
#! frame untouched — any `note_idx` carried in the send signature passes through unchanged.
#! The same procedure root is reusable as both send and receive policy because it does not depend
#! on custom_data.
#!
#! Inputs: [ASSET_ID, ASSET_VALUE, custom_data, pad(7)]
#! Outputs: [ASSET_VALUE, pad(12)]
#! Outputs: [pad(16)]
#!
#! Where:
#! - custom_data is `0` for the receive (account) callback and the output note index for the
#! send (note) callback. This policy does not inspect it; it passes through unchanged.
#! send (note) callback. This policy does not inspect it.
#!
#! Panics if:
#! - the native account is not allowed on the issuing faucet and is not the issuing faucet
Expand Down Expand Up @@ -63,5 +62,8 @@ pub proc check_policy
dropw
# => [ASSET_VALUE, custom_data, pad(7)]
end
# => [ASSET_VALUE, pad(12)]
# => [ASSET_VALUE, custom_data, pad(7)]

dropw drop
# => [pad(16)]
end
Loading
Loading