Skip to content

Implement EIP-2780: "Resource-based intrinsic transaction gas" - #1733

Open
chfast wants to merge 23 commits into
masterfrom
amsterdam/2780
Open

chfast wants to merge 23 commits into
masterfrom
amsterdam/2780

Conversation

@chfast

@chfast chfast commented Sep 22, 2026

Copy link
Copy Markdown
Member

Replace the flat 21000 base with the EIP-2780 resource-based decomposition (EELS #3126), pricing each intrinsic component by the EIP-8038 access costs:

  • TX_BASE 12000: ECDSA recovery, the sender's account access and write, and inclusion in the block.
  • Recipient: COLD_ACCOUNT_ACCESS (3000) for a call, CREATE_ACCESS (11000) plus the init-code word cost for a create; a value transfer adds TRANSFER_LOG_COST (1756, EIP-7708) and TX_VALUE_COST (4244) for a call. A self-transfer touches nothing. The created account's NEW_ACCOUNT state gas is state-dependent and charged at the top frame (EIP-8037), not here.
  • Access list entries at the repriced 3000/3000 plus their calldata-floor tokens (EIP-7981: 80/128 tokens × 16).
  • Authorizations: only the state-independent REGULAR_PER_AUTH_BASE_COST (7816) per tuple. Their state-dependent costs are charged at the top frame when the authorizations are applied: NEW_ACCOUNT state gas when the authority's account leaf does not yet exist; ACCOUNT_WRITE regular gas on the transaction's first write to the authority's leaf, tracked with a written-accounts set seeded from the sender and the value recipient — so a self-sponsored authority pays no ACCOUNT_WRITE and repeated authorizations pay it once; AUTH_BASE state gas for a net-new delegation indicator, at most once per authority and never credited back.

Insufficient gas at a top-frame charge halts the frame: all gas is consumed and the applied delegations are rolled back via a journal checkpoint — applying a delegation now journals the code change (new JournalCodeChange). The delegated-recipient code read at dispatch costs WARM_ACCESS or COLD_ACCOUNT_ACCESS by the target's warmth, and a value transfer materializing a new recipient pre-checks its NEW_ACCOUNT affordability so the halt still rolls the authorizations back. Authorization validation moves into a helper shared by the pre-Amsterdam path, which keeps the flat refund model.

The EIP-7623/7976 calldata floor counts every calldata byte uniformly plus the access-list tokens. The pre-Amsterdam formula is unchanged; the Amsterdam special cases in it are subsumed by the decomposition.

Anchors the calldata floor on the decomposed intrinsic base (execution-specs returns the state-gas reservoir whole when a top-frame charge halts before dispatch: the preparation snapshot rolls every applied delegation back, so the halt consumes at most the regular budget.

Brings the EIP-7702 per-authorization state charges (AUTH_BASE and the authority's NEW_ACCOUNT) with it, since they are only expressible through the top-frame charging model introduced here.

@codspeed

codspeed Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 899 untouched benchmarks


Comparing amsterdam/2780 (45d35e1) with master (6aebdeb)

Open in CodSpeed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Several Amsterdam gas constants and transfer cases are incorrect, and authorization rollback lacks required coverage.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 2 High severity · 2 Medium severity · 2 Low severity

Open (6)
What changed in this PR

Implements EIP-2780 resource-based transaction gas accounting for Amsterdam.

Changes:

  • Decomposes intrinsic gas and calldata-floor calculations.
  • Adds top-frame authorization charging and rollback.
  • Updates gas-accounting tests and fixtures.
File Description
lib/​evmone/​constants.hpp Adds authorization state-gas cost.
test/​state/​account.hpp Adds account-aliveness helper.
test/​state/​state.hpp Adds code-change journal entries.
test/​state/​state.cpp Implements intrinsic and top-frame charging.
test/​unittests/​state_transition_tx_test.cpp Updates intrinsic/floor tests.
test/​unittests/​state_transition_eip8037_state_gas_test.cpp Rebaselines state-gas tests.
test/​unittests/​state_transition_eip7778_block_gas_test.cpp Updates block-gas expectations.
test/​unittests/​tooling_t8n_test.cpp Updates refund expectation.
test/​integration/​evmone-cli/​test/​blockchaintest/​eip7778_block_gas.json Regenerates blockchain fixture.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread test/state/state.cpp Outdated
Comment thread test/state/state.cpp Outdated
Comment thread test/state/state.cpp Outdated
Comment thread test/state/state.cpp Outdated
Comment thread test/state/state.cpp Outdated
Comment thread test/unittests/state_transition_eip7778_block_gas_test.cpp Outdated
@codecov

codecov Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.97959% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 98.07%. Comparing base (6aebdeb) to head (45d35e1).

Files with missing lines Patch % Lines
test/state/state.cpp 98.76% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1733      +/-   ##
==========================================
+ Coverage   97.98%   98.07%   +0.08%     
==========================================
  Files         183      183              
  Lines       16857    16917      +60     
  Branches     3856     3877      +21     
==========================================
+ Hits        16517    16591      +74     
+ Misses        250      244       -6     
+ Partials       90       82       -8     
Flag Coverage Δ
eest-develop 83.89% <98.78%> (+2.11%) ⬆️
eest-develop-gmp 25.86% <31.63%> (-0.03%) ⬇️
eest-legacy 17.12% <24.48%> (+0.01%) ⬆️
eest-libsecp256k1 28.04% <31.63%> (-0.03%) ⬇️
eest-stable 83.90% <98.78%> (+2.13%) ⬆️
evmone-unittests 94.23% <64.28%> (-0.19%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
core 96.26% <98.78%> (+0.31%) ⬆️
tooling 94.29% <ø> (ø)
tests 99.81% <100.00%> (ø)
Files with missing lines Coverage Δ
lib/evmone/state_gas.hpp 100.00% <100.00%> (ø)
...ttests/state_transition_eip7778_block_gas_test.cpp 100.00% <100.00%> (ø)
...ttests/state_transition_eip8037_state_gas_test.cpp 100.00% <100.00%> (ø)
test/unittests/state_transition_tx_test.cpp 100.00% <100.00%> (ø)
test/state/state.cpp 99.07% <98.76%> (+0.41%) ⬆️

... and 4 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

chfast added a commit that referenced this pull request Sep 24, 2026
Move the processing of the transaction's top-level message out of
`transition()` into `process_top_level()`. It applies the EIP-7702
authorizations (their refund now travels in the call result's gas
refund), resolves the recipient's delegation, charges the NEW_ACCOUNT
state-gas of a recipient the call creates, and makes the call, refilling
that state-gas if the call fails.

The NEW_ACCOUNT charge is a pre-execution charge (EIP-8037), so it moves
here from `Host::call()`, which no longer handles the transaction's
state-gas. This prepares EIP-2780 (#1733): its additional pre-execution
charges must revert the applied authorizations when they run out of gas,
which `Host::call()` cannot do.

No behavior change.
@chfast
chfast requested a balanced review from Copilot September 24, 2026 12:27

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Several intrinsic costs and self-transfer behavior diverge from the referenced consensus specification.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 4 High severity · 2 Medium severity · 2 Low severity

Open (8)

Comment thread test/state/state.cpp Outdated
Comment thread test/unittests/state_transition_tx_test.cpp
chfast added a commit that referenced this pull request Sep 28, 2026
…1742)

State the EIP-3529 cap as a fifth of the pre-refund gas instead of the
precomputed numbers, so the expectations follow changes of the intrinsic
gas.

Peeled from #1733.
chfast added a commit that referenced this pull request Sep 28, 2026
…1740)

Follow the execution-specs order, where the recipient's NEW_ACCOUNT
state-gas is charged before the delegation target of tx.to is resolved.
The order is not observable: only a nonexistent recipient is charged and
a delegated recipient has code, so it exists. The new assert pins the
latter.

Peeled from #1733.
chfast added a commit that referenced this pull request Sep 28, 2026
It takes the chain id and the authorization list from the transaction.
EIP-2780 needs its sender, recipient and value there too.

Peeled from #1733.
Decompose the flat 21000 transaction base cost into the resources the
transaction uses. https://eips.ethereum.org/EIPS/eip-2780

- The intrinsic gas is TX_BASE_COST (12000) plus COLD_ACCOUNT_ACCESS for
  a call to another account and TX_VALUE_COST (6000) if it transfers
  value, or CREATE_ACCESS for a create, plus 7816 per authorization.
  The calldata floor is anchored on the same base.
- The state-dependent costs are charged at the top frame, before the
  execution: per valid authorization, NEW_ACCOUNT for a new authority,
  ACCOUNT_WRITE for the first write to it and AUTH_BASE for a net-new
  delegation indicator, replacing the EIP-7702 refund; then the access
  to the delegation target of tx.to and the recipient's NEW_ACCOUNT.
- Running out of gas there halts the transaction: the execution-gas is
  consumed, the state-gas is returned and the applied authorizations
  are reverted. Nothing else changed the state by then, so reverting
  the nonce bumps and code changes of the recorded authorities is
  enough.
With EIP-2780 the Amsterdam state tests pass, so stop ignoring them.
The stable tests skip only the EIP-8037 transaction gas limit cap test,
not implemented yet. The blockchain tests still need EIP-7928 and
EIP-8282, so add the blockchain_ignore parameter to keep ignoring the
Amsterdam fixtures there.
Select them per revision like the access list costs instead of
overwriting them in the Amsterdam branch.
process_authorization_list() yields the refund before Amsterdam and can
only run out of gas from Amsterdam, so return the refund or nullopt
instead of a bool and an output parameter.
Keep the first-write condition free of side effects and charge the gas
in the body.
Snapshot the StateGas after the authorizations as the baseline the
failed call restores, instead of tracking their spill separately.
All three pre-execution halts pass the same arguments, including the
initial state-gas which the lambda captures before the message changes.
Keep the authority account pointer and its previous code hash instead
of the address, so the halt reverts the authorizations without reading
the initial state. The list also tells the first write to an authority,
as in execution-specs, instead of comparing its nonce with the initial
one. process_top_level() no longer needs the StateView.
Put the EIP-2780 charges first and continue with the EIP-7702 refund as
the else branch, so the refund code is unchanged.
The ACCOUNT_WRITE and delegation target charges may leave the gas
negative. Nothing adds gas before the call and a state-gas charge fails
on negative gas unless the reservoir covers it, so the gas stays
negative and a single check before the call halts. The authorizations
applied meanwhile are reverted with the others.
The sanitizers job is the only one with assertions enabled, so run the
Amsterdam state tests there too, ignoring only the transaction gas limit
cap test like the other stable tests.
Compute the Amsterdam recipient cost, the account creation or the access
and the value transfer to another account, as one const added to the
base (EIP-2780).

Spell EXECUTION_PER_AUTH_BASE_COST as the execution-specs formula and
define AUTH_BASE_STATE_GAS where it is used: the state-gas of the
delegation indicator, not a replacement of the EIP-7702 base cost.
Record each authority once, before its first applied authorization,
with its nonce and code hash, so the halt restores them in any order.
The same record tells the first write to the authority.

Charge the recipient in a lambda and handle running out of gas before
the call in one place instead of the halt helper. Check the negative
gas after the charges leaving it: the ACCOUNT_WRITE charges after the
authorization list and the delegation target access right after it.
Name the state-gas left after the authorizations committed_state_gas,
correct the comments of the halt and EXECUTION_PER_AUTH_BASE_COST (defined
in EIP-8037) and leave a TODO about the exclusive recipient charges.
Sum the state-gas of the authorizations and charge it once after the
list, together with the check of the execution-gas. The gas never grows
in the loop, so this fails exactly when charging each cost in turn
would, and the loop has no failure exits.
Record each applied authorization instead of each authority once. Every
one bumped the nonce, so the halt decrements it per record. Only the
records made before an authorization changes the code keep the code
hash, which is still the one from the transaction start, so the halt
restores it in any order.

Count each authorization's execution-gas and state-gas up and charge
them together before recording it.
The halt is reachable only from Amsterdam, where the EIP-7702 refund is
always 0, and it must leave no reverted authority with changed code,
otherwise the state diff would still write the delegation.
The charge takes the cost from the reservoir without checking the gas,
so a negative gas would pass unnoticed whenever the reservoir covers the
cost.
The net-new delegation check and the initial code hash of the record
read code_changed as changed by an earlier authorization of this
transaction.
The halt decrements the nonce once per record, so keep the record and
the nonce bump together. The code change and the nonce bump are
independent.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Comment thread test/state/state.cpp
if (rev < EVMC_AMSTERDAM)
return 0;
if (is_create)
return instr::CREATE_ACCESS;
Comment thread test/state/state.cpp
if (authority_addr != tx.sender && (tx.value == 0 || tx.to != authority_addr) &&
std::ranges::find(applied, &authority, &AppliedAuthorization::authority) ==
applied.end())
execution_gas_cost += instr::ACCOUNT_WRITE;

This branch has not been deployed

No deployments
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