Skip to main content

commonware_storage/qmdb/keyless/operation/
variable.rs

1use crate::{
2    merkle::{Family, Location},
3    qmdb::{
4        any::{value::VariableEncoding, VariableValue},
5        keyless::operation::{Codec, Operation, APPEND_CONTEXT, COMMIT_CONTEXT},
6    },
7};
8use commonware_codec::{EncodeSize, Error as CodecError, Read, ReadExt as _, Write};
9use commonware_runtime::{Buf, BufMut};
10
11impl<V: VariableValue> Codec for VariableEncoding<V> {
12    type ReadCfg = <V as Read>::Cfg;
13
14    fn write_operation<F: Family>(op: &Operation<F, Self>, buf: &mut impl BufMut) {
15        match op {
16            Operation::Append(value) => {
17                APPEND_CONTEXT.write(buf);
18                value.write(buf);
19            }
20            Operation::Commit(metadata, floor) => {
21                COMMIT_CONTEXT.write(buf);
22                metadata.write(buf);
23                floor.write(buf);
24            }
25        }
26    }
27
28    fn read_operation<F: Family>(
29        buf: &mut impl Buf,
30        cfg: &Self::ReadCfg,
31    ) -> Result<Operation<F, Self>, CodecError> {
32        match u8::read(buf)? {
33            APPEND_CONTEXT => Ok(Operation::Append(V::read_cfg(buf, cfg)?)),
34            COMMIT_CONTEXT => {
35                let metadata = Option::<V>::read_cfg(buf, cfg)?;
36                let floor = Location::<F>::read(buf)?;
37                Ok(Operation::Commit(metadata, floor))
38            }
39            e => Err(CodecError::InvalidEnum(e)),
40        }
41    }
42}
43
44impl<F: Family, V: VariableValue> EncodeSize for Operation<F, VariableEncoding<V>> {
45    fn encode_size(&self) -> usize {
46        1 + match self {
47            Self::Append(v) => v.encode_size(),
48            Self::Commit(v, floor) => v.encode_size() + floor.encode_size(),
49        }
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56    use crate::merkle::mmr;
57    use commonware_codec::{DecodeExt, Encode, EncodeSize};
58    use commonware_utils::sequence::U64;
59
60    // Use U64 as the value type: it implements VariableValue and has Cfg = ().
61    type Op = Operation<mmr::Family, VariableEncoding<U64>>;
62
63    #[test]
64    fn append_roundtrip() {
65        let op = Op::Append(U64::new(12345));
66        let decoded = Op::decode(op.encode()).unwrap();
67        assert_eq!(op, decoded);
68    }
69
70    #[test]
71    fn commit_some_roundtrip() {
72        let op = Op::Commit(Some(U64::new(999)), Location::new(77));
73        let decoded = Op::decode(op.encode()).unwrap();
74        assert_eq!(op, decoded);
75    }
76
77    #[test]
78    fn commit_none_roundtrip() {
79        let op = Op::Commit(None, Location::new(42));
80        let decoded = Op::decode(op.encode()).unwrap();
81        assert_eq!(op, decoded);
82    }
83
84    #[test]
85    fn encode_size_matches_encoded_len() {
86        let cases: Vec<Op> = vec![
87            Op::Append(U64::new(0)),
88            Op::Append(U64::new(u64::MAX)),
89            Op::Commit(None, Location::new(0)),
90            Op::Commit(Some(U64::new(42)), Location::new(1234)),
91        ];
92        for op in cases {
93            assert_eq!(op.encode_size(), op.encode().len(), "mismatch for {op:?}");
94        }
95    }
96
97    #[test]
98    fn invalid_context_byte_rejected() {
99        let op = Op::Append(U64::new(1));
100        let mut buf: Vec<u8> = op.encode().to_vec();
101        buf[0] = 0xFF;
102        assert!(matches!(
103            Op::decode(buf.as_ref()).unwrap_err(),
104            CodecError::InvalidEnum(0xFF)
105        ));
106    }
107
108    #[test]
109    fn empty_input_rejected() {
110        assert!(Op::decode(&[] as &[u8]).is_err());
111    }
112
113    #[test]
114    fn append_and_commit_have_different_encodings() {
115        let append = Op::Append(U64::new(1));
116        let commit = Op::Commit(Some(U64::new(1)), Location::new(0));
117        assert_ne!(append.encode().as_ref(), commit.encode().as_ref());
118    }
119
120    #[test]
121    fn context_byte_is_first() {
122        let append = Op::Append(U64::new(0));
123        let commit = Op::Commit(None, Location::new(0));
124        assert_eq!(append.encode()[0], APPEND_CONTEXT);
125        assert_eq!(commit.encode()[0], COMMIT_CONTEXT);
126    }
127
128    #[test]
129    fn commit_floor_overflow_rejected() {
130        // Hand-build a Commit with a varint floor of u64::MAX, which exceeds MAX_LEAVES.
131        use commonware_codec::{varint::UInt, Write};
132        let mut buf = Vec::new();
133        COMMIT_CONTEXT.write(&mut buf);
134        Option::<U64>::None.write(&mut buf);
135        UInt(u64::MAX).write(&mut buf);
136        assert!(matches!(
137            Op::decode(buf.as_ref()).unwrap_err(),
138            CodecError::Invalid(_, _)
139        ));
140    }
141}