Skip to content

Commit d9d29b6

Browse files
authored
fix(network): avoid disconnecting peers that return empty find responses at the chain tip (#10732)
* Add `chain_tip` getter to `MinimumPeerVersion` In some places the `MinimumPeerVersion` becomes the way to access the `ChainTip`, so we should make that explicit instead of adding unrelated methods to `MinimumPeerVersion`. * Remove `MinimumPeerVersion::chain_tip_height` Refactor to keep `MinimumPeerVersion` more focused. * Add `ChainTip::is_at_or_near_network_tip` A helper method to check if the node can be considered to be synced. * Only count stalls while syncing Empty find block or find headers responses are expected once nodes are synced. * Test if stalls aren't tracked when close to tip Ensure that the stall tracker only disconnects peers while the node is syncing. * Test disconnection after empty find responses Empty find blocks or find headers responses can slow down synchronization, so the peers should be penalized with a disconnection. * Also test if the chain tip state is empty Ensure that the initial behavior is to disconnect from peers that return empty find blocks or headers responses. * Test stall counts are preserved after sync Ensure that if the node falls behind, it will still track stalls from peers. * Add a regression test for the disconnection issue Simulate the scenario described in the issue, where an internal Zebra node disconnects from its upstream (and sole peer) because it has finished syncing. * Update changelog List the fix to not disconnect from peers missing blocks when synced.
1 parent 6efb1d4 commit d9d29b6

9 files changed

Lines changed: 486 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ All notable changes to Zebra are documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org).
77

8+
## [Unreleased]
9+
10+
### Fixed
11+
12+
- Don't disconnect from peers that return empty `FindBlocks` or `FindHeaders`
13+
responses when the local node is at or near the chain tip
14+
([#10732](https://github.com/ZcashFoundation/zebra/pull/10732))
15+
816
## [Zebra 6.0.0-rc.0](https://github.com/ZcashFoundation/zebra/releases/tag/v6.0.0-rc.0) - 2026-07-02
917

1018
### Added

zebra-chain/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Added
11+
12+
- `AT_OR_NEAR_TIP_THRESHOLD` constant and `ChainTip::is_at_or_near_network_tip()`
13+
method for determining whether the node is within 5 blocks of the estimated network tip
14+
([#10732](https://github.com/ZcashFoundation/zebra/pull/10732))
15+
816
## [11.0.0] - 2026-07-02
917

1018
### Added

zebra-chain/src/chain_tip.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ mod tests;
1515

1616
pub use network_chain_tip_height_estimator::NetworkChainTipHeightEstimator;
1717

18+
/// The maximum estimated distance to the network chain tip that is considered "at or near tip".
19+
///
20+
/// Allows for normal block-time variance and propagation delay. Considering the 75 second target
21+
/// for the time between blocks on mainnet, this equals approximately 6 minutes of time the node
22+
/// can stay without receiving a new block before being considered far from the tip.
23+
pub const AT_OR_NEAR_TIP_THRESHOLD: block::HeightDiff = 5;
24+
1825
/// An interface for querying the chain tip.
1926
///
2027
/// This trait helps avoid dependencies between:
@@ -118,6 +125,18 @@ pub trait ChainTip {
118125

119126
Some((distance_to_tip, current_height))
120127
}
128+
129+
/// Returns `true` if the node is at or near the network chain tip.
130+
///
131+
/// Returns `false` if the chain is empty or the node is more than
132+
/// [`AT_OR_NEAR_TIP_THRESHOLD`] blocks behind the estimated network tip,
133+
/// meaning stall detection should remain active.
134+
fn is_at_or_near_network_tip(&self, network: &Network) -> bool {
135+
match self.estimate_distance_to_network_chain_tip(network) {
136+
None => false,
137+
Some((distance, _height)) => distance <= AT_OR_NEAR_TIP_THRESHOLD,
138+
}
139+
}
121140
}
122141

123142
/// A chain tip that is always empty and never changes.

zebra-network/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Breaking Changes
11+
12+
- `MinimumPeerVersion::chain_tip_height()` is replaced by `chain_tip()`, which returns a
13+
reference to the underlying chain tip instead of a `Height`
14+
([#10732](https://github.com/ZcashFoundation/zebra/pull/10732))
15+
816
## [10.0.0] - 2026-07-02
917

1018
### Breaking Changes

zebra-network/src/peer/handshake.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ use tracing::{span, Level, Span};
2828
use tracing_futures::Instrument;
2929

3030
use zebra_chain::{
31+
block,
3132
chain_tip::{ChainTip, NoChainTip},
3233
parameters::Network,
3334
serialization::{DateTime32, SerializationError},
@@ -674,6 +675,11 @@ where
674675
}
675676
};
676677

678+
let start_height = minimum_peer_version
679+
.chain_tip()
680+
.best_tip_height()
681+
.unwrap_or(block::Height(0));
682+
677683
let our_version = VersionMessage {
678684
version: constants::CURRENT_NETWORK_PROTOCOL_VERSION,
679685
services: our_services,
@@ -683,7 +689,7 @@ where
683689
address_from: AddrInVersion::new(our_listen_addr, our_services),
684690
nonce: local_nonce,
685691
user_agent: user_agent.clone(),
686-
start_height: minimum_peer_version.chain_tip_height(),
692+
start_height,
687693
relay,
688694
}
689695
.into();

zebra-network/src/peer/minimum_peer_version.rs

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
use std::fmt;
44

5-
use zebra_chain::{block::Height, chain_tip::ChainTip, parameters::Network};
5+
use zebra_chain::{chain_tip::ChainTip, parameters::Network};
66

77
use crate::protocol::external::types::Version;
88

@@ -80,14 +80,9 @@ where
8080
}
8181
}
8282

83-
/// Return the current chain tip height.
84-
///
85-
/// If it is not available return height zero.
86-
pub fn chain_tip_height(&self) -> Height {
87-
match self.chain_tip.best_tip_height() {
88-
Some(height) => height,
89-
None => Height(0),
90-
}
83+
/// Returns a reference to the underlying chain tip.
84+
pub fn chain_tip(&self) -> &C {
85+
&self.chain_tip
9186
}
9287
}
9388

zebra-network/src/peer_set/set.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -943,10 +943,17 @@ where
943943
.take_ready_service(&p2c_key)
944944
.expect("selected peer must be ready");
945945

946-
let track_stalls = matches!(
946+
let is_find_request = matches!(
947947
&req,
948948
Request::FindBlocks { .. } | Request::FindHeaders { .. }
949949
);
950+
let is_syncing = || {
951+
!self
952+
.minimum_peer_version
953+
.chain_tip()
954+
.is_at_or_near_network_tip(&self.network)
955+
};
956+
let track_stalls = is_find_request && is_syncing();
950957

951958
let fut = svc.call(req);
952959
self.push_unready(p2c_key, svc);

0 commit comments

Comments
 (0)