Repository: commonwarexyz/monorepo
Pull request #630: [commonware-storage/mmr] Basic mutable k/v store - part 1
Author: login: roberto-bayardo
Created: 2025-03-18T22:58:24Z | Merged: 2025-03-19T21:00:46Z

Initial mutable mmr implementation with minimal functionality. Supports proving update history but not proving of which value is current for a given key.  Does not currently implement compaction or persistence.


Diff:
diff --git a/storage/src/mmr/mod.rs b/storage/src/mmr/mod.rs
index c5a11aeae0..eba9fdebce 100644
--- a/storage/src/mmr/mod.rs
+++ b/storage/src/mmr/mod.rs
@@ -75,6 +75,7 @@ mod hasher;
 mod iterator;
 pub mod journaled;
 pub mod mem;
+pub mod mutable;
 pub mod verification;
 
 /// Errors that can occur when interacting with an MMR.
@@ -94,4 +95,6 @@ pub enum Error {
     MissingHashes,
     #[error("extra hashes in proof")]
     ExtraHashes,
+    #[error("invalid update")]
+    InvalidUpdate,
 }
diff --git a/storage/src/mmr/mutable.rs b/storage/src/mmr/mutable.rs
new file mode 100644
index 0000000000..716861caa3
--- /dev/null
+++ b/storage/src/mmr/mutable.rs
@@ -0,0 +1,404 @@
+//! An authenticatable & mutable key-value store based on an MMR over the log of state-change
+//! operations.
+//!
+//! # Terminology
+//!
+//! A _key_ in the store either has a _value_ or it doesn't. Two types of _operations_ can be
+//! applied to the store to modify the state of a specific key. A key that has a value can change to
+//! one without a value through the _delete_ operation. The _update_ operation gives a key a
+//! specific value whether it previously had no value or had a different value.
+
+use crate::mmr::{
+    iterator::{leaf_num_to_pos, leaf_pos_to_num},
+    mem::Mmr,
+    verification::Proof,
+    Error,
+};
+use commonware_cryptography::Hasher as CHasher;
+use commonware_utils::Array;
+use std::collections::HashMap;
+use tracing::error;
+
+/// The types of operations that change a key's state in the store.
+#[derive(Clone)]
+pub enum Type<V: Array> {
+    /// Indicates the key no longer has a value.
+    Deleted,
+
+    /// Indicates the key now has the wrapped value.
+    Update(V),
+}
+
+/// An operation applied to the store.
+#[derive(Clone)]
+pub struct Operation<K: Array, V: Array> {
+    /// The key whose state is changed by this operation.
+    pub key: K,
+
+    /// The new state of the key.
+    pub op_type: Type<V>,
+}
+
+/// A mutable key-value store based on an MMR over its log of operations.
+pub struct MutableMmr<K: Array, V: Array, H: CHasher> {
+    /// An MMR over digests of the operations applied to the store. The number of leaves in this MMR
+    /// always equals the number of operations in the `log`.
+    ops: Mmr<H>,
+
+    /// A log of all operations applied to the store in order of occurrence. The index of each
+    /// operation in this vector is called its _location_.
+    ///
+    /// Invariant: An operation's location is always equal to the number of the MMR leaf storing the
+    /// digest of the operation.
+    log: Vec<Operation<K, V>>,
+
+    /// A map from each key to the location in the log containing its most recent update. Only
+    /// contains the keys that currently have a value (that is, deleted keys are not in the map).
+    snapshot: HashMap<K, usize>,
+}
+
+impl<K: Array, V: Array, H: CHasher> Default for MutableMmr<K, V, H> {
+    /// Return a new, empty store.
+    fn default() -> Self {
+        MutableMmr::new()
+    }
+}
+
+impl<K: Array, V: Array, H: CHasher> MutableMmr<K, V, H> {
+    /// Return a new, empty store.
+    pub fn new() -> Self {
+        MutableMmr {
+            ops: Mmr::new(),
+            log: Vec::<Operation<K, V>>::new(),
+            snapshot: HashMap::new(),
+        }
+    }
+
+    /// Return a store initialized to the state corresponding to the operation sequence given by
+    /// `log`.
+    ///
+    /// Performs some checks that the log constitutes a valid sequence of operations, and returns
+    /// [Error::InvalidUpdate] if an invalid sequence is encountered.
+    pub fn init_from_log(hasher: &mut H, log: Vec<Operation<K, V>>) -> Result<Self, Error> {
+        let mut store = MutableMmr {
+            ops: Mmr::new(),
+            log,
+            snapshot: HashMap::new(),
+        };
+
+        for (i, op) in store.log.iter().enumerate() {
+            let digest = Self::op_digest(hasher, op);
+            store.ops.add(hasher, &digest);
+
+            match op.op_type {
+                Type::Deleted => {
+                    let loc = store.snapshot.remove(&op.key);
+                    if loc.is_none() {
+                        // Shouldn't be allowed to delete a key that already has no value.
+                        error!("deleted key {} not found in snapshot", op.key);
+                        return Err(Error::InvalidUpdate);
+                    }
+                }
+                Type::Update(_) => {
+                    store.snapshot.insert(op.key.clone(), i);
+                }
+            };
+        }
+
+        Ok(store)
+    }
+
+    const DELETE_CONTEXT: u8 = 0;
+    const UPDATE_CONTEXT: u8 = 1;
+
+    /// Return a digest of the operation.
+    ///
+    /// The first byte of the digest material is an operation type byte: 0 for Delete and 1 for
+    /// Update. For an update, the value is appended next, followed by the key. For deletion, the
+    /// key is appended without any value.
+    pub fn op_digest(hasher: &mut H, op: &Operation<K, V>) -> H::Digest {
+        match op.op_type {
+            Type::Deleted => hasher.update(&[Self::DELETE_CONTEXT]),
+            Type::Update(ref value) => {
+                hasher.update(&[Self::UPDATE_CONTEXT]);
+                hasher.update(value);
+            }
+        }
+        hasher.update(&op.key);
+        hasher.finalize()
+    }
+
+    /// Get the value of `key` in the store, or None if it has no value.
+    pub fn get(&self, key: &K) -> Option<&V> {
+        let pos = self.snapshot.get(key)?;
+        let op = &self.log[*pos];
+        match &op.op_type {
+            Type::Deleted => panic!("deleted key should not be in snapshot: {}", key),
+            Type::Update(ref v) => Some(v),
+        }
+    }
+
+    /// Get the number of operations that have been applied to this store.
+    pub fn op_count(&self) -> u64 {
+        self.log.len() as u64
+    }
+
+    /// Updates `key` to have value `value`.  If the key already has this same value, then this is a
+    /// no-op.
+    pub fn update(&mut self, hasher: &mut H, key: K, value: V) {
+        let new_loc = self.log.len();
+
+        // Update the snapshot.
+        if let Some(loc) = self.snapshot.get_mut(&key) {
+            let last_value = match self.log[*loc].op_type {
+                Type::Deleted => panic!("deleted key should not be in snapshot: {}", key),
+                Type::Update(ref v) => v,
+            };
+            if value == *last_value {
+                // Trying to assign the same value is a no-op.
+                return;
+            }
+            *loc = new_loc;
+        } else {
+            self.snapshot.insert(key.clone(), new_loc);
+        }
+
+        let op = Type::Update(value.clone());
+        self.apply_op(hasher, key, op);
+    }
+
+    /// Update the log and operations MMR with the given key and operation.
+    fn apply_op(&mut self, hasher: &mut H, key: K, op_type: Type<V>) {
+        let op = Operation { key, op_type };
+
+        // Update the ops MMR.
+        let digest = Self::op_digest(hasher, &op);
+        self.ops.add(hasher, &digest);
+
+        // Append the operation to the log.
+        self.log.push(op);
+    }
+
+    /// Delete `key` and its value from the store. Deleting a key that already has no value is a
+    /// no-op.
+    pub fn delete(&mut self, hasher: &mut H, key: K) {
+        // Remove the key from the snapshot.
+        if self.snapshot.remove(&key).is_none() {
+            return;
+        }
+
+        self.apply_op(hasher, key, Type::Deleted);
+    }
+
+    /// Return the root hash of the mutable MMR.
+    pub fn root(&self, hasher: &mut H) -> H::Digest {
+        self.ops.root(hasher)
+    }
+
+    /// Generate and return:
+    ///  1. a proof of all operations applied to the store in the range starting at (and including)
+    ///     location `start_loc`, and ending at the first of either:
+    ///         - the last operation performed, or
+    ///         - the operation `max_ops` from the start.
+    ///  2. the operations corresponding to the leaves in this range.
+    pub async fn proof_to_tip(
+        &self,
+        start_loc: u64,
+        max_ops: u64,
+    ) -> Result<(Proof<H>, Vec<Operation<K, V>>), Error> {
+        let start_pos = leaf_num_to_pos(start_loc);
+        let end_pos_last = self.ops.last_leaf_pos().unwrap();
+        let end_pos_max = leaf_num_to_pos(start_loc + max_ops - 1);
+        let (end_pos, end_loc) = if end_pos_last < end_pos_max {
+            (end_pos_last, leaf_pos_to_num(end_pos_last).unwrap())
+        } else {
+            (end_pos_max, start_loc + max_ops - 1)
+        };
+
+        let proof = self.ops.range_proof(start_pos, end_pos).await?;
+        let ops = self.log[start_loc as usize..=end_loc as usize].to_vec();
+
+        Ok((proof, ops))
+    }
+
+    /// Return true if the given sequence of `ops` took place starting at location `start_loc` in
+    /// the MMR with the provided root hash.
+    pub fn verify_proof(
+        hasher: &mut H,
+        proof: &Proof<H>,
+        start_loc: u64,
+        ops: &[Operation<K, V>],
+        root_hash: &H::Digest,
+    ) -> bool {
+        let start_pos = leaf_num_to_pos(start_loc);
+        let end_loc = start_loc + ops.len() as u64 - 1;
+        let end_pos = leaf_num_to_pos(end_loc);
+
+        let digests = ops
+            .iter()
+            .map(|op| MutableMmr::op_digest(hasher, op))
+            .collect::<Vec<_>>();
+
+        proof.verify_range_inclusion(hasher, &digests, start_pos, end_pos, root_hash)
+    }
+
+    /// Consume the store and return its log of operations, from which the state of the store can be
+    /// recovered.
+    pub fn to_log(self) -> Vec<Operation<K, V>> {
+        self.log
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    use crate::mmr::iterator::leaf_num_to_pos;
+    use commonware_cryptography::{hash, sha256::Digest, Hasher as CHasher, Sha256};
+    use commonware_runtime::{deterministic::Executor, Runner};
+
+    /// Return an empty store for use in tests.
+    fn empty_store() -> MutableMmr<Digest, Digest, Sha256> {
+        MutableMmr::new()
+    }
+
+    #[test]
+    pub fn test_mutable_mmr_build_basic() {
+        // Build a store with 2 keys and make sure updates and deletions of those keys work as
+        // expected.
+        let mut store = empty_store();
+        let mut hasher = Sha256::new();
+
+        let d1 = <Sha256 as CHasher>::Digest::try_from(&vec![1u8; 32]).unwrap();
+        let d2 = <Sha256 as CHasher>::Digest::try_from(&vec![2u8; 32]).unwrap();
+
+        assert!(store.get(&d1).is_none());
+        assert!(store.get(&d2).is_none());
+
+        store.update(&mut hasher, d1, d2);
+        assert_eq!(store.get(&d1).unwrap(), &d2);
+        assert!(store.get(&d2).is_none());
+
+        store.update(&mut hasher, d2, d1);
+        assert_eq!(store.get(&d1).unwrap(), &d2);
+        assert_eq!(store.get(&d2).unwrap(), &d1);
+
+        store.delete(&mut hasher, d1);
+        assert!(store.get(&d1).is_none());
+        assert_eq!(store.get(&d2).unwrap(), &d1);
+
+        store.update(&mut hasher, d1, d1);
+        assert_eq!(store.get(&d1).unwrap(), &d1);
+
+        store.update(&mut hasher, d2, d2);
+        assert_eq!(store.get(&d2).unwrap(), &d2);
+
+        assert_eq!(store.log.len(), 5); // 4 updates, 1 deletion
+        assert_eq!(store.snapshot.len(), 2);
+
+        let root = store.root(&mut hasher);
+
+        // multiple assignments of the same value should be a no-op.
+        store.update(&mut hasher, d1, d1);
+        store.update(&mut hasher, d2, d2);
+        assert_eq!(store.root(&mut hasher), root);
+
+        // The MMR's size should always be greater than the position of the last leaf.
+        let last_leaf_pos = leaf_num_to_pos(4);
+        assert!(store.ops.size() > last_leaf_pos);
+
+        store.delete(&mut hasher, d1);
+        store.delete(&mut hasher, d2);
+        assert!(store.get(&d1).is_none());
+        assert!(store.get(&d2).is_none());
+
+        let root = store.root(&mut hasher);
+
+        // multiple deletions of the same key should be a no-op.
+        store.delete(&mut hasher, d1);
+        assert_eq!(store.root(&mut hasher), root);
+
+        // deletions of non-existent keys should be a no-op.
+        let d3 = <Sha256 as CHasher>::Digest::try_from(&vec![2u8; 32]).unwrap();
+        store.delete(&mut hasher, d3);
+        assert_eq!(store.root(&mut hasher), root);
+    }
+
+    #[test]
+    pub fn test_mutable_mmr_build_and_authenticate() {
+        let (executor, _, _) = Executor::default();
+        // Build a store with 1000 keys, some of which we update and some of which we delete, and
+        // confirm that the end state of the store matches that of an identically updated hashmap.
+        executor.start(async move {
+            let mut store = empty_store();
+            let mut hasher = Sha256::new();
+            // Store the store's root for every (non-no-op) update.
+            let mut roots = Vec::new();
+
+            let mut map = HashMap::<Digest, Digest>::default();
+            const ELEMENTS: u64 = 1000;
+            for i in 0u64..ELEMENTS {
+                let k = hash(&i.to_be_bytes());
+                let v = hash(&(i * 1000).to_be_bytes());
+                store.update(&mut hasher, k, v);
+                roots.push(store.root(&mut hasher));
+                map.insert(k, v);
+            }
+            // Update every 3rd key
+            for i in 0u64..ELEMENTS {
+                if i % 3 != 0 {
+                    continue;
+                }
+                let k = hash(&i.to_be_bytes());
+                let v = hash(&((i + 1) * 10000).to_be_bytes());
+                store.update(&mut hasher, k, v);
+                roots.push(store.root(&mut hasher));
+                map.insert(k, v);
+            }
+            // Delete every 7th key
+            for i in 0u64..ELEMENTS {
+                if i % 7 != 0 {
+                    continue;
+                }
+                let k = hash(&i.to_be_bytes());
+                store.delete(&mut hasher, k);
+                roots.push(store.root(&mut hasher));
+                map.remove(&k);
+            }
+
+            // Confirm the store's state matches that of the map.
+            let root_hash = roots.last().unwrap();
+            for i in 0u64..1000 {
+                let k = hash(&i.to_be_bytes());
+                if let Some(map_value) = map.get(&k) {
+                    let Some(store_value) = store.get(&k) else {
+                        panic!("key not found in store: {}", k);
+                    };
+                    assert_eq!(map_value, store_value);
+                } else {
+                    assert!(store.get(&k).is_none());
+                }
+            }
+
+            // Test we can recreate the store purely from its log.
+            let log = store.to_log();
+            let store = MutableMmr::init_from_log(&mut hasher, log).unwrap();
+            assert_eq!(*root_hash, store.root(&mut hasher));
+
+            // Make sure size-constrained batches of operations are provable from inception to tip.
+            let end = store.op_count();
+            assert_eq!(roots.len() as u64, end);
+            let max_ops = 4;
+            let root = store.root(&mut hasher);
+            for i in 0..end {
+                let (proof, log) = store.proof_to_tip(i, max_ops).await.unwrap();
+                assert!(MutableMmr::verify_proof(
+                    &mut hasher,
+                    &proof,
+                    i,
+                    &log,
+                    &root
+                ));
+            }
+        });
+    }
+}


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR introduces an initial implementation of a simple mutable key-value store built on a Merkle Mountain Range (MMR). It adds the new mutable MMR module with update and deletion functionality, integrates update proofs into the verification process, and updates the module structure accordingly.

### Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

| File                                | Description                                                 |
|-------------------------------------|-------------------------------------------------------------|
| storage/src/mmr/mutable.rs           | New mutable MMR store implementation with update operations. |
| storage/src/mmr/verification.rs      | Adds update proof structure and updates range proof verification. |
| storage/src/mmr/mod.rs               | Exposes the new mutable module and updates error variants.     |


<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**storage/src/mmr/mutable.rs:92**
* Calling location.unwrap() in the error message when location is None can lead to a panic. Consider removing the unwrap and displaying a more appropriate message or alternative details.
```
error!("deleted key {} at location {} not found in snapshot", record.key, location.unwrap());
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T22:59:06Z
user: login: patrick-ogrady
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T23:19:08Z
user: login: roberto-bayardo
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-18T23:20:41Z
user: login: roberto-bayardo
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-18T23:23:00Z
user: login: roberto-bayardo
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-18T23:23:58Z
user: login: roberto-bayardo
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-19T03:04:27Z
user: login: patrick-ogrady
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T18:11:17Z
user: login: patrick-ogrady
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T18:13:59Z
user: login: patrick-ogrady
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T18:15:09Z
user: login: patrick-ogrady
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T18:15:29Z
user: login: roberto-bayardo
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-19T18:27:27Z
user: login: roberto-bayardo
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-19T19:16:58Z
user: login: roberto-bayardo
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-19T19:19:34Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR introduces a basic mutable key/value MMR store with support for proving update history.  
- Added a new module for mutable functionality.
- Introduced a new error variant "InvalidUpdate" to capture update errors.




state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T20:20:51Z
user: login: patrick-ogrady
body: g2g for part 1 IMO
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T20:29:38Z
user: login: roberto-bayardo
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-19T20:33:04Z
user: login: patrick-ogrady
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T20:38:33Z

Review comments:
diff_hunk: @@ -0,0 +1,382 @@
+//! An in-memory authenticatable & updatable key-value store.
+//!
+//! A _mutable MMR_ is an authenticatable and updatable key-value store based on an [Mmr] over all
+//! updates called the _update tree_.
+
+use crate::mmr::{iterator::leaf_pos_to_num, mem::Mmr, verification::UpdateProof, Error};
+use commonware_cryptography::Hasher as CHasher;
+use commonware_utils::Array;
+use std::collections::HashMap;
+use tracing::error;
+
+#[derive(Clone)]
+pub enum UpdateOp<V: Array> {
+    /// Indicates a key was deleted.
+    Deleted,
+
+    /// Indicates the wrapped value was assigned to a key.
+    Assign(V),
+}
+
+/// A record representing one key/value assignment or deletion.
+#[derive(Clone)]
+pub struct UpdateRecord<K: Array, V: Array> {
+    tree_pos: u64,
+
+    /// The key that was updated.
+    key: K,
+
+    /// The update operation that was performed.
+    update_op: UpdateOp<V>,
path: storage/src/mmr/mutable.rs
user: login: patrick-ogrady
body: I think you could just call this `operation:`
created_at: 2025-03-18T23:15:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,382 @@
+//! An in-memory authenticatable & updatable key-value store.
+//!
+//! A _mutable MMR_ is an authenticatable and updatable key-value store based on an [Mmr] over all
+//! updates called the _update tree_.
+
+use crate::mmr::{iterator::leaf_pos_to_num, mem::Mmr, verification::UpdateProof, Error};
+use commonware_cryptography::Hasher as CHasher;
+use commonware_utils::Array;
+use std::collections::HashMap;
+use tracing::error;
+
+#[derive(Clone)]
+pub enum UpdateOp<V: Array> {
+    /// Indicates a key was deleted.
+    Deleted,
+
+    /// Indicates the wrapped value was assigned to a key.
+    Assign(V),
+}
+
+/// A record representing one key/value assignment or deletion.
+#[derive(Clone)]
+pub struct UpdateRecord<K: Array, V: Array> {
+    tree_pos: u64,
+
+    /// The key that was updated.
+    key: K,
+
+    /// The update operation that was performed.
+    update_op: UpdateOp<V>,
+}
+
+/// A mutable MMR based key-value store.
+pub struct MutableMmr<K: Array, V: Array, H: CHasher> {
+    /// An MMR over digests of the key/value pairs from each UpdateRecord. The number of leaves in
+    /// this MMR always equals the number of records.
+    updates: Mmr<H>,
+
+    /// All updates and deletions made to the store in order of execution. The index of each record
+    /// in this vector is called its _location_, which is equivalent to its leaf's position in the
+    /// updates MMR.
+    records: Vec<UpdateRecord<K, V>>,
+
+    /// A map from each key to the record location containing its most recently assigned value. Keys
+    /// that have been deleted are not stored in this map.
+    snapshot: HashMap<K, usize>,
+}
+
+impl<K: Array, V: Array, H: CHasher> Default for MutableMmr<K, V, H> {
+    fn default() -> Self {
+        MutableMmr::new()
+    }
+}
+
+impl<K: Array, V: Array, H: CHasher> MutableMmr<K, V, H> {
+    /// Create a new, empty mutable MMR.
+    pub fn new() -> Self {
+        MutableMmr {
+            updates: Mmr::new(),
+            snapshot: HashMap::new(),
+            records: Vec::<UpdateRecord<K, V>>::new(),
+        }
+    }
+
+    /// Return a mutable MMR store initialized to the state corresponding to the update sequence
+    /// given by `records`.
+    ///
+    /// Performs some checks that the records constitute a valid sequence of updates, and returns
+    /// [Error::InvalidUpdate] if an invalid sequence is encountered. These checks are not
+    /// exhaustive.
+    pub fn init_from_records(
+        hasher: &mut H,
+        records: Vec<UpdateRecord<K, V>>,
+    ) -> Result<Self, Error> {
+        let mut store = MutableMmr {
+            updates: Mmr::new(),
+            snapshot: HashMap::new(),
+            records,
+        };
+
+        for (i, record) in store.records.iter().enumerate() {
+            let digest = Self::key_update_digest(hasher, &record.key, &record.update_op);
+            let pos = store.updates.add(hasher, &digest);
+            if pos != record.tree_pos {
+                error!("record.tree pos invalid");
+                return Err(Error::InvalidUpdate);
+            }
+
+            let old_location = match record.update_op {
+                UpdateOp::Deleted => {
+                    let location = store.snapshot.remove(&record.key);
+                    if location.is_none() {
+                        error!(
+                            "deleted key {} at location {} not found in snapshot",
+                            record.key,
+                            location.unwrap()
+                        );
+                        return Err(Error::InvalidUpdate);
+                    }
+                    location
+                }
+                UpdateOp::Assign(_) => store.snapshot.insert(record.key.clone(), i),
+            };
+
+            let Some(old_location) = old_location else {
+                continue;
+            };
+            let old_record = &store.records[old_location];
+
+            if old_record.key != record.key {
+                error!("record.key doesn't match old record");
+                return Err(Error::InvalidUpdate);
+            }
+        }
+
+        Ok(store)
+    }
+
+    /// Return a digest of the key plus its update operation.
+    ///
+    /// The first byte of the key material is an operation type byte, 0 for Delete and 1 for Assign.
+    /// For assignment, the value is appended next, followed by the key. For deletion, the key
+    /// alone is appended.
+    pub fn key_update_digest(hasher: &mut H, key: &K, update_op: &UpdateOp<V>) -> H::Digest {
+        match update_op {
+            UpdateOp::Deleted => hasher.update(&[0u8]),
path: storage/src/mmr/mutable.rs
user: login: patrick-ogrady
body: recommend making these consts
created_at: 2025-03-18T23:15:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,382 @@
+//! An in-memory authenticatable & updatable key-value store.
+//!
+//! A _mutable MMR_ is an authenticatable and updatable key-value store based on an [Mmr] over all
+//! updates called the _update tree_.
+
+use crate::mmr::{iterator::leaf_pos_to_num, mem::Mmr, verification::UpdateProof, Error};
+use commonware_cryptography::Hasher as CHasher;
+use commonware_utils::Array;
+use std::collections::HashMap;
+use tracing::error;
+
+#[derive(Clone)]
+pub enum UpdateOp<V: Array> {
+    /// Indicates a key was deleted.
+    Deleted,
+
+    /// Indicates the wrapped value was assigned to a key.
+    Assign(V),
+}
+
+/// A record representing one key/value assignment or deletion.
+#[derive(Clone)]
+pub struct UpdateRecord<K: Array, V: Array> {
+    tree_pos: u64,
+
+    /// The key that was updated.
+    key: K,
+
+    /// The update operation that was performed.
+    update_op: UpdateOp<V>,
+}
+
+/// A mutable MMR based key-value store.
+pub struct MutableMmr<K: Array, V: Array, H: CHasher> {
+    /// An MMR over digests of the key/value pairs from each UpdateRecord. The number of leaves in
+    /// this MMR always equals the number of records.
+    updates: Mmr<H>,
+
+    /// All updates and deletions made to the store in order of execution. The index of each record
+    /// in this vector is called its _location_, which is equivalent to its leaf's position in the
+    /// updates MMR.
+    records: Vec<UpdateRecord<K, V>>,
+
+    /// A map from each key to the record location containing its most recently assigned value. Keys
+    /// that have been deleted are not stored in this map.
+    snapshot: HashMap<K, usize>,
+}
+
+impl<K: Array, V: Array, H: CHasher> Default for MutableMmr<K, V, H> {
+    fn default() -> Self {
+        MutableMmr::new()
+    }
+}
+
+impl<K: Array, V: Array, H: CHasher> MutableMmr<K, V, H> {
+    /// Create a new, empty mutable MMR.
+    pub fn new() -> Self {
+        MutableMmr {
+            updates: Mmr::new(),
+            snapshot: HashMap::new(),
+            records: Vec::<UpdateRecord<K, V>>::new(),
+        }
+    }
+
+    /// Return a mutable MMR store initialized to the state corresponding to the update sequence
+    /// given by `records`.
+    ///
+    /// Performs some checks that the records constitute a valid sequence of updates, and returns
+    /// [Error::InvalidUpdate] if an invalid sequence is encountered. These checks are not
+    /// exhaustive.
+    pub fn init_from_records(
+        hasher: &mut H,
+        records: Vec<UpdateRecord<K, V>>,
+    ) -> Result<Self, Error> {
+        let mut store = MutableMmr {
+            updates: Mmr::new(),
+            snapshot: HashMap::new(),
+            records,
+        };
+
+        for (i, record) in store.records.iter().enumerate() {
+            let digest = Self::key_update_digest(hasher, &record.key, &record.update_op);
+            let pos = store.updates.add(hasher, &digest);
+            if pos != record.tree_pos {
+                error!("record.tree pos invalid");
+                return Err(Error::InvalidUpdate);
+            }
+
+            let old_location = match record.update_op {
+                UpdateOp::Deleted => {
+                    let location = store.snapshot.remove(&record.key);
+                    if location.is_none() {
+                        error!(
+                            "deleted key {} at location {} not found in snapshot",
+                            record.key,
+                            location.unwrap()
+                        );
+                        return Err(Error::InvalidUpdate);
+                    }
+                    location
+                }
+                UpdateOp::Assign(_) => store.snapshot.insert(record.key.clone(), i),
+            };
+
+            let Some(old_location) = old_location else {
+                continue;
+            };
+            let old_record = &store.records[old_location];
+
+            if old_record.key != record.key {
+                error!("record.key doesn't match old record");
+                return Err(Error::InvalidUpdate);
+            }
+        }
+
+        Ok(store)
+    }
+
+    /// Return a digest of the key plus its update operation.
+    ///
+    /// The first byte of the key material is an operation type byte, 0 for Delete and 1 for Assign.
+    /// For assignment, the value is appended next, followed by the key. For deletion, the key
+    /// alone is appended.
+    pub fn key_update_digest(hasher: &mut H, key: &K, update_op: &UpdateOp<V>) -> H::Digest {
+        match update_op {
+            UpdateOp::Deleted => hasher.update(&[0u8]),
+            UpdateOp::Assign(value) => {
+                hasher.update(&[1u8]);
+                hasher.update(value);
path: storage/src/mmr/mutable.rs
user: login: patrick-ogrady
body: If these weren't fixed size, I'd say we might want to use `union_unique` from `utils`.
created_at: 2025-03-18T23:16:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,382 @@
+//! An in-memory authenticatable & updatable key-value store.
+//!
+//! A _mutable MMR_ is an authenticatable and updatable key-value store based on an [Mmr] over all
+//! updates called the _update tree_.
+
+use crate::mmr::{iterator::leaf_pos_to_num, mem::Mmr, verification::UpdateProof, Error};
+use commonware_cryptography::Hasher as CHasher;
+use commonware_utils::Array;
+use std::collections::HashMap;
+use tracing::error;
+
+#[derive(Clone)]
+pub enum UpdateOp<V: Array> {
+    /// Indicates a key was deleted.
+    Deleted,
+
+    /// Indicates the wrapped value was assigned to a key.
+    Assign(V),
+}
+
+/// A record representing one key/value assignment or deletion.
+#[derive(Clone)]
+pub struct UpdateRecord<K: Array, V: Array> {
+    tree_pos: u64,
+
+    /// The key that was updated.
+    key: K,
+
+    /// The update operation that was performed.
+    update_op: UpdateOp<V>,
+}
+
+/// A mutable MMR based key-value store.
+pub struct MutableMmr<K: Array, V: Array, H: CHasher> {
+    /// An MMR over digests of the key/value pairs from each UpdateRecord. The number of leaves in
+    /// this MMR always equals the number of records.
+    updates: Mmr<H>,
+
+    /// All updates and deletions made to the store in order of execution. The index of each record
+    /// in this vector is called its _location_, which is equivalent to its leaf's position in the
+    /// updates MMR.
+    records: Vec<UpdateRecord<K, V>>,
+
+    /// A map from each key to the record location containing its most recently assigned value. Keys
+    /// that have been deleted are not stored in this map.
+    snapshot: HashMap<K, usize>,
+}
+
+impl<K: Array, V: Array, H: CHasher> Default for MutableMmr<K, V, H> {
+    fn default() -> Self {
+        MutableMmr::new()
+    }
+}
+
+impl<K: Array, V: Array, H: CHasher> MutableMmr<K, V, H> {
+    /// Create a new, empty mutable MMR.
+    pub fn new() -> Self {
+        MutableMmr {
+            updates: Mmr::new(),
+            snapshot: HashMap::new(),
+            records: Vec::<UpdateRecord<K, V>>::new(),
+        }
+    }
+
+    /// Return a mutable MMR store initialized to the state corresponding to the update sequence
+    /// given by `records`.
+    ///
+    /// Performs some checks that the records constitute a valid sequence of updates, and returns
+    /// [Error::InvalidUpdate] if an invalid sequence is encountered. These checks are not
+    /// exhaustive.
+    pub fn init_from_records(
+        hasher: &mut H,
+        records: Vec<UpdateRecord<K, V>>,
+    ) -> Result<Self, Error> {
+        let mut store = MutableMmr {
+            updates: Mmr::new(),
+            snapshot: HashMap::new(),
+            records,
+        };
+
+        for (i, record) in store.records.iter().enumerate() {
+            let digest = Self::key_update_digest(hasher, &record.key, &record.update_op);
+            let pos = store.updates.add(hasher, &digest);
+            if pos != record.tree_pos {
+                error!("record.tree pos invalid");
+                return Err(Error::InvalidUpdate);
+            }
+
+            let old_location = match record.update_op {
+                UpdateOp::Deleted => {
+                    let location = store.snapshot.remove(&record.key);
+                    if location.is_none() {
+                        error!(
+                            "deleted key {} at location {} not found in snapshot",
+                            record.key,
+                            location.unwrap()
+                        );
+                        return Err(Error::InvalidUpdate);
+                    }
+                    location
+                }
+                UpdateOp::Assign(_) => store.snapshot.insert(record.key.clone(), i),
+            };
+
+            let Some(old_location) = old_location else {
+                continue;
+            };
+            let old_record = &store.records[old_location];
+
+            if old_record.key != record.key {
+                error!("record.key doesn't match old record");
+                return Err(Error::InvalidUpdate);
+            }
+        }
+
+        Ok(store)
+    }
+
+    /// Return a digest of the key plus its update operation.
+    ///
+    /// The first byte of the key material is an operation type byte, 0 for Delete and 1 for Assign.
+    /// For assignment, the value is appended next, followed by the key. For deletion, the key
+    /// alone is appended.
+    pub fn key_update_digest(hasher: &mut H, key: &K, update_op: &UpdateOp<V>) -> H::Digest {
+        match update_op {
+            UpdateOp::Deleted => hasher.update(&[0u8]),
+            UpdateOp::Assign(value) => {
+                hasher.update(&[1u8]);
+                hasher.update(value);
+            }
+        }
+        hasher.update(key);
+        hasher.finalize()
+    }
+
+    /// Get the value of `key` in the store, or None if it has no value.
+    pub fn get(&self, key: &K) -> Option<&V> {
+        let pos = self.snapshot.get(key)?;
+        let record = &self.records[*pos];
+        match &record.update_op {
+            UpdateOp::Deleted => panic!("deleted key should not be in snapshot: {}", key),
+            UpdateOp::Assign(ref v) => Some(v),
+        }
+    }
+
+    /// Get the update corresponding to the mmr leaf at `pos`, or None if the
+    /// position is not for a leaf.
+    pub fn get_update(&self, pos: u64) -> Option<&UpdateOp<V>> {
+        assert!(pos < self.updates.size());
+        let leaf_num = leaf_pos_to_num(pos)?;
+        let record = &self.records[leaf_num as usize];
+
+        Some(&record.update_op)
+    }
+
+    /// Assigns `value` to `key` in the store.  If the key is already assigned the same value, then
+    /// this is a no-op.
+    pub fn assign(&mut self, hasher: &mut H, key: K, value: V) {
path: storage/src/mmr/mutable.rs
user: login: patrick-ogrady
body: Why not call this `update` (like before)? Just confusion with `update op`?
created_at: 2025-03-18T23:17:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,382 @@
+//! An in-memory authenticatable & updatable key-value store.
+//!
+//! A _mutable MMR_ is an authenticatable and updatable key-value store based on an [Mmr] over all
+//! updates called the _update tree_.
+
+use crate::mmr::{iterator::leaf_pos_to_num, mem::Mmr, verification::UpdateProof, Error};
+use commonware_cryptography::Hasher as CHasher;
+use commonware_utils::Array;
+use std::collections::HashMap;
+use tracing::error;
+
+#[derive(Clone)]
+pub enum UpdateOp<V: Array> {
+    /// Indicates a key was deleted.
+    Deleted,
+
+    /// Indicates the wrapped value was assigned to a key.
+    Assign(V),
+}
+
+/// A record representing one key/value assignment or deletion.
+#[derive(Clone)]
+pub struct UpdateRecord<K: Array, V: Array> {
+    tree_pos: u64,
+
+    /// The key that was updated.
+    key: K,
+
+    /// The update operation that was performed.
+    update_op: UpdateOp<V>,
+}
+
+/// A mutable MMR based key-value store.
+pub struct MutableMmr<K: Array, V: Array, H: CHasher> {
+    /// An MMR over digests of the key/value pairs from each UpdateRecord. The number of leaves in
+    /// this MMR always equals the number of records.
+    updates: Mmr<H>,
+
+    /// All updates and deletions made to the store in order of execution. The index of each record
+    /// in this vector is called its _location_, which is equivalent to its leaf's position in the
+    /// updates MMR.
+    records: Vec<UpdateRecord<K, V>>,
+
+    /// A map from each key to the record location containing its most recently assigned value. Keys
+    /// that have been deleted are not stored in this map.
+    snapshot: HashMap<K, usize>,
+}
+
+impl<K: Array, V: Array, H: CHasher> Default for MutableMmr<K, V, H> {
+    fn default() -> Self {
+        MutableMmr::new()
+    }
+}
+
+impl<K: Array, V: Array, H: CHasher> MutableMmr<K, V, H> {
+    /// Create a new, empty mutable MMR.
+    pub fn new() -> Self {
+        MutableMmr {
+            updates: Mmr::new(),
+            snapshot: HashMap::new(),
+            records: Vec::<UpdateRecord<K, V>>::new(),
+        }
+    }
+
+    /// Return a mutable MMR store initialized to the state corresponding to the update sequence
+    /// given by `records`.
+    ///
+    /// Performs some checks that the records constitute a valid sequence of updates, and returns
+    /// [Error::InvalidUpdate] if an invalid sequence is encountered. These checks are not
+    /// exhaustive.
+    pub fn init_from_records(
+        hasher: &mut H,
+        records: Vec<UpdateRecord<K, V>>,
+    ) -> Result<Self, Error> {
+        let mut store = MutableMmr {
+            updates: Mmr::new(),
+            snapshot: HashMap::new(),
+            records,
+        };
+
+        for (i, record) in store.records.iter().enumerate() {
+            let digest = Self::key_update_digest(hasher, &record.key, &record.update_op);
+            let pos = store.updates.add(hasher, &digest);
+            if pos != record.tree_pos {
+                error!("record.tree pos invalid");
+                return Err(Error::InvalidUpdate);
+            }
+
+            let old_location = match record.update_op {
+                UpdateOp::Deleted => {
+                    let location = store.snapshot.remove(&record.key);
+                    if location.is_none() {
+                        error!(
+                            "deleted key {} at location {} not found in snapshot",
+                            record.key,
+                            location.unwrap()
+                        );
+                        return Err(Error::InvalidUpdate);
+                    }
+                    location
+                }
+                UpdateOp::Assign(_) => store.snapshot.insert(record.key.clone(), i),
+            };
+
+            let Some(old_location) = old_location else {
+                continue;
+            };
+            let old_record = &store.records[old_location];
+
+            if old_record.key != record.key {
+                error!("record.key doesn't match old record");
+                return Err(Error::InvalidUpdate);
+            }
+        }
+
+        Ok(store)
+    }
+
+    /// Return a digest of the key plus its update operation.
+    ///
+    /// The first byte of the key material is an operation type byte, 0 for Delete and 1 for Assign.
+    /// For assignment, the value is appended next, followed by the key. For deletion, the key
+    /// alone is appended.
+    pub fn key_update_digest(hasher: &mut H, key: &K, update_op: &UpdateOp<V>) -> H::Digest {
+        match update_op {
+            UpdateOp::Deleted => hasher.update(&[0u8]),
+            UpdateOp::Assign(value) => {
+                hasher.update(&[1u8]);
+                hasher.update(value);
+            }
+        }
+        hasher.update(key);
+        hasher.finalize()
+    }
+
+    /// Get the value of `key` in the store, or None if it has no value.
+    pub fn get(&self, key: &K) -> Option<&V> {
+        let pos = self.snapshot.get(key)?;
+        let record = &self.records[*pos];
+        match &record.update_op {
+            UpdateOp::Deleted => panic!("deleted key should not be in snapshot: {}", key),
+            UpdateOp::Assign(ref v) => Some(v),
+        }
+    }
+
+    /// Get the update corresponding to the mmr leaf at `pos`, or None if the
+    /// position is not for a leaf.
+    pub fn get_update(&self, pos: u64) -> Option<&UpdateOp<V>> {
+        assert!(pos < self.updates.size());
+        let leaf_num = leaf_pos_to_num(pos)?;
+        let record = &self.records[leaf_num as usize];
+
+        Some(&record.update_op)
+    }
+
+    /// Assigns `value` to `key` in the store.  If the key is already assigned the same value, then
+    /// this is a no-op.
+    pub fn assign(&mut self, hasher: &mut H, key: K, value: V) {
path: storage/src/mmr/mutable.rs
user: login: roberto-bayardo
body: An "update" is either an assignment to or a deletion of a key.
created_at: 2025-03-18T23:20:41Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,382 @@
+//! An in-memory authenticatable & updatable key-value store.
+//!
+//! A _mutable MMR_ is an authenticatable and updatable key-value store based on an [Mmr] over all
+//! updates called the _update tree_.
+
+use crate::mmr::{iterator::leaf_pos_to_num, mem::Mmr, verification::UpdateProof, Error};
+use commonware_cryptography::Hasher as CHasher;
+use commonware_utils::Array;
+use std::collections::HashMap;
+use tracing::error;
+
+#[derive(Clone)]
+pub enum UpdateOp<V: Array> {
+    /// Indicates a key was deleted.
+    Deleted,
+
+    /// Indicates the wrapped value was assigned to a key.
+    Assign(V),
+}
+
+/// A record representing one key/value assignment or deletion.
+#[derive(Clone)]
+pub struct UpdateRecord<K: Array, V: Array> {
+    tree_pos: u64,
+
+    /// The key that was updated.
+    key: K,
+
+    /// The update operation that was performed.
+    update_op: UpdateOp<V>,
+}
+
+/// A mutable MMR based key-value store.
+pub struct MutableMmr<K: Array, V: Array, H: CHasher> {
+    /// An MMR over digests of the key/value pairs from each UpdateRecord. The number of leaves in
+    /// this MMR always equals the number of records.
+    updates: Mmr<H>,
+
+    /// All updates and deletions made to the store in order of execution. The index of each record
+    /// in this vector is called its _location_, which is equivalent to its leaf's position in the
+    /// updates MMR.
+    records: Vec<UpdateRecord<K, V>>,
+
+    /// A map from each key to the record location containing its most recently assigned value. Keys
+    /// that have been deleted are not stored in this map.
+    snapshot: HashMap<K, usize>,
+}
+
+impl<K: Array, V: Array, H: CHasher> Default for MutableMmr<K, V, H> {
+    fn default() -> Self {
+        MutableMmr::new()
+    }
+}
+
+impl<K: Array, V: Array, H: CHasher> MutableMmr<K, V, H> {
+    /// Create a new, empty mutable MMR.
+    pub fn new() -> Self {
+        MutableMmr {
+            updates: Mmr::new(),
+            snapshot: HashMap::new(),
+            records: Vec::<UpdateRecord<K, V>>::new(),
+        }
+    }
+
+    /// Return a mutable MMR store initialized to the state corresponding to the update sequence
+    /// given by `records`.
+    ///
+    /// Performs some checks that the records constitute a valid sequence of updates, and returns
+    /// [Error::InvalidUpdate] if an invalid sequence is encountered. These checks are not
+    /// exhaustive.
+    pub fn init_from_records(
+        hasher: &mut H,
+        records: Vec<UpdateRecord<K, V>>,
+    ) -> Result<Self, Error> {
+        let mut store = MutableMmr {
+            updates: Mmr::new(),
+            snapshot: HashMap::new(),
+            records,
+        };
+
+        for (i, record) in store.records.iter().enumerate() {
+            let digest = Self::key_update_digest(hasher, &record.key, &record.update_op);
+            let pos = store.updates.add(hasher, &digest);
+            if pos != record.tree_pos {
+                error!("record.tree pos invalid");
+                return Err(Error::InvalidUpdate);
+            }
+
+            let old_location = match record.update_op {
+                UpdateOp::Deleted => {
+                    let location = store.snapshot.remove(&record.key);
+                    if location.is_none() {
+                        error!(
+                            "deleted key {} at location {} not found in snapshot",
+                            record.key,
+                            location.unwrap()
+                        );
+                        return Err(Error::InvalidUpdate);
+                    }
+                    location
+                }
+                UpdateOp::Assign(_) => store.snapshot.insert(record.key.clone(), i),
+            };
+
+            let Some(old_location) = old_location else {
+                continue;
+            };
+            let old_record = &store.records[old_location];
+
+            if old_record.key != record.key {
+                error!("record.key doesn't match old record");
+                return Err(Error::InvalidUpdate);
+            }
+        }
+
+        Ok(store)
+    }
+
+    /// Return a digest of the key plus its update operation.
+    ///
+    /// The first byte of the key material is an operation type byte, 0 for Delete and 1 for Assign.
+    /// For assignment, the value is appended next, followed by the key. For deletion, the key
+    /// alone is appended.
+    pub fn key_update_digest(hasher: &mut H, key: &K, update_op: &UpdateOp<V>) -> H::Digest {
+        match update_op {
+            UpdateOp::Deleted => hasher.update(&[0u8]),
path: storage/src/mmr/mutable.rs
user: login: roberto-bayardo
body: done
created_at: 2025-03-18T23:22:59Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,382 @@
+//! An in-memory authenticatable & updatable key-value store.
+//!
+//! A _mutable MMR_ is an authenticatable and updatable key-value store based on an [Mmr] over all
+//! updates called the _update tree_.
+
+use crate::mmr::{iterator::leaf_pos_to_num, mem::Mmr, verification::UpdateProof, Error};
+use commonware_cryptography::Hasher as CHasher;
+use commonware_utils::Array;
+use std::collections::HashMap;
+use tracing::error;
+
+#[derive(Clone)]
+pub enum UpdateOp<V: Array> {
+    /// Indicates a key was deleted.
+    Deleted,
+
+    /// Indicates the wrapped value was assigned to a key.
+    Assign(V),
+}
+
+/// A record representing one key/value assignment or deletion.
+#[derive(Clone)]
+pub struct UpdateRecord<K: Array, V: Array> {
+    tree_pos: u64,
+
+    /// The key that was updated.
+    key: K,
+
+    /// The update operation that was performed.
+    update_op: UpdateOp<V>,
path: storage/src/mmr/mutable.rs
user: login: roberto-bayardo
body: how about just "update" ?  The terminology I'm using now is that there are two types of "updates" that can be made to a key, assignment or deletion.
created_at: 2025-03-18T23:23:57Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,382 @@
+//! An in-memory authenticatable & updatable key-value store.
+//!
+//! A _mutable MMR_ is an authenticatable and updatable key-value store based on an [Mmr] over all
+//! updates called the _update tree_.
+
+use crate::mmr::{iterator::leaf_pos_to_num, mem::Mmr, verification::UpdateProof, Error};
+use commonware_cryptography::Hasher as CHasher;
+use commonware_utils::Array;
+use std::collections::HashMap;
+use tracing::error;
+
+#[derive(Clone)]
+pub enum UpdateOp<V: Array> {
+    /// Indicates a key was deleted.
+    Deleted,
+
+    /// Indicates the wrapped value was assigned to a key.
+    Assign(V),
+}
+
+/// A record representing one key/value assignment or deletion.
+#[derive(Clone)]
+pub struct UpdateRecord<K: Array, V: Array> {
+    tree_pos: u64,
+
+    /// The key that was updated.
+    key: K,
+
+    /// The update operation that was performed.
+    update_op: UpdateOp<V>,
path: storage/src/mmr/mutable.rs
user: login: roberto-bayardo
body: UpdateType ?
created_at: 2025-03-19T03:04:27Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,385 @@
+//! An authenticatable & mutable key-value store based on an MMR over the log of state-change
+//! operations.
+//!
+//! # Terminology
+//!
+//! A _key_ in the store either has a _value_ or it doesn't. Two types of _operations_ can be
+//! applied to the store to modify the state of a specific key. A key that has a value can change to
+//! one without a value through the _delete_ operation. The _update_ operation gives a key a
+//! specific value whether it previously had no value or had a different value.
+
+use crate::mmr::{
+    iterator::{leaf_num_to_pos, leaf_pos_to_num},
+    mem::Mmr,
+    verification::OperationProof as Proof,
+    Error,
+};
+use commonware_cryptography::Hasher as CHasher;
+use commonware_utils::Array;
+use std::collections::HashMap;
+use tracing::error;
+
+/// The types of operations that change a key's state in the store.
+#[derive(Clone)]
+pub enum Type<V: Array> {
+    /// Indicates the key no longer has a value.
+    Deleted,
+
+    /// Indicates the key now has the wrapped value.
+    Update(V),
+}
+
+/// An operation applied to the store.
+#[derive(Clone)]
+pub struct Operation<K: Array, V: Array> {
+    pub key: K,
+    pub op_type: Type<V>,
+}
+
+/// A mutable key-value store based on an MMR over its log of operations.
+pub struct MutableMmr<K: Array, V: Array, H: CHasher> {
+    /// An MMR over digests of the operations applied to the store. The number of leaves in this MMR
+    /// always equals the number of operations in the `log`.
+    ops: Mmr<H>,
+
+    /// A log of all operations applied to the store in order of occurrence. The index of each
+    /// operation in this vector is called its _location_.
+    ///
+    /// Invariant: An operation's location is always equal to the number of the MMR leaf storing the
+    /// digest of the operation.
+    log: Vec<Operation<K, V>>,
+
+    /// A map from each key to the location in the log containing its most recent update. Only
+    /// contains the keys that currently have a value (that is, deleted keys are not in the map).
+    snapshot: HashMap<K, usize>,
+}
+
+impl<K: Array, V: Array, H: CHasher> Default for MutableMmr<K, V, H> {
+    /// Return a new, empty store.
+    fn default() -> Self {
+        MutableMmr::new()
+    }
+}
+
+impl<K: Array, V: Array, H: CHasher> MutableMmr<K, V, H> {
+    /// Return a new, empty store.
+    pub fn new() -> Self {
+        MutableMmr {
+            ops: Mmr::new(),
+            log: Vec::<Operation<K, V>>::new(),
+            snapshot: HashMap::new(),
+        }
+    }
+
+    /// Return a store initialized to the state corresponding to the operation sequence given by
+    /// `log`.
+    ///
+    /// Performs some checks that the log constitutes a valid sequence of operations, and returns
+    /// [Error::InvalidUpdate] if an invalid sequence is encountered.
+    pub fn init_from_log(hasher: &mut H, log: Vec<Operation<K, V>>) -> Result<Self, Error> {
+        let mut store = MutableMmr {
+            ops: Mmr::new(),
+            log,
+            snapshot: HashMap::new(),
+        };
+
+        for (i, op) in store.log.iter().enumerate() {
+            let digest = Self::op_digest(hasher, op);
+            store.ops.add(hasher, &digest);
+
+            match op.op_type {
+                Type::Deleted => {
+                    let location = store.snapshot.remove(&op.key);
+                    if location.is_none() {
+                        // Shouldn't be allowed to delete a key that already has no value.
+                        error!("deleted key {} not found in snapshot", op.key);
+                        return Err(Error::InvalidUpdate);
+                    }
+                    location
+                }
+                Type::Update(_) => store.snapshot.insert(op.key.clone(), i),
+            };
+        }
+
+        Ok(store)
+    }
+
+    const DELETE_CONTEXT: u8 = 0;
+    const UPDATE_CONTEXT: u8 = 1;
+
+    /// Return a digest of the operation.
+    ///
+    /// The first byte of the key material is an operation type byte: 0 for Delete and 1 for Update.
+    /// For an update, the value is appended next, followed by the key. For deletion, the key alone
+    /// is appended.
+    pub fn op_digest(hasher: &mut H, op: &Operation<K, V>) -> H::Digest {
+        match op.op_type {
+            Type::Deleted => hasher.update(&[Self::DELETE_CONTEXT]),
+            Type::Update(ref value) => {
+                hasher.update(&[Self::UPDATE_CONTEXT]);
+                hasher.update(value);
+            }
+        }
+        hasher.update(&op.key);
+        hasher.finalize()
+    }
+
+    /// Get the value of `key` in the store, or None if it has no value.
+    pub fn get(&self, key: &K) -> Option<&V> {
+        let pos = self.snapshot.get(key)?;
+        let op = &self.log[*pos];
+        match &op.op_type {
+            Type::Deleted => panic!("deleted key should not be in snapshot: {}", key),
+            Type::Update(ref v) => Some(v),
+        }
+    }
+
+    /// Get the number of operations that have been applied to this store.
+    pub fn operation_count(&self) -> u64 {
+        self.log.len() as u64
+    }
+
+    /// Updates `key` to have value `value`.  If the key already has this same value, then this is a
+    /// no-op.
+    pub fn update(&mut self, hasher: &mut H, key: K, value: V) {
+        let new_location = self.log.len();
+
+        // Update the snapshot.
+        if let Some(location) = self.snapshot.get_mut(&key) {
+            let last_value = match self.log[*location].op_type {
+                Type::Deleted => panic!("deleted key should not be in snapshot: {}", key),
+                Type::Update(ref v) => v,
+            };
+            if value == *last_value {
+                // Trying to assign the same value is a no-op.
+                return;
+            }
+            *location = new_location;
+        } else {
+            self.snapshot.insert(key.clone(), new_location);
+        }
+
+        let op = Type::Update(value.clone());
+        self.update_work(hasher, key, op);
+    }
+
+    /// Update the log and operations MMR with the given key and operation.
+    fn update_work(&mut self, hasher: &mut H, key: K, op_type: Type<V>) {
path: storage/src/mmr/mutable.rs
user: login: patrick-ogrady
body: nit: `apply()`
created_at: 2025-03-19T18:11:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,385 @@
+//! An authenticatable & mutable key-value store based on an MMR over the log of state-change
+//! operations.
+//!
+//! # Terminology
+//!
+//! A _key_ in the store either has a _value_ or it doesn't. Two types of _operations_ can be
+//! applied to the store to modify the state of a specific key. A key that has a value can change to
+//! one without a value through the _delete_ operation. The _update_ operation gives a key a
+//! specific value whether it previously had no value or had a different value.
+
+use crate::mmr::{
+    iterator::{leaf_num_to_pos, leaf_pos_to_num},
+    mem::Mmr,
+    verification::OperationProof as Proof,
+    Error,
+};
+use commonware_cryptography::Hasher as CHasher;
+use commonware_utils::Array;
+use std::collections::HashMap;
+use tracing::error;
+
+/// The types of operations that change a key's state in the store.
+#[derive(Clone)]
+pub enum Type<V: Array> {
+    /// Indicates the key no longer has a value.
+    Deleted,
+
+    /// Indicates the key now has the wrapped value.
+    Update(V),
+}
+
+/// An operation applied to the store.
+#[derive(Clone)]
+pub struct Operation<K: Array, V: Array> {
+    pub key: K,
+    pub op_type: Type<V>,
+}
+
+/// A mutable key-value store based on an MMR over its log of operations.
+pub struct MutableMmr<K: Array, V: Array, H: CHasher> {
+    /// An MMR over digests of the operations applied to the store. The number of leaves in this MMR
+    /// always equals the number of operations in the `log`.
+    ops: Mmr<H>,
+
+    /// A log of all operations applied to the store in order of occurrence. The index of each
+    /// operation in this vector is called its _location_.
+    ///
+    /// Invariant: An operation's location is always equal to the number of the MMR leaf storing the
+    /// digest of the operation.
+    log: Vec<Operation<K, V>>,
+
+    /// A map from each key to the location in the log containing its most recent update. Only
+    /// contains the keys that currently have a value (that is, deleted keys are not in the map).
+    snapshot: HashMap<K, usize>,
+}
+
+impl<K: Array, V: Array, H: CHasher> Default for MutableMmr<K, V, H> {
+    /// Return a new, empty store.
+    fn default() -> Self {
+        MutableMmr::new()
+    }
+}
+
+impl<K: Array, V: Array, H: CHasher> MutableMmr<K, V, H> {
+    /// Return a new, empty store.
+    pub fn new() -> Self {
+        MutableMmr {
+            ops: Mmr::new(),
+            log: Vec::<Operation<K, V>>::new(),
+            snapshot: HashMap::new(),
+        }
+    }
+
+    /// Return a store initialized to the state corresponding to the operation sequence given by
+    /// `log`.
+    ///
+    /// Performs some checks that the log constitutes a valid sequence of operations, and returns
+    /// [Error::InvalidUpdate] if an invalid sequence is encountered.
+    pub fn init_from_log(hasher: &mut H, log: Vec<Operation<K, V>>) -> Result<Self, Error> {
+        let mut store = MutableMmr {
+            ops: Mmr::new(),
+            log,
+            snapshot: HashMap::new(),
+        };
+
+        for (i, op) in store.log.iter().enumerate() {
+            let digest = Self::op_digest(hasher, op);
+            store.ops.add(hasher, &digest);
+
+            match op.op_type {
+                Type::Deleted => {
+                    let location = store.snapshot.remove(&op.key);
+                    if location.is_none() {
+                        // Shouldn't be allowed to delete a key that already has no value.
+                        error!("deleted key {} not found in snapshot", op.key);
+                        return Err(Error::InvalidUpdate);
+                    }
+                    location
+                }
+                Type::Update(_) => store.snapshot.insert(op.key.clone(), i),
+            };
+        }
+
+        Ok(store)
+    }
+
+    const DELETE_CONTEXT: u8 = 0;
+    const UPDATE_CONTEXT: u8 = 1;
+
+    /// Return a digest of the operation.
+    ///
+    /// The first byte of the key material is an operation type byte: 0 for Delete and 1 for Update.
+    /// For an update, the value is appended next, followed by the key. For deletion, the key alone
+    /// is appended.
+    pub fn op_digest(hasher: &mut H, op: &Operation<K, V>) -> H::Digest {
+        match op.op_type {
+            Type::Deleted => hasher.update(&[Self::DELETE_CONTEXT]),
+            Type::Update(ref value) => {
+                hasher.update(&[Self::UPDATE_CONTEXT]);
+                hasher.update(value);
+            }
+        }
+        hasher.update(&op.key);
+        hasher.finalize()
+    }
+
+    /// Get the value of `key` in the store, or None if it has no value.
+    pub fn get(&self, key: &K) -> Option<&V> {
+        let pos = self.snapshot.get(key)?;
+        let op = &self.log[*pos];
+        match &op.op_type {
+            Type::Deleted => panic!("deleted key should not be in snapshot: {}", key),
+            Type::Update(ref v) => Some(v),
+        }
+    }
+
+    /// Get the number of operations that have been applied to this store.
+    pub fn operation_count(&self) -> u64 {
+        self.log.len() as u64
+    }
+
+    /// Updates `key` to have value `value`.  If the key already has this same value, then this is a
+    /// no-op.
+    pub fn update(&mut self, hasher: &mut H, key: K, value: V) {
+        let new_location = self.log.len();
+
+        // Update the snapshot.
+        if let Some(location) = self.snapshot.get_mut(&key) {
+            let last_value = match self.log[*location].op_type {
+                Type::Deleted => panic!("deleted key should not be in snapshot: {}", key),
+                Type::Update(ref v) => v,
+            };
+            if value == *last_value {
+                // Trying to assign the same value is a no-op.
+                return;
+            }
+            *location = new_location;
+        } else {
+            self.snapshot.insert(key.clone(), new_location);
+        }
+
+        let op = Type::Update(value.clone());
+        self.update_work(hasher, key, op);
+    }
+
+    /// Update the log and operations MMR with the given key and operation.
+    fn update_work(&mut self, hasher: &mut H, key: K, op_type: Type<V>) {
+        let op = Operation { key, op_type };
+
+        // Update the ops MMR.
+        let digest = Self::op_digest(hasher, &op);
+        self.ops.add(hasher, &digest);
+
+        // Append the operation to the log.
+        self.log.push(op);
+    }
+
+    /// Delete `key` and its value from the store. Deleting a key that already has no value is a
+    /// no-op.
+    pub fn delete(&mut self, hasher: &mut H, key: K) {
+        // Remove the key from the snapshot.
+        if self.snapshot.remove(&key).is_none() {
+            return;
+        }
+
+        self.update_work(hasher, key, Type::Deleted);
+    }
+
+    /// Return the root hash of the mutable MMR.
+    pub fn root(&self, hasher: &mut H) -> H::Digest {
+        self.ops.root(hasher)
+    }
+
+    /// Return the size of the underlying MMR.
+    ///
+    /// This value will be the position of the next leaf to be added to the underlying MMR, and can
+    /// be used to retrieve a starting position value for generating a proof over the next batch of
+    /// operations.
+    pub fn size(&self) -> u64 {
+        self.ops.size()
+    }
+
+    /// Generate and return:
+    ///  1. a proof of all operations applied to the store in the range starting at (and including)
+    ///     location `start_location`, and ending at the first of either:
+    ///         - the last operation performed, or
+    ///         - the operation `max_ops` from the start.
+    ///  2. the operations corresponding to the leaves in this range.
+    pub async fn proof_to_tip(
+        &self,
+        start_loc: u64,
+        max_ops: u64,
+    ) -> Result<(Proof<K, V, H>, Vec<Operation<K, V>>), Error> {
+        let start_pos = leaf_num_to_pos(start_loc);
+        let end_pos_last = self.ops.last_leaf_pos().unwrap();
path: storage/src/mmr/mutable.rs
user: login: patrick-ogrady
body: If called on an empty MMR, this would now panic right (as we don't start with anything)?
created_at: 2025-03-19T18:13:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,385 @@
+//! An authenticatable & mutable key-value store based on an MMR over the log of state-change
+//! operations.
+//!
+//! # Terminology
+//!
+//! A _key_ in the store either has a _value_ or it doesn't. Two types of _operations_ can be
+//! applied to the store to modify the state of a specific key. A key that has a value can change to
+//! one without a value through the _delete_ operation. The _update_ operation gives a key a
+//! specific value whether it previously had no value or had a different value.
+
+use crate::mmr::{
+    iterator::{leaf_num_to_pos, leaf_pos_to_num},
+    mem::Mmr,
+    verification::OperationProof as Proof,
+    Error,
+};
+use commonware_cryptography::Hasher as CHasher;
+use commonware_utils::Array;
+use std::collections::HashMap;
+use tracing::error;
+
+/// The types of operations that change a key's state in the store.
+#[derive(Clone)]
+pub enum Type<V: Array> {
+    /// Indicates the key no longer has a value.
+    Deleted,
+
+    /// Indicates the key now has the wrapped value.
+    Update(V),
+}
+
+/// An operation applied to the store.
+#[derive(Clone)]
+pub struct Operation<K: Array, V: Array> {
+    pub key: K,
+    pub op_type: Type<V>,
+}
+
+/// A mutable key-value store based on an MMR over its log of operations.
+pub struct MutableMmr<K: Array, V: Array, H: CHasher> {
+    /// An MMR over digests of the operations applied to the store. The number of leaves in this MMR
+    /// always equals the number of operations in the `log`.
+    ops: Mmr<H>,
+
+    /// A log of all operations applied to the store in order of occurrence. The index of each
+    /// operation in this vector is called its _location_.
+    ///
+    /// Invariant: An operation's location is always equal to the number of the MMR leaf storing the
+    /// digest of the operation.
+    log: Vec<Operation<K, V>>,
+
+    /// A map from each key to the location in the log containing its most recent update. Only
+    /// contains the keys that currently have a value (that is, deleted keys are not in the map).
+    snapshot: HashMap<K, usize>,
+}
+
+impl<K: Array, V: Array, H: CHasher> Default for MutableMmr<K, V, H> {
+    /// Return a new, empty store.
+    fn default() -> Self {
+        MutableMmr::new()
+    }
+}
+
+impl<K: Array, V: Array, H: CHasher> MutableMmr<K, V, H> {
+    /// Return a new, empty store.
+    pub fn new() -> Self {
+        MutableMmr {
+            ops: Mmr::new(),
+            log: Vec::<Operation<K, V>>::new(),
+            snapshot: HashMap::new(),
+        }
+    }
+
+    /// Return a store initialized to the state corresponding to the operation sequence given by
+    /// `log`.
+    ///
+    /// Performs some checks that the log constitutes a valid sequence of operations, and returns
+    /// [Error::InvalidUpdate] if an invalid sequence is encountered.
+    pub fn init_from_log(hasher: &mut H, log: Vec<Operation<K, V>>) -> Result<Self, Error> {
+        let mut store = MutableMmr {
+            ops: Mmr::new(),
+            log,
+            snapshot: HashMap::new(),
+        };
+
+        for (i, op) in store.log.iter().enumerate() {
+            let digest = Self::op_digest(hasher, op);
+            store.ops.add(hasher, &digest);
+
+            match op.op_type {
+                Type::Deleted => {
+                    let location = store.snapshot.remove(&op.key);
+                    if location.is_none() {
+                        // Shouldn't be allowed to delete a key that already has no value.
+                        error!("deleted key {} not found in snapshot", op.key);
+                        return Err(Error::InvalidUpdate);
+                    }
+                    location
+                }
+                Type::Update(_) => store.snapshot.insert(op.key.clone(), i),
+            };
+        }
+
+        Ok(store)
+    }
+
+    const DELETE_CONTEXT: u8 = 0;
path: storage/src/mmr/mutable.rs
user: login: patrick-ogrady
body: I would normally suggest we infer from the enum but think it is better to be explicit (to avoid subtle refactor bugs).
created_at: 2025-03-19T18:15:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,385 @@
+//! An authenticatable & mutable key-value store based on an MMR over the log of state-change
+//! operations.
+//!
+//! # Terminology
+//!
+//! A _key_ in the store either has a _value_ or it doesn't. Two types of _operations_ can be
+//! applied to the store to modify the state of a specific key. A key that has a value can change to
+//! one without a value through the _delete_ operation. The _update_ operation gives a key a
+//! specific value whether it previously had no value or had a different value.
+
+use crate::mmr::{
+    iterator::{leaf_num_to_pos, leaf_pos_to_num},
+    mem::Mmr,
+    verification::OperationProof as Proof,
+    Error,
+};
+use commonware_cryptography::Hasher as CHasher;
+use commonware_utils::Array;
+use std::collections::HashMap;
+use tracing::error;
+
+/// The types of operations that change a key's state in the store.
+#[derive(Clone)]
+pub enum Type<V: Array> {
+    /// Indicates the key no longer has a value.
+    Deleted,
+
+    /// Indicates the key now has the wrapped value.
+    Update(V),
+}
+
+/// An operation applied to the store.
+#[derive(Clone)]
+pub struct Operation<K: Array, V: Array> {
+    pub key: K,
+    pub op_type: Type<V>,
+}
+
+/// A mutable key-value store based on an MMR over its log of operations.
+pub struct MutableMmr<K: Array, V: Array, H: CHasher> {
+    /// An MMR over digests of the operations applied to the store. The number of leaves in this MMR
+    /// always equals the number of operations in the `log`.
+    ops: Mmr<H>,
+
+    /// A log of all operations applied to the store in order of occurrence. The index of each
+    /// operation in this vector is called its _location_.
+    ///
+    /// Invariant: An operation's location is always equal to the number of the MMR leaf storing the
+    /// digest of the operation.
+    log: Vec<Operation<K, V>>,
+
+    /// A map from each key to the location in the log containing its most recent update. Only
+    /// contains the keys that currently have a value (that is, deleted keys are not in the map).
+    snapshot: HashMap<K, usize>,
+}
+
+impl<K: Array, V: Array, H: CHasher> Default for MutableMmr<K, V, H> {
+    /// Return a new, empty store.
+    fn default() -> Self {
+        MutableMmr::new()
+    }
+}
+
+impl<K: Array, V: Array, H: CHasher> MutableMmr<K, V, H> {
+    /// Return a new, empty store.
+    pub fn new() -> Self {
+        MutableMmr {
+            ops: Mmr::new(),
+            log: Vec::<Operation<K, V>>::new(),
+            snapshot: HashMap::new(),
+        }
+    }
+
+    /// Return a store initialized to the state corresponding to the operation sequence given by
+    /// `log`.
+    ///
+    /// Performs some checks that the log constitutes a valid sequence of operations, and returns
+    /// [Error::InvalidUpdate] if an invalid sequence is encountered.
+    pub fn init_from_log(hasher: &mut H, log: Vec<Operation<K, V>>) -> Result<Self, Error> {
+        let mut store = MutableMmr {
+            ops: Mmr::new(),
+            log,
+            snapshot: HashMap::new(),
+        };
+
+        for (i, op) in store.log.iter().enumerate() {
+            let digest = Self::op_digest(hasher, op);
+            store.ops.add(hasher, &digest);
+
+            match op.op_type {
+                Type::Deleted => {
+                    let location = store.snapshot.remove(&op.key);
+                    if location.is_none() {
+                        // Shouldn't be allowed to delete a key that already has no value.
+                        error!("deleted key {} not found in snapshot", op.key);
+                        return Err(Error::InvalidUpdate);
+                    }
+                    location
+                }
+                Type::Update(_) => store.snapshot.insert(op.key.clone(), i),
+            };
+        }
+
+        Ok(store)
+    }
+
+    const DELETE_CONTEXT: u8 = 0;
path: storage/src/mmr/mutable.rs
user: login: patrick-ogrady
body: Alternatively, we could infer from the enum but just hardcode a bunch of roots in the tests.
created_at: 2025-03-19T18:15:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,385 @@
+//! An authenticatable & mutable key-value store based on an MMR over the log of state-change
+//! operations.
+//!
+//! # Terminology
+//!
+//! A _key_ in the store either has a _value_ or it doesn't. Two types of _operations_ can be
+//! applied to the store to modify the state of a specific key. A key that has a value can change to
+//! one without a value through the _delete_ operation. The _update_ operation gives a key a
+//! specific value whether it previously had no value or had a different value.
+
+use crate::mmr::{
+    iterator::{leaf_num_to_pos, leaf_pos_to_num},
+    mem::Mmr,
+    verification::OperationProof as Proof,
+    Error,
+};
+use commonware_cryptography::Hasher as CHasher;
+use commonware_utils::Array;
+use std::collections::HashMap;
+use tracing::error;
+
+/// The types of operations that change a key's state in the store.
+#[derive(Clone)]
+pub enum Type<V: Array> {
+    /// Indicates the key no longer has a value.
+    Deleted,
+
+    /// Indicates the key now has the wrapped value.
+    Update(V),
+}
+
+/// An operation applied to the store.
+#[derive(Clone)]
+pub struct Operation<K: Array, V: Array> {
+    pub key: K,
+    pub op_type: Type<V>,
+}
+
+/// A mutable key-value store based on an MMR over its log of operations.
+pub struct MutableMmr<K: Array, V: Array, H: CHasher> {
+    /// An MMR over digests of the operations applied to the store. The number of leaves in this MMR
+    /// always equals the number of operations in the `log`.
+    ops: Mmr<H>,
+
+    /// A log of all operations applied to the store in order of occurrence. The index of each
+    /// operation in this vector is called its _location_.
+    ///
+    /// Invariant: An operation's location is always equal to the number of the MMR leaf storing the
+    /// digest of the operation.
+    log: Vec<Operation<K, V>>,
+
+    /// A map from each key to the location in the log containing its most recent update. Only
+    /// contains the keys that currently have a value (that is, deleted keys are not in the map).
+    snapshot: HashMap<K, usize>,
+}
+
+impl<K: Array, V: Array, H: CHasher> Default for MutableMmr<K, V, H> {
+    /// Return a new, empty store.
+    fn default() -> Self {
+        MutableMmr::new()
+    }
+}
+
+impl<K: Array, V: Array, H: CHasher> MutableMmr<K, V, H> {
+    /// Return a new, empty store.
+    pub fn new() -> Self {
+        MutableMmr {
+            ops: Mmr::new(),
+            log: Vec::<Operation<K, V>>::new(),
+            snapshot: HashMap::new(),
+        }
+    }
+
+    /// Return a store initialized to the state corresponding to the operation sequence given by
+    /// `log`.
+    ///
+    /// Performs some checks that the log constitutes a valid sequence of operations, and returns
+    /// [Error::InvalidUpdate] if an invalid sequence is encountered.
+    pub fn init_from_log(hasher: &mut H, log: Vec<Operation<K, V>>) -> Result<Self, Error> {
+        let mut store = MutableMmr {
+            ops: Mmr::new(),
+            log,
+            snapshot: HashMap::new(),
+        };
+
+        for (i, op) in store.log.iter().enumerate() {
+            let digest = Self::op_digest(hasher, op);
+            store.ops.add(hasher, &digest);
+
+            match op.op_type {
+                Type::Deleted => {
+                    let location = store.snapshot.remove(&op.key);
+                    if location.is_none() {
+                        // Shouldn't be allowed to delete a key that already has no value.
+                        error!("deleted key {} not found in snapshot", op.key);
+                        return Err(Error::InvalidUpdate);
+                    }
+                    location
+                }
+                Type::Update(_) => store.snapshot.insert(op.key.clone(), i),
+            };
+        }
+
+        Ok(store)
+    }
+
+    const DELETE_CONTEXT: u8 = 0;
+    const UPDATE_CONTEXT: u8 = 1;
+
+    /// Return a digest of the operation.
+    ///
+    /// The first byte of the key material is an operation type byte: 0 for Delete and 1 for Update.
+    /// For an update, the value is appended next, followed by the key. For deletion, the key alone
+    /// is appended.
+    pub fn op_digest(hasher: &mut H, op: &Operation<K, V>) -> H::Digest {
+        match op.op_type {
+            Type::Deleted => hasher.update(&[Self::DELETE_CONTEXT]),
+            Type::Update(ref value) => {
+                hasher.update(&[Self::UPDATE_CONTEXT]);
+                hasher.update(value);
+            }
+        }
+        hasher.update(&op.key);
+        hasher.finalize()
+    }
+
+    /// Get the value of `key` in the store, or None if it has no value.
+    pub fn get(&self, key: &K) -> Option<&V> {
+        let pos = self.snapshot.get(key)?;
+        let op = &self.log[*pos];
+        match &op.op_type {
+            Type::Deleted => panic!("deleted key should not be in snapshot: {}", key),
+            Type::Update(ref v) => Some(v),
+        }
+    }
+
+    /// Get the number of operations that have been applied to this store.
+    pub fn operation_count(&self) -> u64 {
+        self.log.len() as u64
+    }
+
+    /// Updates `key` to have value `value`.  If the key already has this same value, then this is a
+    /// no-op.
+    pub fn update(&mut self, hasher: &mut H, key: K, value: V) {
+        let new_location = self.log.len();
+
+        // Update the snapshot.
+        if let Some(location) = self.snapshot.get_mut(&key) {
+            let last_value = match self.log[*location].op_type {
+                Type::Deleted => panic!("deleted key should not be in snapshot: {}", key),
+                Type::Update(ref v) => v,
+            };
+            if value == *last_value {
+                // Trying to assign the same value is a no-op.
+                return;
+            }
+            *location = new_location;
+        } else {
+            self.snapshot.insert(key.clone(), new_location);
+        }
+
+        let op = Type::Update(value.clone());
+        self.update_work(hasher, key, op);
+    }
+
+    /// Update the log and operations MMR with the given key and operation.
+    fn update_work(&mut self, hasher: &mut H, key: K, op_type: Type<V>) {
+        let op = Operation { key, op_type };
+
+        // Update the ops MMR.
+        let digest = Self::op_digest(hasher, &op);
+        self.ops.add(hasher, &digest);
+
+        // Append the operation to the log.
+        self.log.push(op);
+    }
+
+    /// Delete `key` and its value from the store. Deleting a key that already has no value is a
+    /// no-op.
+    pub fn delete(&mut self, hasher: &mut H, key: K) {
+        // Remove the key from the snapshot.
+        if self.snapshot.remove(&key).is_none() {
+            return;
+        }
+
+        self.update_work(hasher, key, Type::Deleted);
+    }
+
+    /// Return the root hash of the mutable MMR.
+    pub fn root(&self, hasher: &mut H) -> H::Digest {
+        self.ops.root(hasher)
+    }
+
+    /// Return the size of the underlying MMR.
+    ///
+    /// This value will be the position of the next leaf to be added to the underlying MMR, and can
+    /// be used to retrieve a starting position value for generating a proof over the next batch of
+    /// operations.
+    pub fn size(&self) -> u64 {
+        self.ops.size()
+    }
+
+    /// Generate and return:
+    ///  1. a proof of all operations applied to the store in the range starting at (and including)
+    ///     location `start_location`, and ending at the first of either:
+    ///         - the last operation performed, or
+    ///         - the operation `max_ops` from the start.
+    ///  2. the operations corresponding to the leaves in this range.
+    pub async fn proof_to_tip(
+        &self,
+        start_loc: u64,
+        max_ops: u64,
+    ) -> Result<(Proof<K, V, H>, Vec<Operation<K, V>>), Error> {
+        let start_pos = leaf_num_to_pos(start_loc);
+        let end_pos_last = self.ops.last_leaf_pos().unwrap();
path: storage/src/mmr/mutable.rs
user: login: roberto-bayardo
body: Yes implementation assumes the user has provided a valid start_loc (meaning the mmr would never be empty).

created_at: 2025-03-19T18:27:25Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,385 @@
+//! An authenticatable & mutable key-value store based on an MMR over the log of state-change
+//! operations.
+//!
+//! # Terminology
+//!
+//! A _key_ in the store either has a _value_ or it doesn't. Two types of _operations_ can be
+//! applied to the store to modify the state of a specific key. A key that has a value can change to
+//! one without a value through the _delete_ operation. The _update_ operation gives a key a
+//! specific value whether it previously had no value or had a different value.
+
+use crate::mmr::{
+    iterator::{leaf_num_to_pos, leaf_pos_to_num},
+    mem::Mmr,
+    verification::OperationProof as Proof,
+    Error,
+};
+use commonware_cryptography::Hasher as CHasher;
+use commonware_utils::Array;
+use std::collections::HashMap;
+use tracing::error;
+
+/// The types of operations that change a key's state in the store.
+#[derive(Clone)]
+pub enum Type<V: Array> {
+    /// Indicates the key no longer has a value.
+    Deleted,
+
+    /// Indicates the key now has the wrapped value.
+    Update(V),
+}
+
+/// An operation applied to the store.
+#[derive(Clone)]
+pub struct Operation<K: Array, V: Array> {
+    pub key: K,
+    pub op_type: Type<V>,
+}
+
+/// A mutable key-value store based on an MMR over its log of operations.
+pub struct MutableMmr<K: Array, V: Array, H: CHasher> {
+    /// An MMR over digests of the operations applied to the store. The number of leaves in this MMR
+    /// always equals the number of operations in the `log`.
+    ops: Mmr<H>,
+
+    /// A log of all operations applied to the store in order of occurrence. The index of each
+    /// operation in this vector is called its _location_.
+    ///
+    /// Invariant: An operation's location is always equal to the number of the MMR leaf storing the
+    /// digest of the operation.
+    log: Vec<Operation<K, V>>,
+
+    /// A map from each key to the location in the log containing its most recent update. Only
+    /// contains the keys that currently have a value (that is, deleted keys are not in the map).
+    snapshot: HashMap<K, usize>,
+}
+
+impl<K: Array, V: Array, H: CHasher> Default for MutableMmr<K, V, H> {
+    /// Return a new, empty store.
+    fn default() -> Self {
+        MutableMmr::new()
+    }
+}
+
+impl<K: Array, V: Array, H: CHasher> MutableMmr<K, V, H> {
+    /// Return a new, empty store.
+    pub fn new() -> Self {
+        MutableMmr {
+            ops: Mmr::new(),
+            log: Vec::<Operation<K, V>>::new(),
+            snapshot: HashMap::new(),
+        }
+    }
+
+    /// Return a store initialized to the state corresponding to the operation sequence given by
+    /// `log`.
+    ///
+    /// Performs some checks that the log constitutes a valid sequence of operations, and returns
+    /// [Error::InvalidUpdate] if an invalid sequence is encountered.
+    pub fn init_from_log(hasher: &mut H, log: Vec<Operation<K, V>>) -> Result<Self, Error> {
+        let mut store = MutableMmr {
+            ops: Mmr::new(),
+            log,
+            snapshot: HashMap::new(),
+        };
+
+        for (i, op) in store.log.iter().enumerate() {
+            let digest = Self::op_digest(hasher, op);
+            store.ops.add(hasher, &digest);
+
+            match op.op_type {
+                Type::Deleted => {
+                    let location = store.snapshot.remove(&op.key);
+                    if location.is_none() {
+                        // Shouldn't be allowed to delete a key that already has no value.
+                        error!("deleted key {} not found in snapshot", op.key);
+                        return Err(Error::InvalidUpdate);
+                    }
+                    location
+                }
+                Type::Update(_) => store.snapshot.insert(op.key.clone(), i),
+            };
+        }
+
+        Ok(store)
+    }
+
+    const DELETE_CONTEXT: u8 = 0;
+    const UPDATE_CONTEXT: u8 = 1;
+
+    /// Return a digest of the operation.
+    ///
+    /// The first byte of the key material is an operation type byte: 0 for Delete and 1 for Update.
+    /// For an update, the value is appended next, followed by the key. For deletion, the key alone
+    /// is appended.
+    pub fn op_digest(hasher: &mut H, op: &Operation<K, V>) -> H::Digest {
+        match op.op_type {
+            Type::Deleted => hasher.update(&[Self::DELETE_CONTEXT]),
+            Type::Update(ref value) => {
+                hasher.update(&[Self::UPDATE_CONTEXT]);
+                hasher.update(value);
+            }
+        }
+        hasher.update(&op.key);
+        hasher.finalize()
+    }
+
+    /// Get the value of `key` in the store, or None if it has no value.
+    pub fn get(&self, key: &K) -> Option<&V> {
+        let pos = self.snapshot.get(key)?;
+        let op = &self.log[*pos];
+        match &op.op_type {
+            Type::Deleted => panic!("deleted key should not be in snapshot: {}", key),
+            Type::Update(ref v) => Some(v),
+        }
+    }
+
+    /// Get the number of operations that have been applied to this store.
+    pub fn operation_count(&self) -> u64 {
+        self.log.len() as u64
+    }
+
+    /// Updates `key` to have value `value`.  If the key already has this same value, then this is a
+    /// no-op.
+    pub fn update(&mut self, hasher: &mut H, key: K, value: V) {
+        let new_location = self.log.len();
+
+        // Update the snapshot.
+        if let Some(location) = self.snapshot.get_mut(&key) {
+            let last_value = match self.log[*location].op_type {
+                Type::Deleted => panic!("deleted key should not be in snapshot: {}", key),
+                Type::Update(ref v) => v,
+            };
+            if value == *last_value {
+                // Trying to assign the same value is a no-op.
+                return;
+            }
+            *location = new_location;
+        } else {
+            self.snapshot.insert(key.clone(), new_location);
+        }
+
+        let op = Type::Update(value.clone());
+        self.update_work(hasher, key, op);
+    }
+
+    /// Update the log and operations MMR with the given key and operation.
+    fn update_work(&mut self, hasher: &mut H, key: K, op_type: Type<V>) {
path: storage/src/mmr/mutable.rs
user: login: roberto-bayardo
body: changed.
created_at: 2025-03-19T19:16:58Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,385 @@
+//! An authenticatable & mutable key-value store based on an MMR over the log of state-change
+//! operations.
+//!
+//! # Terminology
+//!
+//! A _key_ in the store either has a _value_ or it doesn't. Two types of _operations_ can be
+//! applied to the store to modify the state of a specific key. A key that has a value can change to
+//! one without a value through the _delete_ operation. The _update_ operation gives a key a
+//! specific value whether it previously had no value or had a different value.
+
+use crate::mmr::{
+    iterator::{leaf_num_to_pos, leaf_pos_to_num},
+    mem::Mmr,
+    verification::OperationProof as Proof,
+    Error,
+};
+use commonware_cryptography::Hasher as CHasher;
+use commonware_utils::Array;
+use std::collections::HashMap;
+use tracing::error;
+
+/// The types of operations that change a key's state in the store.
+#[derive(Clone)]
+pub enum Type<V: Array> {
+    /// Indicates the key no longer has a value.
+    Deleted,
+
+    /// Indicates the key now has the wrapped value.
+    Update(V),
+}
+
+/// An operation applied to the store.
+#[derive(Clone)]
+pub struct Operation<K: Array, V: Array> {
+    pub key: K,
+    pub op_type: Type<V>,
+}
+
+/// A mutable key-value store based on an MMR over its log of operations.
+pub struct MutableMmr<K: Array, V: Array, H: CHasher> {
+    /// An MMR over digests of the operations applied to the store. The number of leaves in this MMR
+    /// always equals the number of operations in the `log`.
+    ops: Mmr<H>,
+
+    /// A log of all operations applied to the store in order of occurrence. The index of each
+    /// operation in this vector is called its _location_.
+    ///
+    /// Invariant: An operation's location is always equal to the number of the MMR leaf storing the
+    /// digest of the operation.
+    log: Vec<Operation<K, V>>,
+
+    /// A map from each key to the location in the log containing its most recent update. Only
+    /// contains the keys that currently have a value (that is, deleted keys are not in the map).
+    snapshot: HashMap<K, usize>,
+}
+
+impl<K: Array, V: Array, H: CHasher> Default for MutableMmr<K, V, H> {
+    /// Return a new, empty store.
+    fn default() -> Self {
+        MutableMmr::new()
+    }
+}
+
+impl<K: Array, V: Array, H: CHasher> MutableMmr<K, V, H> {
+    /// Return a new, empty store.
+    pub fn new() -> Self {
+        MutableMmr {
+            ops: Mmr::new(),
+            log: Vec::<Operation<K, V>>::new(),
+            snapshot: HashMap::new(),
+        }
+    }
+
+    /// Return a store initialized to the state corresponding to the operation sequence given by
+    /// `log`.
+    ///
+    /// Performs some checks that the log constitutes a valid sequence of operations, and returns
+    /// [Error::InvalidUpdate] if an invalid sequence is encountered.
+    pub fn init_from_log(hasher: &mut H, log: Vec<Operation<K, V>>) -> Result<Self, Error> {
+        let mut store = MutableMmr {
+            ops: Mmr::new(),
+            log,
+            snapshot: HashMap::new(),
+        };
+
+        for (i, op) in store.log.iter().enumerate() {
+            let digest = Self::op_digest(hasher, op);
+            store.ops.add(hasher, &digest);
+
+            match op.op_type {
+                Type::Deleted => {
+                    let location = store.snapshot.remove(&op.key);
+                    if location.is_none() {
+                        // Shouldn't be allowed to delete a key that already has no value.
+                        error!("deleted key {} not found in snapshot", op.key);
+                        return Err(Error::InvalidUpdate);
+                    }
+                    location
+                }
+                Type::Update(_) => store.snapshot.insert(op.key.clone(), i),
+            };
+        }
+
+        Ok(store)
+    }
+
+    const DELETE_CONTEXT: u8 = 0;
path: storage/src/mmr/mutable.rs
user: login: roberto-bayardo
body: I think keeping it explicit like this may be best since it's part of the specification of the digest.
created_at: 2025-03-19T19:19:34Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,403 @@
+//! An authenticatable & mutable key-value store based on an MMR over the log of state-change
+//! operations.
+//!
+//! # Terminology
+//!
+//! A _key_ in the store either has a _value_ or it doesn't. Two types of _operations_ can be
+//! applied to the store to modify the state of a specific key. A key that has a value can change to
+//! one without a value through the _delete_ operation. The _update_ operation gives a key a
+//! specific value whether it previously had no value or had a different value.
+
+use crate::mmr::{
+    iterator::{leaf_num_to_pos, leaf_pos_to_num},
+    mem::Mmr,
+    verification::Proof,
+    Error,
+};
+use commonware_cryptography::Hasher as CHasher;
+use commonware_utils::Array;
+use std::collections::HashMap;
+use tracing::error;
+
+/// The types of operations that change a key's state in the store.
+#[derive(Clone)]
+pub enum Type<V: Array> {
+    /// Indicates the key no longer has a value.
+    Deleted,
+
+    /// Indicates the key now has the wrapped value.
+    Update(V),
+}
+
+/// An operation applied to the store.
+#[derive(Clone)]
+pub struct Operation<K: Array, V: Array> {
+    /// The key whose state is changed by this operation.
+    pub key: K,
+
+    /// The new state of the key.
+    pub op_type: Type<V>,
+}
+
+/// A mutable key-value store based on an MMR over its log of operations.
+pub struct MutableMmr<K: Array, V: Array, H: CHasher> {
+    /// An MMR over digests of the operations applied to the store. The number of leaves in this MMR
+    /// always equals the number of operations in the `log`.
+    ops: Mmr<H>,
+
+    /// A log of all operations applied to the store in order of occurrence. The index of each
+    /// operation in this vector is called its _location_.
+    ///
+    /// Invariant: An operation's location is always equal to the number of the MMR leaf storing the
+    /// digest of the operation.
+    log: Vec<Operation<K, V>>,
+
+    /// A map from each key to the location in the log containing its most recent update. Only
+    /// contains the keys that currently have a value (that is, deleted keys are not in the map).
+    snapshot: HashMap<K, usize>,
+}
+
+impl<K: Array, V: Array, H: CHasher> Default for MutableMmr<K, V, H> {
+    /// Return a new, empty store.
+    fn default() -> Self {
+        MutableMmr::new()
+    }
+}
+
+impl<K: Array, V: Array, H: CHasher> MutableMmr<K, V, H> {
+    /// Return a new, empty store.
+    pub fn new() -> Self {
+        MutableMmr {
+            ops: Mmr::new(),
+            log: Vec::<Operation<K, V>>::new(),
+            snapshot: HashMap::new(),
+        }
+    }
+
+    /// Return a store initialized to the state corresponding to the operation sequence given by
+    /// `log`.
+    ///
+    /// Performs some checks that the log constitutes a valid sequence of operations, and returns
+    /// [Error::InvalidUpdate] if an invalid sequence is encountered.
+    pub fn init_from_log(hasher: &mut H, log: Vec<Operation<K, V>>) -> Result<Self, Error> {
+        let mut store = MutableMmr {
+            ops: Mmr::new(),
+            log,
+            snapshot: HashMap::new(),
+        };
+
+        for (i, op) in store.log.iter().enumerate() {
+            let digest = Self::op_digest(hasher, op);
+            store.ops.add(hasher, &digest);
+
+            match op.op_type {
+                Type::Deleted => {
+                    let location = store.snapshot.remove(&op.key);
+                    if location.is_none() {
+                        // Shouldn't be allowed to delete a key that already has no value.
path: storage/src/mmr/mutable.rs
user: login: patrick-ogrady
body: When we read from journal, we'll need to be careful to read key dependencies in-order I suppose.
created_at: 2025-03-19T20:27:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,403 @@
+//! An authenticatable & mutable key-value store based on an MMR over the log of state-change
+//! operations.
+//!
+//! # Terminology
+//!
+//! A _key_ in the store either has a _value_ or it doesn't. Two types of _operations_ can be
+//! applied to the store to modify the state of a specific key. A key that has a value can change to
+//! one without a value through the _delete_ operation. The _update_ operation gives a key a
+//! specific value whether it previously had no value or had a different value.
+
+use crate::mmr::{
+    iterator::{leaf_num_to_pos, leaf_pos_to_num},
+    mem::Mmr,
+    verification::Proof,
+    Error,
+};
+use commonware_cryptography::Hasher as CHasher;
+use commonware_utils::Array;
+use std::collections::HashMap;
+use tracing::error;
+
+/// The types of operations that change a key's state in the store.
+#[derive(Clone)]
+pub enum Type<V: Array> {
+    /// Indicates the key no longer has a value.
+    Deleted,
+
+    /// Indicates the key now has the wrapped value.
+    Update(V),
+}
+
+/// An operation applied to the store.
+#[derive(Clone)]
+pub struct Operation<K: Array, V: Array> {
+    /// The key whose state is changed by this operation.
+    pub key: K,
+
+    /// The new state of the key.
+    pub op_type: Type<V>,
+}
+
+/// A mutable key-value store based on an MMR over its log of operations.
+pub struct MutableMmr<K: Array, V: Array, H: CHasher> {
+    /// An MMR over digests of the operations applied to the store. The number of leaves in this MMR
+    /// always equals the number of operations in the `log`.
+    ops: Mmr<H>,
+
+    /// A log of all operations applied to the store in order of occurrence. The index of each
+    /// operation in this vector is called its _location_.
+    ///
+    /// Invariant: An operation's location is always equal to the number of the MMR leaf storing the
+    /// digest of the operation.
+    log: Vec<Operation<K, V>>,
+
+    /// A map from each key to the location in the log containing its most recent update. Only
+    /// contains the keys that currently have a value (that is, deleted keys are not in the map).
+    snapshot: HashMap<K, usize>,
+}
+
+impl<K: Array, V: Array, H: CHasher> Default for MutableMmr<K, V, H> {
+    /// Return a new, empty store.
+    fn default() -> Self {
+        MutableMmr::new()
+    }
+}
+
+impl<K: Array, V: Array, H: CHasher> MutableMmr<K, V, H> {
+    /// Return a new, empty store.
+    pub fn new() -> Self {
+        MutableMmr {
+            ops: Mmr::new(),
+            log: Vec::<Operation<K, V>>::new(),
+            snapshot: HashMap::new(),
+        }
+    }
+
+    /// Return a store initialized to the state corresponding to the operation sequence given by
+    /// `log`.
+    ///
+    /// Performs some checks that the log constitutes a valid sequence of operations, and returns
+    /// [Error::InvalidUpdate] if an invalid sequence is encountered.
+    pub fn init_from_log(hasher: &mut H, log: Vec<Operation<K, V>>) -> Result<Self, Error> {
+        let mut store = MutableMmr {
+            ops: Mmr::new(),
+            log,
+            snapshot: HashMap::new(),
+        };
+
+        for (i, op) in store.log.iter().enumerate() {
+            let digest = Self::op_digest(hasher, op);
+            store.ops.add(hasher, &digest);
+
+            match op.op_type {
+                Type::Deleted => {
+                    let location = store.snapshot.remove(&op.key);
+                    if location.is_none() {
+                        // Shouldn't be allowed to delete a key that already has no value.
path: storage/src/mmr/mutable.rs
user: login: roberto-bayardo
body: yup
created_at: 2025-03-19T20:33:04Z
author_association: COLLABORATOR

PR comments:
user: login: codecov[bot]
created_at: 2025-03-19T22:02:48Z
body: ## [Codecov](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/630?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz) Report
Attention: Patch coverage is `96.62447%` with `8 lines` in your changes missing coverage. Please review.
> Project coverage is 89.44%. Comparing base [(`cd71f8a`)](https://app.codecov.io/gh/commonwarexyz/monorepo/commit/cd71f8ad86e59694f944ecfb76c51074dcb8141f?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz) to head [(`1613d94`)](https://app.codecov.io/gh/commonwarexyz/monorepo/commit/1613d941460f7721304229dc5eadc1bf2dc26247?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz).
> Report is 6 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/630?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz) | Patch % | Lines |
|---|---|---|
| [storage/src/mmr/mutable.rs](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/630?src=pr&el=tree&filepath=storage%2Fsrc%2Fmmr%2Fmutable.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz#diff-c3RvcmFnZS9zcmMvbW1yL211dGFibGUucnM=) | 96.62% | [8 Missing :warning: ](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/630?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz) |

```diff
@@            Coverage Diff             @@
##             main     #630      +/-   ##
==========================================
+ Coverage   89.32%   89.44%   +0.12%     
==========================================
  Files         139      145       +6     
  Lines       36299    36991     +692     
==========================================
+ Hits        32423    33088     +665     
- Misses       3876     3903      +27     
```

| [Files with missing lines](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/630?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz) | Coverage Δ | |
|---|---|---|
| [storage/src/mmr/mutable.rs](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/630?src=pr&el=tree&filepath=storage%2Fsrc%2Fmmr%2Fmutable.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz#diff-c3RvcmFnZS9zcmMvbW1yL211dGFibGUucnM=) | `96.62% <96.62%> (ø)` | |

... and [31 files with indirect coverage changes](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/630/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz)

------

[Continue to review full report in Codecov by Sentry](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/630?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/630?dropdown=coverage&src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz). Last update [cd71f8a...1613d94](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/630?dropdown=coverage&src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz).

<details><summary>🚀 New features to boost your workflow: </summary>

- ❄ [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE