EIP-7987: Transaction Gas Limit Cap at 2^24

The way ZK-EVMs generally work today is that they chunk at the transaction boundary: a monolithic prover proves each transaction (or, if they are small, batch of transactions), and then you aggregate those proofs into a single proof. And so within this architecture, the size of a transaction determines the degree to which you can parallelize. The theoretical minimum latency bound on SNARK-proving a block is something like: tx_gas_limit + recursion_cost * log(number_of_txs). And so if we want safe shorter slot times, pushing down the max tx gas limit cap is one of the key variables for doing that.

Distributed proving also adds pressure to decrease the max tx gas limit because the whole point is that you have a prover that’s very large in aggregate but where each individual piece is smaller, and so single very large transactions are precisely the case where it would break.

I have suggested multiple times to ZK-EVM developers to chunk at the EVM step boundary instead of the transaction boundary to be able to better process multiple transactions. As far as I know almost all of them have not done it. The reason why is that it is much more complex to chunk within a transaction: you have to carry over all kinds of internal state (callstack, memory, refunds, TSTORE…) from one proof to the other. There’s massive dev effort and room for bugs there.

The way that this EIP can improve execution parallelism is in combination with block-level access lists, particularly the version where you require the post-state of each transaction to be included. Any node verifying such a block becomes able to run all transactions in parallel, even if they have cross-dependencies, and verify that each intermediate access list is correct in parallel.

The reason why this EIP improves worst-cases around quadratic execution bugs, is that (I’m pretty sure without exception) all of the quadratic execution bugs we have seen involve interactions within a transaction (eg. depth-N call stack followed by N transactions) and not between transactions. Between transactions there is no shared state (except the Ethereum state itself), and so much less surface area for quadratic execution bugs.

AND MOST IMPORTANTLY setting the limit below 30M is a breaking change, which is a major sin and should be avoided.

I really hope that we do not adopt “breaking changes are a major sin and should be avoided” as our philosophy. “Breaking changes should be done very carefully, the carefulness in proportion to how many things they break” is better. If we treat breaking changes as a sin, then we end up suffering an eternal ongoing penalty for the sake of avoiding a one-time pain, and the amount of backwards-compatibility cruft that we suffer will keep stacking up over time.

We have made breaking changes before, with very positive results. The most famous one was increasing the gas costs of storage IO opcodes in order to improve DoS resistance. The first few rounds of this (40 → 200 → 700) were security-critical. The last round of this was arguably not security-critical, we could have arguably survived SLOAD at 700, but:

  1. It was ok at lower gas limits but it would have made gaslimit increases to the level we see today unsafe.
  2. It would have made statelessness unviable.

An even stronger example was the SELFDESTRUCT nerf (recently, SELFDESTRUCT became only usable to destroy a contract created within the same transaction). The goal of the SELFDESTRUCT nerf was to establish a new invariant: there is a hard O(1) bound on how many state changes can happen within a single transaction. This in turn made client development significantly easier, because clients would be able to have a simple one-layer caching mechanism for the state, instead of a complicated structure that could handle giant contracts being SELFDESTRUCTED, then that operation being reverted, then calls going into that contract, etc.

These changes were good for Ethereum. As a general principle, having more hardline invariants that bound what can happen within a transaction, or a block, is a very good thing in hard-to-predict ways. It makes future client optimizations possible. It makes client code simpler, which increases safety, improves client diversity and reduces development costs for teams. It prevents classes of safety issues that we did not know existed.

A key goal that many people have expressed for this year is scaling the L1. But scaling the L1 is risky, to safety and decentralization. One very good strategy for meeting both goals at the same time is to increase the L1 gaslimit and at the same time establish more and stronger invariants that cut off problematic worst-case scenarios.

I expect that the amount of disruption that proposals like (i) multidimensional gas, (ii) per-code-chunk witness pricing, (iii) serious gas repricings for ZK-EVM safety will be greater than that caused by reducing the tx gas limit, and so we need to be prepared for some applications that are unusually resource-intensive along some margin to need to rewrite parts of their logic regardless.

In fact, I would argue that as the ecosystem matures, the cost of making any breaking change rises every year, and so by constraining the per-tx load now, we would actually reduce total pain, because we get more leeway to do these future repricings (which will happen at a time when the ecosystem is even more mature and entrenched) through increasing limits rather than decreasing them.

4 Likes

I’m somewhat tardy here on the data analysis that I promised during Thursday’s ACDE. But the short of it is that, at least for my purposes (DEX aggregation), the difference between ~16.8M transaction gas limit and 30M transaction gas limit is (surprisingly to me) immaterial.

While 0x API routinely returns transactions consuming >16.8M gas on low-cost Ethereum L2s, closer, manual inspection of the traces of the transactions reveals that (with negligible exceptions) this is always the result of consuming all available on-chain liquidity for tokens with full-range liquidity on tick-based AMMs. Or, to put it another way, these are transactions with extreme price impact on illiquid tokens, and slightly worsening that experience by capping the number of ticks that a swap can cross (consequently, capping state access and gas consumption) does not noticeably worsen the outcome.

My position remains that backwards incompatibility is a sin and should be avoided, but this is now a philosophical argument. Given that there appears to be consensus on the 2^24 number among core devs and that @vbuterin has weighed in against my general position on backwards compatibility, I concede the point.

Thank you for your patience, time, and effort on this. I particularly appreciate the detail with which y’all explained the connections among this EIP, the architecture of ZK provers, and block access lists. If anybody is still interested in the data, I can provide it, but I promise it’s really not that interesting.

2 Likes