Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 15 additions & 13 deletions EIPS/eip-7976.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
eip: 7976
title: Increase Calldata Floor Cost
description: Increase the calldata floor cost to 15/60 gas per byte to reduce maximum block size
description: Increase the calldata floor cost to 64/64 gas per byte to reduce maximum block size
author: Toni Wahrstätter (@nerolation)
discussions-to: https://ethereum-magicians.org/t/eip-7976-further-increase-calldata-cost/24597
status: Draft
Expand All @@ -13,13 +13,13 @@ requires: 7623

## Abstract

This EIP proposes an adjustment to calldata pricing by raising the floor cost from 10/40 to 15/60 gas per zero/non-zero calldata byte. This reduces the worst-case block size by ~33% with minimal impact.
This EIP proposes an adjustment to calldata pricing by raising the floor cost from 10/40 to 64/64 gas per calldata byte. This reduces the worst-case block size by ~37% with minimal impact.

## Motivation

While [EIP-7623](./eip-7623.md) successfully reduced the maximum possible block size by introducing a floor cost of 10/40 gas per byte for data-heavy transactions, continued increases in gas limit demands further optimization. The current floor cost still permits relatively large data-heavy payloads that contribute to block size variance.
While [EIP-7623](./eip-7623.md) successfully reduced the maximum possible block size by introducing a floor cost of 10/40 gas per byte for data-heavy transactions, continued increases in gas limit demands further optimization. The current floor cost still permits relatively large data-heavy payloads that contribute to block size variance. With 64/64 pricing, a 10 MiB uncompressed execution payload would require ~671M gas instead of ~105M gas under 10/40 pricing, providing significant headroom for gas limit increases.

By increasing the floor cost to 15/60 gas per byte, this proposal aims to:
By increasing the floor cost to 64/64 gas per byte, this proposal aims to:

- Further reduce the maximum possible block size for data-heavy transactions
- Create additional headroom for potential block gas limit increases
Expand All @@ -31,10 +31,12 @@ By increasing the floor cost to 15/60 gas per byte, this proposal aims to:
| Parameter | Value |
| ---------------------------- | ----- |
| `STANDARD_TOKEN_COST` | `4` |
| `TOTAL_COST_FLOOR_PER_TOKEN` | `15` |
| `TOTAL_COST_FLOOR_PER_TOKEN` | `16` |

Let `tokens_in_calldata = zero_bytes_in_calldata + nonzero_bytes_in_calldata * 4`.

Let `floor_tokens_in_calldata = (zero_bytes_in_calldata + nonzero_bytes_in_calldata) * 4`.

Let `isContractCreation` be a boolean indicating the respective event.

Let `execution_gas_used` be the gas used for EVM execution with the gas refund subtracted.
Expand All @@ -51,20 +53,20 @@ tx.gasUsed = (
STANDARD_TOKEN_COST * tokens_in_calldata
+ execution_gas_used
+ isContractCreation * (32000 + INITCODE_WORD_COST * words(calldata)),
TOTAL_COST_FLOOR_PER_TOKEN * tokens_in_calldata
TOTAL_COST_FLOOR_PER_TOKEN * floor_tokens_in_calldata
)
)
```

Any transaction with a gas limit below `21000 + TOTAL_COST_FLOOR_PER_TOKEN * tokens_in_calldata` or below its intrinsic gas cost (take the maximum of these two calculations) is considered invalid. This limitation exists because transactions must cover the floor price of their calldata without relying on the execution of the transaction. There are valid cases where `gasUsed` will be below this floor price, but the floor price needs to be reserved in the transaction gas limit.
Any transaction with a gas limit below `21000 + TOTAL_COST_FLOOR_PER_TOKEN * floor_tokens_in_calldata` or below its intrinsic gas cost (take the maximum of these two calculations) is considered invalid. This limitation exists because transactions must cover the floor price of their calldata without relying on the execution of the transaction. There are valid cases where `gasUsed` will be below this floor price, but the floor price needs to be reserved in the transaction gas limit.

## Rationale

With [EIP-7623](./eip-7623.md)'s implementation, data-heavy transactions cost 10/40 gas per zero/non-zero byte, reducing the maximum possible EL payload size to approximately 1.07 MB (`45s_000_000/40`). This EIP further reduces this to approximately 0.72 MB (`45_000_000/60`) for non zero bytes and maintains proportional costs for non-zero bytes.
With [EIP-7623](./eip-7623.md)'s implementation, data-heavy transactions cost 10/40 gas per zero/non-zero byte, reducing the maximum possible EL payload size to approximately 1.07 MB (`45_000_000/40`). This EIP further reduces this to approximately 0.67 MB (`45_000_000/64`).

By increasing calldata costs from 10/40 to 15/60 gas per byte for data-heavy transactions, this EIP provides:
By increasing calldata costs from 10/40 to 64/64 gas per byte for data-heavy transactions, this EIP provides:

- **Enhanced block size reduction**: Maximum data-heavy payload size drops by ~33%.
- **Enhanced block size reduction**: Maximum data-heavy payload size drops by ~37%.
- **Maintained user experience**: Regular users engaging in DeFi, token transfers, and other EVM-heavy operations remain unaffected
- **Better blob incentivization**: Higher calldata costs further encourage migration to blob usage for data availability

Expand All @@ -80,17 +82,17 @@ This is a backwards incompatible gas repricing that requires a scheduled network

Wallet developers and node operators MUST update gas estimation handling to accommodate the new calldata cost rules. Specifically:

1. **Wallets**: Wallets using `eth_estimateGas` MUST be updated to ensure that they correctly account for the updated `TOTAL_COST_FLOOR_PER_TOKEN` parameter of 15. Failure to do so could result in underestimating gas, leading to failed transactions.
1. **Wallets**: Wallets using `eth_estimateGas` MUST be updated to ensure that they correctly account for the updated `TOTAL_COST_FLOOR_PER_TOKEN` parameter of 16. Failure to do so could result in underestimating gas, leading to failed transactions.

2. **Node Software**: RPC methods such as `eth_estimateGas` MUST incorporate the updated formula for gas calculation with the new floor cost values.

Users can maintain their usual workflows without modification, as wallet and RPC updates will handle these changes.

## Test Cases

Testing for this EIP should verify the correct application of the new calldata cost floor of 15/60 gas per zero/non-zero byte:
Testing for this EIP should verify the correct application of the new calldata cost floor of 64/64 gas per byte:

1. **Data-heavy transactions**: Verify transactions with minimal EVM execution pay the floor cost of 15 gas per calldata token
1. **Data-heavy transactions**: Verify transactions with minimal EVM execution pay the floor cost of 64 gas per calldata byte
2. **EVM-heavy transactions**: Confirm transactions with significant computation continue using standard 4/16 gas per byte
3. **Edge cases**: Test transactions at the boundary where execution gas equals or exceeds the floor cost
4. **Gas estimation**: Validate that `eth_estimateGas` correctly accounts for the new `TOTAL_COST_FLOOR_PER_TOKEN` value
Expand Down
144 changes: 54 additions & 90 deletions EIPS/eip-7979.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ These changes are backwards-compatible: all instructions behave as specified whe

In 2016 the lead author set out to write a compiler from the EVM to code for virtual or real register machines. The design was simple -- traverse the EVM stack code in one pass, emitting register code for the target machine. I was immediately stymied by the presence of dynamic jumps, which demand, worst case, a quadratic number of passes. _Dynamic jumps must die._

How to kill them? First, some history.
How to kill them? First, some history, for which the tl;dr is just that static control flow and calls and returns are common industry practice going back a very long ways, and for good reasons.

### Babbage, 1833: Jumps and conditional jumps

In 1833 Charles Babbage began the design of a steam-powered, mechanical, Turing-complete computer. Programs were to be encoded on punched cards which controlled a system of rods, gears and other machinery to implement storage, arithmetic, jumps, and conditional jumps. Jumps were supported by a mechanism for shuffling forwards or backwards through the cards. Its first published description was by L. F. Menabre, 1842[^1]. The translator, Ada Augusta, Countess of Lovelace, made extensive notes. The notes include her famous program for recursively computing Bernoulli numbers — arguably the world's first complete computer program — which used conditional jumps to implement the required nested loops.
In 1833 Charles Babbage began the design of a steam-powered, mechanical, Turing-complete computer. Programs were to be encoded on punched cards which controlled a system of rods, gears and other machinery to implement storage, arithmetic, jumps, and conditional jumps. Jumps were supported by a mechanism for shuffling forwards or backwards a fixed number of the cards. Its first published description was by L. F. Menabre, 1842[^1]. The translator, Ada Augusta, Countess of Lovelace, made extensive notes. The notes include her famous program for recursively computing Bernoulli numbers — arguably the world's first complete computer program — which used conditional jumps to implement the required nested loops.

### Turing, 1945: Calls and returns

Expand Down Expand Up @@ -64,9 +64,9 @@ We propose that the EVM should share these features.

### The EVM control-flow facility

Unlike these machines, the Ethereum Virtual Machine _does not_ provide facilities for calls and returns. Instead, they must be synthesized using the dynamic `JUMP` instruction, which takes its argument on the stack. Further, the EVM provides _only_ this jump. The EVM's dynamic jump causes problems. First, the need to synthesize static jumps and calls with dynamic jumps wastes some space and gas. The much bigger problem is this: jumps that can dynamically branch to any destination in the program can cause quadratic "path explosions" when traversing the program's flow of control. We will give more detailed explanations in the Rationale.
Unlike these machines, the Ethereum Virtual Machine _does not_ provide facilities for calls and returns. Instead, they must be synthesized using the dynamic `JUMP` instruction, which takes its argument on the stack. Further, the EVM provides _only_ this jump. The EVM's dynamic jump causes problems. First, the need to synthesize static jumps and calls with dynamic jumps wastes some space and gas. The much bigger problem is this: jumps that can dynamically branch to any destination in the program can cause quadratic "path explosions" when traversing the program's flow of control, and exponential "state explosions" when constructing ZK circuits. We will give more detailed explanations in the Rationale.

Dynamic jumps are common enough in physical machines, whose instruction sets are of course optimized for physical performance, but less so in virtual machines, whose code is often the source for JIT compilers and other downstream tools — a JIT compiler that can take quadratic time is less than useful. For this reason the Java, Wasm, and .NET VMs do not support dynamic jumps.
Dynamic jumps are common enough in physical machines, whose instruction sets are of course optimized for physical performance, but less so in virtual machines, whose code is often the source for JIT compilers and other downstream tools — a JIT compiler that can take quadratic time or a is less than useful. For this reason the Java, Wasm, and .NET VMs do not support dynamic jumps.

### Why dynamic jumps must die

Expand Down Expand Up @@ -244,7 +244,11 @@ That's 32% fewer bytes and 30% less gas using `CALLSUB` versus using `JUMP`. So

### Do we improve real-time performance?

Some real-time interpreter performance gains will be had, as reflected in the lower gas costs. But large real-time gains will come from AOT and JIT compilers. Crucially, the constraint that stack depths be constant means that in `MAGIC` code a very fast streaming JIT can traverse the control flow of the EVM code in one pass -- as it is executed -- generating machine code as it goes. _(Note again: the Wasm, JVM and .NET VMs share that property.)_ The EVM is a stack machine, but real machines are register machines. So generating virtual register code for a faster interpreter is a win, (I have seen 4X speedups on JVM code) and generating good machine code gives orders of magnitude gains. But for most transactions storage dominates execution time, and gas counting and other overhead always take their toll. So these gains would be most visible in contexts where this overhead is absent, such as for L1 precompiles, on some L2s, and on some EVM-compatible chains. Code can also be compiled to a better instruction set for on-chain ZK work, such as RISC-V.
Some real-time interpreter performance gains will be had, as reflected in the lower gas costs. But large real-time gains will come from AOT and JIT compilers. Crucially, the constraint that stack depths be constant means that in `MAGIC` code a very fast streaming JIT can traverse the control flow of the EVM code in one pass -- as it is executed -- generating machine code as it goes. _(Note again: the Wasm, JVM and .NET VMs share that property.)_ The EVM is a stack machine, but real machines are register machines. So generating virtual register code for a faster interpreter is a win, (I have seen 4X speedups on JVM code) and generating good machine code gives orders of magnitude gains. But for most transactions storage dominates execution time, and gas counting and other overhead always take their toll. So these gains would be most visible in contexts where this overhead is absent, such as for L1 precompiles, on some L2s, and on some EVM-compatible chains.

### Do we improve ZK performance?

Code can also be compiled to a better instruction set for on-chain ZK work, such as RISC-V.

### Why no immediate arguments?

Expand Down Expand Up @@ -358,91 +362,51 @@ Consumed gas: `30`

## Reference Implementation

_(Note: this implementation is known to be incomplete and incorrect.)_

The following is a pseudo-Python implementation of an algorithm for predicating code validity. An equivalent algorithm MUST be run at `CREATE` time in space and time linear in the size of the code. The following algorithm recursively traces the _code_, emulating its control flow and stack use and checking for violations of the rules above. It runs in time proportional to the size of the code, and the possible depth of recursion is proportional to the size of the code.

### Validation Function

We assume that instruction validation and destination analysis has been done, and that we have some constant-time helper functions:

* `is_terminator(opcode) returns true iff opcode is a terminator.`
* `previous_data(pc) returns the immediate data for the instruction before pc (usually a PUSH.)`
* `immediate_size(opcode) returns the size of the immediate data for an opcode.`
* `removed_items(opcode) returns the number of items removed from the data_stack by the opcode.`
* `added_items(opcode) returns the number of items added to the data_stack by the opcode.`

```
# returns true iff code is valid
int []stack_depths
int []max_depths
def validate_code(code: bytes, pc: int, sp: int, bp: int, max: int) -> int, boolean:
while pc < len(code):

# check stack height and return if we have been here before
stack_depth = sp - bp
max_depth = max + stack_depth
if max_depth > 1024
return max_depth, false
if stack_depths[pc] {
if stack_depth != stack_depths[pc]:
return 0, false
if opcode == ENTERSUB:
return max_depths[pc], true
else
return max_depth, true
else:
stack_depths[pc] = stack_depth

if is_terminator(opcode):
return max_depth, true

elif opcode == CALLSUB:

# push return address and set pc to destination
jumpdest = previous_data(pc)
push(return_stack, pc)

# validate and track maximum height
max_depth, valid = validate_code(jumpdest, 0, sp - bp, max)
if !valid:
return max_depth, false
max_depths[jumpdest] = max_depth;

elif opcode == RETURNSUB:

# pop return address and check for preceding call
pc = pop(return_stack)
max_depth = max + stack_depth
return max_depth, true

if opcode == JUMP:

# set pc to destination of jump
pc = previous_data(pc)

elif opcode == JUMPI:

jumpdest = previous_data(pc)

# recurse to validate true side of conditional
max_depth, valid = validate_code(jumpdest, sp, bp)
if !valid:
return max_depth, false

# apply instructions to stack
sp -= removed_items(opcode)
if sp < 0
return so, false
sp += added_items(opcode)

# Skip opcode and any immediate data
pc += 1 + immediate_size(opcode)

max_depth = max + stack_depth
if (max_depth > 1024)
return max_depth, false
return max_depth, true
_(Note: this implementation is likely to be incomplete and incorrect.)_

The following is a pseudo-Python implementation of an algorithm for predicating code validity. An equivalent proof MUST be provided at `CREATE` time in space and time linear in the size of the code. This algorithm uses a list of continuations to traverse the _code_, emulating its control flow and stack use and checking for violations of the rules above. It runs in time proportional to the size of the code, and the size of the list is proportional to the size of the code.

```python
class Validator:
def __init__(self, code):
self.code = list(code)
self.stack_heights = {} # PC -> height
self.visited_pcs = set()

def get_instr_info(self, pc):
if pc >= len(self.code): return None
op = self.code[pc]
# PUSH0 is 1 byte, PUSH1-32 is 1 + N bytes
size = 1 if op == 0x5f else (1 + (op - 0x5f) if 0x60 <= op <= 0x7f else 1)
# (pops, pushes, is_terminator)
deltas = {0x00: (0,0,1), 0x56: (1,0,1), 0x5d: (0,0,1), 0x5e: (1,0,0), 0x5c: (0,1,0)}
p, u, t = deltas.get(op, (0, 0, 0)) # Default pops/pushes omitted for brevity
return op, size, p, u, t

def validate(self):
worklist = [(0, 0, None)] # (PC, height, last_push_val)
while worklist:
pc, height, last_push = worklist.pop()
if pc in self.stack_heights:
if self.stack_heights[pc] != height: raise ValueError("Stack Mismatch")
continue
self.stack_heights[pc] = height
self.visited_pcs.add(pc)

op, size, p, u, is_term = self.get_instr_info(pc)
new_height = height - p + u
if new_height < 0 or new_height > 1024: raise ValueError("Stack Error")

if op in (0x56, 0x57, 0x5e): # JUMP, JUMPI, CALLSUB
if last_push is None: raise ValueError("Static Push Required")
worklist.append((last_push, new_height, None))
if op == 0x57: worklist.append((pc + size, new_height, None))
elif not is_term:
# Capture PUSH value for next instruction
val = int.from_bytes(self.code[pc+1:pc+size], 'big') if size > 1 else 0
worklist.append((pc + size, new_height, val if 0x5f <= op <= 0x7f else None))

return True
```

## Security Considerations
Expand Down
Loading
Loading