Skip to content

Commit e9fecc0

Browse files
committed
harvester and farmer v2 support
1 parent e4b8a7d commit e9fecc0

File tree

5 files changed

+432
-25
lines changed

5 files changed

+432
-25
lines changed
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
from __future__ import annotations
2+
3+
from pathlib import Path
4+
from unittest.mock import MagicMock, patch
5+
6+
import pytest
7+
from chia_rs import ProofOfSpace
8+
from chia_rs.sized_bytes import bytes32
9+
from chia_rs.sized_ints import uint64
10+
11+
from chia._tests.conftest import HarvesterFarmerEnvironment
12+
from chia.harvester.harvester_api import HarvesterAPI
13+
from chia.plotting.util import PlotInfo
14+
from chia.protocols import harvester_protocol
15+
from chia.protocols.harvester_protocol import PoolDifficulty
16+
from chia.server.ws_connection import WSChiaConnection
17+
from chia.simulator.block_tools import BlockTools
18+
19+
20+
def create_signage_point_harvester_from_constants(bt: BlockTools) -> harvester_protocol.NewSignagePointHarvester:
21+
"""create a NewSignagePointHarvester using real constants from block tools"""
22+
# use the pre-generated signage point data from network_protocol_data.py
23+
# but with real constants from block_tools
24+
from chia._tests.util.network_protocol_data import new_signage_point_harvester
25+
26+
# create a version with real constants values
27+
return harvester_protocol.NewSignagePointHarvester(
28+
challenge_hash=new_signage_point_harvester.challenge_hash,
29+
difficulty=uint64(bt.constants.DIFFICULTY_STARTING),
30+
sub_slot_iters=uint64(bt.constants.SUB_SLOT_ITERS_STARTING),
31+
signage_point_index=new_signage_point_harvester.signage_point_index,
32+
sp_hash=new_signage_point_harvester.sp_hash,
33+
pool_difficulties=[], # empty for simplicity, unless testing pool functionality
34+
peak_height=new_signage_point_harvester.peak_height,
35+
last_tx_height=new_signage_point_harvester.last_tx_height,
36+
)
37+
38+
39+
@pytest.mark.anyio
40+
async def test_new_signage_point_harvester_no_keys(
41+
harvester_farmer_environment: HarvesterFarmerEnvironment,
42+
) -> None:
43+
"""test that new_signage_point_harvester returns early when no keys available"""
44+
_farmer_service, _farmer_rpc_client, harvester_service, _harvester_rpc_client, bt = harvester_farmer_environment
45+
harvester_api = harvester_service._server.api
46+
assert isinstance(harvester_api, HarvesterAPI)
47+
48+
# create real signage point data from block tools
49+
new_challenge = create_signage_point_harvester_from_constants(bt)
50+
51+
# mock plot manager to return false for public_keys_available
52+
with patch.object(harvester_api.harvester.plot_manager, "public_keys_available", return_value=False):
53+
mock_peer = MagicMock(spec=WSChiaConnection)
54+
55+
result = harvester_api.new_signage_point_harvester(new_challenge, mock_peer)
56+
assert result is None
57+
58+
59+
@pytest.mark.anyio
60+
async def test_new_signage_point_harvester_happy_path(
61+
harvester_farmer_environment: HarvesterFarmerEnvironment,
62+
) -> None:
63+
"""test successful signage point processing with valid plots"""
64+
_farmer_service, _farmer_rpc_client, harvester_service, _harvester_rpc_client, bt = harvester_farmer_environment
65+
harvester_api = harvester_service._server.api
66+
assert isinstance(harvester_api, HarvesterAPI)
67+
68+
# create real signage point data from block tools
69+
new_challenge = create_signage_point_harvester_from_constants(bt)
70+
71+
mock_peer = MagicMock(spec=WSChiaConnection)
72+
73+
# create mock plot info
74+
mock_prover = MagicMock()
75+
mock_prover.get_id.return_value = bytes32(b"2" * 32)
76+
mock_prover.get_size.return_value = 32
77+
mock_prover.get_qualities_for_challenge.return_value = [bytes32(b"quality" + b"0" * 25)]
78+
79+
mock_plot_info = MagicMock(spec=PlotInfo)
80+
mock_plot_info.prover = mock_prover
81+
mock_plot_info.pool_contract_puzzle_hash = None
82+
83+
plot_path = Path("/fake/plot.plot")
84+
85+
with patch.object(harvester_api.harvester.plot_manager, "public_keys_available", return_value=True):
86+
with patch.object(harvester_api.harvester.plot_manager, "plots", {plot_path: mock_plot_info}):
87+
with patch("chia.harvester.harvester_api.passes_plot_filter", return_value=True):
88+
with patch("chia.harvester.harvester_api.calculate_pos_challenge") as mock_calc_pos:
89+
mock_calc_pos.return_value = bytes32(b"sp_challenge" + b"0" * 20)
90+
91+
with patch("chia.harvester.harvester_api.calculate_iterations_quality") as mock_calc_iter:
92+
# set required_iters low enough to pass the sp_interval_iters check
93+
mock_calc_iter.return_value = uint64(1000)
94+
95+
with patch("chia.harvester.harvester_api.calculate_sp_interval_iters") as mock_sp_interval:
96+
mock_sp_interval.return_value = uint64(10000)
97+
98+
with patch.object(mock_prover, "get_full_proof") as mock_get_proof:
99+
mock_proof = MagicMock(spec=ProofOfSpace)
100+
mock_get_proof.return_value = mock_proof, None
101+
102+
result = harvester_api.new_signage_point_harvester(new_challenge, mock_peer)
103+
# function returns None but should have processed the plot
104+
assert result is None
105+
106+
107+
@pytest.mark.anyio
108+
async def test_new_signage_point_harvester_pool_difficulty_override(
109+
harvester_farmer_environment: HarvesterFarmerEnvironment,
110+
) -> None:
111+
"""test that pool difficulty overrides are applied correctly"""
112+
_farmer_service, _farmer_rpc_client, harvester_service, _harvester_rpc_client, bt = harvester_farmer_environment
113+
harvester_api = harvester_service._server.api
114+
assert isinstance(harvester_api, HarvesterAPI)
115+
116+
mock_peer = MagicMock(spec=WSChiaConnection)
117+
118+
pool_puzzle_hash = bytes32(b"pool" + b"0" * 28)
119+
120+
mock_prover = MagicMock()
121+
mock_prover.get_id.return_value = bytes32(b"2" * 32)
122+
mock_prover.get_size.return_value = 32
123+
mock_prover.get_qualities_for_challenge.return_value = [bytes32(b"quality" + b"0" * 25)]
124+
125+
mock_plot_info = MagicMock(spec=PlotInfo)
126+
mock_plot_info.prover = mock_prover
127+
mock_plot_info.pool_contract_puzzle_hash = pool_puzzle_hash
128+
129+
plot_path = Path("/fake/plot.plot")
130+
131+
pool_difficulty = PoolDifficulty(
132+
pool_contract_puzzle_hash=pool_puzzle_hash,
133+
difficulty=uint64(500), # lower than main difficulty
134+
sub_slot_iters=uint64(67108864), # different from main
135+
)
136+
137+
# create real signage point data from constants with pool difficulty
138+
new_challenge = create_signage_point_harvester_from_constants(bt)
139+
# override with pool difficulty for this test
140+
new_challenge = harvester_protocol.NewSignagePointHarvester(
141+
challenge_hash=new_challenge.challenge_hash,
142+
difficulty=new_challenge.difficulty,
143+
sub_slot_iters=new_challenge.sub_slot_iters,
144+
signage_point_index=new_challenge.signage_point_index,
145+
sp_hash=new_challenge.sp_hash,
146+
pool_difficulties=[pool_difficulty], # add pool difficulty
147+
peak_height=new_challenge.peak_height,
148+
last_tx_height=new_challenge.last_tx_height,
149+
)
150+
151+
with patch.object(harvester_api.harvester.plot_manager, "public_keys_available", return_value=True):
152+
with patch.object(harvester_api.harvester.plot_manager, "plots", {plot_path: mock_plot_info}):
153+
with patch("chia.harvester.harvester_api.passes_plot_filter", return_value=True):
154+
with patch("chia.harvester.harvester_api.calculate_pos_challenge") as mock_calc_pos:
155+
mock_calc_pos.return_value = bytes32(b"sp_challenge" + b"0" * 20)
156+
157+
with patch("chia.harvester.harvester_api.calculate_iterations_quality") as mock_calc_iter:
158+
mock_calc_iter.return_value = uint64(1000)
159+
160+
with patch("chia.harvester.harvester_api.calculate_sp_interval_iters") as mock_sp_interval:
161+
mock_sp_interval.return_value = uint64(10000)
162+
163+
with patch.object(mock_prover, "get_full_proof") as mock_get_proof:
164+
mock_proof = MagicMock(spec=ProofOfSpace)
165+
mock_get_proof.return_value = mock_proof, None
166+
167+
result = harvester_api.new_signage_point_harvester(new_challenge, mock_peer)
168+
169+
# verify that calculate_iterations_quality was called with pool difficulty
170+
mock_calc_iter.assert_called()
171+
call_args = mock_calc_iter.call_args[0]
172+
assert call_args[3] == uint64(500) # pool difficulty was used
173+
174+
assert result is None
175+
176+
177+
@pytest.mark.anyio
178+
async def test_new_signage_point_harvester_prover_error(
179+
harvester_farmer_environment: HarvesterFarmerEnvironment,
180+
) -> None:
181+
"""test error handling when prover fails"""
182+
_farmer_service, _farmer_rpc_client, harvester_service, _harvester_rpc_client, bt = harvester_farmer_environment
183+
harvester_api = harvester_service._server.api
184+
assert isinstance(harvester_api, HarvesterAPI)
185+
186+
# create real signage point data from block tools
187+
new_challenge = create_signage_point_harvester_from_constants(bt)
188+
189+
mock_peer = MagicMock(spec=WSChiaConnection)
190+
191+
mock_prover = MagicMock()
192+
mock_prover.get_id.return_value = bytes32(b"2" * 32)
193+
mock_prover.get_qualities_for_challenge.side_effect = RuntimeError("test error")
194+
195+
mock_plot_info = MagicMock(spec=PlotInfo)
196+
mock_plot_info.prover = mock_prover
197+
mock_plot_info.pool_contract_puzzle_hash = None
198+
199+
plot_path = Path("/fake/plot.plot")
200+
201+
with patch.object(harvester_api.harvester.plot_manager, "public_keys_available", return_value=True):
202+
with patch.object(harvester_api.harvester.plot_manager, "plots", {plot_path: mock_plot_info}):
203+
with patch("chia.harvester.harvester_api.passes_plot_filter", return_value=True):
204+
with patch("chia.harvester.harvester_api.calculate_pos_challenge") as mock_calc_pos:
205+
mock_calc_pos.return_value = bytes32(b"sp_challenge" + b"0" * 20)
206+
207+
# should not raise exception, should handle error gracefully
208+
result = harvester_api.new_signage_point_harvester(new_challenge, mock_peer)
209+
assert result is None

chia/farmer/farmer_api.py

Lines changed: 94 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from typing import TYPE_CHECKING, Any, ClassVar, Optional, Union, cast
77

88
import aiohttp
9-
from chia_rs import AugSchemeMPL, G2Element, PoolTarget, PrivateKey
9+
from chia_rs import AugSchemeMPL, G2Element, PoolTarget, PrivateKey, ProofOfSpace
1010
from chia_rs.sized_bytes import bytes32
1111
from chia_rs.sized_ints import uint8, uint16, uint32, uint64
1212

@@ -15,7 +15,7 @@
1515
from chia.farmer.farmer import Farmer, increment_pool_stats, strip_old_entries
1616
from chia.harvester.harvester_api import HarvesterAPI
1717
from chia.protocols import farmer_protocol, harvester_protocol
18-
from chia.protocols.farmer_protocol import DeclareProofOfSpace, SignedValues
18+
from chia.protocols.farmer_protocol import DeclareProofOfSpace, SignedValues, SolutionResponse
1919
from chia.protocols.harvester_protocol import (
2020
PlotSyncDone,
2121
PlotSyncPathList,
@@ -24,6 +24,7 @@
2424
PoolDifficulty,
2525
SignatureRequestSourceData,
2626
SigningDataKind,
27+
V2Qualities,
2728
)
2829
from chia.protocols.outbound_message import Message, NodeType, make_msg
2930
from chia.protocols.pool_protocol import (
@@ -33,6 +34,7 @@
3334
get_current_authentication_token,
3435
)
3536
from chia.protocols.protocol_message_types import ProtocolMessageTypes
37+
from chia.protocols.solver_protocol import SolverInfo
3638
from chia.server.api_protocol import ApiMetadata
3739
from chia.server.server import ssl_context_for_root
3840
from chia.server.ws_connection import WSChiaConnection
@@ -73,7 +75,7 @@ async def new_proof_of_space(
7375
"""
7476
if new_proof_of_space.sp_hash not in self.farmer.number_of_responses:
7577
self.farmer.number_of_responses[new_proof_of_space.sp_hash] = 0
76-
self.farmer.cache_add_time[new_proof_of_space.sp_hash] = uint64(time.time())
78+
self.farmer.cache_add_time[new_proof_of_space.sp_hash] = uint64(int(time.time()))
7779

7880
max_pos_per_sp = 5
7981

@@ -170,14 +172,14 @@ async def new_proof_of_space(
170172
new_proof_of_space.proof,
171173
)
172174
)
173-
self.farmer.cache_add_time[new_proof_of_space.sp_hash] = uint64(time.time())
175+
self.farmer.cache_add_time[new_proof_of_space.sp_hash] = uint64(int(time.time()))
174176
self.farmer.quality_str_to_identifiers[computed_quality_string] = (
175177
new_proof_of_space.plot_identifier,
176178
new_proof_of_space.challenge_hash,
177179
new_proof_of_space.sp_hash,
178180
peer.peer_node_id,
179181
)
180-
self.farmer.cache_add_time[computed_quality_string] = uint64(time.time())
182+
self.farmer.cache_add_time[computed_quality_string] = uint64(int(time.time()))
181183

182184
await peer.send_message(make_msg(ProtocolMessageTypes.request_signatures, request))
183185

@@ -478,6 +480,92 @@ async def new_proof_of_space(
478480

479481
return
480482

483+
@metadata.request(peer_required=True)
484+
async def v2_qualities(self, quality_collection: V2Qualities, peer: WSChiaConnection) -> None:
485+
"""
486+
This is a response from the harvester for V2 plots, containing only qualities.
487+
We store these qualities and will later use solver service to generate proofs when needed.
488+
"""
489+
if quality_collection.sp_hash not in self.farmer.number_of_responses:
490+
self.farmer.number_of_responses[quality_collection.sp_hash] = 0
491+
self.farmer.cache_add_time[quality_collection.sp_hash] = uint64(int(time.time()))
492+
493+
if quality_collection.sp_hash not in self.farmer.sps:
494+
self.farmer.log.warning(
495+
f"Received V2 quality collection for a signage point that we do not have {quality_collection.sp_hash}"
496+
)
497+
return None
498+
499+
self.farmer.cache_add_time[quality_collection.sp_hash] = uint64(int(time.time()))
500+
501+
self.farmer.log.info(
502+
f"Received V2 quality collection with {len(quality_collection.qualities)} qualities "
503+
f"for plot {quality_collection.plot_identifier[:10]}... from {peer.peer_node_id}"
504+
)
505+
506+
# Process each quality through solver service to get full proofs
507+
for quality in quality_collection.qualities:
508+
solver_info = SolverInfo(
509+
plot_size=quality_collection.plot_size,
510+
plot_diffculty=quality_collection.difficulty, # Note: typo in SolverInfo field name
511+
quality_string=quality,
512+
)
513+
514+
# Call solver service to get proof
515+
# TODO: Add proper solver service node connection to farmer
516+
# For now, assume solver service is available via server connections
517+
proof_bytes = None
518+
519+
# Try to call solver service - this requires farmer to have solver connections configured
520+
try:
521+
# Send solve request to solver service
522+
# This would work if farmer is connected to a solver service node
523+
solver_response = await self.farmer.server.send_to_all_and_wait_first(
524+
[make_msg(ProtocolMessageTypes.solve, solver_info)],
525+
NodeType.FARMER, # TODO: Need SOLVER node type
526+
)
527+
528+
if solver_response is not None and isinstance(solver_response, SolutionResponse):
529+
proof_bytes = solver_response.proof
530+
self.farmer.log.debug(f"Received {len(proof_bytes)} byte proof from solver")
531+
else:
532+
self.farmer.log.warning(f"No valid solver response for quality {quality.hex()[:10]}...")
533+
534+
except Exception as e:
535+
self.farmer.log.error(f"Failed to call solver service for quality {quality.hex()[:10]}...: {e}")
536+
537+
# Fall back to stub if solver service unavailable
538+
if proof_bytes is None:
539+
self.farmer.log.warning("Using stub proof - solver service not available")
540+
proof_bytes = b"stub_proof_from_solver"
541+
542+
# Create ProofOfSpace object using solver response and plot metadata
543+
# Need to calculate sp_challenge_hash for ProofOfSpace constructor
544+
# TODO: We need plot_id to calculate this properly - may need to add to V2Qualities
545+
sp_challenge_hash = quality_collection.challenge_hash # Approximation for now
546+
547+
# Create a NewProofOfSpace object that can go through existing flow
548+
new_proof_of_space = harvester_protocol.NewProofOfSpace(
549+
quality_collection.challenge_hash,
550+
quality_collection.sp_hash,
551+
quality_collection.plot_identifier,
552+
ProofOfSpace(
553+
sp_challenge_hash,
554+
quality_collection.pool_public_key,
555+
quality_collection.pool_contract_puzzle_hash,
556+
quality_collection.plot_public_key,
557+
quality_collection.plot_size,
558+
proof_bytes,
559+
),
560+
quality_collection.signage_point_index,
561+
include_source_signature_data=False,
562+
farmer_reward_address_override=None,
563+
fee_info=None,
564+
)
565+
566+
# Route through existing new_proof_of_space flow
567+
await self.new_proof_of_space(new_proof_of_space, peer)
568+
481569
@metadata.request()
482570
async def respond_signatures(self, response: harvester_protocol.RespondSignatures) -> None:
483571
request = self._process_respond_signatures(response)
@@ -558,7 +646,7 @@ async def new_signage_point(self, new_signage_point: farmer_protocol.NewSignageP
558646

559647
pool_dict[key] = strip_old_entries(pairs=pool_dict[key], before=cutoff_24h)
560648

561-
now = uint64(time.time())
649+
now = uint64(int(time.time()))
562650
self.farmer.cache_add_time[new_signage_point.challenge_chain_sp] = now
563651
missing_signage_points = self.farmer.check_missing_signage_points(now, new_signage_point)
564652
self.farmer.state_changed(

0 commit comments

Comments
 (0)