Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions app/src/infinite_hashes/aps_miner/callbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,38 @@
Future: integrate with ASIC routing logic.
"""

import os

import structlog

from .brainsproxy import update_routing_weights
from .models import AuctionResult, BidResult
from .proxy_routing import pools_config_path, update_subnet_target_hashrate

logger = structlog.get_logger(__name__)

PROXY_MODE_ENV = "APS_MINER_PROXY_MODE"
PROXY_MODE_IHP = "ihp"
PROXY_MODE_BRAIINS = "braiins"


def _resolve_proxy_mode() -> str:
raw_mode = os.environ.get(PROXY_MODE_ENV, "").strip().lower()
if raw_mode == PROXY_MODE_IHP:
return PROXY_MODE_IHP
if raw_mode == PROXY_MODE_BRAIINS:
return PROXY_MODE_BRAIINS

configured_pools_path = os.environ.get("APS_MINER_POOLS_CONFIG_PATH", "").strip()
if configured_pools_path:
try:
if pools_config_path().exists():
return PROXY_MODE_IHP
except OSError:
logger.warning("Unable to inspect APS_MINER_POOLS_CONFIG_PATH; falling back to braiins mode")

return PROXY_MODE_BRAIINS


def handle_auction_results(result: AuctionResult) -> None:
"""
Expand Down Expand Up @@ -46,10 +71,14 @@ def handle_auction_results(result: AuctionResult) -> None:
if lost_bids:
_handle_lost_bids(lost_bids, result)

proxy_mode = _resolve_proxy_mode()
try:
update_routing_weights(won_bids, lost_bids)
if proxy_mode == PROXY_MODE_IHP:
update_subnet_target_hashrate(won_bids, lost_bids)
else:
update_routing_weights(won_bids, lost_bids)
except Exception: # noqa: BLE001
logger.exception("Failed to update Braiins routing weights")
logger.exception("Failed to update proxy routing", proxy_mode=proxy_mode)


def _handle_won_bids(won_bids: list[BidResult], result: AuctionResult) -> None:
Expand Down
140 changes: 140 additions & 0 deletions app/src/infinite_hashes/aps_miner/proxy_routing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
"""
Integration helpers for InfiniteHash Proxy pool routing.

This module updates subnet pool target hashrate in pools.toml so winning bids
receive an absolute allocation while private pools can consume the remainder.
"""

from __future__ import annotations

import os
from collections.abc import Iterable
from pathlib import Path

import structlog
import tomlkit

from .models import BidResult

logger = structlog.get_logger(__name__)

DEFAULT_POOLS_CONFIG_PATH = "/root/src/proxy/pools.toml"
DEFAULT_SUBNET_POOL_NAME = "central-proxy"
DEFAULT_RELOAD_SENTINEL_PATH = "/root/src/proxy/.reload-ihp"


def pools_config_path() -> Path:
configured_path = os.environ.get("APS_MINER_POOLS_CONFIG_PATH", DEFAULT_POOLS_CONFIG_PATH)
return Path(configured_path)


def reload_sentinel_path() -> Path:
configured_path = os.environ.get("APS_MINER_IHP_RELOAD_SENTINEL", DEFAULT_RELOAD_SENTINEL_PATH)
return Path(configured_path)


def _sum_hashrates_ph(bids: Iterable[BidResult]) -> float:
total = 0.0
for bid in bids:
try:
total += float(bid.hashrate)
except (TypeError, ValueError):
logger.warning("Failed to parse bid hashrate for proxy routing", hashrate=bid.hashrate)
return total


def _format_target_hashrate_from_ph(total_ph: float) -> str:
target_th = max(total_ph, 0.0) * 1000.0
target_str = f"{target_th:.3f}".rstrip("0").rstrip(".")
if not target_str:
target_str = "0"
return f"{target_str}TH/s"


def _is_subnet_pool(pool: dict, subnet_pool_name: str) -> bool:
pool_name = str(pool.get("name", "")).strip().lower()
return pool_name == subnet_pool_name


def update_subnet_target_hashrate(won_bids: Iterable[BidResult], _lost_bids: Iterable[BidResult]) -> None:
"""
Update the subnet pool target hashrate in pools.toml.

Won hashrate is assigned to subnet pools as absolute `target_hashrate`.
Remaining hashrate continues to flow to private pools through their weights.
"""
config_path = pools_config_path()
if not config_path.exists():
logger.info("InfiniteHash proxy pools config not present - skipping update", path=str(config_path))
return

subnet_pool_name = os.environ.get("APS_MINER_SUBNET_POOL_NAME", DEFAULT_SUBNET_POOL_NAME).strip().lower()
target_hashrate = _format_target_hashrate_from_ph(_sum_hashrates_ph(won_bids))

try:
with config_path.open("r", encoding="utf-8") as f:
doc = tomlkit.load(f)
except Exception:
logger.exception("Unable to load InfiniteHash proxy pools config", path=str(config_path))
return

pools_section = doc.get("pools")
if not isinstance(pools_section, dict):
logger.warning("Missing [pools] section in proxy config", path=str(config_path))
return

main_pools = pools_section.get("main")
if not isinstance(main_pools, list):
logger.warning("Missing [[pools.main]] entries in proxy config", path=str(config_path))
return

updated = False
matched = 0
for pool in main_pools:
if not isinstance(pool, dict):
continue
if not _is_subnet_pool(pool, subnet_pool_name):
continue

matched += 1
old_target_hashrate = pool.get("target_hashrate")
if old_target_hashrate != target_hashrate:
pool["target_hashrate"] = target_hashrate
updated = True

if "weight" in pool:
del pool["weight"]
updated = True

if matched == 0:
logger.warning(
"No subnet pool entries matched for target hashrate update",
name=subnet_pool_name,
path=str(config_path),
)
return

if not updated:
logger.debug("Subnet target hashrate already up to date", target_hashrate=target_hashrate)
return

try:
with config_path.open("w", encoding="utf-8") as f:
tomlkit.dump(doc, f)
except Exception:
logger.exception("Failed to persist proxy pools config updates", path=str(config_path))
return

sentinel_path = reload_sentinel_path()
try:
sentinel_path.parent.mkdir(parents=True, exist_ok=True)
sentinel_path.touch()
except Exception:
logger.exception("Failed to signal IHP proxy reload", sentinel=str(sentinel_path))

logger.info(
"Updated subnet target hashrate",
name=subnet_pool_name,
target_hashrate=target_hashrate,
matched_pools=matched,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from __future__ import annotations

from datetime import UTC, datetime

from infinite_hashes.aps_miner import callbacks
from infinite_hashes.aps_miner.models import AuctionResult, BidResult


def _build_result() -> AuctionResult:
now = datetime.now(tz=UTC)
return AuctionResult(
epoch_start=1,
start_block=10,
end_block=20,
window_start_time=now,
window_end_time=now,
commitments_count=2,
all_winners=[],
my_bids=[
BidResult(hashrate="0.1", price_fp18=1, won=True),
BidResult(hashrate="0.2", price_fp18=1, won=False),
],
)


def test_handle_auction_results_uses_ihp_proxy_when_configured(monkeypatch) -> None:
calls = {"ihp": 0, "braiins": 0}

def _ihp_handler(*_args, **_kwargs) -> None:
calls["ihp"] += 1

def _braiins_handler(*_args, **_kwargs) -> None:
calls["braiins"] += 1

monkeypatch.setenv("APS_MINER_PROXY_MODE", "ihp")
monkeypatch.setattr(callbacks, "update_subnet_target_hashrate", _ihp_handler)
monkeypatch.setattr(callbacks, "update_routing_weights", _braiins_handler)

callbacks.handle_auction_results(_build_result())

assert calls["ihp"] == 1
assert calls["braiins"] == 0


def test_handle_auction_results_auto_detects_ihp_proxy_from_pools_file(monkeypatch, tmp_path) -> None:
calls = {"ihp": 0, "braiins": 0}

def _ihp_handler(*_args, **_kwargs) -> None:
calls["ihp"] += 1

def _braiins_handler(*_args, **_kwargs) -> None:
calls["braiins"] += 1

pools_file = tmp_path / "pools.toml"
pools_file.write_text("[pools]\n", encoding="utf-8")

monkeypatch.delenv("APS_MINER_PROXY_MODE", raising=False)
monkeypatch.setenv("APS_MINER_POOLS_CONFIG_PATH", str(pools_file))
monkeypatch.setattr(callbacks, "update_subnet_target_hashrate", _ihp_handler)
monkeypatch.setattr(callbacks, "update_routing_weights", _braiins_handler)

callbacks.handle_auction_results(_build_result())

assert calls["ihp"] == 1
assert calls["braiins"] == 0
84 changes: 84 additions & 0 deletions app/src/infinite_hashes/aps_miner/tests/test_proxy_routing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
from __future__ import annotations

import tomli

from infinite_hashes.aps_miner.models import BidResult
from infinite_hashes.aps_miner.proxy_routing import update_subnet_target_hashrate


def test_update_subnet_target_hashrate_sets_absolute_target_and_keeps_private_weights(monkeypatch, tmp_path) -> None:
pools_file = tmp_path / "pools.toml"
sentinel_file = tmp_path / ".reload-ihp"
pools_file.write_text(
"""
[pools]
backup = { name = "backup", host = "backup.pool", port = 3333 }

[[pools.main]]
name = "subnet"
host = "stratum.infinitehash.xyz"
port = 9332
weight = 1

[[pools.main]]
name = "private"
host = "btc.global.luxor.tech"
port = 700
weight = 7
""",
encoding="utf-8",
)

monkeypatch.setenv("APS_MINER_POOLS_CONFIG_PATH", str(pools_file))
monkeypatch.setenv("APS_MINER_SUBNET_POOL_NAME", "subnet")
monkeypatch.setenv("APS_MINER_IHP_RELOAD_SENTINEL", str(sentinel_file))

won_bids = [
BidResult(hashrate="0.1", price_fp18=1, won=True),
BidResult(hashrate="0.2", price_fp18=1, won=True),
]
lost_bids = [BidResult(hashrate="0.3", price_fp18=1, won=False)]

update_subnet_target_hashrate(won_bids, lost_bids)

with pools_file.open("rb") as f:
data = tomli.load(f)

subnet_pool = data["pools"]["main"][0]
private_pool = data["pools"]["main"][1]

assert subnet_pool["target_hashrate"] == "300TH/s"
assert "weight" not in subnet_pool
assert private_pool["weight"] == 7
assert sentinel_file.exists()


def test_update_subnet_target_hashrate_sets_zero_when_no_wins(monkeypatch, tmp_path) -> None:
pools_file = tmp_path / "pools.toml"
sentinel_file = tmp_path / ".reload-ihp"
pools_file.write_text(
"""
[pools]
backup = { name = "backup", host = "backup.pool", port = 3333 }

[[pools.main]]
name = "subnet"
host = "stratum.infinitehash.xyz"
port = 9332
weight = 1
""",
encoding="utf-8",
)

monkeypatch.setenv("APS_MINER_POOLS_CONFIG_PATH", str(pools_file))
monkeypatch.setenv("APS_MINER_SUBNET_POOL_NAME", "subnet")
monkeypatch.setenv("APS_MINER_IHP_RELOAD_SENTINEL", str(sentinel_file))

update_subnet_target_hashrate([], [BidResult(hashrate="0.3", price_fp18=1, won=False)])

with pools_file.open("rb") as f:
data = tomli.load(f)

subnet_pool = data["pools"]["main"][0]
assert subnet_pool["target_hashrate"] == "0TH/s"
assert sentinel_file.exists()
Loading