Skip to main content

commonware_storage/qmdb/keyless/operation/
fixed.rs

1use crate::{
2    merkle::{Family, Location},
3    qmdb::{
4        any::{value::FixedEncoding, FixedValue},
5        keyless::operation::{Codec, Operation, APPEND_CONTEXT, COMMIT_CONTEXT},
6    },
7};
8use commonware_codec::{
9    util::{at_least, ensure_zeros},
10    Error as CodecError, FixedSize, ReadExt as _, Write,
11};
12use commonware_runtime::{Buf, BufMut};
13
14/// Fixed padded operation size: `Commit` is always the larger variant.
15///
16/// - Append: 1 (context) + V::SIZE + padding
17/// - Commit: 1 (context) + 1 (option tag) + V::SIZE + u64::SIZE (floor)
18///
19/// Total = 2 + V::SIZE + u64::SIZE. Append pads to match.
20const fn op_size<V: FixedSize>() -> usize {
21    2 + V::SIZE + u64::SIZE
22}
23
24impl<V: FixedValue> Codec for FixedEncoding<V> {
25    type ReadCfg = ();
26
27    fn write_operation<F: Family>(op: &Operation<F, Self>, buf: &mut impl BufMut) {
28        let total = op_size::<V>();
29        match op {
30            Operation::Append(value) => {
31                APPEND_CONTEXT.write(buf);
32                value.write(buf);
33                // Pad to uniform size: 1 byte (option-tag gap) + u64::SIZE (floor gap).
34                buf.put_bytes(0, total - 1 - V::SIZE);
35            }
36            Operation::Commit(metadata, floor) => {
37                COMMIT_CONTEXT.write(buf);
38                if let Some(metadata) = metadata {
39                    true.write(buf);
40                    metadata.write(buf);
41                } else {
42                    buf.put_bytes(0, 1 + V::SIZE);
43                }
44                buf.put_slice(&floor.as_u64().to_be_bytes());
45            }
46        }
47    }
48
49    fn read_operation<F: Family>(
50        buf: &mut impl Buf,
51        _cfg: &Self::ReadCfg,
52    ) -> Result<Operation<F, Self>, CodecError> {
53        let total = op_size::<V>();
54        at_least(buf, total)?;
55
56        match u8::read(buf)? {
57            APPEND_CONTEXT => {
58                let value = V::read(buf)?;
59                ensure_zeros(buf, total - 1 - V::SIZE)?;
60                Ok(Operation::Append(value))
61            }
62            COMMIT_CONTEXT => {
63                let is_some = bool::read(buf)?;
64                let metadata = if is_some {
65                    Some(V::read(buf)?)
66                } else {
67                    ensure_zeros(buf, V::SIZE)?;
68                    None
69                };
70                let floor = Location::<F>::new(u64::read(buf)?);
71                if !floor.is_valid() {
72                    return Err(CodecError::Invalid(
73                        "storage::qmdb::keyless::operation::fixed::Operation",
74                        "commit floor location overflow",
75                    ));
76                }
77                Ok(Operation::Commit(metadata, floor))
78            }
79            e => Err(CodecError::InvalidEnum(e)),
80        }
81    }
82}
83
84impl<F: Family, V: FixedValue> FixedSize for Operation<F, FixedEncoding<V>> {
85    const SIZE: usize = op_size::<V>();
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use crate::merkle::mmr;
92    use commonware_codec::{DecodeExt, Encode, FixedSize};
93    use commonware_utils::sequence::U64;
94
95    type Op = Operation<mmr::Family, FixedEncoding<U64>>;
96
97    #[test]
98    fn all_variants_have_same_encoded_size() {
99        let append = Op::Append(U64::new(42));
100        let commit_some = Op::Commit(Some(U64::new(99)), Location::new(5));
101        let commit_none = Op::Commit(None, Location::new(0));
102
103        let a = append.encode();
104        let b = commit_some.encode();
105        let c = commit_none.encode();
106
107        assert_eq!(a.len(), Op::SIZE);
108        assert_eq!(b.len(), Op::SIZE);
109        assert_eq!(c.len(), Op::SIZE);
110        assert_eq!(Op::SIZE, 2 + U64::SIZE + u64::SIZE);
111    }
112
113    #[test]
114    fn append_roundtrip() {
115        let op = Op::Append(U64::new(12345));
116        let decoded = Op::decode(op.encode()).unwrap();
117        assert_eq!(op, decoded);
118    }
119
120    #[test]
121    fn commit_some_roundtrip() {
122        let op = Op::Commit(Some(U64::new(999)), Location::new(77));
123        let decoded = Op::decode(op.encode()).unwrap();
124        assert_eq!(op, decoded);
125    }
126
127    #[test]
128    fn commit_none_roundtrip() {
129        let op = Op::Commit(None, Location::new(42));
130        let decoded = Op::decode(op.encode()).unwrap();
131        assert_eq!(op, decoded);
132    }
133
134    #[test]
135    fn invalid_context_byte_rejected() {
136        let mut buf = vec![0u8; Op::SIZE];
137        buf[0] = 0xFF;
138        assert!(matches!(
139            Op::decode(buf.as_ref()).unwrap_err(),
140            CodecError::InvalidEnum(0xFF)
141        ));
142    }
143
144    #[test]
145    fn non_zero_padding_rejected() {
146        // Encode an Append, then corrupt the padding byte.
147        let op = Op::Append(U64::new(1));
148        let mut buf: Vec<u8> = op.encode().to_vec();
149        // Padding is the last byte (part of the floor gap).
150        *buf.last_mut().unwrap() = 0x01;
151        assert!(Op::decode(buf.as_ref()).is_err());
152    }
153
154    #[test]
155    fn truncated_input_rejected() {
156        let op = Op::Append(U64::new(1));
157        let buf = op.encode();
158        // One byte short.
159        assert!(Op::decode(&buf[..buf.len() - 1]).is_err());
160    }
161
162    #[test]
163    fn commit_none_has_zero_value_bytes() {
164        let op = Op::Commit(None, Location::new(0));
165        let buf: Vec<u8> = op.encode().to_vec();
166        // After context byte (0) and option-tag byte (0), all remaining bytes (including the
167        // all-zero floor) should be zero.
168        assert!(buf[2..].iter().all(|&b| b == 0));
169    }
170
171    #[test]
172    fn commit_floor_overflow_rejected() {
173        // Construct a Commit buffer by hand with a floor beyond MAX_LEAVES.
174        let mut buf = vec![0u8; Op::SIZE];
175        buf[0] = COMMIT_CONTEXT;
176        // Option tag = false (None metadata); value bytes already zero.
177        // Last 8 bytes are the floor; write u64::MAX big-endian.
178        let floor_bytes = u64::MAX.to_be_bytes();
179        let floor_offset = Op::SIZE - u64::SIZE;
180        buf[floor_offset..].copy_from_slice(&floor_bytes);
181        assert!(matches!(
182            Op::decode(buf.as_ref()).unwrap_err(),
183            CodecError::Invalid(_, _)
184        ));
185    }
186}