Skip to main content

logicaffeine_compile/codegen_sva/
fp_receipt.rs

1//! The card's measurement of its floating-point DSP block as a receipt (`benchmarks/silicon/d5005/fp_hard_receipts.json`):
2//! what the oracle persona ([`super::fp_oracle`]) measured on the card, tied to the build that compiled it and the image
3//! the card ran, with what each mode's block is for the engine ([`super::fp_hard`]). The block is the vendor's and
4//! encrypted, so a receipt is a measurement recorded as one; the proofs hold everything around it.
5//!
6//! A receipt reads ([`receipt`]) only when:
7//! - the build is a row of the build ledger that passed, naming the engine it compiled (`hw/rtl/logos_engine.v`, by
8//!   SHA-256);
9//! - the card's run configured the image (`gbs.sha256`, the run's own hash of what it loaded), partial reconfiguration
10//!   reported no error (`PR_ERR` all zero), the user clock was set, and the run finished no earlier than it started;
11//! - the measurement finished.
12//!
13//! Each mode is its depths, the one class that agreed with the block on every step (or how many did, and the steps no
14//! class explains), the classes that differed on the fewest steps, and the engine's unit at its latency on that block:
15//! the wrapper's build ([`plan`]) or why there is none. The multiply-add is its depth and its rounding.
16//!
17//! [`record`] refuses besides a receipt whose discriminator ledger does not regenerate the engine the build compiled
18//! (the persona's oracle is that ledger's classes and vectors), and a measurement the ledger already holds. Its inputs
19//! are kept beside the ledger ([`KEPT`] in [`kept_dir`], each as [`kept`] keeps it), so every receipt reads again from
20//! them.
21
22use std::fmt;
23
24use serde::{Deserialize, Serialize};
25
26use super::fp_hard::{plan, Block, MeasureError, Measurement, Op, Registering, Rounding};
27use super::fp_oracle::{Class, Oracle, OracleError};
28use crate::crypto_accel::sha256;
29
30/// How many of the classes nearest the block a mode keeps.
31pub const NEAREST: usize = 3;
32
33/// The inputs a receipt keeps beside the ledger, each under the name the card's run wrote it (the discriminator ledger
34/// as `discriminators.json`), in [`kept_dir`].
35pub const KEPT: [&str; 8] = ["measurement.json", "discriminators.json", "gbs.sha256", "pr.log", "userclk.log", "started", "finished", "logos_engine.v"];
36
37/// The receipt that records `measurement`, by its SHA-256: the card run a hard-float build takes its blocks from, read
38/// against the discriminator ledger kept beside it (`fp_receipts/<kept_dir>/discriminators.json`).
39pub fn receipt_of<'a>(ledger: &'a Ledger, measurement: &str) -> Option<&'a Receipt> {
40    let sha = hex(&sha256(measurement.as_bytes()));
41    ledger.receipts.iter().find(|r| r.measurement_sha256 == sha)
42}
43
44/// A card run's file `name` as the ledger keeps it: as the run wrote it, but for a `sha256sum` line (`gbs.sha256`),
45/// which names its image by its file name alone, so no kept input names one machine's directories.
46pub fn kept(name: &str, text: &str) -> String {
47    if name != "gbs.sha256" {
48        return text.to_string();
49    }
50    let lines: Vec<String> = text
51        .lines()
52        .map(|l| match l.split_once("  ") {
53            Some((hash, path)) => format!("{hash}  {}", path.rsplit('/').next().unwrap_or(path)),
54            None => l.to_string(),
55        })
56        .collect();
57    lines.join("\n") + if text.ends_with('\n') { "\n" } else { "" }
58}
59
60/// Where a receipt's inputs are kept, under `fp_receipts/` beside the ledger: the build and the measurement's first
61/// twelve hex digits, one directory a measurement.
62pub fn kept_dir(receipt: &Receipt) -> String {
63    format!("{}-{}", receipt.build, &receipt.measurement_sha256[..12])
64}
65
66/// What the card's run left beside its measurement, each file as the run wrote it.
67#[derive(Clone, Copy, Debug)]
68pub struct CardRun<'a> {
69    /// `gbs.sha256`: `sha256sum` of the image the run configured.
70    pub gbs_sha256: &'a str,
71    /// `pr.log`: the FME's partial-reconfiguration status and error registers after configuring.
72    pub pr: &'a str,
73    /// `userclk.log`: the user clock the run set.
74    pub user_clock: &'a str,
75    /// `started` and `finished`: the measurement's start and end, `YYYY-MM-DDTHH:MM:SSZ`.
76    pub started: &'a str,
77    pub finished: &'a str,
78    /// `measurement.json`: the host program's output ([`super::fp_oracle::HOST_PROGRAM`]).
79    pub measurement: &'a str,
80}
81
82/// Everything a receipt is read from.
83#[derive(Clone, Copy, Debug)]
84pub struct Inputs<'a> {
85    /// The build's name, a row of `builds`.
86    pub build: &'a str,
87    /// The build ledger (`benchmarks/silicon/d5005/quartus_builds.json`).
88    pub builds: &'a str,
89    /// The discriminator ledger the persona's oracle was made from.
90    pub discriminators: &'a str,
91    /// The SHA-256 of the image, hex.
92    pub image_sha256: &'a str,
93    /// The engine's multiply and add latencies the units are planned at.
94    pub latencies: [usize; 2],
95    pub card: CardRun<'a>,
96}
97
98/// The engine's unit on one mode's block.
99#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
100#[serde(rename_all = "snake_case")]
101pub enum Unit {
102    /// The wrapper at `latency`: the block registered fully or with its output register alone, the registers after it,
103    /// and what the logic around it corrects.
104    Hard { latency: usize, registering: String, pads: usize, flush_operands: bool, flush_results: bool, canonical_nan: bool },
105    /// No wrapper at `latency`, and why.
106    Refused { latency: usize, why: String },
107}
108
109/// One class and the steps on which it differed from the block over every run.
110#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
111pub struct Near {
112    pub class: String,
113    pub differing: u64,
114}
115
116/// One mode of the block as the card measured it.
117#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
118pub struct Mode {
119    /// Its depth fully registered and with its output register alone.
120    pub full: usize,
121    pub least: usize,
122    /// How many classes agreed with the block on every step, and the members of the one that did (empty unless one).
123    pub agreeing: usize,
124    pub class: Vec<String>,
125    /// The steps no class explains.
126    pub unexplained: u64,
127    /// The [`NEAREST`] classes that differed on the fewest steps, fewest first, ties in the family's order; each by its
128    /// first member.
129    pub nearest: Vec<Near>,
130    pub unit: Unit,
131}
132
133/// The steps of each run.
134#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
135pub struct Steps {
136    pub vectors: u64,
137    pub random: u64,
138    pub near: u64,
139}
140
141/// A receipt: the run's identity, the measurement's size, and each mode.
142#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
143pub struct Receipt {
144    pub recorded: String,
145    pub build: String,
146    pub gbs_sha256: String,
147    pub engine_sha256: String,
148    pub discriminators_sha256: String,
149    pub measurement_sha256: String,
150    pub started: String,
151    pub finished: String,
152    pub user_clock_mhz: f64,
153    pub seed: u64,
154    pub steps: Steps,
155    pub product_classes: usize,
156    pub sum_classes: usize,
157    pub multiply: Mode,
158    pub add: Mode,
159    pub multiply_add_latency: usize,
160    pub multiply_add_rounding: String,
161}
162
163/// The receipts ledger.
164#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
165pub struct Ledger {
166    #[serde(rename = "_what")]
167    pub what: String,
168    pub receipts: Vec<Receipt>,
169}
170
171/// What the receipts ledger says of itself when it starts.
172pub const LEDGER_WHAT: &str = "The D5005's floating-point DSP block as the card measured it: each receipt one run of the oracle \
173persona (codegen_sva::fp_oracle) on the image the build compiled, recorded by the d5005_fp_receipt example only when the \
174build passed, its discriminator ledger regenerates the engine it compiled, the card configured that image without error \
175and the measurement finished. Its inputs are kept beside it in fp_receipts/<build>-<measurement>/.";
176
177/// Why a receipt is not read or not recorded.
178#[derive(Clone, Debug, PartialEq)]
179pub enum ReceiptError {
180    /// The build ledger or the receipts ledger is not one.
181    Ledger(String),
182    /// The build ledger has no row of this name.
183    NoBuild(String),
184    /// The build did not pass.
185    BuildFailed(String),
186    /// The build names no one engine it compiled.
187    NoEngine(String),
188    /// The engine given is not the one the build compiled: its SHA-256 and the build's.
189    Engine { build: String, compiled: String, kept: String },
190    /// A module the oracle writes itself that the discriminator ledger does not regenerate as the build compiled it.
191    Regenerated { build: String, module: String },
192    Oracle(OracleError),
193    /// The run configured another image than the one given.
194    Image { configured: String, given: String },
195    /// Partial reconfiguration reported an error, or the run left no error register.
196    Reconfiguration(String),
197    /// The run set no user clock.
198    UserClock,
199    /// A time not `YYYY-MM-DDTHH:MM:SSZ`, or a run that finished before it started.
200    Times { started: String, finished: String },
201    Measure(MeasureError),
202    /// The ledger already holds a receipt of this measurement.
203    Duplicate(String),
204}
205
206impl fmt::Display for ReceiptError {
207    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
208        match self {
209            ReceiptError::Ledger(e) => write!(f, "a ledger: {e}"),
210            ReceiptError::NoBuild(b) => write!(f, "the build ledger has no row {b}"),
211            ReceiptError::BuildFailed(b) => write!(f, "the build {b} did not pass"),
212            ReceiptError::NoEngine(b) => write!(f, "the build {b} names no one hw/rtl/logos_engine.v among its inputs"),
213            ReceiptError::Engine { build, compiled, kept } => write!(
214                f,
215                "the engine given is {}, the build {build} compiled {}",
216                &kept[..12.min(kept.len())],
217                &compiled[..12.min(compiled.len())]
218            ),
219            ReceiptError::Regenerated { build, module } => {
220                write!(f, "the discriminator ledger does not regenerate {module} as the build {build} compiled it")
221            }
222            ReceiptError::Oracle(e) => write!(f, "{e}"),
223            ReceiptError::Image { configured, given } => write!(f, "the card configured the image {configured}, not {given}"),
224            ReceiptError::Reconfiguration(why) => write!(f, "partial reconfiguration: {why}"),
225            ReceiptError::UserClock => write!(f, "the run set no user clock"),
226            ReceiptError::Times { started, finished } => write!(f, "the run started {started:?} and finished {finished:?}"),
227            ReceiptError::Measure(e) => write!(f, "{e}"),
228            ReceiptError::Duplicate(sha) => write!(f, "the ledger already holds a receipt of the measurement {}", &sha[..12.min(sha.len())]),
229        }
230    }
231}
232
233/// The receipt `inputs` make, as recorded at `recorded`.
234pub fn receipt(inputs: &Inputs, recorded: &str) -> Result<Receipt, ReceiptError> {
235    let engine_sha256 = compiled(inputs.builds, inputs.build)?;
236    let oracle = Oracle::from_ledger(inputs.discriminators).map_err(ReceiptError::Oracle)?;
237    let card = &inputs.card;
238    let image = configured(card.gbs_sha256);
239    if image != Some(inputs.image_sha256) {
240        let configured = image.map_or_else(|| card.gbs_sha256.lines().next().unwrap_or("").to_string(), str::to_string);
241        return Err(ReceiptError::Image { configured, given: inputs.image_sha256.to_string() });
242    }
243    reconfiguration(card.pr).map_err(ReceiptError::Reconfiguration)?;
244    let user_clock_mhz = user_clock_mhz(card.user_clock).ok_or(ReceiptError::UserClock)?;
245    let (started, finished) = (card.started.trim(), card.finished.trim());
246    if !time(started) || !time(finished) || finished < started {
247        return Err(ReceiptError::Times { started: started.to_string(), finished: finished.to_string() });
248    }
249    let m = card.measurement;
250    let (seed, [vectors, random, near]) = Measurement::read_runs(m).map_err(ReceiptError::Measure)?;
251    let [multiply, add] = [(Op::Multiply, inputs.latencies[0]), (Op::Add, inputs.latencies[1])].map(|(op, latency)| mode(&oracle, m, op, latency));
252    let (multiply_add_latency, rounding) = Measurement::read_multiply_add(m).map_err(ReceiptError::Measure)?;
253    Ok(Receipt {
254        recorded: recorded.to_string(),
255        build: inputs.build.to_string(),
256        gbs_sha256: inputs.image_sha256.to_string(),
257        engine_sha256,
258        discriminators_sha256: hex(&sha256(inputs.discriminators.as_bytes())),
259        measurement_sha256: hex(&sha256(m.as_bytes())),
260        started: started.to_string(),
261        finished: finished.to_string(),
262        user_clock_mhz,
263        seed,
264        steps: Steps { vectors, random, near },
265        product_classes: oracle.products.len(),
266        sum_classes: oracle.sums.len(),
267        multiply: multiply?,
268        add: add?,
269        multiply_add_latency,
270        multiply_add_rounding: match rounding {
271            Rounding::Once => "once",
272            Rounding::Twice => "twice",
273            Rounding::Other => "other",
274        }
275        .to_string(),
276    })
277}
278
279/// Whether `engine` is the engine the build compiled and was made of `discriminators`: its SHA-256 the build row's
280/// `hw/rtl/logos_engine.v`, and every module of it the oracle writes itself ([`Oracle::own_verilog`]) the text the oracle
281/// of `discriminators` writes now, byte for byte. A receipt is recorded under it; kept, the engine is held by its SHA-256
282/// thereafter, so the oracle's code may move on. The library the engine embeds (the engine's crossing and ports, the
283/// store's memory) is held by the build's SHA-256 alone, so the library may move on between a build and its receipt; an
284/// own module that does not regenerate is refused by name.
285pub fn engine_held(build: &str, builds: &str, discriminators: &str, engine: &str) -> Result<(), ReceiptError> {
286    let compiled = compiled(builds, build)?;
287    let kept = hex(&sha256(engine.as_bytes()));
288    if kept != compiled {
289        return Err(ReceiptError::Engine { build: build.to_string(), compiled, kept });
290    }
291    let oracle = Oracle::from_ledger(discriminators).map_err(ReceiptError::Oracle)?;
292    let own = oracle.own_verilog();
293    let built = super::netlist::modules(engine).unwrap_or_default();
294    for (module, text) in super::netlist::modules(&own).unwrap_or_default() {
295        if !built.iter().any(|(m, t)| *m == module && *t == text) {
296            return Err(ReceiptError::Regenerated { build: build.to_string(), module });
297        }
298    }
299    Ok(())
300}
301
302/// [`receipt`], refused unless `engine` is held ([`engine_held`]) and `ledger` (none: a new ledger) holds no receipt of
303/// the same measurement; with the ledger it is appended to, as written.
304pub fn record(ledger: Option<&str>, inputs: &Inputs, engine: &str, recorded: &str) -> Result<(Receipt, String), ReceiptError> {
305    let receipt = receipt(inputs, recorded)?;
306    engine_held(inputs.build, inputs.builds, inputs.discriminators, engine)?;
307    let mut ledger = match ledger {
308        Some(text) => serde_json::from_str::<Ledger>(text).map_err(|e| ReceiptError::Ledger(e.to_string()))?,
309        None => Ledger { what: LEDGER_WHAT.to_string(), receipts: Vec::new() },
310    };
311    if ledger.receipts.iter().any(|r| r.measurement_sha256 == receipt.measurement_sha256) {
312        return Err(ReceiptError::Duplicate(receipt.measurement_sha256));
313    }
314    ledger.receipts.push(receipt.clone());
315    let mut text = Vec::new();
316    let mut writer = serde_json::Serializer::with_formatter(&mut text, serde_json::ser::PrettyFormatter::with_indent(b" "));
317    ledger.serialize(&mut writer).map_err(|e| ReceiptError::Ledger(e.to_string()))?;
318    text.push(b'\n');
319    Ok((receipt, String::from_utf8(text).expect("JSON is UTF-8")))
320}
321
322/// The engine the build compiled, by its row in the build ledger: a row that passed, naming one `hw/rtl/logos_engine.v`.
323fn compiled(builds: &str, build: &str) -> Result<String, ReceiptError> {
324    let builds: serde_json::Value = serde_json::from_str(builds).map_err(|e| ReceiptError::Ledger(e.to_string()))?;
325    let rows = builds["rows"].as_array().ok_or_else(|| ReceiptError::Ledger("the build ledger has no rows".to_string()))?;
326    let row = rows.iter().find(|r| r["name"] == build).ok_or_else(|| ReceiptError::NoBuild(build.to_string()))?;
327    if row["verdict"]["passed"] != true {
328        return Err(ReceiptError::BuildFailed(build.to_string()));
329    }
330    let engines: Vec<&str> = row["inputs"]
331        .as_array()
332        .into_iter()
333        .flatten()
334        .filter(|i| i["path"].as_str().is_some_and(|p| p.ends_with("/hw/rtl/logos_engine.v")))
335        .filter_map(|i| i["sha256"].as_str())
336        .collect();
337    match engines[..] {
338        [engine] => Ok(engine.to_string()),
339        _ => Err(ReceiptError::NoEngine(build.to_string())),
340    }
341}
342
343/// One mode as the measurement found it, and the engine's unit at `latency` on its block.
344fn mode(oracle: &Oracle, m: &str, op: Op, latency: usize) -> Result<Mode, ReceiptError> {
345    let tally = Measurement::read_tally(oracle, m, op).map_err(ReceiptError::Measure)?;
346    let (full, least) = Measurement::read_depths(m, op).map_err(ReceiptError::Measure)?;
347    let classes = match op {
348        Op::Multiply => &oracle.products,
349        Op::Add => &oracle.sums,
350    };
351    let read = match op {
352        Op::Multiply => Measurement::read_product(oracle, m),
353        Op::Add => Measurement::read_sum(oracle, m),
354    };
355    let (class, unit) = match read {
356        Ok(block) => {
357            let class = classes.iter().find(|c| c.semantics == block.semantics).map(|c| c.members.clone()).unwrap_or_default();
358            (class, unit(op, &block, latency))
359        }
360        Err(e @ MeasureError::Class { .. }) => (Vec::new(), Unit::Refused { latency, why: e.to_string() }),
361        Err(e) => return Err(ReceiptError::Measure(e)),
362    };
363    Ok(Mode {
364        full,
365        least,
366        agreeing: tally.differing.iter().filter(|&&d| d == 0).count(),
367        class,
368        unexplained: tally.unexplained,
369        nearest: nearest(classes, &tally.differing),
370        unit,
371    })
372}
373
374/// The engine's unit at `latency` on `block`.
375fn unit(op: Op, block: &Block, latency: usize) -> Unit {
376    match plan(op, block, latency) {
377        Ok(p) => Unit::Hard {
378            latency,
379            registering: match p.registering {
380                Registering::Full => "full",
381                Registering::Least => "least",
382            }
383            .to_string(),
384            pads: p.pads,
385            flush_operands: p.flush_operands,
386            flush_results: p.flush_results,
387            canonical_nan: p.canonical_nan,
388        },
389        Err(e) => Unit::Refused { latency, why: e.to_string() },
390    }
391}
392
393fn hex(bytes: &[u8]) -> String {
394    bytes.iter().map(|b| format!("{b:02x}")).collect()
395}
396
397/// The image a run configured: the first word of its `sha256sum` line, 64 lower-case hex digits.
398fn configured(line: &str) -> Option<&str> {
399    let word = line.split_whitespace().next()?;
400    (word.len() == 64 && word.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))).then_some(word)
401}
402
403/// Whether partial reconfiguration reported no error: every `PR_ERR` line's register and each of its fields zero.
404fn reconfiguration(pr: &str) -> Result<(), String> {
405    let lines: Vec<&str> = pr.lines().filter(|l| l.starts_with("PR_ERR")).collect();
406    if lines.is_empty() {
407        return Err("the run left no PR_ERR register".to_string());
408    }
409    for line in lines {
410        let (register, fields) = line["PR_ERR".len()..].split_once(':').ok_or_else(|| format!("unread: {line}"))?;
411        let raw = register.trim().strip_prefix("0x").and_then(|h| u64::from_str_radix(h, 16).ok()).ok_or_else(|| format!("unread: {line}"))?;
412        let words: Vec<&str> = fields.split_whitespace().collect();
413        if words.is_empty() || words.len() % 2 != 0 || words.chunks(2).any(|f| f[1].parse::<u64>().is_err()) {
414            return Err(format!("unread: {line}"));
415        }
416        let set: Vec<String> = words.chunks(2).filter(|f| f[1].parse::<u64>() != Ok(0)).map(|f| f.join(" ")).collect();
417        if raw != 0 || !set.is_empty() {
418            return Err(format!("PR_ERR {raw:#x}: {}", set.join(", ")));
419        }
420    }
421    Ok(())
422}
423
424/// The user clock a run left set: the last `High clock = <MHz> MHz` it reported.
425fn user_clock_mhz(log: &str) -> Option<f64> {
426    log.lines()
427        .filter_map(|l| l.trim().strip_prefix("High clock"))
428        .filter_map(|r| r.trim_start().strip_prefix('=')?.trim().strip_suffix("MHz")?.trim().parse::<f64>().ok())
429        .last()
430}
431
432/// Whether `t` is `YYYY-MM-DDTHH:MM:SSZ`.
433fn time(t: &str) -> bool {
434    let b = t.as_bytes();
435    b.len() == 20
436        && b.iter().enumerate().all(|(i, &c)| match i {
437            4 | 7 => c == b'-',
438            10 => c == b'T',
439            13 | 16 => c == b':',
440            19 => c == b'Z',
441            _ => c.is_ascii_digit(),
442        })
443}
444
445/// The [`NEAREST`] classes with the fewest differing steps, fewest first, ties in the classes' order.
446fn nearest(classes: &[Class], differing: &[u64]) -> Vec<Near> {
447    let mut order: Vec<usize> = (0..classes.len().min(differing.len())).collect();
448    order.sort_by_key(|&k| (differing[k], k));
449    order.into_iter().take(NEAREST).map(|k| Near { class: classes[k].members[0].clone(), differing: differing[k] }).collect()
450}
451
452#[cfg(test)]
453#[path = "../../../logicaffeine_tests/unit/logicaffeine_compile/codegen_sva/fp_receipt/tests.rs"]
454mod tests;