1#[cfg(feature = "dsig")]
22use rhdl_dsig_scalable_core::planner::{Dsig, DsigVerify};
23#[cfg(feature = "ubft")]
24use rhdl_dsig_scalable_core::planner::{DsigUbftSign, DsigUbftVerify};
25#[cfg(feature = "ubft")]
26use rhdl_fpga_planner::ubft::UbftReplica;
27#[cfg(all(feature = "ed25519", not(feature = "sha512")))]
28use rhdl_ed25519_scalable_core::planner::Sha512Estimate;
29#[cfg(feature = "ed25519")]
30use rhdl_ed25519_scalable_core::planner::{Ed25519Sign, Ed25519Verify};
31use rhdl_fpga_planner::{Mode, Slot};
32#[cfg(feature = "sha512")]
33use rhdl_sha512_pipelined::planner::Sha512;
34
35pub fn slots() -> Vec<Slot> {
37 #[allow(unused_mut)]
38 let mut slots = Vec::new();
39 #[cfg(feature = "dsig")]
40 {
41 slots.push(Slot::new(Box::new(Dsig::default()), 25.0));
42 slots.push(Slot::new(Box::new(DsigVerify::default()), 25.0));
43 }
44 #[cfg(feature = "ed25519")]
45 {
46 slots.push(Slot::new(Box::new(Ed25519Sign::default()), 25.0));
47 slots.push(Slot::new(Box::new(Ed25519Verify::default()), 25.0));
48 }
49 #[cfg(feature = "sha512")]
50 slots.push(Slot::new(Box::new(Sha512::default()), 0.0));
51 #[cfg(all(feature = "ed25519", not(feature = "sha512")))]
52 slots.push(Slot::new(Box::new(Sha512Estimate::default()), 0.0));
53 #[cfg(feature = "ubft")]
57 {
58 slots.push(Slot::new(Box::new(UbftReplica::default()), 0.0));
59 slots.push(Slot::new(Box::new(DsigUbftSign::default()), 25.0));
60 slots.push(Slot::new(Box::new(DsigUbftVerify::default()), 25.0));
61 slots.extend(rhdl_ed25519_scalable_core::planner::ubft::slots().into_iter().skip(1));
63 }
64 slots
65}
66
67pub fn papers() -> Vec<Mode> {
71 #[allow(unused_mut)]
72 let mut papers = Vec::new();
73 #[cfg(feature = "dsig")]
74 papers.push(Mode::Dsig);
75 #[cfg(feature = "ed25519")]
76 papers.push(Mode::Ed25519);
77 #[cfg(feature = "ubft")]
78 {
79 papers.push(Mode::UbftDsig);
80 papers.push(Mode::UbftEd25519);
81 }
82 papers
83}
84
85pub const DEFAULT_MODE: Mode = Mode::Ed25519;
87
88#[cfg(all(test, feature = "ed25519"))]
89mod ed25519_tests {
90 use super::*;
91 use rhdl_fpga_planner::tui::{App, snapshot};
92
93 #[test]
96 fn this_builds_papers_balance_and_render() {
97 let app = App::new("U280 SIGNATURE PLANNER", DEFAULT_MODE, slots());
98 assert_eq!(app.papers(), papers());
99 for mode in papers() {
100 let app = App::new("U280 SIGNATURE PLANNER", mode, slots());
101 let text = snapshot(&app, 140, 44);
102 println!("{text}");
103 let d = app.plan().delivered;
104 assert!(d.sign.rate > 0.0 && d.verify.rate > 0.0, "{mode:?}: sign {:?} verify {:?}", d.sign, d.verify);
105 }
106 }
107
108 pub(super) fn balanced(mode: Mode, lut: f64, dsp: f64) -> (f64, f64) {
110 let mut app = App::new("T", mode, slots());
111 app.budget.lut_percent = lut;
112 app.budget.dsp_percent = dsp;
113 app.balance();
114 let d = app.plan().delivered;
115 (d.sign.rate, d.verify.rate)
116 }
117
118 #[test]
125 fn rates_scale_with_luts_and_dsps() {
126 for mode in papers().into_iter().filter(|m| !m.is_ubft()) {
127 let mut grid = Vec::new();
128 for lut in [50.0, 100.0, 200.0] {
129 let row: Vec<(f64, f64)> = [50.0, 100.0, 200.0].iter().map(|&dsp| balanced(mode, lut, dsp)).collect();
130 println!(
131 "{mode:?} LUT {lut:>3}%: {}",
132 row.iter().map(|(s, v)| format!("{:5.2}M/{:5.2}M", s / 1e6, v / 1e6)).collect::<Vec<_>>().join(" ")
133 );
134 grid.push(row);
135 }
136 let min = |x: (f64, f64)| x.0.min(x.1);
137 for i in 0..3 {
138 for j in 0..3 {
139 if i > 0 {
140 assert!(min(grid[i][j]) >= 0.98 * min(grid[i - 1][j]), "{mode:?} LUT step at {i},{j}: {grid:?}");
141 }
142 if j > 0 {
143 assert!(min(grid[i][j]) >= 0.98 * min(grid[i][j - 1]), "{mode:?} DSP step at {i},{j}: {grid:?}");
144 }
145 }
146 }
147 assert!(min(grid[2][2]) > 1.3 * min(grid[1][1]), "{mode:?}: 200/200 vs 100/100 {grid:?}");
148 }
149 }
150
151 #[test]
155 fn the_auto_sha_line_never_limits() {
156 for mode in papers().into_iter().filter(|m| !m.is_ubft()) {
159 for (lut, dsp) in [(50.0, 50.0), (100.0, 100.0), (200.0, 200.0)] {
160 let mut app = App::new("T", mode, slots());
161 assert!(app.sha_auto());
162 app.budget.lut_percent = lut;
163 app.budget.dsp_percent = dsp;
164 app.balance();
165 let p = app.plan();
166 let (k, _) = app.sha_lut_knob().unwrap();
167 let line = p.parts[k].1.as_ref().map_or("none".to_string(), |x| x.detail.clone());
168 println!("{mode:?} LUT {lut} DSP {dsp}: {:.2}M/{:.2}M, SHA allowance {} ({line})", p.delivered.sign.rate / 1e6, p.delivered.verify.rate / 1e6, p.guaranteed[k].lut);
169 assert!(!p.delivered.sha_bound, "{mode:?} {lut}/{dsp}: SHA-bound with {line}");
170 if lut == 100.0 {
171 assert!(p.guaranteed[k].lut < 40_000, "{mode:?}: auto line {} LUT", p.guaranteed[k].lut);
172 }
173 }
174 }
175 }
176
177 #[test]
181 fn lut_split_moves_the_sha_line() {
182 use rhdl_fpga_planner::tui::Control;
183 for mode in papers().into_iter().filter(|m| !m.is_ubft()) {
185 let mut app = App::new("T", mode, slots());
186 let (k, j) = app.sha_lut_knob().expect("a SHA line");
187 let before = app.slots[k].component.knobs()[j].value;
188 let rest = app.budget.rest(&app.slots).lut;
189 app.selected = app.controls().iter().position(|c| *c == Control::ShaSplit).unwrap();
190 app.adjust(10.0);
191 assert!(!app.sha_auto(), "moving the split makes the size manual");
192 let after = app.slots[k].component.knobs()[j].value;
193 assert!(after - before >= 20_000.0, "{mode:?}: {before} -> {after}");
194 assert_eq!(app.budget.rest(&app.slots).lut + (after - before) as u32, rest);
195 let (sha, dsig, ed) = app.lut_split();
196 assert!((sha + dsig + ed - 100.0).abs() < 1e-6, "{mode:?}: {sha} + {dsig} + {ed}");
197 app.adjust(50.0);
201 let big = app.slots[k].component.knobs()[j].value;
202 assert!(big >= after + 90_000.0, "{mode:?}: allowance {after} -> {big}");
203 let line = app.plan().parts[k].1.clone().expect("a line");
204 assert!((line.rate - 205e6).abs() < 1.0, "{mode:?}: {}", line.detail);
205 assert!(app.plan().total.fits(&app.budget.chip()));
206 }
207 }
208
209 #[cfg(not(feature = "dsig"))]
212 #[test]
213 fn ed25519_only_build_offers_one_paper() {
214 let mut app = App::new("T", Mode::Dsig, slots());
215 assert_eq!(app.mode, Mode::Ed25519, "a DSig request opens the Ed25519 paper");
216 app.switch_paper();
217 assert_eq!(app.mode, Mode::Ed25519);
218 let text = snapshot(&app, 140, 44);
219 assert!(!text.contains("DSig paper:") && !text.contains("P switches"), "{text}");
220 }
221}
222
223#[cfg(all(test, feature = "dsig"))]
224mod dsig_tests {
225 use super::*;
226 use rhdl_fpga_planner::tui::{App, snapshot};
227
228 #[test]
231 fn dsig_slices_follow_luts_and_rates_are_visible() {
232 for (sign_rounds, verify_rounds) in [(7.0, 7.0), (6.0, 6.0), (6.0, 0.0), (3.0, 3.0)] {
238 println!("table rounds: sign {sign_rounds}, verify {verify_rounds}");
239 for lut in [100, 150, 200] {
240 let mut app = App::new("T", Mode::Dsig, slots());
241 for (slot, rounds) in [(0, sign_rounds), (1, verify_rounds)] {
242 let k = app.slots[slot].component.knobs().iter().position(|k| k.name == "table rounds").expect("table rounds knob");
243 app.slots[slot].component.set_knob(k, rounds);
244 }
245 app.budget.lut_percent = lut as f64;
246 app.balance();
247 let p = app.plan();
248 let d = &p.delivered;
249 let detail = |k: usize| p.parts[k].1.as_ref().map_or("-".to_string(), |x| x.detail.split(',').next().unwrap_or("").to_string());
250 println!(
251 " LUT {lut}%: {:.2}M DSig sign / {:.2}M DSig verify | {} | {} | chip {} LUT {} BRAM36",
252 d.sign.rate / 1e6,
253 d.verify.rate / 1e6,
254 detail(0),
255 detail(1),
256 p.total.lut,
257 p.total.bram36
258 );
259 }
260 }
261 println!("v3 LUT S-box lanes (routed):");
262 let mut slices_seen = Vec::new();
263 for lut in (100..=200).step_by(10) {
264 let mut app = App::new("T", Mode::Dsig, slots());
265 app.budget.lut_percent = lut as f64;
266 app.balance();
267 let p = app.plan();
268 let d = &p.delivered;
269 let detail = |k: usize| p.parts[k].1.as_ref().map_or("-".to_string(), |x| x.detail.split(';').next().unwrap_or("").to_string());
270 let slices: u32 = p.parts[0].1.as_ref().and_then(|x| x.detail.split_whitespace().next()?.parse().ok()).unwrap_or(0);
271 println!(
272 "LUT {lut}%: {:.2}M DSig sign / {:.2}M DSig verify | sign: {} | verify: {} | EdDSA {:.1} %",
273 d.sign.rate / 1e6,
274 d.verify.rate / 1e6,
275 detail(0),
276 detail(1),
277 app.used.share
278 );
279 slices_seen.push(slices);
280 }
281 assert!(slices_seen.windows(2).all(|w| w[1] + 1 >= w[0]), "slices drop as LUTs grow: {slices_seen:?}");
282 assert!(slices_seen.iter().any(|&n| n >= 4), "never reaches 4 slices: {slices_seen:?}");
283 let app = App::new("T", Mode::Ed25519, slots());
284 let text = snapshot(&app, 120, 40);
285 assert!(text.contains("DSig paper:") && text.contains("Ed25519 paper:"), "{text}");
286 println!("{}", text.lines().take(3).collect::<Vec<_>>().join("\n"));
287 }
288
289 #[test]
297 fn dsig_scheme_knobs_reach_both_modules() {
298 use rhdl_fpga_planner::tui::Control;
299 let mut app = App::new("T", Mode::Dsig, slots());
300 let knob = |app: &App, name: &str| {
301 app.controls()
302 .into_iter()
303 .filter(|c| matches!(c, Control::Knob(k, j) if app.slots[*k].component.knobs()[*j].name == name))
304 .collect::<Vec<_>>()
305 };
306 for name in ["HBSS", "chain hash", "Winternitz w", "batch K"] {
307 assert_eq!(knob(&app, name).len(), 2, "{name} under DSig sign and DSig verify");
308 }
309 let u = app.used;
312 println!("EdDSA background needed {:.1} % (sign {:.1} % of it)", u.needed_share, u.needed_sign);
313 assert!(u.needed_share > 0.0 && u.needed_share < 50.0, "{u:?}");
314 assert!((app.powers.sign + app.powers.verify - 100.0).abs() < 1e-9);
315 let p: f64 = app.slots.iter().filter(|s| s.component.fixed().is_none()).map(|s| s.power).sum();
316 assert!((p - 100.0).abs() < 1e-6, "module powers sum to {p}");
317 let build = |app: &App| app.plan().parts[0].1.as_ref().map(|p| (p.cost, p.detail.clone()));
323 let before = build(&app);
324 app.selected = app.controls().iter().position(|c| *c == knob(&app, "chain hash")[0]).unwrap();
325 app.adjust(1.0); for c in knob(&app, "chain hash") {
328 let Control::Knob(k, j) = c else { unreachable!() };
329 assert_eq!(app.slots[k].component.knobs()[j].show(), "BLAKE3", "{}", app.slots[k].component.name());
330 }
331 let after = build(&app);
332 assert!(before.is_some() && after.is_some(), "a DSig sign build before and after");
333 assert_ne!(after, before, "the chain hash changes the DSig sign build");
334
335 app.selected = app.controls().iter().position(|c| *c == knob(&app, "HBSS")[0]).unwrap();
336 app.adjust(1.0); let text = snapshot(&app, 160, 50);
338 assert!(text.contains("HORS is not implemented"), "{text}");
339 }
340}
341
342#[cfg(all(test, feature = "dsig"))]
343mod usage {
344 use super::*;
345 use rhdl_fpga_planner::tui::App;
346
347 #[test]
348 fn every_plan_fits_the_budget() {
352 for (mode, uram, sbox) in [(Mode::Ed25519, false, false), (Mode::Dsig, false, false), (Mode::Dsig, true, false), (Mode::Dsig, true, true)] {
356 for (lut, dsp) in [(50.0, 50.0), (100.0, 100.0), (150.0, 100.0), (200.0, 100.0), (200.0, 200.0), (100.0, 200.0)] {
357 let mut app = App::new("T", mode, slots());
358 if mode == Mode::Dsig {
359 for k in 0..2 {
360 let j = app.slots[k].component.knobs().iter().position(|x| x.name == "table RAM").unwrap();
361 app.slots[k].component.set_knob(j, if uram { 1.0 } else { 0.0 });
362 if let Some(j) = app.slots[k].component.knobs().iter().position(|x| x.name == "S-box RAM") {
363 app.slots[k].component.set_knob(j, if sbox { 0.0 } else { 1.0 });
364 }
365 }
366 }
367 app.budget.lut_percent = lut;
368 app.budget.dsp_percent = dsp;
369 app.balance();
370 let p = app.plan();
371 let c = app.budget.chip();
372 println!("{mode:?} uram-tables {uram} sbox-ram {sbox} LUT {lut} DSP {dsp}: {:.2}M/{:.2}M used {}k/{}k LUT {}/{} DSP {}/{} BRAM {}/{} URAM bg {:.1}",
373 p.delivered.sign.rate/1e6, p.delivered.verify.rate/1e6, p.total.lut/1000, c.lut/1000, p.total.dsp, c.dsp, p.total.bram36, c.bram36, p.total.uram, c.uram, app.used.share);
374 for (k, (sh, pl)) in p.parts.iter().enumerate() {
375 if let Some(pl) = pl {
376 println!(" {:<16} cap {:>7.2}M cost {:>6} LUT {:>4} DSP {:>4} BRAM {:>3} URAM (share {} LUT {} DSP) {}", app.slots[k].component.name(), pl.rate/1e6, pl.cost.lut, pl.cost.dsp, pl.cost.bram36, pl.cost.uram, sh.lut, sh.dsp, pl.detail.chars().take(70).collect::<String>());
377 }
378 }
379 assert!(p.total.fits(&c), "over budget: {:?} > {c:?}", p.total);
380 }
381 }
382 }
383
384 #[test]
385 fn both_sides_deliver_once_their_minimums_fit() {
396 for mode in papers() {
397 let mut app = App::new("T", mode, slots());
398 if !mode.is_ubft() {
401 let percent = if mode == Mode::Dsig { 75.0 } else { 50.0 };
402 app.budget.lut_percent = percent;
403 app.budget.dsp_percent = percent;
404 }
405 app.balance();
406 let p = app.plan();
407 assert!(p.delivered.sign.rate > 0.0 && p.delivered.verify.rate > 0.0, "{mode:?}: {:?}", p.delivered);
408 }
409 }
410
411 #[test]
412 fn build_flags_are_toml_values() {
416 for mode in papers() {
417 for lut in [50.0, 100.0, 200.0] {
418 let mut app = App::new("T", mode, slots());
419 app.budget.lut_percent = lut;
420 app.balance();
421 let p = app.plan();
422 for (k, (_, plan)) in p.parts.iter().enumerate() {
423 let Some(plan) = plan else { continue };
424 let c = &app.slots[k].component;
425 let flags = c.build_flags(plan);
426 for (key, v) in &flags {
427 let ok = v.parse::<f64>().is_ok() || v == "true" || v == "false" || (v.len() >= 2 && v.starts_with('"') && v.ends_with('"'));
428 assert!(ok, "{}: {key} = {v} is not a TOML value", c.name());
429 }
430 if c.name().starts_with("DSig") {
431 assert!(flags.iter().any(|(key, _)| key == "buildable"), "{} has no buildable flag", c.name());
432 }
433 }
434 }
435 }
436 }
437}
438
439#[cfg(all(test, feature = "ubft"))]
440mod ubft_tests {
441 use super::*;
442 use rhdl_fpga_planner::tui::{App, snapshot};
443 use rhdl_fpga_planner::ubft::{quorum_verifies, serial_slot_crypto_us};
444
445 #[test]
449 fn ubft_papers_plan_beside_the_replica() {
450 assert!(papers().contains(&Mode::UbftDsig) && papers().contains(&Mode::UbftEd25519));
451 let replicas = slots().iter().filter(|s| s.component.name().starts_with("uBFT replica")).count();
453 assert_eq!(replicas, 1);
454 for mode in [Mode::UbftDsig, Mode::UbftEd25519] {
455 let app = App::new("U280 SIGNATURE PLANNER", mode, slots());
456 println!("{}", snapshot(&app, 140, 44));
457 let p = app.plan();
458 let d = &p.delivered;
459 assert!(d.sign.rate > 0.0 && d.verify.rate > 0.0, "{mode:?}: {d:?}");
460 for n in [3, 7] { let projected = serial_slot_crypto_us(d, n);
462 assert!(projected.is_finite() && projected > 0.0);
463 assert_eq!(projected, d.sign.latency_us + f64::from(quorum_verifies(n)) * d.verify.latency_us);
464 }
465 if mode == Mode::UbftDsig {
466 assert_eq!(app.used.share, 0.0);
467 for (slot, (_, part)) in app.slots.iter().zip(&p.parts) {
468 if matches!(slot.component.role(), rhdl_fpga_planner::Role::Ed25519Sign | rhdl_fpga_planner::Role::Ed25519Verify) {
469 assert!(part.is_none(), "external certificate core was counted twice");
470 }
471 }
472 }
473 }
474 }
475}