use std::collections::{HashMap, VecDeque};
use std::sync::Arc;
use std::sync::mpsc::Receiver;
use std::time::{Duration, Instant};
use lean_ctx::core::archive::register::{self, Register as Store};
use lean_ctx::core::context_admission::receipt_store::keyed;
use super::{Entry, Lineage, MAX_AGE, Parts, Register, SealedContext, SealedProducer};
use crate::admitted::{Authority, StoredAuthority};
#[allow(clippy::cast_possible_wrap)]
const MAX_AGE_SECONDS: i64 = MAX_AGE.as_secs() as i64;
const PRODUCERS: Store = Store {
name: "sealed-origin",
shard_entries: 64,
max_age_seconds: MAX_AGE_SECONDS,
};
const PARTS: Store = Store {
name: "sealed-parts",
shard_entries: 128,
max_age_seconds: MAX_AGE_SECONDS,
};
const MAX_PRODUCERS_PER_ITEM: usize = 8;
#[cfg_attr(test, allow(dead_code))]
const MAX_QUEUED: usize = 4096;
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct StoredProducer {
recorded_at: i64,
context: SealedContext,
authority: StoredAuthority,
lineage: Option<Lineage>,
}
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct Signed {
body: String,
mac: String,
}
impl Signed {
fn seal<V: serde::Serialize>(key: &str, value: &V) -> Option<Self> {
let body = serde_json::to_string(value).ok()?;
let mac = keyed::sealed_record_ref(key, &body).ok()?;
Some(Self { body, mac })
}
fn open<V: serde::de::DeserializeOwned>(&self, key: &str) -> Option<V> {
let expected = keyed::sealed_record_ref(key, &self.body).ok()?;
if !constant_time_eq(expected.as_bytes(), self.mac.as_bytes()) {
return None;
}
serde_json::from_str(&self.body).ok()
}
}
fn constant_time_eq(left: &[u8], right: &[u8]) -> bool {
left.len() == right.len()
&& left
.iter()
.zip(right)
.fold(0u8, |diff, (a, b)| diff | (a ^ b))
== 0
}
enum Job {
Producer(String, Signed),
Part(String, Signed),
}
pub(super) fn keep_producer(digest: &[u8; 32], producer: &SealedProducer) {
let Some(authority) = producer.authority.stored() else {
return;
};
let key = hex(digest);
let stored = StoredProducer {
recorded_at: register::now(),
context: producer.context.clone(),
authority,
lineage: producer.lineage.as_deref().cloned(),
};
if let Some(signed) = Signed::seal(&key, &stored) {
send(Job::Producer(key, signed));
}
}
pub(super) fn keep_part(key: &[u8; 32], lineage: &Arc<Lineage>) {
let key = hex(key);
if let Some(signed) = Signed::seal(&key, &**lineage) {
send(Job::Part(key, signed));
}
}
pub(super) fn read_register() -> Vec<Entry> {
let now = register::now();
let mut lineages = Interned::default();
let mut loaded = Vec::new();
for (key, _, producers) in PRODUCERS.read_all::<Vec<Signed>>(now) {
let Some(digest) = unhex(&key) else {
continue;
};
for signed in producers {
let Some(stored) = signed.open::<StoredProducer>(&key) else {
continue;
};
let Some(recorded) = recorded_at(stored.recorded_at, now) else {
continue;
};
loaded.push(Entry {
digest,
producer: SealedProducer {
context: stored.context,
authority: Arc::new(Authority::restore(stored.authority)),
lineage: stored.lineage.map(|lineage| lineages.get(lineage)),
},
recorded,
});
}
}
loaded
}
pub(super) fn merge_register(register: &mut Register, mut loaded: Vec<Entry>) {
loaded.extend(std::mem::take(&mut register.entries));
loaded.sort_by_key(|entry| entry.recorded);
let mut entries = VecDeque::from(loaded);
while entries.len() > register.max_entries {
entries.pop_front();
}
register.entries = entries;
}
pub(super) fn read_parts() -> Vec<([u8; 32], Instant, Arc<Lineage>)> {
let now = register::now();
let mut lineages = Interned::default();
let mut loaded: Vec<_> = PARTS
.read_all::<Signed>(now)
.into_iter()
.filter_map(|(key, at, signed)| {
let lineage = signed.open::<Lineage>(&key)?;
Some((unhex(&key)?, recorded_at(at, now)?, lineages.get(lineage)))
})
.collect();
loaded.sort_by_key(|(_, at, _)| *at);
loaded
}
pub(super) fn merge_parts(parts: &mut Parts, loaded: Vec<([u8; 32], Instant, Arc<Lineage>)>) {
for (key, at, lineage) in loaded {
if !parts.known.contains_key(&key) {
parts.insert(key, at, lineage);
}
}
}
#[derive(Default)]
struct Interned(HashMap<[u8; 32], Arc<Lineage>>);
impl Interned {
fn get(&mut self, lineage: Lineage) -> Arc<Lineage> {
let Ok(encoded) = serde_json::to_vec(&lineage) else {
return Arc::new(lineage);
};
Arc::clone(
self.0
.entry(*blake3::hash(&encoded).as_bytes())
.or_insert_with(|| Arc::new(lineage)),
)
}
}
fn recorded_at(at: i64, now: i64) -> Option<Instant> {
let age = u64::try_from(now.checked_sub(at)?).ok()?;
let age = Duration::from_secs(age);
if age >= MAX_AGE {
return None;
}
Instant::now().checked_sub(age)
}
fn hex(bytes: &[u8; 32]) -> String {
blake3::Hash::from_bytes(*bytes).to_hex().to_string()
}
fn unhex(key: &str) -> Option<[u8; 32]> {
blake3::Hash::from_hex(key)
.ok()
.map(|hash| *hash.as_bytes())
}
fn merge_producers(key: &str, current: Vec<Signed>, new: Vec<Signed>) -> Vec<Signed> {
let mut kept: Vec<(StoredProducer, Signed)> = current
.into_iter()
.chain(new)
.filter_map(|signed| Some((signed.open::<StoredProducer>(key)?, signed)))
.collect();
let mut seen: Vec<SealedContext> = Vec::new();
kept.reverse();
kept.retain(|(producer, _)| {
let first = !seen.contains(&producer.context);
if first {
seen.push(producer.context.clone());
}
first
});
kept.sort_by_key(|(producer, _)| producer.recorded_at);
let excess = kept.len().saturating_sub(MAX_PRODUCERS_PER_ITEM);
kept.drain(..excess);
kept.into_iter().map(|(_, signed)| signed).collect()
}
#[allow(clippy::needless_pass_by_value)]
fn merge_lineage(key: &str, current: Signed, new: Signed) -> Signed {
let (Some(mut joined), Some(added)) = (current.open::<Lineage>(key), new.open::<Lineage>(key))
else {
return new;
};
joined.add(&added);
Signed::seal(key, &joined).unwrap_or(new)
}
fn write(jobs: Vec<Job>) {
let mut producers = Vec::new();
let mut parts = Vec::new();
for job in jobs {
match job {
Job::Producer(key, producer) => producers.push((key, vec![producer])),
Job::Part(key, lineage) => parts.push((key, lineage)),
}
}
let now = register::now();
if !producers.is_empty() {
PRODUCERS.update_many(now, producers, merge_producers);
}
if !parts.is_empty() {
PARTS.update_many(now, parts, merge_lineage);
}
}
#[cfg(test)]
fn send(job: Job) {
write(vec![job]);
}
#[cfg(not(test))]
fn send(job: Job) {
use std::sync::mpsc::{self, SyncSender};
use std::sync::{LazyLock, Mutex, PoisonError};
static WRITER: LazyLock<Mutex<Option<SyncSender<Job>>>> = LazyLock::new(|| {
let (sender, receiver) = mpsc::sync_channel(MAX_QUEUED);
let spawned = std::thread::Builder::new()
.name("lean-ctx-sealed-store".into())
.spawn(move || write_loop(&receiver));
Mutex::new(spawned.ok().map(|_| sender))
});
if let Some(sender) = WRITER
.lock()
.unwrap_or_else(PoisonError::into_inner)
.as_ref()
{
let _ = sender.try_send(job);
}
}
#[cfg_attr(test, allow(dead_code))]
fn write_loop(receiver: &Receiver<Job>) {
while let Ok(first) = receiver.recv() {
let mut jobs = vec![first];
jobs.extend(receiver.try_iter());
write(jobs);
}
}