Skip to main content

cpop_protocol/rfc/
checkpoint.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! RFC-compliant checkpoint structure for CBOR encoding.
4//!
5//! Implements the checkpoint CDDL structure from draft-condrey-rats-pop-schema-01:
6//!
7//! ```cddl
8//! checkpoint = {
9//!     1 => uint,           ; sequence
10//!     2 => uuid,           ; checkpoint-id
11//!     3 => pop-timestamp,  ; timestamp
12//!     4 => bstr .size 32,  ; content-hash
13//!     5 => bstr .size 32,  ; prev-hash
14//!     6 => bstr .size 32,  ; checkpoint-hash
15//!     7 => vdf-proof,      ; silicon-anchored VDF
16//!     8 => jitter-binding, ; behavioral binding
17//!     9 => bstr .size 32,  ; chain-mac (PUF-bound)
18//! }
19//! ```
20
21use serde::{Deserialize, Serialize};
22use uuid::Uuid;
23
24use super::fixed_point::{Millibits, RhoMillibits};
25use super::jitter_binding::JitterBinding;
26use super::serde_helpers::{hex_bytes, hex_bytes_32_opt, hex_bytes_vec};
27use super::vdf::VdfProofRfc;
28
29/// RFC-compliant checkpoint for CBOR wire format.
30///
31/// Separate from internal `Checkpoint` to allow different serialization
32/// strategies (JSON for human-readable, CBOR for wire).
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct CheckpointRfc {
35    #[serde(rename = "1")]
36    pub sequence: u64,
37
38    #[serde(rename = "2")]
39    pub checkpoint_id: Uuid,
40
41    #[serde(rename = "3")]
42    pub timestamp: u64,
43
44    #[serde(rename = "4", with = "hex_bytes")]
45    pub content_hash: [u8; 32],
46
47    /// Zeros for the first checkpoint in a chain.
48    #[serde(rename = "5", with = "hex_bytes")]
49    pub prev_hash: [u8; 32],
50
51    #[serde(rename = "6", with = "hex_bytes")]
52    pub checkpoint_hash: [u8; 32],
53
54    #[serde(rename = "7", skip_serializing_if = "Option::is_none")]
55    pub vdf_proof: Option<VdfProofRfc>,
56
57    #[serde(rename = "8", skip_serializing_if = "Option::is_none")]
58    pub jitter_binding: Option<JitterBinding>,
59
60    #[serde(
61        rename = "9",
62        skip_serializing_if = "Option::is_none",
63        with = "hex_bytes_32_opt"
64    )]
65    pub chain_mac: Option<[u8; 32]>,
66}
67
68impl CheckpointRfc {
69    /// Create a checkpoint with a new UUID and zeroed checkpoint hash.
70    pub fn new(sequence: u64, timestamp: u64, content_hash: [u8; 32], prev_hash: [u8; 32]) -> Self {
71        Self {
72            sequence,
73            checkpoint_id: Uuid::new_v4(),
74            timestamp,
75            content_hash,
76            prev_hash,
77            checkpoint_hash: [0u8; 32],
78            vdf_proof: None,
79            jitter_binding: None,
80            chain_mac: None,
81        }
82    }
83
84    /// Attach a VDF proof.
85    pub fn with_vdf(mut self, proof: VdfProofRfc) -> Self {
86        self.vdf_proof = Some(proof);
87        self
88    }
89
90    /// Attach a jitter binding.
91    pub fn with_jitter(mut self, binding: JitterBinding) -> Self {
92        self.jitter_binding = Some(binding);
93        self
94    }
95
96    /// Attach a PUF-bound chain MAC.
97    pub fn with_chain_mac(mut self, mac: [u8; 32]) -> Self {
98        self.chain_mac = Some(mac);
99        self
100    }
101
102    /// Compute and set `checkpoint_hash` over all fields except itself.
103    pub fn compute_hash(&mut self) {
104        use sha2::{Digest, Sha256};
105
106        let mut hasher = Sha256::new();
107
108        hasher.update(b"witnessd-checkpoint-v3");
109
110        hasher.update(self.sequence.to_be_bytes());
111        hasher.update(self.checkpoint_id.as_bytes());
112        hasher.update(self.timestamp.to_be_bytes());
113        hasher.update(self.content_hash);
114        hasher.update(self.prev_hash);
115
116        if let Some(vdf) = &self.vdf_proof {
117            hasher.update(b"\x01");
118            hasher.update(vdf.challenge);
119            hasher.update(vdf.output);
120            hasher.update(vdf.iterations.to_be_bytes());
121            hasher.update(vdf.duration_ms.to_be_bytes());
122        } else {
123            hasher.update(b"\x00");
124        }
125
126        if let Some(jitter) = &self.jitter_binding {
127            hasher.update(b"\x01");
128            hasher.update(jitter.entropy_commitment.hash);
129        } else {
130            hasher.update(b"\x00");
131        }
132
133        if let Some(mac) = &self.chain_mac {
134            hasher.update(b"\x01");
135            hasher.update(mac);
136        } else {
137            hasher.update(b"\x00");
138        }
139
140        self.checkpoint_hash = hasher.finalize().into();
141    }
142
143    /// Validate all fields and return a list of errors (empty if valid).
144    pub fn validate(&self) -> Vec<String> {
145        let mut errors = Vec::new();
146
147        if self.content_hash == [0u8; 32] {
148            errors.push("content_hash is zero".into());
149        }
150
151        if self.checkpoint_hash == [0u8; 32] {
152            errors.push("checkpoint_hash is zero (call compute_hash first)".into());
153        }
154
155        if let Some(vdf) = &self.vdf_proof {
156            errors.extend(vdf.validate());
157        }
158
159        if let Some(jitter) = &self.jitter_binding {
160            errors.extend(jitter.validate());
161        }
162
163        errors
164    }
165
166    /// Return `true` if `validate()` produces no errors.
167    pub fn is_valid(&self) -> bool {
168        self.validate().is_empty()
169    }
170}
171
172/// Silicon-bound VDF proof with bio-binding.
173///
174/// ```cddl
175/// sa-vdf-proof = {
176///     1 => uint,   ; algorithm (20=hmac-sha256-puf)
177///     2 => uint,   ; iterations
178///     3 => uint,   ; cycle-count (RDTSC)
179///     4 => bstr,   ; output
180/// }
181/// ```
182#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct SaVdfProof {
184    /// 20 = HMAC-SHA256-PUF
185    #[serde(rename = "1")]
186    pub algorithm: u32,
187
188    #[serde(rename = "2")]
189    pub iterations: u64,
190
191    #[serde(rename = "3")]
192    pub cycle_count: u64,
193
194    #[serde(rename = "4", with = "hex_bytes_vec")]
195    pub output: Vec<u8>,
196}
197
198/// Biometric binding for checkpoint.
199///
200/// ```cddl
201/// bio-binding = {
202///     1 => uint,   ; rho_millibits (Spearman * 1000)
203///     2 => uint,   ; hurst_millibits (H * 1000)
204///     3 => uint,   ; recognition_gap_ms
205/// }
206/// ```
207#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct BioBinding {
209    #[serde(rename = "1")]
210    pub rho_millibits: RhoMillibits,
211
212    #[serde(rename = "2")]
213    pub hurst_millibits: Millibits,
214
215    #[serde(rename = "3")]
216    pub recognition_gap_ms: u32,
217}
218
219impl BioBinding {
220    /// Create a bio binding from floating-point correlation, Hurst, and gap values.
221    pub fn new(rho: f64, hurst: f64, gap_ms: u32) -> Self {
222        Self {
223            rho_millibits: RhoMillibits::from_float(rho),
224            hurst_millibits: Millibits::from_float(hurst),
225            recognition_gap_ms: gap_ms,
226        }
227    }
228
229    /// Human typing produces Hurst exponents in 0.55..0.85.
230    pub fn is_hurst_human_like(&self) -> bool {
231        let h = self.hurst_millibits.raw();
232        h > 550 && h < 850
233    }
234
235    /// Return `true` if the Spearman rho correlation is within plausible bounds.
236    pub fn is_correlation_valid(&self) -> bool {
237        let rho = self.rho_millibits.raw();
238        (500..=950).contains(&rho)
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    #[test]
247    fn test_checkpoint_creation() {
248        let cp = CheckpointRfc::new(0, 1700000000, [1u8; 32], [0u8; 32]);
249
250        assert_eq!(cp.sequence, 0);
251        assert_eq!(cp.content_hash, [1u8; 32]);
252        assert_eq!(cp.prev_hash, [0u8; 32]);
253    }
254
255    #[test]
256    fn test_checkpoint_hash_computation() {
257        let mut cp = CheckpointRfc::new(1, 1700000000, [1u8; 32], [2u8; 32]);
258
259        assert_eq!(cp.checkpoint_hash, [0u8; 32]);
260        cp.compute_hash();
261        assert_ne!(cp.checkpoint_hash, [0u8; 32]);
262    }
263
264    #[test]
265    fn test_bio_binding_hurst() {
266        let binding = BioBinding::new(0.75, 0.72, 250);
267        assert!(binding.is_hurst_human_like());
268        assert!(binding.is_correlation_valid());
269
270        // White noise (H=0.5)
271        let white_noise = BioBinding::new(0.75, 0.5, 250);
272        assert!(!white_noise.is_hurst_human_like());
273    }
274
275    #[test]
276    fn test_checkpoint_serialization() {
277        let cp = CheckpointRfc::new(0, 1700000000, [1u8; 32], [0u8; 32]);
278
279        let json = serde_json::to_string(&cp).unwrap();
280        assert!(json.contains("\"1\":0")); // sequence
281        assert!(json.contains("\"3\":1700000000")); // timestamp
282    }
283}