Skip to content
Merged
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
14 changes: 8 additions & 6 deletions gittensor/validator/issue_competitions/forward.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ async def issue_competitions(
1. Harvest emissions into the bounty pool
2. Get active issues from the smart contract
3. For each active issue, check GitHub:
- If solved by eligible miner -> vote_solution
- If closed but not by eligible miner -> vote_cancel_issue
- If solved by a registered miner with a non-failed identity -> vote_solution
- If closed without a registered solver identity -> vote_cancel_issue

Args:
self: The validator instance
Expand Down Expand Up @@ -61,13 +61,15 @@ async def issue_competitions(
if harvest_result and harvest_result.get('status') == 'success':
bt.logging.success(f'Harvested emissions! Extrinsic: {harvest_result.get("tx_hash", "")}')

# Build mapping of github_id->hotkey for every registered miner. Bounty
# payouts are not eligibility-gated — any miner who solves a bounty
# issue can receive the reward.
# Build mapping of github_id->hotkey for every non-failed registered
# miner. Bounty payouts are not normal eligibility-gated, but failed
# evaluations are not valid payout targets. Duplicate GitHub IDs are
# penalized in oss_contributions() before this pass and arrive here as
# failed evaluations.
registered_miners = {
eval.github_id: eval.hotkey
for eval in miner_evaluations.values()
if eval.github_id and eval.github_id != '0'
if eval.github_id and eval.github_id != '0' and eval.failed_reason is None
}
bt.logging.info(
f'Issue bounties: {len(registered_miners)} registered miners out of {len(miner_evaluations)} total'
Expand Down
39 changes: 39 additions & 0 deletions tests/validator/test_issue_competitions_forward.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,42 @@ def test_ineligible_miner_still_receives_solution_vote():

contract_client.vote_solution.assert_called_once()
contract_client.vote_cancel_issue.assert_not_called()


def test_failed_miner_does_not_receive_solution_vote():
validator = _make_validator()
contract_client = MagicMock()
contract_client.harvest_emissions.return_value = None
contract_client.get_issues_by_status.return_value = [_make_issue()]
contract_client.vote_cancel_issue.return_value = True

miner_eval = MinerEvaluation(uid=5, hotkey='hk5', github_id='999')
miner_eval.failed_reason = 'Penalized: duplicate GitHub account'

with (
patch('gittensor.validator.issue_competitions.forward.GITTENSOR_VALIDATOR_PAT', 'ghp_validator'),
patch('gittensor.validator.issue_competitions.forward.get_contract_address', return_value='5Contract'),
patch(
'gittensor.validator.issue_competitions.forward.check_github_issue_closed',
return_value={
'is_closed': True,
'solver_github_id': '999',
'pr_number': 42,
'solver_lookup_failed': False,
},
),
patch('gittensor.validator.issue_competitions.forward.get_miner_coldkey') as mock_get_coldkey,
patch(
'gittensor.validator.issue_competitions.forward.IssueCompetitionContractClient',
return_value=contract_client,
),
):
_run(issue_competitions(cast(Any, validator), {5: miner_eval}))

mock_get_coldkey.assert_not_called()
contract_client.vote_solution.assert_not_called()
contract_client.vote_cancel_issue.assert_called_once_with(
issue_id=7,
reason='Issue closed externally (not by a registered miner, solver: 999)',
wallet=validator.wallet,
)
Loading