use serde::{Deserialize, Serialize};
use crate::context::{Context, ContextKey, Fact};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)]
pub struct LamportClock {
time: u64,
}
impl LamportClock {
#[must_use]
pub const fn new() -> Self {
Self { time: 0 }
}
#[must_use]
pub const fn time(&self) -> u64 {
self.time
}
pub fn tick(&mut self) -> u64 {
self.time += 1;
self.time
}
pub fn update(&mut self, received: u64) -> u64 {
self.time = self.time.max(received) + 1;
self.time
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ContentHashError {
InvalidHex,
InvalidLength,
}
impl std::fmt::Display for ContentHashError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidHex => write!(f, "invalid hexadecimal character"),
Self::InvalidLength => write!(f, "invalid string length (expected 64 hex chars)"),
}
}
}
impl std::error::Error for ContentHashError {}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ContentHash(pub [u8; 32]);
impl ContentHash {
#[deprecated(
since = "2.0.0",
note = "Use converge-runtime with Fingerprint trait for cryptographic hashing"
)]
#[must_use]
pub fn compute(content: &str) -> Self {
let mut hash = 0xcbf2_9ce4_8422_2325_u64; for byte in content.as_bytes() {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(0x0100_0000_01b3); }
let bytes = hash.to_le_bytes();
let mut result = [0u8; 32];
for i in 0..4 {
result[i * 8..(i + 1) * 8].copy_from_slice(&bytes);
}
Self(result)
}
#[deprecated(
since = "2.0.0",
note = "Use converge-runtime with Fingerprint trait for cryptographic hashing"
)]
#[must_use]
pub fn compute_fact(fact: &Fact) -> Self {
let combined = format!("{:?}|{}|{}", fact.key, fact.id, fact.content);
#[allow(deprecated)]
Self::compute(&combined)
}
#[deprecated(
since = "2.0.0",
note = "Use converge-runtime with Fingerprint trait for cryptographic hashing"
)]
#[must_use]
pub fn combine(left: &Self, right: &Self) -> Self {
let mut hash = 0xcbf2_9ce4_8422_2325_u64;
for byte in left.0.iter().chain(right.0.iter()) {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(0x0100_0000_01b3);
}
let bytes = hash.to_le_bytes();
let mut result = [0u8; 32];
for i in 0..4 {
result[i * 8..(i + 1) * 8].copy_from_slice(&bytes);
}
Self(result)
}
#[must_use]
pub fn verify(&self, content: &str) -> bool {
#[allow(deprecated)]
let computed = Self::compute(content);
*self == computed
}
#[must_use]
pub fn to_hex(&self) -> String {
self.0.iter().map(|b| format!("{b:02x}")).collect()
}
pub fn from_hex(s: &str) -> Result<Self, ContentHashError> {
if s.len() != 64 {
return Err(ContentHashError::InvalidLength);
}
let mut result = [0u8; 32];
for (i, chunk) in s.as_bytes().chunks(2).enumerate() {
let high = Self::hex_char_to_nibble(chunk[0])?;
let low = Self::hex_char_to_nibble(chunk[1])?;
result[i] = (high << 4) | low;
}
Ok(Self(result))
}
fn hex_char_to_nibble(c: u8) -> Result<u8, ContentHashError> {
match c {
b'0'..=b'9' => Ok(c - b'0'),
b'a'..=b'f' => Ok(c - b'a' + 10),
b'A'..=b'F' => Ok(c - b'A' + 10),
_ => Err(ContentHashError::InvalidHex),
}
}
}
impl std::fmt::Display for ContentHash {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", &self.to_hex()[..16]) }
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MerkleRoot(pub ContentHash);
impl MerkleRoot {
#[allow(deprecated)]
#[must_use]
pub fn compute(hashes: &[ContentHash]) -> Self {
if hashes.is_empty() {
return Self(ContentHash::compute(""));
}
let mut current_level: Vec<ContentHash> = hashes.to_vec();
loop {
if current_level.len() == 1 {
if hashes.len() == 1 {
return Self(ContentHash::combine(¤t_level[0], ¤t_level[0]));
}
return Self(current_level.into_iter().next().unwrap());
}
let mut next_level = Vec::new();
for chunk in current_level.chunks(2) {
let combined = if chunk.len() == 2 {
ContentHash::combine(&chunk[0], &chunk[1])
} else {
ContentHash::combine(&chunk[0], &chunk[0])
};
next_level.push(combined);
}
current_level = next_level;
}
}
#[allow(deprecated)]
#[must_use]
pub fn from_context(ctx: &Context) -> Self {
let mut all_hashes: Vec<ContentHash> = Vec::new();
let mut keys: Vec<_> = ctx.all_keys();
keys.sort();
for key in keys {
for fact in ctx.get(key) {
all_hashes.push(ContentHash::compute_fact(fact));
}
}
Self::compute(&all_hashes)
}
#[must_use]
pub fn to_hex(&self) -> String {
self.0.to_hex()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrackedContext {
pub context: Context,
pub clock: LamportClock,
merkle_root: Option<MerkleRoot>,
fact_hashes: Vec<(ContextKey, String, ContentHash)>,
}
impl TrackedContext {
#[must_use]
pub fn new(context: Context) -> Self {
let mut tracked = Self {
context,
clock: LamportClock::new(),
merkle_root: None,
fact_hashes: Vec::new(),
};
tracked.recompute_hashes();
tracked
}
#[must_use]
pub fn empty() -> Self {
Self::new(Context::new())
}
#[must_use]
pub fn clock_time(&self) -> u64 {
self.clock.time()
}
pub fn tick(&mut self) -> u64 {
self.clock.tick()
}
#[must_use]
pub fn merkle_root(&mut self) -> &MerkleRoot {
if self.merkle_root.is_none() {
self.merkle_root = Some(MerkleRoot::from_context(&self.context));
}
self.merkle_root.as_ref().unwrap()
}
#[allow(deprecated)]
#[must_use]
pub fn verify_integrity(&self) -> bool {
for (key, id, expected_hash) in &self.fact_hashes {
if let Some(fact) = self.context.get(*key).iter().find(|f| &f.id == id) {
if ContentHash::compute_fact(fact) != *expected_hash {
return false;
}
} else {
return false; }
}
true
}
#[allow(deprecated)]
fn recompute_hashes(&mut self) {
self.fact_hashes.clear();
for key in self.context.all_keys() {
for fact in self.context.get(key) {
let hash = ContentHash::compute_fact(fact);
self.fact_hashes.push((key, fact.id.clone(), hash));
}
}
self.merkle_root = None; }
#[allow(deprecated)]
pub fn add_fact(&mut self, fact: Fact) -> Result<bool, crate::error::ConvergeError> {
let key = fact.key;
let id = fact.id.clone();
let hash = ContentHash::compute_fact(&fact);
let changed = self.context.add_fact(fact)?;
if changed {
self.fact_hashes.push((key, id, hash));
self.merkle_root = None; self.clock.tick();
}
Ok(changed)
}
}
impl Default for TrackedContext {
fn default() -> Self {
Self::empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lamport_clock_starts_at_zero() {
let clock = LamportClock::new();
assert_eq!(clock.time(), 0);
}
#[test]
fn lamport_clock_tick_increments() {
let mut clock = LamportClock::new();
assert_eq!(clock.tick(), 1);
assert_eq!(clock.tick(), 2);
assert_eq!(clock.tick(), 3);
}
#[test]
fn lamport_clock_update_takes_max_plus_one() {
let mut clock = LamportClock::new();
clock.tick(); clock.tick();
assert_eq!(clock.update(5), 6);
assert_eq!(clock.update(3), 7);
}
#[test]
#[allow(deprecated)]
fn content_hash_is_deterministic() {
let h1 = ContentHash::compute("hello world");
let h2 = ContentHash::compute("hello world");
assert_eq!(h1, h2);
}
#[test]
#[allow(deprecated)]
fn content_hash_changes_with_content() {
let h1 = ContentHash::compute("hello");
let h2 = ContentHash::compute("world");
assert_ne!(h1, h2);
}
#[test]
#[allow(deprecated)]
fn content_hash_verify_works() {
let hash = ContentHash::compute("test");
assert!(hash.verify("test"));
assert!(!hash.verify("modified"));
}
#[test]
#[allow(deprecated)]
fn content_hash_hex_roundtrip() {
let original = ContentHash::compute("test content");
let hex = original.to_hex();
let restored = ContentHash::from_hex(&hex).unwrap();
assert_eq!(original, restored);
}
#[test]
#[allow(deprecated)]
fn merkle_root_empty_list() {
let root = MerkleRoot::compute(&[]);
let empty_hash = ContentHash::compute("");
assert_eq!(root.0, empty_hash);
}
#[test]
#[allow(deprecated)]
fn merkle_root_single_element() {
let h = ContentHash::compute("only element");
let root = MerkleRoot::compute(std::slice::from_ref(&h));
let expected = ContentHash::combine(&h, &h);
assert_eq!(root.0, expected);
}
#[test]
#[allow(deprecated)]
fn merkle_root_two_elements() {
let h1 = ContentHash::compute("first");
let h2 = ContentHash::compute("second");
let root = MerkleRoot::compute(&[h1.clone(), h2.clone()]);
let expected = ContentHash::combine(&h1, &h2);
assert_eq!(root.0, expected);
}
#[test]
#[allow(deprecated)]
fn merkle_root_is_deterministic() {
let hashes = vec![
ContentHash::compute("a"),
ContentHash::compute("b"),
ContentHash::compute("c"),
];
let root1 = MerkleRoot::compute(&hashes);
let root2 = MerkleRoot::compute(&hashes);
assert_eq!(root1, root2);
}
#[test]
#[allow(deprecated)]
fn merkle_root_changes_with_different_content() {
let hashes1 = vec![ContentHash::compute("a"), ContentHash::compute("b")];
let hashes2 = vec![ContentHash::compute("a"), ContentHash::compute("c")];
let root1 = MerkleRoot::compute(&hashes1);
let root2 = MerkleRoot::compute(&hashes2);
assert_ne!(root1, root2);
}
#[test]
fn tracked_context_starts_empty() {
let tracked = TrackedContext::empty();
assert_eq!(tracked.clock_time(), 0);
assert!(!tracked.context.has(ContextKey::Seeds));
}
#[test]
fn tracked_context_ticks_on_add() {
let mut tracked = TrackedContext::empty();
assert_eq!(tracked.clock_time(), 0);
tracked
.add_fact(Fact::new(ContextKey::Seeds, "s1", "seed"))
.unwrap();
assert_eq!(tracked.clock_time(), 1);
tracked
.add_fact(Fact::new(ContextKey::Seeds, "s2", "seed2"))
.unwrap();
assert_eq!(tracked.clock_time(), 2);
}
#[test]
fn tracked_context_computes_merkle_root() {
let mut tracked = TrackedContext::empty();
tracked
.add_fact(Fact::new(ContextKey::Seeds, "s1", "first"))
.unwrap();
tracked
.add_fact(Fact::new(ContextKey::Seeds, "s2", "second"))
.unwrap();
let root1 = tracked.merkle_root().clone();
tracked
.add_fact(Fact::new(ContextKey::Seeds, "s3", "third"))
.unwrap();
let root2 = tracked.merkle_root().clone();
assert_ne!(root1, root2);
}
#[test]
fn tracked_context_verifies_integrity() {
let mut tracked = TrackedContext::empty();
tracked
.add_fact(Fact::new(ContextKey::Seeds, "s1", "test"))
.unwrap();
assert!(tracked.verify_integrity());
}
}