Skip to content

Commit 478af3b

Browse files
authored
feat(dash-spv): drive masternode sync via PipelineMode state machine (#738)
* feat(dash-spv): drive masternode sync via `PipelineMode` state machine Introduce a `PipelineMode` enum (`QuorumValidation` / `Incremental`) on `MasternodeSyncState` so the sync manager decides per tip whether to fire a full `getqrinfo` or a targeted `GetMnListDiff`. `next_pipeline_mode` is mining-window-aware: inside a cycle's DKG window it picks `QuorumValidation` until the cycle is fully verified, otherwise it picks `Incremental` to keep the tip list fresh. Drop the `CHAINLOCK_RETRY_DELAY` retry (now redundant since `feed_qr_info` is resilient to missing rotation CL sigs) and replace the single `QRINFO_TIMEOUT_SECS` with a `[10, 30, 60]` per-attempt schedule. Add `should_process_qrinfo` to drop duplicate and unsolicited responses, and recover `last_synced_block_hash` from the engine on startup so restarts can resume with a targeted diff. Plumb `QRInfoFeedResult` through `SyncEvent::MasternodeStateUpdated` so consumers can see what was qualified, fully verified, and stored on the same event. Add `qr_infos_requested`, `validated_cycles`, and `rotation_cycles` counters on `MasternodesProgress` so the masternode sync exposes per-cycle progress alongside the existing `diffs_processed`. * fix(dash-spv): guard `Synced` mn-pipeline restarts on pending requests When a `QRInfo` response is processed the `waiting_for_qrinfo` flag clears immediately while the historical `MnListDiff`s still drain through `mnlistdiff_pipeline`. Until then `pipeline_mode` holds `QuorumValidation { qr_info_result }`. A subsequent `BlockHeadersStored` (or `BlockHeaderSyncComplete`) firing in `Synced` could see `next_pipeline_mode` return `Incremental` (the cycle was just marked validated), call `send_tip_mnlistdiff_update`, overwrite `pipeline_mode = Incremental`, and append a tip request onto the still-draining queue. When the shared pipeline finally completes, `complete_pipeline` dispatched to `complete_incremental_pipeline` and the original `qr_info_result` (with rotated quorum data) was discarded. Add the `has_pending_requests()` early-return at the top of both `Synced` arms, mirroring the guard the `tick` handler already uses. The `tick` handler picks up any tip gap once the pipeline drains, so no incremental update is lost. Addresses CodeRabbit review comment on PR [#738](#738) [review comment](#738 (comment)) * fix(dash-spv): cap QRInfo in-flight attempts at `MAX_RETRY_ATTEMPTS` Previously the post-increment retry loop let `qrinfo_retry_count` reach `MAX_RETRY_ATTEMPTS` before the bound check failed, which produced one extra in-flight attempt (4 instead of 3) whose timeout came from the `min()` clamp in `qrinfo_timeout_for` rather than a real schedule slot. Worst-case wall clock was 160s, not the 100s claimed in the doc. Compare `qrinfo_retry_count + 1` against the bound so total in-flight attempts equal `MAX_RETRY_ATTEMPTS` (= `QRINFO_TIMEOUT_SCHEDULE_SECS.len()`), keeping the clamp truly defensive and the documented 100s budget accurate. * fix(dash-spv): gate QRInfo against the active in-flight request tip Replace `waiting_for_qrinfo: bool` and `qrinfo_wait_start: Option<Instant>` with a single `qrinfo_in_flight: Option<QRInfoInFlight>` that also carries the requested tip hash. `should_process_qrinfo` now rejects a response whose `mn_list_diff_tip.block_hash` does not match the active request, closing a race where a late straggler from a previously requested tip could be accepted after a timeout retry rotated the active tip. Addresses CodeRabbit review comment on PR #738 #738 (comment)
1 parent 8dd5f1b commit 478af3b

5 files changed

Lines changed: 919 additions & 138 deletions

File tree

dash-spv-ffi/src/callbacks.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,7 @@ impl FFISyncEventCallbacks {
383383
}
384384
SyncEvent::MasternodeStateUpdated {
385385
height,
386+
..
386387
} => {
387388
if let Some(cb) = self.on_masternode_state_updated {
388389
cb(*height, self.user_data);

dash-spv/src/sync/events.rs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::sync::ManagerIdentifier;
22
use dashcore::ephemerealdata::chain_lock::ChainLock;
33
use dashcore::ephemerealdata::instant_lock::InstantLock;
4+
use dashcore::sml::masternode_list_engine::QRInfoFeedResult;
45
use dashcore::{Address, BlockHash, Txid};
56
use key_wallet_manager::{FilterMatchKey, WalletId};
67
use std::collections::{BTreeMap, BTreeSet};
@@ -117,6 +118,14 @@ pub enum SyncEvent {
117118
MasternodeStateUpdated {
118119
/// New masternode state height
119120
height: u32,
121+
/// QRInfo processing result when this update came through the
122+
/// QuorumValidation pipeline. `None` for Incremental (MnListDiff-only)
123+
/// updates. Consumers that care about rotation cycle storage (e.g.
124+
/// IS lock verification across rotation) can gate on
125+
/// `result.all_fully_verified()` together with
126+
/// `result.stored_cycle_height` to know which cycle was fully
127+
/// verified and stored in `rotated_quorums_per_cycle` by this update.
128+
qr_info_result: Option<QRInfoFeedResult>,
120129
},
121130

122131
/// A manager encountered a recoverable error.
@@ -225,9 +234,18 @@ impl SyncEvent {
225234
}
226235
SyncEvent::MasternodeStateUpdated {
227236
height,
228-
} => {
229-
format!("MasternodeStateUpdated(height={})", height)
230-
}
237+
qr_info_result,
238+
} => match qr_info_result {
239+
Some(s) => format!(
240+
"MasternodeStateUpdated(height={}, qr_info={{stored_cycle_height={:?}, verified={}/{}, newly_qualified={}}})",
241+
height,
242+
s.stored_cycle_height,
243+
s.fully_verified_count,
244+
s.rotated_quorum_count,
245+
s.newly_qualified_count,
246+
),
247+
None => format!("MasternodeStateUpdated(height={})", height),
248+
},
231249
SyncEvent::ManagerError {
232250
manager,
233251
error,

0 commit comments

Comments
 (0)