Skip to main content

reth_eth_wire/
capability.rs

1//! All capability related types
2
3use crate::{
4    errors::{P2PHandshakeError, P2PStreamError},
5    p2pstream::MAX_RESERVED_MESSAGE_ID,
6    protocol::{ProtoVersion, Protocol},
7    version::ParseVersionError,
8    Capability, EthMessageID, EthVersion,
9};
10use derive_more::{Deref, DerefMut};
11use std::{
12    borrow::Cow,
13    collections::{BTreeSet, HashMap},
14};
15
16/// This represents a shared capability, its version, and its message id offset.
17///
18/// The [offset](SharedCapability::message_id_offset) is the message ID offset for this shared
19/// capability, determined during the rlpx handshake.
20///
21/// See also [Message-id based multiplexing](https://github.com/ethereum/devp2p/blob/master/rlpx.md#message-id-based-multiplexing)
22#[derive(Debug, Clone, PartialEq, Eq, Hash)]
23pub enum SharedCapability {
24    /// The `eth` capability.
25    Eth {
26        /// (Highest) negotiated version of the eth capability.
27        version: EthVersion,
28        /// The message ID offset for this capability.
29        ///
30        /// This represents the message ID offset for the first message of the eth capability in
31        /// the message id space.
32        offset: u8,
33    },
34    /// Any other unknown capability.
35    UnknownCapability {
36        /// Shared capability.
37        cap: Capability,
38        /// The message ID offset for this capability.
39        ///
40        /// This represents the message ID offset for the first message of the eth capability in
41        /// the message id space.
42        offset: u8,
43        /// The number of messages of this capability. Needed to calculate range of message IDs in
44        /// demuxing.
45        messages: u8,
46    },
47}
48
49impl SharedCapability {
50    /// Creates a new [`SharedCapability`] based on the given name, offset, version (and messages
51    /// if the capability is custom).
52    ///
53    /// Returns an error if the offset is equal or less than [`MAX_RESERVED_MESSAGE_ID`].
54    pub(crate) fn new(
55        name: &str,
56        version: u8,
57        offset: u8,
58        messages: u8,
59    ) -> Result<Self, SharedCapabilityError> {
60        if offset <= MAX_RESERVED_MESSAGE_ID {
61            return Err(SharedCapabilityError::ReservedMessageIdOffset(offset))
62        }
63
64        match name {
65            "eth" => Ok(Self::eth(EthVersion::try_from(version)?, offset)),
66            _ => Ok(Self::UnknownCapability {
67                cap: Capability::new(name.to_string(), version as usize),
68                offset,
69                messages,
70            }),
71        }
72    }
73
74    /// Creates a new [`SharedCapability`] based on the given name, offset, and version.
75    pub(crate) const fn eth(version: EthVersion, offset: u8) -> Self {
76        Self::Eth { version, offset }
77    }
78
79    /// Returns the capability.
80    pub const fn capability(&self) -> Cow<'_, Capability> {
81        match self {
82            Self::Eth { version, .. } => Cow::Owned(Capability::eth(*version)),
83            Self::UnknownCapability { cap, .. } => Cow::Borrowed(cap),
84        }
85    }
86
87    /// Returns the name of the capability.
88    #[inline]
89    pub fn name(&self) -> &str {
90        match self {
91            Self::Eth { .. } => "eth",
92            Self::UnknownCapability { cap, .. } => cap.name.as_ref(),
93        }
94    }
95
96    /// Returns true if the capability is eth.
97    #[inline]
98    pub const fn is_eth(&self) -> bool {
99        matches!(self, Self::Eth { .. })
100    }
101
102    /// Returns the version of the capability.
103    pub const fn version(&self) -> u8 {
104        match self {
105            Self::Eth { version, .. } => *version as u8,
106            Self::UnknownCapability { cap, .. } => cap.version as u8,
107        }
108    }
109
110    /// Returns the eth version if it's the `eth` capability.
111    pub const fn eth_version(&self) -> Option<EthVersion> {
112        match self {
113            Self::Eth { version, .. } => Some(*version),
114            _ => None,
115        }
116    }
117
118    /// Returns the message ID offset of the current capability.
119    ///
120    /// This represents the message ID offset for the first message of the eth capability in the
121    /// message id space.
122    pub const fn message_id_offset(&self) -> u8 {
123        match self {
124            Self::Eth { offset, .. } | Self::UnknownCapability { offset, .. } => *offset,
125        }
126    }
127
128    /// Returns the message ID offset of the current capability relative to the start of the
129    /// reserved message id space: [`MAX_RESERVED_MESSAGE_ID`].
130    pub const fn relative_message_id_offset(&self) -> u8 {
131        self.message_id_offset() - MAX_RESERVED_MESSAGE_ID - 1
132    }
133
134    /// Returns the number of protocol messages supported by this capability.
135    pub const fn num_messages(&self) -> u8 {
136        match self {
137            Self::Eth { version, .. } => EthMessageID::message_count(*version),
138            Self::UnknownCapability { messages, .. } => *messages,
139        }
140    }
141}
142
143/// Non-empty,ordered list of recognized shared capabilities.
144///
145/// Shared capabilities are ordered alphabetically by case sensitive name.
146#[derive(Debug, Clone, Deref, DerefMut, PartialEq, Eq)]
147pub struct SharedCapabilities(Vec<SharedCapability>);
148
149impl SharedCapabilities {
150    /// Merges the local and peer capabilities and returns a new [`SharedCapabilities`] instance.
151    #[inline]
152    pub fn try_new(
153        local_protocols: Vec<Protocol>,
154        peer_capabilities: Vec<Capability>,
155    ) -> Result<Self, P2PStreamError> {
156        shared_capability_offsets(local_protocols, peer_capabilities).map(Self)
157    }
158
159    /// Iterates over the shared capabilities.
160    #[inline]
161    pub fn iter_caps(&self) -> impl Iterator<Item = &SharedCapability> {
162        self.0.iter()
163    }
164
165    /// Returns the eth capability if it is shared.
166    #[inline]
167    pub fn eth(&self) -> Result<&SharedCapability, P2PStreamError> {
168        self.iter_caps().find(|c| c.is_eth()).ok_or(P2PStreamError::CapabilityNotShared)
169    }
170
171    /// Returns the negotiated eth version if it is shared.
172    #[inline]
173    pub fn eth_version(&self) -> Result<EthVersion, P2PStreamError> {
174        self.iter_caps()
175            .find_map(SharedCapability::eth_version)
176            .ok_or(P2PStreamError::CapabilityNotShared)
177    }
178
179    /// Returns `true` if the shared capabilities are exactly `eth` and `snap/2` (EIP-8189), the
180    /// layout handled by the dedicated [`EthSnapStream`](crate::EthSnapStream).
181    #[inline]
182    pub fn is_exact_eth_snap_v2(&self) -> bool {
183        self.len() == 2 && self.ensure_matching_capability(&Capability::snap_2()).is_ok()
184    }
185
186    /// Returns true if the shared capabilities contain the given capability.
187    #[inline]
188    pub fn contains(&self, cap: &Capability) -> bool {
189        self.find(cap).is_some()
190    }
191
192    /// Returns the shared capability for the given capability.
193    #[inline]
194    pub fn find(&self, cap: &Capability) -> Option<&SharedCapability> {
195        self.0.iter().find(|c| c.version() == cap.version as u8 && c.name() == cap.name)
196    }
197
198    /// Converts a capability-local message ID into the relative `RLPx` message ID used by
199    /// [`P2PStream`](crate::P2PStream).
200    ///
201    /// `P2PStream` strips the reserved p2p message ID range before yielding subprotocol messages,
202    /// so the returned ID is relative to the first shared capability, not the absolute wire ID.
203    #[inline]
204    pub fn relative_message_id(&self, cap: &Capability, message_id: u8) -> Option<u8> {
205        let shared = self.find(cap)?;
206        if message_id >= shared.num_messages() {
207            return None
208        }
209
210        shared.relative_message_id_offset().checked_add(message_id)
211    }
212
213    /// Converts a relative `RLPx` message ID back into the message ID local to `cap`.
214    ///
215    /// Returns `None` if `cap` is not shared, if the relative ID belongs to a different
216    /// capability, or if it is outside the capability's negotiated message range.
217    #[inline]
218    pub fn capability_message_id(&self, cap: &Capability, relative_message_id: u8) -> Option<u8> {
219        let shared = self.find(cap)?;
220        let start = shared.relative_message_id_offset();
221        let end = start.checked_add(shared.num_messages())?;
222
223        (start..end).contains(&relative_message_id).then(|| relative_message_id - start)
224    }
225
226    /// Returns the matching shared capability for the given capability offset.
227    ///
228    /// `offset` is the multiplexed message id offset of the capability relative to the reserved
229    /// message id space. In other words, counting starts at [`MAX_RESERVED_MESSAGE_ID`] + 1, which
230    /// corresponds to the first non-reserved message id.
231    ///
232    /// For example: `offset == 0` corresponds to the first shared message across the shared
233    /// capabilities and will return the first shared capability that supports messages.
234    #[inline]
235    pub fn find_by_relative_offset(&self, offset: u8) -> Option<&SharedCapability> {
236        self.find_by_offset(offset.saturating_add(MAX_RESERVED_MESSAGE_ID + 1))
237    }
238
239    /// Returns the matching shared capability for the given capability offset.
240    ///
241    /// `offset` is the multiplexed message id offset of the capability that includes the reserved
242    /// message id space.
243    ///
244    /// This will always return None if `offset` is less than or equal to
245    /// [`MAX_RESERVED_MESSAGE_ID`] because the reserved message id space is not shared.
246    #[inline]
247    pub fn find_by_offset(&self, offset: u8) -> Option<&SharedCapability> {
248        let mut iter = self.0.iter();
249        let mut cap = iter.next()?;
250        if offset < cap.message_id_offset() {
251            // reserved message id space
252            return None
253        }
254
255        for next in iter {
256            if offset < next.message_id_offset() {
257                return Some(cap)
258            }
259            cap = next
260        }
261
262        let end = u16::from(cap.message_id_offset()) + u16::from(cap.num_messages());
263        (u16::from(offset) < end).then_some(cap)
264    }
265
266    /// Returns the shared capability for the given capability or an error if it's not compatible.
267    #[inline]
268    pub fn ensure_matching_capability(
269        &self,
270        cap: &Capability,
271    ) -> Result<&SharedCapability, UnsupportedCapabilityError> {
272        self.find(cap).ok_or_else(|| UnsupportedCapabilityError { capability: cap.clone() })
273    }
274
275    /// Returns the number of shared capabilities.
276    #[inline]
277    pub const fn len(&self) -> usize {
278        self.0.len()
279    }
280
281    /// Returns true if there are no shared capabilities.
282    #[inline]
283    pub const fn is_empty(&self) -> bool {
284        self.0.is_empty()
285    }
286}
287
288/// Determines the offsets for each shared capability between the input list of peer
289/// capabilities and the input list of locally supported [Protocol].
290///
291/// Additionally, the `p2p` capability version 5 is supported, but is
292/// expected _not_ to be in neither `local_protocols` or `peer_capabilities`.
293///
294/// **Note**: For `local_protocols` this takes [Protocol] because we need to know the number of
295/// messages per versioned capability. From the remote we only get the plain [Capability].
296#[inline]
297pub fn shared_capability_offsets(
298    local_protocols: Vec<Protocol>,
299    peer_capabilities: Vec<Capability>,
300) -> Result<Vec<SharedCapability>, P2PStreamError> {
301    // find intersection of capabilities
302    let our_capabilities =
303        local_protocols.into_iter().map(Protocol::split).collect::<HashMap<_, _>>();
304
305    // map of capability name to version
306    let mut shared_capabilities: HashMap<_, ProtoVersion> = HashMap::default();
307
308    // The `Ord` implementation for capability names should be equivalent to geth (and every other
309    // client), since geth uses golang's default string comparison, which orders strings
310    // lexicographically.
311    // https://golang.org/pkg/strings/#Compare
312    //
313    // This is important because the capability name is used to determine the message id offset, so
314    // if the sorting is not identical, offsets for connected peers could be inconsistent.
315    // This would cause the peers to send messages with the wrong message id, which is usually a
316    // protocol violation.
317    //
318    // The `Ord` implementation for `str` orders strings lexicographically.
319    let mut shared_capability_names = BTreeSet::new();
320
321    // find highest shared version of each shared capability
322    for peer_capability in peer_capabilities {
323        // if we contain this specific capability both peers share it
324        if let Some(messages) = our_capabilities.get(&peer_capability).copied() {
325            // If multiple versions are shared of the same (equal name) capability, the numerically
326            // highest wins, others are ignored
327            if shared_capabilities
328                .get(&peer_capability.name)
329                .is_none_or(|v| peer_capability.version > v.version)
330            {
331                shared_capabilities.insert(
332                    peer_capability.name.clone(),
333                    ProtoVersion { version: peer_capability.version, messages },
334                );
335                shared_capability_names.insert(peer_capability.name);
336            }
337        }
338    }
339
340    // disconnect if we don't share any capabilities
341    if shared_capabilities.is_empty() {
342        return Err(P2PStreamError::HandshakeError(P2PHandshakeError::NoSharedCapabilities))
343    }
344
345    // order versions based on capability name (alphabetical) and select offsets based on
346    // BASE_OFFSET + prev_total_message
347    let mut shared_with_offsets = Vec::new();
348
349    // Message IDs are assumed to be compact from ID 0x10 onwards (0x00-0x0f is reserved for the
350    // "p2p" capability) and given to each shared (equal-version, equal-name) capability in
351    // alphabetic order.
352    //
353    // Message IDs are a single byte, so offsets are accumulated in u16 and the shared capabilities
354    // must fit in IDs 0x10..=0xff.
355    let mut offset = u16::from(MAX_RESERVED_MESSAGE_ID) + 1;
356    for name in shared_capability_names {
357        let proto_version = &shared_capabilities[&name];
358        let start = u8::try_from(offset).map_err(|_| SharedCapabilityError::MessageIdOverflow)?;
359        let shared_capability = SharedCapability::new(
360            &name,
361            proto_version.version as u8,
362            start,
363            proto_version.messages,
364        )?;
365        offset += u16::from(shared_capability.num_messages());
366        if offset > u16::from(u8::MAX) + 1 {
367            return Err(SharedCapabilityError::MessageIdOverflow.into())
368        }
369        shared_with_offsets.push(shared_capability);
370    }
371
372    if shared_with_offsets.is_empty() {
373        return Err(P2PStreamError::HandshakeError(P2PHandshakeError::NoSharedCapabilities))
374    }
375
376    Ok(shared_with_offsets)
377}
378
379/// An error that may occur while creating a [`SharedCapability`].
380#[derive(Debug, thiserror::Error)]
381pub enum SharedCapabilityError {
382    /// Unsupported `eth` version.
383    #[error(transparent)]
384    UnsupportedVersion(#[from] ParseVersionError),
385    /// Thrown when the message id for a [`SharedCapability`] overlaps with the reserved p2p
386    /// message id space [`MAX_RESERVED_MESSAGE_ID`].
387    #[error("message id offset `{0}` is reserved")]
388    ReservedMessageIdOffset(u8),
389    /// Thrown when the shared capabilities need more message ids than fit in the single-byte
390    /// message id space.
391    #[error("shared capabilities exceed the message id space")]
392    MessageIdOverflow,
393}
394
395/// An error thrown when capabilities mismatch.
396#[derive(Debug, thiserror::Error)]
397#[error("unsupported capability {capability}")]
398pub struct UnsupportedCapabilityError {
399    capability: Capability,
400}
401
402impl UnsupportedCapabilityError {
403    /// Creates a new error with the given capability
404    pub const fn new(capability: Capability) -> Self {
405        Self { capability }
406    }
407}
408
409#[cfg(test)]
410mod tests {
411    use super::*;
412    use crate::{Capabilities, Capability, SnapVersion};
413    use alloy_primitives::bytes::Bytes;
414    use alloy_rlp::{Decodable, Encodable};
415    use reth_eth_wire_types::RawCapabilityMessage;
416
417    #[test]
418    fn from_eth_68() {
419        let capability = SharedCapability::new("eth", 68, MAX_RESERVED_MESSAGE_ID + 1, 13).unwrap();
420
421        assert_eq!(capability.name(), "eth");
422        assert_eq!(capability.version(), 68);
423        assert_eq!(
424            capability,
425            SharedCapability::Eth {
426                version: EthVersion::Eth68,
427                offset: MAX_RESERVED_MESSAGE_ID + 1
428            }
429        );
430    }
431
432    #[test]
433    fn from_eth_67() {
434        let capability = SharedCapability::new("eth", 67, MAX_RESERVED_MESSAGE_ID + 1, 13).unwrap();
435
436        assert_eq!(capability.name(), "eth");
437        assert_eq!(capability.version(), 67);
438        assert_eq!(
439            capability,
440            SharedCapability::Eth {
441                version: EthVersion::Eth67,
442                offset: MAX_RESERVED_MESSAGE_ID + 1
443            }
444        );
445    }
446
447    #[test]
448    fn from_eth_66() {
449        let capability = SharedCapability::new("eth", 66, MAX_RESERVED_MESSAGE_ID + 1, 15).unwrap();
450
451        assert_eq!(capability.name(), "eth");
452        assert_eq!(capability.version(), 66);
453        assert_eq!(
454            capability,
455            SharedCapability::Eth {
456                version: EthVersion::Eth66,
457                offset: MAX_RESERVED_MESSAGE_ID + 1
458            }
459        );
460    }
461
462    #[test]
463    fn capabilities_supports_eth() {
464        let capabilities: Capabilities = vec![
465            Capability::new_static("eth", 66),
466            Capability::new_static("eth", 67),
467            Capability::new_static("eth", 68),
468            Capability::new_static("eth", 69),
469            Capability::new_static("eth", 70),
470        ]
471        .into();
472
473        assert!(capabilities.supports_eth());
474        assert!(capabilities.supports_eth_v66());
475        assert!(capabilities.supports_eth_v67());
476        assert!(capabilities.supports_eth_v68());
477        assert!(capabilities.supports_eth_v69());
478        assert!(capabilities.supports_eth_v70());
479    }
480
481    #[test]
482    fn lookup_rejects_message_ids_past_last_capability() {
483        let cap = Capability::new_static("test", 1);
484        let shared =
485            SharedCapabilities::try_new(vec![Protocol::new(cap.clone(), 1)], vec![cap.clone()])
486                .unwrap();
487        let offset = shared.find(&cap).unwrap().message_id_offset();
488
489        assert_eq!(shared.find_by_offset(offset).unwrap().capability().as_ref(), &cap);
490        assert!(shared.find_by_offset(offset + 1).is_none());
491    }
492
493    #[test]
494    fn test_peer_capability_version_zero() {
495        let cap = Capability::new_static("TestName", 0);
496        let local_capabilities: Vec<Protocol> =
497            vec![Protocol::new(cap.clone(), 0), EthVersion::Eth67.into(), EthVersion::Eth68.into()];
498        let peer_capabilities = vec![cap.clone()];
499
500        let shared = shared_capability_offsets(local_capabilities, peer_capabilities).unwrap();
501        assert_eq!(shared.len(), 1);
502        assert_eq!(shared[0], SharedCapability::UnknownCapability { cap, offset: 16, messages: 0 })
503    }
504
505    #[test]
506    fn test_peer_lower_capability_version() {
507        let local_capabilities: Vec<Protocol> =
508            vec![EthVersion::Eth66.into(), EthVersion::Eth67.into(), EthVersion::Eth68.into()];
509        let peer_capabilities: Vec<Capability> = vec![EthVersion::Eth66.into()];
510
511        let shared_capability =
512            shared_capability_offsets(local_capabilities, peer_capabilities).unwrap()[0].clone();
513
514        assert_eq!(
515            shared_capability,
516            SharedCapability::Eth {
517                version: EthVersion::Eth66,
518                offset: MAX_RESERVED_MESSAGE_ID + 1
519            }
520        )
521    }
522
523    #[test]
524    fn test_peer_capability_version_too_low() {
525        let local: Vec<Protocol> = vec![EthVersion::Eth67.into()];
526        let peer_capabilities: Vec<Capability> = vec![EthVersion::Eth66.into()];
527
528        let shared_capability = shared_capability_offsets(local, peer_capabilities);
529
530        assert!(matches!(
531            shared_capability,
532            Err(P2PStreamError::HandshakeError(P2PHandshakeError::NoSharedCapabilities))
533        ))
534    }
535
536    #[test]
537    fn test_peer_capability_version_too_high() {
538        let local_capabilities = vec![EthVersion::Eth66.into()];
539        let peer_capabilities = vec![EthVersion::Eth67.into()];
540
541        let shared_capability = shared_capability_offsets(local_capabilities, peer_capabilities);
542
543        assert!(matches!(
544            shared_capability,
545            Err(P2PStreamError::HandshakeError(P2PHandshakeError::NoSharedCapabilities))
546        ))
547    }
548
549    #[test]
550    fn test_find_by_offset() {
551        let local_capabilities = vec![EthVersion::Eth66.into()];
552        let peer_capabilities = vec![EthVersion::Eth66.into()];
553
554        let shared = SharedCapabilities::try_new(local_capabilities, peer_capabilities).unwrap();
555
556        let shared_eth = shared.find_by_relative_offset(0).unwrap();
557        assert_eq!(shared_eth.name(), "eth");
558
559        let shared_eth = shared.find_by_offset(MAX_RESERVED_MESSAGE_ID + 1).unwrap();
560        assert_eq!(shared_eth.name(), "eth");
561
562        // reserved message id space
563        assert!(shared.find_by_offset(MAX_RESERVED_MESSAGE_ID).is_none());
564    }
565
566    #[test]
567    fn test_find_by_offset_many() {
568        let cap = Capability::new_static("aaa", 1);
569        let proto = Protocol::new(cap.clone(), 5);
570        let local_capabilities = vec![proto.clone(), EthVersion::Eth66.into()];
571        let peer_capabilities = vec![cap, EthVersion::Eth66.into()];
572
573        let shared = SharedCapabilities::try_new(local_capabilities, peer_capabilities).unwrap();
574
575        let shared_eth = shared.find_by_relative_offset(0).unwrap();
576        assert_eq!(shared_eth.name(), proto.cap.name);
577
578        let shared_eth = shared.find_by_offset(MAX_RESERVED_MESSAGE_ID + 1).unwrap();
579        assert_eq!(shared_eth.name(), proto.cap.name);
580
581        // the 5th shared message (0,1,2,3,4) is the last message of the aaa capability
582        let shared_eth = shared.find_by_relative_offset(4).unwrap();
583        assert_eq!(shared_eth.name(), proto.cap.name);
584        let shared_eth = shared.find_by_offset(MAX_RESERVED_MESSAGE_ID + 5).unwrap();
585        assert_eq!(shared_eth.name(), proto.cap.name);
586
587        // the 6th shared message is the first message of the eth capability
588        let shared_eth = shared.find_by_relative_offset(1 + proto.messages()).unwrap();
589        assert_eq!(shared_eth.name(), "eth");
590    }
591
592    /// Negotiates `(name, messages)` protocols with a peer that shares all of them.
593    fn negotiate_all(
594        protocols: &[(&'static str, u8)],
595    ) -> Result<Vec<SharedCapability>, P2PStreamError> {
596        let local_protocols: Vec<_> = protocols
597            .iter()
598            .map(|&(name, messages)| Protocol::new(Capability::new_static(name, 1), messages))
599            .collect();
600        let peer_capabilities = local_protocols.iter().map(|p| p.cap.clone()).collect();
601        shared_capability_offsets(local_protocols, peer_capabilities)
602    }
603
604    #[test]
605    fn shared_capability_offsets_reject_last_capability_past_message_id_space() {
606        // 0x10 + 120 + 121 ends one message id past 0xff.
607        assert!(matches!(
608            negotiate_all(&[("aaa", 120), ("bbb", 121)]),
609            Err(P2PStreamError::ParseSharedCapability(SharedCapabilityError::MessageIdOverflow))
610        ));
611    }
612
613    #[test]
614    fn shared_capability_offsets_reject_capability_after_full_message_id_space() {
615        // 0x10 + 120 + 120 uses every message id, so ccc would start at 0x100.
616        assert!(matches!(
617            negotiate_all(&[("aaa", 120), ("bbb", 120), ("ccc", 1)]),
618            Err(P2PStreamError::ParseSharedCapability(SharedCapabilityError::MessageIdOverflow))
619        ));
620    }
621
622    #[test]
623    fn shared_capability_offsets_fill_message_id_space() {
624        // 0x10 + 120 + 120 uses every message id up to and including 0xff.
625        let shared = SharedCapabilities(negotiate_all(&[("aaa", 120), ("bbb", 120)]).unwrap());
626
627        assert_eq!(shared[1].message_id_offset(), MAX_RESERVED_MESSAGE_ID + 1 + 120);
628        assert_eq!(shared.find_by_offset(u8::MAX).unwrap().name(), "bbb");
629    }
630
631    #[test]
632    fn relative_message_id_accounts_for_intermediate_capabilities() {
633        let intermediate_cap = Capability::new_static("foo", 1);
634        let intermediate = Protocol::new(intermediate_cap.clone(), 3);
635        let snap = Capability::snap_2();
636        let eth = Capability::eth_69();
637        let local_capabilities = vec![EthVersion::Eth69.into(), intermediate, Protocol::snap_2()];
638        let peer_capabilities = vec![eth, intermediate_cap, snap.clone()];
639
640        let shared = SharedCapabilities::try_new(local_capabilities, peer_capabilities).unwrap();
641        let snap_id = shared.relative_message_id(&snap, 2).unwrap();
642
643        assert_eq!(snap_id, EthMessageID::message_count(EthVersion::Eth69) + 3 + 2);
644        assert_eq!(shared.capability_message_id(&snap, snap_id), Some(2));
645    }
646
647    #[test]
648    fn capability_message_id_rejects_other_capability_range() {
649        let intermediate_cap = Capability::new_static("foo", 1);
650        let intermediate = Protocol::new(intermediate_cap.clone(), 3);
651        let snap = Capability::snap_2();
652        let local_capabilities = vec![EthVersion::Eth69.into(), intermediate, Protocol::snap_2()];
653        let peer_capabilities = vec![Capability::eth_69(), intermediate_cap.clone(), snap.clone()];
654
655        let shared = SharedCapabilities::try_new(local_capabilities, peer_capabilities).unwrap();
656        let intermediate_id = shared.relative_message_id(&intermediate_cap, 1).unwrap();
657
658        assert_eq!(shared.capability_message_id(&snap, intermediate_id), None);
659        assert_eq!(shared.relative_message_id(&snap, SnapVersion::V2.message_count()), None);
660    }
661
662    #[test]
663    fn test_raw_capability_rlp() {
664        let msg = RawCapabilityMessage { id: 1, payload: Bytes::from(vec![0x01, 0x02, 0x03]) };
665
666        // Encode the message into bytes
667        let mut encoded = Vec::new();
668        msg.encode(&mut encoded);
669
670        // Decode the bytes back into RawCapabilityMessage
671        let decoded = RawCapabilityMessage::decode(&mut &encoded[..]).unwrap();
672
673        // Verify that the decoded message matches the original
674        assert_eq!(msg, decoded);
675    }
676
677    #[test]
678    fn is_exact_eth_snap_v2_accepts_eth_and_snap() {
679        let shared = SharedCapabilities::try_new(
680            vec![EthVersion::Eth68.into(), Protocol::snap_2()],
681            vec![EthVersion::Eth68.into(), Capability::snap_2()],
682        )
683        .unwrap();
684        assert!(shared.is_exact_eth_snap_v2());
685    }
686
687    #[test]
688    fn is_exact_eth_snap_v2_rejects_eth_only() {
689        let shared = SharedCapabilities::try_new(
690            vec![EthVersion::Eth68.into()],
691            vec![EthVersion::Eth68.into()],
692        )
693        .unwrap();
694        assert!(!shared.is_exact_eth_snap_v2());
695    }
696
697    #[test]
698    fn is_exact_eth_snap_v2_rejects_eth_without_snap() {
699        // eth + a non-snap capability is not the dedicated layout.
700        let cap = Capability::new_static("les", 1);
701        let shared = SharedCapabilities::try_new(
702            vec![EthVersion::Eth68.into(), Protocol::new(cap.clone(), 5)],
703            vec![EthVersion::Eth68.into(), cap],
704        )
705        .unwrap();
706        assert!(!shared.is_exact_eth_snap_v2());
707    }
708
709    #[test]
710    fn is_exact_eth_snap_v2_rejects_eth_snap_plus_extra() {
711        // eth + snap/2 + another capability belongs on the general satellite multiplexer.
712        let cap = Capability::new_static("les", 1);
713        let shared = SharedCapabilities::try_new(
714            vec![EthVersion::Eth68.into(), Protocol::snap_2(), Protocol::new(cap.clone(), 5)],
715            vec![EthVersion::Eth68.into(), Capability::snap_2(), cap],
716        )
717        .unwrap();
718        assert!(!shared.is_exact_eth_snap_v2());
719    }
720}