Skip to content

Commit 25d0f45

Browse files
committed
fix: index rotated quorums by quorum_index and rebuild per cycle
The `Vec<QualifiedQuorumEntry>` had no size bound and no uniqueness check on `quorum_index`. Combined with QRInfo still being requested on every new block (will change soon), repeated `feed_qr_info` calls within a single rotation cycle accumulated duplicate entries that the lookup then returned as stale matches. - Switch the inner container to `BTreeMap<u16, QualifiedQuorumEntry>` keyed by `quorum_index` so insertion replaces by key, lookup is direct, and re-feeds of the same cycle are not causing wrong behaviour anymore. - Introduce `build_cycle_quorum_map` to centralize map construction with index validation and count checks. Both branches of `feed_qr_info` use it to rebuild the per-cycle map from scratch. - Migrates `tests/data/test_DML_diffs/masternode_list_engine.hex` to the new format.
1 parent 77dd87b commit 25d0f45

3 files changed

Lines changed: 155 additions & 88 deletions

File tree

dash/src/sml/masternode_list_engine/message_request_verification.rs

Lines changed: 14 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::collections::BTreeMap;
2+
13
use hashes::Hash;
24

35
use crate::hash_types::QuorumOrderingHash;
@@ -12,7 +14,7 @@ impl MasternodeListEngine {
1214
fn is_lock_potential_quorums(
1315
&self,
1416
instant_lock: &InstantLock,
15-
) -> Result<&Vec<QualifiedQuorumEntry>, MessageVerificationError> {
17+
) -> Result<&BTreeMap<u16, QualifiedQuorumEntry>, MessageVerificationError> {
1618
// Retrieve the cycle hash from the Instant Lock
1719
let cycle_hash = instant_lock.cyclehash;
1820

@@ -29,11 +31,11 @@ impl MasternodeListEngine {
2931
.ok_or(MessageVerificationError::CycleHashNotPresent(cycle_hash))?;
3032

3133
tracing::debug!("Found {} quorums for cyclehash {}", quorums.len(), cycle_hash);
32-
for q in quorums.iter() {
34+
for (idx, q) in quorums.iter() {
3335
tracing::debug!(
34-
" Quorum: hash={}, index={:?}, height at block_container={:?}",
36+
" Quorum: index={}, hash={}, height at block_container={:?}",
37+
idx,
3538
q.quorum_entry.quorum_hash,
36-
q.quorum_entry.quorum_index,
3739
self.block_container.get_height(&q.quorum_entry.quorum_hash)
3840
);
3941
}
@@ -126,16 +128,13 @@ impl MasternodeListEngine {
126128
quorum_index
127129
);
128130

129-
// Find the quorum by its quorum_index field.
130-
let quorum = quorums
131-
.iter()
132-
.find(|q| q.quorum_entry.quorum_index == Some(quorum_index as i16))
133-
.ok_or({
134-
MessageVerificationError::QuorumIndexNotFound(
135-
quorum_index as u16,
136-
instant_lock.cyclehash,
137-
)
138-
})?;
131+
// Look up the quorum by index.
132+
let quorum = quorums.get(&(quorum_index as u16)).ok_or({
133+
MessageVerificationError::QuorumIndexNotFound(
134+
quorum_index as u16,
135+
instant_lock.cyclehash,
136+
)
137+
})?;
139138

140139
tracing::debug!(
141140
"IS lock selected quorum: hash={}, index={:?}, public_key={}, verified={:?}",
@@ -540,50 +539,6 @@ mod tests {
540539
mn_list_engine.verify_chain_lock(&chain_lock).expect("expected to verify chain lock");
541540
}
542541

543-
/// Test that quorums are looked up by their quorum_index field, not by array position.
544-
#[test]
545-
pub fn is_lock_quorum_lookup_by_quorum_index_not_array_position() {
546-
let block_hex =
547-
include_str!("../../../tests/data/test_DML_diffs/masternode_list_engine.hex");
548-
let data = hex::decode(block_hex).expect("decode hex");
549-
let mut mn_list_engine: MasternodeListEngine =
550-
bincode::decode_from_slice(&data, bincode::config::standard())
551-
.expect("expected to decode")
552-
.0;
553-
554-
let lock_data = hex::decode("01018d53e7997ead57409750942af0d5e0aafc06f852a9a52308f4781b6a8220298f00000000c6f9d8c63dd15937ea70aaddb7890daad42c91bf6818e2bf76d183d6f2d9215b4b5f84978fad9dde7ab52bdcc0674be891e9029cc1ef0cb01200000000000000a27c98836c4c04653ab81eb4e07ddfc2c8c2c1036b75247969c05a4f25451cd78913a971f1899d9f2bddec9cf8e0104004f72f20c2856453e5aa3bcd2a8200670ec28feda38f67cc400fc72ef1966956656ec0765478c9d16e9a9e470c07f9ed").expect("expected valid hex");
555-
let lock: InstantLock = deserialize(lock_data.as_slice()).expect("expected to deserialize");
556-
557-
// Get the original result
558-
let (original_quorum, _, original_index) =
559-
mn_list_engine.is_lock_quorum(&lock).expect("expected to get quorum");
560-
let expected_quorum_hash = original_quorum.quorum_entry.quorum_hash;
561-
let expected_quorum_index = original_quorum.quorum_entry.quorum_index;
562-
assert_eq!(original_index, 23);
563-
assert_eq!(expected_quorum_index, Some(23));
564-
565-
// Reverse the quorum array order so array positions no longer match quorum indices
566-
let cycle_hash = lock.cyclehash;
567-
if let Some(quorums) = mn_list_engine.rotated_quorums_per_cycle.get_mut(&cycle_hash) {
568-
quorums.reverse();
569-
570-
// Verify the quorum with index 23 is no longer at position 23
571-
let quorum_at_pos_23 = quorums.get(23);
572-
assert!(
573-
quorum_at_pos_23.is_none()
574-
|| quorum_at_pos_23.unwrap().quorum_entry.quorum_index != Some(23),
575-
"after reversing, quorum at position 23 should not have quorum_index 23"
576-
);
577-
}
578-
579-
// The lookup should still find the correct quorum by its quorum_index field
580-
let (found_quorum, _, found_index) =
581-
mn_list_engine.is_lock_quorum(&lock).expect("expected to find quorum by index");
582-
assert_eq!(found_index, 23);
583-
assert_eq!(found_quorum.quorum_entry.quorum_hash, expected_quorum_hash);
584-
assert_eq!(found_quorum.quorum_entry.quorum_index, expected_quorum_index);
585-
}
586-
587542
/// Test that QuorumIndexNotFound error is returned when the required quorum index is missing.
588543
#[test]
589544
pub fn is_lock_quorum_not_found_error() {
@@ -607,7 +562,7 @@ mod tests {
607562
// Remove the quorum with index 23 from the cycle
608563
let cycle_hash = lock.cyclehash;
609564
if let Some(quorums) = mn_list_engine.rotated_quorums_per_cycle.get_mut(&cycle_hash) {
610-
quorums.retain(|q| q.quorum_entry.quorum_index != Some(23));
565+
quorums.remove(&23);
611566
}
612567

613568
// Now the lookup should return QuorumIndexNotFound

dash/src/sml/masternode_list_engine/mod.rs

Lines changed: 140 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ pub struct MasternodeListEngine {
173173
pub block_container: MasternodeListEngineBlockContainer,
174174
pub masternode_lists: BTreeMap<CoreBlockHeight, MasternodeList>,
175175
pub known_snapshots: BTreeMap<BlockHash, QuorumSnapshot>,
176-
pub rotated_quorums_per_cycle: BTreeMap<BlockHash, Vec<QualifiedQuorumEntry>>,
176+
pub rotated_quorums_per_cycle: BTreeMap<BlockHash, BTreeMap<u16, QualifiedQuorumEntry>>,
177177
#[allow(clippy::type_complexity)]
178178
pub quorum_statuses: BTreeMap<
179179
LLMQType,
@@ -198,6 +198,38 @@ impl Default for MasternodeListEngine {
198198
}
199199
}
200200

201+
/// Builds a per-cycle quorum map keyed by `quorum_index`.
202+
/// Rejects missing, negative, or duplicate indices and validates the final count.
203+
#[cfg(feature = "quorum_validation")]
204+
fn build_cycle_quorum_map(
205+
quorums: Vec<QualifiedQuorumEntry>,
206+
rotation_quorum_type: LLMQType,
207+
) -> Result<BTreeMap<u16, QualifiedQuorumEntry>, QuorumValidationError> {
208+
let mut map = BTreeMap::new();
209+
for quorum in quorums {
210+
let quorum_index = quorum.quorum_entry.quorum_index.ok_or(
211+
QuorumValidationError::RequiredQuorumIndexNotPresent(quorum.quorum_entry.quorum_hash),
212+
)?;
213+
let key = u16::try_from(quorum_index).map_err(|_| {
214+
QuorumValidationError::RequiredQuorumIndexNotPresent(quorum.quorum_entry.quorum_hash)
215+
})?;
216+
if map.contains_key(&key) {
217+
return Err(QuorumValidationError::CorruptedCodeExecution(format!(
218+
"duplicate quorum_index {key} in rotation cycle"
219+
)));
220+
}
221+
map.insert(key, quorum);
222+
}
223+
let expected = rotation_quorum_type.active_quorum_count() as usize;
224+
if map.len() != expected {
225+
return Err(QuorumValidationError::CorruptedCodeExecution(format!(
226+
"rotated quorums per cycle count mismatch: expected {expected}, got {}",
227+
map.len()
228+
)));
229+
}
230+
Ok(map)
231+
}
232+
201233
impl MasternodeListEngine {
202234
/// Creates a new MasternodeListEngine with the specified network configuration.
203235
///
@@ -384,7 +416,7 @@ impl MasternodeListEngine {
384416
self.rotated_quorums_per_cycle
385417
.values()
386418
.find_map(|qualified_entries| {
387-
qualified_entries.iter().find(|qualified_entry| {
419+
qualified_entries.values().find(|qualified_entry| {
388420
qualified_entry.quorum_entry.quorum_hash == quorum_entry.quorum_hash
389421
&& qualified_entry.quorum_entry.llmq_type == quorum_entry.llmq_type
390422
})
@@ -628,7 +660,7 @@ impl MasternodeListEngine {
628660
self.apply_diff(mn_list_diff_tip, None, verify_tip_non_rotated_quorums, sigs)?;
629661

630662
#[cfg(feature = "quorum_validation")]
631-
let qualified_last_commitment_per_index = last_commitment_per_index
663+
let mut qualified_last_commitment_per_index = last_commitment_per_index
632664
.into_iter()
633665
.map(|quorum_entry| {
634666
if let Some(qualified_quorum_entry) =
@@ -685,27 +717,22 @@ impl MasternodeListEngine {
685717
LLMQEntryVerificationStatus,
686718
)> = Vec::new();
687719

688-
let mut qualified_rotated_quorums_per_cycle =
689-
qualified_last_commitment_per_index.first().map(|quorum_entry| {
690-
self.rotated_quorums_per_cycle
691-
.entry(quorum_entry.quorum_entry.quorum_hash)
692-
.or_default()
693-
});
720+
let cycle_key =
721+
qualified_last_commitment_per_index.first().map(|q| q.quorum_entry.quorum_hash);
694722

695-
for mut rotated_quorum in qualified_last_commitment_per_index {
723+
for rotated_quorum in qualified_last_commitment_per_index.iter_mut() {
696724
tracing::debug!(
697-
" Current cycle quorum: hash={}, index={:?}",
725+
" Current cycle quorum: hash={}, raw_quorum_index={:?}, map_key={:?}",
698726
rotated_quorum.quorum_entry.quorum_hash,
699-
rotated_quorum.quorum_entry.quorum_index
727+
rotated_quorum.quorum_entry.quorum_index,
728+
rotated_quorum.quorum_entry.quorum_index.and_then(|i| u16::try_from(i).ok())
700729
);
701730

702731
rotated_quorum.verified = validation_statuses
703732
.get(&rotated_quorum.quorum_entry.quorum_hash)
704733
.cloned()
705734
.unwrap_or_default();
706735

707-
qualified_rotated_quorums_per_cycle.as_mut().unwrap().push(rotated_quorum.clone());
708-
709736
// Store status updates separately to prevent multiple mutable borrows
710737
let masternode_lists_having_quorum_hash_for_quorum_type =
711738
self.quorum_statuses.entry(rotated_quorum.quorum_entry.llmq_type).or_default();
@@ -727,6 +754,14 @@ impl MasternodeListEngine {
727754
*status = rotated_quorum.verified.clone();
728755
}
729756

757+
if let Some(key) = cycle_key {
758+
let cycle_map = build_cycle_quorum_map(
759+
qualified_last_commitment_per_index,
760+
rotation_quorum_type,
761+
)?;
762+
*self.rotated_quorums_per_cycle.entry(key).or_default() = cycle_map;
763+
}
764+
730765
// Apply collected updates after iteration to avoid borrow conflicts
731766
for (heights, quorum_type, quorum_hash, new_status) in updates {
732767
for height in heights {
@@ -816,14 +851,12 @@ impl MasternodeListEngine {
816851
}
817852
}
818853
}
819-
} else if let Some(qualified_rotated_quorums_per_cycle) =
820-
qualified_last_commitment_per_index.first().map(|quorum_entry| {
821-
self.rotated_quorums_per_cycle
822-
.entry(quorum_entry.quorum_entry.quorum_hash)
823-
.or_default()
824-
})
854+
} else if let Some(cycle_key) =
855+
qualified_last_commitment_per_index.first().map(|q| q.quorum_entry.quorum_hash)
825856
{
826-
*qualified_rotated_quorums_per_cycle = qualified_last_commitment_per_index;
857+
let cycle_map =
858+
build_cycle_quorum_map(qualified_last_commitment_per_index, rotation_quorum_type)?;
859+
*self.rotated_quorums_per_cycle.entry(cycle_key).or_default() = cycle_map;
827860
}
828861

829862
#[cfg(not(feature = "quorum_validation"))]
@@ -1065,7 +1098,7 @@ impl MasternodeListEngine {
10651098
&& let Some(cycle_quorums) = self.rotated_quorums_per_cycle.get(&cycle_hash)
10661099
{
10671100
// Only update rotating quorum statuses based on last commitment entries
1068-
for quorum in cycle_quorums {
1101+
for quorum in cycle_quorums.values() {
10691102
if let Some(quorum_entry) =
10701103
hash_to_quorum_entries.get_mut(&quorum.quorum_entry.quorum_hash)
10711104
{
@@ -1134,9 +1167,8 @@ impl MasternodeListEngine {
11341167

11351168
#[cfg(test)]
11361169
mod tests {
1137-
use crate::BlockHash;
1138-
use crate::Network;
11391170
use crate::consensus::deserialize;
1171+
use crate::hashes::Hash;
11401172
use crate::network::message_qrinfo::QRInfo;
11411173
use crate::network::message_sml::MnListDiff;
11421174
use crate::prelude::CoreBlockHeight;
@@ -1149,10 +1181,87 @@ mod tests {
11491181
use crate::sml::masternode_list_engine::{
11501182
MasternodeListEngine, MasternodeListEngineBlockContainer,
11511183
};
1152-
use crate::sml::quorum_entry::qualified_quorum_entry::VerifyingChainLockSignaturesType;
1184+
use crate::sml::quorum_entry::qualified_quorum_entry::{
1185+
QualifiedQuorumEntry, VerifyingChainLockSignaturesType,
1186+
};
11531187
use crate::sml::quorum_validation_error::ClientDataRetrievalError;
1188+
#[cfg(feature = "quorum_validation")]
1189+
use crate::sml::quorum_validation_error::QuorumValidationError;
1190+
use crate::{BlockHash, Network};
11541191
use std::collections::BTreeMap;
11551192

1193+
#[cfg(feature = "quorum_validation")]
1194+
use {
1195+
super::build_cycle_quorum_map,
1196+
crate::QuorumHash,
1197+
crate::bls_sig_utils::{BLSPublicKey, BLSSignature},
1198+
crate::hash_types::QuorumVVecHash,
1199+
crate::transaction::special_transaction::quorum_commitment::QuorumEntry,
1200+
};
1201+
1202+
#[cfg(feature = "quorum_validation")]
1203+
fn make_qualified_quorum_entry(
1204+
llmq_type: LLMQType,
1205+
quorum_index: Option<i16>,
1206+
) -> QualifiedQuorumEntry {
1207+
QuorumEntry {
1208+
version: 2,
1209+
llmq_type,
1210+
quorum_hash: QuorumHash::all_zeros(),
1211+
quorum_index,
1212+
signers: vec![true],
1213+
valid_members: vec![true],
1214+
quorum_public_key: BLSPublicKey::from([0; 48]),
1215+
quorum_vvec_hash: QuorumVVecHash::all_zeros(),
1216+
threshold_sig: BLSSignature::from([0; 96]),
1217+
all_commitment_aggregated_signature: BLSSignature::from([0; 96]),
1218+
}
1219+
.into()
1220+
}
1221+
1222+
#[cfg(feature = "quorum_validation")]
1223+
#[test]
1224+
fn build_cycle_quorum_map_edge_cases() {
1225+
let ty = LLMQType::LlmqtypeTest;
1226+
assert_eq!(ty.active_quorum_count(), 2, "test assumes active_quorum_count == 2");
1227+
1228+
// Valid: two quorums with distinct indices
1229+
let quorums = vec![
1230+
make_qualified_quorum_entry(ty, Some(0)),
1231+
make_qualified_quorum_entry(ty, Some(1)),
1232+
];
1233+
let map = build_cycle_quorum_map(quorums, ty).expect("valid quorums should succeed");
1234+
assert_eq!(map.len(), 2);
1235+
assert!(map.contains_key(&0) && map.contains_key(&1));
1236+
1237+
// Missing index is rejected
1238+
let quorums =
1239+
vec![make_qualified_quorum_entry(ty, Some(0)), make_qualified_quorum_entry(ty, None)];
1240+
let err = build_cycle_quorum_map(quorums, ty).expect_err("missing index should fail");
1241+
assert!(matches!(err, QuorumValidationError::RequiredQuorumIndexNotPresent(_)));
1242+
1243+
// Negative index is rejected
1244+
let quorums = vec![
1245+
make_qualified_quorum_entry(ty, Some(0)),
1246+
make_qualified_quorum_entry(ty, Some(-1)),
1247+
];
1248+
let err = build_cycle_quorum_map(quorums, ty).expect_err("negative index should fail");
1249+
assert!(matches!(err, QuorumValidationError::RequiredQuorumIndexNotPresent(_)));
1250+
1251+
// Duplicate index is rejected
1252+
let quorums = vec![
1253+
make_qualified_quorum_entry(ty, Some(0)),
1254+
make_qualified_quorum_entry(ty, Some(0)),
1255+
];
1256+
let err = build_cycle_quorum_map(quorums, ty).expect_err("duplicate index should fail");
1257+
assert!(matches!(err, QuorumValidationError::CorruptedCodeExecution(_)));
1258+
1259+
// Wrong count is rejected
1260+
let quorums = vec![make_qualified_quorum_entry(ty, Some(0))];
1261+
let err = build_cycle_quorum_map(quorums, ty).expect_err("wrong count should fail");
1262+
assert!(matches!(err, QuorumValidationError::CorruptedCodeExecution(_)));
1263+
}
1264+
11561265
fn verify_masternode_list_quorums(
11571266
mn_list_engine: &MasternodeListEngine,
11581267
masternode_list: &MasternodeList,
@@ -1413,9 +1522,12 @@ mod tests {
14131522
.0;
14141523

14151524
for (cycle_hash, quorums) in mn_list_engine.rotated_quorums_per_cycle.iter() {
1416-
for (i, quorum) in quorums.iter().enumerate() {
1525+
for (index, quorum) in quorums.iter() {
14171526
mn_list_engine.validate_quorum(quorum).unwrap_or_else(|_| {
1418-
panic!("expected to validate quorum {} in cycle hash {}", i, cycle_hash)
1527+
panic!(
1528+
"expected to validate quorum at index {} in cycle hash {}",
1529+
index, cycle_hash
1530+
)
14191531
});
14201532
}
14211533
}
@@ -1433,7 +1545,7 @@ mod tests {
14331545

14341546
for quorums in mn_list_engine.rotated_quorums_per_cycle.values() {
14351547
mn_list_engine
1436-
.validate_rotation_cycle_quorums(quorums.iter().collect::<Vec<_>>().as_slice())
1548+
.validate_rotation_cycle_quorums(quorums.values().collect::<Vec<_>>().as_slice())
14371549
.expect("expected to validated quorums");
14381550
}
14391551
}

dash/tests/data/test_DML_diffs/masternode_list_engine.hex

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)