Skip to content

Commit 11f8a24

Browse files
upbqdnValarDragon
andauthored
fix(zebrad): keep active mempool through sync noise (#10929)
* fix(mempool): keep active mempool through sync noise Keep an already-active mempool running when the legacy sync-status heuristic temporarily reports that Zebra is far from the tip. The initial activation gate still waits for near-tip sync status, but once active, transient sync noise no longer drops mempool storage or cancels queued transaction verification. Chain tip resets reinitialize the active mempool directly so pending transactions can be requeued and reverified. Also raise the close-to-tip threshold to accept recent sync batches averaging up to 100 blocks, matching the intended near-tip behavior covered by the updated tests. (cherry picked from commit 78d1a5e) * test(mempool): rename legacy sync wording * docs(zebrad): clarify mempool activation state update * fix(zebrad): revert global close-to-tip threshold; correct changelog The mempool latch (keeping an already-active mempool alive through transient far-from-tip sync status) is what closes the peer-toggle issue. The MIN_DIST_FROM_TIP 20->101 raise additionally loosened every other consumer of `is_close_to_tip()` — health readiness, sync gossip, block notify, and the getblocktemplate mining RPC — which was unjustified, so revert it. Also rewrite the CHANGELOG entry to describe only the latch (getblocktemplate is not latched; it is still bounded by the network-tip-distance estimate), and drop leftover "legacy" wording missed by the rename commit. * refactor(zebrad): tidy mempool activation log and reset tip-action binding Address review on #10929: - Move the "activating mempool: Zebra is close to the tip" log out of `enable_at_tip` into the initial-activation arm. The helper is also called from the chain-tip reset path, where sync status may report far-from-tip, so the log was misleading there (Copilot). - Bind the `Reset` action via a pattern match instead of `matches!`, dropping the `expect` and the `unwrap` in the reset branch (jvff). * fix(zebrad): address mempool PR review comments - Point the CHANGELOG entry at #10929 instead of the closed, unmerged #10926 (gustavovalverde). - Evaluate `is_caught_up_to_start()` once per `poll_ready` and pass it into `update_state()` instead of recomputing it there (also avoids a duplicate debug-activation log) (gustavovalverde). - Clarify the `fully_notified` assertion message: it is a placeholder that is always None pending the regtest network-info TODO, not active-mempool-specific (jvff). --------- Co-authored-by: Dev Ojha <ValarDragon@users.noreply.github.com> Co-authored-by: Dev Ojha <dojha@berkeley.edu>
1 parent 63b0dab commit 11f8a24

5 files changed

Lines changed: 313 additions & 98 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ and this project adheres to [Semantic Versioning](https://semver.org).
2020

2121
### Fixed
2222

23+
- Keep the mempool active through transient sync-status noise. Once started, the
24+
mempool is no longer cleared and its queued transaction verification is no longer
25+
cancelled when a temporary signal (which lower-work forks or stale peers can
26+
trigger) reports Zebra is far from the tip; initial activation still waits until
27+
Zebra is near the chain tip
28+
([#10929](https://github.com/ZcashFoundation/zebra/pull/10929)).
2329
- Don't disconnect from peers that return empty `FindBlocks` or `FindHeaders`
2430
responses when the local node is at or near the chain tip
2531
([#10732](https://github.com/ZcashFoundation/zebra/pull/10732))

zebrad/src/components/mempool.rs

Lines changed: 77 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,8 @@ impl Mempool {
308308

309309
// Make sure `is_enabled` is accurate.
310310
// Otherwise, it is only updated in `poll_ready`, right before each service call.
311-
service.update_state(None);
311+
let is_caught_up_to_start = service.is_caught_up_to_start();
312+
service.update_state(None, is_caught_up_to_start);
312313

313314
(service, transaction_subscriber)
314315
}
@@ -341,48 +342,70 @@ impl Mempool {
341342
is_debug_enabled
342343
}
343344

344-
/// Update the mempool state (enabled / disabled) depending on how close to
345-
/// the tip is the synchronization, including side effects to state changes.
345+
/// Returns `true` if Zebra is caught up enough to start the mempool.
346+
fn is_caught_up_to_start(&self) -> bool {
347+
self.sync_status.is_close_to_tip() || self.is_enabled_by_debug()
348+
}
349+
350+
/// Replaces the active state with a freshly-initialised [`ActiveState::Enabled`],
351+
/// using `tip_action`'s best tip hash as the `last_seen_tip_hash`.
352+
fn enable_at_tip(&mut self, tip_action: &TipAction) {
353+
let (last_seen_tip_hash, _) = tip_action.best_tip_hash_and_height();
354+
355+
let tx_downloads = Box::pin(TxDownloads::new(
356+
Timeout::new(self.outbound.clone(), TRANSACTION_DOWNLOAD_TIMEOUT),
357+
Timeout::new(self.tx_verifier.clone(), TRANSACTION_VERIFY_TIMEOUT),
358+
self.state.clone(),
359+
));
360+
self.active_state = ActiveState::Enabled {
361+
storage: storage::Storage::new(&self.config),
362+
tx_downloads,
363+
last_seen_tip_hash,
364+
};
365+
}
366+
367+
/// Activate the mempool once Zebra is close enough to the tip.
368+
///
369+
/// Sync status only gates initial activation. Once the mempool is active,
370+
/// this method does not disable it.
346371
///
347-
/// Accepts an optional [`TipAction`] for setting the `last_seen_tip_hash` field
348-
/// when enabling the mempool state, it will not enable the mempool if this is None.
372+
/// Accepts an optional [`TipAction`] for setting the `last_seen_tip_hash`
373+
/// field when enabling the mempool state. It will not enable the mempool if
374+
/// this is [`None`]. `is_caught_up_to_start` is supplied by the caller, which
375+
/// already computes it, to avoid evaluating the sync-status predicate twice.
349376
///
350377
/// Returns `true` if the state changed.
351-
fn update_state(&mut self, tip_action: Option<&TipAction>) -> bool {
352-
let is_close_to_tip = self.sync_status.is_close_to_tip() || self.is_enabled_by_debug();
353-
354-
match (is_close_to_tip, self.is_enabled(), tip_action) {
378+
fn update_state(
379+
&mut self,
380+
tip_action: Option<&TipAction>,
381+
is_caught_up_to_start: bool,
382+
) -> bool {
383+
// TODO: revisit these state transitions when sync status can prove
384+
// whether Zebra is behind the network tip.
385+
match (is_caught_up_to_start, self.is_enabled(), tip_action) {
355386
// the active state is up to date, or there is no tip action to activate the mempool
356387
(false, false, _) | (true, true, _) | (true, false, None) => return false,
357388

358389
// Enable state - there should be a chain tip when Zebra is close to the network tip
359390
(true, false, Some(tip_action)) => {
360-
let (last_seen_tip_hash, tip_height) = tip_action.best_tip_hash_and_height();
361-
362-
info!(?tip_height, "activating mempool: Zebra is close to the tip");
363-
364-
let tx_downloads = Box::pin(TxDownloads::new(
365-
Timeout::new(self.outbound.clone(), TRANSACTION_DOWNLOAD_TIMEOUT),
366-
Timeout::new(self.tx_verifier.clone(), TRANSACTION_VERIFY_TIMEOUT),
367-
self.state.clone(),
368-
));
369-
self.active_state = ActiveState::Enabled {
370-
storage: storage::Storage::new(&self.config),
371-
tx_downloads,
372-
last_seen_tip_hash,
373-
};
374-
}
375-
376-
// Disable state
377-
(false, true, _) => {
378391
info!(
379-
tip_height = ?self.latest_chain_tip.best_tip_height(),
380-
"deactivating mempool: Zebra is syncing lots of blocks"
392+
tip_height = ?tip_action.best_tip_height(),
393+
"activating mempool: Zebra is close to the tip"
381394
);
382395

383-
// This drops the previous ActiveState::Enabled, cancelling its download tasks.
384-
// We don't preserve the previous transactions, because we are syncing lots of blocks.
385-
self.active_state = ActiveState::Disabled;
396+
self.enable_at_tip(tip_action);
397+
}
398+
399+
// TODO: only disable an already-active mempool when validated sync
400+
// state proves Zebra is behind a higher-work chain that follows
401+
// this node's consensus rules.
402+
//
403+
// The sync status can be triggered by lower-work forks,
404+
// stale peers, or peers on incompatible consensus rules, so
405+
// it is strong enough to delay initial activation but not to shut
406+
// down a working mempool.
407+
(false, true, _) => {
408+
return false;
386409
}
387410
};
388411

@@ -526,10 +549,14 @@ impl Service<Request> for Mempool {
526549
Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;
527550

528551
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
529-
let tip_action = self.chain_tip_change.last_tip_change();
552+
let is_caught_up_to_start = self.is_caught_up_to_start();
553+
let should_check_tip = self.is_enabled() || is_caught_up_to_start;
554+
let tip_action = should_check_tip
555+
.then(|| self.chain_tip_change.last_tip_change())
556+
.flatten();
530557

531558
// TODO: Consider broadcasting a `MempoolChange` when the mempool is disabled.
532-
let is_state_changed = self.update_state(tip_action.as_ref());
559+
let is_state_changed = self.update_state(tip_action.as_ref(), is_caught_up_to_start);
533560

534561
tracing::trace!(is_enabled = ?self.is_enabled(), ?is_state_changed, "started polling the mempool...");
535562

@@ -545,9 +572,16 @@ impl Service<Request> for Mempool {
545572
//
546573
// But if the mempool was just freshly enabled,
547574
// skip resetting and removing mined transactions for this tip.
548-
if !is_state_changed && matches!(tip_action, Some(TipAction::Reset { .. })) {
575+
let reset_tip_action = match tip_action.as_ref() {
576+
Some(reset_tip_action @ TipAction::Reset { .. }) if !is_state_changed => {
577+
Some(reset_tip_action)
578+
}
579+
_ => None,
580+
};
581+
582+
if let Some(reset_tip_action) = reset_tip_action {
549583
info!(
550-
tip_height = ?tip_action.as_ref().unwrap().best_tip_height(),
584+
tip_height = ?reset_tip_action.best_tip_height(),
551585
"resetting mempool: switched best chain, skipped blocks, or activated network upgrade"
552586
);
553587

@@ -563,7 +597,13 @@ impl Service<Request> for Mempool {
563597
std::mem::drop(previous_state);
564598

565599
// Re-initialise an empty state.
566-
self.update_state(tip_action.as_ref());
600+
//
601+
// This deliberately bypasses the initial-activation gate in `update_state()`:
602+
// the mempool was already active when the reset arrived, and a
603+
// far-from-tip sync status must not disable an already-active mempool
604+
// (it can be triggered by lower-work forks, stale peers, or peers on
605+
// incompatible consensus rules).
606+
self.enable_at_tip(reset_tip_action);
567607

568608
// Re-verify the transactions that were pending or valid at the previous tip.
569609
// This saves us the time and data needed to re-download them.

zebrad/src/components/mempool/tests.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,11 @@ impl Mempool {
4747
self.dummy_call().await;
4848
}
4949

50-
/// Disable the mempool by pretending the synchronization is far from the tip.
51-
pub async fn disable(&mut self, recent_syncs: &mut RecentSyncLengths) {
50+
/// Pretend the synchronization is far from the tip and poll the mempool.
51+
async fn sync_far_from_tip(&mut self, recent_syncs: &mut RecentSyncLengths) {
5252
// Pretend we're far from the tip
5353
SyncStatus::sync_far_from_tip(recent_syncs);
54-
// Make a dummy request to poll the mempool and make it disable itself
54+
// Make a dummy request to poll the mempool.
5555
self.dummy_call().await;
5656
}
5757

zebrad/src/components/mempool/tests/prop.rs

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -190,9 +190,9 @@ proptest! {
190190
})?;
191191
}
192192

193-
/// Test if the mempool storage is cleared if the syncer falls behind and starts to catch up.
193+
/// Test if the mempool storage is kept if sync status falls behind.
194194
#[test]
195-
fn storage_is_cleared_if_syncer_falls_behind(
195+
fn storage_is_kept_if_sync_status_falls_behind(
196196
network in any::<Network>(),
197197
transaction in standard_verified_unmined_tx_strategy(),
198198
) {
@@ -205,7 +205,7 @@ proptest! {
205205
mut state_service,
206206
mut tx_verifier,
207207
mut recent_syncs,
208-
mut chain_tip_sender,
208+
_chain_tip_sender,
209209
) = setup(&network);
210210

211211
time::pause();
@@ -223,19 +223,15 @@ proptest! {
223223

224224
prop_assert_eq!(mempool.storage().transaction_count(), 1);
225225

226-
// Simulate the synchronizer catching up to the network chain tip.
227-
mempool.disable(&mut recent_syncs).await;
226+
// Simulate sync status reporting a large gap. That signal
227+
// can be caused by lower-work forks or incompatible peers, so it
228+
// should not shut down an already-active mempool.
229+
mempool.sync_far_from_tip(&mut recent_syncs).await;
228230

229-
// This time a call to `poll_ready` should clear the storage.
231+
// This time a call to `poll_ready` should keep the storage.
230232
mempool.dummy_call().await;
231233

232-
// sends a new fake chain tip so that the mempool can be enabled
233-
chain_tip_sender.set_finalized_tip(block1_chain_tip());
234-
235-
// Enable the mempool again so the storage can be accessed.
236-
mempool.enable(&mut recent_syncs).await;
237-
238-
prop_assert_eq!(mempool.storage().transaction_count(), 0);
234+
prop_assert_eq!(mempool.storage().transaction_count(), 1);
239235

240236
peer_set.expect_no_requests().await?;
241237
state_service.expect_no_requests().await?;
@@ -254,14 +250,6 @@ fn genesis_chain_tip() -> Option<ChainTipBlock> {
254250
.ok()
255251
}
256252

257-
fn block1_chain_tip() -> Option<ChainTipBlock> {
258-
zebra_test::vectors::BLOCK_MAINNET_1_BYTES
259-
.zcash_deserialize_into::<Arc<Block>>()
260-
.map(CheckpointVerifiedBlock::from)
261-
.map(ChainTipBlock::from)
262-
.ok()
263-
}
264-
265253
/// Create a new [`Mempool`] instance using mocked services.
266254
fn setup(
267255
network: &Network,

0 commit comments

Comments
 (0)