From b0aac77c2b90929a059fd33d299f68deeaa865cc Mon Sep 17 00:00:00 2001 From: almogdepaz Date: Sun, 6 Jul 2025 18:34:48 +0300 Subject: [PATCH 01/42] add solver service --- chia/protocols/farmer_protocol.py | 7 ++ chia/protocols/outbound_message.py | 1 + chia/protocols/protocol_message_types.py | 6 ++ chia/protocols/solver_protocol.py | 17 ++++ chia/server/start_solver.py | 114 +++++++++++++++++++++++ chia/solver/solver.py | 85 +++++++++++++++++ chia/solver/solver_api.py | 55 +++++++++++ 7 files changed, 285 insertions(+) create mode 100644 chia/protocols/solver_protocol.py create mode 100644 chia/server/start_solver.py create mode 100644 chia/solver/solver.py create mode 100644 chia/solver/solver_api.py diff --git a/chia/protocols/farmer_protocol.py b/chia/protocols/farmer_protocol.py index 0ee840f98931..d34fe7bcda97 100644 --- a/chia/protocols/farmer_protocol.py +++ b/chia/protocols/farmer_protocol.py @@ -105,3 +105,10 @@ class SignedValues(Streamable): quality_string: bytes32 foliage_block_data_signature: G2Element foliage_transaction_block_signature: G2Element + + + +@streamable +@dataclass(frozen=True) +class SolutionResponse(Streamable): + proof: bytes \ No newline at end of file diff --git a/chia/protocols/outbound_message.py b/chia/protocols/outbound_message.py index e3632fa459d4..bf75f2f6a9e0 100644 --- a/chia/protocols/outbound_message.py +++ b/chia/protocols/outbound_message.py @@ -18,6 +18,7 @@ class NodeType(IntEnum): INTRODUCER = 5 WALLET = 6 DATA_LAYER = 7 + SOLVER = 8 @streamable diff --git a/chia/protocols/protocol_message_types.py b/chia/protocols/protocol_message_types.py index 3aea02990a45..97c49331a22f 100644 --- a/chia/protocols/protocol_message_types.py +++ b/chia/protocols/protocol_message_types.py @@ -136,4 +136,10 @@ class ProtocolMessageTypes(Enum): request_cost_info = 106 respond_cost_info = 107 + # new farmer protocol messages + solution_resonse = 108 + + # solver protocol + solve = 109 + error = 255 diff --git a/chia/protocols/solver_protocol.py b/chia/protocols/solver_protocol.py new file mode 100644 index 000000000000..b7989a15b9bb --- /dev/null +++ b/chia/protocols/solver_protocol.py @@ -0,0 +1,17 @@ + +from dataclasses import dataclass + +from chia_rs import PlotSize +from chia_rs.sized_bytes import bytes32 +from chia_rs.sized_ints import uint64 +from chia.util.streamable import Streamable, streamable + + + + +@streamable +@dataclass(frozen=True) +class SolverInfo(Streamable): + plot_size: PlotSize + plot_diffculty: uint64 + quality_string: bytes32 \ No newline at end of file diff --git a/chia/server/start_solver.py b/chia/server/start_solver.py new file mode 100644 index 000000000000..b7b7df3ee0a5 --- /dev/null +++ b/chia/server/start_solver.py @@ -0,0 +1,114 @@ +from __future__ import annotations + +import os +import pathlib +import sys +from multiprocessing import freeze_support +from typing import Any, Optional + +from chia_rs import ConsensusConstants +from chia_rs.sized_ints import uint16 + +from chia.apis import ApiProtocolRegistry +from chia.consensus.constants import replace_str_to_bytes +from chia.consensus.default_constants import DEFAULT_CONSTANTS, update_testnet_overrides +from chia.full_node.full_node import FullNode +from chia.full_node.full_node_api import FullNodeAPI +from chia.full_node.full_node_rpc_api import FullNodeRpcApi +from chia.protocols.outbound_message import NodeType +from chia.server.aliases import FullNodeService +from chia.server.signal_handlers import SignalHandlers +from chia.server.start_service import RpcInfo, Service, async_run +from chia.util.chia_logging import initialize_service_logging +from chia.util.config import load_config, load_config_cli +from chia.util.default_root import resolve_root_path +from chia.util.task_timing import maybe_manage_task_instrumentation + +# See: https://bugs.python.org/issue29288 +"".encode("idna") + +SERVICE_NAME = "solver" + + +async def create_solver_service( + root_path: pathlib.Path, + config: dict[str, Any], + consensus_constants: ConsensusConstants, + connect_to_daemon: bool = True, + override_capabilities: Optional[list[tuple[uint16, str]]] = None, +) -> FullNodeService: + service_config = config[SERVICE_NAME] + + network_id = service_config["selected_network"] + upnp_list = [] + if service_config["enable_upnp"]: + upnp_list = [service_config["port"]] + + node = await FullNode.create( + service_config, + root_path=root_path, + consensus_constants=consensus_constants, + ) + peer_api = FullNodeAPI(node) + + rpc_info: Optional[RpcInfo[FullNodeRpcApi]] = None + if service_config.get("start_rpc_server", True): + rpc_info = (FullNodeRpcApi, service_config["rpc_port"]) + + return Service( + root_path=root_path, + config=config, + node=node, + peer_api=peer_api, + node_type=NodeType.SOLVER, + advertised_port=service_config["port"], + service_name=SERVICE_NAME, + upnp_ports=upnp_list, + # connect_peers=get_unresolved_peer_infos(service_config, NodeType.SOLVER), + on_connect_callback=node.on_connect, + network_id=network_id, + rpc_info=rpc_info, + connect_to_daemon=connect_to_daemon, + override_capabilities=override_capabilities, + class_for_type=ApiProtocolRegistry, + ) + + +async def async_main(service_config: dict[str, Any], root_path: pathlib.Path) -> int: + # TODO: refactor to avoid the double load + config = load_config(root_path, "config.yaml") + config[SERVICE_NAME] = service_config + network_id = service_config["selected_network"] + overrides = service_config["network_overrides"]["constants"][network_id] + update_testnet_overrides(network_id, overrides) + updated_constants = replace_str_to_bytes(DEFAULT_CONSTANTS, **overrides) + initialize_service_logging(service_name=SERVICE_NAME, config=config, root_path=root_path) + + service = await create_solver_service(root_path, config, updated_constants) + async with SignalHandlers.manage() as signal_handlers: + await service.setup_process_global_state(signal_handlers=signal_handlers) + await service.run() + + return 0 + + +def main() -> int: + freeze_support() + root_path = resolve_root_path(override=None) + + with maybe_manage_task_instrumentation( + enable=os.environ.get(f"CHIA_INSTRUMENT_{SERVICE_NAME.upper()}") is not None + ): + service_config = load_config_cli(root_path, "config.yaml", SERVICE_NAME) + # target_peer_count = service_config.get("target_peer_count", 40) - service_config.get( + # "target_outbound_peer_count", 8 + # ) + # if target_peer_count < 0: + # target_peer_count = None + # if not service_config.get("use_chia_loop_policy", True): + # target_peer_count = None + return async_run(coro=async_main(service_config, root_path=root_path)) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/chia/solver/solver.py b/chia/solver/solver.py new file mode 100644 index 000000000000..d3b79db99296 --- /dev/null +++ b/chia/solver/solver.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +import asyncio +import concurrent +import contextlib +import logging +from collections.abc import AsyncIterator +from concurrent.futures.thread import ThreadPoolExecutor +from pathlib import Path +from typing import TYPE_CHECKING, Any, ClassVar, Optional, cast +from chia_rs import ConsensusConstants +from chia.protocols.solver_protocol import SolverInfo +from chia.protocols.outbound_message import NodeType +from chia.rpc.rpc_server import StateChangedProtocol, default_get_connections +from chia.server.server import ChiaServer +from chia.server.ws_connection import WSChiaConnection + + +log = logging.getLogger(__name__) + + +class Solver: + if TYPE_CHECKING: + from chia.rpc.rpc_server import RpcServiceProtocol + _protocol_check: ClassVar[RpcServiceProtocol] = cast("Solver", None) + + root_path: Path + _server: Optional[ChiaServer] + _shut_down: bool + started: bool = False + executor: ThreadPoolExecutor + state_changed_callback: Optional[StateChangedProtocol] = None + constants: ConsensusConstants + event_loop: asyncio.events.AbstractEventLoop + + + + @property + def server(self) -> ChiaServer: + if self._server is None: + raise RuntimeError("server not assigned") + + return self._server + + def __init__(self, root_path: Path, config: dict[str, Any], constants: ConsensusConstants): + self.log = log + self.root_path = root_path + self._shut_down = False + self.executor = concurrent.futures.ThreadPoolExecutor( + max_workers=config["num_threads"], thread_name_prefix="solver-" + ) + self._server = None + self.constants = constants + self.state_changed_callback: Optional[StateChangedProtocol] = None + + + + @contextlib.asynccontextmanager + async def manage(self) -> AsyncIterator[None]: + try: + self.started = True + yield + finally: + self._shut_down = True + + def solve(self, info: SolverInfo) -> Optional[bytes]: + return None + + def get_connections(self, request_node_type: Optional[NodeType]) -> list[dict[str, Any]]: + return default_get_connections(server=self.server, request_node_type=request_node_type) + + async def on_connect(self, connection: WSChiaConnection) -> None: + pass + + + async def on_disconnect(self, connection: WSChiaConnection) -> None: + self.log.info(f"peer disconnected {connection.get_peer_logging()}") + + + def set_server(self, server: ChiaServer) -> None: + self._server = server + + def _set_state_changed_callback(self, callback: StateChangedProtocol) -> None: + self.state_changed_callback = callback + \ No newline at end of file diff --git a/chia/solver/solver_api.py b/chia/solver/solver_api.py new file mode 100644 index 000000000000..5034d5b65cc0 --- /dev/null +++ b/chia/solver/solver_api.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +import json +import logging +import time +from typing import TYPE_CHECKING, Any, ClassVar, Optional, Union, cast + +import aiohttp +from chia.protocols.outbound_message import make_msg +from chia_rs.sized_bytes import bytes32 +from chia_rs.sized_ints import uint8, uint16, uint32, uint64 + +from chia import __version__ +from chia.protocols.farmer_protocol import SolutionResponse +from chia.protocols.outbound_message import Message +from chia.protocols.protocol_message_types import ProtocolMessageTypes +from chia.protocols.solver_protocol import SolverInfo +from chia.server.api_protocol import ApiMetadata +from chia.solver.solver import Solver + + + +class SolverAPI: + if TYPE_CHECKING: + from chia.server.api_protocol import ApiProtocol + _protocol_check: ClassVar[ApiProtocol] = cast("SolverAPI", None) + + log: logging.Logger + solver: Solver + metadata: ClassVar[ApiMetadata] = ApiMetadata() + + def __init__(self, solver: Solver) -> None: + self.log = logging.getLogger(__name__) + self.solver = solver + + def ready(self) -> bool: + return self.solver.started + + @metadata.request() + async def solve( + self, + request: SolverInfo, + ) -> Optional[Message]: + if not self.solver.started: + raise RuntimeError("Solver is not started") + + proof = self.solver.solve(request) + if proof is None: + return None + + response: SolutionResponse = SolutionResponse( + proof=proof, + ) + return make_msg(ProtocolMessageTypes.solution_resonse,response) + \ No newline at end of file From 27209c80cb004c478927018ceab9448e32f33a33 Mon Sep 17 00:00:00 2001 From: almogdepaz Date: Sun, 6 Jul 2025 18:35:29 +0300 Subject: [PATCH 02/42] lint --- chia/protocols/farmer_protocol.py | 3 +-- chia/protocols/solver_protocol.py | 8 ++++---- chia/solver/__init__.py | 0 chia/solver/solver.py | 19 +++++++------------ chia/solver/solver_api.py | 25 ++++++++----------------- 5 files changed, 20 insertions(+), 35 deletions(-) create mode 100644 chia/solver/__init__.py diff --git a/chia/protocols/farmer_protocol.py b/chia/protocols/farmer_protocol.py index d34fe7bcda97..97251d074ee0 100644 --- a/chia/protocols/farmer_protocol.py +++ b/chia/protocols/farmer_protocol.py @@ -107,8 +107,7 @@ class SignedValues(Streamable): foliage_transaction_block_signature: G2Element - @streamable @dataclass(frozen=True) class SolutionResponse(Streamable): - proof: bytes \ No newline at end of file + proof: bytes diff --git a/chia/protocols/solver_protocol.py b/chia/protocols/solver_protocol.py index b7989a15b9bb..8a5669910e8a 100644 --- a/chia/protocols/solver_protocol.py +++ b/chia/protocols/solver_protocol.py @@ -1,17 +1,17 @@ +from __future__ import annotations from dataclasses import dataclass from chia_rs import PlotSize from chia_rs.sized_bytes import bytes32 from chia_rs.sized_ints import uint64 -from chia.util.streamable import Streamable, streamable - +from chia.util.streamable import Streamable, streamable @streamable @dataclass(frozen=True) class SolverInfo(Streamable): - plot_size: PlotSize + plot_size: PlotSize plot_diffculty: uint64 - quality_string: bytes32 \ No newline at end of file + quality_string: bytes32 diff --git a/chia/solver/__init__.py b/chia/solver/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/chia/solver/solver.py b/chia/solver/solver.py index d3b79db99296..20eea23f8441 100644 --- a/chia/solver/solver.py +++ b/chia/solver/solver.py @@ -8,35 +8,35 @@ from concurrent.futures.thread import ThreadPoolExecutor from pathlib import Path from typing import TYPE_CHECKING, Any, ClassVar, Optional, cast + from chia_rs import ConsensusConstants -from chia.protocols.solver_protocol import SolverInfo + from chia.protocols.outbound_message import NodeType +from chia.protocols.solver_protocol import SolverInfo from chia.rpc.rpc_server import StateChangedProtocol, default_get_connections from chia.server.server import ChiaServer from chia.server.ws_connection import WSChiaConnection - log = logging.getLogger(__name__) class Solver: if TYPE_CHECKING: from chia.rpc.rpc_server import RpcServiceProtocol + _protocol_check: ClassVar[RpcServiceProtocol] = cast("Solver", None) root_path: Path - _server: Optional[ChiaServer] + _server: Optional[ChiaServer] _shut_down: bool started: bool = False executor: ThreadPoolExecutor state_changed_callback: Optional[StateChangedProtocol] = None constants: ConsensusConstants event_loop: asyncio.events.AbstractEventLoop - - @property - def server(self) -> ChiaServer: + def server(self) -> ChiaServer: if self._server is None: raise RuntimeError("server not assigned") @@ -52,8 +52,6 @@ def __init__(self, root_path: Path, config: dict[str, Any], constants: Consensus self._server = None self.constants = constants self.state_changed_callback: Optional[StateChangedProtocol] = None - - @contextlib.asynccontextmanager async def manage(self) -> AsyncIterator[None]: @@ -63,7 +61,7 @@ async def manage(self) -> AsyncIterator[None]: finally: self._shut_down = True - def solve(self, info: SolverInfo) -> Optional[bytes]: + def solve(self, info: SolverInfo) -> Optional[bytes]: return None def get_connections(self, request_node_type: Optional[NodeType]) -> list[dict[str, Any]]: @@ -72,14 +70,11 @@ def get_connections(self, request_node_type: Optional[NodeType]) -> list[dict[st async def on_connect(self, connection: WSChiaConnection) -> None: pass - async def on_disconnect(self, connection: WSChiaConnection) -> None: self.log.info(f"peer disconnected {connection.get_peer_logging()}") - def set_server(self, server: ChiaServer) -> None: self._server = server def _set_state_changed_callback(self, callback: StateChangedProtocol) -> None: self.state_changed_callback = callback - \ No newline at end of file diff --git a/chia/solver/solver_api.py b/chia/solver/solver_api.py index 5034d5b65cc0..6cc343e190be 100644 --- a/chia/solver/solver_api.py +++ b/chia/solver/solver_api.py @@ -1,28 +1,20 @@ from __future__ import annotations -import json import logging -import time -from typing import TYPE_CHECKING, Any, ClassVar, Optional, Union, cast +from typing import TYPE_CHECKING, ClassVar, Optional, cast -import aiohttp -from chia.protocols.outbound_message import make_msg -from chia_rs.sized_bytes import bytes32 -from chia_rs.sized_ints import uint8, uint16, uint32, uint64 - -from chia import __version__ from chia.protocols.farmer_protocol import SolutionResponse -from chia.protocols.outbound_message import Message +from chia.protocols.outbound_message import Message, make_msg from chia.protocols.protocol_message_types import ProtocolMessageTypes from chia.protocols.solver_protocol import SolverInfo from chia.server.api_protocol import ApiMetadata from chia.solver.solver import Solver - class SolverAPI: if TYPE_CHECKING: from chia.server.api_protocol import ApiProtocol + _protocol_check: ClassVar[ApiProtocol] = cast("SolverAPI", None) log: logging.Logger @@ -40,16 +32,15 @@ def ready(self) -> bool: async def solve( self, request: SolverInfo, - ) -> Optional[Message]: + ) -> Optional[Message]: if not self.solver.started: raise RuntimeError("Solver is not started") proof = self.solver.solve(request) if proof is None: - return None - + return None + response: SolutionResponse = SolutionResponse( proof=proof, - ) - return make_msg(ProtocolMessageTypes.solution_resonse,response) - \ No newline at end of file + ) + return make_msg(ProtocolMessageTypes.solution_resonse, response) From cc1e06b856e60a23d97e78f91d2bd9f43f4f3808 Mon Sep 17 00:00:00 2001 From: almogdepaz Date: Mon, 7 Jul 2025 18:16:27 +0300 Subject: [PATCH 03/42] wire service evrywhear --- chia/_tests/conftest.py | 8 ++++++++ chia/_tests/solver/__init__.py | 0 chia/_tests/solver/test_solver.py | 30 ++++++++++++++++++++++++++++++ chia/_tests/util/setup_nodes.py | 11 +++++++++++ chia/protocols/shared_protocol.py | 1 + chia/protocols/solver_protocol.py | 5 ++--- chia/server/aliases.py | 3 +++ chia/server/server.py | 10 ++++++++-- chia/server/start_solver.py | 25 ++++++++++--------------- chia/simulator/setup_services.py | 26 ++++++++++++++++++++++++++ chia/solver/solver.py | 1 + chia/ssl/create_ssl.py | 1 + chia/util/initial-config.yaml | 11 +++++++++++ 13 files changed, 112 insertions(+), 20 deletions(-) create mode 100644 chia/_tests/solver/__init__.py create mode 100644 chia/_tests/solver/test_solver.py diff --git a/chia/_tests/conftest.py b/chia/_tests/conftest.py index c01042c453c1..747bade3a2f0 100644 --- a/chia/_tests/conftest.py +++ b/chia/_tests/conftest.py @@ -58,6 +58,7 @@ FarmerService, FullNodeService, HarvesterService, + SolverService, TimelordService, WalletService, ) @@ -70,6 +71,7 @@ setup_full_node, setup_introducer, setup_seeder, + setup_solver, setup_timelord, ) from chia.simulator.start_simulator import SimulatorFullNodeService @@ -1115,6 +1117,12 @@ async def seeder_service(root_path_populated_with_config: Path, database_uri: st yield seeder +@pytest.fixture(scope="function") +async def solver_service(bt: BlockTools) -> AsyncIterator[SolverService]: + async with setup_solver(bt.root_path, bt.constants) as _: + yield _ + + @pytest.fixture(scope="function") def tmp_chia_root(tmp_path): """ diff --git a/chia/_tests/solver/__init__.py b/chia/_tests/solver/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/chia/_tests/solver/test_solver.py b/chia/_tests/solver/test_solver.py new file mode 100644 index 000000000000..4fea5ec154d2 --- /dev/null +++ b/chia/_tests/solver/test_solver.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +import pytest +from chia_rs.sized_bytes import bytes32 +from chia_rs.sized_ints import uint8, uint64 + +from chia.protocols.solver_protocol import SolverInfo +from chia.server.aliases import SolverService + + +@pytest.mark.anyio +async def test_solver_solve(solver_service: SolverService) -> None: + """Test that the solver service can process a solve request.""" + solver = solver_service._node + solver_api = solver_service._api + + # Create test SolverInfo + test_info = SolverInfo(plot_size=uint8(32), plot_diffculty=uint64(1000), quality_string=bytes32.zeros) + + # Call solve directly on the solver + result = solver.solve(test_info) + + # Should return None since it's not implemented + assert result is None + + # Test through the API + api_result = await solver_api.solve(test_info) + + # Should return None since solver.solve returns None + assert api_result is None diff --git a/chia/_tests/util/setup_nodes.py b/chia/_tests/util/setup_nodes.py index f8aa40ab3f0f..71c2a3456685 100644 --- a/chia/_tests/util/setup_nodes.py +++ b/chia/_tests/util/setup_nodes.py @@ -33,6 +33,7 @@ setup_full_node, setup_harvester, setup_introducer, + setup_solver, setup_timelord, setup_vdf_client, setup_vdf_clients, @@ -469,6 +470,16 @@ async def setup_full_system_inner( await asyncio.sleep(backoff) + setup_solver( + b_tools, + shared_b_tools.root_path / "harvester", + UnresolvedPeerInfo(self_hostname, farmer_service._server.get_port()), + consensus_constants, + ) + # solver_service = await async_exit_stack.enter_async_context( + + # ) + full_system = FullSystem( node_1=node_1, node_2=node_2, diff --git a/chia/protocols/shared_protocol.py b/chia/protocols/shared_protocol.py index b628ca7ddb2b..e7f7812bd7e2 100644 --- a/chia/protocols/shared_protocol.py +++ b/chia/protocols/shared_protocol.py @@ -65,6 +65,7 @@ class Capability(IntEnum): NodeType.INTRODUCER: _capabilities, NodeType.WALLET: _capabilities, NodeType.DATA_LAYER: _capabilities, + NodeType.SOLVER: [], } diff --git a/chia/protocols/solver_protocol.py b/chia/protocols/solver_protocol.py index 8a5669910e8a..f95bcc12c44c 100644 --- a/chia/protocols/solver_protocol.py +++ b/chia/protocols/solver_protocol.py @@ -2,9 +2,8 @@ from dataclasses import dataclass -from chia_rs import PlotSize from chia_rs.sized_bytes import bytes32 -from chia_rs.sized_ints import uint64 +from chia_rs.sized_ints import uint8, uint64 from chia.util.streamable import Streamable, streamable @@ -12,6 +11,6 @@ @streamable @dataclass(frozen=True) class SolverInfo(Streamable): - plot_size: PlotSize + plot_size: uint8 plot_diffculty: uint64 quality_string: bytes32 diff --git a/chia/server/aliases.py b/chia/server/aliases.py index 7eb09a1eb4f5..cae3d83a891b 100644 --- a/chia/server/aliases.py +++ b/chia/server/aliases.py @@ -18,6 +18,8 @@ from chia.seeder.crawler_api import CrawlerAPI from chia.seeder.crawler_rpc_api import CrawlerRpcApi from chia.server.start_service import Service +from chia.solver.solver import Solver +from chia.solver.solver_api import SolverAPI from chia.timelord.timelord import Timelord from chia.timelord.timelord_api import TimelordAPI from chia.timelord.timelord_rpc_api import TimelordRpcApi @@ -33,3 +35,4 @@ IntroducerService = Service[Introducer, IntroducerAPI, FullNodeRpcApi] TimelordService = Service[Timelord, TimelordAPI, TimelordRpcApi] WalletService = Service[WalletNode, WalletNodeAPI, WalletRpcApi] +SolverService = Service[Solver, SolverAPI, FullNodeRpcApi] diff --git a/chia/server/server.py b/chia/server/server.py index 7b64c7877afa..c7b39d52f1b0 100644 --- a/chia/server/server.py +++ b/chia/server/server.py @@ -172,8 +172,14 @@ def create( private_cert_path, private_key_path = None, None public_cert_path, public_key_path = None, None - authenticated_client_types = {NodeType.HARVESTER} - authenticated_server_types = {NodeType.HARVESTER, NodeType.FARMER, NodeType.WALLET, NodeType.DATA_LAYER} + authenticated_client_types = {NodeType.HARVESTER, NodeType.SOLVER} + authenticated_server_types = { + NodeType.HARVESTER, + NodeType.FARMER, + NodeType.WALLET, + NodeType.DATA_LAYER, + NodeType.SOLVER, + } if local_type in authenticated_client_types: # Authenticated clients diff --git a/chia/server/start_solver.py b/chia/server/start_solver.py index b7b7df3ee0a5..b0ae49f58303 100644 --- a/chia/server/start_solver.py +++ b/chia/server/start_solver.py @@ -12,13 +12,13 @@ from chia.apis import ApiProtocolRegistry from chia.consensus.constants import replace_str_to_bytes from chia.consensus.default_constants import DEFAULT_CONSTANTS, update_testnet_overrides -from chia.full_node.full_node import FullNode -from chia.full_node.full_node_api import FullNodeAPI from chia.full_node.full_node_rpc_api import FullNodeRpcApi from chia.protocols.outbound_message import NodeType -from chia.server.aliases import FullNodeService +from chia.server.aliases import SolverService from chia.server.signal_handlers import SignalHandlers -from chia.server.start_service import RpcInfo, Service, async_run +from chia.server.start_service import Service, async_run +from chia.solver.solver import Solver +from chia.solver.solver_api import SolverAPI from chia.util.chia_logging import initialize_service_logging from chia.util.config import load_config, load_config_cli from chia.util.default_root import resolve_root_path @@ -30,13 +30,13 @@ SERVICE_NAME = "solver" -async def create_solver_service( +def create_solver_service( root_path: pathlib.Path, config: dict[str, Any], consensus_constants: ConsensusConstants, connect_to_daemon: bool = True, override_capabilities: Optional[list[tuple[uint16, str]]] = None, -) -> FullNodeService: +) -> SolverService: service_config = config[SERVICE_NAME] network_id = service_config["selected_network"] @@ -44,14 +44,10 @@ async def create_solver_service( if service_config["enable_upnp"]: upnp_list = [service_config["port"]] - node = await FullNode.create( - service_config, - root_path=root_path, - consensus_constants=consensus_constants, - ) - peer_api = FullNodeAPI(node) + node = Solver(root_path, service_config, consensus_constants) + peer_api = SolverAPI(node) + network_id = service_config["selected_network"] - rpc_info: Optional[RpcInfo[FullNodeRpcApi]] = None if service_config.get("start_rpc_server", True): rpc_info = (FullNodeRpcApi, service_config["rpc_port"]) @@ -64,7 +60,6 @@ async def create_solver_service( advertised_port=service_config["port"], service_name=SERVICE_NAME, upnp_ports=upnp_list, - # connect_peers=get_unresolved_peer_infos(service_config, NodeType.SOLVER), on_connect_callback=node.on_connect, network_id=network_id, rpc_info=rpc_info, @@ -84,7 +79,7 @@ async def async_main(service_config: dict[str, Any], root_path: pathlib.Path) -> updated_constants = replace_str_to_bytes(DEFAULT_CONSTANTS, **overrides) initialize_service_logging(service_name=SERVICE_NAME, config=config, root_path=root_path) - service = await create_solver_service(root_path, config, updated_constants) + service = create_solver_service(root_path, config, updated_constants) async with SignalHandlers.manage() as signal_handlers: await service.setup_process_global_state(signal_handlers=signal_handlers) await service.run() diff --git a/chia/simulator/setup_services.py b/chia/simulator/setup_services.py index d7c299d33c28..04fc63029173 100644 --- a/chia/simulator/setup_services.py +++ b/chia/simulator/setup_services.py @@ -28,6 +28,7 @@ FullNodeService, HarvesterService, IntroducerService, + SolverService, TimelordService, WalletService, ) @@ -37,6 +38,7 @@ from chia.server.start_full_node import create_full_node_service from chia.server.start_harvester import create_harvester_service from chia.server.start_introducer import create_introducer_service +from chia.server.start_solver import create_solver_service from chia.server.start_timelord import create_timelord_service from chia.server.start_wallet import create_wallet_service from chia.simulator.block_tools import BlockTools, test_constants @@ -506,3 +508,27 @@ async def setup_timelord( async with service.manage(): yield service + + +@asynccontextmanager +async def setup_solver( + root_path: Path, + consensus_constants: ConsensusConstants, + start_service: bool = True, +) -> AsyncGenerator[SolverService, None]: + with create_lock_and_load_config(root_path / "config" / "ssl" / "ca", root_path) as config: + config["logging"]["log_stdout"] = True + config["solver"]["enable_upnp"] = True + config["solver"]["selected_network"] = "testnet0" + config["solver"]["port"] = 0 + config["solver"]["rpc_port"] = 0 + config["solver"]["num_threads"] = 1 + save_config(root_path, "config.yaml", config) + service = create_solver_service( + root_path, + config, + consensus_constants, + ) + + async with service.manage(start=start_service): + yield service diff --git a/chia/solver/solver.py b/chia/solver/solver.py index 20eea23f8441..cc8df2f51a87 100644 --- a/chia/solver/solver.py +++ b/chia/solver/solver.py @@ -62,6 +62,7 @@ async def manage(self) -> AsyncIterator[None]: self._shut_down = True def solve(self, info: SolverInfo) -> Optional[bytes]: + self.log.debug(f"Solve called with SolverInfo: {info}") return None def get_connections(self, request_node_type: Optional[NodeType]) -> list[dict[str, Any]]: diff --git a/chia/ssl/create_ssl.py b/chia/ssl/create_ssl.py index 422ecf25ec60..7f73fdfef04f 100644 --- a/chia/ssl/create_ssl.py +++ b/chia/ssl/create_ssl.py @@ -24,6 +24,7 @@ "crawler", "data_layer", "daemon", + "solver", ] _all_public_node_names: list[str] = ["full_node", "wallet", "farmer", "introducer", "timelord", "data_layer"] diff --git a/chia/util/initial-config.yaml b/chia/util/initial-config.yaml index 4ea7b63894c6..b69ea09a2066 100644 --- a/chia/util/initial-config.yaml +++ b/chia/util/initial-config.yaml @@ -629,6 +629,17 @@ wallet: auto_sign_txs: True +solver: + # The solver server will run on this port + port: 8666 + + network_overrides: *network_overrides + selected_network: *selected_network + + ssl: + private_crt: "config/ssl/solver/private_solver.crt" + private_key: "config/ssl/solver/private_solver.key" + data_layer: # TODO: consider name # TODO: organize consistently with other sections From 77413376f4f442eecde2621915b6bb2554750e52 Mon Sep 17 00:00:00 2001 From: almogdepaz Date: Wed, 16 Jul 2025 15:34:16 +0300 Subject: [PATCH 04/42] prover protocol and v2Prover --- chia/_tests/plot_sync/test_plot_sync.py | 8 +- chia/_tests/plot_sync/test_sync_simulated.py | 37 ++-- chia/_tests/plotting/test_plot_manager.py | 6 +- chia/plot_sync/sender.py | 2 +- chia/plotting/cache.py | 15 +- chia/plotting/check_plots.py | 4 +- chia/plotting/manager.py | 7 +- chia/plotting/prover.py | 184 +++++++++++++++++++ chia/plotting/util.py | 20 +- 9 files changed, 241 insertions(+), 42 deletions(-) create mode 100644 chia/plotting/prover.py diff --git a/chia/_tests/plot_sync/test_plot_sync.py b/chia/_tests/plot_sync/test_plot_sync.py index 9e76d26b7a5a..e889286ea300 100644 --- a/chia/_tests/plot_sync/test_plot_sync.py +++ b/chia/_tests/plot_sync/test_plot_sync.py @@ -65,7 +65,7 @@ class ExpectedResult: def add_valid(self, list_plots: list[MockPlotInfo]) -> None: def create_mock_plot(info: MockPlotInfo) -> Plot: return Plot( - info.prover.get_filename(), + str(info.prover.get_filename()), uint8(0), bytes32.zeros, None, @@ -77,7 +77,7 @@ def create_mock_plot(info: MockPlotInfo) -> Plot: ) self.valid_count += len(list_plots) - self.valid_delta.additions.update({x.prover.get_filename(): create_mock_plot(x) for x in list_plots}) + self.valid_delta.additions.update({str(x.prover.get_filename()): create_mock_plot(x) for x in list_plots}) def remove_valid(self, list_paths: list[Path]) -> None: self.valid_count -= len(list_paths) @@ -193,7 +193,7 @@ async def plot_sync_callback(self, peer_id: bytes32, delta: Optional[Delta]) -> assert path in delta.valid.additions plot = harvester.plot_manager.plots.get(Path(path), None) assert plot is not None - assert plot.prover.get_filename() == delta.valid.additions[path].filename + assert plot.prover.get_filename_str() == delta.valid.additions[path].filename assert plot.prover.get_size() == delta.valid.additions[path].size assert plot.prover.get_id() == delta.valid.additions[path].plot_id assert plot.prover.get_compression_level() == delta.valid.additions[path].compression_level @@ -254,7 +254,7 @@ async def run_sync_test(self) -> None: assert expected.duplicates_delta.empty() for path, plot_info in plot_manager.plots.items(): assert str(path) in receiver.plots() - assert plot_info.prover.get_filename() == receiver.plots()[str(path)].filename + assert plot_info.prover.get_filename_str() == receiver.plots()[str(path)].filename assert plot_info.prover.get_size() == receiver.plots()[str(path)].size assert plot_info.prover.get_id() == receiver.plots()[str(path)].plot_id assert plot_info.prover.get_compression_level() == receiver.plots()[str(path)].compression_level diff --git a/chia/_tests/plot_sync/test_sync_simulated.py b/chia/_tests/plot_sync/test_sync_simulated.py index 1be1b8a51591..c1228e8a3653 100644 --- a/chia/_tests/plot_sync/test_sync_simulated.py +++ b/chia/_tests/plot_sync/test_sync_simulated.py @@ -25,6 +25,7 @@ from chia.plot_sync.sender import Sender from chia.plot_sync.util import Constants from chia.plotting.manager import PlotManager +from chia.plotting.prover import V1Prover from chia.plotting.util import PlotInfo from chia.protocols.harvester_protocol import PlotSyncError, PlotSyncResponse from chia.protocols.outbound_message import make_msg @@ -79,7 +80,7 @@ async def run( removed_paths: list[Path] = [p.prover.get_filename() for p in removed] if removed is not None else [] invalid_dict: dict[Path, int] = {p.prover.get_filename(): 0 for p in self.invalid} keys_missing_set: set[Path] = {p.prover.get_filename() for p in self.keys_missing} - duplicates_set: set[str] = {p.prover.get_filename() for p in self.duplicates} + duplicates_set: set[str] = {p.prover.get_filename_str() for p in self.duplicates} # Inject invalid plots into `PlotManager` of the harvester so that the callback calls below can use them # to sync them to the farmer. @@ -131,30 +132,30 @@ def validate_plot_sync(self) -> None: assert len(self.invalid) == len(self.plot_sync_receiver.invalid()) assert len(self.keys_missing) == len(self.plot_sync_receiver.keys_missing()) for _, plot_info in self.plots.items(): - assert plot_info.prover.get_filename() not in self.plot_sync_receiver.invalid() - assert plot_info.prover.get_filename() not in self.plot_sync_receiver.keys_missing() - assert plot_info.prover.get_filename() in self.plot_sync_receiver.plots() - synced_plot = self.plot_sync_receiver.plots()[plot_info.prover.get_filename()] - assert plot_info.prover.get_filename() == synced_plot.filename + assert plot_info.prover.get_filename_str() not in self.plot_sync_receiver.invalid() + assert plot_info.prover.get_filename_str() not in self.plot_sync_receiver.keys_missing() + assert plot_info.prover.get_filename_str() in self.plot_sync_receiver.plots() + synced_plot = self.plot_sync_receiver.plots()[plot_info.prover.get_filename_str()] + assert plot_info.prover.get_filename_str() == synced_plot.filename assert plot_info.pool_public_key == synced_plot.pool_public_key assert plot_info.pool_contract_puzzle_hash == synced_plot.pool_contract_puzzle_hash assert plot_info.plot_public_key == synced_plot.plot_public_key assert plot_info.file_size == synced_plot.file_size assert uint64(int(plot_info.time_modified)) == synced_plot.time_modified for plot_info in self.invalid: - assert plot_info.prover.get_filename() not in self.plot_sync_receiver.plots() - assert plot_info.prover.get_filename() in self.plot_sync_receiver.invalid() - assert plot_info.prover.get_filename() not in self.plot_sync_receiver.keys_missing() - assert plot_info.prover.get_filename() not in self.plot_sync_receiver.duplicates() + assert plot_info.prover.get_filename_str() not in self.plot_sync_receiver.plots() + assert plot_info.prover.get_filename_str() in self.plot_sync_receiver.invalid() + assert plot_info.prover.get_filename_str() not in self.plot_sync_receiver.keys_missing() + assert plot_info.prover.get_filename_str() not in self.plot_sync_receiver.duplicates() for plot_info in self.keys_missing: - assert plot_info.prover.get_filename() not in self.plot_sync_receiver.plots() - assert plot_info.prover.get_filename() not in self.plot_sync_receiver.invalid() - assert plot_info.prover.get_filename() in self.plot_sync_receiver.keys_missing() - assert plot_info.prover.get_filename() not in self.plot_sync_receiver.duplicates() + assert plot_info.prover.get_filename_str() not in self.plot_sync_receiver.plots() + assert plot_info.prover.get_filename_str() not in self.plot_sync_receiver.invalid() + assert plot_info.prover.get_filename_str() in self.plot_sync_receiver.keys_missing() + assert plot_info.prover.get_filename_str() not in self.plot_sync_receiver.duplicates() for plot_info in self.duplicates: - assert plot_info.prover.get_filename() not in self.plot_sync_receiver.invalid() - assert plot_info.prover.get_filename() not in self.plot_sync_receiver.keys_missing() - assert plot_info.prover.get_filename() in self.plot_sync_receiver.duplicates() + assert plot_info.prover.get_filename_str() not in self.plot_sync_receiver.invalid() + assert plot_info.prover.get_filename_str() not in self.plot_sync_receiver.keys_missing() + assert plot_info.prover.get_filename_str() in self.plot_sync_receiver.duplicates() @dataclass @@ -284,7 +285,7 @@ def get_compression_level(self) -> uint8: return [ PlotInfo( - prover=DiskProver(f"{x}", bytes32.random(seeded_random), 25 + x % 26), + prover=V1Prover(DiskProver(f"{x}", bytes32.random(seeded_random), 25 + x % 26)), pool_public_key=None, pool_contract_puzzle_hash=None, plot_public_key=G1Element(), diff --git a/chia/_tests/plotting/test_plot_manager.py b/chia/_tests/plotting/test_plot_manager.py index 16fcb50bf66e..beb8a192eda6 100644 --- a/chia/_tests/plotting/test_plot_manager.py +++ b/chia/_tests/plotting/test_plot_manager.py @@ -42,8 +42,8 @@ class MockDiskProver: filename: str - def get_filename(self) -> str: - return self.filename + def get_filename(self) -> Path: + return Path(self.filename) @dataclass @@ -614,7 +614,7 @@ def assert_cache(expected: list[MockPlotInfo]) -> None: # Write the modified cache entries to the file cache_path.write_bytes(bytes(VersionedBlob(uint16(CURRENT_VERSION), bytes(cache_data)))) # And now test that plots in invalid_entries are not longer loaded - assert_cache([plot_info for plot_info in plot_infos if plot_info.prover.get_filename() not in invalid_entries]) + assert_cache([plot_info for plot_info in plot_infos if str(plot_info.prover.get_filename()) not in invalid_entries]) @pytest.mark.anyio diff --git a/chia/plot_sync/sender.py b/chia/plot_sync/sender.py index 74be15cae2cf..ec1c51c3937b 100644 --- a/chia/plot_sync/sender.py +++ b/chia/plot_sync/sender.py @@ -39,7 +39,7 @@ def _convert_plot_info_list(plot_infos: list[PlotInfo]) -> list[Plot]: for plot_info in plot_infos: converted.append( Plot( - filename=plot_info.prover.get_filename(), + filename=plot_info.prover.get_filename_str(), size=plot_info.prover.get_size(), plot_id=plot_info.prover.get_id(), pool_public_key=plot_info.pool_public_key, diff --git a/chia/plotting/cache.py b/chia/plotting/cache.py index c70504d4c31f..4d5a5aef8905 100644 --- a/chia/plotting/cache.py +++ b/chia/plotting/cache.py @@ -7,13 +7,16 @@ from dataclasses import dataclass, field from math import ceil from pathlib import Path -from typing import Optional +from typing import TYPE_CHECKING, Optional + +if TYPE_CHECKING: + from chia.plotting.prover import ProverProtocol from chia_rs import G1Element from chia_rs.sized_bytes import bytes32 from chia_rs.sized_ints import uint16, uint64 -from chiapos import DiskProver +from chia.plotting.prover import get_prover_from_bytes from chia.plotting.util import parse_plot_info from chia.types.blockchain_format.proof_of_space import generate_plot_public_key from chia.util.streamable import Streamable, VersionedBlob, streamable @@ -43,7 +46,7 @@ class CacheDataV1(Streamable): @dataclass class CacheEntry: - prover: DiskProver + prover: ProverProtocol farmer_public_key: G1Element pool_public_key: Optional[G1Element] pool_contract_puzzle_hash: Optional[bytes32] @@ -51,7 +54,8 @@ class CacheEntry: last_use: float @classmethod - def from_disk_prover(cls, prover: DiskProver) -> CacheEntry: + def from_prover(cls, prover: ProverProtocol) -> CacheEntry: + """Create CacheEntry from any prover implementation""" ( pool_public_key_or_puzzle_hash, farmer_public_key, @@ -149,8 +153,9 @@ def load(self) -> None: 39: 44367, } for path, cache_entry in cache_data.entries: + prover: ProverProtocol = get_prover_from_bytes(path, cache_entry.prover_data) new_entry = CacheEntry( - DiskProver.from_bytes(cache_entry.prover_data), + prover, cache_entry.farmer_public_key, cache_entry.pool_public_key, cache_entry.pool_contract_puzzle_hash, diff --git a/chia/plotting/check_plots.py b/chia/plotting/check_plots.py index fc4f1197bd5f..0fe7c2567fa5 100644 --- a/chia/plotting/check_plots.py +++ b/chia/plotting/check_plots.py @@ -9,7 +9,7 @@ from typing import Optional from chia_rs import G1Element -from chia_rs.sized_ints import uint32 +from chia_rs.sized_ints import uint8, uint32 from chiapos import Verifier from chia.plotting.manager import PlotManager @@ -133,7 +133,7 @@ def check_plots( log.info("") log.info("") log.info(f"Starting to test each plot with {num} challenges each\n") - total_good_plots: Counter[str] = Counter() + total_good_plots: Counter[uint8] = Counter() total_size = 0 bad_plots_list: list[Path] = [] diff --git a/chia/plotting/manager.py b/chia/plotting/manager.py index f2a9ab8565e5..5cd9acb6eb08 100644 --- a/chia/plotting/manager.py +++ b/chia/plotting/manager.py @@ -9,10 +9,11 @@ from typing import Any, Callable, Optional from chia_rs import G1Element -from chiapos import DiskProver, decompressor_context_queue +from chiapos import decompressor_context_queue from chia.consensus.pos_quality import UI_ACTUAL_SPACE_CONSTANT_FACTOR, _expected_plot_size from chia.plotting.cache import Cache, CacheEntry +from chia.plotting.prover import get_prover_from_file from chia.plotting.util import ( HarvestingMode, PlotInfo, @@ -323,7 +324,7 @@ def process_file(file_path: Path) -> Optional[PlotInfo]: cache_entry = self.cache.get(file_path) cache_hit = cache_entry is not None if not cache_hit: - prover = DiskProver(str(file_path)) + prover = get_prover_from_file(str(file_path)) log.debug(f"process_file {file_path!s}") @@ -343,7 +344,7 @@ def process_file(file_path: Path) -> Optional[PlotInfo]: ) return None - cache_entry = CacheEntry.from_disk_prover(prover) + cache_entry = CacheEntry.from_prover(prover) self.cache.update(file_path, cache_entry) assert cache_entry is not None diff --git a/chia/plotting/prover.py b/chia/plotting/prover.py new file mode 100644 index 000000000000..df70248b8dd8 --- /dev/null +++ b/chia/plotting/prover.py @@ -0,0 +1,184 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from pathlib import Path +from typing import TYPE_CHECKING + +from chia_rs.sized_bytes import bytes32 +from chia_rs.sized_ints import uint8 +from chiapos import DiskProver + +if TYPE_CHECKING: + from chiapos import DiskProver + + +class ProverProtocol(ABC): + """Abstract protocol for all prover implementations (V1 and V2)""" + + @abstractmethod + def get_filename(self) -> Path: + """Returns the filename of the plot""" + + @abstractmethod + def get_filename_str(self) -> str: + """Returns the filename of the plot""" + + @abstractmethod + def get_size(self) -> uint8: + """Returns the k size of the plot""" + + @abstractmethod + def get_memo(self) -> bytes: + """Returns the memo containing keys and other metadata""" + + @abstractmethod + def get_compression_level(self) -> uint8: + """Returns the compression level (0 for uncompressed)""" + + @abstractmethod + def get_version(self) -> int: + """Returns the plot version (1 for V1, 2 for V2)""" + + @abstractmethod + def __bytes__(self) -> bytes: + """Returns the prover serialized as bytes for caching""" + + @abstractmethod + def get_id(self) -> bytes32: + """Returns the plot ID""" + + @abstractmethod + def get_qualities_for_challenge(self, challenge: bytes32) -> list[bytes32]: + """Returns the qualities for a given challenge""" + + @abstractmethod + def get_full_proof(self, challenge: bytes, index: int, parallel_read: bool = True) -> bytes: + """Returns the full proof for a given challenge and index""" + + @classmethod + @abstractmethod + def from_bytes(cls, data: bytes) -> ProverProtocol: + """Create a prover from serialized bytes""" + + +class V2Prover(ProverProtocol): + """V2 Plot Prover implementation - currently stubbed""" + + def __init__(self, filename: str): + self._filename = filename + # TODO: Implement V2 plot file parsing and validation + + def get_filename(self) -> Path: + return Path(self._filename) + + def get_filename_str(self) -> str: + return str(self._filename) + + def get_size(self) -> uint8: + # TODO: Extract k size from V2 plot file + return uint8(32) # Stub value + + def get_memo(self) -> bytes: + # TODO: Extract memo from V2 plot file + return b"" # Stub value + + def get_compression_level(self) -> uint8: + # TODO: Extract compression level from V2 plot file + return uint8(0) # Stub value + + def get_version(self) -> int: + return 2 + + def __bytes__(self) -> bytes: + # TODO: Implement proper V2 prover serialization for caching + # For now, just serialize the filename as a placeholder + return self._filename.encode("utf-8") + + def get_id(self) -> bytes32: + # TODO: Extract plot ID from V2 plot file + return bytes32(b"") # Stub value + + def get_qualities_for_challenge(self, challenge: bytes) -> list[bytes32]: + # TODO: Implement V2 plot quality lookup + return [] # Stub value + + def get_full_proof(self, challenge: bytes, index: int, parallel_read: bool = True) -> bytes: + # TODO: Implement V2 plot proof generation + return b"" # Stub value + + @classmethod + def from_bytes(cls, data: bytes) -> V2Prover: + # TODO: Implement proper V2 prover deserialization from cache + # For now, just deserialize the filename + filename = data.decode("utf-8") + return cls(filename) + + +class V1Prover(ProverProtocol): + """Wrapper for existing DiskProver to implement ProverProtocol""" + + def __init__(self, disk_prover: DiskProver) -> None: + self._disk_prover = disk_prover + + def get_filename(self) -> Path: + return Path(self._disk_prover.get_filename()) + + def get_filename_str(self) -> str: + return str(self._disk_prover.get_filename()) + + def get_size(self) -> uint8: + return uint8(self._disk_prover.get_size()) + + def get_memo(self) -> bytes: + return bytes(self._disk_prover.get_memo()) + + def get_compression_level(self) -> uint8: + return uint8(self._disk_prover.get_compression_level()) + + def get_version(self) -> int: + return 1 + + def __bytes__(self) -> bytes: + return bytes(self._disk_prover) + + def get_id(self) -> bytes32: + return bytes32(self._disk_prover.get_id()) + + def get_qualities_for_challenge(self, challenge: bytes32) -> list[bytes32]: + return [bytes32(quality) for quality in self._disk_prover.get_qualities_for_challenge(challenge)] + + def get_full_proof(self, challenge: bytes, index: int, parallel_read: bool = True) -> bytes: + return bytes(self._disk_prover.get_full_proof(challenge, index, parallel_read)) + + @classmethod + def from_bytes(cls, data: bytes) -> V1Prover: + """Create V1ProverWrapper from serialized bytes""" + from chiapos import DiskProver + + disk_prover = DiskProver.from_bytes(data) + return cls(disk_prover) + + @property + def disk_prover(self) -> DiskProver: + """Access to underlying DiskProver for backwards compatibility""" + return self._disk_prover + + +def get_prover_from_bytes(filename: str, prover_data: bytes) -> ProverProtocol: + """Factory function to create appropriate prover based on plot version""" + if filename.endswith(".plot2"): + return V2Prover.from_bytes(prover_data) + elif filename.endswith(".plot"): + return V1Prover(DiskProver.from_bytes(prover_data)) + else: + raise ValueError(f"Unsupported plot file: {filename}") + + +def get_prover_from_file(filename: str) -> ProverProtocol: + """Factory function to create appropriate prover based on plot version""" + if filename.endswith(".plot2"): + return V2Prover(filename) + elif filename.endswith(".plot"): + return V1Prover(DiskProver(filename)) + else: + raise ValueError(f"Unsupported plot file: {filename}") diff --git a/chia/plotting/util.py b/chia/plotting/util.py index c2ad2a136e05..a7aa314dc44f 100644 --- a/chia/plotting/util.py +++ b/chia/plotting/util.py @@ -4,12 +4,14 @@ from dataclasses import dataclass, field from enum import Enum, IntEnum from pathlib import Path -from typing import Any, Optional, Union +from typing import TYPE_CHECKING, Any, Optional, Union + +if TYPE_CHECKING: + from chia.plotting.prover import ProverProtocol from chia_rs import G1Element, PrivateKey from chia_rs.sized_bytes import bytes32 from chia_rs.sized_ints import uint32 -from chiapos import DiskProver from typing_extensions import final from chia.util.config import load_config, lock_and_load_config, save_config @@ -39,7 +41,7 @@ class PlotsRefreshParameter(Streamable): @dataclass class PlotInfo: - prover: DiskProver + prover: ProverProtocol pool_public_key: Optional[G1Element] pool_contract_puzzle_hash: Optional[bytes32] plot_public_key: G1Element @@ -233,16 +235,22 @@ def get_filenames(directory: Path, recursive: bool, follow_links: bool) -> list[ if follow_links and recursive: import glob - files = glob.glob(str(directory / "**" / "*.plot"), recursive=True) - for file in files: + v1_file_strs = glob.glob(str(directory / "**" / "*.plot"), recursive=True) + v2_file_strs = glob.glob(str(directory / "**" / "*.plot2"), recursive=True) + + for file in v1_file_strs + v2_file_strs: filepath = Path(file).resolve() if filepath.is_file() and not filepath.name.startswith("._"): all_files.append(filepath) else: glob_function = directory.rglob if recursive else directory.glob - all_files = [ + v1_files: list[Path] = [ child for child in glob_function("*.plot") if child.is_file() and not child.name.startswith("._") ] + v2_files: list[Path] = [ + child for child in glob_function("*.plot2") if child.is_file() and not child.name.startswith("._") + ] + all_files = v1_files + v2_files log.debug(f"get_filenames: {len(all_files)} files found in {directory}, recursive: {recursive}") except Exception as e: log.warning(f"Error reading directory {directory} {e}") From 8c4f57843f7d1def844f0778db39603e48bebf02 Mon Sep 17 00:00:00 2001 From: almogdepaz Date: Wed, 16 Jul 2025 15:57:50 +0300 Subject: [PATCH 05/42] format name --- chia/plotting/prover.py | 4 ++-- chia/plotting/util.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/chia/plotting/prover.py b/chia/plotting/prover.py index df70248b8dd8..f9de5d9b440d 100644 --- a/chia/plotting/prover.py +++ b/chia/plotting/prover.py @@ -166,7 +166,7 @@ def disk_prover(self) -> DiskProver: def get_prover_from_bytes(filename: str, prover_data: bytes) -> ProverProtocol: """Factory function to create appropriate prover based on plot version""" - if filename.endswith(".plot2"): + if filename.endswith(".plot_v2"): return V2Prover.from_bytes(prover_data) elif filename.endswith(".plot"): return V1Prover(DiskProver.from_bytes(prover_data)) @@ -176,7 +176,7 @@ def get_prover_from_bytes(filename: str, prover_data: bytes) -> ProverProtocol: def get_prover_from_file(filename: str) -> ProverProtocol: """Factory function to create appropriate prover based on plot version""" - if filename.endswith(".plot2"): + if filename.endswith(".plot_v2"): return V2Prover(filename) elif filename.endswith(".plot"): return V1Prover(DiskProver(filename)) diff --git a/chia/plotting/util.py b/chia/plotting/util.py index a7aa314dc44f..f195c255f4ae 100644 --- a/chia/plotting/util.py +++ b/chia/plotting/util.py @@ -236,7 +236,7 @@ def get_filenames(directory: Path, recursive: bool, follow_links: bool) -> list[ import glob v1_file_strs = glob.glob(str(directory / "**" / "*.plot"), recursive=True) - v2_file_strs = glob.glob(str(directory / "**" / "*.plot2"), recursive=True) + v2_file_strs = glob.glob(str(directory / "**" / "*.plot_v2"), recursive=True) for file in v1_file_strs + v2_file_strs: filepath = Path(file).resolve() @@ -248,7 +248,7 @@ def get_filenames(directory: Path, recursive: bool, follow_links: bool) -> list[ child for child in glob_function("*.plot") if child.is_file() and not child.name.startswith("._") ] v2_files: list[Path] = [ - child for child in glob_function("*.plot2") if child.is_file() and not child.name.startswith("._") + child for child in glob_function("*.plot_v2") if child.is_file() and not child.name.startswith("._") ] all_files = v1_files + v2_files log.debug(f"get_filenames: {len(all_files)} files found in {directory}, recursive: {recursive}") From 6aef294b6102db614f63e20bb0268712f6dd2548 Mon Sep 17 00:00:00 2001 From: almogdepaz Date: Wed, 16 Jul 2025 17:47:49 +0300 Subject: [PATCH 06/42] format --- chia/plotting/cache.py | 2 +- chia/plotting/prover.py | 36 ++++++++++++++---------------------- 2 files changed, 15 insertions(+), 23 deletions(-) diff --git a/chia/plotting/cache.py b/chia/plotting/cache.py index 4d5a5aef8905..d442ac5b07de 100644 --- a/chia/plotting/cache.py +++ b/chia/plotting/cache.py @@ -55,7 +55,7 @@ class CacheEntry: @classmethod def from_prover(cls, prover: ProverProtocol) -> CacheEntry: - """Create CacheEntry from any prover implementation""" + """Create CacheEntry from prover""" ( pool_public_key_or_puzzle_hash, farmer_public_key, diff --git a/chia/plotting/prover.py b/chia/plotting/prover.py index f9de5d9b440d..0c0bc671343a 100644 --- a/chia/plotting/prover.py +++ b/chia/plotting/prover.py @@ -13,15 +13,13 @@ class ProverProtocol(ABC): - """Abstract protocol for all prover implementations (V1 and V2)""" - @abstractmethod def get_filename(self) -> Path: - """Returns the filename of the plot""" + """Returns the filename for the plot""" @abstractmethod def get_filename_str(self) -> str: - """Returns the filename of the plot""" + """Returns the filename string for the plot""" @abstractmethod def get_size(self) -> uint8: @@ -29,19 +27,19 @@ def get_size(self) -> uint8: @abstractmethod def get_memo(self) -> bytes: - """Returns the memo containing keys and other metadata""" + """Returns the memo""" @abstractmethod def get_compression_level(self) -> uint8: - """Returns the compression level (0 for uncompressed)""" + """Returns the compression level""" @abstractmethod def get_version(self) -> int: - """Returns the plot version (1 for V1, 2 for V2)""" + """Returns the plot version""" @abstractmethod def __bytes__(self) -> bytes: - """Returns the prover serialized as bytes for caching""" + """Returns the prover bytes""" @abstractmethod def get_id(self) -> bytes32: @@ -62,11 +60,11 @@ def from_bytes(cls, data: bytes) -> ProverProtocol: class V2Prover(ProverProtocol): - """V2 Plot Prover implementation - currently stubbed""" + """V2 Plot Prover stubb""" def __init__(self, filename: str): self._filename = filename - # TODO: Implement V2 plot file parsing and validation + # TODO: todo_v2_plots Implement plot file parsing and validation def get_filename(self) -> Path: return Path(self._filename) @@ -75,11 +73,11 @@ def get_filename_str(self) -> str: return str(self._filename) def get_size(self) -> uint8: - # TODO: Extract k size from V2 plot file + # TODO: todo_v2_plots get k size from plot return uint8(32) # Stub value def get_memo(self) -> bytes: - # TODO: Extract memo from V2 plot file + # TODO: todo_v2_plots return b"" # Stub value def get_compression_level(self) -> uint8: @@ -90,7 +88,7 @@ def get_version(self) -> int: return 2 def __bytes__(self) -> bytes: - # TODO: Implement proper V2 prover serialization for caching + # TODO: todo_v2_plots Implement prover serialization for caching # For now, just serialize the filename as a placeholder return self._filename.encode("utf-8") @@ -99,17 +97,15 @@ def get_id(self) -> bytes32: return bytes32(b"") # Stub value def get_qualities_for_challenge(self, challenge: bytes) -> list[bytes32]: - # TODO: Implement V2 plot quality lookup + # TODO: todo_v2_plots Implement plot quality lookup return [] # Stub value def get_full_proof(self, challenge: bytes, index: int, parallel_read: bool = True) -> bytes: - # TODO: Implement V2 plot proof generation - return b"" # Stub value + # TODO: todo_v2_plots Implement plot proof generation + return b"" @classmethod def from_bytes(cls, data: bytes) -> V2Prover: - # TODO: Implement proper V2 prover deserialization from cache - # For now, just deserialize the filename filename = data.decode("utf-8") return cls(filename) @@ -152,7 +148,6 @@ def get_full_proof(self, challenge: bytes, index: int, parallel_read: bool = Tru @classmethod def from_bytes(cls, data: bytes) -> V1Prover: - """Create V1ProverWrapper from serialized bytes""" from chiapos import DiskProver disk_prover = DiskProver.from_bytes(data) @@ -160,12 +155,10 @@ def from_bytes(cls, data: bytes) -> V1Prover: @property def disk_prover(self) -> DiskProver: - """Access to underlying DiskProver for backwards compatibility""" return self._disk_prover def get_prover_from_bytes(filename: str, prover_data: bytes) -> ProverProtocol: - """Factory function to create appropriate prover based on plot version""" if filename.endswith(".plot_v2"): return V2Prover.from_bytes(prover_data) elif filename.endswith(".plot"): @@ -175,7 +168,6 @@ def get_prover_from_bytes(filename: str, prover_data: bytes) -> ProverProtocol: def get_prover_from_file(filename: str) -> ProverProtocol: - """Factory function to create appropriate prover based on plot version""" if filename.endswith(".plot_v2"): return V2Prover(filename) elif filename.endswith(".plot"): From 3f3b134acb521324856813afe0fa9e4881dd92d7 Mon Sep 17 00:00:00 2001 From: almogdepaz Date: Wed, 16 Jul 2025 23:06:22 +0300 Subject: [PATCH 07/42] refactor filename --- chia/plotting/prover.py | 4 ++-- chia/plotting/util.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/chia/plotting/prover.py b/chia/plotting/prover.py index 0c0bc671343a..b844fd52204c 100644 --- a/chia/plotting/prover.py +++ b/chia/plotting/prover.py @@ -159,7 +159,7 @@ def disk_prover(self) -> DiskProver: def get_prover_from_bytes(filename: str, prover_data: bytes) -> ProverProtocol: - if filename.endswith(".plot_v2"): + if filename.endswith(".plot2"): return V2Prover.from_bytes(prover_data) elif filename.endswith(".plot"): return V1Prover(DiskProver.from_bytes(prover_data)) @@ -168,7 +168,7 @@ def get_prover_from_bytes(filename: str, prover_data: bytes) -> ProverProtocol: def get_prover_from_file(filename: str) -> ProverProtocol: - if filename.endswith(".plot_v2"): + if filename.endswith(".plot2"): return V2Prover(filename) elif filename.endswith(".plot"): return V1Prover(DiskProver(filename)) diff --git a/chia/plotting/util.py b/chia/plotting/util.py index f195c255f4ae..a7aa314dc44f 100644 --- a/chia/plotting/util.py +++ b/chia/plotting/util.py @@ -236,7 +236,7 @@ def get_filenames(directory: Path, recursive: bool, follow_links: bool) -> list[ import glob v1_file_strs = glob.glob(str(directory / "**" / "*.plot"), recursive=True) - v2_file_strs = glob.glob(str(directory / "**" / "*.plot_v2"), recursive=True) + v2_file_strs = glob.glob(str(directory / "**" / "*.plot2"), recursive=True) for file in v1_file_strs + v2_file_strs: filepath = Path(file).resolve() @@ -248,7 +248,7 @@ def get_filenames(directory: Path, recursive: bool, follow_links: bool) -> list[ child for child in glob_function("*.plot") if child.is_file() and not child.name.startswith("._") ] v2_files: list[Path] = [ - child for child in glob_function("*.plot_v2") if child.is_file() and not child.name.startswith("._") + child for child in glob_function("*.plot2") if child.is_file() and not child.name.startswith("._") ] all_files = v1_files + v2_files log.debug(f"get_filenames: {len(all_files)} files found in {directory}, recursive: {recursive}") From 6e28f8c04b112c350e6225eb9b8f3ee464dce68c Mon Sep 17 00:00:00 2001 From: almogdepaz Date: Thu, 17 Jul 2025 13:03:33 +0300 Subject: [PATCH 08/42] tests/raise unimplemented --- chia/_tests/plotting/test_prover.py | 76 +++++++++++++++++++++++++++++ chia/plotting/prover.py | 46 +++++++++-------- 2 files changed, 102 insertions(+), 20 deletions(-) create mode 100644 chia/_tests/plotting/test_prover.py diff --git a/chia/_tests/plotting/test_prover.py b/chia/_tests/plotting/test_prover.py new file mode 100644 index 000000000000..68c34b8f5fe5 --- /dev/null +++ b/chia/_tests/plotting/test_prover.py @@ -0,0 +1,76 @@ +from __future__ import annotations + +import tempfile +from pathlib import Path + +import pytest + +from chia.plotting.prover import PlotVersion, V2Prover, get_prover_from_file + + +class TestProver: + def test_v2_prover_init_with_nonexistent_file(self) -> None: + prover = V2Prover("/nonexistent/path/test.plot2") + assert prover.get_version() == PlotVersion.V2 + assert prover.get_filename() == Path("/nonexistent/path/test.plot2") + assert prover.get_filename_str() == "/nonexistent/path/test.plot2" + + def test_v2_prover_get_size_raises_error(self) -> None: + prover = V2Prover("/nonexistent/path/test.plot2") + with pytest.raises(NotImplementedError, match="V2 plot format is not yet implemented"): + prover.get_size() + + def test_v2_prover_get_memo_raises_error(self) -> None: + prover = V2Prover("/nonexistent/path/test.plot2") + with pytest.raises(NotImplementedError, match="V2 plot format is not yet implemented"): + prover.get_memo() + + def test_v2_prover_get_compression_level_raises_error(self) -> None: + prover = V2Prover("/nonexistent/path/test.plot2") + with pytest.raises(NotImplementedError, match="V2 plot format is not yet implemented"): + prover.get_compression_level() + + def test_v2_prover_get_id_raises_error(self) -> None: + prover = V2Prover("/nonexistent/path/test.plot2") + with pytest.raises(NotImplementedError, match="V2 plot format is not yet implemented"): + prover.get_id() + + def test_v2_prover_get_qualities_for_challenge_raises_error(self) -> None: + prover = V2Prover("/nonexistent/path/test.plot2") + with pytest.raises(NotImplementedError, match="V2 plot format is not yet implemented"): + prover.get_qualities_for_challenge(b"challenge") + + def test_v2_prover_get_full_proof_raises_error(self) -> None: + prover = V2Prover("/nonexistent/path/test.plot2") + with pytest.raises(NotImplementedError, match="V2 plot format is not yet implemented"): + prover.get_full_proof(b"challenge", 0) + + def test_v2_prover_bytes_raises_error(self) -> None: + prover = V2Prover("/nonexistent/path/test.plot2") + with pytest.raises(NotImplementedError, match="V2 plot format is not yet implemented"): + bytes(prover) + + def test_v2_prover_from_bytes_raises_error(self) -> None: + with pytest.raises(NotImplementedError, match="V2 plot format is not yet implemented"): + V2Prover.from_bytes(b"test_data") + + def test_get_prover_from_file(self) -> None: + prover = get_prover_from_file("/nonexistent/path/test.plot2") + assert prover.get_version() == PlotVersion.V2 + with pytest.raises(NotImplementedError, match="V2 plot format is not yet implemented"): + prover.get_size() + + def test_get_prover_from_file_with_plot1_still_works(self) -> None: + with tempfile.NamedTemporaryFile(suffix=".plot", delete=False) as f: + temp_path = f.name + try: + with pytest.raises(Exception) as exc_info: + get_prover_from_file(temp_path) + assert not isinstance(exc_info.value, NotImplementedError) + finally: + Path(temp_path).unlink() + + def test_unsupported_file_extension_raises_value_error(self) -> None: + """Test that unsupported file extensions raise ValueError""" + with pytest.raises(ValueError, match="Unsupported plot file"): + get_prover_from_file("/nonexistent/path/test.txt") diff --git a/chia/plotting/prover.py b/chia/plotting/prover.py index b844fd52204c..79fc5a174723 100644 --- a/chia/plotting/prover.py +++ b/chia/plotting/prover.py @@ -1,6 +1,7 @@ from __future__ import annotations from abc import ABC, abstractmethod +from enum import IntEnum from pathlib import Path from typing import TYPE_CHECKING @@ -12,6 +13,13 @@ from chiapos import DiskProver +class PlotVersion(IntEnum): + """Enum for plot format versions""" + + V1 = 1 + V2 = 2 + + class ProverProtocol(ABC): @abstractmethod def get_filename(self) -> Path: @@ -34,7 +42,7 @@ def get_compression_level(self) -> uint8: """Returns the compression level""" @abstractmethod - def get_version(self) -> int: + def get_version(self) -> PlotVersion: """Returns the plot version""" @abstractmethod @@ -64,7 +72,6 @@ class V2Prover(ProverProtocol): def __init__(self, filename: str): self._filename = filename - # TODO: todo_v2_plots Implement plot file parsing and validation def get_filename(self) -> Path: return Path(self._filename) @@ -74,40 +81,39 @@ def get_filename_str(self) -> str: def get_size(self) -> uint8: # TODO: todo_v2_plots get k size from plot - return uint8(32) # Stub value + raise NotImplementedError("V2 plot format is not yet implemented") def get_memo(self) -> bytes: # TODO: todo_v2_plots - return b"" # Stub value + raise NotImplementedError("V2 plot format is not yet implemented") def get_compression_level(self) -> uint8: - # TODO: Extract compression level from V2 plot file - return uint8(0) # Stub value + # TODO: todo_v2_plots implement compression level retrieval + raise NotImplementedError("V2 plot format is not yet implemented") - def get_version(self) -> int: - return 2 + def get_version(self) -> PlotVersion: + return PlotVersion.V2 def __bytes__(self) -> bytes: # TODO: todo_v2_plots Implement prover serialization for caching - # For now, just serialize the filename as a placeholder - return self._filename.encode("utf-8") + raise NotImplementedError("V2 plot format is not yet implemented") def get_id(self) -> bytes32: # TODO: Extract plot ID from V2 plot file - return bytes32(b"") # Stub value + raise NotImplementedError("V2 plot format is not yet implemented") - def get_qualities_for_challenge(self, challenge: bytes) -> list[bytes32]: + def get_qualities_for_challenge(self, _challenge: bytes) -> list[bytes32]: # TODO: todo_v2_plots Implement plot quality lookup - return [] # Stub value + raise NotImplementedError("V2 plot format is not yet implemented") - def get_full_proof(self, challenge: bytes, index: int, parallel_read: bool = True) -> bytes: + def get_full_proof(self, _challenge: bytes, _index: int, _parallel_read: bool = True) -> bytes: # TODO: todo_v2_plots Implement plot proof generation - return b"" + raise NotImplementedError("V2 plot format is not yet implemented") @classmethod - def from_bytes(cls, data: bytes) -> V2Prover: - filename = data.decode("utf-8") - return cls(filename) + def from_bytes(cls, _data: bytes) -> V2Prover: + # TODO: todo_v2_plots Implement prover deserialization from cache + raise NotImplementedError("V2 plot format is not yet implemented") class V1Prover(ProverProtocol): @@ -131,8 +137,8 @@ def get_memo(self) -> bytes: def get_compression_level(self) -> uint8: return uint8(self._disk_prover.get_compression_level()) - def get_version(self) -> int: - return 1 + def get_version(self) -> PlotVersion: + return PlotVersion.V1 def __bytes__(self) -> bytes: return bytes(self._disk_prover) From 7811dbb843f993ac27159b4e95f87e0be82df8ea Mon Sep 17 00:00:00 2001 From: almogdepaz Date: Thu, 17 Jul 2025 13:51:30 +0300 Subject: [PATCH 09/42] add get_filename_str to mock --- chia/_tests/plot_sync/test_plot_sync.py | 4 ++-- chia/_tests/plotting/test_plot_manager.py | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/chia/_tests/plot_sync/test_plot_sync.py b/chia/_tests/plot_sync/test_plot_sync.py index e889286ea300..8d6f42ebc5a6 100644 --- a/chia/_tests/plot_sync/test_plot_sync.py +++ b/chia/_tests/plot_sync/test_plot_sync.py @@ -65,7 +65,7 @@ class ExpectedResult: def add_valid(self, list_plots: list[MockPlotInfo]) -> None: def create_mock_plot(info: MockPlotInfo) -> Plot: return Plot( - str(info.prover.get_filename()), + info.prover.get_filename_str(), uint8(0), bytes32.zeros, None, @@ -77,7 +77,7 @@ def create_mock_plot(info: MockPlotInfo) -> Plot: ) self.valid_count += len(list_plots) - self.valid_delta.additions.update({str(x.prover.get_filename()): create_mock_plot(x) for x in list_plots}) + self.valid_delta.additions.update({x.prover.get_filename_str(): create_mock_plot(x) for x in list_plots}) def remove_valid(self, list_paths: list[Path]) -> None: self.valid_count -= len(list_paths) diff --git a/chia/_tests/plotting/test_plot_manager.py b/chia/_tests/plotting/test_plot_manager.py index beb8a192eda6..164348524677 100644 --- a/chia/_tests/plotting/test_plot_manager.py +++ b/chia/_tests/plotting/test_plot_manager.py @@ -45,6 +45,9 @@ class MockDiskProver: def get_filename(self) -> Path: return Path(self.filename) + def get_filename_str(self) -> str: + return self.filename + @dataclass class MockPlotInfo: From 441aed5ef253e3e5513d9b1c4774e4b26e4fcebc Mon Sep 17 00:00:00 2001 From: almogdepaz Date: Thu, 17 Jul 2025 13:56:00 +0300 Subject: [PATCH 10/42] rename methods --- chia/_tests/plot_sync/test_plot_sync.py | 4 +- chia/_tests/plot_sync/test_sync_simulated.py | 56 ++++++++++---------- chia/_tests/plotting/test_plot_manager.py | 4 +- chia/_tests/plotting/test_prover.py | 4 +- chia/plot_sync/sender.py | 2 +- chia/plotting/manager.py | 6 +-- chia/plotting/prover.py | 12 ++--- 7 files changed, 44 insertions(+), 44 deletions(-) diff --git a/chia/_tests/plot_sync/test_plot_sync.py b/chia/_tests/plot_sync/test_plot_sync.py index 8d6f42ebc5a6..ff63e1e519f7 100644 --- a/chia/_tests/plot_sync/test_plot_sync.py +++ b/chia/_tests/plot_sync/test_plot_sync.py @@ -193,7 +193,7 @@ async def plot_sync_callback(self, peer_id: bytes32, delta: Optional[Delta]) -> assert path in delta.valid.additions plot = harvester.plot_manager.plots.get(Path(path), None) assert plot is not None - assert plot.prover.get_filename_str() == delta.valid.additions[path].filename + assert plot.prover.get_filename() == delta.valid.additions[path].filename assert plot.prover.get_size() == delta.valid.additions[path].size assert plot.prover.get_id() == delta.valid.additions[path].plot_id assert plot.prover.get_compression_level() == delta.valid.additions[path].compression_level @@ -254,7 +254,7 @@ async def run_sync_test(self) -> None: assert expected.duplicates_delta.empty() for path, plot_info in plot_manager.plots.items(): assert str(path) in receiver.plots() - assert plot_info.prover.get_filename_str() == receiver.plots()[str(path)].filename + assert plot_info.prover.get_filename() == receiver.plots()[str(path)].filename assert plot_info.prover.get_size() == receiver.plots()[str(path)].size assert plot_info.prover.get_id() == receiver.plots()[str(path)].plot_id assert plot_info.prover.get_compression_level() == receiver.plots()[str(path)].compression_level diff --git a/chia/_tests/plot_sync/test_sync_simulated.py b/chia/_tests/plot_sync/test_sync_simulated.py index c1228e8a3653..d48ab796ba09 100644 --- a/chia/_tests/plot_sync/test_sync_simulated.py +++ b/chia/_tests/plot_sync/test_sync_simulated.py @@ -69,18 +69,18 @@ async def run( initial: bool, ) -> None: for plot_info in loaded: - assert plot_info.prover.get_filename() not in self.plots + assert plot_info.prover.get_filepath() not in self.plots for plot_info in removed: - assert plot_info.prover.get_filename() in self.plots + assert plot_info.prover.get_filepath() in self.plots self.invalid = invalid self.keys_missing = keys_missing self.duplicates = duplicates - removed_paths: list[Path] = [p.prover.get_filename() for p in removed] if removed is not None else [] - invalid_dict: dict[Path, int] = {p.prover.get_filename(): 0 for p in self.invalid} - keys_missing_set: set[Path] = {p.prover.get_filename() for p in self.keys_missing} - duplicates_set: set[str] = {p.prover.get_filename_str() for p in self.duplicates} + removed_paths: list[Path] = [p.prover.get_filepath() for p in removed] if removed is not None else [] + invalid_dict: dict[Path, int] = {p.prover.get_filepath(): 0 for p in self.invalid} + keys_missing_set: set[Path] = {p.prover.get_filepath() for p in self.keys_missing} + duplicates_set: set[str] = {p.prover.get_filename() for p in self.duplicates} # Inject invalid plots into `PlotManager` of the harvester so that the callback calls below can use them # to sync them to the farmer. @@ -91,7 +91,7 @@ async def run( # Inject duplicated plots into `PlotManager` of the harvester so that the callback calls below can use them # to sync them to the farmer. for plot_info in loaded: - plot_path = Path(plot_info.prover.get_filename()) + plot_path = Path(plot_info.prover.get_filepath()) self.harvester.plot_manager.plot_filename_paths[plot_path.name] = (str(plot_path.parent), set()) for duplicate in duplicates_set: plot_path = Path(duplicate) @@ -123,39 +123,39 @@ async def sync_done() -> bool: await time_out_assert(60, sync_done) for plot_info in loaded: - self.plots[plot_info.prover.get_filename()] = plot_info + self.plots[plot_info.prover.get_filepath()] = plot_info for plot_info in removed: - del self.plots[plot_info.prover.get_filename()] + del self.plots[plot_info.prover.get_filepath()] def validate_plot_sync(self) -> None: assert len(self.plots) == len(self.plot_sync_receiver.plots()) assert len(self.invalid) == len(self.plot_sync_receiver.invalid()) assert len(self.keys_missing) == len(self.plot_sync_receiver.keys_missing()) for _, plot_info in self.plots.items(): - assert plot_info.prover.get_filename_str() not in self.plot_sync_receiver.invalid() - assert plot_info.prover.get_filename_str() not in self.plot_sync_receiver.keys_missing() - assert plot_info.prover.get_filename_str() in self.plot_sync_receiver.plots() - synced_plot = self.plot_sync_receiver.plots()[plot_info.prover.get_filename_str()] - assert plot_info.prover.get_filename_str() == synced_plot.filename + assert plot_info.prover.get_filename() not in self.plot_sync_receiver.invalid() + assert plot_info.prover.get_filename() not in self.plot_sync_receiver.keys_missing() + assert plot_info.prover.get_filename() in self.plot_sync_receiver.plots() + synced_plot = self.plot_sync_receiver.plots()[plot_info.prover.get_filename()] + assert plot_info.prover.get_filename() == synced_plot.filename assert plot_info.pool_public_key == synced_plot.pool_public_key assert plot_info.pool_contract_puzzle_hash == synced_plot.pool_contract_puzzle_hash assert plot_info.plot_public_key == synced_plot.plot_public_key assert plot_info.file_size == synced_plot.file_size assert uint64(int(plot_info.time_modified)) == synced_plot.time_modified for plot_info in self.invalid: - assert plot_info.prover.get_filename_str() not in self.plot_sync_receiver.plots() - assert plot_info.prover.get_filename_str() in self.plot_sync_receiver.invalid() - assert plot_info.prover.get_filename_str() not in self.plot_sync_receiver.keys_missing() - assert plot_info.prover.get_filename_str() not in self.plot_sync_receiver.duplicates() + assert plot_info.prover.get_filename() not in self.plot_sync_receiver.plots() + assert plot_info.prover.get_filename() in self.plot_sync_receiver.invalid() + assert plot_info.prover.get_filename() not in self.plot_sync_receiver.keys_missing() + assert plot_info.prover.get_filename() not in self.plot_sync_receiver.duplicates() for plot_info in self.keys_missing: - assert plot_info.prover.get_filename_str() not in self.plot_sync_receiver.plots() - assert plot_info.prover.get_filename_str() not in self.plot_sync_receiver.invalid() - assert plot_info.prover.get_filename_str() in self.plot_sync_receiver.keys_missing() - assert plot_info.prover.get_filename_str() not in self.plot_sync_receiver.duplicates() + assert plot_info.prover.get_filename() not in self.plot_sync_receiver.plots() + assert plot_info.prover.get_filename() not in self.plot_sync_receiver.invalid() + assert plot_info.prover.get_filename() in self.plot_sync_receiver.keys_missing() + assert plot_info.prover.get_filename() not in self.plot_sync_receiver.duplicates() for plot_info in self.duplicates: - assert plot_info.prover.get_filename_str() not in self.plot_sync_receiver.invalid() - assert plot_info.prover.get_filename_str() not in self.plot_sync_receiver.keys_missing() - assert plot_info.prover.get_filename_str() in self.plot_sync_receiver.duplicates() + assert plot_info.prover.get_filename() not in self.plot_sync_receiver.invalid() + assert plot_info.prover.get_filename() not in self.plot_sync_receiver.keys_missing() + assert plot_info.prover.get_filename() in self.plot_sync_receiver.duplicates() @dataclass @@ -417,7 +417,7 @@ async def test_sync_reset_cases( # Inject some data into `PlotManager` of the harvester so that we can validate the reset worked and triggered a # fresh sync of all available data of the plot manager for plot_info in plots[0:10]: - test_data.plots[plot_info.prover.get_filename()] = plot_info + test_data.plots[plot_info.prover.get_filepath()] = plot_info plot_manager.plots = test_data.plots test_data.invalid = plots[10:20] test_data.keys_missing = plots[20:30] @@ -425,8 +425,8 @@ async def test_sync_reset_cases( sender: Sender = test_runner.test_data[0].plot_sync_sender started_sync_id: uint64 = uint64(0) - plot_manager.failed_to_open_filenames = {p.prover.get_filename(): 0 for p in test_data.invalid} - plot_manager.no_key_filenames = {p.prover.get_filename() for p in test_data.keys_missing} + plot_manager.failed_to_open_filenames = {p.prover.get_filepath(): 0 for p in test_data.invalid} + plot_manager.no_key_filenames = {p.prover.get_filepath() for p in test_data.keys_missing} async def wait_for_reset() -> bool: assert started_sync_id != 0 diff --git a/chia/_tests/plotting/test_plot_manager.py b/chia/_tests/plotting/test_plot_manager.py index 164348524677..dbae9a0a879a 100644 --- a/chia/_tests/plotting/test_plot_manager.py +++ b/chia/_tests/plotting/test_plot_manager.py @@ -115,7 +115,7 @@ def refresh_callback(self, event: PlotRefreshEvents, refresh_result: PlotRefresh for value in actual_value: if type(value) is PlotInfo: for plot_info in expected_list: - if plot_info.prover.get_filename() == value.prover.get_filename(): + if plot_info.prover.get_filename() == value.prover.get_filepath(): values_found += 1 continue else: @@ -507,7 +507,7 @@ async def test_plot_info_caching(environment, bt): await refresh_tester.run(expected_result) for path, plot_info in env.refresh_tester.plot_manager.plots.items(): assert path in plot_manager.plots - assert plot_manager.plots[path].prover.get_filename() == plot_info.prover.get_filename() + assert plot_manager.plots[path].prover.get_filepath() == plot_info.prover.get_filepath() assert plot_manager.plots[path].prover.get_id() == plot_info.prover.get_id() assert plot_manager.plots[path].prover.get_memo() == plot_info.prover.get_memo() assert plot_manager.plots[path].prover.get_size() == plot_info.prover.get_size() diff --git a/chia/_tests/plotting/test_prover.py b/chia/_tests/plotting/test_prover.py index 68c34b8f5fe5..68053430d906 100644 --- a/chia/_tests/plotting/test_prover.py +++ b/chia/_tests/plotting/test_prover.py @@ -12,8 +12,8 @@ class TestProver: def test_v2_prover_init_with_nonexistent_file(self) -> None: prover = V2Prover("/nonexistent/path/test.plot2") assert prover.get_version() == PlotVersion.V2 - assert prover.get_filename() == Path("/nonexistent/path/test.plot2") - assert prover.get_filename_str() == "/nonexistent/path/test.plot2" + assert prover.get_filepath() == Path("/nonexistent/path/test.plot2") + assert prover.get_filename() == "/nonexistent/path/test.plot2" def test_v2_prover_get_size_raises_error(self) -> None: prover = V2Prover("/nonexistent/path/test.plot2") diff --git a/chia/plot_sync/sender.py b/chia/plot_sync/sender.py index ec1c51c3937b..74be15cae2cf 100644 --- a/chia/plot_sync/sender.py +++ b/chia/plot_sync/sender.py @@ -39,7 +39,7 @@ def _convert_plot_info_list(plot_infos: list[PlotInfo]) -> list[Plot]: for plot_info in plot_infos: converted.append( Plot( - filename=plot_info.prover.get_filename_str(), + filename=plot_info.prover.get_filename(), size=plot_info.prover.get_size(), plot_id=plot_info.prover.get_id(), pool_public_key=plot_info.pool_public_key, diff --git a/chia/plotting/manager.py b/chia/plotting/manager.py index 5cd9acb6eb08..52fac2d8c25c 100644 --- a/chia/plotting/manager.py +++ b/chia/plotting/manager.py @@ -386,10 +386,10 @@ def process_file(file_path: Path) -> Optional[PlotInfo]: with self.plot_filename_paths_lock: paths: Optional[tuple[str, set[str]]] = self.plot_filename_paths.get(file_path.name) if paths is None: - paths = (str(Path(cache_entry.prover.get_filename()).parent), set()) + paths = (str(Path(cache_entry.prover.get_filepath()).parent), set()) self.plot_filename_paths[file_path.name] = paths else: - paths[1].add(str(Path(cache_entry.prover.get_filename()).parent)) + paths[1].add(str(Path(cache_entry.prover.get_filepath()).parent)) log.warning(f"Have multiple copies of the plot {file_path.name} in {[paths[0], *paths[1]]}.") return None @@ -423,7 +423,7 @@ def process_file(file_path: Path) -> Optional[PlotInfo]: plots_refreshed: dict[Path, PlotInfo] = {} for new_plot in executor.map(process_file, plot_paths): if new_plot is not None: - plots_refreshed[Path(new_plot.prover.get_filename())] = new_plot + plots_refreshed[Path(new_plot.prover.get_filepath())] = new_plot self.plots.update(plots_refreshed) result.duration = time.time() - start_time diff --git a/chia/plotting/prover.py b/chia/plotting/prover.py index 79fc5a174723..a3deeaffd338 100644 --- a/chia/plotting/prover.py +++ b/chia/plotting/prover.py @@ -22,11 +22,11 @@ class PlotVersion(IntEnum): class ProverProtocol(ABC): @abstractmethod - def get_filename(self) -> Path: + def get_filepath(self) -> Path: """Returns the filename for the plot""" @abstractmethod - def get_filename_str(self) -> str: + def get_filename(self) -> str: """Returns the filename string for the plot""" @abstractmethod @@ -73,10 +73,10 @@ class V2Prover(ProverProtocol): def __init__(self, filename: str): self._filename = filename - def get_filename(self) -> Path: + def get_filepath(self) -> Path: return Path(self._filename) - def get_filename_str(self) -> str: + def get_filename(self) -> str: return str(self._filename) def get_size(self) -> uint8: @@ -122,10 +122,10 @@ class V1Prover(ProverProtocol): def __init__(self, disk_prover: DiskProver) -> None: self._disk_prover = disk_prover - def get_filename(self) -> Path: + def get_filepath(self) -> Path: return Path(self._disk_prover.get_filename()) - def get_filename_str(self) -> str: + def get_filename(self) -> str: return str(self._disk_prover.get_filename()) def get_size(self) -> uint8: From bffc7326e82416764dc14a41d3104f4f2218df51 Mon Sep 17 00:00:00 2001 From: almogdepaz Date: Thu, 17 Jul 2025 13:57:22 +0300 Subject: [PATCH 11/42] rename --- chia/_tests/plot_sync/test_plot_sync.py | 4 ++-- chia/_tests/plotting/test_plot_manager.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/chia/_tests/plot_sync/test_plot_sync.py b/chia/_tests/plot_sync/test_plot_sync.py index ff63e1e519f7..9e76d26b7a5a 100644 --- a/chia/_tests/plot_sync/test_plot_sync.py +++ b/chia/_tests/plot_sync/test_plot_sync.py @@ -65,7 +65,7 @@ class ExpectedResult: def add_valid(self, list_plots: list[MockPlotInfo]) -> None: def create_mock_plot(info: MockPlotInfo) -> Plot: return Plot( - info.prover.get_filename_str(), + info.prover.get_filename(), uint8(0), bytes32.zeros, None, @@ -77,7 +77,7 @@ def create_mock_plot(info: MockPlotInfo) -> Plot: ) self.valid_count += len(list_plots) - self.valid_delta.additions.update({x.prover.get_filename_str(): create_mock_plot(x) for x in list_plots}) + self.valid_delta.additions.update({x.prover.get_filename(): create_mock_plot(x) for x in list_plots}) def remove_valid(self, list_paths: list[Path]) -> None: self.valid_count -= len(list_paths) diff --git a/chia/_tests/plotting/test_plot_manager.py b/chia/_tests/plotting/test_plot_manager.py index dbae9a0a879a..e19a18ab6e01 100644 --- a/chia/_tests/plotting/test_plot_manager.py +++ b/chia/_tests/plotting/test_plot_manager.py @@ -42,10 +42,10 @@ class MockDiskProver: filename: str - def get_filename(self) -> Path: + def get_filepath(self) -> Path: return Path(self.filename) - def get_filename_str(self) -> str: + def get_filename(self) -> str: return self.filename @@ -598,7 +598,7 @@ def assert_cache(expected: list[MockPlotInfo]) -> None: test_cache.load() assert len(test_cache) == len(expected) for plot_info in expected: - assert test_cache.get(Path(plot_info.prover.get_filename())) is not None + assert test_cache.get(Path(plot_info.prover.get_filepath())) is not None # Modify two entries, with and without memo modification, they both should remain in the cache after load modify_cache_entry(0, 1500, modify_memo=False) @@ -617,7 +617,7 @@ def assert_cache(expected: list[MockPlotInfo]) -> None: # Write the modified cache entries to the file cache_path.write_bytes(bytes(VersionedBlob(uint16(CURRENT_VERSION), bytes(cache_data)))) # And now test that plots in invalid_entries are not longer loaded - assert_cache([plot_info for plot_info in plot_infos if str(plot_info.prover.get_filename()) not in invalid_entries]) + assert_cache([plot_info for plot_info in plot_infos if str(plot_info.prover.get_filepath()) not in invalid_entries]) @pytest.mark.anyio From 17b0b3435e5c347ea1f1ad7d9e9d41cb9ec59d6c Mon Sep 17 00:00:00 2001 From: almogdepaz Date: Sun, 20 Jul 2025 13:13:47 +0300 Subject: [PATCH 12/42] refactor --- chia/_tests/plot_sync/test_sync_simulated.py | 22 +++--- chia/_tests/plotting/test_plot_manager.py | 11 +-- chia/_tests/plotting/test_prover.py | 25 +++++- chia/harvester/harvester_api.py | 4 +- chia/plotting/cache.py | 1 - chia/plotting/manager.py | 6 +- chia/plotting/prover.py | 82 ++++++-------------- 7 files changed, 66 insertions(+), 85 deletions(-) diff --git a/chia/_tests/plot_sync/test_sync_simulated.py b/chia/_tests/plot_sync/test_sync_simulated.py index d48ab796ba09..7063bf3b74ff 100644 --- a/chia/_tests/plot_sync/test_sync_simulated.py +++ b/chia/_tests/plot_sync/test_sync_simulated.py @@ -69,17 +69,17 @@ async def run( initial: bool, ) -> None: for plot_info in loaded: - assert plot_info.prover.get_filepath() not in self.plots + assert Path(plot_info.prover.get_filename()) not in self.plots for plot_info in removed: - assert plot_info.prover.get_filepath() in self.plots + assert Path(plot_info.prover.get_filename()) in self.plots self.invalid = invalid self.keys_missing = keys_missing self.duplicates = duplicates - removed_paths: list[Path] = [p.prover.get_filepath() for p in removed] if removed is not None else [] - invalid_dict: dict[Path, int] = {p.prover.get_filepath(): 0 for p in self.invalid} - keys_missing_set: set[Path] = {p.prover.get_filepath() for p in self.keys_missing} + removed_paths: list[Path] = [Path(p.prover.get_filename()) for p in removed] if removed is not None else [] + invalid_dict: dict[Path, int] = {Path(p.prover.get_filename()): 0 for p in self.invalid} + keys_missing_set: set[Path] = {Path(p.prover.get_filename()) for p in self.keys_missing} duplicates_set: set[str] = {p.prover.get_filename() for p in self.duplicates} # Inject invalid plots into `PlotManager` of the harvester so that the callback calls below can use them @@ -91,7 +91,7 @@ async def run( # Inject duplicated plots into `PlotManager` of the harvester so that the callback calls below can use them # to sync them to the farmer. for plot_info in loaded: - plot_path = Path(plot_info.prover.get_filepath()) + plot_path = Path(plot_info.prover.get_filename()) self.harvester.plot_manager.plot_filename_paths[plot_path.name] = (str(plot_path.parent), set()) for duplicate in duplicates_set: plot_path = Path(duplicate) @@ -123,9 +123,9 @@ async def sync_done() -> bool: await time_out_assert(60, sync_done) for plot_info in loaded: - self.plots[plot_info.prover.get_filepath()] = plot_info + self.plots[Path(plot_info.prover.get_filename())] = plot_info for plot_info in removed: - del self.plots[plot_info.prover.get_filepath()] + del self.plots[Path(plot_info.prover.get_filename())] def validate_plot_sync(self) -> None: assert len(self.plots) == len(self.plot_sync_receiver.plots()) @@ -417,7 +417,7 @@ async def test_sync_reset_cases( # Inject some data into `PlotManager` of the harvester so that we can validate the reset worked and triggered a # fresh sync of all available data of the plot manager for plot_info in plots[0:10]: - test_data.plots[plot_info.prover.get_filepath()] = plot_info + test_data.plots[Path(plot_info.prover.get_filename())] = plot_info plot_manager.plots = test_data.plots test_data.invalid = plots[10:20] test_data.keys_missing = plots[20:30] @@ -425,8 +425,8 @@ async def test_sync_reset_cases( sender: Sender = test_runner.test_data[0].plot_sync_sender started_sync_id: uint64 = uint64(0) - plot_manager.failed_to_open_filenames = {p.prover.get_filepath(): 0 for p in test_data.invalid} - plot_manager.no_key_filenames = {p.prover.get_filepath() for p in test_data.keys_missing} + plot_manager.failed_to_open_filenames = {Path(p.prover.get_filename()): 0 for p in test_data.invalid} + plot_manager.no_key_filenames = {Path(p.prover.get_filename()) for p in test_data.keys_missing} async def wait_for_reset() -> bool: assert started_sync_id != 0 diff --git a/chia/_tests/plotting/test_plot_manager.py b/chia/_tests/plotting/test_plot_manager.py index e19a18ab6e01..16fcb50bf66e 100644 --- a/chia/_tests/plotting/test_plot_manager.py +++ b/chia/_tests/plotting/test_plot_manager.py @@ -42,9 +42,6 @@ class MockDiskProver: filename: str - def get_filepath(self) -> Path: - return Path(self.filename) - def get_filename(self) -> str: return self.filename @@ -115,7 +112,7 @@ def refresh_callback(self, event: PlotRefreshEvents, refresh_result: PlotRefresh for value in actual_value: if type(value) is PlotInfo: for plot_info in expected_list: - if plot_info.prover.get_filename() == value.prover.get_filepath(): + if plot_info.prover.get_filename() == value.prover.get_filename(): values_found += 1 continue else: @@ -507,7 +504,7 @@ async def test_plot_info_caching(environment, bt): await refresh_tester.run(expected_result) for path, plot_info in env.refresh_tester.plot_manager.plots.items(): assert path in plot_manager.plots - assert plot_manager.plots[path].prover.get_filepath() == plot_info.prover.get_filepath() + assert plot_manager.plots[path].prover.get_filename() == plot_info.prover.get_filename() assert plot_manager.plots[path].prover.get_id() == plot_info.prover.get_id() assert plot_manager.plots[path].prover.get_memo() == plot_info.prover.get_memo() assert plot_manager.plots[path].prover.get_size() == plot_info.prover.get_size() @@ -598,7 +595,7 @@ def assert_cache(expected: list[MockPlotInfo]) -> None: test_cache.load() assert len(test_cache) == len(expected) for plot_info in expected: - assert test_cache.get(Path(plot_info.prover.get_filepath())) is not None + assert test_cache.get(Path(plot_info.prover.get_filename())) is not None # Modify two entries, with and without memo modification, they both should remain in the cache after load modify_cache_entry(0, 1500, modify_memo=False) @@ -617,7 +614,7 @@ def assert_cache(expected: list[MockPlotInfo]) -> None: # Write the modified cache entries to the file cache_path.write_bytes(bytes(VersionedBlob(uint16(CURRENT_VERSION), bytes(cache_data)))) # And now test that plots in invalid_entries are not longer loaded - assert_cache([plot_info for plot_info in plot_infos if str(plot_info.prover.get_filepath()) not in invalid_entries]) + assert_cache([plot_info for plot_info in plot_infos if plot_info.prover.get_filename() not in invalid_entries]) @pytest.mark.anyio diff --git a/chia/_tests/plotting/test_prover.py b/chia/_tests/plotting/test_prover.py index 68053430d906..ce5f30e2cf96 100644 --- a/chia/_tests/plotting/test_prover.py +++ b/chia/_tests/plotting/test_prover.py @@ -2,17 +2,17 @@ import tempfile from pathlib import Path +from unittest.mock import MagicMock, patch import pytest -from chia.plotting.prover import PlotVersion, V2Prover, get_prover_from_file +from chia.plotting.prover import PlotVersion, V1Prover, V2Prover, get_prover_from_bytes, get_prover_from_file class TestProver: def test_v2_prover_init_with_nonexistent_file(self) -> None: prover = V2Prover("/nonexistent/path/test.plot2") assert prover.get_version() == PlotVersion.V2 - assert prover.get_filepath() == Path("/nonexistent/path/test.plot2") assert prover.get_filename() == "/nonexistent/path/test.plot2" def test_v2_prover_get_size_raises_error(self) -> None: @@ -71,6 +71,25 @@ def test_get_prover_from_file_with_plot1_still_works(self) -> None: Path(temp_path).unlink() def test_unsupported_file_extension_raises_value_error(self) -> None: - """Test that unsupported file extensions raise ValueError""" with pytest.raises(ValueError, match="Unsupported plot file"): get_prover_from_file("/nonexistent/path/test.txt") + + +class TestGetProverFromBytes: + def test_get_prover_from_bytes_v2_plot(self) -> None: + with patch("chia.plotting.prover.V2Prover.from_bytes") as mock_v2_from_bytes: + mock_prover = MagicMock() + mock_v2_from_bytes.return_value = mock_prover + result = get_prover_from_bytes("test.plot2", b"test_data") + assert result == mock_prover + + def test_get_prover_from_bytes_v1_plot(self) -> None: + with patch("chia.plotting.prover.DiskProver") as mock_disk_prover_class: + mock_disk_prover = MagicMock() + mock_disk_prover_class.from_bytes.return_value = mock_disk_prover + result = get_prover_from_bytes("test.plot", b"test_data") + assert isinstance(result, V1Prover) + + def test_get_prover_from_bytes_unsupported_extension(self) -> None: + with pytest.raises(ValueError, match="Unsupported plot file"): + get_prover_from_bytes("test.txt", b"test_data") diff --git a/chia/harvester/harvester_api.py b/chia/harvester/harvester_api.py index b547102b4354..d238a98634eb 100644 --- a/chia/harvester/harvester_api.py +++ b/chia/harvester/harvester_api.py @@ -97,7 +97,7 @@ async def new_signage_point_harvester( loop = asyncio.get_running_loop() def blocking_lookup(filename: Path, plot_info: PlotInfo) -> list[tuple[bytes32, ProofOfSpace]]: - # Uses the DiskProver object to lookup qualities. This is a blocking call, + # Uses the Prover object to lookup qualities. This is a blocking call, # so it should be run in a thread pool. try: plot_id = plot_info.prover.get_id() @@ -218,7 +218,7 @@ def blocking_lookup(filename: Path, plot_info: PlotInfo) -> list[tuple[bytes32, async def lookup_challenge( filename: Path, plot_info: PlotInfo ) -> tuple[Path, list[harvester_protocol.NewProofOfSpace]]: - # Executes a DiskProverLookup in a thread pool, and returns responses + # Executes a ProverLookup in a thread pool, and returns responses all_responses: list[harvester_protocol.NewProofOfSpace] = [] if self.harvester._shut_down: return filename, [] diff --git a/chia/plotting/cache.py b/chia/plotting/cache.py index d442ac5b07de..2c5dfbdd6a72 100644 --- a/chia/plotting/cache.py +++ b/chia/plotting/cache.py @@ -55,7 +55,6 @@ class CacheEntry: @classmethod def from_prover(cls, prover: ProverProtocol) -> CacheEntry: - """Create CacheEntry from prover""" ( pool_public_key_or_puzzle_hash, farmer_public_key, diff --git a/chia/plotting/manager.py b/chia/plotting/manager.py index 52fac2d8c25c..5cd9acb6eb08 100644 --- a/chia/plotting/manager.py +++ b/chia/plotting/manager.py @@ -386,10 +386,10 @@ def process_file(file_path: Path) -> Optional[PlotInfo]: with self.plot_filename_paths_lock: paths: Optional[tuple[str, set[str]]] = self.plot_filename_paths.get(file_path.name) if paths is None: - paths = (str(Path(cache_entry.prover.get_filepath()).parent), set()) + paths = (str(Path(cache_entry.prover.get_filename()).parent), set()) self.plot_filename_paths[file_path.name] = paths else: - paths[1].add(str(Path(cache_entry.prover.get_filepath()).parent)) + paths[1].add(str(Path(cache_entry.prover.get_filename()).parent)) log.warning(f"Have multiple copies of the plot {file_path.name} in {[paths[0], *paths[1]]}.") return None @@ -423,7 +423,7 @@ def process_file(file_path: Path) -> Optional[PlotInfo]: plots_refreshed: dict[Path, PlotInfo] = {} for new_plot in executor.map(process_file, plot_paths): if new_plot is not None: - plots_refreshed[Path(new_plot.prover.get_filepath())] = new_plot + plots_refreshed[Path(new_plot.prover.get_filename())] = new_plot self.plots.update(plots_refreshed) result.duration = time.time() - start_time diff --git a/chia/plotting/prover.py b/chia/plotting/prover.py index a3deeaffd338..5d095af07843 100644 --- a/chia/plotting/prover.py +++ b/chia/plotting/prover.py @@ -1,9 +1,7 @@ from __future__ import annotations -from abc import ABC, abstractmethod from enum import IntEnum -from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, ClassVar, Protocol, cast from chia_rs.sized_bytes import bytes32 from chia_rs.sized_ints import uint8 @@ -20,62 +18,30 @@ class PlotVersion(IntEnum): V2 = 2 -class ProverProtocol(ABC): - @abstractmethod - def get_filepath(self) -> Path: - """Returns the filename for the plot""" - - @abstractmethod - def get_filename(self) -> str: - """Returns the filename string for the plot""" - - @abstractmethod - def get_size(self) -> uint8: - """Returns the k size of the plot""" - - @abstractmethod - def get_memo(self) -> bytes: - """Returns the memo""" - - @abstractmethod - def get_compression_level(self) -> uint8: - """Returns the compression level""" - - @abstractmethod - def get_version(self) -> PlotVersion: - """Returns the plot version""" - - @abstractmethod - def __bytes__(self) -> bytes: - """Returns the prover bytes""" - - @abstractmethod - def get_id(self) -> bytes32: - """Returns the plot ID""" - - @abstractmethod - def get_qualities_for_challenge(self, challenge: bytes32) -> list[bytes32]: - """Returns the qualities for a given challenge""" - - @abstractmethod - def get_full_proof(self, challenge: bytes, index: int, parallel_read: bool = True) -> bytes: - """Returns the full proof for a given challenge and index""" +class ProverProtocol(Protocol): + def get_filename(self) -> str: ... + def get_size(self) -> uint8: ... + def get_memo(self) -> bytes: ... + def get_compression_level(self) -> uint8: ... + def get_version(self) -> PlotVersion: ... + def __bytes__(self) -> bytes: ... + def get_id(self) -> bytes32: ... + def get_qualities_for_challenge(self, challenge: bytes32) -> list[bytes32]: ... + def get_full_proof(self, challenge: bytes, index: int, parallel_read: bool = True) -> bytes: ... @classmethod - @abstractmethod - def from_bytes(cls, data: bytes) -> ProverProtocol: - """Create a prover from serialized bytes""" + def from_bytes(cls, data: bytes) -> ProverProtocol: ... -class V2Prover(ProverProtocol): - """V2 Plot Prover stubb""" +class V2Prover: + """Placeholder for future V2 plot format support""" + + if TYPE_CHECKING: + _protocol_check: ClassVar[ProverProtocol] = cast("V2Prover", None) def __init__(self, filename: str): self._filename = filename - def get_filepath(self) -> Path: - return Path(self._filename) - def get_filename(self) -> str: return str(self._filename) @@ -102,29 +68,29 @@ def get_id(self) -> bytes32: # TODO: Extract plot ID from V2 plot file raise NotImplementedError("V2 plot format is not yet implemented") - def get_qualities_for_challenge(self, _challenge: bytes) -> list[bytes32]: + def get_qualities_for_challenge(self, challenge: bytes) -> list[bytes32]: # TODO: todo_v2_plots Implement plot quality lookup raise NotImplementedError("V2 plot format is not yet implemented") - def get_full_proof(self, _challenge: bytes, _index: int, _parallel_read: bool = True) -> bytes: + def get_full_proof(self, challenge: bytes, index: int, parallel_read: bool = True) -> bytes: # TODO: todo_v2_plots Implement plot proof generation raise NotImplementedError("V2 plot format is not yet implemented") @classmethod - def from_bytes(cls, _data: bytes) -> V2Prover: + def from_bytes(cls, data: bytes) -> V2Prover: # TODO: todo_v2_plots Implement prover deserialization from cache raise NotImplementedError("V2 plot format is not yet implemented") -class V1Prover(ProverProtocol): +class V1Prover: """Wrapper for existing DiskProver to implement ProverProtocol""" + if TYPE_CHECKING: + _protocol_check: ClassVar[ProverProtocol] = cast("V1Prover", None) + def __init__(self, disk_prover: DiskProver) -> None: self._disk_prover = disk_prover - def get_filepath(self) -> Path: - return Path(self._disk_prover.get_filename()) - def get_filename(self) -> str: return str(self._disk_prover.get_filename()) From 42f33480006cf7890ab824c26059651e326b931f Mon Sep 17 00:00:00 2001 From: almogdepaz Date: Sun, 20 Jul 2025 15:08:59 +0300 Subject: [PATCH 13/42] improve coverage --- chia/_tests/plotting/test_prover.py | 8 ++++++++ chia/plotting/prover.py | 9 +-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/chia/_tests/plotting/test_prover.py b/chia/_tests/plotting/test_prover.py index ce5f30e2cf96..592280d2df52 100644 --- a/chia/_tests/plotting/test_prover.py +++ b/chia/_tests/plotting/test_prover.py @@ -75,6 +75,14 @@ def test_unsupported_file_extension_raises_value_error(self) -> None: get_prover_from_file("/nonexistent/path/test.txt") +class TestV1Prover: + def test_v1_prover_get_version(self) -> None: + """Test that V1Prover.get_version() returns PlotVersion.V1""" + mock_disk_prover = MagicMock() + prover = V1Prover(mock_disk_prover) + assert prover.get_version() == PlotVersion.V1 + + class TestGetProverFromBytes: def test_get_prover_from_bytes_v2_plot(self) -> None: with patch("chia.plotting.prover.V2Prover.from_bytes") as mock_v2_from_bytes: diff --git a/chia/plotting/prover.py b/chia/plotting/prover.py index 5d095af07843..cd9474b0d9cd 100644 --- a/chia/plotting/prover.py +++ b/chia/plotting/prover.py @@ -120,14 +120,7 @@ def get_full_proof(self, challenge: bytes, index: int, parallel_read: bool = Tru @classmethod def from_bytes(cls, data: bytes) -> V1Prover: - from chiapos import DiskProver - - disk_prover = DiskProver.from_bytes(data) - return cls(disk_prover) - - @property - def disk_prover(self) -> DiskProver: - return self._disk_prover + return cls(DiskProver.from_bytes(data)) def get_prover_from_bytes(filename: str, prover_data: bytes) -> ProverProtocol: From e3a8cfc875e9ffaa32385e3d17be106406e6fd48 Mon Sep 17 00:00:00 2001 From: almogdepaz Date: Tue, 22 Jul 2025 12:06:31 +0300 Subject: [PATCH 14/42] test from bytes --- chia/_tests/plotting/test_plot_manager.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/chia/_tests/plotting/test_plot_manager.py b/chia/_tests/plotting/test_plot_manager.py index 16fcb50bf66e..3108da756943 100644 --- a/chia/_tests/plotting/test_plot_manager.py +++ b/chia/_tests/plotting/test_plot_manager.py @@ -13,12 +13,14 @@ import pytest from chia_rs import G1Element from chia_rs.sized_ints import uint16, uint32 +from chiapos import DiskProver from chia._tests.plotting.util import get_test_plots from chia._tests.util.misc import boolean_datacases from chia._tests.util.time_out_assert import time_out_assert from chia.plotting.cache import CURRENT_VERSION, CacheDataV1 from chia.plotting.manager import Cache, PlotManager +from chia.plotting.prover import V1Prover from chia.plotting.util import ( PlotInfo, PlotRefreshEvents, @@ -743,6 +745,20 @@ async def test_recursive_plot_scan(environment: Environment) -> None: await env.refresh_tester.run(expected_result) +@pytest.mark.anyio +async def test_disk_prover_from_bytes(environment: Environment): + env: Environment = environment + expected_result = PlotRefreshResult() + expected_result.loaded = env.dir_1.plot_info_list() # type: ignore[assignment] + expected_result.processed = len(env.dir_1) + add_plot_directory(env.root_path, str(env.dir_1.path)) + await env.refresh_tester.run(expected_result) + _, plot_info = next(iter(env.refresh_tester.plot_manager.plots.items())) + recreated_prover = V1Prover(DiskProver.from_bytes(bytes(plot_info.prover))) + assert recreated_prover.get_id() == plot_info.prover.get_id() + assert recreated_prover.get_filename() == plot_info.prover.get_filename() + + @boolean_datacases(name="follow_links", false="no_follow", true="follow") @pytest.mark.anyio async def test_recursive_plot_scan_symlinks(environment: Environment, follow_links: bool) -> None: From e9fecc032664e70347e9561ac74768d32dac975e Mon Sep 17 00:00:00 2001 From: almogdepaz Date: Mon, 11 Aug 2025 12:08:29 +0300 Subject: [PATCH 15/42] harvester and farmer v2 support --- chia/_tests/harvester/test_harvester_api.py | 209 ++++++++++++++++++++ chia/farmer/farmer_api.py | 100 +++++++++- chia/harvester/harvester_api.py | 132 +++++++++++-- chia/protocols/harvester_protocol.py | 15 ++ chia/protocols/protocol_message_types.py | 1 + 5 files changed, 432 insertions(+), 25 deletions(-) create mode 100644 chia/_tests/harvester/test_harvester_api.py diff --git a/chia/_tests/harvester/test_harvester_api.py b/chia/_tests/harvester/test_harvester_api.py new file mode 100644 index 000000000000..b5ffb617d370 --- /dev/null +++ b/chia/_tests/harvester/test_harvester_api.py @@ -0,0 +1,209 @@ +from __future__ import annotations + +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest +from chia_rs import ProofOfSpace +from chia_rs.sized_bytes import bytes32 +from chia_rs.sized_ints import uint64 + +from chia._tests.conftest import HarvesterFarmerEnvironment +from chia.harvester.harvester_api import HarvesterAPI +from chia.plotting.util import PlotInfo +from chia.protocols import harvester_protocol +from chia.protocols.harvester_protocol import PoolDifficulty +from chia.server.ws_connection import WSChiaConnection +from chia.simulator.block_tools import BlockTools + + +def create_signage_point_harvester_from_constants(bt: BlockTools) -> harvester_protocol.NewSignagePointHarvester: + """create a NewSignagePointHarvester using real constants from block tools""" + # use the pre-generated signage point data from network_protocol_data.py + # but with real constants from block_tools + from chia._tests.util.network_protocol_data import new_signage_point_harvester + + # create a version with real constants values + return harvester_protocol.NewSignagePointHarvester( + challenge_hash=new_signage_point_harvester.challenge_hash, + difficulty=uint64(bt.constants.DIFFICULTY_STARTING), + sub_slot_iters=uint64(bt.constants.SUB_SLOT_ITERS_STARTING), + signage_point_index=new_signage_point_harvester.signage_point_index, + sp_hash=new_signage_point_harvester.sp_hash, + pool_difficulties=[], # empty for simplicity, unless testing pool functionality + peak_height=new_signage_point_harvester.peak_height, + last_tx_height=new_signage_point_harvester.last_tx_height, + ) + + +@pytest.mark.anyio +async def test_new_signage_point_harvester_no_keys( + harvester_farmer_environment: HarvesterFarmerEnvironment, +) -> None: + """test that new_signage_point_harvester returns early when no keys available""" + _farmer_service, _farmer_rpc_client, harvester_service, _harvester_rpc_client, bt = harvester_farmer_environment + harvester_api = harvester_service._server.api + assert isinstance(harvester_api, HarvesterAPI) + + # create real signage point data from block tools + new_challenge = create_signage_point_harvester_from_constants(bt) + + # mock plot manager to return false for public_keys_available + with patch.object(harvester_api.harvester.plot_manager, "public_keys_available", return_value=False): + mock_peer = MagicMock(spec=WSChiaConnection) + + result = harvester_api.new_signage_point_harvester(new_challenge, mock_peer) + assert result is None + + +@pytest.mark.anyio +async def test_new_signage_point_harvester_happy_path( + harvester_farmer_environment: HarvesterFarmerEnvironment, +) -> None: + """test successful signage point processing with valid plots""" + _farmer_service, _farmer_rpc_client, harvester_service, _harvester_rpc_client, bt = harvester_farmer_environment + harvester_api = harvester_service._server.api + assert isinstance(harvester_api, HarvesterAPI) + + # create real signage point data from block tools + new_challenge = create_signage_point_harvester_from_constants(bt) + + mock_peer = MagicMock(spec=WSChiaConnection) + + # create mock plot info + mock_prover = MagicMock() + mock_prover.get_id.return_value = bytes32(b"2" * 32) + mock_prover.get_size.return_value = 32 + mock_prover.get_qualities_for_challenge.return_value = [bytes32(b"quality" + b"0" * 25)] + + mock_plot_info = MagicMock(spec=PlotInfo) + mock_plot_info.prover = mock_prover + mock_plot_info.pool_contract_puzzle_hash = None + + plot_path = Path("/fake/plot.plot") + + with patch.object(harvester_api.harvester.plot_manager, "public_keys_available", return_value=True): + with patch.object(harvester_api.harvester.plot_manager, "plots", {plot_path: mock_plot_info}): + with patch("chia.harvester.harvester_api.passes_plot_filter", return_value=True): + with patch("chia.harvester.harvester_api.calculate_pos_challenge") as mock_calc_pos: + mock_calc_pos.return_value = bytes32(b"sp_challenge" + b"0" * 20) + + with patch("chia.harvester.harvester_api.calculate_iterations_quality") as mock_calc_iter: + # set required_iters low enough to pass the sp_interval_iters check + mock_calc_iter.return_value = uint64(1000) + + with patch("chia.harvester.harvester_api.calculate_sp_interval_iters") as mock_sp_interval: + mock_sp_interval.return_value = uint64(10000) + + with patch.object(mock_prover, "get_full_proof") as mock_get_proof: + mock_proof = MagicMock(spec=ProofOfSpace) + mock_get_proof.return_value = mock_proof, None + + result = harvester_api.new_signage_point_harvester(new_challenge, mock_peer) + # function returns None but should have processed the plot + assert result is None + + +@pytest.mark.anyio +async def test_new_signage_point_harvester_pool_difficulty_override( + harvester_farmer_environment: HarvesterFarmerEnvironment, +) -> None: + """test that pool difficulty overrides are applied correctly""" + _farmer_service, _farmer_rpc_client, harvester_service, _harvester_rpc_client, bt = harvester_farmer_environment + harvester_api = harvester_service._server.api + assert isinstance(harvester_api, HarvesterAPI) + + mock_peer = MagicMock(spec=WSChiaConnection) + + pool_puzzle_hash = bytes32(b"pool" + b"0" * 28) + + mock_prover = MagicMock() + mock_prover.get_id.return_value = bytes32(b"2" * 32) + mock_prover.get_size.return_value = 32 + mock_prover.get_qualities_for_challenge.return_value = [bytes32(b"quality" + b"0" * 25)] + + mock_plot_info = MagicMock(spec=PlotInfo) + mock_plot_info.prover = mock_prover + mock_plot_info.pool_contract_puzzle_hash = pool_puzzle_hash + + plot_path = Path("/fake/plot.plot") + + pool_difficulty = PoolDifficulty( + pool_contract_puzzle_hash=pool_puzzle_hash, + difficulty=uint64(500), # lower than main difficulty + sub_slot_iters=uint64(67108864), # different from main + ) + + # create real signage point data from constants with pool difficulty + new_challenge = create_signage_point_harvester_from_constants(bt) + # override with pool difficulty for this test + new_challenge = harvester_protocol.NewSignagePointHarvester( + challenge_hash=new_challenge.challenge_hash, + difficulty=new_challenge.difficulty, + sub_slot_iters=new_challenge.sub_slot_iters, + signage_point_index=new_challenge.signage_point_index, + sp_hash=new_challenge.sp_hash, + pool_difficulties=[pool_difficulty], # add pool difficulty + peak_height=new_challenge.peak_height, + last_tx_height=new_challenge.last_tx_height, + ) + + with patch.object(harvester_api.harvester.plot_manager, "public_keys_available", return_value=True): + with patch.object(harvester_api.harvester.plot_manager, "plots", {plot_path: mock_plot_info}): + with patch("chia.harvester.harvester_api.passes_plot_filter", return_value=True): + with patch("chia.harvester.harvester_api.calculate_pos_challenge") as mock_calc_pos: + mock_calc_pos.return_value = bytes32(b"sp_challenge" + b"0" * 20) + + with patch("chia.harvester.harvester_api.calculate_iterations_quality") as mock_calc_iter: + mock_calc_iter.return_value = uint64(1000) + + with patch("chia.harvester.harvester_api.calculate_sp_interval_iters") as mock_sp_interval: + mock_sp_interval.return_value = uint64(10000) + + with patch.object(mock_prover, "get_full_proof") as mock_get_proof: + mock_proof = MagicMock(spec=ProofOfSpace) + mock_get_proof.return_value = mock_proof, None + + result = harvester_api.new_signage_point_harvester(new_challenge, mock_peer) + + # verify that calculate_iterations_quality was called with pool difficulty + mock_calc_iter.assert_called() + call_args = mock_calc_iter.call_args[0] + assert call_args[3] == uint64(500) # pool difficulty was used + + assert result is None + + +@pytest.mark.anyio +async def test_new_signage_point_harvester_prover_error( + harvester_farmer_environment: HarvesterFarmerEnvironment, +) -> None: + """test error handling when prover fails""" + _farmer_service, _farmer_rpc_client, harvester_service, _harvester_rpc_client, bt = harvester_farmer_environment + harvester_api = harvester_service._server.api + assert isinstance(harvester_api, HarvesterAPI) + + # create real signage point data from block tools + new_challenge = create_signage_point_harvester_from_constants(bt) + + mock_peer = MagicMock(spec=WSChiaConnection) + + mock_prover = MagicMock() + mock_prover.get_id.return_value = bytes32(b"2" * 32) + mock_prover.get_qualities_for_challenge.side_effect = RuntimeError("test error") + + mock_plot_info = MagicMock(spec=PlotInfo) + mock_plot_info.prover = mock_prover + mock_plot_info.pool_contract_puzzle_hash = None + + plot_path = Path("/fake/plot.plot") + + with patch.object(harvester_api.harvester.plot_manager, "public_keys_available", return_value=True): + with patch.object(harvester_api.harvester.plot_manager, "plots", {plot_path: mock_plot_info}): + with patch("chia.harvester.harvester_api.passes_plot_filter", return_value=True): + with patch("chia.harvester.harvester_api.calculate_pos_challenge") as mock_calc_pos: + mock_calc_pos.return_value = bytes32(b"sp_challenge" + b"0" * 20) + + # should not raise exception, should handle error gracefully + result = harvester_api.new_signage_point_harvester(new_challenge, mock_peer) + assert result is None diff --git a/chia/farmer/farmer_api.py b/chia/farmer/farmer_api.py index 3a084212964d..dcd1508ee18f 100644 --- a/chia/farmer/farmer_api.py +++ b/chia/farmer/farmer_api.py @@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Any, ClassVar, Optional, Union, cast import aiohttp -from chia_rs import AugSchemeMPL, G2Element, PoolTarget, PrivateKey +from chia_rs import AugSchemeMPL, G2Element, PoolTarget, PrivateKey, ProofOfSpace from chia_rs.sized_bytes import bytes32 from chia_rs.sized_ints import uint8, uint16, uint32, uint64 @@ -15,7 +15,7 @@ from chia.farmer.farmer import Farmer, increment_pool_stats, strip_old_entries from chia.harvester.harvester_api import HarvesterAPI from chia.protocols import farmer_protocol, harvester_protocol -from chia.protocols.farmer_protocol import DeclareProofOfSpace, SignedValues +from chia.protocols.farmer_protocol import DeclareProofOfSpace, SignedValues, SolutionResponse from chia.protocols.harvester_protocol import ( PlotSyncDone, PlotSyncPathList, @@ -24,6 +24,7 @@ PoolDifficulty, SignatureRequestSourceData, SigningDataKind, + V2Qualities, ) from chia.protocols.outbound_message import Message, NodeType, make_msg from chia.protocols.pool_protocol import ( @@ -33,6 +34,7 @@ get_current_authentication_token, ) from chia.protocols.protocol_message_types import ProtocolMessageTypes +from chia.protocols.solver_protocol import SolverInfo from chia.server.api_protocol import ApiMetadata from chia.server.server import ssl_context_for_root from chia.server.ws_connection import WSChiaConnection @@ -73,7 +75,7 @@ async def new_proof_of_space( """ if new_proof_of_space.sp_hash not in self.farmer.number_of_responses: self.farmer.number_of_responses[new_proof_of_space.sp_hash] = 0 - self.farmer.cache_add_time[new_proof_of_space.sp_hash] = uint64(time.time()) + self.farmer.cache_add_time[new_proof_of_space.sp_hash] = uint64(int(time.time())) max_pos_per_sp = 5 @@ -170,14 +172,14 @@ async def new_proof_of_space( new_proof_of_space.proof, ) ) - self.farmer.cache_add_time[new_proof_of_space.sp_hash] = uint64(time.time()) + self.farmer.cache_add_time[new_proof_of_space.sp_hash] = uint64(int(time.time())) self.farmer.quality_str_to_identifiers[computed_quality_string] = ( new_proof_of_space.plot_identifier, new_proof_of_space.challenge_hash, new_proof_of_space.sp_hash, peer.peer_node_id, ) - self.farmer.cache_add_time[computed_quality_string] = uint64(time.time()) + self.farmer.cache_add_time[computed_quality_string] = uint64(int(time.time())) await peer.send_message(make_msg(ProtocolMessageTypes.request_signatures, request)) @@ -478,6 +480,92 @@ async def new_proof_of_space( return + @metadata.request(peer_required=True) + async def v2_qualities(self, quality_collection: V2Qualities, peer: WSChiaConnection) -> None: + """ + This is a response from the harvester for V2 plots, containing only qualities. + We store these qualities and will later use solver service to generate proofs when needed. + """ + if quality_collection.sp_hash not in self.farmer.number_of_responses: + self.farmer.number_of_responses[quality_collection.sp_hash] = 0 + self.farmer.cache_add_time[quality_collection.sp_hash] = uint64(int(time.time())) + + if quality_collection.sp_hash not in self.farmer.sps: + self.farmer.log.warning( + f"Received V2 quality collection for a signage point that we do not have {quality_collection.sp_hash}" + ) + return None + + self.farmer.cache_add_time[quality_collection.sp_hash] = uint64(int(time.time())) + + self.farmer.log.info( + f"Received V2 quality collection with {len(quality_collection.qualities)} qualities " + f"for plot {quality_collection.plot_identifier[:10]}... from {peer.peer_node_id}" + ) + + # Process each quality through solver service to get full proofs + for quality in quality_collection.qualities: + solver_info = SolverInfo( + plot_size=quality_collection.plot_size, + plot_diffculty=quality_collection.difficulty, # Note: typo in SolverInfo field name + quality_string=quality, + ) + + # Call solver service to get proof + # TODO: Add proper solver service node connection to farmer + # For now, assume solver service is available via server connections + proof_bytes = None + + # Try to call solver service - this requires farmer to have solver connections configured + try: + # Send solve request to solver service + # This would work if farmer is connected to a solver service node + solver_response = await self.farmer.server.send_to_all_and_wait_first( + [make_msg(ProtocolMessageTypes.solve, solver_info)], + NodeType.FARMER, # TODO: Need SOLVER node type + ) + + if solver_response is not None and isinstance(solver_response, SolutionResponse): + proof_bytes = solver_response.proof + self.farmer.log.debug(f"Received {len(proof_bytes)} byte proof from solver") + else: + self.farmer.log.warning(f"No valid solver response for quality {quality.hex()[:10]}...") + + except Exception as e: + self.farmer.log.error(f"Failed to call solver service for quality {quality.hex()[:10]}...: {e}") + + # Fall back to stub if solver service unavailable + if proof_bytes is None: + self.farmer.log.warning("Using stub proof - solver service not available") + proof_bytes = b"stub_proof_from_solver" + + # Create ProofOfSpace object using solver response and plot metadata + # Need to calculate sp_challenge_hash for ProofOfSpace constructor + # TODO: We need plot_id to calculate this properly - may need to add to V2Qualities + sp_challenge_hash = quality_collection.challenge_hash # Approximation for now + + # Create a NewProofOfSpace object that can go through existing flow + new_proof_of_space = harvester_protocol.NewProofOfSpace( + quality_collection.challenge_hash, + quality_collection.sp_hash, + quality_collection.plot_identifier, + ProofOfSpace( + sp_challenge_hash, + quality_collection.pool_public_key, + quality_collection.pool_contract_puzzle_hash, + quality_collection.plot_public_key, + quality_collection.plot_size, + proof_bytes, + ), + quality_collection.signage_point_index, + include_source_signature_data=False, + farmer_reward_address_override=None, + fee_info=None, + ) + + # Route through existing new_proof_of_space flow + await self.new_proof_of_space(new_proof_of_space, peer) + @metadata.request() async def respond_signatures(self, response: harvester_protocol.RespondSignatures) -> None: request = self._process_respond_signatures(response) @@ -558,7 +646,7 @@ async def new_signage_point(self, new_signage_point: farmer_protocol.NewSignageP pool_dict[key] = strip_old_entries(pairs=pool_dict[key], before=cutoff_24h) - now = uint64(time.time()) + now = uint64(int(time.time())) self.farmer.cache_add_time[new_signage_point.challenge_chain_sp] = now missing_signage_points = self.farmer.check_missing_signage_points(now, new_signage_point) self.farmer.state_changed( diff --git a/chia/harvester/harvester_api.py b/chia/harvester/harvester_api.py index 994dbb0313a5..7a90a9680018 100644 --- a/chia/harvester/harvester_api.py +++ b/chia/harvester/harvester_api.py @@ -15,10 +15,11 @@ calculate_sp_interval_iters, ) from chia.harvester.harvester import Harvester +from chia.plotting.prover import PlotVersion from chia.plotting.util import PlotInfo, parse_plot_info from chia.protocols import harvester_protocol from chia.protocols.farmer_protocol import FarmingInfo -from chia.protocols.harvester_protocol import Plot, PlotSyncResponse +from chia.protocols.harvester_protocol import Plot, PlotSyncResponse, V2Qualities from chia.protocols.outbound_message import Message, make_msg from chia.protocols.protocol_message_types import ProtocolMessageTypes from chia.server.api_protocol import ApiMetadata @@ -96,6 +97,69 @@ async def new_signage_point_harvester( loop = asyncio.get_running_loop() + def blocking_lookup_v2_qualities(filename: Path, plot_info: PlotInfo) -> Optional[V2Qualities]: + # Uses the V2 Prover object to lookup qualities only. No full proofs generated. + try: + plot_id = plot_info.prover.get_id() + sp_challenge_hash = calculate_pos_challenge( + plot_id, + new_challenge.challenge_hash, + new_challenge.sp_hash, + ) + try: + quality_strings = plot_info.prover.get_qualities_for_challenge(sp_challenge_hash) + except Exception as e: + self.harvester.log.error(f"Exception fetching qualities for V2 plot {filename}. {e}") + return None + + if quality_strings is not None and len(quality_strings) > 0: + # Get the appropriate difficulty for this plot + difficulty = new_challenge.difficulty + sub_slot_iters = new_challenge.sub_slot_iters + if plot_info.pool_contract_puzzle_hash is not None: + # Check for pool-specific difficulty + for pool_difficulty in new_challenge.pool_difficulties: + if pool_difficulty.pool_contract_puzzle_hash == plot_info.pool_contract_puzzle_hash: + difficulty = pool_difficulty.difficulty + sub_slot_iters = pool_difficulty.sub_slot_iters + break + + # Filter qualities that pass the required_iters check (same as V1 flow) + good_qualities = [] + sp_interval_iters = calculate_sp_interval_iters(self.harvester.constants, sub_slot_iters) + + for quality_str in quality_strings: + required_iters: uint64 = calculate_iterations_quality( + self.harvester.constants, + quality_str, + PlotSize.make_v1(plot_info.prover.get_size()), # TODO: todo_v2_plots update for V2 + difficulty, + new_challenge.sp_hash, + sub_slot_iters, + new_challenge.last_tx_height, + ) + + if required_iters < sp_interval_iters: + good_qualities.append(quality_str) + + if len(good_qualities) > 0: + return V2Qualities( + new_challenge.challenge_hash, + new_challenge.sp_hash, + good_qualities[0].hex() + str(filename.resolve()), + good_qualities, + new_challenge.signage_point_index, + plot_info.prover.get_size(), + difficulty, + plot_info.pool_public_key, + plot_info.pool_contract_puzzle_hash, + plot_info.plot_public_key, + ) + return None + except Exception as e: + self.harvester.log.error(f"Unknown error in V2 quality lookup: {e}") + return None + def blocking_lookup(filename: Path, plot_info: PlotInfo) -> list[tuple[bytes32, ProofOfSpace]]: # Uses the Prover object to lookup qualities. This is a blocking call, # so it should be run in a thread pool. @@ -215,6 +279,15 @@ def blocking_lookup(filename: Path, plot_info: PlotInfo) -> list[tuple[bytes32, self.harvester.log.error(f"Unknown error: {e}") return [] + async def lookup_v2_qualities(filename: Path, plot_info: PlotInfo) -> tuple[Path, Optional[V2Qualities]]: + # Executes V2 quality lookup in a thread pool + if self.harvester._shut_down: + return filename, None + v2_quality_collection: Optional[V2Qualities] = await loop.run_in_executor( + self.harvester.executor, blocking_lookup_v2_qualities, filename, plot_info + ) + return filename, v2_quality_collection + async def lookup_challenge( filename: Path, plot_info: PlotInfo ) -> tuple[Path, list[harvester_protocol.NewProofOfSpace]]: @@ -241,6 +314,7 @@ async def lookup_challenge( return filename, all_responses awaitables = [] + v2_awaitables = [] passed = 0 total = 0 with self.harvester.plot_manager: @@ -249,28 +323,32 @@ async def lookup_challenge( # Passes the plot filter (does not check sp filter yet though, since we have not reached sp) # This is being executed at the beginning of the slot total += 1 - - # TODO: todo_v2_plots support v2 plots in PlotManager - filter_prefix_bits = uint8( - calculate_prefix_bits( + if try_plot_info.prover.get_version() == PlotVersion.V2: + # TODO: todo_v2_plots need to check v2 filter + v2_awaitables.append(lookup_v2_qualities(try_plot_filename, try_plot_info)) + passed += 1 + else: + filter_prefix_bits = calculate_prefix_bits( self.harvester.constants, new_challenge.peak_height, PlotSize.make_v1(try_plot_info.prover.get_size()), ) - ) - if passes_plot_filter( - filter_prefix_bits, - try_plot_info.prover.get_id(), - new_challenge.challenge_hash, - new_challenge.sp_hash, - ): - passed += 1 - awaitables.append(lookup_challenge(try_plot_filename, try_plot_info)) + if passes_plot_filter( + filter_prefix_bits, + try_plot_info.prover.get_id(), + new_challenge.challenge_hash, + new_challenge.sp_hash, + ): + passed += 1 + awaitables.append(lookup_challenge(try_plot_filename, try_plot_info)) self.harvester.log.debug(f"new_signage_point_harvester {passed} plots passed the plot filter") # Concurrently executes all lookups on disk, to take advantage of multiple disk parallelism time_taken = time.monotonic() - start total_proofs_found = 0 + total_v2_qualities_found = 0 + + # Process V1 plot responses (existing flow) for filename_sublist_awaitable in asyncio.as_completed(awaitables): filename, sublist = await filename_sublist_awaitable time_taken = time.monotonic() - start @@ -287,7 +365,21 @@ async def lookup_challenge( msg = make_msg(ProtocolMessageTypes.new_proof_of_space, response) await peer.send_message(msg) - now = uint64(time.time()) + # Process V2 plot quality collections (new flow) + for filename_quality_awaitable in asyncio.as_completed(v2_awaitables): + filename, v2_quality_collection = await filename_quality_awaitable + time_taken = time.monotonic() - start + if time_taken > 8: + self.harvester.log.warning( + f"Looking up V2 qualities on {filename} took: {time_taken}. This should be below 8 seconds" + f" to minimize risk of losing rewards." + ) + if v2_quality_collection is not None: + total_v2_qualities_found += len(v2_quality_collection.qualities) + msg = make_msg(ProtocolMessageTypes.v2_qualities, v2_quality_collection) + await peer.send_message(msg) + + now = uint64(int(time.time())) farming_info = FarmingInfo( new_challenge.challenge_hash, @@ -302,9 +394,10 @@ async def lookup_challenge( await peer.send_message(pass_msg) self.harvester.log.info( - f"{len(awaitables)} plots were eligible for farming {new_challenge.challenge_hash.hex()[:10]}..." - f" Found {total_proofs_found} proofs. Time: {time_taken:.5f} s. " - f"Total {self.harvester.plot_manager.plot_count()} plots" + f"challenge_hash: {new_challenge.challenge_hash.hex()[:10]} ..." + f"{len(awaitables) + len(v2_awaitables)} plots were eligible for farming challenge" + f"Found {total_proofs_found} V1 proofs and {total_v2_qualities_found} V2 qualities." + f" Time: {time_taken:.5f} s. Total {self.harvester.plot_manager.plot_count()} plots" ) self.harvester.state_changed( "farming_info", @@ -312,7 +405,8 @@ async def lookup_challenge( "challenge_hash": new_challenge.challenge_hash.hex(), "total_plots": self.harvester.plot_manager.plot_count(), "found_proofs": total_proofs_found, - "eligible_plots": len(awaitables), + "found_v2_qualities": total_v2_qualities_found, + "eligible_plots": len(awaitables) + len(v2_awaitables), "time": time_taken, }, ) diff --git a/chia/protocols/harvester_protocol.py b/chia/protocols/harvester_protocol.py index 5554d5c95f9a..722f0070ffbd 100644 --- a/chia/protocols/harvester_protocol.py +++ b/chia/protocols/harvester_protocol.py @@ -63,6 +63,21 @@ class NewProofOfSpace(Streamable): fee_info: Optional[ProofOfSpaceFeeInfo] +@streamable +@dataclass(frozen=True) +class V2Qualities(Streamable): + challenge_hash: bytes32 + sp_hash: bytes32 + plot_identifier: str + qualities: list[bytes32] + signage_point_index: uint8 + plot_size: uint8 + difficulty: uint64 + pool_public_key: Optional[G1Element] + pool_contract_puzzle_hash: Optional[bytes32] + plot_public_key: G1Element + + # Source data corresponding to the hash that is sent to the Harvester for signing class SigningDataKind(IntEnum): FOLIAGE_BLOCK_DATA = 1 diff --git a/chia/protocols/protocol_message_types.py b/chia/protocols/protocol_message_types.py index 97c49331a22f..5efdcc93bf32 100644 --- a/chia/protocols/protocol_message_types.py +++ b/chia/protocols/protocol_message_types.py @@ -13,6 +13,7 @@ class ProtocolMessageTypes(Enum): new_proof_of_space = 5 request_signatures = 6 respond_signatures = 7 + v2_qualities = 110 # Farmer protocol (farmer <-> full_node) new_signage_point = 8 From 1626309a2ccb7c255971f80ea6277ad0b7d6acf2 Mon Sep 17 00:00:00 2001 From: almogdepaz Date: Mon, 11 Aug 2025 23:18:13 +0300 Subject: [PATCH 16/42] solver service, api, rpc --- chia/_tests/conftest.py | 1 + .../core/custom_types/test_proof_of_space.py | 2 +- chia/_tests/harvester/test_harvester_api.py | 206 +++++++----------- chia/_tests/plotting/test_prover.py | 2 +- chia/_tests/solver/test_solver.py | 30 --- chia/_tests/solver/test_solver_service.py | 160 ++++++++++++++ chia/_tests/util/setup_nodes.py | 3 +- chia/cmds/chia.py | 2 + chia/cmds/cmds_util.py | 3 + chia/cmds/rpc.py | 2 +- chia/cmds/solver.py | 80 +++++++ chia/cmds/solver_funcs.py | 42 ++++ chia/plotting/prover.py | 2 +- chia/protocols/protocol_message_types.py | 2 +- chia/simulator/setup_services.py | 2 + chia/solver/solver.py | 16 +- chia/solver/solver_api.py | 34 ++- chia/solver/solver_rpc_api.py | 52 +++++ chia/solver/solver_rpc_client.py | 25 +++ chia/solver/solver_service.py | 8 + chia/{server => solver}/start_solver.py | 7 +- .../types/blockchain_format/proof_of_space.py | 6 +- chia/util/initial-config.yaml | 17 ++ chia/util/service_groups.py | 2 + pyproject.toml | 1 + 25 files changed, 529 insertions(+), 178 deletions(-) delete mode 100644 chia/_tests/solver/test_solver.py create mode 100644 chia/_tests/solver/test_solver_service.py create mode 100644 chia/cmds/solver.py create mode 100644 chia/cmds/solver_funcs.py create mode 100644 chia/solver/solver_rpc_api.py create mode 100644 chia/solver/solver_rpc_client.py create mode 100644 chia/solver/solver_service.py rename chia/{server => solver}/start_solver.py (96%) diff --git a/chia/_tests/conftest.py b/chia/_tests/conftest.py index e619fcdec66c..53fdf7312abf 100644 --- a/chia/_tests/conftest.py +++ b/chia/_tests/conftest.py @@ -70,6 +70,7 @@ ) from chia.simulator.start_simulator import SimulatorFullNodeService from chia.simulator.wallet_tools import WalletTool +from chia.solver.solver_service import SolverService from chia.timelord.timelord_service import TimelordService from chia.types.peer_info import PeerInfo from chia.util.config import create_default_chia_config, lock_and_load_config diff --git a/chia/_tests/core/custom_types/test_proof_of_space.py b/chia/_tests/core/custom_types/test_proof_of_space.py index ed8520f36051..8dd72e195254 100644 --- a/chia/_tests/core/custom_types/test_proof_of_space.py +++ b/chia/_tests/core/custom_types/test_proof_of_space.py @@ -223,7 +223,7 @@ def test_calculate_plot_difficulty(height: uint32, difficulty: uint8) -> None: class TestProofOfSpace: @pytest.mark.parametrize("prefix_bits", [DEFAULT_CONSTANTS.NUMBER_ZERO_BITS_PLOT_FILTER_V1, 8, 7, 6, 5, 1, 0]) - def test_can_create_proof(self, prefix_bits: int, seeded_random: random.Random) -> None: + def test_can_create_proof(self, prefix_bits: uint8, seeded_random: random.Random) -> None: """ Tests that the change of getting a correct proof is exactly 1/target_filter. """ diff --git a/chia/_tests/harvester/test_harvester_api.py b/chia/_tests/harvester/test_harvester_api.py index b5ffb617d370..043337c9cf29 100644 --- a/chia/_tests/harvester/test_harvester_api.py +++ b/chia/_tests/harvester/test_harvester_api.py @@ -17,126 +17,100 @@ from chia.simulator.block_tools import BlockTools -def create_signage_point_harvester_from_constants(bt: BlockTools) -> harvester_protocol.NewSignagePointHarvester: - """create a NewSignagePointHarvester using real constants from block tools""" - # use the pre-generated signage point data from network_protocol_data.py - # but with real constants from block_tools - from chia._tests.util.network_protocol_data import new_signage_point_harvester +def signage_point_from_block(bt: BlockTools) -> harvester_protocol.NewSignagePointHarvester: + """Create a real NewSignagePointHarvester from actual blockchain blocks.""" + # generate real blocks using BlockTools + blocks = bt.get_consecutive_blocks( + num_blocks=3, + guarantee_transaction_block=True, + farmer_reward_puzzle_hash=bt.farmer_ph, + ) + block = blocks[-1] # always use the last block + # extract real signage point data from the block + sp_index = block.reward_chain_block.signage_point_index + challenge_hash = block.reward_chain_block.pos_ss_cc_challenge_hash + sp_hash = ( + block.reward_chain_block.reward_chain_sp_vdf.output.get_hash() + if block.reward_chain_block.reward_chain_sp_vdf + else challenge_hash + ) - # create a version with real constants values return harvester_protocol.NewSignagePointHarvester( - challenge_hash=new_signage_point_harvester.challenge_hash, + challenge_hash=challenge_hash, difficulty=uint64(bt.constants.DIFFICULTY_STARTING), sub_slot_iters=uint64(bt.constants.SUB_SLOT_ITERS_STARTING), - signage_point_index=new_signage_point_harvester.signage_point_index, - sp_hash=new_signage_point_harvester.sp_hash, - pool_difficulties=[], # empty for simplicity, unless testing pool functionality - peak_height=new_signage_point_harvester.peak_height, - last_tx_height=new_signage_point_harvester.last_tx_height, + signage_point_index=sp_index, + sp_hash=sp_hash, + pool_difficulties=[], + peak_height=block.height, + last_tx_height=block.height, ) -@pytest.mark.anyio -async def test_new_signage_point_harvester_no_keys( - harvester_farmer_environment: HarvesterFarmerEnvironment, -) -> None: - """test that new_signage_point_harvester returns early when no keys available""" - _farmer_service, _farmer_rpc_client, harvester_service, _harvester_rpc_client, bt = harvester_farmer_environment - harvester_api = harvester_service._server.api - assert isinstance(harvester_api, HarvesterAPI) - - # create real signage point data from block tools - new_challenge = create_signage_point_harvester_from_constants(bt) - - # mock plot manager to return false for public_keys_available - with patch.object(harvester_api.harvester.plot_manager, "public_keys_available", return_value=False): - mock_peer = MagicMock(spec=WSChiaConnection) +def create_plot_info() -> PlotInfo: + """Create a realistic PlotInfo mock for testing.""" + mock_prover = MagicMock() + mock_prover.get_id.return_value = bytes32(b"plot_id_123456789012345678901234") # exactly 32 bytes + mock_prover.get_size.return_value = 32 # standard k32 plot + mock_prover.get_qualities_for_challenge.return_value = [ + bytes32(b"quality_123456789012345678901234") + ] # exactly 32 bytes + mock_plot_info = MagicMock(spec=PlotInfo) + mock_plot_info.prover = mock_prover + mock_plot_info.pool_contract_puzzle_hash = None - result = harvester_api.new_signage_point_harvester(new_challenge, mock_peer) - assert result is None + return mock_plot_info @pytest.mark.anyio -async def test_new_signage_point_harvester_happy_path( - harvester_farmer_environment: HarvesterFarmerEnvironment, -) -> None: - """test successful signage point processing with valid plots""" - _farmer_service, _farmer_rpc_client, harvester_service, _harvester_rpc_client, bt = harvester_farmer_environment +async def test_new_signage_point_harvester(harvester_farmer_environment: HarvesterFarmerEnvironment) -> None: + """Test successful signage point processing with real blockchain data.""" + _, _, harvester_service, _, bt = harvester_farmer_environment harvester_api = harvester_service._server.api assert isinstance(harvester_api, HarvesterAPI) - - # create real signage point data from block tools - new_challenge = create_signage_point_harvester_from_constants(bt) - + # use real signage point data from actual block + new_challenge = signage_point_from_block(bt) + # harvester doesn't accept incoming connections, so use mock peer like other tests mock_peer = MagicMock(spec=WSChiaConnection) - - # create mock plot info - mock_prover = MagicMock() - mock_prover.get_id.return_value = bytes32(b"2" * 32) - mock_prover.get_size.return_value = 32 - mock_prover.get_qualities_for_challenge.return_value = [bytes32(b"quality" + b"0" * 25)] - - mock_plot_info = MagicMock(spec=PlotInfo) - mock_plot_info.prover = mock_prover - mock_plot_info.pool_contract_puzzle_hash = None - + # create realistic plot info for testing + mock_plot_info = create_plot_info() plot_path = Path("/fake/plot.plot") with patch.object(harvester_api.harvester.plot_manager, "public_keys_available", return_value=True): with patch.object(harvester_api.harvester.plot_manager, "plots", {plot_path: mock_plot_info}): - with patch("chia.harvester.harvester_api.passes_plot_filter", return_value=True): - with patch("chia.harvester.harvester_api.calculate_pos_challenge") as mock_calc_pos: - mock_calc_pos.return_value = bytes32(b"sp_challenge" + b"0" * 20) - - with patch("chia.harvester.harvester_api.calculate_iterations_quality") as mock_calc_iter: - # set required_iters low enough to pass the sp_interval_iters check - mock_calc_iter.return_value = uint64(1000) - - with patch("chia.harvester.harvester_api.calculate_sp_interval_iters") as mock_sp_interval: - mock_sp_interval.return_value = uint64(10000) - - with patch.object(mock_prover, "get_full_proof") as mock_get_proof: - mock_proof = MagicMock(spec=ProofOfSpace) - mock_get_proof.return_value = mock_proof, None - - result = harvester_api.new_signage_point_harvester(new_challenge, mock_peer) - # function returns None but should have processed the plot - assert result is None + # let passes_plot_filter, calculate_pos_challenge, and calculate_sp_interval_iters use real implementations + with patch("chia.harvester.harvester_api.calculate_iterations_quality", return_value=uint64(1000)): + with patch.object(mock_plot_info.prover, "get_full_proof") as mock_get_proof: + mock_proof = MagicMock(spec=ProofOfSpace) + mock_get_proof.return_value = mock_proof, None + await harvester_api.new_signage_point_harvester(new_challenge, mock_peer) @pytest.mark.anyio -async def test_new_signage_point_harvester_pool_difficulty_override( +async def test_new_signage_point_harvester_pool_difficulty( harvester_farmer_environment: HarvesterFarmerEnvironment, ) -> None: - """test that pool difficulty overrides are applied correctly""" - _farmer_service, _farmer_rpc_client, harvester_service, _harvester_rpc_client, bt = harvester_farmer_environment + """Test pool difficulty overrides with real blockchain signage points.""" + _, _, harvester_service, _, bt = harvester_farmer_environment harvester_api = harvester_service._server.api assert isinstance(harvester_api, HarvesterAPI) + # harvester doesn't accept incoming connections, so use mock peer like other tests mock_peer = MagicMock(spec=WSChiaConnection) - pool_puzzle_hash = bytes32(b"pool" + b"0" * 28) - mock_prover = MagicMock() - mock_prover.get_id.return_value = bytes32(b"2" * 32) - mock_prover.get_size.return_value = 32 - mock_prover.get_qualities_for_challenge.return_value = [bytes32(b"quality" + b"0" * 25)] - - mock_plot_info = MagicMock(spec=PlotInfo) - mock_plot_info.prover = mock_prover + # create realistic plot info for testing + mock_plot_info = create_plot_info() mock_plot_info.pool_contract_puzzle_hash = pool_puzzle_hash - - plot_path = Path("/fake/plot.plot") - + plot_path = Path("/fake/pool_plot.plot") pool_difficulty = PoolDifficulty( pool_contract_puzzle_hash=pool_puzzle_hash, difficulty=uint64(500), # lower than main difficulty sub_slot_iters=uint64(67108864), # different from main ) - # create real signage point data from constants with pool difficulty - new_challenge = create_signage_point_harvester_from_constants(bt) - # override with pool difficulty for this test + # create signage point from real block with pool difficulty + new_challenge = signage_point_from_block(bt) new_challenge = harvester_protocol.NewSignagePointHarvester( challenge_hash=new_challenge.challenge_hash, difficulty=new_challenge.difficulty, @@ -150,60 +124,44 @@ async def test_new_signage_point_harvester_pool_difficulty_override( with patch.object(harvester_api.harvester.plot_manager, "public_keys_available", return_value=True): with patch.object(harvester_api.harvester.plot_manager, "plots", {plot_path: mock_plot_info}): + # mock passes_plot_filter to return True so we can test pool difficulty logic with patch("chia.harvester.harvester_api.passes_plot_filter", return_value=True): - with patch("chia.harvester.harvester_api.calculate_pos_challenge") as mock_calc_pos: - mock_calc_pos.return_value = bytes32(b"sp_challenge" + b"0" * 20) - - with patch("chia.harvester.harvester_api.calculate_iterations_quality") as mock_calc_iter: - mock_calc_iter.return_value = uint64(1000) - - with patch("chia.harvester.harvester_api.calculate_sp_interval_iters") as mock_sp_interval: - mock_sp_interval.return_value = uint64(10000) - - with patch.object(mock_prover, "get_full_proof") as mock_get_proof: - mock_proof = MagicMock(spec=ProofOfSpace) - mock_get_proof.return_value = mock_proof, None - - result = harvester_api.new_signage_point_harvester(new_challenge, mock_peer) - - # verify that calculate_iterations_quality was called with pool difficulty - mock_calc_iter.assert_called() - call_args = mock_calc_iter.call_args[0] - assert call_args[3] == uint64(500) # pool difficulty was used - - assert result is None + with patch("chia.harvester.harvester_api.calculate_iterations_quality") as mock_calc_iter: + mock_calc_iter.return_value = uint64(1000) + with patch.object(mock_plot_info.prover, "get_full_proof") as mock_get_proof: + mock_proof = MagicMock(spec=ProofOfSpace) + mock_get_proof.return_value = mock_proof, None + await harvester_api.new_signage_point_harvester(new_challenge, mock_peer) + # verify that calculate_iterations_quality was called with pool difficulty + mock_calc_iter.assert_called() + call_args = mock_calc_iter.call_args[0] + assert call_args[3] == uint64(500) # pool difficulty was used @pytest.mark.anyio async def test_new_signage_point_harvester_prover_error( harvester_farmer_environment: HarvesterFarmerEnvironment, ) -> None: - """test error handling when prover fails""" - _farmer_service, _farmer_rpc_client, harvester_service, _harvester_rpc_client, bt = harvester_farmer_environment + """Test error handling when prover fails using real blockchain data.""" + _, _, harvester_service, _, bt = harvester_farmer_environment harvester_api = harvester_service._server.api assert isinstance(harvester_api, HarvesterAPI) - # create real signage point data from block tools - new_challenge = create_signage_point_harvester_from_constants(bt) + # create signage point from real block + new_challenge = signage_point_from_block(bt) mock_peer = MagicMock(spec=WSChiaConnection) - mock_prover = MagicMock() - mock_prover.get_id.return_value = bytes32(b"2" * 32) - mock_prover.get_qualities_for_challenge.side_effect = RuntimeError("test error") - - mock_plot_info = MagicMock(spec=PlotInfo) - mock_plot_info.prover = mock_prover - mock_plot_info.pool_contract_puzzle_hash = None - + # create realistic plot info for testing + mock_plot_info = create_plot_info() plot_path = Path("/fake/plot.plot") with patch.object(harvester_api.harvester.plot_manager, "public_keys_available", return_value=True): with patch.object(harvester_api.harvester.plot_manager, "plots", {plot_path: mock_plot_info}): - with patch("chia.harvester.harvester_api.passes_plot_filter", return_value=True): - with patch("chia.harvester.harvester_api.calculate_pos_challenge") as mock_calc_pos: - mock_calc_pos.return_value = bytes32(b"sp_challenge" + b"0" * 20) - - # should not raise exception, should handle error gracefully - result = harvester_api.new_signage_point_harvester(new_challenge, mock_peer) - assert result is None + # let passes_plot_filter and calculate_pos_challenge use real implementations + # make the prover fail during quality check + with patch.object( + mock_plot_info.prover, "get_qualities_for_challenge", side_effect=RuntimeError("test error") + ): + # should not raise exception, should handle error gracefully + await harvester_api.new_signage_point_harvester(new_challenge, mock_peer) diff --git a/chia/_tests/plotting/test_prover.py b/chia/_tests/plotting/test_prover.py index 592280d2df52..5fd753f6d339 100644 --- a/chia/_tests/plotting/test_prover.py +++ b/chia/_tests/plotting/test_prover.py @@ -42,7 +42,7 @@ def test_v2_prover_get_qualities_for_challenge_raises_error(self) -> None: def test_v2_prover_get_full_proof_raises_error(self) -> None: prover = V2Prover("/nonexistent/path/test.plot2") - with pytest.raises(NotImplementedError, match="V2 plot format is not yet implemented"): + with pytest.raises(NotImplementedError, match="V2 plot format require solver to get full proof"): prover.get_full_proof(b"challenge", 0) def test_v2_prover_bytes_raises_error(self) -> None: diff --git a/chia/_tests/solver/test_solver.py b/chia/_tests/solver/test_solver.py deleted file mode 100644 index 4fea5ec154d2..000000000000 --- a/chia/_tests/solver/test_solver.py +++ /dev/null @@ -1,30 +0,0 @@ -from __future__ import annotations - -import pytest -from chia_rs.sized_bytes import bytes32 -from chia_rs.sized_ints import uint8, uint64 - -from chia.protocols.solver_protocol import SolverInfo -from chia.server.aliases import SolverService - - -@pytest.mark.anyio -async def test_solver_solve(solver_service: SolverService) -> None: - """Test that the solver service can process a solve request.""" - solver = solver_service._node - solver_api = solver_service._api - - # Create test SolverInfo - test_info = SolverInfo(plot_size=uint8(32), plot_diffculty=uint64(1000), quality_string=bytes32.zeros) - - # Call solve directly on the solver - result = solver.solve(test_info) - - # Should return None since it's not implemented - assert result is None - - # Test through the API - api_result = await solver_api.solve(test_info) - - # Should return None since solver.solve returns None - assert api_result is None diff --git a/chia/_tests/solver/test_solver_service.py b/chia/_tests/solver/test_solver_service.py new file mode 100644 index 000000000000..abff118072b9 --- /dev/null +++ b/chia/_tests/solver/test_solver_service.py @@ -0,0 +1,160 @@ +from __future__ import annotations + +from typing import Optional +from unittest.mock import patch + +import pytest +from chia_rs.sized_bytes import bytes32 +from chia_rs.sized_ints import uint8, uint64 + +from chia._tests.blockchain.blockchain_test_utils import _validate_and_add_block +from chia.consensus.blockchain import Blockchain +from chia.consensus.get_block_challenge import get_block_challenge +from chia.consensus.pot_iterations import is_overflow_block +from chia.protocols.solver_protocol import SolverInfo +from chia.simulator.block_tools import BlockTools +from chia.simulator.setup_services import setup_solver +from chia.solver.solver_rpc_client import SolverRpcClient +from chia.types.blockchain_format.proof_of_space import verify_and_get_quality_string + + +@pytest.mark.anyio +async def test_solver_api_methods(bt: BlockTools) -> None: + """Test solver api protocol methods with real requests.""" + solver_temp_dir = bt.root_path / "solver_farmer_test" + solver_temp_dir.mkdir(exist_ok=True) + async with setup_solver(solver_temp_dir, bt.constants) as solver_service: + solver = solver_service._node + solver_api = solver_service._api + + # test api ready state + assert solver_api.ready() is True + + # test solve with real SolverInfo + test_info = SolverInfo(plot_size=uint8(32), plot_diffculty=uint64(1500), quality_string=bytes32([42] * 32)) + + # test normal solve operation (stub returns None) + result = solver.solve(test_info) + assert result is None + + # test with mocked return value to verify full flow + expected_proof = b"test_proof_data_12345" + with patch.object(solver, "solve", return_value=expected_proof): + api_result = await solver_api.solve(test_info) + assert api_result is not None + # api returns protocol message for peer communication + from chia.protocols.outbound_message import Message + + assert isinstance(api_result, Message) + + # test error handling - solver not started + original_started = solver.started + solver.started = False + api_result = await solver_api.solve(test_info) + assert api_result is None + solver.started = original_started + + +@pytest.mark.anyio +async def test_non_overflow_genesis(empty_blockchain: Blockchain, bt: BlockTools) -> None: + blocks = bt.get_consecutive_blocks( + num_blocks=3, + guarantee_transaction_block=True, + farmer_reward_puzzle_hash=bt.farmer_ph, + ) + for block in blocks: + await _validate_and_add_block(empty_blockchain, block) + + +@pytest.mark.anyio +async def test_solver_with_real_blocks_and_signage_points( + bt: BlockTools, empty_blockchain: Blockchain, self_hostname: str +) -> None: + blockchain = empty_blockchain + blocks = bt.get_consecutive_blocks( + num_blocks=3, + guarantee_transaction_block=True, + farmer_reward_puzzle_hash=bt.farmer_ph, + ) + for block in blocks: + await _validate_and_add_block(empty_blockchain, block) + block = blocks[-1] # always use the last block + overflow = is_overflow_block(bt.constants, block.reward_chain_block.signage_point_index) + challenge = get_block_challenge(bt.constants, block, blockchain, False, overflow, False) + assert block.reward_chain_block.pos_ss_cc_challenge_hash == challenge + if block.reward_chain_block.challenge_chain_sp_vdf is None: + challenge_chain_sp: bytes32 = challenge + else: + challenge_chain_sp = block.reward_chain_block.challenge_chain_sp_vdf.output.get_hash() + # extract real quality data from blocks using chia's proof of space verification + pos = block.reward_chain_block.proof_of_space + # calculate real quality string from proof of space data + quality_string: Optional[bytes32] = verify_and_get_quality_string( + block.reward_chain_block.proof_of_space, + bt.constants, + challenge, + challenge_chain_sp, + height=block.reward_chain_block.height, + ) + + assert quality_string is not None + quality_hex = quality_string.hex() + + # test solver with real blockchain quality + plot_size = pos.size() + k_size = plot_size.size_v1 if plot_size.size_v1 is not None else plot_size.size_v2 + assert k_size is not None + solver_temp_dir = bt.root_path / "solver_farmer_test" + solver_temp_dir.mkdir(exist_ok=True) + async with setup_solver(solver_temp_dir, bt.constants) as solver_service: + assert solver_service.rpc_server is not None + solver_rpc_client = await SolverRpcClient.create( + self_hostname, solver_service.rpc_server.listen_port, solver_service.root_path, solver_service.config + ) + solve_response = await solver_rpc_client.solve(quality_hex, int(k_size), 1000) + assert solve_response["success"] is True + assert "proof" in solve_response + # stub implementation returns None, real implementation would return actual proof + assert solve_response["proof"] is None + + +@pytest.mark.anyio +async def test_solver_error_handling_and_edge_cases(bt: BlockTools, self_hostname: str) -> None: + """Test solver error handling with invalid requests and edge cases.""" + solver_temp_dir = bt.root_path / "solver_farmer_test" + solver_temp_dir.mkdir(exist_ok=True) + async with setup_solver(solver_temp_dir, bt.constants) as solver_service: + assert solver_service.rpc_server is not None + solver_rpc_client = await SolverRpcClient.create( + self_hostname, solver_service.rpc_server.listen_port, solver_service.root_path, solver_service.config + ) + + # test invalid quality string format + try: + await solver_rpc_client.solve("invalid_hex") + assert False, "should have raised exception for invalid hex" + except Exception: + pass # expected + + # test edge case parameters + valid_quality = "1234567890abcdef" * 4 # valid 32-byte hex + + # test with edge case plot sizes and difficulties + edge_cases = [ + (18, 1), # minimum plot size, minimum difficulty + (50, 999999), # large plot size, high difficulty + ] + + for plot_size, difficulty in edge_cases: + response = await solver_rpc_client.solve(valid_quality, plot_size, difficulty) + assert response["success"] is True + assert "proof" in response + + # test solver handles exception in solve method + solver = solver_service._node + test_info = SolverInfo(plot_size=uint8(32), plot_diffculty=uint64(1000), quality_string=bytes32.zeros) + + with patch.object(solver, "solve", side_effect=RuntimeError("test error")): + # solver api should handle exceptions gracefully + result = await solver_service._api.solve(test_info) + assert result is None # api returns None on error diff --git a/chia/_tests/util/setup_nodes.py b/chia/_tests/util/setup_nodes.py index c79ef05aac6b..1a206d1fe08e 100644 --- a/chia/_tests/util/setup_nodes.py +++ b/chia/_tests/util/setup_nodes.py @@ -475,10 +475,9 @@ async def setup_full_system_inner( await asyncio.sleep(backoff) setup_solver( - b_tools, shared_b_tools.root_path / "harvester", - UnresolvedPeerInfo(self_hostname, farmer_service._server.get_port()), consensus_constants, + True, ) # solver_service = await async_exit_stack.enter_async_context( diff --git a/chia/cmds/chia.py b/chia/cmds/chia.py index 988d95f78a8d..0420676e2672 100644 --- a/chia/cmds/chia.py +++ b/chia/cmds/chia.py @@ -24,6 +24,7 @@ from chia.cmds.plotters import plotters_cmd from chia.cmds.rpc import rpc_cmd from chia.cmds.show import show_cmd +from chia.cmds.solver import solver_cmd from chia.cmds.start import start_cmd from chia.cmds.stop import stop_cmd from chia.cmds.wallet import wallet_cmd @@ -127,6 +128,7 @@ def run_daemon_cmd(ctx: click.Context, wait_for_unlock: bool) -> None: cli.add_command(init_cmd) cli.add_command(rpc_cmd) cli.add_command(show_cmd) +cli.add_command(solver_cmd) cli.add_command(start_cmd) cli.add_command(stop_cmd) cli.add_command(netspace_cmd) diff --git a/chia/cmds/cmds_util.py b/chia/cmds/cmds_util.py index 1362a3b83529..0ba57ea62717 100644 --- a/chia/cmds/cmds_util.py +++ b/chia/cmds/cmds_util.py @@ -23,6 +23,7 @@ from chia.harvester.harvester_rpc_client import HarvesterRpcClient from chia.rpc.rpc_client import ResponseFailureError, RpcClient from chia.simulator.simulator_full_node_rpc_client import SimulatorFullNodeRpcClient +from chia.solver.solver_rpc_client import SolverRpcClient from chia.types.mempool_submission_status import MempoolSubmissionStatus from chia.util.config import load_config from chia.util.errors import CliRpcConnectionError, InvalidPathError @@ -42,6 +43,7 @@ "harvester": HarvesterRpcClient, "data_layer": DataLayerRpcClient, "simulator": SimulatorFullNodeRpcClient, + "solver": SolverRpcClient, } node_config_section_names: dict[type[RpcClient], str] = { @@ -52,6 +54,7 @@ HarvesterRpcClient: "harvester", DataLayerRpcClient: "data_layer", SimulatorFullNodeRpcClient: "full_node", + SolverRpcClient: "solver", } _T_RpcClient = TypeVar("_T_RpcClient", bound=RpcClient) diff --git a/chia/cmds/rpc.py b/chia/cmds/rpc.py index d04cdb41e346..cee4cd2533bb 100644 --- a/chia/cmds/rpc.py +++ b/chia/cmds/rpc.py @@ -13,7 +13,7 @@ from chia.cmds.cmd_classes import ChiaCliContext from chia.util.config import load_config -services: list[str] = ["crawler", "daemon", "farmer", "full_node", "harvester", "timelord", "wallet", "data_layer"] +services: list[str] = ["crawler", "daemon", "farmer", "full_node", "harvester", "timelord", "wallet", "data_layer", "solver"] async def call_endpoint( diff --git a/chia/cmds/solver.py b/chia/cmds/solver.py new file mode 100644 index 000000000000..908aff2be216 --- /dev/null +++ b/chia/cmds/solver.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +from typing import Optional + +import click + +from chia.cmds.cmd_classes import ChiaCliContext + + +@click.group("solver", help="Manage your solver") +def solver_cmd() -> None: + pass + + +@solver_cmd.command("get_state", help="Get current solver state") +@click.option( + "-sp", + "--solver-rpc-port", + help="Set the port where the Solver is hosting the RPC interface. See the rpc_port under solver in config.yaml", + type=int, + default=None, + show_default=True, +) +@click.pass_context +def get_state_cmd( + ctx: click.Context, + solver_rpc_port: Optional[int], +) -> None: + import asyncio + + from chia.cmds.solver_funcs import get_state + + asyncio.run(get_state(ChiaCliContext.set_default(ctx), solver_rpc_port)) + + +@solver_cmd.command("solve", help="Solve a quality string") +@click.option( + "-sp", + "--solver-rpc-port", + help="Set the port where the Solver is hosting the RPC interface. See the rpc_port under solver in config.yaml", + type=int, + default=None, + show_default=True, +) +@click.option( + "-q", + "--quality", + help="Quality string to solve (hex format)", + type=str, + required=True, +) +@click.option( + "-k", + "--plot-size", + help="Plot size (k value, default: 32)", + type=int, + default=32, + show_default=True, +) +@click.option( + "-d", + "--difficulty", + help="Plot difficulty (default: 1000)", + type=int, + default=1000, + show_default=True, +) +@click.pass_context +def solve_cmd( + ctx: click.Context, + solver_rpc_port: Optional[int], + quality: str, + plot_size: int, + difficulty: int, +) -> None: + import asyncio + + from chia.cmds.solver_funcs import solve_quality + + asyncio.run(solve_quality(ChiaCliContext.set_default(ctx), solver_rpc_port, quality, plot_size, difficulty)) \ No newline at end of file diff --git a/chia/cmds/solver_funcs.py b/chia/cmds/solver_funcs.py new file mode 100644 index 000000000000..423437864554 --- /dev/null +++ b/chia/cmds/solver_funcs.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +import json +from typing import Optional + +from chia_rs.sized_bytes import bytes32 + +from chia.cmds.cmd_classes import ChiaCliContext +from chia.cmds.cmds_util import get_any_service_client +from chia.solver.solver_rpc_client import SolverRpcClient + + +async def get_state( + ctx: ChiaCliContext, + solver_rpc_port: Optional[int] = None, +) -> None: + """Get solver state via RPC.""" + try: + async with get_any_service_client(SolverRpcClient, ctx.root_path, solver_rpc_port) as (client, _): + response = await client.get_state() + print(json.dumps(response, indent=2)) + except Exception as e: + print(f"Failed to get solver state: {e}") + + +async def solve_quality( + ctx: ChiaCliContext, + solver_rpc_port: Optional[int] = None, + quality_hex: str = "", + plot_size: int = 32, + difficulty: int = 1000, +) -> None: + """Solve a quality string via RPC.""" + try: + # validate quality string using standard chia pattern + quality_bytes32 = bytes32.from_hexstr(quality_hex) + + async with get_any_service_client(SolverRpcClient, ctx.root_path, solver_rpc_port) as (client, _): + response = await client.solve(quality_hex, plot_size, difficulty) + print(json.dumps(response, indent=2)) + except Exception as e: + print(f"Failed to solve quality: {e}") diff --git a/chia/plotting/prover.py b/chia/plotting/prover.py index cd9474b0d9cd..c88c0676bcda 100644 --- a/chia/plotting/prover.py +++ b/chia/plotting/prover.py @@ -74,7 +74,7 @@ def get_qualities_for_challenge(self, challenge: bytes) -> list[bytes32]: def get_full_proof(self, challenge: bytes, index: int, parallel_read: bool = True) -> bytes: # TODO: todo_v2_plots Implement plot proof generation - raise NotImplementedError("V2 plot format is not yet implemented") + raise NotImplementedError("V2 plot format require solver to get full proof") @classmethod def from_bytes(cls, data: bytes) -> V2Prover: diff --git a/chia/protocols/protocol_message_types.py b/chia/protocols/protocol_message_types.py index 5efdcc93bf32..b5971da8a8a5 100644 --- a/chia/protocols/protocol_message_types.py +++ b/chia/protocols/protocol_message_types.py @@ -138,7 +138,7 @@ class ProtocolMessageTypes(Enum): respond_cost_info = 107 # new farmer protocol messages - solution_resonse = 108 + solution_response = 108 # solver protocol solve = 109 diff --git a/chia/simulator/setup_services.py b/chia/simulator/setup_services.py index 3f9134aec1bd..ac21ae343a0a 100644 --- a/chia/simulator/setup_services.py +++ b/chia/simulator/setup_services.py @@ -37,6 +37,8 @@ from chia.simulator.keyring import TempKeyring from chia.simulator.ssl_certs import get_next_nodes_certs_and_keys, get_next_private_ca_cert_and_key from chia.simulator.start_simulator import SimulatorFullNodeService, create_full_node_simulator_service +from chia.solver.solver_service import SolverService +from chia.solver.start_solver import create_solver_service from chia.ssl.create_ssl import create_all_ssl from chia.timelord.start_timelord import create_timelord_service from chia.timelord.timelord_launcher import VDFClientProcessMgr, find_vdf_client, spawn_process diff --git a/chia/solver/solver.py b/chia/solver/solver.py index cc8df2f51a87..f197a69f5b80 100644 --- a/chia/solver/solver.py +++ b/chia/solver/solver.py @@ -46,24 +46,34 @@ def __init__(self, root_path: Path, config: dict[str, Any], constants: Consensus self.log = log self.root_path = root_path self._shut_down = False + num_threads = config["num_threads"] + self.log.info(f"Initializing solver with {num_threads} threads") self.executor = concurrent.futures.ThreadPoolExecutor( - max_workers=config["num_threads"], thread_name_prefix="solver-" + max_workers=num_threads, thread_name_prefix="solver-" ) self._server = None self.constants = constants self.state_changed_callback: Optional[StateChangedProtocol] = None + self.log.info("Solver initialization complete") @contextlib.asynccontextmanager async def manage(self) -> AsyncIterator[None]: try: + self.log.info("Starting solver service") self.started = True + self.log.info("Solver service started successfully") yield finally: + self.log.info("Shutting down solver service") self._shut_down = True + self.executor.shutdown(wait=True) + self.log.info("Solver service shutdown complete") def solve(self, info: SolverInfo) -> Optional[bytes]: - self.log.debug(f"Solve called with SolverInfo: {info}") - return None + self.log.debug(f"Solve request: plot_size={info.plot_size}, difficulty={info.plot_diffculty}, quality={info.quality_string.hex()}") + # stub implementation - always returns None + self.log.debug("Solve completed (stub implementation)") + return None #todo implement actualy calling the solver def get_connections(self, request_node_type: Optional[NodeType]) -> list[dict[str, Any]]: return default_get_connections(server=self.server, request_node_type=request_node_type) diff --git a/chia/solver/solver_api.py b/chia/solver/solver_api.py index 6cc343e190be..4a85046f465a 100644 --- a/chia/solver/solver_api.py +++ b/chia/solver/solver_api.py @@ -28,19 +28,37 @@ def __init__(self, solver: Solver) -> None: def ready(self) -> bool: return self.solver.started - @metadata.request() + @metadata.request(peer_required=False, reply_types=[ProtocolMessageTypes.solution_response]) async def solve( self, request: SolverInfo, ) -> Optional[Message]: + """ + Solve a V2 plot quality to get the full proof of space. + This is called by the farmer when it receives V2 qualities from harvester. + """ if not self.solver.started: - raise RuntimeError("Solver is not started") - - proof = self.solver.solve(request) - if proof is None: + self.log.error("Solver is not started") return None - response: SolutionResponse = SolutionResponse( - proof=proof, + self.log.debug( + f"Solving quality {request.quality_string.hex()[:10]}... " + f"for plot size {request.plot_size} with difficulty {request.plot_diffculty}" ) - return make_msg(ProtocolMessageTypes.solution_resonse, response) + + try: + proof = self.solver.solve(request) + if proof is None: + self.log.warning(f"Solver returned no proof for quality {request.quality_string.hex()[:10]}...") + return None + + response: SolutionResponse = SolutionResponse( + proof=proof, + ) + + self.log.debug(f"Successfully solved quality, returning {len(proof)} byte proof") + return make_msg(ProtocolMessageTypes.solution_response, response) + + except Exception as e: + self.log.error(f"Error solving quality {request.quality_string.hex()[:10]}...: {e}") + return None diff --git a/chia/solver/solver_rpc_api.py b/chia/solver/solver_rpc_api.py new file mode 100644 index 000000000000..f6521641f92a --- /dev/null +++ b/chia/solver/solver_rpc_api.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, ClassVar, Optional, cast + +from chia_rs.sized_bytes import bytes32 +from chia_rs.sized_ints import uint8, uint64 + +from chia.protocols.solver_protocol import SolverInfo +from chia.rpc.rpc_server import Endpoint, EndpointResult +from chia.solver.solver import Solver +from chia.util.ws_message import WsRpcMessage + + +class SolverRpcApi: + if TYPE_CHECKING: + from chia.rpc.rpc_server import RpcApiProtocol + + _protocol_check: ClassVar[RpcApiProtocol] = cast("SolverRpcApi", None) + + def __init__(self, solver: Solver): + self.service = solver + self.service_name = "chia_solver" + + def get_routes(self) -> dict[str, Endpoint]: + return { + "/solve": self.solve, + "/get_state": self.get_state, + } + + async def _state_changed(self, change: str, change_data: Optional[dict[str, Any]] = None) -> list[WsRpcMessage]: + return [] + + async def solve(self, request: dict[str, Any]) -> EndpointResult: + # extract all required fields from request + quality_string = request["quality_string"] + plot_size = request.get("plot_size", 32) # todo default ? + plot_difficulty = request.get("plot_difficulty", 1000) # todo default ? + + # create complete SolverInfo object with all provided data + solver_info = SolverInfo( + plot_size=uint8(plot_size), + plot_diffculty=uint64(plot_difficulty), + quality_string=bytes32.from_hexstr(quality_string), + ) + + proof = self.service.solve(solver_info) + return {"proof": proof.hex() if proof else None} + + async def get_state(self, _: dict[str, Any]) -> EndpointResult: + return { + "started": self.service.started, + } diff --git a/chia/solver/solver_rpc_client.py b/chia/solver/solver_rpc_client.py new file mode 100644 index 000000000000..55abbee74c42 --- /dev/null +++ b/chia/solver/solver_rpc_client.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from typing import Any + +from chia_rs.sized_bytes import bytes32 + +from chia.rpc.rpc_client import RpcClient + + +class SolverRpcClient(RpcClient): + """ + Client to Chia RPC, connects to a local solver. Uses HTTP/JSON, and converts back from + JSON into native python objects before returning. All api calls use POST requests. + """ + + async def get_state(self) -> dict[str, Any]: + """Get solver state.""" + return await self.fetch("get_state", {}) + + async def solve(self, quality_string: str, plot_size: int = 32, plot_difficulty: int = 1000) -> dict[str, Any]: + """Solve a quality string with optional plot size and difficulty.""" + quality = bytes32.from_hexstr(quality_string) + return await self.fetch( + "solve", {"quality_string": quality.hex(), "plot_size": plot_size, "plot_difficulty": plot_difficulty} + ) diff --git a/chia/solver/solver_service.py b/chia/solver/solver_service.py new file mode 100644 index 000000000000..aa3fdf67f71d --- /dev/null +++ b/chia/solver/solver_service.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from chia.server.start_service import Service +from chia.solver.solver import Solver +from chia.solver.solver_api import SolverAPI +from chia.solver.solver_rpc_api import SolverRpcApi + +SolverService = Service[Solver, SolverAPI, SolverRpcApi] diff --git a/chia/server/start_solver.py b/chia/solver/start_solver.py similarity index 96% rename from chia/server/start_solver.py rename to chia/solver/start_solver.py index b0ae49f58303..babd5de484ed 100644 --- a/chia/server/start_solver.py +++ b/chia/solver/start_solver.py @@ -12,13 +12,13 @@ from chia.apis import ApiProtocolRegistry from chia.consensus.constants import replace_str_to_bytes from chia.consensus.default_constants import DEFAULT_CONSTANTS, update_testnet_overrides -from chia.full_node.full_node_rpc_api import FullNodeRpcApi from chia.protocols.outbound_message import NodeType -from chia.server.aliases import SolverService from chia.server.signal_handlers import SignalHandlers from chia.server.start_service import Service, async_run from chia.solver.solver import Solver from chia.solver.solver_api import SolverAPI +from chia.solver.solver_rpc_api import SolverRpcApi +from chia.solver.solver_service import SolverService from chia.util.chia_logging import initialize_service_logging from chia.util.config import load_config, load_config_cli from chia.util.default_root import resolve_root_path @@ -48,8 +48,9 @@ def create_solver_service( peer_api = SolverAPI(node) network_id = service_config["selected_network"] + rpc_info = None if service_config.get("start_rpc_server", True): - rpc_info = (FullNodeRpcApi, service_config["rpc_port"]) + rpc_info = (SolverRpcApi, service_config["rpc_port"]) return Service( root_path=root_path, diff --git a/chia/types/blockchain_format/proof_of_space.py b/chia/types/blockchain_format/proof_of_space.py index 3befb30fc6d9..e297d76f7ffc 100644 --- a/chia/types/blockchain_format/proof_of_space.py +++ b/chia/types/blockchain_format/proof_of_space.py @@ -103,7 +103,7 @@ def verify_and_get_quality_string( def passes_plot_filter( - prefix_bits: int, + prefix_bits: uint8, plot_id: bytes32, challenge_hash: bytes32, signage_point: bytes32, @@ -119,7 +119,7 @@ def passes_plot_filter( return cast(bool, plot_filter[:prefix_bits].uint == 0) -def calculate_prefix_bits(constants: ConsensusConstants, height: uint32, plot_size: PlotSize) -> int: +def calculate_prefix_bits(constants: ConsensusConstants, height: uint32, plot_size: PlotSize) -> uint8: # v2 plots have a constant plot filter size if plot_size.size_v2 is not None: return constants.NUMBER_ZERO_BITS_PLOT_FILTER_V2 @@ -134,7 +134,7 @@ def calculate_prefix_bits(constants: ConsensusConstants, height: uint32, plot_si elif height >= constants.HARD_FORK_HEIGHT: prefix_bits -= 1 - return max(0, prefix_bits) + return uint8(max(0, prefix_bits)) def calculate_plot_difficulty(constants: ConsensusConstants, height: uint32) -> uint8: diff --git a/chia/util/initial-config.yaml b/chia/util/initial-config.yaml index b69ea09a2066..13bd21a98c7d 100644 --- a/chia/util/initial-config.yaml +++ b/chia/util/initial-config.yaml @@ -633,12 +633,29 @@ solver: # The solver server will run on this port port: 8666 + # Enable or disable UPnP port forwarding + enable_upnp: False + + # Logging configuration + logging: *logging + + # Network overrides and selected network network_overrides: *network_overrides selected_network: *selected_network + # Number of threads for solver operations + num_threads: 4 + + # RPC server configuration + rpc_port: 8667 + start_rpc_server: True + + # SSL configuration ssl: private_crt: "config/ssl/solver/private_solver.crt" private_key: "config/ssl/solver/private_solver.key" + public_crt: "config/ssl/solver/public_solver.crt" + public_key: "config/ssl/solver/public_solver.key" data_layer: # TODO: consider name diff --git a/chia/util/service_groups.py b/chia/util/service_groups.py index b5c05ddce360..2c73906e069d 100644 --- a/chia/util/service_groups.py +++ b/chia/util/service_groups.py @@ -12,6 +12,7 @@ "chia_wallet", "chia_data_layer", "chia_data_layer_http", + "chia_solver", ], "daemon": [], # TODO: should this be `data_layer`? @@ -31,6 +32,7 @@ "crawler": ["chia_crawler"], "seeder": ["chia_crawler", "chia_seeder"], "seeder-only": ["chia_seeder"], + "solver": ["chia_solver"], } diff --git a/pyproject.toml b/pyproject.toml index 753a000a1be1..e8c89790c14a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ chia_full_node_simulator = "chia.simulator.start_simulator:main" chia_data_layer = "chia.data_layer.start_data_layer:main" chia_data_layer_http = "chia.data_layer.data_layer_server:main" chia_data_layer_s3_plugin = "chia.data_layer.s3_plugin_service:run_server" +chia_solver = "chia.solver.start_solver:main" [[tool.poetry.source]] name = "chia" From 87ffc5e98f8fc07d01debddc88e8d7bf62bf1a01 Mon Sep 17 00:00:00 2001 From: almogdepaz Date: Tue, 12 Aug 2025 09:18:40 +0300 Subject: [PATCH 17/42] clenup, add config to tests --- chia/_tests/conftest.py | 8 --- .../core/custom_types/test_proof_of_space.py | 2 +- chia/_tests/harvester/config.py | 4 ++ chia/_tests/harvester/test_harvester_api.py | 56 ++++++++++--------- chia/_tests/solver/config.py | 4 ++ chia/_tests/solver/test_solver_service.py | 50 ++++++----------- chia/_tests/util/setup_nodes.py | 16 +++--- .../_tests/util/test_network_protocol_test.py | 9 +++ chia/cmds/rpc.py | 12 +++- chia/cmds/solver.py | 8 +-- chia/cmds/solver_funcs.py | 9 +-- chia/solver/solver.py | 9 ++- .../types/blockchain_format/proof_of_space.py | 6 +- 13 files changed, 99 insertions(+), 94 deletions(-) create mode 100644 chia/_tests/harvester/config.py create mode 100644 chia/_tests/solver/config.py diff --git a/chia/_tests/conftest.py b/chia/_tests/conftest.py index 53fdf7312abf..51266ab9db1c 100644 --- a/chia/_tests/conftest.py +++ b/chia/_tests/conftest.py @@ -65,12 +65,10 @@ setup_full_node, setup_introducer, setup_seeder, - setup_solver, setup_timelord, ) from chia.simulator.start_simulator import SimulatorFullNodeService from chia.simulator.wallet_tools import WalletTool -from chia.solver.solver_service import SolverService from chia.timelord.timelord_service import TimelordService from chia.types.peer_info import PeerInfo from chia.util.config import create_default_chia_config, lock_and_load_config @@ -1114,12 +1112,6 @@ async def seeder_service(root_path_populated_with_config: Path, database_uri: st yield seeder -@pytest.fixture(scope="function") -async def solver_service(bt: BlockTools) -> AsyncIterator[SolverService]: - async with setup_solver(bt.root_path, bt.constants) as _: - yield _ - - @pytest.fixture(scope="function") def tmp_chia_root(tmp_path): """ diff --git a/chia/_tests/core/custom_types/test_proof_of_space.py b/chia/_tests/core/custom_types/test_proof_of_space.py index 8dd72e195254..ed8520f36051 100644 --- a/chia/_tests/core/custom_types/test_proof_of_space.py +++ b/chia/_tests/core/custom_types/test_proof_of_space.py @@ -223,7 +223,7 @@ def test_calculate_plot_difficulty(height: uint32, difficulty: uint8) -> None: class TestProofOfSpace: @pytest.mark.parametrize("prefix_bits", [DEFAULT_CONSTANTS.NUMBER_ZERO_BITS_PLOT_FILTER_V1, 8, 7, 6, 5, 1, 0]) - def test_can_create_proof(self, prefix_bits: uint8, seeded_random: random.Random) -> None: + def test_can_create_proof(self, prefix_bits: int, seeded_random: random.Random) -> None: """ Tests that the change of getting a correct proof is exactly 1/target_filter. """ diff --git a/chia/_tests/harvester/config.py b/chia/_tests/harvester/config.py new file mode 100644 index 000000000000..b593bfe59ade --- /dev/null +++ b/chia/_tests/harvester/config.py @@ -0,0 +1,4 @@ +from __future__ import annotations + +job_timeout = 70 +checkout_blocks_and_plots = True diff --git a/chia/_tests/harvester/test_harvester_api.py b/chia/_tests/harvester/test_harvester_api.py index 043337c9cf29..82d244bf991b 100644 --- a/chia/_tests/harvester/test_harvester_api.py +++ b/chia/_tests/harvester/test_harvester_api.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock, patch import pytest -from chia_rs import ProofOfSpace +from chia_rs import ConsensusConstants, FullBlock, ProofOfSpace from chia_rs.sized_bytes import bytes32 from chia_rs.sized_ints import uint64 @@ -14,18 +14,12 @@ from chia.protocols import harvester_protocol from chia.protocols.harvester_protocol import PoolDifficulty from chia.server.ws_connection import WSChiaConnection -from chia.simulator.block_tools import BlockTools -def signage_point_from_block(bt: BlockTools) -> harvester_protocol.NewSignagePointHarvester: - """Create a real NewSignagePointHarvester from actual blockchain blocks.""" - # generate real blocks using BlockTools - blocks = bt.get_consecutive_blocks( - num_blocks=3, - guarantee_transaction_block=True, - farmer_reward_puzzle_hash=bt.farmer_ph, - ) - block = blocks[-1] # always use the last block +def signage_point_from_block( + block: FullBlock, constants: ConsensusConstants +) -> harvester_protocol.NewSignagePointHarvester: + """Create a real NewSignagePointHarvester from a blockchain block.""" # extract real signage point data from the block sp_index = block.reward_chain_block.signage_point_index challenge_hash = block.reward_chain_block.pos_ss_cc_challenge_hash @@ -37,8 +31,8 @@ def signage_point_from_block(bt: BlockTools) -> harvester_protocol.NewSignagePoi return harvester_protocol.NewSignagePointHarvester( challenge_hash=challenge_hash, - difficulty=uint64(bt.constants.DIFFICULTY_STARTING), - sub_slot_iters=uint64(bt.constants.SUB_SLOT_ITERS_STARTING), + difficulty=uint64(constants.DIFFICULTY_STARTING), + sub_slot_iters=uint64(constants.SUB_SLOT_ITERS_STARTING), signage_point_index=sp_index, sp_hash=sp_hash, pool_difficulties=[], @@ -63,21 +57,25 @@ def create_plot_info() -> PlotInfo: @pytest.mark.anyio -async def test_new_signage_point_harvester(harvester_farmer_environment: HarvesterFarmerEnvironment) -> None: +async def test_new_signage_point_harvester( + harvester_farmer_environment: HarvesterFarmerEnvironment, + default_400_blocks: list[FullBlock], + blockchain_constants: ConsensusConstants, +) -> None: """Test successful signage point processing with real blockchain data.""" - _, _, harvester_service, _, bt = harvester_farmer_environment + _, _, harvester_service, _, _ = harvester_farmer_environment harvester_api = harvester_service._server.api assert isinstance(harvester_api, HarvesterAPI) # use real signage point data from actual block - new_challenge = signage_point_from_block(bt) + block = default_400_blocks[2] # use a transaction block + new_challenge = signage_point_from_block(block, blockchain_constants) # harvester doesn't accept incoming connections, so use mock peer like other tests mock_peer = MagicMock(spec=WSChiaConnection) # create realistic plot info for testing mock_plot_info = create_plot_info() - plot_path = Path("/fake/plot.plot") with patch.object(harvester_api.harvester.plot_manager, "public_keys_available", return_value=True): - with patch.object(harvester_api.harvester.plot_manager, "plots", {plot_path: mock_plot_info}): + with patch.object(harvester_api.harvester.plot_manager, "plots", {"tmp_path": mock_plot_info}): # let passes_plot_filter, calculate_pos_challenge, and calculate_sp_interval_iters use real implementations with patch("chia.harvester.harvester_api.calculate_iterations_quality", return_value=uint64(1000)): with patch.object(mock_plot_info.prover, "get_full_proof") as mock_get_proof: @@ -89,9 +87,12 @@ async def test_new_signage_point_harvester(harvester_farmer_environment: Harvest @pytest.mark.anyio async def test_new_signage_point_harvester_pool_difficulty( harvester_farmer_environment: HarvesterFarmerEnvironment, + default_400_blocks: list[FullBlock], + tmp_path: Path, + blockchain_constants: ConsensusConstants, ) -> None: """Test pool difficulty overrides with real blockchain signage points.""" - _, _, harvester_service, _, bt = harvester_farmer_environment + _, _, harvester_service, _, _ = harvester_farmer_environment harvester_api = harvester_service._server.api assert isinstance(harvester_api, HarvesterAPI) @@ -102,7 +103,6 @@ async def test_new_signage_point_harvester_pool_difficulty( # create realistic plot info for testing mock_plot_info = create_plot_info() mock_plot_info.pool_contract_puzzle_hash = pool_puzzle_hash - plot_path = Path("/fake/pool_plot.plot") pool_difficulty = PoolDifficulty( pool_contract_puzzle_hash=pool_puzzle_hash, difficulty=uint64(500), # lower than main difficulty @@ -110,7 +110,8 @@ async def test_new_signage_point_harvester_pool_difficulty( ) # create signage point from real block with pool difficulty - new_challenge = signage_point_from_block(bt) + block = default_400_blocks[2] # use a transaction block + new_challenge = signage_point_from_block(block, blockchain_constants) new_challenge = harvester_protocol.NewSignagePointHarvester( challenge_hash=new_challenge.challenge_hash, difficulty=new_challenge.difficulty, @@ -123,7 +124,7 @@ async def test_new_signage_point_harvester_pool_difficulty( ) with patch.object(harvester_api.harvester.plot_manager, "public_keys_available", return_value=True): - with patch.object(harvester_api.harvester.plot_manager, "plots", {plot_path: mock_plot_info}): + with patch.object(harvester_api.harvester.plot_manager, "plots", {tmp_path: mock_plot_info}): # mock passes_plot_filter to return True so we can test pool difficulty logic with patch("chia.harvester.harvester_api.passes_plot_filter", return_value=True): with patch("chia.harvester.harvester_api.calculate_iterations_quality") as mock_calc_iter: @@ -141,23 +142,26 @@ async def test_new_signage_point_harvester_pool_difficulty( @pytest.mark.anyio async def test_new_signage_point_harvester_prover_error( harvester_farmer_environment: HarvesterFarmerEnvironment, + default_400_blocks: list[FullBlock], + tmp_path: Path, + blockchain_constants: ConsensusConstants, ) -> None: """Test error handling when prover fails using real blockchain data.""" - _, _, harvester_service, _, bt = harvester_farmer_environment + _, _, harvester_service, _, _ = harvester_farmer_environment harvester_api = harvester_service._server.api assert isinstance(harvester_api, HarvesterAPI) # create signage point from real block - new_challenge = signage_point_from_block(bt) + block = default_400_blocks[2] # use a transaction block + new_challenge = signage_point_from_block(block, blockchain_constants) mock_peer = MagicMock(spec=WSChiaConnection) # create realistic plot info for testing mock_plot_info = create_plot_info() - plot_path = Path("/fake/plot.plot") with patch.object(harvester_api.harvester.plot_manager, "public_keys_available", return_value=True): - with patch.object(harvester_api.harvester.plot_manager, "plots", {plot_path: mock_plot_info}): + with patch.object(harvester_api.harvester.plot_manager, "plots", {tmp_path: mock_plot_info}): # let passes_plot_filter and calculate_pos_challenge use real implementations # make the prover fail during quality check with patch.object( diff --git a/chia/_tests/solver/config.py b/chia/_tests/solver/config.py new file mode 100644 index 000000000000..b593bfe59ade --- /dev/null +++ b/chia/_tests/solver/config.py @@ -0,0 +1,4 @@ +from __future__ import annotations + +job_timeout = 70 +checkout_blocks_and_plots = True diff --git a/chia/_tests/solver/test_solver_service.py b/chia/_tests/solver/test_solver_service.py index abff118072b9..f1d126749706 100644 --- a/chia/_tests/solver/test_solver_service.py +++ b/chia/_tests/solver/test_solver_service.py @@ -1,9 +1,11 @@ from __future__ import annotations +from pathlib import Path from typing import Optional from unittest.mock import patch import pytest +from chia_rs import ConsensusConstants, FullBlock from chia_rs.sized_bytes import bytes32 from chia_rs.sized_ints import uint8, uint64 @@ -12,18 +14,15 @@ from chia.consensus.get_block_challenge import get_block_challenge from chia.consensus.pot_iterations import is_overflow_block from chia.protocols.solver_protocol import SolverInfo -from chia.simulator.block_tools import BlockTools from chia.simulator.setup_services import setup_solver from chia.solver.solver_rpc_client import SolverRpcClient from chia.types.blockchain_format.proof_of_space import verify_and_get_quality_string @pytest.mark.anyio -async def test_solver_api_methods(bt: BlockTools) -> None: +async def test_solver_api_methods(blockchain_constants: ConsensusConstants, tmp_path: Path) -> None: """Test solver api protocol methods with real requests.""" - solver_temp_dir = bt.root_path / "solver_farmer_test" - solver_temp_dir.mkdir(exist_ok=True) - async with setup_solver(solver_temp_dir, bt.constants) as solver_service: + async with setup_solver(tmp_path, blockchain_constants) as solver_service: solver = solver_service._node solver_api = solver_service._api @@ -55,32 +54,21 @@ async def test_solver_api_methods(bt: BlockTools) -> None: solver.started = original_started -@pytest.mark.anyio -async def test_non_overflow_genesis(empty_blockchain: Blockchain, bt: BlockTools) -> None: - blocks = bt.get_consecutive_blocks( - num_blocks=3, - guarantee_transaction_block=True, - farmer_reward_puzzle_hash=bt.farmer_ph, - ) - for block in blocks: - await _validate_and_add_block(empty_blockchain, block) - - @pytest.mark.anyio async def test_solver_with_real_blocks_and_signage_points( - bt: BlockTools, empty_blockchain: Blockchain, self_hostname: str + blockchain_constants: ConsensusConstants, + default_400_blocks: list[FullBlock], + empty_blockchain: Blockchain, + self_hostname: str, + tmp_path: Path, ) -> None: blockchain = empty_blockchain - blocks = bt.get_consecutive_blocks( - num_blocks=3, - guarantee_transaction_block=True, - farmer_reward_puzzle_hash=bt.farmer_ph, - ) + blocks = default_400_blocks[:3] for block in blocks: await _validate_and_add_block(empty_blockchain, block) block = blocks[-1] # always use the last block - overflow = is_overflow_block(bt.constants, block.reward_chain_block.signage_point_index) - challenge = get_block_challenge(bt.constants, block, blockchain, False, overflow, False) + overflow = is_overflow_block(blockchain_constants, block.reward_chain_block.signage_point_index) + challenge = get_block_challenge(blockchain_constants, block, blockchain, False, overflow, False) assert block.reward_chain_block.pos_ss_cc_challenge_hash == challenge if block.reward_chain_block.challenge_chain_sp_vdf is None: challenge_chain_sp: bytes32 = challenge @@ -91,7 +79,7 @@ async def test_solver_with_real_blocks_and_signage_points( # calculate real quality string from proof of space data quality_string: Optional[bytes32] = verify_and_get_quality_string( block.reward_chain_block.proof_of_space, - bt.constants, + blockchain_constants, challenge, challenge_chain_sp, height=block.reward_chain_block.height, @@ -104,9 +92,7 @@ async def test_solver_with_real_blocks_and_signage_points( plot_size = pos.size() k_size = plot_size.size_v1 if plot_size.size_v1 is not None else plot_size.size_v2 assert k_size is not None - solver_temp_dir = bt.root_path / "solver_farmer_test" - solver_temp_dir.mkdir(exist_ok=True) - async with setup_solver(solver_temp_dir, bt.constants) as solver_service: + async with setup_solver(tmp_path, blockchain_constants) as solver_service: assert solver_service.rpc_server is not None solver_rpc_client = await SolverRpcClient.create( self_hostname, solver_service.rpc_server.listen_port, solver_service.root_path, solver_service.config @@ -119,11 +105,11 @@ async def test_solver_with_real_blocks_and_signage_points( @pytest.mark.anyio -async def test_solver_error_handling_and_edge_cases(bt: BlockTools, self_hostname: str) -> None: +async def test_solver_error_handling_and_edge_cases( + blockchain_constants: ConsensusConstants, self_hostname: str, tmp_path: Path +) -> None: """Test solver error handling with invalid requests and edge cases.""" - solver_temp_dir = bt.root_path / "solver_farmer_test" - solver_temp_dir.mkdir(exist_ok=True) - async with setup_solver(solver_temp_dir, bt.constants) as solver_service: + async with setup_solver(tmp_path, blockchain_constants) as solver_service: assert solver_service.rpc_server is not None solver_rpc_client = await SolverRpcClient.create( self_hostname, solver_service.rpc_server.listen_port, solver_service.root_path, solver_service.config diff --git a/chia/_tests/util/setup_nodes.py b/chia/_tests/util/setup_nodes.py index 1a206d1fe08e..a46d52957d2e 100644 --- a/chia/_tests/util/setup_nodes.py +++ b/chia/_tests/util/setup_nodes.py @@ -43,6 +43,7 @@ ) from chia.simulator.socket import find_available_listen_port from chia.simulator.start_simulator import SimulatorFullNodeService +from chia.solver.solver_service import SolverService from chia.timelord.timelord_service import TimelordService from chia.types.peer_info import UnresolvedPeerInfo from chia.util.hash import std_hash @@ -65,6 +66,7 @@ class FullSystem: introducer: IntroducerAPI timelord: TimelordService timelord_bluebox: TimelordService + solver: SolverService daemon: WebSocketServer @@ -474,14 +476,13 @@ async def setup_full_system_inner( await asyncio.sleep(backoff) - setup_solver( - shared_b_tools.root_path / "harvester", - consensus_constants, - True, + solver_service = await async_exit_stack.enter_async_context( + setup_solver( + shared_b_tools.root_path / "solver", + consensus_constants, + True, + ) ) - # solver_service = await async_exit_stack.enter_async_context( - - # ) full_system = FullSystem( node_1=node_1, @@ -491,6 +492,7 @@ async def setup_full_system_inner( introducer=introducer, timelord=timelord, timelord_bluebox=timelord_bluebox_service, + solver=solver_service, daemon=daemon_ws, ) yield full_system diff --git a/chia/_tests/util/test_network_protocol_test.py b/chia/_tests/util/test_network_protocol_test.py index ca7ecdc38605..66d085eade2c 100644 --- a/chia/_tests/util/test_network_protocol_test.py +++ b/chia/_tests/util/test_network_protocol_test.py @@ -12,6 +12,7 @@ pool_protocol, protocol_message_types, shared_protocol, + solver_protocol, timelord_protocol, wallet_protocol, ) @@ -136,6 +137,7 @@ def test_missing_messages() -> None: "NewSignagePoint", "RequestSignedValues", "SignedValues", + "SolutionResponse", } full_node_msgs = { @@ -188,6 +190,7 @@ def test_missing_messages() -> None: "RequestSignatures", "RespondPlots", "RespondSignatures", + "V2Qualities", } introducer_msgs = {"RequestPeersIntroducer", "RespondPeersIntroducer"} @@ -219,6 +222,8 @@ def test_missing_messages() -> None: "RespondCompactProofOfTime", } + solver_msgs = {"SolverInfo"} + shared_msgs = {"Handshake", "Capability", "Error"} # if these asserts fail, make sure to add the new network protocol messages @@ -252,6 +257,10 @@ def test_missing_messages() -> None: f"message types were added or removed from timelord_protocol. {STANDARD_ADVICE}" ) + assert types_in_module(solver_protocol) == solver_msgs, ( + f"message types were added or removed from shared_protocol. {STANDARD_ADVICE}" + ) + assert types_in_module(shared_protocol) == shared_msgs, ( f"message types were added or removed from shared_protocol. {STANDARD_ADVICE}" ) diff --git a/chia/cmds/rpc.py b/chia/cmds/rpc.py index cee4cd2533bb..0b2f474ab037 100644 --- a/chia/cmds/rpc.py +++ b/chia/cmds/rpc.py @@ -13,7 +13,17 @@ from chia.cmds.cmd_classes import ChiaCliContext from chia.util.config import load_config -services: list[str] = ["crawler", "daemon", "farmer", "full_node", "harvester", "timelord", "wallet", "data_layer", "solver"] +services: list[str] = [ + "crawler", + "daemon", + "farmer", + "full_node", + "harvester", + "timelord", + "wallet", + "data_layer", + "solver", +] async def call_endpoint( diff --git a/chia/cmds/solver.py b/chia/cmds/solver.py index 908aff2be216..a8965ee9f63a 100644 --- a/chia/cmds/solver.py +++ b/chia/cmds/solver.py @@ -27,7 +27,7 @@ def get_state_cmd( solver_rpc_port: Optional[int], ) -> None: import asyncio - + from chia.cmds.solver_funcs import get_state asyncio.run(get_state(ChiaCliContext.set_default(ctx), solver_rpc_port)) @@ -36,7 +36,7 @@ def get_state_cmd( @solver_cmd.command("solve", help="Solve a quality string") @click.option( "-sp", - "--solver-rpc-port", + "--solver-rpc-port", help="Set the port where the Solver is hosting the RPC interface. See the rpc_port under solver in config.yaml", type=int, default=None, @@ -74,7 +74,7 @@ def solve_cmd( difficulty: int, ) -> None: import asyncio - + from chia.cmds.solver_funcs import solve_quality - asyncio.run(solve_quality(ChiaCliContext.set_default(ctx), solver_rpc_port, quality, plot_size, difficulty)) \ No newline at end of file + asyncio.run(solve_quality(ChiaCliContext.set_default(ctx), solver_rpc_port, quality, plot_size, difficulty)) diff --git a/chia/cmds/solver_funcs.py b/chia/cmds/solver_funcs.py index 423437864554..e2e6b7f2e52f 100644 --- a/chia/cmds/solver_funcs.py +++ b/chia/cmds/solver_funcs.py @@ -3,8 +3,6 @@ import json from typing import Optional -from chia_rs.sized_bytes import bytes32 - from chia.cmds.cmd_classes import ChiaCliContext from chia.cmds.cmds_util import get_any_service_client from chia.solver.solver_rpc_client import SolverRpcClient @@ -25,16 +23,13 @@ async def get_state( async def solve_quality( ctx: ChiaCliContext, - solver_rpc_port: Optional[int] = None, - quality_hex: str = "", + solver_rpc_port: Optional[int], + quality_hex: str, plot_size: int = 32, difficulty: int = 1000, ) -> None: """Solve a quality string via RPC.""" try: - # validate quality string using standard chia pattern - quality_bytes32 = bytes32.from_hexstr(quality_hex) - async with get_any_service_client(SolverRpcClient, ctx.root_path, solver_rpc_port) as (client, _): response = await client.solve(quality_hex, plot_size, difficulty) print(json.dumps(response, indent=2)) diff --git a/chia/solver/solver.py b/chia/solver/solver.py index f197a69f5b80..36cd723951ff 100644 --- a/chia/solver/solver.py +++ b/chia/solver/solver.py @@ -48,9 +48,7 @@ def __init__(self, root_path: Path, config: dict[str, Any], constants: Consensus self._shut_down = False num_threads = config["num_threads"] self.log.info(f"Initializing solver with {num_threads} threads") - self.executor = concurrent.futures.ThreadPoolExecutor( - max_workers=num_threads, thread_name_prefix="solver-" - ) + self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=num_threads, thread_name_prefix="solver-") self._server = None self.constants = constants self.state_changed_callback: Optional[StateChangedProtocol] = None @@ -70,10 +68,11 @@ async def manage(self) -> AsyncIterator[None]: self.log.info("Solver service shutdown complete") def solve(self, info: SolverInfo) -> Optional[bytes]: - self.log.debug(f"Solve request: plot_size={info.plot_size}, difficulty={info.plot_diffculty}, quality={info.quality_string.hex()}") + self.log.debug(f"Solve request: quality={info.quality_string.hex()}") # stub implementation - always returns None self.log.debug("Solve completed (stub implementation)") - return None #todo implement actualy calling the solver + # todo implement actualy calling the solver + return None def get_connections(self, request_node_type: Optional[NodeType]) -> list[dict[str, Any]]: return default_get_connections(server=self.server, request_node_type=request_node_type) diff --git a/chia/types/blockchain_format/proof_of_space.py b/chia/types/blockchain_format/proof_of_space.py index e297d76f7ffc..3befb30fc6d9 100644 --- a/chia/types/blockchain_format/proof_of_space.py +++ b/chia/types/blockchain_format/proof_of_space.py @@ -103,7 +103,7 @@ def verify_and_get_quality_string( def passes_plot_filter( - prefix_bits: uint8, + prefix_bits: int, plot_id: bytes32, challenge_hash: bytes32, signage_point: bytes32, @@ -119,7 +119,7 @@ def passes_plot_filter( return cast(bool, plot_filter[:prefix_bits].uint == 0) -def calculate_prefix_bits(constants: ConsensusConstants, height: uint32, plot_size: PlotSize) -> uint8: +def calculate_prefix_bits(constants: ConsensusConstants, height: uint32, plot_size: PlotSize) -> int: # v2 plots have a constant plot filter size if plot_size.size_v2 is not None: return constants.NUMBER_ZERO_BITS_PLOT_FILTER_V2 @@ -134,7 +134,7 @@ def calculate_prefix_bits(constants: ConsensusConstants, height: uint32, plot_si elif height >= constants.HARD_FORK_HEIGHT: prefix_bits -= 1 - return uint8(max(0, prefix_bits)) + return max(0, prefix_bits) def calculate_plot_difficulty(constants: ConsensusConstants, height: uint32) -> uint8: From 322d34864feaa55697ca3e79b756f72b3a82f563 Mon Sep 17 00:00:00 2001 From: almogdepaz Date: Tue, 12 Aug 2025 09:32:27 +0300 Subject: [PATCH 18/42] pre-commit --- chia/_tests/harvester/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 chia/_tests/harvester/__init__.py diff --git a/chia/_tests/harvester/__init__.py b/chia/_tests/harvester/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 From 5748d4b3b73c4b9f1ef5bfc52a45a8e4932b936f Mon Sep 17 00:00:00 2001 From: almogdepaz Date: Tue, 12 Aug 2025 18:28:22 +0300 Subject: [PATCH 19/42] remove mocks from test --- chia/_tests/harvester/test_harvester_api.py | 172 +++++++++----------- chia/_tests/solver/test_solver_service.py | 6 +- 2 files changed, 80 insertions(+), 98 deletions(-) diff --git a/chia/_tests/harvester/test_harvester_api.py b/chia/_tests/harvester/test_harvester_api.py index 82d244bf991b..479adb69e1cb 100644 --- a/chia/_tests/harvester/test_harvester_api.py +++ b/chia/_tests/harvester/test_harvester_api.py @@ -1,5 +1,8 @@ from __future__ import annotations +from collections.abc import AsyncGenerator, Iterator +from contextlib import contextmanager +from dataclasses import dataclass from pathlib import Path from unittest.mock import MagicMock, patch @@ -9,18 +12,46 @@ from chia_rs.sized_ints import uint64 from chia._tests.conftest import HarvesterFarmerEnvironment +from chia._tests.plotting.util import get_test_plots +from chia._tests.util.time_out_assert import time_out_assert from chia.harvester.harvester_api import HarvesterAPI from chia.plotting.util import PlotInfo from chia.protocols import harvester_protocol from chia.protocols.harvester_protocol import PoolDifficulty +from chia.protocols.protocol_message_types import ProtocolMessageTypes from chia.server.ws_connection import WSChiaConnection +@dataclass +class HarvesterTestEnvironment: + """Test environment with real plots loaded for harvester testing.""" + + harvester_api: HarvesterAPI + plot_info: PlotInfo + plot_path: Path + + +@pytest.fixture(scope="function") +async def harvester_environment( + harvester_farmer_environment: HarvesterFarmerEnvironment, +) -> AsyncGenerator[HarvesterTestEnvironment, None]: + """Create a test environment with real plots loaded.""" + _, _, harvester_service, _, _ = harvester_farmer_environment + harvester_api = harvester_service._server.api + assert isinstance(harvester_api, HarvesterAPI) + test_plots = get_test_plots() + assert len(test_plots) > 0, "no test plots available" + plot_manager = harvester_api.harvester.plot_manager + plot_manager.start_refreshing() + await time_out_assert(10, lambda: len(plot_manager.plots) > 0, True) + plot_path, plot_info = next(iter(plot_manager.plots.items())) + yield HarvesterTestEnvironment(harvester_api, plot_info, plot_path) + plot_manager.stop_refreshing() + + def signage_point_from_block( block: FullBlock, constants: ConsensusConstants ) -> harvester_protocol.NewSignagePointHarvester: - """Create a real NewSignagePointHarvester from a blockchain block.""" - # extract real signage point data from the block sp_index = block.reward_chain_block.signage_point_index challenge_hash = block.reward_chain_block.pos_ss_cc_challenge_hash sp_hash = ( @@ -28,7 +59,6 @@ def signage_point_from_block( if block.reward_chain_block.reward_chain_sp_vdf else challenge_hash ) - return harvester_protocol.NewSignagePointHarvester( challenge_hash=challenge_hash, difficulty=uint64(constants.DIFFICULTY_STARTING), @@ -41,77 +71,63 @@ def signage_point_from_block( ) -def create_plot_info() -> PlotInfo: - """Create a realistic PlotInfo mock for testing.""" - mock_prover = MagicMock() - mock_prover.get_id.return_value = bytes32(b"plot_id_123456789012345678901234") # exactly 32 bytes - mock_prover.get_size.return_value = 32 # standard k32 plot - mock_prover.get_qualities_for_challenge.return_value = [ - bytes32(b"quality_123456789012345678901234") - ] # exactly 32 bytes - mock_plot_info = MagicMock(spec=PlotInfo) - mock_plot_info.prover = mock_prover - mock_plot_info.pool_contract_puzzle_hash = None +def create_test_setup( + harvester_environment: HarvesterTestEnvironment, + default_400_blocks: list[FullBlock], + blockchain_constants: ConsensusConstants, +) -> tuple[HarvesterTestEnvironment, harvester_protocol.NewSignagePointHarvester, MagicMock]: + env = harvester_environment + block = default_400_blocks[2] + new_challenge = signage_point_from_block(block, blockchain_constants) + mock_peer = MagicMock(spec=WSChiaConnection) + return env, new_challenge, mock_peer + + +@contextmanager +def mock_successful_proof(plot_info: PlotInfo) -> Iterator[None]: + with patch.object(plot_info.prover, "get_full_proof") as mock_get_proof: + mock_proof = MagicMock(spec=ProofOfSpace) + mock_get_proof.return_value = mock_proof, None + yield - return mock_plot_info + +def assert_farming_info_sent(mock_peer: MagicMock) -> None: + mock_peer.send_message.assert_called() + farming_info_calls = [ + call + for call in mock_peer.send_message.call_args_list + if call[0][0].type == ProtocolMessageTypes.farming_info.value + ] + assert len(farming_info_calls) == 1 @pytest.mark.anyio async def test_new_signage_point_harvester( - harvester_farmer_environment: HarvesterFarmerEnvironment, + harvester_environment: HarvesterTestEnvironment, default_400_blocks: list[FullBlock], blockchain_constants: ConsensusConstants, ) -> None: - """Test successful signage point processing with real blockchain data.""" - _, _, harvester_service, _, _ = harvester_farmer_environment - harvester_api = harvester_service._server.api - assert isinstance(harvester_api, HarvesterAPI) - # use real signage point data from actual block - block = default_400_blocks[2] # use a transaction block - new_challenge = signage_point_from_block(block, blockchain_constants) - # harvester doesn't accept incoming connections, so use mock peer like other tests - mock_peer = MagicMock(spec=WSChiaConnection) - # create realistic plot info for testing - mock_plot_info = create_plot_info() - - with patch.object(harvester_api.harvester.plot_manager, "public_keys_available", return_value=True): - with patch.object(harvester_api.harvester.plot_manager, "plots", {"tmp_path": mock_plot_info}): - # let passes_plot_filter, calculate_pos_challenge, and calculate_sp_interval_iters use real implementations - with patch("chia.harvester.harvester_api.calculate_iterations_quality", return_value=uint64(1000)): - with patch.object(mock_plot_info.prover, "get_full_proof") as mock_get_proof: - mock_proof = MagicMock(spec=ProofOfSpace) - mock_get_proof.return_value = mock_proof, None - await harvester_api.new_signage_point_harvester(new_challenge, mock_peer) + env, new_challenge, mock_peer = create_test_setup(harvester_environment, default_400_blocks, blockchain_constants) + with mock_successful_proof(env.plot_info): + await env.harvester_api.new_signage_point_harvester(new_challenge, mock_peer) + assert_farming_info_sent(mock_peer) @pytest.mark.anyio async def test_new_signage_point_harvester_pool_difficulty( - harvester_farmer_environment: HarvesterFarmerEnvironment, + harvester_environment: HarvesterTestEnvironment, default_400_blocks: list[FullBlock], - tmp_path: Path, blockchain_constants: ConsensusConstants, ) -> None: - """Test pool difficulty overrides with real blockchain signage points.""" - _, _, harvester_service, _, _ = harvester_farmer_environment - harvester_api = harvester_service._server.api - assert isinstance(harvester_api, HarvesterAPI) - - # harvester doesn't accept incoming connections, so use mock peer like other tests - mock_peer = MagicMock(spec=WSChiaConnection) + env, new_challenge, mock_peer = create_test_setup(harvester_environment, default_400_blocks, blockchain_constants) pool_puzzle_hash = bytes32(b"pool" + b"0" * 28) - - # create realistic plot info for testing - mock_plot_info = create_plot_info() - mock_plot_info.pool_contract_puzzle_hash = pool_puzzle_hash + env.plot_info.pool_contract_puzzle_hash = pool_puzzle_hash pool_difficulty = PoolDifficulty( pool_contract_puzzle_hash=pool_puzzle_hash, - difficulty=uint64(500), # lower than main difficulty - sub_slot_iters=uint64(67108864), # different from main + difficulty=uint64(500), + sub_slot_iters=uint64(67108864), ) - # create signage point from real block with pool difficulty - block = default_400_blocks[2] # use a transaction block - new_challenge = signage_point_from_block(block, blockchain_constants) new_challenge = harvester_protocol.NewSignagePointHarvester( challenge_hash=new_challenge.challenge_hash, difficulty=new_challenge.difficulty, @@ -123,49 +139,19 @@ async def test_new_signage_point_harvester_pool_difficulty( last_tx_height=new_challenge.last_tx_height, ) - with patch.object(harvester_api.harvester.plot_manager, "public_keys_available", return_value=True): - with patch.object(harvester_api.harvester.plot_manager, "plots", {tmp_path: mock_plot_info}): - # mock passes_plot_filter to return True so we can test pool difficulty logic - with patch("chia.harvester.harvester_api.passes_plot_filter", return_value=True): - with patch("chia.harvester.harvester_api.calculate_iterations_quality") as mock_calc_iter: - mock_calc_iter.return_value = uint64(1000) - with patch.object(mock_plot_info.prover, "get_full_proof") as mock_get_proof: - mock_proof = MagicMock(spec=ProofOfSpace) - mock_get_proof.return_value = mock_proof, None - await harvester_api.new_signage_point_harvester(new_challenge, mock_peer) - # verify that calculate_iterations_quality was called with pool difficulty - mock_calc_iter.assert_called() - call_args = mock_calc_iter.call_args[0] - assert call_args[3] == uint64(500) # pool difficulty was used + with mock_successful_proof(env.plot_info): + await env.harvester_api.new_signage_point_harvester(new_challenge, mock_peer) + + assert_farming_info_sent(mock_peer) @pytest.mark.anyio async def test_new_signage_point_harvester_prover_error( - harvester_farmer_environment: HarvesterFarmerEnvironment, + harvester_environment: HarvesterTestEnvironment, default_400_blocks: list[FullBlock], - tmp_path: Path, blockchain_constants: ConsensusConstants, ) -> None: - """Test error handling when prover fails using real blockchain data.""" - _, _, harvester_service, _, _ = harvester_farmer_environment - harvester_api = harvester_service._server.api - assert isinstance(harvester_api, HarvesterAPI) - - # create signage point from real block - block = default_400_blocks[2] # use a transaction block - new_challenge = signage_point_from_block(block, blockchain_constants) - - mock_peer = MagicMock(spec=WSChiaConnection) - - # create realistic plot info for testing - mock_plot_info = create_plot_info() - - with patch.object(harvester_api.harvester.plot_manager, "public_keys_available", return_value=True): - with patch.object(harvester_api.harvester.plot_manager, "plots", {tmp_path: mock_plot_info}): - # let passes_plot_filter and calculate_pos_challenge use real implementations - # make the prover fail during quality check - with patch.object( - mock_plot_info.prover, "get_qualities_for_challenge", side_effect=RuntimeError("test error") - ): - # should not raise exception, should handle error gracefully - await harvester_api.new_signage_point_harvester(new_challenge, mock_peer) + env, new_challenge, mock_peer = create_test_setup(harvester_environment, default_400_blocks, blockchain_constants) + with patch.object(env.plot_info.prover, "get_qualities_for_challenge", side_effect=RuntimeError("test error")): + # should not raise exception, should handle error gracefully + await env.harvester_api.new_signage_point_harvester(new_challenge, mock_peer) diff --git a/chia/_tests/solver/test_solver_service.py b/chia/_tests/solver/test_solver_service.py index f1d126749706..b6339f00553f 100644 --- a/chia/_tests/solver/test_solver_service.py +++ b/chia/_tests/solver/test_solver_service.py @@ -21,12 +21,9 @@ @pytest.mark.anyio async def test_solver_api_methods(blockchain_constants: ConsensusConstants, tmp_path: Path) -> None: - """Test solver api protocol methods with real requests.""" async with setup_solver(tmp_path, blockchain_constants) as solver_service: solver = solver_service._node solver_api = solver_service._api - - # test api ready state assert solver_api.ready() is True # test solve with real SolverInfo @@ -108,7 +105,6 @@ async def test_solver_with_real_blocks_and_signage_points( async def test_solver_error_handling_and_edge_cases( blockchain_constants: ConsensusConstants, self_hostname: str, tmp_path: Path ) -> None: - """Test solver error handling with invalid requests and edge cases.""" async with setup_solver(tmp_path, blockchain_constants) as solver_service: assert solver_service.rpc_server is not None solver_rpc_client = await SolverRpcClient.create( @@ -123,7 +119,7 @@ async def test_solver_error_handling_and_edge_cases( pass # expected # test edge case parameters - valid_quality = "1234567890abcdef" * 4 # valid 32-byte hex + valid_quality = "1234567890abcdef" * 4 # test with edge case plot sizes and difficulties edge_cases = [ From 2d675002bab54d443409a8008bb63de77ca878d2 Mon Sep 17 00:00:00 2001 From: almogdepaz Date: Wed, 13 Aug 2025 17:33:51 +0300 Subject: [PATCH 20/42] aync solver, fixtures, tests --- chia/_tests/conftest.py | 21 ++- .../farmer_harvester/test_farmer_harvester.py | 173 ++++++++++++++++++ chia/_tests/solver/test_solver_service.py | 4 +- chia/farmer/farmer.py | 3 + chia/farmer/farmer_api.py | 116 +++++++----- chia/protocols/solver_protocol.py | 2 +- chia/simulator/setup_services.py | 2 + chia/solver/solver.py | 2 - chia/solver/solver_api.py | 8 +- chia/solver/solver_rpc_api.py | 2 +- chia/solver/start_solver.py | 10 +- 11 files changed, 275 insertions(+), 68 deletions(-) diff --git a/chia/_tests/conftest.py b/chia/_tests/conftest.py index 51266ab9db1c..1692c0016252 100644 --- a/chia/_tests/conftest.py +++ b/chia/_tests/conftest.py @@ -65,12 +65,14 @@ setup_full_node, setup_introducer, setup_seeder, + setup_solver, setup_timelord, ) from chia.simulator.start_simulator import SimulatorFullNodeService from chia.simulator.wallet_tools import WalletTool +from chia.solver.solver_service import SolverService from chia.timelord.timelord_service import TimelordService -from chia.types.peer_info import PeerInfo +from chia.types.peer_info import PeerInfo, UnresolvedPeerInfo from chia.util.config import create_default_chia_config, lock_and_load_config from chia.util.db_wrapper import generate_in_memory_db_uri from chia.util.keychain import Keychain @@ -881,6 +883,23 @@ async def farmer_one_harvester(tmp_path: Path, get_b_tools: BlockTools) -> Async yield _ +FarmerOneHarvesterSolver = tuple[list[HarvesterService], FarmerService, SolverService, BlockTools] + + +@pytest.fixture(scope="function") +async def farmer_one_harvester_solver( + tmp_path: Path, get_b_tools: BlockTools +) -> AsyncIterator[FarmerOneHarvesterSolver]: + async with setup_farmer_multi_harvester(get_b_tools, 1, tmp_path, get_b_tools.constants, start_services=True) as ( + harvester_services, + farmer_service, + bt, + ): + farmer_peer = UnresolvedPeerInfo(bt.config["self_hostname"], farmer_service._server.get_port()) + async with setup_solver(tmp_path / "solver", bt.constants, farmer_peers={farmer_peer}) as solver_service: + yield harvester_services, farmer_service, solver_service, bt + + @pytest.fixture(scope="function") async def farmer_one_harvester_not_started( tmp_path: Path, get_b_tools: BlockTools diff --git a/chia/_tests/farmer_harvester/test_farmer_harvester.py b/chia/_tests/farmer_harvester/test_farmer_harvester.py index bf5badc19979..f8af15550304 100644 --- a/chia/_tests/farmer_harvester/test_farmer_harvester.py +++ b/chia/_tests/farmer_harvester/test_farmer_harvester.py @@ -1,9 +1,11 @@ from __future__ import annotations import asyncio +import unittest.mock from math import floor from pathlib import Path from typing import Any, Optional +from unittest.mock import AsyncMock, Mock import pytest from chia_rs import G1Element @@ -23,12 +25,23 @@ from chia.protocols.outbound_message import NodeType, make_msg from chia.protocols.protocol_message_types import ProtocolMessageTypes from chia.simulator.block_tools import BlockTools +from chia.solver.solver_service import SolverService from chia.types.peer_info import UnresolvedPeerInfo from chia.util.config import load_config from chia.util.hash import std_hash from chia.util.keychain import generate_mnemonic +async def get_harvester_peer(farmer: Farmer) -> Any: + """wait for harvester connection and return the peer""" + + def has_harvester_connection() -> bool: + return len(farmer.server.get_connections(NodeType.HARVESTER)) > 0 + + await time_out_assert(10, has_harvester_connection, True) + return farmer.server.get_connections(NodeType.HARVESTER)[0] + + def farmer_is_started(farmer: Farmer) -> bool: return farmer.started @@ -298,3 +311,163 @@ async def test_harvester_has_no_server( harvester_server = harvesters[0]._server assert harvester_server.webserver is None + + +@pytest.mark.anyio +async def test_v2_qualities_new_sp_hash( + farmer_one_harvester_solver: tuple[list[HarvesterService], FarmerService, SolverService, BlockTools], +) -> None: + _, farmer_service, _solver_service, _bt = farmer_one_harvester_solver + farmer_api = farmer_service._api + farmer = farmer_api.farmer + + sp_hash = bytes32(b"1" * 32) + v2_qualities = harvester_protocol.V2Qualities( + challenge_hash=bytes32(b"2" * 32), + sp_hash=sp_hash, + plot_identifier="test_plot_id", + qualities=[bytes32(b"3" * 32)], + signage_point_index=uint8(0), + plot_size=uint8(32), + difficulty=uint64(1000), + pool_public_key=None, + pool_contract_puzzle_hash=bytes32(b"4" * 32), + plot_public_key=G1Element(), + ) + + harvester_peer = await get_harvester_peer(farmer) + await farmer_api.v2_qualities(v2_qualities, harvester_peer) + + assert sp_hash in farmer.number_of_responses + assert farmer.number_of_responses[sp_hash] == 0 + assert sp_hash in farmer.cache_add_time + + +@pytest.mark.anyio +async def test_v2_qualities_missing_sp_hash( + caplog: pytest.LogCaptureFixture, + farmer_one_harvester_solver: tuple[list[HarvesterService], FarmerService, SolverService, BlockTools], +) -> None: + _, farmer_service, _, _ = farmer_one_harvester_solver + farmer_api = farmer_service._api + + sp_hash = bytes32(b"1" * 32) + v2_qualities = harvester_protocol.V2Qualities( + challenge_hash=bytes32(b"2" * 32), + sp_hash=sp_hash, + plot_identifier="test_plot_id", + qualities=[bytes32(b"3" * 32)], + signage_point_index=uint8(0), + plot_size=uint8(32), + difficulty=uint64(1000), + pool_public_key=None, + pool_contract_puzzle_hash=bytes32(b"4" * 32), + plot_public_key=G1Element(), + ) + + harvester_peer = await get_harvester_peer(farmer_api.farmer) + await farmer_api.v2_qualities(v2_qualities, harvester_peer) + + assert f"Received V2 quality collection for a signage point that we do not have {sp_hash}" in caplog.text + + +@pytest.mark.anyio +async def test_v2_qualities_with_existing_sp( + farmer_one_harvester_solver: tuple[list[HarvesterService], FarmerService, SolverService, BlockTools], +) -> None: + _, farmer_service, _, _ = farmer_one_harvester_solver + farmer_api = farmer_service._api + farmer = farmer_api.farmer + + sp_hash = bytes32(b"1" * 32) + challenge_hash = bytes32(b"2" * 32) + + sp = farmer_protocol.NewSignagePoint( + challenge_hash=challenge_hash, + challenge_chain_sp=sp_hash, + reward_chain_sp=std_hash(b"1"), + difficulty=uint64(1000), + sub_slot_iters=uint64(1000), + signage_point_index=uint8(0), + peak_height=uint32(1), + last_tx_height=uint32(0), + ) + + farmer.sps[sp_hash] = [sp] + + v2_qualities = harvester_protocol.V2Qualities( + challenge_hash=challenge_hash, + sp_hash=sp_hash, + plot_identifier="test_plot_id", + qualities=[bytes32(b"3" * 32), bytes32(b"5" * 32)], + signage_point_index=uint8(0), + plot_size=uint8(32), + difficulty=uint64(1000), + pool_public_key=G1Element(), + pool_contract_puzzle_hash=bytes32(b"4" * 32), + plot_public_key=G1Element(), + ) + + harvester_peer = await get_harvester_peer(farmer) + await farmer_api.v2_qualities(v2_qualities, harvester_peer) + + # should store 2 pending requests (one per quality) + assert len(farmer.pending_solver_requests) == 2 + assert sp_hash in farmer.cache_add_time + + +@pytest.mark.anyio +async def test_solution_response_handler( + farmer_one_harvester_solver: tuple[list[HarvesterService], FarmerService, SolverService, BlockTools], +) -> None: + _, farmer_service, _solver_service, _ = farmer_one_harvester_solver + farmer_api = farmer_service._api + farmer = farmer_api.farmer + + # set up a pending request + quality = bytes32(b"3" * 32) + sp_hash = bytes32(b"1" * 32) + challenge_hash = bytes32(b"2" * 32) + + v2_qualities = harvester_protocol.V2Qualities( + challenge_hash=challenge_hash, + sp_hash=sp_hash, + plot_identifier="test_plot_id", + qualities=[quality], + signage_point_index=uint8(0), + plot_size=uint8(32), + difficulty=uint64(1000), + pool_public_key=G1Element(), + pool_contract_puzzle_hash=bytes32(b"4" * 32), + plot_public_key=G1Element(), + ) + + harvester_peer = Mock() + harvester_peer.peer_node_id = "harvester_peer" + + # manually add pending request + farmer.pending_solver_requests[quality.hex()] = { + "quality_collection": v2_qualities, + "peer": harvester_peer, + "quality": quality, + } + + # create solution response + solution_response = farmer_protocol.SolutionResponse(proof=b"test_proof_from_solver") + solver_peer = Mock() + solver_peer.peer_node_id = "solver_peer" + + with unittest.mock.patch.object(farmer_api, "new_proof_of_space", new_callable=AsyncMock) as mock_new_proof: + await farmer_api.solution_response(solution_response, solver_peer) + + # verify new_proof_of_space was called with correct proof + mock_new_proof.assert_called_once() + call_args = mock_new_proof.call_args[0] + new_proof_of_space = call_args[0] + original_peer = call_args[1] + + assert new_proof_of_space.proof.proof == b"test_proof_from_solver" + assert original_peer == harvester_peer + + # verify pending request was removed + assert quality.hex() not in farmer.pending_solver_requests diff --git a/chia/_tests/solver/test_solver_service.py b/chia/_tests/solver/test_solver_service.py index b6339f00553f..2c2aab5921dc 100644 --- a/chia/_tests/solver/test_solver_service.py +++ b/chia/_tests/solver/test_solver_service.py @@ -27,7 +27,7 @@ async def test_solver_api_methods(blockchain_constants: ConsensusConstants, tmp_ assert solver_api.ready() is True # test solve with real SolverInfo - test_info = SolverInfo(plot_size=uint8(32), plot_diffculty=uint64(1500), quality_string=bytes32([42] * 32)) + test_info = SolverInfo(plot_size=uint8(32), plot_difficulty=uint64(1500), quality_string=bytes32([42] * 32)) # test normal solve operation (stub returns None) result = solver.solve(test_info) @@ -134,7 +134,7 @@ async def test_solver_error_handling_and_edge_cases( # test solver handles exception in solve method solver = solver_service._node - test_info = SolverInfo(plot_size=uint8(32), plot_diffculty=uint64(1000), quality_string=bytes32.zeros) + test_info = SolverInfo(plot_size=uint8(32), plot_difficulty=uint64(1000), quality_string=bytes32.zeros) with patch.object(solver, "solve", side_effect=RuntimeError("test error")): # solver api should handle exceptions gracefully diff --git a/chia/farmer/farmer.py b/chia/farmer/farmer.py index 5c15be8a4ffc..93c8e0c465b4 100644 --- a/chia/farmer/farmer.py +++ b/chia/farmer/farmer.py @@ -143,6 +143,9 @@ def __init__( # Quality string to plot identifier and challenge_hash, for use with harvester.RequestSignatures self.quality_str_to_identifiers: dict[bytes32, tuple[str, bytes32, bytes32, bytes32]] = {} + # Track pending solver requests, keyed by quality string hex + self.pending_solver_requests: dict[str, dict[str, Any]] = {} + # number of responses to each signage point self.number_of_responses: dict[bytes32, int] = {} diff --git a/chia/farmer/farmer_api.py b/chia/farmer/farmer_api.py index dcd1508ee18f..7a5248654c6c 100644 --- a/chia/farmer/farmer_api.py +++ b/chia/farmer/farmer_api.py @@ -507,64 +507,84 @@ async def v2_qualities(self, quality_collection: V2Qualities, peer: WSChiaConnec for quality in quality_collection.qualities: solver_info = SolverInfo( plot_size=quality_collection.plot_size, - plot_diffculty=quality_collection.difficulty, # Note: typo in SolverInfo field name + plot_difficulty=quality_collection.difficulty, quality_string=quality, ) - # Call solver service to get proof - # TODO: Add proper solver service node connection to farmer - # For now, assume solver service is available via server connections - proof_bytes = None - - # Try to call solver service - this requires farmer to have solver connections configured try: - # Send solve request to solver service - # This would work if farmer is connected to a solver service node - solver_response = await self.farmer.server.send_to_all_and_wait_first( - [make_msg(ProtocolMessageTypes.solve, solver_info)], - NodeType.FARMER, # TODO: Need SOLVER node type - ) + # store pending request data for matching with response + request_key = quality.hex() + self.farmer.pending_solver_requests[request_key] = { + "quality_collection": quality_collection, + "peer": peer, + "quality": quality, + } - if solver_response is not None and isinstance(solver_response, SolutionResponse): - proof_bytes = solver_response.proof - self.farmer.log.debug(f"Received {len(proof_bytes)} byte proof from solver") - else: - self.farmer.log.warning(f"No valid solver response for quality {quality.hex()[:10]}...") + # send solve request to all solver connections + msg = make_msg(ProtocolMessageTypes.solve, solver_info) + await self.farmer.server.send_to_all([msg], NodeType.SOLVER) + self.farmer.log.debug(f"Sent solve request for quality {quality.hex()[:10]}...") except Exception as e: self.farmer.log.error(f"Failed to call solver service for quality {quality.hex()[:10]}...: {e}") + # clean up pending request + if request_key in self.farmer.pending_solver_requests: + del self.farmer.pending_solver_requests[request_key] - # Fall back to stub if solver service unavailable - if proof_bytes is None: - self.farmer.log.warning("Using stub proof - solver service not available") - proof_bytes = b"stub_proof_from_solver" - - # Create ProofOfSpace object using solver response and plot metadata - # Need to calculate sp_challenge_hash for ProofOfSpace constructor - # TODO: We need plot_id to calculate this properly - may need to add to V2Qualities - sp_challenge_hash = quality_collection.challenge_hash # Approximation for now - - # Create a NewProofOfSpace object that can go through existing flow - new_proof_of_space = harvester_protocol.NewProofOfSpace( - quality_collection.challenge_hash, - quality_collection.sp_hash, - quality_collection.plot_identifier, - ProofOfSpace( - sp_challenge_hash, - quality_collection.pool_public_key, - quality_collection.pool_contract_puzzle_hash, - quality_collection.plot_public_key, - quality_collection.plot_size, - proof_bytes, - ), - quality_collection.signage_point_index, - include_source_signature_data=False, - farmer_reward_address_override=None, - fee_info=None, - ) + @metadata.request() + async def solution_response(self, response: SolutionResponse, peer: WSChiaConnection) -> None: + """ + Handle solution response from solver service. + This is called when a solver responds to a solve request. + """ + self.farmer.log.debug(f"Received solution response: {len(response.proof)} bytes from {peer.peer_node_id}") + + # find the matching pending request + request_key = None + for key, request_data in self.farmer.pending_solver_requests.items(): + # match by quality since SolutionResponse doesn't include the original quality + # this is a simple implementation - could be improved with better matching + if len(self.farmer.pending_solver_requests) == 1: # for now, assume single request + request_key = key + break + + if request_key is None: + self.farmer.log.warning("Received solver response but no matching pending request found") + return + + # get the original request data + request_data = self.farmer.pending_solver_requests.pop(request_key) + quality_collection = request_data["quality_collection"] + original_peer = request_data["peer"] + quality = request_data["quality"] + + # create the proof of space with the solver's proof + proof_bytes = response.proof + if proof_bytes is None or len(proof_bytes) == 0: + self.farmer.log.warning(f"Received empty proof from solver for quality {quality.hex()[:10]}...") + return + + sp_challenge_hash = quality_collection.challenge_hash + new_proof_of_space = harvester_protocol.NewProofOfSpace( + quality_collection.challenge_hash, + quality_collection.sp_hash, + quality_collection.plot_identifier, + ProofOfSpace( + sp_challenge_hash, + quality_collection.pool_public_key, + quality_collection.pool_contract_puzzle_hash, + quality_collection.plot_public_key, + quality_collection.plot_size, + proof_bytes, + ), + quality_collection.signage_point_index, + include_source_signature_data=False, + farmer_reward_address_override=None, + fee_info=None, + ) - # Route through existing new_proof_of_space flow - await self.new_proof_of_space(new_proof_of_space, peer) + # process the proof of space + await self.new_proof_of_space(new_proof_of_space, original_peer) @metadata.request() async def respond_signatures(self, response: harvester_protocol.RespondSignatures) -> None: diff --git a/chia/protocols/solver_protocol.py b/chia/protocols/solver_protocol.py index f95bcc12c44c..1684fe1c3fdc 100644 --- a/chia/protocols/solver_protocol.py +++ b/chia/protocols/solver_protocol.py @@ -12,5 +12,5 @@ @dataclass(frozen=True) class SolverInfo(Streamable): plot_size: uint8 - plot_diffculty: uint64 + plot_difficulty: uint64 quality_string: bytes32 diff --git a/chia/simulator/setup_services.py b/chia/simulator/setup_services.py index ac21ae343a0a..9dba1baed14c 100644 --- a/chia/simulator/setup_services.py +++ b/chia/simulator/setup_services.py @@ -513,6 +513,7 @@ async def setup_solver( root_path: Path, consensus_constants: ConsensusConstants, start_service: bool = True, + farmer_peers: set[UnresolvedPeerInfo] = set(), ) -> AsyncGenerator[SolverService, None]: with create_lock_and_load_config(root_path / "config" / "ssl" / "ca", root_path) as config: config["logging"]["log_stdout"] = True @@ -526,6 +527,7 @@ async def setup_solver( root_path, config, consensus_constants, + farmer_peers, ) async with service.manage(start=start_service): diff --git a/chia/solver/solver.py b/chia/solver/solver.py index 36cd723951ff..3cd1aa8c51dc 100644 --- a/chia/solver/solver.py +++ b/chia/solver/solver.py @@ -69,8 +69,6 @@ async def manage(self) -> AsyncIterator[None]: def solve(self, info: SolverInfo) -> Optional[bytes]: self.log.debug(f"Solve request: quality={info.quality_string.hex()}") - # stub implementation - always returns None - self.log.debug("Solve completed (stub implementation)") # todo implement actualy calling the solver return None diff --git a/chia/solver/solver_api.py b/chia/solver/solver_api.py index 4a85046f465a..167e1a836946 100644 --- a/chia/solver/solver_api.py +++ b/chia/solver/solver_api.py @@ -43,7 +43,7 @@ async def solve( self.log.debug( f"Solving quality {request.quality_string.hex()[:10]}... " - f"for plot size {request.plot_size} with difficulty {request.plot_diffculty}" + f"for plot size {request.plot_size} with difficulty {request.plot_difficulty}" ) try: @@ -52,12 +52,8 @@ async def solve( self.log.warning(f"Solver returned no proof for quality {request.quality_string.hex()[:10]}...") return None - response: SolutionResponse = SolutionResponse( - proof=proof, - ) - self.log.debug(f"Successfully solved quality, returning {len(proof)} byte proof") - return make_msg(ProtocolMessageTypes.solution_response, response) + return make_msg(ProtocolMessageTypes.solution_response, SolutionResponse(proof=proof)) except Exception as e: self.log.error(f"Error solving quality {request.quality_string.hex()[:10]}...: {e}") diff --git a/chia/solver/solver_rpc_api.py b/chia/solver/solver_rpc_api.py index f6521641f92a..03693bc6b713 100644 --- a/chia/solver/solver_rpc_api.py +++ b/chia/solver/solver_rpc_api.py @@ -39,7 +39,7 @@ async def solve(self, request: dict[str, Any]) -> EndpointResult: # create complete SolverInfo object with all provided data solver_info = SolverInfo( plot_size=uint8(plot_size), - plot_diffculty=uint64(plot_difficulty), + plot_difficulty=uint64(plot_difficulty), quality_string=bytes32.from_hexstr(quality_string), ) diff --git a/chia/solver/start_solver.py b/chia/solver/start_solver.py index babd5de484ed..a73fe789678d 100644 --- a/chia/solver/start_solver.py +++ b/chia/solver/start_solver.py @@ -19,6 +19,7 @@ from chia.solver.solver_api import SolverAPI from chia.solver.solver_rpc_api import SolverRpcApi from chia.solver.solver_service import SolverService +from chia.types.peer_info import UnresolvedPeerInfo from chia.util.chia_logging import initialize_service_logging from chia.util.config import load_config, load_config_cli from chia.util.default_root import resolve_root_path @@ -34,6 +35,7 @@ def create_solver_service( root_path: pathlib.Path, config: dict[str, Any], consensus_constants: ConsensusConstants, + farmer_peers: set[UnresolvedPeerInfo] = set(), connect_to_daemon: bool = True, override_capabilities: Optional[list[tuple[uint16, str]]] = None, ) -> SolverService: @@ -62,6 +64,7 @@ def create_solver_service( service_name=SERVICE_NAME, upnp_ports=upnp_list, on_connect_callback=node.on_connect, + connect_peers=farmer_peers, network_id=network_id, rpc_info=rpc_info, connect_to_daemon=connect_to_daemon, @@ -96,13 +99,6 @@ def main() -> int: enable=os.environ.get(f"CHIA_INSTRUMENT_{SERVICE_NAME.upper()}") is not None ): service_config = load_config_cli(root_path, "config.yaml", SERVICE_NAME) - # target_peer_count = service_config.get("target_peer_count", 40) - service_config.get( - # "target_outbound_peer_count", 8 - # ) - # if target_peer_count < 0: - # target_peer_count = None - # if not service_config.get("use_chia_loop_policy", True): - # target_peer_count = None return async_run(coro=async_main(service_config, root_path=root_path)) From 283f8bbbe6e1fc924adf95a3dc36b3858b35e37d Mon Sep 17 00:00:00 2001 From: almogdepaz Date: Wed, 13 Aug 2025 18:08:08 +0300 Subject: [PATCH 21/42] naming --- .../farmer_harvester/test_farmer_harvester.py | 11 ++- chia/farmer/farmer.py | 2 +- chia/farmer/farmer_api.py | 79 ++++++++----------- chia/harvester/harvester_api.py | 12 +-- chia/protocols/solver_protocol.py | 7 ++ 5 files changed, 54 insertions(+), 57 deletions(-) diff --git a/chia/_tests/farmer_harvester/test_farmer_harvester.py b/chia/_tests/farmer_harvester/test_farmer_harvester.py index f8af15550304..f9f8d87e88f7 100644 --- a/chia/_tests/farmer_harvester/test_farmer_harvester.py +++ b/chia/_tests/farmer_harvester/test_farmer_harvester.py @@ -21,7 +21,7 @@ from chia.harvester.harvester_rpc_client import HarvesterRpcClient from chia.harvester.harvester_service import HarvesterService from chia.plotting.util import PlotsRefreshParameter -from chia.protocols import farmer_protocol, harvester_protocol +from chia.protocols import farmer_protocol, harvester_protocol, solver_protocol from chia.protocols.outbound_message import NodeType, make_msg from chia.protocols.protocol_message_types import ProtocolMessageTypes from chia.simulator.block_tools import BlockTools @@ -446,14 +446,13 @@ async def test_solution_response_handler( harvester_peer.peer_node_id = "harvester_peer" # manually add pending request - farmer.pending_solver_requests[quality.hex()] = { - "quality_collection": v2_qualities, + farmer.pending_solver_requests[quality] = { + "quality_data": v2_qualities, "peer": harvester_peer, - "quality": quality, } # create solution response - solution_response = farmer_protocol.SolutionResponse(proof=b"test_proof_from_solver") + solution_response = solver_protocol.SolverResponse(quality_string=quality, proof=b"test_proof_from_solver") solver_peer = Mock() solver_peer.peer_node_id = "solver_peer" @@ -470,4 +469,4 @@ async def test_solution_response_handler( assert original_peer == harvester_peer # verify pending request was removed - assert quality.hex() not in farmer.pending_solver_requests + assert quality not in farmer.pending_solver_requests diff --git a/chia/farmer/farmer.py b/chia/farmer/farmer.py index 93c8e0c465b4..2d33c7a25007 100644 --- a/chia/farmer/farmer.py +++ b/chia/farmer/farmer.py @@ -144,7 +144,7 @@ def __init__( self.quality_str_to_identifiers: dict[bytes32, tuple[str, bytes32, bytes32, bytes32]] = {} # Track pending solver requests, keyed by quality string hex - self.pending_solver_requests: dict[str, dict[str, Any]] = {} + self.pending_solver_requests: dict[bytes32, dict[str, Any]] = {} # number of responses to each signage point self.number_of_responses: dict[bytes32, int] = {} diff --git a/chia/farmer/farmer_api.py b/chia/farmer/farmer_api.py index 7a5248654c6c..303bb396810d 100644 --- a/chia/farmer/farmer_api.py +++ b/chia/farmer/farmer_api.py @@ -15,7 +15,7 @@ from chia.farmer.farmer import Farmer, increment_pool_stats, strip_old_entries from chia.harvester.harvester_api import HarvesterAPI from chia.protocols import farmer_protocol, harvester_protocol -from chia.protocols.farmer_protocol import DeclareProofOfSpace, SignedValues, SolutionResponse +from chia.protocols.farmer_protocol import DeclareProofOfSpace, SignedValues from chia.protocols.harvester_protocol import ( PlotSyncDone, PlotSyncPathList, @@ -34,7 +34,7 @@ get_current_authentication_token, ) from chia.protocols.protocol_message_types import ProtocolMessageTypes -from chia.protocols.solver_protocol import SolverInfo +from chia.protocols.solver_protocol import SolverInfo, SolverResponse from chia.server.api_protocol import ApiMetadata from chia.server.server import ssl_context_for_root from chia.server.ws_connection import WSChiaConnection @@ -481,43 +481,41 @@ async def new_proof_of_space( return @metadata.request(peer_required=True) - async def v2_qualities(self, quality_collection: V2Qualities, peer: WSChiaConnection) -> None: + async def v2_qualities(self, quality_data: V2Qualities, peer: WSChiaConnection) -> None: """ This is a response from the harvester for V2 plots, containing only qualities. We store these qualities and will later use solver service to generate proofs when needed. """ - if quality_collection.sp_hash not in self.farmer.number_of_responses: - self.farmer.number_of_responses[quality_collection.sp_hash] = 0 - self.farmer.cache_add_time[quality_collection.sp_hash] = uint64(int(time.time())) + if quality_data.sp_hash not in self.farmer.number_of_responses: + self.farmer.number_of_responses[quality_data.sp_hash] = 0 + self.farmer.cache_add_time[quality_data.sp_hash] = uint64(int(time.time())) - if quality_collection.sp_hash not in self.farmer.sps: + if quality_data.sp_hash not in self.farmer.sps: self.farmer.log.warning( - f"Received V2 quality collection for a signage point that we do not have {quality_collection.sp_hash}" + f"Received V2 quality collection for a signage point that we do not have {quality_data.sp_hash}" ) return None - self.farmer.cache_add_time[quality_collection.sp_hash] = uint64(int(time.time())) + self.farmer.cache_add_time[quality_data.sp_hash] = uint64(int(time.time())) self.farmer.log.info( - f"Received V2 quality collection with {len(quality_collection.qualities)} qualities " - f"for plot {quality_collection.plot_identifier[:10]}... from {peer.peer_node_id}" + f"Received V2 quality collection with {len(quality_data.qualities)} qualities " + f"for plot {quality_data.plot_identifier[:10]}... from {peer.peer_node_id}" ) # Process each quality through solver service to get full proofs - for quality in quality_collection.qualities: + for quality in quality_data.qualities: solver_info = SolverInfo( - plot_size=quality_collection.plot_size, - plot_difficulty=quality_collection.difficulty, + plot_size=quality_data.plot_size, + plot_difficulty=quality_data.difficulty, quality_string=quality, ) try: # store pending request data for matching with response - request_key = quality.hex() - self.farmer.pending_solver_requests[request_key] = { - "quality_collection": quality_collection, + self.farmer.pending_solver_requests[quality] = { + "quality_data": quality_data, "peer": peer, - "quality": quality, } # send solve request to all solver connections @@ -528,35 +526,28 @@ async def v2_qualities(self, quality_collection: V2Qualities, peer: WSChiaConnec except Exception as e: self.farmer.log.error(f"Failed to call solver service for quality {quality.hex()[:10]}...: {e}") # clean up pending request - if request_key in self.farmer.pending_solver_requests: - del self.farmer.pending_solver_requests[request_key] + if quality in self.farmer.pending_solver_requests: + del self.farmer.pending_solver_requests[quality] @metadata.request() - async def solution_response(self, response: SolutionResponse, peer: WSChiaConnection) -> None: + async def solution_response(self, response: SolverResponse, peer: WSChiaConnection) -> None: """ Handle solution response from solver service. This is called when a solver responds to a solve request. """ self.farmer.log.debug(f"Received solution response: {len(response.proof)} bytes from {peer.peer_node_id}") - # find the matching pending request - request_key = None - for key, request_data in self.farmer.pending_solver_requests.items(): - # match by quality since SolutionResponse doesn't include the original quality - # this is a simple implementation - could be improved with better matching - if len(self.farmer.pending_solver_requests) == 1: # for now, assume single request - request_key = key - break - - if request_key is None: - self.farmer.log.warning("Received solver response but no matching pending request found") + # find the matching pending request using quality_string + + if response.quality_string not in self.farmer.pending_solver_requests: + self.farmer.log.warning(f"Received solver response for unknown quality {response.quality_string}") return # get the original request data - request_data = self.farmer.pending_solver_requests.pop(request_key) - quality_collection = request_data["quality_collection"] + request_data = self.farmer.pending_solver_requests.pop(response.quality_string) + quality_data = request_data["quality_data"] original_peer = request_data["peer"] - quality = request_data["quality"] + quality = response.quality_string # create the proof of space with the solver's proof proof_bytes = response.proof @@ -564,20 +555,20 @@ async def solution_response(self, response: SolutionResponse, peer: WSChiaConnec self.farmer.log.warning(f"Received empty proof from solver for quality {quality.hex()[:10]}...") return - sp_challenge_hash = quality_collection.challenge_hash + sp_challenge_hash = quality_data.challenge_hash new_proof_of_space = harvester_protocol.NewProofOfSpace( - quality_collection.challenge_hash, - quality_collection.sp_hash, - quality_collection.plot_identifier, + quality_data.challenge_hash, + quality_data.sp_hash, + quality_data.plot_identifier, ProofOfSpace( sp_challenge_hash, - quality_collection.pool_public_key, - quality_collection.pool_contract_puzzle_hash, - quality_collection.plot_public_key, - quality_collection.plot_size, + quality_data.pool_public_key, + quality_data.pool_contract_puzzle_hash, + quality_data.plot_public_key, + quality_data.plot_size, proof_bytes, ), - quality_collection.signage_point_index, + quality_data.signage_point_index, include_source_signature_data=False, farmer_reward_address_override=None, fee_info=None, diff --git a/chia/harvester/harvester_api.py b/chia/harvester/harvester_api.py index 7a90a9680018..eb0bb82bf756 100644 --- a/chia/harvester/harvester_api.py +++ b/chia/harvester/harvester_api.py @@ -283,10 +283,10 @@ async def lookup_v2_qualities(filename: Path, plot_info: PlotInfo) -> tuple[Path # Executes V2 quality lookup in a thread pool if self.harvester._shut_down: return filename, None - v2_quality_collection: Optional[V2Qualities] = await loop.run_in_executor( + qualities: Optional[V2Qualities] = await loop.run_in_executor( self.harvester.executor, blocking_lookup_v2_qualities, filename, plot_info ) - return filename, v2_quality_collection + return filename, qualities async def lookup_challenge( filename: Path, plot_info: PlotInfo @@ -367,16 +367,16 @@ async def lookup_challenge( # Process V2 plot quality collections (new flow) for filename_quality_awaitable in asyncio.as_completed(v2_awaitables): - filename, v2_quality_collection = await filename_quality_awaitable + filename, v2_qualities = await filename_quality_awaitable time_taken = time.monotonic() - start if time_taken > 8: self.harvester.log.warning( f"Looking up V2 qualities on {filename} took: {time_taken}. This should be below 8 seconds" f" to minimize risk of losing rewards." ) - if v2_quality_collection is not None: - total_v2_qualities_found += len(v2_quality_collection.qualities) - msg = make_msg(ProtocolMessageTypes.v2_qualities, v2_quality_collection) + if v2_qualities is not None: + total_v2_qualities_found += len(v2_qualities.qualities) + msg = make_msg(ProtocolMessageTypes.v2_qualities, v2_qualities) await peer.send_message(msg) now = uint64(int(time.time())) diff --git a/chia/protocols/solver_protocol.py b/chia/protocols/solver_protocol.py index 1684fe1c3fdc..4cf2afb8095d 100644 --- a/chia/protocols/solver_protocol.py +++ b/chia/protocols/solver_protocol.py @@ -14,3 +14,10 @@ class SolverInfo(Streamable): plot_size: uint8 plot_difficulty: uint64 quality_string: bytes32 + + +@streamable +@dataclass(frozen=True) +class SolverResponse(Streamable): + quality_string: bytes32 + proof: bytes From 2bb408783025ff7345c5b5d1d18bfe3730b59aa3 Mon Sep 17 00:00:00 2001 From: almogdepaz Date: Wed, 13 Aug 2025 21:39:13 +0300 Subject: [PATCH 22/42] remove redundant farmer message --- chia/_tests/util/test_network_protocol_test.py | 3 +-- chia/protocols/farmer_protocol.py | 6 ------ chia/solver/solver_api.py | 8 +++++--- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/chia/_tests/util/test_network_protocol_test.py b/chia/_tests/util/test_network_protocol_test.py index 66d085eade2c..1acfe6b86791 100644 --- a/chia/_tests/util/test_network_protocol_test.py +++ b/chia/_tests/util/test_network_protocol_test.py @@ -137,7 +137,6 @@ def test_missing_messages() -> None: "NewSignagePoint", "RequestSignedValues", "SignedValues", - "SolutionResponse", } full_node_msgs = { @@ -222,7 +221,7 @@ def test_missing_messages() -> None: "RespondCompactProofOfTime", } - solver_msgs = {"SolverInfo"} + solver_msgs = {"SolverInfo", "SolverResponse"} shared_msgs = {"Handshake", "Capability", "Error"} diff --git a/chia/protocols/farmer_protocol.py b/chia/protocols/farmer_protocol.py index 97251d074ee0..0ee840f98931 100644 --- a/chia/protocols/farmer_protocol.py +++ b/chia/protocols/farmer_protocol.py @@ -105,9 +105,3 @@ class SignedValues(Streamable): quality_string: bytes32 foliage_block_data_signature: G2Element foliage_transaction_block_signature: G2Element - - -@streamable -@dataclass(frozen=True) -class SolutionResponse(Streamable): - proof: bytes diff --git a/chia/solver/solver_api.py b/chia/solver/solver_api.py index 167e1a836946..0c2899a73d4d 100644 --- a/chia/solver/solver_api.py +++ b/chia/solver/solver_api.py @@ -3,10 +3,9 @@ import logging from typing import TYPE_CHECKING, ClassVar, Optional, cast -from chia.protocols.farmer_protocol import SolutionResponse from chia.protocols.outbound_message import Message, make_msg from chia.protocols.protocol_message_types import ProtocolMessageTypes -from chia.protocols.solver_protocol import SolverInfo +from chia.protocols.solver_protocol import SolverInfo, SolverResponse from chia.server.api_protocol import ApiMetadata from chia.solver.solver import Solver @@ -53,7 +52,10 @@ async def solve( return None self.log.debug(f"Successfully solved quality, returning {len(proof)} byte proof") - return make_msg(ProtocolMessageTypes.solution_response, SolutionResponse(proof=proof)) + return make_msg( + ProtocolMessageTypes.solution_response, + SolverResponse(proof=proof, quality_string=request.quality_string), + ) except Exception as e: self.log.error(f"Error solving quality {request.quality_string.hex()[:10]}...: {e}") From 7ab94f3c6b1130663af8d71bde8a8b4b6a1ccfa9 Mon Sep 17 00:00:00 2001 From: almogdepaz Date: Mon, 18 Aug 2025 13:37:16 +0300 Subject: [PATCH 23/42] solver fixtures and tests --- chia/_tests/conftest.py | 2 +- .../farmer_harvester/test_farmer_harvester.py | 146 +++++++++++++++++- chia/_tests/util/setup_nodes.py | 69 +++++++++ chia/apis.py | 2 + chia/simulator/setup_services.py | 7 +- 5 files changed, 215 insertions(+), 11 deletions(-) diff --git a/chia/_tests/conftest.py b/chia/_tests/conftest.py index 1692c0016252..2385fa2b2a10 100644 --- a/chia/_tests/conftest.py +++ b/chia/_tests/conftest.py @@ -896,7 +896,7 @@ async def farmer_one_harvester_solver( bt, ): farmer_peer = UnresolvedPeerInfo(bt.config["self_hostname"], farmer_service._server.get_port()) - async with setup_solver(tmp_path / "solver", bt.constants, farmer_peers={farmer_peer}) as solver_service: + async with setup_solver(tmp_path / "solver", bt, bt.constants, farmer_peer=farmer_peer) as solver_service: yield harvester_services, farmer_service, solver_service, bt diff --git a/chia/_tests/farmer_harvester/test_farmer_harvester.py b/chia/_tests/farmer_harvester/test_farmer_harvester.py index f9f8d87e88f7..af1fb2722f20 100644 --- a/chia/_tests/farmer_harvester/test_farmer_harvester.py +++ b/chia/_tests/farmer_harvester/test_farmer_harvester.py @@ -42,6 +42,16 @@ def has_harvester_connection() -> bool: return farmer.server.get_connections(NodeType.HARVESTER)[0] +async def get_solver_peer(farmer: Farmer) -> Any: + """wait for solver connection and return the peer""" + + def has_solver_connection() -> bool: + return len(farmer.server.get_connections(NodeType.SOLVER)) > 0 + + await time_out_assert(60, has_solver_connection, True) + return farmer.server.get_connections(NodeType.SOLVER)[0] + + def farmer_is_started(farmer: Farmer) -> bool: return farmer.started @@ -326,7 +336,7 @@ async def test_v2_qualities_new_sp_hash( challenge_hash=bytes32(b"2" * 32), sp_hash=sp_hash, plot_identifier="test_plot_id", - qualities=[bytes32(b"3" * 32)], + quality_chains=[b"test_quality_chain_1"], signage_point_index=uint8(0), plot_size=uint8(32), difficulty=uint64(1000), @@ -356,7 +366,7 @@ async def test_v2_qualities_missing_sp_hash( challenge_hash=bytes32(b"2" * 32), sp_hash=sp_hash, plot_identifier="test_plot_id", - qualities=[bytes32(b"3" * 32)], + quality_chains=[b"test_quality_chain_1"], signage_point_index=uint8(0), plot_size=uint8(32), difficulty=uint64(1000), @@ -399,7 +409,7 @@ async def test_v2_qualities_with_existing_sp( challenge_hash=challenge_hash, sp_hash=sp_hash, plot_identifier="test_plot_id", - qualities=[bytes32(b"3" * 32), bytes32(b"5" * 32)], + quality_chains=[b"test_quality_chain_1", b"test_quality_chain_2"], signage_point_index=uint8(0), plot_size=uint8(32), difficulty=uint64(1000), @@ -420,7 +430,7 @@ async def test_v2_qualities_with_existing_sp( async def test_solution_response_handler( farmer_one_harvester_solver: tuple[list[HarvesterService], FarmerService, SolverService, BlockTools], ) -> None: - _, farmer_service, _solver_service, _ = farmer_one_harvester_solver + _, farmer_service, _, _ = farmer_one_harvester_solver farmer_api = farmer_service._api farmer = farmer_api.farmer @@ -433,7 +443,7 @@ async def test_solution_response_handler( challenge_hash=challenge_hash, sp_hash=sp_hash, plot_identifier="test_plot_id", - qualities=[quality], + quality_chains=[b"test_quality_chain_for_quality"], signage_point_index=uint8(0), plot_size=uint8(32), difficulty=uint64(1000), @@ -442,8 +452,7 @@ async def test_solution_response_handler( plot_public_key=G1Element(), ) - harvester_peer = Mock() - harvester_peer.peer_node_id = "harvester_peer" + harvester_peer = await get_harvester_peer(farmer) # manually add pending request farmer.pending_solver_requests[quality] = { @@ -470,3 +479,126 @@ async def test_solution_response_handler( # verify pending request was removed assert quality not in farmer.pending_solver_requests + + +@pytest.mark.anyio +async def test_solution_response_unknown_quality( + farmer_one_harvester_solver: tuple[list[HarvesterService], FarmerService, SolverService, BlockTools], +) -> None: + _, farmer_service, _, _ = farmer_one_harvester_solver + farmer_api = farmer_service._api + farmer = farmer_api.farmer + + # get real solver peer connection + solver_peer = await get_solver_peer(farmer) + + # create solution response with unknown quality + solution_response = solver_protocol.SolverResponse(quality_string=bytes32(b"1" * 32), proof=b"test_proof") + + with unittest.mock.patch.object(farmer_api, "new_proof_of_space", new_callable=AsyncMock) as mock_new_proof: + await farmer_api.solution_response(solution_response, solver_peer) + # verify new_proof_of_space was NOT called + mock_new_proof.assert_not_called() + # verify pending requests unchanged + assert len(farmer.pending_solver_requests) == 0 + + +@pytest.mark.anyio +async def test_solution_response_empty_proof( + farmer_one_harvester_solver: tuple[list[HarvesterService], FarmerService, SolverService, BlockTools], +) -> None: + """Test solution_response with empty proof (line 555-556).""" + _, farmer_service, _solver_service, _ = farmer_one_harvester_solver + farmer_api = farmer_service._api + farmer = farmer_api.farmer + + # set up a pending request + quality = bytes32(b"3" * 32) + sp_hash = bytes32(b"1" * 32) + challenge_hash = bytes32(b"2" * 32) + + v2_qualities = harvester_protocol.V2Qualities( + challenge_hash=challenge_hash, + sp_hash=sp_hash, + plot_identifier="test_plot_id", + quality_chains=[b"test_quality_chain_for_quality"], + signage_point_index=uint8(0), + plot_size=uint8(32), + difficulty=uint64(1000), + pool_public_key=G1Element(), + pool_contract_puzzle_hash=bytes32(b"4" * 32), + plot_public_key=G1Element(), + ) + + harvester_peer = Mock() + harvester_peer.peer_node_id = "harvester_peer" + + # manually add pending request + farmer.pending_solver_requests[quality] = { + "quality_data": v2_qualities, + "peer": harvester_peer, + } + + # get real solver peer connection + solver_peer = await get_solver_peer(farmer) + + # create solution response with empty proof + solution_response = solver_protocol.SolverResponse(quality_string=quality, proof=b"") + + with unittest.mock.patch.object(farmer_api, "new_proof_of_space", new_callable=AsyncMock) as mock_new_proof: + await farmer_api.solution_response(solution_response, solver_peer) + + # verify new_proof_of_space was NOT called + mock_new_proof.assert_not_called() + + # verify pending request was removed (cleanup still happens) + assert quality not in farmer.pending_solver_requests + + +@pytest.mark.anyio +async def test_v2_qualities_solver_exception( + farmer_one_harvester_solver: tuple[list[HarvesterService], FarmerService, SolverService, BlockTools], +) -> None: + """Test v2_qualities with solver service exception (lines 526-527, 529-530).""" + _, farmer_service, _solver_service, _ = farmer_one_harvester_solver + farmer_api = farmer_service._api + farmer = farmer_api.farmer + + sp_hash = bytes32(b"1" * 32) + challenge_hash = bytes32(b"2" * 32) + + sp = farmer_protocol.NewSignagePoint( + challenge_hash=challenge_hash, + challenge_chain_sp=sp_hash, + reward_chain_sp=std_hash(b"1"), + difficulty=uint64(1000), + sub_slot_iters=uint64(1000), + signage_point_index=uint8(0), + peak_height=uint32(1), + last_tx_height=uint32(0), + ) + + farmer.sps[sp_hash] = [sp] + + v2_qualities = harvester_protocol.V2Qualities( + challenge_hash=challenge_hash, + sp_hash=sp_hash, + plot_identifier="test_plot_id", + quality_chains=[b"test_quality_chain_1"], + signage_point_index=uint8(0), + plot_size=uint8(32), + difficulty=uint64(1000), + pool_public_key=G1Element(), + pool_contract_puzzle_hash=bytes32(b"4" * 32), + plot_public_key=G1Element(), + ) + + harvester_peer = await get_harvester_peer(farmer) + + # Mock send_to_all to raise an exception + with unittest.mock.patch.object(farmer.server, "send_to_all", side_effect=Exception("Solver connection failed")): + await farmer_api.v2_qualities(v2_qualities, harvester_peer) + + # verify pending request was cleaned up after exception + quality = bytes32(b"3" * 32) + assert quality not in farmer.pending_solver_requests diff --git a/chia/_tests/util/setup_nodes.py b/chia/_tests/util/setup_nodes.py index a46d52957d2e..80a9c0d78487 100644 --- a/chia/_tests/util/setup_nodes.py +++ b/chia/_tests/util/setup_nodes.py @@ -342,6 +342,74 @@ async def setup_farmer_multi_harvester( yield harvester_services, farmer_service, block_tools +@asynccontextmanager +async def setup_farmer_multi_harvester_with_solver( + block_tools: BlockTools, + harvester_count: int, + temp_dir: Path, + consensus_constants: ConsensusConstants, + *, + start_services: bool, +) -> AsyncIterator[tuple[list[HarvesterService], FarmerService, SolverService, BlockTools]]: + async with AsyncExitStack() as async_exit_stack: + farmer_service = await async_exit_stack.enter_async_context( + setup_farmer( + block_tools, + temp_dir / "farmer", + block_tools.config["self_hostname"], + consensus_constants, + port=uint16(0), + start_service=start_services, + ) + ) + if start_services: + farmer_peer = UnresolvedPeerInfo(block_tools.config["self_hostname"], farmer_service._server.get_port()) + else: + farmer_peer = None + harvester_services = [ + await async_exit_stack.enter_async_context( + setup_harvester( + block_tools, + temp_dir / f"harvester_{i}", + farmer_peer, + consensus_constants, + start_service=start_services, + ) + ) + for i in range(harvester_count) + ] + + # Setup solver with farmer peer - CRITICAL: use same BlockTools root path for SSL CA consistency + solver_service = await async_exit_stack.enter_async_context( + setup_solver( + temp_dir / "solver", # Use temp_dir like harvester, not block_tools.root_path + block_tools, # Pass BlockTools so SSL CA can be consistent + consensus_constants, + start_service=start_services, + farmer_peer=farmer_peer, + ) + ) + + # Wait for farmer to be fully started before expecting solver connection + if start_services: + import asyncio + + # Wait for farmer to be fully initialized + timeout = 30 + for i in range(timeout): + if farmer_service._node.started: + print(f"Farmer fully started after {i} seconds") + break + await asyncio.sleep(1) + else: + print(f"WARNING: Farmer not started after {timeout} seconds") + + # Give solver additional time to connect + await asyncio.sleep(3) + + yield harvester_services, farmer_service, solver_service, block_tools + + @asynccontextmanager async def setup_full_system( consensus_constants: ConsensusConstants, @@ -479,6 +547,7 @@ async def setup_full_system_inner( solver_service = await async_exit_stack.enter_async_context( setup_solver( shared_b_tools.root_path / "solver", + shared_b_tools, consensus_constants, True, ) diff --git a/chia/apis.py b/chia/apis.py index 3445b7dd15c6..f30eac27fafb 100644 --- a/chia/apis.py +++ b/chia/apis.py @@ -6,6 +6,7 @@ from chia.introducer.introducer_api import IntroducerAPI from chia.protocols.outbound_message import NodeType from chia.server.api_protocol import ApiProtocol +from chia.solver.solver_api import SolverAPI from chia.timelord.timelord_api import TimelordAPI from chia.wallet.wallet_node_api import WalletNodeAPI @@ -16,4 +17,5 @@ NodeType.TIMELORD: TimelordAPI, NodeType.FARMER: FarmerAPI, NodeType.HARVESTER: HarvesterAPI, + NodeType.SOLVER: SolverAPI, } diff --git a/chia/simulator/setup_services.py b/chia/simulator/setup_services.py index 9dba1baed14c..9b95e02acfbc 100644 --- a/chia/simulator/setup_services.py +++ b/chia/simulator/setup_services.py @@ -511,11 +511,12 @@ async def setup_timelord( @asynccontextmanager async def setup_solver( root_path: Path, + b_tools: BlockTools, consensus_constants: ConsensusConstants, start_service: bool = True, - farmer_peers: set[UnresolvedPeerInfo] = set(), + farmer_peer: Optional[UnresolvedPeerInfo] = None, ) -> AsyncGenerator[SolverService, None]: - with create_lock_and_load_config(root_path / "config" / "ssl" / "ca", root_path) as config: + with create_lock_and_load_config(b_tools.root_path / "config" / "ssl" / "ca", root_path) as config: config["logging"]["log_stdout"] = True config["solver"]["enable_upnp"] = True config["solver"]["selected_network"] = "testnet0" @@ -527,7 +528,7 @@ async def setup_solver( root_path, config, consensus_constants, - farmer_peers, + farmer_peers={farmer_peer} if farmer_peer is not None else set(), ) async with service.manage(start=start_service): From ba75ad25d96d438f001724fd80c5f1fc6ccd557c Mon Sep 17 00:00:00 2001 From: almogdepaz Date: Mon, 18 Aug 2025 14:16:25 +0300 Subject: [PATCH 24/42] change quality_string to quality_chain --- .../farmer_harvester/test_farmer_harvester.py | 8 ++-- chia/_tests/solver/test_solver_service.py | 16 ++++--- chia/farmer/farmer.py | 2 +- chia/farmer/farmer_api.py | 37 +++++++++------- chia/harvester/harvester_api.py | 42 +++++++------------ chia/plotting/prover.py | 8 ++++ chia/protocols/harvester_protocol.py | 2 +- chia/protocols/shared_protocol.py | 3 +- chia/protocols/solver_protocol.py | 8 ++-- chia/solver/solver.py | 2 +- chia/solver/solver_api.py | 11 ++--- chia/solver/solver_rpc_api.py | 9 ++-- 12 files changed, 75 insertions(+), 73 deletions(-) diff --git a/chia/_tests/farmer_harvester/test_farmer_harvester.py b/chia/_tests/farmer_harvester/test_farmer_harvester.py index af1fb2722f20..f69fa358f4e9 100644 --- a/chia/_tests/farmer_harvester/test_farmer_harvester.py +++ b/chia/_tests/farmer_harvester/test_farmer_harvester.py @@ -186,7 +186,7 @@ def log_is_ready() -> bool: ) msg = make_msg(ProtocolMessageTypes.respond_signatures, response) await harvester_service._node.server.send_to_all([msg], NodeType.FARMER) - await time_out_assert(5, log_is_ready) + await time_out_assert(15, log_is_ready) # We fail the sps record check expected_error = f"Do not have challenge hash {challenge_hash}" assert expected_error in caplog.text @@ -461,7 +461,7 @@ async def test_solution_response_handler( } # create solution response - solution_response = solver_protocol.SolverResponse(quality_string=quality, proof=b"test_proof_from_solver") + solution_response = solver_protocol.SolverResponse(quality_chain=quality, proof=b"test_proof_from_solver") solver_peer = Mock() solver_peer.peer_node_id = "solver_peer" @@ -493,7 +493,7 @@ async def test_solution_response_unknown_quality( solver_peer = await get_solver_peer(farmer) # create solution response with unknown quality - solution_response = solver_protocol.SolverResponse(quality_string=bytes32(b"1" * 32), proof=b"test_proof") + solution_response = solver_protocol.SolverResponse(quality_chain=bytes(b"1" * 32), proof=b"test_proof") with unittest.mock.patch.object(farmer_api, "new_proof_of_space", new_callable=AsyncMock) as mock_new_proof: await farmer_api.solution_response(solution_response, solver_peer) @@ -543,7 +543,7 @@ async def test_solution_response_empty_proof( solver_peer = await get_solver_peer(farmer) # create solution response with empty proof - solution_response = solver_protocol.SolverResponse(quality_string=quality, proof=b"") + solution_response = solver_protocol.SolverResponse(quality_chain=quality, proof=b"") with unittest.mock.patch.object(farmer_api, "new_proof_of_space", new_callable=AsyncMock) as mock_new_proof: await farmer_api.solution_response(solution_response, solver_peer) diff --git a/chia/_tests/solver/test_solver_service.py b/chia/_tests/solver/test_solver_service.py index 2c2aab5921dc..4c483b3e683c 100644 --- a/chia/_tests/solver/test_solver_service.py +++ b/chia/_tests/solver/test_solver_service.py @@ -7,13 +7,14 @@ import pytest from chia_rs import ConsensusConstants, FullBlock from chia_rs.sized_bytes import bytes32 -from chia_rs.sized_ints import uint8, uint64 +from chia_rs.sized_ints import uint64 from chia._tests.blockchain.blockchain_test_utils import _validate_and_add_block from chia.consensus.blockchain import Blockchain from chia.consensus.get_block_challenge import get_block_challenge from chia.consensus.pot_iterations import is_overflow_block from chia.protocols.solver_protocol import SolverInfo +from chia.simulator.block_tools import create_block_tools_async from chia.simulator.setup_services import setup_solver from chia.solver.solver_rpc_client import SolverRpcClient from chia.types.blockchain_format.proof_of_space import verify_and_get_quality_string @@ -21,13 +22,14 @@ @pytest.mark.anyio async def test_solver_api_methods(blockchain_constants: ConsensusConstants, tmp_path: Path) -> None: - async with setup_solver(tmp_path, blockchain_constants) as solver_service: + bt = await create_block_tools_async(constants=blockchain_constants) + async with setup_solver(tmp_path, bt, blockchain_constants) as solver_service: solver = solver_service._node solver_api = solver_service._api assert solver_api.ready() is True # test solve with real SolverInfo - test_info = SolverInfo(plot_size=uint8(32), plot_difficulty=uint64(1500), quality_string=bytes32([42] * 32)) + test_info = SolverInfo(plot_difficulty=uint64(1500), quality_chain=b"test_quality_chain_42") # test normal solve operation (stub returns None) result = solver.solve(test_info) @@ -89,7 +91,8 @@ async def test_solver_with_real_blocks_and_signage_points( plot_size = pos.size() k_size = plot_size.size_v1 if plot_size.size_v1 is not None else plot_size.size_v2 assert k_size is not None - async with setup_solver(tmp_path, blockchain_constants) as solver_service: + bt = await create_block_tools_async(constants=blockchain_constants) + async with setup_solver(tmp_path, bt, blockchain_constants) as solver_service: assert solver_service.rpc_server is not None solver_rpc_client = await SolverRpcClient.create( self_hostname, solver_service.rpc_server.listen_port, solver_service.root_path, solver_service.config @@ -105,7 +108,8 @@ async def test_solver_with_real_blocks_and_signage_points( async def test_solver_error_handling_and_edge_cases( blockchain_constants: ConsensusConstants, self_hostname: str, tmp_path: Path ) -> None: - async with setup_solver(tmp_path, blockchain_constants) as solver_service: + bt = await create_block_tools_async(constants=blockchain_constants) + async with setup_solver(tmp_path, bt, blockchain_constants) as solver_service: assert solver_service.rpc_server is not None solver_rpc_client = await SolverRpcClient.create( self_hostname, solver_service.rpc_server.listen_port, solver_service.root_path, solver_service.config @@ -134,7 +138,7 @@ async def test_solver_error_handling_and_edge_cases( # test solver handles exception in solve method solver = solver_service._node - test_info = SolverInfo(plot_size=uint8(32), plot_difficulty=uint64(1000), quality_string=bytes32.zeros) + test_info = SolverInfo(plot_difficulty=uint64(1000), quality_chain=b"test_quality_chain_zeros") with patch.object(solver, "solve", side_effect=RuntimeError("test error")): # solver api should handle exceptions gracefully diff --git a/chia/farmer/farmer.py b/chia/farmer/farmer.py index 2d33c7a25007..e4a06fdc5805 100644 --- a/chia/farmer/farmer.py +++ b/chia/farmer/farmer.py @@ -144,7 +144,7 @@ def __init__( self.quality_str_to_identifiers: dict[bytes32, tuple[str, bytes32, bytes32, bytes32]] = {} # Track pending solver requests, keyed by quality string hex - self.pending_solver_requests: dict[bytes32, dict[str, Any]] = {} + self.pending_solver_requests: dict[bytes, dict[str, Any]] = {} # number of responses to each signage point self.number_of_responses: dict[bytes32, int] = {} diff --git a/chia/farmer/farmer_api.py b/chia/farmer/farmer_api.py index 303bb396810d..e3e250e78c41 100644 --- a/chia/farmer/farmer_api.py +++ b/chia/farmer/farmer_api.py @@ -499,21 +499,20 @@ async def v2_qualities(self, quality_data: V2Qualities, peer: WSChiaConnection) self.farmer.cache_add_time[quality_data.sp_hash] = uint64(int(time.time())) self.farmer.log.info( - f"Received V2 quality collection with {len(quality_data.qualities)} qualities " + f"Received V2 quality collection with {len(quality_data.quality_chains)} quality chains " f"for plot {quality_data.plot_identifier[:10]}... from {peer.peer_node_id}" ) - # Process each quality through solver service to get full proofs - for quality in quality_data.qualities: + # Process each quality chain through solver service to get full proofs + for quality_chain in quality_data.quality_chains: solver_info = SolverInfo( - plot_size=quality_data.plot_size, plot_difficulty=quality_data.difficulty, - quality_string=quality, + quality_chain=quality_chain, ) try: # store pending request data for matching with response - self.farmer.pending_solver_requests[quality] = { + self.farmer.pending_solver_requests[quality_chain] = { "quality_data": quality_data, "peer": peer, } @@ -521,13 +520,13 @@ async def v2_qualities(self, quality_data: V2Qualities, peer: WSChiaConnection) # send solve request to all solver connections msg = make_msg(ProtocolMessageTypes.solve, solver_info) await self.farmer.server.send_to_all([msg], NodeType.SOLVER) - self.farmer.log.debug(f"Sent solve request for quality {quality.hex()[:10]}...") + self.farmer.log.debug(f"Sent solve request for quality {quality_chain.hex()[:10]}...") except Exception as e: - self.farmer.log.error(f"Failed to call solver service for quality {quality.hex()[:10]}...: {e}") + self.farmer.log.error(f"Failed to call solver service for quality {quality_chain.hex()[:10]}...: {e}") # clean up pending request - if quality in self.farmer.pending_solver_requests: - del self.farmer.pending_solver_requests[quality] + if quality_chain in self.farmer.pending_solver_requests: + del self.farmer.pending_solver_requests[quality_chain] @metadata.request() async def solution_response(self, response: SolverResponse, peer: WSChiaConnection) -> None: @@ -539,20 +538,20 @@ async def solution_response(self, response: SolverResponse, peer: WSChiaConnecti # find the matching pending request using quality_string - if response.quality_string not in self.farmer.pending_solver_requests: - self.farmer.log.warning(f"Received solver response for unknown quality {response.quality_string}") + if response.quality_chain not in self.farmer.pending_solver_requests: + self.farmer.log.warning(f"Received solver response for unknown quality {response.quality_chain.hex()}") return # get the original request data - request_data = self.farmer.pending_solver_requests.pop(response.quality_string) + request_data = self.farmer.pending_solver_requests.pop(response.quality_chain) quality_data = request_data["quality_data"] original_peer = request_data["peer"] - quality = response.quality_string + quality = response.quality_chain # create the proof of space with the solver's proof proof_bytes = response.proof if proof_bytes is None or len(proof_bytes) == 0: - self.farmer.log.warning(f"Received empty proof from solver for quality {quality.hex()[:10]}...") + self.farmer.log.warning(f"Received empty proof from solver for quality {quality.hex()}...") return sp_challenge_hash = quality_data.challenge_hash @@ -577,6 +576,14 @@ async def solution_response(self, response: SolverResponse, peer: WSChiaConnecti # process the proof of space await self.new_proof_of_space(new_proof_of_space, original_peer) + def _derive_quality_string_from_chain(self, quality_chain: bytes) -> bytes32: + """Derive 32-byte quality string from quality chain (16 * k bits blob).""" + # TODO: todo_v2_plots implement actual quality string derivation algorithm + # For now, hash the quality chain to get a 32-byte result + from chia.util.hash import std_hash + + return std_hash(quality_chain) + @metadata.request() async def respond_signatures(self, response: harvester_protocol.RespondSignatures) -> None: request = self._process_respond_signatures(response) diff --git a/chia/harvester/harvester_api.py b/chia/harvester/harvester_api.py index eb0bb82bf756..efdc01c4fb0e 100644 --- a/chia/harvester/harvester_api.py +++ b/chia/harvester/harvester_api.py @@ -107,47 +107,29 @@ def blocking_lookup_v2_qualities(filename: Path, plot_info: PlotInfo) -> Optiona new_challenge.sp_hash, ) try: - quality_strings = plot_info.prover.get_qualities_for_challenge(sp_challenge_hash) + quality_chains = plot_info.prover.get_quality_chains_for_challenge(sp_challenge_hash) except Exception as e: - self.harvester.log.error(f"Exception fetching qualities for V2 plot {filename}. {e}") + self.harvester.log.error(f"Exception fetching quality chains for V2 plot {filename}. {e}") return None - if quality_strings is not None and len(quality_strings) > 0: + if quality_chains is not None and len(quality_chains) > 0: # Get the appropriate difficulty for this plot difficulty = new_challenge.difficulty - sub_slot_iters = new_challenge.sub_slot_iters if plot_info.pool_contract_puzzle_hash is not None: # Check for pool-specific difficulty for pool_difficulty in new_challenge.pool_difficulties: if pool_difficulty.pool_contract_puzzle_hash == plot_info.pool_contract_puzzle_hash: difficulty = pool_difficulty.difficulty - sub_slot_iters = pool_difficulty.sub_slot_iters break - # Filter qualities that pass the required_iters check (same as V1 flow) - good_qualities = [] - sp_interval_iters = calculate_sp_interval_iters(self.harvester.constants, sub_slot_iters) - - for quality_str in quality_strings: - required_iters: uint64 = calculate_iterations_quality( - self.harvester.constants, - quality_str, - PlotSize.make_v1(plot_info.prover.get_size()), # TODO: todo_v2_plots update for V2 - difficulty, - new_challenge.sp_hash, - sub_slot_iters, - new_challenge.last_tx_height, - ) - - if required_iters < sp_interval_iters: - good_qualities.append(quality_str) + # Filter quality chains that pass the required_iters check - if len(good_qualities) > 0: + if len(quality_chains) > 0: return V2Qualities( new_challenge.challenge_hash, new_challenge.sp_hash, - good_qualities[0].hex() + str(filename.resolve()), - good_qualities, + quality_chains[0].hex() + str(filename.resolve()), + quality_chains, new_challenge.signage_point_index, plot_info.prover.get_size(), difficulty, @@ -375,7 +357,7 @@ async def lookup_challenge( f" to minimize risk of losing rewards." ) if v2_qualities is not None: - total_v2_qualities_found += len(v2_qualities.qualities) + total_v2_qualities_found += len(v2_qualities.quality_chains) msg = make_msg(ProtocolMessageTypes.v2_qualities, v2_qualities) await peer.send_message(msg) @@ -411,6 +393,14 @@ async def lookup_challenge( }, ) + # def _derive_quality_string_from_chain(self, quality_chain: bytes) -> bytes32: + # """Derive 32-byte quality string from quality chain (16 * k bits blob).""" + # # TODO: todo_v2_plots implement actual quality string derivation algorithm + # # For now, hash the quality chain to get a 32-byte result + # from chia.util.hash import std_hash + + # return std_hash(quality_chain) + @metadata.request(reply_types=[ProtocolMessageTypes.respond_signatures]) async def request_signatures(self, request: harvester_protocol.RequestSignatures) -> Optional[Message]: """ diff --git a/chia/plotting/prover.py b/chia/plotting/prover.py index c88c0676bcda..f5deab54920a 100644 --- a/chia/plotting/prover.py +++ b/chia/plotting/prover.py @@ -27,6 +27,7 @@ def get_version(self) -> PlotVersion: ... def __bytes__(self) -> bytes: ... def get_id(self) -> bytes32: ... def get_qualities_for_challenge(self, challenge: bytes32) -> list[bytes32]: ... + def get_quality_chains_for_challenge(self, challenge: bytes) -> list[bytes]: ... def get_full_proof(self, challenge: bytes, index: int, parallel_read: bool = True) -> bytes: ... @classmethod @@ -72,6 +73,10 @@ def get_qualities_for_challenge(self, challenge: bytes) -> list[bytes32]: # TODO: todo_v2_plots Implement plot quality lookup raise NotImplementedError("V2 plot format is not yet implemented") + def get_quality_chains_for_challenge(self, challenge: bytes) -> list[bytes]: + # TODO: todo_v2_plots Implement quality chain lookup (16 * k bits blobs) + raise NotImplementedError("V2 plot format is not yet implemented") + def get_full_proof(self, challenge: bytes, index: int, parallel_read: bool = True) -> bytes: # TODO: todo_v2_plots Implement plot proof generation raise NotImplementedError("V2 plot format require solver to get full proof") @@ -115,6 +120,9 @@ def get_id(self) -> bytes32: def get_qualities_for_challenge(self, challenge: bytes32) -> list[bytes32]: return [bytes32(quality) for quality in self._disk_prover.get_qualities_for_challenge(challenge)] + def get_quality_chains_for_challenge(self, challenge: bytes) -> list[bytes]: + raise NotImplementedError("V1 does not implement quality chains, only qualities") + def get_full_proof(self, challenge: bytes, index: int, parallel_read: bool = True) -> bytes: return bytes(self._disk_prover.get_full_proof(challenge, index, parallel_read)) diff --git a/chia/protocols/harvester_protocol.py b/chia/protocols/harvester_protocol.py index 722f0070ffbd..dbaac29517e6 100644 --- a/chia/protocols/harvester_protocol.py +++ b/chia/protocols/harvester_protocol.py @@ -69,7 +69,7 @@ class V2Qualities(Streamable): challenge_hash: bytes32 sp_hash: bytes32 plot_identifier: str - qualities: list[bytes32] + quality_chains: list[bytes] # 16 * k bits blobs instead of 32-byte quality strings signage_point_index: uint8 plot_size: uint8 difficulty: uint64 diff --git a/chia/protocols/shared_protocol.py b/chia/protocols/shared_protocol.py index e7f7812bd7e2..ead02792fc79 100644 --- a/chia/protocols/shared_protocol.py +++ b/chia/protocols/shared_protocol.py @@ -17,6 +17,7 @@ NodeType.INTRODUCER: "0.0.36", NodeType.WALLET: "0.0.38", NodeType.DATA_LAYER: "0.0.36", + NodeType.SOLVER: "0.0.37", } """ @@ -65,7 +66,7 @@ class Capability(IntEnum): NodeType.INTRODUCER: _capabilities, NodeType.WALLET: _capabilities, NodeType.DATA_LAYER: _capabilities, - NodeType.SOLVER: [], + NodeType.SOLVER: _capabilities, } diff --git a/chia/protocols/solver_protocol.py b/chia/protocols/solver_protocol.py index 4cf2afb8095d..f09332271974 100644 --- a/chia/protocols/solver_protocol.py +++ b/chia/protocols/solver_protocol.py @@ -2,8 +2,7 @@ from dataclasses import dataclass -from chia_rs.sized_bytes import bytes32 -from chia_rs.sized_ints import uint8, uint64 +from chia_rs.sized_ints import uint64 from chia.util.streamable import Streamable, streamable @@ -11,13 +10,12 @@ @streamable @dataclass(frozen=True) class SolverInfo(Streamable): - plot_size: uint8 plot_difficulty: uint64 - quality_string: bytes32 + quality_chain: bytes # 16 * k bits blob, k (plot size) can be derived from this @streamable @dataclass(frozen=True) class SolverResponse(Streamable): - quality_string: bytes32 + quality_chain: bytes proof: bytes diff --git a/chia/solver/solver.py b/chia/solver/solver.py index 3cd1aa8c51dc..038990df7ed8 100644 --- a/chia/solver/solver.py +++ b/chia/solver/solver.py @@ -68,7 +68,7 @@ async def manage(self) -> AsyncIterator[None]: self.log.info("Solver service shutdown complete") def solve(self, info: SolverInfo) -> Optional[bytes]: - self.log.debug(f"Solve request: quality={info.quality_string.hex()}") + self.log.debug(f"Solve request: quality={info.quality_chain.hex()}") # todo implement actualy calling the solver return None diff --git a/chia/solver/solver_api.py b/chia/solver/solver_api.py index 0c2899a73d4d..cf62dd2304fd 100644 --- a/chia/solver/solver_api.py +++ b/chia/solver/solver_api.py @@ -40,23 +40,20 @@ async def solve( self.log.error("Solver is not started") return None - self.log.debug( - f"Solving quality {request.quality_string.hex()[:10]}... " - f"for plot size {request.plot_size} with difficulty {request.plot_difficulty}" - ) + self.log.debug(f"Solving quality {request.quality_chain.hex()}with difficulty {request.plot_difficulty}") try: proof = self.solver.solve(request) if proof is None: - self.log.warning(f"Solver returned no proof for quality {request.quality_string.hex()[:10]}...") + self.log.warning(f"Solver returned no proof for quality {request.quality_chain.hex()}") return None self.log.debug(f"Successfully solved quality, returning {len(proof)} byte proof") return make_msg( ProtocolMessageTypes.solution_response, - SolverResponse(proof=proof, quality_string=request.quality_string), + SolverResponse(proof=proof, quality_chain=request.quality_chain), ) except Exception as e: - self.log.error(f"Error solving quality {request.quality_string.hex()[:10]}...: {e}") + self.log.error(f"Error solving quality {request.quality_chain.hex()}: {e}") return None diff --git a/chia/solver/solver_rpc_api.py b/chia/solver/solver_rpc_api.py index 03693bc6b713..d6570e700c48 100644 --- a/chia/solver/solver_rpc_api.py +++ b/chia/solver/solver_rpc_api.py @@ -2,8 +2,7 @@ from typing import TYPE_CHECKING, Any, ClassVar, Optional, cast -from chia_rs.sized_bytes import bytes32 -from chia_rs.sized_ints import uint8, uint64 +from chia_rs.sized_ints import uint64 from chia.protocols.solver_protocol import SolverInfo from chia.rpc.rpc_server import Endpoint, EndpointResult @@ -32,15 +31,13 @@ async def _state_changed(self, change: str, change_data: Optional[dict[str, Any] async def solve(self, request: dict[str, Any]) -> EndpointResult: # extract all required fields from request - quality_string = request["quality_string"] - plot_size = request.get("plot_size", 32) # todo default ? + quality_chain = request["quality_chain"] plot_difficulty = request.get("plot_difficulty", 1000) # todo default ? # create complete SolverInfo object with all provided data solver_info = SolverInfo( - plot_size=uint8(plot_size), plot_difficulty=uint64(plot_difficulty), - quality_string=bytes32.from_hexstr(quality_string), + quality_chain=bytes.fromhex(quality_chain), ) proof = self.service.solve(solver_info) From 63fb5f24124520845824d35a63b91e95c7380ead Mon Sep 17 00:00:00 2001 From: almogdepaz Date: Mon, 18 Aug 2025 17:28:09 +0300 Subject: [PATCH 25/42] fix service tests, cleanup --- .../farmer_harvester/test_farmer_harvester.py | 14 +- chia/_tests/solver/test_solver_service.py | 160 ++++-------------- chia/farmer/farmer_api.py | 20 +-- chia/harvester/harvester_api.py | 10 +- chia/solver/solver.py | 2 +- chia/solver/solver_rpc_client.py | 4 +- 6 files changed, 52 insertions(+), 158 deletions(-) diff --git a/chia/_tests/farmer_harvester/test_farmer_harvester.py b/chia/_tests/farmer_harvester/test_farmer_harvester.py index f69fa358f4e9..68db86482b92 100644 --- a/chia/_tests/farmer_harvester/test_farmer_harvester.py +++ b/chia/_tests/farmer_harvester/test_farmer_harvester.py @@ -167,9 +167,6 @@ async def test_farmer_respond_signatures( # messages even though it didn't request them, to cover when the farmer doesn't know # about an sp_hash, so it fails at the sp record check. - def log_is_ready() -> bool: - return len(caplog.text) > 0 - _, _, harvester_service, _, _ = harvester_farmer_environment # We won't have an sp record for this one challenge_hash = bytes32(b"1" * 32) @@ -184,11 +181,16 @@ def log_is_ready() -> bool: include_source_signature_data=False, farmer_reward_address_override=None, ) + + expected_error = f"Do not have challenge hash {challenge_hash}" + + def expected_log_is_ready() -> bool: + return expected_error in caplog.text + msg = make_msg(ProtocolMessageTypes.respond_signatures, response) await harvester_service._node.server.send_to_all([msg], NodeType.FARMER) - await time_out_assert(15, log_is_ready) - # We fail the sps record check - expected_error = f"Do not have challenge hash {challenge_hash}" + await time_out_assert(10, expected_log_is_ready) + # We should find the error message assert expected_error in caplog.text diff --git a/chia/_tests/solver/test_solver_service.py b/chia/_tests/solver/test_solver_service.py index 4c483b3e683c..06f7733fec14 100644 --- a/chia/_tests/solver/test_solver_service.py +++ b/chia/_tests/solver/test_solver_service.py @@ -1,146 +1,56 @@ from __future__ import annotations from pathlib import Path -from typing import Optional from unittest.mock import patch import pytest -from chia_rs import ConsensusConstants, FullBlock -from chia_rs.sized_bytes import bytes32 +from chia_rs import ConsensusConstants from chia_rs.sized_ints import uint64 -from chia._tests.blockchain.blockchain_test_utils import _validate_and_add_block -from chia.consensus.blockchain import Blockchain -from chia.consensus.get_block_challenge import get_block_challenge -from chia.consensus.pot_iterations import is_overflow_block +from chia.protocols.outbound_message import Message from chia.protocols.solver_protocol import SolverInfo from chia.simulator.block_tools import create_block_tools_async +from chia.simulator.keyring import TempKeyring from chia.simulator.setup_services import setup_solver from chia.solver.solver_rpc_client import SolverRpcClient -from chia.types.blockchain_format.proof_of_space import verify_and_get_quality_string @pytest.mark.anyio async def test_solver_api_methods(blockchain_constants: ConsensusConstants, tmp_path: Path) -> None: - bt = await create_block_tools_async(constants=blockchain_constants) - async with setup_solver(tmp_path, bt, blockchain_constants) as solver_service: - solver = solver_service._node - solver_api = solver_service._api - assert solver_api.ready() is True - - # test solve with real SolverInfo - test_info = SolverInfo(plot_difficulty=uint64(1500), quality_chain=b"test_quality_chain_42") - - # test normal solve operation (stub returns None) - result = solver.solve(test_info) - assert result is None - - # test with mocked return value to verify full flow - expected_proof = b"test_proof_data_12345" - with patch.object(solver, "solve", return_value=expected_proof): - api_result = await solver_api.solve(test_info) - assert api_result is not None - # api returns protocol message for peer communication - from chia.protocols.outbound_message import Message - - assert isinstance(api_result, Message) - - # test error handling - solver not started - original_started = solver.started - solver.started = False - api_result = await solver_api.solve(test_info) - assert api_result is None - solver.started = original_started + with TempKeyring(populate=True) as keychain: + bt = await create_block_tools_async(constants=blockchain_constants, keychain=keychain) + async with setup_solver(tmp_path, bt, blockchain_constants) as solver_service: + solver = solver_service._node + solver_api = solver_service._api + assert solver_api.ready() is True + test_info = SolverInfo(plot_difficulty=uint64(1500), quality_chain=b"test_quality_chain_42") + expected_proof = b"test_proof_data_12345" + with patch.object(solver, "solve", return_value=expected_proof): + api_result = await solver_api.solve(test_info) + assert api_result is not None + assert isinstance(api_result, Message) @pytest.mark.anyio -async def test_solver_with_real_blocks_and_signage_points( - blockchain_constants: ConsensusConstants, - default_400_blocks: list[FullBlock], - empty_blockchain: Blockchain, - self_hostname: str, - tmp_path: Path, -) -> None: - blockchain = empty_blockchain - blocks = default_400_blocks[:3] - for block in blocks: - await _validate_and_add_block(empty_blockchain, block) - block = blocks[-1] # always use the last block - overflow = is_overflow_block(blockchain_constants, block.reward_chain_block.signage_point_index) - challenge = get_block_challenge(blockchain_constants, block, blockchain, False, overflow, False) - assert block.reward_chain_block.pos_ss_cc_challenge_hash == challenge - if block.reward_chain_block.challenge_chain_sp_vdf is None: - challenge_chain_sp: bytes32 = challenge - else: - challenge_chain_sp = block.reward_chain_block.challenge_chain_sp_vdf.output.get_hash() - # extract real quality data from blocks using chia's proof of space verification - pos = block.reward_chain_block.proof_of_space - # calculate real quality string from proof of space data - quality_string: Optional[bytes32] = verify_and_get_quality_string( - block.reward_chain_block.proof_of_space, - blockchain_constants, - challenge, - challenge_chain_sp, - height=block.reward_chain_block.height, - ) - - assert quality_string is not None - quality_hex = quality_string.hex() - - # test solver with real blockchain quality - plot_size = pos.size() - k_size = plot_size.size_v1 if plot_size.size_v1 is not None else plot_size.size_v2 - assert k_size is not None - bt = await create_block_tools_async(constants=blockchain_constants) - async with setup_solver(tmp_path, bt, blockchain_constants) as solver_service: - assert solver_service.rpc_server is not None - solver_rpc_client = await SolverRpcClient.create( - self_hostname, solver_service.rpc_server.listen_port, solver_service.root_path, solver_service.config - ) - solve_response = await solver_rpc_client.solve(quality_hex, int(k_size), 1000) - assert solve_response["success"] is True - assert "proof" in solve_response - # stub implementation returns None, real implementation would return actual proof - assert solve_response["proof"] is None - - -@pytest.mark.anyio -async def test_solver_error_handling_and_edge_cases( +async def test_solver_error_handling( blockchain_constants: ConsensusConstants, self_hostname: str, tmp_path: Path ) -> None: - bt = await create_block_tools_async(constants=blockchain_constants) - async with setup_solver(tmp_path, bt, blockchain_constants) as solver_service: - assert solver_service.rpc_server is not None - solver_rpc_client = await SolverRpcClient.create( - self_hostname, solver_service.rpc_server.listen_port, solver_service.root_path, solver_service.config - ) - - # test invalid quality string format - try: - await solver_rpc_client.solve("invalid_hex") - assert False, "should have raised exception for invalid hex" - except Exception: - pass # expected - - # test edge case parameters - valid_quality = "1234567890abcdef" * 4 - - # test with edge case plot sizes and difficulties - edge_cases = [ - (18, 1), # minimum plot size, minimum difficulty - (50, 999999), # large plot size, high difficulty - ] - - for plot_size, difficulty in edge_cases: - response = await solver_rpc_client.solve(valid_quality, plot_size, difficulty) - assert response["success"] is True - assert "proof" in response - - # test solver handles exception in solve method - solver = solver_service._node - test_info = SolverInfo(plot_difficulty=uint64(1000), quality_chain=b"test_quality_chain_zeros") - - with patch.object(solver, "solve", side_effect=RuntimeError("test error")): - # solver api should handle exceptions gracefully - result = await solver_service._api.solve(test_info) - assert result is None # api returns None on error + with TempKeyring(populate=True) as keychain: + bt = await create_block_tools_async(constants=blockchain_constants, keychain=keychain) + async with setup_solver(tmp_path, bt, blockchain_constants) as solver_service: + assert solver_service.rpc_server is not None + solver_rpc_client = await SolverRpcClient.create( + self_hostname, solver_service.rpc_server.listen_port, solver_service.root_path, solver_service.config + ) + try: + await solver_rpc_client.solve("invalid_hex") + assert False, "should have raised exception for invalid hex" + except Exception: + pass # expected + # test solver handles exception in solve method + solver = solver_service._node + test_info = SolverInfo(plot_difficulty=uint64(1000), quality_chain=b"test_quality_chain_zeros") + with patch.object(solver, "solve", side_effect=RuntimeError("test error")): + # solver api should handle exceptions gracefully + result = await solver_service._api.solve(test_info) + assert result is None # api returns None on error diff --git a/chia/farmer/farmer_api.py b/chia/farmer/farmer_api.py index e3e250e78c41..8eb61c366e70 100644 --- a/chia/farmer/farmer_api.py +++ b/chia/farmer/farmer_api.py @@ -75,7 +75,7 @@ async def new_proof_of_space( """ if new_proof_of_space.sp_hash not in self.farmer.number_of_responses: self.farmer.number_of_responses[new_proof_of_space.sp_hash] = 0 - self.farmer.cache_add_time[new_proof_of_space.sp_hash] = uint64(int(time.time())) + self.farmer.cache_add_time[new_proof_of_space.sp_hash] = uint64(time.time()) max_pos_per_sp = 5 @@ -172,14 +172,14 @@ async def new_proof_of_space( new_proof_of_space.proof, ) ) - self.farmer.cache_add_time[new_proof_of_space.sp_hash] = uint64(int(time.time())) + self.farmer.cache_add_time[new_proof_of_space.sp_hash] = uint64(time.time()) self.farmer.quality_str_to_identifiers[computed_quality_string] = ( new_proof_of_space.plot_identifier, new_proof_of_space.challenge_hash, new_proof_of_space.sp_hash, peer.peer_node_id, ) - self.farmer.cache_add_time[computed_quality_string] = uint64(int(time.time())) + self.farmer.cache_add_time[computed_quality_string] = uint64(time.time()) await peer.send_message(make_msg(ProtocolMessageTypes.request_signatures, request)) @@ -488,7 +488,7 @@ async def v2_qualities(self, quality_data: V2Qualities, peer: WSChiaConnection) """ if quality_data.sp_hash not in self.farmer.number_of_responses: self.farmer.number_of_responses[quality_data.sp_hash] = 0 - self.farmer.cache_add_time[quality_data.sp_hash] = uint64(int(time.time())) + self.farmer.cache_add_time[quality_data.sp_hash] = uint64(time.time()) if quality_data.sp_hash not in self.farmer.sps: self.farmer.log.warning( @@ -496,7 +496,7 @@ async def v2_qualities(self, quality_data: V2Qualities, peer: WSChiaConnection) ) return None - self.farmer.cache_add_time[quality_data.sp_hash] = uint64(int(time.time())) + self.farmer.cache_add_time[quality_data.sp_hash] = uint64(time.time()) self.farmer.log.info( f"Received V2 quality collection with {len(quality_data.quality_chains)} quality chains " @@ -576,14 +576,6 @@ async def solution_response(self, response: SolverResponse, peer: WSChiaConnecti # process the proof of space await self.new_proof_of_space(new_proof_of_space, original_peer) - def _derive_quality_string_from_chain(self, quality_chain: bytes) -> bytes32: - """Derive 32-byte quality string from quality chain (16 * k bits blob).""" - # TODO: todo_v2_plots implement actual quality string derivation algorithm - # For now, hash the quality chain to get a 32-byte result - from chia.util.hash import std_hash - - return std_hash(quality_chain) - @metadata.request() async def respond_signatures(self, response: harvester_protocol.RespondSignatures) -> None: request = self._process_respond_signatures(response) @@ -664,7 +656,7 @@ async def new_signage_point(self, new_signage_point: farmer_protocol.NewSignageP pool_dict[key] = strip_old_entries(pairs=pool_dict[key], before=cutoff_24h) - now = uint64(int(time.time())) + now = uint64(time.time()) self.farmer.cache_add_time[new_signage_point.challenge_chain_sp] = now missing_signage_points = self.farmer.check_missing_signage_points(now, new_signage_point) self.farmer.state_changed( diff --git a/chia/harvester/harvester_api.py b/chia/harvester/harvester_api.py index 1dcfff4c9a0e..a09c6f084572 100644 --- a/chia/harvester/harvester_api.py +++ b/chia/harvester/harvester_api.py @@ -363,7 +363,7 @@ async def lookup_challenge( msg = make_msg(ProtocolMessageTypes.v2_qualities, v2_qualities) await peer.send_message(msg) - now = uint64(int(time.time())) + now = uint64(time.time()) farming_info = FarmingInfo( new_challenge.challenge_hash, @@ -395,14 +395,6 @@ async def lookup_challenge( }, ) - # def _derive_quality_string_from_chain(self, quality_chain: bytes) -> bytes32: - # """Derive 32-byte quality string from quality chain (16 * k bits blob).""" - # # TODO: todo_v2_plots implement actual quality string derivation algorithm - # # For now, hash the quality chain to get a 32-byte result - # from chia.util.hash import std_hash - - # return std_hash(quality_chain) - @metadata.request(reply_types=[ProtocolMessageTypes.respond_signatures]) async def request_signatures(self, request: harvester_protocol.RequestSignatures) -> Optional[Message]: """ diff --git a/chia/solver/solver.py b/chia/solver/solver.py index 038990df7ed8..ba84a617961b 100644 --- a/chia/solver/solver.py +++ b/chia/solver/solver.py @@ -69,7 +69,7 @@ async def manage(self) -> AsyncIterator[None]: def solve(self, info: SolverInfo) -> Optional[bytes]: self.log.debug(f"Solve request: quality={info.quality_chain.hex()}") - # todo implement actualy calling the solver + # TODO todo_v2_plots implement actualy calling the solver return None def get_connections(self, request_node_type: Optional[NodeType]) -> list[dict[str, Any]]: diff --git a/chia/solver/solver_rpc_client.py b/chia/solver/solver_rpc_client.py index 55abbee74c42..9105b4676c5c 100644 --- a/chia/solver/solver_rpc_client.py +++ b/chia/solver/solver_rpc_client.py @@ -20,6 +20,4 @@ async def get_state(self) -> dict[str, Any]: async def solve(self, quality_string: str, plot_size: int = 32, plot_difficulty: int = 1000) -> dict[str, Any]: """Solve a quality string with optional plot size and difficulty.""" quality = bytes32.from_hexstr(quality_string) - return await self.fetch( - "solve", {"quality_string": quality.hex(), "plot_size": plot_size, "plot_difficulty": plot_difficulty} - ) + return await self.fetch("solve", {"quality_chain": quality.hex(), "plot_difficulty": plot_difficulty}) From c4ce5ec43f88090988426b7eeb526d1906e8bc17 Mon Sep 17 00:00:00 2001 From: almogdepaz Date: Mon, 18 Aug 2025 17:57:00 +0300 Subject: [PATCH 26/42] name change, fix comments --- .../farmer_harvester/test_farmer_harvester.py | 36 +++++++-------- .../_tests/util/test_network_protocol_test.py | 2 +- chia/farmer/farmer_api.py | 8 ++-- chia/harvester/harvester_api.py | 44 +++++++------------ chia/protocols/harvester_protocol.py | 2 +- chia/protocols/protocol_message_types.py | 2 +- 6 files changed, 42 insertions(+), 52 deletions(-) diff --git a/chia/_tests/farmer_harvester/test_farmer_harvester.py b/chia/_tests/farmer_harvester/test_farmer_harvester.py index 68db86482b92..6a4ac91a85a8 100644 --- a/chia/_tests/farmer_harvester/test_farmer_harvester.py +++ b/chia/_tests/farmer_harvester/test_farmer_harvester.py @@ -326,7 +326,7 @@ async def test_harvester_has_no_server( @pytest.mark.anyio -async def test_v2_qualities_new_sp_hash( +async def test_v2_quality_chains_new_sp_hash( farmer_one_harvester_solver: tuple[list[HarvesterService], FarmerService, SolverService, BlockTools], ) -> None: _, farmer_service, _solver_service, _bt = farmer_one_harvester_solver @@ -334,7 +334,7 @@ async def test_v2_qualities_new_sp_hash( farmer = farmer_api.farmer sp_hash = bytes32(b"1" * 32) - v2_qualities = harvester_protocol.V2Qualities( + v2_quality_chains = harvester_protocol.V2QualityChains( challenge_hash=bytes32(b"2" * 32), sp_hash=sp_hash, plot_identifier="test_plot_id", @@ -348,7 +348,7 @@ async def test_v2_qualities_new_sp_hash( ) harvester_peer = await get_harvester_peer(farmer) - await farmer_api.v2_qualities(v2_qualities, harvester_peer) + await farmer_api.v2_quality_chains(v2_quality_chains, harvester_peer) assert sp_hash in farmer.number_of_responses assert farmer.number_of_responses[sp_hash] == 0 @@ -356,7 +356,7 @@ async def test_v2_qualities_new_sp_hash( @pytest.mark.anyio -async def test_v2_qualities_missing_sp_hash( +async def test_v2_quality_chains_missing_sp_hash( caplog: pytest.LogCaptureFixture, farmer_one_harvester_solver: tuple[list[HarvesterService], FarmerService, SolverService, BlockTools], ) -> None: @@ -364,7 +364,7 @@ async def test_v2_qualities_missing_sp_hash( farmer_api = farmer_service._api sp_hash = bytes32(b"1" * 32) - v2_qualities = harvester_protocol.V2Qualities( + v2_quality_chains = harvester_protocol.V2QualityChains( challenge_hash=bytes32(b"2" * 32), sp_hash=sp_hash, plot_identifier="test_plot_id", @@ -378,13 +378,13 @@ async def test_v2_qualities_missing_sp_hash( ) harvester_peer = await get_harvester_peer(farmer_api.farmer) - await farmer_api.v2_qualities(v2_qualities, harvester_peer) + await farmer_api.v2_quality_chains(v2_quality_chains, harvester_peer) assert f"Received V2 quality collection for a signage point that we do not have {sp_hash}" in caplog.text @pytest.mark.anyio -async def test_v2_qualities_with_existing_sp( +async def test_v2_quality_chains_with_existing_sp( farmer_one_harvester_solver: tuple[list[HarvesterService], FarmerService, SolverService, BlockTools], ) -> None: _, farmer_service, _, _ = farmer_one_harvester_solver @@ -407,7 +407,7 @@ async def test_v2_qualities_with_existing_sp( farmer.sps[sp_hash] = [sp] - v2_qualities = harvester_protocol.V2Qualities( + v2_quality_chains = harvester_protocol.V2QualityChains( challenge_hash=challenge_hash, sp_hash=sp_hash, plot_identifier="test_plot_id", @@ -421,7 +421,7 @@ async def test_v2_qualities_with_existing_sp( ) harvester_peer = await get_harvester_peer(farmer) - await farmer_api.v2_qualities(v2_qualities, harvester_peer) + await farmer_api.v2_quality_chains(v2_quality_chains, harvester_peer) # should store 2 pending requests (one per quality) assert len(farmer.pending_solver_requests) == 2 @@ -441,7 +441,7 @@ async def test_solution_response_handler( sp_hash = bytes32(b"1" * 32) challenge_hash = bytes32(b"2" * 32) - v2_qualities = harvester_protocol.V2Qualities( + v2_quality_chains = harvester_protocol.V2QualityChains( challenge_hash=challenge_hash, sp_hash=sp_hash, plot_identifier="test_plot_id", @@ -458,7 +458,7 @@ async def test_solution_response_handler( # manually add pending request farmer.pending_solver_requests[quality] = { - "quality_data": v2_qualities, + "quality_data": v2_quality_chains, "peer": harvester_peer, } @@ -509,7 +509,7 @@ async def test_solution_response_unknown_quality( async def test_solution_response_empty_proof( farmer_one_harvester_solver: tuple[list[HarvesterService], FarmerService, SolverService, BlockTools], ) -> None: - """Test solution_response with empty proof (line 555-556).""" + """Test solution_response with empty proof.""" _, farmer_service, _solver_service, _ = farmer_one_harvester_solver farmer_api = farmer_service._api farmer = farmer_api.farmer @@ -519,7 +519,7 @@ async def test_solution_response_empty_proof( sp_hash = bytes32(b"1" * 32) challenge_hash = bytes32(b"2" * 32) - v2_qualities = harvester_protocol.V2Qualities( + v2_quality_chains = harvester_protocol.V2QualityChains( challenge_hash=challenge_hash, sp_hash=sp_hash, plot_identifier="test_plot_id", @@ -537,7 +537,7 @@ async def test_solution_response_empty_proof( # manually add pending request farmer.pending_solver_requests[quality] = { - "quality_data": v2_qualities, + "quality_data": v2_quality_chains, "peer": harvester_peer, } @@ -558,10 +558,10 @@ async def test_solution_response_empty_proof( @pytest.mark.anyio -async def test_v2_qualities_solver_exception( +async def test_v2_quality_chains_solver_exception( farmer_one_harvester_solver: tuple[list[HarvesterService], FarmerService, SolverService, BlockTools], ) -> None: - """Test v2_qualities with solver service exception (lines 526-527, 529-530).""" + """Test v2_quality_chains with solver service exception.""" _, farmer_service, _solver_service, _ = farmer_one_harvester_solver farmer_api = farmer_service._api farmer = farmer_api.farmer @@ -582,7 +582,7 @@ async def test_v2_qualities_solver_exception( farmer.sps[sp_hash] = [sp] - v2_qualities = harvester_protocol.V2Qualities( + v2_quality_chains = harvester_protocol.V2QualityChains( challenge_hash=challenge_hash, sp_hash=sp_hash, plot_identifier="test_plot_id", @@ -599,7 +599,7 @@ async def test_v2_qualities_solver_exception( # Mock send_to_all to raise an exception with unittest.mock.patch.object(farmer.server, "send_to_all", side_effect=Exception("Solver connection failed")): - await farmer_api.v2_qualities(v2_qualities, harvester_peer) + await farmer_api.v2_quality_chains(v2_quality_chains, harvester_peer) # verify pending request was cleaned up after exception quality = bytes32(b"3" * 32) diff --git a/chia/_tests/util/test_network_protocol_test.py b/chia/_tests/util/test_network_protocol_test.py index 1acfe6b86791..1f37f49397ba 100644 --- a/chia/_tests/util/test_network_protocol_test.py +++ b/chia/_tests/util/test_network_protocol_test.py @@ -189,7 +189,7 @@ def test_missing_messages() -> None: "RequestSignatures", "RespondPlots", "RespondSignatures", - "V2Qualities", + "V2QualityChains", } introducer_msgs = {"RequestPeersIntroducer", "RespondPeersIntroducer"} diff --git a/chia/farmer/farmer_api.py b/chia/farmer/farmer_api.py index 8eb61c366e70..9c7728845bbc 100644 --- a/chia/farmer/farmer_api.py +++ b/chia/farmer/farmer_api.py @@ -24,7 +24,7 @@ PoolDifficulty, SignatureRequestSourceData, SigningDataKind, - V2Qualities, + V2QualityChains, ) from chia.protocols.outbound_message import Message, NodeType, make_msg from chia.protocols.pool_protocol import ( @@ -481,10 +481,10 @@ async def new_proof_of_space( return @metadata.request(peer_required=True) - async def v2_qualities(self, quality_data: V2Qualities, peer: WSChiaConnection) -> None: + async def v2_quality_chains(self, quality_data: V2QualityChains, peer: WSChiaConnection) -> None: """ - This is a response from the harvester for V2 plots, containing only qualities. - We store these qualities and will later use solver service to generate proofs when needed. + This is a response from the harvester for V2 plots, containing only quality chains (partial proof bytes). + We send these to the solver service and wait for a response with the full proof. """ if quality_data.sp_hash not in self.farmer.number_of_responses: self.farmer.number_of_responses[quality_data.sp_hash] = 0 diff --git a/chia/harvester/harvester_api.py b/chia/harvester/harvester_api.py index a09c6f084572..7dcbbb2023f2 100644 --- a/chia/harvester/harvester_api.py +++ b/chia/harvester/harvester_api.py @@ -19,7 +19,7 @@ from chia.plotting.util import PlotInfo, parse_plot_info from chia.protocols import harvester_protocol from chia.protocols.farmer_protocol import FarmingInfo -from chia.protocols.harvester_protocol import Plot, PlotSyncResponse, V2Qualities +from chia.protocols.harvester_protocol import Plot, PlotSyncResponse, V2QualityChains from chia.protocols.outbound_message import Message, make_msg from chia.protocols.protocol_message_types import ProtocolMessageTypes from chia.server.api_protocol import ApiMetadata @@ -98,8 +98,8 @@ async def new_signage_point_harvester( loop = asyncio.get_running_loop() - def blocking_lookup_v2_qualities(filename: Path, plot_info: PlotInfo) -> Optional[V2Qualities]: - # Uses the V2 Prover object to lookup qualities only. No full proofs generated. + def blocking_lookup_v2_quality_chains(filename: Path, plot_info: PlotInfo) -> Optional[V2QualityChains]: + # Uses the V2 Prover object to lookup qualitie_chains (partial proofs). try: plot_id = plot_info.prover.get_id() sp_challenge_hash = calculate_pos_challenge( @@ -128,7 +128,7 @@ def blocking_lookup_v2_qualities(filename: Path, plot_info: PlotInfo) -> Optiona if len(quality_chains) > 0: size = plot_info.prover.get_size().size_v2 assert size is not None - return V2Qualities( + return V2QualityChains( new_challenge.challenge_hash, new_challenge.sp_hash, quality_chains[0].hex() + str(filename.resolve()), @@ -263,14 +263,6 @@ def blocking_lookup(filename: Path, plot_info: PlotInfo) -> list[tuple[bytes32, self.harvester.log.error(f"Unknown error: {e}") return [] - async def lookup_v2_qualities(filename: Path, plot_info: PlotInfo) -> tuple[Path, Optional[V2Qualities]]: - # Executes V2 quality lookup in a thread pool - if self.harvester._shut_down: - return filename, None - qualities: Optional[V2Qualities] = await loop.run_in_executor( - self.harvester.executor, blocking_lookup_v2_qualities, filename, plot_info - ) - return filename, qualities async def lookup_challenge( filename: Path, plot_info: PlotInfo @@ -309,7 +301,11 @@ async def lookup_challenge( total += 1 if try_plot_info.prover.get_version() == PlotVersion.V2: # TODO: todo_v2_plots need to check v2 filter - v2_awaitables.append(lookup_v2_qualities(try_plot_filename, try_plot_info)) + v2_awaitables.append( + loop.run_in_executor( + self.harvester.executor, blocking_lookup_v2_quality_chains, try_plot_filename, try_plot_info + ) + ) passed += 1 else: filter_prefix_bits = calculate_prefix_bits( @@ -330,7 +326,7 @@ async def lookup_challenge( # Concurrently executes all lookups on disk, to take advantage of multiple disk parallelism time_taken = time.monotonic() - start total_proofs_found = 0 - total_v2_qualities_found = 0 + total_v2_quality_chains_found = 0 # Process V1 plot responses (existing flow) for filename_sublist_awaitable in asyncio.as_completed(awaitables): @@ -350,17 +346,11 @@ async def lookup_challenge( await peer.send_message(msg) # Process V2 plot quality collections (new flow) - for filename_quality_awaitable in asyncio.as_completed(v2_awaitables): - filename, v2_qualities = await filename_quality_awaitable - time_taken = time.monotonic() - start - if time_taken > 8: - self.harvester.log.warning( - f"Looking up V2 qualities on {filename} took: {time_taken}. This should be below 8 seconds" - f" to minimize risk of losing rewards." - ) - if v2_qualities is not None: - total_v2_qualities_found += len(v2_qualities.quality_chains) - msg = make_msg(ProtocolMessageTypes.v2_qualities, v2_qualities) + for quality_awaitable in asyncio.as_completed(v2_awaitables): + v2_quality_chains = await quality_awaitable + if v2_quality_chains is not None: + total_v2_quality_chains_found += len(v2_quality_chains.quality_chains) + msg = make_msg(ProtocolMessageTypes.v2_quality_chains, v2_quality_chains) await peer.send_message(msg) now = uint64(time.time()) @@ -380,7 +370,7 @@ async def lookup_challenge( self.harvester.log.info( f"challenge_hash: {new_challenge.challenge_hash.hex()[:10]} ..." f"{len(awaitables) + len(v2_awaitables)} plots were eligible for farming challenge" - f"Found {total_proofs_found} V1 proofs and {total_v2_qualities_found} V2 qualities." + f"Found {total_proofs_found} V1 proofs and {total_v2_quality_chains_found} V2 qualities." f" Time: {time_taken:.5f} s. Total {self.harvester.plot_manager.plot_count()} plots" ) self.harvester.state_changed( @@ -389,7 +379,7 @@ async def lookup_challenge( "challenge_hash": new_challenge.challenge_hash.hex(), "total_plots": self.harvester.plot_manager.plot_count(), "found_proofs": total_proofs_found, - "found_v2_qualities": total_v2_qualities_found, + "found_v2_quality_chains": total_v2_quality_chains_found, "eligible_plots": len(awaitables) + len(v2_awaitables), "time": time_taken, }, diff --git a/chia/protocols/harvester_protocol.py b/chia/protocols/harvester_protocol.py index dbaac29517e6..4df9c4275089 100644 --- a/chia/protocols/harvester_protocol.py +++ b/chia/protocols/harvester_protocol.py @@ -65,7 +65,7 @@ class NewProofOfSpace(Streamable): @streamable @dataclass(frozen=True) -class V2Qualities(Streamable): +class V2QualityChains(Streamable): challenge_hash: bytes32 sp_hash: bytes32 plot_identifier: str diff --git a/chia/protocols/protocol_message_types.py b/chia/protocols/protocol_message_types.py index b5971da8a8a5..16be50ebda05 100644 --- a/chia/protocols/protocol_message_types.py +++ b/chia/protocols/protocol_message_types.py @@ -13,7 +13,7 @@ class ProtocolMessageTypes(Enum): new_proof_of_space = 5 request_signatures = 6 respond_signatures = 7 - v2_qualities = 110 + v2_quality_chains = 110 # Farmer protocol (farmer <-> full_node) new_signage_point = 8 From cbe39bb6158d671b303e6bc7a65c9880a0417664 Mon Sep 17 00:00:00 2001 From: almogdepaz Date: Mon, 18 Aug 2025 19:01:42 +0300 Subject: [PATCH 27/42] lint --- chia/harvester/harvester_api.py | 1 - 1 file changed, 1 deletion(-) diff --git a/chia/harvester/harvester_api.py b/chia/harvester/harvester_api.py index 7dcbbb2023f2..6c65b8b139cb 100644 --- a/chia/harvester/harvester_api.py +++ b/chia/harvester/harvester_api.py @@ -263,7 +263,6 @@ def blocking_lookup(filename: Path, plot_info: PlotInfo) -> list[tuple[bytes32, self.harvester.log.error(f"Unknown error: {e}") return [] - async def lookup_challenge( filename: Path, plot_info: PlotInfo ) -> tuple[Path, list[harvester_protocol.NewProofOfSpace]]: From e9718c943c82f2879fb0a96daaa5ce0723a063ee Mon Sep 17 00:00:00 2001 From: almogdepaz Date: Tue, 19 Aug 2025 10:47:21 +0300 Subject: [PATCH 28/42] rename to partial proof --- .../farmer_harvester/test_farmer_harvester.py | 54 +++++++------- chia/_tests/solver/test_solver_service.py | 4 +- chia/consensus/pos_quality.py | 6 ++ chia/farmer/farmer_api.py | 70 +++++++++---------- chia/harvester/harvester_api.py | 58 ++++++++++----- chia/plotting/prover.py | 6 +- chia/protocols/harvester_protocol.py | 4 +- chia/protocols/protocol_message_types.py | 2 +- chia/protocols/solver_protocol.py | 4 +- chia/solver/solver.py | 2 +- chia/solver/solver_api.py | 8 +-- chia/solver/solver_rpc_api.py | 4 +- chia/solver/solver_rpc_client.py | 2 +- 13 files changed, 124 insertions(+), 100 deletions(-) diff --git a/chia/_tests/farmer_harvester/test_farmer_harvester.py b/chia/_tests/farmer_harvester/test_farmer_harvester.py index 6a4ac91a85a8..ad5af0ca1442 100644 --- a/chia/_tests/farmer_harvester/test_farmer_harvester.py +++ b/chia/_tests/farmer_harvester/test_farmer_harvester.py @@ -326,7 +326,7 @@ async def test_harvester_has_no_server( @pytest.mark.anyio -async def test_v2_quality_chains_new_sp_hash( +async def test_v2_partial_proofs_new_sp_hash( farmer_one_harvester_solver: tuple[list[HarvesterService], FarmerService, SolverService, BlockTools], ) -> None: _, farmer_service, _solver_service, _bt = farmer_one_harvester_solver @@ -334,11 +334,11 @@ async def test_v2_quality_chains_new_sp_hash( farmer = farmer_api.farmer sp_hash = bytes32(b"1" * 32) - v2_quality_chains = harvester_protocol.V2QualityChains( + partial_proofs = harvester_protocol.PartialProofsData( challenge_hash=bytes32(b"2" * 32), sp_hash=sp_hash, plot_identifier="test_plot_id", - quality_chains=[b"test_quality_chain_1"], + partial_proofs=[b"test_partial_proof_1"], signage_point_index=uint8(0), plot_size=uint8(32), difficulty=uint64(1000), @@ -348,7 +348,7 @@ async def test_v2_quality_chains_new_sp_hash( ) harvester_peer = await get_harvester_peer(farmer) - await farmer_api.v2_quality_chains(v2_quality_chains, harvester_peer) + await farmer_api.partial_proofs(partial_proofs, harvester_peer) assert sp_hash in farmer.number_of_responses assert farmer.number_of_responses[sp_hash] == 0 @@ -356,7 +356,7 @@ async def test_v2_quality_chains_new_sp_hash( @pytest.mark.anyio -async def test_v2_quality_chains_missing_sp_hash( +async def test_v2_partial_proofs_missing_sp_hash( caplog: pytest.LogCaptureFixture, farmer_one_harvester_solver: tuple[list[HarvesterService], FarmerService, SolverService, BlockTools], ) -> None: @@ -364,11 +364,11 @@ async def test_v2_quality_chains_missing_sp_hash( farmer_api = farmer_service._api sp_hash = bytes32(b"1" * 32) - v2_quality_chains = harvester_protocol.V2QualityChains( + partial_proofs = harvester_protocol.PartialProofsData( challenge_hash=bytes32(b"2" * 32), sp_hash=sp_hash, plot_identifier="test_plot_id", - quality_chains=[b"test_quality_chain_1"], + partial_proofs=[b"test_partial_proof_1"], signage_point_index=uint8(0), plot_size=uint8(32), difficulty=uint64(1000), @@ -378,13 +378,13 @@ async def test_v2_quality_chains_missing_sp_hash( ) harvester_peer = await get_harvester_peer(farmer_api.farmer) - await farmer_api.v2_quality_chains(v2_quality_chains, harvester_peer) + await farmer_api.partial_proofs(partial_proofs, harvester_peer) - assert f"Received V2 quality collection for a signage point that we do not have {sp_hash}" in caplog.text + assert f"Received partial proofs for a signage point that we do not have {sp_hash}" in caplog.text @pytest.mark.anyio -async def test_v2_quality_chains_with_existing_sp( +async def test_v2_partial_proofs_with_existing_sp( farmer_one_harvester_solver: tuple[list[HarvesterService], FarmerService, SolverService, BlockTools], ) -> None: _, farmer_service, _, _ = farmer_one_harvester_solver @@ -407,11 +407,11 @@ async def test_v2_quality_chains_with_existing_sp( farmer.sps[sp_hash] = [sp] - v2_quality_chains = harvester_protocol.V2QualityChains( + partial_proofs = harvester_protocol.PartialProofsData( challenge_hash=challenge_hash, sp_hash=sp_hash, plot_identifier="test_plot_id", - quality_chains=[b"test_quality_chain_1", b"test_quality_chain_2"], + partial_proofs=[b"test_partial_proof_1", b"test_partial_proof_2"], signage_point_index=uint8(0), plot_size=uint8(32), difficulty=uint64(1000), @@ -421,7 +421,7 @@ async def test_v2_quality_chains_with_existing_sp( ) harvester_peer = await get_harvester_peer(farmer) - await farmer_api.v2_quality_chains(v2_quality_chains, harvester_peer) + await farmer_api.partial_proofs(partial_proofs, harvester_peer) # should store 2 pending requests (one per quality) assert len(farmer.pending_solver_requests) == 2 @@ -441,11 +441,11 @@ async def test_solution_response_handler( sp_hash = bytes32(b"1" * 32) challenge_hash = bytes32(b"2" * 32) - v2_quality_chains = harvester_protocol.V2QualityChains( + partial_proofs = harvester_protocol.PartialProofsData( challenge_hash=challenge_hash, sp_hash=sp_hash, plot_identifier="test_plot_id", - quality_chains=[b"test_quality_chain_for_quality"], + partial_proofs=[b"test_partial_proof_for_quality"], signage_point_index=uint8(0), plot_size=uint8(32), difficulty=uint64(1000), @@ -458,12 +458,12 @@ async def test_solution_response_handler( # manually add pending request farmer.pending_solver_requests[quality] = { - "quality_data": v2_quality_chains, + "proof_data": partial_proofs, "peer": harvester_peer, } # create solution response - solution_response = solver_protocol.SolverResponse(quality_chain=quality, proof=b"test_proof_from_solver") + solution_response = solver_protocol.SolverResponse(partial_proof=quality, proof=b"test_proof_from_solver") solver_peer = Mock() solver_peer.peer_node_id = "solver_peer" @@ -495,7 +495,7 @@ async def test_solution_response_unknown_quality( solver_peer = await get_solver_peer(farmer) # create solution response with unknown quality - solution_response = solver_protocol.SolverResponse(quality_chain=bytes(b"1" * 32), proof=b"test_proof") + solution_response = solver_protocol.SolverResponse(partial_proof=bytes(b"1" * 32), proof=b"test_proof") with unittest.mock.patch.object(farmer_api, "new_proof_of_space", new_callable=AsyncMock) as mock_new_proof: await farmer_api.solution_response(solution_response, solver_peer) @@ -509,7 +509,6 @@ async def test_solution_response_unknown_quality( async def test_solution_response_empty_proof( farmer_one_harvester_solver: tuple[list[HarvesterService], FarmerService, SolverService, BlockTools], ) -> None: - """Test solution_response with empty proof.""" _, farmer_service, _solver_service, _ = farmer_one_harvester_solver farmer_api = farmer_service._api farmer = farmer_api.farmer @@ -519,11 +518,11 @@ async def test_solution_response_empty_proof( sp_hash = bytes32(b"1" * 32) challenge_hash = bytes32(b"2" * 32) - v2_quality_chains = harvester_protocol.V2QualityChains( + partial_proofs = harvester_protocol.PartialProofsData( challenge_hash=challenge_hash, sp_hash=sp_hash, plot_identifier="test_plot_id", - quality_chains=[b"test_quality_chain_for_quality"], + partial_proofs=[b"test_partial_proof_for_quality"], signage_point_index=uint8(0), plot_size=uint8(32), difficulty=uint64(1000), @@ -537,7 +536,7 @@ async def test_solution_response_empty_proof( # manually add pending request farmer.pending_solver_requests[quality] = { - "quality_data": v2_quality_chains, + "proof_data": partial_proofs, "peer": harvester_peer, } @@ -545,7 +544,7 @@ async def test_solution_response_empty_proof( solver_peer = await get_solver_peer(farmer) # create solution response with empty proof - solution_response = solver_protocol.SolverResponse(quality_chain=quality, proof=b"") + solution_response = solver_protocol.SolverResponse(partial_proof=quality, proof=b"") with unittest.mock.patch.object(farmer_api, "new_proof_of_space", new_callable=AsyncMock) as mock_new_proof: await farmer_api.solution_response(solution_response, solver_peer) @@ -558,10 +557,9 @@ async def test_solution_response_empty_proof( @pytest.mark.anyio -async def test_v2_quality_chains_solver_exception( +async def test_v2_partial_proofs_solver_exception( farmer_one_harvester_solver: tuple[list[HarvesterService], FarmerService, SolverService, BlockTools], ) -> None: - """Test v2_quality_chains with solver service exception.""" _, farmer_service, _solver_service, _ = farmer_one_harvester_solver farmer_api = farmer_service._api farmer = farmer_api.farmer @@ -582,11 +580,11 @@ async def test_v2_quality_chains_solver_exception( farmer.sps[sp_hash] = [sp] - v2_quality_chains = harvester_protocol.V2QualityChains( + partial_proofs = harvester_protocol.PartialProofsData( challenge_hash=challenge_hash, sp_hash=sp_hash, plot_identifier="test_plot_id", - quality_chains=[b"test_quality_chain_1"], + partial_proofs=[b"test_partial_proof_1"], signage_point_index=uint8(0), plot_size=uint8(32), difficulty=uint64(1000), @@ -599,7 +597,7 @@ async def test_v2_quality_chains_solver_exception( # Mock send_to_all to raise an exception with unittest.mock.patch.object(farmer.server, "send_to_all", side_effect=Exception("Solver connection failed")): - await farmer_api.v2_quality_chains(v2_quality_chains, harvester_peer) + await farmer_api.partial_proofs(partial_proofs, harvester_peer) # verify pending request was cleaned up after exception quality = bytes32(b"3" * 32) diff --git a/chia/_tests/solver/test_solver_service.py b/chia/_tests/solver/test_solver_service.py index 06f7733fec14..75a6eb39afd4 100644 --- a/chia/_tests/solver/test_solver_service.py +++ b/chia/_tests/solver/test_solver_service.py @@ -23,7 +23,7 @@ async def test_solver_api_methods(blockchain_constants: ConsensusConstants, tmp_ solver = solver_service._node solver_api = solver_service._api assert solver_api.ready() is True - test_info = SolverInfo(plot_difficulty=uint64(1500), quality_chain=b"test_quality_chain_42") + test_info = SolverInfo(plot_difficulty=uint64(1500), partial_proof=b"test_partial_proof_42") expected_proof = b"test_proof_data_12345" with patch.object(solver, "solve", return_value=expected_proof): api_result = await solver_api.solve(test_info) @@ -49,7 +49,7 @@ async def test_solver_error_handling( pass # expected # test solver handles exception in solve method solver = solver_service._node - test_info = SolverInfo(plot_difficulty=uint64(1000), quality_chain=b"test_quality_chain_zeros") + test_info = SolverInfo(plot_difficulty=uint64(1000), partial_proof=b"test_partial_proof_zeros") with patch.object(solver, "solve", side_effect=RuntimeError("test error")): # solver api should handle exceptions gracefully result = await solver_service._api.solve(test_info) diff --git a/chia/consensus/pos_quality.py b/chia/consensus/pos_quality.py index 5d7e78db7e49..2213b7d1ce6c 100644 --- a/chia/consensus/pos_quality.py +++ b/chia/consensus/pos_quality.py @@ -1,6 +1,7 @@ from __future__ import annotations from chia_rs import PlotSize +from chia_rs.sized_bytes import bytes32 from chia_rs.sized_ints import uint64 # The actual space in bytes of a plot, is _expected_plot_size(k) * UI_ACTUAL_SPACE_CONSTANT_FACTO @@ -15,6 +16,11 @@ } +def quality_for_partial_proof(partial_proof: bytes, challenge: bytes32) -> bytes32: + # TODO todo_v2_plots real implementaion + return challenge + + def _expected_plot_size(size: PlotSize) -> uint64: """ Given the plot size parameter k (which is between 32 and 59), computes the diff --git a/chia/farmer/farmer_api.py b/chia/farmer/farmer_api.py index 9c7728845bbc..e2c057127be0 100644 --- a/chia/farmer/farmer_api.py +++ b/chia/farmer/farmer_api.py @@ -17,6 +17,7 @@ from chia.protocols import farmer_protocol, harvester_protocol from chia.protocols.farmer_protocol import DeclareProofOfSpace, SignedValues from chia.protocols.harvester_protocol import ( + PartialProofsData, PlotSyncDone, PlotSyncPathList, PlotSyncPlotList, @@ -24,7 +25,6 @@ PoolDifficulty, SignatureRequestSourceData, SigningDataKind, - V2QualityChains, ) from chia.protocols.outbound_message import Message, NodeType, make_msg from chia.protocols.pool_protocol import ( @@ -481,52 +481,52 @@ async def new_proof_of_space( return @metadata.request(peer_required=True) - async def v2_quality_chains(self, quality_data: V2QualityChains, peer: WSChiaConnection) -> None: + async def partial_proofs(self, partial_proof_data: PartialProofsData, peer: WSChiaConnection) -> None: """ - This is a response from the harvester for V2 plots, containing only quality chains (partial proof bytes). + This is a response from the harvester for V2 plots, containing only partial proof data. We send these to the solver service and wait for a response with the full proof. """ - if quality_data.sp_hash not in self.farmer.number_of_responses: - self.farmer.number_of_responses[quality_data.sp_hash] = 0 - self.farmer.cache_add_time[quality_data.sp_hash] = uint64(time.time()) + if partial_proof_data.sp_hash not in self.farmer.number_of_responses: + self.farmer.number_of_responses[partial_proof_data.sp_hash] = 0 + self.farmer.cache_add_time[partial_proof_data.sp_hash] = uint64(time.time()) - if quality_data.sp_hash not in self.farmer.sps: + if partial_proof_data.sp_hash not in self.farmer.sps: self.farmer.log.warning( - f"Received V2 quality collection for a signage point that we do not have {quality_data.sp_hash}" + f"Received partial proofs for a signage point that we do not have {partial_proof_data.sp_hash}" ) return None - self.farmer.cache_add_time[quality_data.sp_hash] = uint64(time.time()) + self.farmer.cache_add_time[partial_proof_data.sp_hash] = uint64(time.time()) self.farmer.log.info( - f"Received V2 quality collection with {len(quality_data.quality_chains)} quality chains " - f"for plot {quality_data.plot_identifier[:10]}... from {peer.peer_node_id}" + f"Received V2 partial proof collection with {len(partial_proof_data.partial_proofs)} partail proofs " + f"for plot {partial_proof_data.plot_identifier[:10]}... from {peer.peer_node_id}" ) # Process each quality chain through solver service to get full proofs - for quality_chain in quality_data.quality_chains: + for partial_proof in partial_proof_data.partial_proofs: solver_info = SolverInfo( - plot_difficulty=quality_data.difficulty, - quality_chain=quality_chain, + plot_difficulty=partial_proof_data.difficulty, + partial_proof=partial_proof, ) try: # store pending request data for matching with response - self.farmer.pending_solver_requests[quality_chain] = { - "quality_data": quality_data, + self.farmer.pending_solver_requests[partial_proof] = { + "proof_data": partial_proof_data, "peer": peer, } # send solve request to all solver connections msg = make_msg(ProtocolMessageTypes.solve, solver_info) await self.farmer.server.send_to_all([msg], NodeType.SOLVER) - self.farmer.log.debug(f"Sent solve request for quality {quality_chain.hex()[:10]}...") + self.farmer.log.debug(f"Sent solve request for quality {partial_proof.hex()[:10]}...") except Exception as e: - self.farmer.log.error(f"Failed to call solver service for quality {quality_chain.hex()[:10]}...: {e}") + self.farmer.log.error(f"Failed to call solver service for quality {partial_proof.hex()[:10]}...: {e}") # clean up pending request - if quality_chain in self.farmer.pending_solver_requests: - del self.farmer.pending_solver_requests[quality_chain] + if partial_proof in self.farmer.pending_solver_requests: + del self.farmer.pending_solver_requests[partial_proof] @metadata.request() async def solution_response(self, response: SolverResponse, peer: WSChiaConnection) -> None: @@ -538,36 +538,36 @@ async def solution_response(self, response: SolverResponse, peer: WSChiaConnecti # find the matching pending request using quality_string - if response.quality_chain not in self.farmer.pending_solver_requests: - self.farmer.log.warning(f"Received solver response for unknown quality {response.quality_chain.hex()}") + if response.partial_proof not in self.farmer.pending_solver_requests: + self.farmer.log.warning(f"Received solver response for unknown quality {response.partial_proof.hex()}") return # get the original request data - request_data = self.farmer.pending_solver_requests.pop(response.quality_chain) - quality_data = request_data["quality_data"] + request_data = self.farmer.pending_solver_requests.pop(response.partial_proof) + proof_data = request_data["proof_data"] original_peer = request_data["peer"] - quality = response.quality_chain + quality = response.partial_proof # create the proof of space with the solver's proof proof_bytes = response.proof if proof_bytes is None or len(proof_bytes) == 0: - self.farmer.log.warning(f"Received empty proof from solver for quality {quality.hex()}...") + self.farmer.log.warning(f"Received empty proof from solver for proof {quality.hex()}...") return - sp_challenge_hash = quality_data.challenge_hash + sp_challenge_hash = proof_data.challenge_hash new_proof_of_space = harvester_protocol.NewProofOfSpace( - quality_data.challenge_hash, - quality_data.sp_hash, - quality_data.plot_identifier, + proof_data.challenge_hash, + proof_data.sp_hash, + proof_data.plot_identifier, ProofOfSpace( sp_challenge_hash, - quality_data.pool_public_key, - quality_data.pool_contract_puzzle_hash, - quality_data.plot_public_key, - quality_data.plot_size, + proof_data.pool_public_key, + proof_data.pool_contract_puzzle_hash, + proof_data.plot_public_key, + proof_data.plot_size, proof_bytes, ), - quality_data.signage_point_index, + proof_data.signage_point_index, include_source_signature_data=False, farmer_reward_address_override=None, fee_info=None, diff --git a/chia/harvester/harvester_api.py b/chia/harvester/harvester_api.py index 6c65b8b139cb..82e94d2794fa 100644 --- a/chia/harvester/harvester_api.py +++ b/chia/harvester/harvester_api.py @@ -10,6 +10,7 @@ from chia_rs.sized_bytes import bytes32 from chia_rs.sized_ints import uint32, uint64 +from chia.consensus.pos_quality import quality_for_partial_proof from chia.consensus.pot_iterations import ( calculate_iterations_quality, calculate_sp_interval_iters, @@ -19,7 +20,7 @@ from chia.plotting.util import PlotInfo, parse_plot_info from chia.protocols import harvester_protocol from chia.protocols.farmer_protocol import FarmingInfo -from chia.protocols.harvester_protocol import Plot, PlotSyncResponse, V2QualityChains +from chia.protocols.harvester_protocol import PartialProofsData, Plot, PlotSyncResponse from chia.protocols.outbound_message import Message, make_msg from chia.protocols.protocol_message_types import ProtocolMessageTypes from chia.server.api_protocol import ApiMetadata @@ -98,8 +99,8 @@ async def new_signage_point_harvester( loop = asyncio.get_running_loop() - def blocking_lookup_v2_quality_chains(filename: Path, plot_info: PlotInfo) -> Optional[V2QualityChains]: - # Uses the V2 Prover object to lookup qualitie_chains (partial proofs). + def blocking_lookup_v2_partial_proofs(filename: Path, plot_info: PlotInfo) -> Optional[PartialProofsData]: + # Uses the V2 Prover object to lookup qualities only. No full proofs generated. try: plot_id = plot_info.prover.get_id() sp_challenge_hash = calculate_pos_challenge( @@ -108,31 +109,50 @@ def blocking_lookup_v2_quality_chains(filename: Path, plot_info: PlotInfo) -> Op new_challenge.sp_hash, ) try: - quality_chains = plot_info.prover.get_quality_chains_for_challenge(sp_challenge_hash) + partial_proofs = plot_info.prover.get_partial_proofs_for_challenge(sp_challenge_hash) except Exception as e: - self.harvester.log.error(f"Exception fetching quality chains for V2 plot {filename}. {e}") + self.harvester.log.error(f"Exception fetching partial proof for V2 plot {filename}. {e}") return None - if quality_chains is not None and len(quality_chains) > 0: + if partial_proofs is not None and len(partial_proofs) > 0: # Get the appropriate difficulty for this plot difficulty = new_challenge.difficulty + sub_slot_iters = new_challenge.sub_slot_iters if plot_info.pool_contract_puzzle_hash is not None: # Check for pool-specific difficulty for pool_difficulty in new_challenge.pool_difficulties: if pool_difficulty.pool_contract_puzzle_hash == plot_info.pool_contract_puzzle_hash: difficulty = pool_difficulty.difficulty + sub_slot_iters = pool_difficulty.sub_slot_iters break - # Filter quality chains that pass the required_iters check + # Filter qualities that pass the required_iters check (same as V1 flow) + good_qualities = [] + sp_interval_iters = calculate_sp_interval_iters(self.harvester.constants, sub_slot_iters) + + for quality in partial_proofs: + quality_str = quality_for_partial_proof(quality, new_challenge.challenge_hash) + required_iters: uint64 = calculate_iterations_quality( + self.harvester.constants, + quality_str, + plot_info.prover.get_size(), # TODO: todo_v2_plots update for V2 + difficulty, + new_challenge.sp_hash, + sub_slot_iters, + new_challenge.last_tx_height, + ) + + if required_iters < sp_interval_iters: + good_qualities.append(quality) - if len(quality_chains) > 0: + if len(good_qualities) > 0: size = plot_info.prover.get_size().size_v2 assert size is not None - return V2QualityChains( + return PartialProofsData( new_challenge.challenge_hash, new_challenge.sp_hash, - quality_chains[0].hex() + str(filename.resolve()), - quality_chains, + good_qualities[0].hex() + str(filename.resolve()), + good_qualities, new_challenge.signage_point_index, size, difficulty, @@ -302,7 +322,7 @@ async def lookup_challenge( # TODO: todo_v2_plots need to check v2 filter v2_awaitables.append( loop.run_in_executor( - self.harvester.executor, blocking_lookup_v2_quality_chains, try_plot_filename, try_plot_info + self.harvester.executor, blocking_lookup_v2_partial_proofs, try_plot_filename, try_plot_info ) ) passed += 1 @@ -325,7 +345,7 @@ async def lookup_challenge( # Concurrently executes all lookups on disk, to take advantage of multiple disk parallelism time_taken = time.monotonic() - start total_proofs_found = 0 - total_v2_quality_chains_found = 0 + total_v2_partial_proofs_found = 0 # Process V1 plot responses (existing flow) for filename_sublist_awaitable in asyncio.as_completed(awaitables): @@ -346,10 +366,10 @@ async def lookup_challenge( # Process V2 plot quality collections (new flow) for quality_awaitable in asyncio.as_completed(v2_awaitables): - v2_quality_chains = await quality_awaitable - if v2_quality_chains is not None: - total_v2_quality_chains_found += len(v2_quality_chains.quality_chains) - msg = make_msg(ProtocolMessageTypes.v2_quality_chains, v2_quality_chains) + partial_proofs_data = await quality_awaitable + if partial_proofs_data is not None: + total_v2_partial_proofs_found += len(partial_proofs_data.partial_proofs) + msg = make_msg(ProtocolMessageTypes.partial_proofs, partial_proofs_data) await peer.send_message(msg) now = uint64(time.time()) @@ -369,7 +389,7 @@ async def lookup_challenge( self.harvester.log.info( f"challenge_hash: {new_challenge.challenge_hash.hex()[:10]} ..." f"{len(awaitables) + len(v2_awaitables)} plots were eligible for farming challenge" - f"Found {total_proofs_found} V1 proofs and {total_v2_quality_chains_found} V2 qualities." + f"Found {total_proofs_found} V1 proofs and {total_v2_partial_proofs_found} V2 qualities." f" Time: {time_taken:.5f} s. Total {self.harvester.plot_manager.plot_count()} plots" ) self.harvester.state_changed( @@ -378,7 +398,7 @@ async def lookup_challenge( "challenge_hash": new_challenge.challenge_hash.hex(), "total_plots": self.harvester.plot_manager.plot_count(), "found_proofs": total_proofs_found, - "found_v2_quality_chains": total_v2_quality_chains_found, + "found_v2_partial_proofs": total_v2_partial_proofs_found, "eligible_plots": len(awaitables) + len(v2_awaitables), "time": time_taken, }, diff --git a/chia/plotting/prover.py b/chia/plotting/prover.py index 991ab4c7240c..9ab3499720c9 100644 --- a/chia/plotting/prover.py +++ b/chia/plotting/prover.py @@ -28,7 +28,7 @@ def get_version(self) -> PlotVersion: ... def __bytes__(self) -> bytes: ... def get_id(self) -> bytes32: ... def get_qualities_for_challenge(self, challenge: bytes32) -> list[bytes32]: ... - def get_quality_chains_for_challenge(self, challenge: bytes) -> list[bytes]: ... + def get_partial_proofs_for_challenge(self, challenge: bytes) -> list[bytes]: ... def get_full_proof(self, challenge: bytes, index: int, parallel_read: bool = True) -> bytes: ... @classmethod @@ -74,7 +74,7 @@ def get_qualities_for_challenge(self, challenge: bytes) -> list[bytes32]: # TODO: todo_v2_plots Implement plot quality lookup raise NotImplementedError("V2 plot format is not yet implemented") - def get_quality_chains_for_challenge(self, challenge: bytes) -> list[bytes]: + def get_partial_proofs_for_challenge(self, challenge: bytes) -> list[bytes]: # TODO: todo_v2_plots Implement quality chain lookup (16 * k bits blobs) raise NotImplementedError("V2 plot format is not yet implemented") @@ -121,7 +121,7 @@ def get_id(self) -> bytes32: def get_qualities_for_challenge(self, challenge: bytes32) -> list[bytes32]: return [bytes32(quality) for quality in self._disk_prover.get_qualities_for_challenge(challenge)] - def get_quality_chains_for_challenge(self, challenge: bytes) -> list[bytes]: + def get_partial_proofs_for_challenge(self, challenge: bytes) -> list[bytes]: raise NotImplementedError("V1 does not implement quality chains, only qualities") def get_full_proof(self, challenge: bytes, index: int, parallel_read: bool = True) -> bytes: diff --git a/chia/protocols/harvester_protocol.py b/chia/protocols/harvester_protocol.py index 4df9c4275089..9552e8103736 100644 --- a/chia/protocols/harvester_protocol.py +++ b/chia/protocols/harvester_protocol.py @@ -65,11 +65,11 @@ class NewProofOfSpace(Streamable): @streamable @dataclass(frozen=True) -class V2QualityChains(Streamable): +class PartialProofsData(Streamable): challenge_hash: bytes32 sp_hash: bytes32 plot_identifier: str - quality_chains: list[bytes] # 16 * k bits blobs instead of 32-byte quality strings + partial_proofs: list[bytes] # 16 * k bits blobs instead of 32-byte quality strings signage_point_index: uint8 plot_size: uint8 difficulty: uint64 diff --git a/chia/protocols/protocol_message_types.py b/chia/protocols/protocol_message_types.py index 16be50ebda05..b3824dcffb2f 100644 --- a/chia/protocols/protocol_message_types.py +++ b/chia/protocols/protocol_message_types.py @@ -13,7 +13,7 @@ class ProtocolMessageTypes(Enum): new_proof_of_space = 5 request_signatures = 6 respond_signatures = 7 - v2_quality_chains = 110 + partial_proofs = 110 # Farmer protocol (farmer <-> full_node) new_signage_point = 8 diff --git a/chia/protocols/solver_protocol.py b/chia/protocols/solver_protocol.py index f09332271974..a3c1bb41b368 100644 --- a/chia/protocols/solver_protocol.py +++ b/chia/protocols/solver_protocol.py @@ -11,11 +11,11 @@ @dataclass(frozen=True) class SolverInfo(Streamable): plot_difficulty: uint64 - quality_chain: bytes # 16 * k bits blob, k (plot size) can be derived from this + partial_proof: bytes # 16 * k bits blob, k (plot size) can be derived from this @streamable @dataclass(frozen=True) class SolverResponse(Streamable): - quality_chain: bytes + partial_proof: bytes proof: bytes diff --git a/chia/solver/solver.py b/chia/solver/solver.py index ba84a617961b..6dae46d021e6 100644 --- a/chia/solver/solver.py +++ b/chia/solver/solver.py @@ -68,7 +68,7 @@ async def manage(self) -> AsyncIterator[None]: self.log.info("Solver service shutdown complete") def solve(self, info: SolverInfo) -> Optional[bytes]: - self.log.debug(f"Solve request: quality={info.quality_chain.hex()}") + self.log.debug(f"Solve request: quality={info.partial_proof.hex()}") # TODO todo_v2_plots implement actualy calling the solver return None diff --git a/chia/solver/solver_api.py b/chia/solver/solver_api.py index cf62dd2304fd..1de6ba57a7ff 100644 --- a/chia/solver/solver_api.py +++ b/chia/solver/solver_api.py @@ -40,20 +40,20 @@ async def solve( self.log.error("Solver is not started") return None - self.log.debug(f"Solving quality {request.quality_chain.hex()}with difficulty {request.plot_difficulty}") + self.log.debug(f"Solving quality {request.partial_proof.hex()}with difficulty {request.plot_difficulty}") try: proof = self.solver.solve(request) if proof is None: - self.log.warning(f"Solver returned no proof for quality {request.quality_chain.hex()}") + self.log.warning(f"Solver returned no proof for quality {request.partial_proof.hex()}") return None self.log.debug(f"Successfully solved quality, returning {len(proof)} byte proof") return make_msg( ProtocolMessageTypes.solution_response, - SolverResponse(proof=proof, quality_chain=request.quality_chain), + SolverResponse(proof=proof, partial_proof=request.partial_proof), ) except Exception as e: - self.log.error(f"Error solving quality {request.quality_chain.hex()}: {e}") + self.log.error(f"Error solving quality {request.partial_proof.hex()}: {e}") return None diff --git a/chia/solver/solver_rpc_api.py b/chia/solver/solver_rpc_api.py index d6570e700c48..c213ff4532cb 100644 --- a/chia/solver/solver_rpc_api.py +++ b/chia/solver/solver_rpc_api.py @@ -31,13 +31,13 @@ async def _state_changed(self, change: str, change_data: Optional[dict[str, Any] async def solve(self, request: dict[str, Any]) -> EndpointResult: # extract all required fields from request - quality_chain = request["quality_chain"] + partial_proof = request["partial_proof"] plot_difficulty = request.get("plot_difficulty", 1000) # todo default ? # create complete SolverInfo object with all provided data solver_info = SolverInfo( plot_difficulty=uint64(plot_difficulty), - quality_chain=bytes.fromhex(quality_chain), + partial_proof=bytes.fromhex(partial_proof), ) proof = self.service.solve(solver_info) diff --git a/chia/solver/solver_rpc_client.py b/chia/solver/solver_rpc_client.py index 9105b4676c5c..612f1aad3e42 100644 --- a/chia/solver/solver_rpc_client.py +++ b/chia/solver/solver_rpc_client.py @@ -20,4 +20,4 @@ async def get_state(self) -> dict[str, Any]: async def solve(self, quality_string: str, plot_size: int = 32, plot_difficulty: int = 1000) -> dict[str, Any]: """Solve a quality string with optional plot size and difficulty.""" quality = bytes32.from_hexstr(quality_string) - return await self.fetch("solve", {"quality_chain": quality.hex(), "plot_difficulty": plot_difficulty}) + return await self.fetch("solve", {"partial_proof": quality.hex(), "plot_difficulty": plot_difficulty}) From ad5fb0ee47e2bacb5297f8a93c6136266e4be4fa Mon Sep 17 00:00:00 2001 From: almogdepaz Date: Tue, 19 Aug 2025 17:46:58 +0300 Subject: [PATCH 29/42] network protocol test, use ThreadPoolExecutor --- chia/_tests/util/test_network_protocol_test.py | 2 +- chia/solver/solver.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/chia/_tests/util/test_network_protocol_test.py b/chia/_tests/util/test_network_protocol_test.py index 1f37f49397ba..b81e063b994b 100644 --- a/chia/_tests/util/test_network_protocol_test.py +++ b/chia/_tests/util/test_network_protocol_test.py @@ -189,7 +189,7 @@ def test_missing_messages() -> None: "RequestSignatures", "RespondPlots", "RespondSignatures", - "V2QualityChains", + "PartialProofsData", } introducer_msgs = {"RequestPeersIntroducer", "RespondPeersIntroducer"} diff --git a/chia/solver/solver.py b/chia/solver/solver.py index 6dae46d021e6..fef9dcc43531 100644 --- a/chia/solver/solver.py +++ b/chia/solver/solver.py @@ -1,7 +1,6 @@ from __future__ import annotations import asyncio -import concurrent import contextlib import logging from collections.abc import AsyncIterator @@ -48,7 +47,7 @@ def __init__(self, root_path: Path, config: dict[str, Any], constants: Consensus self._shut_down = False num_threads = config["num_threads"] self.log.info(f"Initializing solver with {num_threads} threads") - self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=num_threads, thread_name_prefix="solver-") + self.executor = ThreadPoolExecutor(max_workers=num_threads, thread_name_prefix="solver-") self._server = None self.constants = constants self.state_changed_callback: Optional[StateChangedProtocol] = None From 7d018af4cad2c37aedfee22a94f5e3162920a97e Mon Sep 17 00:00:00 2001 From: almogdepaz Date: Tue, 19 Aug 2025 22:22:50 +0300 Subject: [PATCH 30/42] naming and types --- chia/harvester/harvester_api.py | 6 +++--- chia/plotting/prover.py | 17 ++++++++--------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/chia/harvester/harvester_api.py b/chia/harvester/harvester_api.py index 82e94d2794fa..8108af92602a 100644 --- a/chia/harvester/harvester_api.py +++ b/chia/harvester/harvester_api.py @@ -130,8 +130,8 @@ def blocking_lookup_v2_partial_proofs(filename: Path, plot_info: PlotInfo) -> Op good_qualities = [] sp_interval_iters = calculate_sp_interval_iters(self.harvester.constants, sub_slot_iters) - for quality in partial_proofs: - quality_str = quality_for_partial_proof(quality, new_challenge.challenge_hash) + for partial_proof in partial_proofs: + quality_str = quality_for_partial_proof(partial_proof, new_challenge.challenge_hash) required_iters: uint64 = calculate_iterations_quality( self.harvester.constants, quality_str, @@ -143,7 +143,7 @@ def blocking_lookup_v2_partial_proofs(filename: Path, plot_info: PlotInfo) -> Op ) if required_iters < sp_interval_iters: - good_qualities.append(quality) + good_qualities.append(partial_proof) if len(good_qualities) > 0: size = plot_info.prover.get_size().size_v2 diff --git a/chia/plotting/prover.py b/chia/plotting/prover.py index 9ab3499720c9..38923eef6739 100644 --- a/chia/plotting/prover.py +++ b/chia/plotting/prover.py @@ -28,8 +28,8 @@ def get_version(self) -> PlotVersion: ... def __bytes__(self) -> bytes: ... def get_id(self) -> bytes32: ... def get_qualities_for_challenge(self, challenge: bytes32) -> list[bytes32]: ... - def get_partial_proofs_for_challenge(self, challenge: bytes) -> list[bytes]: ... - def get_full_proof(self, challenge: bytes, index: int, parallel_read: bool = True) -> bytes: ... + def get_partial_proofs_for_challenge(self, challenge: bytes32) -> list[bytes]: ... + def get_full_proof(self, challenge: bytes32, index: int, parallel_read: bool = True) -> bytes: ... @classmethod def from_bytes(cls, data: bytes) -> ProverProtocol: ... @@ -56,8 +56,7 @@ def get_memo(self) -> bytes: raise NotImplementedError("V2 plot format is not yet implemented") def get_compression_level(self) -> uint8: - # TODO: todo_v2_plots implement compression level retrieval - raise NotImplementedError("V2 plot format is not yet implemented") + raise NotImplementedError("V2 plot format does not support compression level") def get_version(self) -> PlotVersion: return PlotVersion.V2 @@ -70,15 +69,15 @@ def get_id(self) -> bytes32: # TODO: Extract plot ID from V2 plot file raise NotImplementedError("V2 plot format is not yet implemented") - def get_qualities_for_challenge(self, challenge: bytes) -> list[bytes32]: + def get_qualities_for_challenge(self, challenge: bytes32) -> list[bytes32]: # TODO: todo_v2_plots Implement plot quality lookup raise NotImplementedError("V2 plot format is not yet implemented") - def get_partial_proofs_for_challenge(self, challenge: bytes) -> list[bytes]: + def get_partial_proofs_for_challenge(self, challenge: bytes32) -> list[bytes]: # TODO: todo_v2_plots Implement quality chain lookup (16 * k bits blobs) raise NotImplementedError("V2 plot format is not yet implemented") - def get_full_proof(self, challenge: bytes, index: int, parallel_read: bool = True) -> bytes: + def get_full_proof(self, challenge: bytes32, index: int, parallel_read: bool = True) -> bytes: # TODO: todo_v2_plots Implement plot proof generation raise NotImplementedError("V2 plot format require solver to get full proof") @@ -121,10 +120,10 @@ def get_id(self) -> bytes32: def get_qualities_for_challenge(self, challenge: bytes32) -> list[bytes32]: return [bytes32(quality) for quality in self._disk_prover.get_qualities_for_challenge(challenge)] - def get_partial_proofs_for_challenge(self, challenge: bytes) -> list[bytes]: + def get_partial_proofs_for_challenge(self, challenge: bytes32) -> list[bytes]: raise NotImplementedError("V1 does not implement quality chains, only qualities") - def get_full_proof(self, challenge: bytes, index: int, parallel_read: bool = True) -> bytes: + def get_full_proof(self, challenge: bytes32, index: int, parallel_read: bool = True) -> bytes: return bytes(self._disk_prover.get_full_proof(challenge, index, parallel_read)) @classmethod From a86fa01ae1383855c4cbe245cbfe97db2ff0f86d Mon Sep 17 00:00:00 2001 From: almogdepaz Date: Tue, 19 Aug 2025 22:49:24 +0300 Subject: [PATCH 31/42] check filter for both plot versions --- chia/_tests/plotting/test_prover.py | 7 +++-- chia/harvester/harvester_api.py | 44 ++++++++++++++++------------- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/chia/_tests/plotting/test_prover.py b/chia/_tests/plotting/test_prover.py index 5fd753f6d339..50beceabd888 100644 --- a/chia/_tests/plotting/test_prover.py +++ b/chia/_tests/plotting/test_prover.py @@ -5,6 +5,7 @@ from unittest.mock import MagicMock, patch import pytest +from chia_rs.sized_bytes import bytes32 from chia.plotting.prover import PlotVersion, V1Prover, V2Prover, get_prover_from_bytes, get_prover_from_file @@ -27,7 +28,7 @@ def test_v2_prover_get_memo_raises_error(self) -> None: def test_v2_prover_get_compression_level_raises_error(self) -> None: prover = V2Prover("/nonexistent/path/test.plot2") - with pytest.raises(NotImplementedError, match="V2 plot format is not yet implemented"): + with pytest.raises(NotImplementedError, match="V2 plot format does not support compression level"): prover.get_compression_level() def test_v2_prover_get_id_raises_error(self) -> None: @@ -38,12 +39,12 @@ def test_v2_prover_get_id_raises_error(self) -> None: def test_v2_prover_get_qualities_for_challenge_raises_error(self) -> None: prover = V2Prover("/nonexistent/path/test.plot2") with pytest.raises(NotImplementedError, match="V2 plot format is not yet implemented"): - prover.get_qualities_for_challenge(b"challenge") + prover.get_qualities_for_challenge(bytes32(b"1" * 32)) def test_v2_prover_get_full_proof_raises_error(self) -> None: prover = V2Prover("/nonexistent/path/test.plot2") with pytest.raises(NotImplementedError, match="V2 plot format require solver to get full proof"): - prover.get_full_proof(b"challenge", 0) + prover.get_full_proof(bytes32(b"1" * 32), 0) def test_v2_prover_bytes_raises_error(self) -> None: prover = V2Prover("/nonexistent/path/test.plot2") diff --git a/chia/harvester/harvester_api.py b/chia/harvester/harvester_api.py index 8108af92602a..a1e59fd32a94 100644 --- a/chia/harvester/harvester_api.py +++ b/chia/harvester/harvester_api.py @@ -52,6 +52,19 @@ def __init__(self, harvester: Harvester): def ready(self) -> bool: return True + def _plot_passes_filter(self, plot_info: PlotInfo, challenge: harvester_protocol.NewSignagePointHarvester) -> bool: + filter_prefix_bits = calculate_prefix_bits( + self.harvester.constants, + challenge.peak_height, + plot_info.prover.get_size(), + ) + return passes_plot_filter( + filter_prefix_bits, + plot_info.prover.get_id(), + challenge.challenge_hash, + challenge.sp_hash, + ) + @metadata.request(peer_required=True) async def harvester_handshake( self, harvester_handshake: harvester_protocol.HarvesterHandshake, peer: WSChiaConnection @@ -318,26 +331,19 @@ async def lookup_challenge( # Passes the plot filter (does not check sp filter yet though, since we have not reached sp) # This is being executed at the beginning of the slot total += 1 - if try_plot_info.prover.get_version() == PlotVersion.V2: - # TODO: todo_v2_plots need to check v2 filter - v2_awaitables.append( - loop.run_in_executor( - self.harvester.executor, blocking_lookup_v2_partial_proofs, try_plot_filename, try_plot_info + if self._plot_passes_filter(try_plot_info, new_challenge): + if try_plot_info.prover.get_version() == PlotVersion.V2: + # TODO: todo_v2_plots need to check v2 filter + v2_awaitables.append( + loop.run_in_executor( + self.harvester.executor, + blocking_lookup_v2_partial_proofs, + try_plot_filename, + try_plot_info, + ) ) - ) - passed += 1 - else: - filter_prefix_bits = calculate_prefix_bits( - self.harvester.constants, - new_challenge.peak_height, - try_plot_info.prover.get_size(), - ) - if passes_plot_filter( - filter_prefix_bits, - try_plot_info.prover.get_id(), - new_challenge.challenge_hash, - new_challenge.sp_hash, - ): + passed += 1 + else: passed += 1 awaitables.append(lookup_challenge(try_plot_filename, try_plot_info)) self.harvester.log.debug(f"new_signage_point_harvester {passed} plots passed the plot filter") From da8c90e1c9b842c32dcc18efb242a8feb6baf928 Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Wed, 20 Aug 2025 08:28:07 +0200 Subject: [PATCH 32/42] update network protocol tests --- .../util/build_network_protocol_files.py | 7 + chia/_tests/util/network_protocol_data.py | 27 + chia/_tests/util/protocol_messages_bytes-v1.0 | Bin 50571 -> 50900 bytes chia/_tests/util/protocol_messages_json.py | 20 + .../util/test_network_protocol_files.py | 645 +++++++++--------- .../_tests/util/test_network_protocol_json.py | 6 + 6 files changed, 390 insertions(+), 315 deletions(-) diff --git a/chia/_tests/util/build_network_protocol_files.py b/chia/_tests/util/build_network_protocol_files.py index 8e2f6b34c217..4de90f8478ae 100644 --- a/chia/_tests/util/build_network_protocol_files.py +++ b/chia/_tests/util/build_network_protocol_files.py @@ -33,6 +33,12 @@ def visit_farmer_protocol(visitor: Callable[[Any, str], None]) -> None: visitor(request_signed_values, "request_signed_values") visitor(farming_info, "farming_info") visitor(signed_values, "signed_values") + visitor(partial_proof, "partial_proof") + + +def visit_solver_protocol(visitor: Callable[[Any, str], None]) -> None: + visitor(solver_info, "solver_info") + visitor(solver_response, "solver_response") def visit_full_node(visitor: Callable[[Any, str], None]) -> None: @@ -170,6 +176,7 @@ def visit_all_messages(visitor: Callable[[Any, str], None]) -> None: visit_pool_protocol(visitor) visit_timelord_protocol(visitor) visit_shared_protocol(visitor) + visit_solver_protocol(visitor) def get_protocol_bytes() -> bytes: diff --git a/chia/_tests/util/network_protocol_data.py b/chia/_tests/util/network_protocol_data.py index 7e0ca731697c..90ecbffb5af0 100644 --- a/chia/_tests/util/network_protocol_data.py +++ b/chia/_tests/util/network_protocol_data.py @@ -37,6 +37,7 @@ harvester_protocol, introducer_protocol, pool_protocol, + solver_protocol, timelord_protocol, wallet_protocol, ) @@ -150,6 +151,27 @@ ), ) +partial_proof = harvester_protocol.PartialProofsData( + bytes32.fromhex("42743566108589c11bb3811b347900b6351fd3e25bad6c956c0bf1c05a4d93fb"), + bytes32.fromhex("8a346e8dc02e9b44c0571caa74fd99f163d4c5d7deaedac87125528721493f7a"), + "plot-filename", + [b"partial-proof1", b"partial-proof2"], + uint8(4), + uint8(32), + uint64(100000), + G1Element.from_bytes( + bytes.fromhex( + "a04c6b5ac7dfb935f6feecfdd72348ccf1d4be4fe7e26acf271ea3b7d308da61e0a308f7a62495328a81f5147b66634c" + ), + ), + bytes32.fromhex("91240fbacdf93b44c0571caa74fd99f163d4c5d7deaedac87125528721493f7a"), + G1Element.from_bytes( + bytes.fromhex( + "a04c6b5ac7dfb935f6feecfdd72348ccf1d4be4fe7e26acf271ea3b7d308da61e0a308f7a62495328a81f5147b66634c" + ), + ), +) + # FULL NODE PROTOCOL. new_peak = full_node_protocol.NewPeak( @@ -1082,3 +1104,8 @@ uint32(386395693), uint8(224), ) + +# SOLVER PROTOCOL +solver_info = solver_protocol.SolverInfo(uint64(2), b"partial-proof") + +solver_response = solver_protocol.SolverResponse(b"partial-proof", b"full-proof") diff --git a/chia/_tests/util/protocol_messages_bytes-v1.0 b/chia/_tests/util/protocol_messages_bytes-v1.0 index 0d9160933489c5a3a1c6ad131450a3a7eabfb1c4..cc64c54d471f9dc0d6bcf7fd18b490ec0bc13e70 100644 GIT binary patch delta 238 zcmeC~X1>zPyg`puiILT*#57Hywez6#=0<6gN``Hw@|Pb)ug#g7!~O9T?x08Z@&#sB~S diff --git a/chia/_tests/util/protocol_messages_json.py b/chia/_tests/util/protocol_messages_json.py index c89601f7c90e..33bb23dd7b91 100644 --- a/chia/_tests/util/protocol_messages_json.py +++ b/chia/_tests/util/protocol_messages_json.py @@ -65,6 +65,19 @@ "foliage_transaction_block_signature": "0xc00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", } +partial_proof_json: dict[str, Any] = { + "challenge_hash": "0x42743566108589c11bb3811b347900b6351fd3e25bad6c956c0bf1c05a4d93fb", + "sp_hash": "0x8a346e8dc02e9b44c0571caa74fd99f163d4c5d7deaedac87125528721493f7a", + "plot_identifier": "plot-filename", + "partial_proofs": ["0x7061727469616c2d70726f6f6631", "0x7061727469616c2d70726f6f6632"], + "signage_point_index": 4, + "plot_size": 32, + "difficulty": 100000, + "pool_public_key": "0xa04c6b5ac7dfb935f6feecfdd72348ccf1d4be4fe7e26acf271ea3b7d308da61e0a308f7a62495328a81f5147b66634c", + "pool_contract_puzzle_hash": "0x91240fbacdf93b44c0571caa74fd99f163d4c5d7deaedac87125528721493f7a", + "plot_public_key": "0xa04c6b5ac7dfb935f6feecfdd72348ccf1d4be4fe7e26acf271ea3b7d308da61e0a308f7a62495328a81f5147b66634c", +} + new_peak_json: dict[str, Any] = { "header_hash": "0x8a346e8dc02e9b44c0571caa74fd99f163d4c5d7deae9f8ddb00528721493f7a", "height": 2653549198, @@ -2701,3 +2714,10 @@ error_without_data_json: dict[str, Any] = {"code": 1, "message": "Unknown", "data": None} error_with_data_json: dict[str, Any] = {"code": 1, "message": "Unknown", "data": "0x65787472612064617461"} + +solver_info_json: dict[str, Any] = {"plot_difficulty": 2, "partial_proof": "0x7061727469616c2d70726f6f66"} + +solver_response_json: dict[str, Any] = { + "partial_proof": "0x7061727469616c2d70726f6f66", + "proof": "0x66756c6c2d70726f6f66", +} diff --git a/chia/_tests/util/test_network_protocol_files.py b/chia/_tests/util/test_network_protocol_files.py index 071a263e8366..279a45f2008c 100644 --- a/chia/_tests/util/test_network_protocol_files.py +++ b/chia/_tests/util/test_network_protocol_files.py @@ -51,528 +51,543 @@ def test_protocol_bytes() -> None: assert bytes(message_4) == bytes(signed_values) message_bytes, input_bytes = parse_blob(input_bytes) - message_5 = type(new_peak).from_bytes(message_bytes) - assert message_5 == new_peak - assert bytes(message_5) == bytes(new_peak) + message_5 = type(partial_proof).from_bytes(message_bytes) + assert message_5 == partial_proof + assert bytes(message_5) == bytes(partial_proof) message_bytes, input_bytes = parse_blob(input_bytes) - message_6 = type(new_transaction).from_bytes(message_bytes) - assert message_6 == new_transaction - assert bytes(message_6) == bytes(new_transaction) + message_6 = type(new_peak).from_bytes(message_bytes) + assert message_6 == new_peak + assert bytes(message_6) == bytes(new_peak) message_bytes, input_bytes = parse_blob(input_bytes) - message_7 = type(request_transaction).from_bytes(message_bytes) - assert message_7 == request_transaction - assert bytes(message_7) == bytes(request_transaction) + message_7 = type(new_transaction).from_bytes(message_bytes) + assert message_7 == new_transaction + assert bytes(message_7) == bytes(new_transaction) message_bytes, input_bytes = parse_blob(input_bytes) - message_8 = type(respond_transaction).from_bytes(message_bytes) - assert message_8 == respond_transaction - assert bytes(message_8) == bytes(respond_transaction) + message_8 = type(request_transaction).from_bytes(message_bytes) + assert message_8 == request_transaction + assert bytes(message_8) == bytes(request_transaction) message_bytes, input_bytes = parse_blob(input_bytes) - message_9 = type(request_proof_of_weight).from_bytes(message_bytes) - assert message_9 == request_proof_of_weight - assert bytes(message_9) == bytes(request_proof_of_weight) + message_9 = type(respond_transaction).from_bytes(message_bytes) + assert message_9 == respond_transaction + assert bytes(message_9) == bytes(respond_transaction) message_bytes, input_bytes = parse_blob(input_bytes) - message_10 = type(respond_proof_of_weight).from_bytes(message_bytes) - assert message_10 == respond_proof_of_weight - assert bytes(message_10) == bytes(respond_proof_of_weight) + message_10 = type(request_proof_of_weight).from_bytes(message_bytes) + assert message_10 == request_proof_of_weight + assert bytes(message_10) == bytes(request_proof_of_weight) message_bytes, input_bytes = parse_blob(input_bytes) - message_11 = type(request_block).from_bytes(message_bytes) - assert message_11 == request_block - assert bytes(message_11) == bytes(request_block) + message_11 = type(respond_proof_of_weight).from_bytes(message_bytes) + assert message_11 == respond_proof_of_weight + assert bytes(message_11) == bytes(respond_proof_of_weight) message_bytes, input_bytes = parse_blob(input_bytes) - message_12 = type(reject_block).from_bytes(message_bytes) - assert message_12 == reject_block - assert bytes(message_12) == bytes(reject_block) + message_12 = type(request_block).from_bytes(message_bytes) + assert message_12 == request_block + assert bytes(message_12) == bytes(request_block) message_bytes, input_bytes = parse_blob(input_bytes) - message_13 = type(request_blocks).from_bytes(message_bytes) - assert message_13 == request_blocks - assert bytes(message_13) == bytes(request_blocks) + message_13 = type(reject_block).from_bytes(message_bytes) + assert message_13 == reject_block + assert bytes(message_13) == bytes(reject_block) message_bytes, input_bytes = parse_blob(input_bytes) - message_14 = type(respond_blocks).from_bytes(message_bytes) - assert message_14 == respond_blocks - assert bytes(message_14) == bytes(respond_blocks) + message_14 = type(request_blocks).from_bytes(message_bytes) + assert message_14 == request_blocks + assert bytes(message_14) == bytes(request_blocks) message_bytes, input_bytes = parse_blob(input_bytes) - message_15 = type(reject_blocks).from_bytes(message_bytes) - assert message_15 == reject_blocks - assert bytes(message_15) == bytes(reject_blocks) + message_15 = type(respond_blocks).from_bytes(message_bytes) + assert message_15 == respond_blocks + assert bytes(message_15) == bytes(respond_blocks) message_bytes, input_bytes = parse_blob(input_bytes) - message_16 = type(respond_block).from_bytes(message_bytes) - assert message_16 == respond_block - assert bytes(message_16) == bytes(respond_block) + message_16 = type(reject_blocks).from_bytes(message_bytes) + assert message_16 == reject_blocks + assert bytes(message_16) == bytes(reject_blocks) message_bytes, input_bytes = parse_blob(input_bytes) - message_17 = type(new_unfinished_block).from_bytes(message_bytes) - assert message_17 == new_unfinished_block - assert bytes(message_17) == bytes(new_unfinished_block) + message_17 = type(respond_block).from_bytes(message_bytes) + assert message_17 == respond_block + assert bytes(message_17) == bytes(respond_block) message_bytes, input_bytes = parse_blob(input_bytes) - message_18 = type(request_unfinished_block).from_bytes(message_bytes) - assert message_18 == request_unfinished_block - assert bytes(message_18) == bytes(request_unfinished_block) + message_18 = type(new_unfinished_block).from_bytes(message_bytes) + assert message_18 == new_unfinished_block + assert bytes(message_18) == bytes(new_unfinished_block) message_bytes, input_bytes = parse_blob(input_bytes) - message_19 = type(respond_unfinished_block).from_bytes(message_bytes) - assert message_19 == respond_unfinished_block - assert bytes(message_19) == bytes(respond_unfinished_block) + message_19 = type(request_unfinished_block).from_bytes(message_bytes) + assert message_19 == request_unfinished_block + assert bytes(message_19) == bytes(request_unfinished_block) message_bytes, input_bytes = parse_blob(input_bytes) - message_20 = type(new_signage_point_or_end_of_subslot).from_bytes(message_bytes) - assert message_20 == new_signage_point_or_end_of_subslot - assert bytes(message_20) == bytes(new_signage_point_or_end_of_subslot) + message_20 = type(respond_unfinished_block).from_bytes(message_bytes) + assert message_20 == respond_unfinished_block + assert bytes(message_20) == bytes(respond_unfinished_block) message_bytes, input_bytes = parse_blob(input_bytes) - message_21 = type(request_signage_point_or_end_of_subslot).from_bytes(message_bytes) - assert message_21 == request_signage_point_or_end_of_subslot - assert bytes(message_21) == bytes(request_signage_point_or_end_of_subslot) + message_21 = type(new_signage_point_or_end_of_subslot).from_bytes(message_bytes) + assert message_21 == new_signage_point_or_end_of_subslot + assert bytes(message_21) == bytes(new_signage_point_or_end_of_subslot) message_bytes, input_bytes = parse_blob(input_bytes) - message_22 = type(respond_signage_point).from_bytes(message_bytes) - assert message_22 == respond_signage_point - assert bytes(message_22) == bytes(respond_signage_point) + message_22 = type(request_signage_point_or_end_of_subslot).from_bytes(message_bytes) + assert message_22 == request_signage_point_or_end_of_subslot + assert bytes(message_22) == bytes(request_signage_point_or_end_of_subslot) message_bytes, input_bytes = parse_blob(input_bytes) - message_23 = type(respond_end_of_subslot).from_bytes(message_bytes) - assert message_23 == respond_end_of_subslot - assert bytes(message_23) == bytes(respond_end_of_subslot) + message_23 = type(respond_signage_point).from_bytes(message_bytes) + assert message_23 == respond_signage_point + assert bytes(message_23) == bytes(respond_signage_point) message_bytes, input_bytes = parse_blob(input_bytes) - message_24 = type(request_mempool_transaction).from_bytes(message_bytes) - assert message_24 == request_mempool_transaction - assert bytes(message_24) == bytes(request_mempool_transaction) + message_24 = type(respond_end_of_subslot).from_bytes(message_bytes) + assert message_24 == respond_end_of_subslot + assert bytes(message_24) == bytes(respond_end_of_subslot) message_bytes, input_bytes = parse_blob(input_bytes) - message_25 = type(new_compact_vdf).from_bytes(message_bytes) - assert message_25 == new_compact_vdf - assert bytes(message_25) == bytes(new_compact_vdf) + message_25 = type(request_mempool_transaction).from_bytes(message_bytes) + assert message_25 == request_mempool_transaction + assert bytes(message_25) == bytes(request_mempool_transaction) message_bytes, input_bytes = parse_blob(input_bytes) - message_26 = type(request_compact_vdf).from_bytes(message_bytes) - assert message_26 == request_compact_vdf - assert bytes(message_26) == bytes(request_compact_vdf) + message_26 = type(new_compact_vdf).from_bytes(message_bytes) + assert message_26 == new_compact_vdf + assert bytes(message_26) == bytes(new_compact_vdf) message_bytes, input_bytes = parse_blob(input_bytes) - message_27 = type(respond_compact_vdf).from_bytes(message_bytes) - assert message_27 == respond_compact_vdf - assert bytes(message_27) == bytes(respond_compact_vdf) + message_27 = type(request_compact_vdf).from_bytes(message_bytes) + assert message_27 == request_compact_vdf + assert bytes(message_27) == bytes(request_compact_vdf) message_bytes, input_bytes = parse_blob(input_bytes) - message_28 = type(request_peers).from_bytes(message_bytes) - assert message_28 == request_peers - assert bytes(message_28) == bytes(request_peers) + message_28 = type(respond_compact_vdf).from_bytes(message_bytes) + assert message_28 == respond_compact_vdf + assert bytes(message_28) == bytes(respond_compact_vdf) message_bytes, input_bytes = parse_blob(input_bytes) - message_29 = type(respond_peers).from_bytes(message_bytes) - assert message_29 == respond_peers - assert bytes(message_29) == bytes(respond_peers) + message_29 = type(request_peers).from_bytes(message_bytes) + assert message_29 == request_peers + assert bytes(message_29) == bytes(request_peers) message_bytes, input_bytes = parse_blob(input_bytes) - message_30 = type(new_unfinished_block2).from_bytes(message_bytes) - assert message_30 == new_unfinished_block2 - assert bytes(message_30) == bytes(new_unfinished_block2) + message_30 = type(respond_peers).from_bytes(message_bytes) + assert message_30 == respond_peers + assert bytes(message_30) == bytes(respond_peers) message_bytes, input_bytes = parse_blob(input_bytes) - message_31 = type(request_unfinished_block2).from_bytes(message_bytes) - assert message_31 == request_unfinished_block2 - assert bytes(message_31) == bytes(request_unfinished_block2) + message_31 = type(new_unfinished_block2).from_bytes(message_bytes) + assert message_31 == new_unfinished_block2 + assert bytes(message_31) == bytes(new_unfinished_block2) message_bytes, input_bytes = parse_blob(input_bytes) - message_32 = type(request_puzzle_solution).from_bytes(message_bytes) - assert message_32 == request_puzzle_solution - assert bytes(message_32) == bytes(request_puzzle_solution) + message_32 = type(request_unfinished_block2).from_bytes(message_bytes) + assert message_32 == request_unfinished_block2 + assert bytes(message_32) == bytes(request_unfinished_block2) message_bytes, input_bytes = parse_blob(input_bytes) - message_33 = type(puzzle_solution_response).from_bytes(message_bytes) - assert message_33 == puzzle_solution_response - assert bytes(message_33) == bytes(puzzle_solution_response) + message_33 = type(request_puzzle_solution).from_bytes(message_bytes) + assert message_33 == request_puzzle_solution + assert bytes(message_33) == bytes(request_puzzle_solution) message_bytes, input_bytes = parse_blob(input_bytes) - message_34 = type(respond_puzzle_solution).from_bytes(message_bytes) - assert message_34 == respond_puzzle_solution - assert bytes(message_34) == bytes(respond_puzzle_solution) + message_34 = type(puzzle_solution_response).from_bytes(message_bytes) + assert message_34 == puzzle_solution_response + assert bytes(message_34) == bytes(puzzle_solution_response) message_bytes, input_bytes = parse_blob(input_bytes) - message_35 = type(reject_puzzle_solution).from_bytes(message_bytes) - assert message_35 == reject_puzzle_solution - assert bytes(message_35) == bytes(reject_puzzle_solution) + message_35 = type(respond_puzzle_solution).from_bytes(message_bytes) + assert message_35 == respond_puzzle_solution + assert bytes(message_35) == bytes(respond_puzzle_solution) message_bytes, input_bytes = parse_blob(input_bytes) - message_36 = type(send_transaction).from_bytes(message_bytes) - assert message_36 == send_transaction - assert bytes(message_36) == bytes(send_transaction) + message_36 = type(reject_puzzle_solution).from_bytes(message_bytes) + assert message_36 == reject_puzzle_solution + assert bytes(message_36) == bytes(reject_puzzle_solution) message_bytes, input_bytes = parse_blob(input_bytes) - message_37 = type(transaction_ack).from_bytes(message_bytes) - assert message_37 == transaction_ack - assert bytes(message_37) == bytes(transaction_ack) + message_37 = type(send_transaction).from_bytes(message_bytes) + assert message_37 == send_transaction + assert bytes(message_37) == bytes(send_transaction) message_bytes, input_bytes = parse_blob(input_bytes) - message_38 = type(new_peak_wallet).from_bytes(message_bytes) - assert message_38 == new_peak_wallet - assert bytes(message_38) == bytes(new_peak_wallet) + message_38 = type(transaction_ack).from_bytes(message_bytes) + assert message_38 == transaction_ack + assert bytes(message_38) == bytes(transaction_ack) message_bytes, input_bytes = parse_blob(input_bytes) - message_39 = type(request_block_header).from_bytes(message_bytes) - assert message_39 == request_block_header - assert bytes(message_39) == bytes(request_block_header) + message_39 = type(new_peak_wallet).from_bytes(message_bytes) + assert message_39 == new_peak_wallet + assert bytes(message_39) == bytes(new_peak_wallet) message_bytes, input_bytes = parse_blob(input_bytes) - message_40 = type(request_block_headers).from_bytes(message_bytes) - assert message_40 == request_block_headers - assert bytes(message_40) == bytes(request_block_headers) + message_40 = type(request_block_header).from_bytes(message_bytes) + assert message_40 == request_block_header + assert bytes(message_40) == bytes(request_block_header) message_bytes, input_bytes = parse_blob(input_bytes) - message_41 = type(respond_header_block).from_bytes(message_bytes) - assert message_41 == respond_header_block - assert bytes(message_41) == bytes(respond_header_block) + message_41 = type(request_block_headers).from_bytes(message_bytes) + assert message_41 == request_block_headers + assert bytes(message_41) == bytes(request_block_headers) message_bytes, input_bytes = parse_blob(input_bytes) - message_42 = type(respond_block_headers).from_bytes(message_bytes) - assert message_42 == respond_block_headers - assert bytes(message_42) == bytes(respond_block_headers) + message_42 = type(respond_header_block).from_bytes(message_bytes) + assert message_42 == respond_header_block + assert bytes(message_42) == bytes(respond_header_block) message_bytes, input_bytes = parse_blob(input_bytes) - message_43 = type(reject_header_request).from_bytes(message_bytes) - assert message_43 == reject_header_request - assert bytes(message_43) == bytes(reject_header_request) + message_43 = type(respond_block_headers).from_bytes(message_bytes) + assert message_43 == respond_block_headers + assert bytes(message_43) == bytes(respond_block_headers) message_bytes, input_bytes = parse_blob(input_bytes) - message_44 = type(request_removals).from_bytes(message_bytes) - assert message_44 == request_removals - assert bytes(message_44) == bytes(request_removals) + message_44 = type(reject_header_request).from_bytes(message_bytes) + assert message_44 == reject_header_request + assert bytes(message_44) == bytes(reject_header_request) message_bytes, input_bytes = parse_blob(input_bytes) - message_45 = type(respond_removals).from_bytes(message_bytes) - assert message_45 == respond_removals - assert bytes(message_45) == bytes(respond_removals) + message_45 = type(request_removals).from_bytes(message_bytes) + assert message_45 == request_removals + assert bytes(message_45) == bytes(request_removals) message_bytes, input_bytes = parse_blob(input_bytes) - message_46 = type(reject_removals_request).from_bytes(message_bytes) - assert message_46 == reject_removals_request - assert bytes(message_46) == bytes(reject_removals_request) + message_46 = type(respond_removals).from_bytes(message_bytes) + assert message_46 == respond_removals + assert bytes(message_46) == bytes(respond_removals) message_bytes, input_bytes = parse_blob(input_bytes) - message_47 = type(request_additions).from_bytes(message_bytes) - assert message_47 == request_additions - assert bytes(message_47) == bytes(request_additions) + message_47 = type(reject_removals_request).from_bytes(message_bytes) + assert message_47 == reject_removals_request + assert bytes(message_47) == bytes(reject_removals_request) message_bytes, input_bytes = parse_blob(input_bytes) - message_48 = type(respond_additions).from_bytes(message_bytes) - assert message_48 == respond_additions - assert bytes(message_48) == bytes(respond_additions) + message_48 = type(request_additions).from_bytes(message_bytes) + assert message_48 == request_additions + assert bytes(message_48) == bytes(request_additions) message_bytes, input_bytes = parse_blob(input_bytes) - message_49 = type(reject_additions).from_bytes(message_bytes) - assert message_49 == reject_additions - assert bytes(message_49) == bytes(reject_additions) + message_49 = type(respond_additions).from_bytes(message_bytes) + assert message_49 == respond_additions + assert bytes(message_49) == bytes(respond_additions) message_bytes, input_bytes = parse_blob(input_bytes) - message_50 = type(request_header_blocks).from_bytes(message_bytes) - assert message_50 == request_header_blocks - assert bytes(message_50) == bytes(request_header_blocks) + message_50 = type(reject_additions).from_bytes(message_bytes) + assert message_50 == reject_additions + assert bytes(message_50) == bytes(reject_additions) message_bytes, input_bytes = parse_blob(input_bytes) - message_51 = type(reject_header_blocks).from_bytes(message_bytes) - assert message_51 == reject_header_blocks - assert bytes(message_51) == bytes(reject_header_blocks) + message_51 = type(request_header_blocks).from_bytes(message_bytes) + assert message_51 == request_header_blocks + assert bytes(message_51) == bytes(request_header_blocks) message_bytes, input_bytes = parse_blob(input_bytes) - message_52 = type(respond_header_blocks).from_bytes(message_bytes) - assert message_52 == respond_header_blocks - assert bytes(message_52) == bytes(respond_header_blocks) + message_52 = type(reject_header_blocks).from_bytes(message_bytes) + assert message_52 == reject_header_blocks + assert bytes(message_52) == bytes(reject_header_blocks) message_bytes, input_bytes = parse_blob(input_bytes) - message_53 = type(coin_state).from_bytes(message_bytes) - assert message_53 == coin_state - assert bytes(message_53) == bytes(coin_state) + message_53 = type(respond_header_blocks).from_bytes(message_bytes) + assert message_53 == respond_header_blocks + assert bytes(message_53) == bytes(respond_header_blocks) message_bytes, input_bytes = parse_blob(input_bytes) - message_54 = type(register_for_ph_updates).from_bytes(message_bytes) - assert message_54 == register_for_ph_updates - assert bytes(message_54) == bytes(register_for_ph_updates) + message_54 = type(coin_state).from_bytes(message_bytes) + assert message_54 == coin_state + assert bytes(message_54) == bytes(coin_state) message_bytes, input_bytes = parse_blob(input_bytes) - message_55 = type(reject_block_headers).from_bytes(message_bytes) - assert message_55 == reject_block_headers - assert bytes(message_55) == bytes(reject_block_headers) + message_55 = type(register_for_ph_updates).from_bytes(message_bytes) + assert message_55 == register_for_ph_updates + assert bytes(message_55) == bytes(register_for_ph_updates) message_bytes, input_bytes = parse_blob(input_bytes) - message_56 = type(respond_to_ph_updates).from_bytes(message_bytes) - assert message_56 == respond_to_ph_updates - assert bytes(message_56) == bytes(respond_to_ph_updates) + message_56 = type(reject_block_headers).from_bytes(message_bytes) + assert message_56 == reject_block_headers + assert bytes(message_56) == bytes(reject_block_headers) message_bytes, input_bytes = parse_blob(input_bytes) - message_57 = type(register_for_coin_updates).from_bytes(message_bytes) - assert message_57 == register_for_coin_updates - assert bytes(message_57) == bytes(register_for_coin_updates) + message_57 = type(respond_to_ph_updates).from_bytes(message_bytes) + assert message_57 == respond_to_ph_updates + assert bytes(message_57) == bytes(respond_to_ph_updates) message_bytes, input_bytes = parse_blob(input_bytes) - message_58 = type(respond_to_coin_updates).from_bytes(message_bytes) - assert message_58 == respond_to_coin_updates - assert bytes(message_58) == bytes(respond_to_coin_updates) + message_58 = type(register_for_coin_updates).from_bytes(message_bytes) + assert message_58 == register_for_coin_updates + assert bytes(message_58) == bytes(register_for_coin_updates) message_bytes, input_bytes = parse_blob(input_bytes) - message_59 = type(coin_state_update).from_bytes(message_bytes) - assert message_59 == coin_state_update - assert bytes(message_59) == bytes(coin_state_update) + message_59 = type(respond_to_coin_updates).from_bytes(message_bytes) + assert message_59 == respond_to_coin_updates + assert bytes(message_59) == bytes(respond_to_coin_updates) message_bytes, input_bytes = parse_blob(input_bytes) - message_60 = type(request_children).from_bytes(message_bytes) - assert message_60 == request_children - assert bytes(message_60) == bytes(request_children) + message_60 = type(coin_state_update).from_bytes(message_bytes) + assert message_60 == coin_state_update + assert bytes(message_60) == bytes(coin_state_update) message_bytes, input_bytes = parse_blob(input_bytes) - message_61 = type(respond_children).from_bytes(message_bytes) - assert message_61 == respond_children - assert bytes(message_61) == bytes(respond_children) + message_61 = type(request_children).from_bytes(message_bytes) + assert message_61 == request_children + assert bytes(message_61) == bytes(request_children) message_bytes, input_bytes = parse_blob(input_bytes) - message_62 = type(request_ses_info).from_bytes(message_bytes) - assert message_62 == request_ses_info - assert bytes(message_62) == bytes(request_ses_info) + message_62 = type(respond_children).from_bytes(message_bytes) + assert message_62 == respond_children + assert bytes(message_62) == bytes(respond_children) message_bytes, input_bytes = parse_blob(input_bytes) - message_63 = type(respond_ses_info).from_bytes(message_bytes) - assert message_63 == respond_ses_info - assert bytes(message_63) == bytes(respond_ses_info) + message_63 = type(request_ses_info).from_bytes(message_bytes) + assert message_63 == request_ses_info + assert bytes(message_63) == bytes(request_ses_info) message_bytes, input_bytes = parse_blob(input_bytes) - message_64 = type(coin_state_filters).from_bytes(message_bytes) - assert message_64 == coin_state_filters - assert bytes(message_64) == bytes(coin_state_filters) + message_64 = type(respond_ses_info).from_bytes(message_bytes) + assert message_64 == respond_ses_info + assert bytes(message_64) == bytes(respond_ses_info) message_bytes, input_bytes = parse_blob(input_bytes) - message_65 = type(request_remove_puzzle_subscriptions).from_bytes(message_bytes) - assert message_65 == request_remove_puzzle_subscriptions - assert bytes(message_65) == bytes(request_remove_puzzle_subscriptions) + message_65 = type(coin_state_filters).from_bytes(message_bytes) + assert message_65 == coin_state_filters + assert bytes(message_65) == bytes(coin_state_filters) message_bytes, input_bytes = parse_blob(input_bytes) - message_66 = type(respond_remove_puzzle_subscriptions).from_bytes(message_bytes) - assert message_66 == respond_remove_puzzle_subscriptions - assert bytes(message_66) == bytes(respond_remove_puzzle_subscriptions) + message_66 = type(request_remove_puzzle_subscriptions).from_bytes(message_bytes) + assert message_66 == request_remove_puzzle_subscriptions + assert bytes(message_66) == bytes(request_remove_puzzle_subscriptions) message_bytes, input_bytes = parse_blob(input_bytes) - message_67 = type(request_remove_coin_subscriptions).from_bytes(message_bytes) - assert message_67 == request_remove_coin_subscriptions - assert bytes(message_67) == bytes(request_remove_coin_subscriptions) + message_67 = type(respond_remove_puzzle_subscriptions).from_bytes(message_bytes) + assert message_67 == respond_remove_puzzle_subscriptions + assert bytes(message_67) == bytes(respond_remove_puzzle_subscriptions) message_bytes, input_bytes = parse_blob(input_bytes) - message_68 = type(respond_remove_coin_subscriptions).from_bytes(message_bytes) - assert message_68 == respond_remove_coin_subscriptions - assert bytes(message_68) == bytes(respond_remove_coin_subscriptions) + message_68 = type(request_remove_coin_subscriptions).from_bytes(message_bytes) + assert message_68 == request_remove_coin_subscriptions + assert bytes(message_68) == bytes(request_remove_coin_subscriptions) message_bytes, input_bytes = parse_blob(input_bytes) - message_69 = type(request_puzzle_state).from_bytes(message_bytes) - assert message_69 == request_puzzle_state - assert bytes(message_69) == bytes(request_puzzle_state) + message_69 = type(respond_remove_coin_subscriptions).from_bytes(message_bytes) + assert message_69 == respond_remove_coin_subscriptions + assert bytes(message_69) == bytes(respond_remove_coin_subscriptions) message_bytes, input_bytes = parse_blob(input_bytes) - message_70 = type(reject_puzzle_state).from_bytes(message_bytes) - assert message_70 == reject_puzzle_state - assert bytes(message_70) == bytes(reject_puzzle_state) + message_70 = type(request_puzzle_state).from_bytes(message_bytes) + assert message_70 == request_puzzle_state + assert bytes(message_70) == bytes(request_puzzle_state) message_bytes, input_bytes = parse_blob(input_bytes) - message_71 = type(respond_puzzle_state).from_bytes(message_bytes) - assert message_71 == respond_puzzle_state - assert bytes(message_71) == bytes(respond_puzzle_state) + message_71 = type(reject_puzzle_state).from_bytes(message_bytes) + assert message_71 == reject_puzzle_state + assert bytes(message_71) == bytes(reject_puzzle_state) message_bytes, input_bytes = parse_blob(input_bytes) - message_72 = type(request_coin_state).from_bytes(message_bytes) - assert message_72 == request_coin_state - assert bytes(message_72) == bytes(request_coin_state) + message_72 = type(respond_puzzle_state).from_bytes(message_bytes) + assert message_72 == respond_puzzle_state + assert bytes(message_72) == bytes(respond_puzzle_state) message_bytes, input_bytes = parse_blob(input_bytes) - message_73 = type(respond_coin_state).from_bytes(message_bytes) - assert message_73 == respond_coin_state - assert bytes(message_73) == bytes(respond_coin_state) + message_73 = type(request_coin_state).from_bytes(message_bytes) + assert message_73 == request_coin_state + assert bytes(message_73) == bytes(request_coin_state) message_bytes, input_bytes = parse_blob(input_bytes) - message_74 = type(reject_coin_state).from_bytes(message_bytes) - assert message_74 == reject_coin_state - assert bytes(message_74) == bytes(reject_coin_state) + message_74 = type(respond_coin_state).from_bytes(message_bytes) + assert message_74 == respond_coin_state + assert bytes(message_74) == bytes(respond_coin_state) message_bytes, input_bytes = parse_blob(input_bytes) - message_75 = type(request_cost_info).from_bytes(message_bytes) - assert message_75 == request_cost_info - assert bytes(message_75) == bytes(request_cost_info) + message_75 = type(reject_coin_state).from_bytes(message_bytes) + assert message_75 == reject_coin_state + assert bytes(message_75) == bytes(reject_coin_state) message_bytes, input_bytes = parse_blob(input_bytes) - message_76 = type(respond_cost_info).from_bytes(message_bytes) - assert message_76 == respond_cost_info - assert bytes(message_76) == bytes(respond_cost_info) + message_76 = type(request_cost_info).from_bytes(message_bytes) + assert message_76 == request_cost_info + assert bytes(message_76) == bytes(request_cost_info) message_bytes, input_bytes = parse_blob(input_bytes) - message_77 = type(pool_difficulty).from_bytes(message_bytes) - assert message_77 == pool_difficulty - assert bytes(message_77) == bytes(pool_difficulty) + message_77 = type(respond_cost_info).from_bytes(message_bytes) + assert message_77 == respond_cost_info + assert bytes(message_77) == bytes(respond_cost_info) message_bytes, input_bytes = parse_blob(input_bytes) - message_78 = type(harvester_handhsake).from_bytes(message_bytes) - assert message_78 == harvester_handhsake - assert bytes(message_78) == bytes(harvester_handhsake) + message_78 = type(pool_difficulty).from_bytes(message_bytes) + assert message_78 == pool_difficulty + assert bytes(message_78) == bytes(pool_difficulty) message_bytes, input_bytes = parse_blob(input_bytes) - message_79 = type(new_signage_point_harvester).from_bytes(message_bytes) - assert message_79 == new_signage_point_harvester - assert bytes(message_79) == bytes(new_signage_point_harvester) + message_79 = type(harvester_handhsake).from_bytes(message_bytes) + assert message_79 == harvester_handhsake + assert bytes(message_79) == bytes(harvester_handhsake) message_bytes, input_bytes = parse_blob(input_bytes) - message_80 = type(new_proof_of_space).from_bytes(message_bytes) - assert message_80 == new_proof_of_space - assert bytes(message_80) == bytes(new_proof_of_space) + message_80 = type(new_signage_point_harvester).from_bytes(message_bytes) + assert message_80 == new_signage_point_harvester + assert bytes(message_80) == bytes(new_signage_point_harvester) message_bytes, input_bytes = parse_blob(input_bytes) - message_81 = type(request_signatures).from_bytes(message_bytes) - assert message_81 == request_signatures - assert bytes(message_81) == bytes(request_signatures) + message_81 = type(new_proof_of_space).from_bytes(message_bytes) + assert message_81 == new_proof_of_space + assert bytes(message_81) == bytes(new_proof_of_space) message_bytes, input_bytes = parse_blob(input_bytes) - message_82 = type(respond_signatures).from_bytes(message_bytes) - assert message_82 == respond_signatures - assert bytes(message_82) == bytes(respond_signatures) + message_82 = type(request_signatures).from_bytes(message_bytes) + assert message_82 == request_signatures + assert bytes(message_82) == bytes(request_signatures) message_bytes, input_bytes = parse_blob(input_bytes) - message_83 = type(plot).from_bytes(message_bytes) - assert message_83 == plot - assert bytes(message_83) == bytes(plot) + message_83 = type(respond_signatures).from_bytes(message_bytes) + assert message_83 == respond_signatures + assert bytes(message_83) == bytes(respond_signatures) message_bytes, input_bytes = parse_blob(input_bytes) - message_84 = type(request_plots).from_bytes(message_bytes) - assert message_84 == request_plots - assert bytes(message_84) == bytes(request_plots) + message_84 = type(plot).from_bytes(message_bytes) + assert message_84 == plot + assert bytes(message_84) == bytes(plot) message_bytes, input_bytes = parse_blob(input_bytes) - message_85 = type(respond_plots).from_bytes(message_bytes) - assert message_85 == respond_plots - assert bytes(message_85) == bytes(respond_plots) + message_85 = type(request_plots).from_bytes(message_bytes) + assert message_85 == request_plots + assert bytes(message_85) == bytes(request_plots) message_bytes, input_bytes = parse_blob(input_bytes) - message_86 = type(request_peers_introducer).from_bytes(message_bytes) - assert message_86 == request_peers_introducer - assert bytes(message_86) == bytes(request_peers_introducer) + message_86 = type(respond_plots).from_bytes(message_bytes) + assert message_86 == respond_plots + assert bytes(message_86) == bytes(respond_plots) message_bytes, input_bytes = parse_blob(input_bytes) - message_87 = type(respond_peers_introducer).from_bytes(message_bytes) - assert message_87 == respond_peers_introducer - assert bytes(message_87) == bytes(respond_peers_introducer) + message_87 = type(request_peers_introducer).from_bytes(message_bytes) + assert message_87 == request_peers_introducer + assert bytes(message_87) == bytes(request_peers_introducer) message_bytes, input_bytes = parse_blob(input_bytes) - message_88 = type(authentication_payload).from_bytes(message_bytes) - assert message_88 == authentication_payload - assert bytes(message_88) == bytes(authentication_payload) + message_88 = type(respond_peers_introducer).from_bytes(message_bytes) + assert message_88 == respond_peers_introducer + assert bytes(message_88) == bytes(respond_peers_introducer) message_bytes, input_bytes = parse_blob(input_bytes) - message_89 = type(get_pool_info_response).from_bytes(message_bytes) - assert message_89 == get_pool_info_response - assert bytes(message_89) == bytes(get_pool_info_response) + message_89 = type(authentication_payload).from_bytes(message_bytes) + assert message_89 == authentication_payload + assert bytes(message_89) == bytes(authentication_payload) message_bytes, input_bytes = parse_blob(input_bytes) - message_90 = type(post_partial_payload).from_bytes(message_bytes) - assert message_90 == post_partial_payload - assert bytes(message_90) == bytes(post_partial_payload) + message_90 = type(get_pool_info_response).from_bytes(message_bytes) + assert message_90 == get_pool_info_response + assert bytes(message_90) == bytes(get_pool_info_response) message_bytes, input_bytes = parse_blob(input_bytes) - message_91 = type(post_partial_request).from_bytes(message_bytes) - assert message_91 == post_partial_request - assert bytes(message_91) == bytes(post_partial_request) + message_91 = type(post_partial_payload).from_bytes(message_bytes) + assert message_91 == post_partial_payload + assert bytes(message_91) == bytes(post_partial_payload) message_bytes, input_bytes = parse_blob(input_bytes) - message_92 = type(post_partial_response).from_bytes(message_bytes) - assert message_92 == post_partial_response - assert bytes(message_92) == bytes(post_partial_response) + message_92 = type(post_partial_request).from_bytes(message_bytes) + assert message_92 == post_partial_request + assert bytes(message_92) == bytes(post_partial_request) message_bytes, input_bytes = parse_blob(input_bytes) - message_93 = type(get_farmer_response).from_bytes(message_bytes) - assert message_93 == get_farmer_response - assert bytes(message_93) == bytes(get_farmer_response) + message_93 = type(post_partial_response).from_bytes(message_bytes) + assert message_93 == post_partial_response + assert bytes(message_93) == bytes(post_partial_response) message_bytes, input_bytes = parse_blob(input_bytes) - message_94 = type(post_farmer_payload).from_bytes(message_bytes) - assert message_94 == post_farmer_payload - assert bytes(message_94) == bytes(post_farmer_payload) + message_94 = type(get_farmer_response).from_bytes(message_bytes) + assert message_94 == get_farmer_response + assert bytes(message_94) == bytes(get_farmer_response) message_bytes, input_bytes = parse_blob(input_bytes) - message_95 = type(post_farmer_request).from_bytes(message_bytes) - assert message_95 == post_farmer_request - assert bytes(message_95) == bytes(post_farmer_request) + message_95 = type(post_farmer_payload).from_bytes(message_bytes) + assert message_95 == post_farmer_payload + assert bytes(message_95) == bytes(post_farmer_payload) message_bytes, input_bytes = parse_blob(input_bytes) - message_96 = type(post_farmer_response).from_bytes(message_bytes) - assert message_96 == post_farmer_response - assert bytes(message_96) == bytes(post_farmer_response) + message_96 = type(post_farmer_request).from_bytes(message_bytes) + assert message_96 == post_farmer_request + assert bytes(message_96) == bytes(post_farmer_request) message_bytes, input_bytes = parse_blob(input_bytes) - message_97 = type(put_farmer_payload).from_bytes(message_bytes) - assert message_97 == put_farmer_payload - assert bytes(message_97) == bytes(put_farmer_payload) + message_97 = type(post_farmer_response).from_bytes(message_bytes) + assert message_97 == post_farmer_response + assert bytes(message_97) == bytes(post_farmer_response) message_bytes, input_bytes = parse_blob(input_bytes) - message_98 = type(put_farmer_request).from_bytes(message_bytes) - assert message_98 == put_farmer_request - assert bytes(message_98) == bytes(put_farmer_request) + message_98 = type(put_farmer_payload).from_bytes(message_bytes) + assert message_98 == put_farmer_payload + assert bytes(message_98) == bytes(put_farmer_payload) message_bytes, input_bytes = parse_blob(input_bytes) - message_99 = type(put_farmer_response).from_bytes(message_bytes) - assert message_99 == put_farmer_response - assert bytes(message_99) == bytes(put_farmer_response) + message_99 = type(put_farmer_request).from_bytes(message_bytes) + assert message_99 == put_farmer_request + assert bytes(message_99) == bytes(put_farmer_request) message_bytes, input_bytes = parse_blob(input_bytes) - message_100 = type(error_response).from_bytes(message_bytes) - assert message_100 == error_response - assert bytes(message_100) == bytes(error_response) + message_100 = type(put_farmer_response).from_bytes(message_bytes) + assert message_100 == put_farmer_response + assert bytes(message_100) == bytes(put_farmer_response) message_bytes, input_bytes = parse_blob(input_bytes) - message_101 = type(new_peak_timelord).from_bytes(message_bytes) - assert message_101 == new_peak_timelord - assert bytes(message_101) == bytes(new_peak_timelord) + message_101 = type(error_response).from_bytes(message_bytes) + assert message_101 == error_response + assert bytes(message_101) == bytes(error_response) message_bytes, input_bytes = parse_blob(input_bytes) - message_102 = type(new_unfinished_block_timelord).from_bytes(message_bytes) - assert message_102 == new_unfinished_block_timelord - assert bytes(message_102) == bytes(new_unfinished_block_timelord) + message_102 = type(new_peak_timelord).from_bytes(message_bytes) + assert message_102 == new_peak_timelord + assert bytes(message_102) == bytes(new_peak_timelord) message_bytes, input_bytes = parse_blob(input_bytes) - message_103 = type(new_infusion_point_vdf).from_bytes(message_bytes) - assert message_103 == new_infusion_point_vdf - assert bytes(message_103) == bytes(new_infusion_point_vdf) + message_103 = type(new_unfinished_block_timelord).from_bytes(message_bytes) + assert message_103 == new_unfinished_block_timelord + assert bytes(message_103) == bytes(new_unfinished_block_timelord) message_bytes, input_bytes = parse_blob(input_bytes) - message_104 = type(new_signage_point_vdf).from_bytes(message_bytes) - assert message_104 == new_signage_point_vdf - assert bytes(message_104) == bytes(new_signage_point_vdf) + message_104 = type(new_infusion_point_vdf).from_bytes(message_bytes) + assert message_104 == new_infusion_point_vdf + assert bytes(message_104) == bytes(new_infusion_point_vdf) message_bytes, input_bytes = parse_blob(input_bytes) - message_105 = type(new_end_of_sub_slot_bundle).from_bytes(message_bytes) - assert message_105 == new_end_of_sub_slot_bundle - assert bytes(message_105) == bytes(new_end_of_sub_slot_bundle) + message_105 = type(new_signage_point_vdf).from_bytes(message_bytes) + assert message_105 == new_signage_point_vdf + assert bytes(message_105) == bytes(new_signage_point_vdf) message_bytes, input_bytes = parse_blob(input_bytes) - message_106 = type(request_compact_proof_of_time).from_bytes(message_bytes) - assert message_106 == request_compact_proof_of_time - assert bytes(message_106) == bytes(request_compact_proof_of_time) + message_106 = type(new_end_of_sub_slot_bundle).from_bytes(message_bytes) + assert message_106 == new_end_of_sub_slot_bundle + assert bytes(message_106) == bytes(new_end_of_sub_slot_bundle) message_bytes, input_bytes = parse_blob(input_bytes) - message_107 = type(respond_compact_proof_of_time).from_bytes(message_bytes) - assert message_107 == respond_compact_proof_of_time - assert bytes(message_107) == bytes(respond_compact_proof_of_time) + message_107 = type(request_compact_proof_of_time).from_bytes(message_bytes) + assert message_107 == request_compact_proof_of_time + assert bytes(message_107) == bytes(request_compact_proof_of_time) message_bytes, input_bytes = parse_blob(input_bytes) - message_108 = type(error_without_data).from_bytes(message_bytes) - assert message_108 == error_without_data - assert bytes(message_108) == bytes(error_without_data) + message_108 = type(respond_compact_proof_of_time).from_bytes(message_bytes) + assert message_108 == respond_compact_proof_of_time + assert bytes(message_108) == bytes(respond_compact_proof_of_time) message_bytes, input_bytes = parse_blob(input_bytes) - message_109 = type(error_with_data).from_bytes(message_bytes) - assert message_109 == error_with_data - assert bytes(message_109) == bytes(error_with_data) + message_109 = type(error_without_data).from_bytes(message_bytes) + assert message_109 == error_without_data + assert bytes(message_109) == bytes(error_without_data) + + message_bytes, input_bytes = parse_blob(input_bytes) + message_110 = type(error_with_data).from_bytes(message_bytes) + assert message_110 == error_with_data + assert bytes(message_110) == bytes(error_with_data) + + message_bytes, input_bytes = parse_blob(input_bytes) + message_111 = type(solver_info).from_bytes(message_bytes) + assert message_111 == solver_info + assert bytes(message_111) == bytes(solver_info) + + message_bytes, input_bytes = parse_blob(input_bytes) + message_112 = type(solver_response).from_bytes(message_bytes) + assert message_112 == solver_response + assert bytes(message_112) == bytes(solver_response) assert input_bytes == b"" diff --git a/chia/_tests/util/test_network_protocol_json.py b/chia/_tests/util/test_network_protocol_json.py index 3acb9240a83e..cea4321ed030 100644 --- a/chia/_tests/util/test_network_protocol_json.py +++ b/chia/_tests/util/test_network_protocol_json.py @@ -18,6 +18,8 @@ def test_protocol_json() -> None: assert type(farming_info).from_json_dict(farming_info_json) == farming_info assert str(signed_values_json) == str(signed_values.to_json_dict()) assert type(signed_values).from_json_dict(signed_values_json) == signed_values + assert str(partial_proof_json) == str(partial_proof.to_json_dict()) + assert type(partial_proof).from_json_dict(partial_proof_json) == partial_proof assert str(new_peak_json) == str(new_peak.to_json_dict()) assert type(new_peak).from_json_dict(new_peak_json) == new_peak assert str(new_transaction_json) == str(new_transaction.to_json_dict()) @@ -265,3 +267,7 @@ def test_protocol_json() -> None: assert type(error_without_data).from_json_dict(error_without_data_json) == error_without_data assert str(error_with_data_json) == str(error_with_data.to_json_dict()) assert type(error_with_data).from_json_dict(error_with_data_json) == error_with_data + assert str(solver_info_json) == str(solver_info.to_json_dict()) + assert type(solver_info).from_json_dict(solver_info_json) == solver_info + assert str(solver_response_json) == str(solver_response.to_json_dict()) + assert type(solver_response).from_json_dict(solver_response_json) == solver_response From 6159d8dae38cb5563fb152dde999b822f46ef7ad Mon Sep 17 00:00:00 2001 From: almogdepaz Date: Wed, 20 Aug 2025 10:25:46 +0300 Subject: [PATCH 33/42] more pr comments addressed --- chia/harvester/harvester_api.py | 127 +++++++++++++++--------------- chia/protocols/solver_protocol.py | 2 +- 2 files changed, 65 insertions(+), 64 deletions(-) diff --git a/chia/harvester/harvester_api.py b/chia/harvester/harvester_api.py index a1e59fd32a94..e8d3bc625614 100644 --- a/chia/harvester/harvester_api.py +++ b/chia/harvester/harvester_api.py @@ -121,61 +121,62 @@ def blocking_lookup_v2_partial_proofs(filename: Path, plot_info: PlotInfo) -> Op new_challenge.challenge_hash, new_challenge.sp_hash, ) - try: - partial_proofs = plot_info.prover.get_partial_proofs_for_challenge(sp_challenge_hash) - except Exception as e: - self.harvester.log.error(f"Exception fetching partial proof for V2 plot {filename}. {e}") + partial_proofs = plot_info.prover.get_partial_proofs_for_challenge(sp_challenge_hash) + + # If no partial proofs are found, return None + if partial_proofs is None or len(partial_proofs) == 0: return None - if partial_proofs is not None and len(partial_proofs) > 0: - # Get the appropriate difficulty for this plot - difficulty = new_challenge.difficulty - sub_slot_iters = new_challenge.sub_slot_iters - if plot_info.pool_contract_puzzle_hash is not None: - # Check for pool-specific difficulty - for pool_difficulty in new_challenge.pool_difficulties: - if pool_difficulty.pool_contract_puzzle_hash == plot_info.pool_contract_puzzle_hash: - difficulty = pool_difficulty.difficulty - sub_slot_iters = pool_difficulty.sub_slot_iters - break + # Get the appropriate difficulty for this plot + difficulty = new_challenge.difficulty + sub_slot_iters = new_challenge.sub_slot_iters + if plot_info.pool_contract_puzzle_hash is not None: + # Check for pool-specific difficulty + for pool_difficulty in new_challenge.pool_difficulties: + if pool_difficulty.pool_contract_puzzle_hash == plot_info.pool_contract_puzzle_hash: + difficulty = pool_difficulty.difficulty + sub_slot_iters = pool_difficulty.sub_slot_iters + break + + # Filter qualities that pass the required_iters check (same as V1 flow) + good_qualities = [] + sp_interval_iters = calculate_sp_interval_iters(self.harvester.constants, sub_slot_iters) + + for partial_proof in partial_proofs: + quality_str = quality_for_partial_proof(partial_proof, new_challenge.challenge_hash) + required_iters: uint64 = calculate_iterations_quality( + self.harvester.constants, + quality_str, + plot_info.prover.get_size(), # TODO: todo_v2_plots update for V2 + difficulty, + new_challenge.sp_hash, + sub_slot_iters, + new_challenge.last_tx_height, + ) - # Filter qualities that pass the required_iters check (same as V1 flow) - good_qualities = [] - sp_interval_iters = calculate_sp_interval_iters(self.harvester.constants, sub_slot_iters) + if required_iters < sp_interval_iters: + good_qualities.append(partial_proof) - for partial_proof in partial_proofs: - quality_str = quality_for_partial_proof(partial_proof, new_challenge.challenge_hash) - required_iters: uint64 = calculate_iterations_quality( - self.harvester.constants, - quality_str, - plot_info.prover.get_size(), # TODO: todo_v2_plots update for V2 - difficulty, - new_challenge.sp_hash, - sub_slot_iters, - new_challenge.last_tx_height, - ) - - if required_iters < sp_interval_iters: - good_qualities.append(partial_proof) + if len(good_qualities) == 0: + return None - if len(good_qualities) > 0: - size = plot_info.prover.get_size().size_v2 - assert size is not None - return PartialProofsData( - new_challenge.challenge_hash, - new_challenge.sp_hash, - good_qualities[0].hex() + str(filename.resolve()), - good_qualities, - new_challenge.signage_point_index, - size, - difficulty, - plot_info.pool_public_key, - plot_info.pool_contract_puzzle_hash, - plot_info.plot_public_key, - ) + size = plot_info.prover.get_size().size_v2 + assert size is not None + return PartialProofsData( + new_challenge.challenge_hash, + new_challenge.sp_hash, + good_qualities[0].hex() + str(filename.resolve()), + good_qualities, + new_challenge.signage_point_index, + size, + difficulty, + plot_info.pool_public_key, + plot_info.pool_contract_puzzle_hash, + plot_info.plot_public_key, + ) return None - except Exception as e: - self.harvester.log.error(f"Unknown error in V2 quality lookup: {e}") + except Exception: + self.harvester.log.exception("Failed V2 partial proof lookup") return None def blocking_lookup(filename: Path, plot_info: PlotInfo) -> list[tuple[bytes32, ProofOfSpace]]: @@ -331,21 +332,21 @@ async def lookup_challenge( # Passes the plot filter (does not check sp filter yet though, since we have not reached sp) # This is being executed at the beginning of the slot total += 1 - if self._plot_passes_filter(try_plot_info, new_challenge): - if try_plot_info.prover.get_version() == PlotVersion.V2: - # TODO: todo_v2_plots need to check v2 filter - v2_awaitables.append( - loop.run_in_executor( - self.harvester.executor, - blocking_lookup_v2_partial_proofs, - try_plot_filename, - try_plot_info, - ) + if not self._plot_passes_filter(try_plot_info, new_challenge): + continue + if try_plot_info.prover.get_version() == PlotVersion.V2: + v2_awaitables.append( + loop.run_in_executor( + self.harvester.executor, + blocking_lookup_v2_partial_proofs, + try_plot_filename, + try_plot_info, ) - passed += 1 - else: - passed += 1 - awaitables.append(lookup_challenge(try_plot_filename, try_plot_info)) + ) + passed += 1 + else: + passed += 1 + awaitables.append(lookup_challenge(try_plot_filename, try_plot_info)) self.harvester.log.debug(f"new_signage_point_harvester {passed} plots passed the plot filter") # Concurrently executes all lookups on disk, to take advantage of multiple disk parallelism diff --git a/chia/protocols/solver_protocol.py b/chia/protocols/solver_protocol.py index a3c1bb41b368..31736965a5c6 100644 --- a/chia/protocols/solver_protocol.py +++ b/chia/protocols/solver_protocol.py @@ -10,7 +10,7 @@ @streamable @dataclass(frozen=True) class SolverInfo(Streamable): - plot_difficulty: uint64 + plot_strength: uint64 partial_proof: bytes # 16 * k bits blob, k (plot size) can be derived from this From f734c44a928d95c1a46f96d09cc719fd70f335ab Mon Sep 17 00:00:00 2001 From: almogdepaz Date: Wed, 20 Aug 2025 10:32:36 +0300 Subject: [PATCH 34/42] fix rename --- chia/_tests/solver/test_solver_service.py | 4 ++-- chia/farmer/farmer_api.py | 2 +- chia/solver/solver_api.py | 2 +- chia/solver/solver_rpc_api.py | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/chia/_tests/solver/test_solver_service.py b/chia/_tests/solver/test_solver_service.py index 75a6eb39afd4..3b2047adf119 100644 --- a/chia/_tests/solver/test_solver_service.py +++ b/chia/_tests/solver/test_solver_service.py @@ -23,7 +23,7 @@ async def test_solver_api_methods(blockchain_constants: ConsensusConstants, tmp_ solver = solver_service._node solver_api = solver_service._api assert solver_api.ready() is True - test_info = SolverInfo(plot_difficulty=uint64(1500), partial_proof=b"test_partial_proof_42") + test_info = SolverInfo(plot_strength=uint64(1500), partial_proof=b"test_partial_proof_42") expected_proof = b"test_proof_data_12345" with patch.object(solver, "solve", return_value=expected_proof): api_result = await solver_api.solve(test_info) @@ -49,7 +49,7 @@ async def test_solver_error_handling( pass # expected # test solver handles exception in solve method solver = solver_service._node - test_info = SolverInfo(plot_difficulty=uint64(1000), partial_proof=b"test_partial_proof_zeros") + test_info = SolverInfo(plot_strength=uint64(1000), partial_proof=b"test_partial_proof_zeros") with patch.object(solver, "solve", side_effect=RuntimeError("test error")): # solver api should handle exceptions gracefully result = await solver_service._api.solve(test_info) diff --git a/chia/farmer/farmer_api.py b/chia/farmer/farmer_api.py index e2c057127be0..5d6c88cc4950 100644 --- a/chia/farmer/farmer_api.py +++ b/chia/farmer/farmer_api.py @@ -506,7 +506,7 @@ async def partial_proofs(self, partial_proof_data: PartialProofsData, peer: WSCh # Process each quality chain through solver service to get full proofs for partial_proof in partial_proof_data.partial_proofs: solver_info = SolverInfo( - plot_difficulty=partial_proof_data.difficulty, + plot_strength=partial_proof_data.difficulty, partial_proof=partial_proof, ) diff --git a/chia/solver/solver_api.py b/chia/solver/solver_api.py index 1de6ba57a7ff..30b3cf3ce3cc 100644 --- a/chia/solver/solver_api.py +++ b/chia/solver/solver_api.py @@ -40,7 +40,7 @@ async def solve( self.log.error("Solver is not started") return None - self.log.debug(f"Solving quality {request.partial_proof.hex()}with difficulty {request.plot_difficulty}") + self.log.debug(f"Solving quality {request.partial_proof.hex()}with difficulty {request.plot_strength}") try: proof = self.solver.solve(request) diff --git a/chia/solver/solver_rpc_api.py b/chia/solver/solver_rpc_api.py index c213ff4532cb..b6241405b7f0 100644 --- a/chia/solver/solver_rpc_api.py +++ b/chia/solver/solver_rpc_api.py @@ -32,11 +32,11 @@ async def _state_changed(self, change: str, change_data: Optional[dict[str, Any] async def solve(self, request: dict[str, Any]) -> EndpointResult: # extract all required fields from request partial_proof = request["partial_proof"] - plot_difficulty = request.get("plot_difficulty", 1000) # todo default ? + plot_strength = request.get("plot_difficulty", 1000) # todo default ? # create complete SolverInfo object with all provided data solver_info = SolverInfo( - plot_difficulty=uint64(plot_difficulty), + plot_strength == uint64(plot_strength), partial_proof=bytes.fromhex(partial_proof), ) From 3c3e160f8d37b8ee0fc300586755c11ba8bf1720 Mon Sep 17 00:00:00 2001 From: almogdepaz Date: Wed, 20 Aug 2025 13:20:29 +0300 Subject: [PATCH 35/42] fix network formatting error --- chia/_tests/util/protocol_messages_json.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia/_tests/util/protocol_messages_json.py b/chia/_tests/util/protocol_messages_json.py index 33bb23dd7b91..8b59f697afd2 100644 --- a/chia/_tests/util/protocol_messages_json.py +++ b/chia/_tests/util/protocol_messages_json.py @@ -2715,7 +2715,7 @@ error_with_data_json: dict[str, Any] = {"code": 1, "message": "Unknown", "data": "0x65787472612064617461"} -solver_info_json: dict[str, Any] = {"plot_difficulty": 2, "partial_proof": "0x7061727469616c2d70726f6f66"} +solver_info_json: dict[str, Any] = {"plot_strength": 2, "partial_proof": "0x7061727469616c2d70726f6f66"} solver_response_json: dict[str, Any] = { "partial_proof": "0x7061727469616c2d70726f6f66", From 6f0352a033bd10b8e291478bfcfde840c5b825af Mon Sep 17 00:00:00 2001 From: almogdepaz Date: Wed, 20 Aug 2025 13:48:02 +0300 Subject: [PATCH 36/42] change to Assertion errors --- chia/_tests/plotting/test_prover.py | 4 ++-- chia/farmer/farmer.py | 2 +- chia/plotting/prover.py | 7 +++---- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/chia/_tests/plotting/test_prover.py b/chia/_tests/plotting/test_prover.py index 50beceabd888..4e77a5030185 100644 --- a/chia/_tests/plotting/test_prover.py +++ b/chia/_tests/plotting/test_prover.py @@ -26,9 +26,9 @@ def test_v2_prover_get_memo_raises_error(self) -> None: with pytest.raises(NotImplementedError, match="V2 plot format is not yet implemented"): prover.get_memo() - def test_v2_prover_get_compression_level_raises_error(self) -> None: + def test_v2_prover_get_compression_level_raises_assertion_error(self) -> None: prover = V2Prover("/nonexistent/path/test.plot2") - with pytest.raises(NotImplementedError, match="V2 plot format does not support compression level"): + with pytest.raises(AssertionError, match="get_compression_level\\(\\) should never be called on V2 plots"): prover.get_compression_level() def test_v2_prover_get_id_raises_error(self) -> None: diff --git a/chia/farmer/farmer.py b/chia/farmer/farmer.py index e4a06fdc5805..1634eff2619c 100644 --- a/chia/farmer/farmer.py +++ b/chia/farmer/farmer.py @@ -143,7 +143,7 @@ def __init__( # Quality string to plot identifier and challenge_hash, for use with harvester.RequestSignatures self.quality_str_to_identifiers: dict[bytes32, tuple[str, bytes32, bytes32, bytes32]] = {} - # Track pending solver requests, keyed by quality string hex + # Track pending solver requests, keyed by partial proof self.pending_solver_requests: dict[bytes, dict[str, Any]] = {} # number of responses to each signage point diff --git a/chia/plotting/prover.py b/chia/plotting/prover.py index 38923eef6739..3a171dba451d 100644 --- a/chia/plotting/prover.py +++ b/chia/plotting/prover.py @@ -56,7 +56,7 @@ def get_memo(self) -> bytes: raise NotImplementedError("V2 plot format is not yet implemented") def get_compression_level(self) -> uint8: - raise NotImplementedError("V2 plot format does not support compression level") + raise AssertionError("get_compression_level() should never be called on V2 plots") def get_version(self) -> PlotVersion: return PlotVersion.V2 @@ -78,8 +78,7 @@ def get_partial_proofs_for_challenge(self, challenge: bytes32) -> list[bytes]: raise NotImplementedError("V2 plot format is not yet implemented") def get_full_proof(self, challenge: bytes32, index: int, parallel_read: bool = True) -> bytes: - # TODO: todo_v2_plots Implement plot proof generation - raise NotImplementedError("V2 plot format require solver to get full proof") + raise AssertionError("V2 plot format require solver to get full proof") @classmethod def from_bytes(cls, data: bytes) -> V2Prover: @@ -121,7 +120,7 @@ def get_qualities_for_challenge(self, challenge: bytes32) -> list[bytes32]: return [bytes32(quality) for quality in self._disk_prover.get_qualities_for_challenge(challenge)] def get_partial_proofs_for_challenge(self, challenge: bytes32) -> list[bytes]: - raise NotImplementedError("V1 does not implement quality chains, only qualities") + raise AssertionError("V1 does not implement quality chains, only qualities") def get_full_proof(self, challenge: bytes32, index: int, parallel_read: bool = True) -> bytes: return bytes(self._disk_prover.get_full_proof(challenge, index, parallel_read)) From b240f98e493dfb5a93dede04363b25dd8f6b21fc Mon Sep 17 00:00:00 2001 From: almogdepaz Date: Wed, 20 Aug 2025 13:55:08 +0300 Subject: [PATCH 37/42] add todo --- chia/plotting/check_plots.py | 1 + 1 file changed, 1 insertion(+) diff --git a/chia/plotting/check_plots.py b/chia/plotting/check_plots.py index 9bbe42735144..4c8f63a45c49 100644 --- a/chia/plotting/check_plots.py +++ b/chia/plotting/check_plots.py @@ -186,6 +186,7 @@ def process_plot(plot_path: Path, plot_info: PlotInfo, num_start: int, num_end: # Other plot errors cause get_full_proof or validate_proof to throw an AssertionError try: proof_start_time = round(time() * 1000) + # TODO : todo_v2_plots handle v2 plots proof = pr.get_full_proof(challenge, index, parallel_read) proof_spent_time = round(time() * 1000) - proof_start_time if proof_spent_time > 15000: From 82d32526817b8ce59c58d17fd0a64a145a207364 Mon Sep 17 00:00:00 2001 From: almogdepaz Date: Wed, 20 Aug 2025 16:14:11 +0300 Subject: [PATCH 38/42] remove strength from SolverInfo --- chia/_tests/solver/test_solver_service.py | 5 ++--- chia/_tests/util/network_protocol_data.py | 2 +- chia/_tests/util/protocol_messages_bytes-v1.0 | Bin 50900 -> 50892 bytes chia/_tests/util/protocol_messages_json.py | 2 +- chia/cmds/solver_funcs.py | 2 +- chia/farmer/farmer_api.py | 5 +---- chia/harvester/harvester_api.py | 2 +- chia/protocols/solver_protocol.py | 3 --- chia/solver/solver.py | 5 ++--- chia/solver/solver_api.py | 14 +++++++------- chia/solver/solver_rpc_api.py | 14 +------------- chia/solver/solver_rpc_client.py | 9 +++------ 12 files changed, 20 insertions(+), 43 deletions(-) diff --git a/chia/_tests/solver/test_solver_service.py b/chia/_tests/solver/test_solver_service.py index 3b2047adf119..4ab9e45cb53d 100644 --- a/chia/_tests/solver/test_solver_service.py +++ b/chia/_tests/solver/test_solver_service.py @@ -5,7 +5,6 @@ import pytest from chia_rs import ConsensusConstants -from chia_rs.sized_ints import uint64 from chia.protocols.outbound_message import Message from chia.protocols.solver_protocol import SolverInfo @@ -23,7 +22,7 @@ async def test_solver_api_methods(blockchain_constants: ConsensusConstants, tmp_ solver = solver_service._node solver_api = solver_service._api assert solver_api.ready() is True - test_info = SolverInfo(plot_strength=uint64(1500), partial_proof=b"test_partial_proof_42") + test_info = SolverInfo(partial_proof=b"test_partial_proof_42") expected_proof = b"test_proof_data_12345" with patch.object(solver, "solve", return_value=expected_proof): api_result = await solver_api.solve(test_info) @@ -49,7 +48,7 @@ async def test_solver_error_handling( pass # expected # test solver handles exception in solve method solver = solver_service._node - test_info = SolverInfo(plot_strength=uint64(1000), partial_proof=b"test_partial_proof_zeros") + test_info = SolverInfo(partial_proof=b"test_partial_proof_zeros") with patch.object(solver, "solve", side_effect=RuntimeError("test error")): # solver api should handle exceptions gracefully result = await solver_service._api.solve(test_info) diff --git a/chia/_tests/util/network_protocol_data.py b/chia/_tests/util/network_protocol_data.py index 90ecbffb5af0..da04c998dc21 100644 --- a/chia/_tests/util/network_protocol_data.py +++ b/chia/_tests/util/network_protocol_data.py @@ -1106,6 +1106,6 @@ ) # SOLVER PROTOCOL -solver_info = solver_protocol.SolverInfo(uint64(2), b"partial-proof") +solver_info = solver_protocol.SolverInfo(partial_proof=b"partial-proof") solver_response = solver_protocol.SolverResponse(b"partial-proof", b"full-proof") diff --git a/chia/_tests/util/protocol_messages_bytes-v1.0 b/chia/_tests/util/protocol_messages_bytes-v1.0 index cc64c54d471f9dc0d6bcf7fd18b490ec0bc13e70..b171abdcca2f37053a3f2475d13eb1fbe4765d0c 100644 GIT binary patch delta 15 Xcmcc8%Y3GndBgN$jDnLF95Vp`I<5x| delta 23 acmX@p%Y3DmdBgN$oRSO>z%+TrF%tk{U Op partial_proofs = plot_info.prover.get_partial_proofs_for_challenge(sp_challenge_hash) # If no partial proofs are found, return None - if partial_proofs is None or len(partial_proofs) == 0: + if len(partial_proofs) == 0: return None # Get the appropriate difficulty for this plot diff --git a/chia/protocols/solver_protocol.py b/chia/protocols/solver_protocol.py index 31736965a5c6..891bfb846bb9 100644 --- a/chia/protocols/solver_protocol.py +++ b/chia/protocols/solver_protocol.py @@ -2,15 +2,12 @@ from dataclasses import dataclass -from chia_rs.sized_ints import uint64 - from chia.util.streamable import Streamable, streamable @streamable @dataclass(frozen=True) class SolverInfo(Streamable): - plot_strength: uint64 partial_proof: bytes # 16 * k bits blob, k (plot size) can be derived from this diff --git a/chia/solver/solver.py b/chia/solver/solver.py index fef9dcc43531..eedf3b7804e7 100644 --- a/chia/solver/solver.py +++ b/chia/solver/solver.py @@ -11,7 +11,6 @@ from chia_rs import ConsensusConstants from chia.protocols.outbound_message import NodeType -from chia.protocols.solver_protocol import SolverInfo from chia.rpc.rpc_server import StateChangedProtocol, default_get_connections from chia.server.server import ChiaServer from chia.server.ws_connection import WSChiaConnection @@ -66,8 +65,8 @@ async def manage(self) -> AsyncIterator[None]: self.executor.shutdown(wait=True) self.log.info("Solver service shutdown complete") - def solve(self, info: SolverInfo) -> Optional[bytes]: - self.log.debug(f"Solve request: quality={info.partial_proof.hex()}") + def solve(self, partial_proof: bytes) -> Optional[bytes]: + self.log.debug(f"Solve request: quality={partial_proof.hex()}") # TODO todo_v2_plots implement actualy calling the solver return None diff --git a/chia/solver/solver_api.py b/chia/solver/solver_api.py index 30b3cf3ce3cc..4ae365fc17c1 100644 --- a/chia/solver/solver_api.py +++ b/chia/solver/solver_api.py @@ -33,27 +33,27 @@ async def solve( request: SolverInfo, ) -> Optional[Message]: """ - Solve a V2 plot quality to get the full proof of space. - This is called by the farmer when it receives V2 qualities from harvester. + Solve a V2 plot partial proof to get the full proof of space. + This is called by the farmer when it receives V2 parital proofs from harvester. """ if not self.solver.started: self.log.error("Solver is not started") return None - self.log.debug(f"Solving quality {request.partial_proof.hex()}with difficulty {request.plot_strength}") + self.log.debug(f"Solving partial {request.partial_proof.hex()}") try: - proof = self.solver.solve(request) + proof = self.solver.solve(request.partial_proof) if proof is None: - self.log.warning(f"Solver returned no proof for quality {request.partial_proof.hex()}") + self.log.warning(f"Solver returned no proof for parital {request.partial_proof.hex()}") return None - self.log.debug(f"Successfully solved quality, returning {len(proof)} byte proof") + self.log.debug(f"Successfully solved partial proof, returning {len(proof)} byte proof") return make_msg( ProtocolMessageTypes.solution_response, SolverResponse(proof=proof, partial_proof=request.partial_proof), ) except Exception as e: - self.log.error(f"Error solving quality {request.partial_proof.hex()}: {e}") + self.log.error(f"Error solving parital {request.partial_proof.hex()}: {e}") return None diff --git a/chia/solver/solver_rpc_api.py b/chia/solver/solver_rpc_api.py index b6241405b7f0..8be348e944ba 100644 --- a/chia/solver/solver_rpc_api.py +++ b/chia/solver/solver_rpc_api.py @@ -2,9 +2,6 @@ from typing import TYPE_CHECKING, Any, ClassVar, Optional, cast -from chia_rs.sized_ints import uint64 - -from chia.protocols.solver_protocol import SolverInfo from chia.rpc.rpc_server import Endpoint, EndpointResult from chia.solver.solver import Solver from chia.util.ws_message import WsRpcMessage @@ -30,17 +27,8 @@ async def _state_changed(self, change: str, change_data: Optional[dict[str, Any] return [] async def solve(self, request: dict[str, Any]) -> EndpointResult: - # extract all required fields from request partial_proof = request["partial_proof"] - plot_strength = request.get("plot_difficulty", 1000) # todo default ? - - # create complete SolverInfo object with all provided data - solver_info = SolverInfo( - plot_strength == uint64(plot_strength), - partial_proof=bytes.fromhex(partial_proof), - ) - - proof = self.service.solve(solver_info) + proof = self.service.solve(bytes.fromhex(partial_proof)) return {"proof": proof.hex() if proof else None} async def get_state(self, _: dict[str, Any]) -> EndpointResult: diff --git a/chia/solver/solver_rpc_client.py b/chia/solver/solver_rpc_client.py index 612f1aad3e42..9cc0e2d9e783 100644 --- a/chia/solver/solver_rpc_client.py +++ b/chia/solver/solver_rpc_client.py @@ -2,8 +2,6 @@ from typing import Any -from chia_rs.sized_bytes import bytes32 - from chia.rpc.rpc_client import RpcClient @@ -17,7 +15,6 @@ async def get_state(self) -> dict[str, Any]: """Get solver state.""" return await self.fetch("get_state", {}) - async def solve(self, quality_string: str, plot_size: int = 32, plot_difficulty: int = 1000) -> dict[str, Any]: - """Solve a quality string with optional plot size and difficulty.""" - quality = bytes32.from_hexstr(quality_string) - return await self.fetch("solve", {"partial_proof": quality.hex(), "plot_difficulty": plot_difficulty}) + async def solve(self, partial_proof: str) -> dict[str, Any]: + """Solve a partial proof.""" + return await self.fetch("solve", {"partial_proof": partial_proof}) From bbc9fc08cd0c17295c9a100570fc2bd3ef2d1867 Mon Sep 17 00:00:00 2001 From: almogdepaz Date: Wed, 20 Aug 2025 17:49:39 +0300 Subject: [PATCH 39/42] more pr comments, fix test to catch assertion --- chia/_tests/plotting/test_prover.py | 2 +- chia/cmds/solver.py | 32 +++++++---------------------- chia/cmds/solver_funcs.py | 12 +++++------ 3 files changed, 13 insertions(+), 33 deletions(-) diff --git a/chia/_tests/plotting/test_prover.py b/chia/_tests/plotting/test_prover.py index 4e77a5030185..c776841cce87 100644 --- a/chia/_tests/plotting/test_prover.py +++ b/chia/_tests/plotting/test_prover.py @@ -43,7 +43,7 @@ def test_v2_prover_get_qualities_for_challenge_raises_error(self) -> None: def test_v2_prover_get_full_proof_raises_error(self) -> None: prover = V2Prover("/nonexistent/path/test.plot2") - with pytest.raises(NotImplementedError, match="V2 plot format require solver to get full proof"): + with pytest.raises(AssertionError, match="V2 plot format require solver to get full proof"): prover.get_full_proof(bytes32(b"1" * 32), 0) def test_v2_prover_bytes_raises_error(self) -> None: diff --git a/chia/cmds/solver.py b/chia/cmds/solver.py index a8965ee9f63a..5e8f3b5d97d9 100644 --- a/chia/cmds/solver.py +++ b/chia/cmds/solver.py @@ -33,7 +33,7 @@ def get_state_cmd( asyncio.run(get_state(ChiaCliContext.set_default(ctx), solver_rpc_port)) -@solver_cmd.command("solve", help="Solve a quality string") +@solver_cmd.command("solve", help="Solve a partial proof of space, turning it into a full proof") @click.option( "-sp", "--solver-rpc-port", @@ -43,38 +43,20 @@ def get_state_cmd( show_default=True, ) @click.option( - "-q", - "--quality", - help="Quality string to solve (hex format)", + "-p", + "--partial_proof", + help="partial proof string to solve (hex format)", type=str, required=True, ) -@click.option( - "-k", - "--plot-size", - help="Plot size (k value, default: 32)", - type=int, - default=32, - show_default=True, -) -@click.option( - "-d", - "--difficulty", - help="Plot difficulty (default: 1000)", - type=int, - default=1000, - show_default=True, -) @click.pass_context def solve_cmd( ctx: click.Context, solver_rpc_port: Optional[int], - quality: str, - plot_size: int, - difficulty: int, + partial_proof: str, ) -> None: import asyncio - from chia.cmds.solver_funcs import solve_quality + from chia.cmds.solver_funcs import solve_partial_proof - asyncio.run(solve_quality(ChiaCliContext.set_default(ctx), solver_rpc_port, quality, plot_size, difficulty)) + asyncio.run(solve_partial_proof(ChiaCliContext.set_default(ctx), solver_rpc_port, partial_proof)) diff --git a/chia/cmds/solver_funcs.py b/chia/cmds/solver_funcs.py index 522ff8fdd8c8..ac364a3d1a26 100644 --- a/chia/cmds/solver_funcs.py +++ b/chia/cmds/solver_funcs.py @@ -21,17 +21,15 @@ async def get_state( print(f"Failed to get solver state: {e}") -async def solve_quality( +async def solve_partial_proof( ctx: ChiaCliContext, solver_rpc_port: Optional[int], - quality_hex: str, - plot_size: int = 32, - difficulty: int = 1000, + partial_proof: str, ) -> None: - """Solve a quality string via RPC.""" + """Solve a partial proof via RPC.""" try: async with get_any_service_client(SolverRpcClient, ctx.root_path, solver_rpc_port) as (client, _): - response = await client.solve(quality_hex) + response = await client.solve(partial_proof) print(json.dumps(response, indent=2)) except Exception as e: - print(f"Failed to solve quality: {e}") + print(f"Failed to solve partial proof: {e}") From e6c5e885e1783583522f92d0a234e6f4107bc520 Mon Sep 17 00:00:00 2001 From: almogdepaz Date: Thu, 21 Aug 2025 10:24:05 +0300 Subject: [PATCH 40/42] revert strength --- .../farmer_harvester/test_farmer_harvester.py | 6 -- chia/_tests/solver/test_solver_service.py | 26 ----- chia/_tests/util/network_protocol_data.py | 1 - chia/_tests/util/protocol_messages_bytes-v1.0 | Bin 50892 -> 50884 bytes chia/_tests/util/protocol_messages_json.py | 1 - chia/cmds/solver.py | 29 ------ chia/cmds/solver_funcs.py | 14 --- chia/harvester/harvester_api.py | 95 ++++++++++++------ chia/protocols/harvester_protocol.py | 1 - chia/solver/solver.py | 2 +- chia/solver/solver_rpc_api.py | 2 +- chia/solver/solver_rpc_client.py | 4 - 12 files changed, 66 insertions(+), 115 deletions(-) diff --git a/chia/_tests/farmer_harvester/test_farmer_harvester.py b/chia/_tests/farmer_harvester/test_farmer_harvester.py index ad5af0ca1442..17cd8a5c63fb 100644 --- a/chia/_tests/farmer_harvester/test_farmer_harvester.py +++ b/chia/_tests/farmer_harvester/test_farmer_harvester.py @@ -341,7 +341,6 @@ async def test_v2_partial_proofs_new_sp_hash( partial_proofs=[b"test_partial_proof_1"], signage_point_index=uint8(0), plot_size=uint8(32), - difficulty=uint64(1000), pool_public_key=None, pool_contract_puzzle_hash=bytes32(b"4" * 32), plot_public_key=G1Element(), @@ -371,7 +370,6 @@ async def test_v2_partial_proofs_missing_sp_hash( partial_proofs=[b"test_partial_proof_1"], signage_point_index=uint8(0), plot_size=uint8(32), - difficulty=uint64(1000), pool_public_key=None, pool_contract_puzzle_hash=bytes32(b"4" * 32), plot_public_key=G1Element(), @@ -414,7 +412,6 @@ async def test_v2_partial_proofs_with_existing_sp( partial_proofs=[b"test_partial_proof_1", b"test_partial_proof_2"], signage_point_index=uint8(0), plot_size=uint8(32), - difficulty=uint64(1000), pool_public_key=G1Element(), pool_contract_puzzle_hash=bytes32(b"4" * 32), plot_public_key=G1Element(), @@ -448,7 +445,6 @@ async def test_solution_response_handler( partial_proofs=[b"test_partial_proof_for_quality"], signage_point_index=uint8(0), plot_size=uint8(32), - difficulty=uint64(1000), pool_public_key=G1Element(), pool_contract_puzzle_hash=bytes32(b"4" * 32), plot_public_key=G1Element(), @@ -525,7 +521,6 @@ async def test_solution_response_empty_proof( partial_proofs=[b"test_partial_proof_for_quality"], signage_point_index=uint8(0), plot_size=uint8(32), - difficulty=uint64(1000), pool_public_key=G1Element(), pool_contract_puzzle_hash=bytes32(b"4" * 32), plot_public_key=G1Element(), @@ -587,7 +582,6 @@ async def test_v2_partial_proofs_solver_exception( partial_proofs=[b"test_partial_proof_1"], signage_point_index=uint8(0), plot_size=uint8(32), - difficulty=uint64(1000), pool_public_key=G1Element(), pool_contract_puzzle_hash=bytes32(b"4" * 32), plot_public_key=G1Element(), diff --git a/chia/_tests/solver/test_solver_service.py b/chia/_tests/solver/test_solver_service.py index 4ab9e45cb53d..ccb683a9ef39 100644 --- a/chia/_tests/solver/test_solver_service.py +++ b/chia/_tests/solver/test_solver_service.py @@ -11,7 +11,6 @@ from chia.simulator.block_tools import create_block_tools_async from chia.simulator.keyring import TempKeyring from chia.simulator.setup_services import setup_solver -from chia.solver.solver_rpc_client import SolverRpcClient @pytest.mark.anyio @@ -28,28 +27,3 @@ async def test_solver_api_methods(blockchain_constants: ConsensusConstants, tmp_ api_result = await solver_api.solve(test_info) assert api_result is not None assert isinstance(api_result, Message) - - -@pytest.mark.anyio -async def test_solver_error_handling( - blockchain_constants: ConsensusConstants, self_hostname: str, tmp_path: Path -) -> None: - with TempKeyring(populate=True) as keychain: - bt = await create_block_tools_async(constants=blockchain_constants, keychain=keychain) - async with setup_solver(tmp_path, bt, blockchain_constants) as solver_service: - assert solver_service.rpc_server is not None - solver_rpc_client = await SolverRpcClient.create( - self_hostname, solver_service.rpc_server.listen_port, solver_service.root_path, solver_service.config - ) - try: - await solver_rpc_client.solve("invalid_hex") - assert False, "should have raised exception for invalid hex" - except Exception: - pass # expected - # test solver handles exception in solve method - solver = solver_service._node - test_info = SolverInfo(partial_proof=b"test_partial_proof_zeros") - with patch.object(solver, "solve", side_effect=RuntimeError("test error")): - # solver api should handle exceptions gracefully - result = await solver_service._api.solve(test_info) - assert result is None # api returns None on error diff --git a/chia/_tests/util/network_protocol_data.py b/chia/_tests/util/network_protocol_data.py index da04c998dc21..9b0bd7efec68 100644 --- a/chia/_tests/util/network_protocol_data.py +++ b/chia/_tests/util/network_protocol_data.py @@ -158,7 +158,6 @@ [b"partial-proof1", b"partial-proof2"], uint8(4), uint8(32), - uint64(100000), G1Element.from_bytes( bytes.fromhex( "a04c6b5ac7dfb935f6feecfdd72348ccf1d4be4fe7e26acf271ea3b7d308da61e0a308f7a62495328a81f5147b66634c" diff --git a/chia/_tests/util/protocol_messages_bytes-v1.0 b/chia/_tests/util/protocol_messages_bytes-v1.0 index b171abdcca2f37053a3f2475d13eb1fbe4765d0c..c14f08a3d12f078936b967636cada3672126d5e0 100644 GIT binary patch delta 25 hcmX@p%Y3Ald4nD+6T{!h2CUVS1BDGYKVf}w004f53PJz? delta 34 ncmX@o%Y3Gnd4nD+6C>+n1J-H|1`uFuTQE7$-f;6B))xl=s&)#V diff --git a/chia/_tests/util/protocol_messages_json.py b/chia/_tests/util/protocol_messages_json.py index 22f25843135b..c21b0157b68e 100644 --- a/chia/_tests/util/protocol_messages_json.py +++ b/chia/_tests/util/protocol_messages_json.py @@ -72,7 +72,6 @@ "partial_proofs": ["0x7061727469616c2d70726f6f6631", "0x7061727469616c2d70726f6f6632"], "signage_point_index": 4, "plot_size": 32, - "difficulty": 100000, "pool_public_key": "0xa04c6b5ac7dfb935f6feecfdd72348ccf1d4be4fe7e26acf271ea3b7d308da61e0a308f7a62495328a81f5147b66634c", "pool_contract_puzzle_hash": "0x91240fbacdf93b44c0571caa74fd99f163d4c5d7deaedac87125528721493f7a", "plot_public_key": "0xa04c6b5ac7dfb935f6feecfdd72348ccf1d4be4fe7e26acf271ea3b7d308da61e0a308f7a62495328a81f5147b66634c", diff --git a/chia/cmds/solver.py b/chia/cmds/solver.py index 5e8f3b5d97d9..4b34a5e7b14e 100644 --- a/chia/cmds/solver.py +++ b/chia/cmds/solver.py @@ -31,32 +31,3 @@ def get_state_cmd( from chia.cmds.solver_funcs import get_state asyncio.run(get_state(ChiaCliContext.set_default(ctx), solver_rpc_port)) - - -@solver_cmd.command("solve", help="Solve a partial proof of space, turning it into a full proof") -@click.option( - "-sp", - "--solver-rpc-port", - help="Set the port where the Solver is hosting the RPC interface. See the rpc_port under solver in config.yaml", - type=int, - default=None, - show_default=True, -) -@click.option( - "-p", - "--partial_proof", - help="partial proof string to solve (hex format)", - type=str, - required=True, -) -@click.pass_context -def solve_cmd( - ctx: click.Context, - solver_rpc_port: Optional[int], - partial_proof: str, -) -> None: - import asyncio - - from chia.cmds.solver_funcs import solve_partial_proof - - asyncio.run(solve_partial_proof(ChiaCliContext.set_default(ctx), solver_rpc_port, partial_proof)) diff --git a/chia/cmds/solver_funcs.py b/chia/cmds/solver_funcs.py index ac364a3d1a26..91da321ba5ae 100644 --- a/chia/cmds/solver_funcs.py +++ b/chia/cmds/solver_funcs.py @@ -19,17 +19,3 @@ async def get_state( print(json.dumps(response, indent=2)) except Exception as e: print(f"Failed to get solver state: {e}") - - -async def solve_partial_proof( - ctx: ChiaCliContext, - solver_rpc_port: Optional[int], - partial_proof: str, -) -> None: - """Solve a partial proof via RPC.""" - try: - async with get_any_service_client(SolverRpcClient, ctx.root_path, solver_rpc_port) as (client, _): - response = await client.solve(partial_proof) - print(json.dumps(response, indent=2)) - except Exception as e: - print(f"Failed to solve partial proof: {e}") diff --git a/chia/harvester/harvester_api.py b/chia/harvester/harvester_api.py index 91933119bf11..0e4c981efd40 100644 --- a/chia/harvester/harvester_api.py +++ b/chia/harvester/harvester_api.py @@ -3,6 +3,7 @@ import asyncio import logging import time +from collections.abc import Awaitable, Sequence from pathlib import Path from typing import TYPE_CHECKING, ClassVar, Optional, cast @@ -65,6 +66,47 @@ def _plot_passes_filter(self, plot_info: PlotInfo, challenge: harvester_protocol challenge.sp_hash, ) + async def _handle_v1_responses( + self, + awaitables: Sequence[Awaitable[tuple[Path, list[harvester_protocol.NewProofOfSpace]]]], + start_time: float, + peer: WSChiaConnection, + ) -> int: + proofs_found = 0 + for filename_sublist_awaitable in asyncio.as_completed(awaitables): + filename, sublist = await filename_sublist_awaitable + time_taken = time.monotonic() - start_time + if time_taken > 8: + self.harvester.log.warning( + f"Looking up qualities on {filename} took: {time_taken}. This should be below 8 seconds" + f" to minimize risk of losing rewards." + ) + for response in sublist: + proofs_found += 1 + msg = make_msg(ProtocolMessageTypes.new_proof_of_space, response) + await peer.send_message(msg) + return proofs_found + + async def _handle_v2_responses( + self, v2_awaitables: Sequence[Awaitable[Optional[PartialProofsData]]], start_time: float, peer: WSChiaConnection + ) -> int: + partial_proofs_found = 0 + for quality_awaitable in asyncio.as_completed(v2_awaitables): + partial_proofs_data = await quality_awaitable + if partial_proofs_data is None: + continue + time_taken = time.monotonic() - start_time + if time_taken > 8: + self.harvester.log.warning( + f"Looking up partial proofs on {partial_proofs_data.plot_identifier}" + f"took: {time_taken}. This should be below 8 seconds" + f"to minimize risk of losing rewards." + ) + partial_proofs_found += len(partial_proofs_data.partial_proofs) + msg = make_msg(ProtocolMessageTypes.partial_proofs, partial_proofs_data) + await peer.send_message(msg) + return partial_proofs_found + @metadata.request(peer_required=True) async def harvester_handshake( self, harvester_handshake: harvester_protocol.HarvesterHandshake, peer: WSChiaConnection @@ -139,7 +181,7 @@ def blocking_lookup_v2_partial_proofs(filename: Path, plot_info: PlotInfo) -> Op break # Filter qualities that pass the required_iters check (same as V1 flow) - good_qualities = [] + good_partial_proofs = [] sp_interval_iters = calculate_sp_interval_iters(self.harvester.constants, sub_slot_iters) for partial_proof in partial_proofs: @@ -147,7 +189,7 @@ def blocking_lookup_v2_partial_proofs(filename: Path, plot_info: PlotInfo) -> Op required_iters: uint64 = calculate_iterations_quality( self.harvester.constants, quality_str, - plot_info.prover.get_size(), # TODO: todo_v2_plots update for V2 + plot_info.prover.get_size(), difficulty, new_challenge.sp_hash, sub_slot_iters, @@ -155,9 +197,9 @@ def blocking_lookup_v2_partial_proofs(filename: Path, plot_info: PlotInfo) -> Op ) if required_iters < sp_interval_iters: - good_qualities.append(partial_proof) + good_partial_proofs.append(partial_proof) - if len(good_qualities) == 0: + if len(good_partial_proofs) == 0: return None size = plot_info.prover.get_size().size_v2 @@ -165,11 +207,10 @@ def blocking_lookup_v2_partial_proofs(filename: Path, plot_info: PlotInfo) -> Op return PartialProofsData( new_challenge.challenge_hash, new_challenge.sp_hash, - good_qualities[0].hex() + str(filename.resolve()), - good_qualities, + good_partial_proofs[0].hex() + str(filename.resolve()), + good_partial_proofs, new_challenge.signage_point_index, size, - difficulty, plot_info.pool_public_key, plot_info.pool_contract_puzzle_hash, plot_info.plot_public_key, @@ -354,30 +395,22 @@ async def lookup_challenge( total_proofs_found = 0 total_v2_partial_proofs_found = 0 - # Process V1 plot responses (existing flow) - for filename_sublist_awaitable in asyncio.as_completed(awaitables): - filename, sublist = await filename_sublist_awaitable - time_taken = time.monotonic() - start - if time_taken > 8: - self.harvester.log.warning( - f"Looking up qualities on {filename} took: {time_taken}. This should be below 8 seconds" - f" to minimize risk of losing rewards." - ) - else: - pass - # self.harvester.log.info(f"Looking up qualities on {filename} took: {time_taken}") - for response in sublist: - total_proofs_found += 1 - msg = make_msg(ProtocolMessageTypes.new_proof_of_space, response) - await peer.send_message(msg) - - # Process V2 plot quality collections (new flow) - for quality_awaitable in asyncio.as_completed(v2_awaitables): - partial_proofs_data = await quality_awaitable - if partial_proofs_data is not None: - total_v2_partial_proofs_found += len(partial_proofs_data.partial_proofs) - msg = make_msg(ProtocolMessageTypes.partial_proofs, partial_proofs_data) - await peer.send_message(msg) + # run both concurrently + tasks = [] + if awaitables: + tasks.append(self._handle_v1_responses(awaitables, start, peer)) + if v2_awaitables: + tasks.append(self._handle_v2_responses(v2_awaitables, start, peer)) + + if tasks: + results = await asyncio.gather(*tasks) + if len(results) == 2: + total_proofs_found, total_v2_partial_proofs_found = results + elif len(results) == 1: + if awaitables: + total_proofs_found = results[0] + else: + total_v2_partial_proofs_found = results[0] now = uint64(time.time()) diff --git a/chia/protocols/harvester_protocol.py b/chia/protocols/harvester_protocol.py index 9552e8103736..e1c8414b813b 100644 --- a/chia/protocols/harvester_protocol.py +++ b/chia/protocols/harvester_protocol.py @@ -72,7 +72,6 @@ class PartialProofsData(Streamable): partial_proofs: list[bytes] # 16 * k bits blobs instead of 32-byte quality strings signage_point_index: uint8 plot_size: uint8 - difficulty: uint64 pool_public_key: Optional[G1Element] pool_contract_puzzle_hash: Optional[bytes32] plot_public_key: G1Element diff --git a/chia/solver/solver.py b/chia/solver/solver.py index eedf3b7804e7..fa7efea4a1f0 100644 --- a/chia/solver/solver.py +++ b/chia/solver/solver.py @@ -66,7 +66,7 @@ async def manage(self) -> AsyncIterator[None]: self.log.info("Solver service shutdown complete") def solve(self, partial_proof: bytes) -> Optional[bytes]: - self.log.debug(f"Solve request: quality={partial_proof.hex()}") + self.log.debug(f"Solve request: partial={partial_proof.hex()}") # TODO todo_v2_plots implement actualy calling the solver return None diff --git a/chia/solver/solver_rpc_api.py b/chia/solver/solver_rpc_api.py index 8be348e944ba..d129d31eada3 100644 --- a/chia/solver/solver_rpc_api.py +++ b/chia/solver/solver_rpc_api.py @@ -28,7 +28,7 @@ async def _state_changed(self, change: str, change_data: Optional[dict[str, Any] async def solve(self, request: dict[str, Any]) -> EndpointResult: partial_proof = request["partial_proof"] - proof = self.service.solve(bytes.fromhex(partial_proof)) + proof = self.service.solve(partial_proof=partial_proof) return {"proof": proof.hex() if proof else None} async def get_state(self, _: dict[str, Any]) -> EndpointResult: diff --git a/chia/solver/solver_rpc_client.py b/chia/solver/solver_rpc_client.py index 9cc0e2d9e783..44c2a8201ba0 100644 --- a/chia/solver/solver_rpc_client.py +++ b/chia/solver/solver_rpc_client.py @@ -14,7 +14,3 @@ class SolverRpcClient(RpcClient): async def get_state(self) -> dict[str, Any]: """Get solver state.""" return await self.fetch("get_state", {}) - - async def solve(self, partial_proof: str) -> dict[str, Any]: - """Solve a partial proof.""" - return await self.fetch("solve", {"partial_proof": partial_proof}) From 5353a386b919140871b52d2e7dd1898a307f1fb6 Mon Sep 17 00:00:00 2001 From: almogdepaz Date: Thu, 21 Aug 2025 17:51:51 +0300 Subject: [PATCH 41/42] test and comments --- .../farmer_harvester/test_farmer_harvester.py | 23 +++++++++---------- chia/farmer/farmer_api.py | 2 +- chia/solver/solver_rpc_api.py | 6 ----- 3 files changed, 12 insertions(+), 19 deletions(-) diff --git a/chia/_tests/farmer_harvester/test_farmer_harvester.py b/chia/_tests/farmer_harvester/test_farmer_harvester.py index 17cd8a5c63fb..9d25e6815f5a 100644 --- a/chia/_tests/farmer_harvester/test_farmer_harvester.py +++ b/chia/_tests/farmer_harvester/test_farmer_harvester.py @@ -420,7 +420,7 @@ async def test_v2_partial_proofs_with_existing_sp( harvester_peer = await get_harvester_peer(farmer) await farmer_api.partial_proofs(partial_proofs, harvester_peer) - # should store 2 pending requests (one per quality) + # should store 2 pending requests (one per partial proof) assert len(farmer.pending_solver_requests) == 2 assert sp_hash in farmer.cache_add_time @@ -434,7 +434,6 @@ async def test_solution_response_handler( farmer = farmer_api.farmer # set up a pending request - quality = bytes32(b"3" * 32) sp_hash = bytes32(b"1" * 32) challenge_hash = bytes32(b"2" * 32) @@ -453,13 +452,15 @@ async def test_solution_response_handler( harvester_peer = await get_harvester_peer(farmer) # manually add pending request - farmer.pending_solver_requests[quality] = { + farmer.pending_solver_requests[partial_proofs.partial_proofs[0]] = { "proof_data": partial_proofs, "peer": harvester_peer, } # create solution response - solution_response = solver_protocol.SolverResponse(partial_proof=quality, proof=b"test_proof_from_solver") + solution_response = solver_protocol.SolverResponse( + partial_proof=partial_proofs.partial_proofs[0], proof=b"test_proof_from_solver" + ) solver_peer = Mock() solver_peer.peer_node_id = "solver_peer" @@ -476,7 +477,7 @@ async def test_solution_response_handler( assert original_peer == harvester_peer # verify pending request was removed - assert quality not in farmer.pending_solver_requests + assert partial_proofs.partial_proofs[0] not in farmer.pending_solver_requests @pytest.mark.anyio @@ -510,7 +511,6 @@ async def test_solution_response_empty_proof( farmer = farmer_api.farmer # set up a pending request - quality = bytes32(b"3" * 32) sp_hash = bytes32(b"1" * 32) challenge_hash = bytes32(b"2" * 32) @@ -530,8 +530,8 @@ async def test_solution_response_empty_proof( harvester_peer.peer_node_id = "harvester_peer" # manually add pending request - farmer.pending_solver_requests[quality] = { - "proof_data": partial_proofs, + farmer.pending_solver_requests[partial_proofs.partial_proofs[0]] = { + "proof_data": partial_proofs.partial_proofs[0], "peer": harvester_peer, } @@ -539,7 +539,7 @@ async def test_solution_response_empty_proof( solver_peer = await get_solver_peer(farmer) # create solution response with empty proof - solution_response = solver_protocol.SolverResponse(partial_proof=quality, proof=b"") + solution_response = solver_protocol.SolverResponse(partial_proof=partial_proofs.partial_proofs[0], proof=b"") with unittest.mock.patch.object(farmer_api, "new_proof_of_space", new_callable=AsyncMock) as mock_new_proof: await farmer_api.solution_response(solution_response, solver_peer) @@ -548,7 +548,7 @@ async def test_solution_response_empty_proof( mock_new_proof.assert_not_called() # verify pending request was removed (cleanup still happens) - assert quality not in farmer.pending_solver_requests + assert partial_proofs.partial_proofs[0] not in farmer.pending_solver_requests @pytest.mark.anyio @@ -594,5 +594,4 @@ async def test_v2_partial_proofs_solver_exception( await farmer_api.partial_proofs(partial_proofs, harvester_peer) # verify pending request was cleaned up after exception - quality = bytes32(b"3" * 32) - assert quality not in farmer.pending_solver_requests + assert partial_proofs.partial_proofs[0] not in farmer.pending_solver_requests diff --git a/chia/farmer/farmer_api.py b/chia/farmer/farmer_api.py index 76ced1acdf56..1fe745fa932f 100644 --- a/chia/farmer/farmer_api.py +++ b/chia/farmer/farmer_api.py @@ -503,7 +503,7 @@ async def partial_proofs(self, partial_proof_data: PartialProofsData, peer: WSCh f"for plot {partial_proof_data.plot_identifier[:10]}... from {peer.peer_node_id}" ) - # Process each quality chain through solver service to get full proofs + # Process each partial proof chain through solver service to get full proofs for partial_proof in partial_proof_data.partial_proofs: solver_info = SolverInfo(partial_proof=partial_proof) diff --git a/chia/solver/solver_rpc_api.py b/chia/solver/solver_rpc_api.py index d129d31eada3..b427964371df 100644 --- a/chia/solver/solver_rpc_api.py +++ b/chia/solver/solver_rpc_api.py @@ -19,18 +19,12 @@ def __init__(self, solver: Solver): def get_routes(self) -> dict[str, Endpoint]: return { - "/solve": self.solve, "/get_state": self.get_state, } async def _state_changed(self, change: str, change_data: Optional[dict[str, Any]] = None) -> list[WsRpcMessage]: return [] - async def solve(self, request: dict[str, Any]) -> EndpointResult: - partial_proof = request["partial_proof"] - proof = self.service.solve(partial_proof=partial_proof) - return {"proof": proof.hex() if proof else None} - async def get_state(self, _: dict[str, Any]) -> EndpointResult: return { "started": self.service.started, From f45f9d057473e939c9347a7749db33b52885b664 Mon Sep 17 00:00:00 2001 From: almogdepaz Date: Fri, 22 Aug 2025 15:35:11 +0300 Subject: [PATCH 42/42] rename to partial proof --- chia/farmer/farmer_api.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/chia/farmer/farmer_api.py b/chia/farmer/farmer_api.py index 1fe745fa932f..406615444147 100644 --- a/chia/farmer/farmer_api.py +++ b/chia/farmer/farmer_api.py @@ -517,10 +517,12 @@ async def partial_proofs(self, partial_proof_data: PartialProofsData, peer: WSCh # send solve request to all solver connections msg = make_msg(ProtocolMessageTypes.solve, solver_info) await self.farmer.server.send_to_all([msg], NodeType.SOLVER) - self.farmer.log.debug(f"Sent solve request for quality {partial_proof.hex()[:10]}...") + self.farmer.log.debug(f"Sent solve request for partial proof {partial_proof.hex()[:10]}...") except Exception as e: - self.farmer.log.error(f"Failed to call solver service for quality {partial_proof.hex()[:10]}...: {e}") + self.farmer.log.error( + f"Failed to call solver service for partial proof {partial_proof.hex()[:10]}...: {e}" + ) # clean up pending request if partial_proof in self.farmer.pending_solver_requests: del self.farmer.pending_solver_requests[partial_proof] @@ -533,22 +535,24 @@ async def solution_response(self, response: SolverResponse, peer: WSChiaConnecti """ self.farmer.log.debug(f"Received solution response: {len(response.proof)} bytes from {peer.peer_node_id}") - # find the matching pending request using quality_string + # find the matching pending request using partial_proof if response.partial_proof not in self.farmer.pending_solver_requests: - self.farmer.log.warning(f"Received solver response for unknown quality {response.partial_proof.hex()}") + self.farmer.log.warning( + f"Received solver response for unknown partial proof {response.partial_proof.hex()}" + ) return # get the original request data request_data = self.farmer.pending_solver_requests.pop(response.partial_proof) proof_data = request_data["proof_data"] original_peer = request_data["peer"] - quality = response.partial_proof + partial_proof = response.partial_proof # create the proof of space with the solver's proof proof_bytes = response.proof if proof_bytes is None or len(proof_bytes) == 0: - self.farmer.log.warning(f"Received empty proof from solver for proof {quality.hex()}...") + self.farmer.log.warning(f"Received empty proof from solver for proof {partial_proof.hex()}...") return sp_challenge_hash = proof_data.challenge_hash