Files
chia-blockchain/chia/_tests/farmer_harvester/test_farmer.py
T
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Matt
67cdfcda41 build(deps): bump ruff from 0.15.21 to 0.16.0 (#21238)
* build(deps): bump ruff from 0.15.21 to 0.16.0

Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.21 to 0.16.0.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.21...0.16.0)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* affect changes

* Restrict formtting to chia/tools/benchmarks

* Fix generated protocol files

* more noqa

* network protocol files

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Matt <quexington@gmail.com>
2026-08-06 09:34:35 -07:00

1679 lines
66 KiB
Python

from __future__ import annotations
import json
import logging
import re
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from dataclasses import dataclass
from time import time
from types import TracebackType
from typing import Any, cast
from unittest.mock import ANY, AsyncMock
import pytest
from chia_rs import AugSchemeMPL, G1Element, G2Element, PlotParam, PrivateKey, ProofOfSpace
from chia_rs.sized_bytes import bytes32
from chia_rs.sized_ints import uint8, uint16, uint32, uint64
from pytest_mock import MockerFixture
from typing_extensions import Self
from yarl import URL
from chia import __version__
from chia._tests.conftest import FarmerOneHarvester, HarvesterFarmerEnvironment
from chia._tests.util.misc import DataCase, Marks, datacases
from chia.consensus.default_constants import DEFAULT_CONSTANTS
from chia.farmer.farmer import (
UPDATE_POOL_FARMER_INFO_INTERVAL,
Farmer,
increment_pool_stats,
strip_old_entries,
)
from chia.farmer.farmer_service import FarmerService
from chia.harvester.harvester_service import HarvesterService
from chia.pools.pool_config import PoolingShareState
from chia.protocols import farmer_protocol, harvester_protocol, pool_protocol
from chia.protocols.harvester_protocol import NewProofOfSpace, RespondSignatures
from chia.protocols.pool_protocol import (
ErrorResponse,
GetAuthResponse,
GetFarmerResponse,
PoolErrorCode,
PostFarmerResponse,
PutFarmerResponse,
)
from chia.server.ws_connection import WSChiaConnection
from chia.simulator.block_tools import BlockTools
from chia.types.blockchain_format.proof_of_space import (
generate_plot_public_key,
make_pos,
verify_and_get_quality_string,
)
from chia.util.hash import std_hash
from chia.wallet.derive_keys import master_sk_to_singleton_owner_sk, master_sk_to_wallet_sk_unhardened
from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import (
DEFAULT_HIDDEN_PUZZLE_HASH,
calculate_synthetic_secret_key,
)
log = logging.getLogger(__name__)
class StripOldEntriesCase:
pairs: list[tuple[float, int]]
before: float
expected_result: list[tuple[float, int]]
def __init__(self, pairs: list[tuple[float, int]], before: float, expected_result: list[tuple[float, int]]):
self.pairs = pairs
self.before = before
self.expected_result = expected_result
class IncrementPoolStatsCase:
pool_states: dict[bytes32, Any]
p2_singleton_puzzle_hash: bytes32
name: str
current_time: float
count: int
value: int | dict[str, Any] | None
expected_result: Any
def __init__(
self,
p2_singleton_puzzle_hash: bytes32,
name: str,
current_time: float,
count: int,
value: int | dict[str, Any] | None,
expected_result: Any,
):
prepared_p2_singleton_puzzle_hash = std_hash(b"11223344")
self.pool_states = {
prepared_p2_singleton_puzzle_hash: {
"p2_singleton_puzzle_hash": prepared_p2_singleton_puzzle_hash.hex(),
"xxx_since_start": 1,
"xxx_24h": [(1689491043, 1)],
"current_difficulty": 1,
}
}
self.p2_singleton_puzzle_hash = p2_singleton_puzzle_hash
self.name = name
self.current_time = current_time
self.count = count
self.value = value
self.expected_result = expected_result
class DummyHarvesterPeer:
return_invalid_response: bool
peer_node_id: bytes32 = std_hash(b"1")
version: str
def __init__(self, return_invalid_response: bool = False, version: str = "1.0.0"):
self.return_invalid_response = return_invalid_response
self.version = version
async def send_message(self, arg1: Any) -> None:
pass
async def call_api(self, arg1: Any, request: harvester_protocol.RequestSignatures) -> Any:
if self.return_invalid_response:
return 0
local_sk: PrivateKey = PrivateKey.from_bytes(
bytes.fromhex("185c61579e152bbd3b2face3245951e7c67f67f8d2fb2aa4a56e178186c1e5fa")
)
farmer_public_key = G1Element.from_bytes(
bytes.fromhex(
"af59ca047f2f34a4db6e4fa313764f6e71d4c53bc316517e0fa486b8e3d3f36f112ee078e3226854b97791452e3a9fd4"
)
)
agg_pk = generate_plot_public_key(local_sk.get_g1(), farmer_public_key, True)
message = request.messages[0]
signature: G2Element = AugSchemeMPL.sign(local_sk, message, agg_pk)
return RespondSignatures(
plot_identifier=request.plot_identifier,
challenge_hash=request.challenge_hash,
sp_hash=request.sp_hash,
local_pk=local_sk.get_g1(),
farmer_pk=farmer_public_key,
message_signatures=[(message, signature)],
include_source_signature_data=False,
farmer_reward_address_override=None,
)
@dataclass
class NewProofOfSpaceCase:
difficulty: uint64
sub_slot_iters: uint64
challenge_hash: bytes32
sp_hash: bytes32
plot_identifier: str
signage_point_index: uint8
plot_id: bytes32
plot_size: PlotParam
plot_challenge: bytes32
plot_public_key: G1Element
pool_public_key: G1Element | None
pool_contract_puzzle_hash: bytes32
height: uint32
proof: bytes
pool_config: PoolingShareState
pool_difficulty: uint64 | None
authentication_token_timeout: uint8 | None
farmer_private_keys: list[PrivateKey]
authentication_keys: dict[bytes32, PrivateKey]
use_invalid_peer_response: bool
expected_pool_state: dict[str, Any]
marks: Marks = ()
# This creates a test case whose proof of space passes plot filter and quality check
@classmethod
def create_verified_quality_case(
cls,
difficulty: uint64,
sub_slot_iters: uint64,
pool_url: str,
pool_difficulty: uint64 | None,
authentication_token_timeout: uint8 | None,
use_invalid_peer_response: bool,
has_valid_authentication_keys: bool,
expected_pool_stats: dict[str, Any],
) -> NewProofOfSpaceCase:
p2_singleton_puzzle_hash = bytes32.fromhex("302e05a1e6af431c22043ae2a9a8f71148c955c372697cb8ab348160976283df")
pool_config = PoolingShareState(
launcher_id=bytes32.fromhex("ae4ef3b9bfe68949691281a015a9c16630fc8f66d48c19ca548fb80768791afa"),
pool_url=pool_url,
payout_instructions="c2b08e41d766da4116e388357ed957d04ad754623a915f3fd65188a8746cf3e8",
target_puzzle_hash=bytes32.fromhex("344587cf06a39db471d2cc027504e8688a0a67cce961253500c956c73603fd58"),
p2_singleton_puzzle_hash=p2_singleton_puzzle_hash,
owner_public_key=G1Element.from_bytes(
bytes.fromhex(
"8348455278ecec68325b6754b1f3218cde1511ca9393197e7876d7ae04af1c4dd86b0c50601cf5daeb034e8f7c226537"
)
),
key_derivation_index=-1,
)
expected_pool_state = expected_pool_stats.copy()
expected_pool_state["p2_singleton_puzzle_hash"] = p2_singleton_puzzle_hash.hex()
expected_pool_state["pool_config"] = pool_config
return NewProofOfSpaceCase(
difficulty=difficulty,
sub_slot_iters=sub_slot_iters,
challenge_hash=bytes32.fromhex("9dcc155cafa8723627d01d00118767269a15f5f0d496dc9c2940a45b63c0c00c"),
sp_hash=bytes32.fromhex("bca3ac17a0a8fc072188b3738455a3f006528d181c39e31c88e8755492575272"),
plot_identifier="test",
signage_point_index=uint8(1),
plot_id=bytes32.fromhex("baaa6780c53d4b3739b8807b4ae79a76644ddf0d9e03dc7d0a6a0e613e764d9f"),
plot_size=PlotParam.make_v1(32),
plot_challenge=bytes32.fromhex("7580e4c366dc2c94c37ce44943f9629a3cd6e027d7b24cd014adeaa578d4b0a2"),
plot_public_key=G1Element.from_bytes(
bytes.fromhex(
"a6126295fbf0f50dbed8dc41e236241413fdc8a97e650e3ed69d66d0921d3236f8961cc1cf8c1b195521c2d9143048e2"
)
),
pool_public_key=None,
pool_contract_puzzle_hash=p2_singleton_puzzle_hash,
height=uint32(1),
proof=bytes.fromhex(
"2aa93f8c112c274d3c707d94d0a0e07b6710d96fca284baa198ca4632ee1f591"
"edad47d97e854aec76dc08519614a0d17253fc24fbaab66f558b2f6afd2210ac"
"ac09b48e01fd0b8334eae94d55db5df28c80c03a586ce8afe3b986e6aceef4d4"
"93a7c0b1bf30baaafedc1ef9af6a1eee911e3873e9229a0bf18cfc95bcfc3dac"
"36522c84a85fd16a3ff501b44024e371bbf7a3ee9d4ded6e05c82e95083ea5ea"
"d2d64e4a46e0550e98fc56b30760b4bcf439ac0ee675e157fcfecd4b294c6d14"
"987883e3659777c68d6a6962c770a66817f4dc641b5823b077093df10da031c1"
"4b32005bf38eb53944bf3bc5a1d19c21e3b96759b7f557b4668687b7a8a36344"
),
pool_config=pool_config,
pool_difficulty=pool_difficulty,
authentication_token_timeout=authentication_token_timeout,
farmer_private_keys=[
PrivateKey.from_bytes(bytes.fromhex("3cb188d8b2469bb8414a3ec68857959e231a8bec836df199a896ff523b9d2f7d"))
],
authentication_keys=(
{
p2_singleton_puzzle_hash: PrivateKey.from_bytes(
bytes.fromhex("11ed596eb95b31364a9185e948f6b66be30415f816819449d5d40751dc70e786")
),
}
if has_valid_authentication_keys
else {}
),
use_invalid_peer_response=use_invalid_peer_response,
expected_pool_state=expected_pool_state,
)
@pytest.mark.parametrize(
argnames="case",
argvalues=[
pytest.param(StripOldEntriesCase([], 0, []), id="no_params"),
pytest.param(StripOldEntriesCase([(1689491043.3493967, 1)], 1689491044, []), id="stripped"),
pytest.param(
StripOldEntriesCase([(1689491043.3493967, 1)], 1689491043, [(1689491043.3493967, 1)]),
id="not_stripped",
),
],
)
def test_strip_old_entries(case: StripOldEntriesCase) -> None:
assert strip_old_entries(case.pairs, case.before) == case.expected_result
@pytest.mark.parametrize(
argnames="case",
argvalues=[
pytest.param(
IncrementPoolStatsCase(
std_hash(b"0"),
"xxx",
1689491043.3493967,
1,
1,
None,
),
id="p2_singleton_puzzle_hash_not_exist",
),
pytest.param(
IncrementPoolStatsCase(
std_hash(b"11223344"),
"yyy",
1689491043.3493967,
1,
1,
{
"p2_singleton_puzzle_hash": std_hash(b"11223344").hex(),
"xxx_since_start": 1,
"xxx_24h": [(1689491043, 1)],
"current_difficulty": 1,
},
),
id="p2_singleton_puzzle_hash_exist_but_no_change",
),
pytest.param(
IncrementPoolStatsCase(
std_hash(b"11223344"),
"xxx",
1689491044.1,
1,
2,
{
"p2_singleton_puzzle_hash": std_hash(b"11223344").hex(),
"xxx_since_start": 2,
"xxx_24h": [(1689491043, 1), (1689491044, 2)],
"current_difficulty": 1,
},
),
id="value_is_set",
),
pytest.param(
IncrementPoolStatsCase(
std_hash(b"11223344"),
"xxx",
1689491044.1,
1,
None,
{
"p2_singleton_puzzle_hash": std_hash(b"11223344").hex(),
"xxx_since_start": 2,
"xxx_24h": [(1689491043, 1), (1689491044, 1)],
"current_difficulty": 1,
},
),
id="value_is_not_set",
),
pytest.param(
IncrementPoolStatsCase(
std_hash(b"11223344"),
"xxx",
1689577444,
1,
None,
{
"p2_singleton_puzzle_hash": std_hash(b"11223344").hex(),
"xxx_since_start": 2,
"xxx_24h": [(1689577444, 1)],
"current_difficulty": 1,
},
),
id="stripped",
),
],
)
def test_increment_pool_stats(case: IncrementPoolStatsCase) -> None:
increment_pool_stats(
case.pool_states, case.p2_singleton_puzzle_hash, case.name, case.current_time, case.count, case.value
)
if case.expected_result is None:
assert case.p2_singleton_puzzle_hash not in case.pool_states
else:
assert case.pool_states[case.p2_singleton_puzzle_hash] == case.expected_result
@pytest.mark.parametrize(
argnames="case",
argvalues=[
pytest.param(
NewProofOfSpaceCase.create_verified_quality_case(
difficulty=uint64(1),
sub_slot_iters=uint64(1000000000000),
pool_url="",
pool_difficulty=uint64(1),
authentication_token_timeout=uint8(10),
use_invalid_peer_response=False,
has_valid_authentication_keys=True,
expected_pool_stats={
"points_found_since_start": 0,
# Original item format here is (timestamp, value) but we'll ignore timestamp part
# so every `xxx_24h` item in this dict will be list[Any].
"points_found_24h": [],
"points_acknowledged_since_start": 0,
"points_acknowledged_24h": [],
"pool_errors_24h": [],
"valid_partials_since_start": 1,
"valid_partials_24h": [1],
"invalid_partials_since_start": 0,
"invalid_partials_24h": [],
"insufficient_partials_since_start": 0,
"insufficient_partials_24h": [],
"stale_partials_since_start": 0,
"stale_partials_24h": [],
"missing_partials_since_start": 0,
"missing_partials_24h": [],
},
),
# Empty pool_url means solo plotNFT farming
id="empty_pool_url",
),
pytest.param(
NewProofOfSpaceCase.create_verified_quality_case(
difficulty=uint64(1),
sub_slot_iters=uint64(1000000000000),
pool_url="http://localhost",
pool_difficulty=None,
authentication_token_timeout=uint8(10),
use_invalid_peer_response=False,
has_valid_authentication_keys=True,
expected_pool_stats={
"points_found_since_start": 0,
# Original item format here is (timestamp, value) but we'll ignore timestamp part
# so every `xxx_24h` item in this dict will be list[Any].
"points_found_24h": [],
"points_acknowledged_since_start": 0,
"points_acknowledged_24h": [],
"pool_errors_24h": [],
"valid_partials_since_start": 0,
"valid_partials_24h": [],
"invalid_partials_since_start": 0,
"invalid_partials_24h": [],
"insufficient_partials_since_start": 0,
"insufficient_partials_24h": [],
"stale_partials_since_start": 0,
"stale_partials_24h": [],
"missing_partials_since_start": 1,
"missing_partials_24h": [None],
},
),
id="empty_current_difficulty",
),
pytest.param(
NewProofOfSpaceCase.create_verified_quality_case(
difficulty=uint64(1),
sub_slot_iters=uint64(64),
pool_url="http://localhost",
pool_difficulty=uint64(1),
authentication_token_timeout=uint8(10),
use_invalid_peer_response=False,
has_valid_authentication_keys=True,
expected_pool_stats={
"points_found_since_start": 0,
# Original item format here is (timestamp, value) but we'll ignore timestamp part
# so every `xxx_24h` item in this dict will be list[Any].
"points_found_24h": [],
"points_acknowledged_since_start": 0,
"points_acknowledged_24h": [],
"pool_errors_24h": [],
"valid_partials_since_start": 0,
"valid_partials_24h": [],
"invalid_partials_since_start": 0,
"invalid_partials_24h": [],
"insufficient_partials_since_start": 1,
"insufficient_partials_24h": [1],
"stale_partials_since_start": 0,
"stale_partials_24h": [],
"missing_partials_since_start": 0,
"missing_partials_24h": [],
},
),
id="quality_not_good",
),
pytest.param(
NewProofOfSpaceCase.create_verified_quality_case(
difficulty=uint64(1),
sub_slot_iters=uint64(1000000000000),
pool_url="http://localhost",
pool_difficulty=uint64(1),
authentication_token_timeout=None,
use_invalid_peer_response=False,
has_valid_authentication_keys=True,
expected_pool_stats={
"points_found_since_start": 0,
# Original item format here is (timestamp, value) but we'll ignore timestamp part
# so every `xxx_24h` item in this dict will be list[Any].
"points_found_24h": [],
"points_acknowledged_since_start": 0,
"points_acknowledged_24h": [],
"pool_errors_24h": [],
"valid_partials_since_start": 0,
"valid_partials_24h": [],
"invalid_partials_since_start": 0,
"invalid_partials_24h": [],
"insufficient_partials_since_start": 0,
"insufficient_partials_24h": [],
"stale_partials_since_start": 0,
"stale_partials_24h": [],
"missing_partials_since_start": 1,
"missing_partials_24h": [1],
},
),
id="empty_auth_timeout",
),
pytest.param(
NewProofOfSpaceCase.create_verified_quality_case(
difficulty=uint64(1),
sub_slot_iters=uint64(1000000000000),
pool_url="http://localhost",
pool_difficulty=uint64(1),
authentication_token_timeout=uint8(10),
use_invalid_peer_response=True,
has_valid_authentication_keys=True,
expected_pool_stats={
"points_found_since_start": 0,
# Original item format here is (timestamp, value) but we'll ignore timestamp part
# so every `xxx_24h` item in this dict will be list[Any].
"points_found_24h": [],
"points_acknowledged_since_start": 0,
"points_acknowledged_24h": [],
"pool_errors_24h": [],
"valid_partials_since_start": 0,
"valid_partials_24h": [],
"invalid_partials_since_start": 1,
"invalid_partials_24h": [1],
"insufficient_partials_since_start": 0,
"insufficient_partials_24h": [],
"stale_partials_since_start": 0,
"stale_partials_24h": [],
"missing_partials_since_start": 0,
"missing_partials_24h": [],
},
),
id="invalid_peer_response",
),
pytest.param(
NewProofOfSpaceCase.create_verified_quality_case(
difficulty=uint64(1),
sub_slot_iters=uint64(1000000000000),
pool_url="http://localhost",
pool_difficulty=uint64(1),
authentication_token_timeout=uint8(10),
use_invalid_peer_response=False,
has_valid_authentication_keys=False,
expected_pool_stats={
"points_found_since_start": 0,
# Original item format here is (timestamp, value) but we'll ignore timestamp part
# so every `xxx_24h` item in this dict will be list[Any].
"points_found_24h": [],
"points_acknowledged_since_start": 0,
"points_acknowledged_24h": [],
"pool_errors_24h": [],
"valid_partials_since_start": 0,
"valid_partials_24h": [],
"invalid_partials_since_start": 0,
"invalid_partials_24h": [],
"insufficient_partials_since_start": 0,
"insufficient_partials_24h": [],
"stale_partials_since_start": 0,
"stale_partials_24h": [],
"missing_partials_since_start": 1,
"missing_partials_24h": [1],
},
),
id="no_valid_auth_keys",
),
pytest.param(
NewProofOfSpaceCase.create_verified_quality_case(
difficulty=uint64(1),
sub_slot_iters=uint64(1000000000000),
pool_url="http://192.168.0.256",
pool_difficulty=uint64(1),
authentication_token_timeout=uint8(10),
use_invalid_peer_response=False,
has_valid_authentication_keys=True,
expected_pool_stats={
"points_found_since_start": 1,
# Original item format here is (timestamp, value) but we'll ignore timestamp part
# so every `xxx_24h` item in this dict will be list[Any].
"points_found_24h": [1],
"points_acknowledged_since_start": 0,
"points_acknowledged_24h": [],
"pool_errors_24h": [{"error_code": uint16(PoolErrorCode.REQUEST_FAILED.value)}],
"valid_partials_since_start": 0,
"valid_partials_24h": [],
"invalid_partials_since_start": 1,
"invalid_partials_24h": [1],
"insufficient_partials_since_start": 0,
"insufficient_partials_24h": [],
"stale_partials_since_start": 0,
"stale_partials_24h": [],
"missing_partials_since_start": 0,
"missing_partials_24h": [],
},
),
id="error_connecting_pool_server",
),
],
)
@pytest.mark.anyio
async def test_farmer_new_proof_of_space_for_pool_stats(
harvester_farmer_environment: HarvesterFarmerEnvironment,
case: NewProofOfSpaceCase,
) -> None:
farmer_service, _farmer_rpc_client, _, _, _ = harvester_farmer_environment
farmer_api = farmer_service._api
sp = farmer_protocol.NewSignagePoint(
challenge_hash=case.challenge_hash,
challenge_chain_sp=case.sp_hash,
reward_chain_sp=std_hash(b"1"),
difficulty=case.difficulty,
sub_slot_iters=case.sub_slot_iters,
signage_point_index=case.signage_point_index,
peak_height=uint32(1),
last_tx_height=uint32(0),
)
pos = make_pos(
challenge=case.plot_challenge,
pool_public_key=case.pool_public_key,
pool_contract_puzzle_hash=case.pool_contract_puzzle_hash,
plot_public_key=case.plot_public_key,
params=case.plot_size,
proof=case.proof,
)
new_pos = NewProofOfSpace(
challenge_hash=case.challenge_hash,
sp_hash=case.sp_hash,
plot_identifier=case.plot_identifier,
proof=pos,
signage_point_index=case.signage_point_index,
include_source_signature_data=False,
farmer_reward_address_override=None,
fee_info=None,
)
p2_singleton_puzzle_hash = case.pool_contract_puzzle_hash
farmer_api.farmer.constants = DEFAULT_CONSTANTS.replace(POOL_SUB_SLOT_ITERS=case.sub_slot_iters)
farmer_api.farmer._private_keys = case.farmer_private_keys
farmer_api.farmer.authentication_keys = case.authentication_keys
farmer_api.farmer.sps[case.sp_hash] = [sp]
farmer_api.farmer.pool_state[p2_singleton_puzzle_hash] = {
"p2_singleton_puzzle_hash": p2_singleton_puzzle_hash.hex(),
"points_found_since_start": 0,
"points_found_24h": [],
"points_acknowledged_since_start": 0,
"points_acknowledged_24h": [],
"next_farmer_update": 0,
"next_pool_info_update": 0,
"current_points": 0,
"current_difficulty": case.pool_difficulty,
"pool_errors_24h": [],
"valid_partials_since_start": 0,
"valid_partials_24h": [],
"invalid_partials_since_start": 0,
"invalid_partials_24h": [],
"insufficient_partials_since_start": 0,
"insufficient_partials_24h": [],
"stale_partials_since_start": 0,
"stale_partials_24h": [],
"missing_partials_since_start": 0,
"missing_partials_24h": [],
"authentication_token_timeout": case.authentication_token_timeout,
"plot_count": 0,
"pool_config": case.pool_config,
}
assert (
verify_and_get_quality_string(
pos,
DEFAULT_CONSTANTS,
case.challenge_hash,
case.sp_hash,
height=uint32(1),
prev_transaction_block_height=uint32(1),
)
is not None
)
peer: Any = DummyHarvesterPeer(case.use_invalid_peer_response)
await farmer_api.new_proof_of_space(new_pos, peer)
def assert_stats_since_start(name: str) -> None:
assert farmer_api.farmer.pool_state[p2_singleton_puzzle_hash][name] == case.expected_pool_state[name]
def assert_stats_24h(name: str) -> None:
assert len(farmer_api.farmer.pool_state[p2_singleton_puzzle_hash][name]) == len(case.expected_pool_state[name])
for i, stat in enumerate(farmer_api.farmer.pool_state[p2_singleton_puzzle_hash][name]):
assert stat[1] == case.expected_pool_state[name][i]
def assert_pool_errors_24h() -> None:
assert len(farmer_api.farmer.pool_state[p2_singleton_puzzle_hash]["pool_errors_24h"]) == len(
case.expected_pool_state["pool_errors_24h"]
)
for i, stat in enumerate(farmer_api.farmer.pool_state[p2_singleton_puzzle_hash]["pool_errors_24h"]):
assert stat[1]["error_code"] == case.expected_pool_state["pool_errors_24h"][i]["error_code"]
assert_stats_since_start("points_found_since_start")
assert_stats_24h("points_found_24h")
assert_stats_since_start("points_acknowledged_since_start")
assert_stats_24h("points_acknowledged_24h")
assert_pool_errors_24h()
assert_stats_since_start("valid_partials_since_start")
assert_stats_24h("valid_partials_24h")
assert_stats_since_start("invalid_partials_since_start")
assert_stats_24h("invalid_partials_24h")
assert_stats_since_start("insufficient_partials_since_start")
assert_stats_24h("insufficient_partials_24h")
assert_stats_since_start("stale_partials_since_start")
assert_stats_24h("stale_partials_24h")
assert_stats_since_start("missing_partials_since_start")
assert_stats_24h("missing_partials_24h")
@dataclass
class DummyPoolResponse:
ok: bool
status: int
error_code: int | None = None
error_message: str | None = None
new_difficulty: int | None = None
raw_response: dict[str, Any] | None = None
async def text(self) -> str:
if self.raw_response is not None:
return json.dumps(self.raw_response)
json_dict: dict[str, Any] = dict()
if self.error_code:
json_dict["error_code"] = self.error_code
json_dict["error_message"] = self.error_message if self.error_message else "error-msg"
elif self.new_difficulty:
json_dict["new_difficulty"] = self.new_difficulty
return json.dumps(json_dict)
async def __aenter__(self) -> Self:
return self
async def __aexit__(
self,
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
pass
def create_valid_pos(farmer: Farmer) -> tuple[farmer_protocol.NewSignagePoint, ProofOfSpace, NewProofOfSpace]:
case = NewProofOfSpaceCase.create_verified_quality_case(
difficulty=uint64(1),
sub_slot_iters=uint64(1000000000000),
pool_url="https://192.168.0.256",
pool_difficulty=uint64(1),
authentication_token_timeout=uint8(10),
use_invalid_peer_response=False,
has_valid_authentication_keys=True,
expected_pool_stats={},
)
sp = farmer_protocol.NewSignagePoint(
challenge_hash=case.challenge_hash,
challenge_chain_sp=case.sp_hash,
reward_chain_sp=std_hash(b"1"),
difficulty=case.difficulty,
sub_slot_iters=case.sub_slot_iters,
signage_point_index=case.signage_point_index,
peak_height=uint32(1),
last_tx_height=uint32(0),
)
pos = make_pos(
challenge=case.plot_challenge,
pool_public_key=case.pool_public_key,
pool_contract_puzzle_hash=case.pool_contract_puzzle_hash,
plot_public_key=case.plot_public_key,
params=case.plot_size,
proof=case.proof,
)
new_pos = NewProofOfSpace(
challenge_hash=case.challenge_hash,
sp_hash=case.sp_hash,
plot_identifier=case.plot_identifier,
proof=pos,
signage_point_index=case.signage_point_index,
include_source_signature_data=False,
farmer_reward_address_override=None,
fee_info=None,
)
p2_singleton_puzzle_hash = case.pool_contract_puzzle_hash
farmer.constants = DEFAULT_CONSTANTS.replace(POOL_SUB_SLOT_ITERS=case.sub_slot_iters)
farmer._private_keys = case.farmer_private_keys
farmer.authentication_keys = case.authentication_keys
farmer.sps[case.sp_hash] = [sp]
farmer.pool_state[p2_singleton_puzzle_hash] = {
"p2_singleton_puzzle_hash": p2_singleton_puzzle_hash.hex(),
"points_found_since_start": 0,
"points_found_24h": [],
"points_acknowledged_since_start": 0,
"points_acknowledged_24h": [],
"next_farmer_update": 0,
"next_pool_info_update": 0,
"current_points": 0,
"current_difficulty": case.pool_difficulty,
"pool_errors_24h": [],
"valid_partials_since_start": 0,
"valid_partials_24h": [],
"invalid_partials_since_start": 0,
"invalid_partials_24h": [],
"insufficient_partials_since_start": 0,
"insufficient_partials_24h": [],
"stale_partials_since_start": 0,
"stale_partials_24h": [],
"missing_partials_since_start": 0,
"missing_partials_24h": [],
"authentication_token_timeout": case.authentication_token_timeout,
"plot_count": 0,
"pool_config": case.pool_config,
}
return sp, pos, new_pos
def override_pool_state(overrides: dict[str, Any]) -> dict[str, Any]:
pool_state = {
"points_found_since_start": 0,
# Original item format here is (timestamp, value) but we'll ignore timestamp part
# so every `xxx_24h` item in this dict will be list[Any].
"points_found_24h": [],
"points_acknowledged_since_start": 0,
"points_acknowledged_24h": [],
"pool_errors_24h": [],
"valid_partials_since_start": 0,
"valid_partials_24h": [],
"invalid_partials_since_start": 0,
"invalid_partials_24h": [],
"insufficient_partials_since_start": 0,
"insufficient_partials_24h": [],
"stale_partials_since_start": 0,
"stale_partials_24h": [],
"missing_partials_since_start": 0,
"missing_partials_24h": [],
}
for key, value in overrides.items():
pool_state[key] = value
return pool_state
@dataclass
class PoolStateCase:
id: str
pool_response: DummyPoolResponse
expected_pool_state: dict[str, Any]
marks: Marks = ()
@datacases(
PoolStateCase(
"valid_response",
DummyPoolResponse(True, 200, new_difficulty=123),
override_pool_state(
{
"points_found_since_start": 1,
"points_found_24h": [1],
"points_acknowledged_since_start": 123,
"points_acknowledged_24h": [123],
"valid_partials_since_start": 1,
"valid_partials_24h": [1],
}
),
),
PoolStateCase(
"response_not_ok",
DummyPoolResponse(False, 500),
override_pool_state(
{
"points_found_since_start": 1,
"points_found_24h": [1],
"invalid_partials_since_start": 1,
"invalid_partials_24h": [1],
}
),
),
PoolStateCase(
"stale_partial",
DummyPoolResponse(True, 200, error_code=uint16(PoolErrorCode.TOO_LATE.value)),
override_pool_state(
{
"points_found_since_start": 1,
"points_found_24h": [1],
"pool_errors_24h": [{"error_code": uint16(PoolErrorCode.TOO_LATE.value)}],
"stale_partials_since_start": 1,
"stale_partials_24h": [1],
}
),
),
PoolStateCase(
"insufficient_partial",
DummyPoolResponse(True, 200, error_code=uint16(PoolErrorCode.PROOF_NOT_GOOD_ENOUGH.value)),
override_pool_state(
{
"points_found_since_start": 1,
"points_found_24h": [1],
"pool_errors_24h": [{"error_code": uint16(PoolErrorCode.PROOF_NOT_GOOD_ENOUGH.value)}],
"insufficient_partials_since_start": 1,
"insufficient_partials_24h": [1],
}
),
),
PoolStateCase(
"other_failed_partial",
DummyPoolResponse(True, 200, error_code=uint16(PoolErrorCode.SERVER_EXCEPTION.value)),
override_pool_state(
{
"points_found_since_start": 1,
"points_found_24h": [1],
"pool_errors_24h": [{"error_code": uint16(PoolErrorCode.SERVER_EXCEPTION.value)}],
"invalid_partials_since_start": 1,
"invalid_partials_24h": [1],
}
),
),
PoolStateCase(
"malformed_new_difficulty",
DummyPoolResponse(True, 200, raw_response={"new_difficulty": -1}),
override_pool_state(
{
"points_found_since_start": 1,
"points_found_24h": [1],
"current_difficulty": uint64(1),
"invalid_partials_since_start": 1,
"invalid_partials_24h": [1],
}
),
),
)
@pytest.mark.anyio
async def test_farmer_pool_response(
mocker: MockerFixture,
farmer_one_harvester: tuple[list[HarvesterService], FarmerService, BlockTools],
case: PoolStateCase,
) -> None:
_, farmer_service, _ = farmer_one_harvester
assert farmer_service.rpc_server is not None
farmer_api = farmer_service._api
sp, pos, new_pos = create_valid_pos(farmer_api.farmer)
assert pos.pool_contract_puzzle_hash is not None
p2_singleton_puzzle_hash: bytes32 = pos.pool_contract_puzzle_hash
assert (
verify_and_get_quality_string(
pos,
DEFAULT_CONSTANTS,
sp.challenge_hash,
sp.challenge_chain_sp,
height=uint32(1),
prev_transaction_block_height=uint32(1),
)
is not None
)
pool_response = case.pool_response
expected_pool_state = case.expected_pool_state
if "current_difficulty" in expected_pool_state:
farmer_api.farmer.pool_state[p2_singleton_puzzle_hash]["current_difficulty"] = expected_pool_state[
"current_difficulty"
]
mock_http_post = mocker.patch("aiohttp.ClientSession.post", return_value=pool_response)
peer = cast(WSChiaConnection, DummyHarvesterPeer(False))
await farmer_api.new_proof_of_space(new_pos, peer)
mock_http_post.assert_called_once()
def assert_stats_since_start(name: str) -> None:
assert farmer_api.farmer.pool_state[p2_singleton_puzzle_hash][name] == expected_pool_state[name]
def assert_stats_24h(name: str) -> None:
assert len(farmer_api.farmer.pool_state[p2_singleton_puzzle_hash][name]) == len(expected_pool_state[name])
for i, stat in enumerate(farmer_api.farmer.pool_state[p2_singleton_puzzle_hash][name]):
assert stat[1] == expected_pool_state[name][i]
def assert_pool_errors_24h() -> None:
assert len(farmer_api.farmer.pool_state[p2_singleton_puzzle_hash]["pool_errors_24h"]) == len(
expected_pool_state["pool_errors_24h"]
)
for i, stat in enumerate(farmer_api.farmer.pool_state[p2_singleton_puzzle_hash]["pool_errors_24h"]):
assert stat[1]["error_code"] == expected_pool_state["pool_errors_24h"][i]["error_code"]
assert_stats_since_start("points_found_since_start")
assert_stats_24h("points_found_24h")
assert_stats_since_start("points_acknowledged_since_start")
assert_stats_24h("points_acknowledged_24h")
assert_pool_errors_24h()
assert_stats_since_start("valid_partials_since_start")
assert_stats_24h("valid_partials_24h")
assert_stats_since_start("invalid_partials_since_start")
assert_stats_24h("invalid_partials_24h")
assert_stats_since_start("insufficient_partials_since_start")
assert_stats_24h("insufficient_partials_24h")
assert_stats_since_start("stale_partials_since_start")
assert_stats_24h("stale_partials_24h")
assert_stats_since_start("missing_partials_since_start")
assert_stats_24h("missing_partials_24h")
if "current_difficulty" in expected_pool_state:
assert (
farmer_api.farmer.pool_state[p2_singleton_puzzle_hash]["current_difficulty"]
== expected_pool_state["current_difficulty"]
)
def make_pool_info() -> dict[str, Any]:
return {
"name": "Pool Name",
"description": "Pool Description",
"logo_url": "https://subdomain.pool-domain.tld/path/to/logo.svg",
"target_puzzle_hash": "344587cf06a39db471d2cc027504e8688a0a67cce961253500c956c73603fd58",
"fee": "0.01",
"protocol_version": 1,
"relative_lock_height": 100,
"minimum_difficulty": 1,
"authentication_token_timeout": 5,
}
def make_pool_state(p2_singleton_puzzle_hash: bytes32, overrides: dict[str, Any]) -> dict[str, Any]:
pool_info = {
"p2_singleton_puzzle_hash": p2_singleton_puzzle_hash.hex(),
"points_found_since_start": 0,
"points_found_24h": [],
"points_acknowledged_since_start": 0,
"points_acknowledged_24h": [],
"next_farmer_update": 0,
"next_pool_info_update": 0,
"current_points": 0,
"current_difficulty": None,
"pool_errors_24h": [],
"valid_partials_since_start": 0,
"valid_partials_24h": [],
"invalid_partials_since_start": 0,
"invalid_partials_24h": [],
"insufficient_partials_since_start": 0,
"insufficient_partials_24h": [],
"stale_partials_since_start": 0,
"stale_partials_24h": [],
"missing_partials_since_start": 0,
"missing_partials_24h": [],
"authentication_token_timeout": None,
"plot_count": 0,
}
for key, value in overrides.items():
pool_info[key] = value
return pool_info
@dataclass
class DummyClientResponse:
status: int
@dataclass
class DummyPoolInfoResponse:
ok: bool
status: int
url: URL
pool_info: dict[str, Any] | None = None
history: tuple[DummyClientResponse, ...] = ()
async def json(self, **kwargs: object) -> dict[str, Any]:
if self.pool_info is None:
return {} # pragma: no cover
return self.pool_info
async def __aenter__(self) -> Self:
return self
async def __aexit__(
self,
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
pass
@dataclass
class PoolInfoCase(DataCase):
_id: str
initial_pool_url_in_config: str
pool_response: DummyPoolInfoResponse
expected_pool_url_in_config: str
initial_current_difficulty: uint64 | None = None
expected_current_difficulty: uint64 | None = None
marks: Marks = ()
@property
def id(self) -> str:
return self._id
@datacases(
PoolInfoCase(
"valid_response_without_redirect",
initial_pool_url_in_config="https://endpoint-1.pool-domain.tld/some-path",
pool_response=DummyPoolInfoResponse(
ok=True,
status=200,
url=URL("https://endpoint-1.pool-domain.tld/some-path"),
pool_info=make_pool_info(),
),
expected_pool_url_in_config="https://endpoint-1.pool-domain.tld/some-path",
),
PoolInfoCase(
"valid_response_with_301_redirect",
initial_pool_url_in_config="https://endpoint-1.pool-domain.tld/some-path",
pool_response=DummyPoolInfoResponse(
ok=True,
status=200,
url=URL("https://endpoint-1337.pool-domain.tld/some-other-path"),
pool_info=make_pool_info(),
history=tuple([DummyClientResponse(status=301)]),
),
expected_pool_url_in_config="https://endpoint-1337.pool-domain.tld/some-other-path",
),
PoolInfoCase(
"valid_response_with_302_redirect",
initial_pool_url_in_config="https://endpoint-1.pool-domain.tld/some-path",
pool_response=DummyPoolInfoResponse(
ok=True,
status=200,
url=URL("https://endpoint-1337.pool-domain.tld/some-other-path"),
pool_info=make_pool_info(),
history=tuple([DummyClientResponse(status=302)]),
),
expected_pool_url_in_config="https://endpoint-1.pool-domain.tld/some-path",
),
PoolInfoCase(
"valid_response_with_307_redirect",
initial_pool_url_in_config="https://endpoint-1.pool-domain.tld/some-path",
pool_response=DummyPoolInfoResponse(
ok=True,
status=200,
url=URL("https://endpoint-1337.pool-domain.tld/some-other-path"),
pool_info=make_pool_info(),
history=tuple([DummyClientResponse(status=307)]),
),
expected_pool_url_in_config="https://endpoint-1.pool-domain.tld/some-path",
),
PoolInfoCase(
"valid_response_with_308_redirect",
initial_pool_url_in_config="https://endpoint-1.pool-domain.tld/some-path",
pool_response=DummyPoolInfoResponse(
ok=True,
status=200,
url=URL("https://endpoint-1337.pool-domain.tld/some-other-path"),
pool_info=make_pool_info(),
history=tuple([DummyClientResponse(status=308)]),
),
expected_pool_url_in_config="https://endpoint-1337.pool-domain.tld/some-other-path",
),
PoolInfoCase(
"valid_response_with_multiple_308_redirects",
initial_pool_url_in_config="https://endpoint-1.pool-domain.tld/some-path",
pool_response=DummyPoolInfoResponse(
ok=True,
status=200,
url=URL("https://endpoint-1337.pool-domain.tld/some-other-path"),
pool_info=make_pool_info(),
history=tuple([DummyClientResponse(status=308), DummyClientResponse(status=308)]),
),
expected_pool_url_in_config="https://endpoint-1337.pool-domain.tld/some-other-path",
),
PoolInfoCase(
"valid_response_with_multiple_307_and_308_redirects",
initial_pool_url_in_config="https://endpoint-1.pool-domain.tld/some-path",
pool_response=DummyPoolInfoResponse(
ok=True,
status=200,
url=URL("https://endpoint-1337.pool-domain.tld/some-other-path"),
pool_info=make_pool_info(),
history=tuple([DummyClientResponse(status=307), DummyClientResponse(status=308)]),
),
expected_pool_url_in_config="https://endpoint-1.pool-domain.tld/some-path",
),
PoolInfoCase(
"failed_request_without_redirect",
initial_pool_url_in_config="https://endpoint-1.pool-domain.tld/some-path",
pool_response=DummyPoolInfoResponse(
ok=False,
status=500,
url=URL("https://endpoint-1.pool-domain.tld/some-path"),
),
expected_pool_url_in_config="https://endpoint-1.pool-domain.tld/some-path",
),
PoolInfoCase(
"failed_request_with_301_redirect",
initial_pool_url_in_config="https://endpoint-1.pool-domain.tld/some-path",
pool_response=DummyPoolInfoResponse(
ok=False,
status=500,
url=URL("https://endpoint-1337.pool-domain.tld/some-other-path"),
history=tuple([DummyClientResponse(status=301)]),
),
expected_pool_url_in_config="https://endpoint-1.pool-domain.tld/some-path",
),
PoolInfoCase(
"failed_request_with_302_redirect",
initial_pool_url_in_config="https://endpoint-1.pool-domain.tld/some-path",
pool_response=DummyPoolInfoResponse(
ok=False,
status=500,
url=URL("https://endpoint-1337.pool-domain.tld/some-other-path"),
history=tuple([DummyClientResponse(status=302)]),
),
expected_pool_url_in_config="https://endpoint-1.pool-domain.tld/some-path",
),
PoolInfoCase(
"failed_request_with_307_redirect",
initial_pool_url_in_config="https://endpoint-1.pool-domain.tld/some-path",
pool_response=DummyPoolInfoResponse(
ok=False,
status=500,
url=URL("https://endpoint-1337.pool-domain.tld/some-other-path"),
history=tuple([DummyClientResponse(status=307)]),
),
expected_pool_url_in_config="https://endpoint-1.pool-domain.tld/some-path",
),
PoolInfoCase(
"failed_request_with_308_redirect",
initial_pool_url_in_config="https://endpoint-1.pool-domain.tld/some-path",
pool_response=DummyPoolInfoResponse(
ok=False,
status=500,
url=URL("https://endpoint-1337.pool-domain.tld/some-other-path"),
history=tuple([DummyClientResponse(status=308)]),
),
expected_pool_url_in_config="https://endpoint-1.pool-domain.tld/some-path",
),
PoolInfoCase(
"malformed_minimum_difficulty",
initial_pool_url_in_config="https://endpoint-1.pool-domain.tld/some-path",
pool_response=DummyPoolInfoResponse(
ok=True,
status=200,
url=URL("https://endpoint-1.pool-domain.tld/some-path"),
pool_info={**make_pool_info(), "minimum_difficulty": -1},
),
expected_pool_url_in_config="https://endpoint-1.pool-domain.tld/some-path",
initial_current_difficulty=uint64(42),
expected_current_difficulty=uint64(42),
),
PoolInfoCase(
"error_code_in_response",
initial_pool_url_in_config="https://endpoint-1.pool-domain.tld/some-path",
pool_response=DummyPoolInfoResponse(
ok=True,
status=200,
url=URL("https://endpoint-1.pool-domain.tld/some-path"),
pool_info={**make_pool_info(), "error_code": 1, "error_message": "pool unavailable"},
),
expected_pool_url_in_config="https://endpoint-1.pool-domain.tld/some-path",
initial_current_difficulty=uint64(42),
expected_current_difficulty=uint64(42),
),
)
@pytest.mark.parametrize("pool_protocol_version", [1, 2])
@pytest.mark.anyio
async def test_farmer_pool_info_config_update(
mocker: MockerFixture,
farmer_one_harvester: tuple[list[HarvesterService], FarmerService, BlockTools],
case: PoolInfoCase,
pool_protocol_version: int,
) -> None:
_, farmer_service, _ = farmer_one_harvester
p2_singleton_puzzle_hash = bytes32.fromhex("302e05a1e6af431c22043ae2a9a8f71148c955c372697cb8ab348160976283df")
auth_sk = calculate_synthetic_secret_key(
master_sk_to_wallet_sk_unhardened(farmer_service._node.all_root_sks[0], uint32(0)),
DEFAULT_HIDDEN_PUZZLE_HASH,
)
farmer_service._node.authentication_keys = {p2_singleton_puzzle_hash: auth_sk}
pool_state_overrides: dict[str, Any] = {
"next_farmer_update": time() + UPDATE_POOL_FARMER_INFO_INTERVAL,
}
if case.initial_current_difficulty is not None:
pool_state_overrides["current_difficulty"] = case.initial_current_difficulty
pool_state_overrides["authentication_token_timeout"] = 10
farmer_service._node.pool_state[p2_singleton_puzzle_hash] = make_pool_state(
p2_singleton_puzzle_hash,
overrides=pool_state_overrides,
)
PoolingShareState(
owner_public_key=auth_sk.get_g1(),
p2_singleton_puzzle_hash=p2_singleton_puzzle_hash,
payout_instructions="c2b08e41d766da4116e388357ed957d04ad754623a915f3fd65188a8746cf3e8",
pool_url=case.initial_pool_url_in_config,
launcher_id=bytes32.from_hexstr("ae4ef3b9bfe68949691281a015a9c16630fc8f66d48c19ca548fb80768791afa"),
target_puzzle_hash=bytes32.from_hexstr("344587cf06a39db471d2cc027504e8688a0a67cce961253500c956c73603fd58"),
key_derivation_index=0,
version=pool_protocol_version,
).add(root_path=farmer_service.root_path)
mock_http_get = mocker.patch("aiohttp.ClientSession.request", return_value=case.pool_response)
await farmer_service._node.update_pool_state()
mock_http_get.assert_called_once()
with PoolingShareState.acquire(
root_path=farmer_service.root_path, p2_singleton_puzzle_hash=p2_singleton_puzzle_hash
) as pool_config:
assert pool_config.pool_url == case.expected_pool_url_in_config
if case.expected_current_difficulty is not None:
assert (
farmer_service._node.pool_state[p2_singleton_puzzle_hash]["current_difficulty"]
== case.expected_current_difficulty
)
if not case.pool_response.ok:
return
login_link = await farmer_service._node.generate_login_link(pool_config.launcher_id)
assert login_link is not None
escaped_base = re.escape(case.expected_pool_url_in_config)
launcher_hex = re.escape(pool_config.launcher_id.hex())
expected = (
escaped_base
+ rf"/{'v2/' if pool_protocol_version == 2 else ''}login\?launcher_id={launcher_hex}"
+ r"&authentication_token=\d+&signature=[0-9a-f]+"
)
assert re.fullmatch(expected, login_link) is not None
@dataclass
class DummyGetFarmerResponse:
ok: bool
status: int
current_difficulty: int
current_points: int
async def json(self, **kwargs: object) -> dict[str, Any]:
return {
"current_difficulty": self.current_difficulty,
"current_points": self.current_points,
"authentication_public_key": bytes(G1Element()).hex(),
"payout_instructions": "",
}
async def __aenter__(self) -> Self:
return self
async def __aexit__(
self,
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
pass
@pytest.mark.parametrize("pool_protocol_version", [1, 2])
@pytest.mark.anyio
async def test_farmer_info_update(
mocker: MockerFixture,
farmer_one_harvester: tuple[list[HarvesterService], FarmerService, BlockTools],
pool_protocol_version: int,
) -> None:
_, farmer_service, _ = farmer_one_harvester
p2_singleton_puzzle_hash = bytes32.fromhex("302e05a1e6af431c22043ae2a9a8f71148c955c372697cb8ab348160976283df")
launcher_id = bytes32.from_hexstr("ae4ef3b9bfe68949691281a015a9c16630fc8f66d48c19ca548fb80768791afa")
auth_sk = calculate_synthetic_secret_key(
master_sk_to_wallet_sk_unhardened(farmer_service._node.all_root_sks[0], uint32(0)),
DEFAULT_HIDDEN_PUZZLE_HASH,
)
farmer_service._node.authentication_keys = {p2_singleton_puzzle_hash: auth_sk}
farmer_service._node.authentication_tokens = {launcher_id: ("", uint64(time() + 3600))}
pool_state_overrides: dict[str, Any] = {
"next_pool_info_update": uint64.MAXIMUM,
"authentication_token_timeout": uint64.MAXIMUM,
}
farmer_service._node.pool_state[p2_singleton_puzzle_hash] = make_pool_state(
p2_singleton_puzzle_hash,
overrides=pool_state_overrides,
)
PoolingShareState(
owner_public_key=auth_sk.get_g1(),
p2_singleton_puzzle_hash=p2_singleton_puzzle_hash,
payout_instructions="c2b08e41d766da4116e388357ed957d04ad754623a915f3fd65188a8746cf3e8",
pool_url="https://dapool.com",
launcher_id=launcher_id,
target_puzzle_hash=bytes32.from_hexstr("344587cf06a39db471d2cc027504e8688a0a67cce961253500c956c73603fd58"),
key_derivation_index=0,
version=pool_protocol_version,
).add(root_path=farmer_service.root_path)
EXPECTED_CURRENT_DIFFICULTY = 1234
EXPECTED_CURRENT_POINTS = 5678
mocker.patch(
"aiohttp.ClientSession.request",
return_value=DummyGetFarmerResponse(
ok=True,
status=200,
current_difficulty=EXPECTED_CURRENT_DIFFICULTY,
current_points=EXPECTED_CURRENT_POINTS,
),
)
await farmer_service._node.update_pool_state()
assert (
farmer_service._node.pool_state[p2_singleton_puzzle_hash]["current_difficulty"] == EXPECTED_CURRENT_DIFFICULTY
)
assert farmer_service._node.pool_state[p2_singleton_puzzle_hash]["current_points"] == EXPECTED_CURRENT_POINTS
mocker.patch(
"aiohttp.ClientSession.request",
return_value=DummyGetFarmerResponse(
ok=False,
status=404,
current_difficulty=EXPECTED_CURRENT_DIFFICULTY,
current_points=EXPECTED_CURRENT_POINTS,
),
)
farmer_service._node.pool_state[p2_singleton_puzzle_hash] = make_pool_state(
p2_singleton_puzzle_hash,
overrides={
"next_farmer_update": 0,
"current_difficulty": EXPECTED_CURRENT_DIFFICULTY,
"current_points": EXPECTED_CURRENT_POINTS,
},
)
await farmer_service._node.update_pool_state()
assert (
farmer_service._node.pool_state[p2_singleton_puzzle_hash]["current_difficulty"] == EXPECTED_CURRENT_DIFFICULTY
)
assert farmer_service._node.pool_state[p2_singleton_puzzle_hash]["current_points"] == EXPECTED_CURRENT_POINTS
@pytest.mark.anyio
async def test_new_signage_point_skips_pool_with_invalid_difficulty(
mocker: MockerFixture,
farmer_one_harvester: FarmerOneHarvester,
) -> None:
_, farmer_service, _ = farmer_one_harvester
farmer_api = farmer_service._api
send_mock = mocker.patch.object(farmer_service._server, "send_to_all_if", new_callable=AsyncMock)
sp, _, _ = create_valid_pos(farmer_api.farmer)
farmer_api.farmer.sps = {}
valid_p2 = bytes32.fromhex("302e05a1e6af431c22043ae2a9a8f71148c955c372697cb8ab348160976283df")
invalid_p2 = bytes32(std_hash(b"invalid_pool_difficulty"))
invalid_pool_config = PoolingShareState(
launcher_id=bytes32.fromhex("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"),
pool_url="https://bad-pool.example",
payout_instructions="c2b08e41d766da4116e388357ed957d04ad754623a915f3fd65188a8746cf3e8",
target_puzzle_hash=bytes32.fromhex("344587cf06a39db471d2cc027504e8688a0a67cce961253500c956c73603fd58"),
p2_singleton_puzzle_hash=invalid_p2,
owner_public_key=G1Element.from_bytes(
bytes.fromhex(
"8348455278ecec68325b6754b1f3218cde1511ca9393197e7876d7ae04af1c4dd86b0c50601cf5daeb034e8f7c226537"
)
),
key_derivation_index=-1,
)
farmer_api.farmer.pool_state[invalid_p2] = make_pool_state(
invalid_p2,
overrides={
"current_difficulty": -1,
"pool_config": invalid_pool_config,
"authentication_token_timeout": uint8(5),
},
)
self_pool_p2 = bytes32(std_hash(b"self_pool"))
self_pool_config = PoolingShareState(
launcher_id=bytes32.fromhex("1122334455667788990011223344556677889900112233445566778899001122"),
pool_url="",
payout_instructions="c2b08e41d766da4116e388357ed957d04ad754623a915f3fd65188a8746cf3e8",
target_puzzle_hash=bytes32.fromhex("344587cf06a39db471d2cc027504e8688a0a67cce961253500c956c73603fd58"),
p2_singleton_puzzle_hash=self_pool_p2,
owner_public_key=G1Element.from_bytes(
bytes.fromhex(
"8348455278ecec68325b6754b1f3218cde1511ca9393197e7876d7ae04af1c4dd86b0c50601cf5daeb034e8f7c226537"
)
),
key_derivation_index=-1,
)
farmer_api.farmer.pool_state[self_pool_p2] = make_pool_state(
self_pool_p2,
overrides={
"pool_config": self_pool_config,
},
)
await farmer_api.new_signage_point(sp)
assert send_mock.await_count == 2
nsp = harvester_protocol.NewSignagePointHarvester.from_bytes(send_mock.await_args_list[0].args[0][0].data)
assert len(nsp.pool_difficulties) == 1
assert nsp.pool_difficulties[0].pool_contract_puzzle_hash == valid_p2
assert nsp.pool_difficulties[0].difficulty == uint64(1)
@dataclass
class PartialSubmitHeaderCase(DataCase):
_id: str
harvester_peer: DummyHarvesterPeer
expected_headers: dict[str, str]
marks: Marks = ()
@property
def id(self) -> str:
return self._id
@datacases(
PartialSubmitHeaderCase(
"additional version headers",
harvester_peer=DummyHarvesterPeer(
version="1.2.3.asdf42",
),
expected_headers={
"User-Agent": f"Chia Blockchain v.{__version__}",
"chia-farmer-version": __version__,
"chia-harvester-version": "1.2.3.asdf42",
},
),
)
@pytest.mark.anyio
async def test_farmer_additional_headers_on_partial_submit(
mocker: MockerFixture,
farmer_one_harvester: tuple[list[HarvesterService], FarmerService, BlockTools],
case: PartialSubmitHeaderCase,
) -> None:
_, farmer_service, _ = farmer_one_harvester
assert farmer_service.rpc_server is not None
farmer_api = farmer_service._api
sp, pos, new_pos = create_valid_pos(farmer_api.farmer)
assert pos.pool_contract_puzzle_hash is not None
assert (
verify_and_get_quality_string(
pos,
DEFAULT_CONSTANTS,
sp.challenge_hash,
sp.challenge_chain_sp,
height=uint32(1),
prev_transaction_block_height=uint32(1),
)
is not None
)
mock_http_post = mocker.patch(
"aiohttp.ClientSession.post",
return_value=DummyPoolResponse(True, 200, new_difficulty=123),
)
peer = cast(WSChiaConnection, case.harvester_peer)
await farmer_api.new_proof_of_space(new_pos, peer)
mock_http_post.assert_called_once_with(ANY, json=ANY, ssl=ANY, headers=case.expected_headers)
@pytest.mark.parametrize("pool_protocol_version", [1, 2])
@pytest.mark.anyio
async def test_farmer_to_pool_protocol(
mocker: MockerFixture,
farmer_one_harvester: tuple[list[HarvesterService], FarmerService, BlockTools],
pool_protocol_version: int,
caplog: pytest.LogCaptureFixture,
) -> None:
_, farmer_service, _ = farmer_one_harvester
p2_singleton_puzzle_hash = bytes32.fromhex("302e05a1e6af431c22043ae2a9a8f71148c955c372697cb8ab348160976283df")
auth_sk = calculate_synthetic_secret_key(
master_sk_to_wallet_sk_unhardened(farmer_service._node.all_root_sks[0], uint32(0)), DEFAULT_HIDDEN_PUZZLE_HASH
)
farmer_service._node.authentication_keys = {p2_singleton_puzzle_hash: auth_sk}
plotnft_id = bytes32.from_hexstr("ae4ef3b9bfe68949691281a015a9c16630fc8f66d48c19ca548fb80768791afa")
PoolingShareState(
owner_public_key=master_sk_to_singleton_owner_sk(farmer_service._node.all_root_sks[0], uint32(0)).get_g1()
if pool_protocol_version == 1
else auth_sk.get_g1(),
p2_singleton_puzzle_hash=p2_singleton_puzzle_hash,
payout_instructions="c2b08e41d766da4116e388357ed957d04ad754623a915f3fd65188a8746cf3e8",
pool_url="http://doesntmatter.com",
launcher_id=plotnft_id,
target_puzzle_hash=bytes32.from_hexstr("344587cf06a39db471d2cc027504e8688a0a67cce961253500c956c73603fd58"),
key_derivation_index=0,
version=pool_protocol_version,
).add(root_path=farmer_service.root_path)
@dataclass(frozen=True)
class DummyPostFarmerResponse:
ok: bool
async def json(self, **kwargs: object) -> dict[str, Any]:
return PostFarmerResponse(welcome_message="welcome to the pool").to_json_dict()
@dataclass(frozen=True)
class DummyPutFarmerResponse:
ok: bool
async def json(self, **kwargs: object) -> dict[str, Any]:
return PutFarmerResponse(
authentication_public_key=False, suggested_difficulty=False, payout_instructions=True
).to_json_dict()
@dataclass(frozen=True)
class DummyAuthResponse:
ok: bool
async def json(self, **kwargs: object) -> dict[str, Any]:
return GetAuthResponse(
authentication_token="secret", expiration=uint64(farmer_service._node.get_current_time() + 600)
).to_json_dict()
@dataclass(frozen=True)
class DummyGetFarmerResponse:
ok: bool
async def json(self, **kwargs: object) -> dict[str, Any]:
return GetFarmerResponse(
authentication_public_key=auth_sk.get_g1(),
payout_instructions="",
current_difficulty=uint64(0),
current_points=uint64(0),
).to_json_dict()
@dataclass(frozen=True)
class DummyErrorResponse:
ok: bool
status: int | None = None
async def json(self, **kwargs: object) -> dict[str, Any]:
return pool_protocol.ErrorResponse(
error_code=uint16(pool_protocol.PoolErrorCode.SERVER_EXCEPTION.value), error_message=None
).to_json_dict()
@asynccontextmanager
async def client_session_request(method: str, url: str, **kwargs: Any) -> AsyncIterator[Any]:
path = str(URL(url).path).rstrip("/")
if path.endswith("/auth"):
assert method == "GET"
yield DummyAuthResponse(ok=True) # ruff: ignore[fallible-context-manager]
if path.endswith("/farmer"):
if method == "POST":
yield DummyPostFarmerResponse(ok=True) # ruff: ignore[fallible-context-manager]
if method == "PUT":
yield DummyPutFarmerResponse(ok=True) # ruff: ignore[fallible-context-manager]
if method == "GET":
yield DummyGetFarmerResponse(ok=True)
@asynccontextmanager
async def client_session_error(*args: Any, **kwargs: Any) -> AsyncIterator[Any]:
yield DummyErrorResponse(ok=True)
@asynccontextmanager
async def client_session_error_not_ok(*args: Any, **kwargs: Any) -> AsyncIterator[Any]:
yield DummyErrorResponse(ok=False, status=404)
@asynccontextmanager
async def client_session_exception(*args: Any, **kwargs: Any) -> AsyncIterator[Any]:
raise Exception("foo bar")
yield # pragma: no cover
mocker.patch("aiohttp.ClientSession.request", side_effect=client_session_request)
with PoolingShareState.acquire(
root_path=farmer_service.root_path,
p2_singleton_puzzle_hash=p2_singleton_puzzle_hash,
) as pool_config:
pass
assert await farmer_service._node._pool_post_farmer(pool_config, uint8(10)) == PostFarmerResponse(
welcome_message="welcome to the pool"
)
assert await farmer_service._node._pool_put_farmer(pool_config, uint8(10)) == PutFarmerResponse(
authentication_public_key=False, suggested_difficulty=False, payout_instructions=True
)
assert isinstance(await farmer_service._node._pool_get_farmer(pool_config, uint8(10)), GetFarmerResponse)
# Test some errors and especially with getting authentication
if pool_protocol_version == 2:
farmer_service._node.authentication_tokens = {}
mocker.patch("aiohttp.ClientSession.request", side_effect=client_session_error)
with caplog.at_level(logging.WARNING):
assert isinstance(
await farmer_service._node._get_current_authentication_token(pool_config, uint8(10)), ErrorResponse
)
assert "GET /auth response: " in caplog.text
mocker.patch("aiohttp.ClientSession.request", side_effect=client_session_error_not_ok)
with caplog.at_level(logging.ERROR):
assert await farmer_service._node._get_current_authentication_token(pool_config, uint8(10)) is None
assert "Error in GET /auth http://doesntmatter.com, 404" in caplog.text
mocker.patch("aiohttp.ClientSession.request", side_effect=client_session_exception)
with caplog.at_level(logging.ERROR):
assert await farmer_service._node._get_current_authentication_token(pool_config, uint8(10)) is None
assert "Exception in GET /auth http://doesntmatter.com, foo bar" in caplog.text
pool_config.version = 1337
with pytest.raises(ValueError, match=r"Unknown pool protocol version specified in pooling config"):
await farmer_service._node._get_current_authentication_token(pool_config, uint8(10))