use std::collections::BTreeMap;
use crate::hash::Hash;
use crate::merkle::{ConsistencyProof, TreeHead};
mod base64;
mod note;
#[cfg(feature = "witness")]
#[cfg_attr(docsrs, doc(cfg(feature = "witness")))]
pub mod signing;
pub use note::{KeyName, NoteError, NoteSignature, Origin, SignedTreeHead};
#[cfg(feature = "witness")]
pub use signing::Cosigner;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum WitnessError {
#[error("this witness does not track {origin}")]
Untracked {
origin: Origin,
},
#[error(
"{origin} offered a second history at size {size}: this witness has \
already signed {known}, and was shown {offered}"
)]
Fork {
origin: Origin,
size: u64,
known: Hash,
offered: Hash,
},
#[error("{origin} offered {offered} entries, behind the {known} this witness holds")]
Shrunk {
origin: Origin,
known: u64,
offered: u64,
},
#[error("{origin} offered {to} entries with no consistency proof from {from}")]
MissingProof {
origin: Origin,
from: u64,
to: u64,
},
#[error("{origin} offered {to} entries that do not extend the {from} this witness holds")]
BadProof {
origin: Origin,
from: u64,
to: u64,
},
#[error("this witness already tracks {origin} at {size} entries")]
AlreadyTracked {
origin: Origin,
size: u64,
},
#[error("note names {found}, not {expected}")]
OriginMismatch {
expected: Origin,
found: Origin,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Accepted {
pub head: TreeHead,
pub advanced: bool,
}
pub trait WitnessStore {
fn head(&self, origin: &Origin) -> Option<TreeHead>;
fn store(&mut self, origin: &Origin, head: TreeHead);
fn tracked(&self) -> Vec<(Origin, TreeHead)>;
}
#[derive(Debug, Clone, Default)]
pub struct MemoryWitnessStore {
heads: BTreeMap<Origin, TreeHead>,
}
impl MemoryWitnessStore {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn from_heads(heads: impl IntoIterator<Item = (Origin, TreeHead)>) -> Self {
Self {
heads: heads.into_iter().collect(),
}
}
}
impl WitnessStore for MemoryWitnessStore {
fn head(&self, origin: &Origin) -> Option<TreeHead> {
self.heads.get(origin).copied()
}
fn store(&mut self, origin: &Origin, head: TreeHead) {
self.heads.insert(origin.clone(), head);
}
fn tracked(&self) -> Vec<(Origin, TreeHead)> {
self.heads.iter().map(|(o, h)| (o.clone(), *h)).collect()
}
}
#[derive(Debug, Clone)]
pub struct Witness<S> {
store: S,
}
impl<S: WitnessStore> Witness<S> {
#[must_use]
pub fn new(store: S) -> Self {
Self { store }
}
pub fn trust(&mut self, origin: Origin, head: TreeHead) -> Result<(), WitnessError> {
if let Some(existing) = self.store.head(&origin) {
return Err(WitnessError::AlreadyTracked {
origin,
size: existing.size,
});
}
self.store.store(&origin, head);
Ok(())
}
#[must_use]
pub fn head(&self, origin: &Origin) -> Option<TreeHead> {
self.store.head(origin)
}
#[must_use]
pub fn tracked(&self) -> Vec<(Origin, TreeHead)> {
self.store.tracked()
}
#[must_use]
pub fn store(&self) -> &S {
&self.store
}
pub fn offer(
&mut self,
origin: &Origin,
head: TreeHead,
proof: Option<&ConsistencyProof>,
) -> Result<Accepted, WitnessError> {
let known = self
.store
.head(origin)
.ok_or_else(|| WitnessError::Untracked {
origin: origin.clone(),
})?;
if head.size == known.size {
if head.root != known.root {
return Err(WitnessError::Fork {
origin: origin.clone(),
size: head.size,
known: known.root,
offered: head.root,
});
}
return Ok(Accepted {
head: known,
advanced: false,
});
}
if head.size < known.size {
return Err(WitnessError::Shrunk {
origin: origin.clone(),
known: known.size,
offered: head.size,
});
}
if known.size == 0 {
self.store.store(origin, head);
return Ok(Accepted {
head,
advanced: true,
});
}
let proof = proof.ok_or_else(|| WitnessError::MissingProof {
origin: origin.clone(),
from: known.size,
to: head.size,
})?;
if !proof.verify(&known, &head) {
return Err(WitnessError::BadProof {
origin: origin.clone(),
from: known.size,
to: head.size,
});
}
self.store.store(origin, head);
Ok(Accepted {
head,
advanced: true,
})
}
pub fn offer_note(
&mut self,
origin: &Origin,
note: &SignedTreeHead,
proof: Option<&ConsistencyProof>,
) -> Result<Accepted, WitnessError> {
if note.origin != *origin {
return Err(WitnessError::OriginMismatch {
expected: origin.clone(),
found: note.origin.clone(),
});
}
self.offer(origin, note.head, proof)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AddCheckpoint {
note: SignedTreeHead,
proof: ConsistencyProof,
}
impl AddCheckpoint {
#[must_use]
pub fn new(note: SignedTreeHead, proof: ConsistencyProof) -> Self {
Self { note, proof }
}
#[must_use]
pub fn note(&self) -> &SignedTreeHead {
&self.note
}
#[must_use]
pub fn proof(&self) -> &ConsistencyProof {
&self.proof
}
#[must_use]
pub fn to_body(&self) -> Vec<u8> {
let mut out = self.note.to_note();
for hash in &self.proof.path {
out.push_str(&base64::encode(hash.as_bytes()));
out.push('\n');
}
out.into_bytes()
}
pub fn parse_body(body: &[u8], old_size: u64) -> Result<Self, NoteError> {
let text = core::str::from_utf8(body).map_err(|_| NoteError::MalformedRoot)?;
let (note_text, rest) = split_after_signatures(text);
let note = SignedTreeHead::parse(note_text)?;
let mut path = Vec::new();
for line in rest.split('\n').filter(|l| !l.is_empty()) {
let bytes = base64::decode(line).ok_or(NoteError::MalformedRoot)?;
let array: [u8; Hash::LEN] = bytes
.as_slice()
.try_into()
.map_err(|_| NoteError::MalformedRoot)?;
path.push(Hash::from_bytes(array));
}
Ok(Self {
proof: ConsistencyProof {
old_size,
new_size: note.head.size,
path,
},
note,
})
}
}
fn split_after_signatures(text: &str) -> (&str, &str) {
let Some(separator) = text.find("\n\n") else {
return (text, "");
};
let mut at = separator.saturating_add(2);
for line in text.get(at..).unwrap_or_default().split_inclusive('\n') {
if !line.starts_with('\u{2014}') {
break;
}
at = at.saturating_add(line.len());
}
(
text.get(..at).unwrap_or(text),
text.get(at..).unwrap_or_default(),
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::merkle::MerkleLog;
fn origin() -> Origin {
Origin::new("example.com/ledgers/acme").expect("valid")
}
fn log_of(n: u64) -> MerkleLog {
let mut log = MerkleLog::new();
for i in 0..n {
let mut bytes = [0u8; 32];
bytes
.get_mut(..8)
.expect("32 > 8")
.copy_from_slice(&i.to_le_bytes());
log.append(Hash::from_bytes(bytes));
}
log
}
fn witness_at(log: &MerkleLog, size: u64) -> Witness<MemoryWitnessStore> {
let mut witness = Witness::new(MemoryWitnessStore::new());
witness
.trust(origin(), log.head_at(size).expect("in range"))
.expect("first time");
witness
}
#[test]
fn an_untracked_log_is_refused_rather_than_adopted() {
let log = log_of(8);
let mut witness = Witness::new(MemoryWitnessStore::new());
assert!(matches!(
witness.offer(&origin(), log.head(), None),
Err(WitnessError::Untracked { .. })
));
}
#[test]
fn a_growing_log_is_accepted_when_it_proves_it_extends() {
let log = log_of(8);
let mut witness = witness_at(&log, 3);
let proof = log.consistency_proof_between(3, 8).expect("in range");
let accepted = witness
.offer(&origin(), log.head(), Some(&proof))
.expect("extends");
assert!(accepted.advanced);
assert_eq!(witness.head(&origin()), Some(log.head()));
}
#[test]
fn growth_without_a_proof_is_refused() {
let log = log_of(8);
let mut witness = witness_at(&log, 3);
assert!(matches!(
witness.offer(&origin(), log.head(), None),
Err(WitnessError::MissingProof { from: 3, to: 8, .. })
));
}
#[test]
fn a_second_history_at_the_same_size_is_refused() {
let log = log_of(8);
let mut witness = witness_at(&log, 8);
let mut forked = log_of(7);
forked.append(Hash::from_bytes([0xff; 32]));
assert_eq!(forked.len(), log.len());
assert_ne!(forked.root(), log.root());
let refusal = witness.offer(&origin(), forked.head(), None);
assert!(
matches!(refusal, Err(WitnessError::Fork { size: 8, .. })),
"got {refusal:?}"
);
assert_eq!(witness.head(&origin()), Some(log.head()));
}
#[test]
fn a_fork_that_has_moved_on_is_caught_by_the_proof() {
let log = log_of(8);
let mut witness = witness_at(&log, 8);
let mut forked = log_of(7);
forked.append(Hash::from_bytes([0xff; 32]));
forked.append(Hash::from_bytes([0xfe; 32]));
let proof = forked.consistency_proof_between(8, 9).expect("in range");
assert!(matches!(
witness.offer(&origin(), forked.head(), Some(&proof)),
Err(WitnessError::BadProof { from: 8, to: 9, .. })
));
}
#[test]
fn re_offering_the_head_already_held_is_not_an_error() {
let log = log_of(8);
let mut witness = witness_at(&log, 8);
let accepted = witness.offer(&origin(), log.head(), None).expect("same");
assert!(
!accepted.advanced,
"an at-least-once caller resends; refusing would make the witness unusable"
);
}
#[test]
fn a_shrinking_log_is_refused() {
let log = log_of(8);
let mut witness = witness_at(&log, 8);
assert!(matches!(
witness.offer(&origin(), log.head_at(4).expect("in range"), None),
Err(WitnessError::Shrunk {
known: 8,
offered: 4,
..
})
));
}
#[test]
fn a_witness_cannot_be_re_pointed_by_calling_trust_again() {
let log = log_of(8);
let mut witness = witness_at(&log, 8);
let mut forked = log_of(7);
forked.append(Hash::from_bytes([0xff; 32]));
assert!(
matches!(
witness.trust(origin(), forked.head()),
Err(WitnessError::AlreadyTracked { size: 8, .. })
),
"re-pointing is how an operator would launder a fork"
);
}
#[test]
fn from_an_empty_head_the_first_growth_is_taken_on_trust() {
let log = log_of(8);
let mut witness = Witness::new(MemoryWitnessStore::new());
witness
.trust(origin(), log.head_at(0).expect("in range"))
.expect("first time");
let accepted = witness.offer(&origin(), log.head(), None).expect("accepts");
assert!(accepted.advanced);
let mut forked = log_of(7);
forked.append(Hash::from_bytes([0xff; 32]));
assert!(matches!(
witness.offer(&origin(), forked.head(), None),
Err(WitnessError::Fork { .. })
));
}
#[test]
fn a_note_naming_another_log_is_refused() {
let log = log_of(8);
let mut witness = witness_at(&log, 8);
let other = Origin::new("example.com/ledgers/other").expect("valid");
let note = SignedTreeHead::new(other, log.head());
assert!(matches!(
witness.offer_note(&origin(), ¬e, None),
Err(WitnessError::OriginMismatch { .. })
));
}
#[test]
fn a_request_body_round_trips() {
let log = log_of(8);
let proof = log.consistency_proof_between(3, 8).expect("in range");
let request = AddCheckpoint::new(SignedTreeHead::new(origin(), log.head()), proof.clone());
let body = request.to_body();
let parsed = AddCheckpoint::parse_body(&body, 3).expect("parses");
assert_eq!(parsed.note(), request.note());
assert_eq!(parsed.proof(), &proof);
}
#[test]
fn a_request_body_with_signatures_round_trips() {
let log = log_of(8);
let proof = log.consistency_proof_between(3, 8).expect("in range");
let mut note = SignedTreeHead::new(origin(), log.head());
note.add_signature(NoteSignature {
name: KeyName::new("operator.example").expect("valid"),
key_hash: [1, 2, 3, 4],
value: vec![5u8; 64],
});
let request = AddCheckpoint::new(note.clone(), proof.clone());
let parsed = AddCheckpoint::parse_body(&request.to_body(), 3).expect("parses");
assert_eq!(parsed.note(), ¬e, "signature lines are not proof lines");
assert_eq!(parsed.proof(), &proof);
}
#[test]
fn a_reloaded_store_resumes_where_it_left_off() {
let log = log_of(8);
let witness = witness_at(&log, 8);
let rows = witness.tracked();
let mut reloaded = Witness::new(MemoryWitnessStore::from_heads(rows));
let mut forked = log_of(7);
forked.append(Hash::from_bytes([0xff; 32]));
assert!(
matches!(
reloaded.offer(&origin(), forked.head(), None),
Err(WitnessError::Fork { .. })
),
"a witness that forgets across a restart can be restarted into signing a fork"
);
}
}