Skip to main content

entrouter_universal/
chain.rs

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