Vitalik's New Work Quick Read: Multidimensional Gas Pricing

Vitalik Buterin
2024-05-09 20:05:19
Collection
Vitalik discusses Ethereum's multi-dimensional gas pricing, how to balance and choose?

Author: Vitalik Buterin

Compiled by: Karen, Foresight News

In Ethereum, resources have been limited until recently and are priced through a single resource known as "Gas." Gas is a unit of measurement for the "computational effort" required to process a specific transaction or block. Gas combines various types of "computational effort," the most important of which include:

  1. Raw computation (e.g., ADD, MULTIPLY);
  2. Reading and writing to Ethereum storage (e.g., SSTORE, SLOAD, ETH transfers);
  3. Data bandwidth;
  4. The cost of generating ZK-SNARK proofs for block creation.

For example, the transaction I sent consumed a total of 47,085 Gas. This includes: (i) a base cost of 21,000 Gas, (ii) 1,556 Gas consumed by the calldata bytes included as part of the transaction, (iii) 16,500 Gas for reading and writing storage, (iv) 2,149 Gas for generating logs, with the remainder used for EVM execution. The transaction fees that users must pay are proportional to the Gas consumed by the transaction. A block can contain a maximum of 30 million Gas, and the Gas price is continuously adjusted through the EIP-1559 targeting mechanism to ensure that each block averages 15 million Gas.

This approach has a major advantage: because everything is consolidated into a virtual resource, the market design is very simple. Optimizing transactions to minimize costs is straightforward, and optimizing blocks to charge as high fees as possible is relatively easy (excluding MEV), with no strange incentive mechanisms encouraging some transactions to bundle with others to save on fees.

However, this method also has inefficiencies: it treats different resources as interchangeable, while the actual underlying constraints are not the same. To understand this issue, you can first look at the chart below:

Gas limits impose a constraint:

The actual underlying security constraints are often closer to:

This discrepancy leads to Gas limits either arbitrarily excluding blocks that are actually secure or accepting blocks that are not secure, or both.

If there are n types of resources with different security constraints, then one-dimensional Gas may reduce throughput by up to n times. Therefore, there has been long-standing interest in the concept of multi-dimensional Gas, and through EIP-4844, we have now effectively implemented multi-dimensional Gas on Ethereum. This article explores the advantages of this approach and the prospects for further enhancements.

Blob: Multi-dimensional Gas in Dencun

At the beginning of this year, the average block size was 150 kB. A large portion of this was Rollup data: Layer 2 protocols store data on-chain. This data is very expensive: although the transaction costs on Rollup are only 5-10 times that of the corresponding transactions on Ethereum L1, even such costs are too high for many use cases.

So why not reduce the Gas cost of calldata (currently 16 Gas for non-zero bytes and 4 Gas for zero bytes) to make Rollup cheaper? We have done this before and can do it again. But the answer here is: the maximum size of a block is 30,000,000/16=1,875,000 non-zero bytes, and the network can barely handle such large blocks, if at all. Reducing the cost by another factor of 4 would raise the maximum to 7.5 MB, which would pose significant risks to security.

This issue is ultimately resolved by introducing a separate, Rollup-friendly data space (called a blob) within each block.

These two resources have different prices and limits: after the Dencun hard fork, an Ethereum block can contain a maximum of (i) 30 million Gas and (ii) 6 blobs, each blob capable of containing about 125 kB of calldata. Both resources have separate prices and are adjusted through a separate pricing mechanism similar to EIP-1559, targeting an average usage of 15 million Gas and 3 blobs per block.

As a result, the cost of Rollup has decreased by 100 times, the transaction volume on Rollup has increased by more than 3 times, while the theoretical maximum block size has only slightly increased: from about 1.9 MB to about 2.6 MB.

Note: Rollup transaction fees, provided by Growthepie.xyz. The Dencun fork occurred on March 13, 2024, introducing multi-dimensional pricing for blobs.

Multi-dimensional Gas and Stateless Clients

In the near future, the storage proofs for stateless clients will face similar issues. Stateless clients are a new type of client that will be able to verify the chain without storing a large amount or any data locally. Stateless clients achieve this by accepting proofs of specific parts of the Ethereum state that transactions in that block need to access.

The above image shows a stateless client receiving a block, along with a proof of the current values of specific parts of the state touched by the execution of that block (e.g., account balances, code, storage), allowing the node to verify a block without any storage.

A single storage read costs 2,100-2,600 Gas, depending on the type of read, while storage writes are more expensive. On average, a block will perform about 1,000 storage read/write operations (including ETH balance checks, SSTORE and SLOAD calls, contract code reads, and other operations). However, the theoretical maximum is 30,000,000/2,100=14,285 reads. The bandwidth load for stateless clients is proportional to this number.

The current plan is to support stateless clients by transitioning Ethereum's State tree design from Merkle Patricia trees to Verkle trees. However, Verkle trees do not have quantum resistance and are not the optimal choice for newer STARK proof systems. Therefore, many are interested in supporting stateless clients through binary Merkle trees and STARKs, either completely skipping Verkle or upgrading after a few years of transitioning to Verkle, once STARKs become more mature.

STARK proofs based on binary hash tree branches have many advantages, but their key weakness is the long time required to generate proofs: Verkle trees can prove over 100,000 values per second, while hash-based STARKs can typically only prove a few thousand hashes per second, as proving each value requires including many hashes in a "branch."

Considering the numbers predicted today from highly optimized proof systems like Binius and Plonky3, as well as dedicated hashes like Vision-Mark-32, we seem to be in a practical range for a while, where proving 1,000 values is feasible, but proving 14,285 values is not. Average blocks would be fine, but blocks in the potential worst-case scenario (published by an attacker) would compromise the network.

Our default method for handling such situations is repricing: increasing the cost of storage reads to reduce the maximum per block to a safer level. However, we have done this many times already, and doing it again would make too many applications too expensive. A better approach is multi-dimensional Gas: limiting and charging separately for storage access, keeping the average usage at 1,000 storage accesses per block, but setting an upper limit per block, such as 2,000 accesses.

The Universality of Multi-dimensional Gas

Another resource worth considering is the growth of state size: operations that increase the size of Ethereum's state, which then require full nodes to store. The uniqueness of state size growth is that the reasons for limiting it come entirely from long-term sustained usage, rather than peaks.

Therefore, adding a separate Gas dimension for operations that increase state size (e.g., zero-to-non-zero SSTORE, contract creation) may be valuable, but the goals differ: we could set a floating price targeting specific average usage, but not set any limits per block.

This demonstrates a powerful attribute of multi-dimensional Gas: it allows us to separately ask (i) what is the ideal average usage? (ii) what is the safe maximum usage per block? Unlike setting Gas prices based on the maximum per block and letting average usage follow, we have 2n degrees of freedom to set 2n parameters, adjusting each parameter based on considerations of network security.

More complex situations, such as when the security considerations of two resources partially add up, can be handled by making an opcode or resource consume a certain amount of multiple types of Gas (e.g., a zero-to-non-zero SSTORE might consume 5,000 stateless client proof Gas and 20,000 storage expansion Gas).

Each transaction Max (selecting the one that consumes more data or computation)

Let ( x1 ) be the Gas cost for data, and ( x2 ) be the computation Gas cost, so in a one-dimensional Gas system, we can write the Gas cost of a transaction as:

In this scheme, we define the Gas cost of the transaction as:

That is, the transaction is charged based on which of the two resources it consumes more of, rather than simply adding data and computation. This can easily be extended to cover more dimensions (e.g., ( \max(…, x_3 * storage_access) )).

It should be easy to see how this can improve throughput while ensuring security. The theoretical maximum amount of data in a block remains ( \text{Gas LIMIT} / x1 ), exactly the same as in the one-dimensional Gas scheme. Similarly, the theoretical maximum computation amount is ( \text{Gas LIMIT} / x2 ), also the same as in the one-dimensional Gas scheme. However, the Gas cost for any transaction consuming both data and computation will decrease.

This is roughly the scheme proposed in EIP-7623 to reduce the maximum block size while further increasing the blob count. The precise mechanism in EIP-7623 is slightly more complex: it keeps the current calldata price at 16 Gas per byte but adds a floor price of 48 Gas per byte; transactions pay the higher of ( (16 * \text{bytes} + \text{execution_Gas}) ) and ( (48 * \text{bytes}) ). Thus, EIP-7623 reduces the theoretical maximum transaction call data in a block from about 1.9 MB to about 0.6 MB while keeping the costs for most applications unchanged. The benefit of this approach is that it changes very little compared to the current one-dimensional Gas scheme, making it very easy to implement.

However, this approach has two drawbacks:

  1. Even if all other transactions in the block use very little of that resource, transactions that heavily consume one resource will still incur excessive fees unnecessarily;
  2. It incentivizes data-intensive and computation-intensive transactions to bundle into one package to save costs.

I believe that rules like EIP-7623, whether for transaction calldata or other resources, can provide enough benefits that they are worth it, even with these drawbacks.

However, if we are willing to invest (significantly higher) development effort, a more ideal approach emerges.

Multi-dimensional EIP-1559: A More Difficult but Ideal Strategy

Let’s first review how the regular EIP-1559 works. We will focus on the version introduced for blobs in EIP-4844, as it is mathematically more elegant.

We track a parameter ( \text{excess_blobs} ). During each block, we set:

[ \text{excess_blobs} \leftarrow \max(\text{excess_blobs} + \text{len(block.blobs)} - \text{TARGET}, 0) ]

where ( \text{TARGET} = 3 ). That is, if the number of blobs in a block exceeds the target, ( \text{excess_blobs} ) increases, and if the number of blobs in a block is less than the target, ( \text{excess_blobs} ) decreases. We then set ( \text{blob_basefee} = \exp(\text{excess_blobs} / 25.47) ), where ( \exp ) is the exponential function ( \exp(x) = 2.71828^x ).

That is, every time ( \text{excess_blobs} ) increases by about 25, the blob base fee increases by about 2.7 times. If blobs become too expensive, average usage will decline, and ( \text{excess_blobs} ) will begin to decrease, automatically lowering prices again. The price of blobs is continuously adjusted to ensure that, on average, blocks are half full, meaning each block averages 3 blobs.

If there is a short-term peak in usage, there is a limit: each block can contain a maximum of 6 blobs, in which case transactions can compete with each other by increasing priority fees. However, under normal circumstances, each blob only needs to pay the ( \text{blob_basefee} ) plus a small amount of additional priority fee as an incentive to be included.

This Gas pricing has existed in Ethereum for many years: as early as 2020, EIP-1559 introduced a very similar mechanism. Through EIP-4844, we set two independent floating prices for Gas and Blobs.

Note: Gas base fees for one hour on May 8, 2024, measured in gwei. Source: ultrasound.money

In principle, we could add more independent floating fees for storage reads and other types of operations, but I will detail a caveat in the next section.

For users, this experience is very similar to today: you no longer pay a single base fee, but rather two base fees, although your wallet can abstract this from you, only showing you the expected fees and maximum fees you can expect to pay.

For block builders, the best strategy most of the time remains the same as today: include any valid content. Most blocks are not full—whether in Gas or Blobs. A challenging situation arises when there is enough Gas or enough Blobs exceeding the block limit, where builders may need to potentially solve a multi-dimensional knapsack problem to maximize their profits. However, even with reasonably good approximation algorithms, the gains from optimizing profits through proprietary algorithms in this case are much smaller than the gains from using MEV to perform the same operations.

For developers, the main challenge is the need to redesign the functionality of the EVM and its associated infrastructure, which are currently designed based on a single price and single limit, to now adapt to a design that can accommodate multiple prices and multiple limits.

One issue faced by application developers is that optimization becomes slightly more difficult: in some cases, you can no longer clearly say that A is more efficient than B, because if A uses more calldata while B uses more execution, then A may be cheaper when calldata is cheap, but more expensive when calldata is expensive.

However, developers can still achieve reasonably good results by optimizing based on long-term historical average prices.

Multi-dimensional Pricing, EVM, and Sub-calls

There is a problem that does not arise in blobs, nor in the complete multi-dimensional pricing implementation for calldata in EIP-7623, but if we try to price state access or any other resource separately, this problem will arise: namely, the Gas limits in sub-calls.

Gas limits in the EVM exist in two places. First, each transaction sets a Gas limit, which restricts the total amount of Gas that can be used in that transaction. Second, when a contract calls another contract, that call can set its own Gas limit. This allows contracts to call other contracts they do not trust while still ensuring they have enough Gas remaining after the call to perform other computations.

Note: The trace of an account abstraction transaction, where one account calls another account and only provides a limited amount of Gas to the callee, ensuring that even if the callee consumes all the Gas allocated to it, the external call can continue to run.

The challenge is that implementing multi-dimensional Gas between different types of execution seems to require sub-calls to provide multiple limits for each type of Gas, which would require very deep changes to the EVM and would be incompatible with existing applications.

This is one reason why multi-dimensional Gas proposals typically remain at two dimensions: data and execution. Data (whether transaction calldata or blobs) is allocated only outside the EVM, so no changes need to be made inside the EVM to price calldata or blobs separately.

We can think of an "EIP-7623-style solution" to address this issue. This is a simple implementation: during execution, charge 4 times the fee for storage operations; to simplify analysis, assume each storage operation costs 10,000 Gas. At the end of the transaction, refund ( \min(7500 * \text{storage_operations}, \text{execution_Gas}) ). The result is that, after the refund, the user needs to pay the following fees:

[ \text{execution_Gas} + 10,000 * \text{storage_operations} - \min(7500 * \text{storage_operations}, \text{execution_Gas}) ]

This equals:

[ \max(\text{execution_Gas} + 2500 * \text{storage_operations}, 10,000 * \text{storage_operations}) ]

This reflects the structure of EIP-7623. Another approach is to track ( \text{storage_operations} ) and ( \text{execution_Gas} ) in real-time and charge 2500 or 10,000 based on how much ( \max(\text{execution_Gas} + 2500 * \text{storage_operations}, 10,000 * \text{storage_operations}) ) rises when the opcode is called. This avoids the need for transactions to over-allocate Gas, which is primarily recouped through refunds.

We do not achieve fine-grained permissions for sub-calls: sub-calls may consume all of the transaction's allowance for cheap storage operations.

But we do get something good enough, which is that contracts making sub-calls can set a limit and ensure that once the sub-call is executed, the main call still has enough Gas for the required post-processing.

The simplest "complete multi-dimensional pricing solution" I can think of is: we treat the sub-call Gas limits as proportional. That is, suppose there are ( k ) different types of execution, and each transaction sets multi-dimensional limits ( L1…Lk ). Suppose at the current execution point, the remaining Gas is ( g1…gk ). Suppose a CALL opcode is invoked, using a sub-call Gas limit ( S ). Let ( s1 = S ), then ( s2 = s1/g1 * g2 ), ( s3 = s1/g1 * g_3 ), and so on.

That is, we treat the first type of Gas (which is actually VM execution) as a special "account unit," and then allocate other types of Gas so that sub-calls receive the same percentage of available Gas across each type of Gas. This method is somewhat ugly, maximizing backward compatibility.

If we want to make this scheme more "neutral" between different types of Gas without sacrificing backward compatibility, we could simply express the sub-call Gas limit parameter as a portion of the remaining Gas in the current context (e.g., ([1…63]/64)).

However, in either case, it is worth emphasizing that once multi-dimensional execution Gas begins to be introduced, the inherent complexity (ugliness) increases, which seems difficult to avoid.

Thus, our task is to make a complex trade-off: do we accept some degree of increased complexity (ugliness) at the EVM level to safely unlock significant L1 scalability gains, and if so, which specific proposal is most effective for protocol economics and application developers? It is very likely that the two proposals I mentioned above are not the best, but there is still room for more elegant and better proposals.

Special thanks to Ansgar Dietrichs, Barnabe Monnot, and Davide Crapis for their feedback and review.

ChainCatcher reminds readers to view blockchain rationally, enhance risk awareness, and be cautious of various virtual token issuances and speculations. All content on this site is solely market information or related party opinions, and does not constitute any form of investment advice. If you find sensitive information in the content, please click "Report", and we will handle it promptly.
banner
ChainCatcher Building the Web3 world with innovators