1use spine::{Hasher, frontier_for_size, nary_mr};
15
16use crate::consistency::ProofStep;
17use crate::error::{Error, Result};
18use crate::mountain::{bag_path, bag_peaks, mountain_skeleton};
19use crate::schedule::reduction_count;
20
21pub trait NodeReader {
30 type Error;
32
33 fn get_node(
36 &self,
37 alg_id: u64,
38 left: u64,
39 height: u32,
40 ) -> impl std::future::Future<Output = ReadResult<Option<Vec<u8>>, Self>> + Send;
41
42 fn get_leaf(
44 &self,
45 index: u64,
46 ) -> impl std::future::Future<Output = ReadResult<Vec<u8>, Self>> + Send;
47}
48
49pub type ReadResult<T, R> = std::result::Result<T, <R as NodeReader>::Error>;
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum LogKind {
60 Flat,
62 Subtree,
64}
65
66impl LogKind {
67 #[must_use]
69 pub fn to_byte(self) -> u8 {
70 match self {
71 Self::Flat => 0,
72 Self::Subtree => 1,
73 }
74 }
75
76 #[must_use]
80 pub fn from_byte(b: u8) -> Option<Self> {
81 match b {
82 0 => Some(Self::Flat),
83 1 => Some(Self::Subtree),
84 _ => None,
85 }
86 }
87}
88
89#[derive(Debug)]
94pub struct AlgView {
95 pub hasher: Box<dyn Hasher>,
97 pub epochs: Vec<(u64, u64)>,
100 pub frontier: Vec<Vec<u8>>,
102 pub frontier_coords: Vec<(u64, u32)>,
104}
105
106impl Clone for AlgView {
107 fn clone(&self) -> Self {
108 Self {
109 hasher: self.hasher.clone_box(),
110 epochs: self.epochs.clone(),
111 frontier: self.frontier.clone(),
112 frontier_coords: self.frontier_coords.clone(),
113 }
114 }
115}
116
117impl AlgView {
118 #[must_use]
120 pub fn is_active(&self) -> bool {
121 self.epochs.last().is_some_and(|&(_, end)| end == u64::MAX)
122 }
123
124 #[must_use]
126 pub fn is_active_at(&self, i: u64) -> bool {
127 self.epochs
128 .iter()
129 .any(|&(start, end)| start <= i && i < end)
130 }
131
132 #[must_use]
137 pub fn tree_size(&self, global_size: u64) -> u64 {
138 if self.is_active() {
139 global_size
140 } else {
141 self.epochs.last().map_or(0, |&(_, end)| end)
142 }
143 }
144
145 #[must_use]
147 pub fn first_activation(&self) -> u64 {
148 self.epochs.first().map_or(0, |&(start, _)| start)
149 }
150
151 #[must_use]
155 pub fn active_range(&self, lo: u64, hi: u64) -> bool {
156 self.epochs
157 .iter()
158 .any(|&(start, end)| start < hi && end > lo)
159 }
160
161 #[must_use]
163 pub fn fully_active(&self, lo: u64, hi: u64) -> bool {
164 self.epochs
165 .iter()
166 .any(|&(start, end)| start <= lo && hi <= end)
167 }
168
169 #[must_use]
176 pub fn effective_size_at(&self, size: u64) -> u64 {
177 if size <= self.first_activation() {
178 size
181 } else if self.is_active_at(size - 1) {
182 size
183 } else {
184 self.epochs
185 .iter()
186 .filter(|&&(_, end)| end < size)
187 .map(|&(_, end)| end)
188 .max()
189 .unwrap_or(0)
190 }
191 }
192
193 #[must_use]
203 pub fn epochs_at(&self, size: u64) -> Option<Vec<(u64, u64)>> {
204 let mut out = Vec::new();
205 for &(start, end) in &self.epochs {
206 if start > size {
207 break;
208 }
209 if end > size {
210 out.push((start, u64::MAX));
211 break;
212 }
213 out.push((start, end));
214 }
215 if out.is_empty() { None } else { Some(out) }
216 }
217}
218
219#[must_use]
222pub fn compute_root(view: &AlgView, k: usize) -> Vec<u8> {
223 bag_peaks(view.hasher.as_ref(), &view.frontier, k as u64)
224}
225
226pub fn carry<E>(
239 view: &mut AlgView,
240 alg_id: u64,
241 digest: Vec<u8>,
242 count: u64,
243 arity: u64,
244 out_nodes: &mut Vec<(u64, u64, u32, Vec<u8>)>,
245) -> Result<(), E> {
246 view.frontier.push(digest);
247 view.frontier_coords.push((count, 0));
248
249 let merges = reduction_count(count, arity);
250 for _ in 0..merges {
251 let mut children = Vec::with_capacity(arity as usize);
252 let mut coords = Vec::with_capacity(arity as usize);
253 for _ in 0..arity as usize {
254 children.push(view.frontier.pop().ok_or_else(|| Error::Corrupted {
255 alg_id,
256 reason: "frontier stack underflow during reduction".to_string(),
257 })?);
258 coords.push(view.frontier_coords.pop().ok_or_else(|| Error::Corrupted {
259 alg_id,
260 reason: "frontier_coords stack underflow during reduction".to_string(),
261 })?);
262 }
263 children.reverse();
264 coords.reverse();
265 let child_refs: Vec<&[u8]> = children.iter().map(|c| c.as_slice()).collect();
266 let parent = nary_mr(view.hasher.as_ref(), &child_refs);
267
268 let parent_left_index = coords[0].0;
269 let parent_height = coords[0].1 + 1;
270
271 if parent != view.hasher.null() {
272 out_nodes.push((alg_id, parent_left_index, parent_height, parent.clone()));
273 }
274
275 view.frontier.push(parent);
276 view.frontier_coords
277 .push((parent_left_index, parent_height));
278 }
279 Ok(())
280}
281
282pub async fn get_node_hash<R: NodeReader>(
290 reader: &R,
291 view: &AlgView,
292 alg_id: u64,
293 left: u64,
294 height: u32,
295 arity: u64,
296) -> Result<Vec<u8>, R::Error> {
297 let cap = match arity.checked_pow(height) {
298 Some(c) => c,
299 None => return Ok(view.hasher.null()),
300 };
301 let limit = match left.checked_add(cap) {
302 Some(val) => val,
303 None => return Ok(view.hasher.null()),
304 };
305 if !view.active_range(left, limit) {
306 return Ok(view.hasher.null());
307 }
308 if let Some(hash) = reader
309 .get_node(alg_id, left, height)
310 .await
311 .map_err(Error::Read)?
312 {
313 let expected = view.hasher.null().len();
314 if hash.len() != expected {
315 return Err(Error::Corrupted {
316 alg_id,
317 reason: format!(
318 "node at left {left} height {height} has wrong digest length: expected \
319 {expected}, got {}",
320 hash.len()
321 ),
322 });
323 }
324 Ok(hash)
325 } else {
326 Err(Error::Corrupted {
332 alg_id,
333 reason: format!(
334 "missing internal node for algorithm {alg_id} at left {left} height {height}"
335 ),
336 })
337 }
338}
339
340pub async fn inclusion_proof<R: NodeReader>(
348 reader: &R,
349 view: &AlgView,
350 alg_id: u64,
351 index: u64,
352 tree_size: u64,
353 global_size: u64,
354 arity: u64,
355) -> Result<Option<crate::consistency::InclusionProof>, R::Error> {
356 let max_size = view.tree_size(global_size);
357
358 if tree_size == 0 || index >= tree_size || tree_size > max_size {
359 return Ok(None);
360 }
361
362 let k = arity;
363 let coords = frontier_for_size(tree_size, k);
364
365 let mut target_f_idx = None;
366 for (f_idx, &(left, height)) in coords.iter().enumerate() {
367 let cap = k.pow(height);
368 if index >= left && index < left + cap {
369 target_f_idx = Some((f_idx, left, height));
370 break;
371 }
372 }
373
374 let (f_idx, left, height) = match target_f_idx {
375 Some(val) => val,
376 None => return Ok(None),
377 };
378
379 let mut path = Vec::new();
380 log_level_bisection_path_to_height(
381 reader, view, alg_id, left, height, index, 0, arity, &mut path,
382 )
383 .await?;
384 path.reverse();
385
386 let mut peaks = Vec::with_capacity(coords.len());
387 for &(l, h) in &coords {
388 let hash = get_node_hash(reader, view, alg_id, l, h, arity).await?;
389 peaks.push(hash);
390 }
391
392 let bag = bag_path(&peaks, f_idx, view.hasher.as_ref(), arity);
395 path.extend(bag);
396
397 debug_assert!(
401 mountain_skeleton(k, tree_size, index).is_some_and(|skeleton| {
402 skeleton.len() == path.len()
403 && path.iter().zip(skeleton.iter()).all(|(step, shape)| {
404 step.position == shape.position && step.siblings.len() == shape.sibling_count
405 })
406 }),
407 "generated inclusion proof must match the canonical mountain skeleton"
408 );
409
410 Ok(Some(crate::consistency::InclusionProof { path }))
411}
412
413pub async fn leaf_proof<R: NodeReader>(
420 reader: &R,
421 view: &AlgView,
422 alg_id: u64,
423 index: u64,
424 tree_size: u64,
425 global_size: u64,
426 arity: u64,
427) -> Result<Option<spine::LeafProof>, R::Error> {
428 let Some(proof) =
429 inclusion_proof(reader, view, alg_id, index, tree_size, global_size, arity).await?
430 else {
431 return Ok(None);
432 };
433 let leaf_hash = get_node_hash(reader, view, alg_id, index, 0, arity).await?;
435 Ok(Some(spine::LeafProof::new(
436 leaf_hash, index, tree_size, arity, proof.path,
437 )))
438}
439
440#[allow(clippy::too_many_arguments)]
447pub async fn consistency_proof<R: NodeReader>(
448 reader: &R,
449 view: &AlgView,
450 alg_id: u64,
451 old_size: u64,
452 new_size: u64,
453 global_size: u64,
454 arity: u64,
455) -> Result<Option<crate::consistency::ConsistencyProof>, R::Error> {
456 let max_size = view.tree_size(global_size);
457
458 if old_size == 0 || old_size >= new_size || new_size > max_size {
459 return Ok(None);
460 }
461
462 let k = arity;
463 let old_coords = frontier_for_size(old_size, k);
464 let &(boundary_left, boundary_height) = old_coords.last().ok_or_else(|| Error::Corrupted {
465 alg_id,
466 reason: "empty old_coords for non-zero old_size".to_string(),
467 })?;
468
469 let start_hash =
470 get_node_hash(reader, view, alg_id, boundary_left, boundary_height, arity).await?;
471
472 let new_coords = frontier_for_size(new_size, k);
473 let mut target_new_f_idx = None;
474 for (f_idx, &(new_left, new_height)) in new_coords.iter().enumerate() {
475 let cap = k.pow(new_height);
476 if boundary_left >= new_left && boundary_left < new_left + cap {
477 target_new_f_idx = Some((f_idx, new_left, new_height));
478 break;
479 }
480 }
481
482 let (f_idx, left, height) = match target_new_f_idx {
483 Some(val) => val,
484 None => return Ok(None),
485 };
486
487 if height < boundary_height {
488 return Ok(None);
489 }
490
491 let mut peak_path = Vec::new();
497 log_level_bisection_path_to_height(
498 reader,
499 view,
500 alg_id,
501 left,
502 height,
503 boundary_left,
504 boundary_height,
505 arity,
506 &mut peak_path,
507 )
508 .await?;
509 peak_path.reverse();
510
511 let mut new_peaks = Vec::with_capacity(new_coords.len());
512 for &(l, h) in &new_coords {
513 let hash = get_node_hash(reader, view, alg_id, l, h, arity).await?;
514 new_peaks.push(hash);
515 }
516
517 debug_assert!(
521 mountain_skeleton(k, new_size, boundary_left).is_some_and(|skeleton| {
522 let (bh, nh) = (boundary_height as usize, height as usize);
523 skeleton.len() >= nh
524 && skeleton[bh..nh].len() == peak_path.len()
525 && peak_path
526 .iter()
527 .zip(&skeleton[bh..nh])
528 .all(|(step, shape)| {
529 step.position == shape.position
530 && step.siblings.len() == shape.sibling_count
531 })
532 }),
533 "generated consistency peak_path must match the canonical mountain skeleton"
534 );
535
536 Ok(Some(crate::consistency::ConsistencyProof {
537 boundary_hash: start_hash,
538 peak_path,
539 new_peaks,
540 split_index: f_idx,
541 }))
542}
543
544#[allow(clippy::too_many_arguments)]
545async fn log_level_bisection_path_to_height<R: NodeReader>(
546 reader: &R,
547 view: &AlgView,
548 alg_id: u64,
549 left_index: u64,
550 height: u32,
551 target_index: u64,
552 target_height: u32,
553 arity: u64,
554 path: &mut Vec<ProofStep>,
555) -> Result<(), R::Error> {
556 let mut curr_left = left_index;
557 let mut curr_height = height;
558 let k = arity;
559
560 while curr_height > target_height {
561 let child_capacity = k.pow(curr_height - 1);
562 let child_idx = (target_index - curr_left) / child_capacity;
563
564 let mut siblings = Vec::with_capacity(arity as usize - 1);
565 for j in 0..arity as usize {
566 let j_u64 = j as u64;
567 if j_u64 == child_idx {
568 continue;
569 }
570 let c_left = curr_left + j_u64 * child_capacity;
571 let hash = get_node_hash(reader, view, alg_id, c_left, curr_height - 1, arity).await?;
572 siblings.push(hash);
573 }
574
575 path.push(ProofStep {
576 siblings,
577 position: child_idx as usize,
578 });
579
580 curr_left += child_idx * child_capacity;
581 curr_height -= 1;
582 }
583 Ok(())
584}
585
586pub async fn peak_at<R: NodeReader>(
594 reader: &R,
595 view: &AlgView,
596 alg_id: u64,
597 left: u64,
598 height: u32,
599 arity: u64,
600) -> Result<Vec<u8>, R::Error> {
601 get_node_hash(reader, view, alg_id, left, height, arity).await
602}
603
604pub async fn root_for_at<R: NodeReader>(
610 reader: &R,
611 view: &AlgView,
612 alg_id: u64,
613 size: u64,
614 arity: u64,
615) -> Result<Vec<u8>, R::Error> {
616 let alg_size = view.effective_size_at(size);
617
618 if alg_size == 0 {
619 return Ok(view.hasher.empty());
620 }
621
622 let coords = frontier_for_size(alg_size, arity);
623
624 let mut frontier = Vec::with_capacity(coords.len());
625 for &(left, height) in &coords {
626 let hash = get_node_hash(reader, view, alg_id, left, height, arity).await?;
627 frontier.push(hash);
628 }
629
630 Ok(bag_peaks(view.hasher.as_ref(), &frontier, arity))
631}
632
633pub async fn frontier_peaks<R: NodeReader>(
642 reader: &R,
643 view: &AlgView,
644 alg_id: u64,
645 size: u64,
646 arity: u64,
647) -> Result<Vec<Vec<u8>>, R::Error> {
648 let coords = frontier_for_size(size, arity);
649 let mut peaks = Vec::with_capacity(coords.len());
650 for &(left, height) in &coords {
651 peaks.push(peak_at(reader, view, alg_id, left, height, arity).await?);
652 }
653 Ok(peaks)
654}
655
656pub fn validate_epochs<E>(alg_id: u64, epochs: &[(u64, u64)], global_size: u64) -> Result<(), E> {
663 if epochs.is_empty() {
664 return Err(Error::Corrupted {
665 alg_id,
666 reason: "epoch sequence is empty".to_string(),
667 });
668 }
669 let mut last_end = 0;
670 for (i, &(start, end)) in epochs.iter().enumerate() {
671 if start > end {
672 return Err(Error::Corrupted {
673 alg_id,
674 reason: format!("epoch start {start} exceeds end {end}"),
675 });
676 }
677 if start < last_end {
678 return Err(Error::Corrupted {
679 alg_id,
680 reason: format!("epoch start {start} is less than prior end {last_end}"),
681 });
682 }
683 if end != u64::MAX && end > global_size {
684 return Err(Error::Corrupted {
685 alg_id,
686 reason: format!("epoch end {end} exceeds global size {global_size}"),
687 });
688 }
689 if end == u64::MAX && i != epochs.len() - 1 {
690 return Err(Error::Corrupted {
691 alg_id,
692 reason: "open epoch (end = u64::MAX) is not the final entry".to_string(),
693 });
694 }
695 last_end = end;
696 }
697 if let Some(&(start, end)) = epochs.last() {
698 if end == u64::MAX && start > global_size {
699 return Err(Error::Corrupted {
700 alg_id,
701 reason: format!("active epoch start {start} exceeds global size {global_size}"),
702 });
703 }
704 }
705 Ok(())
706}
707
708pub async fn reconstruct_view<R: NodeReader>(
717 reader: &R,
718 alg_id: u64,
719 hasher: Box<dyn Hasher>,
720 epochs: &[(u64, u64)],
721 global_size: u64,
722 arity: u64,
723) -> Result<AlgView, R::Error> {
724 validate_epochs(alg_id, epochs, global_size)?;
725
726 let is_active = epochs.last().is_some_and(|&(_, end)| end == u64::MAX);
727 let tree_size = if is_active {
728 global_size
729 } else {
730 epochs.last().map_or(0, |&(_, end)| end)
731 };
732
733 let mut view = AlgView {
734 hasher,
735 epochs: epochs.to_vec(),
736 frontier: Vec::new(),
737 frontier_coords: Vec::new(),
738 };
739
740 if tree_size == 0 {
741 return Ok(view);
742 }
743
744 let coords = frontier_for_size(tree_size, arity);
745 let mut frontier = Vec::with_capacity(coords.len());
746 for &(left, height) in &coords {
747 let cap = arity.pow(height);
748 let hash = if !view.active_range(left, left + cap) {
749 view.hasher.null()
750 } else {
751 reader
752 .get_node(alg_id, left, height)
753 .await
754 .map_err(Error::Read)?
755 .ok_or_else(|| Error::Corrupted {
756 alg_id,
757 reason: format!(
758 "missing frontier node for algorithm {alg_id} at left {left} height \
759 {height}"
760 ),
761 })?
762 };
763 frontier.push(hash);
764 }
765
766 view.frontier = frontier;
767 view.frontier_coords = coords;
768
769 Ok(view)
770}
771
772#[allow(clippy::type_complexity)]
781#[allow(clippy::too_many_arguments)]
782pub fn reconstruct_subtree_root<'a, R>(
783 reader: &'a R,
784 alg_id: u64,
785 view: &'a AlgView,
786 lo: u64,
787 hi: u64,
788 k: u64,
789 kind: LogKind,
790 store_mixed: bool,
791) -> std::pin::Pin<
792 Box<
793 dyn std::future::Future<Output = Result<(Vec<u8>, Vec<(u64, u32, Vec<u8>)>), R::Error>>
794 + Send
795 + 'a,
796 >,
797>
798where
799 R: NodeReader + Sync + 'a,
800{
801 Box::pin(async move {
802 let size = hi - lo;
803 if size == 0 {
804 return Ok((view.hasher.empty(), Vec::new()));
805 }
806 if size == 1 {
807 if view.is_active_at(lo) {
808 if kind == LogKind::Subtree {
809 if let Some(hash) = reader.get_node(alg_id, lo, 0).await.map_err(Error::Read)? {
810 let expected = view.hasher.null().len();
811 if hash.len() != expected {
812 return Err(Error::Corrupted {
813 alg_id,
814 reason: format!(
815 "subtree node at left {lo} height 0 has wrong digest length: \
816 expected {expected}, got {}",
817 hash.len()
818 ),
819 });
820 }
821 return Ok((hash, Vec::new()));
822 } else {
823 return Ok((view.hasher.null(), Vec::new()));
824 }
825 } else {
826 let data = reader.get_leaf(lo).await.map_err(Error::Read)?;
827 return Ok((view.hasher.leaf(&data), Vec::new()));
828 }
829 }
830 return Ok((view.hasher.null(), Vec::new()));
831 }
832
833 if !view.active_range(lo, hi) {
834 return Ok((view.hasher.null(), Vec::new()));
835 }
836
837 let is_power_of_k = {
838 let mut temp = size;
839 while temp % k == 0 {
840 temp /= k;
841 }
842 temp == 1
843 };
844
845 if is_power_of_k {
846 let height = {
847 let mut h = 0;
848 let mut temp = size;
849 while temp > 1 {
850 temp /= k;
851 h += 1;
852 }
853 h as u32
854 };
855 if view.fully_active(lo, hi) {
856 if let Some(hash) = reader
857 .get_node(alg_id, lo, height)
858 .await
859 .map_err(Error::Read)?
860 {
861 let expected = view.hasher.null().len();
862 if hash.len() != expected {
863 return Err(Error::Corrupted {
864 alg_id,
865 reason: format!(
866 "node at left {lo} height {height} has wrong digest length: \
867 expected {expected}, got {}",
868 hash.len()
869 ),
870 });
871 }
872 return Ok((hash, Vec::new()));
873 }
874 }
875
876 let child_size = size / k;
877 let mut child_hashes = Vec::with_capacity(k as usize);
878 let mut mixed_nodes = Vec::new();
879 for j in 0..k {
880 let c_lo = lo + j * child_size;
881 let c_hi = lo + (j + 1) * child_size;
882 let (child_hash, child_mixed) = reconstruct_subtree_root(
883 reader,
884 alg_id,
885 view,
886 c_lo,
887 c_hi,
888 k,
889 kind,
890 store_mixed,
891 )
892 .await?;
893 child_hashes.push(child_hash);
894 mixed_nodes.extend(child_mixed);
895 }
896 let child_refs: Vec<&[u8]> = child_hashes.iter().map(|c| c.as_slice()).collect();
897 let hash = nary_mr(view.hasher.as_ref(), &child_refs);
898
899 if store_mixed {
900 mixed_nodes.push((lo, height, hash.clone()));
901 }
902 Ok((hash, mixed_nodes))
903 } else {
904 let coords = frontier_for_size(size, k);
905 let mut component_hashes = Vec::with_capacity(coords.len());
906 let mut mixed_nodes = Vec::new();
907 for &(part_left, part_height) in &coords {
908 let cap = k.pow(part_height);
909 let c_lo = lo + part_left;
910 let c_hi = c_lo + cap;
911 let (part_root, part_mixed) = reconstruct_subtree_root(
912 reader,
913 alg_id,
914 view,
915 c_lo,
916 c_hi,
917 k,
918 kind,
919 store_mixed,
920 )
921 .await?;
922 component_hashes.push(part_root);
923 mixed_nodes.extend(part_mixed);
924 }
925
926 let root = bag_peaks(view.hasher.as_ref(), &component_hashes, k);
930 Ok((root, mixed_nodes))
931 }
932 })
933}
934
935#[cfg(test)]
936mod tests {
937 use sha2::Digest as _;
938
939 use super::*;
940
941 #[derive(Debug)]
944 struct TestHasher;
945 impl Hasher for TestHasher {
946 fn leaf(&self, data: &[u8]) -> Vec<u8> {
947 sha2::Sha256::digest(data).to_vec()
948 }
949
950 fn node(&self, children: &[&[u8]]) -> Vec<u8> {
951 let mut h = sha2::Sha256::new();
952 for c in children {
953 h.update(c);
954 }
955 h.finalize().to_vec()
956 }
957
958 fn empty(&self) -> Vec<u8> {
959 sha2::Sha256::digest(b"").to_vec()
960 }
961
962 fn hash(&self, data: &[u8]) -> Vec<u8> {
963 sha2::Sha256::digest(data).to_vec()
964 }
965
966 fn clone_box(&self) -> Box<dyn Hasher> {
967 Box::new(TestHasher)
968 }
969 }
970
971 #[test]
975 fn epochs_at_snapshot_clamping() {
976 let view = AlgView {
977 hasher: Box::new(TestHasher),
978 epochs: vec![(2, 5), (7, 12), (15, u64::MAX)],
979 frontier: Vec::new(),
980 frontier_coords: Vec::new(),
981 };
982 assert_eq!(view.epochs_at(1), None);
984 assert_eq!(view.epochs_at(3), Some(vec![(2, u64::MAX)]));
986 assert_eq!(view.epochs_at(5), Some(vec![(2, 5)]));
988 assert_eq!(view.epochs_at(6), Some(vec![(2, 5)]));
990 assert_eq!(view.epochs_at(7), Some(vec![(2, 5), (7, u64::MAX)]));
992 assert_eq!(
994 view.epochs_at(20),
995 Some(vec![(2, 5), (7, 12), (15, u64::MAX)])
996 );
997 }
998}