diff --git a/chia/simulator/setup_nodes.py b/chia/simulator/setup_nodes.py index 5483ad196e4a..239a64e42ec8 100644 --- a/chia/simulator/setup_nodes.py +++ b/chia/simulator/setup_nodes.py @@ -462,8 +462,13 @@ async def setup_full_system_inner( ) vdf1_port = uint16(find_available_listen_port("vdf1")) vdf2_port = uint16(find_available_listen_port("vdf2")) - timelord_iter = setup_timelord(full_node_0_port, False, consensus_constants, b_tools, vdf_port=vdf1_port) - timelord_bluebox_iter = setup_timelord(uint16(1000), True, consensus_constants, b_tools_1, vdf_port=vdf2_port) + + timelord_iter = setup_timelord( + full_node_0_port, False, consensus_constants, b_tools.config, b_tools.root_path, vdf_port=vdf1_port + ) + timelord_bluebox_iter = setup_timelord( + uint16(1000), True, consensus_constants, b_tools_1.config, b_tools_1.root_path, vdf_port=vdf2_port + ) harvester_service = await harvester_iter.__anext__() harvester = harvester_service._node diff --git a/chia/simulator/setup_services.py b/chia/simulator/setup_services.py index 17d22370e18d..edea2835d8a3 100644 --- a/chia/simulator/setup_services.py +++ b/chia/simulator/setup_services.py @@ -468,10 +468,10 @@ async def setup_timelord( full_node_port: int, sanitizer: bool, consensus_constants: ConsensusConstants, - b_tools: BlockTools, + config: Dict[str, Any], + root_path: Path, vdf_port: uint16 = uint16(0), ) -> AsyncGenerator[Service[Timelord, TimelordAPI], None]: - config = b_tools.config service_config = config["timelord"] service_config["full_node_peer"]["port"] = full_node_port service_config["bluebox_mode"] = sanitizer @@ -481,7 +481,7 @@ async def setup_timelord( service_config["rpc_port"] = uint16(0) service = create_timelord_service( - b_tools.root_path, + root_path, config, consensus_constants, connect_to_daemon=False, diff --git a/chia/timelord/timelord_api.py b/chia/timelord/timelord_api.py index a317e1bb7566..123c0d695c34 100644 --- a/chia/timelord/timelord_api.py +++ b/chia/timelord/timelord_api.py @@ -37,6 +37,20 @@ async def new_peak_timelord(self, new_peak: timelord_protocol.NewPeakTimelord) - if self.timelord.bluebox_mode: return None self.timelord.max_allowed_inactivity_time = 60 + + # if there is a heavier unfinished block from a diff chain, skip + for unf_block in self.timelord.unfinished_blocks: + if unf_block.reward_chain_block.total_iters > new_peak.reward_chain_block.total_iters: + found = False + for rc, total_iters in new_peak.previous_reward_challenges: + if rc == unf_block.rc_prev: + found = True + break + + if not found: + log.info("there is a heavier unfinished block that does not belong to this chain- skip peak") + return None + if new_peak.reward_chain_block.weight > self.timelord.last_state.get_weight(): log.info("Not skipping peak, don't have. Maybe we are not the fastest timelord") log.info( diff --git a/mypy-exclusions.txt b/mypy-exclusions.txt index f616c78cb441..eef517879191 100644 --- a/mypy-exclusions.txt +++ b/mypy-exclusions.txt @@ -153,6 +153,7 @@ tests.wallet.test_wallet_interested_store tests.wallet.test_wallet_key_val_store tests.wallet.test_wallet_user_store tests.weight_proof.test_weight_proof +tests.timelord.test_new_peak tools.analyze-chain tools.run_block tools.test_full_sync diff --git a/tests/conftest.py b/tests/conftest.py index 2748df3c8c03..ac89c12d43e4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -916,13 +916,13 @@ async def introducer_service(bt): @pytest_asyncio.fixture(scope="function") async def timelord(bt): - async for service in setup_timelord(uint16(0), False, bt.constants, bt): + async for service in setup_timelord(uint16(0), False, bt.constants, bt.config, bt.root_path): yield service._api, service._node.server @pytest_asyncio.fixture(scope="function") async def timelord_service(bt): - async for _ in setup_timelord(uint16(0), False, bt.constants, bt): + async for _ in setup_timelord(uint16(0), False, bt.constants, bt.config, bt.root_path): yield _ diff --git a/tests/timelord/__init__.py b/tests/timelord/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/timelord/config.py b/tests/timelord/config.py new file mode 100644 index 000000000000..9d6e160fe6e2 --- /dev/null +++ b/tests/timelord/config.py @@ -0,0 +1,4 @@ +from __future__ import annotations + +parallel = True +checkout_blocks_and_plots = True diff --git a/tests/timelord/test_new_peak.py b/tests/timelord/test_new_peak.py new file mode 100644 index 000000000000..7e9487d9c083 --- /dev/null +++ b/tests/timelord/test_new_peak.py @@ -0,0 +1,212 @@ +from __future__ import annotations + +from typing import List, Optional, Tuple + +import pytest + +from chia.consensus.block_record import BlockRecord +from chia.consensus.blockchain_interface import BlockchainInterface +from chia.consensus.constants import ConsensusConstants +from chia.consensus.difficulty_adjustment import get_next_sub_slot_iters_and_difficulty +from chia.consensus.make_sub_epoch_summary import next_sub_epoch_summary +from chia.protocols import timelord_protocol +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.blockchain_format.sub_epoch_summary import SubEpochSummary +from chia.util.ints import uint128 +from tests.blockchain.blockchain_test_utils import _validate_and_add_block +from tests.util.blockchain import create_blockchain + + +class TestNewPeak: + @pytest.mark.asyncio + async def test_timelord_new_peak_basic(self, bt, timelord, default_1000_blocks) -> None: + b1, db_wrapper1, db_path1 = await create_blockchain(bt.constants, 2) + b2, db_wrapper2, db_path2 = await create_blockchain(bt.constants, 2) + + timelord_api, timelord_server = timelord + for block in default_1000_blocks: + await _validate_and_add_block(b1, block) + await _validate_and_add_block(b2, block) + + peak = timelord_peak_from_block(default_1000_blocks[-1], b1, bt.constants) + assert peak is not None + assert timelord_api.timelord.new_peak is None + await timelord_api.new_peak_timelord(peak) + assert timelord_api.timelord.new_peak is not None + assert timelord_api.timelord.new_peak.reward_chain_block.height == peak.reward_chain_block.height + blocks = bt.get_consecutive_blocks(1, default_1000_blocks) + await _validate_and_add_block(b1, blocks[-1]) + await _validate_and_add_block(b2, blocks[-1]) + + await timelord_api.new_peak_timelord(timelord_peak_from_block(blocks[-1], b1, bt.constants)) + assert timelord_api.timelord.new_peak.reward_chain_block.height == blocks[-1].height + + blocks_1 = bt.get_consecutive_blocks(2, blocks) + await _validate_and_add_block(b1, blocks_1[-2]) + await _validate_and_add_block(b1, blocks_1[-1]) + await timelord_api.new_peak_timelord(timelord_peak_from_block(blocks_1[-2], b1, bt.constants)) + await timelord_api.new_peak_timelord(timelord_peak_from_block(blocks_1[-1], b1, bt.constants)) + assert timelord_api.timelord.new_peak.reward_chain_block.height == blocks_1[-1].height + + # # new unknown peak, weight less then curr peak + # blocks_2 = bt.get_consecutive_blocks(1, blocks) + # await _validate_and_add_block(b2, blocks_2[-1]) + # await timelord_api.new_peak_timelord(timelord_peak_from_block(blocks_2[-1], b2, bt.constants)) + # assert timelord_api.timelord.new_peak.reward_chain_block.height == blocks_1[-1].height + + await db_wrapper1.close() + await db_wrapper2.close() + b1.shut_down() + b2.shut_down() + db_path1.unlink() + db_path2.unlink() + + return None + + @pytest.mark.asyncio + async def test_timelord_new_peak_heavier_unfinished(self, bt, timelord, default_1000_blocks) -> None: + b1, db_wrapper1, db_path1 = await create_blockchain(bt.constants, 2) + b2, db_wrapper2, db_path2 = await create_blockchain(bt.constants, 2) + + timelord_api, timelord_server = timelord + for block in default_1000_blocks: + await _validate_and_add_block(b1, block) + await _validate_and_add_block(b2, block) + + peak = timelord_peak_from_block(default_1000_blocks[-1], b1, bt.constants) + assert peak is not None + assert timelord_api.timelord.new_peak is None + await timelord_api.new_peak_timelord(peak) + assert timelord_api.timelord.new_peak is not None + assert timelord_api.timelord.new_peak.reward_chain_block.height == peak.reward_chain_block.height + + # make two new blocks on tip + blocks_1 = bt.get_consecutive_blocks(2, default_1000_blocks) + await _validate_and_add_block(b1, blocks_1[-2]) + await _validate_and_add_block(b1, blocks_1[-1]) + blocks_2 = bt.get_consecutive_blocks(1, default_1000_blocks) + await _validate_and_add_block(b2, blocks_2[-1]) + block_record = b1.block_record(blocks_1[-1].header_hash) + block = blocks_1[-1] + + ses: Optional[SubEpochSummary] = next_sub_epoch_summary( + bt.constants, + b1, + block_record.required_iters, + block, + True, + ) + + sub_slot_iters, difficulty = get_next_sub_slot_iters_and_difficulty( + bt.constants, + len(block.finished_sub_slots) > 0, + b1.block_record(blocks_1[-1].prev_header_hash), + b1, + ) + + if block.reward_chain_block.signage_point_index == 0: + # find first in slot and find slot challenge + blk = b1.block_record(blocks_1[-1].header_hash) + while blk.first_in_sub_slot is False: + blk = b1.block_record(blocks_1[-1].prev_header_hash) + full_blk = await b1.get_full_block(blk.header_hash) + sub_slot = None + for s in full_blk.finished_sub_slots: + if s is not None and s.challenge_chain.get_hash() == block.reward_chain_block.pos_ss_cc_challenge_hash: + sub_slot = s + if sub_slot is None: + assert block.reward_chain_block.pos_ss_cc_challenge_hash == bt.constants.GENESIS_CHALLENGE + rc_prev = bt.constants.GENESIS_CHALLENGE + else: + rc_prev = sub_slot.reward_chain.get_hash() + else: + assert block.reward_chain_block.reward_chain_sp_vdf is not None + rc_prev = block.reward_chain_block.reward_chain_sp_vdf.challenge + + timelord_unf_block = timelord_protocol.NewUnfinishedBlockTimelord( + block.reward_chain_block.get_unfinished(), + difficulty, + sub_slot_iters, + block.foliage, + ses, + rc_prev, + ) + + timelord_api.new_unfinished_block_timelord(timelord_unf_block) + + await timelord_api.new_peak_timelord(timelord_peak_from_block(blocks_2[-1], b2, bt.constants)) + assert timelord_api.timelord.last_state.get_height() == peak.reward_chain_block.height + + await db_wrapper1.close() + await db_wrapper2.close() + b1.shut_down() + b2.shut_down() + db_path1.unlink() + db_path2.unlink() + + return None + + +def get_recent_reward_challenges( + blockchain: BlockchainInterface, constants: ConsensusConstants +) -> List[Tuple[bytes32, uint128]]: + peak = blockchain.get_peak() + if peak is None: + return [] + recent_rc: List[Tuple[bytes32, uint128]] = [] + curr: Optional[BlockRecord] = peak + while curr is not None and len(recent_rc) < 2 * constants.MAX_SUB_SLOT_BLOCKS: + if curr != peak: + recent_rc.append((curr.reward_infusion_new_challenge, curr.total_iters)) + if curr.first_in_sub_slot: + assert curr.finished_reward_slot_hashes is not None + sub_slot_total_iters = curr.ip_sub_slot_total_iters(constants) + # Start from the most recent + for rc in reversed(curr.finished_reward_slot_hashes): + if sub_slot_total_iters < curr.sub_slot_iters: + break + recent_rc.append((rc, sub_slot_total_iters)) + sub_slot_total_iters = uint128(sub_slot_total_iters - curr.sub_slot_iters) + curr = blockchain.try_block_record(curr.prev_hash) + return list(reversed(recent_rc)) + + +def timelord_peak_from_block(block, blockchain: BlockchainInterface, constants: ConsensusConstants): + peak = blockchain.block_record(block.header_hash) + _, difficulty = get_next_sub_slot_iters_and_difficulty(constants, False, peak, blockchain) + ses: Optional[SubEpochSummary] = next_sub_epoch_summary( + constants, + blockchain, + peak.required_iters, + block, + True, + ) + + recent_rc = get_recent_reward_challenges(blockchain, constants) + curr = peak + while not curr.is_challenge_block(constants) and not curr.first_in_sub_slot: + curr = blockchain.block_record(curr.prev_hash) + + if curr.is_challenge_block(constants): + last_csb_or_eos = curr.total_iters + else: + last_csb_or_eos = curr.ip_sub_slot_total_iters(constants) + + curr = peak + passed_ses_height_but_not_yet_included = True + while (curr.height % constants.SUB_EPOCH_BLOCKS) != 0: + if curr.sub_epoch_summary_included: + passed_ses_height_but_not_yet_included = False + curr = blockchain.block_record(curr.prev_hash) + if curr.sub_epoch_summary_included or curr.height == 0: + passed_ses_height_but_not_yet_included = False + return timelord_protocol.NewPeakTimelord( + block.reward_chain_block, + difficulty, + peak.deficit, + peak.sub_slot_iters, + ses, + recent_rc, + last_csb_or_eos, + passed_ses_height_but_not_yet_included, + )