refactor(amsterdam): Re-factor Amsterdam + Preparation for EIP-8141#3192
refactor(amsterdam): Re-factor Amsterdam + Preparation for EIP-8141#3192gurukamath wants to merge 8 commits into
Conversation
Rename `prepare_dispatch` to `resolve_dispatch`: it resolves the top frame's code and charges the state-dependent dispatch costs, and the name frees "prepare" for a later `prepare_message_call` split. Replace the `set_delegation` match/case on an optional with a plain `if authority is None: continue`. Groundwork for merging Message into the Evm call frame; no behavior change (Amsterdam fixtures fill byte-identically).
…ransaction `check_transaction` now assembles and returns the `TransactionEnvironment` instead of an ad-hoc tuple, consuming the `IntrinsicGasCost` from `validate_transaction` and calling `allocate_execution_gas` for the regular/state gas split. The access-list prewarming and authorizations move in with it, and the transactions-trie write moves after validation -- a transaction that fails validation aborts the block, so recording it first was an unobservable mutation. The chain-id check stays in `process_transaction` before `recover_sender`: for a legacy transaction `chain_id` also validates the `v` value that `recover_sender` then relies on, so it cannot move into `check_transaction` (which runs after `recover_sender`). No behavior change (Amsterdam fixtures fill byte-identically).
Message and Evm were two mutable halves of one frame: the warm sets were aliased, resolve_dispatch double-wrote the code, and the top frame mutated the supposedly immutable message. Fold Message's call parameters onto the Evm dataclass so callers construct child frames directly and the Message->Evm hand-off disappears. - The two entry grants (entry_gas, state_gas_grant) live on the GasMeter, which becomes self-describing: tx_state_gas_used and restore_state_gas_to_entry no longer take the grant as a parameter. - MessageCallOutput is retired; process_message_call returns the finished frame and normalizes error results on it. - prepare_message becomes create_top_level_evm in fork.py and utils/message.py is deleted; system transactions build the frame inline. - The unreachable stack-depth guard moves inside process_message's try, so a fire is settled as the exceptional halt it is rather than escaping the frame. - The EIP-3155 and opcode-count tracers resolve the message-scoped fields via getattr(evm, "message", evm), keeping older forks traced through the same protocols; the Evm trace protocol drops its stale `message` member. No behavior change: the full Amsterdam suite (52,812 cases) fills byte-identically; ruff and mypy clean.
…usion Drop the dead TransactionEnvironment.recipient/.value fields (no readers), and hoist the transaction's resolved blob fee onto the environment as blob_gas_fee, computed once in check_transaction at the current blob gas price. Extract the upfront sender debit into a charge_sender helper that reads that fee, and derive the top frame's create address from tx.nonce directly rather than reading the already-incremented sender nonce back. Byte-identical on Amsterdam fixtures.
Move the depth-0 preparation block -- EIP-7702 delegation application and dispatch resolution -- out of process_message and up into process_message_call, where it runs once before the create-vs-call dispatch. process_message now executes only the code of whichever frame it is given, top-level or child. Byte-identical on the full Amsterdam fixture set.
Split process_message_call into ready_top_level_evm (apply authorizations and resolve dispatch) and run_top_level_evm (dispatch to create or call, then close out). Both mutate the frame in place and are called as a pair by process_transaction and process_unchecked_system_transaction. Everything transaction-scoped -- sender charging, settlement, receipt assembly -- stays outside the pair, so a future frame loop can call it once per top-level frame. Byte-identical on Amsterdam fixtures.
Pass the top frame's finished GasMeter to settle_transaction_gas and derive execution gas used, refund counter, and net state gas from it inside the function, rather than threading each figure through the call. The pre-refund usage is now the intrinsic regular cost plus the execution gas the frame consumed across both dimensions. Byte-identical on the full Amsterdam fixture set.
Move the two post-settlement fee transfers -- the unspent-gas refund and the coinbase priority fee -- into disburse_gas_fees, parameterized on the payer that receives the refund (the sender on the standard path). Pairs with charge_sender: one fronts the maximum fee at inclusion, the other returns the remainder and pays the miner. Byte-identical on Amsterdam fixtures.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## forks/amsterdam #3192 +/- ##
===================================================
- Coverage 93.39% 93.39% -0.01%
===================================================
Files 624 623 -1
Lines 37014 37000 -14
Branches 3386 3385 -1
===================================================
- Hits 34570 34555 -15
- Misses 1671 1672 +1
Partials 773 773
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
| accessed_storage_keys=evm.accessed_storage_keys.copy(), | ||
| ) | ||
| child_evm = process_create_message(child_message) | ||
| child_evm = process_create_message(child_evm) |
There was a problem hiding this comment.
Should process_create_message be renamed, or is the "message" concept still applicable?
|
|
||
| """ | ||
| data = evm.message.data | ||
| data = evm.data |
There was a problem hiding this comment.
data on its own was decent when it was part of Message, but perhaps it should be changed to call_data or something more specific now that it's bound to the Evm itself?
| Execution is organized around a single call frame, the [`Evm`][evm]: one | ||
| object carrying a call's parameters, its machine state (program counter, | ||
| stack, memory), the gas it has left, and the effects it accrues (logs, | ||
| refunds, accounts to delete). A call spawns a child frame, and each |
There was a problem hiding this comment.
| refunds, accounts to delete). A call spawns a child frame, and each | |
| refunds, accounts to delete). A call (or create) spawns a child frame, and each |
| stack, memory), the gas it has left, and the effects it accrues (logs, | ||
| refunds, accounts to delete). A call spawns a child frame, and each | ||
| top-level call is a frame at depth zero; a transaction executes one or | ||
| more such frames, so `.interpreter` drives every frame the same way. |
There was a problem hiding this comment.
| more such frames, so `.interpreter` drives every frame the same way. | |
| more such frames, so [`interpreter`](ref:ethereum.forks.amsterdam.vm.interpreter) drives every frame the same way. |
| @@ -137,45 +157,50 @@ class TransactionEnvironment: | |||
|
|
|||
| @final | |||
| @dataclass | |||
There was a problem hiding this comment.
Huh, can you make Evm frozen?
| Apply any EIP-7702 authorizations and resolve the code the frame | ||
| runs. These state-dependent gas charges go through the frame's gas | ||
| pools; if they exhaust it, roll the preparation back, settle the | ||
| meter, and record the error on the frame so ``run_top_level_evm`` |
| 6. `return_data`: The output of the execution. | ||
| 7. `state_gas_left`: remaining state gas after execution. | ||
| 8. `state_gas_used`: State gas used during execution. | ||
| Ready the transaction's top-level frame for dispatch. |
There was a problem hiding this comment.
Probably just me misunderstanding, but if this function is "readying" a frame for dispatch, should it also be "resolving dispatch"? Seems odd that the function that does the readying also does the thing.
| Dispatch a readied top-level frame and close it out. | ||
|
|
||
| If readying already failed there is nothing to dispatch. Otherwise, | ||
| if `evm.target` is empty the dispatch creates a smart contract, else |
| evm = process_create_message(message) | ||
| tx_state = evm.tx_env.state | ||
|
|
||
| if not evm.error: |
There was a problem hiding this comment.
This is a weird flow, IMO. You're entering the run function with the post-run error already set, then checking the post-run error again after running.
| # Recent forks merge the message fields into the frame itself; | ||
| # older forks keep them on `evm.message`. | ||
| message = getattr(evm, "message", evm) |
There was a problem hiding this comment.
This'll need to get fixed when you backport the refactor, yes? Assuming backporting is still a thing.
Description
Note: This PR does not contain any EIP-8141 (Frame Transaction) implementation code but includes the minimal refactor aimed at accomplishing the following
A series of behavior-preserving refactors that disentangle top-frame ("the
transaction") machinery from generic call-frame execution, as groundwork for
EIP-8141 frame transactions. Each commit fills
byte-identically on the full Amsterdam fixture set; ruff and mypy clean.
What changes (8 commits, ~688/−624):
MessageintoEvm— the two were mutable halves of one framewith aliased warm sets and double-written code.
Message's call parametersfold onto
Evm;check_transactionreturns aTransactionEnvironmentinstead of anad-hoc tuple.
out of
process_messageand up intoprocess_message_call, soprocess_messageonly executes the code of whichever frame it is given.ready_top_level_evm/run_top_level_evm,keeping everything transaction-scoped (sender charging, settlement, receipts)
outside the pair.
Behavior: none. Pure refactor — full Amsterdam suite (52,812 cases) fills
byte-identically.
Related Issues or PRs
#3159
Checklist
just static<type>(<area>): <title>, where<type>and<area>come from an appropriateC-<type>, respectivelyA-<area>, label. The title should match the target squash commit message.Cute Animal Picture