Skip to main content

krabka_remote_storage/worm/manifest/
encoding.rs

1//! The deterministic byte encoding that the manifest hash chain covers.
2//!
3//! `canonical_manifest_bytes` is the single definition of that layout, and
4//! the length-prefix helpers beside it are what make the encoding injective.
5
6use super::ManifestBody;
7
8/// Domain separation for the chain preimage, so a manifest body can never
9/// collide with any other chained value.
10pub const MANIFEST_BODY_DOMAIN: &[u8] = b"krabka-worm-manifest-body-v1\0";
11
12/// Appends a big-endian `u64` length prefix.
13///
14/// `u64` rather than `u32` so the conversion from `usize` is lossless on every
15/// target Krabka builds for. A saturating `u32` prefix would make the encoding
16/// non-injective in principle — two different bodies could share a preimage —
17/// and "a field that large cannot occur" is a claim about callers, not a
18/// property of the encoding. A canonical encoding that a chain head depends on
19/// should not rest on one, and four bytes per field is not worth the argument.
20fn push_len(out: &mut Vec<u8>, len: usize) { out.extend_from_slice(&(len as u64).to_be_bytes()); }
21
22/// Appends a length-prefixed byte string.
23pub(super) fn push_bytes(out: &mut Vec<u8>, bytes: &[u8]) {
24    push_len(out, bytes.len());
25    out.extend_from_slice(bytes);
26}
27
28/// Appends an optional string as a presence byte and, when present, a
29/// length-prefixed body.
30fn push_optional(out: &mut Vec<u8>, value: Option<&str>) {
31    match value {
32        None => out.push(0),
33        Some(text) => {
34            out.push(1);
35            push_bytes(out, text.as_bytes());
36        }
37    }
38}
39
40/// Deterministic byte encoding of a manifest body.
41///
42/// This is the preimage the hash chain covers. The writer and the verifier
43/// both call this function, and a disagreement between them makes every
44/// archive written under the older encoding fail verification with no way to
45/// tell tampering from a format change.
46///
47/// Every integer is big-endian, every length prefix is a `u64`, and every
48/// string is `UTF-8`. Every variable-length field carries a length, so no two
49/// distinct bodies encode to the same bytes.
50///
51/// ```text
52/// MANIFEST_BODY_DOMAIN
53/// format_version:u32
54/// len+topic  topic_id:16  partition:i32  segment_id:16
55/// start_offset:i64  end_offset:i64  max_timestamp_ms:i64
56/// broker_id:i32  event_timestamp_ms:i64  segment_size_bytes:i64
57/// leader_epochs.len():u64, then per entry in BTreeMap order: epoch:i32 offset:i64
58/// txn_index_empty:u8
59/// objects.len():u64, then per entry in vec order:
60///     len+suffix  len+key  size_bytes:u64  sha256:32
61///     e_tag:      0u8 | (1u8 len+value)
62///     version_id: 0u8 | (1u8 len+value)
63///     create_precondition:u8 (format version >= 2 only)
64/// epoch_id:16  seq:u64  prev_head:32
65/// ```
66#[must_use]
67pub fn canonical_manifest_bytes(body: &ManifestBody) -> Vec<u8> {
68    let mut out = Vec::new();
69    out.extend_from_slice(MANIFEST_BODY_DOMAIN);
70    out.extend_from_slice(&body.format_version.to_be_bytes());
71
72    let segment = &body.segment;
73    push_bytes(&mut out, segment.topic.as_bytes());
74    out.extend_from_slice(segment.topic_id.as_bytes());
75    out.extend_from_slice(&segment.partition.to_be_bytes());
76    out.extend_from_slice(segment.segment_id.as_bytes());
77    out.extend_from_slice(&segment.start_offset.to_be_bytes());
78    out.extend_from_slice(&segment.end_offset.to_be_bytes());
79    out.extend_from_slice(&segment.max_timestamp_ms.to_be_bytes());
80    out.extend_from_slice(&segment.broker_id.to_be_bytes());
81    out.extend_from_slice(&segment.event_timestamp_ms.to_be_bytes());
82    out.extend_from_slice(&segment.segment_size_bytes.to_be_bytes());
83    push_len(&mut out, segment.leader_epochs.len());
84    for (epoch, offset) in &segment.leader_epochs {
85        out.extend_from_slice(&epoch.to_be_bytes());
86        out.extend_from_slice(&offset.to_be_bytes());
87    }
88    out.push(u8::from(segment.txn_index_empty));
89
90    push_len(&mut out, body.objects.len());
91    for object in &body.objects {
92        push_bytes(&mut out, object.suffix.as_bytes());
93        push_bytes(&mut out, object.key.as_bytes());
94        out.extend_from_slice(&object.size_bytes.to_be_bytes());
95        out.extend_from_slice(&object.sha256.0);
96        push_optional(&mut out, object.e_tag.as_deref());
97        push_optional(&mut out, object.version_id.as_deref());
98        if body.format_version >= 2 {
99            out.push(u8::from(object.create_precondition));
100        }
101    }
102
103    out.extend_from_slice(body.chain.epoch_id.0.as_bytes());
104    out.extend_from_slice(&body.chain.seq.0.to_be_bytes());
105    out.extend_from_slice(&body.chain.prev_head.0);
106    out
107}
108
109#[cfg(test)]
110mod tests {
111    use {
112        super::*,
113        crate::worm::manifest::{
114            ChainHead, EpochId, ManifestSeq, ObjectEntry, Sha256Digest, manifest_head,
115            test_support::{bare_object, sample_body},
116        },
117        assert2::check,
118        uuid::Uuid,
119    };
120
121    /// One labelled edit to a manifest body, for the preimage-coverage table.
122    struct Mutation {
123        name: &'static str,
124        mutate: Box<dyn Fn(&mut ManifestBody)>,
125    }
126
127    fn mutation(name: &'static str, mutate: impl Fn(&mut ManifestBody) + 'static) -> Mutation {
128        Mutation {
129            name,
130            mutate: Box::new(mutate),
131        }
132    }
133
134    #[test]
135    fn canonical_bytes_survive_json_reserialization() {
136        let body = sample_body();
137        let expected = canonical_manifest_bytes(&body);
138
139        let compact = serde_json::to_string(&body).unwrap();
140        let from_compact: ManifestBody = serde_json::from_str(&compact).unwrap();
141        check!(
142            canonical_manifest_bytes(&from_compact) == expected,
143            "compact"
144        );
145
146        let pretty = serde_json::to_string_pretty(&from_compact).unwrap();
147        let from_pretty: ManifestBody = serde_json::from_str(&pretty).unwrap();
148        check!(canonical_manifest_bytes(&from_pretty) == expected, "pretty");
149
150        // A second full cycle, so an encoding that is stable only on the first
151        // pass does not slip through.
152        let again: ManifestBody =
153            serde_json::from_str(&serde_json::to_string(&from_pretty).unwrap()).unwrap();
154        check!(canonical_manifest_bytes(&again) == expected, "second cycle");
155        check!(again == body, "structural equality");
156        check!(manifest_head(&again) == manifest_head(&body), "head");
157    }
158
159    #[test]
160    fn canonical_bytes_change_with_every_field() {
161        let base = sample_body();
162        let base_bytes = canonical_manifest_bytes(&base);
163
164        let mutations: Vec<Mutation> = vec![
165            mutation("format_version", |b| b.format_version += 1),
166            mutation("segment.topic", |b| {
167                b.segment.topic = "payments".to_owned();
168            }),
169            mutation("segment.topic_id", |b| {
170                b.segment.topic_id = Uuid::from_u128(0x12);
171            }),
172            mutation("segment.partition", |b| b.segment.partition = 4),
173            mutation("segment.segment_id", |b| {
174                b.segment.segment_id = Uuid::from_u128(0x23);
175            }),
176            mutation("segment.start_offset", |b| b.segment.start_offset = 101),
177            mutation("segment.end_offset", |b| b.segment.end_offset = 200),
178            mutation("segment.max_timestamp_ms", |b| {
179                b.segment.max_timestamp_ms += 1;
180            }),
181            mutation("segment.broker_id", |b| b.segment.broker_id = 8),
182            mutation("segment.event_timestamp_ms", |b| {
183                b.segment.event_timestamp_ms += 1;
184            }),
185            mutation("segment.segment_size_bytes", |b| {
186                b.segment.segment_size_bytes += 1;
187            }),
188            mutation("segment.leader_epochs value", |b| {
189                b.segment.leader_epochs.insert(1, 151);
190            }),
191            mutation("segment.leader_epochs extra entry", |b| {
192                b.segment.leader_epochs.insert(2, 180);
193            }),
194            mutation("segment.leader_epochs removed entry", |b| {
195                b.segment.leader_epochs.remove(&1);
196            }),
197            mutation("segment.txn_index_empty", |b| {
198                b.segment.txn_index_empty = true;
199            }),
200            mutation("objects[0].suffix", |b| {
201                b.objects[0].suffix = ".timeindex".to_owned();
202            }),
203            mutation("objects[0].key", |b| {
204                b.objects[0].key = "archive/elsewhere.log".to_owned();
205            }),
206            mutation("objects[0].size_bytes", |b| b.objects[0].size_bytes += 1),
207            mutation("objects[0].sha256", |b| {
208                b.objects[0].sha256 = Sha256Digest::of(b"other body");
209            }),
210            mutation("objects[0].e_tag changed", |b| {
211                b.objects[0].e_tag = Some("\"other\"".to_owned());
212            }),
213            mutation("objects[0].e_tag cleared", |b| b.objects[0].e_tag = None),
214            mutation("objects[1].e_tag set", |b| {
215                b.objects[1].e_tag = Some("\"new\"".to_owned());
216            }),
217            mutation("objects[0].version_id changed", |b| {
218                b.objects[0].version_id = Some("other".to_owned());
219            }),
220            mutation("objects[0].version_id cleared", |b| {
221                b.objects[0].version_id = None;
222            }),
223            mutation("objects[1].version_id set", |b| {
224                b.objects[1].version_id = Some("new".to_owned());
225            }),
226            mutation("objects[0].create_precondition", |b| {
227                b.objects[0].create_precondition = true;
228            }),
229            mutation("objects order", |b| b.objects.swap(0, 1)),
230            mutation("objects count grows", |b| b.objects.push(bare_object())),
231            mutation("objects count shrinks", |b| {
232                b.objects.pop();
233            }),
234            mutation("chain.epoch_id", |b| {
235                b.chain.epoch_id = EpochId(Uuid::from_u128(0x98));
236            }),
237            mutation("chain.seq", |b| b.chain.seq = ManifestSeq(5)),
238            mutation("chain.prev_head", |b| {
239                b.chain.prev_head = ChainHead([8u8; 32]);
240            }),
241        ];
242
243        for Mutation { name, mutate } in mutations {
244            let mut mutated = base.clone();
245            mutate(&mut mutated);
246            check!(mutated != base, "case {name} did not change the body");
247            check!(
248                canonical_manifest_bytes(&mutated) != base_bytes,
249                "case {name} is missing from the preimage"
250            );
251            check!(
252                manifest_head(&mutated) != manifest_head(&base),
253                "case {name} does not move the chain head"
254            );
255        }
256    }
257
258    #[test]
259    fn length_prefixes_prevent_field_boundary_ambiguity() {
260        let mut left = sample_body();
261        left.segment.topic = "ab".to_owned();
262        left.objects = vec![ObjectEntry {
263            key: "c".to_owned(),
264            ..bare_object()
265        }];
266
267        let mut right = left.clone();
268        right.segment.topic = "a".to_owned();
269        right.objects[0].key = "bc".to_owned();
270
271        check!(canonical_manifest_bytes(&left) != canonical_manifest_bytes(&right));
272    }
273}