Skip to main content

commonware_storage/qmdb/keyless/operation/
fixed.rs

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