Skip to content

Commit 52cd108

Browse files
almogdepazarvidn
andauthored
[CHIA-3593] New solver service (#19938)
* add solver service * lint * wire service evrywhear * prover protocol and v2Prover * format name * format * refactor filename * tests/raise unimplemented * add get_filename_str to mock * rename methods * rename * refactor * improve coverage * test from bytes * harvester and farmer v2 support * solver service, api, rpc * clenup, add config to tests * pre-commit * remove mocks from test * aync solver, fixtures, tests * naming * remove redundant farmer message * solver fixtures and tests * change quality_string to quality_chain * fix service tests, cleanup * name change, fix comments * lint * rename to partial proof * network protocol test, use ThreadPoolExecutor * naming and types * check filter for both plot versions * update network protocol tests * more pr comments addressed * fix rename * fix network formatting error * change to Assertion errors * add todo * remove strength from SolverInfo * more pr comments, fix test to catch assertion * revert strength * test and comments * rename to partial proof --------- Co-authored-by: Arvid Norberg <[email protected]>
1 parent 74d5c45 commit 52cd108

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+1775
-373
lines changed

chia/_tests/conftest.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,14 @@
6565
setup_full_node,
6666
setup_introducer,
6767
setup_seeder,
68+
setup_solver,
6869
setup_timelord,
6970
)
7071
from chia.simulator.start_simulator import SimulatorFullNodeService
7172
from chia.simulator.wallet_tools import WalletTool
73+
from chia.solver.solver_service import SolverService
7274
from chia.timelord.timelord_service import TimelordService
73-
from chia.types.peer_info import PeerInfo
75+
from chia.types.peer_info import PeerInfo, UnresolvedPeerInfo
7476
from chia.util.config import create_default_chia_config, lock_and_load_config
7577
from chia.util.db_wrapper import generate_in_memory_db_uri
7678
from chia.util.keychain import Keychain
@@ -881,6 +883,23 @@ async def farmer_one_harvester(tmp_path: Path, get_b_tools: BlockTools) -> Async
881883
yield _
882884

883885

886+
FarmerOneHarvesterSolver = tuple[list[HarvesterService], FarmerService, SolverService, BlockTools]
887+
888+
889+
@pytest.fixture(scope="function")
890+
async def farmer_one_harvester_solver(
891+
tmp_path: Path, get_b_tools: BlockTools
892+
) -> 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:
900+
yield harvester_services, farmer_service, solver_service, bt
901+
902+
884903
@pytest.fixture(scope="function")
885904
async def farmer_one_harvester_not_started(
886905
tmp_path: Path, get_b_tools: BlockTools

chia/_tests/farmer_harvester/test_farmer_harvester.py

Lines changed: 304 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
from __future__ import annotations
22

33
import asyncio
4+
import unittest.mock
45
from math import floor
56
from pathlib import Path
67
from typing import Any, Optional
8+
from unittest.mock import AsyncMock, Mock
79

810
import pytest
911
from chia_rs import G1Element
@@ -19,16 +21,37 @@
1921
from chia.harvester.harvester_rpc_client import HarvesterRpcClient
2022
from chia.harvester.harvester_service import HarvesterService
2123
from chia.plotting.util import PlotsRefreshParameter
22-
from chia.protocols import farmer_protocol, harvester_protocol
24+
from chia.protocols import farmer_protocol, harvester_protocol, solver_protocol
2325
from chia.protocols.outbound_message import NodeType, make_msg
2426
from chia.protocols.protocol_message_types import ProtocolMessageTypes
2527
from chia.simulator.block_tools import BlockTools
28+
from chia.solver.solver_service import SolverService
2629
from chia.types.peer_info import UnresolvedPeerInfo
2730
from chia.util.config import load_config
2831
from chia.util.hash import std_hash
2932
from chia.util.keychain import generate_mnemonic
3033

3134

35+
async def get_harvester_peer(farmer: Farmer) -> Any:
36+
"""wait for harvester connection and return the peer"""
37+
38+
def has_harvester_connection() -> bool:
39+
return len(farmer.server.get_connections(NodeType.HARVESTER)) > 0
40+
41+
await time_out_assert(10, has_harvester_connection, True)
42+
return farmer.server.get_connections(NodeType.HARVESTER)[0]
43+
44+
45+
async def get_solver_peer(farmer: Farmer) -> Any:
46+
"""wait for solver connection and return the peer"""
47+
48+
def has_solver_connection() -> bool:
49+
return len(farmer.server.get_connections(NodeType.SOLVER)) > 0
50+
51+
await time_out_assert(60, has_solver_connection, True)
52+
return farmer.server.get_connections(NodeType.SOLVER)[0]
53+
54+
3255
def farmer_is_started(farmer: Farmer) -> bool:
3356
return farmer.started
3457

@@ -144,9 +167,6 @@ async def test_farmer_respond_signatures(
144167
# messages even though it didn't request them, to cover when the farmer doesn't know
145168
# about an sp_hash, so it fails at the sp record check.
146169

147-
def log_is_ready() -> bool:
148-
return len(caplog.text) > 0
149-
150170
_, _, harvester_service, _, _ = harvester_farmer_environment
151171
# We won't have an sp record for this one
152172
challenge_hash = bytes32(b"1" * 32)
@@ -161,11 +181,16 @@ def log_is_ready() -> bool:
161181
include_source_signature_data=False,
162182
farmer_reward_address_override=None,
163183
)
184+
185+
expected_error = f"Do not have challenge hash {challenge_hash}"
186+
187+
def expected_log_is_ready() -> bool:
188+
return expected_error in caplog.text
189+
164190
msg = make_msg(ProtocolMessageTypes.respond_signatures, response)
165191
await harvester_service._node.server.send_to_all([msg], NodeType.FARMER)
166-
await time_out_assert(5, log_is_ready)
167-
# We fail the sps record check
168-
expected_error = f"Do not have challenge hash {challenge_hash}"
192+
await time_out_assert(10, expected_log_is_ready)
193+
# We should find the error message
169194
assert expected_error in caplog.text
170195

171196

@@ -298,3 +323,275 @@ async def test_harvester_has_no_server(
298323
harvester_server = harvesters[0]._server
299324

300325
assert harvester_server.webserver is None
326+
327+
328+
@pytest.mark.anyio
329+
async def test_v2_partial_proofs_new_sp_hash(
330+
farmer_one_harvester_solver: tuple[list[HarvesterService], FarmerService, SolverService, BlockTools],
331+
) -> None:
332+
_, farmer_service, _solver_service, _bt = farmer_one_harvester_solver
333+
farmer_api = farmer_service._api
334+
farmer = farmer_api.farmer
335+
336+
sp_hash = bytes32(b"1" * 32)
337+
partial_proofs = harvester_protocol.PartialProofsData(
338+
challenge_hash=bytes32(b"2" * 32),
339+
sp_hash=sp_hash,
340+
plot_identifier="test_plot_id",
341+
partial_proofs=[b"test_partial_proof_1"],
342+
signage_point_index=uint8(0),
343+
plot_size=uint8(32),
344+
pool_public_key=None,
345+
pool_contract_puzzle_hash=bytes32(b"4" * 32),
346+
plot_public_key=G1Element(),
347+
)
348+
349+
harvester_peer = await get_harvester_peer(farmer)
350+
await farmer_api.partial_proofs(partial_proofs, harvester_peer)
351+
352+
assert sp_hash in farmer.number_of_responses
353+
assert farmer.number_of_responses[sp_hash] == 0
354+
assert sp_hash in farmer.cache_add_time
355+
356+
357+
@pytest.mark.anyio
358+
async def test_v2_partial_proofs_missing_sp_hash(
359+
caplog: pytest.LogCaptureFixture,
360+
farmer_one_harvester_solver: tuple[list[HarvesterService], FarmerService, SolverService, BlockTools],
361+
) -> None:
362+
_, farmer_service, _, _ = farmer_one_harvester_solver
363+
farmer_api = farmer_service._api
364+
365+
sp_hash = bytes32(b"1" * 32)
366+
partial_proofs = harvester_protocol.PartialProofsData(
367+
challenge_hash=bytes32(b"2" * 32),
368+
sp_hash=sp_hash,
369+
plot_identifier="test_plot_id",
370+
partial_proofs=[b"test_partial_proof_1"],
371+
signage_point_index=uint8(0),
372+
plot_size=uint8(32),
373+
pool_public_key=None,
374+
pool_contract_puzzle_hash=bytes32(b"4" * 32),
375+
plot_public_key=G1Element(),
376+
)
377+
378+
harvester_peer = await get_harvester_peer(farmer_api.farmer)
379+
await farmer_api.partial_proofs(partial_proofs, harvester_peer)
380+
381+
assert f"Received partial proofs for a signage point that we do not have {sp_hash}" in caplog.text
382+
383+
384+
@pytest.mark.anyio
385+
async def test_v2_partial_proofs_with_existing_sp(
386+
farmer_one_harvester_solver: tuple[list[HarvesterService], FarmerService, SolverService, BlockTools],
387+
) -> None:
388+
_, farmer_service, _, _ = farmer_one_harvester_solver
389+
farmer_api = farmer_service._api
390+
farmer = farmer_api.farmer
391+
392+
sp_hash = bytes32(b"1" * 32)
393+
challenge_hash = bytes32(b"2" * 32)
394+
395+
sp = farmer_protocol.NewSignagePoint(
396+
challenge_hash=challenge_hash,
397+
challenge_chain_sp=sp_hash,
398+
reward_chain_sp=std_hash(b"1"),
399+
difficulty=uint64(1000),
400+
sub_slot_iters=uint64(1000),
401+
signage_point_index=uint8(0),
402+
peak_height=uint32(1),
403+
last_tx_height=uint32(0),
404+
)
405+
406+
farmer.sps[sp_hash] = [sp]
407+
408+
partial_proofs = harvester_protocol.PartialProofsData(
409+
challenge_hash=challenge_hash,
410+
sp_hash=sp_hash,
411+
plot_identifier="test_plot_id",
412+
partial_proofs=[b"test_partial_proof_1", b"test_partial_proof_2"],
413+
signage_point_index=uint8(0),
414+
plot_size=uint8(32),
415+
pool_public_key=G1Element(),
416+
pool_contract_puzzle_hash=bytes32(b"4" * 32),
417+
plot_public_key=G1Element(),
418+
)
419+
420+
harvester_peer = await get_harvester_peer(farmer)
421+
await farmer_api.partial_proofs(partial_proofs, harvester_peer)
422+
423+
# should store 2 pending requests (one per partial proof)
424+
assert len(farmer.pending_solver_requests) == 2
425+
assert sp_hash in farmer.cache_add_time
426+
427+
428+
@pytest.mark.anyio
429+
async def test_solution_response_handler(
430+
farmer_one_harvester_solver: tuple[list[HarvesterService], FarmerService, SolverService, BlockTools],
431+
) -> None:
432+
_, farmer_service, _, _ = farmer_one_harvester_solver
433+
farmer_api = farmer_service._api
434+
farmer = farmer_api.farmer
435+
436+
# set up a pending request
437+
sp_hash = bytes32(b"1" * 32)
438+
challenge_hash = bytes32(b"2" * 32)
439+
440+
partial_proofs = harvester_protocol.PartialProofsData(
441+
challenge_hash=challenge_hash,
442+
sp_hash=sp_hash,
443+
plot_identifier="test_plot_id",
444+
partial_proofs=[b"test_partial_proof_for_quality"],
445+
signage_point_index=uint8(0),
446+
plot_size=uint8(32),
447+
pool_public_key=G1Element(),
448+
pool_contract_puzzle_hash=bytes32(b"4" * 32),
449+
plot_public_key=G1Element(),
450+
)
451+
452+
harvester_peer = await get_harvester_peer(farmer)
453+
454+
# manually add pending request
455+
farmer.pending_solver_requests[partial_proofs.partial_proofs[0]] = {
456+
"proof_data": partial_proofs,
457+
"peer": harvester_peer,
458+
}
459+
460+
# create solution response
461+
solution_response = solver_protocol.SolverResponse(
462+
partial_proof=partial_proofs.partial_proofs[0], proof=b"test_proof_from_solver"
463+
)
464+
solver_peer = Mock()
465+
solver_peer.peer_node_id = "solver_peer"
466+
467+
with unittest.mock.patch.object(farmer_api, "new_proof_of_space", new_callable=AsyncMock) as mock_new_proof:
468+
await farmer_api.solution_response(solution_response, solver_peer)
469+
470+
# verify new_proof_of_space was called with correct proof
471+
mock_new_proof.assert_called_once()
472+
call_args = mock_new_proof.call_args[0]
473+
new_proof_of_space = call_args[0]
474+
original_peer = call_args[1]
475+
476+
assert new_proof_of_space.proof.proof == b"test_proof_from_solver"
477+
assert original_peer == harvester_peer
478+
479+
# verify pending request was removed
480+
assert partial_proofs.partial_proofs[0] not in farmer.pending_solver_requests
481+
482+
483+
@pytest.mark.anyio
484+
async def test_solution_response_unknown_quality(
485+
farmer_one_harvester_solver: tuple[list[HarvesterService], FarmerService, SolverService, BlockTools],
486+
) -> None:
487+
_, farmer_service, _, _ = farmer_one_harvester_solver
488+
farmer_api = farmer_service._api
489+
farmer = farmer_api.farmer
490+
491+
# get real solver peer connection
492+
solver_peer = await get_solver_peer(farmer)
493+
494+
# create solution response with unknown quality
495+
solution_response = solver_protocol.SolverResponse(partial_proof=bytes(b"1" * 32), proof=b"test_proof")
496+
497+
with unittest.mock.patch.object(farmer_api, "new_proof_of_space", new_callable=AsyncMock) as mock_new_proof:
498+
await farmer_api.solution_response(solution_response, solver_peer)
499+
# verify new_proof_of_space was NOT called
500+
mock_new_proof.assert_not_called()
501+
# verify pending requests unchanged
502+
assert len(farmer.pending_solver_requests) == 0
503+
504+
505+
@pytest.mark.anyio
506+
async def test_solution_response_empty_proof(
507+
farmer_one_harvester_solver: tuple[list[HarvesterService], FarmerService, SolverService, BlockTools],
508+
) -> None:
509+
_, farmer_service, _solver_service, _ = farmer_one_harvester_solver
510+
farmer_api = farmer_service._api
511+
farmer = farmer_api.farmer
512+
513+
# set up a pending request
514+
sp_hash = bytes32(b"1" * 32)
515+
challenge_hash = bytes32(b"2" * 32)
516+
517+
partial_proofs = harvester_protocol.PartialProofsData(
518+
challenge_hash=challenge_hash,
519+
sp_hash=sp_hash,
520+
plot_identifier="test_plot_id",
521+
partial_proofs=[b"test_partial_proof_for_quality"],
522+
signage_point_index=uint8(0),
523+
plot_size=uint8(32),
524+
pool_public_key=G1Element(),
525+
pool_contract_puzzle_hash=bytes32(b"4" * 32),
526+
plot_public_key=G1Element(),
527+
)
528+
529+
harvester_peer = Mock()
530+
harvester_peer.peer_node_id = "harvester_peer"
531+
532+
# manually add pending request
533+
farmer.pending_solver_requests[partial_proofs.partial_proofs[0]] = {
534+
"proof_data": partial_proofs.partial_proofs[0],
535+
"peer": harvester_peer,
536+
}
537+
538+
# get real solver peer connection
539+
solver_peer = await get_solver_peer(farmer)
540+
541+
# create solution response with empty proof
542+
solution_response = solver_protocol.SolverResponse(partial_proof=partial_proofs.partial_proofs[0], proof=b"")
543+
544+
with unittest.mock.patch.object(farmer_api, "new_proof_of_space", new_callable=AsyncMock) as mock_new_proof:
545+
await farmer_api.solution_response(solution_response, solver_peer)
546+
547+
# verify new_proof_of_space was NOT called
548+
mock_new_proof.assert_not_called()
549+
550+
# verify pending request was removed (cleanup still happens)
551+
assert partial_proofs.partial_proofs[0] not in farmer.pending_solver_requests
552+
553+
554+
@pytest.mark.anyio
555+
async def test_v2_partial_proofs_solver_exception(
556+
farmer_one_harvester_solver: tuple[list[HarvesterService], FarmerService, SolverService, BlockTools],
557+
) -> None:
558+
_, farmer_service, _solver_service, _ = farmer_one_harvester_solver
559+
farmer_api = farmer_service._api
560+
farmer = farmer_api.farmer
561+
562+
sp_hash = bytes32(b"1" * 32)
563+
challenge_hash = bytes32(b"2" * 32)
564+
565+
sp = farmer_protocol.NewSignagePoint(
566+
challenge_hash=challenge_hash,
567+
challenge_chain_sp=sp_hash,
568+
reward_chain_sp=std_hash(b"1"),
569+
difficulty=uint64(1000),
570+
sub_slot_iters=uint64(1000),
571+
signage_point_index=uint8(0),
572+
peak_height=uint32(1),
573+
last_tx_height=uint32(0),
574+
)
575+
576+
farmer.sps[sp_hash] = [sp]
577+
578+
partial_proofs = harvester_protocol.PartialProofsData(
579+
challenge_hash=challenge_hash,
580+
sp_hash=sp_hash,
581+
plot_identifier="test_plot_id",
582+
partial_proofs=[b"test_partial_proof_1"],
583+
signage_point_index=uint8(0),
584+
plot_size=uint8(32),
585+
pool_public_key=G1Element(),
586+
pool_contract_puzzle_hash=bytes32(b"4" * 32),
587+
plot_public_key=G1Element(),
588+
)
589+
590+
harvester_peer = await get_harvester_peer(farmer)
591+
592+
# Mock send_to_all to raise an exception
593+
with unittest.mock.patch.object(farmer.server, "send_to_all", side_effect=Exception("Solver connection failed")):
594+
await farmer_api.partial_proofs(partial_proofs, harvester_peer)
595+
596+
# verify pending request was cleaned up after exception
597+
assert partial_proofs.partial_proofs[0] not in farmer.pending_solver_requests

chia/_tests/harvester/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)