1pub mod scheme;
85pub mod types;
86
87cfg_if::cfg_if! {
88 if #[cfg(not(target_arch = "wasm32"))] {
89 mod config;
90 pub use config::Config;
91 mod engine;
92 pub use engine::Engine;
93 mod metrics;
94 mod safe_tip;
95
96 #[cfg(test)]
97 pub mod mocks;
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::{mocks, Config, Engine};
104 use crate::{
105 aggregation::scheme::{bls12381_multisig, bls12381_threshold, ed25519, secp256r1, Scheme},
106 types::{Epoch, EpochDelta, Height, HeightDelta},
107 };
108 use commonware_cryptography::{
109 bls12381::primitives::variant::{MinPk, MinSig},
110 certificate::mocks::Fixture,
111 ed25519::PublicKey,
112 sha256::Digest as Sha256Digest,
113 };
114 use commonware_macros::{select, test_group, test_traced};
115 use commonware_p2p::simulated::{Link, Network, Oracle, Receiver, Sender};
116 use commonware_parallel::Sequential;
117 use commonware_runtime::{
118 buffer::paged::CacheRef,
119 deterministic::{self, Context},
120 Clock, Quota, Runner, Spawner, Supervisor as _,
121 };
122 use commonware_utils::{
123 channel::{fallible::OneshotExt, oneshot},
124 test_rng, NZUsize, NonZeroDuration, TestRng, NZU16,
125 };
126 use futures::future::join_all;
127 use rand::RngExt as _;
128 use std::{
129 collections::BTreeMap,
130 num::{NonZeroU16, NonZeroU32, NonZeroUsize},
131 time::Duration,
132 };
133 use tracing::debug;
134
135 macro_rules! for_each_fixture {
137 ($cb:ident!($($args:tt)*)) => {
138 $cb!($($args)*, bls12381_threshold_min_pk, bls12381_threshold::fixture::<MinPk, _>);
139 $cb!($($args)*, bls12381_threshold_min_sig, bls12381_threshold::fixture::<MinSig, _>);
140 $cb!($($args)*, bls12381_multisig_min_pk, bls12381_multisig::fixture::<MinPk, _>);
141 $cb!($($args)*, bls12381_multisig_min_sig, bls12381_multisig::fixture::<MinSig, _>);
142 $cb!($($args)*, ed25519, ed25519::fixture);
143 $cb!($($args)*, secp256r1, secp256r1::fixture);
144 };
145 }
146
147 macro_rules! test_for_all_fixtures {
151 ($callee:ident) => {
152 for_each_fixture!(test_for_all_fixtures!(@emit [] $callee));
153 };
154 (slow $callee:ident) => {
155 for_each_fixture!(test_for_all_fixtures!(@emit [#[test_group("slow")]] $callee));
156 };
157 (@emit [$(#[$attr:meta])*] $callee:ident, $suffix:ident, $fixture:expr) => {
158 paste::paste! {
159 $(#[$attr])*
160 #[test_traced("INFO")]
161 fn [<test_ $callee _ $suffix>]() {
162 $callee($fixture);
163 }
164 }
165 };
166 }
167
168 type Registrations<P> = BTreeMap<P, (Sender<P, deterministic::Context>, Receiver<P>)>;
169
170 const PAGE_SIZE: NonZeroU16 = NZU16!(1024);
171 const PAGE_CACHE_SIZE: NonZeroUsize = NZUsize!(10);
172 const TEST_QUOTA: Quota = Quota::per_second(NonZeroU32::MAX);
173 const TEST_NAMESPACE: &[u8] = b"my testing namespace";
174
175 const RELIABLE_LINK: Link = Link {
177 latency: Duration::from_millis(10),
178 jitter: Duration::from_millis(1),
179 success_rate: 1.0,
180 };
181
182 async fn register_participants(
184 oracle: &mut Oracle<PublicKey, deterministic::Context>,
185 participants: &[PublicKey],
186 ) -> Registrations<PublicKey> {
187 let mut registrations = BTreeMap::new();
188 for participant in participants.iter() {
189 let (sender, receiver) = oracle
190 .control(participant.clone())
191 .register(0, TEST_QUOTA)
192 .await
193 .unwrap();
194 registrations.insert(participant.clone(), (sender, receiver));
195 }
196 registrations
197 }
198
199 async fn link_participants(
201 oracle: &mut Oracle<PublicKey, deterministic::Context>,
202 participants: &[PublicKey],
203 link: Link,
204 ) {
205 for v1 in participants.iter() {
206 for v2 in participants.iter() {
207 if v2 == v1 {
208 continue;
209 }
210 oracle
211 .add_link(v1.clone(), v2.clone(), link.clone())
212 .await
213 .unwrap();
214 }
215 }
216 }
217
218 async fn initialize_simulation<S: Scheme<Sha256Digest, PublicKey = PublicKey>>(
220 context: Context,
221 fixture: &Fixture<S>,
222 link: Link,
223 ) -> (
224 Oracle<PublicKey, deterministic::Context>,
225 Registrations<PublicKey>,
226 ) {
227 let (network, mut oracle) = Network::new_with_peers(
228 context.child("network"),
229 commonware_p2p::simulated::Config {
230 max_size: 1024 * 1024,
231 disconnect_on_block: true,
232 tracked_peer_sets: NZUsize!(1),
233 },
234 fixture.participants.clone(),
235 )
236 .await;
237 network.start();
238
239 let registrations = register_participants(&mut oracle, &fixture.participants).await;
240 link_participants(&mut oracle, &fixture.participants, link).await;
241
242 (oracle, registrations)
243 }
244
245 fn spawn_validator_engines<S: Scheme<Sha256Digest, PublicKey = PublicKey>>(
247 context: Context,
248 fixture: &Fixture<S>,
249 registrations: &mut Registrations<PublicKey>,
250 oracle: &mut Oracle<PublicKey, deterministic::Context>,
251 epoch: Epoch,
252 rebroadcast_timeout: Duration,
253 incorrect: Vec<usize>,
254 ) -> BTreeMap<PublicKey, mocks::ReporterMailbox<S, Sha256Digest>> {
255 let mut reporters = BTreeMap::new();
256
257 for (idx, participant) in fixture.participants.iter().enumerate() {
258 let context = context
259 .child("participant")
260 .with_attribute("public_key", participant);
261
262 let provider = mocks::Provider::new();
264 assert!(provider.register(epoch, fixture.schemes[idx].clone()));
265
266 let monitor = mocks::Monitor::new(epoch);
268
269 let strategy = if incorrect.contains(&idx) {
271 mocks::Strategy::Incorrect
272 } else {
273 mocks::Strategy::Correct
274 };
275 let automaton = mocks::Application::new(strategy);
276
277 let (reporter, reporter_mailbox) =
279 mocks::Reporter::new(context.child("reporter"), fixture.verifier.clone());
280 reporter.start();
281 reporters.insert(participant.clone(), reporter_mailbox.clone());
282
283 let blocker = oracle.control(participant.clone());
285
286 let engine = Engine::new(
288 context.child("engine"),
289 Config {
290 monitor,
291 provider,
292 automaton,
293 reporter: reporter_mailbox,
294 blocker,
295 priority_acks: false,
296 rebroadcast_timeout: NonZeroDuration::new_panic(rebroadcast_timeout),
297 epoch_bounds: (EpochDelta::new(1), EpochDelta::new(1)),
298 window: std::num::NonZeroU64::new(10).unwrap(),
299 activity_timeout: HeightDelta::new(100),
300 journal_partition: format!("aggregation-{participant}"),
301 journal_write_buffer: NZUsize!(4096),
302 journal_replay_buffer: NZUsize!(4096),
303 journal_heights_per_section: std::num::NonZeroU64::new(6).unwrap(),
304 journal_compression: Some(3),
305 journal_page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
306 strategy: Sequential,
307 },
308 );
309
310 let (sender, receiver) = registrations.remove(participant).unwrap();
311 engine.start((sender, receiver));
312 }
313
314 reporters
315 }
316
317 async fn await_reporters<S: Scheme<Sha256Digest, PublicKey = PublicKey>>(
319 context: Context,
320 reporters: &BTreeMap<PublicKey, mocks::ReporterMailbox<S, Sha256Digest>>,
321 threshold_height: Height,
322 threshold_epoch: Epoch,
323 ) {
324 let mut receivers = Vec::new();
325 for (reporter, mailbox) in reporters.iter() {
326 let (tx, rx) = oneshot::channel();
328 receivers.push(rx);
329
330 context
331 .child("reporter_watcher")
332 .with_attribute("reporter", reporter)
333 .spawn({
334 let reporter = reporter.clone();
335 let mut mailbox = mailbox.clone();
336 move |context| async move {
337 loop {
338 let (height, epoch) = mailbox
339 .get_tip()
340 .await
341 .unwrap_or((Height::zero(), Epoch::zero()));
342 debug!(
343 %height,
344 epoch = %epoch,
345 %threshold_height,
346 threshold_epoch = %threshold_epoch,
347 ?reporter,
348 "reporter status"
349 );
350 if height >= threshold_height && epoch >= threshold_epoch {
351 debug!(
352 ?reporter,
353 "reporter reached threshold, signaling completion"
354 );
355 tx.send_lossy(reporter.clone());
356 break;
357 }
358 context.sleep(Duration::from_millis(100)).await;
359 }
360 }
361 });
362 }
363
364 let results = join_all(receivers).await;
366 assert_eq!(results.len(), reporters.len());
367
368 for result in results {
370 assert!(result.is_ok(), "reporter was cancelled");
371 }
372 }
373
374 fn all_online<S, F>(fixture: F)
376 where
377 S: Scheme<Sha256Digest, PublicKey = PublicKey>,
378 F: FnOnce(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
379 {
380 let runner = deterministic::Runner::timed(Duration::from_secs(30));
381
382 runner.start(|mut context| async move {
383 let num_validators = 4;
384 let fixture = fixture(&mut context, TEST_NAMESPACE, num_validators);
385 let epoch = Epoch::new(111);
386
387 let (mut oracle, mut registrations) =
388 initialize_simulation(context.child("simulation"), &fixture, RELIABLE_LINK).await;
389
390 let reporters = spawn_validator_engines(
391 context.child("validator"),
392 &fixture,
393 &mut registrations,
394 &mut oracle,
395 epoch,
396 Duration::from_secs(5),
397 vec![],
398 );
399
400 await_reporters(
401 context.child("reporter"),
402 &reporters,
403 Height::new(100),
404 epoch,
405 )
406 .await;
407 });
408 }
409
410 test_for_all_fixtures!(slow all_online);
411
412 fn byzantine_proposer<S, F>(fixture: F)
414 where
415 S: Scheme<Sha256Digest, PublicKey = PublicKey>,
416 F: FnOnce(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
417 {
418 let runner = deterministic::Runner::timed(Duration::from_secs(30));
419
420 runner.start(|mut context| async move {
421 let num_validators = 4;
422 let fixture = fixture(&mut context, TEST_NAMESPACE, num_validators);
423 let epoch = Epoch::new(111);
424
425 let (mut oracle, mut registrations) =
426 initialize_simulation(context.child("simulation"), &fixture, RELIABLE_LINK).await;
427
428 let reporters = spawn_validator_engines(
429 context.child("validator"),
430 &fixture,
431 &mut registrations,
432 &mut oracle,
433 epoch,
434 Duration::from_secs(5),
435 vec![0],
436 );
437
438 await_reporters(
439 context.child("reporter"),
440 &reporters,
441 Height::new(100),
442 epoch,
443 )
444 .await;
445 });
446 }
447
448 test_for_all_fixtures!(byzantine_proposer);
449
450 fn unclean_byzantine_shutdown<S, F>(fixture: F)
451 where
452 S: Scheme<Sha256Digest, PublicKey = PublicKey>,
453 F: Fn(&mut TestRng, &[u8], u32) -> Fixture<S>,
454 {
455 let num_validators = 4;
457 let target_height = Height::new(200); let min_shutdowns = 4; let max_shutdowns = 10; let shutdown_range_min = Duration::from_millis(100);
461 let shutdown_range_max = Duration::from_millis(1_000);
462 let rebroadcast_timeout = NonZeroDuration::new_panic(Duration::from_millis(20));
463
464 let mut prev_checkpoint = None;
465
466 let mut rng = test_rng();
468 let fixture = fixture(&mut rng, TEST_NAMESPACE, num_validators);
469
470 let mut shutdown_count = 0;
472 while shutdown_count < max_shutdowns {
473 let fixture = fixture.clone();
474 let f = move |mut context: Context| {
475 async move {
476 let epoch = Epoch::new(111);
477
478 let (oracle, mut registrations) =
479 initialize_simulation(context.child("simulation"), &fixture, RELIABLE_LINK)
480 .await;
481
482 let (reporter, mut reporter_mailbox) =
486 mocks::Reporter::new(context.child("reporter"), fixture.verifier.clone());
487 reporter.start();
488
489 for (idx, participant) in fixture.participants.iter().enumerate() {
491 let validator_context = context
492 .child("participant")
493 .with_attribute("public_key", participant);
494
495 let provider = mocks::Provider::new();
497 assert!(provider.register(epoch, fixture.schemes[idx].clone()));
498
499 let monitor = mocks::Monitor::new(epoch);
501
502 let strategy = if idx == 0 {
504 mocks::Strategy::Incorrect
505 } else {
506 mocks::Strategy::Correct
507 };
508 let automaton = mocks::Application::new(strategy);
509
510 let blocker = oracle.control(participant.clone());
512
513 let engine = Engine::new(
515 validator_context.child("engine"),
516 Config {
517 monitor,
518 provider,
519 automaton,
520 reporter: reporter_mailbox.clone(),
521 blocker,
522 priority_acks: false,
523 rebroadcast_timeout,
524 epoch_bounds: (EpochDelta::new(1), EpochDelta::new(1)),
525 window: std::num::NonZeroU64::new(10).unwrap(),
526 activity_timeout: HeightDelta::new(1_024), journal_partition: format!("unclean_shutdown_test_{participant}"),
528 journal_write_buffer: NZUsize!(4096),
529 journal_replay_buffer: NZUsize!(4096),
530 journal_heights_per_section: std::num::NonZeroU64::new(6).unwrap(),
531 journal_compression: Some(3),
532 journal_page_cache: CacheRef::from_pooler(
533 &context,
534 PAGE_SIZE,
535 PAGE_CACHE_SIZE,
536 ),
537 strategy: Sequential,
538 },
539 );
540
541 let (sender, receiver) = registrations.remove(participant).unwrap();
542 engine.start((sender, receiver));
543 }
544
545 let completion =
547 context
548 .child("completion_watcher")
549 .spawn(move |context| async move {
550 loop {
551 if let Some(tip_height) =
552 reporter_mailbox.get_contiguous_tip().await
553 {
554 if tip_height >= target_height {
555 break;
556 }
557 }
558 context.sleep(Duration::from_millis(50)).await;
559 }
560 });
561
562 let shutdown_wait =
564 context.random_range(shutdown_range_min..shutdown_range_max);
565 select! {
566 _ = context.sleep(shutdown_wait) => {
567 debug!(shutdown_wait = ?shutdown_wait, "Simulating unclean shutdown");
568 false },
570 _ = completion => {
571 debug!("Shared reporter completed normally");
572 true },
574 }
575 }
576 };
577
578 let (complete, checkpoint) = prev_checkpoint
579 .map_or_else(
580 || {
581 debug!("Starting initial run");
582 deterministic::Runner::timed(Duration::from_secs(45))
583 },
584 |prev_checkpoint| {
585 debug!(shutdown_count, "Restarting from previous context");
586 deterministic::Runner::from(prev_checkpoint)
587 },
588 )
589 .start_and_recover(f);
590
591 if complete && shutdown_count >= min_shutdowns {
592 debug!("Test completed successfully");
593 break;
594 }
595
596 prev_checkpoint = Some(checkpoint);
597 shutdown_count += 1;
598 }
599 }
600
601 test_for_all_fixtures!(slow unclean_byzantine_shutdown);
602
603 fn unclean_shutdown_with_unsigned_height<S, F>(fixture: F)
604 where
605 S: Scheme<Sha256Digest, PublicKey = PublicKey>,
606 F: Fn(&mut TestRng, &[u8], u32) -> Fixture<S>,
607 {
608 let num_validators = 4;
610 let skip_height = Height::new(50); let window = HeightDelta::new(10);
612 let target_height = Height::new(100);
613
614 let mut rng = test_rng();
616 let fixture = fixture(&mut rng, TEST_NAMESPACE, num_validators);
617
618 let f = |context: Context| {
620 let fixture = fixture.clone();
621 async move {
622 let epoch = Epoch::new(111);
623
624 let (oracle, mut registrations) =
626 initialize_simulation(context.child("simulation"), &fixture, RELIABLE_LINK)
627 .await;
628
629 let (reporter, mut reporter_mailbox) =
631 mocks::Reporter::new(context.child("reporter"), fixture.verifier.clone());
632 reporter.start();
633
634 for (idx, participant) in fixture.participants.iter().enumerate() {
636 let validator_context = context
637 .child("participant")
638 .with_attribute("public_key", participant);
639
640 let provider = mocks::Provider::new();
642 assert!(provider.register(epoch, fixture.schemes[idx].clone()));
643
644 let monitor = mocks::Monitor::new(epoch);
646
647 let automaton = mocks::Application::new(mocks::Strategy::Skip {
649 height: skip_height,
650 });
651
652 let blocker = oracle.control(participant.clone());
654
655 let engine = Engine::new(
657 validator_context.child("engine"),
658 Config {
659 monitor,
660 provider,
661 automaton,
662 reporter: reporter_mailbox.clone(),
663 blocker,
664 priority_acks: false,
665 rebroadcast_timeout: NonZeroDuration::new_panic(Duration::from_millis(
666 100,
667 )),
668 epoch_bounds: (EpochDelta::new(1), EpochDelta::new(1)),
669 window: std::num::NonZeroU64::new(window.get()).unwrap(),
670 activity_timeout: HeightDelta::new(100),
671 journal_partition: format!("unsigned_height_test_{participant}"),
672 journal_write_buffer: NZUsize!(4096),
673 journal_replay_buffer: NZUsize!(4096),
674 journal_heights_per_section: std::num::NonZeroU64::new(6).unwrap(),
675 journal_compression: Some(3),
676 journal_page_cache: CacheRef::from_pooler(
677 &context,
678 PAGE_SIZE,
679 PAGE_CACHE_SIZE,
680 ),
681 strategy: Sequential,
682 },
683 );
684
685 let (sender, receiver) = registrations.remove(participant).unwrap();
686 engine.start((sender, receiver));
687 }
688
689 loop {
691 if let Some((tip_height, _)) = reporter_mailbox.get_tip().await {
692 debug!(%tip_height, %skip_height, %target_height, "reporter status");
693 if tip_height >= skip_height.saturating_add(window).previous().unwrap() {
694 return;
696 }
697 }
698 context.sleep(Duration::from_millis(50)).await;
699 }
700 }
701 };
702
703 let (_, checkpoint) =
704 deterministic::Runner::timed(Duration::from_secs(60)).start_and_recover(f);
705
706 let f2 = |context: Context| {
708 async move {
709 let epoch = Epoch::new(111);
710
711 let (oracle, mut registrations) =
713 initialize_simulation(context.child("simulation"), &fixture, RELIABLE_LINK)
714 .await;
715
716 let (reporter, mut reporter_mailbox) =
718 mocks::Reporter::new(context.child("reporter"), fixture.verifier.clone());
719 reporter.start();
720
721 for (idx, participant) in fixture.participants.iter().enumerate() {
723 let validator_context = context
724 .child("participant")
725 .with_attribute("public_key", participant);
726
727 let provider = mocks::Provider::new();
729 assert!(provider.register(epoch, fixture.schemes[idx].clone()));
730
731 let monitor = mocks::Monitor::new(epoch);
733
734 let automaton = mocks::Application::new(mocks::Strategy::Correct);
736
737 let blocker = oracle.control(participant.clone());
739
740 let engine = Engine::new(
742 validator_context.child("engine"),
743 Config {
744 monitor,
745 provider,
746 automaton,
747 reporter: reporter_mailbox.clone(),
748 blocker,
749 priority_acks: false,
750 rebroadcast_timeout: NonZeroDuration::new_panic(Duration::from_millis(
751 100,
752 )),
753 epoch_bounds: (EpochDelta::new(1), EpochDelta::new(1)),
754 window: std::num::NonZeroU64::new(10).unwrap(),
755 activity_timeout: HeightDelta::new(100),
756 journal_partition: format!("unsigned_height_test_{participant}"),
757 journal_write_buffer: NZUsize!(4096),
758 journal_replay_buffer: NZUsize!(4096),
759 journal_heights_per_section: std::num::NonZeroU64::new(6).unwrap(),
760 journal_compression: Some(3),
761 journal_page_cache: CacheRef::from_pooler(
762 &context,
763 PAGE_SIZE,
764 PAGE_CACHE_SIZE,
765 ),
766 strategy: Sequential,
767 },
768 );
769
770 let (sender, receiver) = registrations.remove(participant).unwrap();
771 engine.start((sender, receiver));
772 }
773
774 loop {
776 if let Some(tip_height) = reporter_mailbox.get_contiguous_tip().await {
777 debug!(
778 %tip_height,
779 %skip_height, %target_height, "reporter status on restart"
780 );
781 if tip_height >= target_height {
782 break;
783 }
784 }
785 context.sleep(Duration::from_millis(50)).await;
786 }
787 }
788 };
789
790 deterministic::Runner::from(checkpoint).start(f2);
791 }
792
793 test_for_all_fixtures!(slow unclean_shutdown_with_unsigned_height);
794
795 fn slow_and_lossy_links_seeded<S, F>(fixture: F, seed: u64) -> String
796 where
797 S: Scheme<Sha256Digest, PublicKey = PublicKey>,
798 F: FnOnce(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
799 {
800 let cfg = deterministic::Config::new()
801 .with_seed(seed)
802 .with_timeout(Some(Duration::from_secs(120)));
803 let runner = deterministic::Runner::new(cfg);
804
805 runner.start(|mut context| async move {
806 let num_validators = 4;
807 let fixture = fixture(&mut context, TEST_NAMESPACE, num_validators);
808 let epoch = Epoch::new(111);
809
810 let degraded_link = Link {
812 latency: Duration::from_millis(200),
813 jitter: Duration::from_millis(150),
814 success_rate: 0.5,
815 };
816
817 let (mut oracle, mut registrations) =
818 initialize_simulation(context.child("simulation"), &fixture, degraded_link).await;
819
820 let reporters = spawn_validator_engines(
821 context.child("validator"),
822 &fixture,
823 &mut registrations,
824 &mut oracle,
825 epoch,
826 Duration::from_secs(2),
827 vec![],
828 );
829
830 await_reporters(
831 context.child("reporter"),
832 &reporters,
833 Height::new(100),
834 epoch,
835 )
836 .await;
837
838 context.auditor().state()
839 })
840 }
841
842 fn slow_and_lossy_links<S, F>(fixture: F)
843 where
844 S: Scheme<Sha256Digest, PublicKey = PublicKey>,
845 F: FnOnce(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
846 {
847 slow_and_lossy_links_seeded(fixture, 0);
848 }
849
850 test_for_all_fixtures!(slow slow_and_lossy_links);
851
852 fn determinism<S, F>(fixture: F)
853 where
854 S: Scheme<Sha256Digest, PublicKey = PublicKey>,
855 F: FnOnce(&mut deterministic::Context, &[u8], u32) -> Fixture<S> + Copy,
856 {
857 for seed in 1..6 {
860 assert_eq!(
861 slow_and_lossy_links_seeded(fixture, seed),
862 slow_and_lossy_links_seeded(fixture, seed),
863 );
864 }
865 }
866
867 test_for_all_fixtures!(slow determinism);
868
869 #[test_group("slow")]
870 #[test_traced("INFO")]
871 fn test_distinct_states() {
872 macro_rules! collect {
874 ($vec:ident, $suffix:ident, $fixture:expr) => {
875 $vec.push((
876 stringify!($suffix),
877 slow_and_lossy_links_seeded($fixture, 7),
878 ));
879 };
880 }
881 let mut states = Vec::new();
882 for_each_fixture!(collect!(states));
883 for pair in states.windows(2) {
884 assert_ne!(
885 pair[0].1, pair[1].1,
886 "state {} equals state {}",
887 pair[0].0, pair[1].0
888 );
889 }
890 }
891
892 fn one_offline<S, F>(fixture: F)
893 where
894 S: Scheme<Sha256Digest, PublicKey = PublicKey>,
895 F: FnOnce(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
896 {
897 let runner = deterministic::Runner::timed(Duration::from_secs(30));
898
899 runner.start(|mut context| async move {
900 let num_validators = 5;
901 let mut fixture = fixture(&mut context, TEST_NAMESPACE, num_validators);
902 let epoch = Epoch::new(111);
903
904 fixture.participants.truncate(4);
906 fixture.schemes.truncate(4);
907
908 let (mut oracle, mut registrations) =
909 initialize_simulation(context.child("simulation"), &fixture, RELIABLE_LINK).await;
910
911 let reporters = spawn_validator_engines(
912 context.child("validator"),
913 &fixture,
914 &mut registrations,
915 &mut oracle,
916 epoch,
917 Duration::from_secs(5),
918 vec![],
919 );
920
921 await_reporters(
922 context.child("reporter"),
923 &reporters,
924 Height::new(100),
925 epoch,
926 )
927 .await;
928 });
929 }
930
931 test_for_all_fixtures!(slow one_offline);
932
933 fn network_partition<S, F>(fixture: F)
935 where
936 S: Scheme<Sha256Digest, PublicKey = PublicKey>,
937 F: FnOnce(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
938 {
939 let runner = deterministic::Runner::timed(Duration::from_secs(60));
940
941 runner.start(|mut context| async move {
942 let num_validators = 4;
943 let fixture = fixture(&mut context, TEST_NAMESPACE, num_validators);
944 let epoch = Epoch::new(111);
945
946 let (mut oracle, mut registrations) =
947 initialize_simulation(context.child("simulation"), &fixture, RELIABLE_LINK).await;
948
949 let reporters = spawn_validator_engines(
950 context.child("validator"),
951 &fixture,
952 &mut registrations,
953 &mut oracle,
954 epoch,
955 Duration::from_secs(5),
956 vec![],
957 );
958
959 for v1 in fixture.participants.iter() {
961 for v2 in fixture.participants.iter() {
962 if v2 == v1 {
963 continue;
964 }
965 oracle.remove_link(v1.clone(), v2.clone()).await.unwrap();
966 }
967 }
968 context.sleep(Duration::from_secs(20)).await;
969
970 for v1 in fixture.participants.iter() {
972 for v2 in fixture.participants.iter() {
973 if v2 == v1 {
974 continue;
975 }
976 oracle
977 .add_link(v1.clone(), v2.clone(), RELIABLE_LINK)
978 .await
979 .unwrap();
980 }
981 }
982
983 await_reporters(
984 context.child("reporter"),
985 &reporters,
986 Height::new(100),
987 epoch,
988 )
989 .await;
990 });
991 }
992
993 test_for_all_fixtures!(network_partition);
994
995 fn insufficient_validators<S, F>(fixture: F)
997 where
998 S: Scheme<Sha256Digest, PublicKey = PublicKey>,
999 F: FnOnce(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
1000 {
1001 let runner = deterministic::Runner::timed(Duration::from_secs(15));
1002
1003 runner.start(|mut context| async move {
1004 let num_validators = 5;
1005 let fixture = fixture(&mut context, TEST_NAMESPACE, num_validators);
1006 let epoch = Epoch::new(111);
1007
1008 let (oracle, mut registrations) =
1010 initialize_simulation(context.child("simulation"), &fixture, RELIABLE_LINK)
1011 .await;
1012
1013 let mut reporters =
1015 BTreeMap::<PublicKey, mocks::ReporterMailbox<S, Sha256Digest>>::new();
1016
1017 for (idx, participant) in fixture.participants.iter().take(2).enumerate() {
1019 let context = context.child("participant").with_attribute("public_key", participant);
1020
1021 let provider = mocks::Provider::new();
1023 assert!(provider.register(epoch, fixture.schemes[idx].clone()));
1024
1025 let monitor = mocks::Monitor::new(epoch);
1027
1028 let automaton = mocks::Application::new(mocks::Strategy::Correct);
1030
1031 let (reporter, reporter_mailbox) =
1033 mocks::Reporter::new(context.child("reporter"), fixture.verifier.clone());
1034 reporter.start();
1035 reporters.insert(participant.clone(), reporter_mailbox.clone());
1036
1037 let blocker = oracle.control(participant.clone());
1039
1040 let engine = Engine::new(
1042 context.child("engine"),
1043 Config {
1044 monitor,
1045 provider,
1046 automaton,
1047 reporter: reporter_mailbox,
1048 blocker,
1049 priority_acks: false,
1050 rebroadcast_timeout: NonZeroDuration::new_panic(Duration::from_secs(3)),
1051 epoch_bounds: (EpochDelta::new(1), EpochDelta::new(1)),
1052 window: std::num::NonZeroU64::new(10).unwrap(),
1053 activity_timeout: HeightDelta::new(100),
1054 journal_partition: format!("aggregation-{participant}"),
1055 journal_write_buffer: NZUsize!(4096),
1056 journal_replay_buffer: NZUsize!(4096),
1057 journal_heights_per_section: std::num::NonZeroU64::new(6).unwrap(),
1058 journal_compression: Some(3),
1059 journal_page_cache: CacheRef::from_pooler(
1060 &context,
1061 PAGE_SIZE,
1062 PAGE_CACHE_SIZE,
1063 ),
1064 strategy: Sequential,
1065 },
1066 );
1067
1068 let (sender, receiver) = registrations.remove(participant).unwrap();
1069 engine.start((sender, receiver));
1070 }
1071
1072 context.sleep(Duration::from_secs(12)).await;
1075
1076 let mut any_consensus = false;
1078 for (validator_pk, mut reporter_mailbox) in reporters {
1079 let (tip, _) = reporter_mailbox
1080 .get_tip()
1081 .await
1082 .unwrap_or((Height::zero(), Epoch::zero()));
1083 if !tip.is_zero() {
1084 any_consensus = true;
1085 tracing::warn!(
1086 ?validator_pk,
1087 %tip,
1088 "Unexpected consensus with insufficient validators"
1089 );
1090 }
1091 }
1092
1093 assert!(
1095 !any_consensus,
1096 "Consensus should not be achieved with insufficient validator participation (below quorum)"
1097 );
1098 });
1099 }
1100
1101 test_for_all_fixtures!(insufficient_validators);
1102
1103 fn run_1k<S, F>(fixture: F)
1104 where
1105 S: Scheme<Sha256Digest, PublicKey = PublicKey>,
1106 F: FnOnce(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
1107 {
1108 let cfg = deterministic::Config::new();
1109 let runner = deterministic::Runner::new(cfg);
1110
1111 runner.start(|mut context| async move {
1112 let num_validators = 10;
1114 let fixture = fixture(&mut context, TEST_NAMESPACE, num_validators);
1115 let epoch = Epoch::new(111);
1116
1117 let delayed_link = Link {
1119 latency: Duration::from_millis(80),
1120 jitter: Duration::from_millis(10),
1121 success_rate: 0.98,
1122 };
1123
1124 let (mut oracle, mut registrations) =
1126 initialize_simulation(context.child("simulation"), &fixture, delayed_link).await;
1127
1128 let reporters = spawn_validator_engines(
1130 context.child("validator"),
1131 &fixture,
1132 &mut registrations,
1133 &mut oracle,
1134 epoch,
1135 Duration::from_secs(5),
1136 vec![],
1137 );
1138
1139 await_reporters(
1141 context.child("reporter"),
1142 &reporters,
1143 Height::new(1_000),
1144 epoch,
1145 )
1146 .await;
1147 });
1148 }
1149
1150 #[test_group("slow")]
1151 #[test_traced]
1152 fn test_1k() {
1153 run_1k(mocks::scheme::fixture);
1154 }
1155}