mirror of
https://github.com/Chia-Network/chia-blockchain.git
synced 2024-09-20 08:05:33 +03:00
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:
parent
908f186c1e
commit
8833cc351c
@ -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
|
||||
|
@ -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])
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user