1use std::{
2 collections::HashMap,
3 fmt::Debug,
4 io::{self, BufWriter},
5 time::{SystemTime, UNIX_EPOCH},
6};
7
8use crate::*;
9use itertools::*;
10use rand_chacha::ChaCha20Rng;
11use std::io::Write;
12
13#[derive(clap::Parser, Debug)]
31pub struct Command {
32 #[arg(long, short = 'P')]
34 permissioned_seats: u16,
35 #[arg(long, short = 'R')]
37 registered_seats: u16,
38 #[arg(long, short = 'p')]
40 permissioned_file: Option<String>,
41 #[arg(long, short = 'r')]
43 registered_file: Option<String>,
44 #[arg(long, default_value = "l")]
46 repetitions: u32,
47 #[arg(long)]
49 registered_pool_size: Option<u32>,
50 #[arg(long, default_value = "v2")]
52 ariadne_version: AriadneVersion,
53 #[arg(long, default_value = "false")]
55 output_to_terminal: bool,
56 #[arg(long)]
58 target_dir: Option<String>,
59}
60
61impl Command {
62 pub fn execute(self, mut rng: ChaCha20Rng) {
64 let potential_registered_candidates: Vec<(String, u128)> =
65 self.registered_file.clone().map(load_registered).unwrap_or_default();
66
67 let permissioned_candidates: Vec<String> =
68 self.permissioned_file.clone().map(load_permissioned).unwrap_or_default();
69
70 log::info!(
71 "Number of potential registered candidates: {}",
72 potential_registered_candidates.len()
73 );
74 log::info!("Number of permissioned candidates: {}", permissioned_candidates.len());
75
76 let output: &mut (dyn Write) = if self.output_to_terminal {
77 &mut io::stdout()
78 } else {
79 let file_name = format!(
80 "{}/ariadne-simulation-{}.csv",
81 self.target_dir.clone().unwrap_or(".".into()),
82 SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis()
83 );
84 &mut BufWriter::new(std::fs::File::create(file_name).unwrap())
85 };
86
87 writeln!(
88 output,
89 "{}",
90 [
91 "ariadne_version",
92 "R",
93 "P",
94 "registered_candidates",
95 "total_registered_stake",
96 "registered_file",
97 "permissioned_file",
98 "total_committee_stake",
99 "distinct_members",
100 "max_single_member_seats",
101 "safe_offline_members",
102 "top_safe_offline_stake",
103 "bottom_safe_offline_stake"
104 ]
105 .join(",")
106 )
107 .unwrap();
108 for i in 0..self.repetitions {
109 if i % 100 == 0 && i > 0 {
110 log::info!("Generation progress: {i}/{}", self.repetitions);
111 }
112
113 let (registered_pool_size, registered_candidates) =
115 self.sample_registered(potential_registered_candidates.clone(), &mut rng);
116
117 let committee = self
118 .ariadne_version
119 .select_authorities(
120 self.registered_seats,
121 self.permissioned_seats,
122 registered_candidates.clone(),
123 permissioned_candidates.clone(),
124 &mut rng,
125 )
126 .expect("Selection failed");
127
128 let SelectionStats {
129 bottom_safe_offline_stake,
130 top_safe_offline_stake,
131 distinct_members,
132 max_single_member_seats,
133 safe_offline_members,
134 total_committee_stake,
135 total_registered_stake,
136 } = self.calculate_stats(&committee, ®istered_candidates);
137
138 let registered_file =
139 self.registered_file.clone().map_or("null".to_string(), |f| format!("{f:?}"));
140 let permissioned_file =
141 self.permissioned_file.clone().map_or("null".to_string(), |f| format!("{f:?}"));
142
143 writeln!(
144 output,
145 "{},{},{},{},{},{},{},{},{},{},{},{},{}",
146 self.ariadne_version,
147 self.registered_seats,
148 self.permissioned_seats,
149 registered_pool_size,
150 total_registered_stake,
151 registered_file,
152 permissioned_file,
153 total_committee_stake,
154 distinct_members,
155 max_single_member_seats,
156 safe_offline_members,
157 top_safe_offline_stake,
158 bottom_safe_offline_stake
159 )
160 .expect("Failed to write CSV data row");
161 }
162 }
163
164 fn sample_registered(
165 &self,
166 potential_registered_candidates: Vec<(String, u128)>,
167 rng: &mut ChaCha20Rng,
168 ) -> (usize, Vec<(String, u128)>) {
169 let (registered_pool_size, mut registered_candidates) = match self.registered_pool_size {
170 None => {
171 (potential_registered_candidates.len(), potential_registered_candidates.clone())
172 },
173 Some(registered_pool_size) => {
174 let registered_pool_size =
175 (registered_pool_size as usize).min(potential_registered_candidates.len());
176 let candidates = potential_registered_candidates
177 .clone()
178 .into_iter()
179 .choose_multiple(rng, registered_pool_size);
180 (registered_pool_size, candidates)
181 },
182 };
183 registered_candidates.shuffle(rng);
184 (registered_pool_size, registered_candidates)
185 }
186
187 fn calculate_stats(
188 &self,
189 committee: &[String],
190 registered_candidates: &[(String, u128)],
191 ) -> SelectionStats {
192 let stake_lookup: HashMap<String, u128> = registered_candidates.iter().cloned().collect();
193 let mut member_seat_counts: Vec<(u16, String, u128)> = (committee.iter().cloned())
194 .into_group_map_by(|v| v.clone())
195 .into_iter()
196 .map(|(id, vs)| {
197 (vs.len() as u16, id.clone(), stake_lookup.get(&id).cloned().unwrap_or_default())
198 })
199 .collect();
200 member_seat_counts.sort();
201 member_seat_counts.reverse();
202 let total_seats = self.permissioned_seats + self.registered_seats;
203
204 let mut safe_offline_members = 0;
206 let safety_threshold = (total_seats - 1) / 3;
207 let mut seats = 0;
208 let mut total_committee_stake = 0;
209 let mut top_safe_offline_stake = 0;
210 for (power, _, stake) in &member_seat_counts {
211 seats += power;
212 total_committee_stake += stake;
213 if seats <= safety_threshold {
214 top_safe_offline_stake += stake;
215 safe_offline_members += 1
216 }
217 }
218
219 let mut seats = 0;
220 let mut bottom_safe_offline_stake = 0;
221 for (power, _id, stake) in member_seat_counts.iter().rev() {
222 seats += power;
223 if seats <= safety_threshold {
224 bottom_safe_offline_stake += stake;
225 }
226 }
227
228 let total_registered_stake: u128 = registered_candidates.iter().map(|c| c.1).sum();
229 SelectionStats {
230 bottom_safe_offline_stake,
231 top_safe_offline_stake,
232 distinct_members: member_seat_counts.len(),
233 max_single_member_seats: member_seat_counts[0].0,
234 safe_offline_members: safe_offline_members as usize,
235 total_committee_stake,
236 total_registered_stake,
237 }
238 }
239}
240
241struct SelectionStats {
242 bottom_safe_offline_stake: u128,
243 top_safe_offline_stake: u128,
244 distinct_members: usize,
245 max_single_member_seats: u16,
246 safe_offline_members: usize,
247 total_committee_stake: u128,
248 total_registered_stake: u128,
249}