Found while reviewing #736. All three items are correct in production today (the rotating LLMQ type is always ISD on mainnet/testnet, MN list gaps during fresh sync are rare-and-recoverable, and the duplication is benign), so this is hygiene rather than a bug. Filing for follow-up so the PR stays narrow.
Context
MasternodeListEngine::feed_qr_info (in dash/src/sml/masternode_list_engine/mod.rs) handles two rotation cycles in the same call:
- The current cycle, processed inline from
qr_info.last_commitment_per_index.
- The previous cycle, processed via
validate_and_store_previous_cycle_quorums, which reads quorums off masternode_lists[h] and validates them against the 4 historical CL sigs [sig_h_minus_3c, sig_h_minus_2c, sig_h_minus_c, sig_h].
The two paths share a lot of shape but have drifted in details. After #736 the storage gates were unified via is_cycle_fully_verified, but the items below remain.
1. isd_type hardcoded inside validate_and_store_previous_cycle_quorums
validate_and_store_previous_cycle_quorums derives the rotating LLMQ type from self.network.isd_llmq_type(), while the current-cycle path in feed_qr_info infers it per-call from last_commitment_per_index.first().llmq_type (falling back to isd_llmq_type()):
// validate_and_store_previous_cycle_quorums
let isd_type = self.network.isd_llmq_type();
// feed_qr_info (current-cycle path)
let rotation_quorum_type = last_commitment_per_index
.first()
.map(|q| q.llmq_type)
.unwrap_or(self.network.isd_llmq_type());
In practice this never matters because ISD is the only rotating LLMQ type on mainnet/testnet. But the divergence means the previous-cycle path silently looks at the wrong quorum bucket if a non-ISD rotating LLMQ type ever appears (devnet config, future protocol change, test setup).
Suggested fix: plumb rotation_quorum_type from feed_qr_info into validate_and_store_previous_cycle_quorums so both paths use the same source of truth.
2. Early break in the validation loop can mask a later Invalid
validate_and_store_previous_cycle_quorums walks the four cycle entries:
match entry.verified {
Verified => {}
Invalid(e) => return Err(e),
_ => { all_verified = false; break; } // skipped, unknown, etc.
}
If entry 0 is Skipped and entry 2 is Invalid, the break exits before the Invalid is observed and we silently return Ok(()). This is probably intentional (during fresh sync, missing MN-list context legitimately produces Skipped and we want to defer rather than fail loud), but the cost of being thorough is iterating four entries.
Suggested fix: drop the break. Continue iterating, set all_verified = false on any non-Verified/non-Invalid, and still bail loudly on any Invalid we encounter. Or, if the current behavior is deliberate, add a one-line comment so the next reader does not "fix" it.
3. Duplicated mirror-statuses block
The block in validate_and_store_previous_cycle_quorums that propagates each entry's verified status into self.quorum_statuses and every self.masternode_lists[height] is structurally identical to the equivalent block in the current-cycle path of feed_qr_info. They differ in:
- Variable name (
isd_type vs rotation_quorum_type).
- The current-cycle path also tracks
tip_height insertion into the heights set.
Suggested fix: extract a helper mirror_cycle_quorum_statuses(entries, llmq_type, extra_height: Option<CoreBlockHeight>) (or two narrow helpers) and call it from both sites. This is a noisier refactor than the storage-gate extraction in #736 because it iterates mutably over self.masternode_lists while self.quorum_statuses is also being mutated, so it needs a small updates: Vec<...> shuttle (already present in both call sites).
4. Naming
validate_and_store_previous_cycle_quorums and try_load_previous_cycle_entries use "previous cycle" correctly. The cycle they handle is the one mined in (h-c, h], which is exactly one cycle behind the QRInfo's current/tip cycle in (h, tip]. No rename needed.
Scope
This is a refactor-only follow-up. No behavior change is intended for items 1 and 3. Item 2 is a deliberate decision point.
Found while reviewing #736. All three items are correct in production today (the rotating LLMQ type is always ISD on mainnet/testnet, MN list gaps during fresh sync are rare-and-recoverable, and the duplication is benign), so this is hygiene rather than a bug. Filing for follow-up so the PR stays narrow.
Context
MasternodeListEngine::feed_qr_info(indash/src/sml/masternode_list_engine/mod.rs) handles two rotation cycles in the same call:qr_info.last_commitment_per_index.validate_and_store_previous_cycle_quorums, which reads quorums offmasternode_lists[h]and validates them against the 4 historical CL sigs[sig_h_minus_3c, sig_h_minus_2c, sig_h_minus_c, sig_h].The two paths share a lot of shape but have drifted in details. After #736 the storage gates were unified via
is_cycle_fully_verified, but the items below remain.1.
isd_typehardcoded insidevalidate_and_store_previous_cycle_quorumsvalidate_and_store_previous_cycle_quorumsderives the rotating LLMQ type fromself.network.isd_llmq_type(), while the current-cycle path infeed_qr_infoinfers it per-call fromlast_commitment_per_index.first().llmq_type(falling back toisd_llmq_type()):In practice this never matters because ISD is the only rotating LLMQ type on mainnet/testnet. But the divergence means the previous-cycle path silently looks at the wrong quorum bucket if a non-ISD rotating LLMQ type ever appears (devnet config, future protocol change, test setup).
Suggested fix: plumb
rotation_quorum_typefromfeed_qr_infointovalidate_and_store_previous_cycle_quorumsso both paths use the same source of truth.2. Early
breakin the validation loop can mask a laterInvalidvalidate_and_store_previous_cycle_quorumswalks the four cycle entries:If entry 0 is
Skippedand entry 2 isInvalid, thebreakexits before theInvalidis observed and we silently returnOk(()). This is probably intentional (during fresh sync, missing MN-list context legitimately producesSkippedand we want to defer rather than fail loud), but the cost of being thorough is iterating four entries.Suggested fix: drop the
break. Continue iterating, setall_verified = falseon any non-Verified/non-Invalid, and still bail loudly on anyInvalidwe encounter. Or, if the current behavior is deliberate, add a one-line comment so the next reader does not "fix" it.3. Duplicated mirror-statuses block
The block in
validate_and_store_previous_cycle_quorumsthat propagates each entry'sverifiedstatus intoself.quorum_statusesand everyself.masternode_lists[height]is structurally identical to the equivalent block in the current-cycle path offeed_qr_info. They differ in:isd_typevsrotation_quorum_type).tip_heightinsertion into theheightsset.Suggested fix: extract a helper
mirror_cycle_quorum_statuses(entries, llmq_type, extra_height: Option<CoreBlockHeight>)(or two narrow helpers) and call it from both sites. This is a noisier refactor than the storage-gate extraction in #736 because it iterates mutably overself.masternode_listswhileself.quorum_statusesis also being mutated, so it needs a smallupdates: Vec<...>shuttle (already present in both call sites).4. Naming
validate_and_store_previous_cycle_quorumsandtry_load_previous_cycle_entriesuse "previous cycle" correctly. The cycle they handle is the one mined in(h-c, h], which is exactly one cycle behind the QRInfo's current/tip cycle in(h, tip]. No rename needed.Scope
This is a refactor-only follow-up. No behavior change is intended for items 1 and 3. Item 2 is a deliberate decision point.