Skip to main content

ipfrs_tensorlogic/gradient/
mod.rs

1//! Gradient storage and management for federated learning
2//!
3//! This module provides:
4//! - Gradient delta format (differences from base model)
5//! - Gradient compression (sparsification, quantization, top-k)
6//! - Gradient aggregation (averaging, weighted, momentum)
7//! - Gradient verification (checksum, shape, outliers)
8//! - Backward pass coordination via TensorSwap
9//! - CID-linked computation graph for gradient tracking
10//! - Gradient checkpointing for training resumption
11
12use crate::arrow::TensorDtype;
13use thiserror::Error;
14
15pub mod arrow_ipc;
16pub mod backward_pass;
17pub mod checkpoint;
18pub mod computation_graph;
19pub mod coordination;
20pub mod federated;
21pub mod tensor;
22
23// ── GradientError ──────────────────────────────────────────────────────────
24
25/// Errors that can occur during gradient operations
26#[derive(Debug, Error)]
27pub enum GradientError {
28    #[error("Shape mismatch: expected {expected:?}, got {actual:?}")]
29    ShapeMismatch {
30        expected: Vec<usize>,
31        actual: Vec<usize>,
32    },
33
34    #[error("Checksum verification failed")]
35    ChecksumFailed,
36
37    #[error("Invalid compression ratio: {0}")]
38    InvalidCompressionRatio(f32),
39
40    #[error("Empty gradient set")]
41    EmptyGradientSet,
42
43    #[error("Incompatible dtype: {0:?}")]
44    IncompatibleDtype(TensorDtype),
45
46    #[error("Outlier detected at index {index}: value {value}")]
47    OutlierDetected { index: usize, value: f32 },
48
49    #[error("Invalid gradient: {0}")]
50    InvalidGradient(String),
51
52    #[error("Empty gradients provided")]
53    EmptyGradients,
54
55    #[error("Gradient dimension mismatch between peers")]
56    DimensionMismatch,
57
58    #[error("Node not found in backward pass schedule: {0}")]
59    NodeNotFound(String),
60
61    #[error("Peer not found in step: {0}")]
62    PeerNotFound(String),
63}
64
65// ── Re-exports ─────────────────────────────────────────────────────────────
66
67pub use arrow_ipc::{load_gradient_from_arrow, store_gradient_as_arrow};
68
69pub use backward_pass::{
70    clip_gradient_norm, federated_average, AggregationMethod, BackwardPassConfig,
71    BackwardPassCoordinator as LegacyBackwardPassCoordinator, BackwardPassStats, BackwardPassStep,
72    BackwardStepStatus,
73};
74
75pub use checkpoint::GradientCheckpoint;
76
77pub use computation_graph::{ComputationGraphError, ComputationGraphStore, ComputationNode};
78
79pub use federated::{
80    ClientInfo, ClientState, ConvergenceConfig, ConvergenceDetector, DPMechanism,
81    DifferentialPrivacy, DistributedGradientAccumulator, FederatedError, FederatedRound,
82    GossipModelSync, ModelSyncProtocol, ModelUpdate, PrivacyBudget, RoundStats, SecureAggregation,
83};
84
85pub use tensor::{
86    GradientAggregator, GradientCompressor, GradientDelta, GradientVerifier, LayerGradient,
87    QuantizedGradient, SparseGradient,
88};
89
90pub use coordination::{
91    ArrowBlockError, BackwardPassCoordinator, BackwardPassId, CoordinationError,
92    CoordinationStatus, GradientArrowBlock, GradientContribution,
93};
94
95// ── Tests originally in the top-level gradient module ─────────────────────
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use ipfrs_core::Cid;
101
102    #[test]
103    fn test_sparse_gradient() {
104        let indices = vec![0, 5, 10];
105        let values = vec![1.0, 2.0, 3.0];
106        let shape = vec![20];
107
108        let sparse = SparseGradient::new(indices.clone(), values.clone(), shape);
109
110        assert_eq!(sparse.nnz(), 3);
111        assert_eq!(sparse.total_elements(), 20);
112        assert!((sparse.sparsity_ratio() - 0.85).abs() < 0.01);
113
114        let dense = sparse.to_dense();
115        assert_eq!(dense.len(), 20);
116        assert_eq!(dense[0], 1.0);
117        assert_eq!(dense[5], 2.0);
118        assert_eq!(dense[10], 3.0);
119    }
120
121    #[test]
122    fn test_quantized_gradient() {
123        let values = vec![1.0, 2.0, 3.0, 4.0, 5.0];
124        let shape = vec![5];
125
126        let quantized = QuantizedGradient::from_dense(&values, shape);
127        let dequantized = quantized.to_dense();
128
129        // Check that dequantization is approximately correct
130        // For a small range like [1,5] with 256 quantization levels,
131        // we expect good precision
132        for (i, (orig, deq)) in values.iter().zip(&dequantized).enumerate() {
133            let error = (orig - deq).abs();
134            // Allow for quantization error (scale = 4/255 ≈ 0.0157)
135            assert!(
136                error < 0.02,
137                "Value {} mismatch: orig={}, deq={}, error={}",
138                i,
139                orig,
140                deq,
141                error
142            );
143        }
144    }
145
146    #[test]
147    fn test_gradient_delta() {
148        let base_cid = Cid::default();
149        let mut delta = GradientDelta::new(base_cid);
150
151        delta.add_dense_gradient("layer1".to_string(), vec![1.0, 2.0, 3.0], vec![3]);
152        delta.add_dense_gradient("layer2".to_string(), vec![4.0, 5.0], vec![2]);
153
154        assert_eq!(delta.layer_gradients.len(), 2);
155        assert!(delta.verify_checksum().is_ok());
156    }
157
158    #[test]
159    fn test_top_k_compression() {
160        let values = vec![1.0, 5.0, 2.0, 8.0, 3.0];
161        let shape = vec![5];
162
163        let sparse = GradientCompressor::top_k(&values, shape, 2).expect("test: should succeed");
164
165        assert_eq!(sparse.nnz(), 2);
166        assert!(sparse.values.contains(&8.0));
167        assert!(sparse.values.contains(&5.0));
168    }
169
170    #[test]
171    fn test_threshold_compression() {
172        let values = vec![0.1, 5.0, 0.2, 8.0, 0.3];
173        let shape = vec![5];
174
175        let sparse = GradientCompressor::threshold(&values, shape, 1.0);
176
177        assert_eq!(sparse.nnz(), 2);
178        assert!(sparse.values.contains(&5.0));
179        assert!(sparse.values.contains(&8.0));
180    }
181
182    #[test]
183    fn test_gradient_averaging() {
184        let g1 = vec![1.0, 2.0, 3.0];
185        let g2 = vec![3.0, 4.0, 5.0];
186        let gradients = vec![g1, g2];
187
188        let avg = GradientAggregator::average(&gradients).expect("test: should succeed");
189
190        assert_eq!(avg, vec![2.0, 3.0, 4.0]);
191    }
192
193    #[test]
194    fn test_weighted_averaging() {
195        let g1 = vec![1.0, 2.0, 3.0];
196        let g2 = vec![3.0, 4.0, 5.0];
197        let gradients = vec![g1, g2];
198        let weights = vec![0.25, 0.75];
199
200        let avg = GradientAggregator::weighted_average(&gradients, &weights)
201            .expect("test: should succeed");
202
203        // Expected: 0.25 * [1,2,3] + 0.75 * [3,4,5] = [2.5, 3.5, 4.5]
204        assert!((avg[0] - 2.5).abs() < 0.01);
205        assert!((avg[1] - 3.5).abs() < 0.01);
206        assert!((avg[2] - 4.5).abs() < 0.01);
207    }
208
209    #[test]
210    fn test_momentum() {
211        let current = vec![1.0, 2.0, 3.0];
212        let previous = vec![0.5, 1.0, 1.5];
213
214        let result = GradientAggregator::apply_momentum(&current, &previous, 0.9)
215            .expect("test: should succeed");
216
217        // Expected: 0.9 * previous + current
218        assert!((result[0] - 1.45).abs() < 0.01);
219        assert!((result[1] - 2.9).abs() < 0.01);
220        assert!((result[2] - 4.35).abs() < 0.01);
221    }
222
223    #[test]
224    fn test_gradient_verification() {
225        let gradient = vec![1.0, 2.0, 3.0, 4.0];
226
227        // Test shape verification
228        assert!(GradientVerifier::verify_shape(&gradient, &[4]).is_ok());
229        assert!(GradientVerifier::verify_shape(&gradient, &[2, 2]).is_ok());
230        assert!(GradientVerifier::verify_shape(&gradient, &[5]).is_err());
231
232        // Test finite verification
233        assert!(GradientVerifier::verify_finite(&gradient).is_ok());
234
235        let invalid = vec![1.0, f32::NAN, 3.0];
236        assert!(GradientVerifier::verify_finite(&invalid).is_err());
237    }
238
239    #[test]
240    fn test_gradient_clipping() {
241        let mut gradient = vec![3.0, 4.0]; // L2 norm = 5.0
242
243        GradientVerifier::clip_by_norm(&mut gradient, 2.5);
244
245        let norm = GradientVerifier::l2_norm(&gradient);
246        assert!((norm - 2.5).abs() < 0.01);
247    }
248
249    #[test]
250    fn test_privacy_budget() {
251        let mut budget = PrivacyBudget::new(1.0, 1e-5);
252
253        assert_eq!(budget.remaining_epsilon, 1.0);
254        assert!(!budget.is_exhausted());
255
256        // Consume some budget
257        budget.consume(0.5).expect("test: should succeed");
258        assert_eq!(budget.remaining_epsilon, 0.5);
259        assert!((budget.remaining_fraction() - 0.5).abs() < 1e-6);
260
261        // Consume remaining budget
262        budget.consume(0.5).expect("test: should succeed");
263        assert!(budget.is_exhausted());
264
265        // Should fail when budget is exhausted
266        assert!(budget.consume(0.1).is_err());
267    }
268
269    #[test]
270    fn test_differential_privacy_gaussian() {
271        let mut dp = DifferentialPrivacy::new(1.0, 1e-5, 1.0, DPMechanism::Gaussian);
272        let mut gradient = vec![1.0, 2.0, 3.0, 4.0];
273        let original = gradient.clone();
274
275        dp.add_gaussian_noise(&mut gradient)
276            .expect("test: should succeed");
277
278        // Gradient should be modified (with very high probability)
279        assert_ne!(gradient, original);
280
281        // Values should still be finite
282        assert!(GradientVerifier::verify_finite(&gradient).is_ok());
283
284        // Budget should be consumed
285        assert!(dp.remaining_budget() < 1.0);
286    }
287
288    #[test]
289    fn test_differential_privacy_laplacian() {
290        let mut dp = DifferentialPrivacy::new(1.0, 1e-5, 1.0, DPMechanism::Laplacian);
291        let mut gradient = vec![1.0, 2.0, 3.0, 4.0];
292        let original = gradient.clone();
293
294        dp.add_laplacian_noise(&mut gradient)
295            .expect("test: should succeed");
296
297        // Gradient should be modified (with very high probability)
298        assert_ne!(gradient, original);
299
300        // Values should still be finite
301        assert!(GradientVerifier::verify_finite(&gradient).is_ok());
302
303        // Budget should be consumed
304        assert!(dp.remaining_budget() < 1.0);
305    }
306
307    #[test]
308    fn test_dp_sgd() {
309        let mut dp = DifferentialPrivacy::new(1.0, 1e-5, 1.0, DPMechanism::Gaussian);
310        let mut gradient = vec![3.0, 4.0, 5.0, 6.0]; // L2 norm > 5.0
311        let original_norm = GradientVerifier::l2_norm(&gradient);
312
313        dp.apply_dp_sgd(&mut gradient, 5.0)
314            .expect("test: should succeed");
315
316        // Gradient should be clipped and noised
317        let new_norm = GradientVerifier::l2_norm(&gradient);
318
319        // After clipping and noise, norm might be around 5.0 but not exact due to noise
320        // Just check it's different from original
321        assert!(original_norm != new_norm);
322
323        // Values should still be finite
324        assert!(GradientVerifier::verify_finite(&gradient).is_ok());
325    }
326
327    #[test]
328    fn test_privacy_budget_exhaustion() {
329        let mut dp = DifferentialPrivacy::new(1.0, 1e-5, 1.0, DPMechanism::Gaussian);
330        let mut gradient = vec![1.0, 2.0];
331
332        // Consume budget multiple times
333        // Each call consumes epsilon/100 = 0.01, so we need 100 calls to exhaust budget of 1.0
334        let mut successful_calls = 0;
335        for _ in 0..200 {
336            if dp.add_gaussian_noise(&mut gradient).is_ok() {
337                successful_calls += 1;
338            } else {
339                // Budget exhausted, break
340                break;
341            }
342        }
343
344        // Should have made ~100 successful calls before budget exhaustion
345        assert!(
346            (90..=110).contains(&successful_calls),
347            "Expected ~100 calls, got {}",
348            successful_calls
349        );
350
351        // Budget should be very low or exhausted (allow small epsilon for floating point errors)
352        let remaining = dp.remaining_budget();
353        assert!(
354            remaining < 0.02,
355            "Expected nearly exhausted budget, got {}",
356            remaining
357        );
358
359        // Should fail when trying to consume more than remaining
360        let mut new_gradient = vec![1.0, 2.0];
361        let result = dp.add_gaussian_noise(&mut new_gradient);
362        // Might succeed if there's a tiny bit of budget left, or fail if exhausted
363        // Either way is acceptable at this point
364        let _ = result;
365    }
366
367    #[test]
368    fn test_noise_multiplier_calculation() {
369        let epsilon = 1.0;
370        let delta = 1e-5;
371        let sensitivity = 1.0;
372
373        let multiplier =
374            DifferentialPrivacy::calculate_noise_multiplier(epsilon, delta, sensitivity);
375
376        // Noise multiplier should be positive and reasonable
377        assert!(multiplier > 0.0);
378        assert!(multiplier < 10.0); // Sanity check
379
380        // For higher epsilon (less privacy), noise should be lower
381        let multiplier_high_eps =
382            DifferentialPrivacy::calculate_noise_multiplier(10.0, delta, sensitivity);
383        assert!(multiplier_high_eps < multiplier);
384    }
385
386    #[test]
387    fn test_secure_aggregation() {
388        let mut aggregator = SecureAggregation::new(3);
389
390        assert_eq!(aggregator.participant_count(), 0);
391        assert!(!aggregator.can_aggregate());
392
393        // Add participants
394        aggregator.add_participant();
395        aggregator.add_participant();
396        assert!(!aggregator.can_aggregate());
397
398        aggregator.add_participant();
399        assert!(aggregator.can_aggregate());
400
401        // Test aggregation
402        let g1 = vec![1.0, 2.0, 3.0];
403        let g2 = vec![2.0, 3.0, 4.0];
404        let g3 = vec![3.0, 4.0, 5.0];
405        let gradients = vec![g1, g2, g3];
406
407        let result = aggregator
408            .aggregate_secure(&gradients)
409            .expect("test: should succeed");
410
411        // Should be average of the three gradients
412        assert!((result[0] - 2.0).abs() < 0.01);
413        assert!((result[1] - 3.0).abs() < 0.01);
414        assert!((result[2] - 4.0).abs() < 0.01);
415
416        // Reset
417        aggregator.reset();
418        assert_eq!(aggregator.participant_count(), 0);
419    }
420
421    #[test]
422    fn test_secure_aggregation_insufficient_participants() {
423        let aggregator = SecureAggregation::new(5);
424
425        let g1 = vec![1.0, 2.0];
426        let g2 = vec![3.0, 4.0];
427        let gradients = vec![g1, g2];
428
429        // Should fail because we don't have enough participants
430        let result = aggregator.aggregate_secure(&gradients);
431        assert!(result.is_err());
432    }
433
434    #[test]
435    fn test_dp_mechanism_types() {
436        let gaussian = DPMechanism::Gaussian;
437        let laplacian = DPMechanism::Laplacian;
438
439        assert_eq!(gaussian, DPMechanism::Gaussian);
440        assert_eq!(laplacian, DPMechanism::Laplacian);
441        assert_ne!(gaussian, laplacian);
442    }
443
444    #[test]
445    fn test_client_info() {
446        let mut client = ClientInfo::new("client1".to_string(), 1000);
447
448        assert_eq!(client.client_id, "client1");
449        assert_eq!(client.state, ClientState::Idle);
450        assert_eq!(client.sample_count, 1000);
451
452        client.start_training();
453        assert_eq!(client.state, ClientState::Training);
454
455        client.complete_training();
456        assert_eq!(client.state, ClientState::Completed);
457
458        client.mark_failed();
459        assert_eq!(client.state, ClientState::Failed);
460    }
461
462    #[test]
463    fn test_federated_round() {
464        let model_cid = Cid::default();
465        let mut round = FederatedRound::new(0, model_cid, 5);
466
467        assert_eq!(round.round_num, 0);
468        assert_eq!(round.client_count, 5);
469        assert_eq!(round.completed_count, 0);
470        assert!(!round.is_complete());
471
472        // Mark clients as completed
473        for _ in 0..5 {
474            round.mark_client_completed();
475        }
476
477        assert_eq!(round.completed_count, 5);
478        assert!(round.is_complete());
479
480        // Complete the round
481        let gradient = vec![1.0, 2.0, 3.0];
482        round.complete(gradient.clone());
483
484        assert_eq!(round.aggregated_gradient, Some(gradient));
485        assert!(round.end_time.is_some());
486        assert!(round.duration().is_some());
487    }
488
489    #[test]
490    fn test_convergence_detector() {
491        let mut detector = ConvergenceDetector::new(3, 0.01);
492
493        // Add loss values that are converging
494        detector.add_loss(1.0);
495        detector.add_loss(0.99);
496        detector.add_loss(0.98);
497
498        assert!(detector.has_converged());
499        assert_eq!(detector.latest_loss(), Some(0.98));
500        assert_eq!(detector.history().len(), 3);
501
502        // Reset
503        detector.reset();
504        assert_eq!(detector.history().len(), 0);
505    }
506
507    #[test]
508    fn test_convergence_detector_not_converged() {
509        let mut detector = ConvergenceDetector::new(3, 0.01);
510
511        // Add loss values that are NOT converging
512        detector.add_loss(1.0);
513        detector.add_loss(0.5);
514        detector.add_loss(1.5);
515
516        assert!(!detector.has_converged());
517    }
518
519    #[test]
520    fn test_model_sync_protocol() {
521        let mut protocol = ModelSyncProtocol::new(10, 3, 3, 0.01);
522
523        assert_eq!(protocol.current_round(), 0);
524        assert_eq!(protocol.max_rounds(), 10);
525        assert!(protocol.should_continue());
526
527        // Start round 0
528        let model_cid = Cid::default();
529        let round_num = protocol
530            .start_round(model_cid, 5)
531            .expect("test: should succeed");
532
533        assert_eq!(round_num, 0);
534        assert_eq!(protocol.current_round(), 1);
535        assert_eq!(protocol.total_rounds(), 1);
536
537        // Complete round 0
538        let gradient = vec![1.0, 2.0, 3.0];
539        protocol
540            .complete_round(round_num, gradient.clone(), 1.0)
541            .expect("test: should succeed");
542
543        assert_eq!(protocol.latest_loss(), Some(1.0));
544
545        // Get round info
546        let round = protocol.get_round(0).expect("test: should succeed");
547        assert_eq!(round.round_num, 0);
548        assert_eq!(round.aggregated_gradient, Some(gradient));
549    }
550
551    #[test]
552    fn test_model_sync_protocol_convergence() {
553        let mut protocol = ModelSyncProtocol::new(10, 2, 3, 0.01);
554
555        let model_cid = Cid::default();
556
557        // Run multiple rounds with converging loss
558        for i in 0..3 {
559            protocol
560                .start_round(model_cid, 3)
561                .expect("test: should succeed");
562            let gradient = vec![1.0, 2.0];
563            let loss = 1.0 - (i as f64 * 0.001);
564            protocol
565                .complete_round(i, gradient, loss)
566                .expect("test: should succeed");
567        }
568
569        // Should have converged
570        assert!(protocol.has_converged());
571        assert!(!protocol.should_continue());
572    }
573
574    #[test]
575    fn test_model_sync_protocol_max_rounds() {
576        let mut protocol = ModelSyncProtocol::new(2, 1, 3, 0.01);
577
578        let model_cid = Cid::default();
579
580        // Start 2 rounds (max)
581        protocol
582            .start_round(model_cid, 2)
583            .expect("test: should succeed");
584        protocol
585            .start_round(model_cid, 2)
586            .expect("test: should succeed");
587
588        // Should fail to start a third round
589        let result = protocol.start_round(model_cid, 2);
590        assert!(result.is_err());
591    }
592
593    #[test]
594    fn test_model_sync_protocol_min_clients() {
595        let mut protocol = ModelSyncProtocol::new(10, 5, 3, 0.01);
596
597        let model_cid = Cid::default();
598
599        // Should fail with too few clients
600        let result = protocol.start_round(model_cid, 3);
601        assert!(result.is_err());
602
603        // Should succeed with enough clients
604        let result = protocol.start_round(model_cid, 5);
605        assert!(result.is_ok());
606    }
607
608    #[test]
609    fn test_client_state_enum() {
610        let idle = ClientState::Idle;
611        let training = ClientState::Training;
612        let completed = ClientState::Completed;
613        let failed = ClientState::Failed;
614
615        assert_ne!(idle, training);
616        assert_ne!(training, completed);
617        assert_ne!(completed, failed);
618        assert_eq!(idle, ClientState::Idle);
619    }
620}
621
622// ── DistributedGradientAccumulator tests ─────────────────────────────────
623
624#[cfg(test)]
625mod distributed_accumulator_tests {
626    use super::*;
627    use ipfrs_storage::traits::BlockStore as _;
628
629    /// Build a simple in-memory [`BlockStore`] backed by an Arc<DashMap>.
630    ///
631    /// We use the real `ipfrs_storage::MemoryBlockStore` so we can exercise
632    /// the full `commit_local` / `add_peer_gradient` path without mocking.
633    fn make_store() -> std::sync::Arc<ipfrs_storage::MemoryBlockStore> {
634        std::sync::Arc::new(ipfrs_storage::MemoryBlockStore::new())
635    }
636
637    #[tokio::test]
638    async fn test_distributed_accumulator_fedavg() {
639        let store = make_store();
640
641        let config = BackwardPassConfig::default();
642        let mut acc = DistributedGradientAccumulator::new("session-fedavg", config);
643
644        // Local gradient: [1, 2, 3]
645        let local_grad = vec![1.0f32, 2.0, 3.0];
646        let _cid = acc
647            .commit_local(local_grad, &*store)
648            .await
649            .expect("commit_local");
650
651        // Peer A gradient: [3, 4, 5] — store it directly and add to accumulator.
652        let peer_a_bytes =
653            store_gradient_as_arrow(&[3.0f32, 4.0, 5.0]).expect("peer_a arrow encode");
654        let block_a = ipfrs_core::Block::new(bytes::Bytes::from(peer_a_bytes)).expect("block_a");
655        let cid_a = block_a.cid();
656        store.put(&block_a).await.expect("put block_a");
657
658        acc.add_peer_gradient("peer_a", cid_a, &*store)
659            .await
660            .expect("add_peer_gradient peer_a");
661
662        // Peer B gradient: [5, 6, 7]
663        let peer_b_bytes =
664            store_gradient_as_arrow(&[5.0f32, 6.0, 7.0]).expect("peer_b arrow encode");
665        let block_b = ipfrs_core::Block::new(bytes::Bytes::from(peer_b_bytes)).expect("block_b");
666        let cid_b = block_b.cid();
667        store.put(&block_b).await.expect("put block_b");
668
669        acc.add_peer_gradient("peer_b", cid_b, &*store)
670            .await
671            .expect("add_peer_gradient peer_b");
672
673        assert_eq!(acc.peer_count(), 2, "should have 2 peer gradients");
674
675        // FedAvg of [1,2,3], [3,4,5], [5,6,7] = [3,4,5]
676        let agg = acc.aggregate().expect("aggregate");
677        assert_eq!(agg.len(), 3);
678        assert!((agg[0] - 3.0).abs() < 1e-5, "agg[0] = {}", agg[0]);
679        assert!((agg[1] - 4.0).abs() < 1e-5, "agg[1] = {}", agg[1]);
680        assert!((agg[2] - 5.0).abs() < 1e-5, "agg[2] = {}", agg[2]);
681    }
682
683    #[test]
684    fn test_accumulator_not_ready() {
685        let config = BackwardPassConfig::default();
686        let mut acc = DistributedGradientAccumulator::new("session-not-ready", config);
687
688        // Inject one peer gradient directly (no async store needed here).
689        acc.peer_gradients
690            .insert("peer_x".to_string(), vec![1.0f32, 2.0]);
691
692        // With 1 peer, `is_ready(3)` must return false.
693        assert!(
694            !acc.is_ready(3),
695            "is_ready(3) must be false with only 1 peer"
696        );
697
698        // With 1 peer, `is_ready(1)` must return true.
699        assert!(acc.is_ready(1), "is_ready(1) must be true with 1 peer");
700    }
701}