Skip to content
34 changes: 20 additions & 14 deletions chia/_tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
from chia_rs.sized_ints import uint128

from chia._tests.environments.wallet import WalletEnvironment, WalletState, WalletTestFramework
from chia._tests.util.setup_nodes import setup_farmer_multi_harvester
from chia._tests.util.setup_nodes import setup_farmer_solver_multi_harvester
from chia.full_node.full_node_rpc_client import FullNodeRpcClient
from chia.simulator.block_tools import BlockTools, create_block_tools_async, test_constants
from chia.simulator.keyring import TempKeyring
Expand Down Expand Up @@ -866,7 +866,7 @@ async def farmer_one_harvester_simulator_wallet(
]
]:
async with setup_simulators_and_wallets_service(1, 1, blockchain_constants) as (nodes, wallets, bt):
async with setup_farmer_multi_harvester(bt, 1, tmp_path, bt.constants, start_services=True) as (
async with setup_farmer_solver_multi_harvester(bt, 1, tmp_path, bt.constants, start_services=True) as (
harvester_services,
farmer_service,
_,
Expand All @@ -879,7 +879,9 @@ async def farmer_one_harvester_simulator_wallet(

@pytest.fixture(scope="function")
async def farmer_one_harvester(tmp_path: Path, get_b_tools: BlockTools) -> AsyncIterator[FarmerOneHarvester]:
async with setup_farmer_multi_harvester(get_b_tools, 1, tmp_path, get_b_tools.constants, start_services=True) as _:
async with setup_farmer_solver_multi_harvester(
get_b_tools, 1, tmp_path, get_b_tools.constants, start_services=True
) as _:
yield _


Expand All @@ -890,37 +892,41 @@ async def farmer_one_harvester(tmp_path: Path, get_b_tools: BlockTools) -> Async
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, bt.constants, farmer_peer=farmer_peer) as solver_service:
async with setup_solver(tmp_path / "solver", get_b_tools, get_b_tools.constants) as solver_service:
solver_peer = UnresolvedPeerInfo("127.0.0.1", solver_service._server.get_port())
async with setup_farmer_solver_multi_harvester(
get_b_tools, 1, tmp_path, get_b_tools.constants, start_services=True, solver_peer=solver_peer
) as (harvester_services, farmer_service, bt):
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
) -> AsyncIterator[tuple[list[HarvesterService], FarmerService, BlockTools]]:
async with setup_farmer_multi_harvester(get_b_tools, 1, tmp_path, get_b_tools.constants, start_services=False) as _:
async with setup_farmer_solver_multi_harvester(
get_b_tools, 1, tmp_path, get_b_tools.constants, start_services=False
) as _:
yield _


@pytest.fixture(scope="function")
async def farmer_two_harvester_not_started(
tmp_path: Path, get_b_tools: BlockTools
) -> AsyncIterator[tuple[list[HarvesterService], FarmerService, BlockTools]]:
async with setup_farmer_multi_harvester(get_b_tools, 2, tmp_path, get_b_tools.constants, start_services=False) as _:
async with setup_farmer_solver_multi_harvester(
get_b_tools, 2, tmp_path, get_b_tools.constants, start_services=False
) as _:
yield _


@pytest.fixture(scope="function")
async def farmer_three_harvester_not_started(
tmp_path: Path, get_b_tools: BlockTools
) -> AsyncIterator[tuple[list[HarvesterService], FarmerService, BlockTools]]:
async with setup_farmer_multi_harvester(get_b_tools, 3, tmp_path, get_b_tools.constants, start_services=False) as _:
async with setup_farmer_solver_multi_harvester(
get_b_tools, 3, tmp_path, get_b_tools.constants, start_services=False
) as _:
yield _


Expand Down Expand Up @@ -1303,7 +1309,7 @@ async def farmer_harvester_2_simulators_zero_bits_plot_filter(
]

[harvester_service], farmer_service, _ = await async_exit_stack.enter_async_context(
setup_farmer_multi_harvester(bt, 1, tmp_path, bt.constants, start_services=True)
setup_farmer_solver_multi_harvester(bt, 1, tmp_path, bt.constants, start_services=True)
)

yield farmer_service, harvester_service, simulators[0], simulators[1], bt
Expand Down
4 changes: 2 additions & 2 deletions chia/_tests/farmer_harvester/test_filter_prefix_bits.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

from chia._tests.conftest import ConsensusMode
from chia._tests.core.test_farmer_harvester_rpc import wait_for_plot_sync
from chia._tests.util.setup_nodes import setup_farmer_multi_harvester
from chia._tests.util.setup_nodes import setup_farmer_solver_multi_harvester
from chia._tests.util.time_out_assert import time_out_assert
from chia.farmer.farmer_api import FarmerAPI
from chia.farmer.farmer_rpc_client import FarmerRpcClient
Expand Down Expand Up @@ -60,7 +60,7 @@ async def have_connections() -> bool:
)
new_config = local_b_tools._config
local_b_tools.change_config(new_config)
async with setup_farmer_multi_harvester(
async with setup_farmer_solver_multi_harvester(
local_b_tools, 1, tmp_path, local_b_tools.constants, start_services=True
) as (harvesters, farmer_service, _):
harvester_service = harvesters[0]
Expand Down
5 changes: 3 additions & 2 deletions chia/_tests/util/setup_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,13 +303,14 @@ async def setup_simulators_and_wallets_inner(


@asynccontextmanager
async def setup_farmer_multi_harvester(
async def setup_farmer_solver_multi_harvester(
block_tools: BlockTools,
harvester_count: int,
temp_dir: Path,
consensus_constants: ConsensusConstants,
*,
start_services: bool,
solver_peer: Optional[UnresolvedPeerInfo] = None,
) -> AsyncIterator[tuple[list[HarvesterService], FarmerService, BlockTools]]:
async with AsyncExitStack() as async_exit_stack:
farmer_service = await async_exit_stack.enter_async_context(
Expand All @@ -320,6 +321,7 @@ async def setup_farmer_multi_harvester(
consensus_constants,
port=uint16(0),
start_service=start_services,
solver_peer=solver_peer,
)
)
if start_services:
Expand Down Expand Up @@ -386,7 +388,6 @@ async def setup_farmer_multi_harvester_with_solver(
block_tools, # Pass BlockTools so SSL CA can be consistent
consensus_constants,
start_service=start_services,
farmer_peer=farmer_peer,
)
)

Expand Down
28 changes: 28 additions & 0 deletions chia/cmds/configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
def configure(
root_path: Path,
set_farmer_peer: str,
set_solver_peer: str,
set_node_introducer: str,
set_fullnode_port: str,
set_harvester_port: str,
Expand All @@ -35,6 +36,7 @@ def configure(
crawler_minimum_version_count: Optional[int],
seeder_domain_name: str,
seeder_nameserver: str,
set_solver_trusted_peers_only: str,
) -> None:
config_yaml = "config.yaml"
with lock_and_load_config(root_path, config_yaml, fill_missing_services=True) as config:
Expand All @@ -59,6 +61,22 @@ def configure(
change_made = True
except ValueError:
print("Farmer address must be in format [IP:Port]")
if set_solver_peer:
try:
host, port = parse_host_port(set_solver_peer)
# Set single solver peer (overrides any existing)
config["farmer"]["solver_peers"] = [{"host": host, "port": port}]
print(f"Solver peer updated to {host}:{port}")
change_made = True
except ValueError:
print("Solver address must be in format [IP:Port]")
if set_solver_trusted_peers_only:
config["solver"]["trusted_peers_only"] = str2bool(set_solver_trusted_peers_only)
if str2bool(set_solver_trusted_peers_only):
print("Solver will only accept trusted peer connections")
else:
print("Solver will accept connections from all peers")
change_made = True
if set_fullnode_port:
config["full_node"]["port"] = int(set_fullnode_port)
config["full_node"]["introducer_peer"]["port"] = int(set_fullnode_port)
Expand Down Expand Up @@ -237,6 +255,12 @@ def configure(
)
@click.option("--set-node-introducer", help="Set the introducer for node - IP:Port", type=str)
@click.option("--set-farmer-peer", help="Set the farmer peer for harvester - IP:Port", type=str)
@click.option("--set-solver-peer", help="Set the solver peer for farmer - IP:Port", type=str)
@click.option(
"--set-solver-trusted-peers-only",
help="Enable/disable trusted peer requirement for solver connections",
type=click.Choice(["true", "t", "false", "f"]),
)
@click.option(
"--set-fullnode-port",
help="Set the port to use for the fullnode, useful for testing",
Expand Down Expand Up @@ -292,6 +316,7 @@ def configure(
def configure_cmd(
ctx: click.Context,
set_farmer_peer: str,
set_solver_peer: str,
set_node_introducer: str,
set_fullnode_port: str,
set_harvester_port: str,
Expand All @@ -305,10 +330,12 @@ def configure_cmd(
crawler_minimum_version_count: int,
seeder_domain_name: str,
seeder_nameserver: str,
set_solver_trusted_peers_only: str,
) -> None:
configure(
ChiaCliContext.set_default(ctx).root_path,
set_farmer_peer,
set_solver_peer,
set_node_introducer,
set_fullnode_port,
set_harvester_port,
Expand All @@ -322,4 +349,5 @@ def configure_cmd(
crawler_minimum_version_count,
seeder_domain_name,
seeder_nameserver,
set_solver_trusted_peers_only,
)
23 changes: 23 additions & 0 deletions chia/cmds/farm.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,26 @@ def challenges_cmd(ctx: click.Context, farmer_rpc_port: Optional[int], limit: in
from chia.cmds.farm_funcs import challenges

asyncio.run(challenges(ChiaCliContext.set_default(ctx).root_path, farmer_rpc_port, limit))


@farm_cmd.command("connect-solver", help="Connect to a solver")
@click.option(
"-fp",
"--farmer-rpc-port",
help="Set the port where the Farmer is hosting the RPC interface",
type=int,
default=None,
show_default=True,
)
@click.argument("solver_address", required=True)
@click.pass_context
def connect_solver_cmd(
ctx: click.Context,
farmer_rpc_port: Optional[int],
solver_address: str,
) -> None:
import asyncio

from chia.cmds.farm_funcs import solver_connect

asyncio.run(solver_connect(ChiaCliContext.set_default(ctx).root_path, farmer_rpc_port, solver_address))
31 changes: 31 additions & 0 deletions chia/cmds/farm_funcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from chia.cmds.units import units
from chia.farmer.farmer_rpc_client import FarmerRpcClient
from chia.full_node.full_node_rpc_client import FullNodeRpcClient
from chia.util.config import lock_and_load_config, save_config
from chia.util.errors import CliRpcConnectionError
from chia.util.network import is_localhost
from chia.wallet.wallet_rpc_client import WalletRpcClient
Expand Down Expand Up @@ -217,3 +218,33 @@ def process_harvesters(harvester_peers_in: dict[str, dict[str, Any]]) -> None:
print("For details on farmed rewards and fees you should run 'chia wallet show'")
else:
print("Note: log into your key using 'chia wallet show' to see rewards for each key")


async def solver_connect(root_path: Path, farmer_rpc_port: Optional[int], solver_address: str) -> None:
from chia.util.network import parse_host_port

try:
host, port = parse_host_port(solver_address)
except ValueError:
print("Solver address must be in format [IP:Port]")
return
try:
with lock_and_load_config(root_path, "config.yaml") as config:
config["farmer"]["solver_peers"] = [{"host": host, "port": port}]
save_config(root_path, "config.yaml", config)
print(f"✓ Updated config with solver peer {host}:{port}")
except Exception as e:
print(f"✗ Failed to update config: {e}")
return
try:
async with get_any_service_client(FarmerRpcClient, root_path, farmer_rpc_port) as (farmer_client, _):
result = await farmer_client.connect_to_solver(host, port)
if result.get("success"):
print(f"✓ Connected to solver at {host}:{port}")
else:
error = result.get("error", "Unknown error")
print(f"✗ Failed to connect to solver: {error}")
except CliRpcConnectionError:
print("✗ Could not connect to farmer. Make sure farmer is running.")
except Exception as e:
print(f"✗ Error connecting to solver: {e}")
4 changes: 4 additions & 0 deletions chia/cmds/init_funcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,8 @@ def chia_init(
crawler_minimum_version_count=None,
seeder_domain_name="",
seeder_nameserver="",
set_solver_peer="",
set_solver_trusted_peers_only="",
)
if fix_ssl_permissions:
fix_ssl(root_path)
Expand All @@ -324,6 +326,8 @@ def chia_init(
crawler_minimum_version_count=None,
seeder_domain_name="",
seeder_nameserver="",
set_solver_peer="",
set_solver_trusted_peers_only="",
)
create_all_ssl(root_path)
if fix_ssl_permissions:
Expand Down
20 changes: 20 additions & 0 deletions chia/farmer/farmer_rpc_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
from chia.farmer.farmer import Farmer
from chia.plot_sync.receiver import Receiver
from chia.protocols.harvester_protocol import Plot
from chia.protocols.outbound_message import NodeType
from chia.rpc.rpc_server import Endpoint, EndpointResult
from chia.types.peer_info import PeerInfo
from chia.util.network import resolve
from chia.util.paginator import Paginator
from chia.util.streamable import Streamable, streamable
from chia.util.ws_message import WsRpcMessage, create_payload_dict
Expand Down Expand Up @@ -102,6 +105,7 @@ def get_routes(self) -> dict[str, Endpoint]:
"/get_harvester_plots_keys_missing": self.get_harvester_plots_keys_missing,
"/get_harvester_plots_duplicates": self.get_harvester_plots_duplicates,
"/get_pool_login_link": self.get_pool_login_link,
"/connect_to_solver": self.connect_to_solver,
}

async def _state_changed(self, change: str, change_data: Optional[dict[str, Any]]) -> list[WsRpcMessage]:
Expand Down Expand Up @@ -363,3 +367,19 @@ async def get_pool_login_link(self, request: dict[str, Any]) -> EndpointResult:
if login_link is None:
raise ValueError(f"Failed to generate login link for {launcher_id.hex()}")
return {"login_link": login_link}

async def connect_to_solver(self, request: dict[str, Any]) -> EndpointResult:
for connection in self.service.server.get_connections(NodeType.SOLVER):
host = connection.peer_info.host
port = connection.peer_server_port
await connection.close()
self.service.log.info(f"Disconnected from solver at {host}:{port}")
host = request["host"]
port = request["port"]
target_node = PeerInfo(await resolve(host), port)
on_connect = getattr(self.service, "on_connect", None)
if await self.service.server.start_client(target_node, on_connect):
self.service.log.info(f"Connected to solver at {host}:{port}")
return {"success": True}
else:
return {"success": False, "error": f"Could not connect to solver at {host}:{port}"}
4 changes: 4 additions & 0 deletions chia/farmer/farmer_rpc_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,7 @@ async def get_pool_login_link(self, launcher_id: bytes32) -> Optional[str]:
return cast(Optional[str], result["login_link"])
except ValueError: # not connected to pool.
return None

async def connect_to_solver(self, host: str, port: int) -> dict[str, Any]:
"""Connect farmer to specific solver"""
return await self.fetch("connect_to_solver", {"host": host, "port": port})
3 changes: 2 additions & 1 deletion chia/farmer/start_farmer.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ def create_farmer_service(
node_type=NodeType.FARMER,
advertised_port=service_config["port"],
service_name=SERVICE_NAME,
connect_peers=get_unresolved_peer_infos(service_config, NodeType.FULL_NODE),
connect_peers=get_unresolved_peer_infos(service_config, NodeType.FULL_NODE)
| get_unresolved_peer_infos(service_config, NodeType.SOLVER),
on_connect_callback=node.on_connect,
network_id=network_id,
rpc_info=rpc_info,
Expand Down
5 changes: 5 additions & 0 deletions chia/server/resolve_peer_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
PEER_INFO_MAPPING: dict[NodeType, str] = {
NodeType.FULL_NODE: "full_node_peer",
NodeType.FARMER: "farmer_peer",
NodeType.SOLVER: "solver_peer",
}


Expand All @@ -21,6 +22,10 @@ def get_unresolved_peer_infos(service_config: dict[str, Any], peer_type: NodeTyp
if peer_info is not None:
peer_infos.append(peer_info)

# Default solver peer if none configured
if peer_type == NodeType.SOLVER and not peer_infos:
peer_infos = [{"host": "localhost", "port": 8666}]

return {UnresolvedPeerInfo(host=peer["host"], port=peer["port"]) for peer in peer_infos}


Expand Down
2 changes: 1 addition & 1 deletion chia/server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ def create(
private_cert_path, private_key_path = None, None
public_cert_path, public_key_path = None, None

authenticated_client_types = {NodeType.HARVESTER, NodeType.SOLVER}
authenticated_client_types = {NodeType.HARVESTER, NodeType.FARMER, NodeType.SOLVER}
authenticated_server_types = {
NodeType.HARVESTER,
NodeType.FARMER,
Expand Down
Loading
Loading