Skip to main content

entrouter_universal/
chain.rs

1// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2//  Entrouter Universal - Chain Verification
3//
4//  Each link in the chain references the previous link's
5//  fingerprint. Unbreakable sequence. Cryptographic audit trail.
6//
7//  Use case: race results, financial transactions, anything
8//  where ORDER and INTEGRITY both matter.
9//
10//  If someone tampers with link 3 of a 10-link chain,
11//  links 4-10 all break simultaneously. You know exactly
12//  where the chain was cut.
13// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
14
15use crate::{encode_str, fingerprint_str, UniversalError};
16use serde::{Deserialize, Serialize};
17use std::time::{SystemTime, UNIX_EPOCH};
18
19/// A single link in a cryptographic chain.
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct ChainLink {
22    /// Sequence number (1-based)
23    pub seq: u64,
24    /// Base64 encoded data
25    pub d: String,
26    /// Fingerprint of THIS link's raw data
27    pub f: String,
28    /// Fingerprint of the PREVIOUS link (None for genesis)
29    pub prev: Option<String>,
30    /// Unix timestamp when this link was created
31    pub ts: u64,
32}
33
34impl ChainLink {
35    /// Verify this link's data integrity
36    pub fn verify_data(&self) -> Result<String, UniversalError> {
37        use base64::{engine::general_purpose::STANDARD, Engine};
38        let bytes = STANDARD
39            .decode(&self.d)
40            .map_err(|e| UniversalError::DecodeError(e.to_string()))?;
41        let decoded =
42            String::from_utf8(bytes).map_err(|e| UniversalError::DecodeError(e.to_string()))?;
43        let data_fp = fingerprint_str(&decoded);
44        // Non-genesis links have a chained fingerprint
45        let actual_fp = match &self.prev {
46            Some(prev) => fingerprint_str(&format!("{}{}", data_fp, prev)),
47            None => data_fp,
48        };
49        if actual_fp != self.f {
50            return Err(UniversalError::IntegrityViolation {
51                expected: self.f.clone(),
52                actual: actual_fp,
53            });
54        }
55        Ok(decoded)
56    }
57}
58
59/// The result of verifying a [`Chain`].
60#[derive(Debug, Clone, PartialEq)]
61pub struct ChainVerifyResult {
62    /// `true` if every link is intact and properly linked.
63    pub valid: bool,
64    /// Total number of links inspected.
65    pub total_links: usize,
66    /// 1-based index of the first broken link, if any.
67    pub broken_at: Option<usize>,
68    /// Human-readable reason the chain is broken, if any.
69    pub broken_reason: Option<String>,
70}
71
72impl std::fmt::Display for ChainVerifyResult {
73    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74        if self.valid {
75            write!(f, "Valid chain ({} links)", self.total_links)
76        } else {
77            write!(
78                f,
79                "Broken at link {} of {}: {}",
80                self.broken_at.unwrap_or(0),
81                self.total_links,
82                self.broken_reason.as_deref().unwrap_or("unknown")
83            )
84        }
85    }
86}
87
88/// A cryptographic chain of data.
89/// Each link proves it came after the previous one.
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct Chain {
92    pub links: Vec<ChainLink>,
93}
94
95impl Chain {
96    /// Start a new chain with a genesis link
97    #[must_use]
98    pub fn new(data: &str) -> Self {
99        let ts = SystemTime::now()
100            .duration_since(UNIX_EPOCH)
101            .unwrap_or_default()
102            .as_secs();
103
104        let link = ChainLink {
105            seq: 1,
106            d: encode_str(data),
107            f: fingerprint_str(data),
108            prev: None,
109            ts,
110        };
111
112        Self { links: vec![link] }
113    }
114
115    /// Append a new link referencing the previous link's fingerprint
116    pub fn append(&mut self, data: &str) -> &ChainLink {
117        let prev_fp = self.links.last().map(|l| l.f.clone());
118        let seq = self.links.len() as u64 + 1;
119        let ts = SystemTime::now()
120            .duration_since(UNIX_EPOCH)
121            .unwrap_or_default()
122            .as_secs();
123
124        // Chain fingerprint includes previous link's fingerprint
125        // so you can't reorder or insert links without breaking everything after
126        let combined = format!(
127            "{}{}",
128            fingerprint_str(data),
129            prev_fp.as_deref().unwrap_or("")
130        );
131        let chained_fp = fingerprint_str(&combined);
132
133        self.links.push(ChainLink {
134            seq,
135            d: encode_str(data),
136            f: chained_fp,
137            prev: prev_fp,
138            ts,
139        });
140
141        self.links.last().unwrap()
142    }
143
144    /// Verify the entire chain - every link's data AND every link's
145    /// reference to the previous link's fingerprint
146    pub fn verify(&self) -> ChainVerifyResult {
147        if self.links.is_empty() {
148            return ChainVerifyResult {
149                valid: true,
150                total_links: 0,
151                broken_at: None,
152                broken_reason: None,
153            };
154        }
155
156        for (i, link) in self.links.iter().enumerate() {
157            // Verify data integrity
158            if let Err(e) = link.verify_data() {
159                return ChainVerifyResult {
160                    valid: false,
161                    total_links: self.links.len(),
162                    broken_at: Some(i + 1),
163                    broken_reason: Some(format!("Data integrity: {}", e)),
164                };
165            }
166
167            // Verify chain linkage (skip genesis)
168            if i > 0 {
169                let prev_fp = &self.links[i - 1].f;
170                if link.prev.as_deref() != Some(prev_fp.as_str()) {
171                    return ChainVerifyResult {
172                        valid: false,
173                        total_links: self.links.len(),
174                        broken_at: Some(i + 1),
175                        broken_reason: Some(format!(
176                            "Chain broken: link {} doesn't reference link {}",
177                            i + 1,
178                            i
179                        )),
180                    };
181                }
182            }
183        }
184
185        ChainVerifyResult {
186            valid: true,
187            total_links: self.links.len(),
188            broken_at: None,
189            broken_reason: None,
190        }
191    }
192
193    /// Get the length of the chain
194    pub fn len(&self) -> usize {
195        self.links.len()
196    }
197
198    /// Returns `true` if the chain has no links.
199    pub fn is_empty(&self) -> bool {
200        self.links.is_empty()
201    }
202
203    /// Serialize to JSON - safe to store in Redis, Postgres, send anywhere
204    pub fn to_json(&self) -> Result<String, UniversalError> {
205        serde_json::to_string(self).map_err(|e| UniversalError::SerializationError(e.to_string()))
206    }
207
208    /// Deserialize a chain from a JSON string.
209    pub fn from_json(s: &str) -> Result<Self, UniversalError> {
210        serde_json::from_str(s).map_err(|e| UniversalError::SerializationError(e.to_string()))
211    }
212
213    /// Print a chain report
214    pub fn report(&self) -> String {
215        let result = self.verify();
216        let mut out = String::new();
217        out.push_str("━━━━ Entrouter Universal Chain Report ━━━━\n");
218        out.push_str(&format!(
219            "Links: {} | Valid: {}\n\n",
220            self.links.len(),
221            result.valid
222        ));
223        for link in &self.links {
224            let status = if result.broken_at == Some(link.seq as usize) {
225                "❌"
226            } else {
227                "✅"
228            };
229            out.push_str(&format!(
230                "  Link {}: {} | ts: {} | fp: {}...\n",
231                link.seq,
232                status,
233                link.ts,
234                &link.f[..16]
235            ));
236        }
237        if let Some(reason) = &result.broken_reason {
238            out.push_str(&format!("\n  ❌ {}\n", reason));
239        }
240        out.push_str("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
241        out
242    }
243
244    /// Compare two chains and find where they diverge.
245    pub fn diff(a: &Chain, b: &Chain) -> ChainDiff {
246        let min_len = a.links.len().min(b.links.len());
247        let mut common_length = 0;
248
249        for i in 0..min_len {
250            if a.links[i].f == b.links[i].f {
251                common_length += 1;
252            } else {
253                return ChainDiff {
254                    common_length,
255                    a_extra: a.links.len() - common_length,
256                    b_extra: b.links.len() - common_length,
257                    diverges_at: Some(i + 1), // 1-based
258                };
259            }
260        }
261
262        ChainDiff {
263            common_length,
264            a_extra: a.links.len() - common_length,
265            b_extra: b.links.len() - common_length,
266            diverges_at: None, // one is a prefix of the other (or they're identical)
267        }
268    }
269
270    /// Merge two chains. One must be a prefix of the other.
271    /// Returns the longer chain. If they diverge, returns an error.
272    pub fn merge(a: &Chain, b: &Chain) -> Result<Chain, UniversalError> {
273        let diff = Chain::diff(a, b);
274        if let Some(pos) = diff.diverges_at {
275            return Err(UniversalError::ChainMergeConflict { diverges_at: pos });
276        }
277        // One is a prefix -- return the longer one
278        if a.links.len() >= b.links.len() {
279            Ok(a.clone())
280        } else {
281            Ok(b.clone())
282        }
283    }
284}
285
286/// The result of comparing two chains with [`Chain::diff`].
287#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
288pub struct ChainDiff {
289    /// How many links match from the start.
290    pub common_length: usize,
291    /// Links only in chain A (after the common prefix).
292    pub a_extra: usize,
293    /// Links only in chain B (after the common prefix).
294    pub b_extra: usize,
295    /// 1-based index where they diverge. `None` means one is a prefix of the other.
296    pub diverges_at: Option<usize>,
297}