From a7d5c3e0c4b63d114a18a63c96f5e4a7199e3cbe Mon Sep 17 00:00:00 2001 From: xdustinface Date: Tue, 3 Feb 2026 04:51:35 +0100 Subject: [PATCH] fix: store historical rotating quorums by cycle boundary hash Adds infrastructure for storing historical rotating quorums keyed by cycle boundary hash instead of quorum hash. This enables InstantLock verification for previous cycles by: - Adding `WORK_DIFF_DEPTH` constant (8 blocks offset to cycle boundary) - Adding `store_rotating_quorums_for_cycle()` to extract and store quorums - Collecting work block hashes from `QRInfo` diffs (`h-4c`, `h-3c`, `h-2c`, `h-c`) - Using cycle boundary hash as the key for `rotated_quorums_per_cycle` --- dash/src/sml/masternode_list_engine/mod.rs | 114 ++++++++++++++++++--- 1 file changed, 99 insertions(+), 15 deletions(-) diff --git a/dash/src/sml/masternode_list_engine/mod.rs b/dash/src/sml/masternode_list_engine/mod.rs index 11a2a2e3d..4009b2c8e 100644 --- a/dash/src/sml/masternode_list_engine/mod.rs +++ b/dash/src/sml/masternode_list_engine/mod.rs @@ -16,7 +16,6 @@ use crate::prelude::CoreBlockHeight; use crate::sml::error::SmlError; use crate::sml::llmq_entry_verification::LLMQEntryVerificationStatus; use crate::sml::llmq_type::LLMQType; -#[cfg(feature = "quorum_validation")] use crate::sml::llmq_type::network::NetworkLLMQExt; use crate::sml::masternode_list::MasternodeList; use crate::sml::masternode_list::from_diff::TryIntoWithBlockHashLookup; @@ -33,6 +32,10 @@ use hashes::Hash; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +/// Depth offset between cycle boundary and work block (matches Dash Core WORK_DIFF_DEPTH) +/// The mnListDiffH in QRInfo is at (cycle_height - WORK_DIFF_DEPTH), not at the cycle boundary itself +pub const WORK_DIFF_DEPTH: u32 = 8; + #[derive(Clone, Eq, PartialEq, Default)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "bincode", derive(Encode, Decode))] @@ -398,6 +401,53 @@ impl MasternodeListEngine { self.block_container.feed_block_height(height, block_hash) } + /// Extracts rotating quorums from a masternode list at a given work block height + /// and stores them by cycle boundary hash. + /// + /// This is used to store historical cycle quorums from QRInfo diffs (h-c, h-2c, h-3c, h-4c) + /// so that InstantLocks referencing previous cycles can be verified. + /// + /// # Parameters + /// - `work_block_hash`: The block hash at the "work" height (cycle_boundary - 8) + fn store_rotating_quorums_for_cycle(&mut self, work_block_hash: BlockHash) { + // Calculate the cycle boundary from the work block + let Some(work_block_height) = self.block_container.get_height(&work_block_hash) else { + return; + }; + + let cycle_boundary_height = work_block_height + WORK_DIFF_DEPTH; + let Some(cycle_boundary_hash) = self.block_container.get_hash(&cycle_boundary_height) + else { + return; + }; + + // Skip if we already have quorums for this cycle + if self.rotated_quorums_per_cycle.contains_key(cycle_boundary_hash) { + return; + } + + // Look up the masternode list at the work block height + // The masternode list at this height should have all active rotating quorums + let Some(mn_list) = self.masternode_lists.get(&work_block_height) else { + return; + }; + + // Get the ISD LLMQ type for this network + let isd_type = self.network.isd_llmq_type(); + + // Extract rotating quorums from the masternode list + let Some(quorums_of_type) = mn_list.quorums.get(&isd_type) else { + return; + }; + + let rotating_quorums: Vec = + quorums_of_type.values().cloned().collect(); + + if !rotating_quorums.is_empty() { + self.rotated_quorums_per_cycle.insert(*cycle_boundary_hash, rotating_quorums); + } + } + /// Requests and stores block heights for all hashes referenced in a QRInfo message. /// /// # Parameters @@ -588,6 +638,15 @@ impl MasternodeListEngine { .map(|quorum_entry| quorum_entry.llmq_type) .unwrap_or(self.network.isd_llmq_type()); + // Collect work block hashes for historical cycle quorum storage + // These will be used after diffs are applied to store rotating quorums from masternode lists + let work_block_hash_h_minus_4c = quorum_snapshot_and_mn_list_diff_at_h_minus_4c + .as_ref() + .map(|(_, diff)| diff.block_hash); + let work_block_hash_h_minus_3c = mn_list_diff_at_h_minus_3c.block_hash; + let work_block_hash_h_minus_2c = mn_list_diff_at_h_minus_2c.block_hash; + let work_block_hash_h_minus_c = mn_list_diff_at_h_minus_c.block_hash; + 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 { @@ -609,6 +668,9 @@ impl MasternodeListEngine { #[cfg(feature = "quorum_validation")] let mn_list_diff_at_h_minus_c_block_hash = mn_list_diff_at_h_minus_c.block_hash; let maybe_sigm1 = self.apply_diff(mn_list_diff_at_h_minus_c, None, false, None)?; + // Capture work block hash before diff is consumed (only needed for quorum_validation) + #[cfg(feature = "quorum_validation")] + let work_block_hash = mn_list_diff_h.block_hash; #[cfg(feature = "quorum_validation")] let mn_list_diff_at_h_block_hash = mn_list_diff_h.block_hash; let maybe_sigm0 = self.apply_diff(mn_list_diff_h, None, false, None)?; @@ -624,6 +686,33 @@ impl MasternodeListEngine { let maybe_sigmtip = self.apply_diff(mn_list_diff_tip, None, verify_tip_non_rotated_quorums, sigs)?; + // Store rotating quorums for historical cycles from the masternode lists + // This must happen after diffs are applied so the masternode lists have all quorums + if let Some(hash) = work_block_hash_h_minus_4c { + self.store_rotating_quorums_for_cycle(hash); + } + self.store_rotating_quorums_for_cycle(work_block_hash_h_minus_3c); + self.store_rotating_quorums_for_cycle(work_block_hash_h_minus_2c); + self.store_rotating_quorums_for_cycle(work_block_hash_h_minus_c); + + // Calculate cycle boundary hash from work block (only needed for quorum_validation) + #[cfg(feature = "quorum_validation")] + let cycle_boundary_hash = { + let work_block_height = self.block_container.get_height(&work_block_hash).ok_or( + QuorumValidationError::RequiredBlockNotPresent( + work_block_hash, + "getting work block height for cycle boundary calculation".to_string(), + ), + )?; + let cycle_boundary_height = work_block_height + WORK_DIFF_DEPTH; + *self.block_container.get_hash(&cycle_boundary_height).ok_or( + QuorumValidationError::RequiredBlockNotPresent( + BlockHash::all_zeros(), + format!("getting cycle boundary hash at height {}", cycle_boundary_height), + ), + )? + }; + #[cfg(feature = "quorum_validation")] let qualified_last_commitment_per_index = last_commitment_per_index .into_iter() @@ -682,12 +771,11 @@ impl MasternodeListEngine { LLMQEntryVerificationStatus, )> = Vec::new(); - let mut qualified_rotated_quorums_per_cycle = - qualified_last_commitment_per_index.first().map(|quorum_entry| { - self.rotated_quorums_per_cycle - .entry(quorum_entry.quorum_entry.quorum_hash) - .or_default() - }); + let qualified_rotated_quorums_per_cycle = + self.rotated_quorums_per_cycle.entry(cycle_boundary_hash).or_default(); + + // Clear existing quorums to prevent accumulation when feed_qr_info is called multiple times + qualified_rotated_quorums_per_cycle.clear(); for mut rotated_quorum in qualified_last_commitment_per_index { rotated_quorum.verified = validation_statuses @@ -695,7 +783,7 @@ impl MasternodeListEngine { .cloned() .unwrap_or_default(); - qualified_rotated_quorums_per_cycle.as_mut().unwrap().push(rotated_quorum.clone()); + qualified_rotated_quorums_per_cycle.push(rotated_quorum.clone()); // Store status updates separately to prevent multiple mutable borrows let masternode_lists_having_quorum_hash_for_quorum_type = @@ -807,13 +895,9 @@ impl MasternodeListEngine { } } } - } else if let Some(qualified_rotated_quorums_per_cycle) = - qualified_last_commitment_per_index.first().map(|quorum_entry| { - self.rotated_quorums_per_cycle - .entry(quorum_entry.quorum_entry.quorum_hash) - .or_default() - }) - { + } else { + let qualified_rotated_quorums_per_cycle = + self.rotated_quorums_per_cycle.entry(cycle_boundary_hash).or_default(); *qualified_rotated_quorums_per_cycle = qualified_last_commitment_per_index; }