1use std::collections::BTreeMap;
11use std::sync::OnceLock;
12
13use super::{Hash, InclusionProof, ProofError, empty_root, leaf_hash, node_hash};
14use crate::model::{
15 AccountHandle, AmountError, BALANCE_LEAF_KIND, Balance, Currency, Layer, Seal, TreeHeadEntry,
16};
17
18#[must_use]
28pub fn balance_leaf_bytes<const P: u8>(balance: &Balance<P>) -> [u8; 41] {
29 let bytes = core::iter::once(BALANCE_LEAF_KIND)
30 .chain(balance.debit().to_minor().to_le_bytes())
31 .chain(balance.credit().to_minor().to_le_bytes())
32 .chain(balance.postings().to_le_bytes());
33 let mut leaf = [0u8; 41];
34 for (slot, byte) in leaf.iter_mut().zip(bytes) {
35 *slot = byte;
36 }
37 leaf
38}
39
40#[must_use]
42pub fn zero_leaf_hash() -> Hash {
43 zero_subtree(0)
44}
45
46fn zeros() -> &'static [Hash; 63] {
48 static ZEROS: OnceLock<[Hash; 63]> = OnceLock::new();
49 ZEROS.get_or_init(|| {
50 let mut next = leaf_hash(&balance_leaf_bytes(&Balance::<0>::ZERO));
51 core::array::from_fn(|_| {
52 let current = next;
53 next = node_hash(¤t, ¤t);
54 current
55 })
56 })
57}
58
59fn zero_subtree(level: u32) -> Hash {
61 match usize::try_from(level)
62 .ok()
63 .and_then(|level| zeros().get(level))
64 {
65 Some(hash) => *hash,
66 None => (0..level).fold(zero_subtree(0), |hash, _| node_hash(&hash, &hash)),
68 }
69}
70
71fn split(n: u32) -> u32 {
73 n.saturating_sub(1)
74 .checked_ilog2()
75 .map_or(0, |exponent| 1u32.rotate_left(exponent))
76}
77
78type Nodes = BTreeMap<(u32, u32), Hash>;
81
82#[derive(Clone)]
99pub struct BalanceTree<const P: u8 = 2> {
100 currency: Currency,
101 layer: Layer,
102 accounts: u32,
103 leaves: BTreeMap<u32, Balance<P>>,
104 nodes: OnceLock<Nodes>,
107}
108
109impl<const P: u8> PartialEq for BalanceTree<P> {
110 fn eq(&self, other: &Self) -> bool {
111 (self.currency, self.layer, self.accounts, &self.leaves)
112 == (other.currency, other.layer, other.accounts, &other.leaves)
113 }
114}
115
116impl<const P: u8> Eq for BalanceTree<P> {}
117
118impl<const P: u8> core::fmt::Debug for BalanceTree<P> {
119 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
120 f.debug_struct("BalanceTree")
121 .field("currency", &self.currency)
122 .field("layer", &self.layer)
123 .field("accounts", &self.accounts)
124 .field("leaves", &self.leaves)
125 .finish()
126 }
127}
128
129impl<const P: u8> BalanceTree<P> {
130 #[must_use]
132 pub const fn new(currency: Currency, layer: Layer, accounts: u32) -> Self {
133 Self {
134 currency,
135 layer,
136 accounts,
137 leaves: BTreeMap::new(),
138 nodes: OnceLock::new(),
139 }
140 }
141
142 pub fn set(&mut self, handle: AccountHandle, balance: Balance<P>) -> Result<(), ProofError> {
148 let index = handle.get();
149 if index >= self.accounts {
150 return Err(ProofError::IndexOutOfRange {
151 index: u64::from(index),
152 size: u64::from(self.accounts),
153 });
154 }
155 if balance.is_zero() {
156 self.leaves.remove(&index);
157 } else {
158 self.leaves.insert(index, balance);
159 }
160 self.nodes.take();
161 Ok(())
162 }
163
164 #[must_use]
166 pub const fn currency(&self) -> Currency {
167 self.currency
168 }
169
170 #[must_use]
172 pub const fn layer(&self) -> Layer {
173 self.layer
174 }
175
176 #[must_use]
178 pub const fn accounts(&self) -> u32 {
179 self.accounts
180 }
181
182 #[must_use]
184 pub fn root(&self) -> Hash {
185 if self.accounts == 0 {
186 return empty_root();
187 }
188 let nodes = self.nodes();
189 range_root(0, self.accounts, &mut |level, index| {
190 lookup(nodes, level, index)
191 })
192 }
193
194 pub fn totals(&self) -> Result<Balance<P>, AmountError> {
200 self.leaves
201 .values()
202 .try_fold(Balance::ZERO, |sum, balance| sum.checked_add(balance))
203 }
204
205 pub fn head(&self) -> Result<TreeHeadEntry<P>, AmountError> {
211 Ok(TreeHeadEntry {
212 currency: self.currency,
213 layer: self.layer,
214 root: self.root(),
215 totals: self.totals()?,
216 })
217 }
218
219 pub fn prove(&self, handle: AccountHandle) -> Result<BalanceProof<P>, ProofError> {
226 let index = handle.get();
227 if index >= self.accounts {
228 return Err(ProofError::IndexOutOfRange {
229 index: u64::from(index),
230 size: u64::from(self.accounts),
231 });
232 }
233 let mut path = Vec::new();
234 let nodes = self.nodes();
235 walk_path(0, self.accounts, index, &mut path, &mut |level, at| {
236 lookup(nodes, level, at)
237 });
238 Ok(BalanceProof {
239 currency: self.currency,
240 layer: self.layer,
241 handle,
242 balance: self.leaves.get(&index).copied().unwrap_or(Balance::ZERO),
243 path: InclusionProof {
244 index: u64::from(index),
245 size: u64::from(self.accounts),
246 path,
247 },
248 })
249 }
250
251 fn nodes(&self) -> &Nodes {
255 self.nodes.get_or_init(|| {
256 let mut nodes: Nodes = self
257 .leaves
258 .iter()
259 .map(|(index, balance)| ((0, *index), leaf_hash(&balance_leaf_bytes(balance))))
260 .collect();
261 let mut level_nodes: Vec<u32> = self.leaves.keys().copied().collect();
262 for level in 0..u32::BITS {
263 let width = 2u64.pow(level.saturating_add(1));
264 let mut parents: Vec<u32> = level_nodes.iter().map(|index| index / 2).collect();
265 parents.dedup();
266 parents.retain(|parent| {
268 u64::from(*parent).saturating_add(1).saturating_mul(width)
269 <= u64::from(self.accounts)
270 });
271 for parent in &parents {
272 let child = |index: u32| {
273 nodes
274 .get(&(level, index))
275 .copied()
276 .unwrap_or_else(|| zero_subtree(level))
277 };
278 let left = parent.saturating_mul(2);
281 let hash = node_hash(&child(left), &child(left.saturating_add(1)));
282 nodes.insert((level.saturating_add(1), *parent), hash);
283 }
284 if parents.is_empty() {
285 break;
286 }
287 level_nodes = parents;
288 }
289 nodes
290 })
291 }
292
293 pub(crate) fn leaves(&self) -> impl Iterator<Item = (u32, Balance<P>)> + '_ {
295 self.leaves
296 .iter()
297 .map(|(index, balance)| (*index, *balance))
298 }
299
300 pub(crate) fn leaf(&self, handle: AccountHandle) -> Balance<P> {
302 self.leaves
303 .get(&handle.get())
304 .copied()
305 .unwrap_or(Balance::ZERO)
306 }
307
308 pub(crate) fn stored_nodes(&self) -> impl Iterator<Item = (u32, u32, Hash)> + '_ {
313 self.nodes()
314 .iter()
315 .map(|((level, index), hash)| (*level, *index, *hash))
316 }
317
318 pub(crate) fn from_nodes(
322 currency: Currency,
323 layer: Layer,
324 accounts: u32,
325 leaf: (AccountHandle, Balance<P>),
326 nodes: impl IntoIterator<Item = ((u32, u32), Hash)>,
327 ) -> Self {
328 let mut leaves = BTreeMap::new();
329 if !leaf.1.is_zero() {
330 leaves.insert(leaf.0.get(), leaf.1);
331 }
332 Self {
333 currency,
334 layer,
335 accounts,
336 leaves,
337 nodes: OnceLock::from(nodes.into_iter().collect::<Nodes>()),
338 }
339 }
340}
341
342fn lookup(nodes: &Nodes, level: u32, index: u32) -> Hash {
344 nodes
345 .get(&(level, index))
346 .copied()
347 .unwrap_or_else(|| zero_subtree(level))
348}
349
350fn range_root(lo: u32, n: u32, node: &mut dyn FnMut(u32, u32) -> Hash) -> Hash {
355 if n.is_power_of_two() {
356 let level = n.trailing_zeros();
357 return node(level, lo.checked_shr(level).unwrap_or(0));
358 }
359 let k = split(n);
360 let left = range_root(lo, k, node);
361 let right = range_root(lo.saturating_add(k), n.saturating_sub(k), node);
362 node_hash(&left, &right)
363}
364
365fn walk_path(
367 lo: u32,
368 n: u32,
369 index: u32,
370 out: &mut Vec<Hash>,
371 node: &mut dyn FnMut(u32, u32) -> Hash,
372) {
373 if n <= 1 {
374 return;
375 }
376 let k = split(n);
377 let mid = lo.saturating_add(k);
378 if index < mid {
379 walk_path(lo, k, index, out, node);
380 out.push(range_root(mid, n.saturating_sub(k), node));
381 } else {
382 walk_path(mid, n.saturating_sub(k), index, out, node);
383 out.push(range_root(lo, k, node));
384 }
385}
386
387pub(crate) fn proof_nodes(accounts: u32, handle: AccountHandle) -> Vec<(u32, u32)> {
390 let mut ids = Vec::new();
391 let mut path = Vec::new();
392 walk_path(0, accounts, handle.get(), &mut path, &mut |level, index| {
393 ids.push((level, index));
394 zero_subtree(level)
395 });
396 ids.sort_unstable();
397 ids.dedup();
398 ids
399}
400
401#[derive(Debug, Clone, PartialEq, Eq)]
403#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
404pub struct BalanceProof<const P: u8 = 2> {
405 pub currency: Currency,
407 pub layer: Layer,
409 pub handle: AccountHandle,
411 pub balance: Balance<P>,
413 pub path: InclusionProof,
415}
416
417impl<const P: u8> BalanceProof<P> {
418 pub fn verify(&self, seal: &Seal<P>) -> Result<(), ProofError> {
428 let tree = seal
429 .tree(self.currency, self.layer)
430 .ok_or(ProofError::NoSuchTree {
431 currency: self.currency,
432 layer: self.layer,
433 })?;
434 if self.path.index != u64::from(self.handle.get()) {
435 return Err(ProofError::HandleMismatch {
436 index: self.path.index,
437 handle: self.handle.get(),
438 });
439 }
440 let accounts = seal.parts().accounts;
441 if self.path.size != u64::from(accounts) {
442 return Err(ProofError::AccountCountMismatch {
443 tree_size: self.path.size,
444 accounts,
445 });
446 }
447 let computed = self
448 .path
449 .root_from(&leaf_hash(&balance_leaf_bytes(&self.balance)))?;
450 if computed == tree.root {
451 Ok(())
452 } else {
453 Err(ProofError::RootMismatch {
454 expected: tree.root,
455 computed,
456 })
457 }
458 }
459}
460
461pub fn build_seal_trees<const P: u8>(
479 accounts: u32,
480 balances: impl IntoIterator<Item = (AccountHandle, Currency, Layer, Balance<P>)>,
481) -> Result<Vec<BalanceTree<P>>, ProofError> {
482 let mut trees: BTreeMap<(Currency, Layer), BalanceTree<P>> = BTreeMap::new();
483 for (handle, currency, layer, balance) in balances {
484 if balance.is_zero() {
485 continue;
486 }
487 let tree = trees
488 .entry((currency, layer))
489 .or_insert_with(|| BalanceTree::new(currency, layer, accounts));
490 if tree.leaves.contains_key(&handle.get()) {
491 return Err(ProofError::DuplicateBalance {
492 handle: handle.get(),
493 });
494 }
495 tree.set(handle, balance)?;
496 }
497 Ok(trees.into_values().collect())
498}
499
500#[cfg(test)]
501mod tests {
502 use super::*;
503 use crate::log::hash::counter;
504 use crate::log::root_of;
505 use crate::model::{Amount, EUR, SealParts, Side};
506
507 fn balance(minor: i128) -> Balance {
508 Balance::ZERO
509 .post(Side::Debit, Amount::from_minor(minor).unwrap())
510 .unwrap()
511 }
512
513 #[test]
514 fn a_million_accounts_with_a_thousand_balances_take_under_50000_hashes() {
515 let accounts = 1_000_000u32;
516 let mut tree = BalanceTree::new(EUR, Layer::Settled, accounts);
517 let handles: Vec<u32> = (0..1000u32).map(|i| i * 997 + 3).collect();
518 for (i, handle) in handles.iter().enumerate() {
519 tree.set(AccountHandle::new(*handle).unwrap(), balance(i as i128 + 1))
520 .unwrap();
521 }
522 zeros();
523 let (root, hashes) = counter::count(|| tree.root());
524 assert!(hashes < 50_000, "{hashes} hashes");
525
526 let head = tree.head().unwrap();
527 let seal = Seal::new(SealParts {
528 period: crate::model::Label::new("p").unwrap(),
529 start: time::macros::date!(2026 - 01 - 01),
530 end: time::macros::date!(2026 - 01 - 31),
531 through: 1,
532 accounts,
533 trees: vec![head],
534 })
535 .unwrap();
536 assert_eq!(seal.tree(EUR, Layer::Settled).unwrap().root, root);
537 let zero_handles = (0..1000u32)
538 .map(|i| i * 991 + 1)
539 .filter(|h| !handles.contains(h));
540 for handle in handles.iter().copied().chain(zero_handles) {
541 let proof = tree.prove(AccountHandle::new(handle).unwrap()).unwrap();
542 proof.verify(&seal).unwrap();
543 }
544 }
545
546 #[test]
547 fn sparse_roots_equal_the_explicit_tree() {
548 for accounts in 1..=40u32 {
549 let mut tree = BalanceTree::<2>::new(EUR, Layer::Settled, accounts);
550 let mut explicit = Vec::new();
551 for handle in 0..accounts {
552 let b = if handle % 3 == 1 {
553 balance(i128::from(handle))
554 } else {
555 Balance::ZERO
556 };
557 tree.set(AccountHandle::new(handle).unwrap(), b).unwrap();
558 explicit.push(leaf_hash(&balance_leaf_bytes(&b)));
559 }
560 assert_eq!(tree.root(), root_of(&explicit), "accounts = {accounts}");
561 }
562 }
563}