Multi-Asset Fee Payments #2551
Replies: 2 comments 2 replies
-
|
Thank you for writing this up! The fee procedure/fee approach is along the lines I was thinking of as well, though, I didn't try to generalize things quite as far. Specifically, the approach I was thinking could work like so: We have a static fee conversion procedure defined in the transaction kernel. This conversion procedure would take the following inputs:
The conversion procedure then would work as follows:
Your suggestion takes this a step further to make this procedure user-defined. I think this will work and there are some benefits to this - though, whether these benefits are worth the extra complexity, I'm not sure yet. Specifically, I think the benefits are as follows:
I do think we can progress iteratively here:
I actually don't think that this is a drawback. I think having two separate account deltas is useful because eventually we'll need to make them structurally different as well. Specifically, the pre-fee delta would be relative and it would be useful for authentication purposes (as it is now), but the delta committed to by the transaction outputs would need to be an "absolute" delta (i.e., describing the final state of assets in the vault). |
Beta Was this translation helpful? Give feedback.
-
|
This direction makes sense to me, but I would strongly favor an iterative rollout. If the long-term goal is to decouple the kernel from a special asset model, I would start with the smallest protocol surface that proves the economics and commitment story:
The part I would be most careful about is not the asset construction itself, but quote commitment and replay/staleness boundaries. If a transaction can pay in a non-native asset using external conversion data, it seems important that the transaction summary commits to at least the fee asset identity, the quote/rate inputs, and some expiry or validity boundary. Otherwise the flexibility is great, but the trust surface becomes harder to reason about. So my instinct is: keep the first version boring, explicit, and easy to audit, even if it is less general. If that lands well, the user-defined procedure path looks like a strong second step. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
The current constraints with fees in the tx kernel are:
FungibleAsset. In my mind, this is the main reason we need to have a notion of built-in and (later) custom (non-built-in) assets.My assumption for multi-asset fee payments is that as long as we can make the tx kernel return an arbitrary asset, we have achieved that goal. Basically we can make the batch kernel convert this arbitrary asset (the batch producer may only accept a small set of assets) back into the native asset so that at block level, all fees are paid in the native asset. How exactly the batch kernel does this is out of scope for this discussion, but it could essentially facilitate that swap with its own account or by swapping through a service of its choice.
Generally, I believe there are two approaches to try and resolve some of these issues:
add_feeAPI that takes an arbitrary asset and uses it as the fee.Both of these approaches abstract the construction of the fee asset away from the kernel: The user is responsible for providing an asset that is to be used as the fee, just in different ways. So, both approaches result in decoupling the definition of
FungibleAssetfrom the tx kernel and allow for payments in an asset different from the native asset.Terminology
I'll use "computation units" to refer to the amount from which the fee's amount is derived. It is based on tx cycles, number of created notes, etc. The current implementation that returns these units is basically
ExecutedTransaction::compute_fee.We can also imagine that we'd have multiple such units based on which the actual fee is calculated, but for the purpose of this discussion, I'd just assume one value from which the fee is derived.
Fee API Approach
One option is changing the architecture we have so far and remove automatic creation of the fee asset in the epilogue and instead require users to provide the fee via an explicit API, e.g.
miden::protocol::tx::add_fee. This could be called multiple times from note or tx scripts and the fee assets would be added together (assuming they are the same asset or panic otherwise?).Assuming the general case where users need to pay for their own local transaction by calling
add_feein the tx script, clients won't know the fee amount to pay upfront. Instead clients need to dry-run the tx to get the computation units in order to derive the fee asset from that .By my read of the situation, every transaction would have to be executed three times with this approach:
AUTH_UNAUTHORIZED_EVENTbut for fees to get the computation units.There is some variant of this where we say signature verification is deterministic code and so we can estimate its number of cycles "offline". So we may only need one run to get the number of cycles up to the signature verification, and this may eliminate the second sign step, though I'm not sure how we'd do this in practice as it sounds quite cumbersome.
Benefits:
tx_summary_commitment, meaning no pre-fee and post-fee account delta.ASSETtoASSET_KEYandASSET_VALUE#2396 (comment)).add_fee, though how to estimate the proper amount may still be a challenge.Drawbacks:
TransactionExecutorwould probably not be very straightforward.Fee Script Approach
Allow users to provide a "fee script" or "fee procedure".
create_fee(computation_units: felt) -> Asset. Its only job is to turn the computation units computed by the transaction kernel into an asset. So, in its simplest form, this could be a few lines of MASM code.create_native_fee_assetcall in the epilogue.The challenges with this are:
@fee_script) is part of the account's code and so it is committed to through the account code commitment.TX_SUMMARYand so if a malicious remote prover would swap it for a different tx script, the tx would fail. However, the fee script's effects are not committed to by theTX_SUMMARY, as it runs after the summary is created, and so there needs to be a different way to commit to it. The simplest approach is to include the fee script MAST root in theTX_SUMMARY, which may be good enough. The downside is that the MAST root doesn't provide any form of introspection.What would it look like to turn computation units into a concrete fee asset (like USDC)? Again, there are some variants:
FEE_ARGSas inputs, which are the conversion rate. This means theFEE_ARGSalso need to be committed to, which is fine.FEE_ARGSas inputs to provide the latest conversion rates at execution time. AgainFEE_ARGSmust be committed to viaTX_SUMMARY.To summarize the variants:
FEE_ARGS. This is the middle ground.We don't necessarily have to make a decision for a single variant: For example, both a static fee procedure and optional FEE_ARGS are possible and users can choose one or the other.
Fee script vs procedure:
Benefits:
Drawbacks:
Conclusion
Both approaches would allow for paying fees in any asset. Both also eliminate coupling of the tx kernel to a specific asset representation for the fee asset, which is the last functional reason why we need built-in assets.
Out of the two approaches, I think the fee script/procedure is better due to being largely abstracted away from the user (no explicit API call) and no double signing.
As for fee script vs procedure, I can see pros and cons to both. The fee procedure is nicely consistent with the auth procedure and selected once at creation, limiting ongoing UX burden and does not require an ongoing commitment via the tx summary (minus potential fee args).
I'm now mainly curious if this direction is reasonable or if we need something completely different than what was described.
Note: This does not explore the idea of every account having a public purse from which fees can be taken and to which fees can be refunded, which may be an interesting alternative.
Beta Was this translation helpful? Give feedback.
All reactions