Summary
PR #1038 fixed cross-repo linked-issue leakage on the legacy OSS scoring path (issue #1019). The mirror path was explicitly scoped out at the time because the mirror payload didn't carry repository identity on linked issues:
Out of scope: bounty solver cross-reference filtering is left to #1042, and mirror scoring is not changed because live mirror linked-issue payloads do not currently guarantee linked issue repository identity.
After PR #1202 stripped the legacy pipeline, mirror/scoring.py::_is_valid_linked_issue is the only PR scoring path. The leak that was tolerable when legacy was authoritative is now the entire surface, and the validator has no way to reject cross-repo Closes other-owner/repo#N references for the issue-bonus multiplier.
Failure mode
MirrorLinkedIssue (gittensor/utils/mirror/models.py:75-110) has no repository identity field:
@dataclass
class MirrorLinkedIssue:
number: int
title: str
state: str
state_reason: Optional[str]
author_github_id: Optional[str]
author_association: Optional[str]
created_at: Optional[datetime]
closed_at: Optional[datetime]
updated_at: Optional[datetime]
is_transferred: bool
solved_by_pr: Optional[int]
labels: List[MirrorLabel] = field(default_factory=list)
# NB: no repo_full_name / repository field
_is_valid_linked_issue (gittensor/validator/oss_contributions/mirror/scoring.py:429-487) walks every anti-gaming gate from the legacy path (transferred, self-issue, created-after-PR, state_reason=COMPLETED, edited_after_merge, CLOSED, close-window) except the repository-identity gate — because the field isn't on the payload to gate on.
_calculate_issue_multiplier (scoring.py:399-426) then applies STANDARD_ISSUE_MULTIPLIER = 1.33 or MAINTAINER_ISSUE_MULTIPLIER = 1.66 to the PR's earned score, regardless of which repository the linked issue actually belongs to.
Attack walkthrough
Requires one alt/coordinated GitHub account, no special privileges on the target repo. Re-runnable per PR.
- Miner has GitHub account A. Coordinates with account B (or controls a second account). Cannot be the same account —
_is_valid_linked_issue rejects author == pr_author.
- B files trivial issue
#X in B/throwaway-repo (any repo B controls). State: OPEN. No real bug.
- Miner submits PR
P_M to a registered repo (e.g. entrius/gittensor) with Closes B/throwaway-repo#X in the body.
P_M passes review and merges into entrius/gittensor. The cross-repo close keyword does not auto-close #X — A has no write access to B/throwaway-repo. #X stays OPEN at merge time.
- Within 24 hours of
P_M merging, B manually closes #X as COMPLETED (or merges any cosmetic PR in B/throwaway-repo that auto-closes it).
- das-github-mirror surfaces
P_M.linked_issues = [MirrorLinkedIssue(number=X, state='CLOSED', state_reason='COMPLETED', author_github_id=B, ...)] — no repository identity on the payload.
_is_valid_linked_issue runs:
is_transferred=False ✓
author_github_id (B) != pr.author_github_id (A) ✓
state='CLOSED', state_reason='COMPLETED' ✓
closed_at - merged_at < 1 day ✓
- Repository identity check: not performed — field doesn't exist
- Multiplier applied: 1.33× (STANDARD), or 1.66× if B has
OWNER/MEMBER/COLLABORATOR on B/throwaway-repo (which is trivial if B owns it).
Per-PR uplift: 1.33×–1.66× on earned_score. Orchestration cost: one alt GitHub account + one trivial issue per PR. Detection: indistinguishable from a legitimate same-repo Closes #N in any log line.
Why this only matters now
Pre-#1202, the cross-repo close was caught by the legacy OSS path because PR #1038 fetched repository { nameWithOwner } and filtered. The mirror path was a parallel scoring track but not the authoritative one for closingIssuesReferences semantics; the legacy path's filter caught the leak in the round's final score.
Post-#1202, mirror/scoring.py::_is_valid_linked_issue is the sole consumer of pr.linked_issues for the issue-bonus multiplier. The legacy filter is gone. The mirror payload's missing repository_full_name is now load-bearing.
Two-layer fix
This is genuinely a coordinated change because the data is missing client-side.
Layer 1 — validator-side defensive guard (this repo, can land standalone)
Until the mirror exposes repository identity, the validator should reject linked issues whose repository is known to differ from the PR's repo. Same shape as PR #1038's legacy fix:
# In _is_valid_linked_issue, after the transferred / author / state_reason gates:
li_repo = getattr(li, 'repository_full_name', None)
if li_repo is not None and li_repo.lower() != pr.repo_full_name.lower():
bt.logging.warning(
f'Skipping linked issue #{li.number} - cross-repo reference '
f'(issue in {li_repo}, PR in {pr.repo_full_name})'
)
return False
While MirrorLinkedIssue.repository_full_name doesn't exist, this guard is a no-op. The day Layer 2 lands, the guard activates without further validator-side changes.
Layer 2 — das-github-mirror upstream (separate repo, coordinated)
The mirror's PR response should include repository_full_name on each linked_issues entry (or the GraphQL-shaped repository.name_with_owner). MirrorLinkedIssue.from_dict then plumbs it through:
@classmethod
def from_dict(cls, data: dict) -> 'MirrorLinkedIssue':
return cls(
...,
repository_full_name=data.get('repository_full_name'), # new
...,
)
Track the upstream change as a follow-up. Layer 1 ships independently and gives the validator the gate ready to fire the moment the field is populated.
Why not reject on missing field
A narrower variant — reject any linked issue whose repository_full_name is None — would over-correct today. Every mirror response carries None in that field until Layer 2 lands, so this variant would zero every issue multiplier in the system. The proposed guard fails open (allows the issue) when the field is unknown and fails closed (rejects) when the field disagrees. Once Layer 2 ships and the mirror is fully repopulated, a follow-up issue can tighten the guard to fail-closed-on-unknown.
Acceptance criteria
Related
Summary
PR #1038 fixed cross-repo linked-issue leakage on the legacy OSS scoring path (issue #1019). The mirror path was explicitly scoped out at the time because the mirror payload didn't carry repository identity on linked issues:
After PR #1202 stripped the legacy pipeline,
mirror/scoring.py::_is_valid_linked_issueis the only PR scoring path. The leak that was tolerable when legacy was authoritative is now the entire surface, and the validator has no way to reject cross-repoCloses other-owner/repo#Nreferences for the issue-bonus multiplier.Failure mode
MirrorLinkedIssue(gittensor/utils/mirror/models.py:75-110) has no repository identity field:_is_valid_linked_issue(gittensor/validator/oss_contributions/mirror/scoring.py:429-487) walks every anti-gaming gate from the legacy path (transferred, self-issue, created-after-PR, state_reason=COMPLETED, edited_after_merge, CLOSED, close-window) except the repository-identity gate — because the field isn't on the payload to gate on._calculate_issue_multiplier(scoring.py:399-426) then appliesSTANDARD_ISSUE_MULTIPLIER = 1.33orMAINTAINER_ISSUE_MULTIPLIER = 1.66to the PR's earned score, regardless of which repository the linked issue actually belongs to.Attack walkthrough
Requires one alt/coordinated GitHub account, no special privileges on the target repo. Re-runnable per PR.
_is_valid_linked_issuerejectsauthor == pr_author.#XinB/throwaway-repo(any repo B controls). State: OPEN. No real bug.P_Mto a registered repo (e.g.entrius/gittensor) withCloses B/throwaway-repo#Xin the body.P_Mpasses review and merges intoentrius/gittensor. The cross-repo close keyword does not auto-close#X— A has no write access toB/throwaway-repo.#Xstays OPEN at merge time.P_Mmerging, B manually closes#XasCOMPLETED(or merges any cosmetic PR inB/throwaway-repothat auto-closes it).P_M.linked_issues = [MirrorLinkedIssue(number=X, state='CLOSED', state_reason='COMPLETED', author_github_id=B, ...)]— no repository identity on the payload._is_valid_linked_issueruns:is_transferred=False✓author_github_id (B) != pr.author_github_id (A)✓state='CLOSED', state_reason='COMPLETED'✓closed_at - merged_at < 1 day✓OWNER/MEMBER/COLLABORATORonB/throwaway-repo(which is trivial if B owns it).Per-PR uplift: 1.33×–1.66× on
earned_score. Orchestration cost: one alt GitHub account + one trivial issue per PR. Detection: indistinguishable from a legitimate same-repoCloses #Nin any log line.Why this only matters now
Pre-#1202, the cross-repo close was caught by the legacy OSS path because PR #1038 fetched
repository { nameWithOwner }and filtered. The mirror path was a parallel scoring track but not the authoritative one forclosingIssuesReferencessemantics; the legacy path's filter caught the leak in the round's final score.Post-#1202,
mirror/scoring.py::_is_valid_linked_issueis the sole consumer ofpr.linked_issuesfor the issue-bonus multiplier. The legacy filter is gone. The mirror payload's missingrepository_full_nameis now load-bearing.Two-layer fix
This is genuinely a coordinated change because the data is missing client-side.
Layer 1 — validator-side defensive guard (this repo, can land standalone)
Until the mirror exposes repository identity, the validator should reject linked issues whose repository is known to differ from the PR's repo. Same shape as PR #1038's legacy fix:
While
MirrorLinkedIssue.repository_full_namedoesn't exist, this guard is a no-op. The day Layer 2 lands, the guard activates without further validator-side changes.Layer 2 — das-github-mirror upstream (separate repo, coordinated)
The mirror's PR response should include
repository_full_nameon eachlinked_issuesentry (or the GraphQL-shapedrepository.name_with_owner).MirrorLinkedIssue.from_dictthen plumbs it through:Track the upstream change as a follow-up. Layer 1 ships independently and gives the validator the gate ready to fire the moment the field is populated.
Why not reject on missing field
A narrower variant — reject any linked issue whose
repository_full_name is None— would over-correct today. Every mirror response carriesNonein that field until Layer 2 lands, so this variant would zero every issue multiplier in the system. The proposed guard fails open (allows the issue) when the field is unknown and fails closed (rejects) when the field disagrees. Once Layer 2 ships and the mirror is fully repopulated, a follow-up issue can tighten the guard to fail-closed-on-unknown.Acceptance criteria
MirrorLinkedIssuecarriesrepository_full_name: Optional[str], populated fromdata.get('repository_full_name'), defaulting toNone._is_valid_linked_issuerejects an issue whoserepository_full_nameis known (notNone) and differs frompr.repo_full_name(case-insensitive).repository_full_name is None, the issue passes the cross-repo check — preserves current behavior on existing mirror snapshots until upstream ships the field.tests/validator/oss_contributions/mirror/test_scoring.py:repository_full_name→_is_valid_linked_issuereturnsFalse;_calculate_issue_multiplierreturns1.0.repository_full_name→ behaves as before, returnsSTANDARD_ISSUE_MULTIPLIER/MAINTAINER_ISSUE_MULTIPLIER.repository_full_name=None(older mirror snapshot) → behaves as before, no regression.das-github-mirrormaintainers: link the upstream issue/PR adding the field, mark this validator-side guard as Layer 1 of 2.Related
_PR_TIMELINE_QUERY'sclosingIssuesReferencesfragment — addedrepository { nameWithOwner }).