Dip 7999
dip: 7999
title: Unified multidimensional fee market
description: Let transactions specify one aggregate max_fee budget for all resources, unify fee markets, normalize gas, and generalize DIP-7918.
author: Anders Elowsson (@anderselowsson), Vitalik Buterin (@vbuterin)
Digitalia editing author: Cosimo Constantinos cosimo@juro.net, et al.
discussions-to: https://digitalia-magicians.org/t/dip-7999-unified-multidimensional-fee-market/25010
status: Draft
type: Standards Track
category: Core
created: 2025-08-04
Created for Digitalia: 2025-01-07
requires: 1559, 2718, 4844, 7516, 7691, 7840, 7918
Abstract¶
A unified multidimensional fee market is introduced, where each transaction specifies the maximum amount of USDS it is willing to pay for inclusion using a single max_fee. Upon inclusion, the protocol ensures that the transaction is able to pay the gas for all dimensions, treating the max_fee as fungible across resources. This enables a more efficient use of capital, and enshrines the same representation that users have when they interact with digitalia. The fee market is further unified in terms of a single update fraction under a single fee update mechanism, generalized reserve pricing, and a gas normalization that retains current percentage ranges while keeping the price stable whenever a gas limit changes. Calldata is proposed as the first resource to be added, with avenues for facilitating gas fungibility for DVM resources considered for further expansion.
Motivation¶
A multidimensional fee market enables precise control over resource consumption. It allows the market to fairly price resources according to targets and limits deemed safe by developers, and it allows resources to be consumed at maximum capacity within these limits. Directly expanding the current fee market design to the multidimensional setting can however have negative effects on the user experience (UX) and on economic efficiency. Users are forced to set a max_fee_per_gas for each resource, where a too low allocation in any dimension can render the transaction ineligible.
This DIP leverages the natural fungibility of the user's fee budget by letting users set a single unified max_fee. Instead of using non-fungible per-resource budgets, the single USDS budget can then be allocated dynamically to cover costs wherever they arise, ensuring a more efficient use of capital. Users will be able to specify a lower max_fee than the implied aggregate maximum unless all base fees are perfectly correlated (which reduces to the current design), because they do not need to buffer for spurious price movements in a single resource dimension.
Digitalia's current fee market has "tech debt" in that two separate mechanisms are used: one for regular gas (DIP-1559) and the other for blob gas (DIP-4844). The proposal unifies the fee market under the preferred DIP-4844 design. That design allows for exact control over long-run resource consumption. In a multidimensional setting with individual base fees, we can then for example achieve precise control over state growth, while accommodating temporary spikes. Excess gas of DIP-4844 is further normalized relative to the limit, allowing for a single update fraction across resources that retains current percentage ranges while keeping the price stable if any gas limit changes.
Calldata is added first, to speed up worst-case payload propagation and expand available DVM gas—without compromising gas introspection. A method for facilitating gas aggregation across resources within the DVM is outlined as an avenue for preserving backward compatibility when expanding further. The logic of DIP-7918 is integrated into the multidimensional setting to ensure that calldata has a higher cost per byte than blob data.
Specification¶
The specification inherits its logic from DIP-7706, incorporating the changes necessary for facilitating one aggregate fee, a multidimensional DIP-7918 logic, a systematic approach to DIP-7805, and a stable gas normalization function, etc.
Parameters¶
| Constant | Value | Description |
|---|---|---|
MULTIDIM_TX_TYPE |
TBD |
Identifier for the new transaction type |
DVM_LIMIT_TARGET_RATIO |
2 |
Ratio of DVM gas target to DVM gas limit |
CALLDATA_GAS_PER_TOKEN |
4 |
Gas cost per token for calldata |
TOKENS_PER_NONZERO_BYTE |
4 |
Tokens per non-zero byte of calldata |
CALLDATA_GAS_LIMIT_RATIO |
4 |
Ratio of calldata limit to gas limit |
CALLDATA_LIMIT_TARGET_RATIO |
4 |
Ratio of calldata target to calldata limit |
GAS_RESERVE_FACTOR |
[0, 16, 12] |
Factor by which the base fee can be below the baseline |
GAS_RESERVE_INDEX |
[0, 0, 1] |
Index of the base fee to compare against |
MIN_BASE_FEE_PER_GAS |
1 |
Minimum base fee per gas unit |
GAS_NORMALIZATION_FACTOR |
10**9 |
Normalization factor for the delta excess gas |
BASE_FEE_UPDATE_FRACTION |
4_245_093_508 |
≈ GAS_NORMALIZATION_FACTOR / (2 * ln(1.125)) |
New transaction type¶
Upon activation of this DIP via hard fork, a new DIP-2718 transaction is introduced with TransactionType = MULTIDIM_TX_TYPE.
The DIP-2718 TransactionPayload for this transaction is
[chain_id, nonce, gas_limit, to, value, data, access_list, blob_versioned_hashes, max_fee, max_priority_fee_per_gas, y_parity, r, s]
We require max_fee to be a scalar integer from 0 to 2**128-1 and max_priority_fee_per_gas to be a list of integers from 0 to 2**64-1, either of length 1 or with the same length as the number of resources (initially 3). The gas_limit is initially specified only for the main DVM gas, since other limits can be inferred from the transaction. To facilitate further expansion, the Python spec uses a list with a single element at index 0. Each added integer will range from 0 to 2**64-1.
The intrinsic cost of the new transaction is inherited from DIP-4844, except that the calldata gas cost (16 per non-zero byte, 4 per zero byte) is removed.
Block processing and transaction fees¶
Helpers for vector operations:
def all_less_or_equal(v1: [int, int, int], v2: [int, int, int]) -> bool:
return all(x <= y for x, y in zip(v1, v2))
def vector_add(v1: [int, int, int], v2: [int, int, int]) -> [int, int, int]:
return [x+y for x, y in zip(v1, v2)]
def vector_mul(v1: [int, int, int], v2: [int, int, int]) -> [int, int, int]:
return [x*y for x, y in zip(v1, v2)]
Helper functions for computing scalar max fees/priority fees for all transaction types:
def get_max_fee(tx: Transaction) -> int:
if tx.type == MULTIDIM_TX_TYPE: # New tx type already has a max_fee
return tx.max_fee
elif tx.type == BLOB_TX_TYPE: # Account for blobs in blob txs
blob_gas = len(tx.blob_versioned_hashes) * GAS_PER_BLOB
return tx.max_fee_per_gas*tx.gas_limit + tx.max_fee_per_blob_gas*blob_gas
elif is_dip_1559(tx.type): # DIP-1559 txs have no blobs
return tx.max_fee_per_gas * tx.gas_limit
else: # Legacy transactions have a gasprice
return tx.gasprice * tx.gas_limit
def get_priority_fee(tx, gas: list[int], base_fees: list[int], remaining_fee: int) -> int:
# Precalculate total gas and fees for non-blob resources
tip_indices = [0, 2]
tgas = sum(gas[i] for i in tip_indices)
tfee = sum(gas[i] * base_fees[i] for i in tip_indices)
if tx.type == MULTIDIM_TX_TYPE: # New tx type: Single tip or full vector of tips
if len(tx.max_priority_fee_per_gas) == 1:
tmax_fee = tx.max_fee - gas[1] * base_fees[1] # Ignore blobs (as now)
tpriority_fee = tmax_fee - tfee if tmax_fee > tfee else 0
max_priority_fee = min(tx.max_priority_fee_per_gas[0] * tgas, tpriority_fee)
else:
max_priority_fee = sum(vector_mul(tx.max_priority_fee_per_gas, gas))
return min(max_priority_fee, remaining_fee)
if is_legacy(tx.type): # Legacy tx: The remainder after base fees paid
tmax_fee = tx.gasprice * tgas
return tmax_fee - tfee if tmax_fee > tfee else 0
# DIP-1559 or Blob tx
tmax_fee = tx.max_fee_per_gas * tgas
tpriority_fee = tmax_fee - tfee if tmax_fee > tfee else 0
max_priority_fee = min(tx.max_priority_fee_per_gas * tgas, tpriority_fee)
if tx.type == BLOB_TX_TYPE: # Clamp since blobs reduce the shared max_fee budget
return min(max_priority_fee, remaining_fee)
return max_priority_fee
The calldata resource pricing under this DIP follows previous gas per byte constants but deprecates the floor pricing specified in DIP-7623:
def get_calldata_gas(calldata: bytes) -> int:
tokens = calldata.count(0) + (len(calldata) - calldata.count(0)) * TOKENS_PER_NONZERO_BYTE
return tokens * CALLDATA_GAS_PER_TOKEN
The helper for calculating gas limits from DIP-7706 is adjusted to subtract calldata gas from the gas limit of old transactions:
def get_gas_limits(tx: Transaction) -> list[int]:
calldata_gas = get_calldata_gas(tx.data)
blob_gas = len(getattr(tx, 'blob_versioned_hashes', [])) * GAS_PER_BLOB
if tx.type == MULTIDIM_TX_TYPE: # We use tx.gas_limit as is for new tx type
return [tx.gas_limit, blob_gas, calldata_gas]
else: # Partition old tx.gas_limit into its execution and calldata components.
require(tx.gas_limit >= calldata_gas)
execution_gas = tx.gas_limit - calldata_gas
return [execution_gas, blob_gas, calldata_gas]
The calculation for the required max_fee to process the transaction is done in a separate function for easy future expansion:
def get_required_max_fee(base_fees: list[int], tx_gas_limits: list[int]) -> int:
return sum(vector_mul(base_fees, tx_gas_limits))
At the start of block processing:
- We initialize a vector
gas_used_so_farto[0, 0, 0].
At the start of processing a transaction:
- Derive basic properties of the tx:
max_fee = get_max_fee(tx),base_fees = get_block_base_fees(block.parent),tx_gas_limits = get_gas_limits(tx).max_base = get_required_max_fee(base_fees, tx_gas_limits)
- Require that
all_less_or_equal(vector_add(gas_used_so_far, tx_gas_limits), gas_limits). The block'sgas_limitsare defined in the next subsection.max_base <= max_fee
- Compute the fees to deduct initially:
max_priority_fee = get_priority_fee(tx, tx_gas_limits, base_fees, max_fee - max_base)fee_to_deduct = max_base + max_priority_fee
- Deduct
fee_to_deductbit from the sender, which we define as the address recovered from the transaction’s signature.
At the end of processing a transaction:
- Compute
tx_gas_consumedas a three-item vector, where the first item is the amount of execution gas actually consumed, and the second and third items are the blob and calldata gas amounts, which are equal to their limits fromtx_gas_limits. - Burn
base_fee_paid = sum(vector_mul(base_fees, tx_gas_consumed)). - Transfer to the coinbase
priority_fee_paid = get_priority_fee(tx, tx_gas_consumed, base_fees, fee_to_deduct - base_fee_paid) - Refund the sender
fee_to_deduct - base_fee_paid - priority_fee_paid. - Update
gas_used_so_far = vector_add(gas_used_so_far, tx_gas_consumed).
At the end of processing a block:
- Require each element of
block.gas_used(a vector field in the header) to equal the corresponding element ingas_used_so_far.
Block structure¶
The BlockHeader is updated to remove the blob_gas_used, gas_used, base_fee_per_gas, gas_limit and excess_blob_gas fields, and the following new fields are added, all of the [int, int, int] type: gas_limits, gas_used, excess_gas. The header sequence of the new fields is [..., withdrawals_root, gas_limits, gas_used, excess_gas].
We define the gas_limits
gas_limits[0]follows the existing adjustment formula based on the parentgas_limits[0].gas_limits[1]must equalblobSchedule.max * GAS_PER_BLOBgas_limits[2]must equalgas_limits[0] // CALLDATA_GAS_LIMIT_RATIO.
and the gas_targets
gas_targets[0]must equalgas_limits[0] // DVM_LIMIT_TARGET_RATIO.gas_targets[1]must equalblobSchedule.target * GAS_PER_BLOB.gas_targets[2]must equalgas_limits[2] // CALLDATA_LIMIT_TARGET_RATIO.
The blobSchedule for referencing target and max blobs was introduced in DIP-7840.
Gas accounting¶
We incorporate DIP-7918 to establish a dynamic reserve price for blob and calldata gas. When the market price for a resource drops below its reserve price, the mechanism for reducing its excess_gas (and thus lowering its base fee) is disabled. This leads the base fee to rise with usage until it meets the reserve price.
We normalize the running excess gas so that all resources operate at the same scale with the same BASE_FEE_UPDATE_FRACTION, which also provides a smoother response when any limit is adjusted. Division by gas_limits[i] upholds the same price changes stipulated in DIP-4844 and DIP-7691.
def calc_excess_gas(parent: Header) -> list[int]:
base_fees = get_block_base_fees(parent)
limits = parent.gas_limits
targets = get_block_gas_targets(parent)
new_excess = []
for i in range(len(parent.excess_gas)):
if (GAS_RESERVE_FACTOR[i] > 0
and base_fees[i] * GAS_RESERVE_FACTOR[i] < base_fees[GAS_RESERVE_INDEX[i]]):
# DIP-7918 path. Excess gas rises with usage (up to limit), but cannot fall.
delta = parent.gas_used[i] * (limits[i] - targets[i]) // limits[i]
excess = parent.excess_gas[i] + delta * GAS_NORMALIZATION_FACTOR // limits[i]
else: # Regular path. Excess gas rises and falls with usage as normal
if parent.gas_used[i] >= targets[i]: # Add
delta = parent.gas_used[i] - targets[i]
excess = parent.excess_gas[i] + delta * GAS_NORMALIZATION_FACTOR // limits[i]
else: # ..or subtract
delta = targets[i] - parent.gas_used[i]
deltan = delta * GAS_NORMALIZATION_FACTOR // limits[i]
excess = 0 if parent.excess_gas[i] < deltan else parent.excess_gas[i] - deltan
new_excess.append(excess)
return new_excess
def get_block_base_fees(parent: Header) -> list[int]:
return [
fake_exponential(
MIN_BASE_FEE_PER_GAS,
excess,
BASE_FEE_UPDATE_FRACTION
)
for excess in parent.excess_gas
]
CALLDATABASEFEE instruction¶
We add a CALLDATABASEFEE (0x4b) instruction that returns the calldata base fee of the current block, following the same principles as specified in DIP-7516 for the blob base fee.
| Op | Input | Output | Cost |
|---|---|---|---|
| 0x4b | 0 | 1 | 2 |
Censorship resistance¶
In DIP-7805, includers propose inclusion-list (IL) transactions that the block builder must include, subject to rules evaluated post-execution once remaining capacity is known. We define three resource types based on these rules:
- Conditional resource – An IL transaction consuming this resource must be included if the block has sufficient remaining capacity. If such a transaction is excluded, the block should not be accepted, unless the available space (
gas_limit[i] - gas_used[i]) for at least one of its required conditional resources was insufficient to fit the transaction. - Unconditional resource – An IL transaction consuming this resource must be included as long as sufficient capacity remains for all conditional resources it also uses. A lack of available capacity in the unconditional resource itself is not a valid reason for exclusion.
- Deconditional resource – An IL transaction consuming this resource can always be excluded, regardless of available capacity. For this reason, there is no incentive for includers to list such a transaction.
If DIP-7805 is implemented, it must: (i) continue to treat DVM gas as a conditional resource; (ii) continue to treat blob gas as a deconditional resource; (iii) treat calldata gas as an unconditional resource. Any transaction listed in an IL that consumes calldata and is excluded from the block, despite there being sufficient capacity in all conditional resources it uses, MUST produce an INVALID_INCLUSION_LIST.
Rationale¶
Why go multidimensional?¶
Many digitalia resources such as blobs, calldata, access, and compute are in limited supply each block, constrained by the need to, e.g., timely propagate data or run computations. Upholding the constraints on all these resources via a single meta-resource—"gas"—limits developers' ability to control both supply and demand. By going multidimensional, developers gain a more fine-grained control over the supply of each resource, preventing one from encroaching on the allotment for another. Resources can then be consumed at maximum capacity within each specified target and limit. With separate base fees for each resource, the market can come to an agreement on the appropriate price that leads to consumption at maximum capacity, given a specific user demand and aforementioned constraints on its supply. A multidimensional fee market is thus inherently a tool for scaling digitalia.
User experience¶
digitalia currently uses max_fee_per_gas for DVM gas and max_fee_per_blob_gas for blob gas. A direct expansion of this approach is proposed in DIP-7706, with a vector of fees per gas for each resource. This may be considered unfortunate from a UX perspective, given that the vector will expand with expanding dimensionality. Our casual users tend to be moderately confused already by a single max_fee_per_gas. Most do not primarily think in terms of the individual prices for the resources that the transaction will consume. They think in terms of how much USDS they need to pay for their transactions (or in terms of dollars/fiat). A unified max_fee is in this context an improvement to UX. A unified max_fee_per_gas is possible, but might cause confusion in that the required gas price will differ from the base fees when using several resources—and thus also differ between transactions using different proportions of the resources.
When it comes to the priority fee, the optimal UX between using max_priority_fee and max_priority_fee_per_gas is a bit more nuanced. We decided to use max_priority_fee_per_gas. digitalia already today unifies the priority fee for blob gas and regular gas under a single max_priority_fee_per_gas. It is thus natural to retain this representation, if only for removing friction for wallets. Furthermore, the priority fee should be determined from actual gas usage and not the gas limit. This is however perfectly possible when using max_priority_fee as well, because the max priority fee can be treated as proportional to the limit during processing, thus falling if less gas is consumed.
Retaining max_priority_fee_per_gas can be considered slightly safer for users, in that they will never risk manually submitting a max_priority_fee for an old transaction type. Advanced users may wish to specify one priority fee per resource, to granularly pay according to usage when consuming below the gas limit. This ability was therefore preserved as an optional vector-based priority fee.
Economic efficiency¶
Besides UX, resource-specific fee limits can also be unfortunate at a deeper economic level. Once a user has specified a gas_limit for any non-deterministic dimension, potentially with the assistance of their wallets, multiple separate max_fee_per_gas could exclude transactions that specify a sufficient aggregate fee, due to a drift in relative levels of the base fees.
Consider a multidimensional transaction with a gas_limit vector $\mathbf{l} = (l_1, l_2,\dots, l_n)$, a max_fee_per_gas vector $\mathbf{f} = (f_1, f_2, \dots, f_n)$, and a max_priority_fee_per_gas vector $\mathbf{p} = (p_1, p_2, \dots, p_n)$. The consumed gas of the transaction is denoted $\mathbf{g} = (g_1, g_2, \dots, g_n)$, and the vector of base fees is denoted $\mathbf{b} = (b_1, b_2, \dots, b_n)$. The realized priority fee, after ensuring a sufficient base fee, is then
$$ p{\prime}_i = \min(p_i, f_i-b_i) $$
for all resources $i$. Assume that when the transaction is submitted, the user specifies a max_fee_per_gas vector $\mathbf{f}$ such that all entries individually satisfy all base fee $\mathbf{b}$ criteria
$$ f_i \ge b_i \quad \text{for all resources } i. $$
The gas limits also satisfy the actual gas consumption
$$ l_i \ge g_i \quad \text{for all resources } i. $$
The max_priority_fee_per_gas vector $\mathbf{p}$ is also considered sufficient by many proposers, when they jointly weigh the reward against competing transactions using a weight vector $\mathbf{w}$, considering contention across relevant resources:
$$ \sum p{\prime}_i g_i \ge \sum w_i g_i. $$
While not evaluated in the existing DIP, the base fees $\mathbf{b}$ could also be satisfied in aggregate against the max fees:
$$ \sum f_i l_i \ge \sum b_i l_i. $$
Now assume that the base fee for any of the resources rapidly rises before the transaction is included, such that it becomes higher than the max_fee_per_gas in that dimension. In this scenario, the transaction can no longer be included. This may happen, even though the aggregate fees that the user is offering to pay, $\sum f_i l_i$, remain at a level above the aggregate fees that the protocol demands to execute it, $\sum b_i l_i$, just as initially. The aggregate priority fees may still also satisfy the proposer. The welfare loss consists of a user, a proposer, and a protocol willing to process a transaction, hamstrung by rigidity in the protocol design.
The analysis leads to the following conclusions, all of which apply with increasing emphasis the higher the dimensionality:
- The UX would be simplified by one aggregate USDS fee.
- The protocol would be best served by taking a single aggregate USDS fee from the user.
- The proposer already considers the aggregate impact when making its inclusion decision.
Thus, the proposal is that the user specifies a single USDS max_fee. The protocol first ensures that the max_fee $F$ covers the base fee across gas limits in all dimensions, $F > \sum b_i l_i$, and finally charges the minimum aggregate fee possible. The total fee paid to the protocol is thus the same as in the original multidimensional fee market design. The design is future-proof in that additional dimensions easily are incorporated, retaining the single max_fee.
Priority fee¶
Considerations for the priority fee were already outlined in the UX section. Here, we first expand on why a vector max_priority_fee_per_gas can be beneficial to some users for fine-grained control, but not to most. The user has full control over the priority fee with a single input, as long as gas usage is deterministic across all resources. This input can be either max_priority_fee or max_priority_fee_per_gas—they are equivalent under deterministic gas usage. They are furthermore equivalent under any circumstances if the protocol makes sure to scale the max_priority_fee according to realized gas usage relative to the specified limit. Otherwise, the max_priority_fee will not be reduced when a transaction uses less gas than the limit (this is more often a drawback than a benefit for the user).
The priority fee a user wishes to provide per gas for a resource depends on the (likelihood of) contention across this resource. We can thus expect it to differ between resources. It follows from the principle of degrees of freedom that to gain full control over the transaction's priority fee, a user needs a separate parameter for each independent variable. When gas usage is non-deterministic in one resource—as today—the user needs two priority fees to achieve full granularity. When gas usage is non-deterministic in two resources (and their non-deterministic usage is not perfectly correlated), the user needs three priority fees, etc.
It should here be noted that the required priority fee per gas for some resource cannot be directly ascertained from the priority fee per gas that has been stipulated for that resource in recent transactions. Assume that there are 10 resource and each transaction includes a vector stipulating max_priority_fee_per_gas for each. The builder will in its inclusion decision operate on the aggregate, and its requirements across dimensions can thus only be inferred, not observed directly. This means that per-resource estimates of required priority fees are not trivial, and most may still prefer the simplicity of a single one.
Unified gas accounting, normalization, and reserve pricing mechanism¶
digitalia currently uses two separate fee mechanisms, one for execution gas (DIP-1559) and one for blob gas (DIP-4844). This DIP unifies these mechanisms under the DIP-4844 standard, similar to DIP-7706 but with a few additions. We normalize the excess gas delta by dividing by the gas limit in calc_excess_gas, thus enabling all resources to operate under the same BASE_FEE_UPDATE_FRACTION while also keeping each fee fixed during a hard fork whenever a limit changes.
The reserve pricing mechanism from DIP-7918 is also generalized such that it can be applied to any resource, using another resource as an anchor. The mechanism imposes that if the base fee multiplied by GAS_RESERVE_FACTOR is less than the anchor base fee, the base fee cannot fall any further. Calldata uses blob data as an anchor to ensure that we do not charge less for calldata than blob data (see the separate section on the calldata resource for further details).
Wallet fee estimation logic¶
To suggest a max_fee, the wallet first simulates the transaction to obtain an estimate of the required limit(s) $\mathbf{l}$. It then queries the network for the current vector of base fees $\mathbf{b}$. The expected total fee is $\sum b_i l_i$. The wallet recommends a max_fee that consists of this expected total cost plus a single buffer to account for aggregate volatility in base fees before the transaction is included. To suggest a max_priority_fee_per_gas, the wallet analyzes recent blocks to determine the priority fees that ensure timely inclusion. It focuses on priority fees paid by recently included transactions consuming a similar distribution of resources, as well as overall network contention.
Block building¶
Historical data show that around 90% of blocks are below the gas limit. It is only when blocks are full that the builder is constrained in its block construction (disregarding timing games). Furthermore, a multidimensional knapsack problem only manifests in the event that a block is simultaneously full in multiple dimensions. Ignoring blobs, this is expected to be rare with the proposal, since calldata is assigned a limit four times above the target, which is already set higher than current consumption. The calldata limit is thus unlikely to be reached frequently. The blob dimension is a special case, in that this dimension already exists today, and the number of blob-carrying transactions is fairly low. Generally, the impact on revenue from sophisticated packing algorithms will likely be dwarfed by more significant MEV factors such as transaction ordering and private order flow.
The calldata resource¶
Byte sizes¶
Calldata is separated into its own resource. With the proposed constants, at 60M execution gas, each block will on average contain 60M/(CALLDATA_GAS_LIMIT_RATIO * CALLDATA_LIMIT_TARGET_RATIO) = 3.75M gas of calldata. Focusing on non-zero bytes that cannot be Snappy-compressed, this gas corresponds to around 3.75M/16 = 234kB. The average block size has been around 90kB at a 36M gas limit, which would expand to 150kB at a 60M gas limit if the proportion of calldata in the block remains the same. Using basic assumptions, this proposal thus increases the amount of calldata that will be consumed by over 50%, from 150kB to 234kB at 60M gas. As pointed out in DIP-7706, this will serve to decrease the cost of calldata for our users.
The limit is 60M/CALLDATA_GAS_LIMIT_RATIO = 15M gas of calldata, corresponding to around 15M/16 = 938kB. This is well below the limit under the current specification incorporating DIP‑7623, which is 60M/40 = 1.5MB at 60M gas. Since calldata gas is also no longer accounted for as execution gas, the implied "aggregate" gas limit expands to 75M gas, without materially affecting the maximum workload in terms of execution. In conclusion, the separation of calldata into its own resource increases calldata throughput while reducing costs. It furthermore facilitates scaling by allowing for faster payload propagation in the worst case and keeping all DVM gas available for other operations.
Censorship resistance¶
For censorship resistance (CR) purposes, under the currently proposed CR implementation DIP-7805, builders must include all transactions surfaced in any of the 16 inclusion lists (ILs) of each slot (each up to 8KiB). However, if the block is full, builders can ignore the ILs, to not incentivize them to influence includers for MEV purposes.
When resources have separate limits, the block is treated as "full" already when any single conditional resource reaches its limit. Transactions that use that resource can then be excluded, even if they were surfaced by an IL. This makes it potentially cheaper to "stuff the block" to censor transactions. Specifically, a builder can create dummy transactions consuming a single resource to fill that dimension, ensuring that the block is treated as "full" when evaluating IL transactions. When resources have separate base fees, the builder may target a resource with a lower base fee, at a cost of roughly $b_i \cdot \Delta g_i$, where $\Delta g_i$ is the additional gas in resource $i$ required to reach its limit.
The good news is that calldata has properties allowing the builder to extract MEV while at the same time unconditionally adhering to its gas limit under DIP-7805. This means that this DIP does not impede censorship resistance. Specifically, in the case where all ILs are filled with disjoint transactions, the aggregate size of included transactions can still be at most 8KiB*16 = 131kB. This leaves at the very minimum 938kB - 131kB = 807kB of calldata for the builder to use as it sees fit when extracting MEV, which is sufficient according to the usage patterns we know.
Accordingly, calldata is treated as an "unconditional" resource, as defined in the specification. An includer MUST therefore signal INVALID_INCLUSION_LIST if—and only if—a calldata‑using IL transaction is omitted despite sufficient capacity remaining in all conditional resources it uses. When considering expansion into further resources with tighter conditional limits, it may be necessary to adopt a version of FOCIL with ranked transactions (FOCILR).
Reserve price¶
An DIP-7918 reserve price is used for calldata just as for blobs. This ensures that the equilibrium price does not fall to levels where the fee market update mechanism stops working satisfactorily. Furthermore, the expiry window for blobs is much shorter than the planned rolling expiry window for calldata, making a modest relative reserve price motivated from a resource preservation perspective. Finally, without a reserve price on calldata, it could become cheaper than blob data (particularly below the blob reserve price), and thus a rational choice for L2s.
The latter rationale also motivates the specific parameterization chosen. The DIP-7918 reserve price per byte for calldata is set 1/3 above the price per byte for blob data. This is achieved by tying the DIP-7918 if clause to the blob base fee. The gas per byte is 16 for calldata and 1 for blob data, and the price floor is triggered when base_fees[1] > GAS_RESERVE_FACTOR[2] * base_fees[2]. Given GAS_RESERVE_FACTOR[2] = 12, the DIP-7918 condition activates when the blob base fee is more than 12 times higher than the calldata base fee, at which point calldata costs less than 16/12 = 1+1/3 of the blob price per byte. The calldata base fee is then imposed to not fall further.
Relationship to previous DIPs and other resources¶
We acknowledge that calldata already has been limited by DIP-7623, with further repricings proposed in DIP-7976. These DIPs limit worst-case block sizes by metering and conditionally pricing calldata at the transaction level as opposed to at the block level, having the calldata price vary with a transaction's execution usage. This may lead to secondary markets if users wish to combine different transactions to take advantage of the "rebate" on calldata when consumed together with DVM gas. Treating calldata as a separate resource with a price based on block usage would allow for a more precise control over usage, and transactors would not need to interact with a secondary market to achieve the best price.
Given that the DIP-7623 design already achieves calldata moderation, it must be remembered that an individual fee market for calldata would merely be a first step toward a multidimensional fee market. Calldata is not an endgame. It is likely best to transition over several hard forks, and the options available at the present must then be considered, which leads to a focus on calldata. Another present consideration is block level access lists (BALs) and their interaction.
A resource such as state is more attractive to separate than calldata. We could achieve exact control over state growth, while allowing temporary spikes many times above current gas limits. However, an expansion into other resources requires a multidimensional gas repricing, which has currently not yet been completed. The next section will further discuss remaining complexities of multidimensional DVM gas, and the strategies available for overcoming them.
Expansion paths for multiple DVM resources¶
This DIP has been designed to facilitate a future expansion into multiple DVM resources. We conclude by outlining the challenges inherent to such an expansion, and the different paths that it can take.
DVM without gas observability¶
One long-term vision for the DVM is to move away from gas observability. This is one of the features of EOF (DIP-7692), e.g., through revamped CALL instructions in DIP-7069 such as EXTCALL. The new calls no longer accept a gas stipend as an input parameter, and the DVM instead makes available some reasonable fraction of all gas across dimensions (e.g., 63/64). Legitimate use cases previously handled via gas observability are then instead taken over by, e.g., the PAY opcode DIP-5920.
For compatibility with legacy code, the DVM can in this scenario reinterpret legacy subcalls with a gas parameter by forwarding the same fraction of the caller’s remaining budget in each resource dimension. Concretely, if the call stipulates $g_c$ and the aggregate remaining DVM-gas budget is $g_a$, the callee receives, for each DVM resource with remaining budget $g_r$, the amount $\bigl\lfloor g_r \cdot \min!\bigl(1,\tfrac{g_c}{g_a}\bigr) \bigr\rfloor$. For completeness, the GAS opcode could likewise return, e.g., $g_a$ (the per-call aggregate remaining budget at this point across all resource dimensions). Note, however, that reinterpreting legacy calls and GAS in this way can still change the behavior of contracts that rely on precise gas observability or gas-capped subcalls, and such contracts may break.
DVM that retains gas observability¶
It is possible to expand into multiple DVM resource dimensions without breaking existing contracts that rely on gas observability. By treating gas as fungible across resources (just as today), a single budget can be forwarded and counted toward any resource that consumes it. It should be noted that old contracts may still break due to repricing of the resources they use; indeed, a gas repricing effort is currently underway in Digitalia that likely will cause such breakage. But this may be treated as a separate concern.
Transaction types under multiple DVM resources with non-deterministic limits¶
A key difference between the new and old transaction types is that the old transaction types set only one limit, whereas the new can set several. When adding deterministic resources such as calldata, this is not a concern, because consumption of this resource can be deducted from the user-specified limit as a pre-processing step. But once there is more than one non-deterministic resource—as can be the case when DVM gas is separated into several resources—things get slightly more complicated. If we wish to ensure that old transaction types still can function properly under these circumstances, we must apply the single limit to multiple non-deterministic resources. The following subsections will outline how this can be achieved by aggregating the gas of these separate resources during DVM processing.
The new transaction type can supply several limits, making expansion into multiple DVM resources more straightforward. However, it turns out that we may also wish to retain the ability of the new transaction type to set a single limit for DVM gas. The reason is that the single limit and aggregate processing facilitates backward compatibility for contracts that rely on gas observability. As previously noted, these contracts must be able to supply subcalls with an aggregate gas stipend, to be counted against any DVM resource. Furthermore, there is an inherent simplicity of the single limit that is not to be discounted.
In all considered approaches below, the number of non-deterministic DVM resources still increases at the block level. To retain the ability to reject a transaction that may require more gas for a resource than its associated block limit, it becomes necessary to alter the check on gas_used_so_far. Specifically, when a transaction stipulates only the main DVM gas limit, then this limit must be conservatively counted toward all underspecified dimensions of gas_used_so_far before the limit check:
if len(tx_gas_limits) == len(gas_used_so_far) # Same check as previously
all_less_or_equal(vector_add(gas_used_so_far, tx_gas_limits), gas_limits)
else: # The joint DVM gas limit is expanded to cover all block resources
tx_gas_limits_exp = [tx_gas_limits[0]] * len(gas_limits)
tx_gas_limits_exp[1], tx_gas_limits_exp[2] = tx_gas_limits[1], tx_gas_limits[2]
all_less_or_equal(vector_add(gas_used_so_far, tx_gas_limits_exp), gas_limits)
Multidimensional gas metering¶
An existing proposal by Inês Silva and Crapis is Multidimensional gas metering (not yet submitted as an DIP), which prices gas in one dimension, but applies limits at the block level across several dimensions when updating the (single) DVM base fee. The DVM will thus track multidimensional gas consumption, but perform the limit check on the aggregate, which is also passed on in subcalls. The proposed DIP could be combined with Multidimensional gas metering, for example by using a single DVM gas and calculating metered block gas usage separately, before submitting the outcome as a single resource dimension to the revamped calc_excess_gas().
Multidimensional fee market with aggregate DVM gas¶
To promote full resource utilization while maintaining backward compatibility for subcalls and old transaction formats, we could instead use separate gas prices for each DVM resource, while retaining within the DVM the ability to aggregate the gas consumption across resources. We refer to this as a Multidimensional fee market with aggregate gas. In the fully aggregated scenario, the user stipulates a single gas_limit for DVM gas, and the protocol must make sure that the max_fee covers the maximum possible fee, from consuming gas_limit of the DVM resource that has the most expensive base fee. Execution is then fully backward compatible, and the DVM operates the same way as with Multidimensional gas metering.
The downside is that the user must stipulate an unnecessarily high max_fee allocation, if the most expensive resource is not used. We present three options for alleviating this. The first two options transfer to the block producer the responsibility to ensure that the max_fee indeed covers a transaction's fees determined post-execution. The third option instead enables the transactor to alternatively provide better guarantees in the form of full gas limits.
Option 1: Invalidate a block when the post-transaction check shows that the total fee for a transaction exceeded max_fee. We could either completely remove the pre-execution check max_fee >= get_required_max_fee(base_fees, tx_gas_limits), or only check for deterministic resources in addition to DVM gas across the cheapest DVM resource. During processing, there would still be an aggregated check against the transaction's gas limit, which is the sender's responsibility. In extension, the block's gas limit is already safeguarded pre-execution by conservatively counting tx_gas_limits_exp against it, as outlined in the previous subsection.
Note that the inclusion guarantees of DIP-7805 will not apply to the transactions where the max_fee does not cover the worst-case pre-execution check. These transactions are to be ignored when validating the block post-execution.
Option 2: Give the block producer the ability and responsibility to supply any missing funds as part of the post-transaction check. This could potentially rely on staked builders currently envisioned in DIP-7732, albeit it would be more intuitive to use execution layer balances for this purpose. Another alternative is to charge a block-based base fee at the end of processing a block rather than per transaction, as has been discussed in the past. This naturally extends to a multidimensional setting. Note that (2) reverts to (1) upon failure of the builder to ensure the base fees are covered.
Option 3: Give the transactor the ability to provide limits for all dimensions, if they so wish. The pre-execution check can then be more fine-grained, inheriting the higher capital efficiency previously outlined. This is a hybrid design, where the transactor can choose the option most suitable to their needs if they use the new transaction type. This option will be discussed in more detail in the next subsection. Note that (3) can be combined with (1) if desirable.
Multidimensional fee market with hybrid DVM gas¶
In a Multidimensional fee market with hybrid DVM gas, the user has separate options:
- Provide a single gas limit for the DVM resources. The DVM can then operate as in the aggregate gas model, after the initial restrictive fee allocation check.
- Provide a gas limit for each DVM resource. Capital efficiency is then preserved. The DVM operates without gas observability as previously outlined, with the gas parameter of old subcalls converted to a multidimensional counterpart. The transaction is during processing checked against its multiple limits, as opposed to the aggregate.
It is further possible to give the user the freedom to stipulate some DVM gas limits, but not all. The user can for example set the gas limit to 0 for any resource it knows that it will not use, instead of not setting a gas limit for that resource. The aggregation then takes place only across resources without an individual stipulated limit, reducing the required max_fee allocation while preserving full/a higher level of backward compatibility. These features come at a cost of somewhat increased complexity.
Specification for aggregate and hybrid DVM gas¶
For the hybrid model, the get_gas_limits() function must be updated to alternatively return multiple DVM gas limits, in the case the user stipulates the full list.
def get_gas_limits(tx: Transaction) -> list[int]:
...
if tx.type == MULTIDIM_TX_TYPE:
if len(tx.gas_limit) == 1:
return [tx.gas_limit[0], blob_gas, calldata_gas]
else:
return [tx.gas_limit[0], blob_gas, calldata_gas] + tx.gas_limit[1:]
...
For the aggregate model, the get_required_max_fee() must be updated such that the protocol applies the worst-case DVM base fees across the DVM gas. The new constant DVM_INDICES specifies the DVM resource indices. Note that TX_BASE_COST is treated as a deterministic component of the DVM gas to reduce required capital allocation. Note that even when pursuing Options 1-2 for aggregate gas, the aggregation step is still applied in consideration of DIP-7805.
def get_required_max_fee(base_fees: list[int], tx_gas_limits: list[int]) -> int:
# Fully specified gas limits, apply baseline pattern
if len(base_fees) == len(tx_gas_limits):
return sum(vector_mul(base_fees, tx_gas_limits))
# Otherwise: 1. TX_BASE_COST is treated deterministically (assumed resource 0)
determ_dvm_cost = TX_BASE_COST * base_fees[0]
require(tx_gas_limits[0] >= TX_BASE_COST) # Limit MUST cover TX_BASE_COST.
variable_dvm_limit = tx_gas_limits[0] - TX_BASE_COST
# 2. Determine DVM cost based on most expensive resource
max_dvm_base_fee = max(base_fees[i] for i in DVM_INDICES)
dvm_cost = determ_dvm_cost + variable_dvm_limit * max_dvm_base_fee
# 3. Return total DVM cost + blob cost + calldata cost
return dvm_cost + base_fees[1]*tx_gas_limits[1] + base_fees[2]*tx_gas_limits[2]
For the hybrid model, the line stipulating tip_indices in get_max_priority_fee() must be updated to include all new DVM resources tip_indices = [2] + DVM_INDICES. It is also feasible to adjust the functioning of legacy transactions, to let them tip only according to the headroom between the DVM resource with the highest base fee and the gasprice. The aim would be to prevent them from having to pay an excessive tip when the gasprice was set high merely to cover for the DVM resource with the highest base fee (of which they use little). The tip would then be calculated as the premium above the highest DVM resource base fee.
To allow transactors to specify some limits, the tx.gas_limit list must instead have a nested format, where gas_limit[0] is the aggregate limit for unspecified DVM resources, and [index, limit] pairs then follow: tx.gas_limit = [aggregate, [[index, limit], [index, limit],...]]. The protocol then computes the required max fee from resources with both specified and unspecified limits. The maximum base fee among the resources with unspecified limits is in this case multiplied with the aggregate, and this fee is summed together with the fees of specified limits.
Backwards Compatibility¶
The max_fee_per_gas of the DIP-1559 and DIP-4844 transaction types are converted to a max_fee by multiplication with the stipulated gas limit(s). The converted DIP-4844 transactions will no longer adhere to the individual max_fee_per_gas and max_fee_per_blob_gas, but instead to the aggregated max_fee. This is a deliberate choice since previous individual checks have lower economic efficiency to no clear benefit, but it should nevertheless be noted by transactors. The priority fee retains the same functionality as previously. The old gasprice can be used both for the max_fee and max_priority_fee_per_gas. The gas_limit is finally derived deterministically. The section on expansion paths outlined how old transactions can retain functionality as we expand into several non-deterministic DVM resources.
Backward compatibility for contracts that rely on gas introspection can be resolved by giving users the ability to rely on aggregated DVM gas, while still potentially pricing DVM resources separately. Wallets must be updated to handle multidimensional gas accounting with several base fees.
Security Considerations¶
One concern is the risk of increasing builder centralization due to increased revenue from sophisticated packing algorithms. While the builder is constrained in its block construction when blocks are full, it is only when several limits are reached at the same time that packing complexity markedly changes from today. Given that around 90% of blocks are below the gas limit, and the calldata limit is set to be very permissive, we argue that this will happen very rarely.
One reason for using max_priority_fee_per_gas instead of max_priority_fee is to establish the priority fee as always having a non-aggregate representation. We could possibly otherwise imagine a scenario where a user in the old transaction format manually submits an aggregate max_fee and max_priority_fee, causing loss of funds.
Copyright¶
© Crown © Crown Copyright 2026. Published by the Royal Government of the Dominion of Atlantis.
Licensed under the Juro Restricted License Version 2. See https://juro.net/jrl for details.