Skip to main content

heddle_thread_api/
live_replication_input.rs

1//! Allocation-free protobuf shape checks for live frames, before prost builds
2//! repeated-message vectors. This bounds protobuf framing and retained input;
3//! it is not a bound on arbitrary canonical MessagePack object decoding.
4use std::mem::size_of;
5
6use crate::{contract::*, replication::opening::FRAME_LIMIT, transport};
7
8// Complete recursive message vocabulary reachable through ErrorDetail/StreamFailure
9// and TransferObject. Inline timestamp/duration storage is included in its parent.
10// Each heap-bearing protobuf node needs at least a tag+length (two wire bytes).
11// Four times the largest layout per wire byte covers Vec's minimum/growth
12// capacity and coexistence of an old/new singular value during duplicate merge.
13// Payload byte/string copies are additionally covered by the base reservation.
14const OPAQUE_NODE_SIZES: &[usize] = &[
15    size_of::<api::heddle::api::common::CallFailure>(),
16    size_of::<api::heddle::api::common::ErrorDetail>(),
17    size_of::<api::heddle::api::common::RetryAdvice>(),
18    size_of::<api::heddle::api::common::ConflictDetail>(),
19    size_of::<api::heddle::api::common::CursorFailure>(),
20    size_of::<api::heddle::api::common::CapabilityRequirement>(),
21    size_of::<api::heddle::api::common::PolicyDenial>(),
22    size_of::<api::heddle::api::common::UnknownDetail>(),
23    size_of::<api::heddle::api::common::AmbiguousChangeIdDetail>(),
24    size_of::<api::heddle::api::common::SignupFailure>(),
25    size_of::<api::heddle::api::common::StreamFailure>(),
26    size_of::<api::heddle::api::common::HumanVerificationChallenge>(),
27    size_of::<api::heddle::api::common::OAuthLinkChallenge>(),
28    size_of::<TransferObject>(),
29    size_of::<ObjectAddress>(),
30    size_of::<String>(),
31    size_of::<Vec<u8>>(),
32];
33fn opaque_node_factor() -> usize {
34    OPAQUE_NODE_SIZES.iter().copied().max().unwrap_or(0) * 4
35}
36
37type Result<T> = std::result::Result<T, transport::Error>;
38fn invalid() -> transport::Error {
39    transport::Error::Protocol("live replication input shape exceeds bounds")
40}
41struct Field<'a> {
42    tag: u32,
43    data: Option<&'a [u8]>,
44}
45fn varint(bytes: &mut &[u8]) -> Result<u64> {
46    let mut value = 0u64;
47    for shift in (0..70).step_by(7) {
48        let (&byte, rest) = bytes.split_first().ok_or_else(invalid)?;
49        *bytes = rest;
50        if shift == 63 && byte > 1 {
51            return Err(invalid());
52        }
53        value |= u64::from(byte & 127) << shift;
54        if byte < 128 {
55            return Ok(value);
56        }
57    }
58    Err(invalid())
59}
60fn field<'a>(bytes: &mut &'a [u8]) -> Result<Option<Field<'a>>> {
61    if bytes.is_empty() {
62        return Ok(None);
63    }
64    let key = varint(bytes)?;
65    let tag = u32::try_from(key >> 3).map_err(|_| invalid())?;
66    if tag == 0 {
67        return Err(invalid());
68    }
69    let data = match key & 7 {
70        0 => {
71            varint(bytes)?;
72            None
73        }
74        1 | 5 => {
75            let n = if key & 7 == 1 { 8 } else { 4 };
76            *bytes = bytes.get(n..).ok_or_else(invalid)?;
77            None
78        }
79        2 => {
80            let n = usize::try_from(varint(bytes)?).map_err(|_| invalid())?;
81            let value = bytes.get(..n).ok_or_else(invalid)?;
82            *bytes = &bytes[n..];
83            Some(value)
84        }
85        _ => return Err(invalid()),
86    };
87    Ok(Some(Field { tag, data }))
88}
89fn data<'a>(field: &Field<'a>) -> Result<&'a [u8]> {
90    field.data.ok_or_else(invalid)
91}
92fn count(value: &mut usize, max: usize) -> Result<()> {
93    *value += 1;
94    if *value > max { Err(invalid()) } else { Ok(()) }
95}
96fn id(field: &Field<'_>) -> Result<()> {
97    if data(field)?.len() == 32 {
98        Ok(())
99    } else {
100        Err(invalid())
101    }
102}
103fn signature(mut bytes: &[u8]) -> Result<()> {
104    let (mut keys, mut signatures) = (0, 0);
105    while let Some(f) = field(&mut bytes)? {
106        match f.tag {
107            1 => {
108                count(&mut keys, 1)?;
109                id(&f)?;
110            }
111            2 => {
112                count(&mut signatures, 1)?;
113                if data(&f)?.len() != 64 {
114                    return Err(invalid());
115                }
116            }
117            _ => {}
118        }
119    }
120    if keys == 1 && signatures == 1 {
121        Ok(())
122    } else {
123        Err(invalid())
124    }
125}
126fn record(mut bytes: &[u8]) -> Result<()> {
127    let (mut formats, mut canonicals, mut signatures) = (0, 0, 0);
128    while let Some(f) = field(&mut bytes)? {
129        match f.tag {
130            1 => {
131                count(&mut formats, 1)?;
132                if data(&f)?.len() > 128 {
133                    return Err(invalid());
134                }
135            }
136            2 => {
137                count(&mut canonicals, 1)?;
138                data(&f)?;
139            }
140            3 => {
141                count(&mut signatures, 1)?;
142                signature(data(&f)?)?;
143            }
144            _ => {}
145        }
146    }
147    if formats == 1 && canonicals == 1 && signatures == 1 {
148        Ok(())
149    } else {
150        Err(invalid())
151    }
152}
153fn frontier(mut bytes: &[u8], heads: &mut usize, max: usize) -> Result<()> {
154    while let Some(f) = field(&mut bytes)? {
155        if f.tag == 2 {
156            count(heads, max)?;
157            id(&f)?;
158        }
159    }
160    Ok(())
161}
162fn failure(mut bytes: &[u8], opaque: &mut usize) -> Result<()> {
163    while let Some(f) = field(&mut bytes)? {
164        match f.tag {
165            2 => {
166                data(&f)?;
167            }
168            // Preserve the complete published ErrorDetail contract. It is not
169            // interpreted as source authority; allow conservative decode space
170            // for its nested protobuf structures rather than discarding it.
171            4 => {
172                *opaque = opaque.checked_add(data(&f)?.len()).ok_or_else(invalid)?;
173            }
174            _ => {}
175        }
176    }
177    Ok(())
178}
179fn rejection(mut bytes: &[u8], opaque: &mut usize) -> Result<()> {
180    while let Some(f) = field(&mut bytes)? {
181        match f.tag {
182            1 => id(&f)?,
183            2 => failure(data(&f)?, opaque)?,
184            _ => {}
185        }
186    }
187    Ok(())
188}
189/// A conservative reservation for validated protobuf framing, transient wire
190/// copies during matching, and retained decoded originals/sidecars. Canonical
191/// ThreadOperation decoding remains subject to its independent model limits.
192pub fn reservation(bytes: &[u8], max: usize) -> Result<usize> {
193    if max == 0 || max > 64 || bytes.len() > FRAME_LIMIT {
194        return Err(invalid());
195    }
196    let original_len = bytes.len();
197    let mut bytes = bytes;
198    let mut body_count = 0;
199    let mut objects = 0usize;
200    let mut opaque = 0usize;
201    while let Some(body) = field(&mut bytes)? {
202        if !(2..=5).contains(&body.tag) {
203            return Err(invalid());
204        }
205        count(&mut body_count, 1)?;
206        let mut inner = data(&body)?;
207        let (mut first, mut second, mut third, mut fourth, mut fifth) = (0, 0, 0, 0, 0);
208        let mut heads = 0;
209        while let Some(f) = field(&mut inner)? {
210            match (body.tag, f.tag) {
211                (2, 1) => {
212                    count(&mut first, max)?;
213                    frontier(data(&f)?, &mut heads, max)?;
214                    objects += 1;
215                }
216                (3, 1) => {
217                    count(&mut first, max)?;
218                    id(&f)?;
219                    objects += 1;
220                }
221                (4, 1) => {
222                    count(&mut first, max)?;
223                    record(data(&f)?)?;
224                    objects += 3;
225                }
226                (4, 2) => {
227                    count(&mut second, max)?;
228                    record(data(&f)?)?;
229                    objects += 3;
230                }
231                (4, 3) => {
232                    count(&mut third, max)?;
233                    record(data(&f)?)?;
234                    objects += 3;
235                }
236                (5, 1) | (5, 2) => {
237                    count(&mut first, max)?;
238                    id(&f)?;
239                    objects += 1;
240                }
241                (5, 3) => {
242                    count(&mut first, max)?;
243                    count(&mut third, max)?;
244                    rejection(data(&f)?, &mut opaque)?;
245                    objects += 3;
246                }
247                (5, 4) => {
248                    count(&mut fourth, max)?;
249                    frontier(data(&f)?, &mut heads, max)?;
250                    objects += 1;
251                }
252                (5, 5) => {
253                    count(&mut fifth, max)?;
254                    opaque = opaque.checked_add(data(&f)?.len()).ok_or_else(invalid)?;
255                    objects += 1;
256                }
257                (5, 6) => {
258                    data(&f)?;
259                }
260                _ => {}
261            }
262        }
263    }
264    if body_count != 1 {
265        return Err(invalid());
266    }
267    let container = size_of::<crate::replication::Frame>()
268        + size_of::<crate::replication::InputUnit>()
269        + size_of::<SignedRecord>()
270        + size_of::<RecordSignature>()
271        + size_of::<ReplicationRejection>();
272    original_len
273        .checked_mul(6)
274        .and_then(|value| value.checked_add((objects + 4) * 4 * container))
275        .and_then(|value| value.checked_add(opaque.saturating_mul(opaque_node_factor())))
276        .ok_or_else(invalid)
277}
278
279#[cfg(test)]
280mod tests {
281    use prost::Message;
282
283    use super::*;
284    fn record() -> SignedRecord {
285        SignedRecord {
286            format: "fixture".into(),
287            canonical_record: vec![1; 32],
288            signatures: vec![RecordSignature {
289                public_key: vec![2; 32],
290                signature: vec![3; 64],
291            }],
292        }
293    }
294    fn batch(operations: Vec<SignedRecord>, authority_admissions: Vec<SignedRecord>) -> Vec<u8> {
295        ReplicateThreadRequest {
296            body: Some(replicate_thread_request::Body::Operations(
297                ReplicationOperations {
298                    operations,
299                    authority_admissions,
300                    boundary_acceptances: vec![],
301                },
302            )),
303        }
304        .encode_to_vec()
305    }
306    #[test]
307    fn protobuf_shape_preflight_bounds_record_expansion_before_decode() {
308        let valid = record();
309        let bytes = batch(vec![valid.clone(); 64], vec![valid.clone(); 64]);
310        assert!(
311            reservation(&bytes, 64).expect("maximum separate record and receipt vectors")
312                > bytes.len()
313        );
314        assert!(
315            reservation(&bytes, 63).is_err(),
316            "negotiated count applies before protobuf decode"
317        );
318        let mut duplicate = valid.clone();
319        duplicate.signatures.push(valid.signatures[0].clone());
320        assert!(
321            reservation(&batch(vec![duplicate], vec![]), 64).is_err(),
322            "multiple nested signatures must fail shape preflight"
323        );
324        let mut short = valid;
325        short.signatures[0].signature.truncate(63);
326        assert!(
327            reservation(&batch(vec![short], vec![]), 64).is_err(),
328            "signature shape is checked without allocating decoded vectors"
329        );
330        assert!(reservation(&[0x22, 0xff], 64).is_err(), "truncated length");
331    }
332    #[test]
333    fn protobuf_shape_bounds_boundary_acceptance_carriers() {
334        let valid = record();
335        let encode = |records| {
336            ReplicateThreadRequest {
337                body: Some(replicate_thread_request::Body::Operations(
338                    ReplicationOperations {
339                        operations: vec![valid.clone()],
340                        authority_admissions: vec![],
341                        boundary_acceptances: records,
342                    },
343                )),
344            }
345            .encode_to_vec()
346        };
347        let maximum = encode(vec![valid.clone(); 64]);
348        assert!(reservation(&maximum, 64).expect("bounded acceptance records") > maximum.len());
349        assert!(
350            reservation(&maximum, 63).is_err(),
351            "acceptance count obeys negotiated bound"
352        );
353        let mut duplicate = valid.clone();
354        duplicate.signatures.push(valid.signatures[0].clone());
355        assert!(
356            reservation(&encode(vec![duplicate]), 64).is_err(),
357            "acceptance signature expansion is rejected before decode"
358        );
359    }
360    #[test]
361    fn protobuf_shape_preserves_legal_receipt_details_and_accounts_nested_layouts() {
362        use api::heddle::api::common::{
363            CallFailure, CapabilityRequirement, ErrorDetail, error_detail::Context,
364        };
365        let failure = CallFailure {
366            code: 9,
367            message: "needs a capability".into(),
368            error: Some(ErrorDetail {
369                context: Some(Context::Capability(CapabilityRequirement {
370                    capabilities: vec![String::new(); 128],
371                })),
372                ..Default::default()
373            }),
374        };
375        let receipt = ReplicationReceipt {
376            rejected: vec![ReplicationRejection {
377                operation_id: vec![7; 32],
378                failure: Some(failure),
379            }],
380            accepted_frontiers: vec![CausalFrontier {
381                facet: 1,
382                heads: vec![vec![8; 32]],
383            }],
384            missing_objects: vec![TransferObject {
385                address: Some(ObjectAddress {
386                    algorithm: "blake3".into(),
387                    digest: vec![9; 32],
388                }),
389                kind: "source".into(),
390                ..Default::default()
391            }],
392            sharing_policy_version: vec![5; 32],
393            ..Default::default()
394        };
395        let response = ReplicateThreadResponse {
396            body: Some(replicate_thread_response::Body::Receipt(receipt)),
397        };
398        let bytes = response.encode_to_vec();
399        let reserved = reservation(&bytes, 64)
400            .expect("preserve all existing receipt capability and coverage fields");
401        assert_eq!(
402            ReplicateThreadResponse::decode(bytes.as_slice()).expect("prost response"),
403            response
404        );
405        assert_eq!(
406            OPAQUE_NODE_SIZES.len(),
407            17,
408            "all currently recursive error/transfer node layouts enumerated"
409        );
410        assert!(
411            OPAQUE_NODE_SIZES
412                .iter()
413                .all(|size| opaque_node_factor() >= 4 * size)
414        );
415        assert!(
416            reserved > 128 * size_of::<String>() + bytes.len(),
417            "empty repeated strings still consume vector slots"
418        );
419    }
420}