commonware_storage/qmdb/immutable/
operation.rs

1//! Operations for immutable authenticated databases.
2//!
3//! This module provides the [Operation] type for databases that only support
4//! adding new keyed values (no updates or deletions).
5
6use crate::{
7    mmr::Location,
8    qmdb::{any::VariableValue, operation::Operation as OperationTrait},
9};
10use bytes::{Buf, BufMut};
11use commonware_codec::{EncodeSize, Error as CodecError, Read, ReadExt as _, Write};
12use commonware_utils::{hex, Array};
13use core::fmt::Display;
14
15// Context byte prefixes for identifying the operation type.
16const SET_CONTEXT: u8 = 0;
17const COMMIT_CONTEXT: u8 = 1;
18
19/// An operation applied to an immutable authenticated database.
20///
21/// Unlike mutable database operations, immutable operations only support
22/// setting new values and committing - no updates or deletions.
23#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
24pub enum Operation<K: Array, V: VariableValue> {
25    /// Set a key to a value. The key must not already exist.
26    Set(K, V),
27
28    /// Commit with optional metadata.
29    Commit(Option<V>),
30}
31
32impl<K: Array, V: VariableValue> Operation<K, V> {
33    /// If this is an operation involving a key, returns the key. Otherwise, returns None.
34    pub const fn key(&self) -> Option<&K> {
35        match self {
36            Self::Set(key, _) => Some(key),
37            Self::Commit(_) => None,
38        }
39    }
40
41    /// Returns true if this is a commit operation.
42    pub const fn is_commit(&self) -> bool {
43        matches!(self, Self::Commit(_))
44    }
45}
46
47impl<K: Array, V: VariableValue> EncodeSize for Operation<K, V> {
48    fn encode_size(&self) -> usize {
49        1 + match self {
50            Self::Set(_, v) => K::SIZE + v.encode_size(),
51            Self::Commit(v) => v.encode_size(),
52        }
53    }
54}
55
56impl<K: Array, V: VariableValue> OperationTrait for Operation<K, V> {
57    type Key = K;
58
59    fn key(&self) -> Option<&Self::Key> {
60        self.key()
61    }
62
63    fn is_delete(&self) -> bool {
64        // Immutable databases don't support deletion
65        false
66    }
67
68    fn is_update(&self) -> bool {
69        matches!(self, Self::Set(_, _))
70    }
71
72    fn has_floor(&self) -> Option<Location> {
73        // Immutable databases don't have inactivity floors
74        None
75    }
76}
77
78impl<K: Array, V: VariableValue> Write for Operation<K, V> {
79    fn write(&self, buf: &mut impl BufMut) {
80        match &self {
81            Self::Set(k, v) => {
82                SET_CONTEXT.write(buf);
83                k.write(buf);
84                v.write(buf);
85            }
86            Self::Commit(v) => {
87                COMMIT_CONTEXT.write(buf);
88                v.write(buf);
89            }
90        }
91    }
92}
93
94impl<K: Array, V: VariableValue> Read for Operation<K, V> {
95    type Cfg = <V as Read>::Cfg;
96
97    fn read_cfg(buf: &mut impl Buf, cfg: &Self::Cfg) -> Result<Self, CodecError> {
98        match u8::read(buf)? {
99            SET_CONTEXT => {
100                let key = K::read(buf)?;
101                let value = V::read_cfg(buf, cfg)?;
102                Ok(Self::Set(key, value))
103            }
104            COMMIT_CONTEXT => Ok(Self::Commit(Option::<V>::read_cfg(buf, cfg)?)),
105            e => Err(CodecError::InvalidEnum(e)),
106        }
107    }
108}
109
110impl<K: Array, V: VariableValue> Display for Operation<K, V> {
111    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112        match self {
113            Self::Set(key, value) => write!(f, "[key:{key} value:{}]", hex(&value.encode())),
114            Self::Commit(value) => {
115                if let Some(value) = value {
116                    write!(f, "[commit {}]", hex(&value.encode()))
117                } else {
118                    write!(f, "[commit]")
119                }
120            }
121        }
122    }
123}
124
125#[cfg(feature = "arbitrary")]
126impl<K: Array, V: VariableValue> arbitrary::Arbitrary<'_> for Operation<K, V>
127where
128    K: for<'a> arbitrary::Arbitrary<'a>,
129    V: for<'a> arbitrary::Arbitrary<'a>,
130{
131    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
132        let choice = u.int_in_range(0..=1)?;
133        match choice {
134            0 => {
135                let key = K::arbitrary(u)?;
136                let value = V::arbitrary(u)?;
137                Ok(Self::Set(key, value))
138            }
139            1 => Ok(Self::Commit(Option::<V>::arbitrary(u)?)),
140            _ => unreachable!(),
141        }
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148    use commonware_codec::{DecodeExt, Encode, EncodeSize, FixedSize as _};
149    use commonware_utils::sequence::U64;
150
151    #[test]
152    fn test_operation_key() {
153        let key = U64::new(1234);
154        let value = U64::new(56789);
155
156        let set_op = Operation::Set(key.clone(), value.clone());
157        assert_eq!(&key, set_op.key().unwrap());
158
159        let commit_op = Operation::<U64, U64>::Commit(Some(value));
160        assert_eq!(None, commit_op.key());
161
162        let commit_op_none = Operation::<U64, U64>::Commit(None);
163        assert_eq!(None, commit_op_none.key());
164    }
165
166    #[test]
167    fn test_operation_is_commit() {
168        let key = U64::new(1234);
169        let value = U64::new(56789);
170
171        let set_op = Operation::Set(key, value.clone());
172        assert!(!set_op.is_commit());
173
174        let commit_op = Operation::<U64, U64>::Commit(Some(value));
175        assert!(commit_op.is_commit());
176
177        let commit_op_none = Operation::<U64, U64>::Commit(None);
178        assert!(commit_op_none.is_commit());
179    }
180
181    #[test]
182    fn test_operation_encode_decode() {
183        let key = U64::new(1234);
184        let value = U64::new(56789);
185
186        // Test Set operation
187        let set_op = Operation::Set(key, value.clone());
188        let encoded = set_op.encode();
189        let decoded = Operation::<U64, U64>::decode(encoded).unwrap();
190        assert_eq!(set_op, decoded);
191
192        // Test Commit operation with value
193        let commit_op = Operation::<U64, U64>::Commit(Some(value));
194        let encoded = commit_op.encode();
195        let decoded = Operation::<U64, U64>::decode(encoded).unwrap();
196        assert_eq!(commit_op, decoded);
197
198        // Test Commit operation without value
199        let commit_op = Operation::<U64, U64>::Commit(None);
200        let encoded = commit_op.encode();
201        let decoded = Operation::<U64, U64>::decode(encoded).unwrap();
202        assert_eq!(commit_op, decoded);
203    }
204
205    #[test]
206    fn test_operation_encode_size() {
207        let key = U64::new(1234);
208        let value = U64::new(56789);
209
210        // Test Set operation
211        let set_op = Operation::Set(key, value.clone());
212        assert_eq!(set_op.encode_size(), 1 + U64::SIZE + value.encode_size());
213        assert_eq!(set_op.encode().len(), set_op.encode_size());
214
215        // Test Commit operation with value
216        let commit_op = Operation::<U64, U64>::Commit(Some(value.clone()));
217        assert_eq!(commit_op.encode_size(), 1 + Some(value).encode_size());
218        assert_eq!(commit_op.encode().len(), commit_op.encode_size());
219
220        // Test Commit operation without value
221        let commit_op = Operation::<U64, U64>::Commit(None);
222        assert_eq!(
223            commit_op.encode_size(),
224            1 + Option::<U64>::None.encode_size()
225        );
226        assert_eq!(commit_op.encode().len(), commit_op.encode_size());
227    }
228
229    #[test]
230    fn test_operation_display() {
231        let key = U64::new(1234);
232        let value = U64::new(56789);
233
234        // Test Set operation
235        let set_op = Operation::Set(key.clone(), value.clone());
236        assert_eq!(
237            format!("{set_op}"),
238            format!("[key:{key} value:{}]", hex(&value.encode()))
239        );
240
241        // Test Commit operation with value
242        let commit_op = Operation::<U64, U64>::Commit(Some(value.clone()));
243        assert_eq!(
244            format!("{commit_op}"),
245            format!("[commit {}]", hex(&value.encode()))
246        );
247
248        // Test Commit operation without value
249        let commit_op = Operation::<U64, U64>::Commit(None);
250        assert_eq!(format!("{commit_op}"), "[commit]");
251    }
252
253    #[test]
254    fn test_operation_invalid_context() {
255        let invalid = vec![0xFF, 0, 0, 0, 0, 0, 0, 0, 0, 0];
256        let decoded = Operation::<U64, U64>::decode(invalid.as_ref());
257        assert!(matches!(
258            decoded.unwrap_err(),
259            CodecError::InvalidEnum(0xFF)
260        ));
261    }
262
263    #[test]
264    fn test_operation_insufficient_buffer() {
265        // Test insufficient buffer for Set operation
266        let invalid = vec![SET_CONTEXT];
267        let decoded = Operation::<U64, U64>::decode(invalid.as_ref());
268        assert!(matches!(decoded.unwrap_err(), CodecError::EndOfBuffer));
269
270        // Test insufficient buffer for Commit operation
271        let invalid = vec![COMMIT_CONTEXT];
272        let decoded = Operation::<U64, U64>::decode(invalid.as_ref());
273        assert!(matches!(decoded.unwrap_err(), CodecError::EndOfBuffer));
274    }
275
276    #[test]
277    fn test_operation_roundtrip_all_variants() {
278        let key = U64::new(100);
279        let value = U64::new(1000);
280
281        // Test all operation variants
282        let operations: Vec<Operation<U64, U64>> = vec![
283            Operation::Set(key, value.clone()),
284            Operation::Commit(Some(value)),
285            Operation::Commit(None),
286        ];
287
288        for op in operations {
289            let encoded = op.encode();
290            let decoded = Operation::<U64, U64>::decode(encoded.clone()).unwrap();
291            assert_eq!(op, decoded, "Failed to roundtrip: {op:?}");
292            assert_eq!(encoded.len(), op.encode_size(), "Size mismatch for: {op:?}");
293        }
294    }
295
296    #[cfg(feature = "arbitrary")]
297    mod conformance {
298        use super::*;
299        use commonware_codec::conformance::CodecConformance;
300
301        commonware_conformance::conformance_tests! {
302            CodecConformance<Operation<U64, U64>>
303        }
304    }
305}