Conversation
There was a problem hiding this comment.
Thanks so much for doing this, Leila.
I've added a couple of suggestions for comments.
You'll see I have lots of questions about the methodology, but much of that can be clarified later in a script that enables someone to recreate all of these numbers (as we've just discussed on slack together).
I have some doubts here and there, but having these numbers is better than 0's for alpha. We can iron out the doubts in time for the next release later in the year.
| pub global MANA_PER_MS: u32 = 9083; | ||
| // The conversion rate from L1 gas to L2 gas is set to 1000. | ||
| pub global L1_TO_L2_GAS_CONVERSION_RATE: u32 = 1000; | ||
| // Some values are measured per checkpoint or per epoch and are distributed across all txs, assuming 1 TPS, which |
There was a problem hiding this comment.
Please could you explain why the choice of 1000?
There was a problem hiding this comment.
This is arbitrary 😅 But this makes the L1 cost take up only 0.01 of the total fixed l2 gas. Should we increase it?
There was a problem hiding this comment.
I reckon we delete it.
I asked Claude for a refresher on fees, and specifically how the user is charged for their share of L1 gas if they only pay in L2 gas. Essentially, the user has to pay a minimum fee_per_gas, and it is this minimum fee_per_gas (gas price) which kind of covers the L1 component of the user's cost, instead of L1 gas metering.
Claude:
The minimum mana fee is deterministic and oracle-derived
The rollup contract computes a single deterministic fee per mana for each block. The sequencer has no discretion —
it's enforced as an exact equality on L1:
// ProposeLib.sol
require(_args.header.gasFees.feePerL2Gas == _args.manaMinFee, ...);
Not >=, but ==. The block's fee is exactly what the formula produces.
Two snapshots feed into it
The formula in FeeLib.getManaMinFeeComponentsAt() uses two oracle values:
- L1 gas prices — a lagged snapshot of block.basefee and blob base fee (2-5 slots stale)
- ETH/FeeJuice exchange rate (ethPerFeeAsset) — converts the ETH-denominated cost into Fee Juice terms
The computation is:
costInEth = (300K * L1_baseFee + 3 * 131K * L1_blobFee) / manaTarget [sequencer]
+ (1M * L1_baseFee / epochDuration) / manaTarget + param [prover]
totalInEth = costInEth * congestionMultiplier(excessMana)
feePerMana = totalInEth / ethPerFeeAsset [convert to Fee Juice]
The user's position
The user sets maxFeesPerGas when creating their tx. Their tx is only included in a block where:
block.feePerMana <= user.maxFeesPerGas
If the oracle-derived minimum exceeds the user's max, the tx sits in the mempool until either:
- L1 fees drop, or
- The exchange rate shifts
Once included, the user pays at least gasUsed * block.feePerMana. They can pay slightly more via priority fees, but
never more than their maxFeesPerGas.
So to answer directly
Yes — the user is forced to pay a minimum mana fee that is mechanically derived from two snapshots: the L1 gas price
snapshot and the ETH/FeeJuice exchange rate snapshot. Both are slightly stale. The user's only lever is to set
maxFeesPerGas high enough to be included, or wait for conditions to change.
| // ----------- | ||
| // The following values were calculated by running tests to benchmark AVM simulation and proving time | ||
| // (avm_opcode_spam.test.ts) and determining the average gas per second. Then, we ran the tx validation benchmark | ||
| // (tx_validation.test.ts) and multiplied the validation time by the average gas per second to estimate gas costs. |
There was a problem hiding this comment.
I couldn't find this file. I found a tx_validator_bench.test.ts?
There was a problem hiding this comment.
Oooops yes that's the one!
| // TODO: We need a test suite to formalize and validate these measurements. | ||
| // | ||
| // The fixed L2 gas consists of: | ||
| // - Tx validation (common checks for both private-only and public txs): 9.86ms |
There was a problem hiding this comment.
Does this include proof verification, checking fee juice balance, duplicate nullifier checks, contract class log hashing? I'm surprised it's this quick. I'd have expected longer.
Also, shouldn't duplicate nullifier checks be dependent on the number of nullifiers; and contract class log hashing be dependent on the size of the contract class log?
There was a problem hiding this comment.
Edit: I can see the contract class log hashing is separate, below.
There was a problem hiding this comment.
That includes proof verification and checking fee juice balance.
You are right, checking duplicate nullifiers should be priced per nullifier. I left it as 0 as I thought it was negligible (< 0.25ms). But multiplying by the big mana per ms will actually result in thousands of gas. Will add it!
noir-projects/noir-protocol-circuits/crates/types/src/constants.nr
Outdated
Show resolved
Hide resolved
| // === L2 gas for side effects from private-only txs === | ||
| // The Gas for batch insert note hashes is included in the fixed L2 gas. | ||
| pub global L2_GAS_PER_NOTE_HASH: u32 = 0; | ||
| // The Gas for batch insert nullifiers is included in the fixed L2 gas. |
There was a problem hiding this comment.
| // The Gas for batch insert nullifiers is included in the fixed L2 gas. | |
| // The Gas for batch insert note hashes is included in the fixed L2 gas. | |
| // The rollup always inserts a fixed-size subtree of nullifiers for each tx (even if there are no nullifiers to insert). |
| // The time for validating duplicate nullifiers is negligible. | ||
| pub global L2_GAS_PER_NULLIFIER: u32 = 0; |
There was a problem hiding this comment.
This surprises me (that the cost of validating duplicate nullifiers is negligible), because this is cited as a zcash scaling problem. Maybe the number of nullifiers in the test db wasn't large enough to trigger a slowdown. In a year, there could be 2GB of nullifiers (if all 64 are used every tx, which is of course unrealistic). Still... a DB of the order of GBs really should be used to test duplicate lookup times.
| // Gas for writing message on L1: 24011 L1 gas for inserting epoch out hash root to the outbox. | ||
| // Assuming 1 l2-to-l1 message per checkpoint. | ||
| pub global L2_GAS_PER_L2_TO_L1_MSG: u32 = 750 * L1_TO_L2_GAS_CONVERSION_RATE; |
There was a problem hiding this comment.
Where does 750 come from?
There was a problem hiding this comment.
24011 / 32. 32 is the number of checkpoints per epoch.
Should we assume there will be more l2-to-l1 message per checkpoint/epoch?
There was a problem hiding this comment.
I think we can observe alpha and change it later :)
| // - parity-base: 22084ms * 4 | ||
| // - parity-root: 22825ms | ||
| // - tx-merge: 11892ms * 70 | ||
| // - first-block-root: 17914ms |
There was a problem hiding this comment.
| // - first-block-root: 17914ms | |
| // - first-block-root: 17914ms | |
| // - This slightly over-estimates the cost, because subsequent block-root circuits don't have the overhead | |
| // of verifying parity-root circuits (and other checks that only happen in the first-block-root). |
| // - parity-root: 22825ms | ||
| // - tx-merge: 11892ms * 70 | ||
| // - first-block-root: 17914ms | ||
| // - checkpoint-root-single-block: 54345ms |
There was a problem hiding this comment.
Is this similar in cost to the other checkpoint roots? I think it is.
There was a problem hiding this comment.
The other checkpoint root (for 2 blocks) is more expensive. But I picked these circuits to measure as if there's only 1 block per checkpoint. I will check if it's similar in cost if there are 2 blocks per checkpoint.
There was a problem hiding this comment.
It's 687ms less if we have 2 blocks per checkpoint.
There was a problem hiding this comment.
Lovely, that difference sounds broadly negligible, so probably fine to leave as it is? Wdyt?
There was a problem hiding this comment.
(Although might be worth writing a comment to explain so, if you're still adding extra comments)
| // - Cost on L1 (assuming 1tps): | ||
| // - Propose a checkpoint: 325171 L1 gas -> 4517 per tx | ||
| // - Prove an epoch: 885319 L1 gas -> 384 per tx | ||
| pub global FIXED_L2_GAS: u32 = 44374 * MANA_PER_MS + (4517 + 384) * L1_TO_L2_GAS_CONVERSION_RATE; |
There was a problem hiding this comment.
See big claudey comment. I think we can kill the L1 component (i.e. the + (4517 + 384) * L1_TO_L2_GAS_CONVERSION_RATE; addend
|
I don't know how to undo my approval, but we're still discussing this and have doubts |
Use the gas per ms of the AVM to measure the base L2 gas values.