use std::collections::BTreeMap;
use std::fmt::Debug;
use std::sync::Mutex;
use async_trait::async_trait;
use crate::core::{CheckpointSigner, Digest, KeyId, SignError, merkle};
use super::Checkpoint;
#[derive(Debug, thiserror::Error)]
pub enum WitnessError {
#[error("log '{origin}' shrank from {seen} to {offered} — runs were removed")]
Shrank {
origin: String,
seen: u64,
offered: u64,
},
#[error(
"log '{origin}' at size {offered} does not extend the checkpoint this \
witness cosigned at size {seen} — the history was rewritten or forked"
)]
Forked {
origin: String,
seen: u64,
offered: u64,
},
#[error(
"log '{origin}': the witness is at size {witness_size}; build a consistency proof \
from there and resubmit"
)]
Stale { origin: String, witness_size: u64 },
#[error("log '{origin}': a consistency proof is required to extend size {seen}")]
ProofMissing { origin: String, seen: u64 },
#[error("witness: {0}")]
Unavailable(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Cosignature {
pub key_id: KeyId,
pub signature: Vec<u8>,
}
#[async_trait]
pub trait Witness: Send + Sync + Debug {
async fn cosign(
&self,
checkpoint: &Checkpoint,
old_size: u64,
proof: &[Digest],
) -> Result<Cosignature, WitnessError>;
}
#[derive(Debug)]
pub struct MemoryWitness {
signer: std::sync::Arc<dyn CheckpointSigner>,
seen: Mutex<BTreeMap<String, (u64, Digest)>>,
}
impl MemoryWitness {
#[must_use]
pub fn new(signer: std::sync::Arc<dyn CheckpointSigner>) -> Self {
Self {
signer,
seen: Mutex::new(BTreeMap::new()),
}
}
#[must_use]
pub fn last_seen(&self, origin: &str) -> Option<(u64, Digest)> {
self.seen
.lock()
.expect("witness mutex")
.get(origin)
.copied()
}
}
#[async_trait]
impl Witness for MemoryWitness {
async fn cosign(
&self,
checkpoint: &Checkpoint,
old_size: u64,
proof: &[Digest],
) -> Result<Cosignature, WitnessError> {
{
let mut seen = self
.seen
.lock()
.map_err(|_| WitnessError::Unavailable("witness mutex poisoned".into()))?;
let remembered_size = seen.get(&checkpoint.origin).map_or(0, |(size, _)| *size);
if old_size != remembered_size {
return Err(WitnessError::Stale {
origin: checkpoint.origin.clone(),
witness_size: remembered_size,
});
}
if let Some((remembered, old_root)) = seen.get(&checkpoint.origin).copied() {
if old_size != remembered {
return Err(WitnessError::Stale {
origin: checkpoint.origin.clone(),
witness_size: remembered,
});
}
let old_size = remembered;
if checkpoint.size < old_size {
return Err(WitnessError::Shrank {
origin: checkpoint.origin.clone(),
seen: old_size,
offered: checkpoint.size,
});
}
let unchanged = checkpoint.size == old_size && checkpoint.root == old_root;
if !unchanged {
if proof.is_empty() && old_size > 0 && checkpoint.size > old_size {
return Err(WitnessError::ProofMissing {
origin: checkpoint.origin.clone(),
seen: old_size,
});
}
let old = usize::try_from(old_size).unwrap_or(usize::MAX);
let new = usize::try_from(checkpoint.size).unwrap_or(usize::MAX);
if !merkle::verify_consistency(old, &old_root, new, &checkpoint.root, proof) {
return Err(WitnessError::Forked {
origin: checkpoint.origin.clone(),
seen: old_size,
offered: checkpoint.size,
});
}
}
}
seen.insert(
checkpoint.origin.clone(),
(checkpoint.size, checkpoint.root),
);
}
let note = checkpoint.to_note();
let signature = self
.signer
.sign(&Digest::of(note.as_bytes()))
.await
.map_err(|e| match e {
SignError::Unavailable(d) => WitnessError::Unavailable(d),
SignError::Refused { key_id, detail } => {
WitnessError::Unavailable(format!("key '{key_id}' refused: {detail}"))
}
})?;
Ok(Cosignature {
key_id: self.signer.key_id(),
signature,
})
}
}