1#![doc(
4 html_logo_url = "https://commonware.xyz/imgs/rustdoc_logo.svg",
5 html_favicon_url = "https://commonware.xyz/favicon.ico"
6)]
7
8use commonware_cryptography::{
9 ed25519::{self, PublicKey},
10 Signer,
11};
12use commonware_p2p::Recipients;
13use reqwest::blocking::Client;
14use std::{
15 collections::{BTreeMap, BTreeSet, HashMap},
16 time::Duration,
17};
18use tracing::debug;
19
20const CLOUDPING_BASE: &str = "https://www.cloudping.co/api/latencies";
25const CLOUDPING_DIVISOR: f64 = 2.0; const MILLISECONDS_TO_SECONDS: f64 = 1000.0;
27
28pub type Region = String;
33
34#[derive(Debug, Clone)]
36pub struct RegionConfig {
37 pub count: usize,
38 pub egress_cap: Option<usize>,
39 pub ingress_cap: Option<usize>,
40}
41
42pub type Distribution = BTreeMap<Region, RegionConfig>;
43pub type Behavior = (f64, f64); pub type Latencies = BTreeMap<Region, BTreeMap<Region, Behavior>>;
45
46#[derive(serde::Deserialize)]
52struct CloudPing {
53 pub data: BTreeMap<Region, BTreeMap<Region, f64>>,
54}
55
56struct PeerState {
58 received: BTreeMap<u32, BTreeSet<PublicKey>>,
59 current_index: usize,
60}
61
62#[derive(Clone)]
67pub enum Command {
68 Propose(u32, Option<usize>), Broadcast(u32, Option<usize>), Reply(u32, Option<usize>), Collect(u32, Threshold, Option<(Duration, Duration)>),
72 Wait(u32, Threshold, Option<(Duration, Duration)>),
73 Or(Box<Self>, Box<Self>),
74 And(Box<Self>, Box<Self>),
75}
76
77#[derive(Clone)]
78pub enum Threshold {
79 Count(usize),
80 Percent(f64),
81}
82
83pub const fn crate_version() -> &'static str {
89 env!("CARGO_PKG_VERSION")
90}
91
92pub fn get_latency_data(reload: bool) -> Latencies {
94 if reload {
95 debug!("downloading latency data");
96 download_latency_data()
97 } else {
98 debug!("loading latency data");
99 load_latency_data()
100 }
101}
102
103pub fn parse_task(content: &str) -> Vec<(usize, Command)> {
105 let mut cmds = Vec::new();
106 for (line_num, line) in content.lines().enumerate() {
107 let line = line.trim();
108 if line.is_empty() {
109 continue;
110 }
111 if line.starts_with("#") {
112 continue;
113 }
114
115 let command = if line.contains(" || ")
117 || line.contains(" && ")
118 || line.contains('(')
119 || line.contains(')')
120 {
121 parse_expression(line)
122 } else {
123 parse_single_command(line)
124 };
125
126 cmds.push((line_num + 1, command));
127 }
128 cmds
129}
130
131fn parse_single_command(line: &str) -> Command {
133 let brace_start = line.find('{').expect("Missing opening brace");
134 let brace_end = line.rfind('}').expect("Missing closing brace");
135
136 let command = line[..brace_start].trim();
138 let args_str = &line[brace_start + 1..brace_end];
139 let mut args = Vec::new();
140 let mut current_arg = String::new();
141 let mut paren_depth = 0;
142 let mut in_quotes = false;
143 for ch in args_str.chars() {
144 match ch {
145 '(' => {
146 paren_depth += 1;
147 current_arg.push(ch);
148 }
149 ')' => {
150 paren_depth -= 1;
151 current_arg.push(ch);
152 }
153 '"' => {
154 in_quotes = !in_quotes;
155 current_arg.push(ch);
156 }
157 ',' if paren_depth == 0 && !in_quotes => {
158 if !current_arg.trim().is_empty() {
159 args.push(current_arg.trim().to_string());
160 }
161 current_arg.clear();
162 }
163 _ => {
164 current_arg.push(ch);
165 }
166 }
167 }
168
169 if !current_arg.trim().is_empty() {
171 args.push(current_arg.trim().to_string());
172 }
173 if args.is_empty() {
174 panic!("Missing arguments in curly braces");
175 }
176
177 let id = args[0].parse::<u32>().expect("Invalid id");
179
180 let mut parsed_args: HashMap<String, String> = HashMap::new();
182 for arg in &args[1..] {
183 if let Some(eq_pos) = arg.find('=') {
184 let key = arg[..eq_pos].trim().to_string();
185 let value = arg[eq_pos + 1..].trim().to_string();
186 parsed_args.insert(key, value);
187 } else {
188 panic!("Invalid argument format (expected key=value): {arg}");
189 }
190 }
191
192 match command {
193 "propose" => {
194 let size = parsed_args
195 .get("size")
196 .map(|s| s.parse::<usize>().expect("Invalid size"));
197 Command::Propose(id, size)
198 }
199 "broadcast" => {
200 let size = parsed_args
201 .get("size")
202 .map(|s| s.parse::<usize>().expect("Invalid size"));
203 Command::Broadcast(id, size)
204 }
205 "reply" => {
206 let size = parsed_args
207 .get("size")
208 .map(|s| s.parse::<usize>().expect("Invalid size"));
209 Command::Reply(id, size)
210 }
211 "collect" | "wait" => {
212 let thresh = parsed_args.get("threshold").map_or_else(
213 || {
214 panic!("Missing threshold for {command}");
215 },
216 |thresh_str| {
217 if thresh_str.ends_with('%') {
218 let p = thresh_str
219 .trim_end_matches('%')
220 .parse::<f64>()
221 .expect("Invalid percent")
222 / 100.0;
223 Threshold::Percent(p)
224 } else {
225 let c = thresh_str.parse::<usize>().expect("Invalid count");
226 Threshold::Count(c)
227 }
228 },
229 );
230
231 let delay = parsed_args.get("delay").map(|delay_str| {
232 let delay_str = delay_str.trim_matches('(').trim_matches(')');
233 let parts: Vec<&str> = delay_str.split(',').collect();
234 if parts.len() != 2 {
235 panic!("Invalid delay format (expected (value1,value2)): {delay_str}");
236 }
237 let message = parts[0].parse::<f64>().expect("Invalid message delay")
238 / MILLISECONDS_TO_SECONDS;
239 let message = Duration::from_secs_f64(message);
240 let completion = parts[1].parse::<f64>().expect("Invalid completion delay")
241 / MILLISECONDS_TO_SECONDS;
242 let completion = Duration::from_secs_f64(completion);
243 (message, completion)
244 });
245
246 if command == "collect" {
247 Command::Collect(id, thresh, delay)
248 } else {
249 Command::Wait(id, thresh, delay)
250 }
251 }
252 _ => panic!("Unknown command: {command}"),
253 }
254}
255
256fn parse_expression(line: &str) -> Command {
258 let mut parser = ExpressionParser::new(line);
259 let result = parser.parse_or_expression();
260
261 parser.skip_whitespace();
263 if !parser.is_at_end() {
264 panic!(
265 "Unexpected character '{}' at position {}",
266 parser.peek_char().unwrap_or('\0'),
267 parser.position
268 );
269 }
270
271 result
272}
273
274struct ExpressionParser<'a> {
276 input: &'a str,
277 position: usize,
278}
279
280impl<'a> ExpressionParser<'a> {
281 const fn new(input: &'a str) -> Self {
282 Self { input, position: 0 }
283 }
284
285 fn parse_or_expression(&mut self) -> Command {
287 let mut expr = self.parse_and_expression();
288
289 while self.peek_operator() == Some("||") {
290 self.consume_operator("||");
291 let right = self.parse_and_expression();
292 expr = Command::Or(Box::new(expr), Box::new(right));
293 }
294
295 expr
296 }
297
298 fn parse_and_expression(&mut self) -> Command {
300 let mut expr = self.parse_primary();
301
302 while self.peek_operator() == Some("&&") {
303 self.consume_operator("&&");
304 let right = self.parse_primary();
305 expr = Command::And(Box::new(expr), Box::new(right));
306 }
307
308 expr
309 }
310
311 fn parse_primary(&mut self) -> Command {
313 self.skip_whitespace();
314
315 if self.peek_char() == Some('(') {
316 self.consume_char('(');
317 let expr = self.parse_or_expression();
318 self.skip_whitespace();
319 self.consume_char(')');
320 expr
321 } else {
322 let command_text = self.extract_atomic_command();
324 parse_single_command(&command_text)
325 }
326 }
327
328 fn extract_atomic_command(&mut self) -> String {
330 let start = self.position;
331 let mut paren_depth = 0;
332
333 while self.position < self.input.len() {
334 let ch = self.input.chars().nth(self.position).unwrap();
335
336 if ch == '(' {
337 paren_depth += 1;
338 } else if ch == ')' {
339 if paren_depth == 0 {
340 break; }
342 paren_depth -= 1;
343 } else if paren_depth == 0 {
344 if self.input[self.position..].starts_with(" || ")
346 || self.input[self.position..].starts_with(" && ")
347 {
348 break;
349 }
350 }
351
352 self.position += ch.len_utf8();
353 }
354
355 self.input[start..self.position].trim().to_string()
356 }
357
358 fn peek_operator(&self) -> Option<&'static str> {
360 let remaining = &self.input[self.position..];
361 let trimmed = remaining.trim_start();
362
363 if trimmed.starts_with("||") {
364 Some("||")
365 } else if trimmed.starts_with("&&") {
366 Some("&&")
367 } else {
368 None
369 }
370 }
371
372 fn consume_operator(&mut self, op: &str) {
374 self.skip_whitespace();
375
376 let remaining = &self.input[self.position..];
377 if remaining.starts_with(op) {
378 self.position += op.len();
379 self.skip_whitespace();
380 } else {
381 panic!("Expected operator '{}' at position {}", op, self.position);
382 }
383 }
384
385 fn peek_char(&self) -> Option<char> {
387 self.input[self.position..].chars().next()
388 }
389
390 fn consume_char(&mut self, expected: char) {
392 self.skip_whitespace();
393
394 if let Some(ch) = self.input[self.position..].chars().next() {
395 if ch == expected {
396 self.position += ch.len_utf8();
397 self.skip_whitespace();
398 } else {
399 panic!(
400 "Expected '{}' but found '{}' at position {}",
401 expected, ch, self.position
402 );
403 }
404 } else {
405 panic!("Expected '{expected}' but reached end of input");
406 }
407 }
408
409 fn skip_whitespace(&mut self) {
411 while self.position < self.input.len() {
412 let ch = self.input.chars().nth(self.position).unwrap();
413 if ch.is_whitespace() {
414 self.position += ch.len_utf8();
415 } else {
416 break;
417 }
418 }
419 }
420
421 const fn is_at_end(&self) -> bool {
423 self.position >= self.input.len()
424 }
425}
426
427fn download_latency_data() -> Latencies {
433 let cli = Client::builder().build().unwrap();
434
435 let p50: CloudPing = cli
437 .get(format!("{CLOUDPING_BASE}?percentile=p_50&timeframe=1Y"))
438 .send()
439 .unwrap()
440 .json()
441 .unwrap();
442 let p90: CloudPing = cli
443 .get(format!("{CLOUDPING_BASE}?percentile=p_90&timeframe=1Y"))
444 .send()
445 .unwrap()
446 .json()
447 .unwrap();
448
449 populate_latency_map(p50, p90)
450}
451
452fn load_latency_data() -> Latencies {
454 let p50 = include_str!("p50.json");
455 let p90 = include_str!("p90.json");
456 let p50: CloudPing = serde_json::from_str(p50).unwrap();
457 let p90: CloudPing = serde_json::from_str(p90).unwrap();
458
459 populate_latency_map(p50, p90)
460}
461
462fn populate_latency_map(p50: CloudPing, p90: CloudPing) -> Latencies {
464 let mut map = BTreeMap::new();
465 for (from, inner_p50) in p50.data {
466 let inner_p90 = &p90.data[&from];
467 let mut dest_map = BTreeMap::new();
468 for (to, lat50) in inner_p50 {
469 if let Some(lat90) = inner_p90.get(&to) {
470 dest_map.insert(
471 to.clone(),
472 (
473 lat50 / CLOUDPING_DIVISOR,
474 (lat90 - lat50) / CLOUDPING_DIVISOR,
475 ),
476 );
477 }
478 }
479 map.insert(from, dest_map);
480 }
481
482 map
483}
484
485pub fn mean(data: &[f64]) -> f64 {
491 if data.is_empty() {
492 return 0.0;
493 }
494 let sum = data.iter().sum::<f64>();
495 sum / data.len() as f64
496}
497
498pub fn median(data: &mut [f64]) -> f64 {
501 if data.is_empty() {
502 return 0.0;
503 }
504 data.sort_by(|a, b| a.partial_cmp(b).unwrap());
505 let mid = data.len() / 2;
506 if data.len().is_multiple_of(2) {
507 (data[mid - 1] + data[mid]) / 2.0
508 } else {
509 data[mid]
510 }
511}
512
513pub fn std_dev(data: &[f64]) -> Option<f64> {
515 if data.is_empty() {
516 return None;
517 }
518 let mean_val = mean(data);
519 let variance = data
520 .iter()
521 .map(|value| {
522 let diff = mean_val - *value;
523 diff * diff
524 })
525 .sum::<f64>()
526 / data.len() as f64;
527 Some(variance.sqrt())
528}
529
530pub fn count_peers(distribution: &Distribution) -> usize {
536 let peers = distribution.values().map(|config| config.count).sum();
537 assert!(peers > 1, "must have at least 2 peers");
538 peers
539}
540
541pub fn calculate_proposer_region(proposer_idx: usize, distribution: &Distribution) -> String {
543 let mut current = 0;
544 for (region, config) in distribution {
545 let start = current;
546 current += config.count;
547 if proposer_idx >= start && proposer_idx < current {
548 return region.clone();
549 }
550 }
551 panic!("Proposer index {proposer_idx} out of bounds");
552}
553
554pub fn calculate_threshold(thresh: &Threshold, peers: usize) -> usize {
556 match thresh {
557 Threshold::Percent(p) => ((peers as f64) * *p).ceil() as usize,
558 Threshold::Count(c) => *c,
559 }
560}
561
562pub fn can_command_advance(
564 cmd: &Command,
565 is_proposer: bool,
566 peers: usize,
567 received: &BTreeMap<u32, BTreeSet<PublicKey>>,
568) -> bool {
569 match cmd {
570 Command::Propose(_, _) => true, Command::Broadcast(_, _) => true, Command::Reply(_, _) => true, Command::Collect(id, thresh, _) => {
574 if is_proposer {
575 let count = received.get(id).map_or(0, |s| s.len());
576 let required = calculate_threshold(thresh, peers);
577 count >= required
578 } else {
579 true }
581 }
582 Command::Wait(id, thresh, _) => {
583 let count = received.get(id).map_or(0, |s| s.len());
584 let required = calculate_threshold(thresh, peers);
585 count >= required
586 }
587 Command::Or(cmd1, cmd2) => {
588 can_command_advance(cmd1, is_proposer, peers, received)
590 || can_command_advance(cmd2, is_proposer, peers, received)
591 }
592 Command::And(cmd1, cmd2) => {
593 can_command_advance(cmd1, is_proposer, peers, received)
595 && can_command_advance(cmd2, is_proposer, peers, received)
596 }
597 }
598}
599
600pub fn validate(commands: &[(usize, Command)], peers: usize, proposer: usize) -> bool {
602 let mut peer_states: Vec<PeerState> = (0..peers)
604 .map(|_| PeerState {
605 received: BTreeMap::new(),
606 current_index: 0,
607 })
608 .collect();
609 let keys: Vec<PublicKey> = (0..peers)
610 .map(|i| ed25519::PrivateKey::from_seed(i as u64).public_key())
611 .collect();
612 let mut messages: Vec<(usize, Recipients<PublicKey>, u32)> = Vec::new();
613
614 loop {
616 let mut did_progress = false;
617 for p in 0..peers {
618 let state = &mut peer_states[p];
619 if state.current_index >= commands.len() {
620 continue;
621 }
622
623 loop {
624 if state.current_index >= commands.len() {
626 break;
627 }
628
629 let cmd = &commands[state.current_index].1;
631 let is_proposer = p == proposer;
632 let identity = keys[p].clone();
633
634 let advanced = can_command_advance(cmd, is_proposer, peers, &state.received);
636
637 if advanced {
639 match cmd {
640 Command::Propose(id, _) => {
641 if is_proposer {
642 messages.push((p, Recipients::All, *id));
643 state.received.entry(*id).or_default().insert(identity);
644 }
645 }
646 Command::Broadcast(id, _) => {
647 messages.push((p, Recipients::All, *id));
648 state.received.entry(*id).or_default().insert(identity);
649 }
650 Command::Reply(id, _) => {
651 let proposer_key = keys[proposer].clone();
652 if is_proposer {
653 state.received.entry(*id).or_default().insert(identity);
654 } else {
655 messages.push((p, Recipients::One(proposer_key), *id));
656 }
657 }
658 Command::Collect(_, _, _) | Command::Wait(_, _, _) => {
659 }
661 Command::Or(_, _) | Command::And(_, _) => {
662 }
665 }
666 }
667
668 if advanced {
670 state.current_index += 1;
671 did_progress = true;
672 } else {
673 break;
674 }
675 }
676 }
677
678 let pending = std::mem::take(&mut messages);
680 if !pending.is_empty() {
681 did_progress = true;
682 }
683 for (from, recipients, id) in pending {
684 let from_key = keys[from].clone();
685 match recipients {
686 Recipients::All => {
687 for (to, state) in peer_states.iter_mut().enumerate() {
688 if to != from {
689 state
690 .received
691 .entry(id)
692 .or_default()
693 .insert(from_key.clone());
694 }
695 }
696 }
697 Recipients::One(to_key) => {
698 let to = keys
699 .iter()
700 .position(|k| k == &to_key)
701 .expect("key not found");
702 peer_states[to]
703 .received
704 .entry(id)
705 .or_default()
706 .insert(from_key);
707 }
708 _ => unreachable!(),
709 }
710 }
711
712 if peer_states
714 .iter()
715 .all(|state| state.current_index >= commands.len())
716 {
717 return true;
718 }
719 if !did_progress {
720 return false;
721 }
722 }
723}
724
725#[cfg(test)]
726mod tests {
727 use super::*;
728
729 #[test]
730 fn test_crate_version() {
731 let version = crate_version();
732 assert!(!version.is_empty());
733 }
734
735 #[test]
736 fn test_mean() {
737 assert_eq!(mean(&[]), 0.0);
738 assert_eq!(mean(&[1.0]), 1.0);
739 assert_eq!(mean(&[1.0, 2.0, 3.0]), 2.0);
740 assert_eq!(mean(&[10.0, 20.0, 30.0]), 20.0);
741 }
742
743 #[test]
744 fn test_median() {
745 assert_eq!(median(&mut []), 0.0);
746 assert_eq!(median(&mut [5.0]), 5.0);
747 assert_eq!(median(&mut [1.0, 3.0, 2.0]), 2.0);
748 assert_eq!(median(&mut [1.0, 2.0, 3.0, 4.0]), 2.5);
749 assert_eq!(median(&mut [4.0, 1.0, 3.0, 2.0, 5.0]), 3.0);
750 }
751
752 #[test]
753 fn test_std_dev() {
754 assert_eq!(std_dev(&[]), None);
755 assert_eq!(std_dev(&[1.0]), Some(0.0));
756
757 let result = std_dev(&[1.0, 2.0, 3.0, 4.0, 5.0]);
758 assert!(result.is_some());
759 let std = result.unwrap();
760 assert!((std - std::f64::consts::SQRT_2).abs() < 1e-10);
761 }
762
763 #[test]
764 fn test_calculate_threshold() {
765 assert_eq!(calculate_threshold(&Threshold::Count(5), 10), 5);
766 assert_eq!(calculate_threshold(&Threshold::Percent(0.5), 10), 5);
767 }
768
769 #[test]
770 fn test_populate_latency_map() {
771 let p50_data = BTreeMap::from([(
772 "us-east-1".to_string(),
773 BTreeMap::from([
774 ("us-west-1".to_string(), 50.0),
775 ("eu-west-1".to_string(), 100.0),
776 ]),
777 )]);
778 let p50 = CloudPing { data: p50_data };
779 let p90_data = BTreeMap::from([(
780 "us-east-1".to_string(),
781 BTreeMap::from([
782 ("us-west-1".to_string(), 80.0),
783 ("eu-west-1".to_string(), 150.0),
784 ]),
785 )]);
786 let p90 = CloudPing { data: p90_data };
787
788 let result = populate_latency_map(p50, p90);
789 assert_eq!(result.len(), 1);
790 let us_east = &result["us-east-1"];
791 assert_eq!(us_east["us-west-1"], (25.0, 15.0)); assert_eq!(us_east["eu-west-1"], (50.0, 25.0)); }
794
795 #[test]
796 fn test_parse_task_commands() {
797 let content = r#"
798# This is a comment with new syntax
799propose{1}
800broadcast{2}
801reply{3}
802"#;
803
804 let commands = parse_task(content);
805 assert_eq!(commands.len(), 3);
806
807 match &commands[0].1 {
808 Command::Propose(id, _) => assert_eq!(*id, 1),
809 _ => panic!("Expected Propose command"),
810 }
811
812 match &commands[1].1 {
813 Command::Broadcast(id, _) => assert_eq!(*id, 2),
814 _ => panic!("Expected Broadcast command"),
815 }
816
817 match &commands[2].1 {
818 Command::Reply(id, _) => assert_eq!(*id, 3),
819 _ => panic!("Expected Reply command"),
820 }
821 }
822
823 #[test]
824 fn test_parse_task_collect_command() {
825 let content = "collect{1, threshold=75%}";
826 let commands = parse_task(content);
827 assert_eq!(commands.len(), 1);
828
829 match &commands[0].1 {
830 Command::Collect(id, threshold, delay) => {
831 assert_eq!(*id, 1);
832 match threshold {
833 Threshold::Percent(p) => assert_eq!(*p, 0.75),
834 _ => panic!("Expected Percent threshold"),
835 }
836 assert!(delay.is_none());
837 }
838 _ => panic!("Expected Collect command"),
839 }
840 }
841
842 #[test]
843 fn test_parse_task_wait_with_delay() {
844 let content = "wait{2, threshold=5, delay=(0.5,1.0)}";
845 let commands = parse_task(content);
846 assert_eq!(commands.len(), 1);
847
848 match &commands[0].1 {
849 Command::Wait(id, threshold, delay) => {
850 assert_eq!(*id, 2);
851 match threshold {
852 Threshold::Count(c) => assert_eq!(*c, 5),
853 _ => panic!("Expected Count threshold"),
854 }
855 assert!(delay.is_some());
856 let (msg, comp) = delay.unwrap();
857 assert_eq!(msg, Duration::from_micros(500));
858 assert_eq!(comp, Duration::from_millis(1));
859 }
860 _ => panic!("Expected Wait command"),
861 }
862 }
863
864 #[test]
865 fn test_parse_task_empty_and_comments() {
866 let content = r#"
867# Comment line
868
869# Another comment
870propose{1}
871
872# Final comment
873"#;
874
875 let commands = parse_task(content);
876 assert_eq!(commands.len(), 1);
877 assert_eq!(commands[0].0, 5); }
879
880 #[test]
881 #[should_panic(expected = "Missing opening brace")]
882 fn test_parse_task_invalid_format() {
883 let content = "propose invalid_arg_format";
884 parse_task(content);
885 }
886
887 #[test]
888 #[should_panic(expected = "Missing opening brace")]
889 fn test_parse_task_missing_id() {
890 let content = "propose threshold=50%";
891 parse_task(content);
892 }
893
894 #[test]
895 #[should_panic(expected = "Unknown command")]
896 fn test_parse_task_unknown_command() {
897 let content = "unknown_command{1}";
898 parse_task(content);
899 }
900
901 #[test]
902 #[should_panic(expected = "Missing arguments in curly braces")]
903 fn test_parse_task_empty_braces() {
904 let content = "propose{}";
905 parse_task(content);
906 }
907
908 #[test]
909 #[should_panic(expected = "Missing threshold for wait")]
910 fn test_parse_task_missing_threshold() {
911 let content = "wait{1}";
912 parse_task(content);
913 }
914
915 #[test]
916 fn test_parse_task_or_command() {
917 let content =
918 "wait{1, threshold=67%, delay=(0.1,1)} || wait{2, threshold=1, delay=(0.1,1)}";
919 let commands = parse_task(content);
920 assert_eq!(commands.len(), 1);
921
922 match &commands[0].1 {
923 Command::Or(cmd1, cmd2) => {
924 match cmd1.as_ref() {
925 Command::Wait(id, threshold, delay) => {
926 assert_eq!(*id, 1);
927 match threshold {
928 Threshold::Percent(p) => assert_eq!(*p, 0.67),
929 _ => panic!("Expected Percent threshold"),
930 }
931 assert!(delay.is_some());
932 }
933 _ => panic!("Expected Wait command in first part of OR"),
934 }
935 match cmd2.as_ref() {
936 Command::Wait(id, threshold, delay) => {
937 assert_eq!(*id, 2);
938 match threshold {
939 Threshold::Count(c) => assert_eq!(*c, 1),
940 _ => panic!("Expected Count threshold"),
941 }
942 assert!(delay.is_some());
943 }
944 _ => panic!("Expected Wait command in second part of OR"),
945 }
946 }
947 _ => panic!("Expected Or command"),
948 }
949 }
950
951 #[test]
952 fn test_parse_task_and_command() {
953 let content = "wait{3, threshold=67%} && wait{4, threshold=1}";
954 let commands = parse_task(content);
955 assert_eq!(commands.len(), 1);
956
957 match &commands[0].1 {
958 Command::And(cmd1, cmd2) => {
959 match cmd1.as_ref() {
960 Command::Wait(id, threshold, delay) => {
961 assert_eq!(*id, 3);
962 match threshold {
963 Threshold::Percent(p) => assert_eq!(*p, 0.67),
964 _ => panic!("Expected Percent threshold"),
965 }
966 assert!(delay.is_none());
967 }
968 _ => panic!("Expected Wait command in first part of AND"),
969 }
970 match cmd2.as_ref() {
971 Command::Wait(id, threshold, delay) => {
972 assert_eq!(*id, 4);
973 match threshold {
974 Threshold::Count(c) => assert_eq!(*c, 1),
975 _ => panic!("Expected Count threshold"),
976 }
977 assert!(delay.is_none());
978 }
979 _ => panic!("Expected Wait command in second part of AND"),
980 }
981 }
982 _ => panic!("Expected And command"),
983 }
984 }
985
986 #[test]
987 fn test_parse_task_chained_or_command() {
988 let content = "wait{1, threshold=67%} || wait{2, threshold=1} || wait{3, threshold=50%}";
989 let commands = parse_task(content);
990 assert_eq!(commands.len(), 1);
991
992 match &commands[0].1 {
995 Command::Or(_, _) => {
996 }
999 _ => panic!("Expected Or command"),
1000 }
1001 }
1002
1003 #[test]
1004 fn test_validate_or_and_logic() {
1005 let content = r#"
1006## Propose a block
1007propose{0}
1008
1009## This should fail because we wait for id=0 (which gets 1 message)
1010## AND id=99 (which never gets any messages), so the AND cannot be satisfied
1011wait{0, threshold=1} && wait{99, threshold=1}
1012broadcast{1}
1013 "#;
1014 let commands = parse_task(content);
1015 let completed = validate(&commands, 3, 0);
1016 assert!(!completed);
1017 }
1018
1019 #[test]
1020 fn test_parse_task_or_and_logic() {
1021 let content = r#"
1022## Propose a block
1023propose{0}
1024broadcast{6}
1025
1026## This should fail because we wait for id=0 (which gets 1 message)
1027## AND id=99 (which never gets any messages), so the AND cannot be satisfied
1028wait{0, threshold=1} && (wait{99, threshold=1} || wait{6, threshold=2})
1029broadcast{1}
1030 "#;
1031 let commands = parse_task(content);
1032 let completed = validate(&commands, 3, 0);
1033 assert!(completed);
1034 }
1035
1036 #[test]
1037 fn test_example_files() {
1038 let files = vec![
1039 ("stall.lazy", include_str!("../stall.lazy"), false),
1040 ("echo.lazy", include_str!("../echo.lazy"), true),
1041 ("simplex.lazy", include_str!("../simplex.lazy"), true),
1042 (
1043 "simplex_with_delay.lazy",
1044 include_str!("../simplex_with_delay.lazy"),
1045 true,
1046 ),
1047 (
1048 "simplex_with_certificates.lazy",
1049 include_str!("../simplex_with_certificates.lazy"),
1050 true,
1051 ),
1052 (
1053 "simplex_small_block.lazy",
1054 include_str!("../simplex_small_block.lazy"),
1055 true,
1056 ),
1057 (
1058 "simplex_large_block.lazy",
1059 include_str!("../simplex_large_block.lazy"),
1060 true,
1061 ),
1062 (
1063 "simplex_large_block_coding_50.lazy",
1064 include_str!("../simplex_large_block_coding_50.lazy"),
1065 true,
1066 ),
1067 ("minimmit.lazy", include_str!("../minimmit.lazy"), true),
1068 (
1069 "minimmit_small_block.lazy",
1070 include_str!("../minimmit_small_block.lazy"),
1071 true,
1072 ),
1073 (
1074 "minimmit_large_block.lazy",
1075 include_str!("../minimmit_large_block.lazy"),
1076 true,
1077 ),
1078 (
1079 "minimmit_large_block_coding_50.lazy",
1080 include_str!("../minimmit_large_block_coding_50.lazy"),
1081 true,
1082 ),
1083 (
1084 "kudzu_small_block.lazy",
1085 include_str!("../kudzu_small_block.lazy"),
1086 true,
1087 ),
1088 (
1089 "kudzu_large_block.lazy",
1090 include_str!("../kudzu_large_block.lazy"),
1091 true,
1092 ),
1093 (
1094 "kudzu_large_block_coding_50.lazy",
1095 include_str!("../kudzu_large_block_coding_50.lazy"),
1096 true,
1097 ),
1098 ("hotstuff.lazy", include_str!("../hotstuff.lazy"), true),
1099 ];
1100
1101 for (name, content, expected) in files {
1102 let task = parse_task(content);
1103 let completed = validate(&task, 3, 0);
1104 assert_eq!(completed, expected, "{name}");
1105 }
1106 }
1107
1108 #[test]
1109 fn test_parse_task_simple_parentheses() {
1110 let content = "(wait{1, threshold=67%} && wait{2, threshold=1}) || wait{3, threshold=50%}";
1111 let commands = parse_task(content);
1112 assert_eq!(commands.len(), 1);
1113
1114 match &commands[0].1 {
1115 Command::Or(cmd1, cmd2) => {
1116 match cmd1.as_ref() {
1118 Command::And(and_cmd1, and_cmd2) => {
1119 match and_cmd1.as_ref() {
1120 Command::Wait(id, threshold, _) => {
1121 assert_eq!(*id, 1);
1122 match threshold {
1123 Threshold::Percent(p) => assert_eq!(*p, 0.67),
1124 _ => panic!("Expected Percent threshold"),
1125 }
1126 }
1127 _ => panic!("Expected Wait command in first part of AND"),
1128 }
1129 match and_cmd2.as_ref() {
1130 Command::Wait(id, threshold, _) => {
1131 assert_eq!(*id, 2);
1132 match threshold {
1133 Threshold::Count(c) => assert_eq!(*c, 1),
1134 _ => panic!("Expected Count threshold"),
1135 }
1136 }
1137 _ => panic!("Expected Wait command in second part of AND"),
1138 }
1139 }
1140 _ => panic!("Expected And command in first part of OR"),
1141 }
1142 match cmd2.as_ref() {
1144 Command::Wait(id, threshold, _) => {
1145 assert_eq!(*id, 3);
1146 match threshold {
1147 Threshold::Percent(p) => assert_eq!(*p, 0.50),
1148 _ => panic!("Expected Percent threshold"),
1149 }
1150 }
1151 _ => panic!("Expected Wait command in second part of OR"),
1152 }
1153 }
1154 _ => panic!("Expected Or command"),
1155 }
1156 }
1157
1158 #[test]
1159 fn test_parse_task_nested_parentheses() {
1160 let content = "((wait{1, threshold=1} || wait{2, threshold=1}) && wait{3, threshold=1}) || wait{4, threshold=1}";
1161 let commands = parse_task(content);
1162 assert_eq!(commands.len(), 1);
1163
1164 match &commands[0].1 {
1165 Command::Or(cmd1, cmd2) => {
1166 match cmd1.as_ref() {
1168 Command::And(and_cmd1, and_cmd2) => {
1169 match and_cmd1.as_ref() {
1171 Command::Or(or_cmd1, or_cmd2) => {
1172 match or_cmd1.as_ref() {
1173 Command::Wait(id, _, _) => assert_eq!(*id, 1),
1174 _ => panic!("Expected Wait id=1"),
1175 }
1176 match or_cmd2.as_ref() {
1177 Command::Wait(id, _, _) => assert_eq!(*id, 2),
1178 _ => panic!("Expected Wait id=2"),
1179 }
1180 }
1181 _ => panic!("Expected Or command in first part of AND"),
1182 }
1183 match and_cmd2.as_ref() {
1185 Command::Wait(id, _, _) => assert_eq!(*id, 3),
1186 _ => panic!("Expected Wait id=3"),
1187 }
1188 }
1189 _ => panic!("Expected And command in first part of OR"),
1190 }
1191 match cmd2.as_ref() {
1193 Command::Wait(id, _, _) => assert_eq!(*id, 4),
1194 _ => panic!("Expected Wait id=4"),
1195 }
1196 }
1197 _ => panic!("Expected Or command"),
1198 }
1199 }
1200
1201 #[test]
1202 fn test_parse_task_complex_expression() {
1203 let content = "(wait{1, threshold=1} && wait{2, threshold=1}) || (wait{3, threshold=1} && wait{4, threshold=1})";
1204 let commands = parse_task(content);
1205 assert_eq!(commands.len(), 1);
1206
1207 match &commands[0].1 {
1208 Command::Or(cmd1, cmd2) => {
1209 match cmd1.as_ref() {
1211 Command::And(and_cmd1, and_cmd2) => {
1212 match and_cmd1.as_ref() {
1213 Command::Wait(id, _, _) => assert_eq!(*id, 1),
1214 _ => panic!("Expected Wait id=1"),
1215 }
1216 match and_cmd2.as_ref() {
1217 Command::Wait(id, _, _) => assert_eq!(*id, 2),
1218 _ => panic!("Expected Wait id=2"),
1219 }
1220 }
1221 _ => panic!("Expected And command in first part"),
1222 }
1223 match cmd2.as_ref() {
1224 Command::And(and_cmd1, and_cmd2) => {
1225 match and_cmd1.as_ref() {
1226 Command::Wait(id, _, _) => assert_eq!(*id, 3),
1227 _ => panic!("Expected Wait id=3"),
1228 }
1229 match and_cmd2.as_ref() {
1230 Command::Wait(id, _, _) => assert_eq!(*id, 4),
1231 _ => panic!("Expected Wait id=4"),
1232 }
1233 }
1234 _ => panic!("Expected And command in second part"),
1235 }
1236 }
1237 _ => panic!("Expected Or command"),
1238 }
1239 }
1240
1241 #[test]
1242 fn test_parse_task_operator_precedence() {
1243 let content = "wait{1, threshold=1} || wait{2, threshold=1} && wait{3, threshold=1}";
1245 let commands = parse_task(content);
1246 assert_eq!(commands.len(), 1);
1247
1248 match &commands[0].1 {
1249 Command::Or(cmd1, cmd2) => {
1250 match cmd1.as_ref() {
1252 Command::Wait(id, _, _) => assert_eq!(*id, 1),
1253 _ => panic!("Expected Wait id=1"),
1254 }
1255 match cmd2.as_ref() {
1257 Command::And(and_cmd1, and_cmd2) => {
1258 match and_cmd1.as_ref() {
1259 Command::Wait(id, _, _) => assert_eq!(*id, 2),
1260 _ => panic!("Expected Wait id=2"),
1261 }
1262 match and_cmd2.as_ref() {
1263 Command::Wait(id, _, _) => assert_eq!(*id, 3),
1264 _ => panic!("Expected Wait id=3"),
1265 }
1266 }
1267 _ => panic!("Expected And command"),
1268 }
1269 }
1270 _ => panic!("Expected Or command"),
1271 }
1272 }
1273
1274 #[test]
1275 fn test_parse_task_parentheses_override_precedence() {
1276 let content = "(wait{1, threshold=1} || wait{2, threshold=1}) && wait{3, threshold=1}";
1278 let commands = parse_task(content);
1279 assert_eq!(commands.len(), 1);
1280
1281 match &commands[0].1 {
1282 Command::And(cmd1, cmd2) => {
1283 match cmd1.as_ref() {
1285 Command::Or(or_cmd1, or_cmd2) => {
1286 match or_cmd1.as_ref() {
1287 Command::Wait(id, _, _) => assert_eq!(*id, 1),
1288 _ => panic!("Expected Wait id=1"),
1289 }
1290 match or_cmd2.as_ref() {
1291 Command::Wait(id, _, _) => assert_eq!(*id, 2),
1292 _ => panic!("Expected Wait id=2"),
1293 }
1294 }
1295 _ => panic!("Expected Or command"),
1296 }
1297 match cmd2.as_ref() {
1299 Command::Wait(id, _, _) => assert_eq!(*id, 3),
1300 _ => panic!("Expected Wait id=3"),
1301 }
1302 }
1303 _ => panic!("Expected And command"),
1304 }
1305 }
1306
1307 #[test]
1308 fn test_parse_task_mixed_commands_with_parentheses() {
1309 let content = "(propose{1} && broadcast{2}) || reply{3}";
1310 let commands = parse_task(content);
1311 assert_eq!(commands.len(), 1);
1312
1313 match &commands[0].1 {
1314 Command::Or(cmd1, cmd2) => {
1315 match cmd1.as_ref() {
1316 Command::And(and_cmd1, and_cmd2) => {
1317 match and_cmd1.as_ref() {
1318 Command::Propose(id, _) => assert_eq!(*id, 1),
1319 _ => panic!("Expected Propose id=1"),
1320 }
1321 match and_cmd2.as_ref() {
1322 Command::Broadcast(id, _) => assert_eq!(*id, 2),
1323 _ => panic!("Expected Broadcast id=2"),
1324 }
1325 }
1326 _ => panic!("Expected And command"),
1327 }
1328 match cmd2.as_ref() {
1329 Command::Reply(id, _) => assert_eq!(*id, 3),
1330 _ => panic!("Expected Reply id=3"),
1331 }
1332 }
1333 _ => panic!("Expected Or command"),
1334 }
1335 }
1336
1337 #[test]
1338 #[should_panic(expected = "Expected ')' but reached end of input")]
1339 fn test_parse_task_unmatched_parentheses() {
1340 let content = "(wait{1, threshold=1} && wait{2, threshold=1}";
1341 parse_task(content);
1342 }
1343
1344 #[test]
1345 #[should_panic(expected = "Unexpected character ')' at position")]
1346 fn test_parse_task_extra_closing_paren() {
1347 let content = "wait{1, threshold=1} && wait{2, threshold=1})";
1348 parse_task(content);
1349 }
1350
1351 #[test]
1352 fn test_parse_task_commands_with_message_sizes() {
1353 let content = r#"
1354propose{1, size=1024}
1355broadcast{2, size=100}
1356reply{3, size=64}
1357reply{4}
1358"#;
1359
1360 let commands = parse_task(content);
1361 assert_eq!(commands.len(), 4);
1362
1363 match &commands[0].1 {
1364 Command::Propose(id, size) => {
1365 assert_eq!(*id, 1);
1366 assert_eq!(*size, Some(1024));
1367 }
1368 _ => panic!("Expected Propose command with size"),
1369 }
1370
1371 match &commands[1].1 {
1372 Command::Broadcast(id, size) => {
1373 assert_eq!(*id, 2);
1374 assert_eq!(*size, Some(100));
1375 }
1376 _ => panic!("Expected Broadcast command with size"),
1377 }
1378
1379 match &commands[2].1 {
1380 Command::Reply(id, size) => {
1381 assert_eq!(*id, 3);
1382 assert_eq!(*size, Some(64));
1383 }
1384 _ => panic!("Expected Reply command with size"),
1385 }
1386
1387 match &commands[3].1 {
1388 Command::Reply(id, size) => {
1389 assert_eq!(*id, 4);
1390 assert_eq!(*size, None);
1391 }
1392 _ => panic!("Expected Reply command without size"),
1393 }
1394 }
1395}