Skip to main content

dig_stun/
establish.rs

1//! Provenance and agreement (`SPEC.md` §7): how several untrusted readings of this node's own
2//! reflexive address combine into something worth writing into an on-chain advertisement — or,
3//! failing that, into nothing at all.
4//!
5//! [`establish`] fails closed at every branch except [`FamilyVerdict::Established`] (`SPEC.md` §7.5):
6//! a wrong establishment puts an address into a coin, permanently, with collateral behind it; a
7//! wrong non-establishment costs one epoch's rewards and is visible to the operator.
8
9use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
10
11use crate::scope::{fold_ip, scope_of_ip, Scope};
12
13/// Floor: an address needs at least this many independent source classes agreeing before it is
14/// established (`SPEC.md` §7.4) — the same floor this ecosystem calls corroboration elsewhere
15/// (`dig-node` `SPEC.md` §18.16 `CORROBORATION_FLOOR`). An assumption, not a derived constant.
16pub const MIN_INDEPENDENT_CLASSES: usize = 2;
17/// Floor when EVERY agreeing class is a `peer:*` class (`SPEC.md` §7.4): raised because two peer
18/// classes is exactly two cheap VMs in two provider blocks, and three makes a full eclipse of the
19/// requester's direct pool the only way to forge agreement.
20pub const PEER_ONLY_MIN_CLASSES: usize = 3;
21
22/// One source's report of this node's reflexive address (`SPEC.md` §7.1).
23///
24/// `#[non_exhaustive]`: an additive field is a patch release for consumers, who construct this via
25/// [`Reading::new`] rather than a struct literal.
26#[non_exhaustive]
27#[derive(Debug, Clone)]
28pub struct Reading {
29    /// The independence class of whoever reported this, rendered by `SourceClass`'s
30    /// [`Display`](std::fmt::Display) impl (i.e. `SourceClass::operator(..).to_string()`).
31    /// Two readings corroborate each other exactly when their `source` strings DIFFER and their
32    /// addresses agree — string inequality is the ONLY independence comparison this crate defines.
33    pub source: String,
34    /// Optional identity of the individual reporter (a peer_id, a resolved server address). For
35    /// diagnostics only; never consulted by [`establish`].
36    pub witness: Option<String>,
37    /// The address the source said this node appears at. The PORT is carried but never compared —
38    /// only the IP participates in agreement (`SPEC.md` §7.3).
39    pub addr: SocketAddr,
40}
41
42impl Reading {
43    /// Construct a reading with no witness.
44    pub fn new(source: impl Into<String>, addr: SocketAddr) -> Self {
45        Reading {
46            source: source.into(),
47            witness: None,
48            addr,
49        }
50    }
51
52    /// Attach a witness identity for diagnostics (never consulted by [`establish`]).
53    pub fn with_witness(mut self, witness: impl Into<String>) -> Self {
54        self.witness = Some(witness.into());
55        self
56    }
57}
58
59/// The grammar [`Reading::source`] follows (`SPEC.md` §7.2). Each variant is one independence
60/// class; two readings whose classes render the SAME string are the same class and cannot
61/// corroborate each other.
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub enum SourceClass {
64    /// A server the operator configured (a `DIG_STUN_SERVER` entry). Each configured endpoint is
65    /// its own class — the operator vouched for it specifically.
66    Operator {
67        /// The endpoint host, normalised the same way the operator's config entry was.
68        host: String,
69        /// The endpoint port.
70        port: u16,
71    },
72    /// The DIG relay's co-located STUN server. One class per relay host.
73    Relay {
74        /// The relay's host.
75        host: String,
76    },
77    /// A third-party public STUN host (`stun.l.google.com`, `stun.cloudflare.com`, …). One class
78    /// per host — they are different operators.
79    Public {
80        /// The public STUN host.
81        host: String,
82    },
83    /// A DIG peer answering over IPv4 (`SPEC.md` §6). One class per IPv4 `/16` — the SAME partition
84    /// `dig_gossip::util::ip_address::subnet_group` uses, so a consumer already holding that group
85    /// key renders the identical class from the same IP.
86    PeerV4 {
87        /// The first octet of the peer's transport address.
88        a: u8,
89        /// The second octet of the peer's transport address.
90        b: u8,
91    },
92    /// A DIG peer answering over IPv6. One class per IPv6 `/32`.
93    PeerV6 {
94        /// The first 16-bit group of the peer's transport address.
95        h0: u16,
96        /// The second 16-bit group of the peer's transport address.
97        h1: u16,
98    },
99}
100
101impl SourceClass {
102    /// The class for a server the operator configured.
103    pub fn operator(host: impl Into<String>, port: u16) -> Self {
104        SourceClass::Operator {
105            host: host.into(),
106            port,
107        }
108    }
109
110    /// The class for the DIG relay's co-located STUN server.
111    pub fn relay(host: impl Into<String>) -> Self {
112        SourceClass::Relay { host: host.into() }
113    }
114
115    /// The class for a third-party public STUN host.
116    pub fn public(host: impl Into<String>) -> Self {
117        SourceClass::Public { host: host.into() }
118    }
119
120    /// The class for a DIG peer, from the peer's TRANSPORT address `ip`. `ip` is folded per
121    /// `SPEC.md` §5.3 (`fold_ip`) before its leading bytes are taken, so a mapped or compat IPv6
122    /// peer renders the same class as its plain-IPv4 twin. A consumer that already computed
123    /// `dig_gossip::util::ip_address::subnet_group(ip)` for the same `ip` lands in the same
124    /// partition (`SPEC.md` §7.2) — this crate does not, and must not, re-implement that function.
125    pub fn peer(ip: IpAddr) -> Self {
126        match fold_ip(ip) {
127            IpAddr::V4(v4) => {
128                let o = v4.octets();
129                SourceClass::PeerV4 { a: o[0], b: o[1] }
130            }
131            IpAddr::V6(v6) => {
132                let s = v6.segments();
133                SourceClass::PeerV6 { h0: s[0], h1: s[1] }
134            }
135        }
136    }
137
138    /// Whether this class is one of the two `peer:*` forms (`SPEC.md` §7.3 step 4).
139    fn is_peer(&self) -> bool {
140        matches!(
141            self,
142            SourceClass::PeerV4 { .. } | SourceClass::PeerV6 { .. }
143        )
144    }
145
146    /// Parse a rendered class back out of the grammar. Round-trips every form the
147    /// [`Display`](std::fmt::Display) impl above produces; `None` for anything else.
148    pub fn parse(s: &str) -> Option<Self> {
149        if let Some(rest) = s.strip_prefix("operator:") {
150            let (host, port) = rest.rsplit_once(':')?;
151            return Some(SourceClass::Operator {
152                host: host.to_string(),
153                port: port.parse().ok()?,
154            });
155        }
156        if let Some(host) = s.strip_prefix("relay:") {
157            return Some(SourceClass::Relay {
158                host: host.to_string(),
159            });
160        }
161        if let Some(host) = s.strip_prefix("public:") {
162            return Some(SourceClass::Public {
163                host: host.to_string(),
164            });
165        }
166        if let Some(rest) = s.strip_prefix("peer:v4:") {
167            let (a, b) = rest.split_once('.')?;
168            return Some(SourceClass::PeerV4 {
169                a: a.parse().ok()?,
170                b: b.parse().ok()?,
171            });
172        }
173        if let Some(rest) = s.strip_prefix("peer:v6:") {
174            let (h0, h1) = rest.split_once(':')?;
175            return Some(SourceClass::PeerV6 {
176                h0: u16::from_str_radix(h0, 16).ok()?,
177                h1: u16::from_str_radix(h1, 16).ok()?,
178            });
179        }
180        None
181    }
182}
183
184impl std::fmt::Display for SourceClass {
185    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186        match self {
187            SourceClass::Operator { host, port } => write!(f, "operator:{host}:{port}"),
188            SourceClass::Relay { host } => write!(f, "relay:{host}"),
189            SourceClass::Public { host } => write!(f, "public:{host}"),
190            SourceClass::PeerV4 { a, b } => write!(f, "peer:v4:{a}.{b}"),
191            SourceClass::PeerV6 { h0, h1 } => write!(f, "peer:v6:{h0:04x}:{h1:04x}"),
192        }
193    }
194}
195
196/// The outcome for ONE address family (`SPEC.md` §7.3). Exhaustive: a new variant is a breaking
197/// change.
198#[derive(Debug, Clone, PartialEq, Eq)]
199pub enum FamilyVerdict {
200    /// No reading named an address in this family at all.
201    NoReadings,
202    /// More than one distinct IP was reported in this family — nothing is established, however
203    /// many readings agree with each other, because a single dissenting source is treated as proof
204    /// that something is wrong (`SPEC.md` §7.3 step 3).
205    Disagreement {
206        /// The distinct IPs reported, sorted and deduplicated.
207        addrs: Vec<IpAddr>,
208    },
209    /// The readings agree on one IP, but too few independent classes reported it.
210    Insufficient {
211        /// The number of distinct source classes that reported the agreed IP.
212        classes: usize,
213        /// Whether every one of those classes was a `peer:*` class (`SPEC.md` §7.3 step 4) — when
214        /// true the floor is [`PEER_ONLY_MIN_CLASSES`] rather than [`MIN_INDEPENDENT_CLASSES`].
215        peer_only: bool,
216    },
217    /// Enough independent classes agree, but the agreed IP is not [`Scope::GlobalUnicast`] — a true
218    /// reading of the node's position, and still never something to advertise to strangers.
219    NotGlobal {
220        /// The agreed IP.
221        ip: IpAddr,
222        /// Its scope.
223        scope: Scope,
224    },
225    /// The agreed IP is established: enough independent classes agree, unanimously, on a globally
226    /// routable address.
227    Established {
228        /// The established IP.
229        ip: IpAddr,
230        /// How many independent classes agreed on it.
231        classes: usize,
232    },
233}
234
235/// The result of [`establish`]: one [`FamilyVerdict`] per address family, evaluated independently
236/// (`SPEC.md` §7.3).
237#[derive(Debug, Clone, PartialEq, Eq)]
238pub struct Established {
239    /// The IPv6 family's verdict.
240    pub ipv6: FamilyVerdict,
241    /// The IPv4 family's verdict.
242    pub ipv4: FamilyVerdict,
243}
244
245impl Established {
246    /// The established IPv6 address, or `None` for any verdict other than
247    /// [`FamilyVerdict::Established`].
248    pub fn ipv6_addr(&self) -> Option<Ipv6Addr> {
249        match &self.ipv6 {
250            FamilyVerdict::Established {
251                ip: IpAddr::V6(v6), ..
252            } => Some(*v6),
253            _ => None,
254        }
255    }
256
257    /// The established IPv4 address, or `None` for any verdict other than
258    /// [`FamilyVerdict::Established`].
259    pub fn ipv4_addr(&self) -> Option<Ipv4Addr> {
260        match &self.ipv4 {
261            FamilyVerdict::Established {
262                ip: IpAddr::V4(v4), ..
263            } => Some(*v4),
264            _ => None,
265        }
266    }
267}
268
269/// Combine untrusted `readings` of this node's own reflexive address into an [`Established`]
270/// verdict per address family (`SPEC.md` §7.3).
271///
272/// A reading belongs to the IPv4 family when its address is IPv4, or folds to IPv4 under `SPEC.md`
273/// §5.3 (`fold_ip`); otherwise IPv6. Non-answers — a timeout, an [`crate::observe::Refusal`], an
274/// RPC method-not-found, a parse error — are NOT readings and MUST NOT be passed here: they neither
275/// agree nor dissent.
276pub fn establish(readings: &[Reading]) -> Established {
277    let mut ipv4: Vec<(IpAddr, &str)> = Vec::new();
278    let mut ipv6: Vec<(IpAddr, &str)> = Vec::new();
279    for reading in readings {
280        let folded = fold_ip(reading.addr.ip());
281        let bucket = match folded {
282            IpAddr::V4(_) => &mut ipv4,
283            IpAddr::V6(_) => &mut ipv6,
284        };
285        bucket.push((folded, reading.source.as_str()));
286    }
287
288    Established {
289        ipv4: verdict_for_family(&ipv4),
290        ipv6: verdict_for_family(&ipv6),
291    }
292}
293
294/// The five-step decision of `SPEC.md` §7.3, applied to the readings of ONE address family.
295fn verdict_for_family(readings: &[(IpAddr, &str)]) -> FamilyVerdict {
296    if readings.is_empty() {
297        return FamilyVerdict::NoReadings;
298    }
299
300    let mut addrs: Vec<IpAddr> = readings.iter().map(|(ip, _)| *ip).collect();
301    addrs.sort();
302    addrs.dedup();
303    if addrs.len() > 1 {
304        return FamilyVerdict::Disagreement { addrs };
305    }
306    let ip = addrs[0];
307
308    let mut classes: Vec<&str> = readings.iter().map(|(_, source)| *source).collect();
309    classes.sort_unstable();
310    classes.dedup();
311    let peer_only = classes
312        .iter()
313        .all(|s| SourceClass::parse(s).map(|c| c.is_peer()).unwrap_or(false));
314
315    if classes.len() < MIN_INDEPENDENT_CLASSES {
316        return FamilyVerdict::Insufficient {
317            classes: classes.len(),
318            peer_only,
319        };
320    }
321    if peer_only && classes.len() < PEER_ONLY_MIN_CLASSES {
322        return FamilyVerdict::Insufficient {
323            classes: classes.len(),
324            peer_only: true,
325        };
326    }
327
328    let scope = scope_of_ip(ip);
329    if scope != Scope::GlobalUnicast {
330        return FamilyVerdict::NotGlobal { ip, scope };
331    }
332
333    FamilyVerdict::Established {
334        ip,
335        classes: classes.len(),
336    }
337}