diff --git a/packages/utreexo/src/stump/accumulator.cairo b/packages/utreexo/src/stump/accumulator.cairo index 82955140..9be0c3c9 100644 --- a/packages/utreexo/src/stump/accumulator.cairo +++ b/packages/utreexo/src/stump/accumulator.cairo @@ -1,6 +1,3 @@ -use utils::bits::bit_len; -use utils::sort::bubble_sort; -use utils::bit_shifts::{shl, shr}; use crate::stump::state::UtreexoStumpState; use crate::stump::proof::{UtreexoBatchProof, UtreexoBatchProofTrait}; @@ -12,12 +9,65 @@ pub impl StumpUtreexoAccumulatorImpl of StumpUtreexoAccumulator { // https://github.com/mit-dci/rustreexo/blob/6a8fe53fa545df8f1a30733fa6ca9f7b0077d051/src/accumulator/stump.rs#L252 *self } - - /// Verifies that one or multiple leaves hashes are part of the utreexo forest given a proof. + + /// Verifies that specified leaves are part of the utreexo forest given a proof. fn verify( + self: @UtreexoStumpState, proof: @UtreexoBatchProof, leaves: Span + ) -> Result<(), ByteArray> { + let mut computed_roots = proof.compute_roots(leaves, *self.num_leaves)?; + let mut res = Result::Ok(()); + while let Option::Some(computed_root) = computed_roots.pop_front() { + let mut root_exists = false; + for root_opt in *self.roots { + if let Option::Some(root) = *root_opt { + if root == computed_root { + root_exists = true; + break; + } + } + }; + if !root_exists { + res = Result::Err("Proof verification failed"); + break; + } + }; + res + } + + /// Verifies that the specified leaves are part of the utreexo forest given the proof, + /// deletes them and returns an updated state. + fn verify_and_delete( + self: @UtreexoStumpState, proof: @UtreexoBatchProof, leaves: Span + ) -> Result { + let mut updated_roots = proof.compute_roots_with_deletion(leaves, *self.num_leaves)?; + let mut roots = array![]; + + for root_opt in *self.roots { + if let Option::Some(root) = root_opt { + if let Option::Some(updated_root) = updated_roots.get(0) { + let (old_root, new_root) = updated_root.unbox(); + if *root == *old_root { + updated_roots.pop_front().unwrap(); + roots.append(*new_root); + continue; + } + } + } + roots.append(*root_opt); + }; + + if !updated_roots.is_empty() { + return Result::Err("Proof verification failed"); + } + + Result::Ok(UtreexoStumpState { roots: roots.span(), num_leaves: *self.num_leaves }) + } + + /// Legacy implementation of proof verification. + fn verify_legacy( self: @UtreexoStumpState, proof: @UtreexoBatchProof, del_hashes: Span ) -> Result<(), ByteArray> { - let computed_roots: Span = proof.compute_roots(del_hashes, *self.num_leaves)?; + let computed_roots: Span = proof.compute_roots_legacy(del_hashes, *self.num_leaves)?; let mut number_matched_roots: u32 = 0; for i in 0 @@ -44,266 +94,521 @@ pub impl StumpUtreexoAccumulatorImpl of StumpUtreexoAccumulator { Result::Ok(()) } +} - fn delete( - self: @UtreexoStumpState, leaves: Span, proof: @UtreexoBatchProof - ) -> UtreexoStumpState { - if leaves.len() == 0 { - return *self; - } - - // Extra check that can be removed later. - assert(leaves.len() == (*proof.targets).len(), 'leaves do not match targets'); - - let updated_roots = empty_and_reroot(leaves, proof, *self.num_leaves) - .expect('failed to empty and reroot'); - - let mut roots = array![]; - let mut cnt = 0; - for root in *self - .roots { - if let Option::Some(new_root) = find_root(updated_roots.span(), *root) { - roots.append(new_root); - cnt += 1; - } else { - roots.append(*root); - } - }; +#[cfg(test)] +mod tests { + use super::{UtreexoStumpState, StumpUtreexoAccumulator, UtreexoBatchProof}; - assert(cnt == updated_roots.len(), 'invalid proof'); + #[test] + fn test_verification_1_legacy() { + let state = UtreexoStumpState { + roots: array![ + Option::Some(0x371cb6995ea5e7effcd2e174de264b5b407027a75a231a70c2c8d196107f0e7) + ] + .span(), + num_leaves: 2 + }; + let proof = UtreexoBatchProof { targets: array![0].span(), proof: array![2].span() }; + let del_hashes = array![1]; - UtreexoStumpState { roots: roots.span(), num_leaves: *self.num_leaves } + let result = state.verify_legacy(@proof, del_hashes.span(),); + assert_eq!(result, Result::Ok(())); } -} -impl PositionPartialOrd> of PartialOrd<(u64, T)> { - fn lt(lhs: (u64, T), rhs: (u64, T)) -> bool { - let (l, _) = lhs; - let (r, _) = rhs; - l < r + #[test] + fn test_verification_2_legacy() { + let state = UtreexoStumpState { + roots: array![ + Option::Some(0x1702d734e291ad551b886a70b96446b99e19e405511e71fb5edfc4d2d83ce92), + Option::Some(0x770ad1be69d195e821c8c35051b32492e71592e230b950a99ebf87e98967ca), + Option::Some(0x2392042cbfda7371c81c9d7b456563533c2d6998b9e690a0d97421e6ae51a98), + Option::Some(0xf), + ] + .span(), + num_leaves: 15 + }; + let proof = UtreexoBatchProof { + targets: array![1, 3, 10, 13].span(), + proof: array![ + 0x1, + 0x3, + 0xC, + 0xD, + 0x436e91732c0a83fa238d71460463f4b1fe0dc0b1ebcbc10967a84cec9d13154, + 0xdc9cc50aff0bdadd82a05bbab54015a07fccf2a4e30fa528fdca5a35d5423f + ] + .span() + }; + let del_hashes = array![2, 4, 11, 14]; + + let result = state.verify_legacy(@proof, del_hashes.span(),); + assert_eq!(result, Result::Ok(())); } -} -/// Sets leaves to None and recalculates parent roots. -fn empty_and_reroot( - leaves: Span, proof: @UtreexoBatchProof, total_leaves: u64, -) -> Result, Option)>, ByteArray> { - let mut roots: Array<(Option, Option)> = array![]; - - let total_rows = bit_len(total_leaves - 1); - let proof_positions = get_proof_positions(*proof.targets, total_leaves, total_rows); - - let mut provided_nodes = array![]; // tuples (position, (old_hash, new_hash)) - let mut i = 0; - while i < leaves.len() { - provided_nodes.append((*proof.targets[i], (Option::Some(*leaves[i]), Option::None))); - i += 1; - }; - - let mut i = 0; - while i < proof_positions.len() { - provided_nodes.append((*proof_positions[i], (*proof.proof[i], *proof.proof[i]))); - i += 1; - }; - - provided_nodes = bubble_sort(provided_nodes.span()); - - let mut computed_nodes = array![]; - let mut computed_index = 0; - let mut provided_index = 0; - let mut error = Option::None; - loop { - if let Option::Some((pos, (old_hash, new_hash))) = - get_next( - ref computed_nodes, ref provided_nodes, ref computed_index, ref provided_index - ) { - if is_root_position(pos, total_leaves, total_rows) { - roots.append((old_hash, new_hash)); - continue; - } + #[test] + fn test_verification_3_legacy() { + let state = UtreexoStumpState { + roots: array![ + Option::Some(0x519631921e4905a63203f0cca7f6e6917082f30cef0930aa05bdc4323f6a398), + Option::Some(0x5198dcd61c969dfa8396dd27439ab776d120c2d67294fbcded0aa5f658f9150), + Option::Some(0x21d7ab8efac0146b5b47c8ad5431c3d14d9210319b0be7428fb2382ef115671), + Option::Some(0x74f794e653e00357d8a8ed45fcb74659841190c0821aa4e20bc4e30b2f3dd20), + ] + .span(), + num_leaves: 30 + }; + let proof = UtreexoBatchProof { + targets: array![4, 8, 12, 16, 20, 24, 28].span(), + proof: array![ + 0x6, + 0xA, + 0xE, + 0x12, + 0x16, + 0x1A, + 0x1E, + 0x2797a40dbb8ea4b69a4e3bb4a9ccaa21a9585fcc71f3e5bb053ccae27910f90, + 0x7877cc14d4c8e76cc51aa4c49aa7aadaade0cf475ad63bb37c27c324e145393, + 0x556ea8bad1db13c6bdc3150a8289cd12044fb7e03cf201f35924a8afd4265a6, + 0x41a4ec75a27497daa51261588a60f0956d3fd61e521634bbf36bba6343c3a1b, + 0x3ba731d3734536d7cd5382cb4004ca4c24f1325b6fbeae27bcd6b4f9c0ed714, + 0x117ed04a65093683f13c16cf73d2855f1f099a96581d1dad74eaf34c9a343c8, + 0x79b32f615bbd57783700ae5f8e7b1ef79677c3545c4c69dc31b3aecce1d8fa6 + ] + .span() + }; - if let Option::Some((sibling_pos, (sibling_old_hash, sibling_new_hash))) = - get_next( - ref computed_nodes, ref provided_nodes, ref computed_index, ref provided_index - ) { - if sibling_pos != pos ^ 1 { - error = Option::Some(format!("Missing sibling for {}", pos)); - break; - } + let del_hashes = array![5, 9, 13, 17, 21, 25, 29]; - let parent_pos = get_parent_position(pos, total_rows); - let old_parent_hash = get_parent_hash(old_hash, sibling_old_hash); - let new_parent_hash = get_parent_hash(new_hash, sibling_new_hash); + let result = state.verify_legacy(@proof, del_hashes.span(),); + assert_eq!(result, Result::Ok(())); + } - computed_nodes.append((parent_pos, (old_parent_hash, new_parent_hash))); - } else { - error = Option::Some(format!("Missing sibling for {}", pos)); - break; - } - } else { - break; + #[test] + fn test_verification_1() { + let state = UtreexoStumpState { + roots: array![ + Option::Some(0x371cb6995ea5e7effcd2e174de264b5b407027a75a231a70c2c8d196107f0e7) + ] + .span(), + num_leaves: 2 }; - }; + let proof = UtreexoBatchProof { targets: array![0].span(), proof: array![2].span() }; + let del_hashes = array![1]; - if error.is_some() { - return Result::Err(error.unwrap()); + let result = state.verify(@proof, del_hashes.span(),); + assert_eq!(result, Result::Ok(())); } - Result::Ok(roots) -} + #[test] + fn test_verification_2() { + let state = UtreexoStumpState { + roots: array![ + Option::Some(0x1702d734e291ad551b886a70b96446b99e19e405511e71fb5edfc4d2d83ce92), + Option::Some(0x770ad1be69d195e821c8c35051b32492e71592e230b950a99ebf87e98967ca), + Option::Some(0x2392042cbfda7371c81c9d7b456563533c2d6998b9e690a0d97421e6ae51a98), + Option::Some(0xf), + ] + .span(), + num_leaves: 15 + }; + let proof = UtreexoBatchProof { + targets: array![1, 3, 10, 13].span(), + proof: array![ + 0x1, + 0x3, + 0xC, + 0xD, + 0x436e91732c0a83fa238d71460463f4b1fe0dc0b1ebcbc10967a84cec9d13154, + 0xdc9cc50aff0bdadd82a05bbab54015a07fccf2a4e30fa528fdca5a35d5423f + ] + .span() + }; + let del_hashes = array![2, 4, 11, 14]; -fn get_proof_positions(targets: Span, total_leaves: u64, total_rows: u64,) -> Array { - let mut proof_positions: Array = array![]; - let mut computed_positions: Array = targets.into(); + let result = state.verify(@proof, del_hashes.span(),); + assert_eq!(result, Result::Ok(())); + } - let mut row = 0; - while row <= total_rows { - computed_positions = bubble_sort(computed_positions.span()); - let mut row_positions = get_row_positions(row, computed_positions.span(), total_rows); + #[test] + fn test_verification_3() { + let state = UtreexoStumpState { + roots: array![ + Option::Some(0x519631921e4905a63203f0cca7f6e6917082f30cef0930aa05bdc4323f6a398), + Option::Some(0x5198dcd61c969dfa8396dd27439ab776d120c2d67294fbcded0aa5f658f9150), + Option::Some(0x21d7ab8efac0146b5b47c8ad5431c3d14d9210319b0be7428fb2382ef115671), + Option::Some(0x74f794e653e00357d8a8ed45fcb74659841190c0821aa4e20bc4e30b2f3dd20), + ] + .span(), + num_leaves: 30 + }; + let proof = UtreexoBatchProof { + targets: array![4, 8, 12, 16, 20, 24, 28].span(), + proof: array![ + 0x6, + 0xA, + 0xE, + 0x12, + 0x16, + 0x1A, + 0x1E, + 0x2797a40dbb8ea4b69a4e3bb4a9ccaa21a9585fcc71f3e5bb053ccae27910f90, + 0x7877cc14d4c8e76cc51aa4c49aa7aadaade0cf475ad63bb37c27c324e145393, + 0x556ea8bad1db13c6bdc3150a8289cd12044fb7e03cf201f35924a8afd4265a6, + 0x41a4ec75a27497daa51261588a60f0956d3fd61e521634bbf36bba6343c3a1b, + 0x3ba731d3734536d7cd5382cb4004ca4c24f1325b6fbeae27bcd6b4f9c0ed714, + 0x117ed04a65093683f13c16cf73d2855f1f099a96581d1dad74eaf34c9a343c8, + 0x79b32f615bbd57783700ae5f8e7b1ef79677c3545c4c69dc31b3aecce1d8fa6 + ] + .span() + }; - while let Option::Some(node) = row_positions.pop_front() { - if is_root_position(node, total_leaves, total_rows) { - continue; - } + let del_hashes = array![5, 9, 13, 17, 21, 25, 29]; - if let Option::Some(next) = row_positions.get(0) { - if is_sibling(node, *(next.unbox())) { - row_positions.pop_front().unwrap(); - } else { - proof_positions.append(node ^ 1); - } - } else { - proof_positions.append(node ^ 1); - } + let result = state.verify(@proof, del_hashes.span(),); + assert_eq!(result, Result::Ok(())); + } - computed_positions.append(get_parent_position(node, total_rows)); + #[test] + fn test_verification_4() { + let state = UtreexoStumpState { + roots: array![ + Option::None, + Option::None, + Option::None, + Option::None, + Option::Some( + 2778277074578782368986165095004756321440748237082580104984033528445453379385 + ) + ] + .span(), + num_leaves: 16 }; - row += 1; - }; + assert_eq!( + state + .verify( + @UtreexoBatchProof { + targets: array![0].span(), + proof: array![ + 1, + 359114454570462701179676018441683730149326686283278794303413350979946254235, + 2920447154653459698578961030005574439730780339384884329678783637696763668074, + 361638375607381126502342109490869021742109399020176111227133094637834041180, + ] + .span() + }, + array![0].span() + ), + Result::Ok(()) + ); - proof_positions -} + assert_eq!( + state + .verify( + @UtreexoBatchProof { + targets: array![15].span(), + proof: array![ + 14, + 3009277733733429566191908933097273596911357184294896938062356457901603258437, + 2224391216833402212724735773212940252356536072547264452695587483243907176367, + 1010147253610699894986241683624609428739638206544914327400350896031110287324, + ] + .span() + }, + array![15].span() + ), + Result::Ok(()) + ); -fn get_next, +Drop>( - ref computed: Array<(u64, T)>, - ref provided: Array<(u64, T)>, - ref computed_pos: u32, - ref provided_pos: u32, -) -> Option<(u64, T)> { - let last_computed = computed.get(computed_pos); - let last_provided = provided.get(provided_pos); - - match (last_computed, last_provided) { - ( - Option::Some(node1), Option::Some(node2) - ) => { - let (pos1, hashes1) = *(node1.unbox()); - let (pos2, hashes2) = *(node2.unbox()); - if pos1 < pos2 { - computed_pos += 1; - Option::Some((pos1, hashes1)) - } else { - provided_pos += 1; - Option::Some((pos2, hashes2)) - } - }, - (Option::Some(node), Option::None) => { - computed_pos += 1; - Option::Some(*(node.unbox())) - }, - (Option::None, Option::Some(node)) => { - provided_pos += 1; - Option::Some(*(node.unbox())) - }, - (Option::None, Option::None) => Option::None, - } -} + assert_eq!( + state + .verify( + @UtreexoBatchProof { + targets: array![0, 2, 5].span(), + proof: array![ + 1, + 3, + 4, + 2476911194812244264213538976037850550079366744233323933541290896048104351430, + 361638375607381126502342109490869021742109399020176111227133094637834041180, + ] + .span() + }, + array![0, 2, 5].span() + ), + Result::Ok(()) + ); -fn get_row_positions(row: u64, computed_positions: Span, total_rows: u64) -> Array { - let mut res = array![]; - for x in computed_positions { - if detect_row(*x, total_rows) == row { - res.append(*x); - } - }; - res -} + assert_eq!( + state + .verify( + @UtreexoBatchProof { + targets: array![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] + .span(), + proof: array![].span() + }, + array![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15].span() + ), + Result::Ok(()) + ); -fn get_parent_position(pos: u64, total_rows: u64) -> u64 { - shr(pos, 1_u64) | shl(1_u64, total_rows) -} + assert_eq!( + state + .verify( + @UtreexoBatchProof { + targets: array![16, 2, 5].span(), + proof: array![ + 3, + 4, + 2476911194812244264213538976037850550079366744233323933541290896048104351430, + 361638375607381126502342109490869021742109399020176111227133094637834041180, + ] + .span() + }, + array![ + 2808234728617536643410270558566096818021668177546200472079927509023409591575, + 2, + 5 + ] + .span() + ), + Result::Ok(()) + ); -fn get_parent_hash(left: Option, right: Option) -> Option { - match (left.is_none(), right.is_none()) { - (true, true) => Option::None, - (true, false) => right, - (false, true) => left, - (false, false) => Option::Some(parent_hash(left.unwrap(), right.unwrap())), - } -} + assert_eq!( + state + .verify( + @UtreexoBatchProof { + targets: array![24, 18, 10, 15].span(), + proof: array![ + 11, + 14, + 2476911194812244264213538976037850550079366744233323933541290896048104351430, + 3115762988631556491925147498418117978906005591291390166955707455104569660364, + 3009277733733429566191908933097273596911357184294896938062356457901603258437, + ] + .span() + }, + array![ + 1229371501456391789924831928153470943555736434402105893904574254763197682709, + 3613143053999770272842665473502706307602279676970981102529837756866990003067, + 10, + 15, + ] + .span() + ), + Result::Ok(()) + ); -fn is_sibling(a: u64, b: u64) -> bool { - a ^ 1 == b -} + assert_eq!( + state + .verify( + @UtreexoBatchProof { + targets: array![24, 29].span(), + proof: array![ + 2920447154653459698578961030005574439730780339384884329678783637696763668074, + ] + .span() + }, + array![ + 1229371501456391789924831928153470943555736434402105893904574254763197682709, + 361638375607381126502342109490869021742109399020176111227133094637834041180, + ] + .span() + ), + Result::Ok(()) + ); -fn detect_row(pos: u64, total_rows: u64) -> u64 { - let mut marker: u64 = shl(1_u64, total_rows); - let mut h: u64 = 0; + assert_eq!( + state + .verify( + @UtreexoBatchProof { targets: array![28, 29].span(), proof: array![].span() }, + array![ + 1010147253610699894986241683624609428739638206544914327400350896031110287324, + 361638375607381126502342109490869021742109399020176111227133094637834041180, + ] + .span() + ), + Result::Ok(()) + ); - while pos & marker != 0 { - marker = shr(marker, 1_u64); - h += 1; - }; + assert_eq!( + state + .verify( + @UtreexoBatchProof { targets: array![30].span(), proof: array![].span() }, + array![ + 2778277074578782368986165095004756321440748237082580104984033528445453379385, + ] + .span() + ), + Result::Ok(()) + ); + } - h -} + #[test] + fn test_verification_5() { + let state = UtreexoStumpState { + roots: array![ + Option::Some(14), + Option::Some( + 3009277733733429566191908933097273596911357184294896938062356457901603258437 + ), + Option::Some( + 2224391216833402212724735773212940252356536072547264452695587483243907176367 + ), + Option::Some( + 1010147253610699894986241683624609428739638206544914327400350896031110287324 + ) + ] + .span(), + num_leaves: 15 + }; -fn is_root_position(position: u64, total_leaves: u64, total_rows: u64) -> bool { - let row = detect_row(position, total_rows); + assert_eq!( + state + .verify( + @UtreexoBatchProof { + targets: array![0].span(), + proof: array![ + 1, + 359114454570462701179676018441683730149326686283278794303413350979946254235, + 2920447154653459698578961030005574439730780339384884329678783637696763668074, + ] + .span() + }, + array![0].span() + ), + Result::Ok(()) + ); - let root_present = total_leaves & shl(1, row) != 0; - let root_pos = root_position(total_leaves, row, total_rows); + assert_eq!( + state + .verify( + @UtreexoBatchProof { targets: array![14].span(), proof: array![].span() }, + array![14].span() + ), + Result::Ok(()) + ); - root_present && root_pos == position -} + assert_eq!( + state + .verify( + @UtreexoBatchProof { targets: array![13].span(), proof: array![12].span() }, + array![13].span() + ), + Result::Ok(()) + ); -// TODO: undefined behavior if the given row doesn't have a root -fn root_position(total_leaves: u64, row: u64, total_rows: u64) -> u64 { - let mask = shl(2, total_rows) - 1; - let before = total_leaves & shl(mask, (row + 1)); + assert_eq!( + state + .verify( + @UtreexoBatchProof { + targets: array![0, 2, 5, 14].span(), + proof: array![ + 1, + 3, + 4, + 2476911194812244264213538976037850550079366744233323933541290896048104351430, + ] + .span() + }, + array![0, 2, 5, 14].span() + ), + Result::Ok(()) + ); - let shifted = shr(before, row) | shl(mask, (total_rows + 1 - row)); - shifted & mask -} + assert_eq!( + state + .verify( + @UtreexoBatchProof { + targets: array![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14].span(), + proof: array![].span() + }, + array![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14].span() + ), + Result::Ok(()) + ); -fn find_root( - roots: Span<(Option, Option)>, root: Option -) -> Option> { - let mut res = Option::None; - for (_old, _new) in roots { - if *_old == root { - res = Option::Some(*_new); - break; - } - }; - res -} + assert_eq!( + state + .verify( + @UtreexoBatchProof { + targets: array![16, 2, 5, 14].span(), + proof: array![ + 3, + 4, + 2476911194812244264213538976037850550079366744233323933541290896048104351430, + ] + .span() + }, + array![ + 2808234728617536643410270558566096818021668177546200472079927509023409591575, + 2, + 5, + 14 + ] + .span() + ), + Result::Ok(()) + ); + assert_eq!( + state + .verify( + @UtreexoBatchProof { targets: array![24, 25].span(), proof: array![].span() }, + array![ + 1229371501456391789924831928153470943555736434402105893904574254763197682709, + 2920447154653459698578961030005574439730780339384884329678783637696763668074, + ] + .span() + ), + Result::Ok(()) + ); -#[cfg(test)] -mod tests { - use super::{UtreexoStumpState, StumpUtreexoAccumulator, UtreexoBatchProof}; + assert_eq!( + state + .verify( + @UtreexoBatchProof { + targets: array![24, 26].span(), + proof: array![ + 2920447154653459698578961030005574439730780339384884329678783637696763668074 + ] + .span() + }, + array![ + 1229371501456391789924831928153470943555736434402105893904574254763197682709, + 2224391216833402212724735773212940252356536072547264452695587483243907176367, + ] + .span() + ), + Result::Ok(()) + ); + assert_eq!( + state + .verify( + @UtreexoBatchProof { targets: array![28].span(), proof: array![].span() }, + array![ + 1010147253610699894986241683624609428739638206544914327400350896031110287324, + ] + .span() + ), + Result::Ok(()) + ); + } + #[test] fn test_deletion_1_1() { let state = UtreexoStumpState { roots: array![Option::Some('a')].span(), num_leaves: 1 }; - let batch_proof = UtreexoBatchProof { targets: array![].span(), proof: array![].span() }; + let proof = UtreexoBatchProof { targets: array![].span(), proof: array![].span() }; - let target_hashes = array![]; + let leaves = array![]; - let new_state = state.delete(target_hashes.span(), @batch_proof); + let new_state = state.verify_and_delete(@proof, leaves.span()) + .expect('Verification failed'); assert_eq!(new_state.num_leaves, 1); assert_eq!(new_state.roots, array![Option::Some('a')].span()); @@ -313,11 +618,12 @@ mod tests { fn test_deletion_1_2() { let state = UtreexoStumpState { roots: array![Option::Some('a')].span(), num_leaves: 1 }; - let batch_proof = UtreexoBatchProof { targets: array![0].span(), proof: array![].span() }; + let proof = UtreexoBatchProof { targets: array![0].span(), proof: array![].span() }; - let target_hashes = array!['a']; + let leaves = array!['a']; - let new_state = state.delete(target_hashes.span(), @batch_proof); + let new_state = state.verify_and_delete(@proof, leaves.span()) + .expect('Verification failed'); assert_eq!(new_state.num_leaves, 1); assert_eq!(new_state.roots, array![Option::None].span()); @@ -338,23 +644,20 @@ mod tests { num_leaves: 8 }; - let batch_proof = UtreexoBatchProof { + let proof = UtreexoBatchProof { targets: array![0].span(), proof: array![ - Option::Some('b'), - Option::Some( - 1702961261074558847535372708423978610134065667337563473891781271138689292959 - ), - Option::Some( - 1970675917964935639615849678644334216784892342767290630432190461589093258001 - ), + 'b', + 1702961261074558847535372708423978610134065667337563473891781271138689292959, + 1970675917964935639615849678644334216784892342767290630432190461589093258001 ] .span() }; - let target_hashes = array!['a']; + let leaves = array!['a']; - let new_state = state.delete(target_hashes.span(), @batch_proof); + let new_state = state.verify_and_delete(@proof, leaves.span()) + .expect('Verification failed'); assert_eq!(new_state.num_leaves, 8); assert_eq!( @@ -386,22 +689,19 @@ mod tests { num_leaves: 8 }; - let batch_proof = UtreexoBatchProof { + let proof = UtreexoBatchProof { targets: array![0, 1].span(), proof: array![ - Option::Some( - 1702961261074558847535372708423978610134065667337563473891781271138689292959 - ), - Option::Some( - 1970675917964935639615849678644334216784892342767290630432190461589093258001 - ), + 1702961261074558847535372708423978610134065667337563473891781271138689292959, + 1970675917964935639615849678644334216784892342767290630432190461589093258001 ] .span() }; - let target_hashes = array!['a', 'b']; + let leaves = array!['a', 'b']; - let new_state = state.delete(target_hashes.span(), @batch_proof); + let new_state = state.verify_and_delete(@proof, leaves.span()) + .expect('Verification failed'); assert_eq!(new_state.num_leaves, 8); assert_eq!( @@ -433,20 +733,19 @@ mod tests { num_leaves: 8 }; - let batch_proof = UtreexoBatchProof { + let proof = UtreexoBatchProof { targets: array![0, 1, 2].span(), proof: array![ - Option::Some('d'), - Option::Some( - 1970675917964935639615849678644334216784892342767290630432190461589093258001 - ), + 'd', + 1970675917964935639615849678644334216784892342767290630432190461589093258001 ] .span() }; - let target_hashes = array!['a', 'b', 'c']; + let leaves = array!['a', 'b', 'c']; - let new_state = state.delete(target_hashes.span(), @batch_proof); + let new_state = state.verify_and_delete(@proof, leaves.span()) + .expect('Verification failed'); assert_eq!(new_state.num_leaves, 8); assert_eq!( @@ -478,13 +777,14 @@ mod tests { num_leaves: 8 }; - let batch_proof = UtreexoBatchProof { - targets: array![0, 1, 2, 3, 4, 5, 6].span(), proof: array![Option::Some('h')].span() + let proof = UtreexoBatchProof { + targets: array![0, 1, 2, 3, 4, 5, 6].span(), proof: array!['h'].span() }; - let target_hashes = array!['a', 'b', 'c', 'd', 'e', 'f', 'g']; + let leaves = array!['a', 'b', 'c', 'd', 'e', 'f', 'g']; - let new_state = state.delete(target_hashes.span(), @batch_proof); + let new_state = state.verify_and_delete(@proof, leaves.span()) + .expect('Verification failed'); assert_eq!(new_state.num_leaves, 8); assert_eq!( @@ -508,13 +808,14 @@ mod tests { num_leaves: 8 }; - let batch_proof = UtreexoBatchProof { + let proof = UtreexoBatchProof { targets: array![0, 1, 2, 3, 4, 5, 6, 7].span(), proof: array![].span() }; - let target_hashes = array!['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']; + let leaves = array!['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']; - let new_state = state.delete(target_hashes.span(), @batch_proof); + let new_state = state.verify_and_delete(@proof, leaves.span()) + .expect('Verification failed'); assert_eq!(new_state.num_leaves, 8); assert_eq!( @@ -538,20 +839,19 @@ mod tests { num_leaves: 7 }; - let batch_proof = UtreexoBatchProof { + let proof = UtreexoBatchProof { targets: array![0].span(), proof: array![ - Option::Some('b'), - Option::Some( - 1702961261074558847535372708423978610134065667337563473891781271138689292959 - ) + 'b', + 1702961261074558847535372708423978610134065667337563473891781271138689292959 ] .span() }; - let target_hashes = array!['a']; + let leaves = array!['a']; - let new_state = state.delete(target_hashes.span(), @batch_proof); + let new_state = state.verify_and_delete(@proof, leaves.span()) + .expect('Verification failed'); assert_eq!(new_state.num_leaves, 7); assert_eq!( @@ -585,14 +885,15 @@ mod tests { num_leaves: 7 }; - let batch_proof = UtreexoBatchProof { + let proof = UtreexoBatchProof { targets: array![0, 2, 4, 6].span(), - proof: array![Option::Some('b'), Option::Some('d'), Option::Some('f')].span() + proof: array!['b', 'd', 'f'].span() }; - let target_hashes = array!['a', 'c', 'e', 'g']; + let leaves = array!['a', 'c', 'e', 'g']; - let new_state = state.delete(target_hashes.span(), @batch_proof); + let new_state = state.verify_and_delete(@proof, leaves.span()) + .expect('Verification failed'); assert_eq!(new_state.num_leaves, 7); assert_eq!( @@ -624,14 +925,15 @@ mod tests { num_leaves: 7 }; - let batch_proof = UtreexoBatchProof { + let proof = UtreexoBatchProof { targets: array![1, 3, 5].span(), - proof: array![Option::Some('a'), Option::Some('c'), Option::Some('e')].span() + proof: array!['a', 'c', 'e'].span() }; - let target_hashes = array!['b', 'd', 'f']; + let leaves = array!['b', 'd', 'f']; - let new_state = state.delete(target_hashes.span(), @batch_proof); + let new_state = state.verify_and_delete(@proof, leaves.span()) + .expect('Verification failed'); assert_eq!(new_state.num_leaves, 7); assert_eq!( @@ -663,20 +965,19 @@ mod tests { num_leaves: 7 }; - let batch_proof = UtreexoBatchProof { + let proof = UtreexoBatchProof { targets: array![0, 6].span(), proof: array![ - Option::Some('b'), - Option::Some( - 1702961261074558847535372708423978610134065667337563473891781271138689292959 - ) + 'b', + 1702961261074558847535372708423978610134065667337563473891781271138689292959 ] .span() }; - let target_hashes = array!['a', 'g']; + let leaves = array!['a', 'g']; - let new_state = state.delete(target_hashes.span(), @batch_proof); + let new_state = state.verify_and_delete(@proof, leaves.span()) + .expect('Verification failed'); assert_eq!(new_state.num_leaves, 7); assert_eq!( @@ -710,13 +1011,14 @@ mod tests { num_leaves: 7 }; - let batch_proof = UtreexoBatchProof { + let proof = UtreexoBatchProof { targets: array![4, 5, 6].span(), proof: array![].span() }; - let target_hashes = array!['e', 'f', 'g']; + let leaves = array!['e', 'f', 'g']; - let new_state = state.delete(target_hashes.span(), @batch_proof); + let new_state = state.verify_and_delete(@proof, leaves.span()) + .expect('Verification failed'); assert_eq!(new_state.num_leaves, 7); assert_eq!( @@ -748,18 +1050,19 @@ mod tests { num_leaves: 7 }; - let batch_proof = UtreexoBatchProof { + let proof = UtreexoBatchProof { targets: array![0, 1, 2, 3, 4, 5, 6].span(), proof: array![].span() }; - let target_hashes = array!['a', 'b', 'c', 'd', 'e', 'f', 'g']; + let leaves = array!['a', 'b', 'c', 'd', 'e', 'f', 'g']; - let new_state = state.delete(target_hashes.span(), @batch_proof); + let new_state = state.verify_and_delete(@proof, leaves.span()) + .expect('Verification failed'); assert_eq!(new_state.num_leaves, 7); assert_eq!(new_state.roots, array![Option::None, Option::None, Option::None].span()); } - + #[test] fn test_deletion_7_7() { let state = UtreexoStumpState { @@ -778,177 +1081,89 @@ mod tests { // Remove 0 - let batch_proof = UtreexoBatchProof { + let proof = UtreexoBatchProof { targets: array![0].span(), proof: array![ - Option::Some('b'), - Option::Some( - 1702961261074558847535372708423978610134065667337563473891781271138689292959 - ) + 'b', + 1702961261074558847535372708423978610134065667337563473891781271138689292959 ] .span() }; - let target_hashes = array!['a']; + let leaves = array!['a']; - let new_state = state.delete(target_hashes.span(), @batch_proof); + let new_state = state.verify_and_delete(@proof, leaves.span()) + .expect('Verification 0 failed'); // Remove 1 - let batch_proof = UtreexoBatchProof { - targets: array![1].span(), + let proof = UtreexoBatchProof { + targets: array![8].span(), proof: array![ - Option::None, - Option::Some( - 1702961261074558847535372708423978610134065667337563473891781271138689292959 - ) + 1702961261074558847535372708423978610134065667337563473891781271138689292959 ] .span() }; - let target_hashes = array!['b']; + let leaves = array!['b']; - let new_state = new_state.delete(target_hashes.span(), @batch_proof); + let new_state = new_state.verify_and_delete(@proof, leaves.span()) + .expect('Verification 1 failed'); // Remove 2 - let batch_proof = UtreexoBatchProof { - targets: array![2].span(), proof: array![Option::Some('d'), Option::None].span() + let proof = UtreexoBatchProof { + targets: array![8].span(), proof: array!['d'].span() }; - let target_hashes = array!['c']; + let leaves = array!['c']; - let new_state = new_state.delete(target_hashes.span(), @batch_proof); + let new_state = new_state.verify_and_delete(@proof, leaves.span()) + .expect('Verification 2 failed'); // Remove 3 - let batch_proof = UtreexoBatchProof { - targets: array![3].span(), proof: array![Option::None, Option::None].span() + let proof = UtreexoBatchProof { + targets: array![12].span(), proof: array![].span() }; - let target_hashes = array!['d']; + let leaves = array!['d']; - let new_state = new_state.delete(target_hashes.span(), @batch_proof); + let new_state = new_state.verify_and_delete(@proof, leaves.span()) + .expect('Verification 3 failed'); // Remove 4 - let batch_proof = UtreexoBatchProof { - targets: array![4].span(), proof: array![Option::Some('f')].span() + let proof = UtreexoBatchProof { + targets: array![4].span(), proof: array!['f'].span() }; - let target_hashes = array!['e']; + let leaves = array!['e']; - let new_state = new_state.delete(target_hashes.span(), @batch_proof); + let new_state = new_state.verify_and_delete(@proof, leaves.span()) + .expect('Verification 4 failed'); // Remove 5 - let batch_proof = UtreexoBatchProof { - targets: array![5].span(), proof: array![Option::None].span() + let proof = UtreexoBatchProof { + targets: array![10].span(), proof: array![].span() }; - let target_hashes = array!['f']; + let leaves = array!['f']; - let new_state = new_state.delete(target_hashes.span(), @batch_proof); + let new_state = new_state.verify_and_delete(@proof, leaves.span()) + .expect('Verification 5 failed'); // Remove 6 - let batch_proof = UtreexoBatchProof { targets: array![6].span(), proof: array![].span() }; + let proof = UtreexoBatchProof { targets: array![6].span(), proof: array![].span() }; - let target_hashes = array!['g']; + let leaves = array!['g']; - let new_state = new_state.delete(target_hashes.span(), @batch_proof); + let new_state = new_state.verify_and_delete(@proof, leaves.span()) + .expect('Verification 6 failed'); assert_eq!(new_state.num_leaves, 7); assert_eq!(new_state.roots, array![Option::None, Option::None, Option::None].span()); } } - -#[cfg(test)] -mod tests { - use super::{UtreexoStumpState, StumpUtreexoAccumulator, UtreexoBatchProof}; - - #[test] - fn test_verification_1() { - let state = UtreexoStumpState { - roots: array![ - Option::Some(0x371cb6995ea5e7effcd2e174de264b5b407027a75a231a70c2c8d196107f0e7) - ] - .span(), - num_leaves: 2 - }; - let batch_proof = UtreexoBatchProof { targets: array![0].span(), proof: array![2].span() }; - let del_hashes = array![1]; - - let result = state.verify(@batch_proof, del_hashes.span(),); - assert_eq!(result, Result::Ok(())); - } - - #[test] - fn test_verification_2() { - let state = UtreexoStumpState { - roots: array![ - Option::Some(0x1702d734e291ad551b886a70b96446b99e19e405511e71fb5edfc4d2d83ce92), - Option::Some(0x770ad1be69d195e821c8c35051b32492e71592e230b950a99ebf87e98967ca), - Option::Some(0x2392042cbfda7371c81c9d7b456563533c2d6998b9e690a0d97421e6ae51a98), - Option::Some(0xf), - ] - .span(), - num_leaves: 15 - }; - let batch_proof = UtreexoBatchProof { - targets: array![1, 3, 10, 13].span(), - proof: array![ - 0x1, - 0x3, - 0xC, - 0xD, - 0x436e91732c0a83fa238d71460463f4b1fe0dc0b1ebcbc10967a84cec9d13154, - 0xdc9cc50aff0bdadd82a05bbab54015a07fccf2a4e30fa528fdca5a35d5423f - ] - .span() - }; - let del_hashes = array![2, 4, 11, 14]; - - let result = state.verify(@batch_proof, del_hashes.span(),); - assert_eq!(result, Result::Ok(())); - } - - #[test] - fn test_verification_3() { - let state = UtreexoStumpState { - roots: array![ - Option::Some(0x519631921e4905a63203f0cca7f6e6917082f30cef0930aa05bdc4323f6a398), - Option::Some(0x5198dcd61c969dfa8396dd27439ab776d120c2d67294fbcded0aa5f658f9150), - Option::Some(0x21d7ab8efac0146b5b47c8ad5431c3d14d9210319b0be7428fb2382ef115671), - Option::Some(0x74f794e653e00357d8a8ed45fcb74659841190c0821aa4e20bc4e30b2f3dd20), - ] - .span(), - num_leaves: 30 - }; - let batch_proof = UtreexoBatchProof { - targets: array![4, 8, 12, 16, 20, 24, 28].span(), - proof: array![ - 0x6, - 0xA, - 0xE, - 0x12, - 0x16, - 0x1A, - 0x1E, - 0x2797a40dbb8ea4b69a4e3bb4a9ccaa21a9585fcc71f3e5bb053ccae27910f90, - 0x7877cc14d4c8e76cc51aa4c49aa7aadaade0cf475ad63bb37c27c324e145393, - 0x556ea8bad1db13c6bdc3150a8289cd12044fb7e03cf201f35924a8afd4265a6, - 0x41a4ec75a27497daa51261588a60f0956d3fd61e521634bbf36bba6343c3a1b, - 0x3ba731d3734536d7cd5382cb4004ca4c24f1325b6fbeae27bcd6b4f9c0ed714, - 0x117ed04a65093683f13c16cf73d2855f1f099a96581d1dad74eaf34c9a343c8, - 0x79b32f615bbd57783700ae5f8e7b1ef79677c3545c4c69dc31b3aecce1d8fa6 - ] - .span() - }; - - let del_hashes = array![5, 9, 13, 17, 21, 25, 29]; - - let result = state.verify(@batch_proof, del_hashes.span(),); - assert_eq!(result, Result::Ok(())); - } -} diff --git a/packages/utreexo/src/stump/proof.cairo b/packages/utreexo/src/stump/proof.cairo index 5aa0766a..4fa8b884 100644 --- a/packages/utreexo/src/stump/proof.cairo +++ b/packages/utreexo/src/stump/proof.cairo @@ -17,14 +17,9 @@ impl UtreexoBatchProofDisplay of Display { fn fmt(self: @UtreexoBatchProof, ref f: Formatter) -> Result<(), Error> { let mut targets: ByteArray = Default::default(); let mut proofs: ByteArray = Default::default(); - for proof in *self - .proof { - if let Option::Some(v) = proof { - proofs.append(@format!("{},", v)); - } else { - proofs.append(@"None"); - } - }; + for proof in *self.proof { + proofs.append(@format!("{},", proof)); + }; for target in *self.targets { targets.append(@format!("{},", target)); }; @@ -38,8 +33,289 @@ impl UtreexoBatchProofDisplay of Display { #[generate_trait] pub impl UtreexoBatchProofImpl of UtreexoBatchProofTrait { - /// Computes a set of roots given a proof and leaves hashes. + /// Computes leaves' roots. fn compute_roots( + self: @UtreexoBatchProof, leaves: Span, num_leaves: u64, + ) -> Result, ByteArray> { + // hashes of leaves in the proof + let mut hashes = leaves; + // positions of leaves in the proof (must exactly match hashes) + let mut positions = *self.targets; + // siblings, needed to compute the roots + let mut proof = *self.proof; + + if hashes.len() != positions.len() { + return Result::Err("Leaves do not match proof targets"); + } + + // we pair positions with hashes into a single array + let mut targets = array![]; + while let Option::Some(hash) = hashes.pop_front() { + targets.append((*positions.pop_front().unwrap(), *hash)); + }; + // and sort them by position to align with the proof + targets = bubble_sort(targets.span()); + + // then we take the very first row of the forest: + // length of the row in the actual forest + let mut row_len = num_leaves; + // length of the row in the "perfect forest" (a forest extrapolated into a single perfect tree) + let mut row_cap = u64_next_power_of_two(num_leaves); + // first absolute position in the row ("absolute" means within the perfect forest) + let mut row_start = 0; + // last absolute position in the row + 1 + let mut row_end = row_cap; + // we take all the targets with absolute positions in [row_start; row_end) + // and put them into a row, also converting their positions to relative, i.e. in [0; row_len) + let mut row = extract_row(ref targets, row_start, row_end); + + // here we accumulate the result + let mut roots = array![]; + let mut inner_result: Result, ByteArray> = Result::Ok(array![]); + + // we process the whole forest row by row from the bottom leaves to the top root + while row_len != 0 { + // last relative position in the current row + let last = row_len - 1; + // here we accumulate computed parents that will go to the next row. + let mut next_row_computed = array![]; + // if there are targets on the current row, we take one + while let Option::Some((pos, hash)) = row.pop_front() { + // if its relative position is even + if pos % 2 == 0 { + // and if we have at least one more target after it on the current row + if let Option::Some(box) = row.get(0) { + let (next_pos, next_hash) = box.unbox(); + // and if that target is exactly on the next position + if *next_pos == pos + 1 { + // then they are siblings and we are able to compute their parent directly + // also, since we use relative positions, we can easily calculate position of the parent on the upper row + row.pop_front().unwrap(); + next_row_computed.append((pos / 2, parent_hash(hash, *next_hash))); + } + // or if the next target is not a sibling + else { + // then the sibling must be in the proof, so we take it from there + if let Option::Some(proof_hash) = proof.pop_front() { + // and compute the parent node + next_row_computed.append((pos / 2, parent_hash(hash, *proof_hash))); + } else { + inner_result = Result::Err("Invalid proof"); + break; + } + } + } + // or if there are no more targets on the current row, and we are not at the end of the row, + // there must be a sibling in the proof + else if pos != last { + // so we get the sibling from the proof + if let Option::Some(proof_hash) = proof.pop_front() { + // and compute the parent node + next_row_computed.append((pos / 2, parent_hash(hash, *proof_hash))); + } else { + inner_result = Result::Err("Invalid proof"); + break; + } + } + // or if we are at the end of the row, and the relative position is even, + // there cannot be siblings, which means it's a root + else { + // so we save the root + roots.append(hash); + } + } + // otherwise, if the relative position is odd, then we know for sure that + // there must be a sibling, moreover it must be in the proof + else { + // so we take the sibling from the proof + if let Option::Some(proof_hash) = proof.pop_front() { + // and compute the parent node + next_row_computed.append((pos / 2, parent_hash(*proof_hash, hash))); + } else { + inner_result = Result::Err("Invalid proof"); + break; + } + } + }; + + // after we processed all the targets in the current row and computed their parents, + // we move the parents (and pending targets from the proof) to the next row and continue, + // till we reach the top root + + // here we calculate the next row props + row_len /= 2; + row_cap /= 2; + row_start = row_end; + row_end += row_cap; + + // here we move all computed parents and pending targets to the next row + if targets.is_empty() { + row = next_row_computed; + } else { + let mut next_row_targets = extract_row(ref targets, row_start, row_end); + row = if next_row_targets.is_empty() { + next_row_computed + } else if next_row_computed.is_empty() { + next_row_targets + } else { + // if both arrays are not empty, we merge them into a single sorted array + merge_sorted(ref next_row_targets, ref next_row_computed) + } + } + }; + + // after we processed all rows of the forest, computed roots are all settled in the `roots` array, + // which is automatically ordered, btw + + inner_result?; + Result::Ok(roots) + } + + /// Computes leaves' roots before and after deletion and returns pairs (old_root, new_root). + fn compute_roots_with_deletion( + self: @UtreexoBatchProof, leaves: Span, num_leaves: u64, + ) -> Result)>, ByteArray> { + // the algorithm is practically the same as in the `compute_roots`, with the only difference - we convert + // the targets into pairs (target, None), meaning (old_value, new_value), and compute parent pairs accordingly, + // so in the end we have an array of pairs of roots (old_root, new_root), where old_root can be used to verify + // inclusion and new_root can be used to update utreexo state + + // hashes of leaves in the proof + let mut hashes = leaves; + // positions of leaves in the proof (must exactly match hashes) + let mut positions = *self.targets; + // siblings, needed to compute the roots + let mut proof = *self.proof; + + if hashes.len() != positions.len() { + return Result::Err("Leaves do not match proof targets"); + } + + // we pair positions with hashes into a single array + let mut targets = array![]; + while let Option::Some(hash) = hashes.pop_front() { + targets.append((*positions.pop_front().unwrap(), (*hash, Option::None))); + }; + // and sort them by position to align with the proof + targets = bubble_sort(targets.span()); + + // then we take the very first row of the forest: + // length of the row in the actual forest + let mut row_len = num_leaves; + // length of the row in the "perfect forest" (a forest extrapolated into a single perfect tree) + let mut row_cap = u64_next_power_of_two(num_leaves); + // first absolute position in the row ("absolute" means within the perfect forest) + let mut row_start = 0; + // last absolute position in the row + 1 + let mut row_end = row_cap; + // we take all the targets with absolute positions in [row_start; row_end) + // and put them into a row, also converting their positions to relative, i.e. in [0; row_len) + let mut row = extract_row(ref targets, row_start, row_end); + + // here we accumulate the result + let mut roots = array![]; + let mut inner_result: Result)>, ByteArray> = Result::Ok(array![]); + + // we process the whole forest row by row from the bottom leaves to the top root + while row_len != 0 { + // last relative position in the current row + let last = row_len - 1; + // here we accumulate computed parents that will go to the next row. + let mut next_row_computed = array![]; + // if there are targets on the current row, we take one + while let Option::Some((pos, hash)) = row.pop_front() { + // if its relative position is even + if pos % 2 == 0 { + // and if we have at least one more target after it on the current row + if let Option::Some(box) = row.get(0) { + let (next_pos, next_hash) = box.unbox(); + // and if that target is exactly on the next position + if *next_pos == pos + 1 { + // then they are siblings and we are able to compute their parent directly + // also, since we use relative positions, we can easily calculate position of the parent on the upper row + row.pop_front().unwrap(); + next_row_computed.append((pos / 2, parent_hash_pair(hash, *next_hash))); + } + // or if the next target is not a sibling + else { + // then the sibling must be in the proof, so we take it from there + if let Option::Some(proof_hash) = proof.pop_front() { + // and compute the parent node + next_row_computed.append((pos / 2, parent_hash_pair(hash, (*proof_hash, Option::Some(*proof_hash))))); + } else { + inner_result = Result::Err("Invalid proof"); + break; + } + } + } + // or if there are no more targets on the current row, and we are not at the end of the row, + // there must be a sibling in the proof + else if pos != last { + // so we get the sibling from the proof + if let Option::Some(proof_hash) = proof.pop_front() { + // and compute the parent node + next_row_computed.append((pos / 2, parent_hash_pair(hash, (*proof_hash, Option::Some(*proof_hash))))); + } else { + inner_result = Result::Err("Invalid proof"); + break; + } + } + // or if we are at the end of the row, and the relative position is even, + // there cannot be siblings, which means it's a root + else { + // so we save the root + roots.append(hash); + } + } + // otherwise, if the relative position is odd, then we know for sure that + // there must be a sibling, moreover it must be in the proof + else { + // so we take the sibling from the proof + if let Option::Some(proof_hash) = proof.pop_front() { + // and compute the parent node + next_row_computed.append((pos / 2, parent_hash_pair((*proof_hash, Option::Some(*proof_hash)), hash))); + } else { + inner_result = Result::Err("Invalid proof"); + break; + } + } + }; + + // after we processed all the targets in the current row and computed their parents, + // we move the parents (and pending targets from the proof) to the next row and continue, + // till we reach the top root + + // here we calculate the next row props + row_len /= 2; + row_cap /= 2; + row_start = row_end; + row_end += row_cap; + + // here we move all computed parents and pending targets to the next row + if targets.is_empty() { + row = next_row_computed; + } else { + let mut next_row_targets = extract_row(ref targets, row_start, row_end); + row = if next_row_targets.is_empty() { + next_row_computed + } else if next_row_computed.is_empty() { + next_row_targets + } else { + // if both arrays are not empty, we merge them into a single sorted array + merge_sorted(ref next_row_targets, ref next_row_computed) + } + } + }; + + // after we processed all rows of the forest, computed roots are all settled in the `roots` array, + // which is automatically ordered, btw + + inner_result?; + Result::Ok(roots) + } + + /// Legacy implementation of leaves' roots computation. + fn compute_roots_legacy( self: @UtreexoBatchProof, mut del_hashes: Span, num_leaves: u64, ) -> Result, ByteArray> { // Where all the parent hashes we've calculated in a given row will go to. @@ -174,17 +450,59 @@ pub impl UtreexoBatchProofImpl of UtreexoBatchProofTrait { } } -/// PartialOrd implementation for tuple (u32, felt252). -impl PartialOrdTupleU64Felt252 of PartialOrd<(u64, felt252)> { - fn lt(lhs: (u64, felt252), rhs: (u64, felt252)) -> bool { - let (a, _) = lhs; - let (b, _) = rhs; - - if a < b { - true - } else { - false +/// Extracts all nodes with absolute positions in [row_start; row_end) and transforms their positions to relative +fn extract_row, +Drop>(ref nodes: Array<(u64, T)>, row_start: u64, row_end: u64) -> Array<(u64, T)> { + let mut row = array![]; + while let Option::Some(box) = nodes.get(0) { + let (pos, value) = box.unbox(); + if *pos >= row_end { + break; } - } + nodes.pop_front().unwrap(); + row.append((*pos - row_start, *value)); + }; + row +} + +/// Merges two sorted arrays into a single sorted array +fn merge_sorted>(ref arr1: Array<(u64, T)>, ref arr2: Array<(u64, T)>) -> Array<(u64, T)> { + let mut res = array![]; + while let Option::Some((p1, v1)) = arr1.pop_front() { + while let Option::Some(box) = arr2.get(0) { + let (p2, _) = box.unbox(); + if *p2 > p1 { + break; + } + res.append(arr2.pop_front().unwrap()); + }; + res.append((p1, v1)); + }; + while let Option::Some(node) = arr2.pop_front() { + res.append(node); + }; + res } +/// Takes two nodes containing two values each: (L1, L2) and (R1, R2), and calculates the parent node, +/// that also contains two values (P1 = h(L1, R1), P2 = h(L2, R2)) +fn parent_hash_pair(left: (felt252, Option), right: (felt252, Option)) -> (felt252, Option) { + let (old_left, new_left) = left; + let (old_right, new_right) = right; + let old_parent = parent_hash(old_left, old_right); + let new_parent = match (new_left.is_some(), new_right.is_some()) { + (true, true) => Option::Some(parent_hash(new_left.unwrap(), new_right.unwrap())), + (true, false) => new_left, + (false, true) => new_right, + (false, false) => Option::None, + }; + (old_parent, new_parent) +} + +/// PartialOrd implementation for tuple (u64, T). +impl PositionPartialOrd> of PartialOrd<(u64, T)> { + fn lt(lhs: (u64, T), rhs: (u64, T)) -> bool { + let (l, _) = lhs; + let (r, _) = rhs; + l < r + } +}