Skip to content
Closed
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
2 changes: 1 addition & 1 deletion gittensor/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@

# Combined scoring pool distributed by repository emission_share, then by per-repo PR/issue split.
OSS_EMISSION_SHARE = 0.90
DEFAULT_ISSUE_DISCOVERY_SHARE = 0.5
DEFAULT_ISSUE_DISCOVERY_SHARE = 0.0
EMISSION_SHARE_TOLERANCE = 1e-9

# =============================================================================
Expand Down
3 changes: 2 additions & 1 deletion gittensor/validator/forward.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ async def forward(self: 'Validator') -> None:

Emission blending:
- Combined scoring pool: 90%, allocated by repository emission_share
(validator-voted consensus aggregate when active, baked-in weights otherwise)
(on-chain registry + validator-voted consensus aggregate when active,
baked-in weights otherwise)
- Maintainer cut: per-repo carve-out routed to maintainer miner neurons
- Issue treasury: 10%, flat to UID 111
- Recycle: registry slack and inactive repo slices to UID 0
Expand Down
147 changes: 147 additions & 0 deletions gittensor/validator/repo_registry/loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# The MIT License (MIT)
# Copyright 2025 Entrius
"""On-chain registry -> master repository weights (spec swap R1).

The contract keys repos by GitHub numeric id; this loader resolves them to
lowercase ``owner/name`` dict keys at the load_weights boundary so downstream
scoring sees zero changes (identity shim — renames map forward only). Every
read pins to the snapshot block hash, so all validators load byte-identical
state. Share vectors derive only from contract state at the snapshot block:
the loader never filters or renormalizes on GitHub App install status.

Fallback ladder: contract @ snapshot -> last-good disk cache -> None, which
sends the caller to the baked JSON. A paused or unseeded contract skips the
cache and lands straight on baked (worst case = today's behavior).
"""

import json
import os
from pathlib import Path
from typing import Any, Callable, Dict, Optional, Tuple

import bittensor as bt

from gittensor.validator.repo_registry.contract_client import RepoRegistryContractClient
from gittensor.validator.utils.load_weights import RepositoryConfig, parse_master_repositories

_FP6 = 1_000_000


def _fp6(value: int) -> float:
return value / _FP6


def _fp6_or_none(value: int) -> Optional[float]:
return None if value == 0 else value / _FP6


# Contract param key -> (nested metadata path, field name, decoder).
# Known key -> RepositoryConfig field; missing key -> constants.py default;
# unknown key -> ignored (forward compat).
_PARAM_FIELDS: Dict[int, Tuple[Tuple[str, ...], str, Callable[[int], Any]]] = {
1: ((), 'issue_discovery_share', _fp6),
2: ((), 'default_label_multiplier', _fp6),
3: ((), 'fixed_base_score', _fp6_or_none), # 0 = unset
4: ((), 'maintainer_cut', _fp6),
5: ((), 'trusted_label_pipeline', bool),
6: (('eligibility',), 'min_valid_merged_prs', int),
7: (('eligibility',), 'min_credibility', _fp6),
8: (('eligibility',), 'excessive_pr_penalty_base_threshold', int),
9: (('eligibility',), 'open_pr_threshold_token_score', _fp6),
10: (('eligibility',), 'max_open_pr_threshold', int),
11: (('eligibility',), 'min_valid_solved_issues', int),
12: (('eligibility',), 'min_issue_credibility', _fp6),
13: (('eligibility',), 'min_token_score_for_valid_issue', _fp6),
14: (('eligibility',), 'open_issue_spam_base_threshold', int),
15: (('eligibility',), 'open_issue_spam_token_score_per_slot', _fp6),
16: (('eligibility',), 'max_open_issue_threshold', int),
17: (('scoring',), 'pr_lookback_days', int),
18: (('scoring',), 'open_pr_collateral_percent', _fp6),
19: (('scoring',), 'review_penalty_rate', _fp6),
20: (('scoring',), 'standard_issue_multiplier', _fp6),
21: (('scoring',), 'maintainer_issue_multiplier', _fp6),
22: (('scoring',), 'src_tok_saturation_scale', _fp6),
23: (('scoring', 'time_decay'), 'grace_period_hours', int),
24: (('scoring', 'time_decay'), 'sigmoid_midpoint_days', _fp6),
25: (('scoring', 'time_decay'), 'sigmoid_steepness', _fp6),
26: (('scoring', 'time_decay'), 'min_multiplier', _fp6),
}


class RegistryLoader:
"""Loads the contract registry once per snapshot, pinned to its block hash."""

def __init__(self, client: RepoRegistryContractClient, cache_dir: Path):
self.client = client
self.cache_path = Path(cache_dir) / 'repo_registry_cache.json'
self._memo: Tuple[Optional[int], Optional[Dict[str, RepositoryConfig]]] = (None, None)

def load(self, snapshot: int) -> Optional[Dict[str, RepositoryConfig]]:
"""Registry configs at the snapshot block; None -> caller uses baked JSON."""
memo_snapshot, memo_configs = self._memo
if memo_snapshot == snapshot:
return memo_configs
try:
at = self.client.subtensor.substrate.get_block_hash(snapshot)
if not at:
raise RuntimeError(f'no block hash for snapshot block {snapshot}')
packed = self.client.get_registry(at)
if packed is None:
raise RuntimeError('registry root cell unreadable')
if packed.paused:
bt.logging.warning('repo_registry: contract paused; using baked-in repository weights')
return None
data = {repo.full_name: self._repo_metadata(repo.github_id, at) for repo in self.client.get_all_repos(at)}
if not data:
bt.logging.info('repo_registry: contract registry empty; using baked-in repository weights')
return None
configs = parse_master_repositories(data)
self._save_cache(snapshot, data)
self._memo = (snapshot, configs)
bt.logging.info(f'repo_registry: loaded {len(configs)} repos from contract at snapshot {snapshot}')
return configs
except Exception as e:
bt.logging.warning(f'repo_registry: snapshot {snapshot} load failed ({e}); trying last-good cache')
return self._load_cache()

def _repo_metadata(self, github_id: int, at: str) -> Dict[str, Any]:
"""Decode one repo's on-chain params into master_repositories.json shape."""
meta: Dict[str, Any] = {'emission_share': 0.0} # consensus-voted, overlaid by apply_consensus
for key, value in sorted(self.client.get_params(github_id, at).items()):
spec = _PARAM_FIELDS.get(key)
if spec is None:
continue
path, field, decode = spec
section = meta
for part in path:
section = section.setdefault(part, {})
decoded = decode(value)
if decoded is not None:
section[field] = decoded
labels = self.client.get_label_multipliers(github_id, at)
if labels:
meta['label_multipliers'] = {label: _fp6(value) for label, value in sorted(labels.items())}
patterns = self.client.get_branch_patterns(github_id, at)
if patterns:
meta['additional_acceptable_branches'] = patterns
return meta

def _save_cache(self, snapshot: int, data: Dict[str, Any]) -> None:
try:
tmp_path = self.cache_path.with_suffix('.tmp')
tmp_path.write_text(json.dumps({'snapshot': snapshot, 'repositories': data}))
os.replace(tmp_path, self.cache_path)
except OSError as e:
bt.logging.warning(f'repo_registry: cache write failed ({e})')

def _load_cache(self) -> Optional[Dict[str, RepositoryConfig]]:
try:
payload = json.loads(self.cache_path.read_text())
configs = parse_master_repositories(payload['repositories'])
bt.logging.warning(f'repo_registry: using last-good registry from snapshot {payload["snapshot"]}')
return configs or None
except FileNotFoundError:
return None
except (OSError, ValueError, KeyError, TypeError) as e:
bt.logging.warning(f'repo_registry: corrupt registry cache ({e}); using baked-in repository weights')
return None
78 changes: 42 additions & 36 deletions gittensor/validator/utils/load_weights.py
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,47 @@ def _validate_scoring_configs(configs: Dict[str, RepositoryConfig]) -> None:
)


def parse_master_repositories(data: Dict[str, Any]) -> Dict[str, RepositoryConfig]:
"""Parse and validate master_repositories-shaped data into RepositoryConfig
objects keyed by lowercase full name. Raises RepositoryRegistryError or
ValueError when entries violate the registry contract."""
normalized_data: Dict[str, RepositoryConfig] = {}
for repo_name, metadata in data.items():
try:
if not isinstance(metadata, dict):
raise TypeError(f'expected object metadata, got {type(metadata)}')
config = RepositoryConfig(
emission_share=_coerce_share(repo_name, 'emission_share', metadata['emission_share']),
issue_discovery_share=_coerce_share(
repo_name,
'issue_discovery_share',
metadata.get('issue_discovery_share', DEFAULT_ISSUE_DISCOVERY_SHARE),
),
additional_acceptable_branches=metadata.get('additional_acceptable_branches'),
trusted_label_pipeline=bool(metadata.get('trusted_label_pipeline', False)),
label_multipliers=(
{str(label): float(multiplier) for label, multiplier in metadata['label_multipliers'].items()}
if metadata.get('label_multipliers') is not None
else None
),
default_label_multiplier=float(metadata.get('default_label_multiplier', 1.0)),
fixed_base_score=metadata.get('fixed_base_score'),
eligibility=_parse_eligibility(repo_name, metadata.get('eligibility')),
scoring=_parse_scoring(repo_name, metadata.get('scoring')),
maintainer_cut=_coerce_share(repo_name, 'maintainer_cut', metadata.get('maintainer_cut', 0.0)),
)
normalized_data[repo_name.lower()] = config
except RepositoryRegistryError:
raise
except (KeyError, ValueError, TypeError) as e:
raise ValueError(f'Could not parse config for {repo_name}: {e}') from e

_validate_emission_shares(normalized_data)
_validate_eligibility_configs(normalized_data)
_validate_scoring_configs(normalized_data)
return normalized_data


def load_master_repo_weights() -> Dict[str, RepositoryConfig]:
"""
Load repository emission shares from the local JSON file.
Expand All @@ -532,42 +573,7 @@ def load_master_repo_weights() -> Dict[str, RepositoryConfig]:
if not isinstance(data, dict):
raise RepositoryRegistryError(f'Expected dict from {weights_file}, got {type(data)}')

# Parse JSON data into RepositoryConfig objects
normalized_data: Dict[str, RepositoryConfig] = {}
for repo_name, metadata in data.items():
try:
if not isinstance(metadata, dict):
raise TypeError(f'expected object metadata, got {type(metadata)}')
config = RepositoryConfig(
emission_share=_coerce_share(repo_name, 'emission_share', metadata['emission_share']),
issue_discovery_share=_coerce_share(
repo_name,
'issue_discovery_share',
metadata.get('issue_discovery_share', DEFAULT_ISSUE_DISCOVERY_SHARE),
),
additional_acceptable_branches=metadata.get('additional_acceptable_branches'),
trusted_label_pipeline=bool(metadata.get('trusted_label_pipeline', False)),
label_multipliers=(
{str(label): float(multiplier) for label, multiplier in metadata['label_multipliers'].items()}
if metadata.get('label_multipliers') is not None
else None
),
default_label_multiplier=float(metadata.get('default_label_multiplier', 1.0)),
fixed_base_score=metadata.get('fixed_base_score'),
eligibility=_parse_eligibility(repo_name, metadata.get('eligibility')),
scoring=_parse_scoring(repo_name, metadata.get('scoring')),
maintainer_cut=_coerce_share(repo_name, 'maintainer_cut', metadata.get('maintainer_cut', 0.0)),
)
normalized_data[repo_name.lower()] = config
except RepositoryRegistryError:
raise
except (KeyError, ValueError, TypeError) as e:
raise ValueError(f'Could not parse config for {repo_name}: {e}') from e

_validate_emission_shares(normalized_data)
_validate_eligibility_configs(normalized_data)
_validate_scoring_configs(normalized_data)

normalized_data = parse_master_repositories(data)
bt.logging.debug(f'Successfully loaded {len(normalized_data)} repository entries from {weights_file}')
return normalized_data

Expand Down
27 changes: 23 additions & 4 deletions gittensor/validator/weight_consensus/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import json
import os
from pathlib import Path
from typing import Any, Callable, Dict, Optional
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional

import bittensor as bt

Expand All @@ -27,16 +27,26 @@
)
from gittensor.validator.weight_consensus.publisher import maybe_publish_prefs, resolve_local_prefs

if TYPE_CHECKING:
from gittensor.validator.repo_registry.loader import RegistryLoader

StoreHook = Callable[[int, Dict[str, Dict[str, int]], Dict[str, int], Dict[str, bool], AggregateResult], None]


class ConsensusManager:
"""Computes, caches, and serves the per-snapshot aggregate."""

def __init__(self, backend: ConsensusBackend, cache_dir: Path, store_hook: Optional[StoreHook] = None):
def __init__(
self,
backend: ConsensusBackend,
cache_dir: Path,
store_hook: Optional[StoreHook] = None,
registry_loader: Optional['RegistryLoader'] = None,
):
self.backend = backend
self.cache_path = Path(cache_dir) / 'weight_consensus_cache.json'
self.store_hook = store_hook
self.registry_loader = registry_loader
self._failed_snapshots: set = set()
self._cache: Dict[str, Any] = self._load_cache()

Expand Down Expand Up @@ -120,8 +130,11 @@ def _save_cache(self) -> None:


def run_weight_consensus(validator, master: Dict[str, RepositoryConfig]) -> Dict[str, RepositoryConfig]:
"""Forward-seam orchestrator: publish own vote, fetch the aggregate, overlay
it on the baked registry. Any failure returns the registry untouched."""
"""Forward-seam orchestrator: publish own vote, load the on-chain registry
at the snapshot block, fetch the aggregate, and overlay the shares. The
contract registry replaces the baked weights only when an aggregate is
active — contract repos carry no emission_share of their own. Any failure
returns the baked registry untouched."""
manager: Optional[ConsensusManager] = getattr(validator, 'consensus_manager', None)
if getattr(validator.config.neuron, 'disable_weight_consensus', False) or manager is None:
return master
Expand All @@ -141,6 +154,12 @@ def run_weight_consensus(validator, master: Dict[str, RepositoryConfig]) -> Dict
shares = manager.get_shares(validator.block)
if shares is None:
bt.logging.info('weight_consensus: no active aggregate; using baked-in repository weights')
return master

if manager.registry_loader is not None:
registry = manager.registry_loader.load(compute_snapshot_block(validator.block))
if registry is not None:
master = registry
return apply_consensus(master, shares)
except Exception as e:
bt.logging.error(f'weight_consensus: unexpected failure ({e}); using baked-in repository weights')
Expand Down
5 changes: 4 additions & 1 deletion neurons/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
priority_pat_check,
)
from gittensor.validator.repo_registry.contract_client import RepoRegistryContractClient
from gittensor.validator.repo_registry.loader import RegistryLoader
from gittensor.validator.utils.config import (
CONSENSUS_MIN_VALIDATOR_STAKE_RAO,
REPO_REGISTRY_CONTRACT_ADDRESS,
Expand Down Expand Up @@ -121,10 +122,12 @@ def _init_consensus_manager(self) -> Optional[ConsensusManager]:
try:
client = RepoRegistryContractClient(REPO_REGISTRY_CONTRACT_ADDRESS, self.subtensor)
backend = ContractBackend(client, self.subtensor, self.wallet, self.config.netuid)
cache_dir = Path(self.config.neuron.full_path)
return ConsensusManager(
backend=backend,
cache_dir=Path(self.config.neuron.full_path),
cache_dir=cache_dir,
store_hook=self._store_weight_consensus if self.db_storage else None,
registry_loader=RegistryLoader(client, cache_dir),
)
except Exception as e:
bt.logging.warning(f'weight_consensus: backend init failed ({e}); using baked-in weights')
Expand Down
1 change: 1 addition & 0 deletions tests/validator/test_consensus_apply.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def test_adds_novel_repo_with_defaults(self):
assert novel.emission_share == 0.5
assert novel.maintainer_cut == 0.0
assert novel.default_label_multiplier == 1.0
assert novel.issue_discovery_share == 0.0 # regression: default was 0.5

def test_none_shares_returns_master_unchanged(self):
master = _master()
Expand Down
Loading