mirror of
https://github.com/Chia-Network/chia-blockchain.git
synced 2026-08-24 02:24:23 -05:00
67cdfcda41
* 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>
1679 lines
66 KiB
Python
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))
|