Skip to content

Commit 2554dcf

Browse files
IHP manual test
1 parent aec206a commit 2554dcf

4 files changed

Lines changed: 118 additions & 105 deletions

File tree

app/src/infinite_hashes/aps_miner/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ print(result)
222222

223223
For a full end-to-end manual test with the blockchain simulator, run:
224224

225-
- `python manual_test_miner_compose.py` – builds the miner image (or reuses an override), launches the Docker Compose stack (miner plus Braiins proxy), and validates automatic Braiins profile updates.
225+
- `python manual_test_miner_compose.py` – builds the miner image (or reuses an override), launches the Docker Compose stack (miner plus IHP proxy stack), and validates automatic `pools.toml` target hashrate updates with proxy reload signaling.
226226

227227
## Logging
228228

app/src/manual_test_miner_compose.py

Lines changed: 113 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
#!/usr/bin/env python3
22
"""
3-
Manual integration test: APS miner + Braiins Farm Proxy via Docker Compose.
3+
Manual integration test: APS miner + IHP proxy stack via Docker Compose.
44
55
This script spins up the simulated blockchain, provisions a temporary working
66
directory with Docker Compose assets, and runs the full miner stack (miner
7-
container + farm proxy) to verify that the Braiins profile is updated and
8-
reloaded automatically when auction outcomes change.
7+
container + IHP proxy + reloader sidecar) to verify that target hashrate
8+
routing is updated and reloaded automatically when auction outcomes change.
99
1010
Prerequisites:
1111
- Docker with Compose plugin (`docker compose`) or standalone `docker-compose`
12-
- Ability to pull the miner image referenced in envs/miner/docker-compose.yml
12+
- Ability to pull the miner image referenced in envs/miner-ihp/docker-compose.yml
1313
1414
Usage:
1515
python manual_test_miner_compose.py
@@ -30,6 +30,7 @@
3030
import tempfile
3131
import time
3232
from collections.abc import Sequence
33+
from functools import lru_cache
3334
from pathlib import Path
3435

3536
import bittensor_wallet
@@ -45,7 +46,11 @@
4546
)
4647
from infinite_hashes.testutils.simulator.state import MECHANISM_SPLIT_VALUE_MAX
4748

48-
COMPOSE_SOURCE = Path(__file__).resolve().parents[2] / "envs" / "miner" / "docker-compose.yml"
49+
COMPOSE_SOURCE = Path(__file__).resolve().parents[2] / "envs" / "miner-ihp" / "docker-compose.yml"
50+
MINER_INSTALLER_SOURCE = Path(__file__).resolve().parents[2] / "installer" / "miner_install.sh"
51+
52+
DEFAULT_BACKUP_POOL_HOST = "btc.global.luxor.tech"
53+
DEFAULT_BACKUP_POOL_PORT = 700
4954

5055
LOGGER = structlog.get_logger(__name__)
5156

@@ -283,59 +288,63 @@ def _replace(match: re.Match[str]) -> str:
283288
return compose_target
284289

285290

286-
def create_braiins_profile(config_dir: Path) -> Path:
287-
"""Write the default Braiins profile used for the integration test."""
288-
config_dir.mkdir(parents=True, exist_ok=True)
289-
profile_path = config_dir / "active_profile.toml"
290-
if profile_path.exists():
291-
return profile_path
292-
profile_path.write_text(
293-
"""[[server]]
294-
name = "InfiniteHash"
295-
port = 3333
296-
297-
[[target]]
298-
name = "InfiniteHashLuxorTarget"
299-
url = "stratum+tcp://btc.global.luxor.tech:700"
300-
user_identity = "InfiniteHashLuxor"
301-
identity_pass_through = true
302-
303-
[[target]]
304-
name = "MinerDefaultTarget"
305-
url = "stratum+tcp://btc.global.luxor.tech:700"
306-
user_identity = "MinerDefault"
307-
identity_pass_through = true
308-
309-
[[target]]
310-
name = "MinerBackupTarget"
311-
url = "stratum+tcp://btc.viabtc.io:3333"
312-
user_identity = "MinerBackup"
313-
identity_pass_through = true
314-
315-
[[routing]]
316-
name = "RD"
317-
from = ["InfiniteHash"]
318-
319-
[[routing.goal]]
320-
name = "InfiniteHashLuxorGoal"
321-
hr_weight = 9
322-
323-
[[routing.goal.level]]
324-
targets = ["InfiniteHashLuxorTarget"]
325-
326-
[[routing.goal]]
327-
name = "MinerDefaultGoal"
328-
hr_weight = 10
329-
330-
[[routing.goal.level]]
331-
targets = ["MinerDefaultTarget"]
332-
333-
[[routing.goal.level]]
334-
targets = ["MinerBackupTarget"]
335-
""",
336-
encoding="utf-8",
291+
@lru_cache(maxsize=1)
292+
def _installer_script_text() -> str:
293+
if not MINER_INSTALLER_SOURCE.exists():
294+
raise RuntimeError(f"Installer script not found: {MINER_INSTALLER_SOURCE}")
295+
return MINER_INSTALLER_SOURCE.read_text(encoding="utf-8")
296+
297+
298+
def _extract_single_heredoc_from_function(function_name: str) -> str:
299+
script = _installer_script_text()
300+
function_match = re.search(
301+
rf"(?ms)^{re.escape(function_name)}\(\)\s*\{{\n(?P<body>.*?)^\}}",
302+
script,
303+
)
304+
if function_match is None:
305+
raise RuntimeError(f"Function {function_name} not found in installer script")
306+
307+
body = function_match.group("body")
308+
heredoc_matches = list(
309+
re.finditer(
310+
r"(?ms)<<'?(?P<tag>[A-Z_]+)'?\n(?P<content>.*?)\n\s*(?P=tag)",
311+
body,
312+
)
313+
)
314+
if len(heredoc_matches) != 1:
315+
raise RuntimeError(
316+
f"Expected exactly one heredoc in installer function {function_name}, got {len(heredoc_matches)}"
317+
)
318+
return heredoc_matches[0].group("content").strip("\n") + "\n"
319+
320+
321+
def create_ihp_proxy_env(proxy_dir: Path) -> Path:
322+
"""Write default IHP .env file used by the integration test."""
323+
proxy_dir.mkdir(parents=True, exist_ok=True)
324+
env_path = proxy_dir / ".env"
325+
if env_path.exists():
326+
return env_path
327+
env_template = _extract_single_heredoc_from_function("write_default_ihp_env")
328+
env_path.write_text(env_template, encoding="utf-8")
329+
return env_path
330+
331+
332+
def create_ihp_pools_config(
333+
proxy_dir: Path,
334+
backup_pool_host: str = DEFAULT_BACKUP_POOL_HOST,
335+
backup_pool_port: int = DEFAULT_BACKUP_POOL_PORT,
336+
) -> Path:
337+
"""Write default IHP pools.toml used by the integration test."""
338+
proxy_dir.mkdir(parents=True, exist_ok=True)
339+
pools_path = proxy_dir / "pools.toml"
340+
if pools_path.exists():
341+
return pools_path
342+
pools_template = _extract_single_heredoc_from_function("write_default_ihp_pools")
343+
pools_content = pools_template.replace("${backup_pool_host}", backup_pool_host).replace(
344+
"${backup_pool_port}", str(backup_pool_port)
337345
)
338-
return profile_path
346+
pools_path.write_text(pools_content, encoding="utf-8")
347+
return pools_path
339348

340349

341350
def create_miner_config(config_path: Path) -> None:
@@ -364,36 +373,38 @@ def create_miner_config(config_path: Path) -> None:
364373
config_path.write_text(content, encoding="utf-8")
365374

366375

367-
def load_goal_weights(profile_path: Path) -> dict[str, int]:
368-
"""Return {goal_name: hr_weight} from Braiins profile."""
369-
doc = tomlkit.parse(profile_path.read_text(encoding="utf-8"))
370-
routing = doc.get("routing") or []
371-
weights: dict[str, int] = {}
372-
if not isinstance(routing, list):
373-
return weights
374-
for route in routing:
375-
goals = route.get("goal")
376-
if not isinstance(goals, list):
376+
def load_subnet_target_hashrate(pools_path: Path, pool_name: str = "central-proxy") -> str | None:
377+
"""Return current target_hashrate for subnet pool name from pools.toml."""
378+
doc = tomlkit.parse(pools_path.read_text(encoding="utf-8"))
379+
pools = doc.get("pools")
380+
if not isinstance(pools, dict):
381+
return None
382+
main_pools = pools.get("main")
383+
if not isinstance(main_pools, list):
384+
return None
385+
for pool in main_pools:
386+
if not isinstance(pool, dict):
387+
continue
388+
if str(pool.get("name", "")).strip().lower() != pool_name.lower():
377389
continue
378-
for goal in goals:
379-
if isinstance(goal, dict) and "name" in goal:
380-
weight = goal.get("hr_weight")
381-
if isinstance(weight, int):
382-
weights[str(goal["name"])] = weight
383-
return weights
390+
target_hashrate = pool.get("target_hashrate")
391+
if isinstance(target_hashrate, str):
392+
return target_hashrate
393+
return None
394+
return None
384395

385396

386397
async def run_simulation(
387398
client: httpx.AsyncClient,
388399
validator_wallet,
389400
compose_cmd: list[str],
390401
workdir: Path,
391-
profile_path: Path,
402+
pools_path: Path,
392403
mechanism_share: float,
393404
miner_config_path: Path,
394405
) -> None:
395-
"""Advance the simulated chain and monitor Braiins profile updates."""
396-
services_to_tail = ["miner", "farm-proxy-configurator"]
406+
"""Advance the simulated chain and monitor IHP target hashrate updates."""
407+
services_to_tail = ["miner", "ihp-proxy", "ihp-api", "ihp-proxy-reloader"]
397408
log_processes: dict[str, subprocess.Popen[str]] = {}
398409
log_tasks: list[asyncio.Task[None]] = []
399410
for service in services_to_tail:
@@ -417,8 +428,8 @@ async def run_simulation(
417428
last_window = current_block // BLOCKS_PER_WINDOW
418429
LOGGER.info("Simulation starting", start_block=current_block)
419430

420-
baseline_weights = load_goal_weights(profile_path)
421-
LOGGER.info("Initial Braiins weights", weights=baseline_weights)
431+
baseline_target = load_subnet_target_hashrate(pools_path)
432+
LOGGER.info("Initial IHP subnet target hashrate", target_hashrate=baseline_target)
422433

423434
commitment_scheduled = False
424435
hashrate_update_done = False
@@ -467,14 +478,14 @@ async def run_simulation(
467478
except Exception: # noqa: BLE001
468479
LOGGER.exception("Failed to read commitments")
469480

470-
weights = load_goal_weights(profile_path)
471-
if weights != baseline_weights:
472-
LOGGER.info("Braiins weights updated", weights=weights)
473-
baseline_weights = dict(weights)
481+
target_hashrate = load_subnet_target_hashrate(pools_path)
482+
if target_hashrate != baseline_target:
483+
LOGGER.info("IHP subnet target hashrate updated", target_hashrate=target_hashrate)
484+
baseline_target = target_hashrate
474485

475-
sentinel = profile_path.parent / ".reconfigure"
486+
sentinel = pools_path.parent / ".reload-ihp"
476487
if sentinel.exists():
477-
LOGGER.info("Configurator sentinel detected", path=str(sentinel))
488+
LOGGER.info("IHP reload sentinel detected", path=str(sentinel))
478489

479490
if current_window > last_window:
480491
last_window = current_window
@@ -523,15 +534,14 @@ async def main() -> int:
523534
workdir = Path(tempfile.mkdtemp(prefix="infhash_miner_compose_"))
524535
wallets_dir = workdir / "wallets"
525536
logs_dir = workdir / "logs"
526-
brains_dir = workdir / "brainsproxy" / "config"
527-
for path in (wallets_dir, logs_dir, brains_dir):
537+
proxy_dir = workdir / "proxy"
538+
for path in (wallets_dir, logs_dir, proxy_dir):
528539
path.mkdir(parents=True, exist_ok=True)
529540

530541
LOGGER.info("Working directory prepared", path=str(workdir))
531542

532543
compose_cmd = docker_compose_base_cmd()
533544
compose_file = copy_compose_file(workdir)
534-
profile_path = create_braiins_profile(brains_dir)
535545

536546
LOGGER.info("Compose file ready", compose_path=str(compose_file))
537547

@@ -545,6 +555,10 @@ async def main() -> int:
545555
path=str(wallets_dir),
546556
)
547557

558+
create_ihp_proxy_env(proxy_dir)
559+
pools_path = create_ihp_pools_config(proxy_dir)
560+
LOGGER.info("IHP proxy config written", pools_path=str(pools_path))
561+
548562
miner_config_path = workdir / "config.toml"
549563
create_miner_config(miner_config_path)
550564
LOGGER.info("Miner config written", path=str(miner_config_path))
@@ -585,12 +599,23 @@ async def main() -> int:
585599
validator_wallet,
586600
compose_cmd,
587601
workdir,
588-
profile_path,
602+
pools_path,
589603
mechanism_share,
590604
miner_config_path,
591605
)
592606
except KeyboardInterrupt:
593607
LOGGER.info("Received interrupt, shutting down...")
608+
except Exception: # noqa: BLE001
609+
LOGGER.exception("Manual IHP integration test failed")
610+
if stack_started:
611+
LOGGER.info("Collecting docker compose logs for diagnostics")
612+
with contextlib.suppress(Exception):
613+
subprocess.run(
614+
compose_cmd + ["logs", "--no-color", "--tail", "400"],
615+
cwd=workdir,
616+
check=False,
617+
)
618+
return 1
594619
finally:
595620
LOGGER.info("Cleaning up")
596621
if stack_started:

installer/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ In `braiins` mode, the installer provisions Braiins Farm Proxy (`farm-proxy` and
8282
- APS miner updates subnet allocation by pool `name` in `proxy/pools.toml` (`[[pools.main]]` entry), not by host/port.
8383
- The miner container uses `APS_MINER_SUBNET_POOL_NAME` (default: `central-proxy`) to select which pool gets absolute `target_hashrate`.
8484
- The selected pool name must exist in `proxy/pools.toml`; otherwise no subnet target update is applied.
85-
- During installation, if `proxy/pools.toml` does not exist yet, the script asks for backup/private pool host/port/worker ID and writes those values to `pools.backup`.
85+
- During installation, if `proxy/pools.toml` does not exist yet, the script asks for backup/private pool host/port and writes those values to `pools.backup`.
86+
- Installer default sets `[extranonce].extranonce2_size = 5` to stay compatible with pools that expose `extranonce2_size = 6` and require one extra bridge byte.
8687
- After updating `proxy/pools.toml`, APS miner touches reload sentinel `APS_MINER_IHP_RELOAD_SENTINEL` (default: `/root/src/proxy/.reload-ihp`); sidecar `ihp-proxy-reloader` then runs `kill -HUP 1` in `ihp-proxy` PID namespace.
8788

8889
`braiins` mode:

installer/miner_install.sh

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ write_default_ihp_pools() {
4242
local destination="$1"
4343
local backup_pool_host="$2"
4444
local backup_pool_port="$3"
45-
local backup_pool_worker_id="$4"
4645

4746
cat > "${destination}" <<EOL
4847
[pools]
@@ -51,23 +50,15 @@ write_default_ihp_pools() {
5150
name = "private-backup"
5251
host = "${backup_pool_host}"
5352
port = ${backup_pool_port}
54-
EOL
5553
56-
if [ -n "${backup_pool_worker_id}" ]; then
57-
cat >> "${destination}" <<EOL
58-
worker_id = "${backup_pool_worker_id}"
59-
EOL
60-
fi
61-
62-
cat >> "${destination}" <<'EOL'
6354
[[pools.main]]
6455
name = "central-proxy"
6556
host = "stratum.infinitehash.xyz"
6657
port = 9332
6758
weight = 1
6859
6960
[extranonce]
70-
extranonce2_size = 6
61+
extranonce2_size = 5
7162
7263
[routing]
7364
rebalance_interval_seconds = 10
@@ -251,11 +242,7 @@ if [ "${PROXY_MODE}" = "ihp" ]; then
251242
PRIVATE_POOL_PORT=${PRIVATE_POOL_PORT:-${DEFAULT_PRIVATE_POOL_PORT}}
252243
PRIVATE_POOL_PORT=$(echo "${PRIVATE_POOL_PORT}" | tr -d '[:space:]')
253244

254-
DEFAULT_PRIVATE_POOL_WORKER_ID="infinite.${HOTKEY_IDENTIFIER}.worker1"
255-
read -r -p "Enter IHP backup/private pool worker ID [${DEFAULT_PRIVATE_POOL_WORKER_ID}]: " PRIVATE_POOL_WORKER_ID </dev/tty
256-
PRIVATE_POOL_WORKER_ID=${PRIVATE_POOL_WORKER_ID:-${DEFAULT_PRIVATE_POOL_WORKER_ID}}
257-
258-
write_default_ihp_pools "${NEW_PROXY_POOLS_FILE}" "${PRIVATE_POOL_HOST}" "${PRIVATE_POOL_PORT}" "${PRIVATE_POOL_WORKER_ID}"
245+
write_default_ihp_pools "${NEW_PROXY_POOLS_FILE}" "${PRIVATE_POOL_HOST}" "${PRIVATE_POOL_PORT}"
259246
else
260247
echo "Using existing InfiniteHash Proxy pools file at ${NEW_PROXY_POOLS_FILE}"
261248
fi

0 commit comments

Comments
 (0)