Skip to content
This repository was archived by the owner on Aug 3, 2026. It is now read-only.
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
12 changes: 11 additions & 1 deletion auditor/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,17 @@ def maybe_counter_weight(chain: ChainClient, api: ReportClient) -> None:

epoch_id = _read_str_file(LAST_CLEAN_EPOCH_FILE)
if not epoch_id:
logger.info("counter-weight: due but no clean epoch audited yet — skipping")
# No clean epoch to replay (e.g. the audit-reports repo is empty / 404).
# BURN FALLBACK: still set weights to uid 0 so the auditor-validator
# keeps its vTrust alive + burns to the owner, instead of skipping.
from auditor.weights import submit_burn_weights

logger.info(
"counter-weight: due but no clean epoch (empty/404 audit repo) — "
"BURN fallback to uid 0"
)
if submit_burn_weights(subtensor_url=chain.subtensor_url, netuid=chain.netuid):
_write_int_file(PUBLISHED_FILE, current)
return

logger.info(
Expand Down
56 changes: 56 additions & 0 deletions auditor/weights.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,61 @@ def _load_wallet():
return bt.Wallet(name=name, hotkey=hotkey)


def submit_burn_weights(
subtensor_url: str,
netuid: int,
burn_uid: int | None = None,
) -> bool:
"""Set the auditor's own weights to 100% on the burn uid (default 0 = owner).

Used when there is NO clean audit epoch to replay (e.g. the audit-reports
repo is empty / 404) so the auditor-validator still sets weights every
cadence — keeps its vTrust alive and burns to the owner uid instead of
silently skipping. Signed by the auditor's OWN wallet. Never raises.
"""
if burn_uid is None:
burn_uid = int(os.environ.get("RALPH_BURN_UID", "0"))
wallet = _load_wallet()
if wallet is None:
return False
try:
import bittensor as bt
import torch

subtensor = bt.Subtensor(network=subtensor_url)
metagraph = subtensor.metagraph(netuid=netuid)
except Exception:
logger.exception("burn: failed to connect subtensor at %s", subtensor_url)
return False
try:
auditor_ss58 = wallet.hotkey.ss58_address
if auditor_ss58 not in list(metagraph.hotkeys):
logger.warning(
"burn: auditor hotkey %s not registered on netuid=%d — cannot set weights",
auditor_ss58, netuid,
)
return False
result = subtensor.set_weights(
wallet=wallet,
netuid=netuid,
uids=torch.tensor([burn_uid], dtype=torch.int64),
weights=torch.tensor([1.0], dtype=torch.float32),
wait_for_inclusion=True,
wait_for_finalization=False,
)
success = result.success if hasattr(result, "success") else bool(result)
logger.info("auditor BURN set_weights -> uid %d: success=%s", burn_uid, success)
return bool(success)
except Exception:
logger.exception("auditor burn set_weights failed")
return False
finally:
try:
subtensor.close()
except Exception:
pass


def submit_weights(
subtensor_url: str,
netuid: int,
Expand Down Expand Up @@ -185,6 +240,7 @@ def submit_weights(
"auditor_hotkey_ss58",
"is_enabled",
"is_weight_set_due",
"submit_burn_weights",
"submit_weights",
"weight_set_interval_blocks",
]
24 changes: 24 additions & 0 deletions chain_layer/bittensor_chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,31 @@ def set_weights(self, hotkey_scores: dict[str, float]) -> bool:
if not uids:
print("[chain] no valid UIDs to set weights for")
return False
return self._submit_weight_tensors(uids, weights)

def set_burn_weights(self) -> bool:
"""Fallback: set 100% weight to the burn UID (default 0 = subnet owner).

Used when there is nothing real to score/audit this epoch so the
validator (and the auditor) STILL sets weights every epoch — this keeps
the validator's vTrust alive and burns the epoch's incentive to the
owner uid (standard "burn to owner" pattern) instead of silently setting
nothing. Override the target via env RALPH_BURN_UID.
"""
import os as _os

burn_uid = int(_os.environ.get("RALPH_BURN_UID", "0"))
print(f"[chain] BURN fallback: 100% weight -> uid {burn_uid}")
self.sync()
return self._submit_weight_tensors([burn_uid], [1.0])

def _submit_weight_tensors(self, uids: list[int], weights: list[float]) -> bool:
"""Normalize + submit one set_weights extrinsic for explicit uids/weights.

Shared by set_weights (hotkey-mapped scores) and set_burn_weights (the
uid-0 burn fallback) so both go through the identical rate-limit guard +
extrinsic + event path. Caller is responsible for self.sync().
"""
total = sum(weights) or 1.0
weights = [w / total for w in weights]

Expand Down
13 changes: 13 additions & 0 deletions chain_layer/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,19 @@ def set_weights(self, hotkey_scores: dict[str, float]) -> bool:
})
return True

def set_burn_weights(self) -> bool:
"""Burn fallback (sim): record a 100%-to-burn-uid weight event."""
import os as _os

burn_uid = int(_os.environ.get("RALPH_BURN_UID", "0"))
self.append_event({
"type": "weights_set",
"timestamp": time.time(),
"weights": {f"uid:{burn_uid}": 1.0},
"burn": True,
})
return True

def get_king(self) -> Optional[KingRecord]:
path = self.chain_dir / "king.json"
if not path.exists():
Expand Down
56 changes: 56 additions & 0 deletions tests/test_burn_fallback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""Burn-to-uid-0 fallback: validator + auditor still set weights (keep vTrust +
burn to owner) when there is nothing real to score/audit this epoch."""
from __future__ import annotations

import json
import tempfile
from pathlib import Path


# --------------------------------------------------------- validator flag
def test_validator_burn_fallback_default_on(monkeypatch):
from validator.service import _burn_fallback_enabled

monkeypatch.delenv("RALPH_BURN_FALLBACK", raising=False)
assert _burn_fallback_enabled() is True
for off in ("0", "false", "no", "off", "OFF"):
monkeypatch.setenv("RALPH_BURN_FALLBACK", off)
assert _burn_fallback_enabled() is False
for on in ("1", "true", "yes", "on"):
monkeypatch.setenv("RALPH_BURN_FALLBACK", on)
assert _burn_fallback_enabled() is True


# --------------------------------------------------------- LocalChain burn
def test_localchain_set_burn_weights_records_uid0(monkeypatch):
from chain_layer.local import LocalChain

monkeypatch.delenv("RALPH_BURN_UID", raising=False)
d = Path(tempfile.mkdtemp())
chain = LocalChain(d)
assert chain.set_burn_weights() is True
events = [json.loads(ln) for ln in (d / "events.jsonl").read_text().splitlines() if ln.strip()]
last = events[-1]
assert last["type"] == "weights_set"
assert last.get("burn") is True
assert last["weights"] == {"uid:0": 1.0}


def test_localchain_burn_uid_override(monkeypatch):
from chain_layer.local import LocalChain

monkeypatch.setenv("RALPH_BURN_UID", "7")
d = Path(tempfile.mkdtemp())
chain = LocalChain(d)
chain.set_burn_weights()
events = [json.loads(ln) for ln in (d / "events.jsonl").read_text().splitlines() if ln.strip()]
assert events[-1]["weights"] == {"uid:7": 1.0}


# --------------------------------------------------------- auditor burn
def test_auditor_submit_burn_no_wallet_is_graceful(monkeypatch):
from auditor.weights import submit_burn_weights

monkeypatch.delenv("AUDITOR_WALLET_NAME", raising=False)
# No wallet configured → returns False without raising (read-only).
assert submit_burn_weights("ws://localhost:9944", 40) is False
16 changes: 16 additions & 0 deletions validator/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,15 @@ def _save_pending_weights(chain, weights: dict[str, float]) -> None:
p.write_text(json.dumps(weights, indent=2, sort_keys=True))


def _burn_fallback_enabled() -> bool:
"""Burn-to-uid-0 when nothing is scoreable this epoch. On by default
(standard 'burn to owner' so the validator always sets weights + keeps
vTrust). Disable with RALPH_BURN_FALLBACK in {0,false,no,off}."""
return os.environ.get("RALPH_BURN_FALLBACK", "1").strip().lower() not in {
"0", "false", "no", "off",
}


def _clear_pending_weights(chain) -> None:
p = _pending_weights_path(chain)
if p is not None and p.exists():
Expand Down Expand Up @@ -868,6 +877,13 @@ def run_epoch(
"set_weights returned False — pending_weights.json kept for "
"next-epoch retry. No credit was lost."
)
elif _burn_fallback_enabled():
# BURN FALLBACK: nothing scoreable this epoch (no king, all rejected /
# zero submissions). Still set weights every epoch so the validator
# keeps its vTrust alive — burn the epoch's incentive to the owner uid
# (default 0). Disable with RALPH_BURN_FALLBACK=0.
log_info("no scoreable submissions this epoch — setting BURN weights (uid 0)")
weights_set = chain.set_burn_weights()

# validation-v2 Phase 1: validator audit report + on-chain anchor.
# Build report_json from this epoch's scored results, hash + sign, anchor
Expand Down
Loading