Problem
The mempool startup gate currently requires both:
SyncStatus::is_close_to_tip() to be true, and
- a fresh
TipAction from ChainTipChange::last_tip_change() so the mempool can initialize last_seen_tip_hash.
But poll_ready() calls last_tip_change() before checking whether startup is allowed. last_tip_change() is consuming: once it returns a tip action, it records that block hash as last_change_hash, so the same action is not returned again.
That creates a startup latch edge case:
- A disabled mempool polls and consumes a tip action.
- Zebra is still considered far from tip, so
update_state() matches (false, false, _) and leaves the mempool disabled.
- Later,
RecentSyncLengths / SyncStatus independently changes so is_close_to_tip() becomes true.
- No new tip action exists, so
last_tip_change() returns None.
update_state() matches (true, false, None) and still refuses to enable.
The mempool then remains disabled until another chain tip change arrives.
Desired behavior
Startup should not require a fresh, unconsumed TipAction at the exact moment the close-to-tip gate opens.
Possible fixes:
- initialize the disabled mempool directly from
LatestChainTip once is_caught_up_to_start() is true, or
- preserve a consumed tip action until activation succeeds.
This is separate from active-mempool reset behavior. Resetting/clearing the mempool on TipAction::Reset is fine; the issue is only that disabled startup can consume its one available tip action too early.
Code paths
zebrad/src/components/mempool.rs: poll_ready() calls chain_tip_change.last_tip_change() before update_state().
zebrad/src/components/mempool.rs: update_state() refuses (true, false, None) because there is no tip action to seed last_seen_tip_hash.
zebra-state/src/service/chain_tip.rs: ChainTipChange::last_tip_change() updates last_change_hash when it returns an action.
Problem
The mempool startup gate currently requires both:
SyncStatus::is_close_to_tip()to be true, andTipActionfromChainTipChange::last_tip_change()so the mempool can initializelast_seen_tip_hash.But
poll_ready()callslast_tip_change()before checking whether startup is allowed.last_tip_change()is consuming: once it returns a tip action, it records that block hash aslast_change_hash, so the same action is not returned again.That creates a startup latch edge case:
update_state()matches(false, false, _)and leaves the mempool disabled.RecentSyncLengths/SyncStatusindependently changes sois_close_to_tip()becomes true.last_tip_change()returnsNone.update_state()matches(true, false, None)and still refuses to enable.The mempool then remains disabled until another chain tip change arrives.
Desired behavior
Startup should not require a fresh, unconsumed
TipActionat the exact moment the close-to-tip gate opens.Possible fixes:
LatestChainTiponceis_caught_up_to_start()is true, orThis is separate from active-mempool reset behavior. Resetting/clearing the mempool on
TipAction::Resetis fine; the issue is only that disabled startup can consume its one available tip action too early.Code paths
zebrad/src/components/mempool.rs:poll_ready()callschain_tip_change.last_tip_change()beforeupdate_state().zebrad/src/components/mempool.rs:update_state()refuses(true, false, None)because there is no tip action to seedlast_seen_tip_hash.zebra-state/src/service/chain_tip.rs:ChainTipChange::last_tip_change()updateslast_change_hashwhen it returns an action.