use crate::{
merkle::{Family, Location},
qmdb::Error,
};
use commonware_cryptography::Digest;
use core::iter;
use std::sync::{Arc, Weak};
#[derive(Clone, Copy, Debug)]
pub struct Commitment<F: Family, D: Digest> {
pub size: Location<F>,
pub root: D,
}
impl<F: Family, D: Digest> Commitment<F, D> {
pub(crate) const fn new(size: Location<F>, root: D) -> Self {
Self { size, root }
}
}
impl<F: Family, D: Digest> PartialEq for Commitment<F, D> {
fn eq(&self, other: &Self) -> bool {
self.size == other.size && self.root == other.root
}
}
impl<F: Family, D: Digest> Eq for Commitment<F, D> {}
#[derive(Clone)]
pub struct AncestorBounds<F: Family, D: Digest> {
pub floor: Location<F>,
pub state: Commitment<F, D>,
}
#[derive(Clone)]
pub struct Bounds<F: Family, D: Digest> {
pub base: Commitment<F, D>,
pub db: Commitment<F, D>,
pub tip: Commitment<F, D>,
pub ancestors: Vec<AncestorBounds<F, D>>,
pub inactivity_floor: Location<F>,
}
impl<F: Family, D: Digest> Bounds<F, D> {
pub(crate) const fn from_db(state: Commitment<F, D>, inactivity_floor: Location<F>) -> Self {
Self {
base: state,
db: state,
tip: state,
ancestors: Vec::new(),
inactivity_floor,
}
}
pub(crate) fn validate_apply_to(
&self,
current: Commitment<F, D>,
current_floor: Location<F>,
) -> Result<(), Error<F>> {
validate_batch_applicable(current, self.db, &self.ancestors)?;
validate_commit_floors(
current_floor,
current.size,
&self.ancestors,
self.inactivity_floor,
self.tip
.size
.checked_sub(1)
.expect("merkleized batch includes a commit"),
)
}
}
pub(crate) fn ancestors<T, P>(
parent: Option<Weak<T>>,
mut parent_of: P,
) -> impl Iterator<Item = Arc<T>>
where
P: for<'a> FnMut(&'a T) -> Option<&'a Weak<T>>,
{
let mut next = parent.as_ref().and_then(Weak::upgrade);
iter::from_fn(move || {
let batch = next.take()?;
next = parent_of(&batch).and_then(Weak::upgrade);
Some(batch)
})
}
pub(crate) fn parent_and_ancestors<T, P, I>(
parent: Option<&Arc<T>>,
mut ancestors_of: P,
) -> impl Iterator<Item = Arc<T>> + use<T, P, I>
where
P: FnMut(&Arc<T>) -> I,
I: IntoIterator<Item = Arc<T>>,
{
parent.cloned().into_iter().flat_map(move |parent| {
let ancestors = ancestors_of(&parent);
iter::once(parent).chain(ancestors)
})
}
pub(crate) fn collect_ancestor_bounds<T, F, D, I, L, C>(
ancestors: I,
floor: L,
state: C,
) -> Vec<AncestorBounds<F, D>>
where
F: Family,
D: Digest,
I: IntoIterator<Item = Arc<T>>,
L: Fn(&T) -> Location<F>,
C: Fn(&T) -> Commitment<F, D>,
{
ancestors
.into_iter()
.map(|batch| AncestorBounds {
floor: floor(&batch),
state: state(&batch),
})
.collect()
}
pub(crate) fn effective_boundary<F: Family, D: Digest>(
inherited: Commitment<F, D>,
oldest_live_base: Option<Commitment<F, D>>,
) -> Commitment<F, D> {
oldest_live_base
.filter(|base| base.size > inherited.size)
.unwrap_or(inherited)
}
pub(crate) fn validate_batch_applicable<F: Family, D: Digest>(
current: Commitment<F, D>,
batch_db: Commitment<F, D>,
ancestors: &[AncestorBounds<F, D>],
) -> Result<(), Error<F>> {
if current == batch_db || ancestors.iter().any(|ancestor| ancestor.state == current) {
return Ok(());
}
Err(Error::StaleBatch)
}
pub(crate) fn validate_commit_floors<F: Family, D: Digest>(
starting_floor: Location<F>,
db_size: Location<F>,
ancestors: &[AncestorBounds<F, D>],
tip_floor: Location<F>,
tip_commit_loc: Location<F>,
) -> Result<(), Error<F>> {
let mut prev_floor = starting_floor;
for ancestor in ancestors.iter().rev() {
if ancestor.state.size <= db_size {
continue;
}
let ancestor_commit_loc = ancestor.state.size - 1;
if ancestor.floor < prev_floor {
return Err(Error::FloorRegressed(ancestor.floor, prev_floor));
}
if ancestor.floor > ancestor_commit_loc {
return Err(Error::FloorBeyondSize(ancestor.floor, ancestor_commit_loc));
}
prev_floor = ancestor.floor;
}
if tip_floor < prev_floor {
return Err(Error::FloorRegressed(tip_floor, prev_floor));
}
if tip_floor > tip_commit_loc {
return Err(Error::FloorBeyondSize(tip_floor, tip_commit_loc));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::merkle::mmr;
use commonware_cryptography::sha256;
use std::sync::{Arc, Weak};
type F = mmr::Family;
type D = sha256::Digest;
struct TestBatch {
id: u8,
bounds: Bounds<F, D>,
parent: Option<Weak<Self>>,
}
const fn loc(n: u64) -> Location<F> {
Location::new(n)
}
fn state(size: u64, marker: u8) -> Commitment<F, D> {
Commitment::new(Location::new(size), D::from([marker; 32]))
}
fn ancestor(floor: Location<F>, end: u64, marker: u8) -> AncestorBounds<F, D> {
AncestorBounds {
floor,
state: state(end, marker),
}
}
#[test]
fn validate_batch_applicable_accepts_valid_boundaries() {
let ancestors = vec![ancestor(loc(10), 12, 12), ancestor(loc(14), 16, 16)];
assert!(validate_batch_applicable::<F, D>(state(10, 1), state(10, 1), &ancestors).is_ok());
assert!(validate_batch_applicable::<F, D>(state(16, 16), state(10, 1), &ancestors).is_ok());
}
#[test]
fn validate_batch_applicable_rejects_stale_batch() {
let ancestors = vec![ancestor(loc(10), 12, 12), ancestor(loc(14), 16, 16)];
let result = validate_batch_applicable::<F, D>(state(18, 18), state(10, 1), &ancestors);
assert!(matches!(result, Err(Error::StaleBatch)));
}
#[test]
fn validate_batch_applicable_rejects_equal_size_sibling() {
let ancestors = vec![ancestor(loc(14), 16, 16)];
let result = validate_batch_applicable::<F, D>(state(16, 99), state(10, 1), &ancestors);
assert!(matches!(result, Err(Error::StaleBatch)));
}
#[test]
fn ancestors_iterates_parent_first() {
let grandparent = Arc::new(TestBatch {
id: 1,
bounds: Bounds {
base: state(0, 0),
db: state(0, 0),
tip: state(5, 5),
ancestors: Vec::new(),
inactivity_floor: loc(3),
},
parent: None,
});
let parent = Arc::new(TestBatch {
id: 2,
bounds: Bounds {
base: state(5, 5),
db: state(0, 0),
tip: state(7, 7),
ancestors: vec![ancestor(loc(3), 5, 5)],
inactivity_floor: loc(6),
},
parent: Some(Arc::downgrade(&grandparent)),
});
let ids: Vec<_> = ancestors(Some(Arc::downgrade(&parent)), |batch| batch.parent.as_ref())
.map(|batch| batch.id)
.collect();
assert_eq!(ids, vec![2, 1]);
}
#[test]
fn collect_ancestor_bounds_preserves_pairing_and_order() {
let parent = Arc::new(TestBatch {
id: 1,
bounds: Bounds {
base: state(0, 0),
db: state(0, 0),
tip: state(12, 12),
ancestors: Vec::new(),
inactivity_floor: loc(10),
},
parent: None,
});
let grandparent = Arc::new(TestBatch {
id: 2,
bounds: Bounds {
base: state(0, 0),
db: state(0, 0),
tip: state(8, 8),
ancestors: Vec::new(),
inactivity_floor: loc(6),
},
parent: None,
});
let bounds = collect_ancestor_bounds(
vec![Arc::clone(&parent), Arc::clone(&grandparent)],
|batch| batch.bounds.inactivity_floor,
|batch| state(*batch.bounds.tip.size, *batch.bounds.tip.size as u8),
);
assert_eq!(bounds.len(), 2);
assert_eq!(bounds[0].floor, loc(10));
assert_eq!(bounds[0].state, state(12, 12));
assert_eq!(bounds[1].floor, loc(6));
assert_eq!(bounds[1].state, state(8, 8));
}
#[test]
fn bounds_validates_apply_to_current_state() {
let bounds = Bounds::<F, D> {
base: state(10, 1),
db: state(10, 1),
tip: state(14, 14),
ancestors: vec![ancestor(loc(10), 12, 12)],
inactivity_floor: loc(11),
};
assert!(bounds.validate_apply_to(state(10, 1), loc(9)).is_ok());
let result = bounds.validate_apply_to(state(11, 11), loc(9));
assert!(matches!(result, Err(Error::StaleBatch)));
}
#[test]
fn validate_commit_floors_accepts_monotonic_chain() {
let ancestors = vec![ancestor(loc(6), 7, 7), ancestor(loc(4), 5, 5)];
assert!(
validate_commit_floors::<F, D>(loc(2), loc(1), &ancestors, loc(8), loc(9),).is_ok()
);
}
#[test]
fn validate_commit_floors_skips_committed_ancestors() {
let ancestors = vec![ancestor(loc(1), 7, 7), ancestor(loc(1), 5, 5)];
assert!(
validate_commit_floors::<F, D>(loc(6), loc(7), &ancestors, loc(8), loc(9),).is_ok()
);
}
#[test]
fn validate_commit_floors_rejects_ancestor_regression() {
let ancestors = vec![ancestor(loc(6), 7, 7), ancestor(loc(3), 5, 5)];
let result = validate_commit_floors::<F, D>(loc(4), loc(1), &ancestors, loc(8), loc(9));
assert!(matches!(
result,
Err(Error::FloorRegressed(floor, previous)) if floor == loc(3) && previous == loc(4)
));
}
#[test]
fn validate_commit_floors_rejects_ancestor_floor_beyond_commit() {
let ancestors = vec![ancestor(loc(8), 7, 7), ancestor(loc(4), 5, 5)];
let result = validate_commit_floors::<F, D>(loc(2), loc(1), &ancestors, loc(9), loc(9));
assert!(matches!(
result,
Err(Error::FloorBeyondSize(floor, commit)) if floor == loc(8) && commit == loc(6)
));
}
#[test]
fn validate_commit_floors_rejects_tip_regression() {
let ancestors = vec![ancestor(loc(4), 5, 5)];
let result = validate_commit_floors::<F, D>(loc(2), loc(1), &ancestors, loc(3), loc(9));
assert!(matches!(
result,
Err(Error::FloorRegressed(floor, previous)) if floor == loc(3) && previous == loc(4)
));
}
#[test]
fn validate_commit_floors_rejects_tip_floor_beyond_commit() {
let ancestors = vec![ancestor(loc(4), 5, 5)];
let result = validate_commit_floors::<F, D>(loc(2), loc(1), &ancestors, loc(10), loc(9));
assert!(matches!(
result,
Err(Error::FloorBeyondSize(floor, commit)) if floor == loc(10) && commit == loc(9)
));
}
}