Skip to main content

weida_protocol/
negotiate.rs

1//! Version and capability negotiation.
2//!
3//! Negotiation is an exchange during connection establishment, not a
4//! long-lived control stream (master doc §13, §11). Both sides run this same
5//! function against their own and the peer's HELLO, so both reach the same
6//! verdict without a round trip.
7
8use std::fmt;
9
10use crate::header::{GuaranteeSet, HeaderError, Hello};
11
12/// Capability code `1`, `datagram`: the sender reads QUIC DATAGRAM frames and
13/// FLOW streams (`docs/PROTOCOL.md` §6.1). A FLOW stream or a datagram is
14/// sent only when both HELLOs list it.
15pub const CAPABILITY_DATAGRAM: u64 = 1;
16
17/// Capability code `2`, `path_report`: the sender reads, and will send, one
18/// REPORT stream (`docs/PROTOCOL.md` §6.1, §6.10). A REPORT stream is sent
19/// only when both HELLOs list it.
20pub const CAPABILITY_PATH_REPORT: u64 = 2;
21
22/// The negotiated parameters of a connection.
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub struct Agreed {
25    /// Effective wire protocol version: the highest version both sides support.
26    pub version: u64,
27    /// Largest header the **peer** accepts. Outgoing headers must not exceed
28    /// it; it is not a limit on what we receive.
29    pub send_max_header_bytes: u64,
30    /// The effective guarantee set: the weaker of the two offers, dimension
31    /// by dimension (`docs/PROTOCOL.md` §2.3 step 5).
32    ///
33    /// Between two v0 peers this is [`GuaranteeSet::CORE`], because neither
34    /// declares anything and an absent declaration means `core`.
35    pub guarantees: GuaranteeSet,
36    /// Both HELLOs listed [`CAPABILITY_DATAGRAM`], so FLOW streams and
37    /// datagrams may be sent on this connection.
38    pub datagrams: bool,
39    /// Both HELLOs listed [`CAPABILITY_PATH_REPORT`], so each side sends one
40    /// REPORT stream on this connection.
41    pub path_report: bool,
42}
43
44impl Agreed {
45    /// Refuses an encoded header of `len` bytes that the peer would not read.
46    ///
47    /// `send_max_header_bytes` is the peer's `max_header_bytes`; a longer
48    /// header makes its preamble parser close the connection
49    /// (`docs/PROTOCOL.md` §3.2), so a sender checks the length it is about to
50    /// write rather than learn it from the close.
51    pub fn check_header_len(&self, len: usize) -> Result<(), HeaderError> {
52        if len as u64 > self.send_max_header_bytes {
53            return Err(HeaderError::HeaderTooLong {
54                len,
55                max: self.send_max_header_bytes,
56            });
57        }
58        Ok(())
59    }
60}
61
62/// Why negotiation failed.
63#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64pub enum NegotiateError {
65    /// The version sets do not intersect.
66    NoCommonVersion,
67    /// The peer requires a capability this implementation does not support.
68    UnsupportedRequiredCapability(u64),
69    /// An unordered dimension was declared differently by the two sides, so
70    /// there is no weaker of the two to take.
71    IncomparableGuarantee {
72        /// Dimension that disagreed.
73        dimension: &'static str,
74    },
75    /// The effective set does not reach what one side requires. There is no
76    /// downgrade path: the handshake fails instead
77    /// ([decisions/0006](../../../docs/decisions/0006-guarantee-sets.md) §4.4).
78    GuaranteeNotOffered {
79        /// `true` when it was the peer's requirement that went unmet.
80        peers_requirement: bool,
81    },
82}
83
84impl fmt::Display for NegotiateError {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        match self {
87            NegotiateError::NoCommonVersion => {
88                f.write_str("no wire protocol version is supported by both peers")
89            }
90            NegotiateError::UnsupportedRequiredCapability(c) => {
91                write!(f, "peer requires unsupported capability {c}")
92            }
93            NegotiateError::IncomparableGuarantee { dimension } => {
94                write!(
95                    f,
96                    "the two peers declare different {dimension}, which is not an ordered dimension"
97                )
98            }
99            NegotiateError::GuaranteeNotOffered { peers_requirement } => {
100                let who = if *peers_requirement { "peer" } else { "local" };
101                write!(
102                    f,
103                    "the effective guarantee set does not reach the {who} requirement"
104                )
105            }
106        }
107    }
108}
109
110impl std::error::Error for NegotiateError {}
111
112impl From<NegotiateError> for weida_core::Error {
113    fn from(e: NegotiateError) -> weida_core::Error {
114        weida_core::Error::Negotiation(e.to_string())
115    }
116}
117
118/// Negotiates a connection from the two HELLO headers.
119///
120/// The effective version is the maximum of the intersection of the two version
121/// lists. Every capability the peer marks as required must appear in our own
122/// supported set, so a peer requiring a code we did not list fails. The codes
123/// defined are [`CAPABILITY_DATAGRAM`] and [`CAPABILITY_PATH_REPORT`], each
124/// agreed when both sides list it.
125///
126/// The guarantee set is the weaker of the two offers, and both sides'
127/// requirements must be reachable by it — there is no downgrade path
128/// (`docs/PROTOCOL.md` §2.3 steps 5 and 6). Two v0 peers declare nothing, so
129/// the effective set is `core` and the two steps are no-ops.
130pub fn negotiate(ours: &Hello, theirs: &Hello) -> Result<Agreed, NegotiateError> {
131    let version = theirs
132        .versions
133        .iter()
134        .copied()
135        .filter(|v| ours.versions.contains(v))
136        .max()
137        .ok_or(NegotiateError::NoCommonVersion)?;
138
139    if let Some(missing) = theirs
140        .required_capabilities
141        .iter()
142        .copied()
143        .find(|c| !ours.capabilities.contains(c))
144    {
145        return Err(NegotiateError::UnsupportedRequiredCapability(missing));
146    }
147
148    let guarantees = ours
149        .offered()
150        .intersect(&theirs.offered())
151        .map_err(|dimension| NegotiateError::IncomparableGuarantee { dimension })?;
152    for (required, peers_requirement) in [(theirs.required(), true), (ours.required(), false)] {
153        if !guarantees.reaches(&required) {
154            return Err(NegotiateError::GuaranteeNotOffered { peers_requirement });
155        }
156    }
157
158    Ok(Agreed {
159        version,
160        send_max_header_bytes: theirs.max_header_bytes,
161        guarantees,
162        datagrams: ours.capabilities.contains(&CAPABILITY_DATAGRAM)
163            && theirs.capabilities.contains(&CAPABILITY_DATAGRAM),
164        path_report: ours.capabilities.contains(&CAPABILITY_PATH_REPORT)
165            && theirs.capabilities.contains(&CAPABILITY_PATH_REPORT),
166    })
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    use crate::header::{
174        Acknowledgement, Backpressure, Deduplication, Durability, OrderingMode, ProducerNaming,
175    };
176
177    fn hello(versions: &[u64], caps: &[u64], required: &[u64], max_header: u64) -> Hello {
178        Hello {
179            versions: versions.to_vec(),
180            max_header_bytes: max_header,
181            max_transfers: 1024,
182            capabilities: caps.to_vec(),
183            required_capabilities: required.to_vec(),
184            guarantees_offered: None,
185            guarantees_required: None,
186        }
187    }
188
189    /// A v0 HELLO that declares `offered` and `required` explicitly.
190    fn declaring(offered: GuaranteeSet, required: GuaranteeSet) -> Hello {
191        Hello {
192            guarantees_offered: Some(offered),
193            guarantees_required: Some(required),
194            ..Hello::v0(16384, 1024)
195        }
196    }
197
198    #[test]
199    fn v0_peers_agree_on_version_zero() {
200        let ours = Hello::v0(16384, 1024);
201        let theirs = Hello::v0(4096, 512);
202        let agreed = negotiate(&ours, &theirs).unwrap();
203        assert_eq!(agreed.version, 0);
204        // The send cap comes from the peer's advertised receive cap.
205        assert_eq!(agreed.send_max_header_bytes, 4096);
206    }
207
208    #[test]
209    fn a_header_longer_than_the_peer_accepts_is_refused() {
210        let agreed = negotiate(&Hello::v0(16384, 1024), &Hello::v0(4096, 512)).unwrap();
211        assert_eq!(agreed.check_header_len(4096), Ok(()));
212        assert_eq!(
213            agreed.check_header_len(4097),
214            Err(HeaderError::HeaderTooLong {
215                len: 4097,
216                max: 4096
217            })
218        );
219    }
220
221    #[test]
222    fn the_highest_common_version_wins() {
223        let ours = hello(&[0, 1, 2, 5], &[], &[], 16384);
224        let theirs = hello(&[1, 2, 3], &[], &[], 16384);
225        assert_eq!(negotiate(&ours, &theirs).unwrap().version, 2);
226    }
227
228    #[test]
229    fn version_order_in_the_list_does_not_matter() {
230        let ours = hello(&[5, 0, 2], &[], &[], 16384);
231        let theirs = hello(&[2, 5], &[], &[], 16384);
232        assert_eq!(negotiate(&ours, &theirs).unwrap().version, 5);
233    }
234
235    #[test]
236    fn an_empty_intersection_fails() {
237        let ours = hello(&[0], &[], &[], 16384);
238        for theirs in [hello(&[99], &[], &[], 16384), hello(&[], &[], &[], 16384)] {
239            assert_eq!(
240                negotiate(&ours, &theirs),
241                Err(NegotiateError::NoCommonVersion)
242            );
243        }
244    }
245
246    #[test]
247    fn a_required_capability_we_lack_fails() {
248        let ours = Hello::v0(16384, 1024);
249        let theirs = hello(&[0], &[7], &[7], 16384);
250        assert_eq!(
251            negotiate(&ours, &theirs),
252            Err(NegotiateError::UnsupportedRequiredCapability(7))
253        );
254    }
255
256    #[test]
257    fn a_required_capability_we_have_succeeds() {
258        let ours = hello(&[0], &[7, 9], &[], 16384);
259        let theirs = hello(&[0], &[7], &[7], 8192);
260        assert_eq!(
261            negotiate(&ours, &theirs).unwrap(),
262            Agreed {
263                version: 0,
264                send_max_header_bytes: 8192,
265                guarantees: GuaranteeSet::CORE,
266                datagrams: false,
267                path_report: false,
268            }
269        );
270    }
271
272    #[test]
273    fn datagrams_are_agreed_only_when_both_list_the_code() {
274        let with = hello(&[0], &[CAPABILITY_DATAGRAM], &[], 16384);
275        let without = Hello::v0(16384, 1024);
276        assert!(negotiate(&with, &with).unwrap().datagrams);
277        assert!(!negotiate(&with, &without).unwrap().datagrams);
278        assert!(!negotiate(&without, &with).unwrap().datagrams);
279    }
280
281    #[test]
282    fn path_reports_are_agreed_only_when_both_list_the_code() {
283        let with = hello(&[0], &[CAPABILITY_PATH_REPORT], &[], 16384);
284        let without = hello(&[0], &[CAPABILITY_DATAGRAM], &[], 16384);
285        assert!(negotiate(&with, &with).unwrap().path_report);
286        assert!(!negotiate(&with, &without).unwrap().path_report);
287        assert!(!negotiate(&without, &with).unwrap().path_report);
288    }
289
290    #[test]
291    fn two_v0_peers_negotiate_core_unchanged() {
292        // The property the whole "spec ahead of code" arrangement rests on:
293        // a peer that declares nothing still gets exactly `core`.
294        let agreed = negotiate(&Hello::v0(16384, 1024), &Hello::v0(16384, 1024)).unwrap();
295        assert_eq!(agreed.guarantees, GuaranteeSet::CORE);
296        assert!(agreed.guarantees.is_core());
297    }
298
299    #[test]
300    fn the_effective_set_is_the_weaker_offer_per_dimension() {
301        let strong = GuaranteeSet {
302            ordering: OrderingMode::PerProducerReassemble,
303            deduplication: Deduplication::Bounded,
304            dedup_window_ms: Some(60_000),
305            control_isolated: true,
306            ..GuaranteeSet::CORE
307        };
308        let weaker = GuaranteeSet {
309            ordering: OrderingMode::PerProducerDetect,
310            deduplication: Deduplication::Bounded,
311            dedup_window_ms: Some(5_000),
312            control_isolated: false,
313            ..GuaranteeSet::CORE
314        };
315        let agreed = negotiate(
316            &declaring(strong, GuaranteeSet::CORE),
317            &declaring(weaker, GuaranteeSet::CORE),
318        )
319        .unwrap();
320        assert_eq!(agreed.guarantees.ordering, OrderingMode::PerProducerDetect);
321        // A shorter window is the weaker promise.
322        assert_eq!(agreed.guarantees.dedup_window_ms, Some(5_000));
323        assert!(!agreed.guarantees.control_isolated);
324    }
325
326    #[test]
327    fn a_requested_level_the_peer_does_not_offer_fails() {
328        let wants_ordering = GuaranteeSet {
329            ordering: OrderingMode::PerProducerDetect,
330            ..GuaranteeSet::CORE
331        };
332        // We offer and require detect ordering; the peer offers `core`.
333        let err = negotiate(
334            &declaring(wants_ordering, wants_ordering),
335            &Hello::v0(16384, 1024),
336        )
337        .unwrap_err();
338        assert_eq!(
339            err,
340            NegotiateError::GuaranteeNotOffered {
341                peers_requirement: false
342            }
343        );
344        // And symmetrically, when it is the peer that requires it.
345        let err = negotiate(
346            &Hello::v0(16384, 1024),
347            &declaring(wants_ordering, wants_ordering),
348        )
349        .unwrap_err();
350        assert_eq!(
351            err,
352            NegotiateError::GuaranteeNotOffered {
353                peers_requirement: true
354            }
355        );
356    }
357
358    #[test]
359    fn unordered_dimensions_must_match_exactly() {
360        for (ours, theirs, dimension) in [
361            (
362                GuaranteeSet {
363                    backpressure: Backpressure::Drop,
364                    ..GuaranteeSet::CORE
365                },
366                GuaranteeSet::CORE,
367                "backpressure",
368            ),
369            (
370                GuaranteeSet {
371                    producer_naming: ProducerNaming::Stable,
372                    ..GuaranteeSet::CORE
373                },
374                GuaranteeSet::CORE,
375                "producer naming",
376            ),
377            (
378                GuaranteeSet {
379                    acknowledgement: Acknowledgement::Stored,
380                    durability: Some(Durability::Flushed),
381                    ..GuaranteeSet::CORE
382                },
383                GuaranteeSet {
384                    acknowledgement: Acknowledgement::Stored,
385                    durability: Some(Durability::Written),
386                    ..GuaranteeSet::CORE
387                },
388                "durability",
389            ),
390        ] {
391            let err = negotiate(
392                &declaring(ours, GuaranteeSet::CORE),
393                &declaring(theirs, GuaranteeSet::CORE),
394            )
395            .unwrap_err();
396            assert_eq!(err, NegotiateError::IncomparableGuarantee { dimension });
397        }
398    }
399
400    #[test]
401    fn a_weakened_acknowledgement_drops_the_axes_it_cannot_carry() {
402        let stored = GuaranteeSet {
403            acknowledgement: Acknowledgement::Stored,
404            durability: Some(Durability::Flushed),
405            ..GuaranteeSet::CORE
406        };
407        let agreed = negotiate(
408            &declaring(stored, GuaranteeSet::CORE),
409            &Hello::v0(16384, 1024),
410        )
411        .unwrap();
412        // The peer offers only a transport receipt, so the durability axis
413        // has nothing left to qualify and the set stays legal.
414        assert_eq!(
415            agreed.guarantees.acknowledgement,
416            Acknowledgement::TransportReceipt
417        );
418        assert_eq!(agreed.guarantees.durability, None);
419        assert!(agreed.guarantees.is_core());
420    }
421
422    #[test]
423    fn an_axis_the_peer_never_declared_is_not_reached() {
424        let stored = |durability| GuaranteeSet {
425            acknowledgement: Acknowledgement::Stored,
426            durability,
427            ..GuaranteeSet::CORE
428        };
429        let replicated = |replicas| GuaranteeSet {
430            acknowledgement: Acknowledgement::Replicated,
431            replicas,
432            ..GuaranteeSet::CORE
433        };
434        for (wanted, undeclared) in [
435            (stored(Some(Durability::Flushed)), stored(None)),
436            (replicated(Some(3)), replicated(None)),
437        ] {
438            let err = negotiate(
439                &declaring(wanted, wanted),
440                &declaring(undeclared, GuaranteeSet::CORE),
441            )
442            .unwrap_err();
443            assert_eq!(
444                err,
445                NegotiateError::GuaranteeNotOffered {
446                    peers_requirement: false
447                }
448            );
449            // The peer's side of the same requirement.
450            let err = negotiate(
451                &declaring(undeclared, GuaranteeSet::CORE),
452                &declaring(wanted, wanted),
453            )
454            .unwrap_err();
455            assert_eq!(
456                err,
457                NegotiateError::GuaranteeNotOffered {
458                    peers_requirement: true
459                }
460            );
461        }
462    }
463
464    #[test]
465    fn an_axis_both_sides_declared_survives_the_intersection() {
466        let stored = GuaranteeSet {
467            acknowledgement: Acknowledgement::Stored,
468            durability: Some(Durability::Flushed),
469            ..GuaranteeSet::CORE
470        };
471        let agreed = negotiate(&declaring(stored, stored), &declaring(stored, stored)).unwrap();
472        assert_eq!(agreed.guarantees, stored);
473    }
474
475    #[test]
476    fn optional_peer_capabilities_are_ignored() {
477        let ours = Hello::v0(16384, 1024);
478        let theirs = hello(&[0], &[1, 2, 3], &[], 16384);
479        assert!(negotiate(&ours, &theirs).is_ok());
480    }
481
482    #[test]
483    fn version_mismatch_is_checked_before_capabilities() {
484        let ours = Hello::v0(16384, 1024);
485        let theirs = hello(&[99], &[], &[42], 16384);
486        assert_eq!(
487            negotiate(&ours, &theirs),
488            Err(NegotiateError::NoCommonVersion)
489        );
490    }
491
492    #[test]
493    fn negotiation_is_symmetric_for_the_agreed_version() {
494        let a = hello(&[0, 1], &[], &[], 1000);
495        let b = hello(&[1, 2], &[], &[], 2000);
496        let ab = negotiate(&a, &b).unwrap();
497        let ba = negotiate(&b, &a).unwrap();
498        assert_eq!(ab.version, ba.version);
499        assert_eq!(ab.send_max_header_bytes, 2000);
500        assert_eq!(ba.send_max_header_bytes, 1000);
501    }
502
503    #[test]
504    fn errors_become_negotiation_errors() {
505        let e: weida_core::Error = NegotiateError::NoCommonVersion.into();
506        assert!(matches!(e, weida_core::Error::Negotiation(_)));
507    }
508}