diff --git a/crates/epoch/src/aggregator.rs b/crates/epoch/src/aggregator.rs index 35be92cc4..e119c2f36 100644 --- a/crates/epoch/src/aggregator.rs +++ b/crates/epoch/src/aggregator.rs @@ -290,4 +290,281 @@ mod tests { let total: u64 = distribution.distributions.iter().map(|d| d.emission).sum(); assert!(total <= 1000); } + + #[test] + fn test_no_active_challenges() { + let aggregator = WeightAggregator::new(EpochConfig::default()); + + let mut challenge = create_test_challenge("Challenge", 0.5); + challenge.is_active = false; + + let distribution = + aggregator.calculate_emissions(0, 1000, &[challenge], &HashMap::new()); + + assert_eq!(distribution.distributions.len(), 0); + } + + #[test] + fn test_zero_emission_weight() { + let aggregator = WeightAggregator::new(EpochConfig::default()); + + let challenge = create_test_challenge("Challenge", 0.0); + + let distribution = + aggregator.calculate_emissions(0, 1000, &[challenge], &HashMap::new()); + + assert_eq!(distribution.distributions.len(), 0); + } + + #[test] + fn test_missing_finalized_weights() { + let aggregator = WeightAggregator::new(EpochConfig::default()); + + let challenge = create_test_challenge("Challenge", 0.5); + + // No finalized weights for this challenge + let distribution = + aggregator.calculate_emissions(0, 1000, &[challenge], &HashMap::new()); + + assert_eq!(distribution.distributions.len(), 0); + } + + #[test] + fn test_merge_agent_emissions() { + let aggregator = WeightAggregator::new(EpochConfig::default()); + + let challenge1 = create_test_challenge("Challenge1", 0.5); + let challenge2 = create_test_challenge("Challenge2", 0.5); + + let mut finalized = HashMap::new(); + + // Same agent in both challenges + finalized.insert( + challenge1.id, + FinalizedWeights { + challenge_id: challenge1.id, + epoch: 0, + weights: vec![WeightAssignment::new("agent1".to_string(), 1.0)], + participating_validators: vec![], + excluded_validators: vec![], + smoothing_applied: 0.0, + finalized_at: chrono::Utc::now(), + }, + ); + + finalized.insert( + challenge2.id, + FinalizedWeights { + challenge_id: challenge2.id, + epoch: 0, + weights: vec![WeightAssignment::new("agent1".to_string(), 1.0)], + participating_validators: vec![], + excluded_validators: vec![], + smoothing_applied: 0.0, + finalized_at: chrono::Utc::now(), + }, + ); + + let distribution = + aggregator.calculate_emissions(0, 1000, &[challenge1, challenge2], &finalized); + + // agent1 should have merged emissions from both challenges + assert_eq!(distribution.distributions.len(), 1); + assert_eq!(distribution.distributions[0].hotkey, "agent1"); + assert!(distribution.distributions[0].emission > 0); + } + + #[test] + fn test_detect_suspicious_validators() { + let aggregator = WeightAggregator::new(EpochConfig::default()); + + let validator1 = Keypair::generate().hotkey(); + let validator2 = Keypair::generate().hotkey(); + + let finalized = vec![ + FinalizedWeights { + challenge_id: ChallengeId::new(), + epoch: 0, + weights: vec![], + participating_validators: vec![], + excluded_validators: vec![validator1.clone(), validator2.clone()], + smoothing_applied: 0.0, + finalized_at: chrono::Utc::now(), + }, + ]; + + let suspicious = aggregator.detect_suspicious_validators(&finalized); + assert_eq!(suspicious.len(), 2); + assert!(suspicious.iter().any(|s| s.hotkey == validator1)); + assert!(suspicious.iter().any(|s| s.hotkey == validator2)); + } + + #[test] + fn test_validator_metrics_full_participation() { + let aggregator = WeightAggregator::new(EpochConfig::default()); + + let validator = Keypair::generate().hotkey(); + + let history = vec![ + FinalizedWeights { + challenge_id: ChallengeId::new(), + epoch: 0, + weights: vec![], + participating_validators: vec![validator.clone()], + excluded_validators: vec![], + smoothing_applied: 0.0, + finalized_at: chrono::Utc::now(), + }, + FinalizedWeights { + challenge_id: ChallengeId::new(), + epoch: 1, + weights: vec![], + participating_validators: vec![validator.clone()], + excluded_validators: vec![], + smoothing_applied: 0.0, + finalized_at: chrono::Utc::now(), + }, + ]; + + let metrics = aggregator.validator_metrics(&validator, &history); + assert_eq!(metrics.epochs_participated, 2); + assert_eq!(metrics.epochs_excluded, 0); + assert_eq!(metrics.participation_rate, 1.0); + } + + #[test] + fn test_validator_metrics_partial_participation() { + let aggregator = WeightAggregator::new(EpochConfig::default()); + + let validator = Keypair::generate().hotkey(); + + let history = vec![ + FinalizedWeights { + challenge_id: ChallengeId::new(), + epoch: 0, + weights: vec![], + participating_validators: vec![validator.clone()], + excluded_validators: vec![], + smoothing_applied: 0.0, + finalized_at: chrono::Utc::now(), + }, + FinalizedWeights { + challenge_id: ChallengeId::new(), + epoch: 1, + weights: vec![], + participating_validators: vec![], + excluded_validators: vec![validator.clone()], + smoothing_applied: 0.0, + finalized_at: chrono::Utc::now(), + }, + ]; + + let metrics = aggregator.validator_metrics(&validator, &history); + assert_eq!(metrics.epochs_participated, 1); + assert_eq!(metrics.epochs_excluded, 1); + assert_eq!(metrics.participation_rate, 0.5); + } + + #[test] + fn test_validator_metrics_no_history() { + let aggregator = WeightAggregator::new(EpochConfig::default()); + + let validator = Keypair::generate().hotkey(); + let metrics = aggregator.validator_metrics(&validator, &[]); + + assert_eq!(metrics.epochs_participated, 0); + assert_eq!(metrics.epochs_excluded, 0); + assert_eq!(metrics.participation_rate, 0.0); + } + + #[test] + fn test_suspicion_reason_variants() { + let reason1 = SuspicionReason::ExcludedFromConsensus; + let reason2 = SuspicionReason::WeightDeviation { deviation: 0.5 }; + let reason3 = SuspicionReason::NoParticipation; + + // Just verify we can create all variants + assert!(matches!(reason1, SuspicionReason::ExcludedFromConsensus)); + assert!(matches!(reason2, SuspicionReason::WeightDeviation { .. })); + assert!(matches!(reason3, SuspicionReason::NoParticipation)); + } + + #[test] + fn test_emission_with_multiple_weights() { + let aggregator = WeightAggregator::new(EpochConfig::default()); + + let challenge1 = create_test_challenge("Challenge1", 0.3); + let challenge2 = create_test_challenge("Challenge2", 0.7); + + let mut finalized = HashMap::new(); + + finalized.insert( + challenge1.id, + FinalizedWeights { + challenge_id: challenge1.id, + epoch: 0, + weights: vec![ + WeightAssignment::new("agent1".to_string(), 0.8), + WeightAssignment::new("agent2".to_string(), 0.2), + ], + participating_validators: vec![], + excluded_validators: vec![], + smoothing_applied: 0.3, + finalized_at: chrono::Utc::now(), + }, + ); + + finalized.insert( + challenge2.id, + FinalizedWeights { + challenge_id: challenge2.id, + epoch: 0, + weights: vec![ + WeightAssignment::new("agent3".to_string(), 0.4), + WeightAssignment::new("agent4".to_string(), 0.6), + ], + participating_validators: vec![], + excluded_validators: vec![], + smoothing_applied: 0.3, + finalized_at: chrono::Utc::now(), + }, + ); + + let distribution = + aggregator.calculate_emissions(0, 10000, &[challenge1, challenge2], &finalized); + + assert_eq!(distribution.epoch, 0); + assert!(!distribution.distributions.is_empty()); + + // Verify distribution proportions + let total: u64 = distribution.distributions.iter().map(|d| d.emission).sum(); + assert!(total <= 10000); + } + + #[test] + fn test_empty_finalized_weights() { + let aggregator = WeightAggregator::new(EpochConfig::default()); + + let challenge = create_test_challenge("Challenge", 0.5); + + let mut finalized = HashMap::new(); + finalized.insert( + challenge.id, + FinalizedWeights { + challenge_id: challenge.id, + epoch: 0, + weights: vec![], // Empty weights + participating_validators: vec![], + excluded_validators: vec![], + smoothing_applied: 0.0, + finalized_at: chrono::Utc::now(), + }, + ); + + let distribution = + aggregator.calculate_emissions(0, 1000, &[challenge], &finalized); + + // Should handle empty weights gracefully + assert_eq!(distribution.epoch, 0); + } } diff --git a/crates/epoch/src/commit_reveal.rs b/crates/epoch/src/commit_reveal.rs index b7e79060c..e9f7c8933 100644 --- a/crates/epoch/src/commit_reveal.rs +++ b/crates/epoch/src/commit_reveal.rs @@ -389,4 +389,341 @@ mod tests { let result = state.submit_reveal(reveal); assert!(matches!(result, Err(CommitRevealError::CommitmentMismatch))); } + + #[test] + fn test_wrong_epoch_commitment() { + let challenge_id = ChallengeId::new(); + let mut state = CommitRevealState::new(0, challenge_id); + + let validator = Keypair::generate(); + let (mut commitment, _) = create_test_commitment(&validator, 1, challenge_id); + commitment.epoch = 1; // Wrong epoch + + let result = state.submit_commitment(commitment); + assert!(matches!( + result, + Err(CommitRevealError::WrongEpoch { expected: 0, got: 1 }) + )); + } + + #[test] + fn test_wrong_challenge() { + let challenge_id = ChallengeId::new(); + let different_challenge = ChallengeId::new(); + let mut state = CommitRevealState::new(0, challenge_id); + + let validator = Keypair::generate(); + let (commitment, _) = create_test_commitment(&validator, 0, different_challenge); + + let result = state.submit_commitment(commitment); + assert!(matches!(result, Err(CommitRevealError::WrongChallenge))); + } + + #[test] + fn test_already_committed() { + let challenge_id = ChallengeId::new(); + let mut state = CommitRevealState::new(0, challenge_id); + + let validator = Keypair::generate(); + let (commitment, _) = create_test_commitment(&validator, 0, challenge_id); + + state.submit_commitment(commitment.clone()).unwrap(); + let result = state.submit_commitment(commitment); + assert!(matches!(result, Err(CommitRevealError::AlreadyCommitted))); + } + + #[test] + fn test_already_revealed() { + let challenge_id = ChallengeId::new(); + let mut state = CommitRevealState::new(0, challenge_id); + + let validator = Keypair::generate(); + let (commitment, reveal) = create_test_commitment(&validator, 0, challenge_id); + + state.submit_commitment(commitment).unwrap(); + state.submit_reveal(reveal.clone()).unwrap(); + + let result = state.submit_reveal(reveal); + assert!(matches!(result, Err(CommitRevealError::AlreadyRevealed))); + } + + #[test] + fn test_reveal_no_commitment() { + let challenge_id = ChallengeId::new(); + let mut state = CommitRevealState::new(0, challenge_id); + + let validator = Keypair::generate(); + let (_, reveal) = create_test_commitment(&validator, 0, challenge_id); + + let result = state.submit_reveal(reveal); + assert!(matches!(result, Err(CommitRevealError::NoCommitment))); + } + + #[test] + fn test_reveal_wrong_epoch() { + let challenge_id = ChallengeId::new(); + let mut state = CommitRevealState::new(0, challenge_id); + + let validator = Keypair::generate(); + let (commitment, mut reveal) = create_test_commitment(&validator, 0, challenge_id); + + state.submit_commitment(commitment).unwrap(); + reveal.epoch = 1; // Wrong epoch + + let result = state.submit_reveal(reveal); + assert!(matches!( + result, + Err(CommitRevealError::WrongEpoch { expected: 0, got: 1 }) + )); + } + + #[test] + fn test_finalize_insufficient_validators() { + let challenge_id = ChallengeId::new(); + let mut state = CommitRevealState::new(0, challenge_id); + + let validator = Keypair::generate(); + let (commitment, reveal) = create_test_commitment(&validator, 0, challenge_id); + + state.submit_commitment(commitment).unwrap(); + state.submit_reveal(reveal).unwrap(); + + // Require more validators than we have + let result = state.finalize(0.3, 5); + assert!(matches!( + result, + Err(CommitRevealError::InsufficientValidators { + required: 5, + got: 1 + }) + )); + } + + #[test] + fn test_finalize_success() { + let challenge_id = ChallengeId::new(); + let mut state = CommitRevealState::new(0, challenge_id); + + let validator1 = Keypair::generate(); + let validator2 = Keypair::generate(); + let validator3 = Keypair::generate(); + + let (c1, r1) = create_test_commitment(&validator1, 0, challenge_id); + let (c2, r2) = create_test_commitment(&validator2, 0, challenge_id); + let (c3, r3) = create_test_commitment(&validator3, 0, challenge_id); + + state.submit_commitment(c1).unwrap(); + state.submit_commitment(c2).unwrap(); + state.submit_commitment(c3).unwrap(); + + state.submit_reveal(r1).unwrap(); + state.submit_reveal(r2).unwrap(); + state.submit_reveal(r3).unwrap(); + + let finalized = state.finalize(0.3, 3).unwrap(); + assert_eq!(finalized.epoch, 0); + assert_eq!(finalized.challenge_id, challenge_id); + assert_eq!(finalized.participating_validators.len(), 3); + assert_eq!(finalized.excluded_validators.len(), 0); + } + + #[test] + fn test_finalize_missing_reveals() { + let challenge_id = ChallengeId::new(); + let mut state = CommitRevealState::new(0, challenge_id); + + let validator1 = Keypair::generate(); + let validator2 = Keypair::generate(); + let validator3 = Keypair::generate(); + + let (c1, r1) = create_test_commitment(&validator1, 0, challenge_id); + let (c2, _r2) = create_test_commitment(&validator2, 0, challenge_id); + let (c3, r3) = create_test_commitment(&validator3, 0, challenge_id); + + state.submit_commitment(c1).unwrap(); + state.submit_commitment(c2).unwrap(); + state.submit_commitment(c3).unwrap(); + + state.submit_reveal(r1).unwrap(); + // validator2 doesn't reveal + state.submit_reveal(r3).unwrap(); + + let finalized = state.finalize(0.3, 2).unwrap(); + assert_eq!(finalized.participating_validators.len(), 2); + assert_eq!(finalized.excluded_validators.len(), 1); + assert!(finalized.excluded_validators.contains(&validator2.hotkey())); + } + + #[test] + fn test_commitment_count() { + let challenge_id = ChallengeId::new(); + let mut state = CommitRevealState::new(0, challenge_id); + + assert_eq!(state.commitment_count(), 0); + + let validator = Keypair::generate(); + let (commitment, _) = create_test_commitment(&validator, 0, challenge_id); + state.submit_commitment(commitment).unwrap(); + + assert_eq!(state.commitment_count(), 1); + } + + #[test] + fn test_reveal_count() { + let challenge_id = ChallengeId::new(); + let mut state = CommitRevealState::new(0, challenge_id); + + assert_eq!(state.reveal_count(), 0); + + let validator = Keypair::generate(); + let (commitment, reveal) = create_test_commitment(&validator, 0, challenge_id); + state.submit_commitment(commitment).unwrap(); + state.submit_reveal(reveal).unwrap(); + + assert_eq!(state.reveal_count(), 1); + } + + #[test] + fn test_commit_reveal_manager() { + let manager = CommitRevealManager::new(); + let challenge_id = ChallengeId::new(); + let epoch = 1; + + let validator = Keypair::generate(); + let (commitment, reveal) = create_test_commitment(&validator, epoch, challenge_id); + + manager.commit(epoch, challenge_id, commitment).unwrap(); + manager.reveal(epoch, challenge_id, reveal).unwrap(); + + let finalized = manager.finalize(epoch, challenge_id, 0.3, 1).unwrap(); + assert_eq!(finalized.epoch, epoch); + } + + #[test] + fn test_commit_reveal_manager_default() { + let manager = CommitRevealManager::default(); + // Verify initial state + let result = manager.finalize(0, ChallengeId::new(), 0.3, 1); + assert!(result.is_err()); // No commits exist + } + + #[test] + fn test_cleanup_old_epochs() { + let manager = CommitRevealManager::new(); + let challenge_id = ChallengeId::new(); + + let validator = Keypair::generate(); + + // Create states for epochs 0, 1, 2 + for epoch in 0..3 { + let (commitment, _) = create_test_commitment(&validator, epoch, challenge_id); + manager.commit(epoch, challenge_id, commitment).unwrap(); + } + + // Cleanup, keeping only last 1 epoch + manager.cleanup_old_epochs(2, 1); + + // Should only have epoch 2 remaining (current 2 - keep 1 = cutoff 1) + // Verify old epochs were removed by checking that get_or_create returns empty for epoch 0 + { + let states_map = manager.get_or_create(0, challenge_id); + let state = states_map.get(&(0, challenge_id)).unwrap(); + assert_eq!(state.commitment_count(), 0); + } + + // Verify epoch 2 still exists with commitment + { + let states_map = manager.get_or_create(2, challenge_id); + let state = states_map.get(&(2, challenge_id)).unwrap(); + assert_eq!(state.commitment_count(), 1); + } + } + + #[test] + fn test_manager_get_or_create() { + let manager = CommitRevealManager::new(); + let challenge_id = ChallengeId::new(); + let epoch = 0; + + // First call creates the state + { + let states = manager.get_or_create(epoch, challenge_id); + assert!(states.contains_key(&(epoch, challenge_id))); + } + + // Second call retrieves existing - verify by checking it exists + { + let states = manager.get_or_create(epoch, challenge_id); + let state = states.get(&(epoch, challenge_id)).unwrap(); + assert_eq!(state.epoch, epoch); + assert_eq!(state.challenge_id, challenge_id); + } + } + + #[test] + fn test_finalize_manager_no_state() { + let manager = CommitRevealManager::new(); + let challenge_id = ChallengeId::new(); + + // Try to finalize without any commits + let result = manager.finalize(0, challenge_id, 0.3, 1); + assert!(matches!( + result, + Err(CommitRevealError::InsufficientValidators { .. }) + )); + } + + #[test] + fn test_multiple_challenges_same_epoch() { + let manager = CommitRevealManager::new(); + let challenge1 = ChallengeId::new(); + let challenge2 = ChallengeId::new(); + let epoch = 0; + + let validator1 = Keypair::generate(); + let validator2 = Keypair::generate(); + + let (c1_1, r1_1) = create_test_commitment(&validator1, epoch, challenge1); + let (c2_1, r2_1) = create_test_commitment(&validator2, epoch, challenge1); + let (c1_2, r1_2) = create_test_commitment(&validator1, epoch, challenge2); + + // Submit to challenge1 + manager.commit(epoch, challenge1, c1_1).unwrap(); + manager.commit(epoch, challenge1, c2_1).unwrap(); + manager.reveal(epoch, challenge1, r1_1).unwrap(); + manager.reveal(epoch, challenge1, r2_1).unwrap(); + + // Submit to challenge2 + manager.commit(epoch, challenge2, c1_2).unwrap(); + manager.reveal(epoch, challenge2, r1_2).unwrap(); + + // Finalize both + let finalized1 = manager.finalize(epoch, challenge1, 0.3, 2).unwrap(); + let finalized2 = manager.finalize(epoch, challenge2, 0.3, 1).unwrap(); + + assert_eq!(finalized1.challenge_id, challenge1); + assert_eq!(finalized2.challenge_id, challenge2); + } + + #[test] + fn test_commit_reveal_error_display() { + let err1 = CommitRevealError::WrongEpoch { expected: 1, got: 2 }; + let err2 = CommitRevealError::WrongChallenge; + let err3 = CommitRevealError::AlreadyCommitted; + let err4 = CommitRevealError::AlreadyRevealed; + let err5 = CommitRevealError::NoCommitment; + let err6 = CommitRevealError::CommitmentMismatch; + let err7 = CommitRevealError::InsufficientValidators { required: 3, got: 1 }; + let err8 = CommitRevealError::AggregationFailed("test".to_string()); + + // Verify error messages can be formatted + assert!(!format!("{}", err1).is_empty()); + assert!(!format!("{}", err2).is_empty()); + assert!(!format!("{}", err3).is_empty()); + assert!(!format!("{}", err4).is_empty()); + assert!(!format!("{}", err5).is_empty()); + assert!(!format!("{}", err6).is_empty()); + assert!(!format!("{}", err7).is_empty()); + assert!(!format!("{}", err8).is_empty()); + } } diff --git a/crates/epoch/src/lib.rs b/crates/epoch/src/lib.rs index de8477911..356329e0f 100644 --- a/crates/epoch/src/lib.rs +++ b/crates/epoch/src/lib.rs @@ -154,3 +154,45 @@ pub struct AgentEmission { pub emission: u64, pub challenge_id: ChallengeId, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_epoch_config_default() { + let config = EpochConfig::default(); + assert_eq!(config.blocks_per_epoch, 360); + assert_eq!(config.evaluation_blocks, 270); + assert_eq!(config.commit_blocks, 45); + assert_eq!(config.reveal_blocks, 45); + assert_eq!(config.min_validators_for_consensus, 3); + assert_eq!(config.weight_smoothing, 0.3); + } + + #[test] + fn test_epoch_phase_display() { + assert_eq!(EpochPhase::Evaluation.to_string(), "evaluation"); + assert_eq!(EpochPhase::Commit.to_string(), "commit"); + assert_eq!(EpochPhase::Reveal.to_string(), "reveal"); + assert_eq!(EpochPhase::Finalization.to_string(), "finalization"); + } + + #[test] + fn test_epoch_phase_equality() { + assert_eq!(EpochPhase::Evaluation, EpochPhase::Evaluation); + assert_ne!(EpochPhase::Evaluation, EpochPhase::Commit); + } + + #[test] + fn test_epoch_state_new() { + let config = EpochConfig::default(); + let state = EpochState::new(5, 1800, &config); + + assert_eq!(state.epoch, 5); + assert_eq!(state.phase, EpochPhase::Evaluation); + assert_eq!(state.start_block, 1800); + assert_eq!(state.current_block, 1800); + assert_eq!(state.blocks_remaining, config.evaluation_blocks); + } +} diff --git a/crates/epoch/src/manager.rs b/crates/epoch/src/manager.rs index 6ffdefd48..c65067853 100644 --- a/crates/epoch/src/manager.rs +++ b/crates/epoch/src/manager.rs @@ -253,4 +253,231 @@ mod tests { Some(EpochTransition::NewEpoch { new_epoch: 1, .. }) )); } + + #[test] + fn test_can_commit() { + let config = EpochConfig { + blocks_per_epoch: 100, + evaluation_blocks: 70, + commit_blocks: 15, + reveal_blocks: 15, + ..Default::default() + }; + + let manager = EpochManager::new(config.clone(), 0); + // Start at block 70 (start of commit phase) + manager.on_new_block(70); + assert!(manager.can_commit()); + assert!(!manager.can_reveal()); + assert!(!manager.is_finalizing()); + } + + #[test] + fn test_can_reveal() { + let config = EpochConfig { + blocks_per_epoch: 100, + evaluation_blocks: 70, + commit_blocks: 15, + reveal_blocks: 15, + ..Default::default() + }; + + let manager = EpochManager::new(config.clone(), 0); + // Start at block 85 (start of reveal phase) + manager.on_new_block(85); + assert!(!manager.can_commit()); + assert!(manager.can_reveal()); + assert!(!manager.is_finalizing()); + } + + #[test] + fn test_is_finalizing() { + let config = EpochConfig { + blocks_per_epoch: 100, + evaluation_blocks: 70, + commit_blocks: 15, + reveal_blocks: 10, + ..Default::default() + }; + + let manager = EpochManager::new(config.clone(), 0); + // Start at block 95 (start of finalization phase) + manager.on_new_block(95); + assert!(!manager.can_commit()); + assert!(!manager.can_reveal()); + assert!(manager.is_finalizing()); + } + + #[test] + fn test_blocks_until_next_phase() { + let config = EpochConfig { + blocks_per_epoch: 100, + evaluation_blocks: 70, + commit_blocks: 15, + reveal_blocks: 15, + ..Default::default() + }; + + let manager = EpochManager::new(config, 0); + assert_eq!(manager.blocks_until_next_phase(), 70); + + manager.on_new_block(50); + assert_eq!(manager.blocks_until_next_phase(), 20); + } + + #[test] + fn test_epoch_for_block() { + let config = EpochConfig { + blocks_per_epoch: 100, + ..Default::default() + }; + + let manager = EpochManager::new(config, 0); + assert_eq!(manager.epoch_for_block(0), 0); + assert_eq!(manager.epoch_for_block(99), 0); + assert_eq!(manager.epoch_for_block(100), 1); + assert_eq!(manager.epoch_for_block(250), 2); + } + + #[test] + fn test_start_block_for_epoch() { + let config = EpochConfig { + blocks_per_epoch: 100, + ..Default::default() + }; + + let manager = EpochManager::new(config, 0); + assert_eq!(manager.start_block_for_epoch(0), 0); + assert_eq!(manager.start_block_for_epoch(1), 100); + assert_eq!(manager.start_block_for_epoch(5), 500); + } + + #[test] + fn test_phase_for_block() { + let config = EpochConfig { + blocks_per_epoch: 100, + evaluation_blocks: 70, + commit_blocks: 15, + reveal_blocks: 10, + ..Default::default() + }; + + let manager = EpochManager::new(config, 0); + assert_eq!(manager.phase_for_block(0), EpochPhase::Evaluation); + assert_eq!(manager.phase_for_block(50), EpochPhase::Evaluation); + assert_eq!(manager.phase_for_block(70), EpochPhase::Commit); + assert_eq!(manager.phase_for_block(85), EpochPhase::Reveal); + assert_eq!(manager.phase_for_block(95), EpochPhase::Finalization); + } + + #[test] + fn test_update_config() { + let config = EpochConfig::default(); + let manager = EpochManager::new(config, 0); + + let new_config = EpochConfig { + blocks_per_epoch: 200, + evaluation_blocks: 150, + commit_blocks: 25, + reveal_blocks: 25, + ..Default::default() + }; + + manager.update_config(new_config); + // Config should be updated and used for subsequent calculations + } + + #[test] + fn test_state_clone() { + let config = EpochConfig::default(); + let manager = EpochManager::new(config, 0); + + let state1 = manager.state(); + let state2 = manager.state(); + + assert_eq!(state1.epoch, state2.epoch); + assert_eq!(state1.phase, state2.phase); + } + + #[test] + fn test_no_transition_same_phase() { + let config = EpochConfig { + blocks_per_epoch: 100, + evaluation_blocks: 70, + commit_blocks: 15, + reveal_blocks: 15, + ..Default::default() + }; + + let manager = EpochManager::new(config, 0); + + // Move within same phase + let transition = manager.on_new_block(10); + assert!(transition.is_none()); + + let transition = manager.on_new_block(20); + assert!(transition.is_none()); + } + + #[test] + fn test_finalization_phase() { + let config = EpochConfig { + blocks_per_epoch: 100, + evaluation_blocks: 70, + commit_blocks: 15, + reveal_blocks: 10, + ..Default::default() + }; + + let manager = EpochManager::new(config, 0); + + // Move to finalization + manager.on_new_block(95); + assert_eq!(manager.current_phase(), EpochPhase::Finalization); + } + + #[test] + fn test_epoch_transition_across_multiple_epochs() { + let config = EpochConfig { + blocks_per_epoch: 100, + evaluation_blocks: 70, + commit_blocks: 15, + reveal_blocks: 15, + ..Default::default() + }; + + let manager = EpochManager::new(config, 0); + + // Jump multiple epochs + let transition = manager.on_new_block(250); + assert!(matches!( + transition, + Some(EpochTransition::NewEpoch { + old_epoch: 0, + new_epoch: 2, + .. + }) + )); + + assert_eq!(manager.current_epoch(), 2); + } + + #[test] + fn test_blocks_remaining_decreases() { + let config = EpochConfig { + blocks_per_epoch: 100, + evaluation_blocks: 70, + commit_blocks: 15, + reveal_blocks: 15, + ..Default::default() + }; + + let manager = EpochManager::new(config, 0); + + let initial_remaining = manager.blocks_until_next_phase(); + manager.on_new_block(10); + let new_remaining = manager.blocks_until_next_phase(); + + assert!(new_remaining < initial_remaining); + } } diff --git a/crates/epoch/src/mechanism_weights.rs b/crates/epoch/src/mechanism_weights.rs index 17287d865..0c2f7b0a5 100644 --- a/crates/epoch/src/mechanism_weights.rs +++ b/crates/epoch/src/mechanism_weights.rs @@ -590,4 +590,550 @@ mod tests { assert_eq!(mech_weights.uids[0], BURN_UID); assert_eq!(mech_weights.weights[0], MAX_WEIGHT); } + + #[test] + fn test_zero_emission_weight_goes_to_burn() { + let assignments = vec![WeightAssignment::new("hotkey1".to_string(), 1.0)]; + + let mut hotkey_to_uid: HotkeyUidMap = HashMap::new(); + hotkey_to_uid.insert("hotkey1".to_string(), 1); + + let mech_weights = MechanismWeights::with_hotkey_mapping( + 1, + ChallengeId::new(), + assignments, + 0.0, + &hotkey_to_uid, + ); + + // All weight should go to UID 0 when emission_weight is 0 + assert_eq!(mech_weights.uids.len(), 1); + assert_eq!(mech_weights.uids[0], BURN_UID); + assert_eq!(mech_weights.weights[0], MAX_WEIGHT); + } + + #[test] + fn test_negative_total_weight() { + // Can't really have negative weights, but test zero/invalid case + let assignments = vec![WeightAssignment::new("hotkey1".to_string(), 0.0)]; + + let mut hotkey_to_uid: HotkeyUidMap = HashMap::new(); + hotkey_to_uid.insert("hotkey1".to_string(), 1); + + let mech_weights = MechanismWeights::with_hotkey_mapping( + 1, + ChallengeId::new(), + assignments, + 0.5, + &hotkey_to_uid, + ); + + // Should go to burn when weights are zero + assert_eq!(mech_weights.uids[0], BURN_UID); + } + + #[test] + fn test_hotkey_not_in_mapping() { + let assignments = vec![ + WeightAssignment::new("hotkey1".to_string(), 0.6), + WeightAssignment::new("hotkey_unknown".to_string(), 0.4), + ]; + + let mut hotkey_to_uid: HotkeyUidMap = HashMap::new(); + hotkey_to_uid.insert("hotkey1".to_string(), 1); + + let mech_weights = MechanismWeights::with_hotkey_mapping( + 1, + ChallengeId::new(), + assignments, + 0.5, + &hotkey_to_uid, + ); + + // Should only have weights for hotkey1 + burn + // hotkey_unknown is skipped + assert!(mech_weights.uids.len() <= 2); + } + + #[test] + fn test_uid_zero_skipped() { + let assignments = vec![ + WeightAssignment::new("hotkey_zero".to_string(), 0.5), + WeightAssignment::new("hotkey1".to_string(), 0.5), + ]; + + let mut hotkey_to_uid: HotkeyUidMap = HashMap::new(); + hotkey_to_uid.insert("hotkey_zero".to_string(), BURN_UID); // Maps to UID 0 + hotkey_to_uid.insert("hotkey1".to_string(), 1); + + let mech_weights = MechanismWeights::with_hotkey_mapping( + 1, + ChallengeId::new(), + assignments, + 0.5, + &hotkey_to_uid, + ); + + // hotkey_zero assignment should be skipped, UID 0 gets burn weight + assert!(mech_weights.uids.contains(&BURN_UID)); + assert!(mech_weights.uids.contains(&1)); + } + + #[test] + fn test_as_batch_tuple() { + let assignments = vec![WeightAssignment::new("hotkey1".to_string(), 1.0)]; + + let mut hotkey_to_uid: HotkeyUidMap = HashMap::new(); + hotkey_to_uid.insert("hotkey1".to_string(), 1); + + let mech_weights = MechanismWeights::with_hotkey_mapping( + 5, + ChallengeId::new(), + assignments, + 0.3, + &hotkey_to_uid, + ); + + let (mech_id, uids, weights) = mech_weights.as_batch_tuple(); + assert_eq!(mech_id, 5); + assert_eq!(uids.len(), weights.len()); + } + + #[test] + fn test_mechanism_weight_manager_operations() { + let manager = MechanismWeightManager::new(5); + + assert_eq!(manager.epoch(), 5); + assert_eq!(manager.mechanism_count(), 0); + + let challenge1 = ChallengeId::new(); + manager.register_challenge(challenge1, 1); + + assert_eq!(manager.get_mechanism_for_challenge(&challenge1), Some(1)); + + let weights = vec![WeightAssignment::new("agent".to_string(), 1.0)]; + manager.submit_weights(challenge1, 1, weights, 0.5); + + assert_eq!(manager.mechanism_count(), 1); + assert!(manager.list_mechanisms().contains(&1)); + + let mech_weights = manager.get_mechanism_weights(1); + assert!(mech_weights.is_some()); + + let all_weights = manager.get_all_mechanism_weights(); + assert_eq!(all_weights.len(), 1); + + manager.clear(); + assert_eq!(manager.mechanism_count(), 0); + } + + #[test] + fn test_mechanism_weight_manager_metagraph() { + let manager = MechanismWeightManager::new(1); + let challenge = ChallengeId::new(); + + let mut hotkey_to_uid: HotkeyUidMap = HashMap::new(); + hotkey_to_uid.insert("hotkey1".to_string(), 1); + + let weights = vec![WeightAssignment::new("hotkey1".to_string(), 1.0)]; + + manager.submit_weights_with_metagraph(challenge, 1, weights, 0.5, &hotkey_to_uid); + + assert_eq!(manager.mechanism_count(), 1); + } + + #[test] + fn test_mechanism_commitment_hash() { + let weights = MechanismWeights::new( + 1, + ChallengeId::new(), + vec![WeightAssignment::new("agent".to_string(), 1.0)], + 1.0, + ); + + let salt = b"test_salt"; + let commitment = MechanismCommitment::new(1, 1, &weights, salt); + + assert_eq!(commitment.mechanism_id, 1); + assert_eq!(commitment.epoch, 1); + assert_eq!(commitment.salt, salt); + assert!(!commitment.hash_hex().is_empty()); + } + + #[test] + fn test_mechanism_commitment_different_salts() { + let weights = MechanismWeights::new( + 1, + ChallengeId::new(), + vec![WeightAssignment::new("agent".to_string(), 1.0)], + 1.0, + ); + + let commitment1 = MechanismCommitment::new(1, 1, &weights, b"salt1"); + let commitment2 = MechanismCommitment::new(1, 1, &weights, b"salt2"); + + // Different salts should produce different hashes + assert_ne!(commitment1.commit_hash, commitment2.commit_hash); + } + + #[test] + fn test_mechanism_commit_reveal_manager() { + let manager = MechanismCommitRevealManager::new(); + manager.new_epoch(1); + + let weights = MechanismWeights::new( + 1, + ChallengeId::new(), + vec![WeightAssignment::new("agent".to_string(), 1.0)], + 1.0, + ); + + let salt = b"test_salt".to_vec(); + let commitment = MechanismCommitment::new(1, 1, &weights, &salt); + + manager.commit(commitment.clone()); + + let retrieved = manager.get_commitment(1); + assert!(retrieved.is_some()); + assert_eq!(retrieved.unwrap().mechanism_id, commitment.mechanism_id); + + assert!(manager.reveal(1, weights).is_ok()); + assert!(manager.all_revealed()); + + let revealed_weights = manager.get_revealed_weights(); + assert_eq!(revealed_weights.len(), 1); + } + + #[test] + fn test_mechanism_commit_reveal_mismatch() { + let manager = MechanismCommitRevealManager::new(); + manager.new_epoch(1); + + let assignments1 = vec![WeightAssignment::new("agent1".to_string(), 1.0)]; + let assignments2 = vec![WeightAssignment::new("agent2".to_string(), 1.0)]; + + let mut hotkey_to_uid: HotkeyUidMap = HashMap::new(); + hotkey_to_uid.insert("agent1".to_string(), 1); + hotkey_to_uid.insert("agent2".to_string(), 2); + + let weights1 = MechanismWeights::with_hotkey_mapping( + 1, + ChallengeId::new(), + assignments1, + 1.0, + &hotkey_to_uid, + ); + + let weights2 = MechanismWeights::with_hotkey_mapping( + 1, + ChallengeId::new(), + assignments2, + 1.0, + &hotkey_to_uid, + ); + + let salt = b"test_salt".to_vec(); + let commitment = MechanismCommitment::new(1, 1, &weights1, &salt); + + manager.commit(commitment); + + // Try to reveal with different weights + let result = manager.reveal(1, weights2); + assert!(result.is_err()); + } + + #[test] + fn test_mechanism_commit_reveal_no_commitment() { + let manager = MechanismCommitRevealManager::new(); + manager.new_epoch(1); + + let weights = MechanismWeights::new( + 1, + ChallengeId::new(), + vec![WeightAssignment::new("agent".to_string(), 1.0)], + 1.0, + ); + + // Try to reveal without committing + let result = manager.reveal(1, weights); + assert!(result.is_err()); + } + + #[test] + fn test_mechanism_commit_reveal_manager_default() { + let manager = MechanismCommitRevealManager::default(); + // Verify initial state - with no commitments, all_revealed() returns true (vacuously) + assert!(manager.all_revealed()); + assert!(manager.get_all_commitments().is_empty()); + } + + #[test] + fn test_get_all_commitments() { + let manager = MechanismCommitRevealManager::new(); + manager.new_epoch(1); + + let weights1 = MechanismWeights::new(1, ChallengeId::new(), vec![], 1.0); + let weights2 = MechanismWeights::new(2, ChallengeId::new(), vec![], 1.0); + + let commitment1 = MechanismCommitment::new(1, 1, &weights1, b"salt1"); + let commitment2 = MechanismCommitment::new(2, 1, &weights2, b"salt2"); + + manager.commit(commitment1); + manager.commit(commitment2); + + let all = manager.get_all_commitments(); + assert_eq!(all.len(), 2); + } + + #[test] + fn test_partial_reveals() { + let manager = MechanismCommitRevealManager::new(); + manager.new_epoch(1); + + let weights1 = MechanismWeights::new(1, ChallengeId::new(), vec![], 1.0); + let weights2 = MechanismWeights::new(2, ChallengeId::new(), vec![], 1.0); + + let commitment1 = MechanismCommitment::new(1, 1, &weights1, b"salt1"); + let commitment2 = MechanismCommitment::new(2, 1, &weights2, b"salt2"); + + manager.commit(commitment1); + manager.commit(commitment2); + + // Only reveal mechanism 1 + manager.reveal(1, weights1).unwrap(); + + assert!(!manager.all_revealed()); + } + + #[test] + fn test_emission_weight_clamping() { + let assignments = vec![WeightAssignment::new("hotkey1".to_string(), 1.0)]; + + let mut hotkey_to_uid: HotkeyUidMap = HashMap::new(); + hotkey_to_uid.insert("hotkey1".to_string(), 1); + + // Test clamping to [0, 1] range + let mech_weights_over = MechanismWeights::with_hotkey_mapping( + 1, + ChallengeId::new(), + assignments.clone(), + 1.5, // Over 1.0 + &hotkey_to_uid, + ); + assert_eq!(mech_weights_over.emission_weight, 1.0); + + let mech_weights_under = MechanismWeights::with_hotkey_mapping( + 1, + ChallengeId::new(), + assignments, + -0.5, // Under 0.0 + &hotkey_to_uid, + ); + assert_eq!(mech_weights_under.emission_weight, 0.0); + } + + #[test] + fn test_mechanism_weights_new_warning() { + // Test that MechanismWeights::new() without hotkey mapping works + // (even though it logs a warning) + let assignments = vec![WeightAssignment::new("hotkey1".to_string(), 1.0)]; + + let mech_weights = MechanismWeights::new( + 1, + ChallengeId::new(), + assignments, + 0.5, + ); + + // Should have UID 0 since no hotkeys can be resolved + assert!(mech_weights.uids.contains(&BURN_UID)); + } + + #[test] + fn test_multiple_mechanisms() { + let manager = MechanismWeightManager::new(1); + + let challenge1 = ChallengeId::new(); + let challenge2 = ChallengeId::new(); + let challenge3 = ChallengeId::new(); + + manager.register_challenge(challenge1, 1); + manager.register_challenge(challenge2, 2); + manager.register_challenge(challenge3, 3); + + let weights1 = vec![WeightAssignment::new("a".to_string(), 1.0)]; + let weights2 = vec![WeightAssignment::new("b".to_string(), 1.0)]; + let weights3 = vec![WeightAssignment::new("c".to_string(), 1.0)]; + + manager.submit_weights(challenge1, 1, weights1, 0.3); + manager.submit_weights(challenge2, 2, weights2, 0.3); + manager.submit_weights(challenge3, 3, weights3, 0.4); + + let mechanisms = manager.list_mechanisms(); + assert_eq!(mechanisms.len(), 3); + + let all_weights = manager.get_all_mechanism_weights(); + assert_eq!(all_weights.len(), 3); + } + + #[test] + fn test_mechanism_weights_raw_storage() { + let assignments = vec![ + WeightAssignment::new("hotkey1".to_string(), 0.6), + WeightAssignment::new("hotkey2".to_string(), 0.4), + ]; + + let mut hotkey_to_uid: HotkeyUidMap = HashMap::new(); + hotkey_to_uid.insert("hotkey1".to_string(), 1); + hotkey_to_uid.insert("hotkey2".to_string(), 2); + + let mech_weights = MechanismWeights::with_hotkey_mapping( + 1, + ChallengeId::new(), + assignments.clone(), + 0.5, + &hotkey_to_uid, + ); + + // Verify raw weights are preserved + assert_eq!(mech_weights.raw_weights.len(), 2); + assert_eq!(mech_weights.raw_weights[0].weight, 0.6); + assert_eq!(mech_weights.raw_weights[1].weight, 0.4); + } + + #[test] + fn test_very_small_weights() { + let assignments = vec![ + WeightAssignment::new("hotkey1".to_string(), 0.001), + WeightAssignment::new("hotkey2".to_string(), 0.002), + ]; + + let mut hotkey_to_uid: HotkeyUidMap = HashMap::new(); + hotkey_to_uid.insert("hotkey1".to_string(), 1); + hotkey_to_uid.insert("hotkey2".to_string(), 2); + + let mech_weights = MechanismWeights::with_hotkey_mapping( + 1, + ChallengeId::new(), + assignments, + 0.01, // Very small emission weight + &hotkey_to_uid, + ); + + // Should still handle small weights + assert!(!mech_weights.uids.is_empty()); + let sum: u32 = mech_weights.weights.iter().map(|w| *w as u32).sum(); + assert_eq!(sum, MAX_WEIGHT as u32); + } + + #[test] + fn test_rounding_weights() { + let assignments = vec![ + WeightAssignment::new("hotkey1".to_string(), 0.333), + WeightAssignment::new("hotkey2".to_string(), 0.333), + WeightAssignment::new("hotkey3".to_string(), 0.334), + ]; + + let mut hotkey_to_uid: HotkeyUidMap = HashMap::new(); + hotkey_to_uid.insert("hotkey1".to_string(), 1); + hotkey_to_uid.insert("hotkey2".to_string(), 2); + hotkey_to_uid.insert("hotkey3".to_string(), 3); + + let mech_weights = MechanismWeights::with_hotkey_mapping( + 1, + ChallengeId::new(), + assignments, + 1.0, + &hotkey_to_uid, + ); + + // Verify weights sum correctly despite rounding + let sum: u32 = mech_weights.weights.iter().map(|w| *w as u32).sum(); + assert!((65530..=65540).contains(&sum)); + } + + #[test] + fn test_saturating_sub_in_burn_weight() { + let assignments = vec![WeightAssignment::new("hotkey1".to_string(), 1.0)]; + + let mut hotkey_to_uid: HotkeyUidMap = HashMap::new(); + hotkey_to_uid.insert("hotkey1".to_string(), 1); + + // With 100% emission and rounding, burn weight should be 0 or very small + let mech_weights = MechanismWeights::with_hotkey_mapping( + 1, + ChallengeId::new(), + assignments, + 1.0, + &hotkey_to_uid, + ); + + // Verify proper handling of saturating_sub + let sum: u32 = mech_weights.weights.iter().map(|w| *w as u32).sum(); + assert!((65530..=65540).contains(&sum)); + } + + #[test] + fn test_mechanism_id_storage() { + let mech_weights = MechanismWeights::new( + 255, // Max u8 value + ChallengeId::new(), + vec![], + 0.5, + ); + + assert_eq!(mech_weights.mechanism_id, 255); + + let (mech_id, _, _) = mech_weights.as_batch_tuple(); + assert_eq!(mech_id, 255); + } + + #[test] + fn test_complete_workflow() { + // Complete workflow test + let manager = MechanismWeightManager::new(10); + + let challenge = ChallengeId::new(); + manager.register_challenge(challenge, 5); + + let mut hotkey_to_uid: HotkeyUidMap = HashMap::new(); + hotkey_to_uid.insert("validator1".to_string(), 10); + hotkey_to_uid.insert("validator2".to_string(), 20); + + let weights = vec![ + WeightAssignment::new("validator1".to_string(), 0.7), + WeightAssignment::new("validator2".to_string(), 0.3), + ]; + + manager.submit_weights_with_metagraph(challenge, 5, weights, 0.4, &hotkey_to_uid); + + let retrieved = manager.get_mechanism_weights(5); + assert!(retrieved.is_some()); + + let mech_weights = retrieved.unwrap(); + assert_eq!(mech_weights.mechanism_id, 5); + assert_eq!(mech_weights.challenge_id, challenge); + + let all = manager.get_all_mechanism_weights(); + assert_eq!(all.len(), 1); + + let (mech_id, uids, weights) = &all[0]; + assert_eq!(*mech_id, 5); + assert!(!uids.is_empty()); + assert_eq!(uids.len(), weights.len()); + } + + #[test] + fn test_unknown_challenge_mechanism() { + let manager = MechanismWeightManager::new(1); + let unknown_challenge = ChallengeId::new(); + + assert_eq!(manager.get_mechanism_for_challenge(&unknown_challenge), None); + } + + #[test] + fn test_get_nonexistent_mechanism() { + let manager = MechanismWeightManager::new(1); + assert!(manager.get_mechanism_weights(99).is_none()); + } }