Skip to content
Open
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
29 changes: 22 additions & 7 deletions gittensor/classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import bittensor as bt

from gittensor.constants import MIN_TOKEN_SCORE_FOR_BASE_SCORE
from gittensor.utils.utils import parse_repo_name
from gittensor.validator.configurations.tier_config import Tier, TierConfig, TierStats

Expand Down Expand Up @@ -156,7 +157,8 @@ class PullRequest:
base_score: float = 0.0
issue_multiplier: float = 1.0
open_pr_spam_multiplier: float = 1.0
repository_uniqueness_multiplier: float = 1.0
pioneer_dividend: float = 0.0 # Additive bonus for pioneering a repo
pioneer_rank: int = 0 # 0 = not eligible, 1 = pioneer, 2+ = follower position
time_decay_multiplier: float = 1.0
credibility_multiplier: float = 1.0
raw_credibility: float = 1.0 # Before applying ^k scalar
Expand Down Expand Up @@ -188,24 +190,37 @@ def set_file_changes(self, file_changes: List[FileChange]) -> None:
"""Set the file changes for this pull request"""
self.file_changes = file_changes

def is_pioneer_eligible(self) -> bool:
"""Check if this PR qualifies for pioneer consideration.

A PR is eligible if it is merged, has a tier configuration,
and meets the minimum token score quality gate.
"""
return (
self.repository_tier_configuration is not None
and self.merged_at is not None
and self.token_score >= MIN_TOKEN_SCORE_FOR_BASE_SCORE
)

def calculate_final_earned_score(self) -> float:
"""Combine base score with all multipliers."""
"""Combine base score with all multipliers. Pioneer dividend is added separately after."""
multipliers = {
'repo': self.repo_weight_multiplier,
'issue': self.issue_multiplier,
'spam': self.open_pr_spam_multiplier,
'unique': self.repository_uniqueness_multiplier,
'decay': self.time_decay_multiplier,
'cred': self.credibility_multiplier,
}

self.earned_score = self.base_score * prod(multipliers.values())

# Log all multipliers (credibility shows ^k format)
mult_str = ' × '.join(
f'cred={self.raw_credibility:.2f}^{self.credibility_scalar}' if k == 'cred' else f'{k}={v:.2f}'
for k, v in multipliers.items()
)
def _format_multiplier(k: str, v: float) -> str:
if k == 'cred':
return f'cred={self.raw_credibility:.2f}^{self.credibility_scalar}'
return f'{k}={v:.2f}'

mult_str = ' × '.join(_format_multiplier(k, v) for k, v in multipliers.items())
bt.logging.info(
f'├─ {self.pr_state.value} PR #{self.number} ({self.repository_full_name}) → {self.earned_score:.2f}'
)
Expand Down
97 changes: 30 additions & 67 deletions gittensor/cli/issue_commands/mutations.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
gitt harvest
"""

from pathlib import Path

import click
from rich.panel import Panel

Expand Down Expand Up @@ -164,86 +162,51 @@ def issue_register(

try:
import bittensor as bt
from substrateinterface import Keypair, SubstrateInterface
from substrateinterface.contracts import ContractInstance

with console.status('[bold cyan]Connecting to network...', spinner='dots'):
substrate = SubstrateInterface(url=ws_endpoint)
from gittensor.validator.issue_competitions.contract_client import (
IssueCompetitionContractClient,
)

# CLI flags override config; fall back to config if not explicitly supplied
effective_wallet = wallet_name if wallet_name != 'default' else config.get('wallet', wallet_name)
effective_hotkey = wallet_hotkey if wallet_hotkey != 'default' else config.get('hotkey', wallet_hotkey)

# For local development, check config first, then fall back to //Alice
if network_name.lower() == 'local' and effective_wallet == 'default' and effective_hotkey == 'default':
console.print('[dim]Using //Alice for local development (no config set)...[/dim]')
keypair = Keypair.create_from_uri('//Alice')
else:
# Load wallet from config or CLI args
console.print(f'[dim]Loading wallet {effective_wallet}/{effective_hotkey}...[/dim]')
with console.status('[bold cyan]Loading wallet...', spinner='dots'):
wallet = bt.Wallet(name=effective_wallet, hotkey=effective_hotkey)
# Use COLDKEY for owner-only operations (register_issue requires owner)
# Contract owner is set to deployer's coldkey during contract instantiation
keypair = wallet.coldkey

# Load contract
# Go up 4 levels: mutations.py -> issue_commands -> cli -> gittensor -> REPO_ROOT
contract_metadata = (
Path(__file__).parent.parent.parent.parent
/ 'smart-contracts'
/ 'issues-v0'
/ 'target'
/ 'ink'
/ 'issue_bounty_manager.contract'
)
if not contract_metadata.exists():
console.print(f'[red]Error: Contract metadata not found at {contract_metadata}[/red]')
return

contract_instance = ContractInstance.create_from_address(
contract_address=contract_addr,
metadata_file=str(contract_metadata),
substrate=substrate,
)

console.print('[dim]Submitting transaction...[/dim]')

result = contract_instance.exec(
keypair,
'register_issue',
args={
'github_url': github_url,
'repository_full_name': repo,
'issue_number': issue_number,
'target_bounty': bounty_amount,
},
gas_limit={'ref_time': 10_000_000_000, 'proof_size': 1_000_000},
)
with console.status('[bold cyan]Connecting to network...', spinner='dots'):
subtensor = bt.Subtensor(network=ws_endpoint)

# Check if transaction was successful
if hasattr(result, 'is_success') and not result.is_success:
error_info = getattr(result, 'error_message', None)
is_revert = error_info and isinstance(error_info, dict) and error_info.get('name') == 'ContractReverted'
with console.status('[bold cyan]Initializing contract client...', spinner='dots'):
client = IssueCompetitionContractClient(
contract_address=contract_addr,
subtensor=subtensor,
)

if is_revert:
print_error('Contract rejected the request')
console.print('[yellow]Possible reasons:[/yellow]')
console.print(' \u2022 Issue already registered (same repo + issue number)')
console.print(' \u2022 Bounty too low (minimum 10 ALPHA)')
console.print(' \u2022 Caller is not the contract owner')
elif error_info:
print_error(str(error_info))
console.print('[dim]Submitting transaction...[/dim]')

console.print(f'[cyan]Transaction Hash:[/cyan] {result.extrinsic_hash}')
return
tx_hash = client.register_issue(
github_url=github_url,
repository_full_name=repo,
issue_number=issue_number,
target_bounty=bounty_amount,
wallet=wallet,
)

print_success('Issue registered successfully!')
console.print(f'[cyan]Transaction Hash:[/cyan] {result.extrinsic_hash}')
console.print('[dim]Issue will be visible once bounty is funded via harvest_emissions()[/dim]')
if tx_hash:
print_success('Issue registered successfully!')
console.print(f'[cyan]Transaction Hash:[/cyan] {tx_hash}')
console.print('[dim]Issue will be visible once bounty is funded via harvest_emissions()[/dim]')
else:
print_error('Contract rejected the request')
console.print('[yellow]Possible reasons:[/yellow]')
console.print(' \u2022 Issue already registered (same repo + issue number)')
console.print(' \u2022 Bounty too low (minimum 10 ALPHA)')
console.print(' \u2022 Caller is not the contract owner')

except ImportError as e:
print_error(f'Missing dependency - {e}')
console.print('[dim]Install with: pip install substrate-interface bittensor[/dim]')
console.print('[dim]Install with: pip install bittensor[/dim]')
except Exception as e:
error_msg = str(e)
if 'ContractReverted' in error_msg:
Expand Down
11 changes: 9 additions & 2 deletions gittensor/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,16 @@
DEFAULT_MAX_CONTRIBUTION_SCORE_FOR_FULL_BONUS = 2000

# Boosts
UNIQUE_PR_BOOST = 0.74
MAX_CODE_DENSITY_MULTIPLIER = 3.0

# Pioneer dividend — rewards the first quality contributor to each repository
# Rates applied per follower position (1st follower pays most, diminishing after)
# Dividend capped at PIONEER_DIVIDEND_MAX_RATIO × pioneer's own earned_score
PIONEER_DIVIDEND_RATE_1ST = 0.30 # 1st follower: 30% of their earned_score
PIONEER_DIVIDEND_RATE_2ND = 0.20 # 2nd follower: 20% of their earned_score
PIONEER_DIVIDEND_RATE_REST = 0.10 # 3rd+ followers: 10% of their earned_score
PIONEER_DIVIDEND_MAX_RATIO = 1.0 # Cap dividend at 1× pioneer's own earned_score (max 2× total)

# Issue boosts
MAX_ISSUE_CLOSE_WINDOW_DAYS = 1
MAX_ISSUE_AGE_FOR_MAX_SCORE = 40 # days
Expand Down Expand Up @@ -112,7 +119,7 @@
# =============================================================================
# Spam & Gaming Mitigation
# =============================================================================
MAINTAINER_ASSOCIATIONS = ['OWNER', 'MEMBER', 'COLLABORATOR']
MAINTAINER_ASSOCIATIONS = ['OWNER', 'COLLABORATOR']

# Issue multiplier bonuses
MAX_ISSUE_AGE_BONUS = 0.75 # Max bonus for issue age (scales with sqrt of days open)
Expand Down
2 changes: 1 addition & 1 deletion gittensor/validator/evaluation/reward.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ async def get_rewards(
# Adjust scores for duplicate accounts
detect_and_penalize_miners_sharing_github(miner_evaluations)

# Finalize scores: apply unique contribution multiplier, credibility, sum totals, deduct collateral
# Finalize scores: apply pioneer dividends, credibility, sum totals, deduct collateral
finalize_miner_scores(miner_evaluations)

# Allocate emissions by tier: replace total_score with tier-weighted allocations
Expand Down
Loading