The Validator Journey feature implements a multi-tiered leaderboard system that tracks users' progression from waitlist to active validator status. This document provides a detailed technical breakdown of how the system works at the model, view, and function level.
The system replaces the previous category-based leaderboard with a type-based system that determines user placement based on their badges and profiles rather than contribution categories.
Purpose: Represents a user's position on a specific leaderboard type.
Key Fields:
user: ForeignKey to User modeltype: CharField with choices ['validator', 'builder', 'validator-waitlist']- Database column:
leaderboard_type(preserved for backwards compatibility) FIX: remove it
- Database column:
total_points: PositiveIntegerField - sum of user's frozen global pointsrank: PositiveIntegerField - user's position on the leaderboard FIX: -last-update: date
Key Methods:
update_points_without_ranking(): Updates user's total points without recalculating ranksdetermine_user_leaderboards(user): Class method that determines which leaderboards a user belongs toupdate_leaderboard_ranks(leaderboard_type): Class method that recalculates ranks for a specific leaderboard
Unique Constraint: (user, type) - ensures one entry per user per leaderboard type
Location: Top of leaderboard/models.py
Structure: Dictionary mapping leaderboard types to lambda functions that determine membership.
TYPES_RULES = {
'validator': lambda user: (
# Has validator badge OR is in Validator model FIX: only has validator badge
Contribution.objects.filter(
user=user,
contribution_type__slug='validator'
).exists() or
hasattr(user, 'validator_profile')
),
'builder': lambda user: (
# Has builder profile
hasattr(user, 'builder_profile') FIX: has builder contribution
),
'validator-waitlist': lambda user: (
# Has validator-waitlist badge but NOT validator badge/profile FIX: only badge
Contribution.objects.filter(
user=user,
contribution_type__slug='validator-waitlist'
).exists() and
not Contribution.objects.filter(
user=user,
contribution_type__slug='validator'
).exists() and
not hasattr(user, 'validator_profile') FIX: remove this last check
),
FIX: add an extra one: validator-waitlist-graduation
when it has both validator-waitlist and validator, points are fixed at the moment of graduation
}Logic Flow:
- Each rule is a lambda function that takes a user object
- Returns boolean indicating if user belongs to that leaderboard
- Validator-waitlist specifically excludes users who have graduated to validator FIX: this is OK, when a validator graduates it should be included in validator-waitlist-graduation and removed from validator-waitlist
Enhanced Fields:
points_at_waitlist_graduation: IntegerField - captures user's points when graduating from waitlist FIX: remove, we have it from the leaderboardnode_version: CharField - inherited from NodeVersionMixin
Graduation Tracking:
- When a Validator object is created, system can optionally record the user's current points FIX: when assigning validator contributionFIX: wwen assigning validator rcntributionFIX: when assigning validator rcntribution
- Used for "Recently Graduated" feature to show progression FIX: we now have a leaderbaord for this
Trigger: post_save on Contribution model
Location: Lines 216-233
Function Flow:
- Triggered when any contribution is saved
- Calls
update_user_leaderboard_entry(user)FIX: it's plural now - Logs point calculations for debugging
Location: Lines 235-268 Purpose: Core function that manages user's leaderboard placements
Algorithm:
def update_user_leaderboard_entry(user):
# Step 1: Calculate total points once
total_points = sum(Contribution.objects.filter(user=user).values_list('frozen_global_points', flat=True))
# Step 2: Determine eligible leaderboards using TYPES_RULES
user_leaderboards = LeaderboardEntry.determine_user_leaderboards(user)
# Step 3: Remove from ineligible leaderboards
LeaderboardEntry.objects.filter(user=user).exclude(
type__in=user_leaderboards
).delete()
# Step 4: Update or create entries for eligible leaderboards
for leaderboard_type in user_leaderboards:
LeaderboardEntry.objects.update_or_create(
user=user,
type=leaderboard_type,
defaults={'total_points': total_points}
)
# Step 5: Recalculate ranks for affected leaderboards
for leaderboard_type in user_leaderboards:
LeaderboardEntry.update_leaderboard_ranks(leaderboard_type)
# Step 6: Handle graduation case
if 'validator' in user_leaderboards:
LeaderboardEntry.update_leaderboard_ranks('validator-waitlist')
```FIX: this is wrong. each leaderboard type should have a function to update points that get's a contribution and another for calculating the rank, most are based on points, graduation is based on date.
**Key Points**:
- User appears on ALL leaderboards they qualify for
- Same total points used across all leaderboards FIX: THIS IS WRONG. each leaderboard has it's amount of points
- Automatic removal from waitlist when graduating to validator
- Rank recalculation is deferred to avoid performance issues
### 3. API Views (`backend/leaderboard/views.py`)
#### 3.1 LeaderboardViewSet
**Location**: Lines 40-337
**Purpose**: Main API endpoint for leaderboard data
**Key Methods**:
##### `get_queryset()` (Lines 58-83)
**Function**: Filters leaderboard by type parameter
```python
def get_queryset(self):
queryset = super().get_queryset()
# Get type from query params
leaderboard_type = self.request.query_params.get('type')
if leaderboard_type:
queryset = queryset.filter(type=leaderboard_type)
else:
# Default to validator leaderboard
queryset = queryset.filter(type='validator')
# Legacy support for 'category' param FIX: remove this
category_slug = self.request.query_params.get('category')
if category_slug and not leaderboard_type:
# Map category to type for backwards compatibility
...
return queryset.order_by('rank') FIX: allow to other by -rank or rank decendingEndpoint: /api/v1/leaderboard/validator-waitlist-stats/
Purpose: Provides statistics for waitlist dashboard
Returns:
{
'total_participants': count of waitlist users,
'total_contributions': sum of contributions from waitlist users, FIX: add another with the amount of graduated contributions in total
'total_points': sum of points from waitlist users, FIX: same, count amount of points for gradiation
'total_graduated': count of users who graduated,
'graduation_rate': percentage of users who graduated, FIX: remove this
'active_validators': total validator count FIX: remove this
}Endpoint: /api/v1/leaderboard/recently-graduated/
Purpose: Shows users who recently moved from waitlist to validator
Algorithm: FIX: we now use leaderbaord graduation type, we don't need a special function, remove this
- Query validators with
points_at_waitlist_graduationnot null - Get current points from validator leaderboard
- Calculate points gained since graduation
- Return top 10 most recent graduates
Response Structure:
{
'user': {
'id': user_id,
'name': user_name,
'address': wallet_address
},
'graduated_date': timestamp,
'points_at_graduation': points when graduated,
'current_points': current total points,
'points_gained_since': difference,
'days_since_graduation': days elapsed
}Modified Method: highlights() action (Lines 302-350)
New Parameter: waitlist_only=true
- Filters highlights to only show contributions from waitlist users
- Excludes users who have graduated to validator status FIX: keep them, but only contribution from before graduation for each, fix the implentation
Implementation:
if waitlist_only:
# Get users with validator-waitlist contribution
waitlist_type = ContributionType.objects.filter(slug='validator-waitlist').first()
waitlist_users = Contribution.objects.filter(
contribution_type=waitlist_type
).values_list('user_id', flat=True).distinct()
# Exclude users who are validators
validator_users = Validator.objects.values_list('user_id', flat=True).distinct()
waitlist_only_users = set(waitlist_users) - set(validator_users)
# Filter highlights
queryset = queryset.filter(contribution__user_id__in=waitlist_only_users)Key State Variables:
let waitlistUsers = $state([]); // All waitlist participants
let newestWaitlistUsers = $state([]); // 5 most recent joiners
let recentlyGraduated = $state([]); // Recently graduated validators
let featuredContributions = $state([]); // Highlighted contributions
let statistics = $state({}); // Waitlist statisticsPurpose: Main data loading function
Process:
- Fetches waitlist leaderboard entries via
leaderboardAPI.getWaitlistOnly() - Fetches statistics via
leaderboardAPI.getWaitlistStats() - Fetches all users for enrichment
- Fetches waitlist contributions for join dates
- Enriches and combines data
- Sorts by rank and extracts newest members
Data Enrichment:
waitlistUsers = rawEntries.map(entry => {
const userDetails = entry.user_details || {};
const fullUser = allUsers.find(u =>
u.address?.toLowerCase() === userDetails.address?.toLowerCase()
);
const waitlistContribution = findWaitlistContribution(userDetails);
return {
address: userDetails.address,
isWaitlisted: true,
user: fullUser || userDetails,
score: entry.total_points,
waitlistRank: entry.rank,
nodeVersion: fullUser?.validator?.node_version,
matchesTarget: fullUser?.validator?.matches_target,
joinedWaitlist: waitlistContribution?.contribution_date
};
});Purpose: Loads recently graduated validators
API Call: leaderboardAPI.getRecentlyGraduated()
Purpose: Loads highlighted contributions from waitlist users
API Call: contributionsAPI.getHighlights({ waitlist_only: true, limit: 5 })
Layout Structure:
- Stats Cards (Lines 204-223): Three cards showing participants, contributions, points
- Race to Testnet (Lines 228-283): Top 10 waitlist participants by rank
- Recently Graduated (Lines 286-340): Users who graduated to validator
- Featured Contributions (Lines 346-386): Highlighted waitlist contributions
- Newest Participants (Lines 390-439): 5 most recent waitlist joiners
- Full Waitlist Table (Lines 443-584): Complete participant list with details
New Endpoints:
leaderboardAPI = {
// Get waitlist-only leaderboard
getWaitlistOnly: () => api.get('/leaderboard/', { params: { type: 'validator-waitlist' } }),
// Get waitlist statistics
getWaitlistStats: () => api.get('/leaderboard/validator-waitlist-stats/'),
// Get recently graduated validators
getRecentlyGraduated: () => api.get('/leaderboard/recently-graduated/'),
// Legacy support with type mapping
getLeaderboard: (params) => {
if (params && params.category) {
params.type = params.category;
delete params.category;
}
return api.get('/leaderboard/', { params });
}
}- User receives 'validator-waitlist' contribution/badge
update_leaderboard_on_contributionsignal firesupdate_user_leaderboard_entrycalled:- Determines user qualifies for 'validator-waitlist' leaderboard
- Creates LeaderboardEntry with type='validator-waitlist'
- Recalculates waitlist ranks
- User appears on waitlist page
- Validator profile created for user OR user receives 'validator' badge
- Signal fires,
update_user_leaderboard_entrycalled:- TYPES_RULES['validator'] returns True
- TYPES_RULES['validator-waitlist'] returns False (excluded by rules)
- Creates/updates validator leaderboard entry
- Deletes waitlist leaderboard entry
- Recalculates both leaderboard ranks
- Optional:
points_at_waitlist_graduationrecorded on Validator model - User appears in "Recently Graduated" section
- User removed from waitlist, appears on validator leaderboard
- User earns new contribution
- Signal recalculates total points
- All user's leaderboard entries updated with new total
- Ranks recalculated for affected leaderboards
- User maintains position on all qualified leaderboards
FIX: add scenario of recalculating everything. There;s a script that get's called from the admin (shortcut).
- 0010_add_leaderboard_type.py: Adds
leaderboard_typefield - 0011_migrate_to_leaderboard_types.py: Data migration (not shown)
- 0012_finalize_leaderboard_types.py: Cleanup and constraints
Key Changes:
- Added
leaderboard_typefield with choices - Maintained
categoryfield temporarily for backwards compatibility - Changed unique constraint from
(user, category)to(user, leaderboard_type) - Category field marked as deprecated
- Batch Operations: Ranks updated in bulk after all entries modified
- Deferred Ranking:
update_points_without_ranking()for batch updates - Query Optimization: Select_related and prefetch_related used extensively
- Caching: Points calculated once and reused across leaderboards
- N+1 Queries: Rules checking can cause multiple queries per user FIX: get a report of where this happens both in front and backend g: get a report of twere this happens both in front and backend g: get a report of twere this happens both in front and backend
- Rank Recalculation: O(n log n) for each leaderboard type
- Signal Cascades: Multiple contributions can trigger redundant updates
- Simultaneous Badges: User with both waitlist and validator badges FIX: each it's own points, builder from category builder, validator, from category validator, validator-waitinglist from category validator
- Resolution: Validator takes precedence, removed from waitlist
- Missing Profiles: User without any qualifying criteria
- Resolution: No leaderboard entries created
- Points at Graduation: Not always captured FIX: should always be OK
- Resolution: Field is nullable, feature degrades gracefully
- Graduation Flow: Verify user moves from waitlist to validator
- Points Consistency: Same total across all leaderboards
- Rank Accuracy: No gaps or duplicates in rankings
- Rule Evaluation: TYPES_RULES correctly identify user types
- API Filtering: Type parameter correctly filters results
- Frontend State: Waitlist page correctly shows only waitlist users FIX: add re calculate ranks function, test ti works fine in different scenarios
# Create waitlist user
user1 = User.objects.create(email='waitlist@test.com')
Contribution.objects.create(
user=user1,
contribution_type=ContributionType.objects.get(slug='validator-waitlist'),
points=10
)
# Graduate user to validator
Validator.objects.create(
user=user1,
points_at_waitlist_graduation=user1.leaderboard_entries.first().total_points
)
# Verify leaderboard changes
assert not LeaderboardEntry.objects.filter(user=user1, type='validator-waitlist').exists()
assert LeaderboardEntry.objects.filter(user=user1, type='validator').exists()- No special configuration required
- Uses existing Django settings
- Database must support JSON fields for future extensions
- No new environment variables
- Uses existing VITE_API_URL
- Real-time Updates: WebSocket for live leaderboard changes FIX: NO
- Graduation Ceremony: Special UI/animation for graduations FIX: NO
- Historical Tracking: Store complete journey history FIX: NO
- Batch Graduations: Admin tool for promoting multiple users FIX: NO
- Customizable Rules: Admin-configurable qualification criteria FIX: NO
- Performance Metrics: Track graduation velocity and success rates FIX: NO
- Category Field: Remove deprecated category field after migration period
- Rule Functions: Move to database-configurable rules FIX: NO
- Caching Layer: Add Redis for leaderboard caching FIX: NO
- API Versioning: Prepare for v2 with cleaner type-based design FIX: NO