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}