-
Notifications
You must be signed in to change notification settings - Fork 311
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
docs(yellow-paper): DA metering #5565
Conversation
This stack of pull requests is managed by Graphite. Learn more about stacking. Join @just-mitch and the rest of your teammates on Graphite |
2725685
to
a61f646
Compare
``` | ||
|
||
:::warning Fees are not enforced | ||
It is not enforced by the protocol that the `transaction_fee` is actually paid. It is up to the sequencer to enforce this. |
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 know this is an old decision, but I still feel uneasy about it. Can we have non-enforced fees and 1559 at the same time? Could this lead to sequencers having differentiated payment channels that break standardization? Could this lead to bypassing paying fees to actors that are not strictly the sequencer?
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 am uneasy about it as well.
I think my ideal case is that we do enforce them. Tradeoff is that this necessitates every transaction to have a public component to call teardown. This way, "inclusion fee" becomes the tip to the sequencer, and the other parts of transaction fee are burned. The primary departure here being that the tip to the sequencer is flat, not scaling with any particular gas used. But I don't think that is necessarily a problem.
If we keep it such that purely private transactions can be included, there will necessarily be a way to pay actors via back channels: why else would sequencers include pure private transactions? Block rewards? No MEV opportunity as they are pure private.
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.
Edit: after discussion @spalladino suggests we don't burn da gas
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.
Can we remind ourselves to talk about this topic on the call tomorrow? Sounds like a pretty important decision to get comfortable with - especially if you're uneasy about it Mitch, having written lots of this proposal :)
+ private_kernel_public_inputs.end.gas_used.da_gas_used | ||
|
||
kernel_public_inputs.end.compute_gas_used := | ||
private_kernel_public_inputs.end.compute_gas_used |
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.
Why does the private kernel report compute gas used? I'd expect compute gas to be zero for all private functions.
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.
Yes, agreed, it should be zero for private functions. I left it to reuse the GasUsed
struct.
|
||
Sequencers are paid in `$AZT` **on L1**. | ||
|
||
After a block has been published/verified, the sequencer is paid the sum of the `inclusion_fee`s in the block for all transactions with a non-zero `transaction_fee`. |
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.
Why only the inclusion_fee
and not the rest?
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.
Open conversation on what to pay the sequencer
|
||
`fee_per_da_gas` and `fee_per_compute_gas` are not included in the published data per transaction because they are included in the block header. | ||
|
||
`transaction_fee` reflects the amount of `$AZT` that **was paid** for the transaction. |
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.
Does this need to be included? Can't it be computed from the other fields?
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.
If it is zero, we know it was not paid. True, the amount can be computed from the other fields though.
|
||
It will be the same for the `compute_gas_used`. | ||
|
||
### Out of gas |
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.
Can we add a section on what happens during a revert in a public function? I'd expect that the compute gas consumed so far is left as-is, but the DA gas for revertible is cleared out, since reverted side-effects don't go on-chain, right?
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.
In app logic:
if we run out of compute gas:
- the compute gas charged is the compute_gas_limit
- the da gas charged is non_revertible.da_gas_used
if we run out of da gas: - the compute gas charged is non_revertible.compute_gas_used + revertible.compute_gas_used (up to point of oog)
- the da gas charged is non_revertible.da_gas_used
if any other exception is thrown: - the compute gas charged is non_revertible.compute_gas_used + revertible.compute_gas_used (up to point of exception)
- the da gas charged is non_revertible.da_gas_used
b3de270
to
27cec22
Compare
27cec22
to
c4d4e04
Compare
These include: | ||
|
||
- verification of transaction's private kernel proof | ||
- verification of nullifiers |
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.
Can this not be a per-nullifier quantity of l2
(or compute
) gas?
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.
You're right. I was only considering the transaction nullifier. We could absolutely add a component of the l2_gas_used
that scales off the number of nullifiers emitted.
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.
(Not a response to Phil, but a reaction to this bullet point): The cost of verifying nullifiers will depend on the number of nonzero nullifiers being inserted, so arguably does depend on the transaction.
- We publish full transaction effects for each transaction | ||
|
||
:::warning | ||
Are these assumptions still valid? |
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.
Not sure if the final one "We publish full transaction effects for each transaction" is completely decided. I think there's still uncertainty around whether Aztec will publish the intents (or inputs) of transactions to L1/DA, or whether Aztec will publish state diffs (the outputs) of transactions to L1/DA.
I think it depends on the sequencer selection and preconfirmation designs, and on analysis of the amount of data that would need to be broadcast in each case, and on the auditability of history in a state-diff version of a chain.
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.
We must support pure private transactions
By "pure private transactions", do we mean "a final kernel private proof with no enqueued public function calls"?
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.
Yes!
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.
Exceptional write-up! Thanks Mitch and team! There's so much to think about here, it's a very difficult subject indeed.
I've added comments in places. I confess I was skim-reading towards the end, so I might return to this later after a break to read the end in more depth.
- "DA gas" -> "storage gas", as it covers state growth on L2 and publishing to DA | ||
- "L2 gas" -> "compute gas", as it covers computation on L2 and proving of public VM circuits | ||
- "L1 gas" -> "cross-chain gas", as it covers the cost of l2-to-l1 messages | ||
|
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 the suggested names could lead to confusion, and lots of verbal conversations where people misunderstand the gas being talked about.
E.g. "storage gas" is ambiguous to me because stuff also gets stored on L1.
E.g. "compute gas" is ambiguous because stuff also gets computed on L1.
E.g. "cross-chain gas" is ambiguous, because the DA layer will also be a chain, and because it's only for gas in one direction.
I'll read on, though. Maybe the names will grow on me as I read.
|
||
# Kernel state diagram | ||
|
||
The state diagram is as follows: |
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'd suggest making the data a different shape/colour from the circuits.
# The Aztec token | ||
|
||
:::warning | ||
This token will not be called the Aztec token in the final implementation. It, and the symbol `$AZT`, are placeholders. |
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 still want to call it $JUICE
These include: | ||
|
||
- verification of transaction's private kernel proof | ||
- verification of nullifiers |
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.
(Not a response to Phil, but a reaction to this bullet point): The cost of verifying nullifiers will depend on the number of nonzero nullifiers being inserted, so arguably does depend on the transaction.
- verification of nullifiers | ||
- proving the rollup circuits | ||
- verification of the root rollup proof on L1 | ||
|
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.
There might be some items missing from this list: https://yp-aztec.netlify.app/docs/transactions/validity
note_hash_gas + | ||
nullifier_gas + | ||
l2_to_l1_message_gas + | ||
public_data_writes_gas + |
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.
What about the compute costs associated with inserting this data into the data trees?
- Note hashes tree
- Nullifier tree
- Archive tree
- Public Data tree (might be covered by the vm's gas metering)
Also, the costs of inserting into the note hash tree are less than inserting into an indexed merkle tree. And the costs to include logs will be accompanied by a big cost to re-hash all of the logs on the DA layer (or on L1... not sure). And will L2_to_L1 messages make use of the DA layer? I thought they're published directly to L1? Perhaps there's an L2_to_L1 cost associated with creating the merkle root that then gets put on L1? Or is that root computed on L1 via a frontier tree? Lasse will remember the details.
All that is to say: the formulae for computing these various gas amounts might need to account for more.
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.
What about the costs imposed on all other nodes (that aren't the sequencer creating this block)? I.e. the costs to pay the network to update their nodes' world state dbs and storing this information? (Or, rather than paying everyone (which obviously doesn't happen), it's more paying to outcompete anyone else who wants their tx to be included by the scarce resources of the network). This kind of cost feels very similar to why we pay for ethereum txs.
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.
Should the cost of a new public data write cost more than overwriting an existing public data leaf?
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.
Great points about adding data to the trees and their varying costs. I think we could add scalars to the computations below to compensate differently based on the work required for different insertions e.g.:
note_hash_gas = da_gas_per_field * (number of notes) * a
nullifier_gas = da_gas_per_field * (number of nullifiers) * b
I think doing something like that is predicated on being quite sure that the relative costs of insertions will be what we presently think they are
|
||
Note: it does not know by inspecting the enqueued call itself if the `pay_fee` function will be called, unless it has explicitly inspected/whitelisted the function ahead of time. | ||
|
||
However, it will know that if `pay_fee` is called, then message sender is the one that will pay the fee, and thus must have a balance greater than: |
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.
then message sender is the one that will pay the fee
This might not be a reliable indicator for whose balance will be paying the fee. The msg_sender
of a private->public call could be a randomised value, to give the user privacy over which contract is making the call from private-land. The actual msg.sender that pays the fee could be some public contract that is enqueued, right? Or perhaps pay_fee might be invoked via a transfer_from call?
I might not be making sense, because I've forgotten how this part works.
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.
Presently the pay_fee
function deducts the balance of context.msg_sender
. Maybe this is not correct.
- `max_fee_per_l2_gas` | ||
- `teardown_l2_gas` | ||
- `l1_gas_limit` | ||
- `max_fee_per_l1_gas` |
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'd suggest separating out these gas prices from the gas limits (or renaming the GasLimits
struct)
|
||
### Global gas limits | ||
|
||
The mechanics above imply that there will be a single global gas limit (for each gas dimension) for all enqueued public calls. However, users will still be able to specify individual gas limits for each *nested* call within an enqueued public call. |
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.
users will still be able to specify individual gas limits for each nested call within an enqueued public call
How do they do this? Are there parameters that are baked into the "call" opcodes of the avm? By "users" do you mean the smart contract developer who designs how the nested call is called (and therefore how much gas to send to it)?
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.
Wait, so if I have a tx with two enqueued public function calls A1 and B1:
- I specify a global gas limit of 100 for all enqueued public function calls.
But then we're saying I can specify individual gas limits for each nested function call? Is it the caller of the function who can specify these, or the smart contract developer?
So if A1 calls A1 and B1 calls B2, and I (or the functions A1 & B1?) can somehow specify gas limits of:
- A1: 50 gas, but it uses 20 gas.
- A2: 25 gas, but it uses 20 gas.
- B1: 50 gas, but it uses 20 gas.
- B2: 30 gas, but it uses 20 gas.
Would my tx succeed or fail?
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 understand your example working as:
- A1 starts with 100 gas, nobody can give this a gas limit different from the global tx limit.
- A1 calls A2 and the developer specifies a limit of 25 gas. If it tries to use more it will revert and can be caught by A1, consuming only 25 of A1's limit (the 100 limit it started with).
- A1 itself uses 20 gas. So, in your example, both A1 and A2 use 20 each, 40 total.
- B1 now has an effective limit of 60, again neither the developer or the user can set this themselves
- B1 calls B2 with a limit of 30, They both use 20 gas which does not breach any limit.
80 gas used in total.
|
||
Thus, the user is charged the full `l2_gas_limit`. | ||
|
||
However, we drop the DA and L1 gas used associated with app logic, i.e.: |
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.
Is this because it would be unfair to charge these costs to the user, because these costs will never be borne by the sequencer/l1/da?
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.
Yes. I think it's more correct to say we drop all DA and L1 gas used values for revertible side-effects. This includes anything from private execution.
|
||
:::warning | ||
Are these assumptions still valid? | ||
::: |
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 at this stage all these assumptions are valid
|
||
The fees per unit gas are set by the protocol, and are not part of the user's transaction. | ||
|
||
The fees are: |
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'm not really sure about this. This feels very 1559-ish, which I think it is far from certain that we plan to do.
|
||
However, it will know that if `pay_fee` is called, then message sender is the one that will pay the fee, and thus must have a balance greater than: | ||
``` | ||
msg_sender.balance >= max_fee_per_da_gas * (da_gas_limit + teardown_da_gas) |
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'm a bit confused. Earlier it said:
A transaction fee is the maximum amount of
$AZT
that a user can be charged for a transaction.
transaction_fee = fee_per_da_gas * da_gas_used
+ fee_per_l2_gas * l2_gas_used
+ fee_per_l1_gas * l1_gas_used
+ inclusion_fee
But this suggests that the balance (and the fee paid) are based on max_fee_per_*_gas
. I think this last part is correct, I'm not really sure of the purpose of the fee_per_*_gas
values.
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.
Yes, the transaction_fee
is the true/final amount, and this computation represents the maximal that the user could be charged, so the sequencer can run this computation ahead of time to see if it is possible for the sender to even pay the max they have suggested.
|
||
### All other reverts in App Logic | ||
|
||
These follow the same logic as OutOfDAGas reverts in app logic. |
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.
Presumably not for OutOfL2Gas? If a nested call uses all of it's e.g. 100 L2 gas, that amount is still added to the accumulated gas used of it's caller?
Is there a more clean way to ensure the fee is paid other than this very specific boolean? | ||
How does the VM know that the `pay_fee` function has been called? | ||
Would we rather the VM return function calls that were made, and have the kernel check for the `pay_fee` function? | ||
How do we convince the kernel that the machine state provided in the public inputs is correct? |
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 we should come up with a way of enshrining pay_fee
. Say, in the base rollup for example. All it does is subtract an amount of the gas token from a balance. I think it would significantly simplify things.
If we did this in the base rollup then it can be used for paying for private only transactions and we wouldn't need to prove a VM and kernel iteration for it.
Does this sound sensible to you @iAmMichaelConnor
Adds gas limits, max fee per gas, actual fee per gas, and gas used to circuit structs. These structs are not populated for now, they are just defined. Also updates the corresponding definitions in the yellow paper. Follows the spec outlined by @just-mitch in #5565 but for the following changes: - `GasLimits` is renamed to `GasSettings` (following @iAmMichaelConnor suggestion) - Info for each dimension in `GasSettings` (da/l1/l2) is bundled into a `DimensionGasSettings` struct - `GasSettings` in `PrivateKernelPublicInputs` is stored inside the `ConstantData` struct - Bundled each `fee_per_xx_gas` field in `GlobalVariables` into a `GasFees` struct - Did not yet add the `pending_header` in `CombinedConstantData`
Please read contributing guidelines and remove this line.