diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/identity_nonces.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/identity_nonces.rs index 29ef782d1aa..d9a4bd04ba2 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/identity_nonces.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/identity_nonces.rs @@ -187,3 +187,286 @@ impl StateTransitionHasIdentityNonceValidationV0 for StateTransition { // Version dispatch tests for has_identity_nonce_validation were intentionally removed. // The version-specific routing (v0 vs v1) is covered by strategy tests that exercise // the processor at the platform version used by the test harness. + +#[cfg(test)] +mod tests { + use super::*; + use dpp::state_transition::batch_transition::BatchTransition; + use dpp::state_transition::identity_create_transition::v0::IdentityCreateTransitionV0; + use dpp::state_transition::identity_create_transition::IdentityCreateTransition; + use dpp::state_transition::identity_credit_transfer_transition::v0::IdentityCreditTransferTransitionV0; + use dpp::state_transition::identity_credit_transfer_transition::IdentityCreditTransferTransition; + use dpp::state_transition::identity_credit_withdrawal_transition::v0::IdentityCreditWithdrawalTransitionV0; + use dpp::state_transition::identity_credit_withdrawal_transition::IdentityCreditWithdrawalTransition; + use dpp::state_transition::identity_topup_transition::v0::IdentityTopUpTransitionV0; + use dpp::state_transition::identity_topup_transition::IdentityTopUpTransition; + use dpp::state_transition::identity_update_transition::v0::IdentityUpdateTransitionV0; + use dpp::state_transition::identity_update_transition::IdentityUpdateTransition; + use dpp::state_transition::masternode_vote_transition::v0::MasternodeVoteTransitionV0; + use dpp::state_transition::masternode_vote_transition::MasternodeVoteTransition; + use dpp::version::PlatformVersion; + + /// Helper to build a Batch StateTransition from a default V0. + fn batch_st() -> StateTransition { + StateTransition::Batch(BatchTransition::V0(Default::default())) + } + + fn identity_update_st() -> StateTransition { + StateTransition::IdentityUpdate(IdentityUpdateTransition::from( + IdentityUpdateTransitionV0::default(), + )) + } + + fn identity_credit_transfer_st() -> StateTransition { + StateTransition::IdentityCreditTransfer(IdentityCreditTransferTransition::from( + IdentityCreditTransferTransitionV0::default(), + )) + } + + fn identity_credit_withdrawal_st() -> StateTransition { + StateTransition::IdentityCreditWithdrawal(IdentityCreditWithdrawalTransition::from( + IdentityCreditWithdrawalTransitionV0::default(), + )) + } + + fn identity_create_st() -> StateTransition { + StateTransition::IdentityCreate(IdentityCreateTransition::from( + IdentityCreateTransitionV0::default(), + )) + } + + fn identity_top_up_st() -> StateTransition { + StateTransition::IdentityTopUp(IdentityTopUpTransition::from( + IdentityTopUpTransitionV0::default(), + )) + } + + fn masternode_vote_st() -> StateTransition { + StateTransition::MasternodeVote(MasternodeVoteTransition::from( + MasternodeVoteTransitionV0::default(), + )) + } + + // ---- has_identity_nonce_validation with version 0 (PlatformVersion::first) ---- + + #[test] + fn has_nonce_validation_v0_batch_returns_true() { + let platform_version = PlatformVersion::first(); + if platform_version + .drive_abci + .validation_and_processing + .has_nonce_validation + != 0 + { + return; + } + let result = batch_st() + .has_identity_nonce_validation(platform_version) + .expect("should not error"); + assert!(result); + } + + #[test] + fn has_nonce_validation_v0_identity_update_returns_true() { + let platform_version = PlatformVersion::first(); + if platform_version + .drive_abci + .validation_and_processing + .has_nonce_validation + != 0 + { + return; + } + let result = identity_update_st() + .has_identity_nonce_validation(platform_version) + .expect("should not error"); + assert!(result); + } + + #[test] + fn has_nonce_validation_v0_identity_credit_transfer_returns_true() { + let platform_version = PlatformVersion::first(); + if platform_version + .drive_abci + .validation_and_processing + .has_nonce_validation + != 0 + { + return; + } + let result = identity_credit_transfer_st() + .has_identity_nonce_validation(platform_version) + .expect("should not error"); + assert!(result); + } + + #[test] + fn has_nonce_validation_v0_identity_credit_withdrawal_returns_true() { + let platform_version = PlatformVersion::first(); + if platform_version + .drive_abci + .validation_and_processing + .has_nonce_validation + != 0 + { + return; + } + let result = identity_credit_withdrawal_st() + .has_identity_nonce_validation(platform_version) + .expect("should not error"); + assert!(result); + } + + #[test] + fn has_nonce_validation_v0_identity_create_returns_false() { + let platform_version = PlatformVersion::first(); + if platform_version + .drive_abci + .validation_and_processing + .has_nonce_validation + != 0 + { + return; + } + let result = identity_create_st() + .has_identity_nonce_validation(platform_version) + .expect("should not error"); + assert!(!result); + } + + #[test] + fn has_nonce_validation_v0_identity_top_up_returns_false() { + let platform_version = PlatformVersion::first(); + if platform_version + .drive_abci + .validation_and_processing + .has_nonce_validation + != 0 + { + return; + } + let result = identity_top_up_st() + .has_identity_nonce_validation(platform_version) + .expect("should not error"); + assert!(!result); + } + + #[test] + fn has_nonce_validation_v0_masternode_vote_returns_false() { + let platform_version = PlatformVersion::first(); + if platform_version + .drive_abci + .validation_and_processing + .has_nonce_validation + != 0 + { + return; + } + let result = masternode_vote_st() + .has_identity_nonce_validation(platform_version) + .expect("should not error"); + // In v0, MasternodeVote does NOT have nonce validation + assert!(!result); + } + + // ---- has_identity_nonce_validation with version 1 (PlatformVersion::latest) ---- + + #[test] + fn has_nonce_validation_v1_batch_returns_true() { + let platform_version = PlatformVersion::latest(); + if platform_version + .drive_abci + .validation_and_processing + .has_nonce_validation + != 1 + { + return; + } + let result = batch_st() + .has_identity_nonce_validation(platform_version) + .expect("should not error"); + assert!(result); + } + + #[test] + fn has_nonce_validation_v1_identity_update_returns_true() { + let platform_version = PlatformVersion::latest(); + if platform_version + .drive_abci + .validation_and_processing + .has_nonce_validation + != 1 + { + return; + } + let result = identity_update_st() + .has_identity_nonce_validation(platform_version) + .expect("should not error"); + assert!(result); + } + + #[test] + fn has_nonce_validation_v1_masternode_vote_returns_true() { + let platform_version = PlatformVersion::latest(); + if platform_version + .drive_abci + .validation_and_processing + .has_nonce_validation + != 1 + { + return; + } + let result = masternode_vote_st() + .has_identity_nonce_validation(platform_version) + .expect("should not error"); + // In v1, MasternodeVote DOES have nonce validation + assert!(result); + } + + #[test] + fn has_nonce_validation_v1_identity_create_returns_false() { + let platform_version = PlatformVersion::latest(); + if platform_version + .drive_abci + .validation_and_processing + .has_nonce_validation + != 1 + { + return; + } + let result = identity_create_st() + .has_identity_nonce_validation(platform_version) + .expect("should not error"); + assert!(!result); + } + + #[test] + fn has_nonce_validation_v1_identity_top_up_returns_false() { + let platform_version = PlatformVersion::latest(); + if platform_version + .drive_abci + .validation_and_processing + .has_nonce_validation + != 1 + { + return; + } + let result = identity_top_up_st() + .has_identity_nonce_validation(platform_version) + .expect("should not error"); + assert!(!result); + } + + // ---- unknown version returns error ---- + + #[test] + fn has_nonce_validation_unknown_version_returns_error() { + let mut pv = PlatformVersion::latest().clone(); + pv.drive_abci.validation_and_processing.has_nonce_validation = 99; + + let result = batch_st().has_identity_nonce_validation(&pv); + assert!(result.is_err()); + let err_string = format!("{:?}", result.unwrap_err()); + assert!(err_string.contains("UnknownVersionMismatch")); + } +} diff --git a/packages/rs-drive-abci/src/platform_types/block_proposal/v0.rs b/packages/rs-drive-abci/src/platform_types/block_proposal/v0.rs index 783dd18ace0..b92bfdc0231 100644 --- a/packages/rs-drive-abci/src/platform_types/block_proposal/v0.rs +++ b/packages/rs-drive-abci/src/platform_types/block_proposal/v0.rs @@ -253,3 +253,256 @@ impl<'a> TryFrom<&'a RequestProcessProposal> for BlockProposal<'a> { }) } } + +#[cfg(test)] +mod tests { + use super::*; + use tenderdash_abci::proto::google::protobuf::Timestamp; + + fn valid_timestamp() -> Timestamp { + Timestamp { + seconds: 1_700_000, + nanos: 500_000, + } + } + + fn valid_prepare_proposal() -> RequestPrepareProposal { + RequestPrepareProposal { + max_tx_bytes: 1024, + txs: vec![vec![1, 2, 3]], + local_last_commit: None, + misbehavior: vec![], + height: 10, + time: Some(valid_timestamp()), + next_validators_hash: vec![0u8; 32], + round: 1, + core_chain_locked_height: 500, + proposer_pro_tx_hash: vec![0xAAu8; 32], + proposed_app_version: 5, + version: Some(Consensus { block: 1, app: 2 }), + quorum_hash: vec![0xBBu8; 32], + } + } + + fn valid_process_proposal() -> RequestProcessProposal { + RequestProcessProposal { + txs: vec![vec![4, 5, 6]], + proposed_last_commit: None, + misbehavior: vec![], + hash: vec![0xCCu8; 32], + height: 20, + time: Some(valid_timestamp()), + next_validators_hash: vec![0u8; 32], + round: 2, + core_chain_locked_height: 600, + core_chain_lock_update: None, + proposer_pro_tx_hash: vec![0xDDu8; 32], + proposed_app_version: 7, + version: Some(Consensus { block: 1, app: 3 }), + quorum_hash: vec![0xEEu8; 32], + } + } + + // ---- BlockProposal from RequestPrepareProposal ---- + + #[test] + fn prepare_proposal_valid_conversion() { + let req = valid_prepare_proposal(); + let proposal = BlockProposal::try_from(&req).expect("should succeed"); + + assert_eq!(proposal.height, 10); + assert_eq!(proposal.round, 1); + assert_eq!(proposal.core_chain_locked_height, 500); + assert_eq!(proposal.proposed_app_version, 5); + assert_eq!(proposal.proposer_pro_tx_hash, [0xAAu8; 32]); + assert_eq!(proposal.validator_set_quorum_hash, [0xBBu8; 32]); + assert!(proposal.block_hash.is_none()); // prepare proposal has no block hash + assert!(proposal.core_chain_lock_update.is_none()); // always None for prepare + assert_eq!(proposal.raw_state_transitions.len(), 1); + assert_eq!(proposal.consensus_versions.block, 1); + assert_eq!(proposal.consensus_versions.app, 2); + } + + #[test] + fn prepare_proposal_missing_version_fails() { + let mut req = valid_prepare_proposal(); + req.version = None; + let result = BlockProposal::try_from(&req); + assert!(result.is_err()); + } + + #[test] + fn prepare_proposal_missing_time_fails() { + let mut req = valid_prepare_proposal(); + req.time = None; + let result = BlockProposal::try_from(&req); + assert!(result.is_err()); + } + + #[test] + fn prepare_proposal_invalid_proposer_pro_tx_hash_size_fails() { + let mut req = valid_prepare_proposal(); + req.proposer_pro_tx_hash = vec![0u8; 31]; // wrong size + let result = BlockProposal::try_from(&req); + assert!(result.is_err()); + } + + #[test] + fn prepare_proposal_invalid_quorum_hash_size_fails() { + let mut req = valid_prepare_proposal(); + req.quorum_hash = vec![0u8; 33]; // wrong size + let result = BlockProposal::try_from(&req); + assert!(result.is_err()); + } + + #[test] + fn prepare_proposal_empty_txs() { + let mut req = valid_prepare_proposal(); + req.txs = vec![]; + let proposal = BlockProposal::try_from(&req).expect("should succeed"); + assert!(proposal.raw_state_transitions.is_empty()); + } + + // ---- BlockProposal from RequestProcessProposal ---- + + #[test] + fn process_proposal_valid_conversion() { + let req = valid_process_proposal(); + let proposal = BlockProposal::try_from(&req).expect("should succeed"); + + assert_eq!(proposal.height, 20); + assert_eq!(proposal.round, 2); + assert_eq!(proposal.core_chain_locked_height, 600); + assert_eq!(proposal.proposed_app_version, 7); + assert_eq!(proposal.proposer_pro_tx_hash, [0xDDu8; 32]); + assert_eq!(proposal.validator_set_quorum_hash, [0xEEu8; 32]); + assert_eq!(proposal.block_hash, Some([0xCCu8; 32])); + assert!(proposal.core_chain_lock_update.is_none()); + assert_eq!(proposal.raw_state_transitions.len(), 1); + } + + #[test] + fn process_proposal_missing_version_fails() { + let mut req = valid_process_proposal(); + req.version = None; + let result = BlockProposal::try_from(&req); + assert!(result.is_err()); + } + + #[test] + fn process_proposal_missing_time_fails() { + let mut req = valid_process_proposal(); + req.time = None; + let result = BlockProposal::try_from(&req); + assert!(result.is_err()); + } + + #[test] + fn process_proposal_invalid_proposer_hash_size_fails() { + let mut req = valid_process_proposal(); + req.proposer_pro_tx_hash = vec![0u8; 10]; // wrong size + let result = BlockProposal::try_from(&req); + assert!(result.is_err()); + } + + #[test] + fn process_proposal_invalid_quorum_hash_size_fails() { + let mut req = valid_process_proposal(); + req.quorum_hash = vec![0u8; 64]; // wrong size + let result = BlockProposal::try_from(&req); + assert!(result.is_err()); + } + + #[test] + fn process_proposal_invalid_block_hash_size_fails() { + let mut req = valid_process_proposal(); + req.hash = vec![0u8; 16]; // wrong size + let result = BlockProposal::try_from(&req); + assert!(result.is_err()); + } + + #[test] + fn process_proposal_with_core_chain_lock_update() { + let mut req = valid_process_proposal(); + req.core_chain_lock_update = Some(CoreChainLock { + core_block_height: 700, + core_block_hash: vec![0xFFu8; 32], + signature: vec![0xABu8; 96], + }); + let proposal = BlockProposal::try_from(&req).expect("should succeed"); + assert!(proposal.core_chain_lock_update.is_some()); + let cl = proposal.core_chain_lock_update.unwrap(); + assert_eq!(cl.block_height, 700); + } + + #[test] + fn process_proposal_with_invalid_chain_lock_signature_size_fails() { + let mut req = valid_process_proposal(); + req.core_chain_lock_update = Some(CoreChainLock { + core_block_height: 700, + core_block_hash: vec![0xFFu8; 32], + signature: vec![0xABu8; 48], // wrong size, should be 96 + }); + let result = BlockProposal::try_from(&req); + assert!(result.is_err()); + } + + #[test] + fn process_proposal_with_invalid_chain_lock_hash_size_fails() { + let mut req = valid_process_proposal(); + req.core_chain_lock_update = Some(CoreChainLock { + core_block_height: 700, + core_block_hash: vec![0xFFu8; 16], // wrong size + signature: vec![0xABu8; 96], + }); + let result = BlockProposal::try_from(&req); + assert!(result.is_err()); + } + + // ---- Debug formatting ---- + + #[test] + fn block_proposal_debug_format() { + let req = valid_prepare_proposal(); + let proposal = BlockProposal::try_from(&req).expect("should succeed"); + let debug_str = format!("{:?}", proposal); + assert!(debug_str.contains("BlockProposal")); + assert!(debug_str.contains("height: 10")); + assert!(debug_str.contains("round: 1")); + assert!(debug_str.contains("core_chain_locked_height: 500")); + } + + #[test] + fn block_proposal_debug_with_block_hash() { + let req = valid_process_proposal(); + let proposal = BlockProposal::try_from(&req).expect("should succeed"); + let debug_str = format!("{:?}", proposal); + assert!(debug_str.contains("block_hash")); + // block_hash should be hex-encoded + assert!(debug_str.contains("cccccc")); + } + + // ---- Block time calculation ---- + + #[test] + fn prepare_proposal_block_time_ms_calculated_correctly() { + let mut req = valid_prepare_proposal(); + req.time = Some(Timestamp { + seconds: 1000, + nanos: 500_000_000, // 500ms + }); + let proposal = BlockProposal::try_from(&req).expect("should succeed"); + assert_eq!(proposal.block_time_ms, 1_000_500); + } + + #[test] + fn process_proposal_block_time_ms_calculated_correctly() { + let mut req = valid_process_proposal(); + req.time = Some(Timestamp { + seconds: 2000, + nanos: 250_000_000, // 250ms + }); + let proposal = BlockProposal::try_from(&req).expect("should succeed"); + assert_eq!(proposal.block_time_ms, 2_000_250); + } +} diff --git a/packages/rs-drive-abci/src/platform_types/signature_verification_quorum_set/v0/quorum_set.rs b/packages/rs-drive-abci/src/platform_types/signature_verification_quorum_set/v0/quorum_set.rs index fbf817625da..881347bc6ae 100644 --- a/packages/rs-drive-abci/src/platform_types/signature_verification_quorum_set/v0/quorum_set.rs +++ b/packages/rs-drive-abci/src/platform_types/signature_verification_quorum_set/v0/quorum_set.rs @@ -298,3 +298,411 @@ impl From for SignatureVerificationQuorumSetV0 { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::ChainLockConfig; + use dpp::bls_signatures::{Bls12381G2Impl, SecretKey as BlsPrivateKey}; + use dpp::dashcore::hashes::Hash; + use dpp::dashcore_rpc::json::QuorumType; + + fn make_public_key(seed: u8) -> dpp::bls_signatures::PublicKey { + let mut key_bytes = [0u8; 32]; + key_bytes[0] = seed; + key_bytes[31] = 1; + let sk = + BlsPrivateKey::::from_be_bytes(&key_bytes).expect("valid secret key"); + sk.public_key() + } + + fn make_verification_quorum(seed: u8, index: Option) -> VerificationQuorum { + VerificationQuorum { + index, + public_key: make_public_key(seed), + } + } + + fn make_quorums(seeds: &[(u8, [u8; 32])]) -> Quorums { + seeds + .iter() + .map(|(seed, hash_bytes)| { + ( + QuorumHash::from_byte_array(*hash_bytes), + make_verification_quorum(*seed, None), + ) + }) + .collect() + } + + fn default_chain_lock_config() -> ChainLockConfig { + ChainLockConfig { + quorum_type: QuorumType::Llmq400_60, + quorum_size: 400, + quorum_window: 288, + quorum_active_signers: 4, + quorum_rotation: false, + } + } + + // ---- Construction ---- + + #[test] + fn new_from_quorum_like_config() { + let config = default_chain_lock_config(); + let qs = SignatureVerificationQuorumSetV0::new(&config); + + assert_eq!(qs.config().quorum_type, QuorumType::Llmq400_60); + assert_eq!(qs.config().active_signers, 4); + assert!(!qs.config().rotation); + assert_eq!(qs.config().window, 288); + assert!(qs.current_quorums().is_empty()); + assert!(!qs.has_previous_past_quorums()); + } + + #[test] + fn from_chain_lock_config() { + let config = ChainLockConfig { + quorum_type: QuorumType::Llmq100_67, + quorum_size: 100, + quorum_window: 24, + quorum_active_signers: 24, + quorum_rotation: true, + }; + let qs: SignatureVerificationQuorumSetV0 = config.into(); + + assert_eq!(qs.config().quorum_type, QuorumType::Llmq100_67); + assert_eq!(qs.config().active_signers, 24); + assert!(qs.config().rotation); + assert_eq!(qs.config().window, 24); + } + + // ---- set_current_quorums / current_quorums ---- + + #[test] + fn set_and_get_current_quorums() { + let config = default_chain_lock_config(); + let mut qs = SignatureVerificationQuorumSetV0::new(&config); + + let quorums = make_quorums(&[(1, [1u8; 32]), (2, [2u8; 32])]); + qs.set_current_quorums(quorums); + + assert_eq!(qs.current_quorums().len(), 2); + } + + #[test] + fn current_quorums_mut_allows_insert() { + let config = default_chain_lock_config(); + let mut qs = SignatureVerificationQuorumSetV0::new(&config); + + let hash = QuorumHash::from_byte_array([10u8; 32]); + qs.current_quorums_mut() + .insert(hash, make_verification_quorum(10, None)); + + assert_eq!(qs.current_quorums().len(), 1); + assert!(qs.current_quorums().contains_key(&hash)); + } + + // ---- has_previous_past_quorums ---- + + #[test] + fn has_previous_past_quorums_initially_false() { + let config = default_chain_lock_config(); + let qs = SignatureVerificationQuorumSetV0::new(&config); + assert!(!qs.has_previous_past_quorums()); + } + + // ---- set_previous_past_quorums ---- + + #[test] + fn set_previous_past_quorums_makes_has_previous_true() { + let config = default_chain_lock_config(); + let mut qs = SignatureVerificationQuorumSetV0::new(&config); + + let prev_quorums = make_quorums(&[(1, [1u8; 32])]); + qs.set_previous_past_quorums(prev_quorums, 100, 105); + + assert!(qs.has_previous_past_quorums()); + } + + #[test] + fn set_previous_past_quorums_tracks_previous_change_height() { + let config = default_chain_lock_config(); + let mut qs = SignatureVerificationQuorumSetV0::new(&config); + + // First call: previous_change_height should be None because there was no prior previous + let q1 = make_quorums(&[(1, [1u8; 32])]); + qs.set_previous_past_quorums(q1, 90, 100); + + // Second call: previous_change_height should be Some(100) from the first call + let q2 = make_quorums(&[(2, [2u8; 32])]); + qs.set_previous_past_quorums(q2, 100, 110); + + assert!(qs.has_previous_past_quorums()); + // We verify indirectly via select_quorums behavior + } + + // ---- replace_quorums ---- + + #[test] + fn replace_quorums_moves_current_to_previous() { + let config = default_chain_lock_config(); + let mut qs = SignatureVerificationQuorumSetV0::new(&config); + + let initial = make_quorums(&[(1, [1u8; 32])]); + qs.set_current_quorums(initial); + assert!(!qs.has_previous_past_quorums()); + + let replacement = make_quorums(&[(2, [2u8; 32])]); + qs.replace_quorums(replacement, 100, 105); + + assert!(qs.has_previous_past_quorums()); + // Current quorums should be the replacement + assert_eq!(qs.current_quorums().len(), 1); + assert!(qs + .current_quorums() + .contains_key(&QuorumHash::from_byte_array([2u8; 32]))); + } + + #[test] + fn replace_quorums_twice_updates_previous_change_height() { + let config = default_chain_lock_config(); + let mut qs = SignatureVerificationQuorumSetV0::new(&config); + + let q1 = make_quorums(&[(1, [1u8; 32])]); + qs.set_current_quorums(q1); + + let q2 = make_quorums(&[(2, [2u8; 32])]); + qs.replace_quorums(q2, 90, 100); + + let q3 = make_quorums(&[(3, [3u8; 32])]); + qs.replace_quorums(q3, 100, 110); + + // After two replacements, current should be q3, previous should contain q2, + // and the previous_change_height inside previous should be Some(100). + assert_eq!(qs.current_quorums().len(), 1); + assert!(qs + .current_quorums() + .contains_key(&QuorumHash::from_byte_array([3u8; 32]))); + assert!(qs.has_previous_past_quorums()); + } + + // ---- select_quorums ---- + + #[test] + fn select_quorums_no_previous_returns_current_only() { + let config = default_chain_lock_config(); + let mut qs = SignatureVerificationQuorumSetV0::new(&config); + + let current = make_quorums(&[(1, [1u8; 32])]); + qs.set_current_quorums(current); + + let iter = qs.select_quorums(20, 10); + assert_eq!(iter.len(), 1); + assert!(!iter.should_be_verifiable()); + } + + #[test] + fn select_quorums_verification_above_change_height_returns_current_and_verifiable() { + // Scenario from code comments: + // ------- 100 (previous_quorum_height) ------ 105 (change_quorum_height) ------ 106 (verification_height) + // signing_height must be > SIGN_OFFSET (8) + let config = default_chain_lock_config(); + let mut qs = SignatureVerificationQuorumSetV0::new(&config); + + let initial = make_quorums(&[(1, [1u8; 32])]); + qs.set_current_quorums(initial); + + let replacement = make_quorums(&[(2, [2u8; 32])]); + qs.replace_quorums(replacement, 100, 105); + + // signing_height=114, verification_height=106 >= change_quorum_height=105 + let iter = qs.select_quorums(114, 106); + assert_eq!(iter.len(), 1); + assert!(iter.should_be_verifiable()); + } + + #[test] + fn select_quorums_verification_at_change_height_returns_current_and_verifiable() { + // verification_height == change_quorum_height + let config = default_chain_lock_config(); + let mut qs = SignatureVerificationQuorumSetV0::new(&config); + + let initial = make_quorums(&[(1, [1u8; 32])]); + qs.set_current_quorums(initial); + + let replacement = make_quorums(&[(2, [2u8; 32])]); + qs.replace_quorums(replacement, 100, 105); + + let iter = qs.select_quorums(113, 105); + assert_eq!(iter.len(), 1); + assert!(iter.should_be_verifiable()); + } + + #[test] + fn select_quorums_verification_below_previous_height_returns_previous() { + // Scenario: + // -------- 98 (verification_height) ------- 100 (previous_quorum_height) ------ 105 (change_quorum_height) + let config = default_chain_lock_config(); + let mut qs = SignatureVerificationQuorumSetV0::new(&config); + + let initial = make_quorums(&[(1, [1u8; 32])]); + qs.set_current_quorums(initial); + + let replacement = make_quorums(&[(2, [2u8; 32])]); + qs.replace_quorums(replacement, 100, 105); + + // signing_height=106, verification_height=98 <= previous_quorum_height=100 + let iter = qs.select_quorums(106, 98); + assert_eq!(iter.len(), 1); + // should_be_verifiable is false because previous_change_height is None + assert!(!iter.should_be_verifiable()); + } + + #[test] + fn select_quorums_verification_at_previous_height_returns_previous() { + let config = default_chain_lock_config(); + let mut qs = SignatureVerificationQuorumSetV0::new(&config); + + let initial = make_quorums(&[(1, [1u8; 32])]); + qs.set_current_quorums(initial); + + let replacement = make_quorums(&[(2, [2u8; 32])]); + qs.replace_quorums(replacement, 100, 105); + + // verification_height == previous_quorum_height + let iter = qs.select_quorums(108, 100); + assert_eq!(iter.len(), 1); + assert!(!iter.should_be_verifiable()); + } + + #[test] + fn select_quorums_verification_between_previous_and_change_returns_both() { + // Scenario: + // ------- 100 (previous_quorum_height) ------ 104 (verification_height) -------105 (change_quorum_height) + let config = default_chain_lock_config(); + let mut qs = SignatureVerificationQuorumSetV0::new(&config); + + let initial = make_quorums(&[(1, [1u8; 32])]); + qs.set_current_quorums(initial); + + let replacement = make_quorums(&[(2, [2u8; 32])]); + qs.replace_quorums(replacement, 100, 105); + + // verification_height=104, between 100 and 105 + let iter = qs.select_quorums(112, 104); + assert_eq!(iter.len(), 2); + assert!(!iter.should_be_verifiable()); + } + + #[test] + fn select_quorums_signing_at_or_below_offset_with_previous() { + // When signing_height <= SIGN_OFFSET, none of the first two branches match + // (both require signing_height > SIGN_OFFSET), so we fall to the else + // which pushes both current and previous. + let config = default_chain_lock_config(); + let mut qs = SignatureVerificationQuorumSetV0::new(&config); + + let initial = make_quorums(&[(1, [1u8; 32])]); + qs.set_current_quorums(initial); + + let replacement = make_quorums(&[(2, [2u8; 32])]); + qs.replace_quorums(replacement, 100, 105); + + // signing_height == SIGN_OFFSET (8), not > SIGN_OFFSET + let iter = qs.select_quorums(SIGN_OFFSET, 106); + assert_eq!(iter.len(), 2); + } + + #[test] + fn select_quorums_verifiable_with_previous_change_height() { + // When there's a previous_change_height (from two replacements), + // should_be_verifiable depends on verification_height > previous_change_height. + let config = default_chain_lock_config(); + let mut qs = SignatureVerificationQuorumSetV0::new(&config); + + let q1 = make_quorums(&[(1, [1u8; 32])]); + qs.set_current_quorums(q1); + + // First replacement: creates previous with previous_change_height = None + let q2 = make_quorums(&[(2, [2u8; 32])]); + qs.replace_quorums(q2, 90, 100); + + // Second replacement: creates previous with previous_change_height = Some(100) + let q3 = make_quorums(&[(3, [3u8; 32])]); + qs.replace_quorums(q3, 100, 110); + + // Case: verification_height (95) <= previous_quorum_height (100), + // and 95 < previous_change_height (100), so NOT verifiable + let iter = qs.select_quorums(106, 95); + assert_eq!(iter.len(), 1); // previous quorums only + assert!(!iter.should_be_verifiable()); + + // Case: verification_height (101) > previous_change_height (100), so verifiable + // and 101 between previous_quorum_height(100) and change_quorum_height(110) + let iter2 = qs.select_quorums(112, 101); + assert_eq!(iter2.len(), 2); // both current and previous + assert!(iter2.should_be_verifiable()); + } + + // ---- SelectedQuorumSetIterator ---- + + #[test] + fn selected_quorum_set_iterator_len_and_is_empty() { + let config = default_chain_lock_config(); + let mut qs = SignatureVerificationQuorumSetV0::new(&config); + + let current = make_quorums(&[(1, [1u8; 32])]); + qs.set_current_quorums(current); + + let iter = qs.select_quorums(20, 10); + assert_eq!(iter.len(), 1); + assert!(!iter.is_empty()); + } + + #[test] + fn selected_quorum_set_iterator_iteration() { + let config = default_chain_lock_config(); + let mut qs = SignatureVerificationQuorumSetV0::new(&config); + + let current = make_quorums(&[(1, [1u8; 32])]); + qs.set_current_quorums(current); + + let replacement = make_quorums(&[(2, [2u8; 32])]); + qs.replace_quorums(replacement, 100, 105); + + // Get both quorum sets by falling into the "between" branch + let iter = qs.select_quorums(112, 104); + let items: Vec<_> = iter.collect(); + assert_eq!(items.len(), 2); + // Each item should have a reference to the config + for item in &items { + assert_eq!(item.config.quorum_type, QuorumType::Llmq400_60); + } + } + + // ---- QuorumsWithConfig::choose_quorum ---- + + #[test] + fn quorums_with_config_choose_quorum_delegates() { + let config = default_chain_lock_config(); + let mut qs = SignatureVerificationQuorumSetV0::new(&config); + + let current = make_quorums(&[(1, [1u8; 32])]); + qs.set_current_quorums(current); + + let mut iter = qs.select_quorums(20, 10); + let quorums_with_config = iter.next().unwrap(); + + let request_id = [0u8; 32]; + let result = quorums_with_config.choose_quorum(&request_id); + assert!(result.is_some()); + } + + // ---- SIGN_OFFSET constant ---- + + #[test] + fn sign_offset_is_8() { + assert_eq!(SIGN_OFFSET, 8); + } +} diff --git a/packages/rs-drive-abci/src/platform_types/signature_verification_quorum_set/v0/quorums.rs b/packages/rs-drive-abci/src/platform_types/signature_verification_quorum_set/v0/quorums.rs index 7a38272f900..2781697e1dc 100644 --- a/packages/rs-drive-abci/src/platform_types/signature_verification_quorum_set/v0/quorums.rs +++ b/packages/rs-drive-abci/src/platform_types/signature_verification_quorum_set/v0/quorums.rs @@ -225,3 +225,313 @@ impl SigningQuorum { Ok(BLSSignature::from(signature.as_raw_value().to_compressed())) } } + +#[cfg(test)] +mod tests { + use super::*; + use dpp::bls_signatures::{Bls12381G2Impl, SecretKey as BlsPrivateKey}; + use dpp::dashcore::hashes::Hash; + use dpp::dashcore_rpc::json::QuorumType; + + /// Helper: generate a deterministic BLS public key from a seed byte. + fn make_public_key(seed: u8) -> ThresholdBlsPublicKey { + let mut key_bytes = [0u8; 32]; + key_bytes[0] = seed; + key_bytes[31] = 1; // ensure nonzero + let sk = BlsPrivateKey::::from_be_bytes(&key_bytes) + .expect("expected a valid secret key from test bytes"); + sk.public_key() + } + + fn make_verification_quorum(seed: u8, index: Option) -> VerificationQuorum { + VerificationQuorum { + index, + public_key: make_public_key(seed), + } + } + + fn make_classic_config() -> QuorumConfig { + QuorumConfig { + quorum_type: QuorumType::Llmq100_67, + active_signers: 24, + rotation: false, + window: 24, + } + } + + fn make_rotating_config(active_signers: u16) -> QuorumConfig { + QuorumConfig { + quorum_type: QuorumType::Llmq60_75, + active_signers, + rotation: true, + window: 24, + } + } + + // ---- Quorums default and construction ---- + + #[test] + fn quorums_default_is_empty() { + let q: Quorums = Quorums::default(); + assert!(q.is_empty()); + assert_eq!(q.len(), 0); + } + + #[test] + fn quorums_from_iter_collects_entries() { + let hash1 = QuorumHash::from_byte_array([1u8; 32]); + let hash2 = QuorumHash::from_byte_array([2u8; 32]); + let q: Quorums = vec![ + (hash1, make_verification_quorum(10, None)), + (hash2, make_verification_quorum(20, None)), + ] + .into_iter() + .collect(); + assert_eq!(q.len(), 2); + assert!(q.contains_key(&hash1)); + assert!(q.contains_key(&hash2)); + } + + #[test] + fn quorums_into_iter_yields_all_entries() { + let hash1 = QuorumHash::from_byte_array([3u8; 32]); + let hash2 = QuorumHash::from_byte_array([4u8; 32]); + let q: Quorums = vec![ + (hash1, make_verification_quorum(30, None)), + (hash2, make_verification_quorum(40, None)), + ] + .into_iter() + .collect(); + let entries: Vec<_> = q.into_iter().collect(); + assert_eq!(entries.len(), 2); + } + + #[test] + fn quorums_from_btreemap() { + let mut map = BTreeMap::new(); + map.insert( + QuorumHash::from_byte_array([5u8; 32]), + make_verification_quorum(50, None), + ); + let q: Quorums = Quorums::from(map); + assert_eq!(q.len(), 1); + } + + #[test] + fn quorums_deref_and_deref_mut() { + let hash = QuorumHash::from_byte_array([6u8; 32]); + let mut q: Quorums = Quorums::default(); + // DerefMut: insert via BTreeMap method + q.insert(hash, make_verification_quorum(60, None)); + assert_eq!(q.len(), 1); + // Deref: get via BTreeMap method + assert!(q.get(&hash).is_some()); + } + + // ---- choose_quorum: classic (DIP8) ---- + + #[test] + fn choose_classic_quorum_empty_returns_none() { + let q: Quorums = Quorums::default(); + let config = make_classic_config(); + let request_id = [0u8; 32]; + assert!(q.choose_quorum(&config, &request_id).is_none()); + } + + #[test] + fn choose_classic_quorum_single_returns_that_quorum() { + let hash = QuorumHash::from_byte_array([7u8; 32]); + let q: Quorums = vec![(hash, make_verification_quorum(70, None))] + .into_iter() + .collect(); + let config = make_classic_config(); + let request_id = [0u8; 32]; + let result = q.choose_quorum(&config, &request_id); + assert!(result.is_some()); + let (chosen_hash, _) = result.unwrap(); + assert_eq!(chosen_hash, hash); + } + + #[test] + fn choose_classic_quorum_deterministic() { + let hash1 = QuorumHash::from_byte_array([8u8; 32]); + let hash2 = QuorumHash::from_byte_array([9u8; 32]); + let q: Quorums = vec![ + (hash1, make_verification_quorum(80, None)), + (hash2, make_verification_quorum(90, None)), + ] + .into_iter() + .collect(); + let config = make_classic_config(); + let request_id = [42u8; 32]; + + let result1 = q.choose_quorum(&config, &request_id); + let result2 = q.choose_quorum(&config, &request_id); + assert_eq!(result1.unwrap().0, result2.unwrap().0); + } + + #[test] + fn choose_classic_quorum_different_request_ids_may_differ() { + let hash1 = QuorumHash::from_byte_array([10u8; 32]); + let hash2 = QuorumHash::from_byte_array([11u8; 32]); + let hash3 = QuorumHash::from_byte_array([12u8; 32]); + let q: Quorums = vec![ + (hash1, make_verification_quorum(1, None)), + (hash2, make_verification_quorum(2, None)), + (hash3, make_verification_quorum(3, None)), + ] + .into_iter() + .collect(); + let config = make_classic_config(); + + // Try many request IDs; at least two distinct choices should appear + let mut chosen = std::collections::HashSet::new(); + for i in 0u8..=255 { + let mut rid = [0u8; 32]; + rid[0] = i; + if let Some((h, _)) = q.choose_quorum(&config, &rid) { + chosen.insert(h); + } + } + assert!( + chosen.len() > 1, + "classic quorum selection should distribute across quorums" + ); + } + + // ---- choose_quorum: rotating (DIP24) ---- + + #[test] + fn choose_rotating_quorum_empty_returns_none() { + let q: Quorums = Quorums::default(); + let config = make_rotating_config(32); + let request_id = [0u8; 32]; + assert!(q.choose_quorum(&config, &request_id).is_none()); + } + + #[test] + fn choose_rotating_quorum_finds_matching_index() { + // active_signers = 32, so n = 5 (since 2^5 = 32), mask = 31 + // We need to control request_id so the computed signer index matches an existing quorum. + let config = make_rotating_config(32); + + // Build quorums with indices 0..31 + let quorums: Quorums = (0u32..32) + .map(|i| { + let mut hash_bytes = [0u8; 32]; + hash_bytes[0] = i as u8; + ( + QuorumHash::from_byte_array(hash_bytes), + make_verification_quorum(i as u8, Some(i)), + ) + }) + .collect(); + + let request_id = [0u8; 32]; + let result = quorums.choose_quorum(&config, &request_id); + assert!( + result.is_some(), + "rotating quorum should find a matching index" + ); + let (_, chosen_quorum) = result.unwrap(); + assert!(chosen_quorum.index.is_some()); + } + + #[test] + fn choose_rotating_quorum_no_matching_index_returns_none() { + // Create a quorum with an index that will likely not match the computed signer + let config = make_rotating_config(32); + // Only one quorum with index 999 (out of range for mask = 31) + let q: Quorums = vec![( + QuorumHash::from_byte_array([1u8; 32]), + make_verification_quorum(1, Some(999)), + )] + .into_iter() + .collect(); + + let request_id = [0u8; 32]; + let result = q.choose_quorum(&config, &request_id); + assert!( + result.is_none(), + "no quorum should match index 999 when mask is 31" + ); + } + + #[test] + fn choose_quorum_routes_by_config_rotation_flag() { + let hash = QuorumHash::from_byte_array([20u8; 32]); + let quorum = make_verification_quorum(20, Some(0)); + let q: Quorums = vec![(hash, quorum)].into_iter().collect(); + + let request_id = [0u8; 32]; + + // Non-rotating config should use classic selection + let classic_config = make_classic_config(); + let classic_result = q.choose_quorum(&classic_config, &request_id); + assert!(classic_result.is_some()); + + // Rotating config may or may not find a match depending on the computed signer + let rotating_config = make_rotating_config(1); + let _rotating_result = q.choose_quorum(&rotating_config, &request_id); + // We just verify it does not panic; result depends on signer calculation + } + + // ---- Quorum trait implementations ---- + + #[test] + fn verification_quorum_index_trait() { + let vq_none = make_verification_quorum(1, None); + assert_eq!(Quorum::index(&vq_none), None); + + let vq_some = make_verification_quorum(2, Some(42)); + assert_eq!(Quorum::index(&vq_some), Some(42)); + } + + #[test] + fn signing_quorum_index_trait() { + let sq = SigningQuorum { + index: Some(7), + private_key: [0u8; 32], + }; + assert_eq!(Quorum::index(&sq), Some(7)); + + let sq_none = SigningQuorum { + index: None, + private_key: [0u8; 32], + }; + assert_eq!(Quorum::index(&sq_none), None); + } + + // ---- Debug implementations ---- + + #[test] + fn verification_quorum_debug_format() { + let vq = make_verification_quorum(1, Some(5)); + let debug_str = format!("{:?}", vq); + assert!(debug_str.contains("VerificationQuorum")); + assert!(debug_str.contains("index")); + assert!(debug_str.contains("public_key")); + } + + #[test] + fn quorums_debug_format() { + let hash = QuorumHash::from_byte_array([1u8; 32]); + let q: Quorums = vec![(hash, make_verification_quorum(1, None))] + .into_iter() + .collect(); + let debug_str = format!("{:?}", q); + // Should use debug_map format with quorum hash strings as keys + assert!(!debug_str.is_empty()); + } + + #[test] + fn signing_quorum_debug_format() { + let sq = SigningQuorum { + index: Some(3), + private_key: [0u8; 32], + }; + let debug_str = format!("{:?}", sq); + assert!(debug_str.contains("SigningQuorum")); + assert!(debug_str.contains("index")); + } +}