Conversation
There was a problem hiding this comment.
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
Open (6)
Correct CREATE pricing and value-transfer log behavior · New Absent authorities are not journaled for rollback · New Access-list entries use incorrect repriced gas constants · New Self-transfers incorrectly execute balance writes and emit logs · New Add Amsterdam coverage for authorization-list consensus paths · New Update cold storage cost and dependent gas expectations · New
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.
Codecov Report❌ Patch coverage is
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
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
95594a8 to
16325ec
Compare
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.
16325ec to
7f90f9e
Compare
There was a problem hiding this comment.
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
Open (8)
Authority writes use an outdated 9,000-gas cost · New Access-list costs are incompatible with EIP-8038 · New Absent authorities are not journaled for rollback Correct CREATE pricing and value-transfer log behavior Self-transfers incorrectly execute balance writes and emit logs Access-list entries use incorrect repriced gas constants Update cold storage cost and dependent gas expectations Add Amsterdam coverage for authorization-list consensus paths
…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.
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.
160fa9a to
a2ad824
Compare
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.
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Create, authorization-write, and access-list gas prices diverge from the targeted EIP-2780 schedule.
Review effort: Balanced
Findings: 2
Open (2)
Resolved since last review (8)
Access-list costs are incompatible with EIP-8038 Authority writes use an outdated 9,000-gas cost Absent authorities are not journaled for rollback Correct CREATE pricing and value-transfer log behavior Self-transfers incorrectly execute balance writes and emit logs Access-list entries use incorrect repriced gas constants Update cold storage cost and dependent gas expectations Add Amsterdam coverage for authorization-list consensus paths
| if (rev < EVMC_AMSTERDAM) | ||
| return 0; | ||
| if (is_create) | ||
| return instr::CREATE_ACCESS; |
| 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; |



Replace the flat 21000 base with the EIP-2780 resource-based decomposition (EELS #3126), pricing each intrinsic component by the EIP-8038 access costs:
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.