Skip to content

Commit 3551c90

Browse files
committed
Correctly load uncles across fork boundary
- Add test to validate uncles across chain VMs - Use a universal header sedes, since we can't load the VM before decoding the header - Bonus bugfix: must double the header's gas limit at the VM boundary - Bonus cleanup: remove unused all_header_sedes
1 parent 71f0552 commit 3551c90

File tree

9 files changed

+104
-40
lines changed

9 files changed

+104
-40
lines changed

eth/abc.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1071,7 +1071,7 @@ class ChainDatabaseAPI(HeaderDatabaseAPI):
10711071
# Header API
10721072
#
10731073
@abstractmethod
1074-
def get_block_uncles(self, niece_header: BlockHeaderAPI) -> Tuple[BlockHeaderAPI, ...]:
1074+
def get_block_uncles(self, uncles_hash: Hash32) -> Tuple[BlockHeaderAPI, ...]:
10751075
"""
10761076
Return an iterable of uncle headers, specified by the header's ``uncles_hash``.
10771077
@@ -1128,8 +1128,7 @@ def persist_unexecuted_block(self,
11281128
@abstractmethod
11291129
def persist_uncles(
11301130
self,
1131-
uncles: Tuple[BlockHeaderAPI],
1132-
header_sedes: BlockHeaderSedesAPI) -> Hash32:
1131+
uncles: Tuple[BlockHeaderAPI]) -> Hash32:
11331132
"""
11341133
Persist the list of uncles to the database.
11351134
@@ -3080,7 +3079,6 @@ class VirtualMachineAPI(ConfigurableAPI):
30803079
extra_data_max_bytes: ClassVar[int]
30813080
consensus_class: Type[ConsensusAPI]
30823081
consensus_context: ConsensusContextAPI
3083-
all_header_sedes: ClassVar[Type[BlockHeaderSedesAPI]]
30843082

30853083
@abstractmethod
30863084
def __init__(self,

eth/db/chain.py

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
from eth.abc import (
2727
BlockAPI,
2828
BlockHeaderAPI,
29-
BlockHeaderSedesAPI,
3029
ChainDatabaseAPI,
3130
DatabaseAPI,
3231
AtomicDatabaseAPI,
@@ -60,6 +59,7 @@
6059
from eth.validation import (
6160
validate_word,
6261
)
62+
from eth.vm.header import HeaderSedes
6363
from eth._warnings import catch_and_ignore_import_warning
6464
with catch_and_ignore_import_warning():
6565
import rlp
@@ -153,8 +153,7 @@ def _update_header_chain_gaps(
153153
#
154154
# Header API
155155
#
156-
def get_block_uncles(self, niece_header: BlockHeaderAPI) -> Tuple[BlockHeaderAPI, ...]:
157-
uncles_hash = niece_header.uncles_hash
156+
def get_block_uncles(self, uncles_hash: Hash32) -> Tuple[BlockHeaderAPI, ...]:
158157
validate_word(uncles_hash, title="Uncles Hash")
159158
if uncles_hash == EMPTY_UNCLE_HASH:
160159
return ()
@@ -165,8 +164,7 @@ def get_block_uncles(self, niece_header: BlockHeaderAPI) -> Tuple[BlockHeaderAPI
165164
f"No uncles found for hash {uncles_hash!r}"
166165
) from exc
167166
else:
168-
header_sedes = niece_header
169-
return tuple(rlp.decode(encoded_uncles, sedes=rlp.sedes.CountableList(header_sedes)))
167+
return tuple(rlp.decode(encoded_uncles, sedes=rlp.sedes.CountableList(HeaderSedes)))
170168

171169
@classmethod
172170
def _decanonicalize_old_headers(
@@ -262,7 +260,7 @@ def _persist_block(
262260
cls._add_transaction_to_canonical_chain(db, transaction_hash, header, index)
263261

264262
if block.uncles:
265-
uncles_hash = cls._persist_uncles(db, block.uncles, block.header)
263+
uncles_hash = cls._persist_uncles(db, block.uncles)
266264
else:
267265
uncles_hash = EMPTY_UNCLE_HASH
268266
if uncles_hash != block.header.uncles_hash:
@@ -278,20 +276,18 @@ def _persist_block(
278276

279277
def persist_uncles(
280278
self,
281-
uncles: Tuple[BlockHeaderAPI],
282-
header_sedes: BlockHeaderSedesAPI) -> Hash32:
283-
return self._persist_uncles(self.db, uncles, header_sedes)
279+
uncles: Tuple[BlockHeaderAPI]) -> Hash32:
280+
return self._persist_uncles(self.db, uncles)
284281

285282
@staticmethod
286283
def _persist_uncles(
287284
db: DatabaseAPI,
288-
uncles: Tuple[BlockHeaderAPI, ...],
289-
header_sedes: BlockHeaderSedesAPI) -> Hash32:
285+
uncles: Tuple[BlockHeaderAPI, ...]) -> Hash32:
290286

291287
uncles_hash = keccak(rlp.encode(uncles))
292288
db.set(
293289
uncles_hash,
294-
rlp.encode(uncles, sedes=rlp.sedes.CountableList(header_sedes)))
290+
rlp.encode(uncles, sedes=rlp.sedes.CountableList(HeaderSedes)))
295291
return cast(Hash32, uncles_hash)
296292

297293
#

eth/db/header.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,7 @@
5555
validate_block_number,
5656
validate_word,
5757
)
58-
from eth.vm.forks.london.headers import (
59-
LondonBackwardsHeader,
60-
)
58+
from eth.vm.header import HeaderSedes
6159

6260

6361
class HeaderDB(HeaderDatabaseAPI):
@@ -624,7 +622,6 @@ def _decode_block_header(header_rlp: bytes) -> BlockHeaderAPI:
624622
# Use a deserialization class that can handle any type of header.
625623
# This feels a little hack-y, but we don't know the shape of the header
626624
# at this point. It could be a pre-London header, or a post-London
627-
# header, which includes the base fee. So we use a class that simply
628-
# checks both. Ideally, it will try London first, since all new headers will
629-
# be valid for London (until the next fork that breaks it).
630-
return rlp.decode(header_rlp, sedes=LondonBackwardsHeader)
625+
# header, which includes the base fee. So we use a class that knows how to
626+
# decode both.
627+
return rlp.decode(header_rlp, sedes=HeaderSedes)

eth/vm/forks/frontier/__init__.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from typing import (
2-
ClassVar,
32
Type,
43
)
54

@@ -10,7 +9,6 @@
109
from eth.abc import (
1110
BlockAPI,
1211
BlockHeaderAPI,
13-
BlockHeaderSedesAPI,
1412
ReceiptAPI,
1513
StateAPI,
1614
SignedTransactionAPI,
@@ -21,7 +19,6 @@
2119
UNCLE_DEPTH_PENALTY_FACTOR,
2220
ZERO_HASH32,
2321
)
24-
from eth.rlp.headers import BlockHeader
2522
from eth.rlp.logs import Log
2623
from eth.rlp.receipts import Receipt
2724

@@ -76,7 +73,6 @@ class FrontierVM(VM):
7673
# classes
7774
block_class: Type[BlockAPI] = FrontierBlock
7875
_state_class: Type[StateAPI] = FrontierState
79-
all_header_sedes: ClassVar[Type[BlockHeaderSedesAPI]] = BlockHeader
8076

8177
# methods
8278
create_header_from_parent = staticmethod(create_frontier_header_from_parent) # type: ignore

eth/vm/forks/frontier/blocks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ def from_header(cls, header: BlockHeaderAPI, chaindb: ChainDatabaseAPI) -> "Fron
122122
uncles: Tuple[BlockHeaderAPI, ...] = ()
123123
else:
124124
try:
125-
uncles = chaindb.get_block_uncles(header)
125+
uncles = chaindb.get_block_uncles(header.uncles_hash)
126126
except HeaderNotFound as exc:
127127
raise BlockNotFound(f"Uncles not found in database for {header}: {exc}") from exc
128128

eth/vm/forks/london/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
ELASTICITY_MULTIPLIER,
1616
)
1717
from .headers import (
18-
LondonBackwardsHeader,
1918
calculate_expected_base_fee_per_gas,
2019
compute_london_difficulty,
2120
configure_london_header,
@@ -31,7 +30,6 @@ class LondonVM(BerlinVM):
3130
# classes
3231
block_class: Type[BaseBlock] = LondonBlock
3332
_state_class: Type[BaseState] = LondonState
34-
all_header_sedes = LondonBackwardsHeader
3533

3634
# Methods
3735
create_header_from_parent = staticmethod(create_london_header_from_parent) # type: ignore

eth/vm/forks/london/headers.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from .blocks import LondonBlockHeader
2929
from .constants import (
3030
BASE_FEE_MAX_CHANGE_DENOMINATOR,
31+
ELASTICITY_MULTIPLIER,
3132
INITIAL_BASE_FEE,
3233
)
3334

@@ -72,12 +73,17 @@ def create_header_from_parent(difficulty_fn: Callable[[BlockHeaderAPI, int], int
7273
parent_header: Optional[BlockHeaderAPI],
7374
**header_params: Any) -> BlockHeaderAPI:
7475

75-
# frontier
7676
if 'gas_limit' not in header_params:
77-
header_params['gas_limit'] = compute_gas_limit(
78-
parent_header,
79-
genesis_gas_limit=GENESIS_GAS_LIMIT,
80-
)
77+
if parent_header is not None and parent_header.base_fee_per_gas is None:
78+
# If the previous block was not a London block,
79+
# double the gas limit, so the new target is the old gas limit
80+
header_params['gas_limit'] = parent_header.gas_limit * ELASTICITY_MULTIPLIER
81+
else:
82+
# frontier rules
83+
header_params['gas_limit'] = compute_gas_limit(
84+
parent_header,
85+
genesis_gas_limit=GENESIS_GAS_LIMIT,
86+
)
8187

8288
# byzantium
8389
if 'timestamp' not in header_params:
@@ -125,10 +131,7 @@ class LondonBackwardsHeader(BlockHeaderSedesAPI):
125131

126132
@classmethod
127133
def serialize(cls, obj: BlockHeaderAPI) -> List[bytes]:
128-
if isinstance(obj, LondonBlockHeader):
129-
return LondonBlockHeader.serialize(obj)
130-
else:
131-
return BlockHeader.serialize(obj)
134+
return obj.serialize(obj)
132135

133136
@classmethod
134137
def deserialize(cls, encoded: List[bytes]) -> BlockHeaderAPI:

eth/vm/header.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from eth.vm.forks.london.headers import LondonBackwardsHeader
2+
3+
HeaderSedes = LondonBackwardsHeader
4+
"""
5+
An RLP codec that can decode *all* known header types.
6+
7+
Unfortunately, we often cannot look up the VM to determine the valid codec. For
8+
example, when looking up a header by hash, we don't have the block number yet,
9+
and so can't load the VM configuration to find out which VM's rules to use to
10+
decode the header. Also, it's useful to have this universal sedes class when
11+
decoding multiple uncles that span a fork block.
12+
"""

tests/core/chain-object/test_chain.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,17 @@
22
import rlp
33

44
from eth_utils import decode_hex
5+
from eth_utils.toolz import sliding_window
56

67
from eth import constants
78
from eth.abc import MiningChainAPI
8-
from eth.chains.mainnet import MAINNET_GENESIS_HEADER
9+
from eth.chains.base import MiningChain
10+
from eth.chains.mainnet import (
11+
MAINNET_GENESIS_HEADER,
12+
MAINNET_VMS,
13+
)
914
from eth.chains.ropsten import ROPSTEN_GENESIS_HEADER
15+
from eth.consensus.noproof import NoProofConsensus
1016
from eth.exceptions import (
1117
TransactionNotFound,
1218
)
@@ -25,6 +31,30 @@ def chain(chain_without_block_validation):
2531
return chain_without_block_validation
2632

2733

34+
VM_PAIRS = sliding_window(2, MAINNET_VMS)
35+
36+
37+
@pytest.fixture(params=VM_PAIRS)
38+
def vm_crossover_chain(request, base_db, genesis_state):
39+
start_vm, end_vm = request.param
40+
klass = MiningChain.configure(
41+
__name__='CrossoverTestChain',
42+
vm_configuration=(
43+
(
44+
constants.GENESIS_BLOCK_NUMBER,
45+
start_vm.configure(consensus_class=NoProofConsensus),
46+
),
47+
# Can mine one block of the first VM, then the next block with be the next VM
48+
(
49+
constants.GENESIS_BLOCK_NUMBER + 2,
50+
end_vm.configure(consensus_class=NoProofConsensus),
51+
),
52+
),
53+
chain_id=1337,
54+
)
55+
return klass.from_genesis(base_db, dict(difficulty=1), genesis_state)
56+
57+
2858
@pytest.fixture
2959
def valid_chain(chain_with_block_validation):
3060
return chain_with_block_validation
@@ -200,3 +230,37 @@ def test_get_transaction_receipt(chain, tx):
200230
assert chain.get_canonical_transaction_index(tx.hash) == (1, 0)
201231
assert chain.get_transaction_receipt_by_index(1, 0) == expected_receipt
202232
assert chain.get_transaction_receipt(tx.hash) == expected_receipt
233+
234+
235+
def _mine_result_to_header(mine_all_result):
236+
block_import_result, _, _ = mine_all_result
237+
return block_import_result.imported_block.header
238+
239+
240+
def test_uncles_across_VMs(vm_crossover_chain):
241+
chain = vm_crossover_chain
242+
243+
genesis = chain.get_canonical_block_header_by_number(0)
244+
245+
# Mine in 1st VM
246+
uncle_header1 = chain.mine_block(extra_data=b'uncle1').header
247+
canon_header1 = _mine_result_to_header(
248+
chain.mine_all([], parent_header=genesis)
249+
)
250+
251+
# Mine in 2nd VM
252+
uncle_header2 = chain.mine_block(extra_data=b'uncle2').header
253+
canon_header2 = _mine_result_to_header(
254+
chain.mine_all([], parent_header=canon_header1)
255+
)
256+
257+
# Mine block with uncles from both VMs
258+
canon_block3 = chain.mine_block(uncles=[uncle_header1, uncle_header2])
259+
260+
assert canon_header2.hash == canon_block3.header.parent_hash
261+
262+
assert canon_block3.uncles == (uncle_header1, uncle_header2)
263+
264+
deserialized_block3 = chain.get_canonical_block_by_number(3)
265+
266+
assert deserialized_block3.uncles == (uncle_header1, uncle_header2)

0 commit comments

Comments
 (0)