Skip to content

feat(eip8130): apply EIP-7623 calldata floor to intrinsic gas - #5073

Open
chunter-cb wants to merge 2 commits into
mainfrom
chunter/eip8130-eip7623-calldata-floor
Open

chunter-cb wants to merge 2 commits into
mainfrom
chunter/eip8130-eip7623-calldata-floor

Conversation

@chunter-cb

Copy link
Copy Markdown
Contributor

Summary

Implements ethereum/EIPs#12320 — applies the EIP-7623 calldata floor to EIP-8130 (transaction type 0x79) intrinsic gas.

Why this is needed

EIP-8130's tx_payload_cost prices the serialized transaction at a flat EIP-2028 16/4 per byte with no floor. Standard type-2 transactions get the EIP-7623 floor from revm, but the 8130 path bypasses revm entirely through Eip8130Executor, so the floor never binds on an 0x79 transaction. The practical effect: a data-heavy 8130 transaction can post data availability ~40% cheaper than an equivalent type-2 transaction on the same chain (measured on 20,000 zero bytes: type-2 pays the 221,000 EIP-7623 floor; a self-paid 0x79 pays ~129,000). That reopens the max-block-size hole EIP-7623 was created to close.

An 8130 transaction has no single data field — its bytes live in account_changes, sender_auth, calls, metadata, and payer_auth — so EIP-7623's tx.data-based floor can't reach it, and the L2 adoption profile makes the floor's structure normative (so it can't be fixed in chain config). It belongs in the intrinsic-gas path.

What changed

Ports EIP-7623's max(standard, floor) form over the serialized transaction:

sender_floor       = (sender_intrinsic - payload) + payload_floor
sender_metered_gas = max(sender_intrinsic + execution_gas_used, sender_floor)

payload_floor charges TX_TOTAL_COST_FLOOR_PER_TOKEN (10) per payload token instead of the standard 4, so computation-heavy transactions are unaffected and only data-heavy ones pay the floor.

  • schedule.rs — add TX_TOTAL_COST_FLOOR_PER_TOKEN = 10, pinned to revm's TOTAL_COST_FLOOR_PER_TOKEN by the gas_primitives_match_evm_reference drift tripwire.
  • intrinsic.rs — count payload tokens in one pass, add the payload_floor field and sender_floor() (>= sender_intrinsic).
  • eip8130.rs (executor) — thread sender_floor through Eip8130Outcome; reject gas_limit < sender_floor at inclusion; floor the settled charge in billable_gas (mirrors revm's eip7623_check_gas_floor, comparing against post-refund gas) and the gas estimate.
  • validator.rs — floor check at mempool admission.

payer_auth is metered outside gas_limit and its bytes are already part of payload, so the transaction-level floor covers that padding vector too. (The EIP draft flags the exact placement of a separate payer_auth floor as an open discussion point; this implementation follows the transaction-level option, consistent with how Base already meters payer_auth bytes in tx_payload_cost.)

Tests

  • New unit tests for the floor formula (payload_floor, sender_floor, growth with payload size) and for the settlement floor in billable_gas.
  • Existing warmth tests that carry little calldata are padded with JUMPDEST no-ops (deployed code, not tx bytes, so tokens are unchanged) so their cold/warm SLOAD delta isn't masked by the floor clamp.
  • Green: base-common-eip8130, base-common-evm, base-execution-eip8130, base-execution-txpool, base-execution-eip8130-rpc, base-builder-core.

Note: the upstream EIP PR is still a draft under discussion. Opening this so the implementation and the spec can iterate together.

EIP-8130 priced tx_payload_cost at a flat EIP-2028 16/4 per byte with no
floor, so a data-heavy 0x79 transaction could post data availability ~40%
cheaper than an equivalent type-2 transaction on the same chain: standard
txs get the EIP-7623 floor via revm, but the 8130 path bypasses revm
through Eip8130Executor. This reopens the max-block-size hole EIP-7623 was
created to close.

Port EIP-7623's max(standard, floor) form over the serialized 8130
transaction (its bytes live in account_changes, sender_auth, calls,
metadata, and payer_auth, so there is no single `data` field to floor):

  sender_floor       = (sender_intrinsic - payload) + payload_floor
  sender_metered_gas = max(sender_intrinsic + execution_gas_used, sender_floor)

payload_floor charges TX_TOTAL_COST_FLOOR_PER_TOKEN (10) per payload token
instead of the standard 4, so computation-heavy transactions are unaffected
and only data-heavy ones pay the floor. Enforced at mempool admission and
inclusion (gas_limit >= sender_floor), and applied to the settled charge
and to gas estimation. payer_auth is metered outside gas_limit and its
bytes are already part of payload, so the transaction-level floor covers
that padding vector too.

The 10-per-token rate is pinned to revm's TOTAL_COST_FLOOR_PER_TOKEN by the
gas_primitives_match_evm_reference drift tripwire. Warmth tests that carry
little calldata are padded with JUMPDEST no-ops so their cold/warm SLOAD
delta is not masked by the floor clamp.

Mirrors ethereum/EIPs#12320.
@cb-heimdall

Copy link
Copy Markdown
Collaborator

🟡 Heimdall Review Status

Requirement Status More Info
Reviews 🟡 0/1
Denominator calculation
Show calculation
1 if user is bot 0
1 if user is external 0
2 if repo is sensitive 0
From .codeflow.yml 1
Additional review requirements
Show calculation
Max 0
0
From CODEOWNERS 0
Global minimum 0
Max 1
1
1 if commit is unverified 0
Sum 1

@depot-code-access

depot-code-access Bot commented Sep 16, 2026

Copy link
Copy Markdown

✅ All benchmarks green — 14 within ±2% (deterministic instruction counts). View run

Benchmark details (14)
Benchmark Base (target) Head (this PR) Δ instructions
batch_queue/drain/drain_cached_span_batches 242,027 242,027 +0.0%
batch_transaction/encode_in_place/encode_in_place 4,199,759 4,199,759 +0.0%
batch_transaction/temporary_frame_buffers/temporary_frame_buffers 8,408,350 8,408,350 +0.0%
flashblock_decode/decode/brotli 3,296,484 3,296,484 +0.0%
flashblock_decode/decode/plain_json 2,280,194 2,280,194 +0.0%
flz/compress_len/real_contract_call 43,148 43,148 +0.0%
flz/compress_len/synthetic_0 38,205 38,205 +0.0%
flz/compress_len/synthetic_1 54,682 54,682 +0.0%
flz/compress_len/synthetic_2 147,976 147,976 +0.0%
flz/data_gas 43,059 43,059 +0.0%
flz/tx_estimated_size 43,056 43,056 +0.0%
frame_parse/decode/single_4kib 1,031 1,031 +0.0%
frame_parse/parse_frames/few_large 1,053,062 1,053,062 +0.0%
frame_parse/parse_frames/many_small 154,763 154,763 +0.0%

Comment thread crates/common/evm/src/eip8130.rs Outdated
Comment on lines 1815 to 1818
let execution_gas_available =
intrinsic.execution_gas_available(gas_limit).ok_or_else(|| {
BaseTransactionError::eip8130("EIP-8130 sender-intrinsic gas exceeds the gas limit")
})?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: after the gas_limit < sender_floor guard at line 1810, gas_limit >= sender_floor >= sender_intrinsic is guaranteed (since sender_floor >= sender_intrinsic is a documented invariant of the floor formula). That makes execution_gas_available(gas_limit) infallible here — the ok_or_else branch is dead code.

Not a bug, but worth noting: this silent dead path could mask a real invariant violation if the floor relationship ever broke. Consider either:

  • replacing with an unwrap / expect that documents the invariant ("sender_floor >= sender_intrinsic, checked above"), or
  • adding a debug_assert!(sender_floor >= intrinsic.sender_intrinsic()) before the floor check to make the contract explicit.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — after the floor guard, gas_limit >= sender_floor >= sender_intrinsic makes the execution_gas_available error branch unreachable.

Addressed in a10cb35: debug_assert!(sender_floor >= sender_intrinsic) plus expect documenting the same contract, so a broken floor relationship fails loudly instead of being masked by a dead error string.

@github-actions

Copy link
Copy Markdown
Contributor

Review Summary

PR: feat(eip8130): apply EIP-7623 calldata floor to intrinsic gas

This PR correctly ports the EIP-7623 calldata floor to the EIP-8130 transaction type, closing an underpricing vector where data-heavy 8130 transactions could post DA ~40% cheaper than equivalent type-2 transactions.

What I verified

  • Floor formula correctness: sender_floor = (sender_intrinsic - payload) + payload_floor is algebraically equivalent to replacing the standard 4/16 per-byte rate with the 10/40 per-token floor rate, matching EIP-7623 semantics. The payload_floor >= payload invariant holds by construction (10 > 4 per zero-byte token, 40 > 16 per non-zero-byte token).

  • billable_gas floor placement: The floor is applied to net_used (post-refund, sender-only portion), then payer_auth is added on top. This correctly mirrors revm's eip7623_check_gas_floor which compares the floor against post-refund gas used, and correctly excludes payer_auth from the floor (it's metered outside gas_limit).

  • Estimate-to-admission consistency: The estimate returns max(sender_intrinsic + feasible_pool, sender_floor) + payer_auth, which is always >= sender_floor. The validator checks gas_limit < sender_floor(). An estimate will never be rejected at admission.

  • Validator vs. executor consistency: payload and payload_floor are computed from the serialized bytes (independent of IntrinsicGasInput), and the validator uses worst-case inputs for other components, so sender_floor(worst_case) >= sender_floor(actual). A transaction passing the validator will always pass the executor.

  • Revert path: The simulate revert path applies the same floor before adding payer_auth, consistent with billable_gas.

  • Drift tripwire: TX_TOTAL_COST_FLOOR_PER_TOKEN is pinned to revm's TOTAL_COST_FLOOR_PER_TOKEN in the gas_primitives_match_evm_reference parity test.

  • Block production safety: The new sender_floor check is strictly tighter than the old sender_intrinsic check (it can only reject transactions that were previously undercharged, not ones that would have executed successfully). No new panic, fatal error, or stall path is introduced. The warmth test padding with JUMPDEST no-ops adds deployed code (not tx bytes), so payload tokens are unaffected.

Findings

One minor finding posted inline: the execution_gas_available error path in resolve_execution_gas is now dead code after the sender_floor check, since sender_floor >= sender_intrinsic guarantees the checked subtraction succeeds. Not a bug, but an opportunity to make the invariant explicit.

No critical or blocking findings.

@depot-code-access

depot-code-access Bot commented Sep 16, 2026

Copy link
Copy Markdown

Base Std historical fork tests

Fork Result Passed Failed Skipped base/base base-anvil base-std
Beryl pass 616 0 13 8b3a5ef6 98e7839c 520d069b
Cobalt pass 725 0 14 8b3a5ef6 98e7839c 4571b325

View run

`gas_limit >= sender_floor >= sender_intrinsic` is guaranteed past the
EIP-7623 floor guard, so `execution_gas_available` cannot fail there.
Replace the unreachable `ok_or_else` with a `debug_assert` on the floor
relationship and an `expect` that documents it.
@github-actions

Copy link
Copy Markdown
Contributor

Review Summary

This PR correctly ports EIP-7623's calldata floor to EIP-8130 transactions. The implementation is sound and the changes are consistent across all four surfaces: intrinsic gas computation, executor (execution + estimation), settlement, and mempool validation.

What I verified

  • Floor formula: sender_floor = (sender_intrinsic - payload) + payload_floor correctly mirrors EIP-7623's max(standard, floor) semantics. The invariant payload_floor >= payload holds because the per-token floor rate (10) exceeds the per-token standard rate (4 for zero bytes, but 10 vs 16/4 = 4 per token in both cases, yielding 2.5x). The debug_assert! and .expect() in resolve_execution_gas appropriately document this.
  • Settlement (billable_gas): The floor is applied post-refund (max(net_used, sender_floor)), matching revm's eip7623_check_gas_floor which compares against post-refund gas. payer_auth is correctly added on top, outside the floor.
  • Pre-charge safety: validate_balance charges (gas_limit + payer_auth) * max_fee. Since gas_limit >= sender_floor is enforced before balance validation, the pre-charge always covers the worst-case floor-clamped settlement. No undercharge is possible.
  • Estimation path: Both the revert and success paths correctly apply max(..., sender_floor) so returned estimates are never below the floor, preventing admission rejection.
  • Validator parity: The txpool validator applies the same gas_limit < sender_floor() check using worst_case inputs, consistent with the executor's estimation pipeline.
  • Warmth probe padding: The WARMTH_PROBE_PAD_GAS approach (deployed JUMPDEST no-ops, not tx bytes) correctly raises only execution gas without inflating tx_payload_cost, preserving the cold-vs-warm SLOAD delta the tests measure.
  • Drift tripwire: TX_TOTAL_COST_FLOOR_PER_TOKEN is pinned to revm's TOTAL_COST_FLOOR_PER_TOKEN in the gas_primitives_match_evm_reference parity test.

Block-production sensitivity

This PR touches the metering/gas path (a block-production-sensitive surface). I evaluated against the block production review guide:

  • No new panics in production paths: The .expect() in resolve_execution_gas is behind a gas_limit >= sender_floor guard and a debug_assert!, making it construction-enforced. In release builds the debug_assert! is elided, and the .expect() can only fire if the floor invariant (payload_floor >= payload) is violated — which would require a code change to the constants, caught by the parity test.
  • No new transaction exclusion risk: The floor raises the minimum gas_limit, but any transaction that previously cleared sender_intrinsic and carries little data will also clear sender_floor (the premium is 6 * tokens, negligible for low-data txs). Data-heavy transactions pay more, which is the intended behavior. No valid transaction is permanently excluded.
  • No I/O boundary changes: The PR doesn't change payload serialization, transport, or size limits.

No critical findings.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants