Skip to content

Commit 2ed5b14

Browse files
authored
Piper/fix validation error if new head uncle is current head v2 (ethereum#1204)
* Fix issue 1185 * less fragile check for no-reorg
1 parent dab298e commit 2ed5b14

File tree

5 files changed

+220
-36
lines changed

5 files changed

+220
-36
lines changed

eth/chains/base.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@
2525

2626
from cytoolz import (
2727
assoc,
28+
compose,
2829
groupby,
30+
iterate,
31+
take,
2932
)
3033

3134
from eth_typing import (
@@ -35,7 +38,6 @@
3538
)
3639

3740
from eth_utils import (
38-
to_tuple,
3941
to_set,
4042
ValidationError,
4143
)
@@ -204,7 +206,7 @@ def get_score(self, block_hash):
204206
# Block API
205207
#
206208
@abstractmethod
207-
def get_ancestors(self, limit: int, header: BlockHeader) -> Iterator[BaseBlock]:
209+
def get_ancestors(self, limit: int, header: BlockHeader) -> Tuple[BaseBlock, ...]:
208210
raise NotImplementedError("Chain classes must implement this method")
209211

210212
@abstractmethod
@@ -447,14 +449,27 @@ def ensure_header(self, header: BlockHeader=None) -> BlockHeader:
447449
#
448450
# Block API
449451
#
450-
@to_tuple
451-
def get_ancestors(self, limit: int, header: BlockHeader) -> Iterator[BaseBlock]:
452+
def get_ancestors(self, limit: int, header: BlockHeader) -> Tuple[BaseBlock, ...]:
452453
"""
453454
Return `limit` number of ancestor blocks from the current canonical head.
454455
"""
455-
lower_limit = max(header.block_number - limit, 0)
456-
for n in reversed(range(lower_limit, header.block_number)):
457-
yield self.get_canonical_block_by_number(BlockNumber(n))
456+
ancestor_count = min(header.block_number, limit)
457+
458+
# We construct a temporary block object
459+
vm_class = self.get_vm_class_for_block_number(header.block_number)
460+
block_class = vm_class.get_block_class()
461+
block = block_class(header=header, uncles=[])
462+
463+
ancestor_generator = iterate(compose(
464+
self.get_block_by_hash,
465+
operator.attrgetter('parent_hash'),
466+
operator.attrgetter('header'),
467+
), block)
468+
# we peel off the first element from the iterator which will be the
469+
# temporary block object we constructed.
470+
next(ancestor_generator)
471+
472+
return tuple(take(ancestor_count, ancestor_generator))
458473

459474
def get_block(self) -> BaseBlock:
460475
"""

eth/vm/base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -562,7 +562,7 @@ def pack_block(self, block, *args, **kwargs):
562562
"""
563563
if 'uncles' in kwargs:
564564
uncles = kwargs.pop('uncles')
565-
kwargs.setdefault('uncles_hash', keccak(rlp.encode(block.uncles)))
565+
kwargs.setdefault('uncles_hash', keccak(rlp.encode(uncles)))
566566
else:
567567
uncles = block.uncles
568568

@@ -710,7 +710,7 @@ def validate_block(self, block):
710710
" - header uncle_hash: {2}".format(
711711
len(block.uncles),
712712
local_uncle_hash,
713-
block.header.uncle_hash,
713+
block.header.uncles_hash,
714714
)
715715
)
716716

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import pytest
2+
3+
from eth.chains.base import MiningChain
4+
from eth.db.backends.memory import MemoryDB
5+
6+
7+
@pytest.fixture
8+
def chain(chain_without_block_validation):
9+
if not isinstance(chain_without_block_validation, MiningChain):
10+
pytest.skip('only valid on mining chains')
11+
return chain_without_block_validation
12+
13+
14+
@pytest.fixture
15+
def fork_chain(chain):
16+
# make a duplicate chain with no shared state
17+
fork_db = MemoryDB(chain.chaindb.db.kv_store.copy())
18+
fork_chain = type(chain)(fork_db, chain.header)
19+
20+
return fork_chain
21+
22+
23+
@pytest.mark.parametrize(
24+
'limit',
25+
(0, 1, 2, 5),
26+
)
27+
def test_chain_get_ancestors_from_genesis_block(chain, limit):
28+
header = chain.get_canonical_head()
29+
assert header.block_number == 0
30+
31+
ancestors = chain.get_ancestors(limit, header)
32+
assert ancestors == tuple()
33+
34+
35+
def test_chain_get_ancestors_from_block_1(chain):
36+
genesis = chain.get_canonical_block_by_number(0)
37+
block_1 = chain.mine_block()
38+
header = block_1.header
39+
assert header.block_number == 1
40+
41+
assert chain.get_ancestors(0, header) == tuple()
42+
assert chain.get_ancestors(1, header) == (genesis,)
43+
assert chain.get_ancestors(2, header) == (genesis,)
44+
assert chain.get_ancestors(5, header) == (genesis,)
45+
46+
47+
def test_chain_get_ancestors_from_block_5(chain):
48+
genesis = chain.get_canonical_block_by_number(0)
49+
(
50+
block_1,
51+
block_2,
52+
block_3,
53+
block_4,
54+
block_5,
55+
) = [chain.mine_block() for _ in range(5)]
56+
57+
header = block_5.header
58+
assert header.block_number == 5
59+
60+
assert chain.get_ancestors(0, header) == tuple()
61+
assert chain.get_ancestors(1, header) == (block_4,)
62+
assert chain.get_ancestors(2, header) == (block_4, block_3)
63+
assert chain.get_ancestors(3, header) == (block_4, block_3, block_2)
64+
assert chain.get_ancestors(4, header) == (block_4, block_3, block_2, block_1)
65+
assert chain.get_ancestors(5, header) == (block_4, block_3, block_2, block_1, genesis)
66+
assert chain.get_ancestors(6, header) == (block_4, block_3, block_2, block_1, genesis)
67+
assert chain.get_ancestors(10, header) == (block_4, block_3, block_2, block_1, genesis)
68+
69+
70+
def test_chain_get_ancestors_for_fork_chains(chain, fork_chain):
71+
genesis = chain.get_canonical_block_by_number(0)
72+
(
73+
block_1,
74+
block_2,
75+
block_3,
76+
) = [chain.mine_block() for _ in range(3)]
77+
(
78+
f_block_1,
79+
f_block_2,
80+
f_block_3,
81+
) = [fork_chain.mine_block() for _ in range(3)]
82+
83+
assert block_1 == f_block_1
84+
assert block_2 == f_block_2
85+
assert block_3 == f_block_3
86+
87+
# force the fork chain to diverge
88+
fork_chain.header = fork_chain.header.copy(extra_data=b'fork-it!')
89+
90+
# mine ahead a bit further on both chains.
91+
(
92+
block_4,
93+
block_5,
94+
block_6,
95+
) = [chain.mine_block() for _ in range(3)]
96+
(
97+
f_block_4,
98+
f_block_5,
99+
f_block_6,
100+
) = [fork_chain.mine_block() for _ in range(3)]
101+
102+
# import the fork blocks into the main chain (ensuring they don't cause a reorg)
103+
_, new_chain, _ = chain.import_block(f_block_4)
104+
assert new_chain == tuple()
105+
_, new_chain, _ = chain.import_block(f_block_5)
106+
assert new_chain == tuple()
107+
108+
# check with a block that has been imported
109+
assert chain.get_ancestors(0, f_block_5.header) == tuple()
110+
assert chain.get_ancestors(1, f_block_5.header) == (f_block_4,)
111+
assert chain.get_ancestors(2, f_block_5.header) == (f_block_4, block_3)
112+
assert chain.get_ancestors(3, f_block_5.header) == (f_block_4, block_3, block_2)
113+
assert chain.get_ancestors(4, f_block_5.header) == (f_block_4, block_3, block_2, block_1)
114+
assert chain.get_ancestors(5, f_block_5.header) == (f_block_4, block_3, block_2, block_1, genesis) # noqa: E501
115+
# check that when we hit genesis it self limits
116+
assert chain.get_ancestors(6, f_block_5.header) == (f_block_4, block_3, block_2, block_1, genesis) # noqa: E501
117+
assert chain.get_ancestors(20, f_block_5.header) == (f_block_4, block_3, block_2, block_1, genesis) # noqa: E501
118+
119+
# check with a block that has NOT been imported
120+
assert chain.get_ancestors(0, f_block_6.header) == tuple()
121+
assert chain.get_ancestors(1, f_block_6.header) == (f_block_5,)
122+
assert chain.get_ancestors(2, f_block_6.header) == (f_block_5, f_block_4)
123+
assert chain.get_ancestors(3, f_block_6.header) == (f_block_5, f_block_4, block_3)
124+
assert chain.get_ancestors(4, f_block_6.header) == (f_block_5, f_block_4, block_3, block_2)
125+
assert chain.get_ancestors(5, f_block_6.header) == (f_block_5, f_block_4, block_3, block_2, block_1) # noqa: E501
126+
assert chain.get_ancestors(6, f_block_6.header) == (f_block_5, f_block_4, block_3, block_2, block_1, genesis) # noqa: E501
127+
# check that when we hit genesis it self limits
128+
assert chain.get_ancestors(7, f_block_6.header) == (f_block_5, f_block_4, block_3, block_2, block_1, genesis) # noqa: E501
129+
assert chain.get_ancestors(20, f_block_6.header) == (f_block_5, f_block_4, block_3, block_2, block_1, genesis) # noqa: E501

tests/core/chain-object/test_chain_reorganization.py

Lines changed: 66 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515

1616
@pytest.fixture(params=VM_CLASSES)
17-
def chain(request, genesis_state):
17+
def base_chain(request, genesis_state):
1818
VMClass = request.param.configure(validate_seal=lambda block: None)
1919

2020
class ChainForTest(MiningChain):
@@ -30,20 +30,23 @@ class ChainForTest(MiningChain):
3030
return chain
3131

3232

33-
ZERO_ADDRESS = b'\x00' * 20
34-
35-
36-
def test_import_block_with_reorg(chain, funded_address_private_key):
33+
@pytest.fixture
34+
def chain(base_chain):
3735
# mine 3 "common blocks"
38-
block_0 = chain.get_canonical_block_by_number(0)
36+
block_0 = base_chain.get_canonical_block_by_number(0)
3937
assert block_0.number == 0
40-
block_1 = chain.mine_block()
38+
block_1 = base_chain.mine_block()
4139
assert block_1.number == 1
42-
block_2 = chain.mine_block()
40+
block_2 = base_chain.mine_block()
4341
assert block_2.number == 2
44-
block_3 = chain.mine_block()
42+
block_3 = base_chain.mine_block()
4543
assert block_3.number == 3
4644

45+
return base_chain
46+
47+
48+
@pytest.fixture
49+
def fork_chain(chain):
4750
# make a duplicate chain with no shared state
4851
fork_db = MemoryDB(chain.chaindb.db.kv_store.copy())
4952
fork_chain = type(chain)(fork_db, chain.header)
@@ -56,12 +59,26 @@ def test_import_block_with_reorg(chain, funded_address_private_key):
5659

5760
assert fork_chain.get_canonical_head() == chain.get_canonical_head()
5861

62+
block_0 = chain.get_canonical_block_by_number(0)
5963
assert fork_chain.get_canonical_block_by_number(0) == block_0
64+
65+
block_1 = chain.get_canonical_block_by_number(1)
6066
assert fork_chain.get_canonical_block_by_number(1) == block_1
67+
68+
block_2 = chain.get_canonical_block_by_number(2)
6169
assert fork_chain.get_canonical_block_by_number(2) == block_2
70+
71+
block_3 = chain.get_canonical_block_by_number(3)
6272
assert fork_chain.get_canonical_block_by_number(3) == block_3
6373

64-
# now cause the fork chain to diverge from the main chain
74+
return fork_chain
75+
76+
77+
ZERO_ADDRESS = b'\x00' * 20
78+
79+
80+
def test_import_block_with_reorg(chain, fork_chain, funded_address_private_key):
81+
# cause the fork chain to diverge from the main chain
6582
tx = fork_chain.create_unsigned_transaction(
6683
nonce=0,
6784
gas_price=1,
@@ -72,7 +89,7 @@ def test_import_block_with_reorg(chain, funded_address_private_key):
7289
)
7390
fork_chain.apply_transaction(tx.as_signed_transaction(funded_address_private_key))
7491

75-
# Mine 3 blocks, ensuring that the difficulty of the main chain remains
92+
# Mine 2 blocks, ensuring that the difficulty of the main chain remains
7693
# equal or greater than the fork chain
7794
block_4 = chain.mine_block()
7895
f_block_4 = fork_chain.mine_block()
@@ -87,28 +104,51 @@ def test_import_block_with_reorg(chain, funded_address_private_key):
87104
assert f_block_5.number == 5
88105
assert f_block_5.header.difficulty <= block_5.header.difficulty
89106

90-
f_block_6, block_6 = fork_chain.mine_block(), chain.mine_block()
91-
assert f_block_6 != block_6
92-
assert block_6.number == 6
93-
assert f_block_6.number == 6
94-
assert f_block_6.header.difficulty <= block_6.header.difficulty
95-
96-
# now mine the 7th block which will outpace the main chain difficulty.
97-
f_block_7 = fork_chain.mine_block()
107+
# now mine the 6th block which will outpace the main chain difficulty.
108+
f_block_6 = fork_chain.mine_block()
98109

99110
pre_fork_chain_head = chain.header
100111

101112
# now we proceed to import the blocks from the fork chain into the main
102-
# chain. Blocks 4, 5, and 6 should import resulting in no re-organization.
103-
for block in (f_block_4, f_block_5, f_block_6):
113+
# chain. Blocks 4 and 5 should import resulting in no re-organization.
114+
for block in (f_block_4, f_block_5):
104115
_, new_canonical_blocks, old_canonical_blocks = chain.import_block(block)
105116
assert not new_canonical_blocks
106117
assert not old_canonical_blocks
107118
assert chain.header == pre_fork_chain_head
108119

109-
# now we import block 7 from the fork chain. This should cause a re-org.
110-
_, new_canonical_blocks, old_canonical_blocks = chain.import_block(f_block_7)
111-
assert new_canonical_blocks == (f_block_4, f_block_5, f_block_6, f_block_7)
112-
assert old_canonical_blocks == (block_4, block_5, block_6)
120+
# now we import block 6 from the fork chain. This should cause a re-org.
121+
_, new_canonical_blocks, old_canonical_blocks = chain.import_block(f_block_6)
122+
assert new_canonical_blocks == (f_block_4, f_block_5, f_block_6)
123+
assert old_canonical_blocks == (block_4, block_5)
124+
125+
assert chain.get_canonical_head() == f_block_6.header
126+
127+
128+
def test_import_block_with_reorg_with_current_head_as_uncle(
129+
chain,
130+
fork_chain,
131+
funded_address_private_key):
132+
"""
133+
https://github.com/ethereum/py-evm/issues/1185
134+
"""
135+
# mine a block on the main chain which will eventually become an uncle on
136+
# the main chain after a reorg.
137+
block = chain.mine_block()
138+
139+
# Force the fork_chain to diverge from the main chain
140+
fork_chain.header = fork_chain.header.copy(extra_data=b'fork-it!')
141+
f_block_a = fork_chain.mine_block()
142+
143+
# now mine a block which has the current chain head as an uncle.
144+
assert f_block_a != block
145+
f_block_b = fork_chain.mine_block(uncles=(block.header,))
146+
147+
# ensure that we don't cause a re-org with our first import.
148+
_, new_chain, _ = chain.import_block(f_block_a)
149+
assert new_chain == tuple()
150+
151+
# import the block with the uncle, ensure that the chain did indeed re-org
152+
chain.import_block(f_block_b)
113153

114-
assert chain.get_canonical_head() == f_block_7.header
154+
assert chain.get_canonical_head() == f_block_b.header

trinity/chains/light.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ def get_score(self, block_hash: Hash32) -> int:
128128
#
129129
# Block API
130130
#
131-
def get_ancestors(self, limit: int, header: BlockHeader=None) -> Iterator[BaseBlock]:
131+
def get_ancestors(self, limit: int, header: BlockHeader) -> Tuple[BaseBlock, ...]:
132132
raise NotImplementedError("Chain classes must implement " + inspect.stack()[0][3])
133133

134134
def get_block(self) -> BaseBlock:

0 commit comments

Comments
 (0)