Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
25 changes: 17 additions & 8 deletions gittensor/classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,21 @@ class MinerEvaluation:
# The top-level scalars above are round-level rollups of this map.
repo_evaluations: Dict[str, RepoEvaluation] = field(default_factory=dict)

def get_or_create_repo_evaluation(
self, repo_name: str, repository_full_name: Optional[str] = None
) -> RepoEvaluation:
"""Return the repo evaluation stored under ``repo_name``, creating one if absent.

``repo_name`` is the map key (a lowercased repository_full_name). When a
new entry is created, ``repository_full_name`` seeds it, defaulting to
``repo_name`` when not supplied.
"""
repo_eval = self.repo_evaluations.get(repo_name)
if repo_eval is None:
repo_eval = RepoEvaluation(repository_full_name=repository_full_name or repo_name)
self.repo_evaluations[repo_name] = repo_eval
return repo_eval

@property
def total_prs(self) -> int:
return self.total_merged_prs + self.total_closed_prs + self.total_open_prs
Expand Down Expand Up @@ -578,10 +593,7 @@ def store(self, evaluation: 'MinerEvaluation') -> None:
value = getattr(existing.evaluation, name)
setattr(cached_eval, name, _copy_issue_discovery_value(name, value))
for repo_name, prior_repo in existing.evaluation.repo_evaluations.items():
target = cached_eval.repo_evaluations.get(repo_name)
if target is None:
target = RepoEvaluation(repository_full_name=prior_repo.repository_full_name)
cached_eval.repo_evaluations[repo_name] = target
target = cached_eval.get_or_create_repo_evaluation(repo_name, prior_repo.repository_full_name)
target.copy_issue_discovery_from(prior_repo)

self._cache[evaluation.uid] = CachedEvaluation(
Expand Down Expand Up @@ -621,10 +633,7 @@ def update_issue_discovery(self, evaluation: 'MinerEvaluation') -> None:
setattr(existing.evaluation, name, _copy_issue_discovery_value(name, value))

for repo_name, repo_eval in evaluation.repo_evaluations.items():
target = existing.evaluation.repo_evaluations.get(repo_name)
if target is None:
target = RepoEvaluation(repository_full_name=repo_eval.repository_full_name)
existing.evaluation.repo_evaluations[repo_name] = target
target = existing.evaluation.get_or_create_repo_evaluation(repo_name, repo_eval.repository_full_name)
target.copy_issue_discovery_from(repo_eval)

bt.logging.debug(f'Refreshed cached issue discovery for UID {evaluation.uid}')
Expand Down
20 changes: 18 additions & 2 deletions gittensor/constants.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Entrius 2025
import os
import re
from typing import Dict

Expand Down Expand Up @@ -34,6 +35,21 @@
MIRROR_HTTP_TIMEOUT_SECONDS = 30
MIRROR_MAX_ATTEMPTS = 3

# =============================================================================
# das-gittensor API (https://api.gittensor.io) — repository hyperparameter registry
# =============================================================================
# GET /repos returns the full master-repository registry (full_name -> raw config),
# the sole source of truth for repository hyperparameters. On every successful
# fetch the validator writes the registry to an on-disk last-good cache; when the
# API is unreachable it falls back to that cache (and to built-in knob defaults if
# no cache exists). There is no bundled master_repositories.json.
GITTENSOR_API_DEFAULT_URL = 'https://api.gittensor.io'
REPOS_API_TIMEOUT_SECONDS = 15
REPOS_API_MAX_ATTEMPTS = 3
# On-disk last-good cache for the repos registry. Env-configurable; defaults under
# the user's home so it survives validator restarts. '~' is expanded at use.
REPOS_CACHE_PATH = os.getenv('GITTENSOR_REPOS_CACHE_PATH', '~/.gittensor/cache/repos_registry.json')

# =============================================================================
# Language & File Scoring
# =============================================================================
Expand Down Expand Up @@ -123,14 +139,14 @@
# =============================================================================
# Eligibility Gate (OSS Contributions)
# =============================================================================
# Per-repo defaults — each repo may override these in master_repositories.json.
# Per-repo defaults — each repo may override these via its registry config (GET /repos).
MIN_VALID_MERGED_PRS = 3 # minimum merged PRs (per repo) to receive score
MIN_CREDIBILITY = 0.80 # minimum credibility ratio to receive score

# =============================================================================
# Issue Discovery
# =============================================================================
# Eligibility gate — per-repo defaults, overridable in master_repositories.json.
# Eligibility gate — per-repo defaults, overridable via the registry config (GET /repos).
MIN_VALID_SOLVED_ISSUES = 3 # minimum solved issues where solving PR has token_score >= MIN_TOKEN_SCORE_FOR_VALID_ISSUE
MIN_ISSUE_CREDIBILITY = 0.80 # minimum issue credibility ratio
MIN_TOKEN_SCORE_FOR_VALID_ISSUE = 5 # solving-PR token_score for a solved issue to count as "valid"
Expand Down
4 changes: 2 additions & 2 deletions gittensor/validator/emission_allocation.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def blend_emission_pools(

# Issue treasury (10% flat to UID 111)
if ISSUES_TREASURY_UID > 0 and ISSUES_TREASURY_UID in miner_uids:
treasury_idx = sorted_uids.index(ISSUES_TREASURY_UID)
treasury_idx = uid_index[ISSUES_TREASURY_UID]
rewards[treasury_idx] += ISSUES_TREASURY_EMISSION_SHARE
bt.logging.info(
f'Treasury allocation: UID {ISSUES_TREASURY_UID} receives '
Expand All @@ -67,7 +67,7 @@ def blend_emission_pools(

# Recycle receives registry slack and empty repo slices.
if RECYCLE_UID in miner_uids:
recycle_idx = sorted_uids.index(RECYCLE_UID)
recycle_idx = uid_index[RECYCLE_UID]
rewards[recycle_idx] += recycle_share
if recycle_share > EMISSION_SHARE_TOLERANCE:
bt.logging.info(f'Recycling {recycle_share * 100:.0f}% unclaimed emissions from repo allocation')
Expand Down
21 changes: 21 additions & 0 deletions gittensor/validator/issue_competitions/contract_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,22 @@ def load_contract_metadata() -> Tuple[Dict[str, bytes], Dict[str, List]]:
CONTRACT_SELECTORS, CONTRACT_ARG_TYPES = load_contract_metadata()


def _scale_compact_length(n: int) -> bytes:
"""SCALE-encode a non-negative integer as a compact length prefix.

Used to prefix variable-length SCALE payloads (Vec<u8>, String).
"""
if n < 0:
raise ValueError(f'Length must be non-negative: {n}')
if n < 1 << 6:
return bytes([n << 2])
if n < 1 << 14:
return ((n << 2) | 1).to_bytes(2, 'little')
if n < 1 << 30:
return ((n << 2) | 2).to_bytes(4, 'little')
raise ValueError(f'Length too large for compact encoding: {n}')


class IssueStatus(Enum):
"""Status of an issue in its lifecycle"""

Expand Down Expand Up @@ -568,6 +584,11 @@ def _encode_args(self, method_name: str, args: dict) -> bytes:
encoded += struct.pack('<Q', value)
elif type_def == 'u128':
encoded += struct.pack('<QQ', value & 0xFFFFFFFFFFFFFFFF, value >> 64)
elif type_def == 'str':
if not isinstance(value, str):
raise ValueError(f'Expected str for {arg_name}, got {type(value).__name__}')
data = value.encode('utf-8')
encoded += _scale_compact_length(len(data)) + data
elif type_def == 'AccountId':
if isinstance(value, str):
encoded += bytes.fromhex(self.subtensor.substrate.ss58_decode(value))
Expand Down
17 changes: 4 additions & 13 deletions gittensor/validator/issue_discovery/scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@

import bittensor as bt

from gittensor.classes import Issue, MinerEvaluation, MinerEvaluationCache, RepoEvaluation
from gittensor.classes import Issue, MinerEvaluation, MinerEvaluationCache
from gittensor.constants import (
MAINTAINER_ASSOCIATIONS,
)
Expand Down Expand Up @@ -278,10 +278,7 @@ def _apply_open_issue_counts(evaluation: MinerEvaluation, open_counts: Dict[str,
"""Record per-repo open-issue counts (and the round-level total) for a miner
with no in-window issues to score."""
for repo_name, count in open_counts.items():
repo_eval = evaluation.repo_evaluations.get(repo_name)
if repo_eval is None:
repo_eval = RepoEvaluation(repository_full_name=repo_name)
evaluation.repo_evaluations[repo_name] = repo_eval
repo_eval = evaluation.get_or_create_repo_evaluation(repo_name)
repo_eval.total_open_issues = count
evaluation.total_open_issues = sum(open_counts.values())

Expand All @@ -297,10 +294,7 @@ def _copy_issue_discovery_fields(target: MinerEvaluation, source: MinerEvaluatio
target.total_open_issues = source.total_open_issues
target.issue_discovery_issues = list(source.issue_discovery_issues)
for repo_name, source_repo in source.repo_evaluations.items():
target_repo = target.repo_evaluations.get(repo_name)
if target_repo is None:
target_repo = RepoEvaluation(repository_full_name=source_repo.repository_full_name)
target.repo_evaluations[repo_name] = target_repo
target_repo = target.get_or_create_repo_evaluation(repo_name, source_repo.repository_full_name)
target_repo.copy_issue_discovery_from(source_repo)


Expand Down Expand Up @@ -560,10 +554,7 @@ def _finalize_repo_issue_scores(
acc = repo_acc.get(repo_name) or _RepoIssueAcc()
open_count = open_counts.get(repo_name, 0)

repo_eval = evaluation.repo_evaluations.get(repo_name)
if repo_eval is None:
repo_eval = RepoEvaluation(repository_full_name=repo_name)
evaluation.repo_evaluations[repo_name] = repo_eval
repo_eval = evaluation.get_or_create_repo_evaluation(repo_name)

repo_eval.total_solved_issues = acc.solved
repo_eval.total_valid_solved_issues = acc.valid_solved
Expand Down
Loading