use std::{
cmp::min,
fmt,
future::Future,
net::{Ipv4Addr, SocketAddr},
panic,
pin::Pin,
sync::Arc,
task::{Context, Poll},
time::Duration,
};
use chrono::{TimeZone, Utc};
use futures::{channel::oneshot, future, pin_mut, FutureExt, SinkExt, StreamExt};
use indexmap::IndexSet;
use rand::{rngs::OsRng, RngCore};
use tokio::{
io::{AsyncRead, AsyncWrite},
sync::broadcast,
task::JoinError,
time::{error, timeout, Instant},
};
use tokio_stream::wrappers::IntervalStream;
use tokio_util::codec::Framed;
use tower::Service;
use tracing::{span, Level, Span};
use tracing_futures::Instrument;
use zakura_chain::{
block,
chain_tip::{ChainTip, NoChainTip},
parameters::Network,
serialization::{DateTime32, SerializationError},
};
use crate::{
constants,
meta_addr::MetaAddrChange,
peer::{
CancelHeartbeatTask, Client, ClientRequest, Connection, ErrorSlot, HandshakeError,
MinimumPeerVersion, PeerError,
},
peer_set::{ConnectionTracker, InventoryChange},
protocol::{
external::{types::*, AddrInVersion, Codec, InventoryHash, Message},
internal::{Request, Response},
},
types::MetaAddr,
zakura::{
P2pV2Upgrade, P2pV2UpgradeAccept, P2pV2UpgradeInit, P2pV2UpgradeReject,
ZakuraHandshakeConfig, ZakuraLegacyNonces, ZakuraPeerId, ZakuraProtocolError,
P2P_V2_UPGRADE_COMMAND, PRELUDE_MAGIC,
},
zakura::{ZakuraHandshakeConnector, ZakuraRejectReason, ZakuraUpgradeOutcome},
BoxError, Config, PeerSocketAddr, VersionMessage,
};
#[cfg(test)]
mod tests;
pub struct Handshake<S, C = NoChainTip>
where
S: Service<Request, Response = Response, Error = BoxError> + Clone + Send + 'static,
S::Future: Send,
C: ChainTip + Clone + Send + 'static,
{
config: Config,
user_agent: String,
our_services: PeerServices,
relay: bool,
inbound_service: S,
address_book_updater: tokio::sync::mpsc::Sender<MetaAddrChange>,
inv_collector: broadcast::Sender<InventoryChange>,
minimum_peer_version: MinimumPeerVersion<C>,
nonces: Arc<futures::lock::Mutex<IndexSet<Nonce>>>,
zakura_handshake_connector: Option<ZakuraHandshakeConnector>,
parent_span: Span,
}
impl<S, C> fmt::Debug for Handshake<S, C>
where
S: Service<Request, Response = Response, Error = BoxError> + Clone + Send + 'static,
S::Future: Send,
C: ChainTip + Clone + Send + 'static,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct(std::any::type_name::<Handshake<S, C>>())
.field("config", &self.config)
.field("user_agent", &self.user_agent)
.field("our_services", &self.our_services)
.field("relay", &self.relay)
.field("minimum_peer_version", &self.minimum_peer_version)
.field(
"zakura_handshake_connector",
&self.zakura_handshake_connector,
)
.field("parent_span", &self.parent_span)
.finish()
}
}
impl<S, C> Clone for Handshake<S, C>
where
S: Service<Request, Response = Response, Error = BoxError> + Clone + Send + 'static,
S::Future: Send,
C: ChainTip + Clone + Send + 'static,
{
fn clone(&self) -> Self {
Self {
config: self.config.clone(),
user_agent: self.user_agent.clone(),
our_services: self.our_services,
relay: self.relay,
inbound_service: self.inbound_service.clone(),
address_book_updater: self.address_book_updater.clone(),
inv_collector: self.inv_collector.clone(),
minimum_peer_version: self.minimum_peer_version.clone(),
nonces: self.nonces.clone(),
zakura_handshake_connector: self.zakura_handshake_connector.clone(),
parent_span: self.parent_span.clone(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ConnectionInfo {
pub connected_addr: ConnectedAddr,
pub remote: VersionMessage,
pub local: VersionMessage,
pub negotiated_version: Version,
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum ConnectedAddr {
OutboundDirect {
addr: PeerSocketAddr,
},
InboundDirect {
addr: PeerSocketAddr,
},
OutboundProxy {
proxy_addr: SocketAddr,
transient_local_addr: SocketAddr,
},
InboundProxy {
transient_addr: SocketAddr,
},
Isolated,
}
pub fn get_unspecified_ipv4_addr(network: Network) -> SocketAddr {
(Ipv4Addr::UNSPECIFIED, network.default_port()).into()
}
use ConnectedAddr::*;
impl ConnectedAddr {
pub fn new_outbound_direct(addr: PeerSocketAddr) -> ConnectedAddr {
OutboundDirect { addr }
}
pub fn new_inbound_direct(addr: PeerSocketAddr) -> ConnectedAddr {
InboundDirect { addr }
}
#[allow(unused)]
pub fn new_outbound_proxy(proxy: SocketAddr, local_addr: SocketAddr) -> ConnectedAddr {
OutboundProxy {
proxy_addr: proxy,
transient_local_addr: local_addr,
}
}
#[allow(unused)]
pub fn new_inbound_proxy(proxy: SocketAddr) -> ConnectedAddr {
InboundProxy {
transient_addr: proxy,
}
}
pub fn new_isolated() -> ConnectedAddr {
Isolated
}
pub fn get_address_book_addr(&self) -> Option<PeerSocketAddr> {
match self {
OutboundDirect { addr } | InboundDirect { addr } => Some(*addr),
OutboundProxy { .. } | InboundProxy { .. } | Isolated => None,
}
}
pub fn get_transient_addr(&self) -> Option<PeerSocketAddr> {
match self {
OutboundDirect { addr } => Some(*addr),
InboundDirect { addr } => Some(*addr),
OutboundProxy {
transient_local_addr,
..
} => Some(PeerSocketAddr::from(*transient_local_addr)),
InboundProxy { transient_addr } => Some(PeerSocketAddr::from(*transient_addr)),
Isolated => None,
}
}
pub fn get_transient_addr_label(&self) -> String {
self.get_transient_addr()
.map_or_else(|| "isolated".to_string(), |addr| addr.to_string())
}
pub fn get_short_kind_label(&self) -> &'static str {
match self {
OutboundDirect { .. } => "Out",
InboundDirect { .. } => "In",
OutboundProxy { .. } => "ProxOut",
InboundProxy { .. } => "ProxIn",
Isolated => "Isol",
}
}
pub fn get_alternate_addrs(
&self,
mut canonical_remote: PeerSocketAddr,
) -> impl Iterator<Item = PeerSocketAddr> {
let addrs = match self {
OutboundDirect { addr } => {
if canonical_remote.ip().is_unspecified() {
canonical_remote.set_ip(addr.ip());
}
if canonical_remote.port() == 0 {
canonical_remote.set_port(addr.port());
}
if &canonical_remote != addr {
vec![canonical_remote]
} else {
Vec::new()
}
}
InboundDirect { addr } => {
let maybe_addr = SocketAddr::new(addr.ip(), canonical_remote.port()).into();
if canonical_remote != maybe_addr {
vec![canonical_remote, maybe_addr]
} else {
vec![canonical_remote]
}
}
OutboundProxy { .. } | InboundProxy { .. } => vec![canonical_remote],
Isolated => Vec::new(),
};
addrs.into_iter()
}
pub fn is_inbound(&self) -> bool {
matches!(self, InboundDirect { .. } | InboundProxy { .. })
}
}
impl fmt::Debug for ConnectedAddr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let kind = self.get_short_kind_label();
let addr = self.get_transient_addr_label();
if matches!(self, Isolated) {
f.write_str(kind)
} else {
f.debug_tuple(kind).field(&addr).finish()
}
}
}
pub struct Builder<S, C = NoChainTip>
where
S: Service<Request, Response = Response, Error = BoxError> + Clone + Send + 'static,
S::Future: Send,
C: ChainTip + Clone + Send + 'static,
{
config: Option<Config>,
our_services: Option<PeerServices>,
user_agent: Option<String>,
relay: Option<bool>,
inbound_service: Option<S>,
address_book_updater: Option<tokio::sync::mpsc::Sender<MetaAddrChange>>,
inv_collector: Option<broadcast::Sender<InventoryChange>>,
zakura_handshake_connector: Option<ZakuraHandshakeConnector>,
latest_chain_tip: C,
}
impl<S, C> Builder<S, C>
where
S: Service<Request, Response = Response, Error = BoxError> + Clone + Send + 'static,
S::Future: Send,
C: ChainTip + Clone + Send + 'static,
{
pub fn with_config(mut self, config: Config) -> Self {
self.config = Some(config);
self
}
pub fn with_inbound_service(mut self, inbound_service: S) -> Self {
self.inbound_service = Some(inbound_service);
self
}
pub fn with_inventory_collector(
mut self,
inv_collector: broadcast::Sender<InventoryChange>,
) -> Self {
self.inv_collector = Some(inv_collector);
self
}
pub fn with_address_book_updater(
mut self,
address_book_updater: tokio::sync::mpsc::Sender<MetaAddrChange>,
) -> Self {
self.address_book_updater = Some(address_book_updater);
self
}
pub fn with_advertised_services(mut self, services: PeerServices) -> Self {
self.our_services = Some(services);
self
}
pub fn with_user_agent(mut self, user_agent: String) -> Self {
self.user_agent = Some(user_agent);
self
}
pub fn with_latest_chain_tip<NewC>(self, latest_chain_tip: NewC) -> Builder<S, NewC>
where
NewC: ChainTip + Clone + Send + 'static,
{
Builder {
latest_chain_tip,
config: self.config,
inbound_service: self.inbound_service,
address_book_updater: self.address_book_updater,
our_services: self.our_services,
user_agent: self.user_agent,
relay: self.relay,
inv_collector: self.inv_collector,
zakura_handshake_connector: self.zakura_handshake_connector,
}
}
pub fn with_zakura_handshake_connector(
mut self,
zakura_handshake_connector: ZakuraHandshakeConnector,
) -> Self {
self.zakura_handshake_connector = Some(zakura_handshake_connector);
self
}
pub fn want_transactions(mut self, relay: bool) -> Self {
self.relay = Some(relay);
self
}
pub fn finish(self) -> Result<Handshake<S, C>, &'static str> {
let config = self.config.ok_or("did not specify config")?;
let inbound_service = self
.inbound_service
.ok_or("did not specify inbound service")?;
let inv_collector = self.inv_collector.unwrap_or_else(|| {
let (tx, _) = broadcast::channel(100);
tx
});
let address_book_updater = self.address_book_updater.unwrap_or_else(|| {
let (tx, _rx) = tokio::sync::mpsc::channel(1);
tx
});
let nonces = Arc::new(futures::lock::Mutex::new(IndexSet::new()));
let user_agent = configured_user_agent(&config, self.user_agent.unwrap_or_default());
let our_services = configured_advertised_services(
&config,
self.our_services.unwrap_or_else(PeerServices::empty),
);
let relay = self.relay.unwrap_or(false);
let network = config.network.clone();
let minimum_peer_version = MinimumPeerVersion::new(self.latest_chain_tip, &network);
Ok(Handshake {
config,
user_agent,
our_services,
relay,
inbound_service,
address_book_updater,
inv_collector,
minimum_peer_version,
nonces,
zakura_handshake_connector: self.zakura_handshake_connector,
parent_span: Span::current(),
})
}
}
impl<S> Handshake<S, NoChainTip>
where
S: Service<Request, Response = Response, Error = BoxError> + Clone + Send + 'static,
S::Future: Send,
{
pub fn builder() -> Builder<S, NoChainTip> {
Builder {
config: None,
our_services: None,
user_agent: None,
relay: None,
inbound_service: None,
address_book_updater: None,
inv_collector: None,
zakura_handshake_connector: None,
latest_chain_tip: NoChainTip,
}
}
}
fn configured_advertised_services(config: &Config, mut services: PeerServices) -> PeerServices {
services.remove(PeerServices::NODE_P2P_V2);
if config.v2_p2p() {
services |= PeerServices::NODE_P2P_V2;
}
services
}
fn configured_user_agent(config: &Config, user_agent: String) -> String {
if !config.v2_p2p() {
return user_agent;
}
let zakura_token = format!("Zakura:{}", env!("CARGO_PKG_VERSION"));
let trimmed_user_agent = user_agent.trim_matches('/');
if trimmed_user_agent.is_empty() {
format!("/{zakura_token}/")
} else if trimmed_user_agent
.split('/')
.any(|token| token == zakura_token)
{
format!("/{trimmed_user_agent}/")
} else {
format!("/{zakura_token}/{trimmed_user_agent}/")
}
}
fn should_attempt_zakura_upgrade(config: &Config, connection_info: &ConnectionInfo) -> bool {
config.v2_p2p()
&& connection_info
.remote
.services
.contains(PeerServices::NODE_P2P_V2)
}
#[allow(clippy::too_many_arguments)]
pub async fn negotiate_version<PeerTransport>(
peer_conn: &mut Framed<PeerTransport, Codec>,
connected_addr: &ConnectedAddr,
config: Config,
nonces: Arc<futures::lock::Mutex<IndexSet<Nonce>>>,
user_agent: String,
our_services: PeerServices,
relay: bool,
mut minimum_peer_version: MinimumPeerVersion<impl ChainTip>,
) -> Result<Arc<ConnectionInfo>, HandshakeError>
where
PeerTransport: AsyncRead + AsyncWrite + Unpin + Send + 'static,
{
let local_nonce = Nonce::default();
{
let mut locked_nonces = nonces.lock().await;
let is_unique_nonce = locked_nonces.insert(local_nonce);
if !is_unique_nonce {
return Err(HandshakeError::LocalDuplicateNonce);
}
while locked_nonces.len() > config.peerset_total_connection_limit() {
locked_nonces.shift_remove_index(0);
}
std::mem::drop(locked_nonces);
}
let now = Utc::now().timestamp();
let timestamp = Utc
.timestamp_opt(now - now.rem_euclid(5 * 60), 0)
.single()
.expect("in-range number of seconds and valid nanosecond");
let (their_addr, our_services, our_listen_addr) = match connected_addr {
Isolated => {
let unspec_ipv4 = get_unspecified_ipv4_addr(config.network);
(unspec_ipv4.into(), PeerServices::empty(), unspec_ipv4)
}
_ => {
let their_addr = connected_addr
.get_transient_addr()
.expect("non-Isolated connections have a remote addr");
let advertise_addr = match config.external_addr {
Some(external_addr) => {
info!(?their_addr, ?config.listen_addr, "using external address for Version messages");
external_addr
}
None => config.listen_addr,
};
(their_addr, our_services, advertise_addr)
}
};
let start_height = minimum_peer_version
.chain_tip()
.best_tip_height()
.unwrap_or(block::Height(0));
let our_version = VersionMessage {
version: constants::CURRENT_NETWORK_PROTOCOL_VERSION,
services: our_services,
timestamp,
address_recv: AddrInVersion::new(their_addr, PeerServices::NODE_NETWORK),
address_from: AddrInVersion::new(our_listen_addr, our_services),
nonce: local_nonce,
user_agent: user_agent.clone(),
start_height,
relay,
};
debug!(?our_version, "sending initial version message");
if our_services.contains(PeerServices::NODE_P2P_V2) {
metrics::counter!("zakura.p2p.handshake.service_bit.advertised").increment(1);
}
peer_conn.send(our_version.clone().into()).await?;
let mut remote_msg = peer_conn
.next()
.await
.ok_or(HandshakeError::ConnectionClosed)??;
let remote: VersionMessage = loop {
match remote_msg {
Message::Version(version_message) => {
debug!(?version_message, "got version message from remote peer");
break version_message;
}
_ => {
remote_msg = peer_conn
.next()
.await
.ok_or(HandshakeError::ConnectionClosed)??;
debug!(?remote_msg, "ignoring non-version message from remote peer");
}
}
};
let remote_address_services = remote.address_from.untrusted_services();
if remote_address_services != remote.services {
info!(
?remote.services,
?remote_address_services,
?remote.user_agent,
"peer with inconsistent version services and version address services",
);
}
if remote.services.contains(PeerServices::NODE_P2P_V2) {
metrics::counter!("zakura.p2p.handshake.service_bit.remote").increment(1);
}
let nonce_reuse = nonces.lock().await.contains(&remote.nonce);
if nonce_reuse {
info!(?connected_addr, "rejecting self-connection attempt");
Err(HandshakeError::RemoteNonceReuse)?;
}
let min_version = minimum_peer_version.current();
if remote.version < min_version {
debug!(
remote_ip = ?their_addr,
?remote.version,
?min_version,
?remote.user_agent,
"disconnecting from peer with obsolete network protocol version",
);
metrics::counter!(
"zcash.net.peers.obsolete",
"remote_ip" => their_addr.to_string(),
"remote_version" => remote.version.to_string(),
"min_version" => min_version.to_string(),
"user_agent" => remote.user_agent.clone(),
)
.increment(1);
metrics::gauge!(
"zcash.net.peers.version.obsolete",
"remote_ip" => their_addr.to_string(),
)
.set(remote.version.0 as f64);
return Err(HandshakeError::ObsoleteVersion(remote.version));
}
let negotiated_version = min(constants::CURRENT_NETWORK_PROTOCOL_VERSION, remote.version);
let connection_info = Arc::new(ConnectionInfo {
connected_addr: *connected_addr,
remote,
local: our_version,
negotiated_version,
});
debug!(
remote_ip = ?their_addr,
?connection_info.remote.version,
?negotiated_version,
?min_version,
?connection_info.remote.user_agent,
"negotiated network protocol version with peer",
);
metrics::counter!(
"zcash.net.peers.connected",
"remote_ip" => their_addr.to_string(),
"remote_version" => connection_info.remote.version.to_string(),
"negotiated_version" => negotiated_version.to_string(),
"min_version" => min_version.to_string(),
"user_agent" => connection_info.remote.user_agent.clone(),
)
.increment(1);
metrics::gauge!(
"zcash.net.peers.version.connected",
"remote_ip" => their_addr.to_string(),
)
.set(connection_info.remote.version.0 as f64);
peer_conn.send(Message::Verack).await?;
let mut remote_msg = peer_conn
.next()
.await
.ok_or(HandshakeError::ConnectionClosed)??;
loop {
match remote_msg {
Message::Verack => {
debug!(?remote_msg, "got verack message from remote peer");
break;
}
_ => {
remote_msg = peer_conn
.next()
.await
.ok_or(HandshakeError::ConnectionClosed)??;
debug!(?remote_msg, "ignoring non-verack message from remote peer");
}
}
}
Ok(connection_info)
}
async fn upgrade_to_zakura_handshake<PeerTransport>(
peer_conn: &mut Framed<PeerTransport, Codec>,
connection_info: &ConnectionInfo,
connected_addr: ConnectedAddr,
network: &Network,
dev_network: Option<&str>,
zakura_handshake_connector: Option<ZakuraHandshakeConnector>,
address_book_updater: &tokio::sync::mpsc::Sender<MetaAddrChange>,
) -> Result<ZakuraUpgradeOutcome, HandshakeError>
where
PeerTransport: AsyncRead + AsyncWrite + Unpin + Send + 'static,
{
let Some(connector) = zakura_handshake_connector else {
metrics::counter!("zakura.p2p.handshake.upgrade.error").increment(1);
return Err(HandshakeError::ZakuraUpgrade(
crate::zakura::ZakuraUpgradeError::Unavailable,
));
};
#[cfg(test)]
if let Some(outcome) = connector.consume_test_outcome() {
return Ok(outcome);
}
let Some((local_node_id, local_direct_addresses)) = connector.local_iroh_hints().await else {
return Ok(neutral_upgrade_fallback());
};
let config = ZakuraHandshakeConfig::for_network_with_dev_cohort(network, dev_network);
let nonces = ZakuraLegacyNonces {
local_zebra_nonce: connection_info.local.nonce,
remote_zebra_nonce: connection_info.remote.nonce,
};
let outcome = if connected_addr.is_inbound() {
run_responder_upgrade(
peer_conn,
&connector,
&config,
nonces,
local_node_id,
local_direct_addresses,
)
.await?
} else {
run_initiator_upgrade(
peer_conn,
&connector,
&config,
nonces,
local_node_id,
local_direct_addresses,
)
.await?
};
match &outcome {
ZakuraUpgradeOutcome::Upgraded { peer_id } => {
info!(
?connected_addr,
?peer_id,
remote_services = ?connection_info.remote.services,
"upgraded mutually P2P-v2-capable peer to Zakura",
);
}
ZakuraUpgradeOutcome::Duplicate { peer_id } => {
info!(
?connected_addr,
?peer_id,
"closing duplicate Zakura peer neutrally"
);
metrics::counter!("zakura.p2p.handshake.duplicate").increment(1);
}
ZakuraUpgradeOutcome::Rejected { reason } => {
debug!(
?connected_addr,
?reason,
"Zakura upgrade not completed; continuing on the legacy connection",
);
metrics::counter!(
"zakura.p2p.upgrade.prelude.rejected",
"reason" => format!("{reason:?}"),
"network" => config.network_label(),
)
.increment(1);
}
}
if let ZakuraUpgradeOutcome::Upgraded { peer_id }
| ZakuraUpgradeOutcome::Duplicate { peer_id } = &outcome
{
if !connected_addr.is_inbound() {
if let Some(book_addr) = connected_addr.get_address_book_addr() {
connector.spawn_legacy_liveness_keeper(
peer_id.clone(),
book_addr,
address_book_updater.clone(),
);
}
}
}
Ok(outcome)
}
fn neutral_upgrade_fallback() -> ZakuraUpgradeOutcome {
ZakuraUpgradeOutcome::Rejected {
reason: ZakuraRejectReason::TemporaryUnavailable,
}
}
async fn run_initiator_upgrade<PeerTransport>(
peer_conn: &mut Framed<PeerTransport, Codec>,
connector: &ZakuraHandshakeConnector,
config: &ZakuraHandshakeConfig,
nonces: ZakuraLegacyNonces,
local_node_id: Vec<u8>,
local_direct_addresses: Vec<Vec<u8>>,
) -> Result<ZakuraUpgradeOutcome, HandshakeError>
where
PeerTransport: AsyncRead + AsyncWrite + Unpin + Send + 'static,
{
let mut upgrade_nonce = [0u8; 32];
OsRng.fill_bytes(&mut upgrade_nonce);
let init = P2pV2UpgradeInit {
magic: PRELUDE_MAGIC,
prelude_version: config.prelude_version,
zakura_protocol_min: config.zakura_protocol_min,
zakura_protocol_max: config.zakura_protocol_max,
network_id: config.network_id,
chain_id: config.chain_id,
capabilities: config.supported_capabilities,
local_zebra_nonce: nonces.local_zebra_nonce,
remote_zebra_nonce: nonces.remote_zebra_nonce,
upgrade_nonce,
iroh_node_id: local_node_id,
iroh_direct_addresses: local_direct_addresses,
iroh_relay_hint: None,
max_control_frame_bytes: config.max_control_frame_bytes,
max_open_streams: config.max_open_streams,
};
let Ok(init_bytes) = P2pV2Upgrade::Init(init.clone()).encode() else {
return Ok(neutral_upgrade_fallback());
};
peer_conn.send(Message::P2pV2Upgrade(init_bytes)).await?;
let Some(P2pV2Upgrade::Accept(accept)) = read_upgrade_prelude(peer_conn).await? else {
return Ok(neutral_upgrade_fallback());
};
if accept.validate(config, nonces, &init).is_err() {
return Ok(neutral_upgrade_fallback());
}
let Ok(peer_id) = ZakuraPeerId::new(accept.iroh_node_id.clone()) else {
return Ok(neutral_upgrade_fallback());
};
if !connector
.spawn_zakura_dial_to_hints_and_wait(
&peer_id,
&accept.iroh_node_id,
&accept.iroh_direct_addresses,
)
.await
{
return Ok(neutral_upgrade_fallback());
}
Ok(ZakuraUpgradeOutcome::Upgraded { peer_id })
}
async fn run_responder_upgrade<PeerTransport>(
peer_conn: &mut Framed<PeerTransport, Codec>,
connector: &ZakuraHandshakeConnector,
config: &ZakuraHandshakeConfig,
nonces: ZakuraLegacyNonces,
local_node_id: Vec<u8>,
local_direct_addresses: Vec<Vec<u8>>,
) -> Result<ZakuraUpgradeOutcome, HandshakeError>
where
PeerTransport: AsyncRead + AsyncWrite + Unpin + Send + 'static,
{
let Some(P2pV2Upgrade::Init(init)) = read_upgrade_prelude(peer_conn).await? else {
send_upgrade_reject(peer_conn, config).await?;
return Ok(neutral_upgrade_fallback());
};
let selected_zakura_protocol = match init.validate(config, nonces) {
Ok(selected) => selected,
Err(_) => {
send_upgrade_reject(peer_conn, config).await?;
return Ok(neutral_upgrade_fallback());
}
};
let mut responder_upgrade_nonce = [0u8; 32];
OsRng.fill_bytes(&mut responder_upgrade_nonce);
let accept = P2pV2UpgradeAccept {
magic: PRELUDE_MAGIC,
prelude_version: config.prelude_version,
selected_zakura_protocol,
network_id: config.network_id,
chain_id: config.chain_id,
capabilities: config.supported_capabilities,
initiator_upgrade_nonce: init.upgrade_nonce,
responder_upgrade_nonce,
local_zebra_nonce: nonces.local_zebra_nonce,
remote_zebra_nonce: nonces.remote_zebra_nonce,
iroh_node_id: local_node_id,
iroh_direct_addresses: local_direct_addresses,
iroh_relay_hint: None,
max_control_frame_bytes: config.max_control_frame_bytes,
max_open_streams: config.max_open_streams,
};
let Ok(accept_bytes) = P2pV2Upgrade::Accept(accept).encode() else {
return Ok(neutral_upgrade_fallback());
};
peer_conn.send(Message::P2pV2Upgrade(accept_bytes)).await?;
let Ok(peer_id) = ZakuraPeerId::new(init.iroh_node_id.clone()) else {
return Ok(neutral_upgrade_fallback());
};
if !connector.wait_for_zakura_registration(&peer_id).await {
return Ok(neutral_upgrade_fallback());
}
Ok(ZakuraUpgradeOutcome::Upgraded { peer_id })
}
async fn send_upgrade_reject<PeerTransport>(
peer_conn: &mut Framed<PeerTransport, Codec>,
config: &ZakuraHandshakeConfig,
) -> Result<(), HandshakeError>
where
PeerTransport: AsyncRead + AsyncWrite + Unpin + Send + 'static,
{
let reject = P2pV2UpgradeReject {
magic: PRELUDE_MAGIC,
prelude_version: config.prelude_version,
reason: ZakuraRejectReason::TemporaryUnavailable,
};
if let Ok(reject_bytes) = P2pV2Upgrade::Reject(reject).encode() {
peer_conn.send(Message::P2pV2Upgrade(reject_bytes)).await?;
}
Ok(())
}
fn decode_upgrade_prelude(payload: &[u8]) -> Result<P2pV2Upgrade, HandshakeError> {
decode_upgrade_prelude_with(|| P2pV2Upgrade::decode(payload))
}
fn decode_upgrade_prelude_with(
decode: impl FnOnce() -> Result<P2pV2Upgrade, ZakuraProtocolError> + panic::UnwindSafe,
) -> Result<P2pV2Upgrade, HandshakeError> {
match panic::catch_unwind(decode) {
Ok(Ok(prelude)) => Ok(prelude),
Ok(Err(error)) => {
metrics::counter!("zakura.p2p.upgrade.prelude.malformed").increment(1);
Err(HandshakeError::ZakuraUpgradePreludeMalformed(error))
}
Err(_panic_payload) => {
metrics::counter!(
"peer.message.parse.panics",
"parser" => "upgrade_prelude",
)
.increment(1);
tracing::error!(
command = P2P_V2_UPGRADE_COMMAND,
"peer-controlled message parser panicked; disconnecting legacy peer"
);
Err(SerializationError::Parse("Zakura P2P v2 upgrade prelude parser panicked").into())
}
}
}
async fn read_upgrade_prelude<PeerTransport>(
peer_conn: &mut Framed<PeerTransport, Codec>,
) -> Result<Option<P2pV2Upgrade>, HandshakeError>
where
PeerTransport: AsyncRead + AsyncWrite + Unpin + Send + 'static,
{
const MAX_SKIPPED_MESSAGES: usize = 4;
for _ in 0..MAX_SKIPPED_MESSAGES {
let message = peer_conn
.next()
.await
.ok_or(HandshakeError::ConnectionClosed)??;
if let Message::P2pV2Upgrade(payload) = message {
return decode_upgrade_prelude(&payload).map(Some);
}
}
Ok(None)
}
pub struct HandshakeRequest<PeerTransport>
where
PeerTransport: AsyncRead + AsyncWrite + Unpin + Send + 'static,
{
pub data_stream: PeerTransport,
pub connected_addr: ConnectedAddr,
pub connection_tracker: ConnectionTracker,
}
impl<S, PeerTransport, C> Service<HandshakeRequest<PeerTransport>> for Handshake<S, C>
where
S: Service<Request, Response = Response, Error = BoxError> + Clone + Send + 'static,
S::Future: Send,
C: ChainTip + Clone + Send + 'static,
PeerTransport: AsyncRead + AsyncWrite + Unpin + Send + 'static,
{
type Response = Client;
type Error = BoxError;
type Future =
Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: HandshakeRequest<PeerTransport>) -> Self::Future {
let HandshakeRequest {
data_stream,
connected_addr,
connection_tracker,
} = req;
let negotiator_span = debug_span!("negotiator", peer = ?connected_addr);
let connection_span =
span!(parent: &self.parent_span, Level::INFO, "", peer = ?connected_addr);
let nonces = self.nonces.clone();
let inbound_service = self.inbound_service.clone();
let address_book_updater = self.address_book_updater.clone();
let inv_collector = self.inv_collector.clone();
let config = self.config.clone();
let user_agent = self.user_agent.clone();
let our_services = self.our_services;
let relay = self.relay;
let minimum_peer_version = self.minimum_peer_version.clone();
let zakura_handshake_connector = self.zakura_handshake_connector.clone();
let fut = async move {
debug!(
addr = ?connected_addr,
"negotiating protocol version with remote peer"
);
let handshake_start = Instant::now();
let mut peer_conn = Framed::new(
data_stream,
Codec::builder()
.for_network(&config.network)
.with_metrics_addr_label(connected_addr.get_transient_addr_label())
.finish(),
);
let mut connection_tracker = connection_tracker;
let connection_info = match negotiate_version(
&mut peer_conn,
&connected_addr,
config.clone(),
nonces,
user_agent,
our_services,
relay,
minimum_peer_version,
)
.await
{
Ok(info) => {
let duration = handshake_start.elapsed().as_secs_f64();
metrics::histogram!(
"zcash.net.peer.handshake.duration_seconds",
"result" => "success"
)
.record(duration);
info
}
Err(err) => {
let duration = handshake_start.elapsed().as_secs_f64();
let reason = match &err {
HandshakeError::UnexpectedMessage(_) => "unexpected_message",
HandshakeError::RemoteNonceReuse => "nonce_reuse",
HandshakeError::LocalDuplicateNonce => "duplicate_nonce",
HandshakeError::ConnectionClosed => "connection_closed",
HandshakeError::Io(_) => "io_error",
HandshakeError::Serialization(_) => "serialization",
HandshakeError::ObsoleteVersion(_) => "obsolete_version",
HandshakeError::Timeout => "timeout",
HandshakeError::ZakuraUpgradeSelected
| HandshakeError::ZakuraUpgrade(_)
| HandshakeError::ZakuraUpgradePreludeMalformed(_) => {
unreachable!("negotiate_version returns before Zakura upgrade routing")
}
};
metrics::histogram!(
"zcash.net.peer.handshake.duration_seconds",
"result" => "failure"
)
.record(duration);
metrics::counter!(
"zcash.net.peer.handshake.failures.total",
"reason" => reason
)
.increment(1);
return Err(err);
}
};
let remote_services = connection_info.remote.services;
if should_attempt_zakura_upgrade(&config, &connection_info) {
match upgrade_to_zakura_handshake(
&mut peer_conn,
&connection_info,
connected_addr,
&config.network,
config.zakura.dev_network.as_deref(),
zakura_handshake_connector,
&address_book_updater,
)
.await
{
Ok(
ZakuraUpgradeOutcome::Upgraded { .. }
| ZakuraUpgradeOutcome::Duplicate { .. },
) => {
return Err(HandshakeError::ZakuraUpgradeSelected);
}
Ok(ZakuraUpgradeOutcome::Rejected {
reason: ZakuraRejectReason::TemporaryUnavailable,
}) => {
debug!(
?connected_addr,
"Zakura upgrade is temporarily unavailable; continuing legacy handshake"
);
}
Ok(ZakuraUpgradeOutcome::Rejected { .. }) => {
return Err(HandshakeError::ZakuraUpgradeSelected);
}
Err(error) => return Err(error),
}
}
connection_tracker.mark_open();
if let Some(book_addr) = connected_addr.get_address_book_addr() {
let _ = address_book_updater
.send(MetaAddr::new_connected(
book_addr,
&remote_services,
connected_addr.is_inbound(),
))
.await;
}
let bare_codec = peer_conn.codec_mut();
bare_codec.reconfigure_version(connection_info.negotiated_version);
bare_codec.reconfigure_full_body_len();
debug!("constructing client, spawning server");
let (server_tx, server_rx) = futures::channel::mpsc::channel(0);
let (shutdown_tx, shutdown_rx) = oneshot::channel();
let error_slot = ErrorSlot::default();
let (peer_tx, peer_rx) = peer_conn.split();
let inner_conn_span = connection_span.clone();
let peer_tx = peer_tx.with(move |msg: Message| {
let span = debug_span!(parent: inner_conn_span.clone(), "outbound_metric");
metrics::counter!(
"zcash.net.out.messages",
"command" => msg.command(),
"addr" => connected_addr.get_transient_addr_label(),
)
.increment(1);
future::ready(Ok(msg)).instrument(span)
});
let inbound_ts_collector = address_book_updater.clone();
let inbound_inv_collector = inv_collector.clone();
let ts_inner_conn_span = connection_span.clone();
let inv_inner_conn_span = connection_span.clone();
let peer_rx = peer_rx
.then(move |msg| {
let inbound_ts_collector = inbound_ts_collector.clone();
let span =
debug_span!(parent: ts_inner_conn_span.clone(), "inbound_ts_collector");
async move {
match &msg {
Ok(msg) => {
metrics::counter!(
"zcash.net.in.messages",
"command" => msg.command(),
"addr" => connected_addr.get_transient_addr_label(),
)
.increment(1);
}
Err(err) => {
metrics::counter!(
"zakura.net.in.errors",
"error" => err.to_string(),
"addr" => connected_addr.get_transient_addr_label(),
)
.increment(1);
if let Some(book_addr) = connected_addr.get_address_book_addr() {
let _ = inbound_ts_collector
.send(MetaAddr::new_errored(book_addr, remote_services))
.await;
}
}
}
msg
}
.instrument(span)
})
.then(move |msg| {
let inbound_inv_collector = inbound_inv_collector.clone();
let span = debug_span!(parent: inv_inner_conn_span.clone(), "inventory_filter");
register_inventory_status(msg, connected_addr, inbound_inv_collector)
.instrument(span)
})
.boxed();
let remote_canonical_addr = connection_info.remote.address_from.addr();
let alternate_addrs = connected_addr
.get_alternate_addrs(remote_canonical_addr)
.map(|addr| {
MetaAddr::new_gossiped_meta_addr(
addr,
PeerServices::NODE_NETWORK,
DateTime32::now(),
)
});
let server = Connection::new(
inbound_service,
server_rx,
error_slot.clone(),
peer_tx,
connection_tracker,
connection_info.clone(),
alternate_addrs.collect(),
);
let connection_task = tokio::spawn(
server
.run(peer_rx)
.instrument(connection_span.clone())
.boxed(),
);
let heartbeat_task = tokio::spawn(
send_periodic_heartbeats_with_shutdown_handle(
connected_addr,
shutdown_rx,
server_tx.clone(),
address_book_updater.clone(),
)
.instrument(tracing::debug_span!(parent: connection_span, "heartbeat"))
.boxed(),
);
let client = Client {
connection_info,
shutdown_tx: Some(shutdown_tx),
server_tx,
inv_collector,
error_slot,
connection_task,
heartbeat_task,
};
Ok(client)
};
let fut = timeout(constants::HANDSHAKE_TIMEOUT, fut);
tokio::spawn(fut.instrument(negotiator_span))
.map(
|join_result: Result<
Result<Result<Client, HandshakeError>, error::Elapsed>,
JoinError,
>| {
match join_result {
Ok(Ok(Ok(connection_client))) => Ok(connection_client),
Ok(Ok(Err(handshake_error))) => Err(handshake_error.into()),
Ok(Err(timeout_error)) => Err(timeout_error.into()),
Err(join_error) => match join_error.try_into_panic() {
Ok(panic_reason) => panic::resume_unwind(panic_reason),
Err(join_error) => Err(join_error.into()),
},
}
},
)
.boxed()
}
}
pub(crate) async fn register_inventory_status(
msg: Result<Message, SerializationError>,
connected_addr: ConnectedAddr,
inv_collector: broadcast::Sender<InventoryChange>,
) -> Result<Message, SerializationError> {
match (&msg, connected_addr.get_transient_addr()) {
(Ok(Message::Inv(advertised)), Some(transient_addr)) => {
match advertised.as_slice() {
[advertised @ InventoryHash::Block(_)] => {
debug!(
?advertised,
"registering gossiped advertised block inventory for peer"
);
let _ = inv_collector
.send(InventoryChange::new_available(*advertised, transient_addr));
}
advertised => {
let advertised = advertised
.iter()
.filter(|advertised| advertised.unmined_tx_id().is_some());
debug!(
?advertised,
"registering advertised unmined transaction inventory for peer",
);
if let Some(change) =
InventoryChange::new_available_multi(advertised, transient_addr)
{
let _ = inv_collector.send(change);
}
}
}
}
(Ok(Message::NotFound(missing)), Some(transient_addr)) => {
let missing = missing.iter().filter(|missing| {
missing.unmined_tx_id().is_some() || missing.block_hash().is_some()
});
debug!(?missing, "registering missing inventory for peer");
if let Some(change) = InventoryChange::new_missing_multi(missing, transient_addr) {
let _ = inv_collector.send(change);
}
}
_ => {}
}
msg
}
async fn send_periodic_heartbeats_with_shutdown_handle(
connected_addr: ConnectedAddr,
shutdown_rx: oneshot::Receiver<CancelHeartbeatTask>,
server_tx: futures::channel::mpsc::Sender<ClientRequest>,
heartbeat_ts_collector: tokio::sync::mpsc::Sender<MetaAddrChange>,
) -> Result<(), BoxError> {
use futures::future::Either;
let heartbeat_run_loop = send_periodic_heartbeats_run_loop(
connected_addr,
server_tx,
heartbeat_ts_collector.clone(),
);
pin_mut!(shutdown_rx);
pin_mut!(heartbeat_run_loop);
match future::select(shutdown_rx, heartbeat_run_loop).await {
Either::Left((Ok(CancelHeartbeatTask), _unused_run_loop)) => {
tracing::trace!("shutting down because Client requested shut down");
handle_heartbeat_shutdown(
PeerError::ClientCancelledHeartbeatTask,
&heartbeat_ts_collector,
&connected_addr,
)
.await
}
Either::Left((Err(oneshot::Canceled), _unused_run_loop)) => {
tracing::trace!("shutting down because Client was dropped");
handle_heartbeat_shutdown(
PeerError::ClientDropped,
&heartbeat_ts_collector,
&connected_addr,
)
.await
}
Either::Right((result, _unused_shutdown)) => {
tracing::trace!("shutting down due to heartbeat failure");
result
}
}
}
async fn send_periodic_heartbeats_run_loop(
connected_addr: ConnectedAddr,
mut server_tx: futures::channel::mpsc::Sender<ClientRequest>,
heartbeat_ts_collector: tokio::sync::mpsc::Sender<MetaAddrChange>,
) -> Result<(), BoxError> {
let mut interval = tokio::time::interval_at(
Instant::now() + constants::HEARTBEAT_INTERVAL,
constants::HEARTBEAT_INTERVAL,
);
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
let mut interval_stream = IntervalStream::new(interval);
while let Some(_instant) = interval_stream.next().await {
let ping_sent_at = Instant::now();
if let Some(book_addr) = connected_addr.get_address_book_addr() {
let _ = heartbeat_ts_collector
.send(MetaAddr::new_ping_sent(book_addr, ping_sent_at.into()))
.await;
}
let heartbeat = send_one_heartbeat(&mut server_tx);
let rtt = heartbeat_timeout(heartbeat, &heartbeat_ts_collector, &connected_addr).await?;
if let Some(book_addr) = connected_addr.get_address_book_addr() {
if let Some(rtt) = rtt {
let _ = heartbeat_ts_collector
.send(MetaAddr::new_responded(book_addr, Some(rtt)))
.await;
}
}
}
unreachable!("unexpected IntervalStream termination")
}
async fn send_one_heartbeat(
server_tx: &mut futures::channel::mpsc::Sender<ClientRequest>,
) -> Result<Response, BoxError> {
let (tx, rx) = oneshot::channel();
let request = Request::Ping(Nonce::default());
tracing::trace!(?request, "queueing heartbeat request");
match server_tx.try_send(ClientRequest {
request,
tx,
inv_collector: None,
transient_addr: None,
span: tracing::Span::current(),
}) {
Ok(()) => {}
Err(e) => {
if e.is_disconnected() {
Err(PeerError::ConnectionClosed)?;
} else if e.is_full() {
server_tx.send(e.into_inner()).await?;
} else {
warn!(?e, "unexpected try_send error");
Err(e)?;
};
}
}
server_tx.flush().await?;
tracing::trace!("sent heartbeat request");
let response = rx.await??;
tracing::trace!(?response, "got heartbeat response");
Ok(response)
}
async fn heartbeat_timeout(
fut: impl Future<Output = Result<Response, BoxError>>,
address_book_updater: &tokio::sync::mpsc::Sender<MetaAddrChange>,
connected_addr: &ConnectedAddr,
) -> Result<Option<Duration>, BoxError> {
let response = match timeout(constants::HEARTBEAT_INTERVAL, fut).await {
Ok(inner_result) => {
handle_heartbeat_error(inner_result, address_book_updater, connected_addr).await?
}
Err(elapsed) => {
handle_heartbeat_error(Err(elapsed), address_book_updater, connected_addr).await?
}
};
let rtt = match response {
Response::Pong(rtt) => Some(rtt),
_ => None,
};
Ok(rtt)
}
async fn handle_heartbeat_error<T, E>(
result: Result<T, E>,
address_book_updater: &tokio::sync::mpsc::Sender<MetaAddrChange>,
connected_addr: &ConnectedAddr,
) -> Result<T, E>
where
E: std::fmt::Debug,
{
match result {
Ok(t) => Ok(t),
Err(err) => {
tracing::debug!(?err, "heartbeat error, shutting down");
if let Some(book_addr) = connected_addr.get_address_book_addr() {
let _ = address_book_updater
.send(MetaAddr::new_errored(book_addr, None))
.await;
}
Err(err)
}
}
}
async fn handle_heartbeat_shutdown(
peer_error: PeerError,
address_book_updater: &tokio::sync::mpsc::Sender<MetaAddrChange>,
connected_addr: &ConnectedAddr,
) -> Result<(), BoxError> {
tracing::debug!(?peer_error, "client shutdown, shutting down heartbeat");
if let Some(book_addr) = connected_addr.get_address_book_addr() {
let _ = address_book_updater
.send(MetaAddr::new_shutdown(book_addr))
.await;
}
Err(peer_error.into())
}