Skip to main content

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}