Skip to content

Commit 6bd9360

Browse files
authored
Farmer solver networking (#20019)
* farmer connects to solver * default solver peer * configure solver peer * solver trusted peers * minor fixes * connect to solver cli * fix and rename fixture * pr fixes * load new service config if missing * fix failling test * fix test
1 parent 2d3d732 commit 6bd9360

20 files changed

+233
-114
lines changed

chia/_tests/conftest.py

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@
9292
from chia_rs.sized_ints import uint128
9393

9494
from chia._tests.environments.wallet import WalletEnvironment, WalletState, WalletTestFramework
95-
from chia._tests.util.setup_nodes import setup_farmer_multi_harvester
95+
from chia._tests.util.setup_nodes import setup_farmer_solver_multi_harvester
9696
from chia.full_node.full_node_rpc_client import FullNodeRpcClient
9797
from chia.simulator.block_tools import BlockTools, create_block_tools_async, test_constants
9898
from chia.simulator.keyring import TempKeyring
@@ -866,7 +866,7 @@ async def farmer_one_harvester_simulator_wallet(
866866
]
867867
]:
868868
async with setup_simulators_and_wallets_service(1, 1, blockchain_constants) as (nodes, wallets, bt):
869-
async with setup_farmer_multi_harvester(bt, 1, tmp_path, bt.constants, start_services=True) as (
869+
async with setup_farmer_solver_multi_harvester(bt, 1, tmp_path, bt.constants, start_services=True) as (
870870
harvester_services,
871871
farmer_service,
872872
_,
@@ -879,7 +879,9 @@ async def farmer_one_harvester_simulator_wallet(
879879

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

885887

@@ -890,37 +892,41 @@ async def farmer_one_harvester(tmp_path: Path, get_b_tools: BlockTools) -> Async
890892
async def farmer_one_harvester_solver(
891893
tmp_path: Path, get_b_tools: BlockTools
892894
) -> AsyncIterator[FarmerOneHarvesterSolver]:
893-
async with setup_farmer_multi_harvester(get_b_tools, 1, tmp_path, get_b_tools.constants, start_services=True) as (
894-
harvester_services,
895-
farmer_service,
896-
bt,
897-
):
898-
farmer_peer = UnresolvedPeerInfo(bt.config["self_hostname"], farmer_service._server.get_port())
899-
async with setup_solver(tmp_path / "solver", bt, bt.constants, farmer_peer=farmer_peer) as solver_service:
895+
async with setup_solver(tmp_path / "solver", get_b_tools, get_b_tools.constants) as solver_service:
896+
solver_peer = UnresolvedPeerInfo(get_b_tools.config["self_hostname"], solver_service._server.get_port())
897+
async with setup_farmer_solver_multi_harvester(
898+
get_b_tools, 1, tmp_path, get_b_tools.constants, start_services=True, solver_peer=solver_peer
899+
) as (harvester_services, farmer_service, bt):
900900
yield harvester_services, farmer_service, solver_service, bt
901901

902902

903903
@pytest.fixture(scope="function")
904904
async def farmer_one_harvester_not_started(
905905
tmp_path: Path, get_b_tools: BlockTools
906906
) -> AsyncIterator[tuple[list[HarvesterService], FarmerService, BlockTools]]:
907-
async with setup_farmer_multi_harvester(get_b_tools, 1, tmp_path, get_b_tools.constants, start_services=False) as _:
907+
async with setup_farmer_solver_multi_harvester(
908+
get_b_tools, 1, tmp_path, get_b_tools.constants, start_services=False
909+
) as _:
908910
yield _
909911

910912

911913
@pytest.fixture(scope="function")
912914
async def farmer_two_harvester_not_started(
913915
tmp_path: Path, get_b_tools: BlockTools
914916
) -> AsyncIterator[tuple[list[HarvesterService], FarmerService, BlockTools]]:
915-
async with setup_farmer_multi_harvester(get_b_tools, 2, tmp_path, get_b_tools.constants, start_services=False) as _:
917+
async with setup_farmer_solver_multi_harvester(
918+
get_b_tools, 2, tmp_path, get_b_tools.constants, start_services=False
919+
) as _:
916920
yield _
917921

918922

919923
@pytest.fixture(scope="function")
920924
async def farmer_three_harvester_not_started(
921925
tmp_path: Path, get_b_tools: BlockTools
922926
) -> AsyncIterator[tuple[list[HarvesterService], FarmerService, BlockTools]]:
923-
async with setup_farmer_multi_harvester(get_b_tools, 3, tmp_path, get_b_tools.constants, start_services=False) as _:
927+
async with setup_farmer_solver_multi_harvester(
928+
get_b_tools, 3, tmp_path, get_b_tools.constants, start_services=False
929+
) as _:
924930
yield _
925931

926932

@@ -1301,10 +1307,13 @@ async def farmer_harvester_2_simulators_zero_bits_plot_filter(
13011307
)
13021308
for index in range(len(bts))
13031309
]
1304-
1305-
[harvester_service], farmer_service, _ = await async_exit_stack.enter_async_context(
1306-
setup_farmer_multi_harvester(bt, 1, tmp_path, bt.constants, start_services=True)
1307-
)
1310+
async with setup_solver(tmp_path / "solver", bt, bt.constants) as solver_service:
1311+
solver_peer = UnresolvedPeerInfo(bt.config["self_hostname"], solver_service._server.get_port())
1312+
[harvester_service], farmer_service, _ = await async_exit_stack.enter_async_context(
1313+
setup_farmer_solver_multi_harvester(
1314+
bt, 1, tmp_path, bt.constants, start_services=True, solver_peer=solver_peer
1315+
)
1316+
)
13081317

13091318
yield farmer_service, harvester_service, simulators[0], simulators[1], bt
13101319

chia/_tests/core/test_farmer_harvester_rpc.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,15 @@
3030
plot_matches_filter,
3131
)
3232
from chia.farmer.farmer_rpc_client import FarmerRpcClient
33+
from chia.farmer.farmer_service import FarmerService
34+
from chia.harvester.harvester_service import HarvesterService
3335
from chia.plot_sync.receiver import Receiver, get_list_or_len
3436
from chia.plotting.util import add_plot_directory
3537
from chia.protocols import farmer_protocol
3638
from chia.protocols.harvester_protocol import Plot
37-
from chia.simulator.block_tools import get_plot_dir
39+
from chia.rpc.rpc_client import ResponseFailureError
40+
from chia.simulator.block_tools import BlockTools, get_plot_dir
41+
from chia.solver.solver_service import SolverService
3842
from chia.util.bech32m import decode_puzzle_hash, encode_puzzle_hash
3943
from chia.util.config import load_config, lock_and_load_config, save_config
4044
from chia.util.hash import std_hash
@@ -503,3 +507,32 @@ async def assert_added(path: Path) -> None:
503507
added_directories = await harvester_rpc_client.get_plot_directories()
504508
assert str(test_path) in added_directories
505509
assert str(test_path_other) in added_directories
510+
511+
512+
@pytest.mark.anyio
513+
async def test_farmer_connect_to_solver(
514+
farmer_one_harvester_solver: tuple[list[HarvesterService], FarmerService, SolverService, BlockTools],
515+
) -> None:
516+
_, farmer_service, solver_service, bt = farmer_one_harvester_solver
517+
assert farmer_service.rpc_server is not None
518+
farmer_rpc_client = await FarmerRpcClient.create(
519+
bt.config["self_hostname"],
520+
farmer_service.rpc_server.listen_port,
521+
farmer_service.root_path,
522+
farmer_service.config,
523+
)
524+
525+
try:
526+
# Test successful connection to existing solver
527+
solver_host = bt.config["self_hostname"]
528+
solver_port = solver_service._server.get_port()
529+
result = await farmer_rpc_client.connect_to_solver(solver_host, solver_port)
530+
assert result["success"] is True
531+
532+
# Test connection failure to non-existent solver
533+
with pytest.raises(ResponseFailureError) as exc_info:
534+
await farmer_rpc_client.connect_to_solver("localhost", 65000)
535+
assert "Could not connect to solver" in exc_info.value.response["error"]
536+
finally:
537+
farmer_rpc_client.close()
538+
await farmer_rpc_client.await_closed()

chia/_tests/farmer_harvester/test_filter_prefix_bits.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
from chia._tests.conftest import ConsensusMode
1313
from chia._tests.core.test_farmer_harvester_rpc import wait_for_plot_sync
14-
from chia._tests.util.setup_nodes import setup_farmer_multi_harvester
14+
from chia._tests.util.setup_nodes import setup_farmer_solver_multi_harvester
1515
from chia._tests.util.time_out_assert import time_out_assert
1616
from chia.farmer.farmer_api import FarmerAPI
1717
from chia.farmer.farmer_rpc_client import FarmerRpcClient
@@ -60,7 +60,7 @@ async def have_connections() -> bool:
6060
)
6161
new_config = local_b_tools._config
6262
local_b_tools.change_config(new_config)
63-
async with setup_farmer_multi_harvester(
63+
async with setup_farmer_solver_multi_harvester(
6464
local_b_tools, 1, tmp_path, local_b_tools.constants, start_services=True
6565
) as (harvesters, farmer_service, _):
6666
harvester_service = harvesters[0]

chia/_tests/farmer_harvester/test_third_party_harvesters.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -84,17 +84,16 @@ async def test_harvester_receive_source_signing_data(
8484
full_node_1: FullNode = full_node_service_1._node
8585
full_node_2: FullNode = full_node_service_2._node
8686

87+
await time_out_assert(60, node_type_connected, True, farmer.server, NodeType.HARVESTER)
8788
# Connect peers to each other
8889
farmer_service.add_peer(
8990
UnresolvedPeerInfo(str(full_node_service_2.self_hostname), full_node_service_2._server.get_port())
9091
)
92+
await time_out_assert(60, node_type_connected, True, farmer.server, NodeType.FULL_NODE)
9193
full_node_service_2.add_peer(
9294
UnresolvedPeerInfo(str(full_node_service_1.self_hostname), full_node_service_1._server.get_port())
9395
)
94-
95-
await wait_until_node_type_connected(farmer.server, NodeType.FULL_NODE)
96-
await wait_until_node_type_connected(farmer.server, NodeType.HARVESTER) # Should already be connected
97-
await wait_until_node_type_connected(full_node_1.server, NodeType.FULL_NODE)
96+
await time_out_assert(60, node_type_connected, True, full_node_1.server, NodeType.FULL_NODE)
9897

9998
# Prepare test data
10099
blocks: list[FullBlock]
@@ -429,12 +428,11 @@ def log_has_new_text() -> bool:
429428
return False
430429

431430

432-
async def wait_until_node_type_connected(server: ChiaServer, node_type: NodeType) -> WSChiaConnection:
433-
while True:
434-
for peer in server.all_connections.values():
435-
if peer.connection_type == node_type.value:
436-
return peer
437-
await asyncio.sleep(1)
431+
def node_type_connected(server: ChiaServer, node_type: NodeType) -> bool:
432+
for peer in server.all_connections.values():
433+
if peer.connection_type == node_type.value:
434+
return True
435+
return False
438436

439437

440438
def decode_sp(

chia/_tests/util/setup_nodes.py

Lines changed: 3 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -303,13 +303,14 @@ async def setup_simulators_and_wallets_inner(
303303

304304

305305
@asynccontextmanager
306-
async def setup_farmer_multi_harvester(
306+
async def setup_farmer_solver_multi_harvester(
307307
block_tools: BlockTools,
308308
harvester_count: int,
309309
temp_dir: Path,
310310
consensus_constants: ConsensusConstants,
311311
*,
312312
start_services: bool,
313+
solver_peer: Optional[UnresolvedPeerInfo] = None,
313314
) -> AsyncIterator[tuple[list[HarvesterService], FarmerService, BlockTools]]:
314315
async with AsyncExitStack() as async_exit_stack:
315316
farmer_service = await async_exit_stack.enter_async_context(
@@ -320,6 +321,7 @@ async def setup_farmer_multi_harvester(
320321
consensus_constants,
321322
port=uint16(0),
322323
start_service=start_services,
324+
solver_peer=solver_peer,
323325
)
324326
)
325327
if start_services:
@@ -342,74 +344,6 @@ async def setup_farmer_multi_harvester(
342344
yield harvester_services, farmer_service, block_tools
343345

344346

345-
@asynccontextmanager
346-
async def setup_farmer_multi_harvester_with_solver(
347-
block_tools: BlockTools,
348-
harvester_count: int,
349-
temp_dir: Path,
350-
consensus_constants: ConsensusConstants,
351-
*,
352-
start_services: bool,
353-
) -> AsyncIterator[tuple[list[HarvesterService], FarmerService, SolverService, BlockTools]]:
354-
async with AsyncExitStack() as async_exit_stack:
355-
farmer_service = await async_exit_stack.enter_async_context(
356-
setup_farmer(
357-
block_tools,
358-
temp_dir / "farmer",
359-
block_tools.config["self_hostname"],
360-
consensus_constants,
361-
port=uint16(0),
362-
start_service=start_services,
363-
)
364-
)
365-
if start_services:
366-
farmer_peer = UnresolvedPeerInfo(block_tools.config["self_hostname"], farmer_service._server.get_port())
367-
else:
368-
farmer_peer = None
369-
harvester_services = [
370-
await async_exit_stack.enter_async_context(
371-
setup_harvester(
372-
block_tools,
373-
temp_dir / f"harvester_{i}",
374-
farmer_peer,
375-
consensus_constants,
376-
start_service=start_services,
377-
)
378-
)
379-
for i in range(harvester_count)
380-
]
381-
382-
# Setup solver with farmer peer - CRITICAL: use same BlockTools root path for SSL CA consistency
383-
solver_service = await async_exit_stack.enter_async_context(
384-
setup_solver(
385-
temp_dir / "solver", # Use temp_dir like harvester, not block_tools.root_path
386-
block_tools, # Pass BlockTools so SSL CA can be consistent
387-
consensus_constants,
388-
start_service=start_services,
389-
farmer_peer=farmer_peer,
390-
)
391-
)
392-
393-
# Wait for farmer to be fully started before expecting solver connection
394-
if start_services:
395-
import asyncio
396-
397-
# Wait for farmer to be fully initialized
398-
timeout = 30
399-
for i in range(timeout):
400-
if farmer_service._node.started:
401-
print(f"Farmer fully started after {i} seconds")
402-
break
403-
await asyncio.sleep(1)
404-
else:
405-
print(f"WARNING: Farmer not started after {timeout} seconds")
406-
407-
# Give solver additional time to connect
408-
await asyncio.sleep(3)
409-
410-
yield harvester_services, farmer_service, solver_service, block_tools
411-
412-
413347
@asynccontextmanager
414348
async def setup_full_system(
415349
consensus_constants: ConsensusConstants,

chia/cmds/configure.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
def configure(
2323
root_path: Path,
2424
set_farmer_peer: str,
25+
set_solver_peer: str,
2526
set_node_introducer: str,
2627
set_fullnode_port: str,
2728
set_harvester_port: str,
@@ -35,6 +36,7 @@ def configure(
3536
crawler_minimum_version_count: Optional[int],
3637
seeder_domain_name: str,
3738
seeder_nameserver: str,
39+
set_solver_trusted_peers_only: str,
3840
) -> None:
3941
config_yaml = "config.yaml"
4042
with lock_and_load_config(root_path, config_yaml, fill_missing_services=True) as config:
@@ -59,6 +61,25 @@ def configure(
5961
change_made = True
6062
except ValueError:
6163
print("Farmer address must be in format [IP:Port]")
64+
if set_solver_peer:
65+
try:
66+
host, port = parse_host_port(set_solver_peer)
67+
# ensure farmer section has solver_peers field
68+
if "solver_peers" not in config["farmer"]:
69+
config["farmer"]["solver_peers"] = []
70+
# Set single solver peer (overrides any existing)
71+
config["farmer"]["solver_peers"] = [{"host": host, "port": port}]
72+
print(f"Solver peer updated to {host}:{port}")
73+
change_made = True
74+
except ValueError:
75+
print("Solver address must be in format [IP:Port]")
76+
if set_solver_trusted_peers_only:
77+
config["solver"]["trusted_peers_only"] = str2bool(set_solver_trusted_peers_only)
78+
if str2bool(set_solver_trusted_peers_only):
79+
print("Solver will only accept trusted peer connections")
80+
else:
81+
print("Solver will accept connections from all peers")
82+
change_made = True
6283
if set_fullnode_port:
6384
config["full_node"]["port"] = int(set_fullnode_port)
6485
config["full_node"]["introducer_peer"]["port"] = int(set_fullnode_port)
@@ -237,6 +258,12 @@ def configure(
237258
)
238259
@click.option("--set-node-introducer", help="Set the introducer for node - IP:Port", type=str)
239260
@click.option("--set-farmer-peer", help="Set the farmer peer for harvester - IP:Port", type=str)
261+
@click.option("--set-solver-peer", help="Set the solver peer for farmer - IP:Port", type=str)
262+
@click.option(
263+
"--set-solver-trusted-peers-only",
264+
help="Enable/disable trusted peer requirement for solver connections",
265+
type=click.Choice(["true", "t", "false", "f"]),
266+
)
240267
@click.option(
241268
"--set-fullnode-port",
242269
help="Set the port to use for the fullnode, useful for testing",
@@ -292,6 +319,7 @@ def configure(
292319
def configure_cmd(
293320
ctx: click.Context,
294321
set_farmer_peer: str,
322+
set_solver_peer: str,
295323
set_node_introducer: str,
296324
set_fullnode_port: str,
297325
set_harvester_port: str,
@@ -305,10 +333,12 @@ def configure_cmd(
305333
crawler_minimum_version_count: int,
306334
seeder_domain_name: str,
307335
seeder_nameserver: str,
336+
set_solver_trusted_peers_only: str,
308337
) -> None:
309338
configure(
310339
ChiaCliContext.set_default(ctx).root_path,
311340
set_farmer_peer,
341+
set_solver_peer,
312342
set_node_introducer,
313343
set_fullnode_port,
314344
set_harvester_port,
@@ -322,4 +352,5 @@ def configure_cmd(
322352
crawler_minimum_version_count,
323353
seeder_domain_name,
324354
seeder_nameserver,
355+
set_solver_trusted_peers_only,
325356
)

0 commit comments

Comments
 (0)