Skip to content

Commit

Permalink
fix short_sync_backtrack (#18951)
Browse files Browse the repository at this point in the history
* fix short_sync_backtrack

* add test for a 1-block reorg in short_sync_backtrack() (#18966)

---------

Co-authored-by: Arvid Norberg <[email protected]>
  • Loading branch information
almogdepaz and arvidn authored Dec 4, 2024
1 parent 29d2eed commit fe9eb67
Show file tree
Hide file tree
Showing 2 changed files with 141 additions and 13 deletions.
145 changes: 134 additions & 11 deletions chia/_tests/core/full_node/test_full_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
from chia.simulator.keyring import TempKeyring
from chia.simulator.setup_services import setup_full_node
from chia.simulator.simulator_protocol import FarmNewBlockProtocol
from chia.simulator.wallet_tools import WalletTool
from chia.types.blockchain_format.classgroup import ClassgroupElement
from chia.types.blockchain_format.foliage import Foliage, FoliageTransactionBlock, TransactionsInfo
from chia.types.blockchain_format.program import Program
Expand Down Expand Up @@ -2299,9 +2300,18 @@ async def validate_coin_set(coin_store: CoinStore, blocks: list[FullBlock]) -> N
prev_hash = block.header_hash
rewards = block.get_included_reward_coins()
records = {rec.coin.name(): rec for rec in await coin_store.get_coins_added_at_height(block.height)}

# validate reward coins
for reward in rewards:
rec = records.pop(reward.name())
assert rec is not None
assert rec.confirmed_block_index == block.height
assert rec.coin == reward
assert rec.coinbase

if block.transactions_generator is None:
if len(records) > 0: # pragma: no cover
print(f"height: {block.height} rewards: {rewards} TX: No")
print(f"height: {block.height} unexpected coins in the DB: {records} TX: No")
print_coin_records(records)
assert records == {}
continue
Expand All @@ -2310,16 +2320,9 @@ async def validate_coin_set(coin_store: CoinStore, blocks: list[FullBlock]) -> N
# TODO: Support block references
assert False

# validate reward coins
for reward in rewards:
rec = records.pop(reward.name())
assert rec is not None
assert rec.confirmed_block_index == block.height
assert rec.coin == reward
assert rec.coinbase

flags = get_flags_for_height_and_constants(block.height, test_constants)
additions, removals = additions_and_removals(bytes(block.transactions_generator), [], flags, test_constants)

for add, hint in additions:
rec = records.pop(add.name())
assert rec is not None
Expand All @@ -2328,7 +2331,7 @@ async def validate_coin_set(coin_store: CoinStore, blocks: list[FullBlock]) -> N
assert not rec.coinbase

if len(records) > 0: # pragma: no cover
print(f"height: {block.height} rewards: {rewards} TX: Yes")
print(f"height: {block.height} unexpected coins in the DB: {records} TX: Yes")
print_coin_records(records)
assert records == {}

Expand All @@ -2340,7 +2343,7 @@ async def validate_coin_set(coin_store: CoinStore, blocks: list[FullBlock]) -> N
assert rec.coin == rem

if len(records) > 0: # pragma: no cover
print(f"height: {block.height} rewards: {rewards} TX: Yes")
print(f"height: {block.height} unexpected removals: {records} TX: Yes")
print_coin_records(records)
assert records == {}

Expand Down Expand Up @@ -2531,6 +2534,126 @@ def check_nodes_in_sync2():
await validate_coin_set(full_node_3.full_node._coin_store, blocks)


@pytest.mark.anyio
async def test_shallow_reorg_nodes(
three_nodes,
self_hostname: str,
bt: BlockTools,
):
full_node_1, full_node_2, _ = three_nodes

# node 1 has chan A, then we replace the top block and ensure
# node 2 follows along correctly

await connect_and_get_peer(full_node_1.full_node.server, full_node_2.full_node.server, self_hostname)

wallet_a = WalletTool(bt.constants)
WALLET_A_PUZZLE_HASHES = [wallet_a.get_new_puzzlehash() for _ in range(2)]
coinbase_puzzlehash = WALLET_A_PUZZLE_HASHES[0]
receiver_puzzlehash = WALLET_A_PUZZLE_HASHES[1]

chain = bt.get_consecutive_blocks(
10,
farmer_reward_puzzle_hash=coinbase_puzzlehash,
pool_reward_puzzle_hash=receiver_puzzlehash,
guarantee_transaction_block=True,
)
await add_blocks_in_batches(chain, full_node_1.full_node)

all_coins = []
for spend_block in chain:
for coin in spend_block.get_included_reward_coins():
if coin.puzzle_hash == coinbase_puzzlehash:
all_coins.append(coin)

def check_nodes_in_sync():
p1 = full_node_2.full_node.blockchain.get_peak()
p2 = full_node_1.full_node.blockchain.get_peak()
return p1 == p2

await time_out_assert(10, check_nodes_in_sync)
await validate_coin_set(full_node_1.full_node.blockchain.coin_store, chain)
await validate_coin_set(full_node_2.full_node.blockchain.coin_store, chain)

# we spend a coin in the next block
spend_bundle = wallet_a.generate_signed_transaction(uint64(1_000), receiver_puzzlehash, all_coins.pop())

# make a non transaction block with fewer iterations than a, which should
# replace it
chain_b = bt.get_consecutive_blocks(
1,
chain,
guarantee_transaction_block=False,
seed=b"{seed}",
)

chain_a = bt.get_consecutive_blocks(
1,
chain,
farmer_reward_puzzle_hash=coinbase_puzzlehash,
pool_reward_puzzle_hash=receiver_puzzlehash,
transaction_data=spend_bundle,
guarantee_transaction_block=True,
min_signage_point=chain_b[-1].reward_chain_block.signage_point_index,
)

print(f"chain A: {chain_a[-1].header_hash.hex()}")
print(f"chain B: {chain_b[-1].header_hash.hex()}")

assert chain_b[-1].total_iters < chain_a[-1].total_iters

await add_blocks_in_batches(chain_a[-1:], full_node_1.full_node, chain[-1].header_hash)

await time_out_assert(10, check_nodes_in_sync)
await validate_coin_set(full_node_1.full_node.blockchain.coin_store, chain_a)
await validate_coin_set(full_node_2.full_node.blockchain.coin_store, chain_a)

await add_blocks_in_batches(chain_b[-1:], full_node_1.full_node, chain[-1].header_hash)

# make sure node 1 reorged onto chain B
assert full_node_1.full_node.blockchain.get_peak().header_hash == chain_b[-1].header_hash

await time_out_assert(10, check_nodes_in_sync)
await validate_coin_set(full_node_1.full_node.blockchain.coin_store, chain_b)
await validate_coin_set(full_node_2.full_node.blockchain.coin_store, chain_b)

# now continue building the chain on top of B
# since spend_bundle was supposed to have been reorged-out, we should be
# able to include it in another block, howerver, since we replaced a TX
# block with a non-TX block, it won't be available immediately at height 11

# add a TX block, this will make spend_bundle valid in the next block
chain = bt.get_consecutive_blocks(
1,
chain,
farmer_reward_puzzle_hash=coinbase_puzzlehash,
pool_reward_puzzle_hash=receiver_puzzlehash,
guarantee_transaction_block=True,
)
for coin in chain[-1].get_included_reward_coins():
if coin.puzzle_hash == coinbase_puzzlehash:
all_coins.append(coin)

for i in range(3):
chain = bt.get_consecutive_blocks(
1,
chain,
farmer_reward_puzzle_hash=coinbase_puzzlehash,
pool_reward_puzzle_hash=receiver_puzzlehash,
transaction_data=spend_bundle,
guarantee_transaction_block=True,
)
for coin in chain[-1].get_included_reward_coins():
if coin.puzzle_hash == coinbase_puzzlehash:
all_coins.append(coin)
spend_bundle = wallet_a.generate_signed_transaction(uint64(1_000), receiver_puzzlehash, all_coins.pop())

await add_blocks_in_batches(chain[-4:], full_node_1.full_node, chain[-5].header_hash)
await time_out_assert(10, check_nodes_in_sync)
await validate_coin_set(full_node_1.full_node.blockchain.coin_store, chain)
await validate_coin_set(full_node_2.full_node.blockchain.coin_store, chain)


@pytest.mark.anyio
@pytest.mark.limit_consensus_modes(allowed=[ConsensusMode.HARD_FORK_2_0], reason="save time")
async def test_eviction_from_bls_cache(one_node_one_block: tuple[FullNodeSimulator, ChiaServer, BlockTools]) -> None:
Expand Down
9 changes: 7 additions & 2 deletions chia/full_node/full_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -704,10 +704,13 @@ async def short_sync_backtrack(
break
curr_height -= 1
if found_fork_point:
first_block = blocks[-1] # blocks are reveresd this is the lowest block to add
# we create the fork_info and pass it here so it would be updated on each call to add_block
fork_info = ForkInfo(first_block.height - 1, first_block.height - 1, first_block.prev_header_hash)
for block in reversed(blocks):
# when syncing, we won't share any signatures with the
# mempool, so there's no need to pass in the BLS cache.
await self.add_block(block, peer)
await self.add_block(block, peer, fork_info=fork_info)
except (asyncio.CancelledError, Exception):
self.sync_store.decrement_backtrack_syncing(node_id=peer.peer_node_id)
raise
Expand Down Expand Up @@ -1993,6 +1996,7 @@ async def add_block(
peer: Optional[WSChiaConnection] = None,
bls_cache: Optional[BLSCache] = None,
raise_on_disconnected: bool = False,
fork_info: Optional[ForkInfo] = None,
) -> Optional[Message]:
"""
Add a full block from a peer full node (or ourselves).
Expand Down Expand Up @@ -2119,7 +2123,8 @@ async def add_block(
f"{block.height}: {Err(pre_validation_result.error).name}"
)
else:
fork_info = ForkInfo(block.height - 1, block.height - 1, block.prev_header_hash)
if fork_info is None:
fork_info = ForkInfo(block.height - 1, block.height - 1, block.prev_header_hash)
(added, error_code, state_change_summary) = await self.blockchain.add_block(
block, pre_validation_result, ssi, fork_info
)
Expand Down

0 comments on commit fe9eb67

Please sign in to comment.