Repository: commonwarexyz/monorepo
Pull request #661: [commonware-storage/mmr/operaetion] add operation that represents raising of the inactive operations floor
Author: login: roberto-bayardo
Created: 2025-03-27T22:32:49Z | Merged: 2025-03-28T14:21:32Z

This makes the mutable MMR inactivity floor easily persistable & provable.


Diff:
diff --git a/storage/src/mmr/mutable.rs b/storage/src/mmr/mutable.rs
index 90b546482f..2ca989cffb 100644
--- a/storage/src/mmr/mutable.rs
+++ b/storage/src/mmr/mutable.rs
@@ -85,7 +85,7 @@ impl<K: Array, V: Array, H: CHasher> MutableMmr<K, V, H> {
             ops: Mmr::<H>::init(vec![], oldest_retained_pos, store_state.pinned_nodes),
             log: store_state.log.into(),
             snapshot: HashMap::new(),
-            inactivity_floor_loc: store_state.pruned_loc,
+            inactivity_floor_loc: 0,
             pruned_loc: store_state.pruned_loc,
         };
 
@@ -94,10 +94,15 @@ impl<K: Array, V: Array, H: CHasher> MutableMmr<K, V, H> {
             store.ops.add(hasher, &digest);
 
             match op.to_type() {
-                Type::Deleted(key) => store.snapshot.remove(&key),
+                Type::Deleted(key) => {
+                    store.snapshot.remove(&key);
+                }
                 Type::Update(key, _) => {
                     let loc = store_state.pruned_loc + i as u64;
-                    store.snapshot.insert(key, loc)
+                    store.snapshot.insert(key, loc);
+                }
+                Type::Floor(loc) => {
+                    store.inactivity_floor_loc = loc;
                 }
             };
         }
@@ -124,7 +129,7 @@ impl<K: Array, V: Array, H: CHasher> MutableMmr<K, V, H> {
         let v = self.log[i].to_value();
         assert!(
             v.is_some(),
-            "deleted key should not be in snapshot: {:?}",
+            "snapshot should only reference non-empty values {:?}",
             key
         );
 
@@ -147,7 +152,7 @@ impl<K: Array, V: Array, H: CHasher> MutableMmr<K, V, H> {
             let last_value = self.log[i as usize].to_value();
             assert!(
                 last_value.is_some(),
-                "deleted key should not be in snapshot: {:?}",
+                "snapshot should only reference non-empty values {:?}",
                 key
             );
             if value == last_value.unwrap() {
@@ -256,8 +261,8 @@ impl<K: Array, V: Array, H: CHasher> MutableMmr<K, V, H> {
     /// Raise the inactivity floor by exactly `max_steps` steps. Each step either advances over an
     /// inactive operation, or re-applies an active operation to the tip and then advances over it.
     ///
-    /// This method does not change the state of the store's snapshot, but it may change the root
-    /// hash of the store if some step re-applies an active operation.
+    /// This method does not change the state of the store's snapshot, but it always changes the
+    /// root since it applies at least one (floor) operation.
     #[allow(dead_code)] // TODO: Remove when method gets used.
     pub fn raise_inactivity_floor(&mut self, hasher: &mut H, max_steps: u64) {
         for _ in 0..max_steps {
@@ -278,6 +283,7 @@ impl<K: Array, V: Array, H: CHasher> MutableMmr<K, V, H> {
             }
             self.inactivity_floor_loc += 1;
         }
+        self.apply_op(hasher, Operation::floor(self.inactivity_floor_loc));
     }
 
     /// Prune any historical operations that are known to be inactive (those preceding the
@@ -347,6 +353,7 @@ mod test {
         // We should be able to advance over the 3 inactive operations.
         store.raise_inactivity_floor(&mut hasher, 3);
         assert_eq!(store.inactivity_floor_loc, 3);
+        assert_eq!(store.log.len(), 6); // 4 updates, 1 deletion, 1 floor
 
         let root = store.root(&mut hasher);
 
@@ -354,7 +361,7 @@ mod test {
         store.update(&mut hasher, d1, d1);
         store.update(&mut hasher, d2, d2);
         // Log and root should be unchanged.
-        assert_eq!(store.log.len(), 5);
+        assert_eq!(store.log.len(), 6);
         assert_eq!(store.root(&mut hasher), root);
 
         // The MMR's size should always be greater than the position of the last leaf.
@@ -365,14 +372,14 @@ mod test {
         store.delete(&mut hasher, d2);
         assert!(store.get(&d1).is_none());
         assert!(store.get(&d2).is_none());
-        assert_eq!(store.log.len(), 7); // 4 updates, 3 deletions
+        assert_eq!(store.log.len(), 8); // 4 updates, 3 deletions, 1 floor
         assert_eq!(store.inactivity_floor_loc, 3);
 
         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.log.len(), 7);
+        assert_eq!(store.log.len(), 8);
         assert_eq!(store.root(&mut hasher), root);
 
         // deletions of non-existent keys should be a no-op.
@@ -385,13 +392,14 @@ mod test {
         let store_state: StoreState<Digest, Digest, Sha256> = store.to_state();
         assert_eq!(store_state.pruned_loc, 3);
         let mut store = MutableMmr::init_from_state(&mut hasher, store_state).unwrap();
-        assert_eq!(store.log.len(), 4);
+        assert_eq!(store.log.len(), 5);
         assert_eq!(store.root(&mut hasher), root);
 
         store.raise_inactivity_floor(&mut hasher, 10);
+        let root = store.root(&mut hasher);
         let store_state: StoreState<Digest, Digest, Sha256> = store.to_state();
         let mut store = MutableMmr::init_from_state(&mut hasher, store_state).unwrap();
-        assert_eq!(store.log.len(), 0);
+        assert_eq!(store.log.len(), 1); // 1 inactivity floor op
         assert_eq!(store.root(&mut hasher), root);
 
         // Make sure converting to/from store_state works with some active elements.
@@ -400,19 +408,20 @@ mod test {
         store.delete(&mut hasher, d1);
         store.update(&mut hasher, d2, d1);
         store.update(&mut hasher, d1, d2);
-        assert_eq!(store.log.len(), 5);
+        assert_eq!(store.log.len(), 6);
         assert_eq!(store.snapshot.len(), 2);
         let root = store.root(&mut hasher);
 
         let store_state: StoreState<Digest, Digest, Sha256> = store.to_state();
-        assert_eq!(store_state.pruned_loc, 7);
+        assert_eq!(store_state.pruned_loc, 8);
         let mut store = MutableMmr::init_from_state(&mut hasher, store_state).unwrap();
-        assert_eq!(store.pruned_loc, 7);
+        assert_eq!(store.pruned_loc, 8);
         assert_eq!(store.root(&mut hasher), root);
 
         // Pruning inactive ops should not affect root or current state.
         let old_pruned_loc = store.pruned_loc;
         store.raise_inactivity_floor(&mut hasher, 2);
+        let root = store.root(&mut hasher);
         store.prune_known_inactive();
         assert!(old_pruned_loc < store.pruned_loc);
         assert_eq!(store.root(&mut hasher), root);
@@ -473,17 +482,19 @@ mod test {
 
             // Confirm the recreated store has an operations log that was pruned of all operations
             // preceding the last known inactivity floor.
-            assert_eq!(store.op_count(), 1533);
+            assert_eq!(store.op_count(), 1534);
             assert_eq!(store.pruned_loc, 100);
             assert_eq!(store.inactivity_floor_loc, 100);
-            assert_eq!(store.log.len(), 1533 - 100);
+            assert_eq!(store.log.len(), 1534 - 100);
             assert_eq!(store.snapshot.len(), 857);
 
             // Raise the inactivity floor to the point where all inactive operations can be pruned.
             store.raise_inactivity_floor(&mut hasher, 3000);
             assert_eq!(store.inactivity_floor_loc, 3100);
-            // Inactivity floor should be 857 operations from tip since 857 operations are active.
-            assert_eq!(store.op_count(), 3100 + 857);
+            // Inactivity floor should be 858 operations from tip since 858 operations are active
+            // (counting the floor op itself).
+            assert_eq!(store.op_count(), 3100 + 858);
+            assert_eq!(store.snapshot.len(), 857);
 
             // Confirm the store's state matches that of the separate map we computed independently.
             store.prune_known_inactive();
diff --git a/storage/src/mmr/operation.rs b/storage/src/mmr/operation.rs
index c36645008f..f9ae432fab 100644
--- a/storage/src/mmr/operation.rs
+++ b/storage/src/mmr/operation.rs
@@ -22,8 +22,10 @@ pub enum Error<K: Array, V: Array> {
     InvalidValue(<V as Array>::Error),
     #[error("invalid context byte")]
     InvalidContextByte,
-    #[error("delete operation as non-zero value")]
+    #[error("delete operation has non-zero value")]
     InvalidDeleteOp,
+    #[error("floor operation has non-zero bytes after location")]
+    InvalidFloorOp,
 }
 
 /// The types of operations that can change the state of the store.
@@ -34,6 +36,9 @@ pub enum Type<K: Array, V: Array> {
 
     /// Indicates the key now has the wrapped value.
     Update(K, V),
+
+    /// Indicates the floor on inactive operations has been raised.
+    Floor(u64),
 }
 
 /// An `Array` implementation for operations applied to a `MutableMmr` K/V store.
@@ -47,16 +52,18 @@ pub struct Operation<K: Array, V: Array> {
 impl<K: Array, V: Array> Operation<K, V> {
     const DELETE_CONTEXT: u8 = 0;
     const UPDATE_CONTEXT: u8 = 1;
+    const FLOOR_CONTEXT: u8 = 2;
 
     /// Create a new operation of the given type.
     pub fn new(t: Type<K, V>) -> Self {
         match t {
             Type::Deleted(key) => Self::delete(key),
             Type::Update(key, value) => Self::update(key, value),
+            Type::Floor(loc) => Self::floor(loc),
         }
     }
 
-    /// Create a new update operation from `key` and `value`.
+    /// Create a new update operation that makes `key` have value `value`.
     pub fn update(key: K, value: V) -> Self {
         let mut data = Vec::with_capacity(Self::LEN_ENCODED);
         data.push(Self::UPDATE_CONTEXT);
@@ -69,6 +76,7 @@ impl<K: Array, V: Array> Operation<K, V> {
         }
     }
 
+    /// Create a new delete operation that removes any value assigned to `key`.
     pub fn delete(key: K) -> Self {
         let mut data = Vec::with_capacity(Self::LEN_ENCODED);
         data.push(Self::DELETE_CONTEXT);
@@ -81,6 +89,20 @@ impl<K: Array, V: Array> Operation<K, V> {
         }
     }
 
+    /// Create a new floor operation that raises the floor on inactive operations to `loc`. This
+    /// operation is not supported for stores whose operations encode to less than 9 bytes.
+    pub fn floor(loc: u64) -> Self {
+        let mut data = Vec::with_capacity(Self::LEN_ENCODED);
+        data.push(Self::FLOOR_CONTEXT);
+        data.extend_from_slice(&loc.to_be_bytes());
+        data.resize(Self::LEN_ENCODED, 0);
+
+        Self {
+            data,
+            _phantom: std::marker::PhantomData,
+        }
+    }
+
     pub fn to_key(&self) -> K {
         K::try_from(&self.data[1..K::LEN_ENCODED + 1]).unwrap()
     }
@@ -93,6 +115,10 @@ impl<K: Array, V: Array> Operation<K, V> {
                 let value = V::try_from(&self.data[K::LEN_ENCODED + 1..]).unwrap();
                 Type::Update(key, value)
             }
+            Self::FLOOR_CONTEXT => {
+                let loc = u64::from_be_bytes(self.data[1..9].try_into().unwrap());
+                Type::Floor(loc)
+            }
             _ => unreachable!(),
         }
     }
@@ -101,9 +127,17 @@ impl<K: Array, V: Array> Operation<K, V> {
         match self.data[0] {
             Self::DELETE_CONTEXT => None,
             Self::UPDATE_CONTEXT => Some(V::try_from(&self.data[K::LEN_ENCODED + 1..]).unwrap()),
+            Self::FLOOR_CONTEXT => None,
             _ => unreachable!(),
         }
     }
+
+    // Assert that the encoded operation is at least 9 bytes long to ensure there is room for floor
+    // operation.
+    const _ASSERT: () = assert!(
+        Self::LEN_ENCODED >= 9,
+        "operation too small for floor operation"
+    );
 }
 
 impl<K: Array, V: Array> Codec for Operation<K, V> {
@@ -150,6 +184,12 @@ impl<K: Array, V: Array> TryFrom<&[u8]> for Operation<K, V> {
                     return Err(Error::InvalidDeleteOp);
                 }
             }
+            Self::FLOOR_CONTEXT => {
+                // Check if the remaining bytes are all zeros
+                if !value[9..].iter().all(|&b| b == 0) {
+                    return Err(Error::InvalidFloorOp);
+                }
+            }
             _ => {
                 return Err(Error::InvalidContextByte);
             }
@@ -196,6 +236,7 @@ impl<K: Array, V: Array> Display for Operation<K, V> {
         match self.to_type() {
             Type::Deleted(key) => write!(f, "[key:{} <deleted>]", key),
             Type::Update(key, value) => write!(f, "[key:{} value:{}]", key, value),
+            Type::Floor(loc) => write!(f, "[floor:{}]", loc),
         }
     }
 }
@@ -235,6 +276,12 @@ mod tests {
         assert_eq!(None, from.to_value());
         assert_eq!(delete_op, from);
 
+        let floor_op = Operation::<U64, U64>::new(Type::Floor(42));
+        let from = Operation::<U64, U64>::try_from(floor_op.as_ref()).unwrap();
+        assert_eq!(None, from.to_value());
+        assert!(matches!(from.to_type(), Type::Floor(42)));
+        assert_eq!(floor_op, from);
+
         // test non-zero byte detection in delete operation
         let mut invalid = delete_op.to_vec();
         invalid[U64::LEN_ENCODED + 4] = 0xFF;


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

This PR introduces a new "Floor" operation to represent raising the inactivity floor in the mutable MMR, making the inactivity floor persistable and provable.  
- Adds a new Floor variant and associated methods in the operation module.  
- Updates error messages and test cases to support the new floor operation.  
- Modifies the MMR initialization and floor-raising logic in the mutable module.

### Reviewed Changes

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

| File                                | Description                                                         |
| ----------------------------------- | ------------------------------------------------------------------- |
| storage/src/mmr/operation.rs        | Adds Floor variant, floor constructor, serialization/deserialization, and tests |
| storage/src/mmr/mutable.rs          | Integrates Floor op in initialization, update logic, and updates test expectations |


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

**storage/src/mmr/mutable.rs:91**
* Resetting 'inactivity_floor_loc' to 0 here may override the initial value from store_state.pruned_loc, which could lead to unintended behavior. Please verify that this change is intentional and update accordingly if needed.
```
inactivity_floor_loc: 0,
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T22:41:38Z
user: login: patrick-ogrady
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T22:43:45Z
user: login: patrick-ogrady
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T22:47:09Z
user: login: roberto-bayardo
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-27T22:57:53Z
user: login: roberto-bayardo
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-27T22:59:02Z
user: login: roberto-bayardo
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-27T23:11:16Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This pull request adds support for a new floor operation that raises the inactivity floor in a mutable MMR, making it persistable and provable. The changes include a new enum variant and error for floor operations, implementation of associated constructors and decoding/encoding logic, and updated tests and state initialization in the mutable MMR.

### Reviewed Changes

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

| File                                  | Description                                                                           |
| ------------------------------------- | ------------------------------------------------------------------------------------- |
| storage/src/mmr/operation.rs           | Introduces the Floor operation type, its encoding/decoding, and associated error logic. |
| storage/src/mmr/mutable.rs             | Updates persistence and application of the inactivity floor, including changes to test assertions. |


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

**storage/src/mmr/mutable.rs:88**
* The initialization of inactivity_floor_loc has been changed from store_state.pruned_loc to 0. Please verify that this change aligns with the intended persistence of the inactivity floor state.
```
inactivity_floor_loc: 0,
```
**storage/src/mmr/mutable.rs:364**
* Ensure that the updated expected log length here accounts for the additional floor operation; confirm that all operation types are consistently considered in tests.
```
assert_eq!(store.log.len(), 6);
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T23:11:54Z
user: login: patrick-ogrady
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T23:26:53Z
user: login: patrick-ogrady
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T23:27:03Z
user: login: patrick-ogrady
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T23:27:20Z
user: login: patrick-ogrady
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T23:36:06Z
user: login: roberto-bayardo
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-28T01:42:35Z
user: login: patrick-ogrady
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T13:50:24Z
user: login: patrick-ogrady
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T13:50:30Z

Review comments:
diff_hunk: @@ -81,6 +89,22 @@ impl<K: Array, V: Array> Operation<K, V> {
         }
     }
 
+    /// Create a new floor operation that raises the floor on inactive operations to `loc`. This
+    /// operation is not supported for stores whose operations encode to less than 9 bytes.
+    pub fn floor(loc: u64) -> Self {
+        // Panic if the encoding size is too small to support this operation type.
+        assert!(Self::LEN_ENCODED >= 9);
path: storage/src/mmr/operation.rs
user: login: patrick-ogrady
body: Can this be asserted at compile-time somehow I wonder? I know golang has that `init()` function...wonder if there is something similar we can do.
created_at: 2025-03-27T22:43:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -81,6 +89,22 @@ impl<K: Array, V: Array> Operation<K, V> {
         }
     }
 
+    /// Create a new floor operation that raises the floor on inactive operations to `loc`. This
+    /// operation is not supported for stores whose operations encode to less than 9 bytes.
+    pub fn floor(loc: u64) -> Self {
+        // Panic if the encoding size is too small to support this operation type.
+        assert!(Self::LEN_ENCODED >= 9);
+        let mut data = Vec::with_capacity(Self::LEN_ENCODED);
+        data.push(Self::FLOOR_CONTEXT);
+        data.extend_from_slice(&loc.to_be_bytes());
+        data.resize(Self::LEN_ENCODED, 0);
+
+        Self {
+            data,
+            _phantom: std::marker::PhantomData,
+        }
+    }
+
     pub fn to_key(&self) -> K {
path: storage/src/mmr/operation.rs
user: login: patrick-ogrady
body: If we work directly with the enums and just call `encode` on them when needed (flipping the abstraction), I think this could become clearer (i.e. we should never be able to call to_key/to_value on a FLOOR by construction).
created_at: 2025-03-27T22:46:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -150,6 +179,12 @@ impl<K: Array, V: Array> TryFrom<&[u8]> for Operation<K, V> {
                     return Err(Error::InvalidDeleteOp);
                 }
             }
+            Self::FLOOR_CONTEXT => {
+                // Check if the remaining bytes are all zeros
+                if !value[9..].iter().all(|&b| b == 0) {
path: storage/src/mmr/operation.rs
user: login: patrick-ogrady
body: nit: I wonder if there is some bit check we can do here rather than iterating over bytes that is faster. 
created_at: 2025-03-27T22:46:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -81,6 +89,22 @@ impl<K: Array, V: Array> Operation<K, V> {
         }
     }
 
+    /// Create a new floor operation that raises the floor on inactive operations to `loc`. This
+    /// operation is not supported for stores whose operations encode to less than 9 bytes.
+    pub fn floor(loc: u64) -> Self {
+        // Panic if the encoding size is too small to support this operation type.
+        assert!(Self::LEN_ENCODED >= 9);
+        let mut data = Vec::with_capacity(Self::LEN_ENCODED);
+        data.push(Self::FLOOR_CONTEXT);
+        data.extend_from_slice(&loc.to_be_bytes());
+        data.resize(Self::LEN_ENCODED, 0);
+
+        Self {
+            data,
+            _phantom: std::marker::PhantomData,
+        }
+    }
+
     pub fn to_key(&self) -> K {
path: storage/src/mmr/operation.rs
user: login: roberto-bayardo
body: they are in array form in the journal though... so if we also keep them around in enum form aren't we basically caching each twice?
created_at: 2025-03-27T22:57:53Z
author_association: COLLABORATOR
diff_hunk: @@ -150,6 +179,12 @@ impl<K: Array, V: Array> TryFrom<&[u8]> for Operation<K, V> {
                     return Err(Error::InvalidDeleteOp);
                 }
             }
+            Self::FLOOR_CONTEXT => {
+                // Check if the remaining bytes are all zeros
+                if !value[9..].iter().all(|&b| b == 0) {
path: storage/src/mmr/operation.rs
user: login: roberto-bayardo
body: The Claude/Gemini didn't seem to have better ideas but yeah you'd think there would be something like a memcmp possible?
created_at: 2025-03-27T22:59:02Z
author_association: COLLABORATOR
diff_hunk: @@ -81,6 +89,22 @@ impl<K: Array, V: Array> Operation<K, V> {
         }
     }
 
+    /// Create a new floor operation that raises the floor on inactive operations to `loc`. This
+    /// operation is not supported for stores whose operations encode to less than 9 bytes.
+    pub fn floor(loc: u64) -> Self {
+        // Panic if the encoding size is too small to support this operation type.
+        assert!(Self::LEN_ENCODED >= 9);
path: storage/src/mmr/operation.rs
user: login: roberto-bayardo
body: Added a compile-time assertion at the end of the impl -- there might be a more concise way though?
created_at: 2025-03-27T23:11:16Z
author_association: COLLABORATOR
diff_hunk: @@ -150,6 +179,12 @@ impl<K: Array, V: Array> TryFrom<&[u8]> for Operation<K, V> {
                     return Err(Error::InvalidDeleteOp);
                 }
             }
+            Self::FLOOR_CONTEXT => {
+                // Check if the remaining bytes are all zeros
+                if !value[9..].iter().all(|&b| b == 0) {
path: storage/src/mmr/operation.rs
user: login: patrick-ogrady
body: 🤷 who knows
created_at: 2025-03-27T23:26:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -150,6 +179,12 @@ impl<K: Array, V: Array> TryFrom<&[u8]> for Operation<K, V> {
                     return Err(Error::InvalidDeleteOp);
                 }
             }
+            Self::FLOOR_CONTEXT => {
+                // Check if the remaining bytes are all zeros
+                if !value[9..].iter().all(|&b| b == 0) {
path: storage/src/mmr/operation.rs
user: login: patrick-ogrady
body: I guess I'm asking too much lol
created_at: 2025-03-27T23:27:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -81,6 +89,22 @@ impl<K: Array, V: Array> Operation<K, V> {
         }
     }
 
+    /// Create a new floor operation that raises the floor on inactive operations to `loc`. This
+    /// operation is not supported for stores whose operations encode to less than 9 bytes.
+    pub fn floor(loc: u64) -> Self {
+        // Panic if the encoding size is too small to support this operation type.
+        assert!(Self::LEN_ENCODED >= 9);
+        let mut data = Vec::with_capacity(Self::LEN_ENCODED);
+        data.push(Self::FLOOR_CONTEXT);
+        data.extend_from_slice(&loc.to_be_bytes());
+        data.resize(Self::LEN_ENCODED, 0);
+
+        Self {
+            data,
+            _phantom: std::marker::PhantomData,
+        }
+    }
+
     pub fn to_key(&self) -> K {
path: storage/src/mmr/operation.rs
user: login: patrick-ogrady
body: I think there may be a clean way to do this but let's do it after we get things working.
created_at: 2025-03-27T23:27:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -101,9 +127,23 @@ impl<K: Array, V: Array> Operation<K, V> {
         match self.data[0] {
             Self::DELETE_CONTEXT => None,
             Self::UPDATE_CONTEXT => Some(V::try_from(&self.data[K::LEN_ENCODED + 1..]).unwrap()),
+            Self::FLOOR_CONTEXT => None,
             _ => unreachable!(),
         }
     }
+
+    #[allow(dead_code)]
path: storage/src/mmr/operation.rs
user: login: patrick-ogrady
body: I found a cleaner way to do this:
```
diff --git a/storage/src/mmr/operation.rs b/storage/src/mmr/operation.rs
index 9c67979f..800c4534 100644
--- a/storage/src/mmr/operation.rs
+++ b/storage/src/mmr/operation.rs
@@ -54,6 +54,13 @@ impl<K: Array, V: Array> Operation<K, V> {
     const UPDATE_CONTEXT: u8 = 1;
     const FLOOR_CONTEXT: u8 = 2;
 
+    // Assert that the encoded operation is at least 9 bytes long to ensure
+    // there is room for floor operation.
+    const _ASSERT: () = assert!(
+        K::LEN_ENCODED + 1 + V::LEN_ENCODED >= 9,
+        "operation too small for floor operation"
+    );
+
     /// Create a new operation of the given type.
     pub fn new(t: Type<K, V>) -> Self {
         match t {
@@ -131,19 +138,6 @@ impl<K: Array, V: Array> Operation<K, V> {
             _ => unreachable!(),
         }
     }
-
-    #[allow(dead_code)]
-    fn check_size() {
-        // This function is never called but forces compile-time evaluation
-        // of the size constraint.
-        const fn assert_size<K: Array, V: Array>() {
-            let len = K::LEN_ENCODED + 1 + V::LEN_ENCODED;
-            assert!(len >= 9, "Operation encoding size too small");
-        }
-
-        // This will be checked at compile time
-        assert_size::<K, V>();
-    }
 }
 
 impl<K: Array, V: Array> Codec for Operation<K, V> {
```
created_at: 2025-03-27T23:36:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -101,9 +127,23 @@ impl<K: Array, V: Array> Operation<K, V> {
         match self.data[0] {
             Self::DELETE_CONTEXT => None,
             Self::UPDATE_CONTEXT => Some(V::try_from(&self.data[K::LEN_ENCODED + 1..]).unwrap()),
+            Self::FLOOR_CONTEXT => None,
             _ => unreachable!(),
         }
     }
+
+    #[allow(dead_code)]
path: storage/src/mmr/operation.rs
user: login: roberto-bayardo
body: nice!  Simplified it a bit more with Self::LEN_ENCODED instead of adding the components, but it seems to work?

created_at: 2025-03-28T01:42:35Z
author_association: COLLABORATOR
diff_hunk: @@ -101,9 +127,17 @@ impl<K: Array, V: Array> Operation<K, V> {
         match self.data[0] {
             Self::DELETE_CONTEXT => None,
             Self::UPDATE_CONTEXT => Some(V::try_from(&self.data[K::LEN_ENCODED + 1..]).unwrap()),
+            Self::FLOOR_CONTEXT => None,
             _ => unreachable!(),
         }
     }
+
+    // Assert that the encoded operation is at least 9 bytes long to ensure there is room for floor
+    // operation.
+    const _ASSERT: () = assert!(
path: storage/src/mmr/operation.rs
user: login: patrick-ogrady
body: nit: I prefer this at the top near the other consts
created_at: 2025-03-28T13:50:23Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov[bot]
created_at: 2025-03-28T14:21:53Z
body: ## [Codecov](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/661?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 `90.47619%` with `4 lines` in your changes missing coverage. Please review.
> Project coverage is 88.77%. Comparing base [(`7091abb`)](https://app.codecov.io/gh/commonwarexyz/monorepo/commit/7091abb8d15e6d75f4bcd6b68171cae1bebdecb8?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz) to head [(`2f74a9c`)](https://app.codecov.io/gh/commonwarexyz/monorepo/commit/2f74a9c161f5269bad0c4c9f33f9a5290297e373?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz).
> Report is 1 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/661?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/661?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=) | 86.66% | [2 Missing :warning: ](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/661?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz) |
| [storage/src/mmr/operation.rs](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/661?src=pr&el=tree&filepath=storage%2Fsrc%2Fmmr%2Foperation.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz#diff-c3RvcmFnZS9zcmMvbW1yL29wZXJhdGlvbi5ycw==) | 92.59% | [2 Missing :warning: ](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/661?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz) |

```diff
@@            Coverage Diff             @@
##             main     #661      +/-   ##
==========================================
+ Coverage   88.76%   88.77%   +0.01%     
==========================================
  Files         148      148              
  Lines       37798    37833      +35     
==========================================
+ Hits        33552    33588      +36     
+ Misses       4246     4245       -1     
```

| [Files with missing lines](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/661?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/661?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=) | `97.58% <86.66%> (+0.08%)` | :arrow_up: |
| [storage/src/mmr/operation.rs](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/661?src=pr&el=tree&filepath=storage%2Fsrc%2Fmmr%2Foperation.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz#diff-c3RvcmFnZS9zcmMvbW1yL29wZXJhdGlvbi5ycw==) | `96.73% <92.59%> (+1.19%)` | :arrow_up: |

------

[Continue to review full report in Codecov by Sentry](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/661?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/661?dropdown=coverage&src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=commonwarexyz). Last update [7091abb...2f74a9c](https://app.codecov.io/gh/commonwarexyz/monorepo/pull/661?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> :rocket: New features to boost your workflow: </summary>

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