From 330ee0a469a19c400b91319b2884a54980b27a80 Mon Sep 17 00:00:00 2001 From: Michal Zukowski Date: Tue, 27 Jan 2026 15:32:09 +0100 Subject: [PATCH] Enforce v2 commitments --- app/src/infinite_hashes/aps_miner/tasks.py | 48 +++++++++++++++++--- app/src/infinite_hashes/auctions/utils.py | 8 +++- app/src/infinite_hashes/consensus/bidding.py | 7 +++ app/src/tests/integration/conftest.py | 1 + 4 files changed, 55 insertions(+), 9 deletions(-) diff --git a/app/src/infinite_hashes/aps_miner/tasks.py b/app/src/infinite_hashes/aps_miner/tasks.py index 532cbfa..763d96c 100644 --- a/app/src/infinite_hashes/aps_miner/tasks.py +++ b/app/src/infinite_hashes/aps_miner/tasks.py @@ -5,6 +5,7 @@ """ import asyncio +import os from collections import Counter from typing import Any @@ -14,10 +15,12 @@ from infinite_hashes.auctions import utils as auction_utils from infinite_hashes.consensus.bidding import ( + MAX_BIDDING_COMMITMENT_WORKER_SIZE_FP18, + MIN_BIDDING_COMMITMENT_WORKER_SIZE_FP18, BiddingCommitment, select_auction_winners_async, ) -from infinite_hashes.consensus.price import _parse_decimal_to_fp18_int +from infinite_hashes.consensus.price import _fp18_to_min_decimal_str, _parse_decimal_to_fp18_int from .callbacks import handle_auction_results from .config import MinerConfig @@ -57,17 +60,48 @@ def _get_hotkey(config: MinerConfig) -> str | None: def _build_commitment_from_config(config: MinerConfig) -> tuple[str, list[Any]]: price_fp18 = _parse_decimal_to_fp18_int(config.workers.price_multiplier) - # Auto mode: - # - workers.hashrates list => v=1 - # - workers.worker_sizes dict (or hashrates dict normalized into worker_sizes) => v=2 + # Always use v2 commitments. + # - workers.worker_sizes dict (or hashrates dict normalized into worker_sizes) => v2 + # - workers.hashrates list => aggregate into v2 counts if config.workers.worker_sizes is not None: v2_bids = [("BTC", price_fp18, dict(config.workers.worker_sizes))] commit = BiddingCommitment(t="b", bids=v2_bids, v=2) return commit.to_compact(), v2_bids - bids = [(hr, price_fp18) for hr in (config.workers.hashrates or [])] - commit = BiddingCommitment(t="b", bids=bids, v=1) - return commit.to_compact(), bids + allow_v1 = os.getenv("APS_MINER_ALLOW_V1", "").strip().lower() in {"1", "true", "yes", "on"} + if allow_v1: + bids = [(hr, price_fp18) for hr in (config.workers.hashrates or [])] + commit = BiddingCommitment(t="b", bids=bids, v=1) + return commit.to_compact(), bids + + worker_sizes: dict[str, int] = {} + for hr in config.workers.hashrates or []: + hr_fp = _parse_decimal_to_fp18_int(str(hr)) + if hr_fp <= 0: + raise ValueError(f"invalid worker hashrate: {hr}") + full = hr_fp // MAX_BIDDING_COMMITMENT_WORKER_SIZE_FP18 + remainder = hr_fp % MAX_BIDDING_COMMITMENT_WORKER_SIZE_FP18 + + if remainder != 0 and remainder < MIN_BIDDING_COMMITMENT_WORKER_SIZE_FP18: + if full <= 0: + raise ValueError(f"worker hashrate below v2 min size ({MIN_BIDDING_COMMITMENT_WORKER_SIZE_FP18}): {hr}") + delta = MIN_BIDDING_COMMITMENT_WORKER_SIZE_FP18 - remainder + adjusted = MAX_BIDDING_COMMITMENT_WORKER_SIZE_FP18 - delta + full -= 1 + remainder = MIN_BIDDING_COMMITMENT_WORKER_SIZE_FP18 + adj_key = _fp18_to_min_decimal_str(adjusted) + worker_sizes[adj_key] = worker_sizes.get(adj_key, 0) + 1 + + if full > 0: + full_key = _fp18_to_min_decimal_str(MAX_BIDDING_COMMITMENT_WORKER_SIZE_FP18) + worker_sizes[full_key] = worker_sizes.get(full_key, 0) + int(full) + if remainder > 0: + rem_key = _fp18_to_min_decimal_str(remainder) + worker_sizes[rem_key] = worker_sizes.get(rem_key, 0) + 1 + + v2_bids = [("BTC", price_fp18, worker_sizes)] + commit = BiddingCommitment(t="b", bids=v2_bids, v=2) + return commit.to_compact(), v2_bids def _hashrate_to_fp18(value: Any) -> int | None: diff --git a/app/src/infinite_hashes/auctions/utils.py b/app/src/infinite_hashes/auctions/utils.py index cd658de..4f5b5df 100644 --- a/app/src/infinite_hashes/auctions/utils.py +++ b/app/src/infinite_hashes/auctions/utils.py @@ -4,7 +4,7 @@ import structlog import turbobt -from infinite_hashes.consensus.bidding import BiddingCommitment +from infinite_hashes.consensus.bidding import BiddingCommitment, v2_only_active from infinite_hashes.consensus.parser import parse_commitment logger = structlog.get_logger(__name__) @@ -28,6 +28,8 @@ def parse_bidding_commitments( commits: dict[str, bytes | str], + *, + block_number: int | None = None, ) -> dict[str, list[tuple[str, int] | tuple[str, int, int]]]: """Parse bidding commitments, safely handling potential binary suffixes. @@ -44,6 +46,8 @@ def parse_bidding_commitments( model = parse_commitment(raw, expected_types=[BiddingCommitment]) if model is None: continue + if v2_only_active(block_number) and int(getattr(model, "v", 1) or 1) < 2: + continue try: if int(getattr(model, "v", 1) or 1) >= 2: @@ -189,7 +193,7 @@ async def fetch_bids_for_start_block( start_blk = await bittensor.block(start_block).get() commits_raw = await subnet.commitments.fetch(block_hash=start_blk.hash) - bids_by_hotkey = parse_bidding_commitments(commits_raw) + bids_by_hotkey = parse_bidding_commitments(commits_raw, block_number=start_block) state = getattr(getattr(bittensor, "subtensor", None), "state", None) if state is not None and bids_by_hotkey: diff --git a/app/src/infinite_hashes/consensus/bidding.py b/app/src/infinite_hashes/consensus/bidding.py index de370ac..48963b5 100644 --- a/app/src/infinite_hashes/consensus/bidding.py +++ b/app/src/infinite_hashes/consensus/bidding.py @@ -29,6 +29,13 @@ # Optional budget cap from validator commitments (defaults to no cap). DEFAULT_BUDGET_CAP = 1.0 ILP_BIG_M_SWITCH_BLOCK = 7405572 +V2_ONLY_SWITCH_BLOCK = 7414264 # v2-only activation (≈2h after block 7413664) + + +def v2_only_active(block_number: int | None) -> bool: + if block_number is None: + return False + return block_number >= V2_ONLY_SWITCH_BLOCK class BiddingCommitment(CompactCommitment): diff --git a/app/src/tests/integration/conftest.py b/app/src/tests/integration/conftest.py index 01664ad..4aa1eae 100644 --- a/app/src/tests/integration/conftest.py +++ b/app/src/tests/integration/conftest.py @@ -18,6 +18,7 @@ # Set Django settings for integration tests os.environ.setdefault("DJANGO_SETTINGS_MODULE", "infinite_hashes.settings") os.environ.setdefault("PRICE_COMMITMENT_BUDGET_CAP", "1.0") +os.environ.setdefault("APS_MINER_ALLOW_V1", "1") class _ProxyWorkersHandler(BaseHTTPRequestHandler):