#![allow(clippy::arithmetic_side_effects, clippy::indexing_slicing)]
use crate::hash::Hash;
use super::{ProofError, node_hash};
#[must_use]
pub fn count(size: u64) -> u64 {
if size == 0 {
return 0;
}
size.saturating_mul(2)
.saturating_sub(u64::from(size.count_ones()))
}
#[must_use]
pub fn index_of(level: u8, index: u64) -> u64 {
let mut n = index;
for _ in 0..level {
n = n.saturating_mul(2).saturating_add(1);
}
let mut at = 0u64;
while n > 0 {
at = at.saturating_add(n);
n >>= 1;
}
at.saturating_add(u64::from(level))
}
#[must_use]
pub fn split_index(stored: u64) -> (u8, u64) {
let (level, record) = level_and_record(stored);
(level, record >> u32::from(level))
}
fn level_and_record(stored: u64) -> (u8, u64) {
let mut record = stored / 2;
let mut at = index_of(0, record);
loop {
let next = at + 1 + u64::from((record + 1).trailing_zeros());
if next > stored {
break;
}
record += 1;
at = next;
}
(u8::try_from(stored - at).unwrap_or(u8::MAX), record)
}
#[must_use]
pub fn size_from_count(stored: u64) -> Option<u64> {
if stored == 0 {
return Some(0);
}
let (mut low, mut high) = (0u64, stored);
while low < high {
let mid = low + (high - low).div_ceil(2);
if count(mid) <= stored {
low = mid;
} else {
high = mid - 1;
}
}
(count(low) == stored).then_some(low)
}
fn split(n: u64) -> (u64, u8) {
let mut level = 0u8;
while level < 63 && (1u64 << (level + 1)) < n {
level += 1;
}
(1u64 << level, level)
}
fn subtree_indices(lo: u64, hi: u64, need: &mut Vec<u64>) {
let mut lo = lo;
while lo < hi {
let (width, level) = split(hi - lo + 1);
need.push(index_of(level, lo >> level));
lo += width;
}
}
fn subtree_hash(lo: u64, hi: u64, hashes: &[Hash]) -> Option<(Hash, &[Hash])> {
let mut wanted = 0usize;
let mut at = lo;
while at < hi {
let (width, _) = split(hi - at + 1);
wanted = wanted.checked_add(1)?;
at += width;
}
let (used, rest) = hashes.split_at_checked(wanted)?;
let mut folded = *used.last()?;
for left in used.iter().rev().skip(1) {
folded = node_hash(left, &folded);
}
Some((folded, rest))
}
fn inclusion_indices(lo: u64, hi: u64, n: u64, need: &mut Vec<u64>) {
if lo + 1 == hi {
return;
}
let (width, _) = split(hi - lo);
if n < lo + width {
inclusion_indices(lo, lo + width, n, need);
subtree_indices(lo + width, hi, need);
} else {
subtree_indices(lo, lo + width, need);
inclusion_indices(lo + width, hi, n, need);
}
}
fn inclusion_path<'a>(
lo: u64,
hi: u64,
n: u64,
hashes: &'a [Hash],
out: &mut Vec<Hash>,
) -> Option<&'a [Hash]> {
if lo + 1 == hi {
return Some(hashes);
}
let (width, _) = split(hi - lo);
let (sibling, rest) = if n < lo + width {
let rest = inclusion_path(lo, lo + width, n, hashes, out)?;
subtree_hash(lo + width, hi, rest)?
} else {
let (sibling, rest) = subtree_hash(lo, lo + width, hashes)?;
(sibling, inclusion_path(lo + width, hi, n, rest, out)?)
};
out.push(sibling);
Some(rest)
}
fn consistency_indices(lo: u64, hi: u64, n: u64, need: &mut Vec<u64>) {
if n == hi {
if lo != 0 {
subtree_indices(lo, hi, need);
}
return;
}
let (width, _) = split(hi - lo);
if n <= lo + width {
consistency_indices(lo, lo + width, n, need);
subtree_indices(lo + width, hi, need);
} else {
subtree_indices(lo, lo + width, need);
consistency_indices(lo + width, hi, n, need);
}
}
fn consistency_path<'a>(
lo: u64,
hi: u64,
n: u64,
hashes: &'a [Hash],
out: &mut Vec<Hash>,
) -> Option<&'a [Hash]> {
if n == hi {
if lo == 0 {
return Some(hashes);
}
let (root, rest) = subtree_hash(lo, hi, hashes)?;
out.push(root);
return Some(rest);
}
let (width, _) = split(hi - lo);
let (sibling, rest) = if n <= lo + width {
let rest = consistency_path(lo, lo + width, n, hashes, out)?;
subtree_hash(lo + width, hi, rest)?
} else {
let (sibling, rest) = subtree_hash(lo, lo + width, hashes)?;
(sibling, consistency_path(lo + width, hi, n, rest, out)?)
};
out.push(sibling);
Some(rest)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error("expected {expected} nodes for this plan, got {found}")]
pub struct MalformedNodes {
pub expected: usize,
pub found: usize,
}
fn expect_len(plan: &[u64], nodes: &[Hash]) -> Result<(), MalformedNodes> {
if plan.len() == nodes.len() {
Ok(())
} else {
Err(MalformedNodes {
expected: plan.len(),
found: nodes.len(),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RootPlan {
size: u64,
need: Vec<u64>,
}
impl RootPlan {
#[must_use]
pub fn new(size: u64) -> Self {
let mut need = Vec::new();
subtree_indices(0, size, &mut need);
Self { size, need }
}
#[must_use]
pub fn nodes(&self) -> &[u64] {
&self.need
}
pub fn assemble(&self, nodes: &[Hash]) -> Result<Hash, MalformedNodes> {
expect_len(&self.need, nodes)?;
if self.size == 0 {
return Ok(super::empty_root());
}
subtree_hash(0, self.size, nodes)
.map(|(root, _)| root)
.ok_or(MalformedNodes {
expected: self.need.len(),
found: nodes.len(),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InclusionPlan {
index: u64,
size: u64,
need: Vec<u64>,
}
impl InclusionPlan {
pub fn new(index: u64, size: u64) -> Result<Self, ProofError> {
if index >= size {
return Err(ProofError::IndexOutOfRange { index, size });
}
let mut need = Vec::new();
inclusion_indices(0, size, index, &mut need);
Ok(Self { index, size, need })
}
#[must_use]
pub fn nodes(&self) -> &[u64] {
&self.need
}
pub fn assemble(&self, nodes: &[Hash]) -> Result<super::InclusionProof, MalformedNodes> {
expect_len(&self.need, nodes)?;
let mut path = Vec::new();
inclusion_path(0, self.size, self.index, nodes, &mut path).ok_or(MalformedNodes {
expected: self.need.len(),
found: nodes.len(),
})?;
Ok(super::InclusionProof {
leaf_index: self.index,
tree_size: self.size,
path,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConsistencyPlan {
old: u64,
new: u64,
need: Vec<u64>,
}
impl ConsistencyPlan {
pub fn new(old: u64, new: u64) -> Result<Self, ProofError> {
if old == 0 {
return Err(ProofError::EmptyOldTree { new_size: new });
}
if old > new {
return Err(ProofError::SizeOutOfRange {
from: old,
size: new,
});
}
let mut need = Vec::new();
if old < new {
consistency_indices(0, new, old, &mut need);
}
Ok(Self { old, new, need })
}
#[must_use]
pub fn nodes(&self) -> &[u64] {
&self.need
}
pub fn assemble(&self, nodes: &[Hash]) -> Result<super::ConsistencyProof, MalformedNodes> {
expect_len(&self.need, nodes)?;
let mut path = Vec::new();
if self.old < self.new {
consistency_path(0, self.new, self.old, nodes, &mut path).ok_or(MalformedNodes {
expected: self.need.len(),
found: nodes.len(),
})?;
}
Ok(super::ConsistencyProof {
old_size: self.old,
new_size: self.new,
path,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_numbering_round_trips() {
for stored in 0..2_000u64 {
let (level, index) = split_index(stored);
assert_eq!(
index_of(level, index),
stored,
"position {stored} split to ({level}, {index}) and back to something else"
);
}
}
#[test]
fn the_numbering_matches_the_documented_layout() {
assert_eq!(index_of(0, 0), 0);
assert_eq!(index_of(0, 1), 1);
assert_eq!(index_of(1, 0), 2);
assert_eq!(index_of(0, 2), 3);
assert_eq!(index_of(0, 3), 4);
assert_eq!(index_of(1, 1), 5);
assert_eq!(index_of(2, 0), 6);
}
#[test]
fn appending_a_record_writes_one_node_per_closed_subtree() {
let mut running = 0u64;
for n in 0..4_000u64 {
assert_eq!(count(n), running, "count disagreed at {n}");
running += 1 + u64::from((n + 1).trailing_zeros());
}
}
#[test]
fn storage_is_just_under_two_hashes_per_record() {
for size in [1u64, 2, 3, 7, 8, 1_000, 1_048_576] {
let nodes = count(size);
assert!(nodes < 2 * size, "size {size} stored {nodes}");
assert_eq!(
2 * size - nodes,
u64::from(size.count_ones()),
"size {size} stored {nodes}"
);
}
}
#[test]
fn the_node_count_inverts_to_the_size() {
for size in 0..5_000u64 {
assert_eq!(
size_from_count(count(size)),
Some(size),
"count did not invert at size {size}"
);
}
}
#[test]
fn a_count_between_two_sizes_is_refused() {
for size in 1..500u64 {
let full = count(size);
let previous = count(size - 1);
for between in (previous + 1)..full {
assert_eq!(
size_from_count(between),
None,
"{between} nodes is between sizes {} and {size}",
size - 1
);
}
}
}
#[test]
fn the_last_position_names_the_last_record() {
for size in 1..2_000u64 {
let top = count(size) - 1;
assert_eq!(
super::level_and_record(top).1 + 1,
size,
"the highest position did not name the last record at size {size}"
);
}
}
#[test]
fn a_proof_reads_a_logarithmic_number_of_nodes() {
for size in [2u64, 16, 1_024, 1_048_576] {
let bound = usize::try_from(size.ilog2()).expect("small") + 2;
let plan = InclusionPlan::new(size / 3, size).expect("in range");
assert!(
plan.nodes().len() <= bound,
"inclusion at size {size} read {} nodes, over the bound of {bound}",
plan.nodes().len()
);
let plan = ConsistencyPlan::new(size / 3 + 1, size).expect("in range");
assert!(
plan.nodes().len() <= bound,
"consistency at size {size} read {} nodes, over the bound of {bound}",
plan.nodes().len()
);
}
}
#[test]
fn a_plan_names_the_count_it_needs_rather_than_truncating() {
let plan = InclusionPlan::new(3, 8).expect("in range");
let short = vec![Hash::from_bytes([0; 32]); plan.nodes().len() - 1];
assert!(matches!(
plan.assemble(&short),
Err(MalformedNodes { found, .. }) if found == short.len()
));
let long = vec![Hash::from_bytes([0; 32]); plan.nodes().len() + 1];
assert!(plan.assemble(&long).is_err());
}
#[test]
fn a_proof_from_the_empty_tree_is_not_planned() {
assert!(matches!(
ConsistencyPlan::new(0, 8),
Err(ProofError::EmptyOldTree { new_size: 8 })
));
}
#[test]
fn a_leaf_beyond_the_tree_is_not_planned() {
assert!(matches!(
InclusionPlan::new(8, 8),
Err(ProofError::IndexOutOfRange { index: 8, size: 8 })
));
}
#[test]
fn a_shrinking_consistency_proof_is_not_planned() {
assert!(matches!(
ConsistencyPlan::new(9, 8),
Err(ProofError::SizeOutOfRange { from: 9, size: 8 })
));
}
}