diff --git a/benches/proof_benchmarks.rs b/benches/proof_benchmarks.rs index 33c3fa3..49d6014 100644 --- a/benches/proof_benchmarks.rs +++ b/benches/proof_benchmarks.rs @@ -55,7 +55,7 @@ fn proof_verification(c: &mut Criterion) { let accumulator_size = 100; let hashes = generate_test_hashes(accumulator_size, 42); let stump = Stump::new(); - let (stump, _) = stump.modify(&hashes, &[], &Proof::default()).unwrap(); + let stump = stump.modify(&hashes, &[], &Proof::default()).unwrap(); for target_count in [1, 10].iter() { let del_hashes = hashes[..*target_count].to_vec(); diff --git a/benches/stump_benchmarks.rs b/benches/stump_benchmarks.rs index 7fa5e16..c4927bc 100644 --- a/benches/stump_benchmarks.rs +++ b/benches/stump_benchmarks.rs @@ -50,7 +50,7 @@ fn stump_verify(c: &mut Criterion) { let test_size = 1000; let hashes = generate_test_hashes(test_size, 42); let stump = Stump::new(); - let (stump, _) = stump.modify(&hashes, &[], &Proof::default()).unwrap(); + let stump = stump.modify(&hashes, &[], &Proof::default()).unwrap(); for proof_size in [1, 10, 100].iter() { let del_hashes = hashes[..*proof_size].to_vec(); diff --git a/examples/full-accumulator.rs b/examples/full-accumulator.rs index 12160c0..6bffff7 100644 --- a/examples/full-accumulator.rs +++ b/examples/full-accumulator.rs @@ -29,8 +29,7 @@ fn main() { // Verify the proof. Notice how we use the del_hashes returned by `prove` here. let s = Stump::new() .modify(&elements, &[], &Proof::default()) - .unwrap() - .0; + .unwrap(); assert_eq!(s.verify(&proof, &[elements[0]]), Ok(true)); // Now we want to update the MemForest, by removing the first utxo, and adding a new one. // This would be in case we received a new block with a transaction spending the first utxo, diff --git a/examples/proof-update.rs b/examples/proof-update.rs index cb02cfd..9df9407 100644 --- a/examples/proof-update.rs +++ b/examples/proof-update.rs @@ -18,13 +18,20 @@ use rustreexo::accumulator::stump::Stump; fn main() { let s = Stump::new(); + // Get the hashes of the UTXOs we want to insert let utxos = get_utxo_hashes1(); - // Add the UTXOs to the accumulator. update_data is the data we need to update the proof - // after the accumulator is updated. - let (s, update_data) = s.modify(&utxos, &[], &Proof::default()).unwrap(); + + // Compute the update data for this block. Notice that we called this before updating the + // accumulator, as the accumulator state is required to compute this data. + let update_data = s.get_update_data(&utxos, &[], &Proof::default()).unwrap(); + + // Now, update the accumulator with these UTXOs, no STXOs are being spent in this example. + let s = s.modify(&utxos, &[], &Proof::default()).unwrap(); + // Create an empty proof, we'll update it to hold our UTXOs let p = Proof::default(); + // Update the proof with the UTXOs we added to the accumulator. This proof was initially empty, // but we can instruct this function to remember some UTXOs, given their positions in the list of // UTXOs we added to the accumulator. In this example, we ask it to cache 0 and 1. @@ -35,6 +42,7 @@ fn main() { let (p, cached_hashes) = p .update(vec![], utxos.clone(), vec![], vec![0, 1], update_data) .unwrap(); + // This should be a valid proof over 0 and 1. assert_eq!(p.n_targets(), 2); assert_eq!(s.verify(&p, &cached_hashes), Ok(true)); @@ -42,14 +50,18 @@ fn main() { // Get a subset of the proof, for the first UTXO only let p1 = p.get_proof_subset(&cached_hashes, &[0], s.leaves).unwrap(); + // Should still be valid assert_eq!(s.verify(&p1, &cached_hashes), Ok(true)); // Assume we have a block that (beyond coinbase) spends our UTXO `0` and creates 7 new UTXOs // We'll remove `0` as it got spent, and add 1..7 to our cache. let new_utxos = get_utxo_hashes2(); + // First, update the accumulator - let (stump, update_data) = s.modify(&new_utxos, &[utxos[0]], &p1).unwrap(); + let stump = s.modify(&new_utxos, &[utxos[0]], &p1).unwrap(); + // and the proof + let update_data = s.get_update_data(&utxos, &[], &Proof::default()).unwrap(); let (p2, cached_hashes) = p .update( cached_hashes, diff --git a/examples/simple-stump-update.rs b/examples/simple-stump-update.rs index 139b1b9..2304dfa 100644 --- a/examples/simple-stump-update.rs +++ b/examples/simple-stump-update.rs @@ -27,10 +27,7 @@ fn main() { // Create a new Stump, and add the utxos to it. Notice how we don't use the full return here, // but only the Stump. To understand what is the second return value, see the documentation // for `Stump::modify`, or the proof-update example. - let s = Stump::new() - .modify(&utxos, &[], &Proof::default()) - .unwrap() - .0; + let s = Stump::new().modify(&utxos, &[], &Proof::default()).unwrap(); // Create a proof that the first utxo is in the Stump. let proof = Proof::new(vec![0], vec![utxos[1]]); assert_eq!(s.verify(&proof, &[utxos[0]]), Ok(true)); @@ -42,7 +39,7 @@ fn main() { "d3bd63d53c5a70050a28612a2f4b2019f40951a653ae70736d93745efb1124fa", ) .unwrap(); - let s = s.modify(&[new_utxo], &[utxos[0]], &proof).unwrap().0; + let s = s.modify(&[new_utxo], &[utxos[0]], &proof).unwrap(); // Now we can verify that the new utxo is in the Stump, and the old one is not. let new_proof = Proof::new(vec![2], vec![new_utxo]); assert_eq!(s.verify(&new_proof, &[new_utxo]), Ok(true)); diff --git a/src/accumulator/proof.rs b/src/accumulator/proof.rs index b586fcc..2e86b51 100644 --- a/src/accumulator/proof.rs +++ b/src/accumulator/proof.rs @@ -48,7 +48,7 @@ //! hashes.push(sha256::Hash::from_engine(engine).into()) //! } //! // Add the UTXOs to the accumulator -//! let s = s.modify(&hashes, &vec![], &Proof::default()).unwrap().0; +//! let s = s.modify(&hashes, &vec![], &Proof::default()).unwrap(); //! // Create a proof for the targets //! let p = Proof::new(targets, proof_hashes); //! // Verify the proof @@ -70,6 +70,8 @@ use super::util; use super::util::get_proof_positions; use super::util::read_u64; use super::util::tree_rows; +#[cfg(doc)] +use crate::accumulator::stump::Stump; #[derive(Clone, Debug, Eq, PartialEq)] #[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))] @@ -120,10 +122,10 @@ impl Default for Proof { /// This alias is used when we need to return the nodes and roots for a proof /// if we are not concerned with deleting those elements. pub(crate) type NodesAndRootsCurrent = (Vec<(u64, Hash)>, Vec); -/// This is used when we need to return the nodes and roots for a proof -/// if we are concerned with deleting those elements. The difference is that -/// we need to retun the old and updatated roots in the accumulator. -pub(crate) type NodesAndRootsOldNew = (Vec<(u64, Hash)>, Vec<(Hash, Hash)>); + +/// This pairs old and new values for roots. We use this when computing deletions, +/// as we need to return both the old root (before deletion) and the new root (after deletion). +pub(crate) type RootsOldNew = Vec<(Hash, Hash)>; impl Proof { /// Creates a proof from a vector of target and hashes. @@ -260,7 +262,7 @@ impl Proof { /// engine.input(&[i]); /// hashes.push(sha256::Hash::from_engine(engine).into()) /// } - /// let s = s.modify(&hashes, &vec![], &Proof::default()).unwrap().0; + /// let s = s.modify(&hashes, &vec![], &Proof::default()).unwrap(); /// let p = Proof::new(targets, proof_hashes); /// assert!(s.verify(&p, &[hashes[0]]).expect("This proof is valid")); /// ``` @@ -426,15 +428,15 @@ impl Proof { /// It will compute all roots that contains elements in the proof, by hasing the nodes /// in the path to the root. This function returns the calculated roots and the hashes /// that were calculated in the process. + /// /// This function is used for updating the accumulator **and** verifying proofs. It returns /// the roots computed from the proof (that should be equal to some roots in the present - /// accumulator) and the hashes for a accumulator where the proof elements are removed. - /// If at least one returned element doesn't exist in the accumulator, the proof is invalid. + /// accumulator). If at least one returned element doesn't exist in the accumulator, the proof is invalid. pub(crate) fn calculate_hashes_delete( &self, del_hashes: &[(Hash, Hash)], num_leaves: u64, - ) -> Result, String> { + ) -> Result, String> { // Where all the root hashes that we've calculated will go to. let total_rows = util::tree_rows(num_leaves); @@ -495,13 +497,7 @@ impl Proof { computed.push((parent, (old_parent_hash, parent_hash))); } - // we shouldn't return the hashes in the proof - nodes.extend(computed); - let nodes = nodes - .into_iter() - .map(|(pos, (_, new_hash))| (pos, new_hash)) - .collect(); - Ok((nodes, calculated_root_hashes)) + Ok(calculated_root_hashes) } /// This function computes a set of roots from a proof. @@ -511,7 +507,9 @@ impl Proof { /// hashes that were calculated in the process. /// This differs from `calculate_hashes_delelte` as this one is only used for verifying /// proofs, it doesn't compute the roots after the deletion, only the roots that are - /// needed for verification (i.e. the current accumulator). + /// needed for verification (i.e. the current accumulator). `calculate_hashes_delete` also + /// won't return the hashes that were calculated, we won't need them for updating the + /// accumulator. pub(crate) fn calculate_hashes( &self, del_hashes: &[Hash], @@ -565,7 +563,13 @@ impl Proof { return Err(format!("Missing sibling for {next_pos}")); } - let parent_hash = AccumulatorHash::parent_hash(&next_hash, &sibling_hash); + let parent_hash = match (next_hash.is_empty(), sibling_hash.is_empty()) { + (true, true) => AccumulatorHash::empty(), + (true, false) => sibling_hash, + (false, true) => next_hash, + (false, false) => AccumulatorHash::parent_hash(&next_hash, &sibling_hash), + }; + let parent = util::parent(next_pos, total_rows); computed.push((parent, parent_hash)); } @@ -607,10 +611,64 @@ impl Proof { } } - /// Uses the data passed in to update a proof, creating a valid proof for a given - /// set of targets, after an update. This is useful for caching UTXOs. You grab a proof - /// for it once and then keep updating it every block, yielding an always valid proof - /// over those UTXOs. + /// Utreexo is a dynamic accumulator, meaning that leaves can be added and removed. + /// This causes the proof to also change over time, if you have a proof for a given + /// block height, it may not be valid for block height + 1, the elements in this proof + /// may change position, or even be deleted. + /// + /// If you have a proof that's valid, but is for a block a few heights behind, you can use + /// [`Proof::update`] to update the proof to be valid for the current [`Stump`]. + /// + /// Before calling this method, you will need to use [`Stump::get_update_data`] to + /// get the changes that happened in the accumulator for that block. This includes the nodes + /// added and deleted in that block, as well as the previous number of leaves. + /// + /// After computing this, you can use [`Proof::update`] to update your proof to be valid for + /// the next accumulator. Call these for all blocks between your proof's height and the current + /// height, and you'll have a valid proof for the current [`Stump`]. + /// + /// # Example + /// + /// ``` + /// use std::str::FromStr; + /// + /// use rustreexo::accumulator::node_hash::BitcoinNodeHash; + /// use rustreexo::accumulator::proof::Proof; + /// use rustreexo::accumulator::stump::Stump; + /// + /// let s = Stump::new(); + /// let utxos = vec![BitcoinNodeHash::from_str( + /// "b151a956139bb821d4effa34ea95c17560e0135d1e4661fc23cedc3af49dac42", + /// ) + /// .unwrap()]; + /// let del_hashes = vec![]; + /// let proof = Proof::default(); + /// let final_stump = s.modify(&utxos, &del_hashes, &proof).unwrap(); + /// + /// // The update data, computed from the block changes + /// let update_data = s.get_update_data(&utxos, &del_hashes, &proof).unwrap(); + /// + /// // These are the hashes for all the targets currently in the proof. For us, since + /// // the proof is empty, this is also empty. + /// let cached_hashes = vec![]; + /// + /// // This tells `update` which UTXOs created on this block we should cache. After + /// // using this, our proof will now contain those UTXOs. It is an index, in the same order + /// // as they appear in `utxos`. + /// let cache_new_utxos = vec![0]; + /// + /// // The target for UTXOs being deleted in this block. You will usually find this along + /// // with this block's proof + /// let targets = vec![]; + /// + /// // Call update to get the new proof and the hashes for the utxos being cached + /// let (proof_updated, cached_hashes) = proof + /// .update(cached_hashes, utxos, targets, cache_new_utxos, update_data) + /// .unwrap(); + /// + /// // Now we can verify this proof with the UTXO added in this block + /// assert!(final_stump.verify(&proof_updated, &cached_hashes).unwrap()); + /// ``` pub fn update( self, cached_hashes: Vec, @@ -1006,7 +1064,10 @@ mod tests { let block_proof = Proof::new(case_values.update.proof.targets.clone(), block_proof_hashes); - let (stump, updated) = stump.modify(&utxos, &del_hashes, &block_proof).unwrap(); + let new_stump = stump.modify(&utxos, &del_hashes, &block_proof).unwrap(); + let updated = stump + .get_update_data(&utxos, &del_hashes, &block_proof) + .unwrap(); let (cached_proof, cached_hashes) = cached_proof .update( cached_hashes.clone(), @@ -1017,7 +1078,7 @@ mod tests { ) .unwrap(); - let res = stump.verify(&cached_proof, &cached_hashes); + let res = new_stump.verify(&cached_proof, &cached_hashes); let expected_roots: Vec<_> = case_values .expected_roots @@ -1032,7 +1093,7 @@ mod tests { .collect(); assert_eq!(res, Ok(true)); assert_eq!(cached_proof.targets, case_values.expected_targets); - assert_eq!(stump.roots, expected_roots); + assert_eq!(new_stump.roots, expected_roots); assert_eq!(cached_hashes, expected_cached_hashes); } } @@ -1192,7 +1253,7 @@ mod tests { fn test_update_proof_delete() { let preimages = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; let hashes = preimages.into_iter().map(hash_from_u8).collect::>(); - let (stump, _) = Stump::new() + let stump = Stump::new() .modify(&hashes, &[], &Proof::default()) .unwrap(); @@ -1220,13 +1281,22 @@ mod tests { let proof = Proof::new(vec![1, 2, 6], proof_hashes); - let (stump, modified) = stump + let new_stump = stump .modify( &[], &[hash_from_u8(1), hash_from_u8(2), hash_from_u8(6)], &proof, ) .unwrap(); + + let modified = stump + .get_update_data( + &[], + &[hash_from_u8(1), hash_from_u8(2), hash_from_u8(6)], + &proof, + ) + .unwrap(); + let (new_proof, _) = cached_proof .update_proof_remove( vec![1, 2, 6], @@ -1236,7 +1306,7 @@ mod tests { ) .unwrap(); - let res = stump.verify(&new_proof, &[hash_from_u8(0), hash_from_u8(7)]); + let res = new_stump.verify(&new_proof, &[hash_from_u8(0), hash_from_u8(7)]); assert_eq!(res, Ok(true)); } @@ -1249,8 +1319,7 @@ mod tests { // Create a new stump with 8 leaves and 1 root let s = Stump::new() .modify(&hashes, &[], &Proof::default()) - .expect("This stump is valid") - .0; + .expect("This stump is valid"); // Nodes that will be deleted let del_hashes = vec![hashes[0], hashes[2], hashes[4], hashes[6]]; @@ -1342,35 +1411,18 @@ mod tests { .map(|hash| (hash, BitcoinNodeHash::empty())) .collect::>(); - let (computed, roots) = p.calculate_hashes_delete(&del_hashes, 8).unwrap(); + let roots = p.calculate_hashes_delete(&del_hashes, 8).unwrap(); let expected_root_old = BitcoinNodeHash::from_str( "b151a956139bb821d4effa34ea95c17560e0135d1e4661fc23cedc3af49dac42", ) .unwrap(); + let expected_root_new = BitcoinNodeHash::from_str( "726fdd3b432cc59e68487d126e70f0db74a236267f8daeae30b31839a4e7ebed", ) .unwrap(); - let computed_positions = [0_u64, 1, 9, 13, 8, 12, 14].to_vec(); - let computed_hashes = [ - "0000000000000000000000000000000000000000000000000000000000000000", - "4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a", - "9576f4ade6e9bc3a6458b506ce3e4e890df29cb14cb5d3d887672aef55647a2b", - "29590a14c1b09384b94a2c0e94bf821ca75b62eacebc47893397ca88e3bbcbd7", - "4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a", - "2b77298feac78ab51bc5079099a074c6d789bd350442f5079fcba2b3402694e5", - "726fdd3b432cc59e68487d126e70f0db74a236267f8daeae30b31839a4e7ebed", - ] - .iter() - .map(|hash| BitcoinNodeHash::from_str(hash).unwrap()) - .collect::>(); - let expected_computed: Vec<_> = computed_positions - .into_iter() - .zip(computed_hashes) - .collect(); assert_eq!(roots, vec![(expected_root_old, expected_root_new)]); - assert_eq!(computed, expected_computed); } #[test] @@ -1392,8 +1444,7 @@ mod tests { // Create a new stump with 8 leaves and 1 root let s = Stump::new() .modify(&hashes, &[], &Proof::default()) - .expect("This stump is valid") - .0; + .expect("This stump is valid"); // Nodes that will be deleted let del_hashes = vec![hashes[0], hashes[2], hashes[4], hashes[6]]; diff --git a/src/accumulator/stump.rs b/src/accumulator/stump.rs index c337bb4..29c8258 100644 --- a/src/accumulator/stump.rs +++ b/src/accumulator/stump.rs @@ -1,6 +1,6 @@ //! A [Stump] is a basic data structure used in Utreexo. It only holds the roots and the number of leaves -//! in the accumulator. This is useful to create lightweight nodes, the still validates, but is more compact, -//! perfect to clients running on low-power devices. +//! in the accumulator. This is useful to create lightweight nodes, the still validates, but is more compact. +//! This is useful for clients running is super constrained environments, such as mobile devices. //! //! ## Example //! ``` @@ -23,7 +23,7 @@ //! // that modify is a pure function that doesn't modify the old Stump. //! let s = s.modify(&utxos, &stxos, &Proof::::default()); //! assert!(s.is_ok()); -//! assert_eq!(s.unwrap().0.roots, utxos); +//! assert_eq!(s.unwrap().roots, utxos); //! ``` use std::collections::BTreeSet; @@ -38,9 +38,9 @@ use serde::Serialize; use super::node_hash::AccumulatorHash; use super::node_hash::BitcoinNodeHash; -use super::proof::NodesAndRootsOldNew; use super::proof::Proof; use super::util; +use crate::accumulator::proof::RootsOldNew; #[derive(Debug, Clone, Default)] pub struct UpdateData { @@ -113,7 +113,7 @@ impl Stump { /// .iter() /// .map(|&el| BitcoinNodeHash::from([el; 32])) /// .collect::>(); - /// let (stump, _) = Stump::new() + /// let stump = Stump::new() /// .modify(&hashes, &[], &Proof::default()) /// .unwrap(); /// let mut writer = Vec::new(); @@ -151,9 +151,9 @@ impl Stump { /// let s_old = Stump::new(); /// let mut s_new = Stump::new(); /// - /// let s_old = s_old.modify(&vec![], &vec![], &Proof::default()).unwrap().0; + /// let s_old = s_old.modify(&vec![], &vec![], &Proof::default()).unwrap(); /// s_new = s_old.clone(); - /// s_new = s_new.modify(&vec![], &vec![], &Proof::default()).unwrap().0; + /// s_new = s_new.modify(&vec![], &vec![], &Proof::default()).unwrap(); /// /// // A reorg happened /// s_new.undo(s_old); @@ -191,7 +191,20 @@ impl Stump { /// matters, you can only modify, providing a list of utxos to be added, /// and txos to be removed, along with it's proof. Either may be /// empty. + /// + /// ## Adding stoxs to the stump + /// + /// If you know that an element will be deleted in the future, you can + /// pass an empty hash as the utxo to be added. There's a possible + /// optimization here, where you can apply the changes caused by deletion + /// during addition. If you do so, you don't need to call `modify` when + /// this leaf gets deleted. This will save you some hashing, as well as + /// you won't need proofs anymore for those leaves, thus saving you + /// bandwidth. + /// ///# Example + /// Using the normal addition + /// /// ``` /// use std::str::FromStr; /// @@ -207,15 +220,39 @@ impl Stump { /// let stxos = vec![]; /// let s = s.modify(&utxos, &stxos, &Proof::default()); /// assert!(s.is_ok()); - /// assert_eq!(s.unwrap().0.roots, utxos); + /// assert_eq!(s.unwrap().roots, utxos); + /// ``` + /// + /// Using addition with implicit deletion + /// + /// ``` + /// use std::str::FromStr; + /// + /// use rustreexo::accumulator::node_hash::AccumulatorHash; + /// use rustreexo::accumulator::node_hash::BitcoinNodeHash; + /// use rustreexo::accumulator::proof::Proof; + /// use rustreexo::accumulator::stump::Stump; + /// + /// let s = Stump::new(); + /// let utxos = vec![ + /// BitcoinNodeHash::from_str( + /// "b151a956139bb821d4effa34ea95c17560e0135d1e4661fc23cedc3af49dac42", + /// ) + /// .unwrap(), + /// BitcoinNodeHash::empty(), // This UTXO is going to be deleted in the future + /// ]; + /// let stxos = vec![]; + /// let s = s.modify(&utxos, &stxos, &Proof::default()).unwrap(); + /// assert!(s.roots.contains(&utxos[0])); + /// assert!(!s.roots.contains(&utxos[1])); // The empty hash won't be in the roots /// ``` pub fn modify( &self, utxos: &[Hash], del_hashes: &[Hash], proof: &Proof, - ) -> Result<(Self, UpdateData), StumpError> { - let (intermediate, mut computed_roots) = self.remove(del_hashes, proof)?; + ) -> Result { + let mut computed_roots = self.remove(del_hashes, proof)?; let mut new_roots = vec![]; for root in self.roots.iter() { @@ -236,13 +273,102 @@ impl Stump { return Err(StumpError::EmptyProof); } - let (roots, updated, destroyed) = Self::add(new_roots, utxos, self.leaves); + let roots = Self::add(new_roots, utxos, self.leaves); let new_stump = Self { leaves: self.leaves + utxos.len() as u64, roots, }; + Ok(new_stump) + } + + /// Utreexo is a dynamic accumulator, meaning that leaves can be added and removed. + /// This causes the proof to also change over time, if you have a proof for a given + /// block height, it may not be valid for block height + 1, the elements in this proof + /// may change position, or even be deleted. + /// + /// If you have a proof that's valid, but is for a block a few heights behind, you can use + /// [`Proof::update`] to update the proof to be valid for the current [`Stump`]. However, to + /// update a proof, you need to know everything that changed in the accumulator since the proof + /// was created. This function will give you everything you need. + /// + /// To use it, you must know what was added -- this is computed by looking at the block. You + /// also need the block's [`Proof`] and the hashes that were deleted in that block. For this + /// one, you either store them on disk, or request them from the network. + /// + /// After computing this, you can use [`Proof::update`] to update your proof to be valid for + /// the next accumulator. Call these for all blocks between your proof's height and the current + /// height, and you'll have a valid proof for the current [`Stump`]. + /// + /// # Example + /// + /// ``` + /// use std::str::FromStr; + /// + /// use rustreexo::accumulator::node_hash::BitcoinNodeHash; + /// use rustreexo::accumulator::proof::Proof; + /// use rustreexo::accumulator::stump::Stump; + /// + /// let s = Stump::new(); + /// let utxos = vec![BitcoinNodeHash::from_str( + /// "b151a956139bb821d4effa34ea95c17560e0135d1e4661fc23cedc3af49dac42", + /// ) + /// .unwrap()]; + /// let del_hashes = vec![]; + /// let proof = Proof::default(); + /// let final_stump = s.modify(&utxos, &del_hashes, &proof).unwrap(); + /// + /// // The update data, computed from the block changes + /// let update_data = s.get_update_data(&utxos, &del_hashes, &proof).unwrap(); + /// + /// // These are the hashes for all the targets currently in the proof. For us, since + /// // the proof is empty, this is also empty. + /// let cached_hashes = vec![]; + /// + /// // This tells `update` which UTXOs created on this block we should cache. After + /// // using this, our proof will now contain those UTXOs. It is an index, in the same order + /// // as they appear in `utxos`. + /// let cache_new_utxos = vec![0]; + /// + /// // The target for UTXOs being deleted in this block. You will usually find this along + /// // with this block's proof + /// let targets = vec![]; + /// + /// // Call update to get the new proof and the hashes for the utxos being cached + /// let (proof_updated, cached_hashes) = proof + /// .update(cached_hashes, utxos, targets, cache_new_utxos, update_data) + /// .unwrap(); + /// + /// // Now we can verify this proof with the UTXO added in this block + /// assert!(final_stump.verify(&proof_updated, &cached_hashes).unwrap()); + /// ``` + pub fn get_update_data( + &self, + utxos: &[Hash], + del_hashes: &[Hash], + proof: &Proof, + ) -> Result, StumpError> { + let zeroed_del_hashes = del_hashes.iter().map(|_| Hash::empty()).collect::>(); + let (intermediate, _) = proof + .calculate_hashes(&zeroed_del_hashes, self.leaves) + .map_err(StumpError::InvalidProof)?; + let mut computed_roots = self.remove(del_hashes, proof)?; + let mut new_roots = vec![]; + + for root in self.roots.iter() { + if let Some(pos) = computed_roots.iter().position(|(old, _new)| old == root) { + let (old_root, new_root) = computed_roots.remove(pos); + if old_root == *root { + new_roots.push(new_root); + continue; + } + } + + new_roots.push(*root); + } + let (_, updated, destroyed) = Self::compute_update_data_add(new_roots, utxos, self.leaves); + let update_data = UpdateData { new_add: updated, prev_num_leaves: self.leaves, @@ -250,7 +376,7 @@ impl Stump { new_del: intermediate, }; - Ok((new_stump, update_data)) + Ok(update_data) } /// Deserialize the Stump from a Reader @@ -269,7 +395,7 @@ impl Stump { /// .iter() /// .map(|&el| BitcoinNodeHash::from([el; 32])) /// .collect::>(); - /// let (stump, _) = Stump::new() + /// let stump = Stump::new() /// .modify(&hashes, &[], &Proof::default()) /// .unwrap(); /// assert_eq!( @@ -294,12 +420,9 @@ impl Stump { &self, del_hashes: &[Hash], proof: &Proof, - ) -> Result, StumpError> { + ) -> Result, StumpError> { if del_hashes.is_empty() { - return Ok(( - vec![], - self.roots.iter().map(|root| (*root, *root)).collect(), - )); + return Ok(self.roots.iter().map(|root| (*root, *root)).collect()); } let del_hashes = del_hashes @@ -312,8 +435,32 @@ impl Stump { .map_err(StumpError::InvalidProof) } + fn add(mut roots: Vec, utxos: &[Hash], mut leaves: u64) -> Vec { + for add in utxos.iter() { + let pos = leaves; + let mut h = 0; + let mut to_add = *add; + while (pos >> h) & 1 == 1 { + let root = roots.pop().unwrap(); + + to_add = match (root.is_empty(), to_add.is_empty()) { + (true, true) => Hash::empty(), + (true, false) => to_add, + (false, true) => root, + (false, false) => AccumulatorHash::parent_hash(&root, &to_add), + }; + + h += 1; + } + roots.push(to_add); + leaves += 1; + } + + roots + } + /// Adds new leaves into the root - fn add( + fn compute_update_data_add( mut roots: Vec, utxos: &[Hash], mut leaves: u64, @@ -456,7 +603,7 @@ mod test { .map(|&el| CustomHash([el; 32])) .collect::>(); - let (stump, _) = s + let stump = s .modify( &hashes, &[], @@ -504,6 +651,7 @@ mod test { .iter() .map(|hash| BitcoinNodeHash::from_str(hash).unwrap()) .collect(); + let stump = Stump { leaves: data.leaves, roots, @@ -525,7 +673,9 @@ mod test { .map(|hash| BitcoinNodeHash::from_str(hash).unwrap()) .collect::>(); let proof = Proof::new(data.proof_targets, proof_hashes); - let (_, updated) = stump.modify(&utxos, &del_hashes, &proof).unwrap(); + let updated = stump + .get_update_data(&utxos, &del_hashes, &proof) + .expect("Update data should be computed correctly"); // Positions returned after addition let new_add_hash: Vec<_> = data .new_add_hash @@ -563,8 +713,8 @@ mod test { let preimages = vec![0, 1, 2, 3]; let hashes = preimages.into_iter().map(hash_from_u8).collect::>(); - let (_, updated) = Stump::new() - .modify(&hashes, &[], &Proof::default()) + let updated = Stump::new() + .get_update_data(&hashes, &[], &Proof::default()) .unwrap(); let positions = vec![0, 1, 2, 3, 4, 5, 6]; @@ -592,8 +742,7 @@ mod test { fn test_serde_rtt() { let stump = Stump::new() .modify(&[hash_from_u8(0), hash_from_u8(1)], &[], &Proof::default()) - .unwrap() - .0; + .unwrap(); let serialized = serde_json::to_string(&stump).expect("Serialization failed"); let deserialized: Stump = serde_json::from_str(&serialized).expect("Deserialization failed"); @@ -601,6 +750,27 @@ mod test { } fn run_case_with_deletion(case: TestCase) { + // If you happen to know whether a leaf will be deleted in the future, you can + // pretend you've added it, but already account for its deletion. This way, you + // won't have to call modify twice (once for addition, once for deletion), but + // the final accumulator will be the same. + // + // This is useful for swift sync style clients, where you have a bitmap telling + // whether a txout is unspent or not. Using this, you don't need to download + // block proofs and perform fewer hashing, due to not calling `delete` at all. + let leaf_hashes_without_deletions = case + .leaf_preimages + .iter() + .map(|preimage| { + let targets = case.target_values.as_ref().unwrap(); + if targets.contains(&(*preimage as u64)) { + BitcoinNodeHash::empty() + } else { + hash_from_u8(*preimage) + } + }) + .collect::>(); + let leaf_hashes = case .leaf_preimages .into_iter() @@ -634,11 +804,16 @@ mod test { }) .collect::>(); - let (stump, _) = Stump::new() + let stump = Stump::new() .modify(&leaf_hashes, &[], &Proof::default()) .expect("This stump is valid"); - let (stump, _) = stump.modify(&[], &target_hashes, &proof).unwrap(); + let stump = stump.modify(&[], &target_hashes, &proof).unwrap(); + let stump_no_deletion = Stump::new() + .modify(&leaf_hashes_without_deletions, &[], &Proof::default()) + .expect("This stump is valid"); + assert_eq!(stump.roots, roots); + assert_eq!(stump.roots, stump_no_deletion.roots); } fn run_single_addition_case(case: TestCase) { @@ -651,7 +826,7 @@ mod test { .map(|value| hash_from_u8(*value)) .collect::>(); - let (s, _) = s + let s = s .modify(&hashes, &[], &Proof::default()) .expect("Stump from test cases are valid"); @@ -673,8 +848,7 @@ mod test { let s_old = Stump::new(); let s_old = s_old .modify(&hashes, &[], &Proof::default()) - .expect("Stump from test cases are valid") - .0; + .expect("Stump from test cases are valid"); let mut s_new = s_old.clone(); @@ -700,7 +874,7 @@ mod test { .iter() .map(|&el| BitcoinNodeHash::from([el; 32])) .collect::>(); - let (stump, _) = Stump::new() + let stump = Stump::new() .modify(&hashes, &[], &Proof::default()) .unwrap(); let mut writer = Vec::new(); @@ -732,6 +906,36 @@ mod test { run_case_with_deletion(i); } } + + #[test] + fn test_update_no_explicit_deletion() { + let leaf_preimages = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; + let target_values = [0, 4, 5, 6, 7, 8]; + let leaves = leaf_preimages + .iter() + .map(|preimage| { + if target_values.contains(preimage) { + BitcoinNodeHash::empty() + } else { + hash_from_u8(*preimage) + } + }) + .collect::>(); + + let acc = Stump::new() + .modify(&leaves, &[], &Proof::default()) + .unwrap(); + + let expected_roots = [ + "2b77298feac78ab51bc5079099a074c6d789bd350442f5079fcba2b3402694e5", + "84915b5adf9243dd83d67bb7d25b7a0c595ea1c37b97412e21e480c1a46f93bf", + ] + .iter() + .map(|&hash| BitcoinNodeHash::from_str(hash).unwrap()) + .collect::>(); + + assert_eq!(acc.roots, expected_roots); + } } #[cfg(bench)] @@ -788,8 +992,7 @@ mod bench { .collect::>(); let acc = Stump::new() .modify(&leaves, &vec![], &Proof::default()) - .unwrap() - .0; + .unwrap(); let proof = Proof::new(target_values.to_vec(), proofhashes); bencher.iter(move || acc.modify(&leaves, &target_hashes, &proof)); }