Skip to content

Follow-up cleanup: validate_and_store_previous_cycle_quorums and current-cycle path in feed_qr_info #129

@xdustinface

Description

@xdustinface

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions