diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 24ec1c5..0000000 --- a/.editorconfig +++ /dev/null @@ -1,20 +0,0 @@ -# Documentation available at editorconfig.org - -root=true - -[*] -ident_style = space -ident_size = 4 -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true - -[*.rs] -max_line_length = 100 - -[*.md] -trim_trailing_whitespace = false - -[*.yml] -ident_size = 2 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 3d35fb2..0000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,34 +0,0 @@ -# See https://pre-commit.com for more information -# See https://pre-commit.com/hooks.html for more hooks -repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 - hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - - id: check-yaml - - id: check-json - - id: check-toml - - id: pretty-format-json - - id: check-added-large-files - - id: check-case-conflict - - id: check-executables-have-shebangs - - id: check-merge-conflict - - id: detect-private-key - - repo: local - hooks: - - id: lint - name: Make lint - stages: [commit] - language: rust - entry: make lint - - id: doc - name: Make doc - stages: [commit] - language: rust - entry: make doc - - id: check - name: Make check - stages: [commit] - language: rust - entry: make check diff --git a/CHANGELOG.md b/CHANGELOG.md index 1804c64..a54f616 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,10 @@ - [BREAKING] Updated Winterfell dependency to v0.12 (#374). - Added debug-only duplicate column check in `build_subtree` (#378). -## 0.13.3 (2025-02-12) +## 0.13.3 (2025-02-18) -- Implement `PartialSmt` (#372). +- Implement `PartialSmt` (#372, #381). +- Fix panic in `PartialMmr::untrack` (#382). ## 0.13.2 (2025-01-24) diff --git a/Cargo.lock b/Cargo.lock index 51856c4..706961d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -120,9 +120,9 @@ checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" [[package]] name = "blake3" -version = "1.5.5" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8ee0c1824c4dea5b5f81736aff91bae041d2c07ee1192bec91054e10e3e601e" +checksum = "1230237285e3e10cde447185e8975408ae24deaa67205ce684805c25bc0c7937" dependencies = [ "arrayref", "arrayvec", @@ -160,9 +160,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.12" +version = "1.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "755717a7de9ec452bf7f3f1a3099085deabd7f2962b861dae91ecd7a365903d2" +checksum = "0c3d1b2e905a3a7b00a6141adb0e4c0bb941d11caf55349d863942a1cc44e3c9" dependencies = [ "jobserver", "libc", @@ -204,9 +204,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.28" +version = "4.5.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e77c3243bd94243c03672cb5154667347c457ca271254724f9f393aee1c05ff" +checksum = "92b7b18d71fad5313a1e320fa9897994228ce274b60faa4d694fe0ea89cd9e6d" dependencies = [ "clap_builder", "clap_derive", @@ -214,9 +214,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.27" +version = "4.5.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" +checksum = "a35db2071778a7344791a4fb4f95308b5673d219dee3ae348b86642574ecc90c" dependencies = [ "anstream", "anstyle", @@ -972,9 +972,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.16.0" +version = "3.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" +checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230" dependencies = [ "cfg-if", "fastrand", @@ -1016,9 +1016,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "unarray" @@ -1275,6 +1275,15 @@ dependencies = [ "bitflags", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags", +] + [[package]] name = "zerocopy" version = "0.7.35" diff --git a/src/merkle/mmr/partial.rs b/src/merkle/mmr/partial.rs index b29c4f5..d4eec97 100644 --- a/src/merkle/mmr/partial.rs +++ b/src/merkle/mmr/partial.rs @@ -341,16 +341,13 @@ impl PartialMmr { pub fn untrack(&mut self, leaf_pos: usize) { let mut idx = InOrderIndex::from_leaf_pos(leaf_pos); - self.nodes.remove(&idx.sibling()); - // `idx` represent the element that can be computed by the authentication path, because // these elements can be computed they are not saved for the authentication of the current // target. In other words, if the idx is present it was added for the authentication of // another element, and no more elements should be removed otherwise it would remove that // element's authentication data. - while !self.nodes.contains_key(&idx) { + while self.nodes.remove(&idx.sibling()).is_some() && !self.nodes.contains_key(&idx) { idx = idx.parent(); - self.nodes.remove(&idx.sibling()); } } @@ -949,4 +946,32 @@ mod tests { assert_eq!(partial_mmr, decoded); } + + #[test] + fn test_partial_mmr_untrack() { + // build the MMR + let mmr: Mmr = LEAVES.into(); + + // get path and node for position 1 + let node1 = mmr.get(1).unwrap(); + let proof1 = mmr.open(1).unwrap(); + + // get path and node for position 2 + let node2 = mmr.get(2).unwrap(); + let proof2 = mmr.open(2).unwrap(); + + // create partial MMR and add authentication path to nodes at position 1 and 2 + let mut partial_mmr: PartialMmr = mmr.peaks().into(); + partial_mmr.track(1, node1, &proof1.merkle_path).unwrap(); + partial_mmr.track(2, node2, &proof2.merkle_path).unwrap(); + + // untrack nodes at positions 1 and 2 + partial_mmr.untrack(1); + partial_mmr.untrack(2); + + // nodes should not longer be tracked + assert!(!partial_mmr.is_tracked(1)); + assert!(!partial_mmr.is_tracked(2)); + assert_eq!(partial_mmr.nodes().count(), 0); + } } diff --git a/src/merkle/smt/partial.rs b/src/merkle/smt/partial.rs index 6d8b8eb..934d2f6 100644 --- a/src/merkle/smt/partial.rs +++ b/src/merkle/smt/partial.rs @@ -138,17 +138,25 @@ impl PartialSmt { Ok(previous_value) } - /// Adds a leaf and its merkle path to this [`PartialSmt`] and returns the value that - /// was previously present at this key, if any. + /// Adds an [`SmtProof`] to this [`PartialSmt`]. /// - /// If this function was called, the `key` can subsequently be updated to a new value and - /// produce a correct new tree root. + /// This is a convenience method which calls [`Self::add_path`] on the proof. See its + /// documentation for details on errors. + pub fn add_proof(&mut self, proof: SmtProof) -> Result<(), MerkleError> { + let (path, leaf) = proof.into_parts(); + self.add_path(leaf, path) + } + + /// Adds a leaf and its merkle path to this [`PartialSmt`]. + /// + /// If this function was called, any key that is part of the `leaf` can subsequently be updated + /// to a new value and produce a correct new tree root. /// /// # Errors /// /// Returns an error if: /// - the new root after the insertion of the leaf and the path does not match the existing root - /// (except if the tree was previously empty). If an error is returned, the tree is left in an + /// (except when the first leaf is added). If an error is returned, the tree is left in an /// inconsistent state. pub fn add_path(&mut self, leaf: SmtLeaf, path: MerklePath) -> Result<(), MerkleError> { let mut current_index = leaf.index().index; @@ -193,9 +201,10 @@ impl PartialSmt { // Check the newly added merkle path is consistent with the existing tree. If not, the // merkle path was invalid or computed from another tree. - // We skip this check if the root is empty since this indicates we're adding the first - // merkle path in which case we have to update the tree root to the root from the path. - if self.root() != Smt::EMPTY_ROOT && self.root() != node_hash_at_current_index { + // + // We skip this check if we have just inserted the first leaf since we assume that leaf's + // root is correct and all subsequent leaves that will be added must have the same root. + if self.0.num_leaves() != 1 && self.root() != node_hash_at_current_index { return Err(MerkleError::ConflictingRoots { expected_root: self.root(), actual_root: node_hash_at_current_index, @@ -358,4 +367,68 @@ mod tests { assert_eq!(partial.get_leaf(&key1).unwrap(), full.get_leaf(&key1)); assert_eq!(partial.get_leaf(&key2).unwrap(), full.get_leaf(&key2)); } + + /// Tests that adding proofs to a partial SMT whose roots are not the same will result in an + /// error. + /// + /// This test uses only empty values in the partial SMT. + #[test] + fn partial_smt_root_mismatch_on_empty_values() { + let key0 = RpoDigest::from(Word::from(rand_array())); + let key1 = RpoDigest::from(Word::from(rand_array())); + let key2 = RpoDigest::from(Word::from(rand_array())); + + let value0 = EMPTY_WORD; + let value1 = Word::from(rand_array()); + let value2 = EMPTY_WORD; + + let kv_pairs = vec![(key0, value0)]; + + let mut full = Smt::with_entries(kv_pairs).unwrap(); + // This proof will be stale after we insert another value. + let stale_proof0 = full.open(&key0); + + // Insert a non-empty value so the root actually changes. + full.insert(key1, value1); + full.insert(key2, value2); + + let proof2 = full.open(&key2); + + let mut partial = PartialSmt::new(); + + partial.add_proof(stale_proof0).unwrap(); + let err = partial.add_proof(proof2).unwrap_err(); + assert_matches!(err, MerkleError::ConflictingRoots { .. }); + } + + /// Tests that adding proofs to a partial SMT whose roots are not the same will result in an + /// error. + /// + /// This test uses only non-empty values in the partial SMT. + #[test] + fn partial_smt_root_mismatch_on_non_empty_values() { + let key0 = RpoDigest::from(Word::from(rand_array())); + let key1 = RpoDigest::from(Word::from(rand_array())); + let key2 = RpoDigest::from(Word::from(rand_array())); + + let value0 = Word::from(rand_array()); + let value1 = Word::from(rand_array()); + let value2 = Word::from(rand_array()); + + let kv_pairs = vec![(key0, value0), (key1, value1)]; + + let mut full = Smt::with_entries(kv_pairs).unwrap(); + // This proof will be stale after we insert another value. + let stale_proof0 = full.open(&key0); + + full.insert(key2, value2); + + let proof2 = full.open(&key2); + + let mut partial = PartialSmt::new(); + + partial.add_proof(stale_proof0).unwrap(); + let err = partial.add_proof(proof2).unwrap_err(); + assert_matches!(err, MerkleError::ConflictingRoots { .. }); + } }