1
1
from __future__ import annotations
2
2
3
3
import asyncio
4
+ import unittest .mock
4
5
from math import floor
5
6
from pathlib import Path
6
7
from typing import Any , Optional
8
+ from unittest .mock import AsyncMock , Mock
7
9
8
10
import pytest
9
11
from chia_rs import G1Element
19
21
from chia .harvester .harvester_rpc_client import HarvesterRpcClient
20
22
from chia .harvester .harvester_service import HarvesterService
21
23
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
23
25
from chia .protocols .outbound_message import NodeType , make_msg
24
26
from chia .protocols .protocol_message_types import ProtocolMessageTypes
25
27
from chia .simulator .block_tools import BlockTools
28
+ from chia .solver .solver_service import SolverService
26
29
from chia .types .peer_info import UnresolvedPeerInfo
27
30
from chia .util .config import load_config
28
31
from chia .util .hash import std_hash
29
32
from chia .util .keychain import generate_mnemonic
30
33
31
34
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
+
32
55
def farmer_is_started (farmer : Farmer ) -> bool :
33
56
return farmer .started
34
57
@@ -144,9 +167,6 @@ async def test_farmer_respond_signatures(
144
167
# messages even though it didn't request them, to cover when the farmer doesn't know
145
168
# about an sp_hash, so it fails at the sp record check.
146
169
147
- def log_is_ready () -> bool :
148
- return len (caplog .text ) > 0
149
-
150
170
_ , _ , harvester_service , _ , _ = harvester_farmer_environment
151
171
# We won't have an sp record for this one
152
172
challenge_hash = bytes32 (b"1" * 32 )
@@ -161,11 +181,16 @@ def log_is_ready() -> bool:
161
181
include_source_signature_data = False ,
162
182
farmer_reward_address_override = None ,
163
183
)
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
+
164
190
msg = make_msg (ProtocolMessageTypes .respond_signatures , response )
165
191
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
169
194
assert expected_error in caplog .text
170
195
171
196
@@ -298,3 +323,275 @@ async def test_harvester_has_no_server(
298
323
harvester_server = harvesters [0 ]._server
299
324
300
325
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
0 commit comments