Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ All notable changes to Zebra are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org).

## [Unreleased]

### Fixed

- Outbound peer slots no longer fill up with peers that advertise no services, which could stall
a fresh sync at genesis when most reachable listeners are non-serving. While syncing, Zebra
now requires the `NODE_NETWORK` service from outbound peers; at or near the network tip it
accepts non-serving peers (like pruned nodes) again
([#11071](https://github.com/ZcashFoundation/zebra/pull/11071))

## [Zebra 6.2.1](https://github.com/ZcashFoundation/zebra/releases/tag/v6.2.1) - 2026-07-22

### Changed
Expand Down
16 changes: 16 additions & 0 deletions zebra-network/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- `HandshakeError::MissingRequiredServices`, returned when an outbound handshake is rejected
because the remote peer's `version` message doesn't advertise `NODE_NETWORK`
([#11071](https://github.com/ZcashFoundation/zebra/pull/11071)).

### Changed

- While the node is syncing, outbound connections to peers that don't advertise `NODE_NETWORK`
are rejected during the handshake, so a fresh sync's outbound slots aren't occupied by
non-serving peers. At or near the network tip, such peers (like pruned nodes, which can serve
recent blocks) are accepted again. Inbound and isolated connections are unaffected
([#11071](https://github.com/ZcashFoundation/zebra/pull/11071)).

## [10.2.0] - 2026-07-17

### Added
Expand Down
6 changes: 6 additions & 0 deletions zebra-network/src/peer/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,12 @@ pub enum HandshakeError {
/// The remote peer offered a version older than our minimum version.
#[error("Peer offered obsolete version: {0:?}")]
ObsoleteVersion(crate::protocol::external::types::Version),
/// The remote peer is missing services that are required for outbound peers.
#[error("Peer is missing required services, advertised: {services:?}")]
MissingRequiredServices {
/// The services advertised by the remote peer in its `version` message.
services: crate::protocol::external::types::PeerServices,
},
/// Sending or receiving a message timed out.
#[error("Timeout when sending or receiving a message to peer")]
Timeout,
Expand Down
50 changes: 50 additions & 0 deletions zebra-network/src/peer/handshake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,12 @@ where
.single()
.expect("in-range number of seconds and valid nanosecond");

// Whether this node is still syncing, used below to decide whether outbound peers must
// advertise `NODE_NETWORK`. Read before the match below, which can move `config.network`.
let is_syncing = !minimum_peer_version
.chain_tip()
.is_at_or_near_network_tip(&config.network);

let (their_addr, our_services, our_listen_addr) = match connected_addr {
// Version messages require an address, so we use
// an unspecified address for Isolated connections
Expand Down Expand Up @@ -783,6 +789,43 @@ where
return Err(HandshakeError::ObsoleteVersion(remote.version));
}

// # Security
//
// While syncing, require `NODE_NETWORK` from outbound peers: peers without it can't serve
// us historic blocks, but still occupy outbound slots and receive syncer block requests.
// When many reachable listeners are non-serving, those slots can fill up and stall a fresh
// sync (#11061). This mirrors Bitcoin Core, which requires block-serving peers during
// initial block download.
//
// At or near the network tip the requirement is dropped, because non-serving peers (like
// pruned nodes) can still serve recent blocks and transactions. Inbound and isolated
// connections are always exempt, so light clients can still connect to us.
if is_syncing
&& matches!(connected_addr, OutboundDirect { .. } | OutboundProxy { .. })
&& !remote.services.contains(PeerServices::NODE_NETWORK)
{
debug!(
remote_ip = ?their_addr,
?remote.services,
?remote.user_agent,
"disconnecting from non-serving peer",
);

// the value is the number of rejected handshakes, by peer IP and advertised services
metrics::counter!(
"zcash.net.peers.missing_services",
"remote_ip" => their_addr.to_string(),
"remote_services" => format!("{:?}", remote.services),
"user_agent" => remote.user_agent.clone(),
)
.increment(1);

// Disconnect if the outbound peer doesn't advertise the required services.
return Err(HandshakeError::MissingRequiredServices {
services: remote.services,
});
}

let negotiated_version = min(constants::CURRENT_NETWORK_PROTOCOL_VERSION, remote.version);

// Limit containing struct size, and avoid multiple duplicates of 300+ bytes of data.
Expand Down Expand Up @@ -961,6 +1004,9 @@ where
HandshakeError::Io(_) => "io_error",
HandshakeError::Serialization(_) => "serialization",
HandshakeError::ObsoleteVersion(_) => "obsolete_version",
HandshakeError::MissingRequiredServices { .. } => {
"missing_required_services"
}
HandshakeError::Timeout => "timeout",
};
metrics::histogram!(
Expand All @@ -973,6 +1019,10 @@ where
"reason" => reason
)
.increment(1);

// Rejected non-serving peers are reported by the crawler's `report_failed`
// without their services, so they get the standard failure backoff and can
// be dialed again once the node is near the network tip (#11061).
return Err(err);
}
};
Expand Down
2 changes: 2 additions & 0 deletions zebra-network/src/peer/handshake/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

use super::*;

mod vectors;

impl<S, C> Handshake<S, C>
where
S: Service<Request, Response = Response, Error = BoxError> + Clone + Send + 'static,
Expand Down
Loading