@@ -707,6 +707,43 @@ impl MasternodeListEngine {
707707 hashes
708708 }
709709
710+ /// `rotated_quorums_per_cycle` is the authoritative map for IS lock
711+ /// verification, so only store a cycle when every entry is `Verified`.
712+ /// Skipped entries (e.g. from incomplete CL sigs or missing context) cannot
713+ /// sign and must not enter the map. A later QRInfo with complete context
714+ /// will store the cycle. Also preserve an already-fully-Verified cycle
715+ /// across subsequent QRInfo responses: a thin `mn_list_diff_h` can produce
716+ /// Skipped entries, and a `verify_rotated_quorums == false` call can leave
717+ /// entries unverified, neither of which must downgrade it.
718+ ///
719+ /// Returns the height of the stored cycle, or `None` if storage was
720+ /// skipped because the gate did not fire.
721+ #[ cfg( feature = "quorum_validation" ) ]
722+ fn store_cycle_if_fully_verified (
723+ & mut self ,
724+ cycle_key : BlockHash ,
725+ qualified_last_commitment_per_index : Vec < QualifiedQuorumEntry > ,
726+ rotation_quorum_type : LLMQType ,
727+ ) -> Result < Option < CoreBlockHeight > , QuorumValidationError > {
728+ let already_fully_verified =
729+ self . rotated_quorums_per_cycle . get ( & cycle_key) . is_some_and ( |existing| {
730+ !existing. is_empty ( )
731+ && existing
732+ . values ( )
733+ . all ( |q| matches ! ( q. verified, LLMQEntryVerificationStatus :: Verified ) )
734+ } ) ;
735+ let all_entries_verified = qualified_last_commitment_per_index
736+ . iter ( )
737+ . all ( |q| matches ! ( q. verified, LLMQEntryVerificationStatus :: Verified ) ) ;
738+ if !all_entries_verified || already_fully_verified {
739+ return Ok ( None ) ;
740+ }
741+ let cycle_map =
742+ build_cycle_quorum_map ( qualified_last_commitment_per_index, rotation_quorum_type) ?;
743+ * self . rotated_quorums_per_cycle . entry ( cycle_key) . or_default ( ) = cycle_map;
744+ Ok ( self . block_container . get_height ( & cycle_key) )
745+ }
746+
710747 /// Processes and applies a QRInfo message to the masternode list engine.
711748 ///
712749 /// The caller is expected to pre-populate [`Self::block_container`] with heights
@@ -974,32 +1011,11 @@ impl MasternodeListEngine {
9741011 . count ( ) ;
9751012
9761013 if let Some ( key) = cycle_key {
977- // `rotated_quorums_per_cycle` is the authoritative map for IS
978- // lock verification, so only store a cycle when every entry is
979- // `Verified`. Skipped entries (e.g. from incomplete CL sigs or
980- // missing context) cannot sign and must not enter the map. A
981- // later QRInfo with complete context will store the cycle.
982- // Also preserve an already-fully-Verified cycle across
983- // subsequent QRInfo responses: a thin `mn_list_diff_h` can
984- // produce Skipped entries that must not downgrade it.
985- let already_fully_verified =
986- self . rotated_quorums_per_cycle . get ( & key) . is_some_and ( |existing| {
987- !existing. is_empty ( )
988- && existing. values ( ) . all ( |q| {
989- matches ! ( q. verified, LLMQEntryVerificationStatus :: Verified )
990- } )
991- } ) ;
992- let all_entries_verified = qualified_last_commitment_per_index
993- . iter ( )
994- . all ( |q| matches ! ( q. verified, LLMQEntryVerificationStatus :: Verified ) ) ;
995- if all_entries_verified && !already_fully_verified {
996- let cycle_map = build_cycle_quorum_map (
997- qualified_last_commitment_per_index,
998- rotation_quorum_type,
999- ) ?;
1000- * self . rotated_quorums_per_cycle . entry ( key) . or_default ( ) = cycle_map;
1001- stored_cycle_height = self . block_container . get_height ( & key) ;
1002- }
1014+ stored_cycle_height = self . store_cycle_if_fully_verified (
1015+ key,
1016+ qualified_last_commitment_per_index,
1017+ rotation_quorum_type,
1018+ ) ?;
10031019 }
10041020
10051021 // Apply collected updates after iteration to avoid borrow conflicts
@@ -1098,26 +1114,11 @@ impl MasternodeListEngine {
10981114 . iter ( )
10991115 . filter ( |q| matches ! ( q. verified, LLMQEntryVerificationStatus :: Verified ) )
11001116 . count ( ) ;
1101- // Never overwrite an already-fully-Verified cycle with unverified entries from a
1102- // `verify_rotated_quorums == false` call.
1103- let already_fully_verified =
1104- self . rotated_quorums_per_cycle . get ( & cycle_key) . is_some_and ( |existing| {
1105- !existing. is_empty ( )
1106- && existing
1107- . values ( )
1108- . all ( |q| matches ! ( q. verified, LLMQEntryVerificationStatus :: Verified ) )
1109- } ) ;
1110- let all_entries_verified = qualified_last_commitment_per_index
1111- . iter ( )
1112- . all ( |q| matches ! ( q. verified, LLMQEntryVerificationStatus :: Verified ) ) ;
1113- if all_entries_verified && !already_fully_verified {
1114- let cycle_map = build_cycle_quorum_map (
1115- qualified_last_commitment_per_index,
1116- rotation_quorum_type,
1117- ) ?;
1118- * self . rotated_quorums_per_cycle . entry ( cycle_key) . or_default ( ) = cycle_map;
1119- stored_cycle_height = self . block_container . get_height ( & cycle_key) ;
1120- }
1117+ stored_cycle_height = self . store_cycle_if_fully_verified (
1118+ cycle_key,
1119+ qualified_last_commitment_per_index,
1120+ rotation_quorum_type,
1121+ ) ?;
11211122 }
11221123
11231124 #[ cfg( not( feature = "quorum_validation" ) ) ]
0 commit comments