Skip to content

fix: mirror-path _is_valid_linked_issue accepts cross-repo Closes references #1243

Description

@aliangm

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.

  1. 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.
  2. B files trivial issue #X in B/throwaway-repo (any repo B controls). State: OPEN. No real bug.
  3. Miner submits PR P_M to a registered repo (e.g. entrius/gittensor) with Closes B/throwaway-repo#X in the body.
  4. 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.
  5. 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).
  6. 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.
  7. _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
  8. 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

  • MirrorLinkedIssue carries repository_full_name: Optional[str], populated from data.get('repository_full_name'), defaulting to None.
  • _is_valid_linked_issue rejects an issue whose repository_full_name is known (not None) and differs from pr.repo_full_name (case-insensitive).
  • When repository_full_name is None, the issue passes the cross-repo check — preserves current behavior on existing mirror snapshots until upstream ships the field.
  • Test fixtures in tests/validator/oss_contributions/mirror/test_scoring.py:
    • Cross-repo linked issue with known mismatched repository_full_name_is_valid_linked_issue returns False; _calculate_issue_multiplier returns 1.0.
    • Same-repo linked issue with known matching repository_full_name → behaves as before, returns STANDARD_ISSUE_MULTIPLIER / MAINTAINER_ISSUE_MULTIPLIER.
    • Linked issue with repository_full_name=None (older mirror snapshot) → behaves as before, no regression.
  • PR description coordinates with das-github-mirror maintainers: link the upstream issue/PR adding the field, mark this validator-side guard as Layer 1 of 2.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions