use std::collections::BTreeMap;
use super::Error;
use super::bundle::{ProofBundle, Proven};
use super::checkpoint::SignedCheckpoint;
use super::error::ForkEvidence;
use super::note::KeyName;
use super::policy::Policy;
use super::signing::{Vkey, unambiguous};
use crate::log::{ConsistencyProof, ProofError, TreeHead};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Update {
First,
Unchanged,
Advanced {
from: TreeHead,
},
}
#[derive(Debug, Clone, Default)]
struct Followed {
keys: Vec<Vkey>,
head: Option<TreeHead>,
note: Option<SignedCheckpoint>,
}
#[derive(Debug, Clone, Default)]
pub struct Verifier {
logs: BTreeMap<KeyName, Followed>,
policy: Option<Policy>,
}
impl Verifier {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn from_policy(policy: Policy) -> Self {
let mut verifier = Self::new();
for key in policy.logs() {
verifier.add(key.clone());
}
verifier.policy = Some(policy);
verifier
}
pub fn log(mut self, key: Vkey) -> Result<Self, Error> {
if let Some(followed) = self.logs.get(key.name()) {
unambiguous(followed.keys.iter().chain([&key]))?;
}
self.add(key);
Ok(self)
}
fn add(&mut self, key: Vkey) {
let followed = self.logs.entry(key.name().clone()).or_default();
if !followed.keys.contains(&key) {
followed.keys.push(key);
}
}
#[must_use]
pub fn pin(mut self, origin: KeyName, head: TreeHead) -> Self {
let followed = self.logs.entry(origin).or_default();
followed.head = Some(head);
followed.note = None;
self
}
#[must_use]
pub fn head(&self, origin: &KeyName) -> Option<TreeHead> {
self.logs.get(origin).and_then(|followed| followed.head)
}
pub fn heads(&self) -> impl Iterator<Item = (&KeyName, TreeHead)> {
self.logs
.iter()
.filter_map(|(origin, followed)| Some((origin, followed.head?)))
}
pub fn update(
&mut self,
checkpoint: &SignedCheckpoint,
proof: Option<&ConsistencyProof>,
) -> Result<Update, Error> {
let origin = checkpoint.checkpoint().origin();
let followed = self
.logs
.get(origin)
.filter(|followed| !followed.keys.is_empty())
.ok_or_else(|| Error::UnknownOrigin(origin.clone()))?;
checkpoint.verify(&followed.keys)?;
if let Some(policy) = &self.policy {
policy.verify(checkpoint)?;
}
let offered = checkpoint.checkpoint().head();
let evidence = |trusted: TreeHead| {
Box::new(ForkEvidence {
trusted_head: trusted,
trusted: followed.note.clone(),
offered: checkpoint.clone(),
proof: proof.cloned(),
})
};
let update = match followed.head {
None if proof.is_none() => Update::First,
None => return Err(Error::ProofShape),
Some(trusted) if offered.size == trusted.size => {
if offered.root == trusted.root {
Update::Unchanged
} else {
return Err(Error::Fork(evidence(trusted)));
}
}
Some(trusted) if offered.size < trusted.size => {
return Err(Error::Rollback { trusted, offered });
}
Some(trusted) => match consistency(trusted, offered, proof) {
Consistency::Proven => Update::Advanced { from: trusted },
Consistency::Unproven(error) => return Err(error),
Consistency::Conflict => return Err(Error::Inconsistent(evidence(trusted))),
},
};
if let Some(followed) = self.logs.get_mut(origin)
&& (update != Update::Unchanged || followed.note.is_none())
{
followed.head = Some(offered);
followed.note = Some(checkpoint.clone());
}
Ok(update)
}
pub fn verify<const P: u8>(&self, bundle: &ProofBundle<P>) -> Result<Proven<P>, Error> {
let checkpoint = bundle.checkpoint().checkpoint();
let origin = checkpoint.origin();
let followed = self
.logs
.get(origin)
.ok_or_else(|| Error::UnknownOrigin(origin.clone()))?;
let offered = checkpoint.head();
if followed.head != Some(offered) {
return Err(Error::NotAtTrustedHead {
trusted: followed.head,
offered,
});
}
bundle.verify_inclusion()
}
}
enum Consistency {
Proven,
Unproven(Error),
Conflict,
}
fn consistency(
trusted: TreeHead,
offered: TreeHead,
proof: Option<&ConsistencyProof>,
) -> Consistency {
let Some(proof) = proof else {
return if trusted.size == 0 {
Consistency::Proven
} else {
Consistency::Unproven(Error::ProofShape)
};
};
if (proof.old_size, proof.new_size) != (trusted.size, offered.size) {
return Consistency::Unproven(Error::ProofShape);
}
if trusted.size == 0 {
return if proof.path.is_empty() {
Consistency::Proven
} else {
Consistency::Unproven(Error::ProofShape)
};
}
match proof.verify(&trusted, &offered) {
Ok(()) => Consistency::Proven,
Err(ProofError::RootMismatch { expected, computed })
if expected == trusted.root && computed != trusted.root =>
{
let prefix = TreeHead {
size: trusted.size,
root: computed,
};
if proof.verify(&prefix, &offered).is_ok() {
Consistency::Conflict
} else {
Consistency::Unproven(Error::Proof(ProofError::RootMismatch {
expected,
computed,
}))
}
}
Err(error) => Consistency::Unproven(Error::Proof(error)),
}
}