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_bps: Option<usize>,
39 pub ingress_bps: 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() % 2 == 0 {
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_certificates.lazy",
1041 include_str!("../simplex_with_certificates.lazy"),
1042 true,
1043 ),
1044 ("minimmit.lazy", include_str!("../minimmit.lazy"), true),
1045 ("hotstuff.lazy", include_str!("../hotstuff.lazy"), true),
1046 ];
1047
1048 for (name, content, expected) in files {
1049 let task = parse_task(content);
1050 let completed = validate(&task, 3, 0);
1051 assert_eq!(completed, expected, "{name}");
1052 }
1053 }
1054
1055 #[test]
1056 fn test_parse_task_simple_parentheses() {
1057 let content = "(wait{1, threshold=67%} && wait{2, threshold=1}) || wait{3, threshold=50%}";
1058 let commands = parse_task(content);
1059 assert_eq!(commands.len(), 1);
1060
1061 match &commands[0].1 {
1062 Command::Or(cmd1, cmd2) => {
1063 match cmd1.as_ref() {
1065 Command::And(and_cmd1, and_cmd2) => {
1066 match and_cmd1.as_ref() {
1067 Command::Wait(id, threshold, _) => {
1068 assert_eq!(*id, 1);
1069 match threshold {
1070 Threshold::Percent(p) => assert_eq!(*p, 0.67),
1071 _ => panic!("Expected Percent threshold"),
1072 }
1073 }
1074 _ => panic!("Expected Wait command in first part of AND"),
1075 }
1076 match and_cmd2.as_ref() {
1077 Command::Wait(id, threshold, _) => {
1078 assert_eq!(*id, 2);
1079 match threshold {
1080 Threshold::Count(c) => assert_eq!(*c, 1),
1081 _ => panic!("Expected Count threshold"),
1082 }
1083 }
1084 _ => panic!("Expected Wait command in second part of AND"),
1085 }
1086 }
1087 _ => panic!("Expected And command in first part of OR"),
1088 }
1089 match cmd2.as_ref() {
1091 Command::Wait(id, threshold, _) => {
1092 assert_eq!(*id, 3);
1093 match threshold {
1094 Threshold::Percent(p) => assert_eq!(*p, 0.50),
1095 _ => panic!("Expected Percent threshold"),
1096 }
1097 }
1098 _ => panic!("Expected Wait command in second part of OR"),
1099 }
1100 }
1101 _ => panic!("Expected Or command"),
1102 }
1103 }
1104
1105 #[test]
1106 fn test_parse_task_nested_parentheses() {
1107 let content = "((wait{1, threshold=1} || wait{2, threshold=1}) && wait{3, threshold=1}) || wait{4, threshold=1}";
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() {
1118 Command::Or(or_cmd1, or_cmd2) => {
1119 match or_cmd1.as_ref() {
1120 Command::Wait(id, _, _) => assert_eq!(*id, 1),
1121 _ => panic!("Expected Wait id=1"),
1122 }
1123 match or_cmd2.as_ref() {
1124 Command::Wait(id, _, _) => assert_eq!(*id, 2),
1125 _ => panic!("Expected Wait id=2"),
1126 }
1127 }
1128 _ => panic!("Expected Or command in first part of AND"),
1129 }
1130 match and_cmd2.as_ref() {
1132 Command::Wait(id, _, _) => assert_eq!(*id, 3),
1133 _ => panic!("Expected Wait id=3"),
1134 }
1135 }
1136 _ => panic!("Expected And command in first part of OR"),
1137 }
1138 match cmd2.as_ref() {
1140 Command::Wait(id, _, _) => assert_eq!(*id, 4),
1141 _ => panic!("Expected Wait id=4"),
1142 }
1143 }
1144 _ => panic!("Expected Or command"),
1145 }
1146 }
1147
1148 #[test]
1149 fn test_parse_task_complex_expression() {
1150 let content = "(wait{1, threshold=1} && wait{2, threshold=1}) || (wait{3, threshold=1} && wait{4, threshold=1})";
1151 let commands = parse_task(content);
1152 assert_eq!(commands.len(), 1);
1153
1154 match &commands[0].1 {
1155 Command::Or(cmd1, cmd2) => {
1156 match cmd1.as_ref() {
1158 Command::And(and_cmd1, and_cmd2) => {
1159 match and_cmd1.as_ref() {
1160 Command::Wait(id, _, _) => assert_eq!(*id, 1),
1161 _ => panic!("Expected Wait id=1"),
1162 }
1163 match and_cmd2.as_ref() {
1164 Command::Wait(id, _, _) => assert_eq!(*id, 2),
1165 _ => panic!("Expected Wait id=2"),
1166 }
1167 }
1168 _ => panic!("Expected And command in first part"),
1169 }
1170 match cmd2.as_ref() {
1171 Command::And(and_cmd1, and_cmd2) => {
1172 match and_cmd1.as_ref() {
1173 Command::Wait(id, _, _) => assert_eq!(*id, 3),
1174 _ => panic!("Expected Wait id=3"),
1175 }
1176 match and_cmd2.as_ref() {
1177 Command::Wait(id, _, _) => assert_eq!(*id, 4),
1178 _ => panic!("Expected Wait id=4"),
1179 }
1180 }
1181 _ => panic!("Expected And command in second part"),
1182 }
1183 }
1184 _ => panic!("Expected Or command"),
1185 }
1186 }
1187
1188 #[test]
1189 fn test_parse_task_operator_precedence() {
1190 let content = "wait{1, threshold=1} || wait{2, threshold=1} && wait{3, threshold=1}";
1192 let commands = parse_task(content);
1193 assert_eq!(commands.len(), 1);
1194
1195 match &commands[0].1 {
1196 Command::Or(cmd1, cmd2) => {
1197 match cmd1.as_ref() {
1199 Command::Wait(id, _, _) => assert_eq!(*id, 1),
1200 _ => panic!("Expected Wait id=1"),
1201 }
1202 match cmd2.as_ref() {
1204 Command::And(and_cmd1, and_cmd2) => {
1205 match and_cmd1.as_ref() {
1206 Command::Wait(id, _, _) => assert_eq!(*id, 2),
1207 _ => panic!("Expected Wait id=2"),
1208 }
1209 match and_cmd2.as_ref() {
1210 Command::Wait(id, _, _) => assert_eq!(*id, 3),
1211 _ => panic!("Expected Wait id=3"),
1212 }
1213 }
1214 _ => panic!("Expected And command"),
1215 }
1216 }
1217 _ => panic!("Expected Or command"),
1218 }
1219 }
1220
1221 #[test]
1222 fn test_parse_task_parentheses_override_precedence() {
1223 let content = "(wait{1, threshold=1} || wait{2, threshold=1}) && wait{3, threshold=1}";
1225 let commands = parse_task(content);
1226 assert_eq!(commands.len(), 1);
1227
1228 match &commands[0].1 {
1229 Command::And(cmd1, cmd2) => {
1230 match cmd1.as_ref() {
1232 Command::Or(or_cmd1, or_cmd2) => {
1233 match or_cmd1.as_ref() {
1234 Command::Wait(id, _, _) => assert_eq!(*id, 1),
1235 _ => panic!("Expected Wait id=1"),
1236 }
1237 match or_cmd2.as_ref() {
1238 Command::Wait(id, _, _) => assert_eq!(*id, 2),
1239 _ => panic!("Expected Wait id=2"),
1240 }
1241 }
1242 _ => panic!("Expected Or command"),
1243 }
1244 match cmd2.as_ref() {
1246 Command::Wait(id, _, _) => assert_eq!(*id, 3),
1247 _ => panic!("Expected Wait id=3"),
1248 }
1249 }
1250 _ => panic!("Expected And command"),
1251 }
1252 }
1253
1254 #[test]
1255 fn test_parse_task_mixed_commands_with_parentheses() {
1256 let content = "(propose{1} && broadcast{2}) || reply{3}";
1257 let commands = parse_task(content);
1258 assert_eq!(commands.len(), 1);
1259
1260 match &commands[0].1 {
1261 Command::Or(cmd1, cmd2) => {
1262 match cmd1.as_ref() {
1263 Command::And(and_cmd1, and_cmd2) => {
1264 match and_cmd1.as_ref() {
1265 Command::Propose(id, _) => assert_eq!(*id, 1),
1266 _ => panic!("Expected Propose id=1"),
1267 }
1268 match and_cmd2.as_ref() {
1269 Command::Broadcast(id, _) => assert_eq!(*id, 2),
1270 _ => panic!("Expected Broadcast id=2"),
1271 }
1272 }
1273 _ => panic!("Expected And command"),
1274 }
1275 match cmd2.as_ref() {
1276 Command::Reply(id, _) => assert_eq!(*id, 3),
1277 _ => panic!("Expected Reply id=3"),
1278 }
1279 }
1280 _ => panic!("Expected Or command"),
1281 }
1282 }
1283
1284 #[test]
1285 #[should_panic(expected = "Expected ')' but reached end of input")]
1286 fn test_parse_task_unmatched_parentheses() {
1287 let content = "(wait{1, threshold=1} && wait{2, threshold=1}";
1288 parse_task(content);
1289 }
1290
1291 #[test]
1292 #[should_panic(expected = "Unexpected character ')' at position")]
1293 fn test_parse_task_extra_closing_paren() {
1294 let content = "wait{1, threshold=1} && wait{2, threshold=1})";
1295 parse_task(content);
1296 }
1297
1298 #[test]
1299 fn test_parse_task_commands_with_message_sizes() {
1300 let content = r#"
1301propose{1, size=1024}
1302broadcast{2, size=100}
1303reply{3, size=64}
1304reply{4}
1305"#;
1306
1307 let commands = parse_task(content);
1308 assert_eq!(commands.len(), 4);
1309
1310 match &commands[0].1 {
1311 Command::Propose(id, size) => {
1312 assert_eq!(*id, 1);
1313 assert_eq!(*size, Some(1024));
1314 }
1315 _ => panic!("Expected Propose command with size"),
1316 }
1317
1318 match &commands[1].1 {
1319 Command::Broadcast(id, size) => {
1320 assert_eq!(*id, 2);
1321 assert_eq!(*size, Some(100));
1322 }
1323 _ => panic!("Expected Broadcast command with size"),
1324 }
1325
1326 match &commands[2].1 {
1327 Command::Reply(id, size) => {
1328 assert_eq!(*id, 3);
1329 assert_eq!(*size, Some(64));
1330 }
1331 _ => panic!("Expected Reply command with size"),
1332 }
1333
1334 match &commands[3].1 {
1335 Command::Reply(id, size) => {
1336 assert_eq!(*id, 4);
1337 assert_eq!(*size, None);
1338 }
1339 _ => panic!("Expected Reply command without size"),
1340 }
1341 }
1342}