diff --git a/app/src/infinite_hashes/auctions/mechanism_split.py b/app/src/infinite_hashes/auctions/mechanism_split.py index 21e2203..830c2be 100644 --- a/app/src/infinite_hashes/auctions/mechanism_split.py +++ b/app/src/infinite_hashes/auctions/mechanism_split.py @@ -5,6 +5,7 @@ from typing import Any MECHANISM_SPLIT_DENOMINATOR = 65535 +AUCTION_MECHANISM_SHARE_FRACTION = 1.0 async def fetch_mechanism_share_fraction( diff --git a/app/src/infinite_hashes/consensus/bidding.py b/app/src/infinite_hashes/consensus/bidding.py index 48963b5..8a7ca9d 100644 --- a/app/src/infinite_hashes/consensus/bidding.py +++ b/app/src/infinite_hashes/consensus/bidding.py @@ -6,7 +6,7 @@ import structlog from pydantic import Field -from infinite_hashes.auctions.mechanism_split import fetch_mechanism_share_fraction +from infinite_hashes.auctions.mechanism_split import AUCTION_MECHANISM_SHARE_FRACTION from .commitment import CompactCommitment from .price import ( @@ -312,11 +312,7 @@ async def select_auction_winners_async( share_fp18 = miners_share_fp18 share_fraction: float | None = None if share_fp18 is None: - share_fraction = await fetch_mechanism_share_fraction( - bt, - netuid, - mechanism_id, - ) + share_fraction = AUCTION_MECHANISM_SHARE_FRACTION share_fp18 = int(share_fraction * BASE_MINER_SHARE * FP) miner_share_per_block = float(share_fp18) / FP diff --git a/app/src/infinite_hashes/consensus/tests/test_bidding_budget.py b/app/src/infinite_hashes/consensus/tests/test_bidding_budget.py index 0144acc..d7a5cfa 100644 --- a/app/src/infinite_hashes/consensus/tests/test_bidding_budget.py +++ b/app/src/infinite_hashes/consensus/tests/test_bidding_budget.py @@ -4,6 +4,8 @@ MECHANISM_SPLIT_DENOMINATOR, fetch_mechanism_share_fraction, ) +from infinite_hashes.consensus.bidding import FP, _compute_budget_ph +from infinite_hashes.testutils.integration.budget_helper import alpha_tao_to_fp18, compute_alpha_tao_for_budget class _FakeState: @@ -56,3 +58,24 @@ class _NoStateBT: with pytest.raises(RuntimeError): await fetch_mechanism_share_fraction(_NoStateBT(), netuid=1, mechanism_id=1) + + +def test_alpha_tao_helper_keeps_full_budget_reachable_for_100_percent_auction_share(): + target_budget_ph = 10.0 + alpha_tao = compute_alpha_tao_for_budget( + target_budget_ph, + tao_usdc=45.0, + hashp_usdc=50.0, + mechanism_share=1.0, + ) + + computed_budget_ph = _compute_budget_ph( + alpha_tao_to_fp18(alpha_tao), + int(45.0 * FP), + int(50.0 * FP), + 0, + 0, + int(0.41 * FP), + ) + + assert computed_budget_ph >= target_budget_ph diff --git a/app/src/infinite_hashes/settings.py b/app/src/infinite_hashes/settings.py index 69083e7..88a3f55 100644 --- a/app/src/infinite_hashes/settings.py +++ b/app/src/infinite_hashes/settings.py @@ -280,22 +280,8 @@ def wrapped(*args, **kwargs): CELERY_MESSAGE_COMPRESSION = "gzip" # result compression CELERY_SEND_EVENTS = True # needed for worker monitoring CELERY_BEAT_SCHEDULE = { # type: ignore - # Legacy mechanism 0 (weight-based) - "calculate_weights": { - "task": "infinite_hashes.validator.tasks.calculate_weights", - "schedule": datetime.timedelta(minutes=1), - "options": { - "expires": datetime.timedelta(minutes=1).total_seconds(), - }, - }, - "set_weights": { - "task": "infinite_hashes.validator.tasks.set_weights", - "schedule": datetime.timedelta(minutes=1), - "options": { - "expires": datetime.timedelta(minutes=1).total_seconds(), - }, - }, - # Auction mechanism 1 + # Legacy mechanism 0 scheduling disabled. Auction weights are mirrored to + # both mechanisms from the auction pipeline. "process_auctions": { "task": "infinite_hashes.validator.tasks.process_auctions", "schedule": datetime.timedelta(minutes=5), diff --git a/app/src/infinite_hashes/testutils/integration/budget_helper.py b/app/src/infinite_hashes/testutils/integration/budget_helper.py index 7348c30..0d87c20 100644 --- a/app/src/infinite_hashes/testutils/integration/budget_helper.py +++ b/app/src/infinite_hashes/testutils/integration/budget_helper.py @@ -3,8 +3,10 @@ Allows easy control of available PH budget by adjusting ALPHA_TAO price. """ +import math + MECHANISM_SPLIT_DENOMINATOR = 65535 -DEFAULT_MECHANISM_SPLIT = (52428, 13107) # 80% -> mechanism 0, 20% -> mechanism 1 +DEFAULT_MECHANISM_SPLIT = (0, 65535) # 0% -> mechanism 0, 100% -> mechanism 1 DEFAULT_MECHANISM_0_SHARE = DEFAULT_MECHANISM_SPLIT[0] / MECHANISM_SPLIT_DENOMINATOR DEFAULT_MECHANISM_1_SHARE = DEFAULT_MECHANISM_SPLIT[1] / MECHANISM_SPLIT_DENOMINATOR @@ -72,7 +74,9 @@ def alpha_tao_to_fp18(alpha_tao: float) -> int: >>> alpha_tao_to_fp18(0.25) 250000000000000000 """ - return int(alpha_tao * 10**18) + # Round upward so test scenarios targeting an exact PH budget do not end up + # infinitesimally below the desired budget after FP18 truncation. + return math.ceil(alpha_tao * 10**18) def compute_budget_summary( diff --git a/app/src/infinite_hashes/validator/tasks.py b/app/src/infinite_hashes/validator/tasks.py index f766fd7..43be9d9 100644 --- a/app/src/infinite_hashes/validator/tasks.py +++ b/app/src/infinite_hashes/validator/tasks.py @@ -19,7 +19,7 @@ from django.conf import settings from django.db import transaction -from infinite_hashes.auctions.mechanism_split import fetch_mechanism_share_fraction +from infinite_hashes.auctions.mechanism_split import AUCTION_MECHANISM_SHARE_FRACTION from infinite_hashes.celery import app from infinite_hashes.utils import run_async from infinite_hashes.validator import auction_processing as auct @@ -30,6 +30,8 @@ WEIGHT_SETTING_ATTEMPTS = 100 WEIGHT_SETTING_FAILURE_BACKOFF = 5 WEIGHT_SETTING_FINALIZATION_TIMEOUT = 120 +AUCTION_WEIGHT_MECHANISM_IDS = (0, 1) +BASE_MINER_SHARE = 0.41 Hashrates: TypeAlias = dict[str, list[int]] @@ -674,25 +676,14 @@ def _check_all_windows_processed(): ) blocks_per_day = 7200 - base_miner_share = 0.41 # Base miner allocation per block (pre-mechanism split) - mechanism_share_fraction = await fetch_mechanism_share_fraction( - bittensor, - settings.BITTENSOR_NETUID, - mechanism_id=1, - ) - if mechanism_share_fraction is None or mechanism_share_fraction <= 0: - raise RuntimeError( - f"Mechanism emission share must be positive; received {mechanism_share_fraction!r} for mechanism 1" - ) - - miner_share_per_block = base_miner_share * mechanism_share_fraction + mechanism_share_fraction = AUCTION_MECHANISM_SHARE_FRACTION + miner_share_per_block = BASE_MINER_SHARE * mechanism_share_fraction daily_alpha = blocks_per_day * miner_share_per_block logger.debug( - "Mechanism emission share", - mechanism_id=1, + "Auction emission share fixed", mechanism_share_fraction=mechanism_share_fraction, - base_miner_share=base_miner_share, + base_miner_share=BASE_MINER_SHARE, miner_share_per_block=miner_share_per_block, daily_alpha=daily_alpha, ) @@ -892,7 +883,7 @@ def calculate_auction_weights(*, event_loop: Any = None): async def set_auction_weights_async() -> bool: - """Set weights for mechanism 1 using set_mechanism_weights.""" + """Set auction weights for both mechanism 0 and mechanism 1.""" batches = [ batch async for batch in WeightsBatch.objects.filter( @@ -976,60 +967,71 @@ async def set_auction_weights_async() -> bool: batch.scored = True if not weights_by_uid: - logger.warning("No weights to set for mechanism 1") + logger.warning("No auction weights to set") return False - # Commit weights using commit/reveal scheme (same pattern as mechanism 0) - reveal_round = None - use_direct_set = False - try: - async for attempt in tenacity.AsyncRetrying( - before_sleep=tenacity.before_sleep_log(logger, logging.ERROR), - stop=tenacity.stop_after_attempt(1), - wait=tenacity.wait_fixed(WEIGHT_SETTING_FAILURE_BACKOFF), - ): - with attempt: - reveal_round = await commit_mechanism_weights( - bittensor=bittensor, - netuid=settings.BITTENSOR_NETUID, - mechanism_id=1, - weights=weights_by_uid, - version_key=0, - ) - except Exception as exc: - message = str(exc).lower() - if isinstance(exc, turbobt.subtensor.exceptions.CommitRevealDisabled) or ( - "commitrevealdisabled" in message - or "commitrevealv3disabled" in message - or "commit reveal" in message - or "commit/reveal" in message - ): - logger.warning( - "Commit-reveal disabled for mechanism 1, falling back to set_mechanism_weights", - error=str(exc), - ) - use_direct_set = True - else: - raise + submission_results = [] + weights_u16 = normalize_weights_to_u16(weights_by_uid) - if use_direct_set: - weights_u16 = normalize_weights_to_u16(weights_by_uid) + for mechanism_id in AUCTION_WEIGHT_MECHANISM_IDS: + reveal_round = None + use_direct_set = False try: - uids, weight_values = zip(*weights_u16.items()) - except ValueError: - uids, weight_values = [], [] + async for attempt in tenacity.AsyncRetrying( + before_sleep=tenacity.before_sleep_log(logger, logging.ERROR), + stop=tenacity.stop_after_attempt(1), + wait=tenacity.wait_fixed(WEIGHT_SETTING_FAILURE_BACKOFF), + ): + with attempt: + reveal_round = await commit_mechanism_weights( + bittensor=bittensor, + netuid=settings.BITTENSOR_NETUID, + mechanism_id=mechanism_id, + weights=weights_by_uid, + version_key=0, + ) + except Exception as exc: + message = str(exc).lower() + if isinstance(exc, turbobt.subtensor.exceptions.CommitRevealDisabled) or ( + "commitrevealdisabled" in message + or "commitrevealv3disabled" in message + or "commit reveal" in message + or "commit/reveal" in message + ): + logger.warning( + "Commit-reveal disabled for mechanism, falling back to set_mechanism_weights", + mechanism_id=mechanism_id, + error=str(exc), + ) + use_direct_set = True + else: + raise - extrinsic = await bittensor.subtensor.subtensor_module.set_mechanism_weights( - settings.BITTENSOR_NETUID, - list(uids), - mechanism_id=1, - weights=list(weight_values), - version_key=0, - wallet=bittensor.wallet, - ) - await asyncio.wait_for( - extrinsic.wait_for_finalization(), - timeout=WEIGHT_SETTING_FINALIZATION_TIMEOUT, + if use_direct_set: + try: + uids, weight_values = zip(*weights_u16.items()) + except ValueError: + uids, weight_values = [], [] + + extrinsic = await bittensor.subtensor.subtensor_module.set_mechanism_weights( + settings.BITTENSOR_NETUID, + list(uids), + mechanism_id=mechanism_id, + weights=list(weight_values), + version_key=0, + wallet=bittensor.wallet, + ) + await asyncio.wait_for( + extrinsic.wait_for_finalization(), + timeout=WEIGHT_SETTING_FINALIZATION_TIMEOUT, + ) + + submission_results.append( + { + "mechanism_id": mechanism_id, + "mode": "direct" if use_direct_set else "commit_reveal", + "reveal_round": reveal_round, + } ) await WeightsBatch.objects.abulk_update( @@ -1037,19 +1039,12 @@ async def set_auction_weights_async() -> bool: fields=("scored",), ) - if use_direct_set: - logger.info( - "Auction weights set for mechanism 1 (direct): %s UIDs, batches: [%s]", - len(weights_by_uid), - ", ".join(str(batch.id) for batch in batches), - ) - else: - logger.info( - "Auction weights committed for mechanism 1 (commit/reveal): %s UIDs, reveal_round: %s, batches: [%s]", - len(weights_by_uid), - reveal_round, - ", ".join(str(batch.id) for batch in batches), - ) + logger.info( + "Auction weights submitted", + mechanisms=submission_results, + uid_count=len(weights_by_uid), + batches=[batch.id for batch in batches], + ) return True diff --git a/app/src/infinite_hashes/validator/tests/test_tasks.py b/app/src/infinite_hashes/validator/tests/test_tasks.py index cf50fd8..d99c597 100644 --- a/app/src/infinite_hashes/validator/tests/test_tasks.py +++ b/app/src/infinite_hashes/validator/tests/test_tasks.py @@ -9,6 +9,7 @@ from infinite_hashes.validator.tasks import ( calculate_weights, scrape_metrics, + set_auction_weights, set_weights, ) @@ -143,6 +144,38 @@ def test_set_weights_expired_batches(bittensor): assert batch.should_be_scored is False +@pytest.mark.django_db +def test_set_auction_weights_sets_mech0_and_mech1(bittensor, monkeypatch): + bittensor.head.get = unittest.mock.AsyncMock( + return_value=unittest.mock.MagicMock( + number=1010, + ), + ) + bittensor.subnet.return_value.epoch.return_value = range(719, 1080) + + batch = WeightsBatch.objects.create( + block=1007, + epoch_start=719, + mechanism_id=1, + should_be_scored=True, + weights={ + "hotkey_1": 1, + "hotkey_3": 3, + }, + ) + + commit_mock = unittest.mock.AsyncMock(side_effect=[111, 111]) + monkeypatch.setattr("infinite_hashes.validator.tasks.commit_mechanism_weights", commit_mock) + + assert set_auction_weights() + + mechanism_ids = [call.kwargs["mechanism_id"] for call in commit_mock.await_args_list] + assert mechanism_ids == [0, 1] + + batch.refresh_from_db() + assert batch.scored is True + + @pytest.mark.django_db @pytest.mark.api_integration @override_settings(BITTENSOR_NETWORK="wss://entrypoint-finney.opentensor.ai:443")