Update EIP-2780: clarify 2780 - #11891
Conversation
|
✅ All reviewers have approved. |
|
|
||
| Once the transaction passes the intrinsic gas check, the state-dependent charges are applied at runtime — as the first frame is entered — drawing from the same gas the transaction supplies. Like every charge in this EIP, each runtime charge is split into regular gas and state gas per [EIP-8037](./eip-8037.md): account accesses and writes are regular gas, while durable state growth (new account leaves and net-new delegation bytes) is state gas. This mirrors how a `CALL` frame is metered: accesses, account creation, and writes are charged as state is touched, not up front. Because these charges require state access, they are evaluated only after [EIP-7702](./eip-7702.md) authorizations are applied. | ||
|
|
||
| Before any runtime charge is applied, `accessed_addresses` is initialized at transaction start under the pre-existing rules, unchanged: the sender, `tx.to`(if not `None`), the precompiles ([EIP-2929](./eip-2929.md)), the access-list entries ([EIP-2930](./eip-2930.md)), and the coinbase ([EIP-3651](./eip-3651.md)). |
There was a problem hiding this comment.
Imo, this does not need clarification as we dont change EIP-2929/EIP-3651 behaviour.
@misilva73 if you think this is worth adding, i would move it later in EIP not in Abstract
There was a problem hiding this comment.
I am fine to remove this clarification, but i would prefer to remove the and added to accessed_addresses in runtime gas charging. It confuses me a lot as the to is already pre-warmed with pre-amsterdam rules.
There was a problem hiding this comment.
I tend to agree. I would not put it in the abstract. Happy to move it to the spec sections if you think it helps with clarity
|
|
||
| - if the authority account does not already exist, charge `STATE_BYTES_PER_NEW_ACCOUNT × CPSB` in state gas for the new account leaf; | ||
| - if this is the first write to the authority within the transaction (i.e. the authority differs from `tx.to`), charge `ACCOUNT_WRITE` in regular gas; | ||
| - if this is the first write to the authority within the transaction, charge `ACCOUNT_WRITE` in regular gas; the charge is skipped when the authority's write is already paid for: as `tx.sender` (by `TX_BASE_COST`), or as `tx.to` of a value-bearing transaction (by `TX_VALUE_COST`); |
There was a problem hiding this comment.
So this is a custom rule, this feels unnecessary, and it clashes with other parts.
tx.sender nonce and balance should already be changed before applying the auth item, so first if this is the first write to the authority stands.
tx.to check for ACCOUNT_WRITE is charged after auth list, so when applying delegation, it is charged, but when doing tx.to check account would exist, so no ACCOUNT_WRITE is going to charged.
There was a problem hiding this comment.
tx.sender nonce and balance should already be changed before applying the auth item, so first if this is the first write to the authority stands.
Yes, true. Maybe I can change the words in this way:
if this is the first write to the authority within the transaction, charge ACCOUNT_WRITE in regular gas. The first write actually covers the scenarios I mentioned.
There was a problem hiding this comment.
tx.to check for ACCOUNT_WRITE is charged after auth list, so when applying delegation, it is charged, but when doing tx.to check account would exist, so no ACCOUNT_WRITE is going to charged.
Sorry I don't understand this reply.
What I meant is: if authority == tx.to, and if we haven't charged ACCOUNT_WRITE for tx.to (e.g, value == 0, in this case, we only charge Cold access in IntrinsicGas), then we need to charge the ACCOUNT_WRITE in auth processing.
But true, as you said, the first write to the authority within the transaction also covers this scenario.
I am wondering if these clarifications in detail will be useful for implementors. If you guys think it's not, i can also remove it
There was a problem hiding this comment.
It is not clarifications that you are proposing here, it is custom rules.
if we skip tx.to charge in eip7702 but change the account, later in path we need to check if tx.to exists and charge ACCOUNT_WRITE, in this example tx.to would exist but we would have skipped eip7702 charge. So it is not correct
There was a problem hiding this comment.
Maybe my words is confusing, what I am trying to specify is:
In these cases, the authority has already been charged and the Account-write shouldn't be applied one more time:
- authority == tx.sender
- authority has been written by preceding valid authorizations
- authority == tx.to and tx.value != zero
It's all the cases I can imagine that Account-write has been applied for this authority and so i express these words as "clarifications"
There was a problem hiding this comment.
Anyway, what do you think I change the sentence as below?
- if this is the first write to the authority within the transaction, charge ACCOUNT_WRITE in regular gas;
i.e. the authority differs from tx.to this part is not correct and confusing. Differs from tx.to doesn't mean the authority is modified for the first time.
There was a problem hiding this comment.
I think we should make this clearer. I think it would help to explicitly add int he EIP the three cases @rjl493456442 listed, i.e.:
- authority == tx.sender
- authority has been written by preceding valid authorizations
- authority == tx.to and tx.value != zero
| - **[EIP-2930](./eip-2930.md) (access lists).** Access lists keep their existing per-entry charges and warming semantics for execution-level touches. Listing the recipient or an authority does not reduce the intrinsic cold-rate charges; it only pre-warms the address for execution. | ||
| - **[EIP-7623](./eip-7623.md) (calldata floor).** EIP-7623 floors a transaction's cost at `21,000 + TOTAL_COST_FLOOR_PER_TOKEN × tokens_in_calldata`, where the flat `21,000` is the intrinsic base this EIP decomposes. With this EIP active, that base term is replaced by the transaction's decomposed regular-gas intrinsic — the sum of the regular-gas intrinsic primitives (`TX_BASE_COST + COLD_ACCOUNT_ACCESS + TX_VALUE_COST + TRANSFER_LOG_COST` for value transfers, `TX_BASE_COST + CREATE_ACCESS` for contract creation) — so the floor and its validity check rest on the same per-transaction base as the rest of intrinsic gas rather than a stale constant. Like the floor, this base is state-independent, so the state-dependent runtime charges (the new-account state-gas charge and the delegation-target access) do not enter it. The calldata schedule itself is unchanged; only the base the floor sits on moves. | ||
| - **[EIP-7702](./eip-7702.md) (set EOA code).** `TX_BASE_COST` is unchanged even when the sender temporarily assumes code; clients must not perform a disk code load to classify the sender, since EIP-7702 provides the code inline. When `tx.to` is a delegated account, resolving the delegation loads the target's code, charged `COLD_ACCOUNT_ACCESS` on first touch or warm thereafter, following the standard EIP-2929 `accessed_addresses` model. Setting a delegation (writing the 23-byte pointer) is distinct from resolving one (reading the target's code); the resolution charge is genuine work and is not prepaid by the authorization. Authorization cost itself is decomposed: `REGULAR_PER_AUTH_BASE_COST` — which already includes the authority's cold access — is charged per authorization at the intrinsic phase, while the state-dependent charges (`STATE_BYTES_PER_NEW_ACCOUNT × CPSB` plus `ACCOUNT_WRITE` for a non-existent authority, and `STATE_BYTES_PER_AUTH_BASE × CPSB` for net-new delegation bytes) are charged at runtime during authorization processing, replacing the worst-case `PER_EMPTY_ACCOUNT_COST` intrinsic charge and refund. | ||
| - **[EIP-7702](./eip-7702.md) (set EOA code).** `TX_BASE_COST` is unchanged even when the sender temporarily assumes code; clients must not perform a disk code load to classify the sender, since EIP-7702 provides the code inline. When `tx.to` is a delegated account, resolving the delegation loads the target's code, charged `COLD_ACCOUNT_ACCESS` on first touch or warm thereafter, following the standard EIP-2929 `accessed_addresses` model. Setting a delegation (writing the 23-byte pointer) is distinct from resolving one (reading the target's code); the resolution charge is genuine work and is not prepaid by the authorization. Authorization cost itself is decomposed: `REGULAR_PER_AUTH_BASE_COST` — which already includes the authority's cold access — is charged per authorization at the intrinsic phase, while the state-dependent charges (`STATE_BYTES_PER_NEW_ACCOUNT × CPSB` for a non-existent authority, `ACCOUNT_WRITE` for the first write to the authority within the transaction, and `STATE_BYTES_PER_AUTH_BASE × CPSB` for net-new delegation bytes) are charged at runtime during authorization processing, replacing the worst-case `PER_EMPTY_ACCOUNT_COST` intrinsic charge and refund. |
There was a problem hiding this comment.
github is not great at showing this diff. The change is:
STATE_BYTES_PER_NEW_ACCOUNT × CPSB plus ACCOUNT_WRITE for a non-existent authority, and STATE_BYTES_PER_AUTH_BASE × CPSB for net-new delegation bytes
to
STATE_BYTES_PER_NEW_ACCOUNT × CPSB for a non-existent authority, ACCOUNT_WRITE for the first write to the authority within the transaction, and STATE_BYTES_PER_AUTH_BASE × CPSB for net-new delegation bytes
There was a problem hiding this comment.
Exactly, the real diff is to decouple the account-creation (STATE_BYTES_PER_NEW_ACCOUNT × CPSB ) with ACCOUNT_WRITE, one stands for state growth, one stands for database write, making no sense to pair them
Co-authored-by: rakita <rakita@users.noreply.github.com>
misilva73
left a comment
There was a problem hiding this comment.
Two small things I think we should change. Besides that, all good.
|
|
||
| Once the transaction passes the intrinsic gas check, the state-dependent charges are applied at runtime — as the first frame is entered — drawing from the same gas the transaction supplies. Like every charge in this EIP, each runtime charge is split into regular gas and state gas per [EIP-8037](./eip-8037.md): account accesses and writes are regular gas, while durable state growth (new account leaves and net-new delegation bytes) is state gas. This mirrors how a `CALL` frame is metered: accesses, account creation, and writes are charged as state is touched, not up front. Because these charges require state access, they are evaluated only after [EIP-7702](./eip-7702.md) authorizations are applied. | ||
|
|
||
| Before any runtime charge is applied, `accessed_addresses` is initialized at transaction start under the pre-existing rules, unchanged: the sender, `tx.to`(if not `None`), the precompiles ([EIP-2929](./eip-2929.md)), the access-list entries ([EIP-2930](./eip-2930.md)), and the coinbase ([EIP-3651](./eip-3651.md)). |
There was a problem hiding this comment.
I tend to agree. I would not put it in the abstract. Happy to move it to the spec sections if you think it helps with clarity
|
|
||
| - if the authority account does not already exist, charge `STATE_BYTES_PER_NEW_ACCOUNT × CPSB` in state gas for the new account leaf; | ||
| - if this is the first write to the authority within the transaction (i.e. the authority differs from `tx.to`), charge `ACCOUNT_WRITE` in regular gas; | ||
| - if this is the first write to the authority within the transaction, charge `ACCOUNT_WRITE` in regular gas; the charge is skipped when the authority's write is already paid for: as `tx.sender` (by `TX_BASE_COST`), or as `tx.to` of a value-bearing transaction (by `TX_VALUE_COST`); |
There was a problem hiding this comment.
I think we should make this clearer. I think it would help to explicitly add int he EIP the three cases @rjl493456442 listed, i.e.:
- authority == tx.sender
- authority has been written by preceding valid authorizations
- authority == tx.to and tx.value != zero
| - if this is the first write to the authority within the transaction, charge `ACCOUNT_WRITE` in regular gas; the charge is skipped when the write is already paid for, namely when the authority: | ||
| - is `tx.sender`, covered by `TX_BASE_COST`; | ||
| - was written by a preceding valid authorization; | ||
| - is `tx.to` of a value-bearing transaction, covered by `TX_VALUE_COST`; |
There was a problem hiding this comment.
TX_VALUE_COST is 4244 and the ACCOUNT_WRITE is 8000. So I'm not sure tx.to is covered. Might be worth looking into.
There was a problem hiding this comment.
https://hackmd.io/@bFEBbZiVSAO0IURh9qzEFg/
Yes, but I think it's a technical debt, maybe @misilva73 has some insights. At least in the EIP description, it says TX_VALUE_COST is for recipient balance update and value transfer
There was a problem hiding this comment.
TX_VALUE_COST was derived in two steps:
- First, we take the diff between the cost of a transfer without value (15k) and a transfer with value (21k). This is based on benchmarks. Note that Erigon is still working on their optimizations, so I am taking the 2nd worst client for now.
- To this diff, we remove the log cost - this cost covers mostly history growth, so the execution cost is orthogonal.
| `REGULAR_PER_AUTH_BASE_COST` already prices the authority access at the cold rate at the intrinsic phase, so no account-access charge is added at runtime for the authority; the authority is added to `accessed_addresses`, per [EIP-7702](./eip-7702.md). | ||
|
|
||
| Then the recipient account is loaded and added to `accessed_addresses` — its access was already charged at the cold rate at the intrinsic phase — and unless the transaction is a self-transfer or a contract creation: | ||
| Then the recipient account is added to the BAL — its access was already charged at the cold rate at the intrinsic phase, and it is warm from transaction start — and unless the transaction is a self-transfer or a contract creation: |
There was a problem hiding this comment.
This slightly contradicts EIP-7928 as it currently stands. It says
"Transaction sender and recipient addresses (even for zero-value transfers)" MUST be included in the BAL.
If we include the recipient at this stage, the BAL for a transaction which OOGs at the auth processing will not have the recipient (in contradiction to EIP-7928).
There was a problem hiding this comment.
This was true for EIP-7928 where it can't halt in runtime. This EIP changes this behaviour and it makes sense that is changes BAL.
| `REGULAR_PER_AUTH_BASE_COST` already prices the authority access at the cold rate at the intrinsic phase, so no account-access charge is added at runtime for the authority; the authority is added to `accessed_addresses`, per [EIP-7702](./eip-7702.md). | ||
|
|
||
| Then the recipient account is loaded and added to `accessed_addresses` — its access was already charged at the cold rate at the intrinsic phase — and unless the transaction is a self-transfer or a contract creation: | ||
| Then the recipient account is added to the BAL — its access was already charged at the cold rate at the intrinsic phase, and it is warm from transaction start — and unless the transaction is a self-transfer or a contract creation: |
There was a problem hiding this comment.
Also, the not self-transfer gate should not be applied here. Self-transfer to an account which already has a 7702 delegation should be charged for accessing the delegation. This is already in the table as "ETH transfer to self, sender 7702 delegated". We must make this text consistent with the table
There was a problem hiding this comment.
Sorry Guru, I don't get your point here.
For self-transfer, we need to charge the delegation-target access if it has delegation, and it's explicitly specified below.
I don't get why it's relevant with not self-transfer gate. Can you elaborate it?
There was a problem hiding this comment.
It may just be an issue with how I am reading it. The way it is currently structured, it seems to say
Then the recipient account is added to the BAL and unless the transaction is a self-transfer or a contract creation:
- if the recipient is non-existent and tx.value > 0, charge STATE_BYTES_PER_NEW_ACCOUNT × CPSB in state gas;
- if the recipient is an [EIP-7702](https://github.com/rjl493456442/EIPs/blob/2780-clarify/EIPS/eip-7702.md) delegated account, additionally charge the delegation-target access in regular gas — COLD_ACCOUNT_ACCESS if cold, WARM_ACCESS if warm.
Which makes me think the 2 charges should not apply if it is a self transfer. In any case, this is very minor
|
@eth-bot rerun |
eth-bot
left a comment
There was a problem hiding this comment.
All Reviewers Have Approved; Performing Automatic Merge...
Implement the spec changes of EIP-2780 and EIP-8037. See the spec diffs in - ethereum/EIPs#11844 - ethereum/EIPs#11891 - ethereum/EIPs#11906 - ethereum/EIPs@a4801f3 --------- Co-authored-by: MariusVanDerWijden <m.vanderwijden@live.de>
) Aligns Amsterdam to the **glamsterdam devnet-7** spec and the `tests-glamsterdam-devnet@v7.2.0` fixtures. All Amsterdam `state_tests` and `blockchain_tests` pass (interpreter + JIT + AOT); no regressions in earlier-fork state suites or the unit/ee-test suite. Ports bluealloy/revm#3795 to evm2's architecture. ## EIP-2780 runtime gas phase (ethereum/EIPs#11844) State-dependent transaction charges move from the intrinsic phase to a runtime phase applied as the first frame is entered. Running out of gas there is an **included out-of-gas halt** consuming all regular gas, not a transaction rejection. - **Create transactions**: account-creation state gas is charged at the create frame's entry (`execute_create_message`), conditional on the destination not already existing. The intrinsic `create_state_gas` and its refund path are removed. - **EIP-7702**: the intrinsic per-auth charge drops to the state-independent `REGULAR_PER_AUTH_BASE_COST` (7,816); `ACCOUNT_WRITE`, new-account, and delegation-bytes state gas are charged per authority at runtime on a transaction-level `GasTracker` (the `RuntimeAuthCharges` accounting), stopping at the first unaffordable charge. A runtime out-of-gas reverts the applied delegations and includes the tx as an OOG halt. - **Delegated recipient (depth 0)**: resolution is deferred into the frame (`apply_eip2780_call_charges`) and the target load is gated on gas (`skip_cold_load`, as nested calls do), so a cold, unafforded target stays out of the EIP-7928 block access list. `MessageResult::runtime_gas_oog` signals a recipient-charge OOG back to the 7702 handler so it drops the delegations too. The CALL/CREATE runtime charges live in the shared `execute_call_message` / `execute_create_message` (gated on `depth == 0` + feature, not tx type), so **all transaction types** get them; only the authorization-specific charges are in the 7702 handler. ## Devnet-7 spec alignment - **ethereum/EIPs#11836**: calldata floor anchored on the decomposed intrinsic base instead of flat `TX_BASE`. - **ethereum/EIPs#11891**: 7702 `ACCOUNT_WRITE` charged once per authority, skipped when already paid (sender / value-recipient / a preceding valid authorization). - **ethereum/EIPs#11858**: `CREATE`/`CREATE2` account-creation state gas charged conditionally at access (endowment/nonce pre-check + a single destination read), refilled on failure. Mirrored in the JIT builtin. - **ethereum/EIPs#11854**: `SSTORE` covers the slot's access cost before the implicit read; an unaffordable cold access skips the read (`Evm::sstore` now honors `skip_cold_load`). - **EIP-8282**: builder deposit/exit system-contract addresses updated. - **EIP-7623** floor binds the block regular-gas component (`TxResult::regular_gas_spent = max(total - state, floor)`). ## Cleanups - Remove the superseded devnet-6 create accounting (`create_initial_state_gas`, `refund_create_state_gas`, `MessageResult::created_target_was_alive`, `settle_gas`'s `is_create` path). - Remove `rollback_failed_execution`: failed execution is already rolled back to the message's own checkpoint (and halt gas zeroed) inside `execute_message`, so it was redundant. ## Follow-up refactors - **Message construction**: `Message` now carries its `destination` — the CREATE/CREATE2 address is derived when the message is constructed (`Message::derive_destination`) instead of being re-derived at frame entry. - **EIP-7702 authorization unification**: both gas regimes share one `apply_auth_list` loop (validate → account → apply) driven by an `AuthAccounting` strategy — `RuntimeAuthCharges` (EIP-2780 runtime metering, abortable mid-list) and `AuthRefunds` (pre-Amsterdam pessimistic-intrinsic refunds, owning the EIP-8037 gating). The handler tail funnels every exit through a single `settle` closure (`settle_oog` for the runtime OOG halts), and `initial_gas_and_reservoir` loses its always-zero `state_refund` parameter — the pre-Amsterdam state refund is credited straight to the reservoir at the call site, matching execution-specs `set_delegation`. Crate constants are now `pub`. The `Message`-carries-bytecode refactor (with the EIP-8037 create depth check) and the regular→execution gas rename are split out into the stacked PR #329. ## Fixtures / harness - Bump devnet fixtures to v7.2.0 (setup script, CI, `AGENTS.md`). - eest: honor an explicit per-transaction `chainId` (v7 fixtures test type-0 `INVALID_CHAINID`). ## Testing - Amsterdam `state_tests`: 406/406; `blockchain_tests`: 657/657. - JIT + AOT Amsterdam state suites pass on v7.2.0. - Unit + ee-tests: 2829/2829; Prague/Osaka/Cancun/Berlin/London/Shanghai state suites unchanged. Two revm changes were no-ops in evm2: EIP-7708 (already implemented) and `JournalEntry::CodeChange` recording prior code (evm2's `AccountChange` already snapshots the full prior `AccountInfo`).
Implement the spec changes of EIP-2780 and EIP-8037. See the spec diffs in - ethereum/EIPs#11844 - ethereum/EIPs#11891 - ethereum/EIPs#11906 - ethereum/EIPs@a4801f3 --------- Co-authored-by: MariusVanDerWijden <m.vanderwijden@live.de>
Implement the spec changes of EIP-2780 and EIP-8037. See the spec diffs in - ethereum/EIPs#11844 - ethereum/EIPs#11891 - ethereum/EIPs#11906 - ethereum/EIPs@a4801f3 --------- Co-authored-by: MariusVanDerWijden <m.vanderwijden@live.de>
Implement the spec changes of EIP-2780 and EIP-8037. See the spec diffs in - ethereum/EIPs#11844 - ethereum/EIPs#11891 - ethereum/EIPs#11906 - ethereum/EIPs@a4801f3 --------- Co-authored-by: MariusVanDerWijden <m.vanderwijden@live.de>
Implement the spec changes of EIP-2780 and EIP-8037. See the spec diffs in - ethereum/EIPs#11844 - ethereum/EIPs#11891 - ethereum/EIPs#11906 - ethereum/EIPs@a4801f3 --------- Co-authored-by: MariusVanDerWijden <m.vanderwijden@live.de>
This PR clarifies a few things: