use crate::{ merkle::Family, qmdb::{ any::{FixedValue, value::FixedEncoding}, keyless::operation::{APPEND_CONTEXT, COMMIT_CONTEXT, Codec, Operation}, operation::{commit_fixed_operation_size, read_commit_fixed, write_commit_fixed}, }, }; use commonware_codec::{ Error as CodecError, FixedSize, ReadExt as _, Write, util::{at_least, ensure_zeros}, }; use commonware_runtime::{Buf, BufMut}; /// Fixed padded operation size: `Commit` is always the larger variant, so the uniform size is the /// commit size, which `Append` pads to match. const fn op_size() -> usize { commit_fixed_operation_size::() } impl Codec for FixedEncoding { type ReadCfg = (); fn write_operation(op: &Operation, buf: &mut impl BufMut) { let total = op_size::(); match op { Operation::Append(value) => { APPEND_CONTEXT.write(buf); value.write(buf); // Pad to uniform size: 1 byte (option-tag gap) + u64::SIZE (floor gap). buf.put_bytes(0, total - 1 - V::SIZE); } Operation::Commit(metadata, floor) => { COMMIT_CONTEXT.write(buf); write_commit_fixed(metadata, *floor, buf); } } } fn read_operation( buf: &mut impl Buf, _cfg: &Self::ReadCfg, ) -> Result, CodecError> { let total = op_size::(); at_least(buf, total)?; match u8::read(buf)? { APPEND_CONTEXT => { let value = V::read(buf)?; ensure_zeros(buf, total - 1 - V::SIZE)?; Ok(Operation::Append(value)) } COMMIT_CONTEXT => { let (metadata, floor) = read_commit_fixed(buf)?; Ok(Operation::Commit(metadata, floor)) } e => Err(CodecError::InvalidEnum(e)), } } } impl FixedSize for Operation> { const SIZE: usize = op_size::(); } #[cfg(test)] mod tests { use super::*; use crate::merkle::{Location, mmr}; use commonware_codec::{DecodeExt, Encode, FixedSize}; use commonware_utils::sequence::U64; type Op = Operation>; #[test] fn all_variants_have_same_encoded_size() { let append = Op::Append(U64::new(42)); let commit_some = Op::Commit(Some(U64::new(99)), Location::new(5)); let commit_none = Op::Commit(None, Location::new(0)); let a = append.encode(); let b = commit_some.encode(); let c = commit_none.encode(); assert_eq!(a.len(), Op::SIZE); assert_eq!(b.len(), Op::SIZE); assert_eq!(c.len(), Op::SIZE); assert_eq!(Op::SIZE, 2 + U64::SIZE + u64::SIZE); } #[test] fn append_roundtrip() { let op = Op::Append(U64::new(12345)); let decoded = Op::decode(op.encode()).unwrap(); assert_eq!(op, decoded); } #[test] fn commit_some_roundtrip() { let op = Op::Commit(Some(U64::new(999)), Location::new(77)); let decoded = Op::decode(op.encode()).unwrap(); assert_eq!(op, decoded); } #[test] fn commit_none_roundtrip() { let op = Op::Commit(None, Location::new(42)); let decoded = Op::decode(op.encode()).unwrap(); assert_eq!(op, decoded); } #[test] fn invalid_context_byte_rejected() { let mut buf = vec![0u8; Op::SIZE]; buf[0] = 0xFF; assert!(matches!( Op::decode(buf.as_ref()).unwrap_err(), CodecError::InvalidEnum(0xFF) )); } #[test] fn non_zero_padding_rejected() { // Encode an Append, then corrupt the padding byte. let op = Op::Append(U64::new(1)); let mut buf: Vec = op.encode().to_vec(); // Padding is the last byte (part of the floor gap). *buf.last_mut().unwrap() = 0x01; assert!(Op::decode(buf.as_ref()).is_err()); } #[test] fn truncated_input_rejected() { let op = Op::Append(U64::new(1)); let buf = op.encode(); // One byte short. assert!(Op::decode(&buf[..buf.len() - 1]).is_err()); } #[test] fn commit_none_has_zero_value_bytes() { let op = Op::Commit(None, Location::new(0)); let buf: Vec = op.encode().to_vec(); // After context byte (0) and option-tag byte (0), all remaining bytes (including the // all-zero floor) should be zero. assert!(buf[2..].iter().all(|&b| b == 0)); } #[test] fn commit_floor_overflow_rejected() { // Construct a Commit buffer by hand with a floor beyond MAX_LEAVES. let mut buf = vec![0u8; Op::SIZE]; buf[0] = COMMIT_CONTEXT; // Option tag = false (None metadata); value bytes already zero. // Last 8 bytes are the floor; write u64::MAX big-endian. let floor_bytes = u64::MAX.to_be_bytes(); let floor_offset = Op::SIZE - u64::SIZE; buf[floor_offset..].copy_from_slice(&floor_bytes); assert!(matches!( Op::decode(buf.as_ref()).unwrap_err(), CodecError::Invalid(_, _) )); } #[test] fn commit_nonzero_metadata_bytes_rejected() { // Construct a Commit buffer by hand with option tag = false (None metadata) but a // nonzero byte in the metadata region. let mut buf = vec![0u8; Op::SIZE]; buf[0] = COMMIT_CONTEXT; buf[2] = 0x01; assert!(matches!( Op::decode(buf.as_ref()).unwrap_err(), CodecError::Invalid(_, _) )); } }