|
11 | 11 |
|
12 | 12 | from eth.constants import (
|
13 | 13 | BLANK_ROOT_HASH,
|
| 14 | + ZERO_ADDRESS, |
14 | 15 | )
|
15 | 16 | from eth.chains.base import (
|
16 | 17 | MiningChain,
|
|
19 | 20 | from eth.db.chain import (
|
20 | 21 | ChainDB,
|
21 | 22 | )
|
| 23 | +from eth.db.chain_gaps import GENESIS_CHAIN_GAPS |
22 | 24 | from eth.db.schema import SchemaV1
|
23 | 25 | from eth.exceptions import (
|
24 | 26 | BlockNotFound,
|
25 | 27 | HeaderNotFound,
|
26 | 28 | ParentNotFound,
|
27 | 29 | ReceiptNotFound,
|
| 30 | + CheckpointsMustBeCanonical, |
28 | 31 | )
|
29 | 32 | from eth.rlp.headers import (
|
30 | 33 | BlockHeader,
|
31 | 34 | )
|
| 35 | +from eth.tools.builder.chain import api |
32 | 36 | from eth.tools.rlp import (
|
33 | 37 | assert_headers_eq,
|
34 | 38 | )
|
@@ -87,10 +91,149 @@ def chain(chain_without_block_validation):
|
87 | 91 | def test_chaindb_add_block_number_to_hash_lookup(chaindb, block):
|
88 | 92 | block_number_to_hash_key = SchemaV1.make_block_number_to_hash_lookup_key(block.number)
|
89 | 93 | assert not chaindb.exists(block_number_to_hash_key)
|
| 94 | + assert chaindb.get_chain_gaps() == GENESIS_CHAIN_GAPS |
90 | 95 | chaindb.persist_block(block)
|
91 | 96 | assert chaindb.exists(block_number_to_hash_key)
|
92 | 97 |
|
93 | 98 |
|
| 99 | +@pytest.mark.parametrize( |
| 100 | + 'has_uncle, has_transaction, can_fetch_block', |
| 101 | + ( |
| 102 | + # Uncle block gets de-canonicalized by a header that has another uncle |
| 103 | + (True, False, False,), |
| 104 | + # Uncle block gets de-canonicalized by a header that has transactions |
| 105 | + (False, True, False,), |
| 106 | + # Has uncle and transactions |
| 107 | + (True, True, False,), |
| 108 | + # Uncle block gets de-canonicalized by a header that has no uncles nor transactions which |
| 109 | + # means this is a "Header-only" block. Even though we technically do not need to re-open |
| 110 | + # a gap here, we don't have a good way of detecting that special case and hence open a gap. |
| 111 | + (False, False, True,), |
| 112 | + ) |
| 113 | +) |
| 114 | +def test_block_gap_tracking(chain, |
| 115 | + funded_address, |
| 116 | + funded_address_private_key, |
| 117 | + has_uncle, |
| 118 | + has_transaction, |
| 119 | + can_fetch_block): |
| 120 | + |
| 121 | + # Mine three common blocks |
| 122 | + common_chain = api.build( |
| 123 | + chain, |
| 124 | + api.mine_blocks(3), |
| 125 | + ) |
| 126 | + |
| 127 | + assert common_chain.get_canonical_head().block_number == 3 |
| 128 | + assert common_chain.chaindb.get_chain_gaps() == ((), 4) |
| 129 | + |
| 130 | + tx = new_transaction( |
| 131 | + common_chain.get_vm(), |
| 132 | + from_=funded_address, |
| 133 | + to=ZERO_ADDRESS, |
| 134 | + private_key=funded_address_private_key, |
| 135 | + ) |
| 136 | + uncle = api.build(common_chain, api.mine_block()).get_canonical_block_header_by_number(4) |
| 137 | + uncles = [uncle] if has_uncle else [] |
| 138 | + transactions = [tx] if has_transaction else [] |
| 139 | + |
| 140 | + # Split and have the main chain mine four blocks, the uncle chain two blocks |
| 141 | + main_chain, uncle_chain = api.build( |
| 142 | + common_chain, |
| 143 | + api.chain_split( |
| 144 | + ( |
| 145 | + # We have four different scenarios for our replaced blocks: |
| 146 | + # 1. Replaced by a trivial block without uncles or transactions |
| 147 | + # 2. Replaced by a block with transactions |
| 148 | + # 3. Replaced by a block with uncles |
| 149 | + # 4. 2 and 3 combined |
| 150 | + api.mine_block(uncles=uncles, transactions=transactions), |
| 151 | + api.mine_block(), |
| 152 | + api.mine_block(), |
| 153 | + api.mine_block(), |
| 154 | + ), |
| 155 | + # This will be the uncle chain |
| 156 | + (api.mine_block(extra_data=b'fork-it'), api.mine_block(),), |
| 157 | + ), |
| 158 | + ) |
| 159 | + |
| 160 | + main_head = main_chain.get_canonical_head() |
| 161 | + assert main_head.block_number == 7 |
| 162 | + assert uncle_chain.get_canonical_head().block_number == 5 |
| 163 | + |
| 164 | + assert main_chain.chaindb.get_chain_gaps() == ((), 8) |
| 165 | + assert uncle_chain.chaindb.get_chain_gaps() == ((), 6) |
| 166 | + |
| 167 | + main_header_6 = main_chain.chaindb.get_canonical_block_header_by_number(6) |
| 168 | + main_header_6_score = main_chain.chaindb.get_score(main_header_6.hash) |
| 169 | + |
| 170 | + gap_chain = api.copy(common_chain) |
| 171 | + assert gap_chain.get_canonical_head() == common_chain.get_canonical_head() |
| 172 | + |
| 173 | + gap_chain.chaindb.persist_checkpoint_header(main_header_6, main_header_6_score) |
| 174 | + # We created a gap in the chain of headers |
| 175 | + assert gap_chain.chaindb.get_header_chain_gaps() == (((4, 5),), 7) |
| 176 | + # ...but not in the chain of blocks (yet!) |
| 177 | + assert gap_chain.chaindb.get_chain_gaps() == ((), 4) |
| 178 | + block_7 = main_chain.get_canonical_block_by_number(7) |
| 179 | + block_7_receipts = block_7.get_receipts(main_chain.chaindb) |
| 180 | + # Persist block 7 on top of the checkpoint |
| 181 | + gap_chain.chaindb.persist_unexecuted_block(block_7, block_7_receipts) |
| 182 | + assert gap_chain.chaindb.get_header_chain_gaps() == (((4, 5),), 8) |
| 183 | + # Now we have a gap in the chain of blocks, too |
| 184 | + assert gap_chain.chaindb.get_chain_gaps() == (((4, 6),), 8) |
| 185 | + |
| 186 | + # Overwriting header 3 doesn't cause us to re-open a block gap |
| 187 | + gap_chain.chaindb.persist_header_chain([ |
| 188 | + main_chain.chaindb.get_canonical_block_header_by_number(3) |
| 189 | + ]) |
| 190 | + assert gap_chain.chaindb.get_chain_gaps() == (((4, 6),), 8) |
| 191 | + |
| 192 | + # Now get the uncle block |
| 193 | + uncle_block = uncle_chain.get_canonical_block_by_number(4) |
| 194 | + uncle_block_receipts = uncle_block.get_receipts(uncle_chain.chaindb) |
| 195 | + |
| 196 | + # Put the uncle block in the gap |
| 197 | + gap_chain.chaindb.persist_unexecuted_block(uncle_block, uncle_block_receipts) |
| 198 | + assert gap_chain.chaindb.get_header_chain_gaps() == (((5, 5),), 8) |
| 199 | + assert gap_chain.chaindb.get_chain_gaps() == (((5, 6),), 8) |
| 200 | + |
| 201 | + # Trying to save another uncle errors as its header isn't the parent of the checkpoint |
| 202 | + second_uncle = uncle_chain.get_canonical_block_by_number(5) |
| 203 | + second_uncle_receipts = second_uncle.get_receipts(uncle_chain.chaindb) |
| 204 | + with pytest.raises(CheckpointsMustBeCanonical): |
| 205 | + gap_chain.chaindb.persist_unexecuted_block(second_uncle, second_uncle_receipts) |
| 206 | + |
| 207 | + # Now close the gap in the header chain with the actual correct headers |
| 208 | + actual_headers = [ |
| 209 | + main_chain.chaindb.get_canonical_block_header_by_number(block_number) |
| 210 | + for block_number in range(4, 7) |
| 211 | + ] |
| 212 | + gap_chain.chaindb.persist_header_chain(actual_headers) |
| 213 | + # No more gaps in the header chain |
| 214 | + assert gap_chain.chaindb.get_header_chain_gaps() == ((), 8) |
| 215 | + # We detected the de-canonicalized uncle and re-opened the block gap |
| 216 | + assert gap_chain.chaindb.get_chain_gaps() == (((4, 6),), 8) |
| 217 | + |
| 218 | + if can_fetch_block: |
| 219 | + # We can fetch the block even if the gap tracking reports it as missing if the block is |
| 220 | + # a "trivial" block, meaning one that doesn't have transactions nor uncles and hence |
| 221 | + # can be loaded by just the header alone. |
| 222 | + block_4 = gap_chain.get_canonical_block_by_number(4) |
| 223 | + assert block_4 == main_chain.get_canonical_block_by_number(4) |
| 224 | + else: |
| 225 | + # The uncle block was implicitly de-canonicalized with its header, |
| 226 | + # hence we can not fetch it any longer. |
| 227 | + with pytest.raises(BlockNotFound): |
| 228 | + gap_chain.get_canonical_block_by_number(4) |
| 229 | + # Add the missing block and assert the gap shrinks |
| 230 | + assert gap_chain.chaindb.get_chain_gaps() == (((4, 6),), 8) |
| 231 | + block_4 = main_chain.get_canonical_block_by_number(4) |
| 232 | + block_4_receipts = block_4.get_receipts(main_chain.chaindb) |
| 233 | + gap_chain.chaindb.persist_unexecuted_block(block_4, block_4_receipts) |
| 234 | + assert gap_chain.chaindb.get_chain_gaps() == (((5, 6),), 8) |
| 235 | + |
| 236 | + |
94 | 237 | def test_chaindb_persist_header(chaindb, header):
|
95 | 238 | with pytest.raises(HeaderNotFound):
|
96 | 239 | chaindb.get_block_header_by_hash(header.hash)
|
|
0 commit comments