@@ -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+
201233impl 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) ]
11361169mod 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 }
0 commit comments