Skip to main content

dig_peer_protocol/
node_type.rs

1//! [`NodeType`] — the service role a peer declares when it registers.
2//!
3//! ## Why this is not `chia_protocol::NodeType`
4//!
5//! `NodeType` travels inside [`RegisterPeer`](crate::RegisterPeer), a **DIG** message on the DIG
6//! opcode band (218). A field of a DIG message is part of the DIG wire, so DIG owns it; sourcing
7//! it from `chia-protocol` made a chia version bump a change to a DIG message body.
8//!
9//! ## The discriminants are the wire, and they are frozen
10//!
11//! Each variant's value IS its encoded byte — the encoding below is a single byte, nothing more.
12//! The values match what DIG peers are exchanging today and MUST NOT be renumbered:
13//! `tests/golden_wire_vectors.rs` pins `FullNode` and `Introducer` as absolute hex inside a real
14//! `RegisterPeer` body, so a renumbering fails there rather than silently re-labelling every
15//! registered peer on the network.
16//!
17//! Variants exist for roles DIG itself never registers as (a harvester does not join the DIG
18//! gossip network). They are kept so that a peer speaking a fuller node vocabulary round-trips
19//! rather than being rejected, and so the numbering can never be reused for something else.
20
21use std::io::Cursor;
22
23use chia_sha2::Sha256;
24use chia_traits::{Error, Result, Streamable};
25
26/// The service role a peer declares. One byte on the wire.
27#[repr(u8)]
28#[derive(Hash, Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord)]
29pub enum NodeType {
30    /// A full node. DIG gossip peers register as this.
31    FullNode = 1,
32    /// A harvester.
33    Harvester = 2,
34    /// A farmer.
35    Farmer = 3,
36    /// A timelord.
37    Timelord = 4,
38    /// An introducer — the peer-discovery role DIG registers against.
39    Introducer = 5,
40    /// A wallet.
41    Wallet = 6,
42    /// A data-layer node.
43    DataLayer = 7,
44}
45
46impl NodeType {
47    /// Every variant, in discriminant order.
48    ///
49    /// Exists so tests and exhaustiveness checks enumerate the real set rather than a transcribed
50    /// list that could drift from the enum.
51    pub const ALL: [Self; 7] = [
52        Self::FullNode,
53        Self::Harvester,
54        Self::Farmer,
55        Self::Timelord,
56        Self::Introducer,
57        Self::Wallet,
58        Self::DataLayer,
59    ];
60
61    /// The single byte this role occupies on the wire.
62    #[must_use]
63    pub fn to_byte(self) -> u8 {
64        self as u8
65    }
66
67    /// The role a wire byte names, or `None` when the byte names no role.
68    ///
69    /// Unknown bytes are refused rather than mapped to a default: a peer declaring a role this
70    /// build cannot interpret must surface as an error, never silently become a full node.
71    #[must_use]
72    pub fn from_byte(byte: u8) -> Option<Self> {
73        Self::ALL.into_iter().find(|role| role.to_byte() == byte)
74    }
75}
76
77impl TryFrom<u8> for NodeType {
78    type Error = UnknownNodeType;
79
80    fn try_from(byte: u8) -> std::result::Result<Self, UnknownNodeType> {
81        Self::from_byte(byte).ok_or(UnknownNodeType(byte))
82    }
83}
84
85/// A wire byte that names no [`NodeType`].
86#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
87#[error("{0} is not a known node type")]
88pub struct UnknownNodeType(pub u8);
89
90impl Streamable for NodeType {
91    fn update_digest(&self, digest: &mut Sha256) {
92        digest.update([self.to_byte()]);
93    }
94
95    fn stream(&self, out: &mut Vec<u8>) -> Result<()> {
96        out.push(self.to_byte());
97        Ok(())
98    }
99
100    fn parse<const TRUSTED: bool>(input: &mut Cursor<&[u8]>) -> Result<Self> {
101        let byte = u8::parse::<TRUSTED>(input)?;
102        Self::from_byte(byte).ok_or(Error::InvalidEnum)
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    /// The discriminants ARE the wire format, pinned as absolute values. This is the test that
111    /// fails if anyone renumbers the enum, which is a network-wide event and never a refactor.
112    #[test]
113    fn discriminants_are_frozen_at_their_wire_values() {
114        assert_eq!(NodeType::FullNode.to_byte(), 1);
115        assert_eq!(NodeType::Harvester.to_byte(), 2);
116        assert_eq!(NodeType::Farmer.to_byte(), 3);
117        assert_eq!(NodeType::Timelord.to_byte(), 4);
118        assert_eq!(NodeType::Introducer.to_byte(), 5);
119        assert_eq!(NodeType::Wallet.to_byte(), 6);
120        assert_eq!(NodeType::DataLayer.to_byte(), 7);
121    }
122
123    /// Streaming emits exactly the discriminant and nothing else — no length prefix, no padding.
124    /// Driven over every variant so a single hard-coded byte cannot pass.
125    #[test]
126    fn streams_as_exactly_one_byte_for_every_variant() {
127        for role in NodeType::ALL {
128            assert_eq!(role.to_bytes().expect("encode"), vec![role.to_byte()]);
129        }
130    }
131
132    #[test]
133    fn every_variant_round_trips_through_parse() {
134        for role in NodeType::ALL {
135            let decoded = NodeType::from_bytes(&role.to_bytes().expect("encode")).expect("decode");
136            assert_eq!(decoded, role);
137        }
138    }
139
140    /// Both ends of the valid range plus a mid-range gap-free sweep: every byte that is NOT a
141    /// discriminant must be refused. `0` matters specifically — a zero byte is what a truncated
142    /// or zero-filled buffer produces, and mapping it to a role would make corruption look like
143    /// a valid registration.
144    #[test]
145    fn a_byte_naming_no_role_is_refused_rather_than_defaulted() {
146        for byte in 0..=u8::MAX {
147            let is_known = (1..=7).contains(&byte);
148            assert_eq!(
149                NodeType::from_byte(byte).is_some(),
150                is_known,
151                "byte {byte} disagreed with the known-role set"
152            );
153            assert_eq!(NodeType::from_bytes(&[byte]).is_ok(), is_known);
154        }
155    }
156
157    #[test]
158    fn try_from_reports_the_offending_byte() {
159        assert_eq!(NodeType::try_from(5), Ok(NodeType::Introducer));
160        assert_eq!(NodeType::try_from(0), Err(UnknownNodeType(0)));
161        assert_eq!(NodeType::try_from(8), Err(UnknownNodeType(8)));
162        assert_eq!(UnknownNodeType(9).to_string(), "9 is not a known node type");
163    }
164
165    /// `ALL` must actually contain every variant, or the sweep above silently narrows.
166    #[test]
167    fn all_covers_the_whole_enum() {
168        assert_eq!(NodeType::ALL.len(), 7);
169        let mut bytes: Vec<u8> = NodeType::ALL.iter().map(|r| r.to_byte()).collect();
170        bytes.sort_unstable();
171        bytes.dedup();
172        assert_eq!(bytes, vec![1, 2, 3, 4, 5, 6, 7]);
173    }
174}