mirror of
https://github.com/Chia-Network/chia-blockchain.git
synced 2026-08-24 10:05:29 -05:00
* Use a separate config for pooling information * New PlotNFT drivers * PlotNFT2 Wallet * PlotNFT V2 RPCs and CLI * Integrate v2 pooling protocol into farmer * Comment by @cursor * Fix test * Comments by @cursor * whoops * Comments by @cursor * Comments by @cursor * Comments by @cursor * Some tidying * fix test for memo adjustment * tweak additional memos again * empirical testing with v1 pools * Add expiration to `GetAuthRequest` * Remove pyjwt dep * Revert `get_login_link` to old behavior * Add an extra derivation to auth key for v2 * unhardened * Fix login link test * Fix login link test again * Fix network protocol data * Comments by cursor * Fix test * Use correct pool url after redirect * Fix test for coverage * Upstream wallet fixes * Add `REMARK` option to `launch` * pre-commit * chmod * test coverage * Add wallet name * test coverage * moar test coverage * commentsby @cursor * moar test coverage * fix custody architecture namespace * Comments by @matt-o-how * Fix /GET farmer to omit the signature rather than None * Fix protocol test * fix test * comments by @cursor * diff minimizatino * event dispatch * Fix test deadlock * Comments by @cursor * test coverage * Comments by @cursor * Comments by @cursor
1025 lines
48 KiB
Python
1025 lines
48 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import contextlib
|
|
import logging
|
|
import sys
|
|
import time
|
|
import traceback
|
|
from collections.abc import AsyncIterator
|
|
from dataclasses import dataclass
|
|
from datetime import datetime, timezone
|
|
from math import floor
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING, Any, ClassVar, Literal, TypeVar, cast
|
|
|
|
import aiohttp
|
|
from chia_rs import AugSchemeMPL, ConsensusConstants, G1Element, G2Element, PrivateKey, ProofOfSpace
|
|
from chia_rs.sized_bytes import bytes32
|
|
from chia_rs.sized_ints import uint8, uint16, uint32, uint64
|
|
|
|
from chia.daemon.keychain_proxy import KeychainProxy, connect_to_keychain_and_validate, wrap_local_keychain
|
|
from chia.plot_sync.delta import Delta
|
|
from chia.plot_sync.receiver import Receiver
|
|
from chia.pools.pool_config import PoolingShareState, perform_migration_from_old_config
|
|
from chia.protocols import farmer_protocol, harvester_protocol, pool_protocol
|
|
from chia.protocols.outbound_message import NodeType, make_msg
|
|
from chia.protocols.protocol_message_types import ProtocolMessageTypes
|
|
from chia.rpc.rpc_server import StateChangedProtocol, default_get_connections
|
|
from chia.server.server import ChiaServer, ssl_context_for_root
|
|
from chia.server.ws_connection import WSChiaConnection
|
|
from chia.ssl.create_ssl import get_mozilla_ca_crt
|
|
from chia.util.bech32m import decode_puzzle_hash, encode_puzzle_hash
|
|
from chia.util.config import load_config, lock_and_load_config, save_config
|
|
from chia.util.errors import KeychainProxyConnectionFailure
|
|
from chia.util.hash import std_hash
|
|
from chia.util.keychain import Keychain
|
|
from chia.util.logging import TimedDuplicateFilter
|
|
from chia.util.profiler import profile_task
|
|
from chia.util.streamable import Streamable
|
|
from chia.util.task_referencer import create_referenced_task
|
|
from chia.wallet.derive_keys import (
|
|
find_authentication_sk,
|
|
find_owner_sk,
|
|
master_sk_to_farmer_sk,
|
|
master_sk_to_pool_sk,
|
|
master_sk_to_wallet_sk_unhardened,
|
|
match_address_to_sk,
|
|
singleton_owner_sk_to_authv2_key,
|
|
)
|
|
from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import (
|
|
DEFAULT_HIDDEN_PUZZLE_HASH,
|
|
calculate_synthetic_secret_key,
|
|
)
|
|
from chia.wallet.puzzles.singleton_top_layer import SINGLETON_MOD
|
|
|
|
singleton_mod_hash = SINGLETON_MOD.get_tree_hash()
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
UPDATE_POOL_INFO_INTERVAL: int = 3600
|
|
UPDATE_POOL_INFO_FAILURE_RETRY_INTERVAL: int = 120
|
|
UPDATE_POOL_FARMER_INFO_INTERVAL: int = 300
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class GetPoolInfoResult:
|
|
pool_info: pool_protocol.GetPoolInfoResponse
|
|
new_pool_url: str | None
|
|
|
|
|
|
def strip_old_entries(pairs: list[tuple[float, Any]], before: float) -> list[tuple[float, Any]]:
|
|
for index, [timestamp, points] in enumerate(pairs):
|
|
if timestamp >= before:
|
|
if index == 0:
|
|
return pairs
|
|
if index > 0:
|
|
return pairs[index:]
|
|
return []
|
|
|
|
|
|
def increment_pool_stats(
|
|
pool_states: dict[bytes32, Any],
|
|
p2_singleton_puzzlehash: bytes32,
|
|
name: str,
|
|
current_time: float,
|
|
count: int = 1,
|
|
value: int | dict[str, Any] | None = None,
|
|
) -> None:
|
|
if p2_singleton_puzzlehash not in pool_states:
|
|
return
|
|
pool_state = pool_states[p2_singleton_puzzlehash]
|
|
if f"{name}_since_start" in pool_state:
|
|
pool_state[f"{name}_since_start"] += count
|
|
if f"{name}_24h" in pool_state:
|
|
if value is None:
|
|
pool_state[f"{name}_24h"].append((uint32(current_time), pool_state["current_difficulty"]))
|
|
else:
|
|
pool_state[f"{name}_24h"].append((uint32(current_time), value))
|
|
|
|
# Age out old 24h information for every signage point regardless
|
|
# of any failures. Note that this still lets old data remain if
|
|
# the client isn't receiving signage points.
|
|
cutoff_24h = current_time - (24 * 60 * 60)
|
|
pool_state[f"{name}_24h"] = strip_old_entries(pairs=pool_state[f"{name}_24h"], before=cutoff_24h)
|
|
return
|
|
|
|
|
|
_T_Response = TypeVar("_T_Response", bound=Streamable)
|
|
|
|
|
|
async def make_pool_protocol_request(
|
|
*,
|
|
self: Farmer,
|
|
pool_config: PoolingShareState,
|
|
method: Literal["GET", "POST", "PUT"],
|
|
endpoint_name: str,
|
|
request: Streamable | None,
|
|
response_type: type[_T_Response],
|
|
) -> tuple[_T_Response | pool_protocol.ErrorResponse | None, aiohttp.ClientResponse | None]:
|
|
self.log.debug("%s /%s request %s", method, endpoint_name, request)
|
|
try:
|
|
async with aiohttp.ClientSession(trust_env=True) as session:
|
|
async with session.request(
|
|
method,
|
|
self._url_for_endpoint(pool_config, endpoint_name),
|
|
ssl=ssl_context_for_root(get_mozilla_ca_crt(), log=self.log),
|
|
**(
|
|
# the POST and PUT requests always are not None
|
|
{"json": request.to_json_dict()} # type: ignore[union-attr]
|
|
if method in {"POST", "PUT"}
|
|
else {"params": request.to_json_dict() if request else None}
|
|
),
|
|
) as resp:
|
|
if resp.ok:
|
|
json_response = await resp.json(content_type=None)
|
|
log_level = logging.INFO
|
|
if "error_code" in json_response:
|
|
log_level = logging.WARNING
|
|
increment_pool_stats(
|
|
self.pool_state,
|
|
pool_config.p2_singleton_puzzle_hash,
|
|
"pool_errors",
|
|
time.time(),
|
|
value=json_response,
|
|
)
|
|
self.log.log(
|
|
log_level,
|
|
f"{method} /{endpoint_name} response: {json_response}",
|
|
)
|
|
if "error_code" in json_response:
|
|
error_response = pool_protocol.ErrorResponse.from_json_dict(json_response)
|
|
if (
|
|
error_response.error_code == pool_protocol.PoolErrorCode.INVALID_AUTHENTICATION_TOKEN.value
|
|
and pool_config.launcher_id in self.authentication_tokens
|
|
):
|
|
self.authentication_tokens.pop(pool_config.launcher_id)
|
|
return (error_response, resp)
|
|
else:
|
|
return (response_type.from_json_dict(json_response), resp)
|
|
else:
|
|
self.handle_failed_pool_response(
|
|
pool_config.p2_singleton_puzzle_hash,
|
|
f"Error in {method} /{endpoint_name} {pool_config.pool_url}, {resp.status}",
|
|
)
|
|
return None, resp
|
|
except Exception as e:
|
|
self.handle_failed_pool_response(
|
|
pool_config.p2_singleton_puzzle_hash,
|
|
f"Exception in {method} /{endpoint_name} {pool_config.pool_url}, {e}",
|
|
)
|
|
return None, None
|
|
|
|
|
|
"""
|
|
HARVESTER PROTOCOL (FARMER <-> HARVESTER)
|
|
"""
|
|
|
|
|
|
class Farmer:
|
|
if TYPE_CHECKING:
|
|
from chia.rpc.rpc_server import RpcServiceProtocol
|
|
|
|
_protocol_check: ClassVar[RpcServiceProtocol] = cast("Farmer", None)
|
|
|
|
def __init__(
|
|
self,
|
|
root_path: Path,
|
|
farmer_config: dict[str, Any],
|
|
pool_config: dict[str, Any],
|
|
consensus_constants: ConsensusConstants,
|
|
local_keychain: Keychain | None = None,
|
|
):
|
|
self.keychain_proxy: KeychainProxy | None = None
|
|
self.local_keychain = local_keychain
|
|
self._root_path = root_path
|
|
self.config = farmer_config
|
|
self.pool_config = pool_config
|
|
# Keep track of all sps, keyed on challenge chain signage point hash
|
|
self.sps: dict[bytes32, list[farmer_protocol.NewSignagePoint]] = {}
|
|
|
|
# Keep track of harvester plot identifier (str), target sp index, and PoSpace for each challenge
|
|
self.proofs_of_space: dict[bytes32, list[tuple[str, ProofOfSpace]]] = {}
|
|
|
|
# Quality string to plot identifier and challenge_hash, for use with harvester.RequestSignatures
|
|
self.quality_str_to_identifiers: dict[bytes32, tuple[str, bytes32, bytes32, bytes32]] = {}
|
|
|
|
# Track pending solver requests, keyed by partial proof
|
|
self.pending_solver_requests: dict[bytes, dict[str, Any]] = {}
|
|
|
|
# number of responses to each signage point
|
|
self.number_of_responses: dict[bytes32, int] = {}
|
|
|
|
# A dictionary of keys to time added. These keys refer to keys in the above 4 dictionaries. This is used
|
|
# to periodically clear the memory
|
|
self.cache_add_time: dict[bytes32, uint64] = {}
|
|
|
|
self.plot_sync_receivers: dict[bytes32, Receiver] = {}
|
|
|
|
self.cache_clear_task: asyncio.Task[None] | None = None
|
|
self.update_pool_state_task: asyncio.Task[None] | None = None
|
|
self.constants = consensus_constants
|
|
self._shut_down = False
|
|
self.server: Any = None
|
|
self.state_changed_callback: StateChangedProtocol | None = None
|
|
self.log = log
|
|
self.log.addFilter(TimedDuplicateFilter("No pool specific authentication_token_timeout.*", 60 * 10))
|
|
self.log.addFilter(TimedDuplicateFilter("No pool specific difficulty has been set.*", 60 * 10))
|
|
|
|
self.started = False
|
|
self.harvester_handshake_task: asyncio.Task[None] | None = None
|
|
|
|
# From p2_singleton_puzzle_hash to pool state dict
|
|
self.pool_state: dict[bytes32, dict[str, Any]] = {}
|
|
|
|
# From p2_singleton to auth PrivateKey
|
|
self.authentication_keys: dict[bytes32, PrivateKey] = {}
|
|
self.authentication_tokens: dict[bytes32, tuple[str, uint64] | None] = {}
|
|
|
|
# Last time we updated pool_state based on the config file
|
|
self.last_config_access_time: float = 0
|
|
|
|
self.all_root_sks: list[PrivateKey] = []
|
|
|
|
# Use to find missing signage points. (new_signage_point, time)
|
|
self.prev_signage_point: tuple[uint64, farmer_protocol.NewSignagePoint] | None = None
|
|
perform_migration_from_old_config(self._root_path)
|
|
|
|
@contextlib.asynccontextmanager
|
|
async def manage(self) -> AsyncIterator[None]:
|
|
async def start_task() -> None:
|
|
# `Farmer.setup_keys` returns `False` if there are no keys setup yet. In this case we just try until it
|
|
# succeeds or until we need to shut down.
|
|
while not self._shut_down:
|
|
if await self.setup_keys():
|
|
self.update_pool_state_task = create_referenced_task(self._periodically_update_pool_state_task())
|
|
self.cache_clear_task = create_referenced_task(self._periodically_clear_cache_and_refresh_task())
|
|
log.debug("start_task: initialized")
|
|
self.started = True
|
|
return
|
|
await asyncio.sleep(1)
|
|
|
|
if self.config.get("enable_profiler", False):
|
|
if sys.getprofile() is not None:
|
|
self.log.warning("not enabling profiler, getprofile() is already set")
|
|
else:
|
|
create_referenced_task(profile_task(self._root_path, "farmer", self.log), known_unreferenced=True)
|
|
|
|
create_referenced_task(start_task(), known_unreferenced=True)
|
|
try:
|
|
yield
|
|
finally:
|
|
self._shut_down = True
|
|
|
|
if self.cache_clear_task is not None:
|
|
await self.cache_clear_task
|
|
if self.update_pool_state_task is not None:
|
|
await self.update_pool_state_task
|
|
if self.keychain_proxy is not None:
|
|
proxy = self.keychain_proxy
|
|
self.keychain_proxy = None
|
|
await proxy.close()
|
|
await asyncio.sleep(0.5) # https://docs.aiohttp.org/en/stable/client_advanced.html#graceful-shutdown
|
|
self.started = False
|
|
|
|
def get_connections(self, request_node_type: NodeType | None) -> list[dict[str, Any]]:
|
|
return default_get_connections(server=self.server, request_node_type=request_node_type)
|
|
|
|
async def ensure_keychain_proxy(self) -> KeychainProxy:
|
|
if self.keychain_proxy is None:
|
|
if self.local_keychain:
|
|
self.keychain_proxy = wrap_local_keychain(self.local_keychain, log=self.log)
|
|
else:
|
|
self.keychain_proxy = await connect_to_keychain_and_validate(self._root_path, self.log)
|
|
if not self.keychain_proxy:
|
|
raise KeychainProxyConnectionFailure
|
|
return self.keychain_proxy
|
|
|
|
async def get_all_private_keys(self) -> list[tuple[PrivateKey, bytes]]:
|
|
keychain_proxy = await self.ensure_keychain_proxy()
|
|
return await keychain_proxy.get_all_private_keys()
|
|
|
|
async def setup_keys(self) -> bool:
|
|
no_keys_error_str = "No keys exist. Please run 'chia keys generate' or open the UI."
|
|
try:
|
|
self.all_root_sks = [sk for sk, _ in await self.get_all_private_keys()]
|
|
except KeychainProxyConnectionFailure:
|
|
return False
|
|
|
|
self._private_keys = [master_sk_to_farmer_sk(sk) for sk in self.all_root_sks] + [
|
|
master_sk_to_pool_sk(sk) for sk in self.all_root_sks
|
|
]
|
|
|
|
if len(self.get_public_keys()) == 0:
|
|
log.warning(no_keys_error_str)
|
|
return False
|
|
|
|
config = load_config(self._root_path, "config.yaml")
|
|
if "xch_target_address" not in self.config:
|
|
self.config = config["farmer"]
|
|
if "xch_target_address" not in self.pool_config:
|
|
self.pool_config = config["pool"]
|
|
if "xch_target_address" not in self.config or "xch_target_address" not in self.pool_config:
|
|
log.debug("xch_target_address missing in the config")
|
|
return False
|
|
|
|
# This is the farmer configuration
|
|
self.farmer_target_encoded = self.config["xch_target_address"]
|
|
self.farmer_target = decode_puzzle_hash(self.farmer_target_encoded)
|
|
|
|
self.pool_public_keys = [G1Element.from_bytes(bytes.fromhex(pk)) for pk in self.config["pool_public_keys"]]
|
|
|
|
# This is the self pooling configuration, which is only used for original self-pooled plots
|
|
self.pool_target_encoded = self.pool_config["xch_target_address"]
|
|
self.pool_target = decode_puzzle_hash(self.pool_target_encoded)
|
|
self.pool_sks_map = {bytes(key.get_g1()): key for key in self.get_private_keys()}
|
|
|
|
assert len(self.farmer_target) == 32
|
|
assert len(self.pool_target) == 32
|
|
if len(self.pool_sks_map) == 0:
|
|
log.warning(no_keys_error_str)
|
|
return False
|
|
|
|
return True
|
|
|
|
def _set_state_changed_callback(self, callback: StateChangedProtocol) -> None:
|
|
self.state_changed_callback = callback
|
|
|
|
async def on_connect(self, peer: WSChiaConnection) -> None:
|
|
self.state_changed("add_connection", {})
|
|
|
|
async def handshake_task() -> None:
|
|
# Wait until the task in `Farmer._start` is done so that we have keys available for the handshake. Bail out
|
|
# early if we need to shut down or if the harvester is not longer connected.
|
|
# TODO: switch to event driven code
|
|
while not self.started and not self._shut_down and peer in self.server.get_connections(): # noqa: ASYNC110
|
|
await asyncio.sleep(1)
|
|
|
|
if self._shut_down:
|
|
log.debug("handshake_task: shutdown")
|
|
self.harvester_handshake_task = None
|
|
return
|
|
|
|
if peer not in self.server.get_connections():
|
|
log.debug("handshake_task: disconnected")
|
|
self.harvester_handshake_task = None
|
|
return
|
|
|
|
# Sends a handshake to the harvester
|
|
handshake = harvester_protocol.HarvesterHandshake(
|
|
self.get_public_keys(),
|
|
self.pool_public_keys,
|
|
)
|
|
msg = make_msg(ProtocolMessageTypes.harvester_handshake, handshake)
|
|
await peer.send_message(msg)
|
|
self.harvester_handshake_task = None
|
|
|
|
if peer.connection_type is NodeType.HARVESTER:
|
|
self.plot_sync_receivers[peer.peer_node_id] = Receiver(peer, self.plot_sync_callback, self.constants)
|
|
self.harvester_handshake_task = create_referenced_task(handshake_task())
|
|
|
|
def set_server(self, server: ChiaServer) -> None:
|
|
self.server = server
|
|
|
|
def state_changed(self, change: str, data: dict[str, Any]) -> None:
|
|
if self.state_changed_callback is not None:
|
|
self.state_changed_callback(change, data)
|
|
|
|
def get_current_time(self) -> uint64:
|
|
return uint64(time.time())
|
|
|
|
def handle_failed_pool_response(self, p2_singleton_puzzle_hash: bytes32, error_message: str) -> None:
|
|
self.log.error(error_message)
|
|
increment_pool_stats(
|
|
self.pool_state,
|
|
p2_singleton_puzzle_hash,
|
|
"pool_errors",
|
|
time.time(),
|
|
value=pool_protocol.ErrorResponse(
|
|
uint16(pool_protocol.PoolErrorCode.REQUEST_FAILED.value), error_message
|
|
).to_json_dict(),
|
|
)
|
|
|
|
async def on_disconnect(self, connection: WSChiaConnection) -> None:
|
|
self.log.info(f"peer disconnected {connection.get_peer_logging()}")
|
|
self.state_changed("close_connection", {})
|
|
if connection.connection_type is NodeType.HARVESTER:
|
|
del self.plot_sync_receivers[connection.peer_node_id]
|
|
self.state_changed("harvester_removed", {"node_id": connection.peer_node_id})
|
|
|
|
async def plot_sync_callback(self, peer_id: bytes32, delta: Delta | None) -> None:
|
|
log.debug(f"plot_sync_callback: peer_id {peer_id}, delta {delta}")
|
|
receiver: Receiver = self.plot_sync_receivers[peer_id]
|
|
harvester_updated: bool = delta is not None and not delta.empty()
|
|
if receiver.initial_sync() or harvester_updated:
|
|
self.state_changed("harvester_update", receiver.to_dict(True))
|
|
|
|
def _url_for_endpoint(self, pool_config: PoolingShareState, endpoint: str) -> str:
|
|
if pool_config.version == 1:
|
|
return f"{pool_config.pool_url}/{endpoint}"
|
|
else:
|
|
return f"{pool_config.pool_url}/v2/{endpoint}"
|
|
|
|
async def _get_current_authentication_token(
|
|
self, pool_config: PoolingShareState, authentication_token_timeout: uint8
|
|
) -> str | pool_protocol.ErrorResponse | None:
|
|
if pool_config.version == 1:
|
|
return str(pool_protocol.get_current_authentication_token(authentication_token_timeout))
|
|
elif pool_config.version == 2:
|
|
cached_auth_token = self.authentication_tokens.get(pool_config.launcher_id, None)
|
|
if cached_auth_token is None or datetime.fromtimestamp(
|
|
cached_auth_token[1], tz=timezone.utc
|
|
) < datetime.fromtimestamp(self.get_current_time(), tz=timezone.utc):
|
|
auth_response = await self._pool_get_auth(pool_config)
|
|
if isinstance(auth_response, pool_protocol.GetAuthResponse):
|
|
self.authentication_tokens[pool_config.launcher_id] = (
|
|
auth_response.authentication_token,
|
|
auth_response.expiration,
|
|
)
|
|
return auth_response.authentication_token
|
|
else:
|
|
return auth_response
|
|
else:
|
|
# seems sketchy because in theory we should check for non-None here but
|
|
# the auth token can't be None AND expired so semantics guarantee a non-None, non-expired token here
|
|
return cached_auth_token[0]
|
|
else:
|
|
raise ValueError("Unknown pool protocol version specified in pooling config")
|
|
|
|
async def _pool_get_auth(
|
|
self, pool_config: PoolingShareState
|
|
) -> pool_protocol.GetAuthResponse | pool_protocol.ErrorResponse | None:
|
|
timestamp = self.get_current_time()
|
|
message = bytes(timestamp) + bytes(pool_config.launcher_id) + pool_config.target_puzzle_hash
|
|
authentication_sk: PrivateKey | None = self.get_authentication_sk(pool_config)
|
|
if authentication_sk is None:
|
|
return None
|
|
signature: G2Element = AugSchemeMPL.sign(singleton_owner_sk_to_authv2_key(authentication_sk), message)
|
|
response, _ = await make_pool_protocol_request(
|
|
self=self,
|
|
pool_config=pool_config,
|
|
method="GET",
|
|
endpoint_name="auth",
|
|
request=pool_protocol.GetAuthRequest(
|
|
launcher_id=pool_config.launcher_id,
|
|
timestamp=timestamp,
|
|
signature=signature,
|
|
),
|
|
response_type=pool_protocol.GetAuthResponse,
|
|
)
|
|
return response
|
|
|
|
async def _pool_get_pool_info(self, pool_config: PoolingShareState) -> GetPoolInfoResult | None:
|
|
response, client_response = await make_pool_protocol_request(
|
|
self=self,
|
|
pool_config=pool_config,
|
|
method="GET",
|
|
endpoint_name="pool_info",
|
|
request=None,
|
|
response_type=pool_protocol.GetPoolInfoResponse,
|
|
)
|
|
if client_response is None:
|
|
return None
|
|
new_pool_url: str | None = None
|
|
response_url_str = f"{client_response.url}"
|
|
if (
|
|
response_url_str != self._url_for_endpoint(pool_config, "pool_info")
|
|
and len(client_response.history) > 0
|
|
and all(r.status in {301, 308} for r in client_response.history)
|
|
):
|
|
new_pool_url = response_url_str.replace("/pool_info", "")
|
|
new_pool_url = new_pool_url.replace(f"/v{pool_config.version}", "")
|
|
|
|
if isinstance(response, pool_protocol.GetPoolInfoResponse):
|
|
return GetPoolInfoResult(pool_info=response, new_pool_url=new_pool_url)
|
|
else:
|
|
return None
|
|
|
|
async def _pool_get_farmer(
|
|
self, pool_config: PoolingShareState, authentication_token_timeout: uint8
|
|
) -> pool_protocol.GetFarmerResponse | pool_protocol.ErrorResponse | None:
|
|
authentication_token = await self._get_current_authentication_token(pool_config, authentication_token_timeout)
|
|
if not isinstance(authentication_token, str):
|
|
self.log.error(f"Failed to authenticate to pool before aquiring farmer details: {pool_config.pool_url}")
|
|
return authentication_token
|
|
if pool_config.version == 1:
|
|
message: bytes32 = std_hash(
|
|
pool_protocol.AuthenticationPayloadV1(
|
|
"get_farmer",
|
|
pool_config.launcher_id,
|
|
pool_config.target_puzzle_hash,
|
|
uint64(authentication_token),
|
|
)
|
|
)
|
|
authentication_sk = self.get_authentication_sk(pool_config)
|
|
if authentication_sk is None:
|
|
return None
|
|
get_farmer_params: pool_protocol.GetFarmerRequestV1 | pool_protocol.GetFarmerRequestV2 = (
|
|
pool_protocol.GetFarmerRequestV1(
|
|
authentication_token=uint64(authentication_token),
|
|
launcher_id=pool_config.launcher_id,
|
|
signature=AugSchemeMPL.sign(authentication_sk, message),
|
|
authentication_token_v2="",
|
|
)
|
|
)
|
|
else:
|
|
get_farmer_params = pool_protocol.GetFarmerRequestV2(
|
|
authentication_token=uint64(0),
|
|
launcher_id=pool_config.launcher_id,
|
|
authentication_token_v2=authentication_token,
|
|
)
|
|
|
|
response, _ = await make_pool_protocol_request(
|
|
self=self,
|
|
pool_config=pool_config,
|
|
method="GET",
|
|
endpoint_name="farmer",
|
|
request=get_farmer_params,
|
|
response_type=pool_protocol.GetFarmerResponse,
|
|
)
|
|
return response
|
|
|
|
async def _pool_post_farmer(
|
|
self, pool_config: PoolingShareState, authentication_token_timeout: uint8
|
|
) -> pool_protocol.PostFarmerResponse | pool_protocol.ErrorResponse | None:
|
|
auth_sk: PrivateKey | None = self.get_authentication_sk(pool_config)
|
|
if auth_sk is None:
|
|
return None
|
|
|
|
if pool_config.version == 1:
|
|
authentication_token = await self._get_current_authentication_token(
|
|
pool_config, authentication_token_timeout
|
|
)
|
|
if authentication_token is None:
|
|
self.log.error(f"Attempting to POST farmer details without being logged into {pool_config.pool_url}")
|
|
return None
|
|
# impossible for this to fail when get_authentication_sk above succeeds
|
|
owner_sk = find_owner_sk(self.all_root_sks, pool_config.owner_public_key)[0] # type: ignore[index]
|
|
else:
|
|
owner_sk = singleton_owner_sk_to_authv2_key(auth_sk)
|
|
post_farmer_payload = pool_protocol.PostFarmerPayload(
|
|
pool_config.launcher_id,
|
|
uint64(authentication_token) if pool_config.version == 1 else uint64(0), # type: ignore[arg-type]
|
|
auth_sk.get_g1(),
|
|
pool_config.payout_instructions,
|
|
None,
|
|
)
|
|
signature: G2Element = AugSchemeMPL.sign(owner_sk, post_farmer_payload.get_hash())
|
|
post_farmer_request = pool_protocol.PostFarmerRequest(post_farmer_payload, signature)
|
|
response, _ = await make_pool_protocol_request(
|
|
self=self,
|
|
pool_config=pool_config,
|
|
method="POST",
|
|
endpoint_name="farmer",
|
|
request=post_farmer_request,
|
|
response_type=pool_protocol.PostFarmerResponse,
|
|
)
|
|
return response
|
|
|
|
async def _pool_put_farmer(
|
|
self, pool_config: PoolingShareState, authentication_token_timeout: uint8
|
|
) -> pool_protocol.PutFarmerResponse | pool_protocol.ErrorResponse | None:
|
|
auth_sk: PrivateKey | None = self.get_authentication_sk(pool_config)
|
|
if auth_sk is None:
|
|
return None
|
|
authentication_token = await self._get_current_authentication_token(pool_config, authentication_token_timeout)
|
|
if not isinstance(authentication_token, str):
|
|
self.log.error(f"Attempting to PUT farmer details without being logged into {pool_config.pool_url}")
|
|
return authentication_token
|
|
put_farmer_payload = pool_protocol.PutFarmerPayload(
|
|
pool_config.launcher_id,
|
|
uint64(authentication_token) if pool_config.version == 1 else uint64(0),
|
|
auth_sk.get_g1(),
|
|
pool_config.payout_instructions,
|
|
None,
|
|
authentication_token_v2=authentication_token,
|
|
)
|
|
if pool_config.version == 1:
|
|
# impossible for this to fail when get_authentication_sk above succeeds
|
|
owner_sk = find_owner_sk(self.all_root_sks, pool_config.owner_public_key)[0] # type: ignore[index]
|
|
signature = AugSchemeMPL.sign(owner_sk, put_farmer_payload.get_hash())
|
|
else:
|
|
signature = None
|
|
put_farmer_request = pool_protocol.PutFarmerRequest(put_farmer_payload, signature)
|
|
response, _ = await make_pool_protocol_request(
|
|
self=self,
|
|
pool_config=pool_config,
|
|
method="PUT",
|
|
endpoint_name="farmer",
|
|
request=put_farmer_request,
|
|
response_type=pool_protocol.PutFarmerResponse,
|
|
)
|
|
return response
|
|
|
|
def get_authentication_sk(self, pool_config: PoolingShareState) -> PrivateKey | None:
|
|
if pool_config.version == 1:
|
|
if pool_config.p2_singleton_puzzle_hash in self.authentication_keys:
|
|
return self.authentication_keys[pool_config.p2_singleton_puzzle_hash]
|
|
auth_sk: PrivateKey | None = find_authentication_sk(self.all_root_sks, pool_config.owner_public_key)
|
|
if auth_sk is not None:
|
|
self.authentication_keys[pool_config.p2_singleton_puzzle_hash] = auth_sk
|
|
return auth_sk
|
|
else:
|
|
for sk in self.all_root_sks:
|
|
auth_sk = calculate_synthetic_secret_key(
|
|
master_sk_to_wallet_sk_unhardened(sk, uint32(pool_config.key_derivation_index)),
|
|
DEFAULT_HIDDEN_PUZZLE_HASH,
|
|
)
|
|
if auth_sk.get_g1() == pool_config.owner_public_key:
|
|
return auth_sk
|
|
|
|
self.log.error(f"Failed to get authentication sk for pool {pool_config.pool_url}")
|
|
return None
|
|
|
|
async def update_pool_state(self) -> None:
|
|
config = load_config(self._root_path, "config.yaml")
|
|
|
|
p2_singleton_puzhashes = PoolingShareState.get_all_p2_singleton_puzzle_hashes(root_path=self._root_path)
|
|
for p2_singleton_puzzle_hash in p2_singleton_puzhashes:
|
|
try:
|
|
with PoolingShareState.acquire(
|
|
root_path=self._root_path, p2_singleton_puzzle_hash=p2_singleton_puzzle_hash, read_only=True
|
|
) as pool_config:
|
|
pass # Just releases the config without any edits
|
|
except Exception as e:
|
|
self.log.error(f"Error loading config for {p2_singleton_puzzle_hash}, {e}")
|
|
continue
|
|
|
|
try:
|
|
if p2_singleton_puzzle_hash not in self.pool_state:
|
|
self.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": 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,
|
|
"pool_config": pool_config,
|
|
}
|
|
self.log.info(f"Added pool: {pool_config}")
|
|
else:
|
|
self.pool_state[p2_singleton_puzzle_hash]["pool_config"] = pool_config
|
|
|
|
pool_state = self.pool_state[p2_singleton_puzzle_hash]
|
|
|
|
# Skip state update when self pooling
|
|
if pool_config.pool_url == "":
|
|
continue
|
|
|
|
enforce_https = config["full_node"]["selected_network"] == "mainnet"
|
|
if enforce_https and not pool_config.pool_url.startswith("https://"):
|
|
self.log.error(f"Pool URLs must be HTTPS on mainnet {pool_config.pool_url}")
|
|
continue
|
|
|
|
# TODO: Improve error handling below, inform about unexpected failures
|
|
if time.time() >= pool_state["next_pool_info_update"]:
|
|
pool_state["next_pool_info_update"] = time.time() + UPDATE_POOL_INFO_INTERVAL
|
|
# Makes a GET request to the pool to get the updated information
|
|
pool_info_result = await self._pool_get_pool_info(pool_config)
|
|
if pool_info_result is not None:
|
|
pool_info = pool_info_result.pool_info
|
|
pool_state["authentication_token_timeout"] = pool_info.authentication_token_timeout
|
|
# Only update the first time from GET /pool_info, gets updated from GET /farmer later
|
|
if pool_state["current_difficulty"] is None:
|
|
pool_state["current_difficulty"] = pool_info.minimum_difficulty
|
|
else:
|
|
pool_state["next_pool_info_update"] = time.time() + UPDATE_POOL_INFO_FAILURE_RETRY_INTERVAL
|
|
|
|
if pool_info_result is not None and pool_info_result.new_pool_url is not None:
|
|
with PoolingShareState.acquire(
|
|
root_path=self._root_path, p2_singleton_puzzle_hash=p2_singleton_puzzle_hash
|
|
) as editable_pool_config:
|
|
editable_pool_config.pool_url = pool_info_result.new_pool_url
|
|
self.pool_state[p2_singleton_puzzle_hash]["pool_config"] = editable_pool_config
|
|
pool_config = editable_pool_config
|
|
|
|
if time.time() >= pool_state["next_farmer_update"]:
|
|
pool_state["next_farmer_update"] = time.time() + UPDATE_POOL_FARMER_INFO_INTERVAL
|
|
authentication_token_timeout = pool_state["authentication_token_timeout"]
|
|
|
|
async def update_pool_farmer_info() -> tuple[
|
|
pool_protocol.GetFarmerResponse | None, pool_protocol.PoolErrorCode | None
|
|
]:
|
|
# Run a GET /farmer to see if the farmer is already known by the pool
|
|
response = await self._pool_get_farmer(pool_config, authentication_token_timeout)
|
|
if response is not None:
|
|
if not isinstance(response, pool_protocol.ErrorResponse):
|
|
pool_state["current_difficulty"] = response.current_difficulty
|
|
pool_state["current_points"] = response.current_points
|
|
return response, None
|
|
else:
|
|
try:
|
|
error_code = pool_protocol.PoolErrorCode(response.error_code)
|
|
return None, error_code
|
|
except ValueError:
|
|
self.log.error(f"Invalid error code received from the pool: {response.error_code}")
|
|
return None, None
|
|
return None, None
|
|
|
|
if authentication_token_timeout is not None:
|
|
farmer_info, error_code = await update_pool_farmer_info()
|
|
if error_code == pool_protocol.PoolErrorCode.FARMER_NOT_KNOWN:
|
|
post_response = await self._pool_post_farmer(pool_config, authentication_token_timeout)
|
|
if post_response is not None and not isinstance(post_response, pool_protocol.ErrorResponse):
|
|
self.log.info(
|
|
f"Welcome message from {pool_config.pool_url}: {post_response.welcome_message}"
|
|
)
|
|
# Now we should be able to update the local farmer info
|
|
(farmer_info, farmer_is_known) = await update_pool_farmer_info()
|
|
if farmer_info is None and not farmer_is_known:
|
|
self.log.error("Failed to update farmer info after POST /farmer.")
|
|
|
|
# Update the farmer information on the pool if the payout instructions changed or if the
|
|
# signature is invalid (latter to make sure the pool has the correct auth public key).
|
|
payout_instructions_update_required: bool = (
|
|
farmer_info is not None
|
|
and pool_config.payout_instructions.lower() != farmer_info.payout_instructions.lower()
|
|
)
|
|
if (
|
|
payout_instructions_update_required
|
|
or error_code == pool_protocol.PoolErrorCode.INVALID_SIGNATURE
|
|
):
|
|
await self._pool_put_farmer(pool_config, authentication_token_timeout)
|
|
else:
|
|
self.log.warning(
|
|
"No pool specific authentication_token_timeout has been set for "
|
|
f"{p2_singleton_puzzle_hash}, check communication with the pool."
|
|
)
|
|
|
|
except Exception as e:
|
|
tb = traceback.format_exc()
|
|
self.log.error(f"Exception in update_pool_state for {pool_config.pool_url}, {e} {tb}")
|
|
|
|
def get_public_keys(self) -> list[G1Element]:
|
|
return [child_sk.get_g1() for child_sk in self._private_keys]
|
|
|
|
def get_private_keys(self) -> list[PrivateKey]:
|
|
return self._private_keys
|
|
|
|
async def get_reward_targets(self, search_for_private_key: bool, max_ph_to_search: int = 500) -> dict[str, Any]:
|
|
if search_for_private_key:
|
|
all_sks = await self.get_all_private_keys()
|
|
have_farmer_sk, have_pool_sk = False, False
|
|
search_addresses: list[bytes32] = [self.farmer_target, self.pool_target]
|
|
for sk, _ in all_sks:
|
|
found_addresses: set[bytes32] = match_address_to_sk(sk, search_addresses, max_ph_to_search)
|
|
|
|
if not have_farmer_sk and self.farmer_target in found_addresses:
|
|
search_addresses.remove(self.farmer_target)
|
|
have_farmer_sk = True
|
|
|
|
if not have_pool_sk and self.pool_target in found_addresses:
|
|
search_addresses.remove(self.pool_target)
|
|
have_pool_sk = True
|
|
|
|
if have_farmer_sk and have_pool_sk:
|
|
break
|
|
|
|
return {
|
|
"farmer_target": self.farmer_target_encoded,
|
|
"pool_target": self.pool_target_encoded,
|
|
"have_farmer_sk": have_farmer_sk,
|
|
"have_pool_sk": have_pool_sk,
|
|
}
|
|
return {
|
|
"farmer_target": self.farmer_target_encoded,
|
|
"pool_target": self.pool_target_encoded,
|
|
}
|
|
|
|
def set_reward_targets(self, farmer_target_encoded: str | None, pool_target_encoded: str | None) -> None:
|
|
with lock_and_load_config(self._root_path, "config.yaml") as config:
|
|
if farmer_target_encoded is not None:
|
|
self.farmer_target_encoded = farmer_target_encoded
|
|
self.farmer_target = decode_puzzle_hash(farmer_target_encoded)
|
|
config["farmer"]["xch_target_address"] = farmer_target_encoded
|
|
if pool_target_encoded is not None:
|
|
self.pool_target_encoded = pool_target_encoded
|
|
self.pool_target = decode_puzzle_hash(pool_target_encoded)
|
|
config["pool"]["xch_target_address"] = pool_target_encoded
|
|
save_config(self._root_path, "config.yaml", config)
|
|
|
|
async def set_payout_instructions(self, launcher_id: bytes32, payout_instructions: str) -> None:
|
|
for p2_singleton_puzzle_hash, pool_state_dict in self.pool_state.items():
|
|
if launcher_id == pool_state_dict["pool_config"].launcher_id:
|
|
with PoolingShareState.acquire(
|
|
root_path=self._root_path, p2_singleton_puzzle_hash=p2_singleton_puzzle_hash
|
|
) as pool_config:
|
|
pool_config.payout_instructions = payout_instructions
|
|
# Force a GET /farmer which triggers the PUT /farmer if it detects the changed instructions
|
|
pool_state_dict["next_farmer_update"] = 0
|
|
return
|
|
|
|
self.log.warning(f"Launcher id: {launcher_id} not found")
|
|
|
|
async def generate_login_link(self, launcher_id: bytes32) -> str | None:
|
|
for pool_state in self.pool_state.values():
|
|
pool_config: PoolingShareState = pool_state["pool_config"]
|
|
if pool_config.launcher_id != launcher_id:
|
|
continue
|
|
|
|
authentication_sk: PrivateKey | None = self.get_authentication_sk(pool_config)
|
|
if authentication_sk is None:
|
|
return None
|
|
if pool_config.version == 2:
|
|
authentication_sk = singleton_owner_sk_to_authv2_key(authentication_sk)
|
|
|
|
authentication_token_timeout = pool_state["authentication_token_timeout"]
|
|
if authentication_token_timeout is None:
|
|
self.log.error(
|
|
f"No pool specific authentication_token_timeout has been set for"
|
|
f"{pool_config.p2_singleton_puzzle_hash}, check communication with the pool."
|
|
)
|
|
return None
|
|
|
|
auth_token = str(pool_protocol.get_current_authentication_token(authentication_token_timeout))
|
|
message: bytes = std_hash(
|
|
pool_protocol.AuthenticationPayloadV1(
|
|
"get_login",
|
|
pool_config.launcher_id,
|
|
pool_config.target_puzzle_hash,
|
|
uint64(auth_token),
|
|
)
|
|
)
|
|
signature: G2Element = AugSchemeMPL.sign(authentication_sk, message)
|
|
return (
|
|
self._url_for_endpoint(pool_config, "login") + f"?launcher_id={pool_config.launcher_id.hex()}"
|
|
f"&authentication_token={auth_token}"
|
|
f"&signature={bytes(signature).hex()}"
|
|
)
|
|
|
|
return None
|
|
|
|
async def get_harvesters(self, counts_only: bool = False) -> dict[str, Any]:
|
|
harvesters: list[dict[str, Any]] = []
|
|
for connection in self.server.get_connections(NodeType.HARVESTER):
|
|
self.log.debug(f"get_harvesters host: {connection.peer_info.host}, node_id: {connection.peer_node_id}")
|
|
receiver = self.plot_sync_receivers.get(connection.peer_node_id)
|
|
if receiver is not None:
|
|
harvesters.append(receiver.to_dict(counts_only))
|
|
else:
|
|
self.log.debug(
|
|
f"get_harvesters invalid peer: {connection.peer_info.host}, node_id: {connection.peer_node_id}"
|
|
)
|
|
|
|
return {"harvesters": harvesters}
|
|
|
|
def get_receiver(self, node_id: bytes32) -> Receiver:
|
|
receiver: Receiver | None = self.plot_sync_receivers.get(node_id)
|
|
if receiver is None:
|
|
raise KeyError(f"Receiver missing for {node_id}")
|
|
return receiver
|
|
|
|
def check_missing_signage_points(
|
|
self, timestamp: uint64, new_signage_point: farmer_protocol.NewSignagePoint
|
|
) -> tuple[uint64, uint32] | None:
|
|
if self.prev_signage_point is None:
|
|
self.prev_signage_point = (timestamp, new_signage_point)
|
|
return None
|
|
|
|
prev_time, prev_sp = self.prev_signage_point
|
|
self.prev_signage_point = (timestamp, new_signage_point)
|
|
|
|
if prev_sp.challenge_hash == new_signage_point.challenge_hash:
|
|
missing_sps = new_signage_point.signage_point_index - prev_sp.signage_point_index - 1
|
|
if missing_sps > 0:
|
|
return timestamp, uint32(missing_sps)
|
|
return None
|
|
|
|
actual_sp_interval_seconds = float(timestamp - prev_time)
|
|
if actual_sp_interval_seconds <= 0:
|
|
return None
|
|
|
|
expected_sp_interval_seconds = self.constants.SUB_SLOT_TIME_TARGET / self.constants.NUM_SPS_SUB_SLOT
|
|
allowance = 1.6 # Should be chosen from the range (1 <= allowance < 2)
|
|
if actual_sp_interval_seconds < expected_sp_interval_seconds * allowance:
|
|
return None
|
|
|
|
skipped_sps = uint32(floor(actual_sp_interval_seconds / expected_sp_interval_seconds))
|
|
return timestamp, skipped_sps
|
|
|
|
async def _periodically_update_pool_state_task(self) -> None:
|
|
time_slept = 0
|
|
while not self._shut_down:
|
|
# Every time the config file changes, read it to check the pool state
|
|
state_path = PoolingShareState.state_path(self._root_path)
|
|
if state_path.exists():
|
|
stat = state_path.stat()
|
|
if stat.st_mtime > self.last_config_access_time:
|
|
# If we detect the config file changed, refresh private keys first just in case
|
|
self.all_root_sks = [sk for sk, _ in await self.get_all_private_keys()]
|
|
self.last_config_access_time = stat.st_mtime
|
|
await self.update_pool_state()
|
|
time_slept = 0
|
|
continue
|
|
if time_slept > 60:
|
|
await self.update_pool_state()
|
|
time_slept = 0
|
|
time_slept += 1
|
|
await asyncio.sleep(1)
|
|
|
|
async def _periodically_clear_cache_and_refresh_task(self) -> None:
|
|
time_slept = 0
|
|
refresh_slept = 0
|
|
while not self._shut_down:
|
|
try:
|
|
if time_slept > self.constants.SUB_SLOT_TIME_TARGET:
|
|
now = time.time()
|
|
removed_keys: list[bytes32] = []
|
|
for key, add_time in self.cache_add_time.items():
|
|
if now - float(add_time) > self.constants.SUB_SLOT_TIME_TARGET * 3:
|
|
self.sps.pop(key, None)
|
|
self.proofs_of_space.pop(key, None)
|
|
self.quality_str_to_identifiers.pop(key, None)
|
|
self.number_of_responses.pop(key, None)
|
|
removed_keys.append(key)
|
|
for key in removed_keys:
|
|
self.cache_add_time.pop(key, None)
|
|
time_slept = 0
|
|
log.debug(
|
|
f"Cleared farmer cache. Num sps: {len(self.sps)} {len(self.proofs_of_space)} "
|
|
f"{len(self.quality_str_to_identifiers)} {len(self.number_of_responses)}"
|
|
)
|
|
time_slept += 1
|
|
refresh_slept += 1
|
|
# Periodically refresh GUI to show the correct download/upload rate.
|
|
if refresh_slept >= 30:
|
|
self.state_changed("add_connection", {})
|
|
refresh_slept = 0
|
|
|
|
except Exception:
|
|
log.error(f"_periodically_clear_cache_and_refresh_task failed: {traceback.format_exc()}")
|
|
|
|
await asyncio.sleep(1)
|
|
|
|
def notify_farmer_reward_taken_by_harvester_as_fee(
|
|
self, sp: farmer_protocol.NewSignagePoint, proof_of_space: harvester_protocol.NewProofOfSpace
|
|
) -> None:
|
|
"""
|
|
Apply a fee quality convention (see CHIP-22: https://github.com/Chia-Network/chips/pull/88)
|
|
given the proof and signage point. This will be tested against the fee threshold reported
|
|
by the harvester (if any), and logged.
|
|
"""
|
|
assert proof_of_space.farmer_reward_address_override is not None
|
|
|
|
challenge_str = str(sp.challenge_hash)
|
|
|
|
ph_prefix = self.config["network_overrides"]["config"][self.config["selected_network"]]["address_prefix"]
|
|
farmer_reward_puzzle_hash = encode_puzzle_hash(proof_of_space.farmer_reward_address_override, ph_prefix)
|
|
|
|
self.log.info(
|
|
f"Farmer reward for challenge '{challenge_str}' "
|
|
+ f"taken by harvester for reward address '{farmer_reward_puzzle_hash}'"
|
|
)
|
|
|
|
fee_quality = calculate_harvester_fee_quality(proof_of_space.proof.proof, sp.challenge_hash)
|
|
fee_quality_rate = float(fee_quality) / float(0xFFFFFFFF) * 100.0
|
|
|
|
if proof_of_space.fee_info is not None:
|
|
fee_threshold = proof_of_space.fee_info.applied_fee_threshold
|
|
fee_threshold_rate = float(fee_threshold) / float(0xFFFFFFFF) * 100.0
|
|
|
|
if fee_quality <= fee_threshold:
|
|
self.log.info(
|
|
f"Fee threshold passed for challenge '{challenge_str}': "
|
|
+ f"{fee_quality_rate:.3f}%/{fee_threshold_rate:.3f}% ({fee_quality}/{fee_threshold})"
|
|
)
|
|
else:
|
|
self.log.warning(
|
|
f"Invalid fee threshold for challenge '{challenge_str}': "
|
|
+ f"{fee_quality_rate:.3f}%/{fee_threshold_rate:.3f}% ({fee_quality}/{fee_threshold})"
|
|
)
|
|
self.log.warning(
|
|
"Harvester illegitimately took a fee reward that "
|
|
+ "did not belong to it or it incorrectly applied the fee convention."
|
|
)
|
|
else:
|
|
self.log.warning(
|
|
"Harvester illegitimately took reward by failing to provide its fee rate "
|
|
+ f"for challenge '{challenge_str}'. "
|
|
+ f"Fee quality was {fee_quality_rate:.3f}% ({fee_quality} or 0x{fee_quality:08x})"
|
|
)
|
|
|
|
|
|
def calculate_harvester_fee_quality(proof: bytes, challenge: bytes32) -> uint32:
|
|
"""
|
|
This calculates the 'fee quality' given a convention between farmers and third party harvesters.
|
|
See CHIP-22: https://github.com/Chia-Network/chips/pull/88
|
|
"""
|
|
return uint32(int.from_bytes(std_hash(proof + challenge)[32 - 4 :], byteorder="big", signed=False))
|