From 7c8d7e50ba64e167c552a71a9945c1c4e0192d05 Mon Sep 17 00:00:00 2001 From: xdustinface Date: Sat, 11 Apr 2026 23:20:52 +1000 Subject: [PATCH] refactor: derive rotation cycle key from the cycle boundary block Previously the per-cycle map was keyed by the quorum hash of whichever entry happened to be first in the incoming commitment list. In practice this produces the cycle boundary block hash because the wire format is ordered by `quorum_index` and the quorum at index 0 has its DKG start block at the cycle boundary itself, so the old and new keys should match in theory. Derive the key directly from the work block in `mn_list_diff_h` (at `cycle_boundary - WORK_DIFF_DEPTH`) and look up the cycle boundary block hash in the block container. This expresses the intent at the call site, removes the implicit dependency on wire ordering, and fails loudly when the block container does not yet have the cycle boundary block. --- dash/src/sml/masternode_list_engine/mod.rs | 54 ++++++++++++++++------ 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/dash/src/sml/masternode_list_engine/mod.rs b/dash/src/sml/masternode_list_engine/mod.rs index 4cc88824b..142e3b797 100644 --- a/dash/src/sml/masternode_list_engine/mod.rs +++ b/dash/src/sml/masternode_list_engine/mod.rs @@ -631,6 +631,19 @@ impl MasternodeListEngine { .map(|quorum_entry| quorum_entry.llmq_type) .unwrap_or(self.network.isd_llmq_type()); + // IS locks reference the cycle boundary block hash as their `cyclehash` field, + // so `rotated_quorums_per_cycle` must be keyed by that hash. + #[cfg(feature = "quorum_validation")] + let cycle_boundary_hash = { + let cycle_boundary_height = h_height + WORK_DIFF_DEPTH; + *self.block_container.get_hash(&cycle_boundary_height).ok_or( + QuorumValidationError::RequiredBlockNotPresent( + BlockHash::all_zeros(), + format!("cycle boundary at height {cycle_boundary_height}"), + ), + )? + }; + if let Some((quorum_snapshot_at_h_minus_4c, mn_list_diff_at_h_minus_4c)) = quorum_snapshot_and_mn_list_diff_at_h_minus_4c { @@ -725,9 +738,6 @@ impl MasternodeListEngine { LLMQEntryVerificationStatus, )> = Vec::new(); - let cycle_key = - qualified_last_commitment_per_index.first().map(|q| q.quorum_entry.quorum_hash); - for rotated_quorum in qualified_last_commitment_per_index.iter_mut() { tracing::debug!( " Current cycle quorum: hash={}, raw_quorum_index={:?}, map_key={:?}", @@ -762,13 +772,9 @@ impl MasternodeListEngine { *status = rotated_quorum.verified.clone(); } - if let Some(key) = cycle_key { - let cycle_map = build_cycle_quorum_map( - qualified_last_commitment_per_index, - rotation_quorum_type, - )?; - *self.rotated_quorums_per_cycle.entry(key).or_default() = cycle_map; - } + let cycle_map = + build_cycle_quorum_map(qualified_last_commitment_per_index, rotation_quorum_type)?; + *self.rotated_quorums_per_cycle.entry(cycle_boundary_hash).or_default() = cycle_map; // Apply collected updates after iteration to avoid borrow conflicts for (heights, quorum_type, quorum_hash, new_status) in updates { @@ -859,12 +865,10 @@ impl MasternodeListEngine { } } } - } else if let Some(cycle_key) = - qualified_last_commitment_per_index.first().map(|q| q.quorum_entry.quorum_hash) - { + } else { let cycle_map = build_cycle_quorum_map(qualified_last_commitment_per_index, rotation_quorum_type)?; - *self.rotated_quorums_per_cycle.entry(cycle_key).or_default() = cycle_map; + *self.rotated_quorums_per_cycle.entry(cycle_boundary_hash).or_default() = cycle_map; } #[cfg(not(feature = "quorum_validation"))] @@ -1187,7 +1191,7 @@ mod tests { }; use crate::sml::masternode_list::MasternodeList; use crate::sml::masternode_list_engine::{ - MasternodeListEngine, MasternodeListEngineBlockContainer, + MasternodeListEngine, MasternodeListEngineBlockContainer, WORK_DIFF_DEPTH, }; use crate::sml::quorum_entry::qualified_quorum_entry::{ QualifiedQuorumEntry, VerifyingChainLockSignaturesType, @@ -1437,6 +1441,8 @@ mod tests { .expect("expected to apply diff"); } + let h_work_block_hash = qr_info.mn_list_diff_h.block_hash; + masternode_list_engine .feed_qr_info:: Result>( qr_info, true, true, None, @@ -1452,6 +1458,24 @@ mod tests { .1, &[Llmqtype400_85, Llmqtype50_60, Llmqtype400_60], ); + + // Verify the cycle map is keyed by the cycle boundary hash, not a quorum hash. + let h_work_height = masternode_list_engine + .block_container + .get_height(&h_work_block_hash) + .expect("h work block must be in container after feed_qr_info"); + let expected_cycle_boundary_hash = masternode_list_engine + .block_container + .get_hash(&(h_work_height + WORK_DIFF_DEPTH)) + .copied() + .expect("cycle boundary hash must be in container after feed_qrinfo_heights_to_engine"); + assert!( + masternode_list_engine + .rotated_quorums_per_cycle + .contains_key(&expected_cycle_boundary_hash), + "rotated_quorums_per_cycle should be keyed by the cycle boundary hash {}", + expected_cycle_boundary_hash + ); } #[test]