From e498d74ff1b7f67ed6f21637dedb4a40780dc55b Mon Sep 17 00:00:00 2001 From: Michal Zukowski Date: Fri, 23 Jan 2026 12:50:53 +0100 Subject: [PATCH] Budget cap feauture with 40% default --- app/src/infinite_hashes/consensus/bidding.py | 26 +++++- app/src/infinite_hashes/consensus/price.py | 2 + app/src/infinite_hashes/settings.py | 1 + app/src/infinite_hashes/validator/tasks.py | 10 ++- .../validator/tests/settings.py | 1 + app/src/tests/integration/conftest.py | 1 + .../integration/test_budget_cap_commitment.py | 90 +++++++++++++++++++ 7 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 app/src/tests/integration/test_budget_cap_commitment.py diff --git a/app/src/infinite_hashes/consensus/bidding.py b/app/src/infinite_hashes/consensus/bidding.py index 9e8711a..4433e55 100644 --- a/app/src/infinite_hashes/consensus/bidding.py +++ b/app/src/infinite_hashes/consensus/bidding.py @@ -10,6 +10,7 @@ from .commitment import CompactCommitment from .price import ( + BUDGET_CAP_FIELD, _fp18_to_min_decimal_str, _parse_decimal_to_fp18_int, compute_price_consensus, @@ -24,6 +25,9 @@ MAX_BIDDING_COMMITMENT_WORKERS = 1000 MAX_BIDDING_COMMITMENT_WORKER_SIZE_FP18 = 450 * 10**15 +# Optional budget cap from validator commitments (defaults to no cap). +DEFAULT_BUDGET_CAP = 1.0 + class BiddingCommitment(CompactCommitment): """Compact commitment for bids (versioned). @@ -252,6 +256,15 @@ def _fp_div(a: int, b: int) -> int: return (a * FP) // b +def _budget_cap_from_fp18(cap_fp18: int | None) -> float: + if cap_fp18 is None: + return DEFAULT_BUDGET_CAP + cap = float(cap_fp18) / FP + if cap <= 0: + return DEFAULT_BUDGET_CAP + return cap + + async def select_auction_winners_async( bt: Any, netuid: int, @@ -301,6 +314,8 @@ async def select_auction_winners_async( end_block, share_fp18, ) + budget_cap = _budget_cap_from_fp18(prices.get(BUDGET_CAP_FIELD)) + budget_ph_capped = budget_ph * budget_cap if budget_ph <= 0: logger.warning( "Computed non-positive auction budget", @@ -326,8 +341,11 @@ async def select_auction_winners_async( miner_share_per_block=miner_share_per_block, share_fraction=share_fraction, budget_ph=budget_ph, + budget_cap=budget_cap, + budget_ph_capped=budget_ph_capped, bid_hotkeys=len(bids_by_hotkey or {}), ) + budget_ph = budget_ph_capped workers = _build_worker_items(bids_by_hotkey, max_price_multiplier) if not workers: @@ -341,7 +359,7 @@ async def select_auction_winners_async( async def _fetch_prices(bt: Any, netuid: int, block_number: int) -> dict[str, int] | None: - metrics = ["TAO_USDC", "ALPHA_TAO", "HASHP_USDC"] + metrics = ["TAO_USDC", "ALPHA_TAO", "HASHP_USDC", BUDGET_CAP_FIELD] res = await compute_price_consensus(netuid, block_number, metrics, bt=bt) try: tao_usdc = int(res.get("TAO_USDC")) @@ -349,7 +367,11 @@ async def _fetch_prices(bt: Any, netuid: int, block_number: int) -> dict[str, in hashp_usdc = int(res.get("HASHP_USDC")) except (TypeError, ValueError): return None - return {"TAO_USDC": tao_usdc, "ALPHA_TAO": alpha_tao, "HASHP_USDC": hashp_usdc} + cap_raw = res.get(BUDGET_CAP_FIELD) + cap_fp18 = int(DEFAULT_BUDGET_CAP * FP) + if isinstance(cap_raw, int): + cap_fp18 = cap_raw + return {"TAO_USDC": tao_usdc, "ALPHA_TAO": alpha_tao, "HASHP_USDC": hashp_usdc, BUDGET_CAP_FIELD: cap_fp18} def _compute_budget_ph( diff --git a/app/src/infinite_hashes/consensus/price.py b/app/src/infinite_hashes/consensus/price.py index eb6032c..cbc732a 100644 --- a/app/src/infinite_hashes/consensus/price.py +++ b/app/src/infinite_hashes/consensus/price.py @@ -12,6 +12,8 @@ logger = structlog.get_logger(__name__) +BUDGET_CAP_FIELD = "cap" + class PriceCommitment(CompactCommitment): # Compact type discriminator 'p' only diff --git a/app/src/infinite_hashes/settings.py b/app/src/infinite_hashes/settings.py index c77148b..4ee91b1 100644 --- a/app/src/infinite_hashes/settings.py +++ b/app/src/infinite_hashes/settings.py @@ -265,6 +265,7 @@ def wrapped(*args, **kwargs): AUCTION_WINDOW_BLOCKS = 60 AUCTION_ILP_CBC_MAX_NODES = 100_000 MAX_PRICE_MULTIPLIER = 1.05 +PRICE_COMMITMENT_BUDGET_CAP = env.str("PRICE_COMMITMENT_BUDGET_CAP", default="0.4") CONSTANCE_BACKEND = "constance.backends.database.DatabaseBackend" CONSTANCE_CONFIG = { # type: ignore diff --git a/app/src/infinite_hashes/validator/tasks.py b/app/src/infinite_hashes/validator/tasks.py index 6a1b414..807a013 100644 --- a/app/src/infinite_hashes/validator/tasks.py +++ b/app/src/infinite_hashes/validator/tasks.py @@ -1579,7 +1579,11 @@ def publish_local_commitment(*, event_loop: Any = None): async def _publish_price_commitment_async(prices: dict[str, int], banned_hotkeys: set[str]): """Publish price commitment with ban bitmap to the chain.""" - from infinite_hashes.consensus.price import PriceCommitment + from infinite_hashes.consensus.price import ( + BUDGET_CAP_FIELD, + PriceCommitment, + _parse_decimal_to_fp18_int, + ) async with turbobt.Bittensor( settings.BITTENSOR_NETWORK, @@ -1602,6 +1606,10 @@ async def _publish_price_commitment_async(prices: dict[str, int], banned_hotkeys # Create ban bitmap bans_bitmap = PriceCommitment.create_ban_bitmap(banned_uids) + cap_fp18 = _parse_decimal_to_fp18_int(settings.PRICE_COMMITMENT_BUDGET_CAP) + prices = dict(prices) + prices[BUDGET_CAP_FIELD] = cap_fp18 + # Create commitment with prices and bans commit = PriceCommitment(t="p", prices=prices, bans=bans_bitmap, v=1) payload = commit.to_compact_bytes() diff --git a/app/src/infinite_hashes/validator/tests/settings.py b/app/src/infinite_hashes/validator/tests/settings.py index 5e7fa06..a677fbf 100644 --- a/app/src/infinite_hashes/validator/tests/settings.py +++ b/app/src/infinite_hashes/validator/tests/settings.py @@ -10,6 +10,7 @@ BITTENSOR_NETWORK = "local" BITTENSOR_WALLET_HOTKEY_NAME = "default" BITTENSOR_WALLET_NAME = "default" +PRICE_COMMITMENT_BUDGET_CAP = "1.0" # For API integration tests, use separate _FOR_TESTS env vars # Otherwise use dummy values for unit tests diff --git a/app/src/tests/integration/conftest.py b/app/src/tests/integration/conftest.py index d0fff65..01664ad 100644 --- a/app/src/tests/integration/conftest.py +++ b/app/src/tests/integration/conftest.py @@ -17,6 +17,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") class _ProxyWorkersHandler(BaseHTTPRequestHandler): diff --git a/app/src/tests/integration/test_budget_cap_commitment.py b/app/src/tests/integration/test_budget_cap_commitment.py new file mode 100644 index 0000000..8b77ce9 --- /dev/null +++ b/app/src/tests/integration/test_budget_cap_commitment.py @@ -0,0 +1,90 @@ +"""Budget cap commitment integration test. + +Runs two scenarios to prove the price commitment cap field scales ILP budget: +- cap=1.0 keeps the full budget +- cap=0.5 halves the budget and reduces winners +""" + +import pytest +import structlog +from django.conf import settings as django_settings + +from infinite_hashes.testutils.integration.scenario import ( + AssertWeightsEvent, + RegisterMiner, + RegisterValidator, + Scenario, + SetCommitment, + SetPrices, + TimeAddress, + perfect_delivery_hook, +) +from infinite_hashes.testutils.integration.scenario_runner import ScenarioRunner + +from .helpers import compute_expected_weights + +logger = structlog.get_logger(__name__) + + +@pytest.mark.asyncio +@pytest.mark.django_db(transaction=False) +@pytest.mark.integration +@pytest.mark.slow +async def test_budget_cap_commitment_scales_budget(monkeypatch, django_db_setup) -> None: + """Ensure budget cap from price commitments scales auction budget.""" + base_budget_ph = 10.0 + t0 = TimeAddress(-1, 5, 0) + + async def _run_with_cap(cap_str: str, expected_weights: dict[str, float]) -> None: + monkeypatch.setenv("PRICE_COMMITMENT_BUDGET_CAP", cap_str) + monkeypatch.setattr(django_settings, "PRICE_COMMITMENT_BUDGET_CAP", cap_str, raising=False) + + scenario = Scenario( + num_epochs=1, + default_delivery_hook=perfect_delivery_hook, + ) + + scenario.add_events( + RegisterValidator(time=t0, name="validator_0", stake=10_000.0), + RegisterMiner( + time=t0, + name="miner_0", + workers=[ + {"identifier": "worker_0", "hashrate_ph": "6.0", "price_multiplier": "1.0"}, + ], + ), + RegisterMiner( + time=t0, + name="miner_1", + workers=[ + {"identifier": "worker_1", "hashrate_ph": "4.0", "price_multiplier": "1.0"}, + ], + ), + SetPrices(time=t0.b_dt(1), validator_name="validator_0", ph_budget=base_budget_ph), + SetCommitment(time=t0.b_dt(2), miner_name="miner_0"), + SetCommitment(time=t0.b_dt(2), miner_name="miner_1"), + ) + + scenario.add_event( + AssertWeightsEvent( + time=TimeAddress(1, 0, 1), + for_epoch=0, + expected_weights={"validator_0": expected_weights}, + ) + ) + + logger.info("Running budget cap scenario", cap=cap_str) + await ScenarioRunner.execute(scenario, random_seed=12345) + + expected_full = compute_expected_weights( + budget_ph=base_budget_ph, + miner_0=[(6.0, 1.0, [1] * 6)], + miner_1=[(4.0, 1.0, [1] * 6)], + ) + await _run_with_cap("1.0", expected_full) + + expected_half = compute_expected_weights( + budget_ph=base_budget_ph, + miner_1=[(4.0, 1.0, [1] * 6)], + ) + await _run_with_cap("0.5", expected_half)