chore: refactor tx kernel from ASSET to ASSET_KEY and ASSET_VALUE#2396
chore: refactor tx kernel from ASSET to ASSET_KEY and ASSET_VALUE#2396PhilippGackstatter wants to merge 47 commits intonextfrom
ASSET to ASSET_KEY and ASSET_VALUE#2396Conversation
| exec.compute_and_remove_fee | ||
| # => [FEE_ASSET] | ||
| # => [FEE_ASSET_VALUE] | ||
|
|
||
| # TODO(programmable_assets): Decide what the public tx output of the fee asset looks like. | ||
| # store fee asset in local | ||
| loc_storew_be.4 dropw |
There was a problem hiding this comment.
I thought a bit about @bobbinth's suggestion of putting the fee asset into an output note that the batch would consume. I think there are some pros and cons to using a stack output:
- Using a stack output means we can remove the fee very late in the process and we have a pretty good, maybe eventually even a perfect estimate of the remaining number of cycles the transaction will take. So, we can compute the fee from within the transaction with high accuracy. When using an output note, this doesn't become impossible, just a lot harder.
- The con is that the stack output is basically special behavior that bypasses asset preservation checks and is inconsistent with how assets are usually handled, and so an output note is nicer, as the fee asset doesn't violate asset preservation and essentially follows protocol rules.
I think whether this is an issue or not depends on how we move forward with fees in general. If we want to allow payments in any asset, maybe we need to drop the computation of the fee in the tx kernel since computing the fee and then converting it into some asset may end up more complicated than letting users specify some fee asset explicitly via an API. And in that case, I think the above wouldn't apply anymore and the output note handling would be easier.
There was a problem hiding this comment.
Using a stack output means we can remove the fee very late in the process and we have a pretty good, maybe eventually even a perfect estimate of the remaining number of cycles the transaction will take. So, we can compute the fee from within the transaction with high accuracy. When using an output note, this doesn't become impossible, just a lot harder.
Would it actually be a lot harder? My thinking was that instead of keeping the asset on the stack, we'd just create a new not here in the epilogue and add the asset to it. This should result in an easy-to-estimate (maybe even constant) number of cycles. But maybe I missed some complexity elsewhere?
There was a problem hiding this comment.
My assumption was that this fee note would need to be committed to by OUTPUT_NOTES_COMMITMENT and that needs to be signed as part of TX_SUMMARY_COMMITMENT, so that it's a "proper signed" output note.
If we don't do that and create a note directly from the epilogue, we'd probably need to have a "pre-fee output notes commitment" and "post-fee output notes commitment", similar to the account delta, which is not super nice, but possible.
I previously thought we'd have to recompute the post-fee output notes commitment which would be a variable number of cycles, but I guess it could be done in constant cycles by caching the hasher state of the pre-fee output notes commitment and just doing one more permutation.
I would probably try to come up with a design for "fee payments in any asset" before we commit to refactor it to the output note variant, since one or the other might be easier, even though currently I think it's largely orthogonal to that design - just being cautious.
igamigo
left a comment
There was a problem hiding this comment.
Awesome work! Couldn't find any potential problems with the implementation. Follow-ups and the PR cutoff also make sense.
crates/miden-protocol/asm/kernels/transaction/lib/epilogue.masm
Outdated
Show resolved
Hide resolved
crates/miden-protocol/asm/kernels/transaction/lib/prologue.masm
Outdated
Show resolved
Hide resolved
|
@copilot work on the nits from @mmagician 's PR review |
|
@mmagician I've opened a new pull request, #2438, to work on those changes. Once the pull request is ready, I'll request review from you. |
…it ASSET_PTR constants, rename variables, add test Co-authored-by: mmagician <8402446+mmagician@users.noreply.github.com>
…logue Co-authored-by: mmagician <8402446+mmagician@users.noreply.github.com>
Co-authored-by: mmagician <8402446+mmagician@users.noreply.github.com>
Co-authored-by: mmagician <8402446+mmagician@users.noreply.github.com>
Co-authored-by: mmagician <8402446+mmagician@users.noreply.github.com>
refactor: address review nits for asset key/value refactoring
bobbinth
left a comment
There was a problem hiding this comment.
Looks good! Thank you! I left some comments inline, but pretty much everything could be addressed in follow-up PRs.
| exec.compute_and_remove_fee | ||
| # => [FEE_ASSET] | ||
| # => [FEE_ASSET_VALUE] | ||
|
|
||
| # TODO(programmable_assets): Decide what the public tx output of the fee asset looks like. | ||
| # store fee asset in local | ||
| loc_storew_be.4 dropw |
There was a problem hiding this comment.
Using a stack output means we can remove the fee very late in the process and we have a pretty good, maybe eventually even a perfect estimate of the remaining number of cycles the transaction will take. So, we can compute the fee from within the transaction with high accuracy. When using an output note, this doesn't become impossible, just a lot harder.
Would it actually be a lot harder? My thinking was that instead of keeping the asset on the stack, we'd just create a new not here in the epilogue and add the asset to it. This should result in an easy-to-estimate (maybe even constant) number of cycles. But maybe I missed some complexity elsewhere?
| exec.rpo256::init_no_padding | ||
| # => [PERM, PERM, PERM, assets_ptr, assets_end_ptr, note_ptr] | ||
|
|
||
| # loop condition: counter != rounded_num_assets | ||
| dup.15 dup.15 neq | ||
| # => [should_loop, PERM, PERM, PERM, assets_ptr+8, note_ptr, counter+2, rounded_num_assets] | ||
| end | ||
| # => [PERM, PERM, PERM, assets_ptr+8n, note_ptr, counter+2n, rounded_num_assets] | ||
| # Compute assets commitment and validate. | ||
| # --------------------------------------------------------------------------------------------- | ||
|
|
||
| exec.mem::pipe_double_words_to_memory | ||
| exec.rpo256::squeeze_digest |
There was a problem hiding this comment.
Could we not use pipe_double_words_preimage_to_memory here instead of doing this manually? (maybe relevant in some other places too)
There was a problem hiding this comment.
We could. The main reason I'm not using that is because it is bad for debugging, since it will show the generic error from pipe_double_words_preimage_to_memory if there is a commitment mismatch. If we had VM backtraces/stacktraces in error messages or some other easy way to debug, this would be a no-brainer.
For now, I think the current code is easy enough since it only pulls the equality check out of pipe_double_words_preimage_to_memory.
| exec.memory::get_input_vault_root_ptr | ||
| movdn.4 | ||
| # => [ASSET, input_vault_root_ptr] | ||
| movdn.8 |
There was a problem hiding this comment.
Should we use a constant here rather than 8?
There was a problem hiding this comment.
Do you mean something like movdn.ASSET_SIZE? I think I haven't seen this done in the kernel / MASM code yet. We always (?) use immediate values when moving elements on the stack. I guess we could start using constants, but it would be a new pattern to my knowledge.
| exec.memory::get_input_vault_root_ptr | ||
| movdn.4 | ||
| # => [ASSET, input_vault_root_ptr] | ||
| movdn.8 |
There was a problem hiding this comment.
Same comment as above (also relevant in a few more places in this file).
| #! Inputs: [ASSET_KEY, vault_root_ptr] | ||
| #! Outputs: [ASSET] | ||
| #! Outputs: [ASSET_VALUE] | ||
| #! | ||
| #! Where: | ||
| #! - vault_root_ptr is a pointer to the memory location at which the vault root is stored. | ||
| #! - ASSET_KEY is the asset vault key of the asset to fetch. | ||
| #! - ASSET is the asset from the vault, which can be the EMPTY_WORD if it isn't present. | ||
| #! - ASSET_VALUE is the value of the asset from the vault, which can be the EMPTY_WORD if it isn't present. | ||
| pub proc get_asset |
There was a problem hiding this comment.
Not for this PR, but I wonder if this should return [ASSET_KEY, ASSET_VALUE] - though, it depends on how we use this procedure.
There was a problem hiding this comment.
I think the pattern of not returning outputs that are equal to inputs is generally cleaner (and better for generated code: #1717), since the caller can cheaply duplicate the values if they need it. For kernel-internal procedures like those in memory I think that makes sense as an optimization, but at the kernel procedure level, I'm not sure.
This PR refactors the tx kernel internals from the previous
ASSETtoASSET_KEYandASSET_VALUE. I.e. assets are now represented by two instead of one word.To keep this change managable:
api.masm). In one of the next PRs, this will go away and the kernel procedures will instead take the key in addition to the value.get_assetskernel procedure.Notable changes
miden::core.NoteAssetsby implementingSequentialCommit.output_note::add_assetwas refactored heavily. It now uses a single procedure for both fungible and non-fungible assets which should prepare nicely for the unified assets in the short-term future.add_asset_rawlogic can later be generalized to: if asset is found and can be merged (mergeprocedure is notEMPTY_WORD) it will call it, otherwise it will abort.asset_vault::remove_assettakes the ASSET_KEY and ASSET_VALUE to remove from the vault. The reason the value is required is so an asset can be partially removed from the vault, e.g. "remove 30 tokens from the total of 80 tokens in the vault". This can work similarly in the future with assets that are splittable (e.g. fungible assets).faucet::burncurrently returns the same ASSET_VALUE that it takes as input. It should either be removed (in line withadd_asset_to_noteprocedure results overhead when calling from Rust #1717) , or return the previous value of the vault. I think the latter makes the most sense as it makes the API strictly more useful. For that reason, I kept the return value so we can more easily change this in the future. The same applies tonative_account::remove_asset.mint_fungible_asset. It previously suggested it returned the same asset it took, but it actually returns the merged asset.Review
The most heavily refactored modules are asset, asset_vault, output_note.
Note that
asset.masmwill change again when changing layouts of asset keys and values, so a very deep review may not be necessary at this point.I tried to keep the commits organized by the module that was refactored (and what conceptually was updated as a consequence), so reviewing by commit should be mostly fine.
Migration
Calls to
{input_note, active_note, output_note}::get_assetsmust be updated since the returned memory layout has changed from previous[ASSET]to[ASSET_KEY, ASSET_VALUE].Potential Follow-Ups
NoteAssets::add_assetrecomputes the commitment every time. This currently results in a lot of hashing in the tx host, which seems unnecessary. To make this a bit more efficient:VecinOutputNoteBuilderand delayNoteAssetscreation untilOutputNoteBuilder::buildis called.NoteAssetsimmutable by removingadd_asset. It seems to only exist for the purpose ofOutputNoteBuilder. I think this seems like the best solution and is more in line with most other types, which are immutable.faucet::burnshould return previous value. E.g. if we have a total of 80 tokens in the vault and 30 are burnt, then 50 should be returned. This may be useful, for example, to easily check what the remaining balance is after burning. For instance,faucets::burncould set token_supply to that returned value after the burning. The same applies tonative_account::remove_asset.part of #2328