Skip to content

Commit 87c52b4

Browse files
authored
refactor(specs): port state refactor to older forks (#2750)
* refactor(specs): apply state refactor to bpo forks * refactor(specs): apply state refactor to post-merge forks Apply the state tracker refactor (originally done for amsterdam) to the remaining post-merge forks: osaka, prague, cancun, shanghai, and paris. For each fork: - Delete fork-specific state.py and trie.py - Add fork-specific state_tracker.py with BlockState, TransactionState, and the associated read/write helpers - Update fork.py to wrap chain.state in a BlockState, thread TransactionState through check_transaction / process_transaction / system transactions / withdrawals, and apply the block diff via apply_changes_to_state at the end of state_transition - Update vm/__init__.py to use BlockState/TransactionState and import Trie from ethereum.merkle_patricia_trie - Update interpreter to use copy_tx_state/restore_tx_state in place of begin/commit/rollback_transaction - Update utils/message.py, vm/eoa_delegation.py, and vm/instructions/* to read state via evm.message.tx_env.state - Update blocks.py docstring cross-refs to the shared state / trie modules * refactor(specs): apply state refactor to pre-merge forks Apply the state tracker refactor (originally done for amsterdam) to the remaining pre-merge forks: gray_glacier, arrow_glacier, london, berlin, muir_glacier, istanbul, constantinople, byzantium, spurious_dragon, tangerine_whistle, dao_fork, homestead, and frontier. For each fork: - Delete fork-specific state.py and trie.py - Add fork-specific state_tracker.py with BlockState, TransactionState, and the associated read/write helpers - Update fork.py to wrap chain.state in a BlockState, thread TransactionState through check_transaction / process_transaction / block-reward payout, and apply the block diff via apply_changes_to_state at the end of state_transition - Update vm/__init__.py to use BlockState/TransactionState and import Trie from ethereum.merkle_patricia_trie - Update interpreter to use copy_tx_state/restore_tx_state in place of begin/commit/rollback_transaction - Update utils/message.py and vm/instructions/* to read state via evm.message.tx_env.state - Update blocks.py docstring cross-refs to the shared state / trie modules For pre-merge forks, the state_tracker also exposes `create_ether` (used for paying ommer/miner rewards) and the pre-EIP-161 forks have `destroy_touched_empty_accounts` and `touch_account`. Pre-byzantium receipts include an intermediate state root, computed by committing the tx's writes to the block state and calling `pre_state.compute_state_root_and_trie_changes` on the accumulated block diff. The DAO fork irregular state transition now creates a throwaway BlockState/TransactionState, applies the moves, and flushes the diff back to `chain.state`. * fix(specs): track storage clears across pre-EIP-6780 destructions Pre-EIP-6780 ``SELFDESTRUCT`` fully wipes an account's storage, and a later ``CREATE2`` at the same address must start from empty storage. After the state refactor, ``destroy_storage`` only removed tx-level writes, so reads still fell through to the pre_state's storage trie — ``CREATE2`` then saw ``account_has_storage`` return True and bailed without bumping the nonce. Track wiped addresses in a new ``storage_clears`` set on ``BlockState``, ``TransactionState``, and ``BlockDiff``. Reads short-circuit on it, and ``compute_state_root_and_trie_changes`` / ``apply_changes_to_state`` drop the corresponding storage tries before re-applying writes, so post-wipe writes begin from empty storage. Applies to Frontier through Shanghai; post-6780 forks leave the set empty. * refactor(evm_tools): wire t8n through ForkLoad accessors The t8n tool kept a mix of direct imports from ``amsterdam`` and raw ``_module(...)`` lookups into each fork's ``state_tracker`` and ``block_access_lists`` modules, and ``ForkLoad`` still carried fallback shims for helpers (``State``, ``set_account``, ``copy_trie``, ``root``, etc.) that now live exclusively in the shared ``ethereum.state`` and ``ethereum.merkle_patricia_trie`` modules. Collapse all three: * Drop the fork-independent shims from ``ForkLoad`` and import the helpers directly at the t8n / fixture-loader call sites. * Expose ``BlockState``, ``TransactionState``, ``extract_block_diff``, ``incorporate_tx_into_block``, ``BlockAccessIndex``, ``BlockAccessListBuilder``, and ``validate_block_access_list_gas_limit`` as ``ForkLoad`` properties so t8n never has to hardcode ``amsterdam`` or reach into ``_module``. * Fix ``pay_block_rewards`` to wrap ``BlockState`` in a ``TransactionState`` via the fork's own state_tracker (the previous code passed the block-scoped state straight to ``create_ether``, which expects the tx-scoped wrapper). * Remove the now-dead ``has_block_state`` feature flag and its fallback branches — every fork has a ``state_tracker`` module. * chore(tests): adjust json_loader tests for the state refactor Point the json_loader tests at the shared ``ethereum.state`` and ``ethereum.merkle_patricia_trie`` modules now that per-fork ``state`` and ``trie`` modules no longer exist, and switch to the State-method accessors (``state.get_storage(...)`` etc.) instead of the old module-level functions. Skip ``test_optimized_state`` wholesale: the optimized state integration still imports per-fork ``state`` modules and exposes a pre-refactor ``destroy_storage(state, addr)`` API, both removed by the refactor. Tracked by #2256 pending the optimized state redesign. Extend ``vulture_whitelist.py`` to cover the ``@add_item``-registered ``begin_transaction`` / ``commit_transaction`` / ``rollback_transaction`` patches in ``ethereum_optimized.state_db``. * refactor(specs): drop fork_types re-exports of shared state types The state refactor initially re-imported ``Account``, ``Address``, ``EMPTY_ACCOUNT``, and ``EMPTY_CODE_HASH`` from ``ethereum.state`` into each pre-merge fork's ``fork_types.py`` (plus a matching ``__all__``) so existing call sites like ``from .fork_types import Address`` could stay untouched. ``Root`` was similarly duplicated as ``Root = Hash32`` in every fork. ``docc`` does not follow ``__all__`` re-exports when resolving cross-references, which breaks ``just docs-spec`` on the first consumer that imports ``EMPTY_ACCOUNT`` through a fork's ``fork_types``. Drop the re-exports from all 13 pre-merge forks and import the shared symbols directly from ``ethereum.state`` at every call site — matching the shape that shanghai / paris already use. Each ``fork_types.py`` collapses to ``Bloom``, ``encode_account``, and the ``Account`` type import that ``encode_account`` needs. Also update ``tests/json_loader/test_optimized_state.py`` to import ``EMPTY_ACCOUNT`` from ``ethereum.state`` rather than through the dropped ``frontier.fork_types`` re-export. * refactor(specs): address PR #2750 review feedback Apply review suggestions from @SamWilsn: * ``ethereum.state.BlockDiff.storage_clears`` docstring switches to markdown style with a cross-ref to ``storage_changes``, so docc renders the link. * In every fork's ``state_tracker.get_account``, replace the ``isinstance(account, Account)`` guard with an explicit ``if account is None`` check. The ``isinstance`` was a leftover from when ``fork_types.Account`` was a separate class; now that every fork pulls ``Account`` directly from ``ethereum.state``, the ``is None`` form expresses the intent ("is there an account?") rather than the implementation ("is this an account?"). * Reintroduce the ``create_ether`` helper in every post-merge fork's state tracker (paris through bpo5). It was dropped during the post-merge state refactor because the PoS forks no longer ``pay_rewards`` to a miner, but it remains the natural helper for ``process_withdrawals``. Updates the withdrawal call sites in shanghai onwards (paris itself has no caller; ``create_ether`` is added there purely to keep sibling parity with london and shanghai). Also relocate ``create_ether`` in every pre-merge fork from its legacy spot in the trailing "Pre-EIP-161 cleanup" section to immediately after ``move_ether`` — they are structurally the same kind of balance mutator, and the new placement is consistent across every fork. * Rewrite the ``untracked_state`` comment in ``process_checked_system_transaction`` (prague through bpo5) to make the throwaway-scope and "always calls process_unchecked_system_transaction below" contracts explicit.
1 parent 44923d4 commit 87c52b4

320 files changed

Lines changed: 21976 additions & 30466 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/ethereum/forks/amsterdam/fork.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
BlockState,
7272
TransactionState,
7373
account_exists_and_is_empty,
74+
create_ether,
7475
destroy_account,
7576
extract_block_diff,
7677
get_account,
@@ -696,14 +697,14 @@ def process_checked_system_transaction(
696697
Output of processing the system transaction.
697698
698699
"""
699-
# Read through BlockState (not pre-state) so that a system contract
700-
# deployed by an earlier transaction in the same block is visible.
701-
# See EIP-7002 and EIP-7251 for this edge case.
702-
#
703-
# This read is not recorded in the state tracker.
704-
# However, this is fine because `process_unchecked_system_transaction`
705-
# does its own get_account on the TransactionState that we do incorporate
706-
# into BlockState.
700+
# Pre-check that the system contract has code. We use a throwaway
701+
# TransactionState here that is *never* propagated back to BlockState
702+
# (no incorporate_tx_into_block call); the same get_account / get_code
703+
# lookups are performed and properly tracked by
704+
# process_unchecked_system_transaction below, which this function
705+
# always calls. Reading via a TransactionState (rather than directly
706+
# against pre_state) lets us see system contracts deployed earlier in
707+
# the same block — see EIP-7002 and EIP-7251 for this edge case.
707708
untracked_state = TransactionState(parent=block_env.state)
708709
system_contract_code = get_code(
709710
untracked_state,
@@ -1108,9 +1109,7 @@ def process_withdrawals(
11081109
rlp.encode(wd),
11091110
)
11101111

1111-
current_balance = get_account(wd_state, wd.address).balance
1112-
new_balance = current_balance + wd.amount * GWEI_TO_WEI
1113-
set_account_balance(wd_state, wd.address, new_balance)
1112+
create_ether(wd_state, wd.address, wd.amount * GWEI_TO_WEI)
11141113

11151114
incorporate_tx_into_block(wd_state, block_env.block_access_list_builder)
11161115

src/ethereum/forks/amsterdam/state_tracker.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,10 @@ def get_account(tx_state: TransactionState, address: Address) -> Account:
140140
141141
"""
142142
account = get_account_optional(tx_state, address)
143-
if isinstance(account, Account):
144-
return account
145-
else:
143+
if account is None:
146144
return EMPTY_ACCOUNT
145+
else:
146+
return account
147147

148148

149149
def get_code(tx_state: TransactionState, code_hash: Hash32) -> Bytes:
@@ -568,6 +568,29 @@ def increase_recipient_balance(recipient: Account) -> None:
568568
modify_state(tx_state, recipient_address, increase_recipient_balance)
569569

570570

571+
def create_ether(
572+
tx_state: TransactionState, address: Address, amount: U256
573+
) -> None:
574+
"""
575+
Add newly created ether to an account.
576+
577+
Parameters
578+
----------
579+
tx_state :
580+
The transaction state.
581+
address :
582+
Address of the account to which ether is added.
583+
amount :
584+
The amount of ether to be added to the account of interest.
585+
586+
"""
587+
588+
def increase_balance(account: Account) -> None:
589+
account.balance += amount
590+
591+
modify_state(tx_state, address, increase_balance)
592+
593+
571594
def set_account_balance(
572595
tx_state: TransactionState, address: Address, amount: U256
573596
) -> None:

src/ethereum/forks/arrow_glacier/blocks.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@
1818
from ethereum_types.numeric import U256, Uint
1919

2020
from ethereum.crypto.hash import Hash32
21+
from ethereum.state import Address, Root
2122

22-
from .fork_types import Address, Bloom, Root
23+
from .fork_types import Bloom
2324
from .transactions import (
2425
AccessListTransaction,
2526
FeeMarketTransaction,
@@ -69,13 +70,14 @@ class Header:
6970
Root hash ([`keccak256`]) of the state trie after executing all
7071
transactions in this block. It represents the state of the Ethereum Virtual
7172
Machine (EVM) after all transactions in this block have been processed. It
72-
is computed using the [`state_root()`] function, which computes the root
73-
of the Merkle-Patricia [Trie] representing the Ethereum world state.
73+
is computed using [`compute_state_root_and_trie_changes()`][changes],
74+
which computes the root of the Merkle-Patricia [Trie] representing the
75+
Ethereum world state after applying the block's state changes.
7476
7577
[`keccak256`]: ref:ethereum.crypto.hash.keccak256
76-
[`state_root()`]: ref:ethereum.forks.arrow_glacier.state.state_root
77-
[Trie]: ref:ethereum.forks.arrow_glacier.trie.Trie
78-
"""
78+
[changes]: ref:ethereum.state.State.compute_state_root_and_trie_changes
79+
[Trie]: ref:ethereum.merkle_patricia_trie.Trie
80+
""" # noqa: E501
7981

8082
transactions_root: Root
8183
"""
@@ -85,8 +87,8 @@ class Header:
8587
transactions as the parameter.
8688
8789
[`keccak256`]: ref:ethereum.crypto.hash.keccak256
88-
[`root()`]: ref:ethereum.forks.arrow_glacier.trie.root
89-
[Trie]: ref:ethereum.forks.arrow_glacier.trie.Trie
90+
[`root()`]: ref:ethereum.merkle_patricia_trie.root
91+
[Trie]: ref:ethereum.merkle_patricia_trie.Trie
9092
"""
9193

9294
receipt_root: Root
@@ -96,8 +98,8 @@ class Header:
9698
function over the Merkle-Patricia [trie] constructed from the receipts.
9799
98100
[`keccak256`]: ref:ethereum.crypto.hash.keccak256
99-
[`root()`]: ref:ethereum.forks.arrow_glacier.trie.root
100-
[Trie]: ref:ethereum.forks.arrow_glacier.trie.Trie
101+
[`root()`]: ref:ethereum.merkle_patricia_trie.root
102+
[Trie]: ref:ethereum.merkle_patricia_trie.Trie
101103
"""
102104

103105
bloom: Bloom

src/ethereum/forks/arrow_glacier/fork.py

Lines changed: 55 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@
2828
InvalidSenderError,
2929
NonceMismatchError,
3030
)
31+
from ethereum.merkle_patricia_trie import root, trie_set
32+
from ethereum.state import (
33+
EMPTY_CODE_HASH,
34+
Address,
35+
State,
36+
apply_changes_to_state,
37+
)
3138

3239
from . import vm
3340
from .blocks import Block, Header, Log, Receipt, encode_receipt
@@ -36,17 +43,18 @@
3643
InsufficientMaxFeePerGasError,
3744
PriorityFeeGreaterThanMaxFeeError,
3845
)
39-
from .fork_types import EMPTY_CODE_HASH, Address
40-
from .state import (
41-
State,
46+
from .state_tracker import (
47+
BlockState,
48+
TransactionState,
4249
account_exists_and_is_empty,
4350
create_ether,
4451
destroy_account,
4552
destroy_touched_empty_accounts,
53+
extract_block_diff,
4654
get_account,
55+
incorporate_tx_into_block,
4756
increment_nonce,
4857
set_account_balance,
49-
state_root,
5058
)
5159
from .transactions import (
5260
AccessListTransaction,
@@ -59,7 +67,6 @@
5967
recover_sender,
6068
validate_transaction,
6169
)
62-
from .trie import root, trie_set
6370
from .utils.message import prepare_message
6471
from .vm.gas import GasCosts
6572
from .vm.interpreter import process_message_call
@@ -174,9 +181,11 @@ def state_transition(chain: BlockChain, block: Block) -> None:
174181
validate_header(chain, block.header)
175182
validate_ommers(block.ommers, block.header, chain)
176183

184+
block_state = BlockState(pre_state=chain.state)
185+
177186
block_env = vm.BlockEnvironment(
178187
chain_id=chain.chain_id,
179-
state=chain.state,
188+
state=block_state,
180189
block_gas_limit=block.header.gas_limit,
181190
block_hashes=get_last_256_block_hashes(chain),
182191
coinbase=block.header.coinbase,
@@ -191,7 +200,12 @@ def state_transition(chain: BlockChain, block: Block) -> None:
191200
transactions=block.transactions,
192201
ommers=block.ommers,
193202
)
194-
block_state_root = state_root(block_env.state)
203+
block_diff = extract_block_diff(block_state)
204+
block_state_root, _ = chain.state.compute_state_root_and_trie_changes(
205+
block_diff.account_changes,
206+
block_diff.storage_changes,
207+
block_diff.storage_clears,
208+
)
195209
transactions_root = root(block_output.transactions_trie)
196210
receipt_root = root(block_output.receipts_trie)
197211
block_logs_bloom = logs_bloom(block_output.block_logs)
@@ -209,6 +223,7 @@ def state_transition(chain: BlockChain, block: Block) -> None:
209223
if block_logs_bloom != block.header.bloom:
210224
raise InvalidBlock
211225

226+
apply_changes_to_state(chain.state, block_diff)
212227
chain.blocks.append(block)
213228
if len(chain.blocks) > 255:
214229
# Real clients have to store more blocks to deal with reorgs, but the
@@ -430,6 +445,7 @@ def check_transaction(
430445
block_env: vm.BlockEnvironment,
431446
block_output: vm.BlockOutput,
432447
tx: Transaction,
448+
tx_state: TransactionState,
433449
) -> Tuple[Address, Uint]:
434450
"""
435451
Check if the transaction is includable in the block.
@@ -442,6 +458,8 @@ def check_transaction(
442458
The block output for the current block.
443459
tx :
444460
The transaction.
461+
tx_state :
462+
The transaction state tracker.
445463
446464
Returns
447465
-------
@@ -472,7 +490,7 @@ def check_transaction(
472490
if tx.gas > gas_available:
473491
raise GasUsedExceedsLimitError("gas used exceeds limit")
474492
sender_address = recover_sender(block_env.chain_id, tx)
475-
sender_account = get_account(block_env.state, sender_address)
493+
sender_account = get_account(tx_state, sender_address)
476494

477495
if isinstance(tx, FeeMarketTransaction):
478496
if tx.max_fee_per_gas < tx.max_priority_fee_per_gas:
@@ -581,7 +599,7 @@ def apply_body(
581599
for i, tx in enumerate(map(decode_transaction, transactions)):
582600
process_transaction(block_env, block_output, tx, Uint(i))
583601

584-
pay_rewards(block_env.state, block_env.number, block_env.coinbase, ommers)
602+
pay_rewards(block_env, ommers)
585603

586604
return block_output
587605

@@ -663,9 +681,7 @@ def validate_ommers(
663681

664682

665683
def pay_rewards(
666-
state: State,
667-
block_number: Uint,
668-
coinbase: Address,
684+
block_env: vm.BlockEnvironment,
669685
ommers: Tuple[Header, ...],
670686
) -> None:
671687
"""
@@ -684,25 +700,24 @@ def pay_rewards(
684700
685701
Parameters
686702
----------
687-
state :
688-
Current account state.
689-
block_number :
690-
Position of the block within the chain.
691-
coinbase :
692-
Address of account which receives block reward and transaction fees.
703+
block_env :
704+
The block scoped environment.
693705
ommers :
694706
List of ommers mentioned in the current block.
695707
696708
"""
709+
rewards_state = TransactionState(parent=block_env.state)
697710
ommer_count = U256(len(ommers))
698711
miner_reward = BLOCK_REWARD + (ommer_count * (BLOCK_REWARD // U256(32)))
699-
create_ether(state, coinbase, miner_reward)
712+
create_ether(rewards_state, block_env.coinbase, miner_reward)
700713

701714
for ommer in ommers:
702715
# Ommer age with respect to the current block.
703-
ommer_age = U256(block_number - ommer.number)
716+
ommer_age = U256(block_env.number - ommer.number)
704717
ommer_miner_reward = ((U256(8) - ommer_age) * BLOCK_REWARD) // U256(8)
705-
create_ether(state, ommer.coinbase, ommer_miner_reward)
718+
create_ether(rewards_state, ommer.coinbase, ommer_miner_reward)
719+
720+
incorporate_tx_into_block(rewards_state)
706721

707722

708723
def process_transaction(
@@ -735,6 +750,8 @@ def process_transaction(
735750
Index of the transaction in the block.
736751
737752
"""
753+
tx_state = TransactionState(parent=block_env.state)
754+
738755
trie_set(
739756
block_output.transactions_trie,
740757
rlp.encode(index),
@@ -750,21 +767,20 @@ def process_transaction(
750767
block_env=block_env,
751768
block_output=block_output,
752769
tx=tx,
770+
tx_state=tx_state,
753771
)
754772

755-
sender_account = get_account(block_env.state, sender)
773+
sender_account = get_account(tx_state, sender)
756774

757775
effective_gas_fee = tx.gas * effective_gas_price
758776

759777
gas = tx.gas - intrinsic_gas
760-
increment_nonce(block_env.state, sender)
778+
increment_nonce(tx_state, sender)
761779

762780
sender_balance_after_gas_fee = (
763781
Uint(sender_account.balance) - effective_gas_fee
764782
)
765-
set_account_balance(
766-
block_env.state, sender, U256(sender_balance_after_gas_fee)
767-
)
783+
set_account_balance(tx_state, sender, U256(sender_balance_after_gas_fee))
768784

769785
access_list_addresses = set()
770786
access_list_storage_keys = set()
@@ -780,6 +796,7 @@ def process_transaction(
780796
gas=gas,
781797
access_list_addresses=access_list_addresses,
782798
access_list_storage_keys=access_list_storage_keys,
799+
state=tx_state,
783800
index_in_block=index,
784801
tx_hash=get_transaction_hash(encode_transaction(tx)),
785802
)
@@ -801,28 +818,28 @@ def process_transaction(
801818
transaction_fee = tx_gas_used_after_refund * priority_fee_per_gas
802819

803820
# refund gas
804-
sender_balance_after_refund = get_account(
805-
block_env.state, sender
806-
).balance + U256(gas_refund_amount)
807-
set_account_balance(block_env.state, sender, sender_balance_after_refund)
821+
sender_balance_after_refund = get_account(tx_state, sender).balance + U256(
822+
gas_refund_amount
823+
)
824+
set_account_balance(tx_state, sender, sender_balance_after_refund)
808825

809826
# transfer miner fees
810827
coinbase_balance_after_mining_fee = get_account(
811-
block_env.state, block_env.coinbase
828+
tx_state, block_env.coinbase
812829
).balance + U256(transaction_fee)
813830
if coinbase_balance_after_mining_fee != 0:
814831
set_account_balance(
815-
block_env.state,
832+
tx_state,
816833
block_env.coinbase,
817834
coinbase_balance_after_mining_fee,
818835
)
819-
elif account_exists_and_is_empty(block_env.state, block_env.coinbase):
820-
destroy_account(block_env.state, block_env.coinbase)
836+
elif account_exists_and_is_empty(tx_state, block_env.coinbase):
837+
destroy_account(tx_state, block_env.coinbase)
821838

822839
for address in tx_output.accounts_to_delete:
823-
destroy_account(block_env.state, address)
840+
destroy_account(tx_state, address)
824841

825-
destroy_touched_empty_accounts(block_env.state, tx_output.touched_accounts)
842+
destroy_touched_empty_accounts(tx_state, tx_output.touched_accounts)
826843

827844
block_output.block_gas_used += tx_gas_used_after_refund
828845

@@ -841,6 +858,8 @@ def process_transaction(
841858

842859
block_output.block_logs += tx_output.logs
843860

861+
incorporate_tx_into_block(tx_state)
862+
844863

845864
def check_gas_limit(gas_limit: Uint, parent_gas_limit: Uint) -> bool:
846865
"""

0 commit comments

Comments
 (0)