Skip to main content

rns_net/common/
management.rs

1//! Remote management destinations for the Reticulum transport node.
2//!
3//! Implements the server-side handlers for:
4//! - `/status` on `rnstransport.remote.management` destination
5//! - `/path` on `rnstransport.remote.management` destination
6//! - `/list` on `rnstransport.info.blackhole` destination
7//!
8//! Python reference: Transport.py:220-241, 2591-2643, 3243-3249
9
10use rns_core::constants;
11use rns_core::destination::destination_hash;
12use rns_core::hash::truncated_hash;
13use rns_core::msgpack::{self, Value};
14use rns_core::transport::types::{InterfaceId, InterfaceInfo};
15use rns_core::transport::TransportEngine;
16
17use super::interface_stats::InterfaceStats;
18use super::time;
19
20/// View trait for interface status, decoupling management from InterfaceEntry.
21pub trait InterfaceStatusView {
22    fn id(&self) -> InterfaceId;
23    fn info(&self) -> &InterfaceInfo;
24    fn online(&self) -> bool;
25    fn stats(&self) -> &InterfaceStats;
26    fn tx_drops(&self) -> u64 {
27        0
28    }
29    fn tx_dropped_bytes(&self) -> u64 {
30        0
31    }
32    fn tx_stalled(&self) -> bool {
33        false
34    }
35    fn tx_buffered(&self) -> usize {
36        0
37    }
38}
39
40/// Get the path hash for "/status".
41pub fn status_path_hash() -> [u8; 16] {
42    truncated_hash(b"/status")
43}
44
45/// Get the path hash for "/path".
46pub fn path_path_hash() -> [u8; 16] {
47    truncated_hash(b"/path")
48}
49
50/// Get the path hash for "/list".
51pub fn list_path_hash() -> [u8; 16] {
52    truncated_hash(b"/list")
53}
54
55/// Check if a path hash matches a known management path.
56pub fn is_management_path(path_hash: &[u8; 16]) -> bool {
57    *path_hash == status_path_hash()
58        || *path_hash == path_path_hash()
59        || *path_hash == list_path_hash()
60}
61
62/// Compute the remote management destination hash.
63///
64/// Destination: `rnstransport.remote.management` with transport identity.
65pub fn management_dest_hash(transport_identity_hash: &[u8; 16]) -> [u8; 16] {
66    destination_hash(
67        "rnstransport",
68        &["remote", "management"],
69        Some(transport_identity_hash),
70    )
71}
72
73/// Compute the blackhole info destination hash.
74///
75/// Destination: `rnstransport.info.blackhole` with transport identity.
76pub fn blackhole_dest_hash(transport_identity_hash: &[u8; 16]) -> [u8; 16] {
77    destination_hash(
78        "rnstransport",
79        &["info", "blackhole"],
80        Some(transport_identity_hash),
81    )
82}
83
84/// Compute the probe responder destination hash.
85///
86/// Destination: `rnstransport.probe` with transport identity.
87pub fn probe_dest_hash(transport_identity_hash: &[u8; 16]) -> [u8; 16] {
88    destination_hash("rnstransport", &["probe"], Some(transport_identity_hash))
89}
90
91/// Build the 10-byte random blob carried by an announce.
92///
93/// Matches Python `Destination.announce()`: 5 random bytes followed by the
94/// emission time in whole seconds, big-endian, truncated to 5 bytes. Path
95/// update rules compare this emission time, so it must be a real timestamp.
96pub fn announce_random_blob(rng: &mut dyn rns_crypto::Rng, now: f64) -> [u8; 10] {
97    let mut random_hash = [0u8; 10];
98    rng.fill_bytes(&mut random_hash[..5]);
99    let now_secs = if now.is_finite() && now > 0.0 {
100        now as u64
101    } else {
102        0
103    };
104    random_hash[5..10].copy_from_slice(&now_secs.to_be_bytes()[3..8]);
105    random_hash
106}
107
108/// Packet context for a built-in announce: `PATH_RESPONSE` when it answers
109/// a path request, as Python `Destination.announce(path_response=True)`.
110fn announce_context(path_response: bool) -> u8 {
111    if path_response {
112        constants::CONTEXT_PATH_RESPONSE
113    } else {
114        constants::CONTEXT_NONE
115    }
116}
117
118/// A destination this node announces under its transport identity and
119/// answers itself (`Transport.mgmt_destinations`).
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub enum BuiltinDestination {
122    /// `rnstransport.remote.management`
123    RemoteManagement,
124    /// `rnstransport.info.blackhole`
125    Blackhole,
126    /// `rnstransport.probe`
127    Probe,
128}
129
130impl BuiltinDestination {
131    pub const ALL: [BuiltinDestination; 3] = [
132        BuiltinDestination::RemoteManagement,
133        BuiltinDestination::Blackhole,
134        BuiltinDestination::Probe,
135    ];
136
137    pub fn aspects(self) -> &'static [&'static str] {
138        match self {
139            BuiltinDestination::RemoteManagement => &["remote", "management"],
140            BuiltinDestination::Blackhole => &["info", "blackhole"],
141            BuiltinDestination::Probe => &["probe"],
142        }
143    }
144
145    pub fn dest_hash(self, transport_identity_hash: &[u8; 16]) -> [u8; 16] {
146        destination_hash(
147            "rnstransport",
148            self.aspects(),
149            Some(transport_identity_hash),
150        )
151    }
152
153    pub fn name_hash(self) -> [u8; 10] {
154        rns_core::destination::name_hash("rnstransport", self.aspects())
155    }
156
157    /// The destination as the stamper sees it, for grinding its stamp.
158    pub fn destination(self, transport_identity_hash: [u8; 16]) -> crate::destination::Destination {
159        crate::destination::Destination::single_in(
160            "rnstransport",
161            self.aspects(),
162            rns_core::types::IdentityHash(transport_identity_hash),
163        )
164    }
165
166    pub fn label(self) -> &'static str {
167        match self {
168            BuiltinDestination::RemoteManagement => "management destination",
169            BuiltinDestination::Blackhole => "blackhole info",
170            BuiltinDestination::Probe => "probe responder",
171        }
172    }
173}
174
175/// Build an announce packet for a built-in destination, with `app_data`
176/// (the stamp extension, when destinations are stamped) appended.
177///
178/// Returns raw packet bytes ready for `engine.handle_outbound()`.
179pub fn build_builtin_announce(
180    destination: BuiltinDestination,
181    identity: &rns_crypto::identity::Identity,
182    rng: &mut dyn rns_crypto::Rng,
183    now: f64,
184    path_response: bool,
185    app_data: Option<&[u8]>,
186) -> Option<Vec<u8>> {
187    let dest_hash = destination.dest_hash(identity.hash());
188    let name_hash = destination.name_hash();
189    let random_hash = announce_random_blob(rng, now);
190
191    let (announce_data, _has_ratchet) = rns_core::announce::AnnounceData::pack(
192        identity,
193        &dest_hash,
194        &name_hash,
195        &random_hash,
196        None, // no ratchet
197        app_data,
198    )
199    .ok()?;
200
201    let flags = rns_core::packet::PacketFlags {
202        header_type: constants::HEADER_1,
203        context_flag: constants::FLAG_UNSET,
204        transport_type: constants::TRANSPORT_BROADCAST,
205        destination_type: constants::DESTINATION_SINGLE,
206        packet_type: constants::PACKET_TYPE_ANNOUNCE,
207    };
208
209    let packet = rns_core::packet::RawPacket::pack(
210        flags,
211        0,
212        &dest_hash,
213        None,
214        announce_context(path_response),
215        &announce_data,
216    )
217    .ok()?;
218
219    Some(packet.raw)
220}
221
222/// Build an unstamped announce packet for the probe responder destination.
223pub fn build_probe_announce(
224    identity: &rns_crypto::identity::Identity,
225    rng: &mut dyn rns_crypto::Rng,
226    now: f64,
227    path_response: bool,
228) -> Option<Vec<u8>> {
229    build_builtin_announce(
230        BuiltinDestination::Probe,
231        identity,
232        rng,
233        now,
234        path_response,
235        None,
236    )
237}
238
239/// Management configuration.
240#[derive(Debug, Clone, Default)]
241pub struct ManagementConfig {
242    /// Enable remote management destination.
243    pub enable_remote_management: bool,
244    /// Identity hashes allowed to query management.
245    pub remote_management_allowed: Vec<[u8; 16]>,
246    /// Enable blackhole list publication.
247    pub publish_blackhole: bool,
248}
249
250/// Handle a `/status` request.
251///
252/// Request data: msgpack([include_lstats, include_profiling]) where both are bool.
253/// Response: msgpack([interface_stats_dict, link_count?, profiling_results?])
254/// matching Python format.
255pub fn handle_status_request(
256    data: &[u8],
257    engine: &TransportEngine,
258    interfaces: &[&dyn InterfaceStatusView],
259    started: f64,
260    probe_responder_hash: Option<[u8; 16]>,
261) -> Option<Vec<u8>> {
262    // Decode request data
263    let (include_lstats, include_profiling) = match msgpack::unpack_exact(data) {
264        Ok(Value::Array(arr)) if !arr.is_empty() => (
265            arr[0].as_bool().unwrap_or(false),
266            arr.get(1).and_then(Value::as_bool).unwrap_or(false),
267        ),
268        _ => (false, false),
269    };
270
271    // Build interface stats
272    let mut iface_list = Vec::new();
273    let mut total_rxb: u64 = 0;
274    let mut total_txb: u64 = 0;
275    let mut total_rxpps = 0.0;
276    let mut total_txpps = 0.0;
277    let mut total_rx_packets: u64 = 0;
278    let mut total_tx_packets: u64 = 0;
279    let mut total_announce_rx: u64 = 0;
280    let mut total_announce_tx: u64 = 0;
281    let mut total_path_request_rx: u64 = 0;
282    let mut total_path_request_tx: u64 = 0;
283    let mut total_arxb: u64 = 0;
284    let mut total_atxb: u64 = 0;
285    let mut total_arxc: u64 = 0;
286    let mut total_atxc: u64 = 0;
287    let mut total_prxb: u64 = 0;
288    let mut total_ptxb: u64 = 0;
289    let mut total_prxc: u64 = 0;
290    let mut total_ptxc: u64 = 0;
291    let mut total_rxs = 0.0;
292    let mut total_txs = 0.0;
293    let mut total_arxs = 0.0;
294    let mut total_atxs = 0.0;
295    let mut total_prxs = 0.0;
296    let mut total_ptxs = 0.0;
297    let mut total_arxf = 0.0;
298    let mut total_atxf = 0.0;
299    let mut total_prxf = 0.0;
300    let mut total_ptxf = 0.0;
301
302    for entry in interfaces {
303        let id = entry.id();
304        let info = entry.info();
305        let stats = entry.stats();
306
307        if !info.is_local_client {
308            total_rxb += stats.rxb;
309            total_txb += stats.txb;
310            total_rxpps += stats.traffic_rates.rxpps;
311            total_txpps += stats.traffic_rates.txpps;
312            total_rx_packets += stats.rx_packets;
313            total_tx_packets += stats.tx_packets;
314            total_announce_rx += stats.arxc;
315            total_announce_tx += stats.atxc;
316            total_path_request_rx += stats.prxc;
317            total_path_request_tx += stats.ptxc;
318            total_arxb += stats.arxb;
319            total_atxb += stats.atxb;
320            total_arxc += stats.arxc;
321            total_atxc += stats.atxc;
322            total_prxb += stats.prxb;
323            total_ptxb += stats.ptxb;
324            total_prxc += stats.prxc;
325            total_ptxc += stats.ptxc;
326            total_rxs += stats.traffic_rates.rxs;
327            total_txs += stats.traffic_rates.txs;
328            total_arxs += stats.traffic_rates.arxs;
329            total_atxs += stats.traffic_rates.atxs;
330            total_prxs += stats.traffic_rates.prxs;
331            total_ptxs += stats.traffic_rates.ptxs;
332            total_arxf += stats.incoming_announce_freq();
333            total_atxf += stats.outgoing_announce_freq();
334            total_prxf += stats.incoming_path_request_freq();
335            total_ptxf += stats.outgoing_path_request_freq();
336        }
337
338        let mut ifstats: Vec<(&str, Value)> = vec![
339            ("name", Value::Str(info.name.clone())),
340            ("short_name", Value::Str(info.name.clone())),
341            ("status", Value::Bool(entry.online())),
342            ("mode", Value::UInt(info.mode as u64)),
343            ("rxb", Value::UInt(stats.rxb)),
344            ("txb", Value::UInt(stats.txb)),
345            ("txdrp", Value::UInt(entry.tx_drops())),
346            ("txdrb", Value::UInt(entry.tx_dropped_bytes())),
347            ("txstalled", Value::Bool(entry.tx_stalled())),
348            ("txbuffered", Value::UInt(entry.tx_buffered() as u64)),
349            ("rxs", Value::Float(stats.traffic_rates.rxs)),
350            ("txs", Value::Float(stats.traffic_rates.txs)),
351            ("arxb", Value::UInt(stats.arxb)),
352            ("atxb", Value::UInt(stats.atxb)),
353            ("arxc", Value::UInt(stats.arxc)),
354            ("atxc", Value::UInt(stats.atxc)),
355            ("arxs", Value::Float(stats.traffic_rates.arxs)),
356            ("atxs", Value::Float(stats.traffic_rates.atxs)),
357            ("prxb", Value::UInt(stats.prxb)),
358            ("ptxb", Value::UInt(stats.ptxb)),
359            ("prxc", Value::UInt(stats.prxc)),
360            ("ptxc", Value::UInt(stats.ptxc)),
361            ("prxs", Value::Float(stats.traffic_rates.prxs)),
362            ("ptxs", Value::Float(stats.traffic_rates.ptxs)),
363            (
364                "protocol_violations",
365                Value::UInt(stats.protocol_violations),
366            ),
367            ("ifac_violations", Value::UInt(stats.ifac_violations)),
368            ("packet_filter_hits", Value::UInt(stats.packet_filter_hits)),
369            ("hop_limiter_hits", Value::UInt(stats.hop_limiter_hits)),
370            (
371                "stamped_path_requests",
372                Value::UInt(stats.stamped_path_requests),
373            ),
374        ];
375        if let Some(br) = info.bitrate {
376            ifstats.push(("bitrate", Value::UInt(br)));
377        } else {
378            ifstats.push(("bitrate", Value::Nil));
379        }
380        ifstats.push((
381            "incoming_announce_freq",
382            Value::Float(stats.incoming_announce_freq()),
383        ));
384        ifstats.push((
385            "outgoing_announce_freq",
386            Value::Float(stats.outgoing_announce_freq()),
387        ));
388        ifstats.push((
389            "held_announces",
390            Value::UInt(engine.held_announce_count(&id) as u64),
391        ));
392        if let Some(rssi) = stats.rssi {
393            ifstats.push(("rssi", Value::Int(i64::from(rssi))));
394        }
395        if let Some(snr) = stats.snr {
396            ifstats.push(("snr", Value::Float(f64::from(snr))));
397        }
398        if let Some(quality) = stats.signal_quality {
399            ifstats.push(("signal_quality", Value::Float(quality)));
400        }
401        if let Some(frequency) = stats.frequency {
402            ifstats.push(("frequency", Value::UInt(u64::from(frequency))));
403        }
404        if let Some(bandwidth) = stats.bandwidth {
405            ifstats.push(("bandwidth", Value::UInt(u64::from(bandwidth))));
406        }
407        if let Some(txpower) = stats.txpower {
408            ifstats.push(("txpower", Value::Int(i64::from(txpower))));
409        }
410        if let Some(sf) = stats.spreading_factor {
411            ifstats.push(("sf", Value::UInt(u64::from(sf))));
412        }
413        if let Some(cr) = stats.coding_rate {
414            ifstats.push(("cr", Value::UInt(u64::from(cr))));
415        }
416        if let Some(channel) = stats.channel {
417            ifstats.push(("channel", Value::UInt(u64::from(channel))));
418        }
419        if let Some(modulation) = stats.modulation.as_ref() {
420            ifstats.push(("modulation", Value::Str(modulation.clone())));
421        }
422
423        // IFAC info
424        ifstats.push(("ifac_signature", Value::Nil));
425        ifstats.push((
426            "ifac_size",
427            if info.bitrate.is_some() {
428                Value::UInt(0)
429            } else {
430                Value::Nil
431            },
432        ));
433        ifstats.push(("ifac_netname", Value::Nil));
434
435        // Unused by Rust but expected by Python clients
436        ifstats.push(("clients", Value::Nil));
437        ifstats.push(("announce_queue", Value::Nil));
438
439        // Build as map
440        let map = ifstats
441            .into_iter()
442            .map(|(k, v)| (Value::Str(k.into()), v))
443            .collect();
444        iface_list.push(Value::Map(map));
445    }
446
447    // Build top-level stats dict
448    let mut stats: Vec<(&str, Value)> = vec![
449        ("interfaces", Value::Array(iface_list)),
450        ("rxb", Value::UInt(total_rxb)),
451        ("txb", Value::UInt(total_txb)),
452        ("rxpps", Value::Float(total_rxpps)),
453        ("txpps", Value::Float(total_txpps)),
454        ("rxpkts", Value::UInt(total_rx_packets)),
455        ("txpkts", Value::UInt(total_tx_packets)),
456        ("arxpkts", Value::UInt(total_announce_rx)),
457        ("atxpkts", Value::UInt(total_announce_tx)),
458        ("prxpkts", Value::UInt(total_path_request_rx)),
459        ("ptxpkts", Value::UInt(total_path_request_tx)),
460        ("rxs", Value::Float(total_rxs)),
461        ("txs", Value::Float(total_txs)),
462        ("arxb", Value::UInt(total_arxb)),
463        ("atxb", Value::UInt(total_atxb)),
464        ("arxc", Value::UInt(total_arxc)),
465        ("atxc", Value::UInt(total_atxc)),
466        ("arxs", Value::Float(total_arxs)),
467        ("atxs", Value::Float(total_atxs)),
468        ("prxb", Value::UInt(total_prxb)),
469        ("ptxb", Value::UInt(total_ptxb)),
470        ("prxc", Value::UInt(total_prxc)),
471        ("ptxc", Value::UInt(total_ptxc)),
472        ("prxs", Value::Float(total_prxs)),
473        ("ptxs", Value::Float(total_ptxs)),
474        ("arxf", Value::Float(total_arxf)),
475        ("atxf", Value::Float(total_atxf)),
476        ("prxf", Value::Float(total_prxf)),
477        ("ptxf", Value::Float(total_ptxf)),
478    ];
479
480    let (path_pool_total, path_pool_unused) = engine.path_pool_counts();
481    let total_links = engine.link_table_count();
482    let active_links = engine.active_link_count();
483    stats.push(("tlinks", Value::UInt(total_links as u64)));
484    stats.push(("alinks", Value::UInt(active_links as u64)));
485    stats.push((
486        "plinks",
487        Value::UInt(total_links.saturating_sub(active_links) as u64),
488    ));
489    stats.push(("tpcount", Value::UInt(path_pool_total as u64)));
490    stats.push(("unupcount", Value::UInt(path_pool_unused as u64)));
491    stats.push((
492        "actpcount",
493        Value::UInt(path_pool_total.saturating_sub(path_pool_unused) as u64),
494    ));
495
496    if let Some(identity_hash) = engine.config().identity_hash {
497        stats.push(("transport_id", Value::Bin(identity_hash.to_vec())));
498        stats.push(("transport_uptime", Value::Float(time::now() - started)));
499    }
500    stats.push((
501        "probe_responder",
502        match probe_responder_hash {
503            Some(hash) => Value::Bin(hash.to_vec()),
504            None => Value::Nil,
505        },
506    ));
507    stats.push(("rss", Value::Nil));
508
509    let stats_map = stats
510        .into_iter()
511        .map(|(k, v)| (Value::Str(k.into()), v))
512        .collect();
513
514    // Build response: [stats_dict] or [stats_dict, link_count]
515    let mut response = vec![Value::Map(stats_map)];
516    if include_lstats {
517        let link_count = engine.link_table_count();
518        response.push(Value::UInt(link_count as u64));
519    }
520    if include_profiling {
521        response.push(crate::profiling::results_msgpack());
522    }
523
524    Some(msgpack::pack(&Value::Array(response)))
525}
526
527/// Handle a `/path` request.
528///
529/// Request data: msgpack([command, destination_hash?, max_hops?])
530/// - command = "table" → returns path table entries
531/// - command = "rates" → returns rate table entries
532pub fn handle_path_request(data: &[u8], engine: &TransportEngine) -> Option<Vec<u8>> {
533    let arr = match msgpack::unpack_exact(data) {
534        Ok(Value::Array(arr)) if !arr.is_empty() => arr,
535        _ => return None,
536    };
537
538    let command = match &arr[0] {
539        Value::Str(s) => s.as_str(),
540        _ => return None,
541    };
542
543    let dest_filter: Option<[u8; 16]> = if arr.len() > 1 {
544        match &arr[1] {
545            Value::Bin(b) if b.len() == 16 => {
546                let mut h = [0u8; 16];
547                h.copy_from_slice(b);
548                Some(h)
549            }
550            _ => None,
551        }
552    } else {
553        None
554    };
555
556    let max_hops: Option<u8> = if arr.len() > 2 {
557        arr[2].as_uint().map(|v| v as u8)
558    } else {
559        None
560    };
561
562    match command {
563        "table" => {
564            let paths = engine.get_path_table(max_hops);
565            let mut entries = Vec::new();
566            for p in &paths {
567                if let Some(ref filter) = dest_filter {
568                    if p.0 != *filter {
569                        continue;
570                    }
571                }
572                // p = (dest_hash, timestamp, next_hop, hops, expires, interface)
573                let entry = vec![
574                    (Value::Str("hash".into()), Value::Bin(p.0.to_vec())),
575                    (Value::Str("timestamp".into()), Value::Float(p.1)),
576                    (Value::Str("via".into()), Value::Bin(p.2.to_vec())),
577                    (Value::Str("hops".into()), Value::UInt(p.3 as u64)),
578                    (Value::Str("expires".into()), Value::Float(p.4)),
579                    (Value::Str("used".into()), Value::Float(p.5)),
580                    (Value::Str("interface".into()), Value::Str(p.6.clone())),
581                ];
582                entries.push(Value::Map(entry));
583            }
584            Some(msgpack::pack(&Value::Array(entries)))
585        }
586        "rates" => {
587            let rates = engine.get_rate_table();
588            let mut entries = Vec::new();
589            for r in &rates {
590                if let Some(ref filter) = dest_filter {
591                    if r.0 != *filter {
592                        continue;
593                    }
594                }
595                // r = (dest_hash, last, rate_violations, blocked_until, timestamps)
596                let timestamps: Vec<Value> = r.4.iter().map(|t| Value::Float(*t)).collect();
597                let entry = vec![
598                    (Value::Str("hash".into()), Value::Bin(r.0.to_vec())),
599                    (Value::Str("last".into()), Value::Float(r.1)),
600                    (
601                        Value::Str("rate_violations".into()),
602                        Value::UInt(r.2 as u64),
603                    ),
604                    (Value::Str("blocked_until".into()), Value::Float(r.3)),
605                    (Value::Str("timestamps".into()), Value::Array(timestamps)),
606                ];
607                entries.push(Value::Map(entry));
608            }
609            Some(msgpack::pack(&Value::Array(entries)))
610        }
611        _ => None,
612    }
613}
614
615/// Handle a `/list` (blackhole list) request.
616///
617/// Returns the blackholed_identities dict as msgpack.
618pub fn handle_blackhole_list_request(engine: &TransportEngine) -> Option<Vec<u8>> {
619    let blackholed = engine.get_blackholed();
620    let mut map_entries = Vec::new();
621    for (hash, created, expires, reason) in &blackholed {
622        let mut entry = vec![
623            (Value::Str("created".into()), Value::Float(*created)),
624            (Value::Str("expires".into()), Value::Float(*expires)),
625        ];
626        if let Some(r) = reason {
627            entry.push((Value::Str("reason".into()), Value::Str(r.clone())));
628        }
629        map_entries.push((Value::Bin(hash.to_vec()), Value::Map(entry)));
630    }
631    Some(msgpack::pack(&Value::Map(map_entries)))
632}
633
634/// Build an unstamped announce packet for the management destination.
635pub fn build_management_announce(
636    identity: &rns_crypto::identity::Identity,
637    rng: &mut dyn rns_crypto::Rng,
638    now: f64,
639    path_response: bool,
640) -> Option<Vec<u8>> {
641    build_builtin_announce(
642        BuiltinDestination::RemoteManagement,
643        identity,
644        rng,
645        now,
646        path_response,
647        None,
648    )
649}
650
651/// Build an unstamped announce packet for the blackhole info destination.
652pub fn build_blackhole_announce(
653    identity: &rns_crypto::identity::Identity,
654    rng: &mut dyn rns_crypto::Rng,
655    now: f64,
656    path_response: bool,
657) -> Option<Vec<u8>> {
658    build_builtin_announce(
659        BuiltinDestination::Blackhole,
660        identity,
661        rng,
662        now,
663        path_response,
664        None,
665    )
666}