diff --git a/deploy/docker-compose.validator.yml b/deploy/docker-compose.validator.yml index 0956ced..fac0dbb 100644 --- a/deploy/docker-compose.validator.yml +++ b/deploy/docker-compose.validator.yml @@ -32,8 +32,6 @@ services: command: - epoch - loop - - --arena - - "${MT_ARENA:?set the arena id}" - --home - /var/lib/microtensor logging: diff --git a/docs/validator_setup.md b/docs/validator_setup.md index 8fb862f..62382b0 100644 --- a/docs/validator_setup.md +++ b/docs/validator_setup.md @@ -81,7 +81,6 @@ cd microtensor-subnet/deploy cat > .env < MT_WALLET_HOTKEY= -MT_ARENA=sql_gen MT_GPUS=0,1,2 DOCKER_GID=$(getent group docker | cut -d: -f3) EOF @@ -89,7 +88,7 @@ docker compose -f docker-compose.validator.yml up -d docker compose -f docker-compose.validator.yml logs -f ``` -The loop polls the chain every ten minutes. When an arena's evaluation window opens it downloads the epoch bundle, opens it with your key, sets up the baselines, evaluates the submissions on the signed roster one at a time at their committed revisions, posts each miner's signed result as it finishes, sets weights, and posts the signed epoch report. An epoch already reported is not run again; the record is `/var/lib/microtensor/state/.epochs.json`. +You do not choose arenas or miners: every ten minutes the loop asks the server which arenas are live on your network, and the signed roster names the miners. When an arena's evaluation window opens it downloads the epoch bundle, opens it with your key, sets up the baselines, evaluates the submissions on the signed roster one at a time at their committed revisions, posts each miner's signed result as it finishes, sets weights, and posts the signed epoch report. An epoch already reported is not run again; the record is `/var/lib/microtensor/state/.epochs.json`. ## What one epoch does diff --git a/microtensor_subnet/cli/epoch.py b/microtensor_subnet/cli/epoch.py index 480ad77..9ecf96c 100644 --- a/microtensor_subnet/cli/epoch.py +++ b/microtensor_subnet/cli/epoch.py @@ -2,6 +2,7 @@ import argparse import asyncio +import dataclasses import json import logging import os @@ -103,7 +104,6 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> run.set_defaults(handler=_run) loop = inner.add_parser("loop", help="run every epoch of an arena as its window opens") - loop.add_argument("--arena", required=True) loop.add_argument("--server", default=os.environ.get("MT_SERVER_URL", PUBLIC_SERVER_URL)) loop.add_argument("--relay", default=os.environ.get("MT_RELAY_URL", "")) loop.add_argument( @@ -503,7 +503,7 @@ def _settings(args: argparse.Namespace, config: Any) -> EpochConfig: return EpochConfig( chain=config, home=args.home, - arena_id=args.arena, + arena_id=getattr(args, "arena", "") or "", server_url=args.server, relay_url=args.relay or args.server, gpus=tuple(g.strip() for g in str(args.gpus).split(",") if g.strip()), @@ -515,6 +515,44 @@ def _settings(args: argparse.Namespace, config: Any) -> EpochConfig: ) +def _poll_arena( + args: argparse.Namespace, + config: Any, + client: Any, + server: ServerClient, + wallet: Any, + state: Path, + arena_id: str, + block: int, +) -> None: + record = state / f"{arena_id}.epochs.json" + done: set[int] = set() + if record.is_file(): + done = {int(i) for i in json.loads(record.read_text(encoding="utf-8"))} + arena = server.arena(arena_id, block) + current = dict((arena or {}).get("current_epoch") or {}) + index = current.get("index") + if arena is None: + log.warning("arena %s is unknown to %s", arena_id, args.server) + return + if current.get("phase") != "window" or index is None or int(index) in done: + log.info("block %d: %s epoch %s is %s", block, arena_id, index, current.get("phase")) + return + log.info("epoch %s of %s is open at block %d", index, arena_id, block) + settings = dataclasses.replace(_settings(args, config), arena_id=arena_id) + outcome = run_epoch(settings, chain=client, wallet=wallet, epoch_index=int(index)) + done.add(int(index)) + record.write_text(json.dumps(sorted(done)), encoding="utf-8") + log.info( + "epoch %s of %s done: %d scored, %d refused, %d weights", + index, + arena_id, + len(outcome.scores), + len(outcome.failures), + len(outcome.weights), + ) + + def _loop(args: argparse.Namespace) -> int: logging.basicConfig(level=logging.INFO) config = chain_config(args) @@ -523,35 +561,14 @@ def _loop(args: argparse.Namespace) -> int: server = ServerClient(args.server, hotkey_address(wallet), wallet) state = Path(args.home) / "state" state.mkdir(parents=True, exist_ok=True) - record = state / f"{args.arena}.epochs.json" - done: set[int] = set() - if record.is_file(): - done = {int(i) for i in json.loads(record.read_text(encoding="utf-8"))} - settings = _settings(args, config) while True: try: block = client.block() - arena = server.arena(args.arena, block) - current = dict((arena or {}).get("current_epoch") or {}) - index = current.get("index") - if arena is None: - log.warning("arena %s is unknown to %s", args.arena, args.server) - elif current.get("phase") == "window" and index is not None and int(index) not in done: - log.info("epoch %s of %s is open at block %d", index, args.arena, block) - outcome = run_epoch(settings, chain=client, wallet=wallet, epoch_index=int(index)) - done.add(int(index)) - record.write_text(json.dumps(sorted(done)), encoding="utf-8") - log.info( - "epoch %s done: %d scored, %d refused, %d weights", - index, - len(outcome.scores), - len(outcome.failures), - len(outcome.weights), - ) - else: - log.info( - "block %d: %s epoch %s is %s", block, args.arena, index, current.get("phase") - ) + arena_ids = server.live_arenas(config.network) + if not arena_ids: + log.info("block %d: the server lists no live arena on %s", block, config.network) + for arena_id in arena_ids: + _poll_arena(args, config, client, server, wallet, state, arena_id, block) except (EpochError, ReleaseError, ChainError, ServerError) as exc: log.error("epoch loop: %s", exc) if args.once: diff --git a/microtensor_subnet/validator/client.py b/microtensor_subnet/validator/client.py index a691024..b6cfc58 100644 --- a/microtensor_subnet/validator/client.py +++ b/microtensor_subnet/validator/client.py @@ -16,6 +16,9 @@ BUNDLE_TIMEOUT_SECONDS: Final[float] = 600.0 +ARENA_NETWORKS: dict[str, str] = {"test": "testnet", "finney": "mainnet"} + + class ServerError(RuntimeError): pass @@ -64,6 +67,15 @@ def _request( ) return response.json() if response.content else None + def live_arenas(self, network: str) -> list[str]: + found = self._request("GET", "/v2/arenas") or {} + wanted = ARENA_NETWORKS.get(network, network) + return sorted( + str(a.get("id")) + for a in found.get("arenas") or [] + if a.get("state") == "live" and a.get("network") == wanted and a.get("id") + ) + def arena(self, arena_id: str, block: int | None = None) -> dict[str, Any] | None: query = f"?block={block}" if block is not None else "" found = self._request("GET", f"/v2/arenas/{arena_id}{query}")