Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions app/src/infinite_hashes/consensus/bidding.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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).
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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",
Expand All @@ -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:
Expand All @@ -341,15 +359,19 @@ 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"))
alpha_tao = int(res.get("ALPHA_TAO"))
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(
Expand Down
2 changes: 2 additions & 0 deletions app/src/infinite_hashes/consensus/price.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

logger = structlog.get_logger(__name__)

BUDGET_CAP_FIELD = "cap"


class PriceCommitment(CompactCommitment):
# Compact type discriminator 'p' only
Expand Down
1 change: 1 addition & 0 deletions app/src/infinite_hashes/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 9 additions & 1 deletion app/src/infinite_hashes/validator/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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()
Expand Down
1 change: 1 addition & 0 deletions app/src/infinite_hashes/validator/tests/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions app/src/tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
90 changes: 90 additions & 0 deletions app/src/tests/integration/test_budget_cap_commitment.py
Original file line number Diff line number Diff line change
@@ -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)