Skip to content
Merged
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
b0aac77
add solver service
almogdepaz Jul 6, 2025
27209c8
lint
almogdepaz Jul 6, 2025
cc1e06b
wire service evrywhear
almogdepaz Jul 7, 2025
7741337
prover protocol and v2Prover
almogdepaz Jul 16, 2025
8c4f578
format name
almogdepaz Jul 16, 2025
6aef294
format
almogdepaz Jul 16, 2025
3f3b134
refactor filename
almogdepaz Jul 16, 2025
6e28f8c
tests/raise unimplemented
almogdepaz Jul 17, 2025
7811dbb
add get_filename_str to mock
almogdepaz Jul 17, 2025
441aed5
rename methods
almogdepaz Jul 17, 2025
bffc732
rename
almogdepaz Jul 17, 2025
17b0b34
refactor
almogdepaz Jul 20, 2025
42f3348
improve coverage
almogdepaz Jul 20, 2025
e3a8cfc
test from bytes
almogdepaz Jul 22, 2025
95a83c7
Merge branch 'plotmanager_v2' into new_solver_service
almogdepaz Jul 28, 2025
e4b8a7d
Merge branch 'main' into new_solver_service
almogdepaz Aug 11, 2025
e9fecc0
harvester and farmer v2 support
almogdepaz Aug 11, 2025
1626309
solver service, api, rpc
almogdepaz Aug 11, 2025
87ffc5e
clenup, add config to tests
almogdepaz Aug 12, 2025
322d348
pre-commit
almogdepaz Aug 12, 2025
5748d4b
remove mocks from test
almogdepaz Aug 12, 2025
2d67500
aync solver, fixtures, tests
almogdepaz Aug 13, 2025
283f8bb
naming
almogdepaz Aug 13, 2025
2bb4087
remove redundant farmer message
almogdepaz Aug 13, 2025
7ab94f3
solver fixtures and tests
almogdepaz Aug 18, 2025
ba75ad2
change quality_string to quality_chain
almogdepaz Aug 18, 2025
93e1d00
Merge branch 'main' into new_solver_service
almogdepaz Aug 18, 2025
63fb5f2
fix service tests, cleanup
almogdepaz Aug 18, 2025
c4ce5ec
name change, fix comments
almogdepaz Aug 18, 2025
cbe39bb
lint
almogdepaz Aug 18, 2025
e9718c9
rename to partial proof
almogdepaz Aug 19, 2025
ad5fb0e
network protocol test, use ThreadPoolExecutor
almogdepaz Aug 19, 2025
7d018af
naming and types
almogdepaz Aug 19, 2025
a86fa01
check filter for both plot versions
almogdepaz Aug 19, 2025
da8c90e
update network protocol tests
arvidn Aug 20, 2025
6159d8d
more pr comments addressed
almogdepaz Aug 20, 2025
f734c44
fix rename
almogdepaz Aug 20, 2025
7a1760f
update network protocol tests (#19977)
arvidn Aug 20, 2025
3c3e160
fix network formatting error
almogdepaz Aug 20, 2025
6f0352a
change to Assertion errors
almogdepaz Aug 20, 2025
b240f98
add todo
almogdepaz Aug 20, 2025
82d3252
remove strength from SolverInfo
almogdepaz Aug 20, 2025
bbc9fc0
more pr comments, fix test to catch assertion
almogdepaz Aug 20, 2025
e6c5e88
revert strength
almogdepaz Aug 21, 2025
5353a38
test and comments
almogdepaz Aug 21, 2025
f45f9d0
rename to partial proof
almogdepaz Aug 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion chia/_tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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, bt.constants, farmer_peer=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
Expand Down
312 changes: 305 additions & 7 deletions chia/_tests/farmer_harvester/test_farmer_harvester.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -19,16 +21,37 @@
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
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]


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

Expand Down Expand Up @@ -144,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)
Expand All @@ -161,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(5, 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


Expand Down Expand Up @@ -298,3 +323,276 @@ 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_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
farmer_api = farmer_service._api
farmer = farmer_api.farmer

sp_hash = bytes32(b"1" * 32)
partial_proofs = harvester_protocol.PartialProofsData(
challenge_hash=bytes32(b"2" * 32),
sp_hash=sp_hash,
plot_identifier="test_plot_id",
partial_proofs=[b"test_partial_proof_1"],
signage_point_index=uint8(0),
plot_size=uint8(32),
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.partial_proofs(partial_proofs, 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_partial_proofs_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)
partial_proofs = harvester_protocol.PartialProofsData(
challenge_hash=bytes32(b"2" * 32),
sp_hash=sp_hash,
plot_identifier="test_plot_id",
partial_proofs=[b"test_partial_proof_1"],
signage_point_index=uint8(0),
plot_size=uint8(32),
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.partial_proofs(partial_proofs, harvester_peer)

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_partial_proofs_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]

partial_proofs = harvester_protocol.PartialProofsData(
challenge_hash=challenge_hash,
sp_hash=sp_hash,
plot_identifier="test_plot_id",
partial_proofs=[b"test_partial_proof_1", b"test_partial_proof_2"],
signage_point_index=uint8(0),
plot_size=uint8(32),
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.partial_proofs(partial_proofs, 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, _, _ = 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)

partial_proofs = harvester_protocol.PartialProofsData(
challenge_hash=challenge_hash,
sp_hash=sp_hash,
plot_identifier="test_plot_id",
partial_proofs=[b"test_partial_proof_for_quality"],
signage_point_index=uint8(0),
plot_size=uint8(32),
pool_public_key=G1Element(),
pool_contract_puzzle_hash=bytes32(b"4" * 32),
plot_public_key=G1Element(),
)

harvester_peer = await get_harvester_peer(farmer)

# manually add pending request
farmer.pending_solver_requests[quality] = {
"proof_data": partial_proofs,
"peer": harvester_peer,
}

# create solution response
solution_response = solver_protocol.SolverResponse(partial_proof=quality, 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 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(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)
# 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:
_, 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)

partial_proofs = harvester_protocol.PartialProofsData(
challenge_hash=challenge_hash,
sp_hash=sp_hash,
plot_identifier="test_plot_id",
partial_proofs=[b"test_partial_proof_for_quality"],
signage_point_index=uint8(0),
plot_size=uint8(32),
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] = {
"proof_data": partial_proofs,
"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(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)

# 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_partial_proofs_solver_exception(
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

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]

partial_proofs = harvester_protocol.PartialProofsData(
challenge_hash=challenge_hash,
sp_hash=sp_hash,
plot_identifier="test_plot_id",
partial_proofs=[b"test_partial_proof_1"],
signage_point_index=uint8(0),
plot_size=uint8(32),
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.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
Empty file.
Loading
Loading