Skip to content
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

Closed
wants to merge 1 commit into from
Closed

Conversation

just-mitch
Copy link
Collaborator

Please read contributing guidelines and remove this line.

Copy link
Collaborator Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

Join @just-mitch and the rest of your teammates on Graphite Graphite

@just-mitch just-mitch changed the title yellow paper changes to document changes for DA metering docs: yellow paper changes to document changes for DA metering Apr 3, 2024
@just-mitch just-mitch changed the title docs: yellow paper changes to document changes for DA metering docs(yellow-paper): DA metering Apr 3, 2024
@just-mitch just-mitch marked this pull request as ready for review April 4, 2024 15:46
```

:::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.
Copy link
Collaborator

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?

Copy link
Collaborator Author

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.

Copy link
Collaborator Author

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

Copy link
Contributor

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 :)

yellow-paper/docs/gas-and-fees/gas-metering-and-fees.md Outdated Show resolved Hide resolved
yellow-paper/docs/gas-and-fees/gas-metering-and-fees.md Outdated Show resolved Hide resolved
+ 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
Copy link
Collaborator

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.

Copy link
Collaborator Author

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`.
Copy link
Collaborator

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?

Copy link
Collaborator Author

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.
Copy link
Collaborator

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?

Copy link
Collaborator Author

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.

yellow-paper/docs/gas-and-fees/gas-metering-and-fees.md Outdated Show resolved Hide resolved
yellow-paper/docs/gas-and-fees/gas-metering-and-fees.md Outdated Show resolved Hide resolved

It will be the same for the `compute_gas_used`.

### Out of gas
Copy link
Collaborator

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?

Copy link
Collaborator Author

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

@just-mitch just-mitch force-pushed the docs-da-metering branch 2 times, most recently from b3de270 to 27cec22 Compare April 5, 2024 15:09
These include:

- verification of transaction's private kernel proof
- verification of nullifiers
Copy link
Collaborator

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?

Copy link
Collaborator Author

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.

Copy link
Contributor

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?
Copy link
Contributor

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.

Copy link
Contributor

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"?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes!

Copy link
Contributor

@iAmMichaelConnor iAmMichaelConnor left a 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

Copy link
Contributor

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:
Copy link
Contributor

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.
Copy link
Contributor

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
Copy link
Contributor

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

Copy link
Contributor

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

Comment on lines +223 to +226
note_hash_gas +
nullifier_gas +
l2_to_l1_message_gas +
public_data_writes_gas +
Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor

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?

Copy link
Collaborator Author

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:
Copy link
Contributor

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.

Copy link
Collaborator Author

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`
Copy link
Contributor

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.
Copy link
Contributor

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)?

Copy link
Contributor

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?

Copy link
Collaborator

@PhilWindle PhilWindle Apr 10, 2024

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.:
Copy link
Contributor

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?

Copy link
Collaborator

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?
:::
Copy link
Collaborator

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:
Copy link
Collaborator

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)
Copy link
Collaborator

@PhilWindle PhilWindle Apr 10, 2024

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.

Copy link
Collaborator Author

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.
Copy link
Collaborator

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?
Copy link
Collaborator

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

spalladino added a commit that referenced this pull request Apr 11, 2024
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`
@just-mitch just-mitch closed this Apr 26, 2024
@ludamad ludamad deleted the docs-da-metering branch August 22, 2024 15:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants