reorg fixes (#10943)

* when going through a reorg, maintain all chain state until the very end, when the new fork has been fully validated and added

* when rolling back the chain, also rollback the height-to-hash map

* add tests
This commit is contained in:
Arvid Norberg 2022-03-31 17:26:23 +02:00 committed by GitHub
parent 908f186c1e
commit 8833cc351c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 326 additions and 11 deletions

View File

@ -381,10 +381,6 @@ class Blockchain(BlockchainInterface):
for coin_record in roll_changes:
latest_coin_state[coin_record.name] = coin_record
# Rollback sub_epoch_summaries
self.__height_map.rollback(fork_height)
await self.block_store.rollback(fork_height)
# Collect all blocks from fork point to new peak
blocks_to_add: List[Tuple[FullBlock, BlockRecord]] = []
curr = block_record.header_hash
@ -445,6 +441,10 @@ class Blockchain(BlockchainInterface):
hint_coin_state[key] = {}
hint_coin_state[key][coin_id] = latest_coin_state[coin_id]
# we made it to the end successfully
# Rollback sub_epoch_summaries
self.__height_map.rollback(fork_height)
await self.block_store.rollback(fork_height)
await self.block_store.set_in_chain([(br.header_hash,) for br in records_to_add])
# Changes the peak to be the new peak

View File

@ -230,6 +230,7 @@ class BlockHeightMap:
heights_to_delete.append(ses_included_height)
for height in heights_to_delete:
del self.__sub_epoch_summaries[height]
del self.__height_to_hash[(fork_height + 1) * 32 :]
def get_ses(self, height: uint32) -> SubEpochSummary:
return SubEpochSummary.from_bytes(self.__sub_epoch_summaries[height])

View File

@ -11,7 +11,7 @@ import time
from argparse import Namespace
from dataclasses import replace
from pathlib import Path
from typing import Callable, Dict, List, Optional, Tuple, Any
from typing import Callable, Dict, List, Optional, Tuple, Any, Union
from blspy import AugSchemeMPL, G1Element, G2Element, PrivateKey
from chiabip158 import PyBIP158
@ -435,7 +435,7 @@ class BlockTools:
normalized_to_identity_cc_sp: bool = False,
normalized_to_identity_cc_ip: bool = False,
current_time: bool = False,
previous_generator: CompressorArg = None,
previous_generator: Optional[Union[CompressorArg, List[uint32]]] = None,
genesis_timestamp: Optional[uint64] = None,
force_plot_id: Optional[bytes32] = None,
) -> List[FullBlock]:
@ -588,12 +588,14 @@ class BlockTools:
pool_target = PoolTarget(self.pool_ph, uint32(0))
if transaction_data is not None:
if previous_generator is not None:
if type(previous_generator) is CompressorArg:
block_generator: Optional[BlockGenerator] = best_solution_generator_from_template(
previous_generator, transaction_data
)
else:
block_generator = simple_solution_generator(transaction_data)
if type(previous_generator) is list:
block_generator = BlockGenerator(block_generator.program, [], previous_generator)
aggregate_signature = transaction_data.aggregated_signature
else:
@ -861,7 +863,7 @@ class BlockTools:
else:
pool_target = PoolTarget(self.pool_ph, uint32(0))
if transaction_data is not None:
if previous_generator is not None:
if previous_generator is not None and type(previous_generator) is CompressorArg:
block_generator = best_solution_generator_from_template(
previous_generator, transaction_data
)

View File

@ -4,7 +4,7 @@ from chia.consensus.blockchain import Blockchain, ReceiveBlockResult
from chia.consensus.multiprocess_validation import PreValidationResult
from chia.types.full_block import FullBlock
from chia.util.errors import Err
from chia.util.ints import uint64
from chia.util.ints import uint64, uint32
async def check_block_store_invariant(bc: Blockchain):
@ -42,6 +42,7 @@ async def _validate_and_add_block(
expected_result: Optional[ReceiveBlockResult] = None,
expected_error: Optional[Err] = None,
skip_prevalidation: bool = False,
fork_point_with_peak: Optional[uint32] = None,
) -> None:
# Tries to validate and add the block, and checks that there are no errors in the process and that the
# block is added to the peak.
@ -74,7 +75,7 @@ async def _validate_and_add_block(
await check_block_store_invariant(blockchain)
return None
result, err, _, _ = await blockchain.receive_block(block, results)
result, err, _, _ = await blockchain.receive_block(block, results, fork_point_with_peak=fork_point_with_peak)
await check_block_store_invariant(blockchain)
if expected_error is None and expected_result != ReceiveBlockResult.INVALID_BLOCK:

View File

@ -2949,3 +2949,302 @@ class TestReorgs:
assert blocks
assert len(blocks) == 200
assert blocks[-1].height == 199
@pytest.mark.asyncio
async def test_reorg_new_ref(empty_blockchain, bt):
b = empty_blockchain
wallet_a = WalletTool(b.constants)
WALLET_A_PUZZLE_HASHES = [wallet_a.get_new_puzzlehash() for _ in range(5)]
coinbase_puzzlehash = WALLET_A_PUZZLE_HASHES[0]
receiver_puzzlehash = WALLET_A_PUZZLE_HASHES[1]
blocks = bt.get_consecutive_blocks(
5,
farmer_reward_puzzle_hash=coinbase_puzzlehash,
pool_reward_puzzle_hash=receiver_puzzlehash,
guarantee_transaction_block=True,
)
all_coins = []
for spend_block in blocks[:5]:
for coin in list(spend_block.get_included_reward_coins()):
if coin.puzzle_hash == coinbase_puzzlehash:
all_coins.append(coin)
spend_bundle_0 = wallet_a.generate_signed_transaction(1000, receiver_puzzlehash, all_coins.pop())
blocks = bt.get_consecutive_blocks(
15,
block_list_input=blocks,
farmer_reward_puzzle_hash=coinbase_puzzlehash,
pool_reward_puzzle_hash=receiver_puzzlehash,
transaction_data=spend_bundle_0,
guarantee_transaction_block=True,
)
for block in blocks:
await _validate_and_add_block(b, block)
assert b.get_peak().height == 19
print("first chain done")
# Make sure a ref back into the reorg chain itself works as expected
blocks_reorg_chain = bt.get_consecutive_blocks(
1,
blocks[:10],
seed=b"2",
farmer_reward_puzzle_hash=coinbase_puzzlehash,
pool_reward_puzzle_hash=receiver_puzzlehash,
)
spend_bundle = wallet_a.generate_signed_transaction(1000, receiver_puzzlehash, all_coins.pop())
blocks_reorg_chain = bt.get_consecutive_blocks(
2,
blocks_reorg_chain,
seed=b"2",
farmer_reward_puzzle_hash=coinbase_puzzlehash,
pool_reward_puzzle_hash=receiver_puzzlehash,
transaction_data=spend_bundle,
guarantee_transaction_block=True,
)
spend_bundle2 = wallet_a.generate_signed_transaction(1000, receiver_puzzlehash, all_coins.pop())
blocks_reorg_chain = bt.get_consecutive_blocks(
4, blocks_reorg_chain, seed=b"2", previous_generator=[uint32(5), uint32(11)], transaction_data=spend_bundle2
)
blocks_reorg_chain = bt.get_consecutive_blocks(4, blocks_reorg_chain, seed=b"2")
for i, block in enumerate(blocks_reorg_chain):
fork_point_with_peak = None
if i < 10:
expected = ReceiveBlockResult.ALREADY_HAVE_BLOCK
elif i < 20:
expected = ReceiveBlockResult.ADDED_AS_ORPHAN
else:
expected = ReceiveBlockResult.NEW_PEAK
fork_point_with_peak = uint32(1)
await _validate_and_add_block(b, block, expected_result=expected, fork_point_with_peak=fork_point_with_peak)
assert b.get_peak().height == 20
# this test doesn't reorg, but _reconsider_peak() is passed a stale
# "fork_height" to make it look like it's in a reorg, but all the same blocks
# are just added back.
@pytest.mark.asyncio
async def test_reorg_stale_fork_height(empty_blockchain, bt):
b = empty_blockchain
wallet_a = WalletTool(b.constants)
WALLET_A_PUZZLE_HASHES = [wallet_a.get_new_puzzlehash() for _ in range(5)]
coinbase_puzzlehash = WALLET_A_PUZZLE_HASHES[0]
receiver_puzzlehash = WALLET_A_PUZZLE_HASHES[1]
blocks = bt.get_consecutive_blocks(
5,
farmer_reward_puzzle_hash=coinbase_puzzlehash,
pool_reward_puzzle_hash=receiver_puzzlehash,
guarantee_transaction_block=True,
)
all_coins = []
for spend_block in blocks:
for coin in list(spend_block.get_included_reward_coins()):
if coin.puzzle_hash == coinbase_puzzlehash:
all_coins.append(coin)
spend_bundle_0 = wallet_a.generate_signed_transaction(1000, receiver_puzzlehash, all_coins.pop())
# Make sure a ref back into the reorg chain itself works as expected
spend_bundle = wallet_a.generate_signed_transaction(1000, receiver_puzzlehash, all_coins.pop())
# make sure we have a transaction block, with at least one transaction in it
blocks = bt.get_consecutive_blocks(
5,
blocks,
farmer_reward_puzzle_hash=coinbase_puzzlehash,
pool_reward_puzzle_hash=receiver_puzzlehash,
transaction_data=spend_bundle,
guarantee_transaction_block=True,
)
# this block (height 10) refers back to the generator in block 5
spend_bundle2 = wallet_a.generate_signed_transaction(1000, receiver_puzzlehash, all_coins.pop())
blocks = bt.get_consecutive_blocks(4, blocks, previous_generator=[uint32(5)], transaction_data=spend_bundle2)
for block in blocks[:5]:
await _validate_and_add_block(b, block, expected_result=ReceiveBlockResult.NEW_PEAK)
# fake the fork_height to make every new block look like a reorg
for block in blocks[5:]:
await _validate_and_add_block(b, block, expected_result=ReceiveBlockResult.NEW_PEAK, fork_point_with_peak=2)
assert b.get_peak().height == 13
@pytest.mark.asyncio
async def test_chain_failed_rollback(empty_blockchain, bt):
b = empty_blockchain
wallet_a = WalletTool(b.constants)
WALLET_A_PUZZLE_HASHES = [wallet_a.get_new_puzzlehash() for _ in range(5)]
coinbase_puzzlehash = WALLET_A_PUZZLE_HASHES[0]
receiver_puzzlehash = WALLET_A_PUZZLE_HASHES[1]
blocks = bt.get_consecutive_blocks(
20,
farmer_reward_puzzle_hash=coinbase_puzzlehash,
pool_reward_puzzle_hash=receiver_puzzlehash,
)
for block in blocks:
await _validate_and_add_block(b, block)
assert b.get_peak().height == 19
print("first chain done")
# Make sure a ref back into the reorg chain itself works as expected
all_coins = []
for spend_block in blocks[:10]:
for coin in list(spend_block.get_included_reward_coins()):
if coin.puzzle_hash == coinbase_puzzlehash:
all_coins.append(coin)
spend_bundle = wallet_a.generate_signed_transaction(1000, receiver_puzzlehash, all_coins.pop())
blocks_reorg_chain = bt.get_consecutive_blocks(
11,
blocks[:10],
seed=b"2",
farmer_reward_puzzle_hash=coinbase_puzzlehash,
pool_reward_puzzle_hash=receiver_puzzlehash,
transaction_data=spend_bundle,
guarantee_transaction_block=True,
)
for block in blocks_reorg_chain[10:-1]:
await _validate_and_add_block(b, block, expected_result=ReceiveBlockResult.ADDED_AS_ORPHAN)
# Incorrectly set the height as spent in DB to trigger an error
print(f"{await b.coin_store.get_coin_record(spend_bundle.coin_spends[0].coin.name())}")
print(spend_bundle.coin_spends[0].coin.name())
# await b.coin_store._set_spent([spend_bundle.coin_spends[0].coin.name()], 8)
await b.coin_store.rollback_to_block(2)
print(f"{await b.coin_store.get_coin_record(spend_bundle.coin_spends[0].coin.name())}")
try:
await _validate_and_add_block(b, blocks_reorg_chain[-1])
except AssertionError:
pass
assert b.get_peak().height == 19
@pytest.mark.asyncio
async def test_reorg_flip_flop(empty_blockchain, bt):
b = empty_blockchain
wallet_a = WalletTool(b.constants)
WALLET_A_PUZZLE_HASHES = [wallet_a.get_new_puzzlehash() for _ in range(5)]
coinbase_puzzlehash = WALLET_A_PUZZLE_HASHES[0]
receiver_puzzlehash = WALLET_A_PUZZLE_HASHES[1]
chain_a = bt.get_consecutive_blocks(
10,
farmer_reward_puzzle_hash=coinbase_puzzlehash,
pool_reward_puzzle_hash=receiver_puzzlehash,
guarantee_transaction_block=True,
)
all_coins = []
for spend_block in chain_a:
for coin in list(spend_block.get_included_reward_coins()):
if coin.puzzle_hash == coinbase_puzzlehash:
all_coins.append(coin)
# this is a transaction block at height 10
spend_bundle = wallet_a.generate_signed_transaction(1000, receiver_puzzlehash, all_coins.pop())
chain_a = bt.get_consecutive_blocks(
5,
chain_a,
farmer_reward_puzzle_hash=coinbase_puzzlehash,
pool_reward_puzzle_hash=receiver_puzzlehash,
transaction_data=spend_bundle,
guarantee_transaction_block=True,
)
spend_bundle = wallet_a.generate_signed_transaction(1000, receiver_puzzlehash, all_coins.pop())
chain_a = bt.get_consecutive_blocks(5, chain_a, previous_generator=[uint32(10)], transaction_data=spend_bundle)
spend_bundle = wallet_a.generate_signed_transaction(1000, receiver_puzzlehash, all_coins.pop())
chain_a = bt.get_consecutive_blocks(
20,
chain_a,
farmer_reward_puzzle_hash=coinbase_puzzlehash,
pool_reward_puzzle_hash=receiver_puzzlehash,
transaction_data=spend_bundle,
guarantee_transaction_block=True,
)
# chain A is 40 blocks deep
# chain B share the first 20 blocks with chain A
# add 5 blocks on top of the first 20, to form chain B
chain_b = bt.get_consecutive_blocks(
5,
chain_a[:20],
seed=b"2",
farmer_reward_puzzle_hash=coinbase_puzzlehash,
pool_reward_puzzle_hash=receiver_puzzlehash,
)
spend_bundle = wallet_a.generate_signed_transaction(1000, receiver_puzzlehash, all_coins.pop())
# this is a transaction block at height 15 (in Chain B)
chain_b = bt.get_consecutive_blocks(
5,
chain_b,
seed=b"2",
farmer_reward_puzzle_hash=coinbase_puzzlehash,
pool_reward_puzzle_hash=receiver_puzzlehash,
transaction_data=spend_bundle,
guarantee_transaction_block=True,
)
spend_bundle = wallet_a.generate_signed_transaction(1000, receiver_puzzlehash, all_coins.pop())
chain_b = bt.get_consecutive_blocks(
10, chain_b, seed=b"2", previous_generator=[uint32(15)], transaction_data=spend_bundle
)
assert len(chain_a) == len(chain_b)
counter = 0
for b1, b2 in zip(chain_a, chain_b):
# alternate the order we add blocks from the two chains, to ensure one
# chain overtakes the other one in weight every other time
if counter % 2 == 0:
block1, block2 = b2, b1
else:
block1, block2 = b1, b2
counter += 1
fork_height = 2 if counter > 3 else None
preval: List[PreValidationResult] = await b.pre_validate_blocks_multiprocessing(
[block1], {}, validate_signatures=False
)
result, err, _, _ = await b.receive_block(block1, preval[0], fork_point_with_peak=fork_height)
assert not err
preval: List[PreValidationResult] = await b.pre_validate_blocks_multiprocessing(
[block2], {}, validate_signatures=False
)
result, err, _, _ = await b.receive_block(block2, preval[0], fork_point_with_peak=fork_height)
assert not err
assert b.get_peak().height == 39
chain_b = bt.get_consecutive_blocks(
10,
chain_b,
seed=b"2",
farmer_reward_puzzle_hash=coinbase_puzzlehash,
pool_reward_puzzle_hash=receiver_puzzlehash,
)
for block in chain_b[40:]:
await _validate_and_add_block(b, block)

View File

@ -371,7 +371,15 @@ class TestBlockHeightMap:
assert height_map.get_hash(5) == gen_block_hash(5)
height_map.rollback(5)
assert height_map.contains_height(0)
assert height_map.contains_height(1)
assert height_map.contains_height(2)
assert height_map.contains_height(3)
assert height_map.contains_height(4)
assert height_map.contains_height(5)
assert not height_map.contains_height(6)
assert not height_map.contains_height(7)
assert not height_map.contains_height(8)
assert height_map.get_hash(5) == gen_block_hash(5)
assert height_map.get_ses(0) == gen_ses(0)
@ -401,8 +409,12 @@ class TestBlockHeightMap:
assert height_map.get_hash(6) == gen_block_hash(6)
height_map.rollback(6)
assert height_map.contains_height(6)
assert not height_map.contains_height(7)
assert height_map.get_hash(6) == gen_block_hash(6)
with pytest.raises(AssertionError) as _:
height_map.get_hash(7)
assert height_map.get_ses(0) == gen_ses(0)
assert height_map.get_ses(2) == gen_ses(2)