mirror of
https://github.com/Chia-Network/chia-blockchain.git
synced 2026-08-24 02:24:23 -05:00
[LABS-397] Add v2 support to farmer (#20739)
* 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
This commit is contained in:
@@ -2,6 +2,9 @@ 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
|
||||
@@ -20,13 +23,25 @@ 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 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
|
||||
from chia.protocols import farmer_protocol, harvester_protocol, pool_protocol
|
||||
from chia.protocols.harvester_protocol import NewProofOfSpace, RespondSignatures
|
||||
from chia.protocols.pool_protocol import PoolErrorCode
|
||||
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 (
|
||||
@@ -35,6 +50,11 @@ from chia.types.blockchain_format.proof_of_space import (
|
||||
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__)
|
||||
|
||||
@@ -1026,11 +1046,11 @@ class DummyPoolInfoResponse:
|
||||
pool_info: dict[str, Any] | None = None
|
||||
history: tuple[DummyClientResponse, ...] = ()
|
||||
|
||||
async def text(self) -> str:
|
||||
async def json(self, **kwargs: object) -> dict[str, Any]:
|
||||
if self.pool_info is None:
|
||||
return ""
|
||||
return {} # pragma: no cover
|
||||
|
||||
return json.dumps(self.pool_info)
|
||||
return self.pool_info
|
||||
|
||||
async def __aenter__(self) -> Self:
|
||||
return self
|
||||
@@ -1224,42 +1244,42 @@ class PoolInfoCase(DataCase):
|
||||
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")
|
||||
farmer_service._node.authentication_keys = {
|
||||
p2_singleton_puzzle_hash: PrivateKey.from_bytes(
|
||||
bytes.fromhex("11ed596eb95b31364a9185e948f6b66be30415f816819449d5d40751dc70e786")
|
||||
),
|
||||
}
|
||||
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=G1Element.from_bytes(
|
||||
bytes.fromhex(
|
||||
"84c3fcf9d5581c1ddc702cb0f3b4a06043303b334dd993ab42b2c320ebfa98e5ce558448615b3f69638ba92cf7f43da5"
|
||||
)
|
||||
),
|
||||
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=-1,
|
||||
key_derivation_index=0,
|
||||
version=pool_protocol_version,
|
||||
).add(root_path=farmer_service.root_path)
|
||||
mock_http_get = mocker.patch("aiohttp.ClientSession.get", return_value=case.pool_response)
|
||||
mock_http_get = mocker.patch("aiohttp.ClientSession.request", return_value=case.pool_response)
|
||||
|
||||
await farmer_service._node.update_pool_state()
|
||||
|
||||
@@ -1273,6 +1293,122 @@ async def test_farmer_pool_info_config_update(
|
||||
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
|
||||
@@ -1398,3 +1534,145 @@ async def test_farmer_additional_headers_on_partial_submit(
|
||||
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) # noqa: RUF075
|
||||
if path.endswith("/farmer"):
|
||||
if method == "POST":
|
||||
yield DummyPostFarmerResponse(ok=True) # noqa: RUF075
|
||||
if method == "PUT":
|
||||
yield DummyPutFarmerResponse(ok=True) # noqa: RUF075
|
||||
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))
|
||||
|
||||
@@ -595,7 +595,7 @@ async def test_plotnft_cli_join(
|
||||
).run()
|
||||
|
||||
pool_response_dict["relative_lock_height"] = LOCK_HEIGHT
|
||||
pool_response_dict["protocol_version"] = 2
|
||||
pool_response_dict["protocol_version"] = 1 if version == 2 else 2
|
||||
mock_get.return_value.__aenter__.return_value.text.return_value = json.dumps(pool_response_dict)
|
||||
|
||||
with pytest.raises(CliRpcConnectionError, match="Incorrect version"):
|
||||
@@ -609,7 +609,7 @@ async def test_plotnft_cli_join(
|
||||
).run()
|
||||
|
||||
pool_response_dict["relative_lock_height"] = LOCK_HEIGHT
|
||||
pool_response_dict["protocol_version"] = 1
|
||||
pool_response_dict["protocol_version"] = 1 if version == 1 else 2
|
||||
mock_get.return_value.__aenter__.return_value.text.return_value = json.dumps(pool_response_dict)
|
||||
|
||||
if prompt:
|
||||
|
||||
@@ -15,6 +15,7 @@ from chia_rs import (
|
||||
InfusedChallengeChainSubSlot,
|
||||
PartialProof,
|
||||
PoolTarget,
|
||||
Program,
|
||||
ProofOfSpace,
|
||||
RespondToPhUpdates,
|
||||
RewardChainBlock,
|
||||
@@ -1003,7 +1004,7 @@ respond_peers_introducer = introducer_protocol.RespondPeersIntroducer(
|
||||
|
||||
|
||||
# POOL PROTOCOL
|
||||
authentication_payload = pool_protocol.AuthenticationPayload(
|
||||
authentication_payload = pool_protocol.AuthenticationPayloadV1(
|
||||
"method",
|
||||
bytes32(bytes.fromhex("0251e3b3a1aacc689091b6b085be7a8d319bd9d1a015faae969cb76d8a45607c")),
|
||||
bytes32(bytes.fromhex("9de241b508b5e9e2073b7645291cfaa9458d33935340399a861acf2ee1770440")),
|
||||
@@ -1020,10 +1021,12 @@ get_pool_info_response = pool_protocol.GetPoolInfoResponse(
|
||||
"pool description.",
|
||||
bytes32(bytes.fromhex("f6b5120ff1ab7ba661e3b2c91c8b373a8aceea8e4eb6ce3f085f3e80a8655b36")),
|
||||
uint8(76),
|
||||
Program.to(None),
|
||||
)
|
||||
|
||||
post_partial_request = pool_protocol.PostPartialRequest(
|
||||
post_partial_payload,
|
||||
"",
|
||||
g2_element,
|
||||
)
|
||||
|
||||
@@ -1073,6 +1076,7 @@ put_farmer_payload = pool_protocol.PutFarmerPayload(
|
||||
),
|
||||
"payload",
|
||||
uint64(201241879360854600),
|
||||
authentication_token_v2="",
|
||||
)
|
||||
|
||||
put_farmer_request = pool_protocol.PutFarmerRequest(
|
||||
|
||||
Binary file not shown.
@@ -2454,6 +2454,7 @@ get_pool_info_response_json: dict[str, Any] = {
|
||||
"description": "pool description.",
|
||||
"target_puzzle_hash": "0xf6b5120ff1ab7ba661e3b2c91c8b373a8aceea8e4eb6ce3f085f3e80a8655b36",
|
||||
"authentication_token_timeout": 76,
|
||||
"pool_memoization": "0x80",
|
||||
}
|
||||
|
||||
post_partial_payload_json: dict[str, Any] = {
|
||||
@@ -2496,6 +2497,7 @@ post_partial_request_json: dict[str, Any] = {
|
||||
"end_of_sub_slot": False,
|
||||
"harvester_id": "0xf98dff6bdcc3926b33cb8ab22e11bd15c13d6a9b6832ac948b3273f5ccd8e7ec",
|
||||
},
|
||||
"authentication_token_v2": "",
|
||||
"aggregate_signature": "0xc00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
||||
}
|
||||
|
||||
@@ -2535,6 +2537,7 @@ put_farmer_payload_json: dict[str, Any] = {
|
||||
"authentication_public_key": "0xa04c6b5ac7dfb935f6feecfdd72348ccf1d4be4fe7e26acf271ea3b7d308da61e0a308f7a62495328a81f5147b66634c",
|
||||
"payout_instructions": "payload",
|
||||
"suggested_difficulty": 201241879360854600,
|
||||
"authentication_token_v2": "",
|
||||
}
|
||||
|
||||
put_farmer_request_json: dict[str, Any] = {
|
||||
@@ -2544,6 +2547,7 @@ put_farmer_request_json: dict[str, Any] = {
|
||||
"authentication_public_key": "0xa04c6b5ac7dfb935f6feecfdd72348ccf1d4be4fe7e26acf271ea3b7d308da61e0a308f7a62495328a81f5147b66634c",
|
||||
"payout_instructions": "payload",
|
||||
"suggested_difficulty": 201241879360854600,
|
||||
"authentication_token_v2": "",
|
||||
},
|
||||
"signature": "0xc00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
||||
}
|
||||
|
||||
@@ -201,7 +201,11 @@ def test_missing_messages() -> None:
|
||||
introducer_msgs = {"RequestPeersIntroducer", "RespondPeersIntroducer"}
|
||||
|
||||
pool_msgs = {
|
||||
"AuthenticationPayload",
|
||||
"AuthenticationPayloadV1",
|
||||
"GetAuthRequest",
|
||||
"GetFarmerRequestV1",
|
||||
"GetFarmerRequestV2",
|
||||
"GetAuthResponse",
|
||||
"ErrorResponse",
|
||||
"GetFarmerResponse",
|
||||
"GetPoolInfoResponse",
|
||||
|
||||
@@ -26,7 +26,6 @@ from chia.cmds.wallet_funcs import print_balance, wallet_coin_unit
|
||||
from chia.farmer.farmer_rpc_client import FarmerRpcClient
|
||||
from chia.pools.pool_config import PoolingShareState
|
||||
from chia.pools.pool_wallet_info import NewPoolWalletInitialTargetState, PoolSingletonState, PoolWalletInfo
|
||||
from chia.protocols.pool_protocol import POOL_PROTOCOL_VERSION
|
||||
from chia.rpc.rpc_client import ResponseFailureError
|
||||
from chia.server.server import ssl_context_for_root
|
||||
from chia.ssl.create_ssl import get_mozilla_ca_crt
|
||||
@@ -68,8 +67,8 @@ async def create_pool_args(pool_url: str) -> dict[str, Any]:
|
||||
|
||||
if json_dict["relative_lock_height"] > 1000:
|
||||
raise ValueError("Relative lock height too high for this pool, cannot join")
|
||||
if json_dict["protocol_version"] != POOL_PROTOCOL_VERSION:
|
||||
raise ValueError(f"Incorrect version: {json_dict['protocol_version']}, should be {POOL_PROTOCOL_VERSION}")
|
||||
if json_dict["protocol_version"] not in {1, 2}:
|
||||
raise ValueError(f"Incorrect version: {json_dict['protocol_version']}, should be 1 or 2")
|
||||
|
||||
header_msg = f"\n---- Pool parameters fetched from {pool_url} ----"
|
||||
print(header_msg)
|
||||
@@ -368,7 +367,10 @@ async def join_pool(
|
||||
raise CliRpcConnectionError(f"Pool URLs must be HTTPS on mainnet {pool_url}.")
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(f"{pool_url}/pool_info", ssl=ssl_context_for_root(get_mozilla_ca_crt())) as response:
|
||||
async with session.get(
|
||||
f"{pool_url}/{'v2/' if pool_wallet_info.current.version == 2 else ''}pool_info",
|
||||
ssl=ssl_context_for_root(get_mozilla_ca_crt()),
|
||||
) as response:
|
||||
if response.ok:
|
||||
json_dict = json.loads(await response.text())
|
||||
else:
|
||||
@@ -379,9 +381,9 @@ async def join_pool(
|
||||
if json_dict["relative_lock_height"] > 1000:
|
||||
raise CliRpcConnectionError("Relative lock height too high for this pool, cannot join")
|
||||
|
||||
if json_dict["protocol_version"] != POOL_PROTOCOL_VERSION:
|
||||
if json_dict["protocol_version"] != pool_wallet_info.current.version:
|
||||
raise CliRpcConnectionError(
|
||||
f"Incorrect version: {json_dict['protocol_version']}, should be {POOL_PROTOCOL_VERSION}"
|
||||
f"Incorrect version: {json_dict['protocol_version']}, should be {pool_wallet_info.current.version}"
|
||||
)
|
||||
|
||||
pprint(json_dict)
|
||||
|
||||
+313
-231
@@ -2,16 +2,16 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import json
|
||||
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, cast
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, Literal, TypeVar, cast
|
||||
|
||||
import aiohttp
|
||||
from chia_rs import AugSchemeMPL, ConsensusConstants, G1Element, G2Element, PrivateKey, ProofOfSpace
|
||||
@@ -22,20 +22,8 @@ from chia.daemon.keychain_proxy import KeychainProxy, connect_to_keychain_and_va
|
||||
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
|
||||
from chia.protocols import farmer_protocol, harvester_protocol, pool_protocol
|
||||
from chia.protocols.outbound_message import NodeType, make_msg
|
||||
from chia.protocols.pool_protocol import (
|
||||
AuthenticationPayload,
|
||||
ErrorResponse,
|
||||
GetFarmerResponse,
|
||||
GetPoolInfoResponse,
|
||||
PoolErrorCode,
|
||||
PostFarmerPayload,
|
||||
PostFarmerRequest,
|
||||
PutFarmerPayload,
|
||||
PutFarmerRequest,
|
||||
get_current_authentication_token,
|
||||
)
|
||||
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
|
||||
@@ -48,13 +36,20 @@ 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
|
||||
|
||||
@@ -69,7 +64,7 @@ UPDATE_POOL_FARMER_INFO_INTERVAL: int = 300
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GetPoolInfoResult:
|
||||
pool_info: GetPoolInfoResponse
|
||||
pool_info: pool_protocol.GetPoolInfoResponse
|
||||
new_pool_url: str | None
|
||||
|
||||
|
||||
@@ -110,6 +105,72 @@ def increment_pool_stats(
|
||||
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)
|
||||
"""
|
||||
@@ -173,6 +234,7 @@ class Farmer:
|
||||
|
||||
# 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
|
||||
@@ -323,6 +385,9 @@ class Farmer:
|
||||
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(
|
||||
@@ -330,7 +395,9 @@ class Farmer:
|
||||
p2_singleton_puzzle_hash,
|
||||
"pool_errors",
|
||||
time.time(),
|
||||
value=ErrorResponse(uint16(PoolErrorCode.REQUEST_FAILED.value), error_message).to_json_dict(),
|
||||
value=pool_protocol.ErrorResponse(
|
||||
uint16(pool_protocol.PoolErrorCode.REQUEST_FAILED.value), error_message
|
||||
).to_json_dict(),
|
||||
)
|
||||
|
||||
async def on_disconnect(self, connection: WSChiaConnection) -> None:
|
||||
@@ -347,199 +414,222 @@ class Farmer:
|
||||
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:
|
||||
try:
|
||||
async with aiohttp.ClientSession(trust_env=True) as session:
|
||||
url = f"{pool_config.pool_url}/pool_info"
|
||||
async with session.get(url, ssl=ssl_context_for_root(get_mozilla_ca_crt(), log=self.log)) as resp:
|
||||
if resp.ok:
|
||||
response: dict[str, Any] = json.loads(await resp.text())
|
||||
self.log.info(f"GET /pool_info response: {response}")
|
||||
if "error_code" in response:
|
||||
self.handle_failed_pool_response(
|
||||
pool_config.p2_singleton_puzzle_hash,
|
||||
f"Error in GET /pool_info {pool_config.pool_url}, {response}",
|
||||
)
|
||||
return None
|
||||
try:
|
||||
pool_info = GetPoolInfoResponse.from_json_dict(response)
|
||||
except Exception as e:
|
||||
self.handle_failed_pool_response(
|
||||
pool_config.p2_singleton_puzzle_hash,
|
||||
f"Invalid GET /pool_info response {pool_config.pool_url}, {e}",
|
||||
)
|
||||
return None
|
||||
new_pool_url: str | None = None
|
||||
response_url_str = f"{resp.url}"
|
||||
if (
|
||||
response_url_str != url
|
||||
and len(resp.history) > 0
|
||||
and all(r.status in {301, 308} for r in resp.history)
|
||||
):
|
||||
new_pool_url = response_url_str.replace("/pool_info", "")
|
||||
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}", "")
|
||||
|
||||
return GetPoolInfoResult(pool_info=pool_info, new_pool_url=new_pool_url)
|
||||
else:
|
||||
self.handle_failed_pool_response(
|
||||
pool_config.p2_singleton_puzzle_hash,
|
||||
f"Error in GET /pool_info {pool_config.pool_url}, {resp.status}",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.handle_failed_pool_response(
|
||||
pool_config.p2_singleton_puzzle_hash, f"Exception in GET /pool_info {pool_config.pool_url}, {e}"
|
||||
)
|
||||
|
||||
return None
|
||||
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, authentication_sk: PrivateKey
|
||||
) -> dict[str, Any] | None:
|
||||
authentication_token = get_current_authentication_token(authentication_token_timeout)
|
||||
message: bytes32 = std_hash(
|
||||
AuthenticationPayload(
|
||||
"get_farmer", pool_config.launcher_id, pool_config.target_puzzle_hash, authentication_token
|
||||
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,
|
||||
)
|
||||
signature: G2Element = AugSchemeMPL.sign(authentication_sk, message)
|
||||
get_farmer_params: dict[str, str | int] = {
|
||||
"launcher_id": pool_config.launcher_id.hex(),
|
||||
"authentication_token": authentication_token,
|
||||
"signature": bytes(signature).hex(),
|
||||
}
|
||||
try:
|
||||
async with aiohttp.ClientSession(trust_env=True) as session:
|
||||
async with session.get(
|
||||
f"{pool_config.pool_url}/farmer",
|
||||
params=get_farmer_params,
|
||||
ssl=ssl_context_for_root(get_mozilla_ca_crt(), log=self.log),
|
||||
) as resp:
|
||||
if resp.ok:
|
||||
response: dict[str, Any] = json.loads(await resp.text())
|
||||
log_level = logging.INFO
|
||||
if "error_code" in response:
|
||||
log_level = logging.WARNING
|
||||
increment_pool_stats(
|
||||
self.pool_state,
|
||||
pool_config.p2_singleton_puzzle_hash,
|
||||
"pool_errors",
|
||||
time.time(),
|
||||
value=response,
|
||||
)
|
||||
self.log.log(log_level, f"GET /farmer response: {response}")
|
||||
return response
|
||||
else:
|
||||
self.handle_failed_pool_response(
|
||||
pool_config.p2_singleton_puzzle_hash,
|
||||
f"Error in GET /farmer {pool_config.pool_url}, {resp.status}",
|
||||
)
|
||||
except Exception as e:
|
||||
self.handle_failed_pool_response(
|
||||
pool_config.p2_singleton_puzzle_hash, f"Exception in GET /farmer {pool_config.pool_url}, {e}"
|
||||
)
|
||||
return None
|
||||
return response
|
||||
|
||||
async def _pool_post_farmer(
|
||||
self, pool_config: PoolingShareState, authentication_token_timeout: uint8, owner_sk: PrivateKey
|
||||
) -> dict[str, Any] | None:
|
||||
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)
|
||||
assert auth_sk is not None
|
||||
post_farmer_payload: PostFarmerPayload = PostFarmerPayload(
|
||||
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,
|
||||
get_current_authentication_token(authentication_token_timeout),
|
||||
uint64(authentication_token) if pool_config.version == 1 else uint64(0), # type: ignore[arg-type]
|
||||
auth_sk.get_g1(),
|
||||
pool_config.payout_instructions,
|
||||
None,
|
||||
)
|
||||
assert owner_sk.get_g1() == pool_config.owner_public_key
|
||||
signature: G2Element = AugSchemeMPL.sign(owner_sk, post_farmer_payload.get_hash())
|
||||
post_farmer_request = PostFarmerRequest(post_farmer_payload, signature)
|
||||
self.log.debug(f"POST /farmer request {post_farmer_request}")
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(
|
||||
f"{pool_config.pool_url}/farmer",
|
||||
json=post_farmer_request.to_json_dict(),
|
||||
ssl=ssl_context_for_root(get_mozilla_ca_crt(), log=self.log),
|
||||
) as resp:
|
||||
if resp.ok:
|
||||
response: dict[str, Any] = json.loads(await resp.text())
|
||||
log_level = logging.INFO
|
||||
if "error_code" in response:
|
||||
log_level = logging.WARNING
|
||||
increment_pool_stats(
|
||||
self.pool_state,
|
||||
pool_config.p2_singleton_puzzle_hash,
|
||||
"pool_errors",
|
||||
time.time(),
|
||||
value=response,
|
||||
)
|
||||
self.log.log(log_level, f"POST /farmer response: {response}")
|
||||
return response
|
||||
else:
|
||||
self.handle_failed_pool_response(
|
||||
pool_config.p2_singleton_puzzle_hash,
|
||||
f"Error in POST /farmer {pool_config.pool_url}, {resp.status}",
|
||||
)
|
||||
except Exception as e:
|
||||
self.handle_failed_pool_response(
|
||||
pool_config.p2_singleton_puzzle_hash, f"Exception in POST /farmer {pool_config.pool_url}, {e}"
|
||||
)
|
||||
return None
|
||||
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, owner_sk: PrivateKey
|
||||
) -> None:
|
||||
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)
|
||||
assert auth_sk is not None
|
||||
put_farmer_payload: PutFarmerPayload = PutFarmerPayload(
|
||||
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,
|
||||
get_current_authentication_token(authentication_token_timeout),
|
||||
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,
|
||||
)
|
||||
assert owner_sk.get_g1() == pool_config.owner_public_key
|
||||
signature: G2Element = AugSchemeMPL.sign(owner_sk, put_farmer_payload.get_hash())
|
||||
put_farmer_request = PutFarmerRequest(put_farmer_payload, signature)
|
||||
self.log.debug(f"PUT /farmer request {put_farmer_request}")
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.put(
|
||||
f"{pool_config.pool_url}/farmer",
|
||||
json=put_farmer_request.to_json_dict(),
|
||||
ssl=ssl_context_for_root(get_mozilla_ca_crt(), log=self.log),
|
||||
) as resp:
|
||||
if resp.ok:
|
||||
response: dict[str, Any] = json.loads(await resp.text())
|
||||
log_level = logging.INFO
|
||||
if "error_code" in response:
|
||||
log_level = logging.WARNING
|
||||
increment_pool_stats(
|
||||
self.pool_state,
|
||||
pool_config.p2_singleton_puzzle_hash,
|
||||
"pool_errors",
|
||||
time.time(),
|
||||
value=response,
|
||||
)
|
||||
self.log.log(log_level, f"PUT /farmer response: {response}")
|
||||
else:
|
||||
self.handle_failed_pool_response(
|
||||
pool_config.p2_singleton_puzzle_hash,
|
||||
f"Error in PUT /farmer {pool_config.pool_url}, {resp.status}",
|
||||
)
|
||||
except Exception as e:
|
||||
self.handle_failed_pool_response(
|
||||
pool_config.p2_singleton_puzzle_hash, f"Exception in PUT /farmer {pool_config.pool_url}, {e}"
|
||||
)
|
||||
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.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
|
||||
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")
|
||||
@@ -556,11 +646,6 @@ class Farmer:
|
||||
continue
|
||||
|
||||
try:
|
||||
authentication_sk: PrivateKey | None = self.get_authentication_sk(pool_config)
|
||||
if authentication_sk is None:
|
||||
self.log.error(f"Could not find authentication sk for {p2_singleton_puzzle_hash}")
|
||||
continue
|
||||
|
||||
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(),
|
||||
@@ -621,49 +706,42 @@ class Farmer:
|
||||
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[GetFarmerResponse | None, PoolErrorCode | None]:
|
||||
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, authentication_sk
|
||||
)
|
||||
farmer_response: GetFarmerResponse | None = None
|
||||
error_code_response: PoolErrorCode | None = None
|
||||
response = await self._pool_get_farmer(pool_config, authentication_token_timeout)
|
||||
if response is not None:
|
||||
if "error_code" not in response:
|
||||
farmer_response = GetFarmerResponse.from_json_dict(response)
|
||||
if farmer_response is not None:
|
||||
pool_state["current_difficulty"] = farmer_response.current_difficulty
|
||||
pool_state["current_points"] = farmer_response.current_points
|
||||
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_response = PoolErrorCode(response["error_code"])
|
||||
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 farmer_response, error_code_response
|
||||
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 == PoolErrorCode.FARMER_NOT_KNOWN:
|
||||
# Make the farmer known on the pool with a POST /farmer
|
||||
owner_sk_and_index = find_owner_sk(self.all_root_sks, pool_config.owner_public_key)
|
||||
assert owner_sk_and_index is not None
|
||||
post_response = await self._pool_post_farmer(
|
||||
pool_config, authentication_token_timeout, owner_sk_and_index[0]
|
||||
)
|
||||
if post_response is not None and "error_code" not in post_response:
|
||||
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']}"
|
||||
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()
|
||||
(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.")
|
||||
|
||||
@@ -673,12 +751,11 @@ class Farmer:
|
||||
farmer_info is not None
|
||||
and pool_config.payout_instructions.lower() != farmer_info.payout_instructions.lower()
|
||||
)
|
||||
if payout_instructions_update_required or error_code == PoolErrorCode.INVALID_SIGNATURE:
|
||||
owner_sk_and_index = find_owner_sk(self.all_root_sks, pool_config.owner_public_key)
|
||||
assert owner_sk_and_index is not None
|
||||
await self._pool_put_farmer(
|
||||
pool_config, authentication_token_timeout, owner_sk_and_index[0]
|
||||
)
|
||||
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 "
|
||||
@@ -758,8 +835,10 @@ class Farmer:
|
||||
|
||||
authentication_sk: PrivateKey | None = self.get_authentication_sk(pool_config)
|
||||
if authentication_sk is None:
|
||||
self.log.error(f"Could not find authentication sk for {pool_config.p2_singleton_puzzle_hash}")
|
||||
continue
|
||||
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(
|
||||
@@ -768,16 +847,19 @@ class Farmer:
|
||||
)
|
||||
return None
|
||||
|
||||
authentication_token = get_current_authentication_token(authentication_token_timeout)
|
||||
message: bytes32 = std_hash(
|
||||
AuthenticationPayload(
|
||||
"get_login", pool_config.launcher_id, pool_config.target_puzzle_hash, authentication_token
|
||||
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 (
|
||||
pool_config.pool_url
|
||||
+ f"/login?launcher_id={launcher_id.hex()}&authentication_token={authentication_token}"
|
||||
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()}"
|
||||
)
|
||||
|
||||
|
||||
@@ -353,7 +353,25 @@ class FarmerAPI:
|
||||
|
||||
agg_sig: G2Element = AugSchemeMPL.aggregate([plot_signature, authentication_signature])
|
||||
|
||||
post_partial_request: PostPartialRequest = PostPartialRequest(payload, agg_sig)
|
||||
current_auth_token = await self.farmer._get_current_authentication_token(
|
||||
pool_state_dict["pool_config"], authentication_token_timeout
|
||||
)
|
||||
if not isinstance(current_auth_token, str):
|
||||
self.farmer.log.error(
|
||||
f"Not logged into pool while trying to POST partial: {pool_state_dict['pool_config'].pool_url}"
|
||||
)
|
||||
increment_pool_stats(
|
||||
self.farmer.pool_state,
|
||||
p2_singleton_puzzle_hash,
|
||||
"missing_partials",
|
||||
time.time(),
|
||||
)
|
||||
self.farmer.state_changed(
|
||||
"failed_partial",
|
||||
{"p2_singleton_puzzle_hash": p2_singleton_puzzle_hash.hex()},
|
||||
)
|
||||
return
|
||||
post_partial_request: PostPartialRequest = PostPartialRequest(payload, current_auth_token, agg_sig)
|
||||
self.farmer.log.info(
|
||||
f"Submitting partial for {post_partial_request.payload.launcher_id.hex()} to {pool_url}"
|
||||
)
|
||||
@@ -369,7 +387,7 @@ class FarmerAPI:
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(
|
||||
f"{pool_url}/partial",
|
||||
self.farmer._url_for_endpoint(pool_state_dict["pool_config"], "partial"),
|
||||
json=post_partial_request.to_json_dict(),
|
||||
ssl=ssl_context_for_root(get_mozilla_ca_crt(), log=self.farmer.log),
|
||||
headers={
|
||||
|
||||
@@ -4,7 +4,7 @@ import time
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
from chia_rs import G1Element, G2Element, ProofOfSpace
|
||||
from chia_rs import G1Element, G2Element, Program, ProofOfSpace
|
||||
from chia_rs.sized_bytes import bytes32
|
||||
from chia_rs.sized_ints import uint8, uint16, uint32, uint64
|
||||
|
||||
@@ -35,13 +35,31 @@ class PoolErrorCode(Enum):
|
||||
# Used to verify GET /farmer and GET /login
|
||||
@streamable
|
||||
@dataclass(frozen=True)
|
||||
class AuthenticationPayload(Streamable):
|
||||
class AuthenticationPayloadV1(Streamable):
|
||||
method_name: str
|
||||
launcher_id: bytes32
|
||||
target_puzzle_hash: bytes32
|
||||
authentication_token: uint64
|
||||
|
||||
|
||||
# GET /auth (only v2)
|
||||
|
||||
|
||||
@streamable
|
||||
@dataclass(frozen=True)
|
||||
class GetAuthRequest(Streamable):
|
||||
launcher_id: bytes32
|
||||
timestamp: uint64
|
||||
signature: G2Element
|
||||
|
||||
|
||||
@streamable
|
||||
@dataclass(frozen=True)
|
||||
class GetAuthResponse(Streamable):
|
||||
authentication_token: str
|
||||
expiration: uint64
|
||||
|
||||
|
||||
# GET /pool_info
|
||||
@streamable
|
||||
@dataclass(frozen=True)
|
||||
@@ -55,6 +73,7 @@ class GetPoolInfoResponse(Streamable):
|
||||
description: str
|
||||
target_puzzle_hash: bytes32
|
||||
authentication_token_timeout: uint8
|
||||
pool_memoization: Program = Program.to(None) # addition from v1
|
||||
|
||||
|
||||
# POST /partial
|
||||
@@ -75,6 +94,7 @@ class PostPartialPayload(Streamable):
|
||||
@dataclass(frozen=True)
|
||||
class PostPartialRequest(Streamable):
|
||||
payload: PostPartialPayload
|
||||
authentication_token_v2: str
|
||||
aggregate_signature: G2Element
|
||||
|
||||
|
||||
@@ -86,6 +106,18 @@ class PostPartialResponse(Streamable):
|
||||
|
||||
|
||||
# GET /farmer
|
||||
@streamable
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class GetFarmerRequestV2(Streamable):
|
||||
authentication_token: uint64
|
||||
launcher_id: bytes32
|
||||
authentication_token_v2: str
|
||||
|
||||
|
||||
@streamable
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class GetFarmerRequestV1(GetFarmerRequestV2):
|
||||
signature: G2Element | None = None
|
||||
|
||||
|
||||
# Response in success case
|
||||
@@ -136,13 +168,14 @@ class PutFarmerPayload(Streamable):
|
||||
authentication_public_key: G1Element | None
|
||||
payout_instructions: str | None
|
||||
suggested_difficulty: uint64 | None
|
||||
authentication_token_v2: str
|
||||
|
||||
|
||||
@streamable
|
||||
@dataclass(frozen=True)
|
||||
class PutFarmerRequest(Streamable):
|
||||
payload: PutFarmerPayload
|
||||
signature: G2Element
|
||||
signature: G2Element | None
|
||||
|
||||
|
||||
# Response in success case
|
||||
|
||||
@@ -93,6 +93,10 @@ def master_sk_to_pooling_authentication_sk(master: PrivateKey, pool_wallet_index
|
||||
return _derive_path(master, [12381, 8444, 6, pool_wallet_index * 10000 + index])
|
||||
|
||||
|
||||
def singleton_owner_sk_to_authv2_key(singleton_owner_sk: PrivateKey) -> PrivateKey:
|
||||
return _derive_path_unhardened(singleton_owner_sk, [12381])
|
||||
|
||||
|
||||
def find_owner_sk(all_sks: list[PrivateKey], owner_pk: G1Element) -> tuple[PrivateKey, uint32] | None:
|
||||
for pool_wallet_index in range(MAX_POOL_WALLETS):
|
||||
for sk in all_sks:
|
||||
|
||||
@@ -997,7 +997,7 @@ class WalletStateManager:
|
||||
else:
|
||||
matched_plotnft_wallet_id = None
|
||||
if matched_plotnft_wallet_id is None and coin_spend.coin.parent_coin_info == next_plot_nft.launcher_id:
|
||||
matched_plotnft_wallet_id = uint32(len(self.wallets) + 1)
|
||||
matched_plotnft_wallet_id = uint32(max(self.wallets.keys()) + 1)
|
||||
self.wallets[matched_plotnft_wallet_id] = await PlotNFT2Wallet.create(
|
||||
wallet_state_manager=self,
|
||||
xch_wallet=self.main_wallet,
|
||||
@@ -1030,11 +1030,10 @@ class WalletStateManager:
|
||||
tx_config_loader = tx_config_loader.override(
|
||||
min_coin_amount=self.config.get("auto_claim", {}).get("min_amount"),
|
||||
)
|
||||
assert self.wallet_node.logged_in_fingerprint is not None
|
||||
return tx_config_loader.autofill(
|
||||
constants=self.constants,
|
||||
config=self.config,
|
||||
logged_in_fingerprint=self.wallet_node.logged_in_fingerprint,
|
||||
logged_in_fingerprint=self.root_pubkey.get_fingerprint(),
|
||||
)
|
||||
|
||||
async def auto_claim_coins(self) -> None:
|
||||
|
||||
Reference in New Issue
Block a user