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
1 change: 1 addition & 0 deletions gittensor/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
# =============================================================================
DEFAULT_REPO_WEIGHT = 0.01 # fallback weight for repos not in master_repositories.json
PR_LOOKBACK_DAYS = 35 # rolling window for scoring
REGISTRATION_GRACE_DAYS = 7
MERGED_PR_BASE_SCORE = 25
MIN_TOKEN_SCORE_FOR_BASE_SCORE = 5 # PRs below this get 0 base score
MAX_CONTRIBUTION_BONUS = 25
Expand Down
29 changes: 26 additions & 3 deletions gittensor/utils/github_api_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,7 @@ def try_add_open_or_closed_pr(
pr_raw: Dict,
pr_state: str,
lookback_date_filter: datetime,
registration_cutoff: Optional[datetime] = None,
) -> None:
"""
Attempts to add an OPEN or CLOSED PR to miner_eval if eligible.
Expand All @@ -795,6 +796,7 @@ def try_add_open_or_closed_pr(
pr_raw: Raw PR data from GraphQL
pr_state: GitHub PR state (OPEN, CLOSED, MERGED)
lookback_date_filter: Date filter for lookback period
registration_cutoff: Skip closed PRs whose closed_at is earlier than this cutoff
"""
# Ignore all maintainer contributions
if not os.environ.get('DEV_MODE') and pr_raw.get('authorAssociation') in MAINTAINER_ASSOCIATIONS:
Expand Down Expand Up @@ -822,6 +824,9 @@ def try_add_open_or_closed_pr(
miner_eval.add_stale_closed_pull_request(pr_raw)
return

if registration_cutoff is not None and closed_dt < registration_cutoff:
return

if closed_dt >= lookback_date_filter:
miner_eval.add_closed_pull_request(pr_raw)

Expand All @@ -831,6 +836,7 @@ def should_skip_merged_pr(
repository_full_name: str,
repo_config: RepositoryConfig,
lookback_date_filter: datetime,
registration_cutoff: Optional[datetime] = None,
) -> tuple[bool, Optional[str]]:
"""
Validate a merged PR against all eligibility criteria.
Expand All @@ -840,6 +846,7 @@ def should_skip_merged_pr(
repository_full_name (str): Full repository name (owner/repo)
repo_config (RepositoryConfig): Repository configuration
lookback_date_filter (datetime): Date filter for lookback period
registration_cutoff (Optional[datetime]): Skip merged PRs whose merged_at is earlier than this cutoff

Returns:
tuple[bool, Optional[str]]: (should_skip, skip_reason) - True if PR should be skipped with reason
Expand All @@ -857,6 +864,12 @@ def should_skip_merged_pr(
f'Skipping PR #{pr_raw["number"]} in {repository_full_name} - merged before {PR_LOOKBACK_DAYS}-day lookback window',
)

if registration_cutoff is not None and merged_dt < registration_cutoff:
return (
True,
f'Skipping PR #{pr_raw["number"]} in {repository_full_name} - merged before miner registration on this validator',
)

# Skip if PR author is a maintainer
author_association = pr_raw.get('authorAssociation')
if not os.environ.get('DEV_MODE') and author_association in MAINTAINER_ASSOCIATIONS:
Expand Down Expand Up @@ -912,7 +925,10 @@ def should_skip_merged_pr(


def load_miners_prs(
miner_eval: MinerEvaluation, master_repositories: Dict[str, RepositoryConfig], max_prs: int = 1000
miner_eval: MinerEvaluation,
master_repositories: Dict[str, RepositoryConfig],
max_prs: int = 1000,
registration_cutoff: Optional[datetime] = None,
) -> None:
"""
Fetches user PRs via GraphQL API and categorize them by state.
Expand All @@ -922,6 +938,7 @@ def load_miners_prs(
miner_eval: The MinerEvaluation object containing github details + more
master_repositories: Repository metadata (name -> RepositoryConfig)
max_prs: Maximum merged PRs to fetch
registration_cutoff: Skip PRs (merged or closed) whose effective timestamp predates this cutoff
"""
bt.logging.info('*****Fetching PRs*****')
miner_eval.github_pr_fetch_failed = False
Expand Down Expand Up @@ -1005,11 +1022,17 @@ def load_miners_prs(
continue

if pr_state in (PRState.OPEN.value, PRState.CLOSED.value):
try_add_open_or_closed_pr(miner_eval, pr_raw, pr_state, lookback_date_filter)
try_add_open_or_closed_pr(
miner_eval, pr_raw, pr_state, lookback_date_filter, registration_cutoff=registration_cutoff
)
continue

should_skip, skip_reason = should_skip_merged_pr(
pr_raw, repository_full_name, repo_config, lookback_date_filter
pr_raw,
repository_full_name,
repo_config,
lookback_date_filter,
registration_cutoff=registration_cutoff,
)

if should_skip:
Expand Down
12 changes: 11 additions & 1 deletion gittensor/validator/oss_contributions/mirror/load.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,15 @@ def load_mirror_miner_prs(
mirror_eval: MirrorMinerEvaluation,
mirror_repos: Dict[str, RepositoryConfig],
client: Optional[MirrorClient] = None,
registration_cutoff: Optional[datetime] = None,
) -> None:
"""Populate mirror_eval with PRs fetched from the mirror service.

Args:
mirror_eval: container to populate; must already have github_id set
mirror_repos: repo configs to filter against (only mirror_enabled entries)
client: optional MirrorClient for dependency injection in tests
registration_cutoff: skip merged/closed PRs whose effective timestamp predates this cutoff
"""

bt.logging.info('*****Fetching PRs from mirror*****')
Expand All @@ -65,7 +67,7 @@ def load_mirror_miner_prs(

for pr in response.pull_requests:
try:
_maybe_add_pr(mirror_eval, pr, mirror_repos, lookback_date)
_maybe_add_pr(mirror_eval, pr, mirror_repos, lookback_date, registration_cutoff=registration_cutoff)
except Exception as e:
bt.logging.warning(f'Error processing mirror PR #{pr.pr_number} ({pr.repo_full_name}): {e}')

Expand All @@ -80,6 +82,7 @@ def _maybe_add_pr(
pr: MirrorPullRequest,
mirror_repos: Dict[str, RepositoryConfig],
lookback_date: datetime,
registration_cutoff: Optional[datetime] = None,
) -> None:
"""Apply load-time filters and bucket pr by state if it passes."""

Expand Down Expand Up @@ -112,12 +115,19 @@ def _maybe_add_pr(
# closing an old PR shouldn't trigger a fresh credibility penalty).
if pr.created_at < lookback_date:
return
if registration_cutoff is not None and pr.closed_at is not None and pr.closed_at < registration_cutoff:
return
mirror_eval.closed_prs.append(ScoredMirrorPR(pr=pr))
elif pr.state == 'MERGED':
# Apply the merge-eligibility gate at LOAD time (matches legacy parity —
# should_skip_merged_pr runs inside load_miners_prs before adding). If we
# deferred to scoring, rejected PRs would remain in mirror_merged_prs and
# inflate the merged_count used in check_eligibility, distorting credibility.
if registration_cutoff is not None and pr.merged_at is not None and pr.merged_at < registration_cutoff:
bt.logging.debug(
f'Skipping mirror PR #{pr.pr_number} in {pr.repo_full_name} - merged before miner registration on this validator'
)
return
candidate = ScoredMirrorPR(pr=pr)
should_skip, reason = _should_skip_merged_mirror_pr(candidate, repo_config)
if should_skip:
Expand Down
12 changes: 10 additions & 2 deletions gittensor/validator/oss_contributions/reward.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# Copyright © 2025 Entrius
from __future__ import annotations

from datetime import datetime
from typing import TYPE_CHECKING, Dict, Optional, Set, Tuple

import bittensor as bt
Expand Down Expand Up @@ -40,6 +41,7 @@ async def evaluate_miners_pull_requests(
token_config: TokenConfig,
stale_hotkey: Optional[str] = None,
stored_github_id: Optional[str] = None,
registration_cutoff: Optional[datetime] = None,
) -> MinerEvaluation:
"""
Entry point from taking a miners response -> Get PRs -> Score PRs
Expand All @@ -53,6 +55,7 @@ async def evaluate_miners_pull_requests(
token_config: Token-based scoring weights configuration
stale_hotkey: If set, the UID has a stored PAT from this old hotkey (re-registration detected)
stored_github_id: GitHub id recorded when the stored PAT was accepted
registration_cutoff: If set, skip merged/closed PRs whose effective timestamp predates this cutoff

Returns:
MinerEvaluation: The object containing scores, valid_prs, etc.
Expand Down Expand Up @@ -87,7 +90,7 @@ async def evaluate_miners_pull_requests(

if legacy_repos:
with session_scope():
load_miners_prs(miner_eval, legacy_repos)
load_miners_prs(miner_eval, legacy_repos, registration_cutoff=registration_cutoff)
score_miner_prs(miner_eval, legacy_repos, programming_languages, token_config)

if mirror_repos:
Expand All @@ -97,7 +100,9 @@ async def evaluate_miners_pull_requests(
github_id=miner_eval.github_id,
)
with MirrorClient() as mirror_client:
load_mirror_miner_prs(mirror_eval, mirror_repos, client=mirror_client)
load_mirror_miner_prs(
mirror_eval, mirror_repos, client=mirror_client, registration_cutoff=registration_cutoff
)
score_mirror_miner_prs(mirror_eval, mirror_repos, programming_languages, token_config, client=mirror_client)
combine(miner_eval, mirror_eval)

Expand Down Expand Up @@ -147,6 +152,8 @@ async def get_rewards(
else:
stale_hotkey = pat_entry.get('hotkey')

registration_cutoff = pat_storage.get_registration_cutoff(uid, hotkey)

# Calculate score
miner_evaluation = await evaluate_miners_pull_requests(
uid,
Expand All @@ -157,6 +164,7 @@ async def get_rewards(
token_config,
stale_hotkey=stale_hotkey,
stored_github_id=stored_github_id,
registration_cutoff=registration_cutoff,
)
miner_evaluations[uid] = miner_evaluation

Expand Down
32 changes: 30 additions & 2 deletions gittensor/validator/pat_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@
import os
import tempfile
import threading
from datetime import datetime, timezone
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Optional

from gittensor.constants import REGISTRATION_GRACE_DAYS

PATS_FILE = Path(__file__).resolve().parents[2] / 'data' / 'miner_pats.json'

_lock = threading.Lock()
Expand All @@ -37,14 +39,25 @@ def save_pat(uid: int, hotkey: str, pat: str, github_id: str) -> None:
"""Upsert a PAT entry by UID. Creates the file if needed."""
with _lock:
entries = _read_file()
now_iso = datetime.now(timezone.utc).isoformat()

prior = next((e for e in entries if e.get('uid') == uid), None)

first_registered_at: Optional[str]
if prior is not None and prior.get('hotkey') == hotkey:
first_registered_at = prior.get('first_registered_at') if 'first_registered_at' in prior else None
else:
first_registered_at = now_iso

entry = {
'uid': uid,
'hotkey': hotkey,
'pat': pat,
'github_id': github_id,
'stored_at': datetime.now(timezone.utc).isoformat(),
'stored_at': now_iso,
}
if first_registered_at is not None:
entry['first_registered_at'] = first_registered_at

for i, existing in enumerate(entries):
if existing.get('uid') == uid:
Expand All @@ -56,6 +69,21 @@ def save_pat(uid: int, hotkey: str, pat: str, github_id: str) -> None:
_write_file(entries)


def get_registration_cutoff(uid: int, hotkey: str) -> Optional[datetime]:
"""Return the registration scoring cutoff for this miner, or None when no gate applies."""
entry = get_pat_by_uid(uid)
if entry is None or entry.get('hotkey') != hotkey:
return None
raw = entry.get('first_registered_at')
if not raw:
return None
try:
first_registered_at = datetime.fromisoformat(raw)
except ValueError:
return None
return first_registered_at - timedelta(days=REGISTRATION_GRACE_DAYS)


def get_pat_by_uid(uid: int) -> Optional[dict]:
"""Look up a single PAT entry by UID. Returns None if not found."""
with _lock:
Expand Down
Loading
Loading