1use 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
30pub const NEAREST: usize = 3;
32
33pub const KEPT: [&str; 8] = ["measurement.json", "discriminators.json", "gbs.sha256", "pr.log", "userclk.log", "started", "finished", "logos_engine.v"];
36
37pub 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
44pub 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
60pub fn kept_dir(receipt: &Receipt) -> String {
63 format!("{}-{}", receipt.build, &receipt.measurement_sha256[..12])
64}
65
66#[derive(Clone, Copy, Debug)]
68pub struct CardRun<'a> {
69 pub gbs_sha256: &'a str,
71 pub pr: &'a str,
73 pub user_clock: &'a str,
75 pub started: &'a str,
77 pub finished: &'a str,
78 pub measurement: &'a str,
80}
81
82#[derive(Clone, Copy, Debug)]
84pub struct Inputs<'a> {
85 pub build: &'a str,
87 pub builds: &'a str,
89 pub discriminators: &'a str,
91 pub image_sha256: &'a str,
93 pub latencies: [usize; 2],
95 pub card: CardRun<'a>,
96}
97
98#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
100#[serde(rename_all = "snake_case")]
101pub enum Unit {
102 Hard { latency: usize, registering: String, pads: usize, flush_operands: bool, flush_results: bool, canonical_nan: bool },
105 Refused { latency: usize, why: String },
107}
108
109#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
111pub struct Near {
112 pub class: String,
113 pub differing: u64,
114}
115
116#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
118pub struct Mode {
119 pub full: usize,
121 pub least: usize,
122 pub agreeing: usize,
124 pub class: Vec<String>,
125 pub unexplained: u64,
127 pub nearest: Vec<Near>,
130 pub unit: Unit,
131}
132
133#[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#[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#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
165pub struct Ledger {
166 #[serde(rename = "_what")]
167 pub what: String,
168 pub receipts: Vec<Receipt>,
169}
170
171pub 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#[derive(Clone, Debug, PartialEq)]
179pub enum ReceiptError {
180 Ledger(String),
182 NoBuild(String),
184 BuildFailed(String),
186 NoEngine(String),
188 Engine { build: String, compiled: String, kept: String },
190 Regenerated { build: String, module: String },
192 Oracle(OracleError),
193 Image { configured: String, given: String },
195 Reconfiguration(String),
197 UserClock,
199 Times { started: String, finished: String },
201 Measure(MeasureError),
202 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
233pub 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
279pub 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
302pub 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
322fn 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
343fn 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
374fn 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
397fn 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
403fn 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
424fn 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
432fn 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
445fn 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;