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