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