rhdl_fpga_planner/ubft.rs
1//! The uBFT modes' shared parts: the replica the services sit beside, the
2//! baseline they replace, and what a consensus slot spends on crypto.
3//!
4//! uBFT ([YiJiang0908/ubft_new]) is an HLS kernel on the U280. On its slow
5//! path every replica signs a certificate share for the slot and checks
6//! peers' shares until it holds a quorum of `2f + 1`: its own plus `2f`
7//! peers'. uBFT tolerates `f = (n − 1) / 2` faults among `n` replicas
8//! (`ubft.cpp`: `shared.f = (node_num - 1) / 2`), not the usual
9//! `(n − 1) / 3`, so its quorum is nearly every replica. Today it calls the Vitis Security
10//! library's Ed25519 inline, so each signature stalls the state machine.
11//! The services of this repository (`rhdl_ed25519_scalable_core::ubft`,
12//! DSig's `ubft` module) sit beside it on AXI streams instead.
13//!
14//! In a uBFT mode the planner plans only uBFT modules ([`Mode::admits`]):
15//! the [`UbftReplica`] historical allowance comes off the budget, and the
16//! services divide the rest. The headline uses [`serial_slot_crypto_us`]:
17//! one signature followed by `2f` sequential peer verifications. The
18//! overlapping model in [`slot_crypto_us`] is a separate scenario, not
19//! the implemented controller schedule.
20//!
21//! [YiJiang0908/ubft_new]: https://github.com/YiJiang0908/ubft_new
22
23use crate::*;
24
25/// uBFT's whole routed design, platform shell included: "LUT as logic"
26/// from its link report. An upper bound for the replica alone (its
27/// artifacts carry no per-kernel utilization).
28pub const UBFT_DESIGN_LUT: u32 = 346_232;
29/// DSPs of uBFT's whole routed design.
30pub const UBFT_DESIGN_DSP: u32 = 36;
31/// The baseline: Vitis Security L1 Ed25519 signing inside uBFT, cycles of
32/// its own wrapper in HLS 2023.2 C/RTL cosimulation (key cached).
33pub const UBFT_BASELINE_SIGN_CYCLES: f64 = 3_071_249.0;
34/// The baseline's verification, cycles (same cosimulation).
35pub const UBFT_BASELINE_VERIFY_CYCLES: f64 = 10_411_579.0;
36/// Compiler-selected kernel frequency after timing failure (down from 300
37/// MHz), not runtime clock readback. Time conversions using it are projections.
38pub const UBFT_BASELINE_MHZ: f64 = 161.3;
39/// HLS's LUT estimate of the baseline signer.
40pub const UBFT_BASELINE_SIGN_LUT: f64 = 59_603.0;
41/// HLS's LUT estimate of the baseline verifier.
42pub const UBFT_BASELINE_VERIFY_LUT: f64 = 130_836.0;
43/// Replica counts the headline reports: uBFT's natural sizes `2f + 1`
44/// for `f` = 1, 3, 5 (it supports up to 12 replicas).
45pub const UBFT_REPLICAS: [u32; 3] = [3, 7, 11];
46
47/// The user's target: rhdl_ubft completes correct operations at least this
48/// many times faster on the card than the baseline.
49///
50/// The ratio is taken against a *repaired* ubft_new (the minimal fixes that
51/// let it decide at all), with matched replica count, payload, identities and
52/// warm or cold crypto. The pinned ubft_new (`a7537bd`) makes no progress
53/// ([`Baseline::Stalls`]): that is reported as a failure, never used as a
54/// divisor.
55pub const UBFT_TARGET_SPEEDUP: f64 = 5.0;
56
57/// What the baseline achieved in a comparison.
58#[derive(Clone, Copy, Debug, PartialEq)]
59pub enum Baseline {
60 /// The pinned code decided nothing (its fast votes reach one peer and
61 /// its slow path never publishes certify flags).
62 Stalls,
63 /// The repaired baseline's time per completed operation, µs.
64 Repaired(f64),
65}
66
67/// The speedup of `ours_us` per completed operation over `baseline`, and
68/// whether it meets [`UBFT_TARGET_SPEEDUP`]. `None` when there is no
69/// meaningful ratio (the baseline stalls, or ours did not complete).
70pub fn speedup(ours_us: f64, baseline: Baseline) -> Option<(f64, bool)> {
71 match baseline {
72 Baseline::Repaired(b) if ours_us > 0.0 && ours_us.is_finite() && b > 0.0 && b.is_finite() => {
73 let r = b / ours_us;
74 Some((r, r >= UBFT_TARGET_SPEEDUP))
75 }
76 _ => None,
77 }
78}
79
80/// Peers' shares a replica of `n` must verify for a quorum: `2f` with
81/// uBFT's `f = (n − 1) / 2` (`ubft_machines.cpp` waits for `2f + 1`
82/// shares, its own included).
83pub fn quorum_verifies(n: u32) -> u32 {
84 2 * (n.saturating_sub(1) / 2)
85}
86
87/// Crypto time of one slow-path slot on a chip that delivers `d`, in µs:
88/// sign the share, then verify `2f` peers' shares. Verifications overlap
89/// in the verifier: the first takes its idle latency, the rest follow at
90/// the verify side's rate. A model: it ignores network time and assumes
91/// the peers' shares arrive together.
92pub fn slot_crypto_us(d: &Delivered, n: u32) -> f64 {
93 let v = quorum_verifies(n);
94 if v == 0 {
95 return d.sign.latency_us;
96 }
97 if d.verify.rate <= 0.0 || d.sign.rate <= 0.0 {
98 return f64::INFINITY;
99 }
100 d.sign.latency_us + d.verify.latency_us + 1e6 * f64::from(v - 1) / d.verify.rate
101}
102
103/// Current replica's serial request model: it waits for a peer verdict before
104/// issuing the next verification. Extra service capacity is not automatically
105/// exploitable by that controller. Network, fetch and framing costs are omitted.
106pub fn serial_slot_crypto_us(d: &Delivered, n: u32) -> f64 {
107 let verifies = quorum_verifies(n);
108 if d.sign.rate <= 0.0 || (verifies > 0 && d.verify.rate <= 0.0) { return f64::INFINITY; }
109 d.sign.latency_us + f64::from(verifies) * d.verify.latency_us
110}
111
112/// The baseline's crypto time per slot in µs: one inline signature, then
113/// `2f` verifications one after another, at uBFT's clock.
114pub fn baseline_slot_us(n: u32) -> f64 {
115 (UBFT_BASELINE_SIGN_CYCLES + f64::from(quorum_verifies(n)) * UBFT_BASELINE_VERIFY_CYCLES) / UBFT_BASELINE_MHZ
116}
117
118/// The uBFT replica the services sit beside: a fixed allowance off the
119/// budget, sized by [`UBFT_DESIGN_LUT`] and [`UBFT_DESIGN_DSP`] (an upper
120/// bound: the shell is included). Plans as itself, with no rate.
121#[derive(Clone, Debug)]
122pub struct UbftReplica {
123 /// Hardware capacity; runtime membership must still be odd.
124 pub nodes: u32,
125 /// Compile-time protocol window.
126 pub window: u32,
127}
128impl Default for UbftReplica {
129 fn default() -> Self { Self { nodes: 8, window: 3 } }
130}
131
132impl UbftReplica {
133 /// Its footprint.
134 pub fn cost() -> Cost {
135 Cost { lut: UBFT_DESIGN_LUT, dsp: UBFT_DESIGN_DSP, bram36: 0, uram: 0 }
136 }
137}
138
139impl Component for UbftReplica {
140 fn name(&self) -> String {
141 "uBFT replica (whole design, upper bound)".into()
142 }
143 fn role(&self) -> Role {
144 Role::Other
145 }
146 fn knobs(&self) -> Vec<Knob> {
147 vec![Knob::range("replica capacity (N)", self.nodes as f64, 3.0, 16.0, 1.0),
148 Knob::range("protocol window (G)", self.window as f64, 1.0, 8.0, 1.0)]
149 }
150 fn set_knob(&mut self, index: usize, value: f64) {
151 if !value.is_finite() { return; }
152 match index { 0 => self.nodes = (value as u32).clamp(3, 16), 1 => self.window = (value as u32).clamp(1, 8), _ => {} }
153 }
154 fn build_flags(&self, _plan: &Plan) -> BuildFlags {
155 vec![("RHDL_UBFT_NODES".into(), self.nodes.to_string()),
156 ("RHDL_UBFT_WINDOW".into(), self.window.to_string())]
157 }
158 fn plan(&self, share: &Share, _cx: &Context) -> Option<Plan> {
159 let cost = UbftReplica::cost();
160 cost.fits(share).then(|| Plan {
161 cost,
162 rate: 0.0,
163 unit: "slot",
164 limit: "fixed".into(),
165 detail: format!(
166 "Historical whole-design allowance: {} LUT, {} DSP incl. shell; BRAM/URAM unknown, not zero measured",
167 UBFT_DESIGN_LUT, UBFT_DESIGN_DSP
168 ),
169 demands: Vec::new(),
170 supplies: Vec::new(),
171 // Measured historical LUT/DSP totals are used as an allowance;
172 // this is not the current RHDL replica's measured four-resource cost.
173 measured: false,
174 latency_cycles: 0.0,
175 in_flight: 0.0,
176 batch: None,
177 covers: Vec::new(),
178 })
179 }
180 fn fixed(&self) -> Option<Share> {
181 Some(UbftReplica::cost())
182 }
183 fn ubft(&self) -> bool {
184 true
185 }
186}
187
188impl Budget {
189 /// The whole device: every LUT and DSP of the U280 (the budget's
190 /// percentages above 100 %), the dynamic region's BRAM and all URAM.
191 /// The uBFT modes start here, because the replica's footprint
192 /// ([`UbftReplica`]) already counts the shell.
193 pub fn whole_device() -> Budget {
194 Budget {
195 lut_percent: 100.0 * U280_LUT as f64 / BASE_LUT as f64,
196 dsp_percent: 100.0 * U280_DSP as f64 / BASE_DSP as f64,
197 bram_percent: 100.0,
198 uram_percent: 100.0,
199 }
200 }
201}