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
55This script spins up the simulated blockchain, provisions a temporary working
66directory 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
1010Prerequisites:
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
1414Usage:
1515 python manual_test_miner_compose.py
3030import tempfile
3131import time
3232from collections .abc import Sequence
33+ from functools import lru_cache
3334from pathlib import Path
3435
3536import bittensor_wallet
4546)
4647from 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
5055LOGGER = 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
341350def 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
386397async 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 :
0 commit comments