1use std::time::Duration;
7
8use iroh::{endpoint, Endpoint, NodeAddr, NodeId, RelayMode, SecretKey};
9use serde::{Deserialize, Serialize};
10use thiserror::Error;
11use tokio::sync::watch;
12
13use crate::{
14 meta_addr::{MetaAddr, MetaAddrChange},
15 PeerSocketAddr,
16};
17
18mod block_sync;
19mod discovery;
20mod exchange;
21mod handler;
22mod handshake;
23mod header_sync;
24mod legacy_gossip;
25#[cfg(any(test, feature = "zakura-testkit"))]
26pub mod testkit;
27mod trace;
28pub mod transport;
29
30pub use block_sync::*;
31pub use discovery::*;
32pub use exchange::*;
33pub use handler::*;
34pub use handshake::*;
35pub use header_sync::*;
36pub use legacy_gossip::*;
37pub use trace::{
38 commit_state_trace, peer_label as zakura_trace_peer_label,
39 reject_reason_label as zakura_trace_reject_reason_label, ZakuraTrace, ZakuraTraceEvent,
40 BLOCK_SYNC_TABLE, COMMIT_STATE_TABLE, CONN_TABLE, HANDSHAKE_TABLE, HEADER_SYNC_TABLE,
41 LEGACY_REQUEST_TABLE, QUEUE_SEND_TABLE, RATELIMIT_TABLE, STREAM_TABLE,
42};
43pub use transport::*;
44
45#[cfg(any(test, feature = "zakura-testkit"))]
46pub(crate) use handler::run_native_initiator_handshake_without_trace as run_native_initiator_handshake;
47
48#[cfg(test)]
49use std::sync::{
50 atomic::{AtomicUsize, Ordering},
51 Arc,
52};
53
54pub const IROH_VERSION: &str = "0.92.0";
56
57pub const ZAKURA_CAP_LEGACY_GOSSIP: u64 = 1 << 0;
59
60pub const ZAKURA_CAP_DISCOVERY: u64 = 1 << 2;
62
63pub const ZAKURA_CAP_HEADER_SYNC: u64 = 1 << 4;
65
66pub const DEFAULT_SERVICE_MAX_PEERS: usize = 256;
72pub const DEFAULT_SERVICE_INBOUND_QUEUE_DEPTH: usize = 128;
74pub const DEFAULT_SERVICE_OUTBOUND_QUEUE_DEPTH: usize = 128;
76pub const DEFAULT_SERVICE_MAX_PENDING_ESCALATIONS: usize = 32;
78
79#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
81#[serde(deny_unknown_fields, default)]
82pub struct ServicePeerLimits {
83 pub max_inbound_peers: usize,
85 pub max_outbound_peers: usize,
87 pub inbound_queue_depth: usize,
91 pub outbound_queue_depth: usize,
95 pub max_pending_escalations: usize,
99}
100
101impl Default for ServicePeerLimits {
102 fn default() -> Self {
103 Self {
104 max_inbound_peers: DEFAULT_SERVICE_MAX_PEERS,
105 max_outbound_peers: DEFAULT_SERVICE_MAX_PEERS,
106 inbound_queue_depth: DEFAULT_SERVICE_INBOUND_QUEUE_DEPTH,
107 outbound_queue_depth: DEFAULT_SERVICE_OUTBOUND_QUEUE_DEPTH,
108 max_pending_escalations: DEFAULT_SERVICE_MAX_PENDING_ESCALATIONS,
109 }
110 }
111}
112
113#[derive(Copy, Clone, Debug, Eq, PartialEq)]
115pub enum ServiceAdmissionDecision {
116 Admit,
118 RejectFull,
120 RejectNotUseful,
122 RejectBackoff,
124 RejectUnsupported,
126}
127
128#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
130pub enum ServicePeerDirection {
131 Inbound,
133 Outbound,
135}
136
137pub type ZakuraConnId = u64;
139
140impl ServicePeerDirection {
141 pub(crate) fn trace_label(self) -> &'static str {
142 match self {
143 Self::Inbound => "inbound",
144 Self::Outbound => "outbound",
145 }
146 }
147}
148
149#[derive(Copy, Clone, Debug, Eq, PartialEq)]
151pub struct ServicePeerSnapshot {
152 pub inbound_peers: usize,
154 pub outbound_peers: usize,
156 pub inbound_slots_free: usize,
158 pub outbound_slots_free: usize,
160}
161
162impl ServicePeerSnapshot {
163 pub fn new(inbound_peers: usize, outbound_peers: usize, limits: ServicePeerLimits) -> Self {
165 Self {
166 inbound_peers,
167 outbound_peers,
168 inbound_slots_free: limits.max_inbound_peers.saturating_sub(inbound_peers),
169 outbound_slots_free: limits.max_outbound_peers.saturating_sub(outbound_peers),
170 }
171 }
172}
173
174impl Default for ServicePeerSnapshot {
175 fn default() -> Self {
176 Self::new(0, 0, ServicePeerLimits::default())
177 }
178}
179
180const ZAKURA_LIVENESS_APPEAR_TIMEOUT: Duration = Duration::from_secs(15);
188
189const ZAKURA_LIVENESS_REFRESH_INTERVAL: Duration = Duration::from_secs(45);
195
196pub fn direct_endpoint_builder(secret_key: SecretKey) -> endpoint::Builder {
201 Endpoint::builder()
202 .relay_mode(RelayMode::Disabled)
203 .clear_discovery()
204 .secret_key(secret_key)
205}
206
207#[derive(Clone, Debug, Eq, PartialEq)]
209pub enum ZakuraUpgradeOutcome {
210 Upgraded {
212 peer_id: ZakuraPeerId,
214 },
215
216 Duplicate {
218 peer_id: ZakuraPeerId,
220 },
221
222 Rejected {
224 reason: ZakuraRejectReason,
226 },
227}
228
229#[derive(Error, Debug)]
231pub enum ZakuraUpgradeError {
232 #[error("Zakura P2P v2 upgrade selected but no Zakura handshake connector is available")]
234 Unavailable,
235}
236
237#[derive(Clone, Debug, Default)]
243pub struct ZakuraHandshakeConnector {
244 endpoint: Option<ZakuraEndpoint>,
248 #[cfg(test)]
249 test_outcome: Option<(Arc<AtomicUsize>, ZakuraUpgradeOutcome)>,
250}
251
252impl ZakuraHandshakeConnector {
253 pub fn unavailable() -> Self {
256 Self {
257 endpoint: None,
258 #[cfg(test)]
259 test_outcome: None,
260 }
261 }
262
263 pub(crate) fn new_with_endpoint(endpoint: ZakuraEndpoint) -> Self {
266 Self {
267 endpoint: Some(endpoint),
268 #[cfg(test)]
269 test_outcome: None,
270 }
271 }
272
273 pub(crate) async fn local_iroh_hints(&self) -> Option<(Vec<u8>, Vec<Vec<u8>>)> {
277 let endpoint = self.endpoint.as_ref()?;
278 Some(endpoint.local_upgrade_hints().await)
279 }
280
281 pub(crate) async fn spawn_zakura_dial_to_hints_and_wait(
289 &self,
290 peer_id: &ZakuraPeerId,
291 node_id: &[u8],
292 direct_addresses: &[Vec<u8>],
293 ) -> bool {
294 let Some(endpoint) = self.endpoint.as_ref() else {
295 return false;
296 };
297 let Some(node_addr) = node_addr_from_hints(node_id, direct_addresses) else {
298 return false;
299 };
300 let mut registered = endpoint.supervisor().subscribe();
301 if !endpoint.ensure_upgrade_native_dial(node_addr) {
302 return false;
303 }
304 if wait_for_zakura_peer(&mut registered, peer_id, ZAKURA_LIVENESS_APPEAR_TIMEOUT).await {
305 return true;
306 }
307
308 if !registered.borrow().iter().any(|id| id == peer_id) {
317 endpoint.cancel_upgrade_native_dial(peer_id);
318 }
319 false
320 }
321
322 pub(crate) async fn wait_for_zakura_registration(&self, peer_id: &ZakuraPeerId) -> bool {
335 let Some(endpoint) = self.endpoint.as_ref() else {
336 return false;
337 };
338 let mut registered = endpoint.supervisor().subscribe();
339 wait_for_zakura_peer(&mut registered, peer_id, ZAKURA_LIVENESS_APPEAR_TIMEOUT).await
340 }
341
342 pub(crate) fn spawn_legacy_liveness_keeper(
358 &self,
359 peer_id: ZakuraPeerId,
360 book_addr: PeerSocketAddr,
361 address_book_updater: tokio::sync::mpsc::Sender<MetaAddrChange>,
362 ) {
363 let Some(endpoint) = self.endpoint.as_ref() else {
364 return;
365 };
366 let registered = endpoint.supervisor().subscribe();
367 tokio::spawn(run_legacy_liveness_keeper(
368 registered,
369 peer_id,
370 book_addr,
371 address_book_updater,
372 ZAKURA_LIVENESS_APPEAR_TIMEOUT,
373 ZAKURA_LIVENESS_REFRESH_INTERVAL,
374 ));
375 }
376
377 #[cfg(test)]
380 pub(crate) fn consume_test_outcome(&self) -> Option<ZakuraUpgradeOutcome> {
381 let (calls, outcome) = self.test_outcome.as_ref()?;
382 calls.fetch_add(1, Ordering::SeqCst);
383 Some(outcome.clone())
384 }
385
386 #[cfg(test)]
388 pub(crate) fn for_test(calls: Arc<AtomicUsize>, outcome: ZakuraUpgradeOutcome) -> Self {
389 Self {
390 endpoint: None,
391 test_outcome: Some((calls, outcome)),
392 }
393 }
394}
395
396fn node_addr_from_hints(node_id: &[u8], direct_addresses: &[Vec<u8>]) -> Option<NodeAddr> {
404 let node_id_bytes: [u8; 32] = node_id.try_into().ok()?;
405 let node_id = NodeId::from_bytes(&node_id_bytes).ok()?;
406
407 let direct: Vec<std::net::SocketAddr> = direct_addresses
408 .iter()
409 .filter_map(|address| std::str::from_utf8(address).ok()?.parse().ok())
410 .collect();
411
412 if direct.is_empty() {
413 return None;
414 }
415
416 Some(NodeAddr::new(node_id).with_direct_addresses(direct))
417}
418
419async fn run_legacy_liveness_keeper(
426 mut registered: watch::Receiver<Vec<ZakuraPeerId>>,
427 peer_id: ZakuraPeerId,
428 book_addr: PeerSocketAddr,
429 address_book_updater: tokio::sync::mpsc::Sender<MetaAddrChange>,
430 appear_timeout: Duration,
431 refresh_interval: Duration,
432) {
433 if !wait_for_zakura_peer(&mut registered, &peer_id, appear_timeout).await {
434 return;
435 }
436
437 loop {
438 if address_book_updater
441 .send(MetaAddr::new_responded(book_addr, None))
442 .await
443 .is_err()
444 {
445 break;
447 }
448
449 tokio::select! {
452 changed = registered.changed() => {
453 if changed.is_err() {
454 break;
455 }
456 }
457 _ = tokio::time::sleep(refresh_interval) => {}
458 }
459
460 if !registered.borrow().iter().any(|id| id == &peer_id) {
461 break;
464 }
465 }
466}
467
468async fn wait_for_zakura_peer(
471 registered: &mut watch::Receiver<Vec<ZakuraPeerId>>,
472 peer_id: &ZakuraPeerId,
473 appear_timeout: Duration,
474) -> bool {
475 tokio::time::timeout(appear_timeout, async {
476 loop {
477 if registered.borrow().iter().any(|id| id == peer_id) {
478 return true;
479 }
480 if registered.changed().await.is_err() {
481 return false;
482 }
483 }
484 })
485 .await
486 .unwrap_or(false)
487}
488
489#[cfg(test)]
490mod tests {
491 use std::{
492 error::Error,
493 net::{Ipv4Addr, SocketAddrV4},
494 };
495
496 use iroh::{
497 endpoint::Connection,
498 protocol::{AcceptError, ProtocolHandler, Router},
499 SecretKey, Watcher as _,
500 };
501
502 use super::*;
503 use crate::{CacheDir, Config};
504
505 #[derive(Debug, Clone)]
506 struct SmokeProtocolHandler;
507
508 impl ProtocolHandler for SmokeProtocolHandler {
509 async fn accept(&self, _connection: Connection) -> Result<(), AcceptError> {
510 Ok(())
511 }
512 }
513
514 #[tokio::test]
515 async fn iroh_endpoint_starts_without_relay_or_discovery() -> Result<(), Box<dyn Error>> {
516 let secret_key = SecretKey::from_bytes(&[7; 32]);
517
518 let endpoint = direct_endpoint_builder(secret_key)
519 .bind_addr_v4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0))
520 .bind()
521 .await?;
522
523 let router = Router::builder(endpoint)
524 .accept(b"/zakura/smoke/0", SmokeProtocolHandler)
525 .spawn();
526
527 let addr = router.endpoint().node_addr().initialized().await;
528
529 assert_eq!(addr.node_id, router.endpoint().node_id());
530 assert!(addr.direct_addresses().next().is_some());
531 assert!(addr.relay_url().is_none());
532 assert!(router.endpoint().discovery().is_none());
533
534 router.shutdown().await?;
535
536 Ok(())
537 }
538
539 #[test]
540 fn zakura_secret_key_path_uses_identity_dir() {
541 let config = Config {
542 cache_dir: CacheDir::custom_path("/tmp/zakura-cache"),
543 identity_dir: "/tmp/zakura-identities".into(),
544 ..Config::default()
545 };
546 let key_file =
547 crate::config::zakura_secret_key_file_path(&config.identity_dir, &config.network);
548
549 assert!(
550 !key_file.starts_with("/tmp/zakura-cache"),
551 "Zakura identity keys must not be stored under the peer cache directory",
552 );
553 assert_eq!(
554 key_file
555 .parent()
556 .and_then(|path| path.file_name())
557 .and_then(|name| name.to_str()),
558 Some("zakura-identities"),
559 "Zakura identity keys must be stored under network.identity_dir",
560 );
561 assert_eq!(
562 key_file.file_name().and_then(|name| name.to_str()),
563 Some("mainnet.zakura-iroh-secret-key"),
564 );
565 }
566
567 fn test_peer_id() -> ZakuraPeerId {
568 ZakuraPeerId::new(vec![7u8; 32]).expect("32-byte node id is within bounds")
569 }
570
571 fn test_book_addr() -> PeerSocketAddr {
572 "127.0.0.1:18233"
573 .parse::<std::net::SocketAddr>()
574 .expect("valid socket addr")
575 .into()
576 }
577
578 #[tokio::test]
581 async fn legacy_liveness_keeper_refreshes_until_deregistered() {
582 let peer_id = test_peer_id();
583 let (registered_tx, registered_rx) = watch::channel(vec![peer_id.clone()]);
584 let (updater_tx, mut updater_rx) = tokio::sync::mpsc::channel(16);
585
586 let keeper = tokio::spawn(run_legacy_liveness_keeper(
587 registered_rx,
588 peer_id.clone(),
589 test_book_addr(),
590 updater_tx,
591 Duration::from_secs(5),
592 Duration::from_millis(20),
593 ));
594
595 for _ in 0..2 {
597 let change = tokio::time::timeout(Duration::from_secs(1), updater_rx.recv())
598 .await
599 .expect("keeper refreshes liveness on a registered peer")
600 .expect("the keeper holds the sender open");
601 assert!(
602 matches!(change, MetaAddrChange::UpdateResponded { addr, .. } if addr == test_book_addr()),
603 "keeper should refresh the upgraded peer's responded liveness, got {change:?}",
604 );
605 }
606
607 registered_tx.send(Vec::new()).expect("receiver is alive");
609 tokio::time::timeout(Duration::from_secs(2), keeper)
610 .await
611 .expect("keeper exits after the peer deregisters")
612 .expect("keeper task does not panic");
613 }
614
615 #[tokio::test]
618 async fn legacy_liveness_keeper_exits_when_peer_never_registers() {
619 let peer_id = test_peer_id();
620 let (_registered_tx, registered_rx) = watch::channel(Vec::new());
621 let (updater_tx, mut updater_rx) = tokio::sync::mpsc::channel(16);
622
623 let keeper = tokio::spawn(run_legacy_liveness_keeper(
624 registered_rx,
625 peer_id,
626 test_book_addr(),
627 updater_tx,
628 Duration::from_millis(50),
629 Duration::from_millis(20),
630 ));
631
632 tokio::time::timeout(Duration::from_secs(2), keeper)
633 .await
634 .expect("keeper exits after the appear timeout")
635 .expect("keeper task does not panic");
636
637 assert!(
638 updater_rx.try_recv().is_err(),
639 "keeper must not refresh liveness for a peer that never registered",
640 );
641 }
642}