From fc17daa23e19713876f7041d94221d51ec28861e Mon Sep 17 00:00:00 2001 From: xdustinface Date: Sun, 3 May 2026 14:22:48 +1000 Subject: [PATCH 01/17] fix(dash): classify missing infrastructure errors as `Skipped` `QuorumValidationError` carries both "the quorum data is bad" cases and "the caller didn't supply the data we need to verify". `update_quorum_status` and the rotated-quorum lookup in `validation.rs` mapped several of the latter to `LLMQEntryVerificationStatus::Invalid`, misreporting unverifiable quorums as bad. Add a `From for LLMQEntryVerificationStatus` impl that routes the infrastructure-data variants to `Skipped`, and let both call sites use `.into()`: - `RequiredBlockHeightNotPresent` becomes `Skipped(MissedList)`. - `RequiredSnapshotNotPresent` becomes `Skipped(UnknownBlock)`. - `RequiredRotatedChainLockSigsNotPresent` becomes a new `Skipped(MissingRotationChainLockSigs)` variant for the QRInfo case where a historical diff covers a range with no successful rotating DKG, so `feed_qr_info` can't populate the 4-sig tuple. All other variants still map to `Invalid`. --- dash/src/sml/llmq_entry_verification.rs | 36 +++++++++++++++++++ .../sml/masternode_list_engine/validation.rs | 6 ++-- .../quorum_entry/qualified_quorum_entry.rs | 22 +++--------- 3 files changed, 42 insertions(+), 22 deletions(-) diff --git a/dash/src/sml/llmq_entry_verification.rs b/dash/src/sml/llmq_entry_verification.rs index 69568bbff..4584c84f9 100644 --- a/dash/src/sml/llmq_entry_verification.rs +++ b/dash/src/sml/llmq_entry_verification.rs @@ -14,6 +14,13 @@ pub enum LLMQEntryVerificationSkipStatus { NotMarkedForVerification, MissedList(CoreBlockHeight), UnknownBlock(BlockHash), + /// The quorum entry came through without an attached + /// `VerifyingChainLockSignaturesType::Rotating`. Typically happens when + /// a QRInfo's historical diff covers a block range in which no rotating + /// DKG successfully committed, so `apply_diff` extracts no + /// `rotation_sig` and `feed_qr_info` can't populate the 4-sig tuple for + /// the quorums in `lastCommitmentPerIndex`. + MissingRotationChainLockSigs(BlockHash), OtherContext(String), } @@ -30,6 +37,9 @@ impl Display for LLMQEntryVerificationSkipStatus { LLMQEntryVerificationSkipStatus::UnknownBlock(block_hash) => { format!("UnknownBlock({})", block_hash) } + LLMQEntryVerificationSkipStatus::MissingRotationChainLockSigs(quorum_hash) => { + format!("MissingRotationChainLockSigs({})", quorum_hash) + } LLMQEntryVerificationSkipStatus::OtherContext(message) => { format!("OtherContext({message})") } @@ -49,6 +59,32 @@ pub enum LLMQEntryVerificationStatus { Skipped(LLMQEntryVerificationSkipStatus), Invalid(QuorumValidationError), } +impl From for LLMQEntryVerificationStatus { + /// Classify a validation error as either `Skipped` (missing infrastructure + /// data that the caller should have provided) or `Invalid` (the quorum + /// data itself is genuinely bad). + fn from(error: QuorumValidationError) -> Self { + match error { + QuorumValidationError::RequiredBlockNotPresent(block_hash, _) => { + Self::Skipped(LLMQEntryVerificationSkipStatus::UnknownBlock(block_hash)) + } + QuorumValidationError::RequiredMasternodeListNotPresent(height) + | QuorumValidationError::RequiredBlockHeightNotPresent(height) => { + Self::Skipped(LLMQEntryVerificationSkipStatus::MissedList(height)) + } + QuorumValidationError::RequiredSnapshotNotPresent(hash) => { + Self::Skipped(LLMQEntryVerificationSkipStatus::UnknownBlock(hash)) + } + QuorumValidationError::RequiredRotatedChainLockSigsNotPresent(quorum_hash) => { + Self::Skipped(LLMQEntryVerificationSkipStatus::MissingRotationChainLockSigs( + quorum_hash, + )) + } + other => Self::Invalid(other), + } + } +} + impl Display for LLMQEntryVerificationStatus { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_str( diff --git a/dash/src/sml/masternode_list_engine/validation.rs b/dash/src/sml/masternode_list_engine/validation.rs index 011989c7b..b84a4a4ad 100644 --- a/dash/src/sml/masternode_list_engine/validation.rs +++ b/dash/src/sml/masternode_list_engine/validation.rs @@ -109,11 +109,9 @@ impl MasternodeListEngine { let masternodes_by_quorum_hash = match self.find_rotated_masternodes_for_quorums(quorums) { Ok(masternodes_by_quorum_hash) => masternodes_by_quorum_hash, Err(e) => { + let status: LLMQEntryVerificationStatus = e.into(); for quorum in quorums { - return_statuses.insert( - quorum.quorum_entry.quorum_hash, - LLMQEntryVerificationStatus::Invalid(e.clone()), - ); + return_statuses.insert(quorum.quorum_entry.quorum_hash, status.clone()); } return return_statuses; } diff --git a/dash/src/sml/quorum_entry/qualified_quorum_entry.rs b/dash/src/sml/quorum_entry/qualified_quorum_entry.rs index f9d29250f..a9571a79c 100644 --- a/dash/src/sml/quorum_entry/qualified_quorum_entry.rs +++ b/dash/src/sml/quorum_entry/qualified_quorum_entry.rs @@ -66,23 +66,9 @@ impl QualifiedQuorumEntry { /// /// * `result` - A `Result` containing either success (`Ok`) or a `QuorumValidationError`. pub fn update_quorum_status(&mut self, result: Result<(), QuorumValidationError>) { - match result { - Err(QuorumValidationError::RequiredBlockNotPresent(block_hash, _)) => { - self.verified = LLMQEntryVerificationStatus::Skipped( - LLMQEntryVerificationSkipStatus::UnknownBlock(block_hash), - ); - } - Err(QuorumValidationError::RequiredMasternodeListNotPresent(block_height)) => { - self.verified = LLMQEntryVerificationStatus::Skipped( - LLMQEntryVerificationSkipStatus::MissedList(block_height), - ); - } - Err(e) => { - self.verified = LLMQEntryVerificationStatus::Invalid(e); - } - Ok(_) => { - self.verified = LLMQEntryVerificationStatus::Verified; - } - } + self.verified = match result { + Ok(_) => LLMQEntryVerificationStatus::Verified, + Err(e) => e.into(), + }; } } From b7ee408d7ed1c3725122a9b23a524c2a4bffa9d5 Mon Sep 17 00:00:00 2001 From: xdustinface Date: Mon, 4 May 2026 18:07:58 +1000 Subject: [PATCH 02/17] refactor: use `QuorumHash` for `MissingRotationChainLockSigs` variant Addresses manki review comment on PR #125 https://github.com/xdustinface/rust-dashcore/pull/125#discussion_r3179193651 --- dash/src/sml/llmq_entry_verification.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dash/src/sml/llmq_entry_verification.rs b/dash/src/sml/llmq_entry_verification.rs index 4584c84f9..130f009bc 100644 --- a/dash/src/sml/llmq_entry_verification.rs +++ b/dash/src/sml/llmq_entry_verification.rs @@ -3,9 +3,9 @@ use core::fmt::{Display, Formatter}; #[cfg(feature = "bincode")] use bincode::{Decode, Encode}; -use crate::BlockHash; use crate::prelude::CoreBlockHeight; use crate::sml::quorum_validation_error::QuorumValidationError; +use crate::{BlockHash, QuorumHash}; #[derive(Clone, Ord, PartialOrd, PartialEq, Eq, Hash, Debug)] #[cfg_attr(feature = "bincode", derive(Encode, Decode))] @@ -20,7 +20,7 @@ pub enum LLMQEntryVerificationSkipStatus { /// DKG successfully committed, so `apply_diff` extracts no /// `rotation_sig` and `feed_qr_info` can't populate the 4-sig tuple for /// the quorums in `lastCommitmentPerIndex`. - MissingRotationChainLockSigs(BlockHash), + MissingRotationChainLockSigs(QuorumHash), OtherContext(String), } From e9ad1c0ff021761c5ad9647ddc208703f93f016a Mon Sep 17 00:00:00 2001 From: xdustinface Date: Mon, 4 May 2026 18:08:37 +1000 Subject: [PATCH 03/17] refactor: add `MissingSnapshot` skip-status variant Distinguish missing snapshot from missing block in `LLMQEntryVerificationSkipStatus` so downstream retry/back-off logic can target snapshot fetches separately. Mirrors the existing `MissingRotationChainLockSigs` pattern. Addresses manki review comment on PR #125 https://github.com/xdustinface/rust-dashcore/pull/125#discussion_r3179193647 --- dash/src/sml/llmq_entry_verification.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/dash/src/sml/llmq_entry_verification.rs b/dash/src/sml/llmq_entry_verification.rs index 130f009bc..87f5399c0 100644 --- a/dash/src/sml/llmq_entry_verification.rs +++ b/dash/src/sml/llmq_entry_verification.rs @@ -14,6 +14,10 @@ pub enum LLMQEntryVerificationSkipStatus { NotMarkedForVerification, MissedList(CoreBlockHeight), UnknownBlock(BlockHash), + /// The snapshot required to validate this quorum entry was not provided + /// by the caller. Distinct from `UnknownBlock` so retry/back-off logic + /// can target snapshot fetches separately from block fetches. + MissingSnapshot(BlockHash), /// The quorum entry came through without an attached /// `VerifyingChainLockSignaturesType::Rotating`. Typically happens when /// a QRInfo's historical diff covers a block range in which no rotating @@ -37,6 +41,9 @@ impl Display for LLMQEntryVerificationSkipStatus { LLMQEntryVerificationSkipStatus::UnknownBlock(block_hash) => { format!("UnknownBlock({})", block_hash) } + LLMQEntryVerificationSkipStatus::MissingSnapshot(block_hash) => { + format!("MissingSnapshot({})", block_hash) + } LLMQEntryVerificationSkipStatus::MissingRotationChainLockSigs(quorum_hash) => { format!("MissingRotationChainLockSigs({})", quorum_hash) } @@ -73,7 +80,7 @@ impl From for LLMQEntryVerificationStatus { Self::Skipped(LLMQEntryVerificationSkipStatus::MissedList(height)) } QuorumValidationError::RequiredSnapshotNotPresent(hash) => { - Self::Skipped(LLMQEntryVerificationSkipStatus::UnknownBlock(hash)) + Self::Skipped(LLMQEntryVerificationSkipStatus::MissingSnapshot(hash)) } QuorumValidationError::RequiredRotatedChainLockSigsNotPresent(quorum_hash) => { Self::Skipped(LLMQEntryVerificationSkipStatus::MissingRotationChainLockSigs( From e064ef8b332b4036a7ab9fb1c96a530dc1835fa1 Mon Sep 17 00:00:00 2001 From: xdustinface Date: Mon, 4 May 2026 18:10:40 +1000 Subject: [PATCH 04/17] test: cover `From` classification arms Adds unit tests for each match arm of the `LLMQEntryVerificationStatus` conversion impl so future rearrangements cannot silently mis-classify infrastructure errors as `Invalid` (or vice versa). Addresses manki review comment on PR #125 https://github.com/xdustinface/rust-dashcore/pull/125#discussion_r3179193654 --- dash/src/sml/llmq_entry_verification.rs | 80 +++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/dash/src/sml/llmq_entry_verification.rs b/dash/src/sml/llmq_entry_verification.rs index 87f5399c0..d3ee91698 100644 --- a/dash/src/sml/llmq_entry_verification.rs +++ b/dash/src/sml/llmq_entry_verification.rs @@ -105,3 +105,83 @@ impl Display for LLMQEntryVerificationStatus { ) } } + +#[cfg(test)] +mod tests { + use hashes::Hash; + + use super::*; + + fn dummy_hash(byte: u8) -> BlockHash { + BlockHash::from_byte_array([byte; 32]) + } + + #[test] + fn required_block_not_present_maps_to_skipped_unknown_block() { + let hash = dummy_hash(1); + let status: LLMQEntryVerificationStatus = + QuorumValidationError::RequiredBlockNotPresent(hash, "ctx".to_string()).into(); + assert_eq!( + status, + LLMQEntryVerificationStatus::Skipped(LLMQEntryVerificationSkipStatus::UnknownBlock( + hash, + )) + ); + } + + #[test] + fn required_masternode_list_not_present_maps_to_skipped_missed_list() { + let status: LLMQEntryVerificationStatus = + QuorumValidationError::RequiredMasternodeListNotPresent(42).into(); + assert_eq!( + status, + LLMQEntryVerificationStatus::Skipped(LLMQEntryVerificationSkipStatus::MissedList(42)) + ); + } + + #[test] + fn required_block_height_not_present_maps_to_skipped_missed_list() { + let status: LLMQEntryVerificationStatus = + QuorumValidationError::RequiredBlockHeightNotPresent(99).into(); + assert_eq!( + status, + LLMQEntryVerificationStatus::Skipped(LLMQEntryVerificationSkipStatus::MissedList(99)) + ); + } + + #[test] + fn required_snapshot_not_present_maps_to_skipped_missing_snapshot() { + let hash = dummy_hash(2); + let status: LLMQEntryVerificationStatus = + QuorumValidationError::RequiredSnapshotNotPresent(hash).into(); + assert_eq!( + status, + LLMQEntryVerificationStatus::Skipped(LLMQEntryVerificationSkipStatus::MissingSnapshot( + hash, + )) + ); + } + + #[test] + fn required_rotated_chain_lock_sigs_not_present_maps_to_skipped() { + let hash = dummy_hash(3); + let status: LLMQEntryVerificationStatus = + QuorumValidationError::RequiredRotatedChainLockSigsNotPresent(hash).into(); + assert_eq!( + status, + LLMQEntryVerificationStatus::Skipped( + LLMQEntryVerificationSkipStatus::MissingRotationChainLockSigs(hash), + ) + ); + } + + #[test] + fn other_error_maps_to_invalid() { + let status: LLMQEntryVerificationStatus = + QuorumValidationError::InvalidQuorumPublicKey.into(); + assert_eq!( + status, + LLMQEntryVerificationStatus::Invalid(QuorumValidationError::InvalidQuorumPublicKey) + ); + } +} From 524933990934baf0d608979153e7fa4ae28e2ccc Mon Sep 17 00:00:00 2001 From: xdustinface Date: Mon, 4 May 2026 20:59:33 +1000 Subject: [PATCH 05/17] refactor: classify three more missing-infrastructure errors as `Skipped` Adds From-impl arms for `RequiredRotatedChainLockSigNotPresent` (singular), `RequiredChainLockNotPresent`, and `VerifyingMasternodeListNotPresent`, all of which represent missing caller-provided infrastructure rather than bad quorum data. Without these arms they incorrectly fell through to `Invalid`, which would cause callers to treat infrastructure-missing situations as genuine quorum corruption. Introduces a singular `MissingRotationChainLockSig(u8, BlockHash)` skip variant mirroring the singular/plural pair on `QuorumValidationError`. The two emit sites carry genuinely different shapes (per-offset diff hash for the singular case, quorum hash for the plural), so collapsing them into one variant would be lossy. Addresses CodeRabbit review comment on PR #125 https://github.com/xdustinface/rust-dashcore/pull/125#discussion_r3180217611 --- dash/src/sml/llmq_entry_verification.rs | 59 ++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/dash/src/sml/llmq_entry_verification.rs b/dash/src/sml/llmq_entry_verification.rs index d3ee91698..579856b4d 100644 --- a/dash/src/sml/llmq_entry_verification.rs +++ b/dash/src/sml/llmq_entry_verification.rs @@ -25,6 +25,12 @@ pub enum LLMQEntryVerificationSkipStatus { /// `rotation_sig` and `feed_qr_info` can't populate the 4-sig tuple for /// the quorums in `lastCommitmentPerIndex`. MissingRotationChainLockSigs(QuorumHash), + /// A specific rotation chain-lock signature at offset `h - n` was not + /// present for the masternode diff at the given block hash. The first + /// field is the rotation offset, the second is the diff block hash. + /// Distinct from `MissingRotationChainLockSigs`, which covers the case + /// where the entire 4-sig tuple is absent. + MissingRotationChainLockSig(u8, BlockHash), OtherContext(String), } @@ -47,6 +53,12 @@ impl Display for LLMQEntryVerificationSkipStatus { LLMQEntryVerificationSkipStatus::MissingRotationChainLockSigs(quorum_hash) => { format!("MissingRotationChainLockSigs({})", quorum_hash) } + LLMQEntryVerificationSkipStatus::MissingRotationChainLockSig( + offset, + block_hash, + ) => { + format!("MissingRotationChainLockSig(h - {}, {})", offset, block_hash) + } LLMQEntryVerificationSkipStatus::OtherContext(message) => { format!("OtherContext({message})") } @@ -76,17 +88,26 @@ impl From for LLMQEntryVerificationStatus { Self::Skipped(LLMQEntryVerificationSkipStatus::UnknownBlock(block_hash)) } QuorumValidationError::RequiredMasternodeListNotPresent(height) - | QuorumValidationError::RequiredBlockHeightNotPresent(height) => { + | QuorumValidationError::RequiredBlockHeightNotPresent(height) + | QuorumValidationError::VerifyingMasternodeListNotPresent(height) => { Self::Skipped(LLMQEntryVerificationSkipStatus::MissedList(height)) } QuorumValidationError::RequiredSnapshotNotPresent(hash) => { Self::Skipped(LLMQEntryVerificationSkipStatus::MissingSnapshot(hash)) } + QuorumValidationError::RequiredChainLockNotPresent(_, block_hash) => { + Self::Skipped(LLMQEntryVerificationSkipStatus::UnknownBlock(block_hash)) + } QuorumValidationError::RequiredRotatedChainLockSigsNotPresent(quorum_hash) => { Self::Skipped(LLMQEntryVerificationSkipStatus::MissingRotationChainLockSigs( quorum_hash, )) } + QuorumValidationError::RequiredRotatedChainLockSigNotPresent(offset, block_hash) => { + Self::Skipped(LLMQEntryVerificationSkipStatus::MissingRotationChainLockSig( + offset, block_hash, + )) + } other => Self::Invalid(other), } } @@ -175,6 +196,42 @@ mod tests { ); } + #[test] + fn required_rotated_chain_lock_sig_not_present_maps_to_skipped() { + let hash = dummy_hash(4); + let status: LLMQEntryVerificationStatus = + QuorumValidationError::RequiredRotatedChainLockSigNotPresent(2, hash).into(); + assert_eq!( + status, + LLMQEntryVerificationStatus::Skipped( + LLMQEntryVerificationSkipStatus::MissingRotationChainLockSig(2, hash), + ) + ); + } + + #[test] + fn required_chain_lock_not_present_maps_to_skipped_unknown_block() { + let hash = dummy_hash(5); + let status: LLMQEntryVerificationStatus = + QuorumValidationError::RequiredChainLockNotPresent(7, hash).into(); + assert_eq!( + status, + LLMQEntryVerificationStatus::Skipped(LLMQEntryVerificationSkipStatus::UnknownBlock( + hash, + )) + ); + } + + #[test] + fn verifying_masternode_list_not_present_maps_to_skipped_missed_list() { + let status: LLMQEntryVerificationStatus = + QuorumValidationError::VerifyingMasternodeListNotPresent(123).into(); + assert_eq!( + status, + LLMQEntryVerificationStatus::Skipped(LLMQEntryVerificationSkipStatus::MissedList(123)) + ); + } + #[test] fn other_error_maps_to_invalid() { let status: LLMQEntryVerificationStatus = From 2eaa1a89e5dafad4ca1bc198448eb0e63c80c4ae Mon Sep 17 00:00:00 2001 From: xdustinface Date: Mon, 4 May 2026 21:47:32 +1000 Subject: [PATCH 06/17] fix: preserve `Invalid` statuses on infrastructure-error overwrite The bulk-overwrite path in `validate_rotation_cycle_quorums_validation_statuses` unconditionally replaced every quorum's status when `find_rotated_masternodes_for_quorums` errored, including entries the structure-validation pass had already marked as `Invalid`. Now that infrastructure errors map to `Skipped`, this would silently downgrade structurally-broken quorums from `Invalid` to `Skipped`, telling the caller to retry with more data instead of rejecting the quorum. Skip entries that are already `Invalid` in the overwrite loop. Addresses manki review comment on PR #125 https://github.com/xdustinface/rust-dashcore/pull/125#discussion_r3181131413 --- dash/src/sml/masternode_list_engine/validation.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dash/src/sml/masternode_list_engine/validation.rs b/dash/src/sml/masternode_list_engine/validation.rs index b84a4a4ad..687537356 100644 --- a/dash/src/sml/masternode_list_engine/validation.rs +++ b/dash/src/sml/masternode_list_engine/validation.rs @@ -111,6 +111,12 @@ impl MasternodeListEngine { Err(e) => { let status: LLMQEntryVerificationStatus = e.into(); for quorum in quorums { + if matches!( + return_statuses.get(&quorum.quorum_entry.quorum_hash), + Some(LLMQEntryVerificationStatus::Invalid(_)) + ) { + continue; + } return_statuses.insert(quorum.quorum_entry.quorum_hash, status.clone()); } return return_statuses; From 0de52820902c60f64455677dfb0b058b078b18c6 Mon Sep 17 00:00:00 2001 From: xdustinface Date: Mon, 4 May 2026 21:48:48 +1000 Subject: [PATCH 07/17] refactor: add `MissingChainLock(CoreBlockHeight, BlockHash)` skip variant Map `RequiredChainLockNotPresent` to a dedicated `MissingChainLock` skip variant carrying both the expected height and the block hash, instead of folding it into `UnknownBlock` and discarding the height. This mirrors the already-added `MissingSnapshot` pattern and lets retry logic dispatch a chain-lock fetch instead of a block fetch. Addresses manki review comments on PR #125 https://github.com/xdustinface/rust-dashcore/pull/125#discussion_r3181131428 https://github.com/xdustinface/rust-dashcore/pull/125#discussion_r3181131444 --- dash/src/sml/llmq_entry_verification.rs | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/dash/src/sml/llmq_entry_verification.rs b/dash/src/sml/llmq_entry_verification.rs index 579856b4d..c36026637 100644 --- a/dash/src/sml/llmq_entry_verification.rs +++ b/dash/src/sml/llmq_entry_verification.rs @@ -18,6 +18,12 @@ pub enum LLMQEntryVerificationSkipStatus { /// by the caller. Distinct from `UnknownBlock` so retry/back-off logic /// can target snapshot fetches separately from block fetches. MissingSnapshot(BlockHash), + /// The chain lock at the given height/block hash was not provided by the + /// caller. The block itself may be known; the chain-lock signature for + /// it just hasn't been fetched yet. Distinct from `UnknownBlock` so + /// retry logic can dispatch to a chain-lock fetch instead of a block + /// fetch. + MissingChainLock(CoreBlockHeight, BlockHash), /// The quorum entry came through without an attached /// `VerifyingChainLockSignaturesType::Rotating`. Typically happens when /// a QRInfo's historical diff covers a block range in which no rotating @@ -50,6 +56,9 @@ impl Display for LLMQEntryVerificationSkipStatus { LLMQEntryVerificationSkipStatus::MissingSnapshot(block_hash) => { format!("MissingSnapshot({})", block_hash) } + LLMQEntryVerificationSkipStatus::MissingChainLock(height, block_hash) => { + format!("MissingChainLock({}, {})", height, block_hash) + } LLMQEntryVerificationSkipStatus::MissingRotationChainLockSigs(quorum_hash) => { format!("MissingRotationChainLockSigs({})", quorum_hash) } @@ -95,8 +104,10 @@ impl From for LLMQEntryVerificationStatus { QuorumValidationError::RequiredSnapshotNotPresent(hash) => { Self::Skipped(LLMQEntryVerificationSkipStatus::MissingSnapshot(hash)) } - QuorumValidationError::RequiredChainLockNotPresent(_, block_hash) => { - Self::Skipped(LLMQEntryVerificationSkipStatus::UnknownBlock(block_hash)) + QuorumValidationError::RequiredChainLockNotPresent(height, block_hash) => { + Self::Skipped(LLMQEntryVerificationSkipStatus::MissingChainLock( + height, block_hash, + )) } QuorumValidationError::RequiredRotatedChainLockSigsNotPresent(quorum_hash) => { Self::Skipped(LLMQEntryVerificationSkipStatus::MissingRotationChainLockSigs( @@ -210,14 +221,14 @@ mod tests { } #[test] - fn required_chain_lock_not_present_maps_to_skipped_unknown_block() { + fn required_chain_lock_not_present_maps_to_skipped_missing_chain_lock() { let hash = dummy_hash(5); let status: LLMQEntryVerificationStatus = QuorumValidationError::RequiredChainLockNotPresent(7, hash).into(); assert_eq!( status, - LLMQEntryVerificationStatus::Skipped(LLMQEntryVerificationSkipStatus::UnknownBlock( - hash, + LLMQEntryVerificationStatus::Skipped(LLMQEntryVerificationSkipStatus::MissingChainLock( + 7, hash, )) ); } From 651ae16073ca4578b5a4cc0582dc6447b3f52518 Mon Sep 17 00:00:00 2001 From: xdustinface Date: Mon, 4 May 2026 21:52:39 +1000 Subject: [PATCH 08/17] test: cover `Skipped` classification and `Invalid` preservation in rotation-cycle statuses Adds a unit test that exercises `validate_rotation_cycle_quorums_validation_statuses` with two quorums: one structurally broken and one that triggers the `find_rotated_masternodes_for_quorums` infrastructure-error path. Asserts that the broken quorum keeps its `Invalid` status, and the infrastructure-error quorum surfaces as `Skipped(UnknownBlock(_))` rather than being misclassified as `Invalid`. This locks in both the behavioral change introduced by this PR and the bulk-overwrite guard. Addresses manki review comment on PR #125 https://github.com/xdustinface/rust-dashcore/pull/125#discussion_r3181131455 --- dash/src/sml/llmq_entry_verification.rs | 10 +-- .../sml/masternode_list_engine/validation.rs | 86 +++++++++++++++++++ 2 files changed, 90 insertions(+), 6 deletions(-) diff --git a/dash/src/sml/llmq_entry_verification.rs b/dash/src/sml/llmq_entry_verification.rs index c36026637..521b414f9 100644 --- a/dash/src/sml/llmq_entry_verification.rs +++ b/dash/src/sml/llmq_entry_verification.rs @@ -105,9 +105,7 @@ impl From for LLMQEntryVerificationStatus { Self::Skipped(LLMQEntryVerificationSkipStatus::MissingSnapshot(hash)) } QuorumValidationError::RequiredChainLockNotPresent(height, block_hash) => { - Self::Skipped(LLMQEntryVerificationSkipStatus::MissingChainLock( - height, block_hash, - )) + Self::Skipped(LLMQEntryVerificationSkipStatus::MissingChainLock(height, block_hash)) } QuorumValidationError::RequiredRotatedChainLockSigsNotPresent(quorum_hash) => { Self::Skipped(LLMQEntryVerificationSkipStatus::MissingRotationChainLockSigs( @@ -227,9 +225,9 @@ mod tests { QuorumValidationError::RequiredChainLockNotPresent(7, hash).into(); assert_eq!( status, - LLMQEntryVerificationStatus::Skipped(LLMQEntryVerificationSkipStatus::MissingChainLock( - 7, hash, - )) + LLMQEntryVerificationStatus::Skipped( + LLMQEntryVerificationSkipStatus::MissingChainLock(7, hash,) + ) ); } diff --git a/dash/src/sml/masternode_list_engine/validation.rs b/dash/src/sml/masternode_list_engine/validation.rs index 687537356..aa9a49d51 100644 --- a/dash/src/sml/masternode_list_engine/validation.rs +++ b/dash/src/sml/masternode_list_engine/validation.rs @@ -170,3 +170,89 @@ impl MasternodeListEngine { return_statuses } } + +#[cfg(all(test, feature = "quorum_validation"))] +mod tests { + use hashes::Hash; + + use super::*; + use crate::bls_sig_utils::{BLSPublicKey, BLSSignature}; + use crate::hash_types::QuorumVVecHash; + use crate::sml::llmq_entry_verification::{ + LLMQEntryVerificationSkipStatus, LLMQEntryVerificationStatus, + }; + use crate::sml::llmq_type::LLMQType; + use crate::sml::quorum_entry::qualified_quorum_entry::QualifiedQuorumEntry; + use crate::transaction::special_transaction::quorum_commitment::QuorumEntry; + + fn rotating_quorum( + quorum_hash: QuorumHash, + quorum_index: i16, + valid_structure: bool, + ) -> QualifiedQuorumEntry { + let (signers, valid_members, public_key, threshold_sig, agg_sig) = if valid_structure { + ( + vec![true; 4], + vec![true; 4], + BLSPublicKey::from([1; 48]), + BLSSignature::from([1; 96]), + BLSSignature::from([1; 96]), + ) + } else { + ( + vec![false; 4], + vec![false; 4], + BLSPublicKey::from([0; 48]), + BLSSignature::from([0; 96]), + BLSSignature::from([0; 96]), + ) + }; + QuorumEntry { + version: 2, + llmq_type: LLMQType::LlmqtypeTestDIP0024, + quorum_hash, + quorum_index: Some(quorum_index), + signers, + valid_members, + quorum_public_key: public_key, + quorum_vvec_hash: QuorumVVecHash::all_zeros(), + threshold_sig, + all_commitment_aggregated_signature: agg_sig, + } + .into() + } + + #[test] + fn rotation_cycle_statuses_classify_infra_error_as_skipped_and_preserve_invalid() { + let engine = MasternodeListEngine::default(); + + let broken_hash = QuorumHash::from_byte_array([1; 32]); + let unknown_hash = QuorumHash::from_byte_array([2; 32]); + let broken = rotating_quorum(broken_hash, 0, false); + let unknown_block = rotating_quorum(unknown_hash, 1, true); + + let statuses = + engine.validate_rotation_cycle_quorums_validation_statuses(&[&broken, &unknown_block]); + + assert!( + matches!( + statuses.get(&broken_hash), + Some(LLMQEntryVerificationStatus::Invalid( + QuorumValidationError::InsufficientSigners { .. } + )), + ), + "structurally-broken quorum must keep its Invalid status, got {:?}", + statuses.get(&broken_hash), + ); + assert!( + matches!( + statuses.get(&unknown_hash), + Some(LLMQEntryVerificationStatus::Skipped( + LLMQEntryVerificationSkipStatus::UnknownBlock(_), + )), + ), + "infrastructure-error quorum must surface as Skipped, got {:?}", + statuses.get(&unknown_hash), + ); + } +} From 27ded68df9a617f33e99eaf645cef68e01025896 Mon Sep 17 00:00:00 2001 From: xdustinface Date: Mon, 4 May 2026 22:34:29 +1000 Subject: [PATCH 09/17] fix: route per-quorum validate error through `Skipped`/`Invalid` classifier The per-quorum `validate()` Err branch in `validate_rotation_cycle_quorums_validation_statuses` mapped errors directly to `LLMQEntryVerificationStatus::Invalid(e)`, bypassing the `From` classifier that the bulk-overwrite path uses. Infrastructure errors surfaced by `quorum.validate()` itself (e.g. a missing chain-lock sig) would therefore be misclassified as bad quorum data instead of `Skipped`. Convert through `.into()` so both error paths classify consistently. Addresses manki review comment on PR #125 https://github.com/xdustinface/rust-dashcore/pull/125#discussion_r3181406178 --- dash/src/sml/masternode_list_engine/validation.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/dash/src/sml/masternode_list_engine/validation.rs b/dash/src/sml/masternode_list_engine/validation.rs index aa9a49d51..3a18e1ae6 100644 --- a/dash/src/sml/masternode_list_engine/validation.rs +++ b/dash/src/sml/masternode_list_engine/validation.rs @@ -160,10 +160,7 @@ impl MasternodeListEngine { ); } Err(e) => { - return_statuses.insert( - quorum.quorum_entry.quorum_hash, - LLMQEntryVerificationStatus::Invalid(e), - ); + return_statuses.insert(quorum.quorum_entry.quorum_hash, e.into()); } } } From 66f20eb8ad7b7e4dbce9a41fe926c9294c3d0062 Mon Sep 17 00:00:00 2001 From: xdustinface Date: Mon, 4 May 2026 22:36:15 +1000 Subject: [PATCH 10/17] test: widen Invalid assertion and cover all-quorums-Skipped path The existing structure-broken assertion was tied to `InsufficientSigners` specifically, but the test's intent is just that any pre-existing `Invalid` status is preserved by the bulk-overwrite guard. A change in `validate_structure`'s check order would silently flip the assertion into a misleading failure. Match `Invalid(_)` instead. Also adds a second scenario covering the case where every quorum reaches the bulk-overwrite path with no pre-existing `Invalid`. This catches any off-by-one in the guard logic that would skip entries it shouldn't. Addresses manki review comments on PR #125 https://github.com/xdustinface/rust-dashcore/pull/125#discussion_r3181406185 https://github.com/xdustinface/rust-dashcore/pull/125#discussion_r3181406198 --- .../sml/masternode_list_engine/validation.rs | 36 +++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/dash/src/sml/masternode_list_engine/validation.rs b/dash/src/sml/masternode_list_engine/validation.rs index 3a18e1ae6..037e703d4 100644 --- a/dash/src/sml/masternode_list_engine/validation.rs +++ b/dash/src/sml/masternode_list_engine/validation.rs @@ -232,13 +232,8 @@ mod tests { engine.validate_rotation_cycle_quorums_validation_statuses(&[&broken, &unknown_block]); assert!( - matches!( - statuses.get(&broken_hash), - Some(LLMQEntryVerificationStatus::Invalid( - QuorumValidationError::InsufficientSigners { .. } - )), - ), - "structurally-broken quorum must keep its Invalid status, got {:?}", + matches!(statuses.get(&broken_hash), Some(LLMQEntryVerificationStatus::Invalid(_))), + "structurally-broken quorum must keep an Invalid status, got {:?}", statuses.get(&broken_hash), ); assert!( @@ -252,4 +247,31 @@ mod tests { statuses.get(&unknown_hash), ); } + + #[test] + fn rotation_cycle_statuses_classify_all_quorums_as_skipped_when_no_pre_existing_invalid() { + let engine = MasternodeListEngine::default(); + + let hash_a = QuorumHash::from_byte_array([3; 32]); + let hash_b = QuorumHash::from_byte_array([4; 32]); + let quorum_a = rotating_quorum(hash_a, 0, true); + let quorum_b = rotating_quorum(hash_b, 1, true); + + let statuses = + engine.validate_rotation_cycle_quorums_validation_statuses(&[&quorum_a, &quorum_b]); + + for hash in [hash_a, hash_b] { + assert!( + matches!( + statuses.get(&hash), + Some(LLMQEntryVerificationStatus::Skipped( + LLMQEntryVerificationSkipStatus::UnknownBlock(_), + )), + ), + "every quorum must be Skipped when find_rotated_masternodes_for_quorums errors and no entry was pre-marked Invalid, got {:?} for {:?}", + statuses.get(&hash), + hash, + ); + } + } } From ab6091c8b33fc5fb4e4aa27ba48b0194a0eef1f3 Mon Sep 17 00:00:00 2001 From: xdustinface Date: Mon, 4 May 2026 22:37:49 +1000 Subject: [PATCH 11/17] test: cover `update_quorum_status` delegation to `From` classifier Adds direct tests on `QualifiedQuorumEntry::update_quorum_status` so a regression that breaks the `From` delegation (e.g. an accidental `Self::Invalid(e)` fallback) gets caught at the method boundary rather than only via the indirect `From`-impl tests. Covers an infrastructure-error path (`RequiredSnapshotNotPresent` -> `Skipped`), a bad-data path (`InvalidQuorumPublicKey` -> `Invalid`), and the `Ok` path (`Verified`). Also refreshes the now-stale docstring on `update_quorum_status`, which still claimed only block/masternode-list errors became `Skipped`. Addresses manki review comment on PR #125 https://github.com/xdustinface/rust-dashcore/pull/125#discussion_r3181406196 --- .../quorum_entry/qualified_quorum_entry.rs | 79 ++++++++++++++++--- 1 file changed, 70 insertions(+), 9 deletions(-) diff --git a/dash/src/sml/quorum_entry/qualified_quorum_entry.rs b/dash/src/sml/quorum_entry/qualified_quorum_entry.rs index a9571a79c..0a42901d9 100644 --- a/dash/src/sml/quorum_entry/qualified_quorum_entry.rs +++ b/dash/src/sml/quorum_entry/qualified_quorum_entry.rs @@ -56,15 +56,11 @@ impl From for QualifiedQuorumEntry { impl QualifiedQuorumEntry { /// Updates the verification status of the quorum based on a validation result. /// - /// This method processes the result of a quorum validation and updates the `verified` field accordingly: - /// - If validation succeeds (`Ok(_)`), the status is set to `Verified`. - /// - If validation fails due to a missing block, it is marked as `Skipped` with `UnknownBlock`. - /// - If validation fails due to a missing masternode list, it is marked as `Skipped` with `MissedList`. - /// - Other errors result in the quorum being marked as `Invalid`. - /// - /// # Arguments - /// - /// * `result` - A `Result` containing either success (`Ok`) or a `QuorumValidationError`. + /// On `Ok`, sets `verified` to `Verified`. On `Err`, classifies the error + /// via `From for LLMQEntryVerificationStatus`, + /// which decides whether the failure is `Skipped` (missing infrastructure + /// the caller should have provided) or `Invalid` (genuinely bad quorum + /// data). pub fn update_quorum_status(&mut self, result: Result<(), QuorumValidationError>) { self.verified = match result { Ok(_) => LLMQEntryVerificationStatus::Verified, @@ -72,3 +68,68 @@ impl QualifiedQuorumEntry { }; } } + +#[cfg(test)] +mod tests { + use hashes::Hash; + + use super::*; + use crate::QuorumHash; + use crate::bls_sig_utils::{BLSPublicKey, BLSSignature}; + use crate::hash_types::QuorumVVecHash; + use crate::sml::llmq_type::LLMQType; + + fn dummy_qualified_quorum_entry() -> QualifiedQuorumEntry { + QuorumEntry { + version: 2, + llmq_type: LLMQType::LlmqtypeTestDIP0024, + quorum_hash: QuorumHash::all_zeros(), + quorum_index: Some(0), + signers: vec![true; 4], + valid_members: vec![true; 4], + quorum_public_key: BLSPublicKey::from([1; 48]), + quorum_vvec_hash: QuorumVVecHash::all_zeros(), + threshold_sig: BLSSignature::from([1; 96]), + all_commitment_aggregated_signature: BLSSignature::from([1; 96]), + } + .into() + } + + #[test] + fn update_quorum_status_classifies_infra_error_as_skipped() { + let mut entry = dummy_qualified_quorum_entry(); + let snapshot_hash = QuorumHash::from_byte_array([7; 32]); + + entry.update_quorum_status(Err(QuorumValidationError::RequiredSnapshotNotPresent( + snapshot_hash, + ))); + + assert_eq!( + entry.verified, + LLMQEntryVerificationStatus::Skipped(LLMQEntryVerificationSkipStatus::MissingSnapshot( + snapshot_hash, + )), + ); + } + + #[test] + fn update_quorum_status_classifies_bad_data_error_as_invalid() { + let mut entry = dummy_qualified_quorum_entry(); + + entry.update_quorum_status(Err(QuorumValidationError::InvalidQuorumPublicKey)); + + assert_eq!( + entry.verified, + LLMQEntryVerificationStatus::Invalid(QuorumValidationError::InvalidQuorumPublicKey), + ); + } + + #[test] + fn update_quorum_status_marks_ok_as_verified() { + let mut entry = dummy_qualified_quorum_entry(); + + entry.update_quorum_status(Ok(())); + + assert_eq!(entry.verified, LLMQEntryVerificationStatus::Verified); + } +} From 6a36650d6c4de0e037078576980a99dc3a1da93f Mon Sep 17 00:00:00 2001 From: xdustinface Date: Mon, 4 May 2026 22:38:38 +1000 Subject: [PATCH 12/17] docs: explain why `VerifyingMasternodeListNotPresent` maps to `Skipped` The reclassification of `VerifyingMasternodeListNotPresent` from `Invalid` to `Skipped` was a behavioral change carried by the `From` impl rewrite but not called out anywhere in the diff. Add a short rationale so a future reader (or code-review pass) does not have to reverse-engineer why the verifying-list variant is grouped with the other height-based caller- infrastructure errors. Addresses manki review comments on PR #125 https://github.com/xdustinface/rust-dashcore/pull/125#discussion_r3181131436 https://github.com/xdustinface/rust-dashcore/pull/125#discussion_r3181406154 --- dash/src/sml/llmq_entry_verification.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dash/src/sml/llmq_entry_verification.rs b/dash/src/sml/llmq_entry_verification.rs index 521b414f9..c3e87ccf4 100644 --- a/dash/src/sml/llmq_entry_verification.rs +++ b/dash/src/sml/llmq_entry_verification.rs @@ -96,6 +96,11 @@ impl From for LLMQEntryVerificationStatus { QuorumValidationError::RequiredBlockNotPresent(block_hash, _) => { Self::Skipped(LLMQEntryVerificationSkipStatus::UnknownBlock(block_hash)) } + // `VerifyingMasternodeListNotPresent` is grouped here because the + // verifying masternode list at the validation height is caller- + // supplied infrastructure, not quorum data. Treating it as + // `Skipped` mirrors the sibling `RequiredMasternodeListNotPresent` + // case and lets the caller refetch instead of rejecting the quorum. QuorumValidationError::RequiredMasternodeListNotPresent(height) | QuorumValidationError::RequiredBlockHeightNotPresent(height) | QuorumValidationError::VerifyingMasternodeListNotPresent(height) => { From 9728462af463515ea202ea6af730c8e01ce8ef2c Mon Sep 17 00:00:00 2001 From: xdustinface Date: Mon, 4 May 2026 23:00:13 +1000 Subject: [PATCH 13/17] refactor: collapse `From` tests into table-driven form Nine near-identical `#[test]` functions all built one error variant, called `.into()`, and asserted the resulting `LLMQEntryVerificationStatus`. Replace them with a single test driving a table of `(error, expected_status)` pairs so adding a new arm in the future is a one-row change. --- dash/src/sml/llmq_entry_verification.rs | 168 ++++++++++-------------- 1 file changed, 66 insertions(+), 102 deletions(-) diff --git a/dash/src/sml/llmq_entry_verification.rs b/dash/src/sml/llmq_entry_verification.rs index c3e87ccf4..004cd5ebb 100644 --- a/dash/src/sml/llmq_entry_verification.rs +++ b/dash/src/sml/llmq_entry_verification.rs @@ -152,107 +152,71 @@ mod tests { } #[test] - fn required_block_not_present_maps_to_skipped_unknown_block() { - let hash = dummy_hash(1); - let status: LLMQEntryVerificationStatus = - QuorumValidationError::RequiredBlockNotPresent(hash, "ctx".to_string()).into(); - assert_eq!( - status, - LLMQEntryVerificationStatus::Skipped(LLMQEntryVerificationSkipStatus::UnknownBlock( - hash, - )) - ); - } - - #[test] - fn required_masternode_list_not_present_maps_to_skipped_missed_list() { - let status: LLMQEntryVerificationStatus = - QuorumValidationError::RequiredMasternodeListNotPresent(42).into(); - assert_eq!( - status, - LLMQEntryVerificationStatus::Skipped(LLMQEntryVerificationSkipStatus::MissedList(42)) - ); - } - - #[test] - fn required_block_height_not_present_maps_to_skipped_missed_list() { - let status: LLMQEntryVerificationStatus = - QuorumValidationError::RequiredBlockHeightNotPresent(99).into(); - assert_eq!( - status, - LLMQEntryVerificationStatus::Skipped(LLMQEntryVerificationSkipStatus::MissedList(99)) - ); - } - - #[test] - fn required_snapshot_not_present_maps_to_skipped_missing_snapshot() { - let hash = dummy_hash(2); - let status: LLMQEntryVerificationStatus = - QuorumValidationError::RequiredSnapshotNotPresent(hash).into(); - assert_eq!( - status, - LLMQEntryVerificationStatus::Skipped(LLMQEntryVerificationSkipStatus::MissingSnapshot( - hash, - )) - ); - } - - #[test] - fn required_rotated_chain_lock_sigs_not_present_maps_to_skipped() { - let hash = dummy_hash(3); - let status: LLMQEntryVerificationStatus = - QuorumValidationError::RequiredRotatedChainLockSigsNotPresent(hash).into(); - assert_eq!( - status, - LLMQEntryVerificationStatus::Skipped( - LLMQEntryVerificationSkipStatus::MissingRotationChainLockSigs(hash), - ) - ); - } - - #[test] - fn required_rotated_chain_lock_sig_not_present_maps_to_skipped() { - let hash = dummy_hash(4); - let status: LLMQEntryVerificationStatus = - QuorumValidationError::RequiredRotatedChainLockSigNotPresent(2, hash).into(); - assert_eq!( - status, - LLMQEntryVerificationStatus::Skipped( - LLMQEntryVerificationSkipStatus::MissingRotationChainLockSig(2, hash), - ) - ); - } - - #[test] - fn required_chain_lock_not_present_maps_to_skipped_missing_chain_lock() { - let hash = dummy_hash(5); - let status: LLMQEntryVerificationStatus = - QuorumValidationError::RequiredChainLockNotPresent(7, hash).into(); - assert_eq!( - status, - LLMQEntryVerificationStatus::Skipped( - LLMQEntryVerificationSkipStatus::MissingChainLock(7, hash,) - ) - ); - } - - #[test] - fn verifying_masternode_list_not_present_maps_to_skipped_missed_list() { - let status: LLMQEntryVerificationStatus = - QuorumValidationError::VerifyingMasternodeListNotPresent(123).into(); - assert_eq!( - status, - LLMQEntryVerificationStatus::Skipped(LLMQEntryVerificationSkipStatus::MissedList(123)) - ); - } - - #[test] - fn other_error_maps_to_invalid() { - let status: LLMQEntryVerificationStatus = - QuorumValidationError::InvalidQuorumPublicKey.into(); - assert_eq!( - status, - LLMQEntryVerificationStatus::Invalid(QuorumValidationError::InvalidQuorumPublicKey) - ); + fn from_quorum_validation_error_classifies_each_arm() { + let h1 = dummy_hash(1); + let h2 = dummy_hash(2); + let h3 = dummy_hash(3); + let h4 = dummy_hash(4); + let h5 = dummy_hash(5); + + let cases: Vec<(QuorumValidationError, LLMQEntryVerificationStatus)> = vec![ + ( + QuorumValidationError::RequiredBlockNotPresent(h1, "ctx".to_string()), + LLMQEntryVerificationStatus::Skipped( + LLMQEntryVerificationSkipStatus::UnknownBlock(h1), + ), + ), + ( + QuorumValidationError::RequiredMasternodeListNotPresent(42), + LLMQEntryVerificationStatus::Skipped(LLMQEntryVerificationSkipStatus::MissedList( + 42, + )), + ), + ( + QuorumValidationError::RequiredBlockHeightNotPresent(99), + LLMQEntryVerificationStatus::Skipped(LLMQEntryVerificationSkipStatus::MissedList( + 99, + )), + ), + ( + QuorumValidationError::VerifyingMasternodeListNotPresent(123), + LLMQEntryVerificationStatus::Skipped(LLMQEntryVerificationSkipStatus::MissedList( + 123, + )), + ), + ( + QuorumValidationError::RequiredSnapshotNotPresent(h2), + LLMQEntryVerificationStatus::Skipped( + LLMQEntryVerificationSkipStatus::MissingSnapshot(h2), + ), + ), + ( + QuorumValidationError::RequiredChainLockNotPresent(7, h5), + LLMQEntryVerificationStatus::Skipped( + LLMQEntryVerificationSkipStatus::MissingChainLock(7, h5), + ), + ), + ( + QuorumValidationError::RequiredRotatedChainLockSigsNotPresent(h3), + LLMQEntryVerificationStatus::Skipped( + LLMQEntryVerificationSkipStatus::MissingRotationChainLockSigs(h3), + ), + ), + ( + QuorumValidationError::RequiredRotatedChainLockSigNotPresent(2, h4), + LLMQEntryVerificationStatus::Skipped( + LLMQEntryVerificationSkipStatus::MissingRotationChainLockSig(2, h4), + ), + ), + ( + QuorumValidationError::InvalidQuorumPublicKey, + LLMQEntryVerificationStatus::Invalid(QuorumValidationError::InvalidQuorumPublicKey), + ), + ]; + + for (error, expected) in cases { + let actual: LLMQEntryVerificationStatus = error.clone().into(); + assert_eq!(actual, expected, "case: {error:?}"); + } } } From 3c51db3f81c2ce26434bd9ca8d86ac5401a1b110 Mon Sep 17 00:00:00 2001 From: xdustinface Date: Mon, 4 May 2026 23:00:18 +1000 Subject: [PATCH 14/17] refactor: collapse `update_quorum_status` tests into one delegation test The three per-case tests duplicated the `From` table-driven coverage. Keep one test that exercises both an `Ok` and an `Err` path through `update_quorum_status` so a regression breaking the delegation is still caught at this layer, and let the `From` impl tests cover the classification matrix. --- .../quorum_entry/qualified_quorum_entry.rs | 29 ++++--------------- 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/dash/src/sml/quorum_entry/qualified_quorum_entry.rs b/dash/src/sml/quorum_entry/qualified_quorum_entry.rs index 0a42901d9..acbd9a461 100644 --- a/dash/src/sml/quorum_entry/qualified_quorum_entry.rs +++ b/dash/src/sml/quorum_entry/qualified_quorum_entry.rs @@ -96,14 +96,16 @@ mod tests { } #[test] - fn update_quorum_status_classifies_infra_error_as_skipped() { + fn update_quorum_status_delegates_to_classifier() { let mut entry = dummy_qualified_quorum_entry(); - let snapshot_hash = QuorumHash::from_byte_array([7; 32]); + entry.update_quorum_status(Ok(())); + assert_eq!(entry.verified, LLMQEntryVerificationStatus::Verified); + let snapshot_hash = QuorumHash::from_byte_array([7; 32]); + let mut entry = dummy_qualified_quorum_entry(); entry.update_quorum_status(Err(QuorumValidationError::RequiredSnapshotNotPresent( snapshot_hash, ))); - assert_eq!( entry.verified, LLMQEntryVerificationStatus::Skipped(LLMQEntryVerificationSkipStatus::MissingSnapshot( @@ -111,25 +113,4 @@ mod tests { )), ); } - - #[test] - fn update_quorum_status_classifies_bad_data_error_as_invalid() { - let mut entry = dummy_qualified_quorum_entry(); - - entry.update_quorum_status(Err(QuorumValidationError::InvalidQuorumPublicKey)); - - assert_eq!( - entry.verified, - LLMQEntryVerificationStatus::Invalid(QuorumValidationError::InvalidQuorumPublicKey), - ); - } - - #[test] - fn update_quorum_status_marks_ok_as_verified() { - let mut entry = dummy_qualified_quorum_entry(); - - entry.update_quorum_status(Ok(())); - - assert_eq!(entry.verified, LLMQEntryVerificationStatus::Verified); - } } From fd6dc9889fa7ce6f387771dd9739d2e28dddf547 Mon Sep 17 00:00:00 2001 From: xdustinface Date: Mon, 4 May 2026 23:05:44 +1000 Subject: [PATCH 15/17] refactor: route structure-validation errors through `From` classifier The two `validate_structure` Err branches at the top of `validate_rotation_cycle_quorums_validation_statuses` constructed `LLMQEntryVerificationStatus::Invalid(e)` directly, bypassing the canonical `From` classifier that the rest of the function uses. If a structure-validation error variant is ever reclassified as infrastructure-missing, the direct construction would silently preserve the old behavior. Funnel both sites through `.into()` so all classification flows through the single canonical path. Addresses manki review comment on PR #125 https://github.com/xdustinface/rust-dashcore/pull/125#discussion_r3181657394 --- .../src/sml/masternode_list_engine/validation.rs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/dash/src/sml/masternode_list_engine/validation.rs b/dash/src/sml/masternode_list_engine/validation.rs index 037e703d4..f7c5ec8b7 100644 --- a/dash/src/sml/masternode_list_engine/validation.rs +++ b/dash/src/sml/masternode_list_engine/validation.rs @@ -89,19 +89,15 @@ impl MasternodeListEngine { // first let's do basic structure validation for quorum in quorums { if let Err(e) = quorum.quorum_entry.validate_structure() { - return_statuses.insert( - quorum.quorum_entry.quorum_hash, - LLMQEntryVerificationStatus::Invalid(e), - ); + return_statuses.insert(quorum.quorum_entry.quorum_hash, e.into()); } else if !quorum.quorum_entry.llmq_type.is_rotating_quorum_type() { return_statuses.insert( quorum.quorum_entry.quorum_hash, - LLMQEntryVerificationStatus::Invalid( - QuorumValidationError::ExpectedOnlyRotatedQuorums( - quorum.quorum_entry.quorum_hash, - quorum.quorum_entry.llmq_type, - ), - ), + QuorumValidationError::ExpectedOnlyRotatedQuorums( + quorum.quorum_entry.quorum_hash, + quorum.quorum_entry.llmq_type, + ) + .into(), ); } } From f6731a93eac86b7cc012bc62a7ebc820f2437f75 Mon Sep 17 00:00:00 2001 From: xdustinface Date: Mon, 4 May 2026 23:06:59 +1000 Subject: [PATCH 16/17] test: assert on `Skipped(_)` not the specific skip variant Both rotation-cycle status tests asserted `Skipped(UnknownBlock(_))`, which is the specific sub-variant produced by `MasternodeListEngine::default()` today. If the empty engine ever errors with a different infrastructure variant (e.g. `RequiredMasternodeListNotPresent` -> `MissedList`), the assertion would fail misleadingly even though the intent of the test (classification as `Skipped`, not `Invalid`) is still satisfied. Match `Skipped(_)` so the assertion verifies the classification boundary the test is actually about. Addresses manki review comments on PR #125 https://github.com/xdustinface/rust-dashcore/pull/125#discussion_r3181657366 https://github.com/xdustinface/rust-dashcore/pull/125#discussion_r3181657406 --- .../sml/masternode_list_engine/validation.rs | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/dash/src/sml/masternode_list_engine/validation.rs b/dash/src/sml/masternode_list_engine/validation.rs index f7c5ec8b7..bc9316064 100644 --- a/dash/src/sml/masternode_list_engine/validation.rs +++ b/dash/src/sml/masternode_list_engine/validation.rs @@ -171,9 +171,7 @@ mod tests { use super::*; use crate::bls_sig_utils::{BLSPublicKey, BLSSignature}; use crate::hash_types::QuorumVVecHash; - use crate::sml::llmq_entry_verification::{ - LLMQEntryVerificationSkipStatus, LLMQEntryVerificationStatus, - }; + use crate::sml::llmq_entry_verification::LLMQEntryVerificationStatus; use crate::sml::llmq_type::LLMQType; use crate::sml::quorum_entry::qualified_quorum_entry::QualifiedQuorumEntry; use crate::transaction::special_transaction::quorum_commitment::QuorumEntry; @@ -233,12 +231,7 @@ mod tests { statuses.get(&broken_hash), ); assert!( - matches!( - statuses.get(&unknown_hash), - Some(LLMQEntryVerificationStatus::Skipped( - LLMQEntryVerificationSkipStatus::UnknownBlock(_), - )), - ), + matches!(statuses.get(&unknown_hash), Some(LLMQEntryVerificationStatus::Skipped(_))), "infrastructure-error quorum must surface as Skipped, got {:?}", statuses.get(&unknown_hash), ); @@ -258,12 +251,7 @@ mod tests { for hash in [hash_a, hash_b] { assert!( - matches!( - statuses.get(&hash), - Some(LLMQEntryVerificationStatus::Skipped( - LLMQEntryVerificationSkipStatus::UnknownBlock(_), - )), - ), + matches!(statuses.get(&hash), Some(LLMQEntryVerificationStatus::Skipped(_))), "every quorum must be Skipped when find_rotated_masternodes_for_quorums errors and no entry was pre-marked Invalid, got {:?} for {:?}", statuses.get(&hash), hash, From 48d06628cc702184d7a9d4a4912e38112408a08b Mon Sep 17 00:00:00 2001 From: xdustinface Date: Mon, 4 May 2026 23:08:01 +1000 Subject: [PATCH 17/17] test: cover `Display` formatting for new skip-status variants Adds a table-driven test asserting the formatted output of `MissingSnapshot`, `MissingChainLock`, `MissingRotationChainLockSigs`, and `MissingRotationChainLockSig` so a typo in any of the format strings (in particular the `h - {offset}` style for the singular rotation-sig case) is caught at unit-test time rather than only by an observant log reader. Addresses manki review comment on PR #125 https://github.com/xdustinface/rust-dashcore/pull/125#discussion_r3181657410 --- dash/src/sml/llmq_entry_verification.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/dash/src/sml/llmq_entry_verification.rs b/dash/src/sml/llmq_entry_verification.rs index 004cd5ebb..58d85cc3a 100644 --- a/dash/src/sml/llmq_entry_verification.rs +++ b/dash/src/sml/llmq_entry_verification.rs @@ -219,4 +219,28 @@ mod tests { assert_eq!(actual, expected, "case: {error:?}"); } } + + #[test] + fn skip_status_display_formats_new_variants() { + let h = dummy_hash(1); + let cases: Vec<(LLMQEntryVerificationSkipStatus, String)> = vec![ + (LLMQEntryVerificationSkipStatus::MissingSnapshot(h), format!("MissingSnapshot({h})")), + ( + LLMQEntryVerificationSkipStatus::MissingChainLock(42, h), + format!("MissingChainLock(42, {h})"), + ), + ( + LLMQEntryVerificationSkipStatus::MissingRotationChainLockSigs(h), + format!("MissingRotationChainLockSigs({h})"), + ), + ( + LLMQEntryVerificationSkipStatus::MissingRotationChainLockSig(2, h), + format!("MissingRotationChainLockSig(h - 2, {h})"), + ), + ]; + + for (status, expected) in cases { + assert_eq!(status.to_string(), expected, "case: {status:?}"); + } + } }