use std::time::Duration;
use iroh::{endpoint, Endpoint, NodeAddr, NodeId, RelayMode, SecretKey};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tokio::sync::watch;
use crate::{
meta_addr::{MetaAddr, MetaAddrChange},
PeerSocketAddr,
};
mod block_sync;
mod discovery;
mod exchange;
mod handler;
mod handshake;
mod header_sync;
mod legacy_gossip;
#[cfg(any(test, feature = "zakura-testkit"))]
pub mod testkit;
mod trace;
pub mod transport;
pub use block_sync::*;
pub use discovery::*;
pub use exchange::*;
pub use handler::*;
pub use handshake::*;
pub use header_sync::*;
pub use legacy_gossip::*;
pub use trace::{
commit_state_trace, peer_label as zakura_trace_peer_label,
reject_reason_label as zakura_trace_reject_reason_label, ZakuraTrace, ZakuraTraceEvent,
BLOCK_SYNC_TABLE, COMMIT_STATE_TABLE, CONN_TABLE, HANDSHAKE_TABLE, HEADER_SYNC_TABLE,
LEGACY_REQUEST_TABLE, QUEUE_SEND_TABLE, RATELIMIT_TABLE, STREAM_TABLE,
};
pub use transport::*;
#[cfg(any(test, feature = "zakura-testkit"))]
pub(crate) use handler::run_native_initiator_handshake_without_trace as run_native_initiator_handshake;
#[cfg(test)]
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,
};
pub const IROH_VERSION: &str = "0.92.0";
pub const ZAKURA_CAP_LEGACY_GOSSIP: u64 = 1 << 0;
pub const ZAKURA_CAP_DISCOVERY: u64 = 1 << 2;
pub const ZAKURA_CAP_HEADER_SYNC: u64 = 1 << 4;
pub const DEFAULT_SERVICE_MAX_PEERS: usize = 256;
pub const DEFAULT_SERVICE_INBOUND_QUEUE_DEPTH: usize = 128;
pub const DEFAULT_SERVICE_OUTBOUND_QUEUE_DEPTH: usize = 128;
pub const DEFAULT_SERVICE_MAX_PENDING_ESCALATIONS: usize = 32;
#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct ServicePeerLimits {
pub max_inbound_peers: usize,
pub max_outbound_peers: usize,
pub inbound_queue_depth: usize,
pub outbound_queue_depth: usize,
pub max_pending_escalations: usize,
}
impl Default for ServicePeerLimits {
fn default() -> Self {
Self {
max_inbound_peers: DEFAULT_SERVICE_MAX_PEERS,
max_outbound_peers: DEFAULT_SERVICE_MAX_PEERS,
inbound_queue_depth: DEFAULT_SERVICE_INBOUND_QUEUE_DEPTH,
outbound_queue_depth: DEFAULT_SERVICE_OUTBOUND_QUEUE_DEPTH,
max_pending_escalations: DEFAULT_SERVICE_MAX_PENDING_ESCALATIONS,
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ServiceAdmissionDecision {
Admit,
RejectFull,
RejectNotUseful,
RejectBackoff,
RejectUnsupported,
}
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
pub enum ServicePeerDirection {
Inbound,
Outbound,
}
pub type ZakuraConnId = u64;
impl ServicePeerDirection {
pub(crate) fn trace_label(self) -> &'static str {
match self {
Self::Inbound => "inbound",
Self::Outbound => "outbound",
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct ServicePeerSnapshot {
pub inbound_peers: usize,
pub outbound_peers: usize,
pub inbound_slots_free: usize,
pub outbound_slots_free: usize,
}
impl ServicePeerSnapshot {
pub fn new(inbound_peers: usize, outbound_peers: usize, limits: ServicePeerLimits) -> Self {
Self {
inbound_peers,
outbound_peers,
inbound_slots_free: limits.max_inbound_peers.saturating_sub(inbound_peers),
outbound_slots_free: limits.max_outbound_peers.saturating_sub(outbound_peers),
}
}
}
impl Default for ServicePeerSnapshot {
fn default() -> Self {
Self::new(0, 0, ServicePeerLimits::default())
}
}
const ZAKURA_LIVENESS_APPEAR_TIMEOUT: Duration = Duration::from_secs(15);
const ZAKURA_LIVENESS_REFRESH_INTERVAL: Duration = Duration::from_secs(45);
pub fn direct_endpoint_builder(secret_key: SecretKey) -> endpoint::Builder {
Endpoint::builder()
.relay_mode(RelayMode::Disabled)
.clear_discovery()
.secret_key(secret_key)
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ZakuraUpgradeOutcome {
Upgraded {
peer_id: ZakuraPeerId,
},
Duplicate {
peer_id: ZakuraPeerId,
},
Rejected {
reason: ZakuraRejectReason,
},
}
#[derive(Error, Debug)]
pub enum ZakuraUpgradeError {
#[error("Zakura P2P v2 upgrade selected but no Zakura handshake connector is available")]
Unavailable,
}
#[derive(Clone, Debug, Default)]
pub struct ZakuraHandshakeConnector {
endpoint: Option<ZakuraEndpoint>,
#[cfg(test)]
test_outcome: Option<(Arc<AtomicUsize>, ZakuraUpgradeOutcome)>,
}
impl ZakuraHandshakeConnector {
pub fn unavailable() -> Self {
Self {
endpoint: None,
#[cfg(test)]
test_outcome: None,
}
}
pub(crate) fn new_with_endpoint(endpoint: ZakuraEndpoint) -> Self {
Self {
endpoint: Some(endpoint),
#[cfg(test)]
test_outcome: None,
}
}
pub(crate) async fn local_iroh_hints(&self) -> Option<(Vec<u8>, Vec<Vec<u8>>)> {
let endpoint = self.endpoint.as_ref()?;
Some(endpoint.local_upgrade_hints().await)
}
pub(crate) async fn spawn_zakura_dial_to_hints_and_wait(
&self,
peer_id: &ZakuraPeerId,
node_id: &[u8],
direct_addresses: &[Vec<u8>],
) -> bool {
let Some(endpoint) = self.endpoint.as_ref() else {
return false;
};
let Some(node_addr) = node_addr_from_hints(node_id, direct_addresses) else {
return false;
};
let mut registered = endpoint.supervisor().subscribe();
if !endpoint.ensure_upgrade_native_dial(node_addr) {
return false;
}
if wait_for_zakura_peer(&mut registered, peer_id, ZAKURA_LIVENESS_APPEAR_TIMEOUT).await {
return true;
}
if !registered.borrow().iter().any(|id| id == peer_id) {
endpoint.cancel_upgrade_native_dial(peer_id);
}
false
}
pub(crate) async fn wait_for_zakura_registration(&self, peer_id: &ZakuraPeerId) -> bool {
let Some(endpoint) = self.endpoint.as_ref() else {
return false;
};
let mut registered = endpoint.supervisor().subscribe();
wait_for_zakura_peer(&mut registered, peer_id, ZAKURA_LIVENESS_APPEAR_TIMEOUT).await
}
pub(crate) fn spawn_legacy_liveness_keeper(
&self,
peer_id: ZakuraPeerId,
book_addr: PeerSocketAddr,
address_book_updater: tokio::sync::mpsc::Sender<MetaAddrChange>,
) {
let Some(endpoint) = self.endpoint.as_ref() else {
return;
};
let registered = endpoint.supervisor().subscribe();
tokio::spawn(run_legacy_liveness_keeper(
registered,
peer_id,
book_addr,
address_book_updater,
ZAKURA_LIVENESS_APPEAR_TIMEOUT,
ZAKURA_LIVENESS_REFRESH_INTERVAL,
));
}
#[cfg(test)]
pub(crate) fn consume_test_outcome(&self) -> Option<ZakuraUpgradeOutcome> {
let (calls, outcome) = self.test_outcome.as_ref()?;
calls.fetch_add(1, Ordering::SeqCst);
Some(outcome.clone())
}
#[cfg(test)]
pub(crate) fn for_test(calls: Arc<AtomicUsize>, outcome: ZakuraUpgradeOutcome) -> Self {
Self {
endpoint: None,
test_outcome: Some((calls, outcome)),
}
}
}
fn node_addr_from_hints(node_id: &[u8], direct_addresses: &[Vec<u8>]) -> Option<NodeAddr> {
let node_id_bytes: [u8; 32] = node_id.try_into().ok()?;
let node_id = NodeId::from_bytes(&node_id_bytes).ok()?;
let direct: Vec<std::net::SocketAddr> = direct_addresses
.iter()
.filter_map(|address| std::str::from_utf8(address).ok()?.parse().ok())
.collect();
if direct.is_empty() {
return None;
}
Some(NodeAddr::new(node_id).with_direct_addresses(direct))
}
async fn run_legacy_liveness_keeper(
mut registered: watch::Receiver<Vec<ZakuraPeerId>>,
peer_id: ZakuraPeerId,
book_addr: PeerSocketAddr,
address_book_updater: tokio::sync::mpsc::Sender<MetaAddrChange>,
appear_timeout: Duration,
refresh_interval: Duration,
) {
if !wait_for_zakura_peer(&mut registered, &peer_id, appear_timeout).await {
return;
}
loop {
if address_book_updater
.send(MetaAddr::new_responded(book_addr, None))
.await
.is_err()
{
break;
}
tokio::select! {
changed = registered.changed() => {
if changed.is_err() {
break;
}
}
_ = tokio::time::sleep(refresh_interval) => {}
}
if !registered.borrow().iter().any(|id| id == &peer_id) {
break;
}
}
}
async fn wait_for_zakura_peer(
registered: &mut watch::Receiver<Vec<ZakuraPeerId>>,
peer_id: &ZakuraPeerId,
appear_timeout: Duration,
) -> bool {
tokio::time::timeout(appear_timeout, async {
loop {
if registered.borrow().iter().any(|id| id == peer_id) {
return true;
}
if registered.changed().await.is_err() {
return false;
}
}
})
.await
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use std::{
error::Error,
net::{Ipv4Addr, SocketAddrV4},
};
use iroh::{
endpoint::Connection,
protocol::{AcceptError, ProtocolHandler, Router},
SecretKey, Watcher as _,
};
use super::*;
use crate::{CacheDir, Config};
#[derive(Debug, Clone)]
struct SmokeProtocolHandler;
impl ProtocolHandler for SmokeProtocolHandler {
async fn accept(&self, _connection: Connection) -> Result<(), AcceptError> {
Ok(())
}
}
#[tokio::test]
async fn iroh_endpoint_starts_without_relay_or_discovery() -> Result<(), Box<dyn Error>> {
let secret_key = SecretKey::from_bytes(&[7; 32]);
let endpoint = direct_endpoint_builder(secret_key)
.bind_addr_v4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0))
.bind()
.await?;
let router = Router::builder(endpoint)
.accept(b"/zakura/smoke/0", SmokeProtocolHandler)
.spawn();
let addr = router.endpoint().node_addr().initialized().await;
assert_eq!(addr.node_id, router.endpoint().node_id());
assert!(addr.direct_addresses().next().is_some());
assert!(addr.relay_url().is_none());
assert!(router.endpoint().discovery().is_none());
router.shutdown().await?;
Ok(())
}
#[test]
fn zakura_secret_key_path_uses_identity_dir() {
let config = Config {
cache_dir: CacheDir::custom_path("/tmp/zakura-cache"),
identity_dir: "/tmp/zakura-identities".into(),
..Config::default()
};
let key_file =
crate::config::zakura_secret_key_file_path(&config.identity_dir, &config.network);
assert!(
!key_file.starts_with("/tmp/zakura-cache"),
"Zakura identity keys must not be stored under the peer cache directory",
);
assert_eq!(
key_file
.parent()
.and_then(|path| path.file_name())
.and_then(|name| name.to_str()),
Some("zakura-identities"),
"Zakura identity keys must be stored under network.identity_dir",
);
assert_eq!(
key_file.file_name().and_then(|name| name.to_str()),
Some("mainnet.zakura-iroh-secret-key"),
);
}
fn test_peer_id() -> ZakuraPeerId {
ZakuraPeerId::new(vec![7u8; 32]).expect("32-byte node id is within bounds")
}
fn test_book_addr() -> PeerSocketAddr {
"127.0.0.1:18233"
.parse::<std::net::SocketAddr>()
.expect("valid socket addr")
.into()
}
#[tokio::test]
async fn legacy_liveness_keeper_refreshes_until_deregistered() {
let peer_id = test_peer_id();
let (registered_tx, registered_rx) = watch::channel(vec![peer_id.clone()]);
let (updater_tx, mut updater_rx) = tokio::sync::mpsc::channel(16);
let keeper = tokio::spawn(run_legacy_liveness_keeper(
registered_rx,
peer_id.clone(),
test_book_addr(),
updater_tx,
Duration::from_secs(5),
Duration::from_millis(20),
));
for _ in 0..2 {
let change = tokio::time::timeout(Duration::from_secs(1), updater_rx.recv())
.await
.expect("keeper refreshes liveness on a registered peer")
.expect("the keeper holds the sender open");
assert!(
matches!(change, MetaAddrChange::UpdateResponded { addr, .. } if addr == test_book_addr()),
"keeper should refresh the upgraded peer's responded liveness, got {change:?}",
);
}
registered_tx.send(Vec::new()).expect("receiver is alive");
tokio::time::timeout(Duration::from_secs(2), keeper)
.await
.expect("keeper exits after the peer deregisters")
.expect("keeper task does not panic");
}
#[tokio::test]
async fn legacy_liveness_keeper_exits_when_peer_never_registers() {
let peer_id = test_peer_id();
let (_registered_tx, registered_rx) = watch::channel(Vec::new());
let (updater_tx, mut updater_rx) = tokio::sync::mpsc::channel(16);
let keeper = tokio::spawn(run_legacy_liveness_keeper(
registered_rx,
peer_id,
test_book_addr(),
updater_tx,
Duration::from_millis(50),
Duration::from_millis(20),
));
tokio::time::timeout(Duration::from_secs(2), keeper)
.await
.expect("keeper exits after the appear timeout")
.expect("keeper task does not panic");
assert!(
updater_rx.try_recv().is_err(),
"keeper must not refresh liveness for a peer that never registered",
);
}
}