Skip to main content

dsfb_densor_runtime/
receipt.rs

1//! The run-level sealed receipt — the tamper-evident record of a whole pipeline run.
2//!
3//! A `RuntimeReceiptV1` collects the ordered [`StageReceiptSummary`]s of an executed pipeline and seals them
4//! (with the manifest's pipeline id + frozen-authority digest) into a single `receipt_hash`. It is the substrate
5//! analogue of the chemical court record's `bundle_root`: a deterministic, re-derivable digest over exactly what
6//! ran, so a verifier can confirm the run was not altered after the fact.
7
8use crate::manifest::DensorManifest;
9use crate::seal::{to_hex, CanonicalHasher};
10use crate::stage::StageReceiptSummary;
11use serde::{Deserialize, Serialize};
12
13/// A sealed record of one pipeline run (schema v1).
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15pub struct RuntimeReceiptV1 {
16    pub pipeline_id: String,
17    /// The ordered per-stage receipt summaries (input/output hashes + authorities).
18    pub stages: Vec<StageReceiptSummary>,
19    /// The load-bearing non-claim.
20    pub non_claim: String,
21    /// SHA-256 (via [`CanonicalHasher`]) over the pipeline id, the frozen-authority allow-list, and every stage
22    /// summary in order — the run's tamper-evident root.
23    pub receipt_hash: String,
24}
25
26impl RuntimeReceiptV1 {
27    pub const NON_CLAIM: &'static str = "A deterministic record of which stages ran over which densors against \
28        which frozen authorities — NOT a claim that the result is correct, optimal, or meaningful in any domain; \
29        the runtime is a mechanism, the meaning lives in the authorities it cites.";
30
31    fn seal(
32        pipeline_id: &str,
33        manifest: &DensorManifest,
34        stages: &[StageReceiptSummary],
35    ) -> String {
36        let mut h = CanonicalHasher::new();
37        h.field("schema", b"dsfb_densor_runtime_receipt_v1");
38        h.field("pipeline_id", pipeline_id.as_bytes());
39        // The frozen-authority allow-list (name+digest, in manifest order) — binds the run to its anchors.
40        for a in &manifest.authorities {
41            h.field("authority_name", a.name.as_bytes());
42            h.hash32("authority_hash", &a.hash);
43        }
44        for s in stages {
45            h.field("stage_id", s.stage_id.as_bytes());
46            h.hash32("input_hash", &s.input_hash);
47            h.hash32("output_hash", &s.output_hash);
48            for a in &s.authority_hashes {
49                h.field("stage_authority", a.name.as_bytes());
50                h.hash32("stage_authority_hash", &a.hash);
51            }
52        }
53        h.field("non_claim", Self::NON_CLAIM.as_bytes());
54        h.finalize_hex()
55    }
56
57    /// Build a sealed run receipt from the manifest + the ordered stage summaries.
58    pub fn build(manifest: &DensorManifest, stages: Vec<StageReceiptSummary>) -> Self {
59        let receipt_hash = Self::seal(&manifest.pipeline_id, manifest, &stages);
60        RuntimeReceiptV1 {
61            pipeline_id: manifest.pipeline_id.clone(),
62            stages,
63            non_claim: Self::NON_CLAIM.to_string(),
64            receipt_hash,
65        }
66    }
67
68    /// Re-derive the seal and confirm it matches (tamper-evident). Requires the same manifest the run used.
69    pub fn verify(&self, manifest: &DensorManifest) -> bool {
70        self.non_claim == Self::NON_CLAIM
71            && self.receipt_hash == Self::seal(&self.pipeline_id, manifest, &self.stages)
72    }
73
74    /// The 12-char short form of the receipt hash, for logs.
75    pub fn short(&self) -> &str {
76        if self.receipt_hash.len() >= 12 {
77            &self.receipt_hash[..12]
78        } else {
79            &self.receipt_hash
80        }
81    }
82}
83
84/// Convenience: hex of a raw digest (re-exported through the crate root for callers building receipts by hand).
85pub fn hex(h: &[u8; 32]) -> String {
86    to_hex(h)
87}