use std::collections::BTreeMap;
use std::sync::OnceLock;
use super::{Hash, InclusionProof, ProofError, empty_root, leaf_hash, node_hash};
use crate::model::{
AccountHandle, AmountError, BALANCE_LEAF_KIND, Balance, Currency, Layer, Seal, TreeHeadEntry,
};
#[must_use]
pub fn balance_leaf_bytes<const P: u8>(balance: &Balance<P>) -> [u8; 41] {
let bytes = core::iter::once(BALANCE_LEAF_KIND)
.chain(balance.debit().to_minor().to_le_bytes())
.chain(balance.credit().to_minor().to_le_bytes())
.chain(balance.postings().to_le_bytes());
let mut leaf = [0u8; 41];
for (slot, byte) in leaf.iter_mut().zip(bytes) {
*slot = byte;
}
leaf
}
#[must_use]
pub fn zero_leaf_hash() -> Hash {
zero_subtree(0)
}
fn zeros() -> &'static [Hash; 63] {
static ZEROS: OnceLock<[Hash; 63]> = OnceLock::new();
ZEROS.get_or_init(|| {
let mut next = leaf_hash(&balance_leaf_bytes(&Balance::<0>::ZERO));
core::array::from_fn(|_| {
let current = next;
next = node_hash(¤t, ¤t);
current
})
})
}
fn zero_subtree(level: u32) -> Hash {
match usize::try_from(level)
.ok()
.and_then(|level| zeros().get(level))
{
Some(hash) => *hash,
None => (0..level).fold(zero_subtree(0), |hash, _| node_hash(&hash, &hash)),
}
}
fn split(n: u32) -> u32 {
n.saturating_sub(1)
.checked_ilog2()
.map_or(0, |exponent| 1u32.rotate_left(exponent))
}
type Nodes = BTreeMap<(u32, u32), Hash>;
#[derive(Clone)]
pub struct BalanceTree<const P: u8 = 2> {
currency: Currency,
layer: Layer,
accounts: u32,
leaves: BTreeMap<u32, Balance<P>>,
nodes: OnceLock<Nodes>,
}
impl<const P: u8> PartialEq for BalanceTree<P> {
fn eq(&self, other: &Self) -> bool {
(self.currency, self.layer, self.accounts, &self.leaves)
== (other.currency, other.layer, other.accounts, &other.leaves)
}
}
impl<const P: u8> Eq for BalanceTree<P> {}
impl<const P: u8> core::fmt::Debug for BalanceTree<P> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("BalanceTree")
.field("currency", &self.currency)
.field("layer", &self.layer)
.field("accounts", &self.accounts)
.field("leaves", &self.leaves)
.finish()
}
}
impl<const P: u8> BalanceTree<P> {
#[must_use]
pub const fn new(currency: Currency, layer: Layer, accounts: u32) -> Self {
Self {
currency,
layer,
accounts,
leaves: BTreeMap::new(),
nodes: OnceLock::new(),
}
}
pub fn set(&mut self, handle: AccountHandle, balance: Balance<P>) -> Result<(), ProofError> {
let index = handle.get();
if index >= self.accounts {
return Err(ProofError::IndexOutOfRange {
index: u64::from(index),
size: u64::from(self.accounts),
});
}
if balance.is_zero() {
self.leaves.remove(&index);
} else {
self.leaves.insert(index, balance);
}
self.nodes.take();
Ok(())
}
#[must_use]
pub const fn currency(&self) -> Currency {
self.currency
}
#[must_use]
pub const fn layer(&self) -> Layer {
self.layer
}
#[must_use]
pub const fn accounts(&self) -> u32 {
self.accounts
}
#[must_use]
pub fn root(&self) -> Hash {
if self.accounts == 0 {
return empty_root();
}
let nodes = self.nodes();
range_root(0, self.accounts, &mut |level, index| {
lookup(nodes, level, index)
})
}
pub fn totals(&self) -> Result<Balance<P>, AmountError> {
self.leaves
.values()
.try_fold(Balance::ZERO, |sum, balance| sum.checked_add(balance))
}
pub fn head(&self) -> Result<TreeHeadEntry<P>, AmountError> {
Ok(TreeHeadEntry {
currency: self.currency,
layer: self.layer,
root: self.root(),
totals: self.totals()?,
})
}
pub fn prove(&self, handle: AccountHandle) -> Result<BalanceProof<P>, ProofError> {
let index = handle.get();
if index >= self.accounts {
return Err(ProofError::IndexOutOfRange {
index: u64::from(index),
size: u64::from(self.accounts),
});
}
let mut path = Vec::new();
let nodes = self.nodes();
walk_path(0, self.accounts, index, &mut path, &mut |level, at| {
lookup(nodes, level, at)
});
Ok(BalanceProof {
currency: self.currency,
layer: self.layer,
handle,
balance: self.leaves.get(&index).copied().unwrap_or(Balance::ZERO),
path: InclusionProof {
index: u64::from(index),
size: u64::from(self.accounts),
path,
},
})
}
fn nodes(&self) -> &Nodes {
self.nodes.get_or_init(|| {
let mut nodes: Nodes = self
.leaves
.iter()
.map(|(index, balance)| ((0, *index), leaf_hash(&balance_leaf_bytes(balance))))
.collect();
let mut level_nodes: Vec<u32> = self.leaves.keys().copied().collect();
for level in 0..u32::BITS {
let width = 2u64.pow(level.saturating_add(1));
let mut parents: Vec<u32> = level_nodes.iter().map(|index| index / 2).collect();
parents.dedup();
parents.retain(|parent| {
u64::from(*parent).saturating_add(1).saturating_mul(width)
<= u64::from(self.accounts)
});
for parent in &parents {
let child = |index: u32| {
nodes
.get(&(level, index))
.copied()
.unwrap_or_else(|| zero_subtree(level))
};
let left = parent.saturating_mul(2);
let hash = node_hash(&child(left), &child(left.saturating_add(1)));
nodes.insert((level.saturating_add(1), *parent), hash);
}
if parents.is_empty() {
break;
}
level_nodes = parents;
}
nodes
})
}
pub(crate) fn leaves(&self) -> impl Iterator<Item = (u32, Balance<P>)> + '_ {
self.leaves
.iter()
.map(|(index, balance)| (*index, *balance))
}
pub(crate) fn leaf(&self, handle: AccountHandle) -> Balance<P> {
self.leaves
.get(&handle.get())
.copied()
.unwrap_or(Balance::ZERO)
}
pub(crate) fn stored_nodes(&self) -> impl Iterator<Item = (u32, u32, Hash)> + '_ {
self.nodes()
.iter()
.map(|((level, index), hash)| (*level, *index, *hash))
}
pub(crate) fn from_nodes(
currency: Currency,
layer: Layer,
accounts: u32,
leaf: (AccountHandle, Balance<P>),
nodes: impl IntoIterator<Item = ((u32, u32), Hash)>,
) -> Self {
let mut leaves = BTreeMap::new();
if !leaf.1.is_zero() {
leaves.insert(leaf.0.get(), leaf.1);
}
Self {
currency,
layer,
accounts,
leaves,
nodes: OnceLock::from(nodes.into_iter().collect::<Nodes>()),
}
}
}
fn lookup(nodes: &Nodes, level: u32, index: u32) -> Hash {
nodes
.get(&(level, index))
.copied()
.unwrap_or_else(|| zero_subtree(level))
}
fn range_root(lo: u32, n: u32, node: &mut dyn FnMut(u32, u32) -> Hash) -> Hash {
if n.is_power_of_two() {
let level = n.trailing_zeros();
return node(level, lo.checked_shr(level).unwrap_or(0));
}
let k = split(n);
let left = range_root(lo, k, node);
let right = range_root(lo.saturating_add(k), n.saturating_sub(k), node);
node_hash(&left, &right)
}
fn walk_path(
lo: u32,
n: u32,
index: u32,
out: &mut Vec<Hash>,
node: &mut dyn FnMut(u32, u32) -> Hash,
) {
if n <= 1 {
return;
}
let k = split(n);
let mid = lo.saturating_add(k);
if index < mid {
walk_path(lo, k, index, out, node);
out.push(range_root(mid, n.saturating_sub(k), node));
} else {
walk_path(mid, n.saturating_sub(k), index, out, node);
out.push(range_root(lo, k, node));
}
}
pub(crate) fn proof_nodes(accounts: u32, handle: AccountHandle) -> Vec<(u32, u32)> {
let mut ids = Vec::new();
let mut path = Vec::new();
walk_path(0, accounts, handle.get(), &mut path, &mut |level, index| {
ids.push((level, index));
zero_subtree(level)
});
ids.sort_unstable();
ids.dedup();
ids
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct BalanceProof<const P: u8 = 2> {
pub currency: Currency,
pub layer: Layer,
pub handle: AccountHandle,
pub balance: Balance<P>,
pub path: InclusionProof,
}
impl<const P: u8> BalanceProof<P> {
pub fn verify(&self, seal: &Seal<P>) -> Result<(), ProofError> {
let tree = seal
.tree(self.currency, self.layer)
.ok_or(ProofError::NoSuchTree {
currency: self.currency,
layer: self.layer,
})?;
if self.path.index != u64::from(self.handle.get()) {
return Err(ProofError::HandleMismatch {
index: self.path.index,
handle: self.handle.get(),
});
}
let accounts = seal.parts().accounts;
if self.path.size != u64::from(accounts) {
return Err(ProofError::AccountCountMismatch {
tree_size: self.path.size,
accounts,
});
}
let computed = self
.path
.root_from(&leaf_hash(&balance_leaf_bytes(&self.balance)))?;
if computed == tree.root {
Ok(())
} else {
Err(ProofError::RootMismatch {
expected: tree.root,
computed,
})
}
}
}
pub fn build_seal_trees<const P: u8>(
accounts: u32,
balances: impl IntoIterator<Item = (AccountHandle, Currency, Layer, Balance<P>)>,
) -> Result<Vec<BalanceTree<P>>, ProofError> {
let mut trees: BTreeMap<(Currency, Layer), BalanceTree<P>> = BTreeMap::new();
for (handle, currency, layer, balance) in balances {
if balance.is_zero() {
continue;
}
let tree = trees
.entry((currency, layer))
.or_insert_with(|| BalanceTree::new(currency, layer, accounts));
if tree.leaves.contains_key(&handle.get()) {
return Err(ProofError::DuplicateBalance {
handle: handle.get(),
});
}
tree.set(handle, balance)?;
}
Ok(trees.into_values().collect())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::log::hash::counter;
use crate::log::root_of;
use crate::model::{Amount, EUR, SealParts, Side};
fn balance(minor: i128) -> Balance {
Balance::ZERO
.post(Side::Debit, Amount::from_minor(minor).unwrap())
.unwrap()
}
#[test]
fn a_million_accounts_with_a_thousand_balances_take_under_50000_hashes() {
let accounts = 1_000_000u32;
let mut tree = BalanceTree::new(EUR, Layer::Settled, accounts);
let handles: Vec<u32> = (0..1000u32).map(|i| i * 997 + 3).collect();
for (i, handle) in handles.iter().enumerate() {
tree.set(AccountHandle::new(*handle).unwrap(), balance(i as i128 + 1))
.unwrap();
}
zeros();
let (root, hashes) = counter::count(|| tree.root());
assert!(hashes < 50_000, "{hashes} hashes");
let head = tree.head().unwrap();
let seal = Seal::new(SealParts {
period: crate::model::Label::new("p").unwrap(),
start: time::macros::date!(2026 - 01 - 01),
end: time::macros::date!(2026 - 01 - 31),
through: 1,
accounts,
trees: vec![head],
})
.unwrap();
assert_eq!(seal.tree(EUR, Layer::Settled).unwrap().root, root);
let zero_handles = (0..1000u32)
.map(|i| i * 991 + 1)
.filter(|h| !handles.contains(h));
for handle in handles.iter().copied().chain(zero_handles) {
let proof = tree.prove(AccountHandle::new(handle).unwrap()).unwrap();
proof.verify(&seal).unwrap();
}
}
#[test]
fn sparse_roots_equal_the_explicit_tree() {
for accounts in 1..=40u32 {
let mut tree = BalanceTree::<2>::new(EUR, Layer::Settled, accounts);
let mut explicit = Vec::new();
for handle in 0..accounts {
let b = if handle % 3 == 1 {
balance(i128::from(handle))
} else {
Balance::ZERO
};
tree.set(AccountHandle::new(handle).unwrap(), b).unwrap();
explicit.push(leaf_hash(&balance_leaf_bytes(&b)));
}
assert_eq!(tree.root(), root_of(&explicit), "accounts = {accounts}");
}
}
}