use std::collections::BTreeSet;
use calimero_primitives::identity::PublicKey;
use calimero_storage::rotation_log::{RotationLog, RotationLogEntry};
#[must_use]
pub fn latest_writers(log: &RotationLog) -> Option<BTreeSet<PublicKey>> {
if let Some(entry) = log.entries.last() {
return Some(entry.new_writers.clone());
}
log.snapshot.as_ref().map(|s| s.writers.clone())
}
#[must_use]
pub fn writers_at<F>(
log: &RotationLog,
causal_parents: &[[u8; 32]],
happens_before: F,
) -> Option<BTreeSet<PublicKey>>
where
F: Fn(&[u8; 32], &[u8; 32]) -> bool,
{
if causal_parents.is_empty() {
return latest_writers(log);
}
let reachable: Vec<&RotationLogEntry> = log
.entries
.iter()
.filter(|e| {
causal_parents
.iter()
.any(|p| e.delta_id == *p || happens_before(&e.delta_id, p))
})
.collect();
let latest = reachable.into_iter().max_by(|a, b| {
if happens_before(&a.delta_id, &b.delta_id) {
return std::cmp::Ordering::Less;
}
if happens_before(&b.delta_id, &a.delta_id) {
return std::cmp::Ordering::Greater;
}
match a.delta_hlc.cmp(&b.delta_hlc) {
std::cmp::Ordering::Equal => {}
non_eq => return non_eq,
}
match (&a.signer, &b.signer) {
(Some(sa), Some(sb)) => sb.digest().cmp(sa.digest()),
(Some(_), None) => std::cmp::Ordering::Greater,
(None, Some(_)) => std::cmp::Ordering::Less,
(None, None) => std::cmp::Ordering::Equal,
}
});
if let Some(entry) = latest {
return Some(entry.new_writers.clone());
}
log.snapshot.as_ref().map(|s| s.writers.clone())
}
#[cfg(test)]
mod tests {
use core::num::NonZeroU128;
use calimero_storage::logical_clock::{HybridTimestamp, Timestamp, ID, NTP64};
use calimero_storage::rotation_log::{RotationLog, RotationLogEntry, RotationSnapshot};
use super::*;
fn pk(b: u8) -> PublicKey {
PublicKey::from([b; 32])
}
fn entry(
delta_id: u8,
hlc_time: u64,
signer: u8,
writers: &[u8],
nonce: u64,
) -> RotationLogEntry {
let id_u128 = NonZeroU128::new(u128::from(signer) + 1).unwrap();
let ts = Timestamp::new(NTP64(hlc_time), ID::from(id_u128));
RotationLogEntry {
delta_id: [delta_id; 32],
delta_hlc: HybridTimestamp::new(ts),
signer: Some(pk(signer)),
new_writers: writers.iter().copied().map(pk).collect(),
writers_nonce: nonce,
}
}
fn log_of(entries: Vec<RotationLogEntry>) -> RotationLog {
RotationLog {
snapshot: None,
entries,
}
}
#[test]
fn empty_log_returns_none() {
let log = RotationLog::empty();
assert_eq!(latest_writers(&log), None);
assert_eq!(writers_at(&log, &[[0; 32]], |_, _| false), None);
}
#[test]
fn latest_writers_returns_last_appended() {
let log = log_of(vec![
entry(1, 100, 0xAA, &[0xAA], 1),
entry(2, 200, 0xBB, &[0xBB], 2),
]);
assert_eq!(
latest_writers(&log),
Some([0xBB].into_iter().map(pk).collect())
);
}
#[test]
fn writers_at_with_empty_parents_falls_back_to_latest() {
let log = log_of(vec![
entry(1, 100, 0xAA, &[0xAA], 1),
entry(2, 200, 0xBB, &[0xBB], 2),
]);
assert_eq!(
writers_at(&log, &[], |_, _| false),
Some([0xBB].into_iter().map(pk).collect())
);
}
#[test]
fn writers_at_returns_only_reachable_entries() {
let log = log_of(vec![
entry(1, 100, 0xAA, &[0xAA, 0xBB], 1),
entry(2, 200, 0xBB, &[0xBB, 0xCC], 2),
]);
let happens_before = |a: &[u8; 32], b: &[u8; 32]| a == &[1; 32] && b == &[2; 32];
let writers = writers_at(&log, &[[1; 32]], happens_before);
assert_eq!(writers, Some([0xAA, 0xBB].into_iter().map(pk).collect()));
let writers = writers_at(&log, &[[2; 32]], happens_before);
assert_eq!(writers, Some([0xBB, 0xCC].into_iter().map(pk).collect()));
}
#[test]
fn writers_at_concurrent_siblings_resolved_by_hlc() {
let log = log_of(vec![
entry(1, 20, 0xAA, &[0xAA], 10),
entry(2, 21, 0xBB, &[0xBB], 10),
]);
let none_precede = |_: &[u8; 32], _: &[u8; 32]| false;
let writers = writers_at(&log, &[[1; 32], [2; 32]], none_precede);
assert_eq!(writers, Some([0xBB].into_iter().map(pk).collect()));
}
#[test]
fn writers_at_hlc_tie_resolved_by_signer_bytes() {
let identical_ts = HybridTimestamp::new(Timestamp::new(
NTP64(50),
ID::from(NonZeroU128::new(1).unwrap()),
));
let mk = |delta_id: u8, signer: u8, writers: &[u8]| RotationLogEntry {
delta_id: [delta_id; 32],
delta_hlc: identical_ts,
signer: Some(pk(signer)),
new_writers: writers.iter().copied().map(pk).collect(),
writers_nonce: 10,
};
let log = log_of(vec![mk(1, 0xAA, &[0xAA, 0xCC]), mk(2, 0xBB, &[0xBB, 0xDD])]);
let none_precede = |_: &[u8; 32], _: &[u8; 32]| false;
let writers = writers_at(&log, &[[1; 32], [2; 32]], none_precede);
assert_eq!(writers, Some([0xAA, 0xCC].into_iter().map(pk).collect()));
}
#[test]
fn writers_at_causal_precedence_overrides_hlc() {
let log = log_of(vec![
entry(1, 999, 0xAA, &[0xAA], 1), entry(2, 100, 0xBB, &[0xBB], 2), ]);
let happens_before = |a: &[u8; 32], b: &[u8; 32]| a == &[1; 32] && b == &[2; 32];
let writers = writers_at(&log, &[[2; 32]], happens_before);
assert_eq!(writers, Some([0xBB].into_iter().map(pk).collect()));
}
#[test]
fn writers_at_unreachable_entries_ignored() {
let log = log_of(vec![entry(1, 100, 0xAA, &[0xAA], 1)]);
let writers = writers_at(&log, &[[42; 32]], |_, _| false);
assert_eq!(writers, None);
}
#[test]
fn writers_at_falls_back_to_snapshot_when_entries_unreachable() {
let snap_writers: BTreeSet<PublicKey> = [0xEE].into_iter().map(pk).collect();
let log = RotationLog {
snapshot: Some(RotationSnapshot {
writers: snap_writers.clone(),
cutoff_index: 5,
}),
entries: vec![entry(99, 100, 0xFF, &[0xFF], 99)],
};
let writers = writers_at(&log, &[[1; 32]], |_, _| false);
assert_eq!(writers, Some(snap_writers));
}
}