From 1e035c025c756a9affb815f506b1b26d0cf35187 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 23 Apr 2026 20:10:52 +0800 Subject: [PATCH 1/5] test(drive): cover verify/state_transition error paths --- .../v0/mod.rs | 1755 +++++++++++++++++ 1 file changed, 1755 insertions(+) diff --git a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs index b4d15d23b6f..87d8cfff186 100644 --- a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs +++ b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs @@ -2779,6 +2779,1761 @@ mod tests { ); } + // ----------------------------------------------------------------------- + // Additional error-path coverage for state transitions whose error arms + // were not previously tested. These tests target proof decoding / early + // validation errors on state transition variants that were not covered + // by the existing test suite. + // ----------------------------------------------------------------------- + + // --- AddressFundsTransfer: empty proof returns error. + #[test] + fn verify_address_funds_transfer_empty_proof_returns_error() { + let platform_version = PlatformVersion::latest(); + use dpp::address_funds::PlatformAddress; + use dpp::state_transition::address_funds_transfer_transition::v0::AddressFundsTransferTransitionV0; + use dpp::state_transition::address_funds_transfer_transition::AddressFundsTransferTransition; + use std::collections::BTreeMap; + + let mut inputs = BTreeMap::new(); + inputs.insert(PlatformAddress::P2pkh([1u8; 20]), (1u32, 1000u64)); + let mut outputs = BTreeMap::new(); + outputs.insert(PlatformAddress::P2pkh([2u8; 20]), 500u64); + + let st = StateTransition::AddressFundsTransfer(AddressFundsTransferTransition::V0( + AddressFundsTransferTransitionV0 { + inputs, + outputs, + fee_strategy: vec![], + user_fee_increase: 0, + input_witnesses: vec![], + }, + )); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected error for address funds transfer with empty proof, got: {:?}", + result + ); + } + + // --- AddressCreditWithdrawal: empty proof returns error. + #[test] + fn verify_address_credit_withdrawal_empty_proof_returns_error() { + let platform_version = PlatformVersion::latest(); + use dpp::address_funds::PlatformAddress; + use dpp::identity::core_script::CoreScript; + use dpp::state_transition::address_credit_withdrawal_transition::v0::AddressCreditWithdrawalTransitionV0; + use dpp::state_transition::address_credit_withdrawal_transition::AddressCreditWithdrawalTransition; + use dpp::withdrawal::Pooling; + use std::collections::BTreeMap; + + let mut inputs = BTreeMap::new(); + inputs.insert(PlatformAddress::P2pkh([3u8; 20]), (1u32, 2000u64)); + + let st = StateTransition::AddressCreditWithdrawal(AddressCreditWithdrawalTransition::V0( + AddressCreditWithdrawalTransitionV0 { + inputs, + output: None, + fee_strategy: vec![], + core_fee_per_byte: 1, + pooling: Pooling::Never, + output_script: CoreScript::from_bytes(vec![]), + user_fee_increase: 0, + input_witnesses: vec![], + }, + )); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected error for address credit withdrawal with empty proof, got: {:?}", + result + ); + } + + // --- AddressCreditWithdrawal with change output: empty proof returns error. + #[test] + fn verify_address_credit_withdrawal_with_change_output_empty_proof_returns_error() { + let platform_version = PlatformVersion::latest(); + use dpp::address_funds::PlatformAddress; + use dpp::identity::core_script::CoreScript; + use dpp::state_transition::address_credit_withdrawal_transition::v0::AddressCreditWithdrawalTransitionV0; + use dpp::state_transition::address_credit_withdrawal_transition::AddressCreditWithdrawalTransition; + use dpp::withdrawal::Pooling; + use std::collections::BTreeMap; + + let mut inputs = BTreeMap::new(); + inputs.insert(PlatformAddress::P2pkh([3u8; 20]), (1u32, 2000u64)); + + let st = StateTransition::AddressCreditWithdrawal(AddressCreditWithdrawalTransition::V0( + AddressCreditWithdrawalTransitionV0 { + inputs, + output: Some((PlatformAddress::P2pkh([4u8; 20]), 1000u64)), + fee_strategy: vec![], + core_fee_per_byte: 1, + pooling: Pooling::Never, + output_script: CoreScript::from_bytes(vec![]), + user_fee_increase: 0, + input_witnesses: vec![], + }, + )); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected error for address credit withdrawal (with change) empty proof, got: {:?}", + result + ); + } + + // --- AddressFundingFromAssetLock: empty proof returns error. + #[test] + fn verify_address_funding_from_asset_lock_empty_proof_returns_error() { + let platform_version = PlatformVersion::latest(); + use dpp::address_funds::PlatformAddress; + use dpp::identity::state_transition::asset_lock_proof::AssetLockProof; + use dpp::identity::state_transition::asset_lock_proof::InstantAssetLockProof; + use dpp::platform_value::BinaryData; + use dpp::state_transition::address_funding_from_asset_lock_transition::v0::AddressFundingFromAssetLockTransitionV0; + use dpp::state_transition::address_funding_from_asset_lock_transition::AddressFundingFromAssetLockTransition; + use std::collections::BTreeMap; + + let mut outputs = BTreeMap::new(); + outputs.insert(PlatformAddress::P2pkh([7u8; 20]), None); + + let st = StateTransition::AddressFundingFromAssetLock( + AddressFundingFromAssetLockTransition::V0(AddressFundingFromAssetLockTransitionV0 { + asset_lock_proof: AssetLockProof::Instant(InstantAssetLockProof::default()), + inputs: BTreeMap::new(), + outputs, + fee_strategy: vec![], + user_fee_increase: 0, + signature: BinaryData::new(vec![]), + input_witnesses: vec![], + }), + ); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected error for address funding from asset lock with empty proof, got: {:?}", + result + ); + } + + // --- Shield: empty proof returns error. + #[test] + fn verify_shield_empty_proof_returns_error() { + let platform_version = PlatformVersion::latest(); + use dpp::address_funds::PlatformAddress; + use dpp::state_transition::shield_transition::v0::ShieldTransitionV0; + use dpp::state_transition::shield_transition::ShieldTransition; + use std::collections::BTreeMap; + + let mut inputs = BTreeMap::new(); + inputs.insert(PlatformAddress::P2pkh([8u8; 20]), (1u32, 1000u64)); + + let st = StateTransition::Shield(ShieldTransition::V0(ShieldTransitionV0 { + inputs, + actions: vec![], + amount: 500, + anchor: [0u8; 32], + proof: vec![], + binding_signature: [0u8; 64], + fee_strategy: vec![], + user_fee_increase: 0, + input_witnesses: vec![], + })); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected error for shield with empty proof, got: {:?}", + result + ); + } + + // --- ShieldFromAssetLock with invalid (no-output) Instant asset lock proof + // should return InvalidTransition("shield from asset lock has no outpoint"). + // Default InstantAssetLockProof has no AssetLockPayload, so out_point() returns None. + #[test] + fn verify_shield_from_asset_lock_missing_outpoint_returns_invalid_transition() { + let platform_version = PlatformVersion::latest(); + use dpp::identity::state_transition::asset_lock_proof::AssetLockProof; + use dpp::identity::state_transition::asset_lock_proof::InstantAssetLockProof; + use dpp::platform_value::BinaryData; + use dpp::state_transition::shield_from_asset_lock_transition::v0::ShieldFromAssetLockTransitionV0; + use dpp::state_transition::shield_from_asset_lock_transition::ShieldFromAssetLockTransition; + + let st = StateTransition::ShieldFromAssetLock(ShieldFromAssetLockTransition::V0( + ShieldFromAssetLockTransitionV0 { + asset_lock_proof: AssetLockProof::Instant(InstantAssetLockProof::default()), + actions: vec![], + value_balance: 0, + anchor: [0u8; 32], + proof: vec![], + binding_signature: [0u8; 64], + signature: BinaryData::new(vec![]), + }, + )); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + let err = result.expect_err("expected InvalidTransition error, got Ok"); + match err { + crate::error::Error::Proof(ProofError::InvalidTransition(msg)) => { + assert!( + msg.contains("shield from asset lock has no outpoint"), + "unexpected InvalidTransition message: {msg}" + ); + } + other => panic!("expected Error::Proof(InvalidTransition), got {:?}", other), + } + } + + // --- ShieldFromAssetLock with Chain asset lock proof (has outpoint) and + // empty GroveDB proof: should return an error from grove-db verification. + #[test] + fn verify_shield_from_asset_lock_chain_empty_proof_returns_error() { + let platform_version = PlatformVersion::latest(); + use dpp::dashcore::OutPoint; + use dpp::identity::state_transition::asset_lock_proof::chain::ChainAssetLockProof; + use dpp::identity::state_transition::asset_lock_proof::AssetLockProof; + use dpp::platform_value::BinaryData; + use dpp::state_transition::shield_from_asset_lock_transition::v0::ShieldFromAssetLockTransitionV0; + use dpp::state_transition::shield_from_asset_lock_transition::ShieldFromAssetLockTransition; + + let st = StateTransition::ShieldFromAssetLock(ShieldFromAssetLockTransition::V0( + ShieldFromAssetLockTransitionV0 { + asset_lock_proof: AssetLockProof::Chain(ChainAssetLockProof { + core_chain_locked_height: 100, + out_point: OutPoint::from([11u8; 36]), + }), + actions: vec![], + value_balance: 0, + anchor: [0u8; 32], + proof: vec![], + binding_signature: [0u8; 64], + signature: BinaryData::new(vec![]), + }, + )); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected error for shield from asset lock (chain) with empty proof, got: {:?}", + result + ); + } + + // --- IdentityCreateFromAddresses: empty proof returns error. + #[test] + fn verify_identity_create_from_addresses_empty_proof_returns_error() { + let platform_version = PlatformVersion::latest(); + use dpp::address_funds::PlatformAddress; + use dpp::state_transition::identity_create_from_addresses_transition::v0::IdentityCreateFromAddressesTransitionV0; + use dpp::state_transition::identity_create_from_addresses_transition::IdentityCreateFromAddressesTransition; + use std::collections::BTreeMap; + + let mut inputs = BTreeMap::new(); + inputs.insert(PlatformAddress::P2pkh([9u8; 20]), (1u32, 2000u64)); + + let st = StateTransition::IdentityCreateFromAddresses( + IdentityCreateFromAddressesTransition::V0(IdentityCreateFromAddressesTransitionV0 { + public_keys: vec![], + inputs, + output: None, + fee_strategy: vec![], + user_fee_increase: 0, + input_witnesses: vec![], + }), + ); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected error for identity create from addresses with empty proof, got: {:?}", + result + ); + } + + // --- IdentityTopUpFromAddresses: empty proof returns error. + #[test] + fn verify_identity_top_up_from_addresses_empty_proof_returns_error() { + let platform_version = PlatformVersion::latest(); + use dpp::address_funds::PlatformAddress; + use dpp::state_transition::identity_topup_from_addresses_transition::v0::IdentityTopUpFromAddressesTransitionV0; + use dpp::state_transition::identity_topup_from_addresses_transition::IdentityTopUpFromAddressesTransition; + use std::collections::BTreeMap; + + let mut inputs = BTreeMap::new(); + inputs.insert(PlatformAddress::P2pkh([10u8; 20]), (1u32, 500u64)); + + let st = StateTransition::IdentityTopUpFromAddresses( + IdentityTopUpFromAddressesTransition::V0(IdentityTopUpFromAddressesTransitionV0 { + inputs, + output: None, + identity_id: dpp::prelude::Identifier::from([11u8; 32]), + fee_strategy: vec![], + user_fee_increase: 0, + input_witnesses: vec![], + }), + ); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected error for identity top up from addresses with empty proof, got: {:?}", + result + ); + } + + // --- IdentityCreditWithdrawal V1 empty proof returns error. + #[test] + fn verify_identity_credit_withdrawal_v1_empty_proof_returns_error() { + let platform_version = PlatformVersion::latest(); + use dpp::state_transition::identity_credit_withdrawal_transition::v1::IdentityCreditWithdrawalTransitionV1; + use dpp::state_transition::identity_credit_withdrawal_transition::IdentityCreditWithdrawalTransition; + use dpp::withdrawal::Pooling; + + let st = StateTransition::IdentityCreditWithdrawal(IdentityCreditWithdrawalTransition::V1( + IdentityCreditWithdrawalTransitionV1 { + identity_id: dpp::prelude::Identifier::random(), + amount: 100, + core_fee_per_byte: 1, + pooling: Pooling::Never, + output_script: None, + nonce: 1, + user_fee_increase: 0, + ..Default::default() + }, + )); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected error for identity credit withdrawal v1 with empty proof, got: {:?}", + result + ); + } + + // --- MasternodeVote: empty proof (but with a known contract) returns + // an Error::Proof / Error::GroveDB because the proof can't be decoded. + #[test] + fn verify_masternode_vote_empty_proof_with_known_contract_returns_error() { + let (_drive, contract) = setup_drive_and_contract(); + let platform_version = PlatformVersion::latest(); + use dpp::state_transition::masternode_vote_transition::v0::MasternodeVoteTransitionV0; + use dpp::state_transition::masternode_vote_transition::MasternodeVoteTransition; + use dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; + use dpp::voting::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePoll; + use dpp::voting::vote_polls::VotePoll; + use dpp::voting::votes::resource_vote::v0::ResourceVoteV0; + use dpp::voting::votes::resource_vote::ResourceVote; + use dpp::voting::votes::Vote; + + let st = StateTransition::MasternodeVote(MasternodeVoteTransition::V0( + MasternodeVoteTransitionV0 { + pro_tx_hash: dpp::prelude::Identifier::from([1u8; 32]), + voter_identity_id: dpp::prelude::Identifier::from([2u8; 32]), + vote: Vote::ResourceVote(ResourceVote::V0(ResourceVoteV0 { + vote_poll: VotePoll::ContestedDocumentResourceVotePoll( + ContestedDocumentResourceVotePoll { + contract_id: contract.id(), + document_type_name: "preorder".to_string(), + index_name: "idx".to_string(), + index_values: vec![], + }, + ), + resource_vote_choice: ResourceVoteChoice::Abstain, + })), + nonce: 1, + signature_public_key_id: 0, + signature: Default::default(), + }, + )); + + let contract_arc = Arc::new(contract.clone()); + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(Some(contract_arc.clone())); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected error for masternode vote with empty proof, got: {:?}", + result + ); + } + + // --- ShieldedWithdrawal with no nullifiers and a known contract that + // triggers InvalidTransition("shielded withdrawal has no nullifiers") via + // the first_nullifier guard. + // + // We route past the grove-db verification by providing `actions: vec![]` + // (so nullifiers() is empty) and a valid empty proof for nullifiers. + // Because proof is empty, grove-db errors out first — we only check for + // some Error variant here (this is a defensive test that ensures the + // known-contract branch doesn't crash). + #[test] + fn verify_shielded_withdrawal_with_known_contract_empty_proof_returns_error() { + let platform_version = PlatformVersion::latest(); + use dpp::identity::core_script::CoreScript; + use dpp::state_transition::shielded_withdrawal_transition::v0::ShieldedWithdrawalTransitionV0; + use dpp::state_transition::shielded_withdrawal_transition::ShieldedWithdrawalTransition; + use dpp::withdrawal::Pooling; + + let st = StateTransition::ShieldedWithdrawal(ShieldedWithdrawalTransition::V0( + ShieldedWithdrawalTransitionV0 { + actions: vec![], + unshielding_amount: 0, + anchor: [0u8; 32], + proof: vec![], + binding_signature: [0u8; 64], + core_fee_per_byte: 1, + pooling: Pooling::Never, + output_script: CoreScript::from_bytes(vec![]), + }, + )); + + // Load the real withdrawals contract so the known_contracts_provider + // returns Some. Empty proof will still fail, but we exercise the + // contract-lookup path. + use dpp::data_contracts::withdrawals_contract; + use dpp::system_data_contracts::{load_system_data_contract, SystemDataContract}; + let withdrawals = + load_system_data_contract(SystemDataContract::Withdrawals, platform_version) + .expect("expected withdrawals system contract"); + let withdrawals_arc = Arc::new(withdrawals); + let expected_id = withdrawals_contract::ID; + let known_contracts_provider_fn: &ContractLookupFn = &|id| { + if id == &expected_id { + Ok(Some(withdrawals_arc.clone())) + } else { + Ok(None) + } + }; + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected error for shielded withdrawal (known contract, empty proof), got: {:?}", + result + ); + } + + // --- Batch V1 with Token Mint transition + unknown contract returns + // UnknownContract error (token transition branch). + #[test] + fn verify_batch_token_mint_unknown_contract_returns_error() { + let platform_version = PlatformVersion::latest(); + + use dpp::state_transition::batch_transition::batched_transition::token_mint_transition::v0::TokenMintTransitionV0; + use dpp::state_transition::batch_transition::batched_transition::token_mint_transition::TokenMintTransition; + use dpp::state_transition::batch_transition::batched_transition::BatchedTransition; + use dpp::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; + use dpp::state_transition::batch_transition::token_base_transition::TokenBaseTransition; + use dpp::state_transition::batch_transition::BatchTransition; + use dpp::state_transition::batch_transition::BatchTransitionV1; + + let token_base = TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract_id: dpp::prelude::Identifier::from([44u8; 32]), + token_id: dpp::prelude::Identifier::from([45u8; 32]), + using_group_info: None, + }); + let token_transition = + TokenTransition::Mint(TokenMintTransition::V0(TokenMintTransitionV0 { + base: token_base, + amount: 99, + issued_to_identity_id: Some(dpp::prelude::Identifier::from([77u8; 32])), + public_note: None, + })); + + let st = StateTransition::Batch(BatchTransition::V1(BatchTransitionV1 { + owner_id: dpp::prelude::Identifier::from([2u8; 32]), + transitions: vec![BatchedTransition::Token(token_transition)], + user_fee_increase: 0, + signature_public_key_id: 0, + signature: Default::default(), + })); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + let err = result.expect_err("expected UnknownContract, got Ok"); + match err { + crate::error::Error::Proof(ProofError::UnknownContract(msg)) => { + assert!( + msg.contains("unknown contract") && msg.contains("token verification"), + "unexpected UnknownContract message: {msg}" + ); + } + other => panic!("expected Error::Proof(UnknownContract), got {:?}", other), + } + } + + // --- Batch V1 with Token Transfer transition + unknown contract. + #[test] + fn verify_batch_token_transfer_unknown_contract_returns_error() { + let platform_version = PlatformVersion::latest(); + + use dpp::state_transition::batch_transition::batched_transition::token_transfer_transition::v0::TokenTransferTransitionV0; + use dpp::state_transition::batch_transition::batched_transition::token_transfer_transition::TokenTransferTransition; + use dpp::state_transition::batch_transition::batched_transition::BatchedTransition; + use dpp::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; + use dpp::state_transition::batch_transition::token_base_transition::TokenBaseTransition; + use dpp::state_transition::batch_transition::BatchTransition; + use dpp::state_transition::batch_transition::BatchTransitionV1; + + let token_base = TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract_id: dpp::prelude::Identifier::from([58u8; 32]), + token_id: dpp::prelude::Identifier::from([59u8; 32]), + using_group_info: None, + }); + let token_transition = + TokenTransition::Transfer(TokenTransferTransition::V0(TokenTransferTransitionV0 { + base: token_base, + amount: 100, + recipient_id: dpp::prelude::Identifier::from([88u8; 32]), + public_note: None, + shared_encrypted_note: None, + private_encrypted_note: None, + })); + + let st = StateTransition::Batch(BatchTransition::V1(BatchTransitionV1 { + owner_id: dpp::prelude::Identifier::from([3u8; 32]), + transitions: vec![BatchedTransition::Token(token_transition)], + user_fee_increase: 0, + signature_public_key_id: 0, + signature: Default::default(), + })); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + let err = result.expect_err("expected UnknownContract, got Ok"); + assert!( + matches!( + err, + crate::error::Error::Proof(ProofError::UnknownContract(_)) + ), + "expected Error::Proof(UnknownContract), got: {:?}", + err + ); + } + + // --- Batch V1 with Token Freeze transition + unknown contract. + #[test] + fn verify_batch_token_freeze_unknown_contract_returns_error() { + let platform_version = PlatformVersion::latest(); + + use dpp::state_transition::batch_transition::batched_transition::token_freeze_transition::v0::TokenFreezeTransitionV0; + use dpp::state_transition::batch_transition::batched_transition::token_freeze_transition::TokenFreezeTransition; + use dpp::state_transition::batch_transition::batched_transition::BatchedTransition; + use dpp::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; + use dpp::state_transition::batch_transition::token_base_transition::TokenBaseTransition; + use dpp::state_transition::batch_transition::BatchTransition; + use dpp::state_transition::batch_transition::BatchTransitionV1; + + let token_base = TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract_id: dpp::prelude::Identifier::from([71u8; 32]), + token_id: dpp::prelude::Identifier::from([72u8; 32]), + using_group_info: None, + }); + let token_transition = + TokenTransition::Freeze(TokenFreezeTransition::V0(TokenFreezeTransitionV0 { + base: token_base, + identity_to_freeze_id: dpp::prelude::Identifier::from([91u8; 32]), + public_note: None, + })); + + let st = StateTransition::Batch(BatchTransition::V1(BatchTransitionV1 { + owner_id: dpp::prelude::Identifier::from([4u8; 32]), + transitions: vec![BatchedTransition::Token(token_transition)], + user_fee_increase: 0, + signature_public_key_id: 0, + signature: Default::default(), + })); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + let err = result.expect_err("expected UnknownContract, got Ok"); + assert!( + matches!( + err, + crate::error::Error::Proof(ProofError::UnknownContract(_)) + ), + "expected Error::Proof(UnknownContract), got: {:?}", + err + ); + } + + // --- Batch V1 with Token Unfreeze transition + unknown contract. + #[test] + fn verify_batch_token_unfreeze_unknown_contract_returns_error() { + let platform_version = PlatformVersion::latest(); + + use dpp::state_transition::batch_transition::batched_transition::token_unfreeze_transition::v0::TokenUnfreezeTransitionV0; + use dpp::state_transition::batch_transition::batched_transition::token_unfreeze_transition::TokenUnfreezeTransition; + use dpp::state_transition::batch_transition::batched_transition::BatchedTransition; + use dpp::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; + use dpp::state_transition::batch_transition::token_base_transition::TokenBaseTransition; + use dpp::state_transition::batch_transition::BatchTransition; + use dpp::state_transition::batch_transition::BatchTransitionV1; + + let token_base = TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract_id: dpp::prelude::Identifier::from([81u8; 32]), + token_id: dpp::prelude::Identifier::from([82u8; 32]), + using_group_info: None, + }); + let token_transition = + TokenTransition::Unfreeze(TokenUnfreezeTransition::V0(TokenUnfreezeTransitionV0 { + base: token_base, + frozen_identity_id: dpp::prelude::Identifier::from([92u8; 32]), + public_note: None, + })); + + let st = StateTransition::Batch(BatchTransition::V1(BatchTransitionV1 { + owner_id: dpp::prelude::Identifier::from([5u8; 32]), + transitions: vec![BatchedTransition::Token(token_transition)], + user_fee_increase: 0, + signature_public_key_id: 0, + signature: Default::default(), + })); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + let err = result.expect_err("expected UnknownContract, got Ok"); + assert!( + matches!( + err, + crate::error::Error::Proof(ProofError::UnknownContract(_)) + ), + "expected Error::Proof(UnknownContract), got: {:?}", + err + ); + } + + // --- Batch V1 with Token DestroyFrozenFunds transition + unknown contract. + #[test] + fn verify_batch_token_destroy_frozen_funds_unknown_contract_returns_error() { + let platform_version = PlatformVersion::latest(); + + use dpp::state_transition::batch_transition::batched_transition::token_destroy_frozen_funds_transition::v0::TokenDestroyFrozenFundsTransitionV0; + use dpp::state_transition::batch_transition::batched_transition::token_destroy_frozen_funds_transition::TokenDestroyFrozenFundsTransition; + use dpp::state_transition::batch_transition::batched_transition::BatchedTransition; + use dpp::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; + use dpp::state_transition::batch_transition::token_base_transition::TokenBaseTransition; + use dpp::state_transition::batch_transition::BatchTransition; + use dpp::state_transition::batch_transition::BatchTransitionV1; + + let token_base = TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract_id: dpp::prelude::Identifier::from([101u8; 32]), + token_id: dpp::prelude::Identifier::from([102u8; 32]), + using_group_info: None, + }); + let token_transition = TokenTransition::DestroyFrozenFunds( + TokenDestroyFrozenFundsTransition::V0(TokenDestroyFrozenFundsTransitionV0 { + base: token_base, + frozen_identity_id: dpp::prelude::Identifier::from([103u8; 32]), + public_note: None, + }), + ); + + let st = StateTransition::Batch(BatchTransition::V1(BatchTransitionV1 { + owner_id: dpp::prelude::Identifier::from([6u8; 32]), + transitions: vec![BatchedTransition::Token(token_transition)], + user_fee_increase: 0, + signature_public_key_id: 0, + signature: Default::default(), + })); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + let err = result.expect_err("expected UnknownContract, got Ok"); + assert!( + matches!( + err, + crate::error::Error::Proof(ProofError::UnknownContract(_)) + ), + "expected Error::Proof(UnknownContract), got: {:?}", + err + ); + } + + // --- Batch V1 with Token Claim transition + unknown contract. + #[test] + fn verify_batch_token_claim_unknown_contract_returns_error() { + let platform_version = PlatformVersion::latest(); + + use dpp::state_transition::batch_transition::batched_transition::token_claim_transition::v0::TokenClaimTransitionV0; + use dpp::state_transition::batch_transition::batched_transition::token_claim_transition::TokenClaimTransition; + use dpp::state_transition::batch_transition::batched_transition::BatchedTransition; + use dpp::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; + use dpp::state_transition::batch_transition::token_base_transition::TokenBaseTransition; + use dpp::data_contract::associated_token::token_distribution_key::TokenDistributionType; + use dpp::state_transition::batch_transition::BatchTransition; + use dpp::state_transition::batch_transition::BatchTransitionV1; + + let token_base = TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract_id: dpp::prelude::Identifier::from([111u8; 32]), + token_id: dpp::prelude::Identifier::from([112u8; 32]), + using_group_info: None, + }); + let token_transition = + TokenTransition::Claim(TokenClaimTransition::V0(TokenClaimTransitionV0 { + base: token_base, + distribution_type: TokenDistributionType::PreProgrammed, + public_note: None, + })); + + let st = StateTransition::Batch(BatchTransition::V1(BatchTransitionV1 { + owner_id: dpp::prelude::Identifier::from([7u8; 32]), + transitions: vec![BatchedTransition::Token(token_transition)], + user_fee_increase: 0, + signature_public_key_id: 0, + signature: Default::default(), + })); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + let err = result.expect_err("expected UnknownContract, got Ok"); + assert!( + matches!( + err, + crate::error::Error::Proof(ProofError::UnknownContract(_)) + ), + "expected Error::Proof(UnknownContract), got: {:?}", + err + ); + } + + // --- Batch with a single document Replace transition + unknown contract + // returns UnknownContract. + #[test] + fn verify_batch_document_replace_unknown_contract_returns_error() { + let platform_version = PlatformVersion::latest(); + + use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; + use dpp::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; + use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; + use dpp::state_transition::batch_transition::document_replace_transition::DocumentReplaceTransition; + use dpp::state_transition::batch_transition::document_replace_transition::DocumentReplaceTransitionV0; + use dpp::state_transition::batch_transition::BatchTransition; + use dpp::state_transition::batch_transition::BatchTransitionV0; + + let base = DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: Default::default(), + identity_contract_nonce: 1, + document_type_name: "test".to_string(), + data_contract_id: Default::default(), + }); + + let st = StateTransition::Batch(BatchTransition::V0(BatchTransitionV0 { + owner_id: Default::default(), + transitions: vec![DocumentTransition::Replace(DocumentReplaceTransition::V0( + DocumentReplaceTransitionV0 { + base, + revision: 2, + data: Default::default(), + }, + ))], + ..Default::default() + })); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + let err = result.expect_err("expected UnknownContract, got Ok"); + assert!( + matches!( + err, + crate::error::Error::Proof(ProofError::UnknownContract(_)) + ), + "expected Error::Proof(UnknownContract), got: {:?}", + err + ); + } + + // --- Batch with a single document Transfer transition + unknown contract. + #[test] + fn verify_batch_document_transfer_unknown_contract_returns_error() { + let platform_version = PlatformVersion::latest(); + + use dpp::state_transition::batch_transition::batched_transition::document_transfer_transition::DocumentTransferTransition; + use dpp::state_transition::batch_transition::batched_transition::document_transfer_transition::DocumentTransferTransitionV0; + use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; + use dpp::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; + use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; + use dpp::state_transition::batch_transition::BatchTransition; + use dpp::state_transition::batch_transition::BatchTransitionV0; + + let base = DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: Default::default(), + identity_contract_nonce: 1, + document_type_name: "test".to_string(), + data_contract_id: Default::default(), + }); + + let st = StateTransition::Batch(BatchTransition::V0(BatchTransitionV0 { + owner_id: Default::default(), + transitions: vec![DocumentTransition::Transfer( + DocumentTransferTransition::V0(DocumentTransferTransitionV0 { + base, + revision: 1, + recipient_owner_id: dpp::prelude::Identifier::from([77u8; 32]), + }), + )], + ..Default::default() + })); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + let err = result.expect_err("expected UnknownContract, got Ok"); + assert!( + matches!( + err, + crate::error::Error::Proof(ProofError::UnknownContract(_)) + ), + "expected Error::Proof(UnknownContract), got: {:?}", + err + ); + } + + // --- Batch with a single document UpdatePrice transition + unknown contract. + #[test] + fn verify_batch_document_update_price_unknown_contract_returns_error() { + let platform_version = PlatformVersion::latest(); + + use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; + use dpp::state_transition::batch_transition::batched_transition::document_update_price_transition::DocumentUpdatePriceTransition; + use dpp::state_transition::batch_transition::batched_transition::document_update_price_transition::DocumentUpdatePriceTransitionV0; + use dpp::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; + use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; + use dpp::state_transition::batch_transition::BatchTransition; + use dpp::state_transition::batch_transition::BatchTransitionV0; + + let base = DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: Default::default(), + identity_contract_nonce: 1, + document_type_name: "test".to_string(), + data_contract_id: Default::default(), + }); + + let st = StateTransition::Batch(BatchTransition::V0(BatchTransitionV0 { + owner_id: Default::default(), + transitions: vec![DocumentTransition::UpdatePrice( + DocumentUpdatePriceTransition::V0(DocumentUpdatePriceTransitionV0 { + base, + revision: 1, + price: 500, + }), + )], + ..Default::default() + })); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + let err = result.expect_err("expected UnknownContract, got Ok"); + assert!( + matches!( + err, + crate::error::Error::Proof(ProofError::UnknownContract(_)) + ), + "expected Error::Proof(UnknownContract), got: {:?}", + err + ); + } + + // --- Batch with a single document Purchase transition + unknown contract. + #[test] + fn verify_batch_document_purchase_unknown_contract_returns_error() { + let platform_version = PlatformVersion::latest(); + + use dpp::state_transition::batch_transition::batched_transition::document_purchase_transition::DocumentPurchaseTransition; + use dpp::state_transition::batch_transition::batched_transition::document_purchase_transition::DocumentPurchaseTransitionV0; + use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; + use dpp::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; + use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; + use dpp::state_transition::batch_transition::BatchTransition; + use dpp::state_transition::batch_transition::BatchTransitionV0; + + let base = DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: Default::default(), + identity_contract_nonce: 1, + document_type_name: "test".to_string(), + data_contract_id: Default::default(), + }); + + let st = StateTransition::Batch(BatchTransition::V0(BatchTransitionV0 { + owner_id: Default::default(), + transitions: vec![DocumentTransition::Purchase( + DocumentPurchaseTransition::V0(DocumentPurchaseTransitionV0 { + base, + revision: 1, + price: 500, + }), + )], + ..Default::default() + })); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + let err = result.expect_err("expected UnknownContract, got Ok"); + assert!( + matches!( + err, + crate::error::Error::Proof(ProofError::UnknownContract(_)) + ), + "expected Error::Proof(UnknownContract), got: {:?}", + err + ); + } + + // --- Batch V1 with an empty transitions vec returns InvalidTransition + // (covers the V1 batch "no transition" arm). + #[test] + fn verify_batch_v1_empty_transitions_returns_error() { + let platform_version = PlatformVersion::latest(); + + use dpp::state_transition::batch_transition::BatchTransition; + use dpp::state_transition::batch_transition::BatchTransitionV1; + let st = StateTransition::Batch(BatchTransition::V1(BatchTransitionV1 { + owner_id: Default::default(), + transitions: vec![], + user_fee_increase: 0, + signature_public_key_id: 0, + signature: Default::default(), + })); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + let err = result.expect_err("expected InvalidTransition, got Ok"); + match err { + Error::Proof(ProofError::InvalidTransition(msg)) => { + assert!( + msg.contains("no transition"), + "expected 'no transition' message, got: {msg}" + ); + } + other => panic!("expected InvalidTransition, got: {:?}", other), + } + } + + // --- Batch V1 with two transitions returns InvalidTransition (too-many arm). + #[test] + fn verify_batch_v1_too_many_transitions_returns_error() { + let platform_version = PlatformVersion::latest(); + + use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; + use dpp::state_transition::batch_transition::batched_transition::BatchedTransition; + use dpp::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; + use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; + use dpp::state_transition::batch_transition::document_delete_transition::DocumentDeleteTransition; + use dpp::state_transition::batch_transition::document_delete_transition::DocumentDeleteTransitionV0; + use dpp::state_transition::batch_transition::BatchTransition; + use dpp::state_transition::batch_transition::BatchTransitionV1; + + let make_delete = |nonce: u64| -> BatchedTransition { + let base = DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: Default::default(), + identity_contract_nonce: nonce, + document_type_name: "x".to_string(), + data_contract_id: Default::default(), + }); + BatchedTransition::Document(DocumentTransition::Delete(DocumentDeleteTransition::V0( + DocumentDeleteTransitionV0 { base }, + ))) + }; + + let st = StateTransition::Batch(BatchTransition::V1(BatchTransitionV1 { + owner_id: Default::default(), + transitions: vec![make_delete(1), make_delete(2)], + user_fee_increase: 0, + signature_public_key_id: 0, + signature: Default::default(), + })); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + let err = result.expect_err("expected InvalidTransition, got Ok"); + match err { + Error::Proof(ProofError::InvalidTransition(msg)) => { + assert!( + msg.contains("does not support more than one document"), + "expected too-many-document message, got: {msg}" + ); + } + other => panic!("expected InvalidTransition, got: {:?}", other), + } + } + + // --- IdentityCreditTransferToAddresses with address recipients returns + // error for empty proof (exercises the recipient_addresses()-iteration arm). + #[test] + fn verify_identity_credit_transfer_to_addresses_with_recipients_empty_proof_errors() { + let platform_version = PlatformVersion::latest(); + use dpp::address_funds::PlatformAddress; + use dpp::state_transition::identity_credit_transfer_to_addresses_transition::v0::IdentityCreditTransferToAddressesTransitionV0; + use dpp::state_transition::identity_credit_transfer_to_addresses_transition::IdentityCreditTransferToAddressesTransition; + use std::collections::BTreeMap; + + let mut recipient_addresses = BTreeMap::new(); + recipient_addresses.insert(PlatformAddress::P2pkh([21u8; 20]), 100u64); + + let st = StateTransition::IdentityCreditTransferToAddresses( + IdentityCreditTransferToAddressesTransition::V0( + IdentityCreditTransferToAddressesTransitionV0 { + identity_id: dpp::prelude::Identifier::from([22u8; 32]), + recipient_addresses, + ..Default::default() + }, + ), + ); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected error for ICTTA with recipients empty proof, got: {:?}", + result + ); + } + + // --- Non-empty, malformed proof bytes → decode / verification error. + // + // Exercises the decoder path inside grove-db for a non-empty but + // garbage proof, which should error out (either Error::GroveDB or + // Error::Proof). + #[test] + fn verify_identity_create_garbage_proof_returns_error() { + let platform_version = PlatformVersion::latest(); + use dpp::state_transition::identity_create_transition::IdentityCreateTransition; + use dpp::state_transition::state_transitions::identity::identity_create_transition::v0::IdentityCreateTransitionV0; + let st = StateTransition::IdentityCreate(IdentityCreateTransition::V0( + IdentityCreateTransitionV0 { + identity_id: dpp::prelude::Identifier::random(), + ..Default::default() + }, + )); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + // A short, non-empty garbage proof triggers the decoder error path. + let bad_proof: [u8; 8] = [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x11]; + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &bad_proof, + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected error for garbage proof bytes, got: {:?}", + result + ); + } + + // --- DataContractCreate with a larger garbage proof returns an error. + #[test] + fn verify_data_contract_create_garbage_proof_returns_error() { + let platform_version = PlatformVersion::latest(); + let created_contract = + get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version); + let contract = created_contract.data_contract(); + let data_contract_serialized: DataContractInSerializationFormat = contract + .clone() + .try_into_platform_versioned(platform_version) + .expect("expected to serialize contract"); + + use dpp::state_transition::data_contract_create_transition::DataContractCreateTransition; + use dpp::state_transition::data_contract_create_transition::DataContractCreateTransitionV0; + let st = StateTransition::DataContractCreate(DataContractCreateTransition::V0( + DataContractCreateTransitionV0 { + data_contract: data_contract_serialized, + identity_nonce: 0, + user_fee_increase: 0, + signature_public_key_id: 0, + signature: Default::default(), + }, + )); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + let garbage_proof: [u8; 32] = [0x5A; 32]; + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &garbage_proof, + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected error for data contract create with garbage proof, got: {:?}", + result + ); + } + + // --- IdentityCreditTransfer: garbage proof returns error. + #[test] + fn verify_identity_credit_transfer_garbage_proof_returns_error() { + let platform_version = PlatformVersion::latest(); + + use dpp::state_transition::identity_credit_transfer_transition::IdentityCreditTransferTransition; + use dpp::state_transition::state_transitions::identity::identity_credit_transfer_transition::v0::IdentityCreditTransferTransitionV0; + let st = StateTransition::IdentityCreditTransfer(IdentityCreditTransferTransition::V0( + IdentityCreditTransferTransitionV0 { + identity_id: dpp::prelude::Identifier::random(), + recipient_id: dpp::prelude::Identifier::random(), + amount: 50, + nonce: 1, + ..Default::default() + }, + )); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + let garbage_proof = vec![0xFFu8; 20]; + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &garbage_proof, + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected error for identity credit transfer with garbage proof, got: {:?}", + result + ); + } + + // --- IdentityCreate: garbage proof with a contract provider that errors + // internally does not affect the Identity path (provider is not queried). + // This sanity-checks that an IdentityCreate with an erroring provider + // still fails on the proof (not on the provider call). + #[test] + fn verify_identity_create_erroring_provider_ignored_for_identity_flow() { + let platform_version = PlatformVersion::latest(); + use dpp::state_transition::identity_create_transition::IdentityCreateTransition; + use dpp::state_transition::state_transitions::identity::identity_create_transition::v0::IdentityCreateTransitionV0; + let st = StateTransition::IdentityCreate(IdentityCreateTransition::V0( + IdentityCreateTransitionV0 { + identity_id: dpp::prelude::Identifier::random(), + ..Default::default() + }, + )); + + // Provider returns an error, but the IdentityCreate branch never + // consults the provider. The error surface is the empty proof. + let known_contracts_provider_fn: &ContractLookupFn = &|_id| { + Err(Error::Drive( + crate::error::drive::DriveError::CorruptedDriveState( + "unused provider error".to_string(), + ), + )) + }; + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected proof/grovedb error (not provider error), got: {:?}", + result + ); + } + + // --- MasternodeVote: provider callback errors → error is propagated. + #[test] + fn verify_masternode_vote_provider_errors_is_propagated() { + let platform_version = PlatformVersion::latest(); + use dpp::state_transition::masternode_vote_transition::v0::MasternodeVoteTransitionV0; + use dpp::state_transition::masternode_vote_transition::MasternodeVoteTransition; + use dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; + use dpp::voting::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePoll; + use dpp::voting::vote_polls::VotePoll; + use dpp::voting::votes::resource_vote::v0::ResourceVoteV0; + use dpp::voting::votes::resource_vote::ResourceVote; + use dpp::voting::votes::Vote; + + let st = StateTransition::MasternodeVote(MasternodeVoteTransition::V0( + MasternodeVoteTransitionV0 { + pro_tx_hash: dpp::prelude::Identifier::from([1u8; 32]), + voter_identity_id: dpp::prelude::Identifier::from([2u8; 32]), + vote: Vote::ResourceVote(ResourceVote::V0(ResourceVoteV0 { + vote_poll: VotePoll::ContestedDocumentResourceVotePoll( + ContestedDocumentResourceVotePoll { + contract_id: dpp::prelude::Identifier::from([7u8; 32]), + document_type_name: "some_type".to_string(), + index_name: "idx".to_string(), + index_values: vec![], + }, + ), + resource_vote_choice: ResourceVoteChoice::Abstain, + })), + nonce: 1, + signature_public_key_id: 0, + signature: Default::default(), + }, + )); + + // Provider returns Err — the error should be propagated as-is. + let known_contracts_provider_fn: &ContractLookupFn = &|_id| { + Err(Error::Drive( + crate::error::drive::DriveError::CorruptedDriveState( + "synthetic provider failure".to_string(), + ), + )) + }; + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + let err = result.expect_err("expected provider error, got Ok"); + match err { + Error::Drive(crate::error::drive::DriveError::CorruptedDriveState(msg)) => { + assert_eq!(msg, "synthetic provider failure"); + } + other => panic!("expected CorruptedDriveState, got: {:?}", other), + } + } + + // --- Batch with document transition: provider error is propagated. + #[test] + fn verify_batch_document_provider_error_is_propagated() { + let platform_version = PlatformVersion::latest(); + + use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; + use dpp::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; + use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; + use dpp::state_transition::batch_transition::document_delete_transition::DocumentDeleteTransition; + use dpp::state_transition::batch_transition::document_delete_transition::DocumentDeleteTransitionV0; + use dpp::state_transition::batch_transition::BatchTransition; + use dpp::state_transition::batch_transition::BatchTransitionV0; + + let base = DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: Default::default(), + identity_contract_nonce: 1, + document_type_name: "x".to_string(), + data_contract_id: Default::default(), + }); + + let st = StateTransition::Batch(BatchTransition::V0(BatchTransitionV0 { + owner_id: Default::default(), + transitions: vec![DocumentTransition::Delete(DocumentDeleteTransition::V0( + DocumentDeleteTransitionV0 { base }, + ))], + ..Default::default() + })); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| { + Err(Error::Drive( + crate::error::drive::DriveError::CorruptedDriveState( + "synthetic batch provider failure".to_string(), + ), + )) + }; + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + let err = result.expect_err("expected provider error, got Ok"); + match err { + Error::Drive(crate::error::drive::DriveError::CorruptedDriveState(msg)) => { + assert_eq!(msg, "synthetic batch provider failure"); + } + other => panic!("expected CorruptedDriveState, got: {:?}", other), + } + } + + // --- DataContractCreate empty proof + keeps_history=true: exercises the + // happy-path arm up to proof verification but with an empty proof so it + // errors out. + #[test] + fn verify_data_contract_create_keeps_history_empty_proof_returns_error() { + let platform_version = PlatformVersion::latest(); + // Build a contract with keeps_history=true + use dpp::data_contract::config::v0::DataContractConfigV0; + use dpp::data_contract::config::DataContractConfig; + use dpp::data_contract::v1::DataContractV1; + use dpp::prelude::DataContract; + let contract = DataContract::V1(DataContractV1 { + id: dpp::prelude::Identifier::from([99u8; 32]), + version: 1, + owner_id: Default::default(), + document_types: Default::default(), + config: DataContractConfig::V0(DataContractConfigV0 { + can_be_deleted: false, + readonly: false, + keeps_history: true, + documents_keep_history_contract_default: false, + documents_mutable_contract_default: false, + documents_can_be_deleted_contract_default: false, + requires_identity_encryption_bounded_key: None, + requires_identity_decryption_bounded_key: None, + }), + schema_defs: None, + created_at: None, + updated_at: None, + created_at_block_height: None, + updated_at_block_height: None, + created_at_epoch: None, + updated_at_epoch: None, + groups: Default::default(), + tokens: Default::default(), + keywords: Vec::new(), + description: None, + }); + let data_contract_serialized: DataContractInSerializationFormat = contract + .clone() + .try_into_platform_versioned(platform_version) + .expect("expected to serialize contract"); + use dpp::state_transition::data_contract_create_transition::DataContractCreateTransition; + use dpp::state_transition::data_contract_create_transition::DataContractCreateTransitionV0; + let st = StateTransition::DataContractCreate(DataContractCreateTransition::V0( + DataContractCreateTransitionV0 { + data_contract: data_contract_serialized, + identity_nonce: 0, + user_fee_increase: 0, + signature_public_key_id: 0, + signature: Default::default(), + }, + )); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected error for keeps_history contract create with empty proof, got: {:?}", + result + ); + } + + // --- DataContractUpdate keeps_history=true + empty proof: errors out. + #[test] + fn verify_data_contract_update_keeps_history_empty_proof_returns_error() { + let platform_version = PlatformVersion::latest(); + use dpp::data_contract::config::v0::DataContractConfigV0; + use dpp::data_contract::config::DataContractConfig; + use dpp::data_contract::v1::DataContractV1; + use dpp::prelude::DataContract; + let contract = DataContract::V1(DataContractV1 { + id: dpp::prelude::Identifier::from([98u8; 32]), + version: 2, + owner_id: Default::default(), + document_types: Default::default(), + config: DataContractConfig::V0(DataContractConfigV0 { + can_be_deleted: false, + readonly: false, + keeps_history: true, + documents_keep_history_contract_default: false, + documents_mutable_contract_default: false, + documents_can_be_deleted_contract_default: false, + requires_identity_encryption_bounded_key: None, + requires_identity_decryption_bounded_key: None, + }), + schema_defs: None, + created_at: None, + updated_at: None, + created_at_block_height: None, + updated_at_block_height: None, + created_at_epoch: None, + updated_at_epoch: None, + groups: Default::default(), + tokens: Default::default(), + keywords: Vec::new(), + description: None, + }); + let data_contract_serialized: DataContractInSerializationFormat = contract + .clone() + .try_into_platform_versioned(platform_version) + .expect("expected to serialize contract"); + use dpp::state_transition::data_contract_update_transition::DataContractUpdateTransition; + use dpp::state_transition::data_contract_update_transition::DataContractUpdateTransitionV0; + let st = StateTransition::DataContractUpdate(DataContractUpdateTransition::V0( + DataContractUpdateTransitionV0 { + identity_contract_nonce: 1, + data_contract: data_contract_serialized, + user_fee_increase: 0, + signature_public_key_id: 0, + signature: Default::default(), + }, + )); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected error for keeps_history contract update with empty proof, got: {:?}", + result + ); + } + + // --- Batch V0 with a Document Create transition in contested status + // + unknown contract returns UnknownContract. The contested_status arm + // is a distinct branch that other tests don't cover. + #[test] + fn verify_batch_document_contested_create_unknown_contract_returns_error() { + let platform_version = PlatformVersion::latest(); + + use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; + use dpp::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; + use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; + use dpp::state_transition::batch_transition::document_create_transition::DocumentCreateTransition; + use dpp::state_transition::batch_transition::document_create_transition::DocumentCreateTransitionV0; + use dpp::state_transition::batch_transition::BatchTransition; + use dpp::state_transition::batch_transition::BatchTransitionV0; + + let base = DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: Default::default(), + identity_contract_nonce: 1, + document_type_name: "t".to_string(), + data_contract_id: Default::default(), + }); + + let create_transition = DocumentCreateTransition::V0(DocumentCreateTransitionV0 { + base, + entropy: [0u8; 32], + data: Default::default(), + // Contested (prefunded) create → separate branch in verify() + prefunded_voting_balance: Some(("dash".to_string(), 1000)), + }); + + let st = StateTransition::Batch(BatchTransition::V0(BatchTransitionV0 { + owner_id: Default::default(), + transitions: vec![DocumentTransition::Create(create_transition)], + ..Default::default() + })); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + let err = result.expect_err("expected UnknownContract, got Ok"); + assert!( + matches!( + err, + crate::error::Error::Proof(ProofError::UnknownContract(_)) + ), + "expected Error::Proof(UnknownContract), got: {:?}", + err + ); + } + // --- Batch with a single Token transition + unknown contract returns // UnknownContract error (covers the token transition branch). #[test] From a6b940c282b827de9c1d3978770c5da286d24895 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 23 Apr 2026 20:32:34 +0800 Subject: [PATCH 2/5] test(dpp): cover document v0 edge cases Adds 56 tests targeting uncovered paths in rs-dpp/src/document/: - platform_value_conversion.rs: round-trip tests for to_map_value, into_map_value, to_object, into_value, and from_platform_value (including non-map error path). Previously had zero tests. - serialize.rs: missing-required-field errors for serialize_v0/v1/v2 using the withdrawal contract (requires $createdAt/$updatedAt) and family/person contract (required user properties). Direct from_bytes_v0/v1/v2 too-small-buffer errors, v1/v2 truncated-post-id errors, V0-then-V1 fallback path, V0-contract feature_version gating. - cbor_conversion.rs: from_map missing $id/$ownerId, creator_id parsing, empty/truncated CBOR, user-property preservation. - json_conversion.rs: minimal document emits only id/owner keys, null creator/id handling, empty-object default-document, base58 identifier emission. - is_equal_ignoring_timestamps: nonexistent-field ignore list, empty-vec ignore list, None/Some revision comparison, self-equality. - get_raw_for_document_type: None-valued system fields return None, owner_id override only affects $ownerId path, unknown document_type_name surfaces DocumentTypeNotFound. - accessors/mod.rs: Document enum getter/setter delegation, bump_revision saturation, get() by path. Previously had zero tests. - document/mod.rs: increment_revision overflow, From, Display version prefix, get_raw_for_document_type dispatch. Documents a V0 consensus-frozen quirk: serialize_v2 / from_bytes_v2 intentionally skip the creator_id byte for non-transferable / TradeMode::None document types (contactRequest in dashpay), so a creator_id set on such a document is lost on round-trip. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/rs-dpp/src/document/accessors/mod.rs | 150 ++++++ .../get_raw_for_document_type/v0/mod.rs | 133 ++++++ .../is_equal_ignoring_timestamps/v0/mod.rs | 52 +++ packages/rs-dpp/src/document/mod.rs | 117 +++++ .../rs-dpp/src/document/v0/cbor_conversion.rs | 124 +++++ .../rs-dpp/src/document/v0/json_conversion.rs | 136 ++++++ .../document/v0/platform_value_conversion.rs | 151 +++++++ packages/rs-dpp/src/document/v0/serialize.rs | 427 ++++++++++++++++++ 8 files changed, 1290 insertions(+) diff --git a/packages/rs-dpp/src/document/accessors/mod.rs b/packages/rs-dpp/src/document/accessors/mod.rs index b27f5800dd4..ecae8be34d1 100644 --- a/packages/rs-dpp/src/document/accessors/mod.rs +++ b/packages/rs-dpp/src/document/accessors/mod.rs @@ -214,3 +214,153 @@ impl DocumentV0Setters for Document { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::document::v0::DocumentV0; + use crate::prelude::Revision; + + fn make_doc() -> Document { + Document::V0(DocumentV0 { + id: Identifier::new([1u8; 32]), + owner_id: Identifier::new([2u8; 32]), + properties: BTreeMap::new(), + revision: Some(1), + created_at: Some(10), + updated_at: Some(20), + transferred_at: Some(30), + created_at_block_height: Some(100), + updated_at_block_height: Some(200), + transferred_at_block_height: Some(300), + created_at_core_block_height: Some(1), + updated_at_core_block_height: Some(2), + transferred_at_core_block_height: Some(3), + creator_id: Some(Identifier::new([9u8; 32])), + }) + } + + // ================================================================ + // Document-enum getters forward to the V0 variant. + // ================================================================ + + #[test] + fn getters_forward_all_fields_to_v0() { + let doc = make_doc(); + assert_eq!(doc.id(), Identifier::new([1u8; 32])); + assert_eq!(doc.owner_id(), Identifier::new([2u8; 32])); + assert_eq!(doc.id_ref(), &Identifier::new([1u8; 32])); + assert_eq!(doc.owner_id_ref(), &Identifier::new([2u8; 32])); + assert_eq!(doc.revision(), Some(1)); + assert_eq!(doc.created_at(), Some(10)); + assert_eq!(doc.updated_at(), Some(20)); + assert_eq!(doc.transferred_at(), Some(30)); + assert_eq!(doc.created_at_block_height(), Some(100)); + assert_eq!(doc.updated_at_block_height(), Some(200)); + assert_eq!(doc.transferred_at_block_height(), Some(300)); + assert_eq!(doc.created_at_core_block_height(), Some(1)); + assert_eq!(doc.updated_at_core_block_height(), Some(2)); + assert_eq!(doc.transferred_at_core_block_height(), Some(3)); + assert_eq!(doc.creator_id(), Some(Identifier::new([9u8; 32]))); + assert!(doc.properties().is_empty()); + } + + // ================================================================ + // Document-enum properties_consumed returns the inner BTreeMap. + // ================================================================ + + #[test] + fn properties_consumed_forwards_to_v0() { + let Document::V0(mut v0) = make_doc(); + v0.properties.insert("k".into(), Value::U64(77)); + let doc = Document::V0(v0); + let map = doc.properties_consumed(); + assert_eq!(map.get("k"), Some(&Value::U64(77))); + } + + // ================================================================ + // Document-enum properties_mut allows mutation. + // ================================================================ + + #[test] + fn properties_mut_allows_mutation_via_enum() { + let mut doc = make_doc(); + doc.properties_mut().insert("x".into(), Value::U64(5)); + assert_eq!(doc.properties().get("x"), Some(&Value::U64(5))); + } + + // ================================================================ + // Document-enum setters forward to the V0 variant. + // ================================================================ + + #[test] + fn setters_forward_all_fields_to_v0() { + let mut doc = make_doc(); + doc.set_id(Identifier::new([42u8; 32])); + doc.set_owner_id(Identifier::new([43u8; 32])); + let mut p = BTreeMap::new(); + p.insert("z".into(), Value::Bool(true)); + doc.set_properties(p.clone()); + doc.set_revision(Some(99)); + doc.set_created_at(Some(1_000_000)); + doc.set_updated_at(Some(2_000_000)); + doc.set_transferred_at(Some(3_000_000)); + doc.set_created_at_block_height(Some(111)); + doc.set_updated_at_block_height(Some(222)); + doc.set_transferred_at_block_height(Some(333)); + doc.set_created_at_core_block_height(Some(4)); + doc.set_updated_at_core_block_height(Some(5)); + doc.set_transferred_at_core_block_height(Some(6)); + doc.set_creator_id(Some(Identifier::new([7u8; 32]))); + + assert_eq!(doc.id(), Identifier::new([42u8; 32])); + assert_eq!(doc.owner_id(), Identifier::new([43u8; 32])); + assert_eq!(doc.properties(), &p); + assert_eq!(doc.revision(), Some(99)); + assert_eq!(doc.created_at(), Some(1_000_000)); + assert_eq!(doc.updated_at(), Some(2_000_000)); + assert_eq!(doc.transferred_at(), Some(3_000_000)); + assert_eq!(doc.created_at_block_height(), Some(111)); + assert_eq!(doc.updated_at_block_height(), Some(222)); + assert_eq!(doc.transferred_at_block_height(), Some(333)); + assert_eq!(doc.created_at_core_block_height(), Some(4)); + assert_eq!(doc.updated_at_core_block_height(), Some(5)); + assert_eq!(doc.transferred_at_core_block_height(), Some(6)); + assert_eq!(doc.creator_id(), Some(Identifier::new([7u8; 32]))); + } + + // ================================================================ + // Document-enum bump_revision forwards to the V0 saturating_add. + // Unlike increment_revision, bump_revision never errors and + // saturates at Revision::MAX. + // ================================================================ + + #[test] + fn bump_revision_via_enum_saturates_at_max() { + let mut doc = make_doc(); + doc.set_revision(Some(Revision::MAX)); + doc.bump_revision(); + assert_eq!(doc.revision(), Some(Revision::MAX)); + } + + #[test] + fn bump_revision_via_enum_is_noop_for_none() { + let mut doc = make_doc(); + doc.set_revision(None); + doc.bump_revision(); + assert_eq!(doc.revision(), None); + } + + // ================================================================ + // Document::get (default impl on the trait) returns a property + // at the given path or None. + // ================================================================ + + #[test] + fn get_returns_property_by_path() { + let mut doc = make_doc(); + doc.properties_mut().insert("a".into(), Value::U64(1)); + assert_eq!(doc.get("a"), Some(&Value::U64(1))); + assert_eq!(doc.get("missing"), None); + } +} diff --git a/packages/rs-dpp/src/document/document_methods/get_raw_for_document_type/v0/mod.rs b/packages/rs-dpp/src/document/document_methods/get_raw_for_document_type/v0/mod.rs index f09744a2af6..2494a3d0dd4 100644 --- a/packages/rs-dpp/src/document/document_methods/get_raw_for_document_type/v0/mod.rs +++ b/packages/rs-dpp/src/document/document_methods/get_raw_for_document_type/v0/mod.rs @@ -393,6 +393,139 @@ mod tests { // User-defined property serialization // ================================================================ + // ================================================================ + // None-valued system fields should return None (not panic). + // ================================================================ + + fn minimal_doc() -> DocumentV0 { + DocumentV0 { + id: Identifier::new([1u8; 32]), + owner_id: Identifier::new([2u8; 32]), + properties: BTreeMap::new(), + revision: None, + created_at: None, + updated_at: None, + transferred_at: None, + created_at_block_height: None, + updated_at_block_height: None, + transferred_at_block_height: None, + created_at_core_block_height: None, + updated_at_core_block_height: None, + transferred_at_core_block_height: None, + creator_id: None, + } + } + + #[test] + fn get_raw_returns_none_for_unset_optional_system_fields() { + let platform_version = PlatformVersion::latest(); + let contract = json_document_to_contract( + "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json", + false, + platform_version, + ) + .expect("expected contract"); + let document_type = contract + .document_type_for_name("profile") + .expect("expected document type"); + + let doc = minimal_doc(); + let keys = [ + "$createdAt", + "$updatedAt", + "$transferredAt", + "$createdAtBlockHeight", + "$updatedAtBlockHeight", + "$transferredAtBlockHeight", + "$createdAtCoreBlockHeight", + "$updatedAtCoreBlockHeight", + "$transferredAtCoreBlockHeight", + "$creatorId", + ]; + for k in keys { + let raw = doc + .get_raw_for_document_type_v0(k, document_type, None, platform_version) + .expect("should succeed"); + assert_eq!(raw, None, "{k} should yield None when unset"); + } + } + + // ================================================================ + // get_raw_for_document_type_v0: $ownerId with owner_id override + // takes precedence even if the document's owner is different, but + // for $id the override is NOT applied (only $ownerId is gated). + // ================================================================ + + #[test] + fn get_raw_owner_id_override_only_affects_dollar_owner_id_path() { + let platform_version = PlatformVersion::latest(); + let contract = json_document_to_contract( + "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json", + false, + platform_version, + ) + .expect("expected contract"); + let document_type = contract + .document_type_for_name("profile") + .expect("expected document type"); + + let doc = make_document_with_known_ids(); + let override_owner = [0xFF; 32]; + + // $ownerId path sees the override + let raw_owner = doc + .get_raw_for_document_type_v0( + "$ownerId", + document_type, + Some(override_owner), + platform_version, + ) + .expect("owner should succeed"); + assert_eq!(raw_owner, Some(Vec::from(override_owner))); + + // $id path ignores the owner override + let raw_id = doc + .get_raw_for_document_type_v0( + "$id", + document_type, + Some(override_owner), + platform_version, + ) + .expect("id should succeed"); + assert_eq!( + raw_id, + Some(doc.id.to_vec()), + "$id path should not be affected by owner_id override" + ); + } + + // ================================================================ + // get_raw_for_contract with unknown document_type_name errors. + // ================================================================ + + #[test] + fn get_raw_for_contract_with_unknown_document_type_errors() { + use crate::document::document_methods::DocumentGetRawForContractV0; + let platform_version = PlatformVersion::latest(); + let contract = json_document_to_contract( + "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json", + false, + platform_version, + ) + .expect("expected contract"); + + let doc = make_document_with_known_ids(); + let err = doc + .get_raw_for_contract_v0("$id", "nonExistentType", &contract, None, platform_version) + .expect_err("unknown document type should fail"); + match err { + crate::ProtocolError::DataContractError( + crate::data_contract::errors::DataContractError::DocumentTypeNotFound(_), + ) => {} + other => panic!("expected DocumentTypeNotFound, got {:?}", other), + } + } + #[test] fn get_raw_serializes_user_defined_property() { let platform_version = PlatformVersion::latest(); diff --git a/packages/rs-dpp/src/document/document_methods/is_equal_ignoring_timestamps/v0/mod.rs b/packages/rs-dpp/src/document/document_methods/is_equal_ignoring_timestamps/v0/mod.rs index 14ea66b1ee3..a27dcb20c27 100644 --- a/packages/rs-dpp/src/document/document_methods/is_equal_ignoring_timestamps/v0/mod.rs +++ b/packages/rs-dpp/src/document/document_methods/is_equal_ignoring_timestamps/v0/mod.rs @@ -220,4 +220,56 @@ mod tests { "empty-property documents with same ids should be equal ignoring timestamps" ); } + + // ================================================================ + // also_ignore_fields with a field that doesn't exist in either + // document: behaves the same as no ignore list. + // ================================================================ + + #[test] + fn also_ignore_fields_with_nonexistent_field_has_no_effect() { + let doc1 = make_base_document(); + let doc2 = make_base_document(); + assert!(doc1.is_equal_ignoring_time_based_fields_v0( + &doc2, + Some(vec!["this_field_does_not_exist"]) + )); + } + + // ================================================================ + // Empty also_ignore_fields vec: equivalent to None. + // ================================================================ + + #[test] + fn also_ignore_fields_empty_vec_is_equivalent_to_none() { + let doc1 = make_base_document(); + let mut doc2 = make_base_document(); + doc2.properties + .insert("name".to_string(), Value::Text("Bob".to_string())); + // Empty ignore list means all properties are compared. + assert!(!doc1.is_equal_ignoring_time_based_fields_v0(&doc2, Some(vec![]))); + } + + // ================================================================ + // Revision None vs Some is not equal + // ================================================================ + + #[test] + fn documents_with_none_and_some_revision_are_not_equal() { + let mut doc1 = make_base_document(); + let doc2 = make_base_document(); + doc1.revision = None; + assert!(!doc1.is_equal_ignoring_time_based_fields_v0(&doc2, None)); + } + + // ================================================================ + // Self-equal: same document equals itself (time-based differences + // shouldn't matter when both sides are identical). + // ================================================================ + + #[test] + fn a_document_equals_itself_ignoring_time_based_fields() { + let doc = make_base_document(); + assert!(doc.is_equal_ignoring_time_based_fields_v0(&doc, None)); + } } diff --git a/packages/rs-dpp/src/document/mod.rs b/packages/rs-dpp/src/document/mod.rs index 4a1309555c2..9076383b6b3 100644 --- a/packages/rs-dpp/src/document/mod.rs +++ b/packages/rs-dpp/src/document/mod.rs @@ -604,4 +604,121 @@ mod tests { assert_eq!(hash1, hash2, "hash should be deterministic"); assert!(!hash1.is_empty(), "hash should not be empty"); } + + // ================================================================ + // increment_revision: overflow from Revision::MAX surfaces an + // Overflow ProtocolError (not a silent saturate — this is the + // Document-enum path which uses checked_add). + // ================================================================ + + #[test] + fn increment_revision_errors_on_overflow() { + let mut doc = Document::V0(DocumentV0 { + id: platform_value::Identifier::new([1u8; 32]), + owner_id: platform_value::Identifier::new([2u8; 32]), + properties: Default::default(), + revision: Some(crate::prelude::Revision::MAX), + created_at: None, + updated_at: None, + transferred_at: None, + created_at_block_height: None, + updated_at_block_height: None, + transferred_at_block_height: None, + created_at_core_block_height: None, + updated_at_core_block_height: None, + transferred_at_core_block_height: None, + creator_id: None, + }); + let err = doc.increment_revision().expect_err("MAX + 1 must overflow"); + match err { + ProtocolError::Overflow(_) => {} + other => panic!("expected ProtocolError::Overflow, got {:?}", other), + } + } + + // ================================================================ + // From for Document produces a V0 variant. + // ================================================================ + + #[test] + fn from_document_v0_produces_v0_variant() { + let v0 = DocumentV0 { + id: platform_value::Identifier::new([1u8; 32]), + owner_id: platform_value::Identifier::new([2u8; 32]), + properties: Default::default(), + revision: Some(7), + created_at: None, + updated_at: None, + transferred_at: None, + created_at_block_height: None, + updated_at_block_height: None, + transferred_at_block_height: None, + created_at_core_block_height: None, + updated_at_core_block_height: None, + transferred_at_core_block_height: None, + creator_id: None, + }; + let document: Document = v0.clone().into(); + match document { + Document::V0(inner) => assert_eq!(inner, v0), + } + } + + // ================================================================ + // Document Display forwards to DocumentV0 Display with a version + // prefix. + // ================================================================ + + #[test] + fn document_display_has_version_prefix() { + let doc = Document::V0(DocumentV0 { + id: platform_value::Identifier::new([1u8; 32]), + owner_id: platform_value::Identifier::new([2u8; 32]), + properties: Default::default(), + revision: None, + created_at: None, + updated_at: None, + transferred_at: None, + created_at_block_height: None, + updated_at_block_height: None, + transferred_at_block_height: None, + created_at_core_block_height: None, + updated_at_core_block_height: None, + transferred_at_core_block_height: None, + creator_id: None, + }); + let s = format!("{}", doc); + assert!( + s.starts_with("v0 : "), + "Display should prefix with version, got: {s}" + ); + } + + // ================================================================ + // get_raw_for_document_type dispatches via platform version 0 + // to the V0 implementation. + // ================================================================ + + #[test] + fn get_raw_for_document_type_dispatch_path_returns_id() { + let platform_version = PlatformVersion::latest(); + let contract = json_document_to_contract( + "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json", + false, + platform_version, + ) + .expect("expected contract"); + let document_type = contract + .document_type_for_name("profile") + .expect("expected document type"); + + let document = document_type + .random_document(Some(11), platform_version) + .expect("expected random document"); + + let raw = document + .get_raw_for_document_type("$id", document_type, None, platform_version) + .expect("should succeed"); + assert_eq!(raw, Some(document.id().to_vec())); + } } diff --git a/packages/rs-dpp/src/document/v0/cbor_conversion.rs b/packages/rs-dpp/src/document/v0/cbor_conversion.rs index 957cd0ec101..626ad8d0764 100644 --- a/packages/rs-dpp/src/document/v0/cbor_conversion.rs +++ b/packages/rs-dpp/src/document/v0/cbor_conversion.rs @@ -498,6 +498,130 @@ mod tests { // Round-trip via from_map: construct map, parse, verify // ================================================================ + // ================================================================ + // from_map missing $id / $ownerId errors + // ================================================================ + + #[test] + fn from_map_missing_id_fails_when_not_provided() { + // Only owner_id in the map, no explicit document_id — from_map should + // error on the ID extraction step. + let mut map = BTreeMap::new(); + map.insert( + property_names::OWNER_ID.to_string(), + Value::Bytes32([4u8; 32]), + ); + + let result = DocumentV0::from_map(map, None, None); + assert!( + result.is_err(), + "from_map without $id or explicit document_id should fail" + ); + } + + #[test] + fn from_map_missing_owner_id_fails_when_not_provided() { + let mut map = BTreeMap::new(); + map.insert(property_names::ID.to_string(), Value::Bytes32([3u8; 32])); + + let result = DocumentV0::from_map(map, None, None); + assert!( + result.is_err(), + "from_map without $ownerId or explicit owner_id should fail" + ); + } + + // ================================================================ + // from_map: creator id parsing + // ================================================================ + + #[test] + fn from_map_extracts_creator_id_when_present_as_identifier() { + let creator = Identifier::new([0xCD; 32]); + let mut map = BTreeMap::new(); + map.insert(property_names::ID.to_string(), Value::Bytes32([1u8; 32])); + map.insert( + property_names::OWNER_ID.to_string(), + Value::Bytes32([2u8; 32]), + ); + map.insert( + property_names::CREATOR_ID.to_string(), + Value::Identifier(creator.to_buffer()), + ); + + let doc = DocumentV0::from_map(map, None, None).expect("from_map should succeed"); + assert_eq!(doc.creator_id, Some(creator)); + } + + #[test] + fn from_map_creator_id_missing_stays_none() { + let mut map = BTreeMap::new(); + map.insert(property_names::ID.to_string(), Value::Bytes32([1u8; 32])); + map.insert( + property_names::OWNER_ID.to_string(), + Value::Bytes32([2u8; 32]), + ); + + let doc = DocumentV0::from_map(map, None, None).expect("from_map should succeed"); + assert_eq!(doc.creator_id, None); + } + + // ================================================================ + // from_cbor: bytes starting with truncated ciborium data fail + // ================================================================ + + #[test] + fn from_cbor_rejects_empty_buffer() { + let platform_version = PlatformVersion::latest(); + let result = DocumentV0::from_cbor(&[], None, None, platform_version); + assert!( + result.is_err(), + "from_cbor should fail on an empty input buffer" + ); + } + + #[test] + fn from_cbor_rejects_truncated_map_bytes() { + // A map header byte that claims to be a map but has no content. + let platform_version = PlatformVersion::latest(); + let result = DocumentV0::from_cbor(&[0xA1], None, None, platform_version); + assert!( + result.is_err(), + "from_cbor should fail on a truncated map prefix" + ); + } + + // ================================================================ + // to_cbor round-trip preserves owner_id/creator_id etc + // ================================================================ + + #[test] + fn cbor_round_trip_via_to_cbor_and_from_cbor_preserves_fields() { + let platform_version = PlatformVersion::latest(); + let doc = make_document_v0_with_timestamps(); + + let bytes = doc.to_cbor().expect("to_cbor succeeds"); + let recovered = crate::document::Document::from_cbor(&bytes, None, None, platform_version) + .expect("from_cbor succeeds"); + assert_eq!(doc.id, recovered.id()); + assert_eq!(doc.owner_id, recovered.owner_id()); + assert_eq!(doc.revision, recovered.revision()); + } + + // ================================================================ + // DocumentForCbor: TryFrom returns a CBOR-ready structure whose + // `properties` map preserves user-defined keys. + // ================================================================ + + #[test] + fn document_for_cbor_preserves_user_properties() { + let doc = make_document_v0_with_timestamps(); + let cbor = DocumentForCbor::try_from(doc.clone()).expect("try_from succeeds"); + // "name" and "age" are part of the test fixture + assert!(cbor.properties.contains_key("name")); + assert!(cbor.properties.contains_key("age")); + } + #[test] fn from_map_with_all_timestamp_variants() { let mut map = BTreeMap::new(); diff --git a/packages/rs-dpp/src/document/v0/json_conversion.rs b/packages/rs-dpp/src/document/v0/json_conversion.rs index 692a5e59310..57b891d438d 100644 --- a/packages/rs-dpp/src/document/v0/json_conversion.rs +++ b/packages/rs-dpp/src/document/v0/json_conversion.rs @@ -487,6 +487,142 @@ mod tests { assert_eq!(doc.creator_id, None); } + // ================================================================ + // to_json_with_identifiers_using_bytes: minimal document has only + // $id and $ownerId keys (no optional fields rendered). + // ================================================================ + + #[test] + fn to_json_with_identifiers_using_bytes_minimal_document_has_only_id_and_owner() { + let platform_version = PlatformVersion::latest(); + let doc = make_minimal_document_v0(); + let json = doc + .to_json_with_identifiers_using_bytes(platform_version) + .expect("to_json_with_identifiers_using_bytes should succeed"); + let obj = json.as_object().expect("object"); + assert!(obj.contains_key(property_names::ID)); + assert!(obj.contains_key(property_names::OWNER_ID)); + // None-valued optional fields are NOT emitted by this serializer. + assert!(!obj.contains_key(property_names::CREATED_AT)); + assert!(!obj.contains_key(property_names::UPDATED_AT)); + assert!(!obj.contains_key(property_names::TRANSFERRED_AT)); + assert!(!obj.contains_key(property_names::CREATED_AT_BLOCK_HEIGHT)); + assert!(!obj.contains_key(property_names::UPDATED_AT_BLOCK_HEIGHT)); + assert!(!obj.contains_key(property_names::TRANSFERRED_AT_BLOCK_HEIGHT)); + assert!(!obj.contains_key(property_names::CREATED_AT_CORE_BLOCK_HEIGHT)); + assert!(!obj.contains_key(property_names::UPDATED_AT_CORE_BLOCK_HEIGHT)); + assert!(!obj.contains_key(property_names::TRANSFERRED_AT_CORE_BLOCK_HEIGHT)); + assert!(!obj.contains_key(property_names::CREATOR_ID)); + assert!(!obj.contains_key(property_names::REVISION)); + } + + // ================================================================ + // to_json_with_identifiers_using_bytes: id/owner emitted as + // base58 strings (via serde_json derive on Identifier). + // ================================================================ + + #[test] + fn to_json_with_identifiers_using_bytes_emits_base58_identifiers() { + let platform_version = PlatformVersion::latest(); + let doc = make_minimal_document_v0(); + let json = doc + .to_json_with_identifiers_using_bytes(platform_version) + .expect("should succeed"); + let obj = json.as_object().expect("object"); + // $id and $ownerId are serialized as base58 strings by Identifier's + // Serialize impl, which is what the underlying json! macro uses. + let id_val = obj.get(property_names::ID).expect("id present"); + assert!(id_val.is_string(), "expected base58 string for $id"); + let owner_val = obj.get(property_names::OWNER_ID).expect("owner present"); + assert!(owner_val.is_string(), "expected base58 string for $ownerId"); + } + + // ================================================================ + // from_json_value handles null creator_id by leaving it None. + // ================================================================ + + #[test] + fn from_json_value_with_null_creator_id_stays_none() { + let platform_version = PlatformVersion::latest(); + let json_val = json!({ + "$id": bs58::encode([1u8; 32]).into_string(), + "$ownerId": bs58::encode([2u8; 32]).into_string(), + "$creatorId": JsonValue::Null, + }); + let doc = DocumentV0::from_json_value::(json_val, platform_version) + .expect("from_json_value should succeed with null creator_id"); + assert_eq!(doc.creator_id, None); + } + + // ================================================================ + // from_json_value handles null id/owner by leaving them defaulted. + // ================================================================ + + #[test] + fn from_json_value_with_null_id_leaves_default() { + let platform_version = PlatformVersion::latest(); + let json_val = json!({ + "$id": JsonValue::Null, + "$ownerId": bs58::encode([2u8; 32]).into_string(), + }); + let doc = DocumentV0::from_json_value::(json_val, platform_version) + .expect("from_json_value should succeed with null $id"); + // Default Identifier is all-zeros. + assert_eq!(doc.id, Identifier::new([0u8; 32])); + } + + // ================================================================ + // to_json_with_identifiers_using_bytes: multiple user-defined + // properties are all included. + // ================================================================ + + #[test] + fn to_json_with_identifiers_using_bytes_with_multiple_properties() { + let platform_version = PlatformVersion::latest(); + let mut props = BTreeMap::new(); + props.insert("a".to_string(), Value::U64(1)); + props.insert("b".to_string(), Value::Text("two".to_string())); + props.insert("c".to_string(), Value::Bool(true)); + let doc = DocumentV0 { + id: Identifier::new([1u8; 32]), + owner_id: Identifier::new([2u8; 32]), + properties: props, + revision: None, + created_at: None, + updated_at: None, + transferred_at: None, + created_at_block_height: None, + updated_at_block_height: None, + transferred_at_block_height: None, + created_at_core_block_height: None, + updated_at_core_block_height: None, + transferred_at_core_block_height: None, + creator_id: None, + }; + let json = doc + .to_json_with_identifiers_using_bytes(platform_version) + .expect("should succeed"); + let obj = json.as_object().expect("object"); + assert_eq!(obj.get("a").and_then(|v| v.as_u64()), Some(1)); + assert_eq!(obj.get("b").and_then(|v| v.as_str()), Some("two")); + assert_eq!(obj.get("c").and_then(|v| v.as_bool()), Some(true)); + } + + // ================================================================ + // from_json_value: an empty object produces a fully-defaulted doc + // ================================================================ + + #[test] + fn from_json_value_empty_object_returns_default_document() { + let platform_version = PlatformVersion::latest(); + let doc = DocumentV0::from_json_value::(json!({}), platform_version) + .expect("from_json_value should succeed with empty object"); + assert_eq!(doc.id, Identifier::new([0u8; 32])); + assert_eq!(doc.owner_id, Identifier::new([0u8; 32])); + assert_eq!(doc.revision, None); + assert!(doc.properties.is_empty()); + } + // ================================================================ // from_json_value with creator_id // ================================================================ diff --git a/packages/rs-dpp/src/document/v0/platform_value_conversion.rs b/packages/rs-dpp/src/document/v0/platform_value_conversion.rs index a3d42b775a7..f30a3bd91e1 100644 --- a/packages/rs-dpp/src/document/v0/platform_value_conversion.rs +++ b/packages/rs-dpp/src/document/v0/platform_value_conversion.rs @@ -29,3 +29,154 @@ impl DocumentPlatformValueMethodsV0<'_> for DocumentV0 { Ok(platform_value::from_value(document_value)?) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::document::property_names; + use platform_value::Identifier; + use platform_version::version::PlatformVersion; + + fn minimal_doc() -> DocumentV0 { + DocumentV0 { + id: Identifier::new([1u8; 32]), + owner_id: Identifier::new([2u8; 32]), + properties: BTreeMap::new(), + revision: None, + created_at: None, + updated_at: None, + transferred_at: None, + created_at_block_height: None, + updated_at_block_height: None, + transferred_at_block_height: None, + created_at_core_block_height: None, + updated_at_core_block_height: None, + transferred_at_core_block_height: None, + creator_id: None, + } + } + + fn full_doc() -> DocumentV0 { + let mut props = BTreeMap::new(); + props.insert("name".into(), Value::Text("Eve".into())); + props.insert("score".into(), Value::U64(42)); + DocumentV0 { + id: Identifier::new([7u8; 32]), + owner_id: Identifier::new([8u8; 32]), + properties: props, + revision: Some(3), + created_at: Some(1_700_000_000_000), + updated_at: Some(1_700_000_100_000), + transferred_at: Some(1_700_000_200_000), + created_at_block_height: Some(10), + updated_at_block_height: Some(20), + transferred_at_block_height: Some(30), + created_at_core_block_height: Some(1), + updated_at_core_block_height: Some(2), + transferred_at_core_block_height: Some(3), + creator_id: Some(Identifier::new([9u8; 32])), + } + } + + // ================================================================ + // to_map_value: produces a BTreeMap keyed by the + // documented serde field names (with the $-prefixed renames). + // ================================================================ + + #[test] + fn to_map_value_contains_id_and_owner_id_keys() { + let doc = minimal_doc(); + let map = doc.to_map_value().expect("to_map_value should succeed"); + assert!(map.contains_key(property_names::ID)); + assert!(map.contains_key(property_names::OWNER_ID)); + } + + #[test] + fn to_map_value_contains_all_set_optional_fields() { + let doc = full_doc(); + let map = doc.to_map_value().expect("to_map_value should succeed"); + assert!(map.contains_key(property_names::REVISION)); + assert!(map.contains_key(property_names::CREATED_AT)); + assert!(map.contains_key(property_names::UPDATED_AT)); + assert!(map.contains_key(property_names::TRANSFERRED_AT)); + assert!(map.contains_key(property_names::CREATED_AT_BLOCK_HEIGHT)); + assert!(map.contains_key(property_names::UPDATED_AT_BLOCK_HEIGHT)); + assert!(map.contains_key(property_names::TRANSFERRED_AT_BLOCK_HEIGHT)); + assert!(map.contains_key(property_names::CREATED_AT_CORE_BLOCK_HEIGHT)); + assert!(map.contains_key(property_names::UPDATED_AT_CORE_BLOCK_HEIGHT)); + assert!(map.contains_key(property_names::TRANSFERRED_AT_CORE_BLOCK_HEIGHT)); + assert!(map.contains_key(property_names::CREATOR_ID)); + // User-defined properties are flattened into the map + assert!(map.contains_key("name")); + assert!(map.contains_key("score")); + } + + // ================================================================ + // into_map_value: same shape as to_map_value, consumes self + // ================================================================ + + #[test] + fn into_map_value_consumes_and_returns_same_shape_as_to_map_value() { + let doc = full_doc(); + let from_ref = doc.to_map_value().expect("to_map_value"); + let from_owned = doc.into_map_value().expect("into_map_value"); + assert_eq!(from_ref, from_owned); + } + + // ================================================================ + // to_object / into_value: produce a Value::Map + // ================================================================ + + #[test] + fn to_object_returns_a_map_value() { + let doc = full_doc(); + let v = doc.to_object().expect("to_object"); + assert!(v.is_map(), "Expected a Value::Map, got {:?}", v); + } + + #[test] + fn into_value_consumes_and_returns_a_map_value() { + let doc = full_doc(); + let v = doc.into_value().expect("into_value"); + assert!(v.is_map(), "Expected a Value::Map, got {:?}", v); + } + + // ================================================================ + // from_platform_value round-trip: to_object -> from_platform_value + // ================================================================ + + #[test] + fn from_platform_value_round_trip_preserves_all_fields() { + let platform_version = PlatformVersion::latest(); + let doc = full_doc(); + let v = doc.to_object().expect("to_object"); + let recovered = DocumentV0::from_platform_value(v, platform_version) + .expect("from_platform_value should succeed"); + assert_eq!(doc, recovered); + } + + #[test] + fn from_platform_value_round_trip_with_minimal_fields() { + let platform_version = PlatformVersion::latest(); + let doc = minimal_doc(); + let v = doc.to_object().expect("to_object"); + let recovered = DocumentV0::from_platform_value(v, platform_version) + .expect("from_platform_value should succeed"); + assert_eq!(doc, recovered); + } + + // ================================================================ + // from_platform_value error path: non-map Value should fail + // ================================================================ + + #[test] + fn from_platform_value_with_non_map_value_returns_error() { + let platform_version = PlatformVersion::latest(); + let bad = Value::Text("not a document".to_string()); + let result = DocumentV0::from_platform_value(bad, platform_version); + assert!( + result.is_err(), + "from_platform_value with a non-map Value should fail" + ); + } +} diff --git a/packages/rs-dpp/src/document/v0/serialize.rs b/packages/rs-dpp/src/document/v0/serialize.rs index 60ed3b3f496..ab5e3249ffe 100644 --- a/packages/rs-dpp/src/document/v0/serialize.rs +++ b/packages/rs-dpp/src/document/v0/serialize.rs @@ -2167,6 +2167,433 @@ mod tests { ); } + // ================================================================ + // Missing-required-field errors in serialize_v0 / v1 / v2. + // The withdrawal contract requires both $createdAt and $updatedAt, + // so a DocumentV0 lacking those should fail serialization. + // ================================================================ + + fn doc_with_ids() -> DocumentV0 { + DocumentV0 { + id: Identifier::new([1u8; 32]), + owner_id: Identifier::new([2u8; 32]), + properties: BTreeMap::new(), + revision: Some(1), + created_at: None, + updated_at: None, + transferred_at: None, + created_at_block_height: None, + updated_at_block_height: None, + transferred_at_block_height: None, + created_at_core_block_height: None, + updated_at_core_block_height: None, + transferred_at_core_block_height: None, + creator_id: None, + } + } + + #[test] + fn serialize_v0_missing_created_at_errors_when_required() { + let platform_version = PlatformVersion::latest(); + let contract = withdrawals_contract(platform_version); + let document_type = contract + .document_type_for_name("withdrawal") + .expect("withdrawal document type"); + + // Build a document missing $createdAt. Don't include any user-defined + // required properties either — we want to trigger the $createdAt path + // before the user-property path. + let doc = doc_with_ids(); + + let err = doc + .serialize_v0(document_type) + .expect_err("serialize_v0 should fail for missing $createdAt"); + match err { + ProtocolError::DataContractError(DataContractError::MissingRequiredKey(msg)) => { + assert!( + msg.contains("created at"), + "expected missing-created-at message, got: {msg}" + ); + } + other => panic!( + "expected MissingRequiredKey for created_at, got {:?}", + other + ), + } + } + + #[test] + fn serialize_v0_missing_updated_at_errors_when_required() { + let platform_version = PlatformVersion::latest(); + let contract = withdrawals_contract(platform_version); + let document_type = contract + .document_type_for_name("withdrawal") + .expect("withdrawal document type"); + + // Supply $createdAt but not $updatedAt — both are required. + let mut doc = doc_with_ids(); + doc.created_at = Some(1_700_000_000_000); + + let err = doc + .serialize_v0(document_type) + .expect_err("serialize_v0 should fail for missing $updatedAt"); + match err { + ProtocolError::DataContractError(DataContractError::MissingRequiredKey(msg)) => { + assert!( + msg.contains("updated at"), + "expected missing-updated-at message, got: {msg}" + ); + } + other => panic!( + "expected MissingRequiredKey for updated_at, got {:?}", + other + ), + } + } + + #[test] + fn serialize_v1_missing_created_at_errors_when_required() { + let platform_version = PlatformVersion::latest(); + let contract = withdrawals_contract(platform_version); + let document_type = contract + .document_type_for_name("withdrawal") + .expect("withdrawal document type"); + + let doc = doc_with_ids(); + let err = doc + .serialize_v1(document_type) + .expect_err("serialize_v1 should fail for missing $createdAt"); + assert!(matches!( + err, + ProtocolError::DataContractError(DataContractError::MissingRequiredKey(_)) + )); + } + + #[test] + fn serialize_v2_missing_created_at_errors_when_required() { + let platform_version = PlatformVersion::latest(); + let contract = withdrawals_contract(platform_version); + let document_type = contract + .document_type_for_name("withdrawal") + .expect("withdrawal document type"); + + let doc = doc_with_ids(); + let err = doc + .serialize_v2(document_type) + .expect_err("serialize_v2 should fail for missing $createdAt"); + assert!(matches!( + err, + ProtocolError::DataContractError(DataContractError::MissingRequiredKey(_)) + )); + } + + #[test] + fn serialize_v0_missing_required_user_property_errors() { + // Family `person` requires `firstName`, `lastName`, `age`. + let platform_version = PlatformVersion::first(); + let contract = family_contract(platform_version); + let document_type = contract + .document_type_for_name("person") + .expect("person document type"); + + // Document with only ids, no user-defined required properties set. + let doc = doc_with_ids(); + + let err = doc + .serialize_v0(document_type) + .expect_err("serialize_v0 should fail for missing required property"); + match err { + ProtocolError::DataContractError(DataContractError::MissingRequiredKey(msg)) => { + // The error message includes the field name for user-defined required fields. + let any_expected = msg.contains("firstName") + || msg.contains("lastName") + || msg.contains("age") + || msg.contains("required field"); + assert!(any_expected, "unexpected error message: {msg}"); + } + other => panic!("expected MissingRequiredKey, got {:?}", other), + } + } + + // ================================================================ + // from_bytes: V1 prefix dispatches to from_bytes_v1 directly + // ================================================================ + + #[test] + fn from_bytes_v1_prefix_dispatches_to_v1_path() { + let platform_version = PlatformVersion::latest(); + let (contract, type_name) = dashpay_contract_and_type(platform_version); + let document_type = contract + .document_type_for_name(&type_name) + .expect("expected document type"); + + let document = document_type + .random_document(Some(123), platform_version) + .expect("expected random document"); + let crate::document::Document::V0(doc_v0) = &document; + + // Bypass the V0-contract gate by calling serialize_v1 directly: the + // resulting varint-1 prefix must round-trip through from_bytes. + let bytes = doc_v0.serialize_v1(document_type).expect("serialize_v1"); + let (ver, _) = u64::decode_var(&bytes).expect("varint"); + assert_eq!(ver, 1); + let recovered = DocumentV0::from_bytes(&bytes, document_type, platform_version) + .expect("from_bytes should dispatch to v1"); + assert_eq!(*doc_v0, recovered); + } + + // ================================================================ + // from_bytes: V2 prefix round-trip for documents with a creator_id + // (contactRequest is transferable, so v2 records the creator flag). + // ================================================================ + + #[test] + fn from_bytes_v2_non_transferable_type_does_not_persist_creator_id() { + // frozen: V0 consensus behavior — contactRequest is non-transferable + // with TradeMode::None, so v2 intentionally skips the creator_id byte + // in both serialize_v2 and from_bytes_v2. Assigning a creator_id on + // the source document is therefore NOT round-tripped. + let platform_version = PlatformVersion::latest(); + let (contract, type_name) = dashpay_contract_and_type(platform_version); + let document_type = contract + .document_type_for_name(&type_name) + .expect("expected document type"); + + let document = document_type + .random_document(Some(321), platform_version) + .expect("expected random document"); + let crate::document::Document::V0(mut doc_v0) = document; + // Even setting a creator_id here has no on-wire effect for this type. + doc_v0.creator_id = Some(Identifier::new([0xAB; 32])); + + let bytes = doc_v0.serialize_v2(document_type).expect("serialize_v2"); + let (ver, _) = u64::decode_var(&bytes).expect("varint"); + assert_eq!(ver, 2); + + let recovered = DocumentV0::from_bytes(&bytes, document_type, platform_version) + .expect("from_bytes should dispatch to v2"); + assert_eq!(doc_v0.id, recovered.id); + assert_eq!(doc_v0.owner_id, recovered.owner_id); + assert_eq!( + recovered.creator_id, None, + "creator_id is not encoded for non-transferable / TradeMode::None types" + ); + } + + #[test] + fn from_bytes_v2_prefix_round_trip_with_none_creator_id() { + let platform_version = PlatformVersion::latest(); + let (contract, type_name) = dashpay_contract_and_type(platform_version); + let document_type = contract + .document_type_for_name(&type_name) + .expect("expected document type"); + + let document = document_type + .random_document(Some(999), platform_version) + .expect("expected random document"); + let crate::document::Document::V0(mut doc_v0) = document; + // creator_id is None — exercise the else-branch of v2's creator check. + doc_v0.creator_id = None; + + let bytes = doc_v0.serialize_v2(document_type).expect("serialize_v2"); + let recovered = + DocumentV0::from_bytes(&bytes, document_type, platform_version).expect("from_bytes v2"); + assert_eq!(recovered.creator_id, None); + } + + // ================================================================ + // from_bytes_v1 / v2 directly — too-small buffers should error + // before we read any id / owner id bytes. + // ================================================================ + + #[test] + fn from_bytes_v1_direct_too_small_buffer_errors() { + let platform_version = PlatformVersion::first(); + let (contract, type_name) = dashpay_contract_and_type(platform_version); + let document_type = contract + .document_type_for_name(&type_name) + .expect("expected document type"); + + let result = DocumentV0::from_bytes_v1(&[0u8; 10], document_type, platform_version); + assert!( + result.is_err(), + "from_bytes_v1 should fail for buffer < 64 bytes" + ); + } + + #[test] + fn from_bytes_v2_direct_too_small_buffer_errors() { + let platform_version = PlatformVersion::first(); + let (contract, type_name) = dashpay_contract_and_type(platform_version); + let document_type = contract + .document_type_for_name(&type_name) + .expect("expected document type"); + + let result = DocumentV0::from_bytes_v2(&[0u8; 10], document_type, platform_version); + assert!( + result.is_err(), + "from_bytes_v2 should fail for buffer < 64 bytes" + ); + } + + #[test] + fn from_bytes_v0_direct_too_small_buffer_errors() { + let platform_version = PlatformVersion::first(); + let (contract, type_name) = dashpay_contract_and_type(platform_version); + let document_type = contract + .document_type_for_name(&type_name) + .expect("expected document type"); + + let result = DocumentV0::from_bytes_v0(&[0u8; 10], document_type, platform_version); + assert!( + result.is_err(), + "from_bytes_v0 should fail for buffer < 64 bytes" + ); + } + + // ================================================================ + // from_bytes: V1 prefix with truncated post-id data errors + // ================================================================ + + #[test] + fn from_bytes_v1_truncated_post_ids_errors() { + let platform_version = PlatformVersion::latest(); + let (contract, type_name) = dashpay_contract_and_type(platform_version); + let document_type = contract + .document_type_for_name(&type_name) + .expect("expected document type"); + + // V1 varint + 64 bytes (id + owner_id) — nothing after that, so the + // revision / timestamp_flags read must fail. + let mut buf = 1u64.encode_var_vec(); + buf.extend_from_slice(&[0xCD; 64]); + + let result = DocumentV0::from_bytes(&buf, document_type, platform_version); + assert!( + result.is_err(), + "v1 with truncated post-ids should fail deserialization" + ); + } + + #[test] + fn from_bytes_v2_truncated_post_ids_errors() { + let platform_version = PlatformVersion::latest(); + let (contract, type_name) = dashpay_contract_and_type(platform_version); + let document_type = contract + .document_type_for_name(&type_name) + .expect("expected document type"); + + let mut buf = 2u64.encode_var_vec(); + buf.extend_from_slice(&[0xCD; 64]); + + let result = DocumentV0::from_bytes(&buf, document_type, platform_version); + assert!( + result.is_err(), + "v2 with truncated post-ids should fail deserialization" + ); + } + + // ================================================================ + // serialize_specific_version: V0 contract + feature_version 0 + // should succeed (the V0-gated NotSupported branch is NOT hit). + // ================================================================ + + #[test] + fn serialize_specific_version_v0_contract_feature_version_0_succeeds() { + let platform_version = PlatformVersion::first(); + let (contract, type_name) = dashpay_contract_and_type(platform_version); + let document_type = contract + .document_type_for_name(&type_name) + .expect("expected document type"); + + let document = document_type + .random_document(Some(5), platform_version) + .expect("expected random document"); + let crate::document::Document::V0(doc_v0) = &document; + + // feature_version 0 is explicitly allowed for V0 contracts. + let bytes = doc_v0 + .serialize_specific_version(document_type, &contract, 0) + .expect("serialize_specific_version v0 should succeed on a V0 contract"); + let (ver, _) = u64::decode_var(&bytes).expect("varint decode"); + assert_eq!(ver, 0); + } + + // ================================================================ + // serialize_specific_version: feature_version 2 with a non-V0 + // contract (latest platform version) should succeed. + // ================================================================ + + #[test] + fn serialize_specific_version_rejects_v2_for_v0_contract() { + // V0 contracts always force serialize_v0, so feature_version 2 is + // rejected with NotSupported before reaching the version dispatch. + let platform_version = PlatformVersion::latest(); + let (contract, type_name) = dashpay_contract_and_type(platform_version); + let document_type = contract + .document_type_for_name(&type_name) + .expect("expected document type"); + + let document = document_type + .random_document(Some(17), platform_version) + .expect("expected random document"); + let crate::document::Document::V0(doc_v0) = &document; + + if matches!(&contract, DataContract::V0(_)) { + let err = doc_v0 + .serialize_specific_version(document_type, &contract, 2) + .expect_err("V0 contract should reject v2"); + match err { + ProtocolError::NotSupported(_) => {} + other => panic!("expected NotSupported, got {:?}", other), + } + } + } + + // ================================================================ + // from_bytes V0-then-V1 fallback: a valid V1 buffer with a V0 + // varint prefix should still round-trip via the fallback path. + // Construct bytes by serializing v1 and then overwriting the + // varint prefix to 0. + // ================================================================ + + #[test] + fn from_bytes_v0_falls_back_to_v1_on_decoding_error() { + // Use a contract whose properties are all integers so v0 (I64) and v1 + // (actual type) produce different encoded lengths / types. family's + // `person` has one integer field `age`, suitable for fallback testing. + let platform_version = PlatformVersion::latest(); + let contract = family_contract(platform_version); + let document_type = contract + .document_type_for_name("person") + .expect("person document type"); + + // Random document serialized in v1 format (integers kept as native). + let document = document_type + .random_document(Some(55), platform_version) + .expect("random document"); + let crate::document::Document::V0(doc_v0) = document; + + // Serialize in v1 explicitly. + let mut v1_bytes = doc_v0 + .serialize_v1(document_type) + .expect("serialize_v1 should succeed"); + // Overwrite the varint-1 prefix with varint-0. + v1_bytes[0] = 0; + + // from_bytes will dispatch to v0, which may fail, but the implementation + // then retries via v1. Either way, it must not return UnknownVersionMismatch. + let result = DocumentV0::from_bytes(&v1_bytes, document_type, platform_version); + match result { + Ok(recovered) => assert_eq!(recovered, doc_v0), + Err(e) => { + // If v1-decode also fails, we get the original v0 error — not a + // version mismatch, since the prefix was 0. + assert!(!matches!(e, ProtocolError::UnknownVersionMismatch { .. })); + } + } + } + // ================================================================ // Known-bytes deserialization (golden test) // ================================================================ From e46faaa3541154f14c22046932fc9d24abe378c2 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 23 Apr 2026 21:07:22 +0800 Subject: [PATCH 3/5] test(drive-abci): cover query handler error paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add 60 tests across nine gRPC query handlers in rs-drive-abci/src/query/ that target real user-facing error paths — malformed identifiers, edge-case list sizes, mixed valid/invalid identifier lists, contract-info branch boundaries, start_at info variants, address parsing, chunk alignment, and mixed proof/non-proof response paths. - address_funds/addresses_infos: 7 tests (P2SH roundtrip, empty bytes, first-invalid short-circuit, mixed known/unknown proofs) - data_contract_based_queries/data_contract: 6 tests (found+serialize, found+proof, absence proof, various invalid id shapes) - shielded/encrypted_notes: 9 tests (chunk alignment, boundary at chunk size, count=max, prove path) - system/current_quorums_info: 5 tests populating validator sets via TestQuorumInfo (descending sort, banned validator, proposer propagation, multi-set) - token_queries/identities_token_infos: 7 tests (mixed frozen/unfrozen, list with one invalid id, empty list, max+proof) - token_queries/token_direct_purchase_prices: 5 tests (empty+proof, variable-price tier integrity, mixed known/unknown proof) - token_queries/token_perpetual_distribution_last_claim: 6 tests (u16::MAX boundary for token position, nonexistent position, identity-id-with- contract-info ordering) - token_queries/token_pre_programmed_distributions: 9 tests (start_at recipient included true/false/none, prove+start_at, far-future start) - voting/contested_resource_identity_votes: 6 tests (invalid/valid start_at poll identifier, offset+descending, offset at u16::MAX boundary) --- .../address_funds/addresses_infos/v0/mod.rs | 145 +++++++++ .../data_contract/v0/mod.rs | 158 +++++++++- .../query/shielded/encrypted_notes/v0/mod.rs | 221 +++++++++++++ .../system/current_quorums_info/v0/mod.rs | 290 ++++++++++++++++++ .../identities_token_infos/v0/mod.rs | 191 ++++++++++++ .../token_direct_purchase_prices/v0/mod.rs | 155 ++++++++++ .../v0/mod.rs | 171 +++++++++++ .../v0/mod.rs | 215 +++++++++++++ .../v0/mod.rs | 177 +++++++++++ 9 files changed, 1722 insertions(+), 1 deletion(-) diff --git a/packages/rs-drive-abci/src/query/address_funds/addresses_infos/v0/mod.rs b/packages/rs-drive-abci/src/query/address_funds/addresses_infos/v0/mod.rs index 77c512dafe7..77653c9f223 100644 --- a/packages/rs-drive-abci/src/query/address_funds/addresses_infos/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/address_funds/addresses_infos/v0/mod.rs @@ -367,4 +367,149 @@ mod tests { "expected validation errors for empty address list proof" ); } + + #[test] + fn test_addresses_infos_proof_single_existing_address() { + // Prove path with a single stored address returns a Proof variant. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + setup_two_address_balances(&platform, version); + + let request = GetAddressesInfosRequestV0 { + addresses: vec![ADDR1.to_bytes()], + prove: true, + }; + + let result = platform + .query_addresses_infos_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(result.errors.is_empty(), "{:?}", result.errors); + let response = result.data.unwrap(); + assert!(matches!( + response.result, + Some(get_addresses_infos_response_v0::Result::Proof(_)) + )); + assert!(response.metadata.is_some()); + } + + #[test] + fn test_addresses_infos_proof_with_unknown_address() { + // Prove path with only unknown addresses still returns a Proof + // (an absence proof). + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + setup_two_address_balances(&platform, version); + + let unknown = PlatformAddress::P2pkh([42; 20]); + let request = GetAddressesInfosRequestV0 { + addresses: vec![unknown.to_bytes()], + prove: true, + }; + + let result = platform + .query_addresses_infos_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(result.errors.is_empty(), "{:?}", result.errors); + let response = result.data.unwrap(); + assert!(matches!( + response.result, + Some(get_addresses_infos_response_v0::Result::Proof(_)) + )); + } + + #[test] + fn test_addresses_infos_first_invalid_short_circuits_list() { + // A list with one malformed address aborts the whole query with + // InvalidArgument. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetAddressesInfosRequestV0 { + addresses: vec![ADDR1.to_bytes(), vec![0xFF, 0xFF]], + prove: false, + }; + + let result = platform + .query_addresses_infos_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::InvalidArgument(msg)] if msg.contains("invalid key_of_type") + )); + } + + #[test] + fn test_addresses_infos_p2sh_address_roundtrip() { + // Ensure P2SH addresses parse and round-trip correctly alongside P2PKH. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + setup_two_address_balances(&platform, version); + + // ADDR2 is a P2SH address by setup. + let request = GetAddressesInfosRequestV0 { + addresses: vec![ADDR2.to_bytes()], + prove: false, + }; + + let result = platform + .query_addresses_infos_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(result.errors.is_empty(), "{:?}", result.errors); + let response = result.data.unwrap(); + match response.result { + Some(get_addresses_infos_response_v0::Result::AddressInfoEntries(entries)) => { + assert_eq!(entries.address_info_entries.len(), 1); + let entry = &entries.address_info_entries[0]; + assert_eq!(entry.address, ADDR2.to_bytes()); + let ban = entry.balance_and_nonce.as_ref().expect("present"); + assert_eq!(ban.balance, BALANCE2); + assert_eq!(ban.nonce, NONCE2); + } + other => panic!("expected AddressInfoEntries result, got {:?}", other), + } + } + + #[test] + fn test_addresses_infos_empty_bytes_invalid() { + // Completely empty address bytes should also fail parsing. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetAddressesInfosRequestV0 { + addresses: vec![vec![]], + prove: false, + }; + + let result = platform + .query_addresses_infos_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::InvalidArgument(msg)] if msg.contains("invalid key_of_type") + )); + } + + #[test] + fn test_addresses_infos_proof_with_mixed_known_unknown() { + // Prove path with mixed known + unknown addresses returns a proof + // (covers both slots). + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + setup_two_address_balances(&platform, version); + + let unknown = PlatformAddress::P2sh([55; 20]); + let request = GetAddressesInfosRequestV0 { + addresses: vec![ADDR1.to_bytes(), unknown.to_bytes()], + prove: true, + }; + + let result = platform + .query_addresses_infos_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(result.errors.is_empty(), "{:?}", result.errors); + assert!(matches!( + result.data.unwrap().result, + Some(get_addresses_infos_response_v0::Result::Proof(_)) + )); + } } diff --git a/packages/rs-drive-abci/src/query/data_contract_based_queries/data_contract/v0/mod.rs b/packages/rs-drive-abci/src/query/data_contract_based_queries/data_contract/v0/mod.rs index 97bfdfa83de..e14ea123aac 100644 --- a/packages/rs-drive-abci/src/query/data_contract_based_queries/data_contract/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/data_contract_based_queries/data_contract/v0/mod.rs @@ -78,8 +78,11 @@ impl Platform { #[cfg(test)] mod tests { use super::*; - use crate::query::tests::{assert_invalid_identifier, setup_platform}; + use crate::query::tests::{assert_invalid_identifier, setup_platform, store_data_contract}; use dpp::dashcore::Network; + use dpp::data_contract::accessors::v0::DataContractV0Getters; + use dpp::serialization::PlatformDeserializableWithPotentialValidationFromVersionedStructure; + use dpp::tests::fixtures::get_data_contract_fixture; #[test] fn test_invalid_data_contract_id() { @@ -137,4 +140,157 @@ mod tests { }) )); } + + #[test] + fn test_invalid_data_contract_id_zero_length() { + // Completely empty id bytes should fall through the same identifier + // validation path. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetDataContractRequestV0 { + id: vec![], + prove: false, + }; + + let result = platform + .query_data_contract_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::InvalidArgument(msg)] if msg.contains("32 bytes long") + )); + } + + #[test] + fn test_invalid_data_contract_id_too_long() { + // More than 32 bytes is still invalid. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetDataContractRequestV0 { + id: vec![1; 33], + prove: false, + }; + + let result = platform + .query_data_contract_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::InvalidArgument(msg)] if msg.contains("32 bytes long") + )); + } + + #[test] + fn test_invalid_data_contract_id_for_proof() { + // Even with prove=true the identifier check must come first. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetDataContractRequestV0 { + id: vec![0; 7], + prove: true, + }; + + let result = platform.query_data_contract_v0(request, &state, version); + + assert_invalid_identifier(result.unwrap()); + } + + #[test] + fn test_data_contract_found_returns_serialized() { + // Happy path: store a contract via helpers, then fetch it without + // proof. We should get a non-empty DataContract back. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let created = get_data_contract_fixture(None, 0, version.protocol_version); + store_data_contract(&platform, created.data_contract(), version); + let contract_id = created.data_contract().id(); + + let request = GetDataContractRequestV0 { + id: contract_id.to_vec(), + prove: false, + }; + + let result = platform + .query_data_contract_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!( + result.errors.is_empty(), + "expected no errors, got {:?}", + result.errors + ); + let data = result.data.expect("expected data"); + match data.result { + Some(get_data_contract_response_v0::Result::DataContract(bytes)) => { + assert!( + !bytes.is_empty(), + "expected a non-empty serialized data contract" + ); + // Round-trip through the deserializer to prove the bytes are valid. + let round_tripped = + dpp::data_contract::DataContract::versioned_deserialize(&bytes, false, version) + .expect("contract should deserialize"); + assert_eq!(round_tripped.id(), contract_id); + } + other => panic!("expected DataContract result, got {:?}", other), + } + assert!(data.metadata.is_some(), "expected metadata present"); + } + + #[test] + fn test_data_contract_found_with_proof() { + // Prove path with an actually-stored contract should return a Proof + // variant with metadata. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let created = get_data_contract_fixture(None, 0, version.protocol_version); + store_data_contract(&platform, created.data_contract(), version); + let contract_id = created.data_contract().id(); + + let request = GetDataContractRequestV0 { + id: contract_id.to_vec(), + prove: true, + }; + + let result = platform + .query_data_contract_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!( + result.errors.is_empty(), + "expected no errors, got {:?}", + result.errors + ); + let data = result.data.expect("expected data"); + assert!(matches!( + data.result, + Some(get_data_contract_response_v0::Result::Proof(_)) + )); + assert!(data.metadata.is_some()); + } + + #[test] + fn test_data_contract_not_found_proof_is_still_proof() { + // A nonexistent but well-formed identifier with prove=true should not + // fail, but should return an (absence) Proof wrapped in the response. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetDataContractRequestV0 { + id: vec![0xAB; 32], + prove: true, + }; + + let result = platform + .query_data_contract_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(result.errors.is_empty(), "{:?}", result.errors); + let data = result.data.expect("expected data"); + assert!(matches!( + data.result, + Some(get_data_contract_response_v0::Result::Proof(_)) + )); + } } diff --git a/packages/rs-drive-abci/src/query/shielded/encrypted_notes/v0/mod.rs b/packages/rs-drive-abci/src/query/shielded/encrypted_notes/v0/mod.rs index bb7ac956fd1..1a8afbb7766 100644 --- a/packages/rs-drive-abci/src/query/shielded/encrypted_notes/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/encrypted_notes/v0/mod.rs @@ -146,3 +146,224 @@ impl Platform { Ok(QueryValidationResult::new_with_data(response)) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::query::tests::setup_platform; + use dpp::dashcore::Network; + + fn max_chunk_size(version: &PlatformVersion) -> u64 { + version + .drive_abci + .query + .shielded_queries + .max_encrypted_notes_per_query as u64 + } + + #[test] + fn test_v0_non_aligned_start_index_errors() { + // Non-aligned start_index branch: returns InvalidArgument directly. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetShieldedEncryptedNotesRequestV0 { + start_index: 5, // not aligned to chunk size + count: 10, + prove: false, + }; + + let result = platform + .query_shielded_encrypted_notes_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::InvalidArgument(msg)] if msg.contains("not chunk-aligned") + )); + } + + #[test] + fn test_v0_non_aligned_large_start_index_errors() { + // An almost-aligned value (chunk_size + 1) must still be rejected. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + let chunk = max_chunk_size(version); + + let request = GetShieldedEncryptedNotesRequestV0 { + start_index: chunk + 1, + count: 10, + prove: false, + }; + + let result = platform + .query_shielded_encrypted_notes_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::InvalidArgument(msg)] if msg.contains("not chunk-aligned") + )); + } + + #[test] + fn test_v0_aligned_start_at_chunk_size_boundary_ok() { + // An aligned start_index equal to exactly chunk_size should succeed + // (fresh pool → empty result set). + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + let chunk = max_chunk_size(version); + + let request = GetShieldedEncryptedNotesRequestV0 { + start_index: chunk, + count: 1, + prove: false, + }; + + let result = platform + .query_shielded_encrypted_notes_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(result.errors.is_empty(), "{:?}", result.errors); + let data = result.data.unwrap(); + match data.result { + Some(get_shielded_encrypted_notes_response_v0::Result::EncryptedNotes(notes)) => { + assert!(notes.entries.is_empty()); + } + other => panic!("expected EncryptedNotes, got {:?}", other), + } + } + + #[test] + fn test_v0_aligned_start_at_multiple_of_chunk_size_ok() { + // start_index = 2 * chunk_size must also be accepted. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + let chunk = max_chunk_size(version); + + let request = GetShieldedEncryptedNotesRequestV0 { + start_index: chunk * 2, + count: 1, + prove: false, + }; + + let result = platform + .query_shielded_encrypted_notes_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(result.errors.is_empty(), "{:?}", result.errors); + } + + #[test] + fn test_v0_count_one_yields_limit_one() { + // count=1 bypasses the "0 or > max" branch and sets effective=1. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetShieldedEncryptedNotesRequestV0 { + start_index: 0, + count: 1, + prove: false, + }; + + let result = platform + .query_shielded_encrypted_notes_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(result.errors.is_empty(), "{:?}", result.errors); + let data = result.data.unwrap(); + match data.result { + Some(get_shielded_encrypted_notes_response_v0::Result::EncryptedNotes(notes)) => { + // Empty state → empty entries even with count=1. + assert!(notes.entries.is_empty()); + } + other => panic!("expected EncryptedNotes, got {:?}", other), + } + } + + #[test] + fn test_v0_prove_path_aligned_start() { + // Prove path on empty state with aligned start_index should return a + // Proof variant. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetShieldedEncryptedNotesRequestV0 { + start_index: 0, + count: 16, + prove: true, + }; + + let result = platform + .query_shielded_encrypted_notes_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(result.errors.is_empty(), "{:?}", result.errors); + assert!(matches!( + result.data, + Some(GetShieldedEncryptedNotesResponseV0 { + result: Some(get_shielded_encrypted_notes_response_v0::Result::Proof(_)), + metadata: Some(_), + }) + )); + } + + #[test] + fn test_v0_prove_path_rejects_unaligned_start() { + // Non-aligned start_index is rejected even with prove=true — the + // alignment check is *before* the prove branch. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetShieldedEncryptedNotesRequestV0 { + start_index: 3, + count: 4, + prove: true, + }; + + let result = platform + .query_shielded_encrypted_notes_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::InvalidArgument(msg)] if msg.contains("not chunk-aligned") + )); + } + + #[test] + fn test_v0_count_exactly_max_is_accepted() { + // count == max is neither `0` nor `> max`, so it falls through the + // inner `else` that keeps count as-is. Covers that fallthrough. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + let max = version + .drive_abci + .query + .shielded_queries + .max_encrypted_notes_per_query as u32; + + let request = GetShieldedEncryptedNotesRequestV0 { + start_index: 0, + count: max, + prove: false, + }; + + let result = platform + .query_shielded_encrypted_notes_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(result.errors.is_empty(), "{:?}", result.errors); + } + + #[test] + fn test_v0_start_index_zero_is_always_aligned() { + // start_index = 0 is always aligned (any X % chunk_size for 0 is 0). + // Exercises the `start_index % chunk_size == 0` short-path. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetShieldedEncryptedNotesRequestV0 { + start_index: 0, + count: 8, + prove: false, + }; + + let result = platform + .query_shielded_encrypted_notes_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(result.errors.is_empty(), "{:?}", result.errors); + } +} diff --git a/packages/rs-drive-abci/src/query/system/current_quorums_info/v0/mod.rs b/packages/rs-drive-abci/src/query/system/current_quorums_info/v0/mod.rs index 98926279cb8..ee6fa018a85 100644 --- a/packages/rs-drive-abci/src/query/system/current_quorums_info/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/system/current_quorums_info/v0/mod.rs @@ -75,8 +75,67 @@ impl Platform { #[cfg(test)] mod tests { use super::*; + use crate::mimic::test_quorum::TestQuorumInfo; + use crate::platform_types::validator_set::ValidatorSet; use crate::query::tests::setup_platform; + use dpp::block::extended_block_info::v0::ExtendedBlockInfoV0; + use dpp::dashcore::hashes::Hash; use dpp::dashcore::Network; + use dpp::dashcore::{ProTxHash, QuorumHash}; + use indexmap::IndexMap; + use rand::rngs::StdRng; + use rand::SeedableRng; + use std::sync::Arc; + + /// Build a `ValidatorSet::V0` for a given quorum hash, core height, and + /// set of pro_tx_hashes. Uses the `TestQuorumInfo` helper so the BLS keys + /// are well-formed. + fn make_validator_set( + quorum_hash: QuorumHash, + core_height: u32, + pro_tx_hashes: Vec, + seed: u64, + ) -> ValidatorSet { + let mut rng = StdRng::seed_from_u64(seed); + let info = TestQuorumInfo::from_quorum_hash_and_pro_tx_hashes( + core_height, + quorum_hash, + None, + pro_tx_hashes, + &mut rng, + ); + ValidatorSet::V0(info.into()) + } + + /// Store a map of validator sets, a current quorum hash, and an optional + /// last-committed-block proposer into the platform state. + fn install_validator_sets( + platform: &crate::test::helpers::setup::TempPlatform, + sets: IndexMap, + current_quorum_hash: QuorumHash, + proposer_pro_tx_hash: Option<[u8; 32]>, + ) { + let mut state = (**platform.state.load()).clone(); + state.set_validator_sets(sets); + state.set_current_validator_set_quorum_hash(current_quorum_hash); + + if let Some(proposer) = proposer_pro_tx_hash { + state.set_last_committed_block_info(Some( + ExtendedBlockInfoV0 { + basic_info: dpp::block::block_info::BlockInfo::default(), + app_hash: [0u8; 32], + quorum_hash: current_quorum_hash.as_byte_array().to_owned(), + block_id_hash: [0u8; 32], + proposer_pro_tx_hash: proposer, + signature: [0u8; 96], + round: 0, + } + .into(), + )); + } + + platform.state.store(Arc::new(state)); + } #[test] fn test_query_current_quorums_info_empty_state() { @@ -103,4 +162,235 @@ mod tests { assert!(data.last_block_proposer.iter().all(|b| *b == 0)); assert!(data.metadata.is_some()); } + + #[test] + fn test_query_current_quorums_info_single_set_mapping() { + // With a single populated validator set we exercise the `.map` branch + // that copies members, threshold public key, quorum hash, and core + // height into the response. + let (platform, _state, _version) = setup_platform(None, Network::Testnet, None); + + let quorum_hash = QuorumHash::from_byte_array([7u8; 32]); + let pro_tx_hashes = vec![ + ProTxHash::from_byte_array([1u8; 32]), + ProTxHash::from_byte_array([2u8; 32]), + ProTxHash::from_byte_array([3u8; 32]), + ]; + let validator_set = make_validator_set(quorum_hash, 1000, pro_tx_hashes.clone(), 42); + + let mut sets = IndexMap::new(); + sets.insert(quorum_hash, validator_set); + install_validator_sets(&platform, sets, quorum_hash, Some([9u8; 32])); + + // Re-load state now that we stored new one. + let state = platform.state.load_full(); + + let result = platform + .query_current_quorums_info_v0(GetCurrentQuorumsInfoRequestV0 {}, &state) + .expect("expected query to succeed"); + + let data = result.into_data().expect("expected data"); + + assert_eq!(data.quorum_hashes.len(), 1); + assert_eq!(data.quorum_hashes[0], quorum_hash.as_byte_array().to_vec()); + assert_eq!(data.validator_sets.len(), 1); + let vs = &data.validator_sets[0]; + assert_eq!(vs.quorum_hash, quorum_hash.as_byte_array().to_vec()); + assert_eq!(vs.core_height, 1000); + assert_eq!(vs.members.len(), 3); + // threshold public key is 48 bytes compressed BLS key + assert_eq!(vs.threshold_public_key.len(), 48); + // current_quorum_hash reflects what we set + assert_eq!( + data.current_quorum_hash, + quorum_hash.as_byte_array().to_vec() + ); + // last_block_proposer reflects the extended block info + assert_eq!(data.last_block_proposer, vec![9u8; 32]); + } + + #[test] + fn test_query_current_quorums_info_multiple_sets_sorted_descending() { + // When more than one validator set is present, the response must order + // them by core_height descending. This covers the `sort_by_key` path. + let (platform, _state, _version) = setup_platform(None, Network::Testnet, None); + + let qh1 = QuorumHash::from_byte_array([1u8; 32]); + let qh2 = QuorumHash::from_byte_array([2u8; 32]); + let qh3 = QuorumHash::from_byte_array([3u8; 32]); + + let set_low = make_validator_set( + qh1, + 100, + vec![ + ProTxHash::from_byte_array([10u8; 32]), + ProTxHash::from_byte_array([11u8; 32]), + ], + 1, + ); + let set_high = make_validator_set( + qh2, + 500, + vec![ + ProTxHash::from_byte_array([20u8; 32]), + ProTxHash::from_byte_array([21u8; 32]), + ], + 2, + ); + let set_mid = make_validator_set( + qh3, + 300, + vec![ + ProTxHash::from_byte_array([30u8; 32]), + ProTxHash::from_byte_array([31u8; 32]), + ], + 3, + ); + + // Intentionally insert out of order. Response must sort by core_height desc. + let mut sets = IndexMap::new(); + sets.insert(qh1, set_low); + sets.insert(qh2, set_high); + sets.insert(qh3, set_mid); + + install_validator_sets(&platform, sets, qh2, None); + + let state = platform.state.load_full(); + + let data = platform + .query_current_quorums_info_v0(GetCurrentQuorumsInfoRequestV0 {}, &state) + .expect("expected query to succeed") + .into_data() + .expect("expected data"); + + assert_eq!(data.validator_sets.len(), 3); + // Descending by core_height + let heights: Vec = data.validator_sets.iter().map(|v| v.core_height).collect(); + assert_eq!(heights, vec![500, 300, 100]); + let returned_hashes: Vec> = data.quorum_hashes.to_vec(); + assert_eq!(returned_hashes.len(), 3); + assert_eq!(returned_hashes[0], qh2.as_byte_array().to_vec()); + assert_eq!(returned_hashes[1], qh3.as_byte_array().to_vec()); + assert_eq!(returned_hashes[2], qh1.as_byte_array().to_vec()); + assert_eq!(data.current_quorum_hash, qh2.as_byte_array().to_vec()); + } + + #[test] + fn test_query_current_quorums_info_banned_validator_propagates() { + // Validators with `is_banned = true` should still appear in the + // response but carry the flag through. + let (platform, _state, _version) = setup_platform(None, Network::Testnet, None); + + let quorum_hash = QuorumHash::from_byte_array([5u8; 32]); + let pro_tx_hashes = vec![ + ProTxHash::from_byte_array([11u8; 32]), + ProTxHash::from_byte_array([12u8; 32]), + ]; + let mut validator_set = make_validator_set(quorum_hash, 42, pro_tx_hashes, 7); + + // Mutate one member to be banned. The `ValidatorSet` enum currently + // has only one variant but we still destructure it rather than assume. + let ValidatorSet::V0(v0) = &mut validator_set; + let first_key = *v0.members.keys().next().expect("at least one member"); + v0.members.get_mut(&first_key).unwrap().is_banned = true; + + let mut sets = IndexMap::new(); + sets.insert(quorum_hash, validator_set); + install_validator_sets(&platform, sets, quorum_hash, None); + + let state = platform.state.load_full(); + + let data = platform + .query_current_quorums_info_v0(GetCurrentQuorumsInfoRequestV0 {}, &state) + .expect("expected query to succeed") + .into_data() + .expect("expected data"); + + assert_eq!(data.validator_sets.len(), 1); + let vs = &data.validator_sets[0]; + let banned_count = vs.members.iter().filter(|m| m.is_banned).count(); + assert_eq!( + banned_count, 1, + "one banned validator should be reported as banned" + ); + let not_banned = vs.members.iter().filter(|m| !m.is_banned).count(); + assert_eq!(not_banned, 1); + // node_ip is a non-empty string constructed by TestQuorumInfo + for m in &vs.members { + assert!(!m.node_ip.is_empty()); + assert_eq!(m.pro_tx_hash.len(), 32); + } + } + + #[test] + fn test_query_current_quorums_info_current_quorum_hash_not_in_sets() { + // The response returns `current_quorum_hash` even if no set in the map + // actually matches it. This covers a defensive read path. + let (platform, _state, _version) = setup_platform(None, Network::Testnet, None); + + let present_hash = QuorumHash::from_byte_array([8u8; 32]); + let unrelated_current = QuorumHash::from_byte_array([99u8; 32]); + let set = make_validator_set( + present_hash, + 250, + vec![ + ProTxHash::from_byte_array([44u8; 32]), + ProTxHash::from_byte_array([45u8; 32]), + ], + 99, + ); + + let mut sets = IndexMap::new(); + sets.insert(present_hash, set); + install_validator_sets(&platform, sets, unrelated_current, None); + + let state = platform.state.load_full(); + + let data = platform + .query_current_quorums_info_v0(GetCurrentQuorumsInfoRequestV0 {}, &state) + .expect("expected query to succeed") + .into_data() + .expect("expected data"); + + assert_eq!(data.validator_sets.len(), 1); + assert_eq!( + data.current_quorum_hash, + unrelated_current.as_byte_array().to_vec() + ); + // The set's quorum hash is NOT the current one. + assert_ne!(data.validator_sets[0].quorum_hash, data.current_quorum_hash); + } + + #[test] + fn test_query_current_quorums_info_last_block_proposer_reflected() { + // Verify the proposer pro-tx-hash from last_committed_block_info is + // returned verbatim as `last_block_proposer`. + let (platform, _state, _version) = setup_platform(None, Network::Testnet, None); + + let quorum_hash = QuorumHash::from_byte_array([6u8; 32]); + let set = make_validator_set( + quorum_hash, + 10, + vec![ + ProTxHash::from_byte_array([77u8; 32]), + ProTxHash::from_byte_array([78u8; 32]), + ], + 11, + ); + let mut sets = IndexMap::new(); + sets.insert(quorum_hash, set); + + let proposer = [123u8; 32]; + install_validator_sets(&platform, sets, quorum_hash, Some(proposer)); + + let state = platform.state.load_full(); + + let data = platform + .query_current_quorums_info_v0(GetCurrentQuorumsInfoRequestV0 {}, &state) + .expect("expected query to succeed") + .into_data() + .expect("expected data"); + + assert_eq!(data.last_block_proposer, proposer.to_vec()); + } } diff --git a/packages/rs-drive-abci/src/query/token_queries/identities_token_infos/v0/mod.rs b/packages/rs-drive-abci/src/query/token_queries/identities_token_infos/v0/mod.rs index 60b31c187fb..0bb8b92be42 100644 --- a/packages/rs-drive-abci/src/query/token_queries/identities_token_infos/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/token_queries/identities_token_infos/v0/mod.rs @@ -332,4 +332,195 @@ mod tests { }) )); } + + #[test] + fn test_invalid_token_id_with_multiple_valid_identities() { + // Invalid token_id fires first even when identity_ids are well-formed. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetIdentitiesTokenInfosRequestV0 { + token_id: vec![0; 31], + identity_ids: vec![vec![1; 32], vec![2; 32]], + prove: false, + }; + + let result = platform + .query_identities_token_infos_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::InvalidArgument(msg)] if msg.contains("token_id") + )); + } + + #[test] + fn test_one_invalid_identity_in_list_rejected() { + // A mixed list with one malformed identity fails the entire call. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetIdentitiesTokenInfosRequestV0 { + token_id: vec![0; 32], + identity_ids: vec![vec![1; 32], vec![2; 5], vec![3; 32]], + prove: false, + }; + + let result = platform + .query_identities_token_infos_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::InvalidArgument(msg)] if msg.contains("identity_id") + )); + } + + #[test] + fn test_empty_identity_ids_list() { + // Empty list is accepted (len 0 does not exceed max) and yields empty + // token_infos. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetIdentitiesTokenInfosRequestV0 { + token_id: vec![0; 32], + identity_ids: vec![], + prove: false, + }; + + let result = platform + .query_identities_token_infos_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(result.errors.is_empty(), "{:?}", result.errors); + let data = result.data.unwrap(); + match data.result { + Some(get_identities_token_infos_response_v0::Result::IdentityTokenInfos(infos)) => { + assert!(infos.token_infos.is_empty()); + } + _ => panic!("expected IdentityTokenInfos result"), + } + } + + #[test] + fn test_query_mixed_frozen_and_unfrozen_identities() { + // Query multiple identities, one frozen (id 2), one unfrozen (id 1). + // Both entries should appear; the frozen one has Some(info) with + // frozen=true, the unfrozen one has info=None. + let (platform, state, version, _, token_ids, identity_ids) = + setup_platform_with_token_state(); + + let request = GetIdentitiesTokenInfosRequestV0 { + token_id: token_ids[0].to_vec(), + identity_ids: vec![identity_ids[0].to_vec(), identity_ids[1].to_vec()], + prove: false, + }; + + let result = platform + .query_identities_token_infos_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(result.errors.is_empty(), "{:?}", result.errors); + let data = result.data.unwrap(); + match data.result { + Some(get_identities_token_infos_response_v0::Result::IdentityTokenInfos(infos)) => { + assert_eq!(infos.token_infos.len(), 2); + let mut frozen_found = false; + let mut none_found = false; + for entry in &infos.token_infos { + if entry.identity_id == identity_ids[0].to_vec() { + assert!( + entry.info.is_none(), + "unfrozen identity should have None info" + ); + none_found = true; + } else if entry.identity_id == identity_ids[1].to_vec() { + let info = entry + .info + .as_ref() + .expect("frozen identity should have info"); + assert!(info.frozen); + frozen_found = true; + } + } + assert!(frozen_found && none_found); + } + _ => panic!("expected IdentityTokenInfos result"), + } + } + + #[test] + fn test_query_multiple_identities_proof() { + // Prove path with multiple identities should return Proof variant. + let (platform, state, version, _, token_ids, identity_ids) = + setup_platform_with_token_state(); + + let request = GetIdentitiesTokenInfosRequestV0 { + token_id: token_ids[0].to_vec(), + identity_ids: vec![ + identity_ids[0].to_vec(), + identity_ids[1].to_vec(), + identity_ids[2].to_vec(), + ], + prove: true, + }; + + let result = platform + .query_identities_token_infos_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(result.errors.is_empty()); + assert!(matches!( + result.data, + Some(GetIdentitiesTokenInfosResponseV0 { + result: Some(get_identities_token_infos_response_v0::Result::Proof(_)), + metadata: Some(_), + }) + )); + } + + #[test] + fn test_invalid_token_id_zero_length() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetIdentitiesTokenInfosRequestV0 { + token_id: vec![], + identity_ids: vec![vec![0; 32]], + prove: false, + }; + + let result = platform + .query_identities_token_infos_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::InvalidArgument(msg)] if msg.contains("token_id") + )); + } + + #[test] + fn test_identity_ids_at_max_limit_with_proof_still_proceeds() { + // A full-size batch with prove=true should not hit the size guard. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + let max = version.drive_abci.query.max_returned_elements as usize; + + let request = GetIdentitiesTokenInfosRequestV0 { + token_id: vec![0; 32], + identity_ids: (0..max).map(|i| vec![i as u8; 32]).collect(), + prove: true, + }; + + let result = platform + .query_identities_token_infos_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!( + !result.errors.iter().any(|e| matches!( + e, + QueryError::Query(drive::error::query::QuerySyntaxError::InvalidLimit(_)) + )), + "should not reject at exactly max: {:?}", + result.errors + ); + } } diff --git a/packages/rs-drive-abci/src/query/token_queries/token_direct_purchase_prices/v0/mod.rs b/packages/rs-drive-abci/src/query/token_queries/token_direct_purchase_prices/v0/mod.rs index d82ca1553af..6ad62aa2a38 100644 --- a/packages/rs-drive-abci/src/query/token_queries/token_direct_purchase_prices/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/token_queries/token_direct_purchase_prices/v0/mod.rs @@ -409,4 +409,159 @@ mod tests { }) )); } + + #[test] + fn test_empty_token_ids_with_proof_still_rejected() { + // Empty list is rejected regardless of prove flag — the len check comes + // before any branching. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetTokenDirectPurchasePricesRequestV0 { + token_ids: vec![], + prove: true, + }; + + let result = platform + .query_token_direct_purchase_prices_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::InvalidArgument(msg)] if msg.contains("at least one token id") + )); + } + + #[test] + fn test_one_invalid_token_id_in_list_fails_whole_query() { + // A single malformed id taints the whole list — the try_into call + // short-circuits. + let (platform, state, version, _, token_ids, _) = setup_platform_with_token_state(); + + let request = GetTokenDirectPurchasePricesRequestV0 { + token_ids: vec![token_ids[0].to_vec(), vec![0; 12], token_ids[1].to_vec()], + prove: false, + }; + + let result = platform + .query_token_direct_purchase_prices_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::InvalidArgument(msg)] if msg.contains("token_ids") + )); + } + + #[test] + fn test_mixed_existing_and_unknown_tokens_return_entries() { + // Known token with a fixed price mixed with unknown + no-price tokens. + let (platform, state, version, _, token_ids, _) = setup_platform_with_token_state(); + + let unknown = vec![0xCC; 32]; + + let request = GetTokenDirectPurchasePricesRequestV0 { + token_ids: vec![ + token_ids[0].to_vec(), // no price + token_ids[1].to_vec(), // fixed price 25 + unknown.clone(), // unknown + ], + prove: false, + }; + + let result = platform + .query_token_direct_purchase_prices_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(result.errors.is_empty(), "{:?}", result.errors); + let data = result.data.unwrap(); + match data.result { + Some( + get_token_direct_purchase_prices_response_v0::Result::TokenDirectPurchasePrices( + prices, + ), + ) => { + assert_eq!(prices.token_direct_purchase_price.len(), 3); + let mut saw_fixed = false; + let mut none_count = 0; + for entry in &prices.token_direct_purchase_price { + match &entry.price { + Some(Price::FixedPrice(p)) => { + assert_eq!(*p, 25); + saw_fixed = true; + } + Some(other) => panic!("unexpected price variant: {:?}", other), + None => none_count += 1, + } + } + assert!(saw_fixed, "expected at least one fixed-price entry"); + assert_eq!(none_count, 2, "expected two entries with no price"); + } + other => panic!("expected TokenDirectPurchasePrices result, got {:?}", other), + } + } + + #[test] + fn test_variable_price_preserves_multiple_tiers() { + // Token 2 has SetPrices over multiple quantity tiers (10 tiers: 0..900 + // step 100). Verify the Vec length matches. + let (platform, state, version, _, token_ids, _) = setup_platform_with_token_state(); + + let request = GetTokenDirectPurchasePricesRequestV0 { + token_ids: vec![token_ids[2].to_vec()], + prove: false, + }; + + let result = platform + .query_token_direct_purchase_prices_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(result.errors.is_empty()); + let data = result.data.unwrap(); + match data.result { + Some( + get_token_direct_purchase_prices_response_v0::Result::TokenDirectPurchasePrices( + prices, + ), + ) => { + assert_eq!(prices.token_direct_purchase_price.len(), 1); + match &prices.token_direct_purchase_price[0].price { + Some(Price::VariablePrice(schedule)) => { + // 0, 100, 200, ..., 900 = 10 entries + assert_eq!(schedule.price_for_quantity.len(), 10); + // Sanity: quantity + price always sum to 1000 by setup. + for pfq in &schedule.price_for_quantity { + assert_eq!(pfq.quantity + pfq.price, 1000); + } + } + other => panic!("expected VariablePrice, got {:?}", other), + } + } + _ => panic!("expected TokenDirectPurchasePrices result"), + } + } + + #[test] + fn test_proof_with_mixed_valid_and_unknown_tokens() { + // Prove path with mixed known + unknown tokens should still return a + // Proof variant (unknowns become absence proofs). + let (platform, state, version, _, token_ids, _) = setup_platform_with_token_state(); + + let request = GetTokenDirectPurchasePricesRequestV0 { + token_ids: vec![token_ids[1].to_vec(), vec![0xEE; 32]], + prove: true, + }; + + let result = platform + .query_token_direct_purchase_prices_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(result.errors.is_empty(), "{:?}", result.errors); + let data = result.data.unwrap(); + assert!(matches!( + data.result, + Some(get_token_direct_purchase_prices_response_v0::Result::Proof( + _ + )) + )); + } } diff --git a/packages/rs-drive-abci/src/query/token_queries/token_perpetual_distribution_last_claim/v0/mod.rs b/packages/rs-drive-abci/src/query/token_queries/token_perpetual_distribution_last_claim/v0/mod.rs index 23650327df9..01847e0f782 100644 --- a/packages/rs-drive-abci/src/query/token_queries/token_perpetual_distribution_last_claim/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/token_queries/token_perpetual_distribution_last_claim/v0/mod.rs @@ -415,4 +415,175 @@ mod tests { }) )); } + + #[test] + fn test_invalid_identity_id_with_contract_info() { + // Identity id validation fires before contract_info is touched. + let (platform, state, version, contract_id, token_ids, _) = + setup_platform_with_token_state(); + + let request = GetTokenPerpetualDistributionLastClaimRequestV0 { + token_id: token_ids[0].to_vec(), + contract_info: Some(ContractTokenInfo { + contract_id: contract_id.to_vec(), + token_contract_position: 0, + }), + identity_id: vec![0; 7], + prove: false, + }; + + let result = platform + .query_token_perpetual_distribution_last_claim_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::InvalidArgument(msg)] if msg.contains("identity_id") + )); + } + + #[test] + fn test_invalid_token_id_with_proof() { + // Even with prove=true the token_id check fires first. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetTokenPerpetualDistributionLastClaimRequestV0 { + token_id: vec![0; 31], + contract_info: None, + identity_id: vec![0; 32], + prove: true, + }; + + let result = platform + .query_token_perpetual_distribution_last_claim_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::InvalidArgument(msg)] if msg.contains("token_id") + )); + } + + #[test] + fn test_token_position_at_u16_boundary_not_rejected_for_size() { + // u16::MAX is the *boundary*; only `> u16::MAX as u32` is rejected. + // Passing exactly u16::MAX should *not* hit the + // "token_contract_position must be less than u16::MAX" error. It may + // hit some other InvalidArgument variant (bad position), but not the + // size rejection. + let (platform, state, version, contract_id, token_ids, identity_ids) = + setup_platform_with_token_state(); + + let request = GetTokenPerpetualDistributionLastClaimRequestV0 { + token_id: token_ids[0].to_vec(), + contract_info: Some(ContractTokenInfo { + contract_id: contract_id.to_vec(), + token_contract_position: u16::MAX as u32, + }), + identity_id: identity_ids[0].to_vec(), + prove: false, + }; + + let result = platform + .query_token_perpetual_distribution_last_claim_v0(request, &state, version) + .expect("expected query to succeed"); + + // There must NOT be a "token_contract_position must be less than + // u16::MAX" validation error at the boundary. + let has_size_err = result.errors.iter().any(|e| { + matches!(e, + QueryError::InvalidArgument(msg) + if msg.contains("token_contract_position must be less than") + ) + }); + assert!( + !has_size_err, + "u16::MAX should not trigger the size validation: errors={:?}", + result.errors + ); + } + + #[test] + fn test_nonexistent_token_position_on_existing_contract() { + // Contract exists but requested position does not → handler currently + // surfaces a protocol error via `QueryError::Protocol`. + let (platform, state, version, contract_id, token_ids, identity_ids) = + setup_platform_with_token_state(); + + let request = GetTokenPerpetualDistributionLastClaimRequestV0 { + token_id: token_ids[0].to_vec(), + contract_info: Some(ContractTokenInfo { + contract_id: contract_id.to_vec(), + token_contract_position: 999, + }), + identity_id: identity_ids[0].to_vec(), + prove: false, + }; + + let result = platform + .query_token_perpetual_distribution_last_claim_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!( + !result.errors.is_empty(), + "expected some error for unknown token position" + ); + } + + #[test] + fn test_query_with_contract_info_returns_block_height_paid_at() { + // Happy path with contract_info that *does* have perpetual + // distribution rules: the handler takes the typed-moment branch. + let (platform, state, version, contract_id, token_ids, identity_ids) = + setup_platform_with_token_state(); + + let request = GetTokenPerpetualDistributionLastClaimRequestV0 { + token_id: token_ids[0].to_vec(), + contract_info: Some(ContractTokenInfo { + contract_id: contract_id.to_vec(), + token_contract_position: 0, + }), + identity_id: identity_ids[1].to_vec(), + prove: false, + }; + + let result = platform + .query_token_perpetual_distribution_last_claim_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(result.errors.is_empty(), "{:?}", result.errors); + let data = result.data.unwrap(); + assert!(matches!( + data.result, + Some(get_token_perpetual_distribution_last_claim_response_v0::Result::LastClaim(_)) + )); + assert!(data.metadata.is_some()); + } + + #[test] + fn test_invalid_contract_id_bytes_length_zero() { + // contract_id with zero length should fail the 32-byte identifier + // check inside the contract_info branch. + let (platform, state, version, _, token_ids, identity_ids) = + setup_platform_with_token_state(); + + let request = GetTokenPerpetualDistributionLastClaimRequestV0 { + token_id: token_ids[0].to_vec(), + contract_info: Some(ContractTokenInfo { + contract_id: vec![], + token_contract_position: 0, + }), + identity_id: identity_ids[0].to_vec(), + prove: false, + }; + + let result = platform + .query_token_perpetual_distribution_last_claim_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::InvalidArgument(msg)] if msg.contains("contract_id") + )); + } } diff --git a/packages/rs-drive-abci/src/query/token_queries/token_pre_programmed_distributions/v0/mod.rs b/packages/rs-drive-abci/src/query/token_queries/token_pre_programmed_distributions/v0/mod.rs index 5923925df75..e4d9ce570eb 100644 --- a/packages/rs-drive-abci/src/query/token_queries/token_pre_programmed_distributions/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/token_queries/token_pre_programmed_distributions/v0/mod.rs @@ -460,4 +460,219 @@ mod tests { "unexpected error: {msg}" ); } + + #[test] + fn test_invalid_token_id_zero_length() { + // Completely empty token_id bytes should fail the identifier check. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetTokenPreProgrammedDistributionsRequestV0 { + token_id: vec![], + start_at_info: None, + limit: None, + prove: false, + }; + + let result = platform + .query_token_pre_programmed_distributions_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::InvalidArgument(msg)] if msg.contains("token_id") + )); + } + + #[test] + fn test_query_with_start_at_recipient_included_false() { + // start_recipient supplied + start_recipient_included = Some(false). + // Exercises the "Some(false)" branch of the unwrap_or(true). + let (platform, state, version, _, token_ids, identity_ids) = + setup_platform_with_token_state(); + + let request = GetTokenPreProgrammedDistributionsRequestV0 { + token_id: token_ids[2].to_vec(), + start_at_info: Some(StartAtInfo { + start_time_ms: 1000, + start_recipient: Some(identity_ids[0].to_vec()), + start_recipient_included: Some(false), + }), + limit: None, + prove: false, + }; + + let result = platform + .query_token_pre_programmed_distributions_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(result.errors.is_empty(), "{:?}", result.errors); + let data = result.data.unwrap(); + assert!(matches!( + data.result, + Some(get_token_pre_programmed_distributions_response_v0::Result::TokenDistributions(_)) + )); + } + + #[test] + fn test_query_with_start_at_recipient_included_true() { + // Explicit Some(true) — the default branch is unwrap_or(true) so this + // is the same behavior as passing None; but we want to cover the + // explicit wrapping. + let (platform, state, version, _, token_ids, identity_ids) = + setup_platform_with_token_state(); + + let request = GetTokenPreProgrammedDistributionsRequestV0 { + token_id: token_ids[2].to_vec(), + start_at_info: Some(StartAtInfo { + start_time_ms: 1000, + start_recipient: Some(identity_ids[0].to_vec()), + start_recipient_included: Some(true), + }), + limit: None, + prove: false, + }; + + let result = platform + .query_token_pre_programmed_distributions_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(result.errors.is_empty(), "{:?}", result.errors); + } + + #[test] + fn test_query_with_start_at_recipient_included_none() { + // start_recipient provided but included=None (→ defaults to true). + let (platform, state, version, _, token_ids, identity_ids) = + setup_platform_with_token_state(); + + let request = GetTokenPreProgrammedDistributionsRequestV0 { + token_id: token_ids[2].to_vec(), + start_at_info: Some(StartAtInfo { + start_time_ms: 1000, + start_recipient: Some(identity_ids[0].to_vec()), + start_recipient_included: None, + }), + limit: None, + prove: false, + }; + + let result = platform + .query_token_pre_programmed_distributions_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(result.errors.is_empty(), "{:?}", result.errors); + } + + #[test] + fn test_query_with_proof_and_start_at() { + // Exercises the prove path when start_at is supplied. + let (platform, state, version, _, token_ids, _) = setup_platform_with_token_state(); + + let request = GetTokenPreProgrammedDistributionsRequestV0 { + token_id: token_ids[2].to_vec(), + start_at_info: Some(StartAtInfo { + start_time_ms: 5000, + start_recipient: None, + start_recipient_included: None, + }), + limit: Some(5), + prove: true, + }; + + let result = platform + .query_token_pre_programmed_distributions_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(result.errors.is_empty(), "{:?}", result.errors); + assert!(matches!( + result.data, + Some(GetTokenPreProgrammedDistributionsResponseV0 { + result: Some(get_token_pre_programmed_distributions_response_v0::Result::Proof(_)), + metadata: Some(_), + }) + )); + } + + #[test] + fn test_query_with_start_at_future_returns_empty() { + // Start-time far past any existing distribution → empty result, but + // still success. + let (platform, state, version, _, token_ids, _) = setup_platform_with_token_state(); + + let request = GetTokenPreProgrammedDistributionsRequestV0 { + token_id: token_ids[2].to_vec(), + start_at_info: Some(StartAtInfo { + start_time_ms: u64::MAX, + start_recipient: None, + start_recipient_included: None, + }), + limit: None, + prove: false, + }; + + let result = platform + .query_token_pre_programmed_distributions_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(result.errors.is_empty(), "{:?}", result.errors); + let data = result.data.unwrap(); + match data.result { + Some( + get_token_pre_programmed_distributions_response_v0::Result::TokenDistributions( + dists, + ), + ) => { + assert!(dists.token_distributions.is_empty()); + } + _ => panic!("expected TokenDistributions result"), + } + } + + #[test] + fn test_query_with_proof_and_invalid_token_id() { + // Invalid token id short-circuits before the prove branch. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetTokenPreProgrammedDistributionsRequestV0 { + token_id: vec![1; 31], + start_at_info: None, + limit: None, + prove: true, + }; + + let result = platform + .query_token_pre_programmed_distributions_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::InvalidArgument(msg)] if msg.contains("token_id") + )); + } + + #[test] + fn test_query_with_invalid_start_recipient_with_proof() { + // Invalid start_recipient also fails in the prove path. + let (platform, state, version, _, token_ids, _) = setup_platform_with_token_state(); + + let request = GetTokenPreProgrammedDistributionsRequestV0 { + token_id: token_ids[2].to_vec(), + start_at_info: Some(StartAtInfo { + start_time_ms: 1000, + start_recipient: Some(vec![0; 7]), + start_recipient_included: None, + }), + limit: None, + prove: true, + }; + + let result = platform + .query_token_pre_programmed_distributions_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::InvalidArgument(msg)] if msg.contains("start_recipient") + )); + } } diff --git a/packages/rs-drive-abci/src/query/voting/contested_resource_identity_votes/v0/mod.rs b/packages/rs-drive-abci/src/query/voting/contested_resource_identity_votes/v0/mod.rs index 383965a4197..5190fe26b25 100644 --- a/packages/rs-drive-abci/src/query/voting/contested_resource_identity_votes/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/voting/contested_resource_identity_votes/v0/mod.rs @@ -398,4 +398,181 @@ mod tests { }) if contested_resource_identity_votes.is_empty() && finished_results )); } + + #[test] + fn test_query_contested_resource_identity_votes_invalid_identity_id_zero_length() { + // Zero-length identifier hits the same validation as 8-byte. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetContestedResourceIdentityVotesRequestV0 { + identity_id: vec![], + limit: None, + offset: None, + order_ascending: true, + start_at_vote_poll_id_info: None, + prove: false, + }; + + let result = platform + .query_contested_resource_identity_votes_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::InvalidArgument(msg)] if msg.contains("identity_id") + )); + } + + #[test] + fn test_query_contested_resource_identity_votes_offset_at_u16_max_accepted() { + // offset == u16::MAX is within u16 range; the size guard rejects only + // `> u16::MAX`. This covers the exact boundary. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetContestedResourceIdentityVotesRequestV0 { + identity_id: vec![0; 32], + limit: None, + offset: Some(u16::MAX as u32), + order_ascending: true, + start_at_vote_poll_id_info: None, + prove: false, + }; + + let result = platform + .query_contested_resource_identity_votes_v0(request, &state, version) + .expect("expected query to succeed"); + + // Should not hit the "offset out of bounds" error. + let has_offset_err = result.errors.iter().any(|e| { + matches!( + e, + QueryError::InvalidArgument(msg) if msg.contains("offset out of bounds") + ) + }); + assert!( + !has_offset_err, + "u16::MAX should not trigger offset bounds error: {:?}", + result.errors + ); + } + + #[test] + fn test_query_contested_resource_identity_votes_start_at_invalid_identifier_errors() { + // Invalid bytes for the start_at poll identifier should surface as an + // outer Err because the `?` propagates through a platform_value::Error. + use dapi_grpc::platform::v0::get_contested_resource_identity_votes_request::get_contested_resource_identity_votes_request_v0::StartAtVotePollIdInfo; + + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetContestedResourceIdentityVotesRequestV0 { + identity_id: vec![0; 32], + limit: None, + offset: None, + order_ascending: true, + start_at_vote_poll_id_info: Some(StartAtVotePollIdInfo { + start_at_poll_identifier: vec![0; 7], + start_poll_identifier_included: true, + }), + prove: false, + }; + + let err = platform + .query_contested_resource_identity_votes_v0(request, &state, version) + .expect_err("invalid start_at_poll_identifier should propagate an Err"); + + let msg = format!("{:?}", err); + assert!( + msg.contains("bytes") || msg.to_lowercase().contains("identifier"), + "unexpected error: {msg}" + ); + } + + #[test] + fn test_query_contested_resource_identity_votes_valid_start_at_empty_state() { + // Valid 32-byte start_at poll identifier + empty state → success with + // empty result set. + use dapi_grpc::platform::v0::get_contested_resource_identity_votes_request::get_contested_resource_identity_votes_request_v0::StartAtVotePollIdInfo; + + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetContestedResourceIdentityVotesRequestV0 { + identity_id: vec![0; 32], + limit: None, + offset: None, + order_ascending: true, + start_at_vote_poll_id_info: Some(StartAtVotePollIdInfo { + start_at_poll_identifier: vec![0xAB; 32], + start_poll_identifier_included: false, + }), + prove: false, + }; + + let result = platform + .query_contested_resource_identity_votes_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(result.errors.is_empty(), "{:?}", result.errors); + assert!(matches!( + result.data, + Some(GetContestedResourceIdentityVotesResponseV0 { + result: Some(get_contested_resource_identity_votes_response_v0::Result::Votes(_)), + metadata: Some(_), + }) + )); + } + + #[test] + fn test_query_contested_resource_identity_votes_with_offset_and_descending() { + // offset + descending should still succeed on empty state. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetContestedResourceIdentityVotesRequestV0 { + identity_id: vec![0; 32], + limit: Some(5), + offset: Some(10), + order_ascending: false, + start_at_vote_poll_id_info: None, + prove: false, + }; + + let result = platform + .query_contested_resource_identity_votes_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(result.errors.is_empty(), "{:?}", result.errors); + } + + #[test] + fn test_query_contested_resource_identity_votes_start_at_proof() { + // Prove path with a valid start_at identifier exercises + // `execute_with_proof`'s happy path on empty state. + use dapi_grpc::platform::v0::get_contested_resource_identity_votes_request::get_contested_resource_identity_votes_request_v0::StartAtVotePollIdInfo; + + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetContestedResourceIdentityVotesRequestV0 { + identity_id: vec![0; 32], + limit: None, + offset: None, + order_ascending: true, + start_at_vote_poll_id_info: Some(StartAtVotePollIdInfo { + start_at_poll_identifier: vec![0x55; 32], + start_poll_identifier_included: true, + }), + prove: true, + }; + + let result = platform + .query_contested_resource_identity_votes_v0(request, &state, version) + .expect("expected query to succeed"); + + // An empty vote state can still yield a Proof (absence proof). + assert!(matches!( + result.data, + Some(GetContestedResourceIdentityVotesResponseV0 { + result: Some(get_contested_resource_identity_votes_response_v0::Result::Proof(_)), + metadata: Some(_), + }) + )); + } } From ce83ae66b88b358f34840c53652d0909db26f5cf Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 23 Apr 2026 21:09:40 +0800 Subject: [PATCH 4/5] test(drive-abci): cover platform_events error paths Add 72 tests across 22 submodules of execution/platform_events/ covering error branches, short-circuits, and pure-constructor helpers: - masternode identity helpers (owner/voter/operator key constructors, identity factories, identifier derivation) - block_start/clear_drive_block_cache: idempotency and lock recovery - block_end/update_state_cache: quorum rotation + genesis clearing - block_end/update_checkpoints: disabled/misconfigured short-circuits - block_processing_end_events: shielded-pool pruning/anchor wrappers - core_based_updates/update_core_info: same-height short-circuit - core_instant_send_lock: invalid-signature-format returns Ok(false) - epoch/get_genesis_time: genesis-height echo vs drive lookup - epoch/gather_epoch_info: genesis-path, no-previous-block default - fee_pool_outwards_distribution/fetch_reward_shares_list: empty cases - initialization/initial_core_height: fork-inactive, height-not-locked, genesis-time-in-future, happy path, equality boundary - voting/clean_up_after_vote_polls_end: empty-input no-op branches - voting/remove_votes_for_removed_masternodes: no-diff no-op - voting/run_dao_platform_events: empty-platform success path Tests only; no production code modified. --- .../block_end/update_checkpoints/v0/mod.rs | 113 +++++++++ .../block_end/update_state_cache/v0/mod.rs | 162 +++++++++++++ .../prune_shielded_pool_anchors/v0/mod.rs | 77 ++++++ .../record_shielded_pool_anchor/v0/mod.rs | 68 ++++++ .../clear_drive_block_cache/v0/mod.rs | 47 ++++ .../update_core_info/v0/mod.rs | 94 ++++++++ .../create_operator_identity/v0/mod.rs | 107 +++++++++ .../create_owner_identity/v0/mod.rs | 86 +++++++ .../create_voter_identity/v0/mod.rs | 110 +++++++++ .../get_operator_identifier/v0/mod.rs | 94 ++++++++ .../get_operator_identity_keys/v0/mod.rs | 113 +++++++++ .../get_owner_identity_owner_key/v0/mod.rs | 58 +++++ .../v0/mod.rs | 42 ++++ .../get_voter_identity_key/v0/mod.rs | 44 ++++ .../verify_recent_signature_locally/v0/mod.rs | 94 ++++++++ .../epoch/gather_epoch_info/v0/mod.rs | 133 +++++++++++ .../epoch/get_genesis_time/v0/mod.rs | 110 +++++++++ .../v0/mod.rs | 66 +++++ .../initial_core_height/v0/mod.rs | 226 ++++++++++++++++++ .../clean_up_after_vote_polls_end/v0/mod.rs | 97 ++++++++ .../v0/mod.rs | 75 ++++++ .../voting/run_dao_platform_events/v0/mod.rs | 64 +++++ 22 files changed, 2080 insertions(+) diff --git a/packages/rs-drive-abci/src/execution/platform_events/block_end/update_checkpoints/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/block_end/update_checkpoints/v0/mod.rs index 30188b3f61c..f3b297ce7e0 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/block_end/update_checkpoints/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/block_end/update_checkpoints/v0/mod.rs @@ -89,3 +89,116 @@ where Ok(true) } } + +#[cfg(test)] +mod tests { + use crate::execution::types::block_execution_context::v0::BlockExecutionContextV0; + use crate::execution::types::block_execution_context::BlockExecutionContext; + use crate::execution::types::block_state_info::v0::BlockStateInfoV0; + use crate::execution::types::block_state_info::BlockStateInfo; + use crate::platform_types::epoch_info::v0::EpochInfoV0; + use crate::platform_types::epoch_info::EpochInfo; + use crate::platform_types::withdrawal::unsigned_withdrawal_txs::v0::UnsignedWithdrawalTxs; + use crate::test::helpers::setup::TestPlatformBuilder; + use dpp::version::PlatformVersion; + use std::collections::BTreeMap; + + fn make_context( + platform: &crate::test::helpers::setup::TempPlatform, + ) -> BlockExecutionContext { + let platform_state = platform.state.load(); + let block_platform_state = platform_state.as_ref().clone(); + + BlockExecutionContext::V0(BlockExecutionContextV0 { + block_state_info: BlockStateInfo::V0(BlockStateInfoV0 { + height: 1, + round: 0, + block_time_ms: 1_000_000, + previous_block_time_ms: None, + proposer_pro_tx_hash: [0u8; 32], + core_chain_locked_height: 1, + block_hash: None, + app_hash: None, + }), + epoch_info: EpochInfo::V0(EpochInfoV0 { + current_epoch_index: 0, + previous_epoch_index: None, + is_epoch_change: false, + }), + unsigned_withdrawal_transactions: UnsignedWithdrawalTxs::default(), + block_address_balance_changes: BTreeMap::new(), + block_platform_state, + proposer_results: None, + }) + } + + /// With `disable_checkpoints = true` in testing config, `should_checkpoint` + /// returns `None`, so `update_checkpoints_v0` must early-return `Ok(false)` + /// without creating any checkpoint directory. + #[test] + fn v0_returns_false_when_checkpoints_disabled_in_test_config() { + let platform_version = PlatformVersion::latest(); + let platform_config = crate::config::PlatformConfig { + testing_configs: crate::config::PlatformTestConfig { + disable_checkpoints: true, + ..Default::default() + }, + ..Default::default() + }; + let platform = TestPlatformBuilder::new() + .with_config(platform_config) + .build_with_mock_rpc() + .set_genesis_state(); + + let ctx = make_context(&platform); + let got = platform + .update_checkpoints_v0(&ctx, platform_version) + .expect("must succeed when disabled"); + assert!(!got, "returns false when no checkpoint is needed"); + } + + /// If the platform version reports `should_checkpoint = None`, no checkpoint + /// is ever scheduled — `update_checkpoints_v0` must short-circuit to + /// `Ok(false)` without touching the filesystem. + #[test] + fn v0_returns_false_when_should_checkpoint_version_is_none() { + let platform_version = PlatformVersion::latest(); + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + + let mut modified_version = platform_version.clone(); + modified_version + .drive_abci + .methods + .block_end + .should_checkpoint = None; + + let ctx = make_context(&platform); + let got = platform + .update_checkpoints_v0(&ctx, &modified_version) + .expect("must succeed when should_checkpoint is None"); + assert!(!got); + } + + /// Set `frequency_seconds = 0` — `should_checkpoint` returns `None`, so + /// `update_checkpoints_v0` returns `Ok(false)`. This exercises the + /// misconfiguration-tolerance branch from a higher level than + /// `should_checkpoint`'s own tests. + #[test] + fn v0_returns_false_when_frequency_is_zero() { + let platform_version = PlatformVersion::latest(); + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + + let mut modified_version = platform_version.clone(); + modified_version.drive_abci.checkpoints.frequency_seconds = 0; + + let ctx = make_context(&platform); + let got = platform + .update_checkpoints_v0(&ctx, &modified_version) + .expect("must succeed when frequency is zero"); + assert!(!got); + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/block_end/update_state_cache/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/block_end/update_state_cache/v0/mod.rs index 1ad5218ef04..8e3b8d0e285 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/block_end/update_state_cache/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/block_end/update_state_cache/v0/mod.rs @@ -61,3 +61,165 @@ where Ok(()) } } + +#[cfg(test)] +mod tests { + use crate::test::helpers::setup::TestPlatformBuilder; + use dpp::block::block_info::BlockInfo; + use dpp::block::extended_block_info::v0::ExtendedBlockInfoV0; + use dpp::block::extended_block_info::ExtendedBlockInfo; + use dpp::dashcore::hashes::Hash; + use dpp::dashcore::QuorumHash; + use dpp::version::PlatformVersion; + + use crate::platform_types::platform_state::PlatformStateV0Methods; + + fn make_extended_block_info(height: u64) -> ExtendedBlockInfo { + ExtendedBlockInfo::V0(ExtendedBlockInfoV0 { + basic_info: BlockInfo { + time_ms: 1_000_000, + height, + core_height: 10, + epoch: Default::default(), + }, + app_hash: [1u8; 32], + quorum_hash: [2u8; 32], + block_id_hash: [3u8; 32], + proposer_pro_tx_hash: [4u8; 32], + signature: [5u8; 96], + round: 0, + }) + } + + /// When no `next_validator_set_quorum_hash` is set on `block_platform_state`, + /// the current quorum hash must remain unchanged, and both the genesis info and + /// last committed block info must be updated accordingly. + #[test] + fn v0_no_next_quorum_hash_preserves_current_quorum_and_sets_block_info() { + let platform_version = PlatformVersion::latest(); + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + + let loaded = platform.state.load(); + let mut block_platform_state = loaded.as_ref().clone(); + drop(loaded); + + let original_quorum = QuorumHash::from_byte_array([0x42u8; 32]); + block_platform_state.set_current_validator_set_quorum_hash(original_quorum); + block_platform_state.set_next_validator_set_quorum_hash(None); + + let transaction = platform.drive.grove.start_transaction(); + let extended = make_extended_block_info(7); + + platform + .update_state_cache_v0( + extended, + block_platform_state, + &transaction, + platform_version, + ) + .expect("update_state_cache_v0 must succeed"); + + let state = platform.state.load(); + assert_eq!( + state.current_validator_set_quorum_hash(), + original_quorum, + "current quorum hash must be preserved when next is None" + ); + assert!( + state.last_committed_block_info().is_some(), + "last_committed_block_info must be populated" + ); + assert_eq!( + state.last_committed_block_height(), + 7, + "height must come from the extended block info we passed" + ); + assert!( + state.genesis_block_info().is_none(), + "genesis_block_info must be cleared" + ); + } + + /// When `next_validator_set_quorum_hash` is set, it must be moved to the + /// current quorum hash and cleared from `next`. + #[test] + fn v0_next_quorum_hash_rotates_into_current() { + let platform_version = PlatformVersion::latest(); + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + + let loaded = platform.state.load(); + let mut block_platform_state = loaded.as_ref().clone(); + drop(loaded); + + let old = QuorumHash::from_byte_array([0x11u8; 32]); + let new = QuorumHash::from_byte_array([0x22u8; 32]); + block_platform_state.set_current_validator_set_quorum_hash(old); + block_platform_state.set_next_validator_set_quorum_hash(Some(new)); + + let transaction = platform.drive.grove.start_transaction(); + let extended = make_extended_block_info(42); + + platform + .update_state_cache_v0( + extended, + block_platform_state, + &transaction, + platform_version, + ) + .expect("must succeed"); + + let state = platform.state.load(); + assert_eq!( + state.current_validator_set_quorum_hash(), + new, + "current quorum must become the prior next quorum" + ); + assert!( + state.next_validator_set_quorum_hash().is_none(), + "next quorum must be consumed" + ); + } + + /// `genesis_block_info` is always cleared on update_state_cache, regardless of + /// whether the prior block platform state had one set. + #[test] + fn v0_clears_genesis_block_info() { + let platform_version = PlatformVersion::latest(); + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + + let loaded = platform.state.load(); + let mut block_platform_state = loaded.as_ref().clone(); + drop(loaded); + + // Even if genesis was set, update_state_cache must clear it. + block_platform_state.set_genesis_block_info(Some(BlockInfo { + time_ms: 1, + height: 0, + core_height: 0, + epoch: Default::default(), + })); + + let transaction = platform.drive.grove.start_transaction(); + let extended = make_extended_block_info(1); + + platform + .update_state_cache_v0( + extended, + block_platform_state, + &transaction, + platform_version, + ) + .expect("must succeed"); + + assert!( + platform.state.load().genesis_block_info().is_none(), + "genesis_block_info must always be cleared" + ); + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/prune_shielded_pool_anchors/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/prune_shielded_pool_anchors/v0/mod.rs index f6dfcdc6d63..2ee3e913e08 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/prune_shielded_pool_anchors/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/prune_shielded_pool_anchors/v0/mod.rs @@ -42,3 +42,80 @@ where .map_err(Error::Drive) } } + +#[cfg(test)] +mod tests { + use crate::test::helpers::setup::TestPlatformBuilder; + use dpp::version::PlatformVersion; + + /// When `block_height` is not a multiple of `pruning_interval`, the method + /// must return `Ok(())` without touching storage. + #[test] + fn v0_noop_when_not_on_pruning_interval_boundary() { + let platform_version = PlatformVersion::latest(); + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + let transaction = platform.drive.grove.start_transaction(); + + // Pick a height that is guaranteed not to be a multiple of any realistic interval >= 2: + // height = 1 is only a multiple of 1, so for intervals >= 2 this early-returns. + // If `shielded_anchor_pruning_interval` is 1 in this version, the branch is + // not exercised — that is acceptable; the call must still succeed. + platform + .prune_shielded_pool_anchors_v0(1, &transaction, platform_version) + .expect("prune must succeed at height 1"); + } + + /// When `block_height <= retention_blocks`, the method returns early (no cutoff + /// to compute). We pick `block_height = 0` which always satisfies this for + /// any non-zero retention value. + #[test] + fn v0_noop_when_below_retention_depth() { + let platform_version = PlatformVersion::latest(); + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + let transaction = platform.drive.grove.start_transaction(); + + platform + .prune_shielded_pool_anchors_v0(0, &transaction, platform_version) + .expect("prune must succeed at height 0"); + } + + /// Calling the pruner with a very large block height on an empty state should + /// not fail — there is simply nothing to remove, and the delegated drive + /// method is expected to treat this as a no-op. + #[test] + fn v0_large_height_on_empty_state_is_ok() { + let platform_version = PlatformVersion::latest(); + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + let transaction = platform.drive.grove.start_transaction(); + + // Choose a height that is almost certainly a multiple of the pruning + // interval regardless of version: a power-of-two large enough that, + // combined with retention, the cutoff is positive. + platform + .prune_shielded_pool_anchors_v0(1_048_576, &transaction, platform_version) + .expect("prune must succeed on empty state at large height"); + } + + /// Pruning is idempotent on an empty state: calling it twice must not panic + /// or return an error. + #[test] + fn v0_idempotent_on_empty_state() { + let platform_version = PlatformVersion::latest(); + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + let transaction = platform.drive.grove.start_transaction(); + + for _ in 0..3 { + platform + .prune_shielded_pool_anchors_v0(1_048_576, &transaction, platform_version) + .expect("prune must be idempotent"); + } + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/record_shielded_pool_anchor/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/record_shielded_pool_anchor/v0/mod.rs index b5c800d2f72..83d80e18af2 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/record_shielded_pool_anchor/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/record_shielded_pool_anchor/v0/mod.rs @@ -25,3 +25,71 @@ where .map_err(Error::Drive) } } + +#[cfg(test)] +mod tests { + use crate::test::helpers::setup::TestPlatformBuilder; + use dpp::version::PlatformVersion; + + /// On a fresh genesis state with no shielded pool activity, calling + /// `record_shielded_pool_anchor_if_changed_v0` must be a no-op that succeeds — + /// the drive helper must detect that the commitment tree did not change. + #[test] + fn v0_ok_on_fresh_state() { + let platform_version = PlatformVersion::latest(); + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + let transaction = platform.drive.grove.start_transaction(); + + platform + .record_shielded_pool_anchor_if_changed_v0(1, &transaction, platform_version) + .expect("must succeed on fresh state"); + } + + /// The wrapper must be idempotent — calling it repeatedly with the same height + /// (or varied heights) on an unchanging commitment tree must not fail. + #[test] + fn v0_idempotent_across_heights_when_tree_unchanged() { + let platform_version = PlatformVersion::latest(); + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + let transaction = platform.drive.grove.start_transaction(); + + for h in [1u64, 2, 3, 100, 1_000_000] { + platform + .record_shielded_pool_anchor_if_changed_v0(h, &transaction, platform_version) + .expect("must be idempotent when tree unchanged"); + } + } + + /// Height = 0 (genesis) must be accepted without panic/error. + #[test] + fn v0_accepts_height_zero() { + let platform_version = PlatformVersion::latest(); + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + let transaction = platform.drive.grove.start_transaction(); + + platform + .record_shielded_pool_anchor_if_changed_v0(0, &transaction, platform_version) + .expect("must accept block_height = 0"); + } + + /// `u64::MAX` as block height must not overflow inside the wrapper (which + /// does no arithmetic of its own). + #[test] + fn v0_accepts_max_height() { + let platform_version = PlatformVersion::latest(); + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + let transaction = platform.drive.grove.start_transaction(); + + platform + .record_shielded_pool_anchor_if_changed_v0(u64::MAX, &transaction, platform_version) + .expect("must accept u64::MAX without overflow"); + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/block_start/clear_drive_block_cache/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/block_start/clear_drive_block_cache/v0/mod.rs index ecf723fa2ad..cab42fced01 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/block_start/clear_drive_block_cache/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/block_start/clear_drive_block_cache/v0/mod.rs @@ -20,3 +20,50 @@ where protocol_versions_counter.unblock_global_cache(); } } + +#[cfg(test)] +mod tests { + use crate::test::helpers::setup::TestPlatformBuilder; + + /// `clear_drive_block_cache_v0` on a fresh platform must succeed without panicking, + /// even when the caches are empty and the global cache is already unblocked. + #[test] + fn v0_noop_on_fresh_platform() { + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + + platform.clear_drive_block_cache_v0(); + } + + /// The operation is idempotent: calling it multiple times in a row must not + /// panic or leave the cache in a poisoned state. + #[test] + fn v0_idempotent_across_repeated_calls() { + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + + for _ in 0..5 { + platform.clear_drive_block_cache_v0(); + } + } + + /// After `clear_drive_block_cache_v0`, the protocol_versions_counter's + /// global cache must be unblocked (ready for reads). We exercise this by + /// calling the method and then confirming a subsequent call continues to + /// succeed — which it can't do if the write lock became poisoned. + #[test] + fn v0_leaves_cache_usable_after_call() { + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + + platform.clear_drive_block_cache_v0(); + // If the write lock were poisoned the second call would panic. + platform.clear_drive_block_cache_v0(); + + // Confirm we can still take a read lock on the counter afterwards. + let _counter = platform.drive.cache.protocol_versions_counter.read(); + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_core_info/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_core_info/v0/mod.rs index 550fac49db6..b78a0bc141a 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_core_info/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_core_info/v0/mod.rs @@ -68,3 +68,97 @@ where ) } } + +#[cfg(test)] +mod tests { + use crate::platform_types::platform_state::PlatformStateV0Methods; + use crate::test::helpers::setup::TestPlatformBuilder; + use dpp::block::block_info::BlockInfo; + use dpp::version::PlatformVersion; + + /// When `is_init_chain = false` and the current block state already has the + /// same core height, `update_core_info_v0` must early-return `Ok(())` without + /// calling Core RPC. This is critical because skipping this branch would + /// fire RPC calls we haven't set up in the mock. + #[test] + fn v0_returns_ok_when_same_core_height_and_not_init() { + let platform_version = PlatformVersion::latest(); + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + + let state = platform.state.load(); + let mut block_platform_state = state.as_ref().clone(); + drop(state); + + // Read the already-committed core height off the block state — the v0 + // path early-returns only if we pass the SAME value as `core_block_height`. + let same_core_height = block_platform_state.last_committed_core_height(); + + let transaction = platform.drive.grove.start_transaction(); + let block_info = BlockInfo { + time_ms: 0, + height: 1, + core_height: same_core_height, + epoch: Default::default(), + }; + + platform + .update_core_info_v0( + None, + &mut block_platform_state, + same_core_height, + /*is_init_chain=*/ false, + &block_info, + &transaction, + platform_version, + ) + .expect("same-height non-init must short-circuit OK"); + } + + /// Even with the same core height, `is_init_chain = true` must bypass the + /// early return — but since we can't easily mock the downstream RPC, we + /// only assert the path is reachable (it will try to call core_rpc and + /// panic in the mock; we catch only the short-circuit case above). + /// This test simply confirms the same-height-short-circuit is governed + /// ONLY by `is_init_chain = false`: setting it to `true` with matching + /// heights must NOT early-return — which we observe indirectly by the + /// fact that the same-height non-init test above passes without touching + /// the mock RPC at all. + #[test] + fn v0_short_circuit_is_tied_to_is_init_chain_false() { + // Re-confirm that both conditions together are required by running + // the short-circuit case twice; idempotency guards against flakiness. + let platform_version = PlatformVersion::latest(); + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + + let state = platform.state.load(); + let mut block_platform_state = state.as_ref().clone(); + drop(state); + + let same_core_height = block_platform_state.last_committed_core_height(); + let transaction = platform.drive.grove.start_transaction(); + let block_info = BlockInfo { + time_ms: 0, + height: 1, + core_height: same_core_height, + epoch: Default::default(), + }; + + for _ in 0..2 { + platform + .update_core_info_v0( + None, + &mut block_platform_state, + same_core_height, + false, + &block_info, + &transaction, + platform_version, + ) + .expect("idempotent short-circuit"); + } + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_operator_identity/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_operator_identity/v0/mod.rs index ca6f25d6718..8e9be14377c 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_operator_identity/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_operator_identity/v0/mod.rs @@ -28,3 +28,110 @@ where Ok(identity) } } + +#[cfg(test)] +mod tests { + use crate::platform_types::platform::Platform; + use crate::rpc::core::MockCoreRPCLike; + use dpp::dashcore::hashes::Hash; + use dpp::dashcore::{ProTxHash, Txid}; + use dpp::dashcore_rpc::dashcore_rpc_json::{DMNState, MasternodeListItem, MasternodeType}; + use dpp::identity::accessors::IdentityGettersV0; + use dpp::version::PlatformVersion; + use std::net::SocketAddr; + use std::str::FromStr; + + fn make_masternode( + pro_tx: [u8; 32], + operator_payout_address: Option<[u8; 20]>, + platform_node_id: Option<[u8; 20]>, + ) -> MasternodeListItem { + MasternodeListItem { + node_type: MasternodeType::Regular, + pro_tx_hash: ProTxHash::from_byte_array(pro_tx), + collateral_hash: Txid::from_byte_array([0u8; 32]), + collateral_index: 0, + collateral_address: [0u8; 20], + operator_reward: 0.0, + state: DMNState { + service: SocketAddr::from_str("1.2.3.4:1234").unwrap(), + registered_height: 0, + pose_revived_height: None, + pose_ban_height: None, + revocation_reason: 0, + owner_address: [0u8; 20], + voting_address: [0u8; 20], + payout_address: [0u8; 20], + pub_key_operator: vec![1u8; 48], + operator_payout_address, + platform_node_id, + platform_p2p_port: None, + platform_http_port: None, + }, + } + } + + /// Operator identity with neither optional address carries exactly one key + /// (the BLS12_381 operator key). + #[test] + fn v0_operator_identity_minimal_one_key() { + let platform_version = PlatformVersion::latest(); + let mn = make_masternode([0x10u8; 32], None, None); + + let identity = + Platform::::create_operator_identity_v0(&mn, platform_version) + .expect("must succeed"); + + assert_eq!(identity.public_keys().len(), 1); + } + + /// With both `operator_payout_address` and `platform_node_id` set, operator + /// identity has exactly three keys. + #[test] + fn v0_operator_identity_full_three_keys() { + let platform_version = PlatformVersion::latest(); + let mn = make_masternode([0x20u8; 32], Some([0xaau8; 20]), Some([0xbbu8; 20])); + + let identity = + Platform::::create_operator_identity_v0(&mn, platform_version) + .expect("must succeed"); + assert_eq!(identity.public_keys().len(), 3); + } + + /// Different pro_tx_hash values must produce different operator identity ids + /// even if other MN fields are identical. + #[test] + fn v0_distinct_pro_tx_hashes_yield_distinct_operator_identities() { + let platform_version = PlatformVersion::latest(); + let a = make_masternode([0x01u8; 32], None, None); + let b = make_masternode([0x02u8; 32], None, None); + + let ia = Platform::::create_operator_identity_v0(&a, platform_version) + .expect("a"); + let ib = Platform::::create_operator_identity_v0(&b, platform_version) + .expect("b"); + assert_ne!(ia.id(), ib.id()); + } + + /// Only payout_address set → 2 keys (operator + transfer). + #[test] + fn v0_operator_identity_with_only_payout() { + let platform_version = PlatformVersion::latest(); + let mn = make_masternode([0x30u8; 32], Some([0xccu8; 20]), None); + let identity = + Platform::::create_operator_identity_v0(&mn, platform_version) + .expect("must succeed"); + assert_eq!(identity.public_keys().len(), 2); + } + + /// Only platform_node_id set → 2 keys (operator + node). + #[test] + fn v0_operator_identity_with_only_node_id() { + let platform_version = PlatformVersion::latest(); + let mn = make_masternode([0x40u8; 32], None, Some([0xddu8; 20])); + let identity = + Platform::::create_operator_identity_v0(&mn, platform_version) + .expect("must succeed"); + assert_eq!(identity.public_keys().len(), 2); + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/v0/mod.rs index 497ec67d267..503fe5ac07c 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_owner_identity/v0/mod.rs @@ -32,3 +32,89 @@ where Ok(masternode_identifier.into()) } } + +#[cfg(test)] +mod tests { + use crate::platform_types::platform::Platform; + use crate::rpc::core::MockCoreRPCLike; + use dpp::dashcore::hashes::Hash; + use dpp::dashcore::{ProTxHash, Txid}; + use dpp::dashcore_rpc::dashcore_rpc_json::{DMNState, MasternodeListItem, MasternodeType}; + use dpp::identity::accessors::IdentityGettersV0; + use dpp::prelude::Identifier; + use dpp::version::PlatformVersion; + use std::net::SocketAddr; + use std::str::FromStr; + + fn make_masternode(pro_tx: [u8; 32], payout_address: [u8; 20]) -> MasternodeListItem { + MasternodeListItem { + node_type: MasternodeType::Regular, + pro_tx_hash: ProTxHash::from_byte_array(pro_tx), + collateral_hash: Txid::from_byte_array([0u8; 32]), + collateral_index: 0, + collateral_address: [0u8; 20], + operator_reward: 0.0, + state: DMNState { + service: SocketAddr::from_str("1.2.3.4:1234").unwrap(), + registered_height: 0, + pose_revived_height: None, + pose_ban_height: None, + revocation_reason: 0, + owner_address: [0u8; 20], + voting_address: [0u8; 20], + payout_address, + pub_key_operator: vec![0u8; 48], + operator_payout_address: None, + platform_node_id: None, + platform_p2p_port: None, + platform_http_port: None, + }, + } + } + + /// v0 owner identity id is the pro_tx_hash cast to Identifier. Any refactor that + /// alters this contract (e.g. hashing the pro_tx) would break the identity mapping. + #[test] + fn v0_owner_identifier_equals_pro_tx_hash() { + let pro_tx = [0x33u8; 32]; + let mn = make_masternode(pro_tx, [0u8; 20]); + + let id = Platform::::get_owner_identifier(&mn) + .expect("owner identifier must not fail"); + let expected: Identifier = pro_tx.into(); + assert_eq!(id, expected); + } + + /// v0 owner identity carries exactly one identity public key (the withdrawal key), + /// and its id must match `get_owner_identifier`. + #[test] + fn v0_create_owner_identity_has_single_key_and_matches_identifier() { + let platform_version = PlatformVersion::latest(); + let pro_tx = [0xabu8; 32]; + let mn = make_masternode(pro_tx, [0x12u8; 20]); + + let identity = Platform::::create_owner_identity_v0(&mn, platform_version) + .expect("create_owner_identity_v0 must succeed"); + + assert_eq!(identity.id(), Identifier::from(pro_tx)); + assert_eq!( + identity.public_keys().len(), + 1, + "owner identity has exactly one key" + ); + } + + /// Different pro_tx_hash must produce different owner identity ids. + #[test] + fn v0_distinct_pro_tx_hashes_yield_distinct_owner_identities() { + let platform_version = PlatformVersion::latest(); + let a = make_masternode([0x01u8; 32], [0u8; 20]); + let b = make_masternode([0x02u8; 32], [0u8; 20]); + + let ia = + Platform::::create_owner_identity_v0(&a, platform_version).expect("a"); + let ib = + Platform::::create_owner_identity_v0(&b, platform_version).expect("b"); + assert_ne!(ia.id(), ib.id()); + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_voter_identity/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_voter_identity/v0/mod.rs index ddb7fd05aaf..b5a1b148cbd 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_voter_identity/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/create_voter_identity/v0/mod.rs @@ -39,3 +39,113 @@ where ) } } + +#[cfg(test)] +mod tests { + use crate::platform_types::platform::Platform; + use crate::rpc::core::MockCoreRPCLike; + use dpp::dashcore::hashes::Hash; + use dpp::dashcore::{ProTxHash, Txid}; + use dpp::dashcore_rpc::dashcore_rpc_json::{DMNState, MasternodeListItem, MasternodeType}; + use dpp::identifier::MasternodeIdentifiers; + use dpp::identity::accessors::IdentityGettersV0; + use dpp::prelude::Identifier; + use dpp::version::PlatformVersion; + use std::net::SocketAddr; + use std::str::FromStr; + + fn make_masternode(pro_tx: [u8; 32], voting_address: [u8; 20]) -> MasternodeListItem { + MasternodeListItem { + node_type: MasternodeType::Regular, + pro_tx_hash: ProTxHash::from_byte_array(pro_tx), + collateral_hash: Txid::from_byte_array([0u8; 32]), + collateral_index: 0, + collateral_address: [0u8; 20], + operator_reward: 0.0, + state: DMNState { + service: SocketAddr::from_str("1.2.3.4:1234").unwrap(), + registered_height: 0, + pose_revived_height: None, + pose_ban_height: None, + revocation_reason: 0, + owner_address: [0u8; 20], + voting_address, + payout_address: [0u8; 20], + pub_key_operator: vec![0u8; 48], + operator_payout_address: None, + platform_node_id: None, + platform_p2p_port: None, + platform_http_port: None, + }, + } + } + + /// v0 voter identity id must equal `create_voter_identifier(pro_tx, voting_key)`. + /// It must carry exactly one identity public key (the voter key). + #[test] + fn v0_voter_identity_id_matches_voter_identifier() { + let platform_version = PlatformVersion::latest(); + let pro_tx = [0x42u8; 32]; + let voting_key = [0x21u8; 20]; + + let identity = Platform::::create_voter_identity_v0( + &pro_tx, + &voting_key, + platform_version, + ) + .expect("v0 must succeed on valid inputs"); + + let expected_id: Identifier = Identifier::create_voter_identifier(&pro_tx, &voting_key); + assert_eq!(identity.id(), expected_id); + assert_eq!( + identity.public_keys().len(), + 1, + "voter identity has exactly one key" + ); + } + + /// The `from_masternode_list_item` flavor must derive the same identity as the + /// direct constructor applied to pro_tx_hash + voting_address from the item. + #[test] + fn v0_from_masternode_list_item_agrees_with_direct_constructor() { + let platform_version = PlatformVersion::latest(); + let pro_tx = [0x77u8; 32]; + let voting_key = [0x55u8; 20]; + let mn = make_masternode(pro_tx, voting_key); + + let direct = Platform::::create_voter_identity_v0( + &pro_tx, + &voting_key, + platform_version, + ) + .expect("direct"); + let via_mn = + Platform::::create_voter_identity_from_masternode_list_item_v0( + &mn, + platform_version, + ) + .expect("via mn"); + + assert_eq!(direct.id(), via_mn.id()); + } + + /// Changing the voting key must change the resulting identifier. + #[test] + fn v0_distinct_voting_keys_yield_distinct_identities() { + let platform_version = PlatformVersion::latest(); + let pro_tx = [0x01u8; 32]; + let a = Platform::::create_voter_identity_v0( + &pro_tx, + &[0xaau8; 20], + platform_version, + ) + .expect("a"); + let b = Platform::::create_voter_identity_v0( + &pro_tx, + &[0xbbu8; 20], + platform_version, + ) + .expect("b"); + assert_ne!(a.id(), b.id()); + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/get_operator_identifier/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/get_operator_identifier/v0/mod.rs index 3ba6c6fb519..8cb6c517ca9 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/get_operator_identifier/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/get_operator_identifier/v0/mod.rs @@ -18,3 +18,97 @@ where ) } } + +#[cfg(test)] +mod tests { + use crate::platform_types::platform::Platform; + use crate::rpc::core::MockCoreRPCLike; + use dpp::dashcore::hashes::Hash; + use dpp::dashcore::{ProTxHash, Txid}; + use dpp::dashcore_rpc::dashcore_rpc_json::{DMNState, MasternodeListItem, MasternodeType}; + use dpp::identifier::MasternodeIdentifiers; + use dpp::prelude::Identifier; + use std::net::SocketAddr; + use std::str::FromStr; + + fn make_masternode(pro_tx: [u8; 32], pub_key_operator: Vec) -> MasternodeListItem { + MasternodeListItem { + node_type: MasternodeType::Regular, + pro_tx_hash: ProTxHash::from_byte_array(pro_tx), + collateral_hash: Txid::from_byte_array([0u8; 32]), + collateral_index: 0, + collateral_address: [0u8; 20], + operator_reward: 0.0, + state: DMNState { + service: SocketAddr::from_str("1.2.3.4:1234").unwrap(), + registered_height: 0, + pose_revived_height: None, + pose_ban_height: None, + revocation_reason: 0, + owner_address: [0u8; 20], + voting_address: [0u8; 20], + payout_address: [0u8; 20], + pub_key_operator, + operator_payout_address: None, + platform_node_id: None, + platform_p2p_port: None, + platform_http_port: None, + }, + } + } + + /// v0 operator identifier is `create_operator_identifier(pro_tx_hash, pub_key_operator)`. + /// This test pins the contract so any silent drift (e.g. swapping args) is caught. + #[test] + fn v0_matches_create_operator_identifier() { + let pro_tx = [0x22u8; 32]; + let pub_key_operator = vec![0x33u8; 48]; + let mn = make_masternode(pro_tx, pub_key_operator.clone()); + + let got = + Platform::::get_operator_identifier_from_masternode_list_item_v0(&mn); + let expected = Identifier::create_operator_identifier(&pro_tx, pub_key_operator.as_slice()); + assert_eq!(got, expected); + } + + /// Different operator public keys with the same pro_tx_hash must produce + /// different identifiers (otherwise key rotation would collide). + #[test] + fn v0_distinct_pub_keys_produce_distinct_identifiers() { + let pro_tx = [0x11u8; 32]; + let a = make_masternode(pro_tx, vec![0xaau8; 48]); + let b = make_masternode(pro_tx, vec![0xbbu8; 48]); + + let id_a = + Platform::::get_operator_identifier_from_masternode_list_item_v0(&a); + let id_b = + Platform::::get_operator_identifier_from_masternode_list_item_v0(&b); + assert_ne!(id_a, id_b); + } + + /// Different pro_tx_hash with the same operator key must also produce different + /// identifiers (otherwise two distinct masternodes would alias). + #[test] + fn v0_distinct_pro_tx_hashes_produce_distinct_identifiers() { + let pub_key = vec![0xccu8; 48]; + let a = make_masternode([0x01u8; 32], pub_key.clone()); + let b = make_masternode([0x02u8; 32], pub_key); + + let id_a = + Platform::::get_operator_identifier_from_masternode_list_item_v0(&a); + let id_b = + Platform::::get_operator_identifier_from_masternode_list_item_v0(&b); + assert_ne!(id_a, id_b); + } + + /// Deterministic: same inputs must always produce the same identifier. + #[test] + fn v0_is_deterministic() { + let mn = make_masternode([0x99u8; 32], vec![0x88u8; 48]); + let first = + Platform::::get_operator_identifier_from_masternode_list_item_v0(&mn); + let second = + Platform::::get_operator_identifier_from_masternode_list_item_v0(&mn); + assert_eq!(first, second); + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/get_operator_identity_keys/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/get_operator_identity_keys/v0/mod.rs index ff3a8899500..99e2917a20a 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/get_operator_identity_keys/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/get_operator_identity_keys/v0/mod.rs @@ -59,3 +59,116 @@ where Ok(identity_public_keys) } } + +#[cfg(test)] +mod tests { + use crate::platform_types::platform::Platform; + use crate::rpc::core::MockCoreRPCLike; + use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; + use dpp::identity::{KeyType, Purpose, SecurityLevel}; + + /// When both `operator_payout_address` and `platform_node_id` are `None`, the + /// function must return exactly one key: the BLS12_381 SYSTEM CRITICAL operator key. + #[test] + fn v0_only_operator_key_when_no_optionals() { + let pub_key_operator = vec![1u8; 48]; + let keys = Platform::::get_operator_identity_keys_v0( + pub_key_operator.clone(), + None, + None, + ) + .expect("must succeed"); + + assert_eq!(keys.len(), 1, "exactly 1 key when both optionals are None"); + assert_eq!(keys[0].id(), 0); + assert_eq!(keys[0].key_type(), KeyType::BLS12_381); + assert_eq!(keys[0].purpose(), Purpose::SYSTEM); + assert_eq!(keys[0].security_level(), SecurityLevel::CRITICAL); + assert!(keys[0].read_only()); + assert_eq!(keys[0].data().as_slice(), pub_key_operator.as_slice()); + } + + /// With only `operator_payout_address` set, return 2 keys: the operator key and + /// a TRANSFER ECDSA_HASH160 key at id=1 carrying the payout address. + #[test] + fn v0_two_keys_with_only_payout_address() { + let pub_key_operator = vec![2u8; 48]; + let payout = [7u8; 20]; + let keys = Platform::::get_operator_identity_keys_v0( + pub_key_operator, + Some(payout), + None, + ) + .expect("must succeed"); + + assert_eq!(keys.len(), 2); + assert_eq!(keys[1].id(), 1); + assert_eq!(keys[1].key_type(), KeyType::ECDSA_HASH160); + assert_eq!(keys[1].purpose(), Purpose::TRANSFER); + assert_eq!(keys[1].security_level(), SecurityLevel::CRITICAL); + assert!(keys[1].read_only()); + assert_eq!(keys[1].data().as_slice(), &payout); + } + + /// With only `platform_node_id` set, return 2 keys: the operator key and + /// an EDDSA_25519_HASH160 SYSTEM CRITICAL key at id=2 carrying the node_id. + /// Note: key id 2 is used even though id 1 is empty — ids are position-based in the schema. + #[test] + fn v0_two_keys_with_only_node_id() { + let pub_key_operator = vec![3u8; 48]; + let node_id = [4u8; 20]; + let keys = Platform::::get_operator_identity_keys_v0( + pub_key_operator, + None, + Some(node_id), + ) + .expect("must succeed"); + + assert_eq!(keys.len(), 2); + assert_eq!( + keys[1].id(), + 2, + "node_id key must be id=2 even when payout is absent" + ); + assert_eq!(keys[1].key_type(), KeyType::EDDSA_25519_HASH160); + assert_eq!(keys[1].purpose(), Purpose::SYSTEM); + assert_eq!(keys[1].security_level(), SecurityLevel::CRITICAL); + assert_eq!(keys[1].data().as_slice(), &node_id); + } + + /// With both optionals set, return 3 keys: operator (id=0), payout (id=1), node_id (id=2). + #[test] + fn v0_three_keys_when_both_optionals_present() { + let pub_key_operator = vec![4u8; 48]; + let payout = [5u8; 20]; + let node_id = [6u8; 20]; + let keys = Platform::::get_operator_identity_keys_v0( + pub_key_operator.clone(), + Some(payout), + Some(node_id), + ) + .expect("must succeed"); + + assert_eq!(keys.len(), 3); + assert_eq!(keys[0].id(), 0); + assert_eq!(keys[1].id(), 1); + assert_eq!(keys[2].id(), 2); + assert_eq!(keys[0].data().as_slice(), pub_key_operator.as_slice()); + assert_eq!(keys[1].data().as_slice(), &payout); + assert_eq!(keys[2].data().as_slice(), &node_id); + } + + /// The function accepts `pub_key_operator` of any length and stores it verbatim + /// (no length validation in this v0 helper). This guards the current contract. + #[test] + fn v0_operator_pub_key_stored_verbatim_including_unusual_length() { + let pub_key_operator = vec![9u8; 10]; // not a real BLS key length + let keys = Platform::::get_operator_identity_keys_v0( + pub_key_operator.clone(), + None, + None, + ) + .expect("helper does not validate key length"); + assert_eq!(keys[0].data().as_slice(), pub_key_operator.as_slice()); + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/get_owner_identity_owner_key/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/get_owner_identity_owner_key/v0/mod.rs index ba099e99ed1..1f574a260c7 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/get_owner_identity_owner_key/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/get_owner_identity_owner_key/v0/mod.rs @@ -22,3 +22,61 @@ impl Platform { .into()) } } + +#[cfg(test)] +mod tests { + use crate::platform_types::platform::Platform; + use crate::rpc::core::MockCoreRPCLike; + use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; + use dpp::identity::{KeyType, Purpose, SecurityLevel}; + + /// The owner key is an ECDSA_HASH160 OWNER key with CRITICAL security, read-only, + /// carrying the provided 20-byte address as its data. This helper is a pure constructor + /// — any drift in the hard-coded key fields must be caught here. + #[test] + fn v0_builds_read_only_critical_owner_ecdsa_hash160_key() { + let address = [7u8; 20]; + let key_id = 5u32; + + let key = Platform::::get_owner_identity_owner_key_v0(address, key_id) + .expect("v0 constructor must not fail for a well-formed address"); + + assert_eq!(key.id(), key_id); + assert_eq!(key.key_type(), KeyType::ECDSA_HASH160); + assert_eq!(key.purpose(), Purpose::OWNER); + assert_eq!(key.security_level(), SecurityLevel::CRITICAL); + assert!(key.read_only(), "owner key must be read-only"); + assert!( + key.disabled_at().is_none(), + "owner key must not be disabled at construction" + ); + assert!( + key.contract_bounds().is_none(), + "owner key must have no contract bounds" + ); + assert_eq!(key.data().as_slice(), &address); + } + + /// The key's `id` field is threaded through unchanged; test with a couple of + /// extreme/representative values to guard the assignment line. + #[test] + fn v0_threads_key_id_through_for_multiple_values() { + let address = [0u8; 20]; + for key_id in [0u32, 1u32, u32::MAX] { + let key = Platform::::get_owner_identity_owner_key_v0(address, key_id) + .expect("v0 constructor must not fail"); + assert_eq!(key.id(), key_id); + } + } + + /// Distinct addresses must produce distinct key data. Guards against any + /// accidental aliasing or forgotten copy. + #[test] + fn v0_distinct_addresses_produce_distinct_keys() { + let addr_a = [0x11u8; 20]; + let addr_b = [0x22u8; 20]; + let a = Platform::::get_owner_identity_owner_key_v0(addr_a, 0).expect("a"); + let b = Platform::::get_owner_identity_owner_key_v0(addr_b, 0).expect("b"); + assert_ne!(a.data().as_slice(), b.data().as_slice()); + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/get_owner_identity_withdrawal_key/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/get_owner_identity_withdrawal_key/v0/mod.rs index cec144d34a5..1dd9bf3c26e 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/get_owner_identity_withdrawal_key/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/get_owner_identity_withdrawal_key/v0/mod.rs @@ -26,3 +26,45 @@ where .into()) } } + +#[cfg(test)] +mod tests { + use crate::platform_types::platform::Platform; + use crate::rpc::core::MockCoreRPCLike; + use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; + use dpp::identity::{KeyType, Purpose, SecurityLevel}; + + /// Withdrawal key is a TRANSFER purpose, CRITICAL, read-only ECDSA_HASH160 key + /// pinned to the payout address. Any drift in the key constants should fail this test. + #[test] + fn v0_builds_transfer_critical_ecdsa_hash160_key() { + let payout_address = [3u8; 20]; + let key_id = 11u32; + + let key = Platform::::get_owner_identity_withdrawal_key_v0( + payout_address, + key_id, + ) + .expect("v0 constructor must not fail"); + + assert_eq!(key.id(), key_id); + assert_eq!(key.key_type(), KeyType::ECDSA_HASH160); + assert_eq!(key.purpose(), Purpose::TRANSFER); + assert_eq!(key.security_level(), SecurityLevel::CRITICAL); + assert!(key.read_only()); + assert!(key.disabled_at().is_none()); + assert!(key.contract_bounds().is_none()); + assert_eq!(key.data().as_slice(), &payout_address); + } + + /// Zero-initialized payout address (valid 20-byte value) must be accepted without + /// panic — the constructor is just a pure data shaping helper. + #[test] + fn v0_accepts_zero_payout_address() { + let payout_address = [0u8; 20]; + let key = + Platform::::get_owner_identity_withdrawal_key_v0(payout_address, 0) + .expect("zero address must be accepted"); + assert_eq!(key.data().as_slice(), &payout_address); + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/get_voter_identity_key/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/get_voter_identity_key/v0/mod.rs index 28246bb4ebd..6a3178b48d7 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/get_voter_identity_key/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/get_voter_identity_key/v0/mod.rs @@ -26,3 +26,47 @@ where .into()) } } + +#[cfg(test)] +mod tests { + use crate::platform_types::platform::Platform; + use crate::rpc::core::MockCoreRPCLike; + use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; + use dpp::identity::{KeyType, Purpose, SecurityLevel}; + + /// Voter key is VOTING purpose, HIGH security (NOT CRITICAL, distinguishing + /// it from owner/withdrawal), read-only, ECDSA_HASH160. Any of those constants + /// changing silently would break the identity-key schema. + #[test] + fn v0_builds_voting_high_ecdsa_hash160_key() { + let voting_address = [9u8; 20]; + let key_id = 0u32; + + let key = Platform::::get_voter_identity_key_v0(voting_address, key_id) + .expect("v0 constructor must not fail"); + + assert_eq!(key.id(), key_id); + assert_eq!(key.key_type(), KeyType::ECDSA_HASH160); + assert_eq!(key.purpose(), Purpose::VOTING); + assert_eq!( + key.security_level(), + SecurityLevel::HIGH, + "voter key is HIGH, not CRITICAL" + ); + assert!(key.read_only()); + assert!(key.disabled_at().is_none()); + assert!(key.contract_bounds().is_none()); + assert_eq!(key.data().as_slice(), &voting_address); + } + + /// Voting address bytes must be preserved verbatim in the key's data. + #[test] + fn v0_preserves_voting_address_bytes() { + let voting_address: [u8; 20] = [ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, + ]; + let key = Platform::::get_voter_identity_key_v0(voting_address, 42) + .expect("must succeed"); + assert_eq!(key.data().as_slice(), &voting_address); + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_instant_send_lock/verify_recent_signature_locally/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_instant_send_lock/verify_recent_signature_locally/v0/mod.rs index a4aa430888d..2ed186f8a02 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_instant_send_lock/verify_recent_signature_locally/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_instant_send_lock/verify_recent_signature_locally/v0/mod.rs @@ -154,3 +154,97 @@ impl Debug for InstantLockDebug<'_> { .finish() } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::test::helpers::setup::TestPlatformBuilder; + use dpp::dashcore::InstantLock; + + /// An all-zero compressed BLS signature is not a valid G2 point. The + /// `from_compressed` decoder returns `None`, which is converted into + /// `Ok(false)` — NOT an error. This pins the early-return "invalid format + /// means not verified" contract. + #[test] + fn v0_invalid_signature_format_returns_ok_false() { + let platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_initial_state_structure(); + + let platform_state = platform.state.load(); + + let bad = InstantLock { + version: 1, + inputs: vec![], + txid: dpp::dashcore::Txid::from([7u8; 32]), + cyclehash: [0u8; 32].into(), + signature: [0u8; 96].into(), + }; + + let got = verify_recent_instant_lock_signature_locally_v0(&bad, &platform_state) + .expect("invalid sig format must NOT produce Err"); + assert!( + !got, + "invalid signature format must be reported as not verified" + ); + } + + /// Different invalid signature bytes must all take the early-return path + /// (Ok(false)) without touching the quorum set — this protects against + /// refactors that accidentally probe quorums before validating the + /// signature format. + #[test] + fn v0_various_invalid_signature_bytes_return_ok_false() { + let platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_initial_state_structure(); + + let platform_state = platform.state.load(); + + // These byte patterns are not valid compressed G2 points (the flag + // bits for compressed encoding are specific), so `from_compressed` + // returns None and the function short-circuits to Ok(false). + let bad_sigs: [[u8; 96]; 3] = [[0u8; 96], [0xffu8; 96], [0x0au8; 96]]; + + for (i, sig_bytes) in bad_sigs.iter().enumerate() { + let il = InstantLock { + version: 1, + inputs: vec![], + txid: dpp::dashcore::Txid::from([i as u8; 32]), + cyclehash: [0u8; 32].into(), + signature: (*sig_bytes).into(), + }; + let got = verify_recent_instant_lock_signature_locally_v0(&il, &platform_state) + .expect("invalid sig must yield Ok, not Err"); + assert!(!got, "invalid sig must be reported as not verified"); + } + } + + /// The version field of an InstantLock does not affect the signature + /// format check — an invalid signature must still return Ok(false) for + /// any version number. + #[test] + fn v0_version_does_not_affect_invalid_signature_handling() { + let platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_initial_state_structure(); + + let platform_state = platform.state.load(); + + for version in [0u8, 1u8, 2u8, 255u8] { + let il = InstantLock { + version, + inputs: vec![], + txid: dpp::dashcore::Txid::from([7u8; 32]), + cyclehash: [0u8; 32].into(), + signature: [0u8; 96].into(), + }; + let got = verify_recent_instant_lock_signature_locally_v0(&il, &platform_state) + .expect("must not Err on bad sig regardless of version"); + assert!(!got); + } + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/epoch/gather_epoch_info/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/epoch/gather_epoch_info/v0/mod.rs index 0ccd04de1fe..50aab439a21 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/epoch/gather_epoch_info/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/epoch/gather_epoch_info/v0/mod.rs @@ -44,3 +44,136 @@ impl Platform { ) } } + +#[cfg(test)] +mod tests { + use crate::platform_types::block_proposal::v0::BlockProposal; + use crate::test::helpers::setup::TestPlatformBuilder; + use dpp::version::PlatformVersion; + use tenderdash_abci::proto::version::Consensus; + + fn make_block_proposal( + height: u64, + block_time_ms: u64, + raw: &Vec>, + ) -> BlockProposal<'_> { + BlockProposal { + consensus_versions: Consensus { block: 1, app: 1 }, + block_hash: None, + height, + round: 0, + core_chain_locked_height: 1, + core_chain_lock_update: None, + proposed_app_version: 1, + proposer_pro_tx_hash: [0u8; 32], + validator_set_quorum_hash: [0u8; 32], + block_time_ms, + raw_state_transitions: raw, + } + } + + /// At the configured genesis height, `gather_epoch_info_v0` must succeed and + /// report `current_epoch_index = 0` — because `get_genesis_time` echoes the + /// proposal's block_time_ms, the time since genesis is zero, which maps to + /// epoch 0. Guards the genesis-path integration between `get_genesis_time` + /// and `EpochInfoV0::from_genesis_time_and_block_info`. + #[test] + fn v0_at_genesis_height_yields_epoch_zero() { + let platform_version = PlatformVersion::latest(); + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_initial_state_structure(); + + let genesis_height = platform.platform.config.abci.genesis_height; + let transaction = platform.drive.grove.start_transaction(); + let platform_state = platform.state.load(); + let raw: Vec> = Vec::new(); + let proposal = make_block_proposal(genesis_height, 1_000_000, &raw); + + use crate::platform_types::epoch_info::v0::EpochInfoV0Getters; + let info = platform + .platform + .gather_epoch_info_v0(&proposal, &transaction, &platform_state, platform_version) + .expect("gather_epoch_info_v0 must succeed at genesis height"); + + assert_eq!(info.current_epoch_index(), 0); + assert_eq!(info.previous_epoch_index(), None); + } + + /// If drive has no cached genesis time and the height is NOT the genesis + /// height, `gather_epoch_info_v0` must propagate the `DriveIncoherence` + /// error from `get_genesis_time`. This pins the error-propagation contract. + #[test] + fn v0_above_genesis_without_stored_time_propagates_error() { + let platform_version = PlatformVersion::latest(); + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_initial_state_structure(); + + let genesis_height = platform.platform.config.abci.genesis_height; + let transaction = platform.drive.grove.start_transaction(); + let platform_state = platform.state.load(); + let raw: Vec> = Vec::new(); + let proposal = make_block_proposal(genesis_height + 1, 2_000_000, &raw); + + let err = platform + .platform + .gather_epoch_info_v0(&proposal, &transaction, &platform_state, platform_version) + .expect_err("must fail when genesis time is missing"); + + let msg = err.to_string().to_lowercase(); + assert!( + msg.contains("genesis"), + "error must mention genesis time: {}", + msg + ); + } + + /// When platform_state has NO `last_committed_block_time_ms`, the epoch info + /// calculation takes the default branch and returns + /// `EpochInfoV0::default()` — `current_epoch_index = 0`, `is_epoch_change = true`. + /// This pins the "no previous block" short-circuit. + #[test] + fn v0_no_previous_block_time_returns_default_epoch_info() { + let platform_version = PlatformVersion::latest(); + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_initial_state_structure(); + + // Supply a cached genesis time so the drive lookup succeeds when we + // pass a non-genesis height. + const GENESIS_MS: u64 = 10_000_000; + platform.drive.set_genesis_time(GENESIS_MS); + + let genesis_height = platform.platform.config.abci.genesis_height; + let transaction = platform.drive.grove.start_transaction(); + let platform_state = platform.state.load(); + + // We have no last_committed_block_info on the state, so + // `last_committed_block_time_ms()` is None. The calculation returns the + // default EpochInfoV0 regardless of how far past genesis we are. + let block_time_ms = GENESIS_MS + + 10 * platform + .platform + .config + .execution + .epoch_time_length_s + .saturating_mul(1000); + let raw: Vec> = Vec::new(); + let proposal = make_block_proposal(genesis_height + 10, block_time_ms, &raw); + + use crate::platform_types::epoch_info::v0::EpochInfoV0Getters; + let info = platform + .platform + .gather_epoch_info_v0(&proposal, &transaction, &platform_state, platform_version) + .expect("must succeed with cached genesis time"); + + assert_eq!( + info.current_epoch_index(), + 0, + "default epoch info when no previous block" + ); + assert!(info.is_epoch_change(), "default is_epoch_change is true"); + assert_eq!(info.previous_epoch_index(), None); + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/epoch/get_genesis_time/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/epoch/get_genesis_time/v0/mod.rs index 15b0c6c3c94..a74a8bc7020 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/epoch/get_genesis_time/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/epoch/get_genesis_time/v0/mod.rs @@ -36,3 +36,113 @@ impl Platform { } } } + +#[cfg(test)] +mod tests { + use crate::error::execution::ExecutionError; + use crate::error::Error; + use crate::test::helpers::setup::TestPlatformBuilder; + + /// At the configured genesis height, `get_genesis_time_v0` must simply echo + /// back `block_time_ms` — it does not consult drive storage. + #[test] + fn v0_returns_block_time_at_genesis_height() { + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_initial_state_structure(); + + let genesis_height = platform.platform.config.abci.genesis_height; + let transaction = platform.drive.grove.start_transaction(); + + let got = platform + .platform + .get_genesis_time_v0(genesis_height, 1_234_567, &transaction) + .expect("at genesis height must succeed"); + + assert_eq!(got, 1_234_567); + } + + /// Above genesis height with no stored genesis time, the fallback must return + /// a `DriveIncoherence` execution error. We use `set_initial_state_structure` + /// to ensure the tree exists but no genesis time has been cached. + #[test] + fn v0_above_genesis_without_stored_time_returns_drive_incoherence() { + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_initial_state_structure(); + + let genesis_height = platform.platform.config.abci.genesis_height; + let transaction = platform.drive.grove.start_transaction(); + + let err = platform + .platform + .get_genesis_time_v0(genesis_height + 1, 42, &transaction) + .expect_err("must fail when genesis time is not set"); + + match err { + Error::Execution(ExecutionError::DriveIncoherence(msg)) => { + assert!( + msg.contains("genesis time"), + "error must mention genesis time: {}", + msg + ); + } + other => panic!("expected DriveIncoherence error, got: {:?}", other), + } + } + + /// When the drive cache has a genesis time set, `get_genesis_time_v0` at a + /// height ABOVE genesis must return the cached time — NOT the supplied + /// `block_time_ms`. This pins the "non-genesis-height path reads from drive" + /// contract. + #[test] + fn v0_above_genesis_with_cached_time_returns_drive_value() { + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_initial_state_structure(); + + // Manually seed the cached genesis time so that the drive lookup hits. + const GENESIS_TIME_MS: u64 = 123_456_789; + platform.drive.set_genesis_time(GENESIS_TIME_MS); + + let genesis_height = platform.platform.config.abci.genesis_height; + let transaction = platform.drive.grove.start_transaction(); + + // Passing a sentinel block_time_ms that we expect to be ignored. + let got = platform + .platform + .get_genesis_time_v0(genesis_height + 5, 0xdead_beef, &transaction) + .expect("must succeed when drive has genesis time"); + + assert_eq!( + got, GENESIS_TIME_MS, + "above-genesis must read from drive, not echo block_time_ms" + ); + } + + /// At genesis height, even if drive has a cached genesis time, the supplied + /// `block_time_ms` is echoed back — we do NOT consult the drive. This pins + /// the short-circuit at the top of the function. + #[test] + fn v0_at_genesis_height_short_circuits_drive_lookup() { + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_initial_state_structure(); + + // Seed the drive with a value that differs from block_time_ms. + platform.drive.set_genesis_time(999_999_999); + + let genesis_height = platform.platform.config.abci.genesis_height; + let transaction = platform.drive.grove.start_transaction(); + + let got = platform + .platform + .get_genesis_time_v0(genesis_height, 42, &transaction) + .expect("genesis-height path must succeed"); + + assert_eq!( + got, 42, + "at genesis height, we must echo block_time_ms and ignore drive" + ); + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/fee_pool_outwards_distribution/fetch_reward_shares_list_for_masternode/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/fee_pool_outwards_distribution/fetch_reward_shares_list_for_masternode/v0/mod.rs index d64499dcd53..cc8d6341b6e 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/fee_pool_outwards_distribution/fetch_reward_shares_list_for_masternode/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/fee_pool_outwards_distribution/fetch_reward_shares_list_for_masternode/v0/mod.rs @@ -78,3 +78,69 @@ impl Platform { Ok(query_documents_outcome.documents().to_owned()) } } + +#[cfg(test)] +mod tests { + use crate::test::helpers::setup::TestPlatformBuilder; + use dpp::version::PlatformVersion; + + /// On a genesis-state platform, no reward share documents exist for any + /// owner, so the v0 query must return an empty `Vec`. This is the canonical + /// "masternode has no explicit reward shares" case. + #[test] + fn v0_returns_empty_for_unknown_masternode_owner() { + let platform_version = PlatformVersion::latest(); + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + + let transaction = platform.drive.grove.start_transaction(); + let docs = platform + .fetch_reward_shares_list_for_masternode_v0( + &[0x11u8; 32], + Some(&transaction), + platform_version, + ) + .expect("must succeed on a genesis state without reward shares"); + + assert!(docs.is_empty(), "unknown owner must yield empty list"); + } + + /// Different owner ids on an empty state must all return empty lists — + /// the query is by `$ownerId`, so the owner bytes must actually matter + /// for the query shape (not cached across calls). + #[test] + fn v0_different_owners_all_return_empty_on_fresh_state() { + let platform_version = PlatformVersion::latest(); + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + + let transaction = platform.drive.grove.start_transaction(); + for owner in [[0u8; 32], [1u8; 32], [0xffu8; 32]] { + let docs = platform + .fetch_reward_shares_list_for_masternode_v0( + &owner, + Some(&transaction), + platform_version, + ) + .expect("must succeed for any owner"); + assert!(docs.is_empty(), "empty state must yield empty list"); + } + } + + /// Without a transaction handle, the query must still succeed: the query + /// itself is read-only and does not require a transaction. + #[test] + fn v0_works_without_transaction() { + let platform_version = PlatformVersion::latest(); + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + + let docs = platform + .fetch_reward_shares_list_for_masternode_v0(&[7u8; 32], None, platform_version) + .expect("must succeed without a transaction"); + assert!(docs.is_empty()); + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/initialization/initial_core_height/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/initialization/initial_core_height/v0/mod.rs index 7cfa2462482..dec8d5cf85c 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/initialization/initial_core_height/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/initialization/initial_core_height/v0/mod.rs @@ -89,3 +89,229 @@ where } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::rpc::core::MockCoreRPCLike; + use crate::test::helpers::setup::TestPlatformBuilder; + use dpp::dashcore::hashes::Hash; + use dpp::dashcore::{BlockHash, ChainLock}; + use dpp::dashcore_rpc::dashcore_rpc_json::{ + Bip9SoftforkInfo, Bip9SoftforkStatus, SoftforkInfo, SoftforkType, + }; + + fn active_fork(height: u32) -> SoftforkInfo { + SoftforkInfo { + softfork_type: SoftforkType::Bip9, + active: true, + height: Some(height), + bip9: Some(Bip9SoftforkInfo { + status: Bip9SoftforkStatus::Active, + bit: None, + start_time: 0, + timeout: 0, + since: height, + statistics: None, + }), + } + } + + fn inactive_fork() -> SoftforkInfo { + SoftforkInfo { + softfork_type: SoftforkType::Bip9, + active: false, + height: None, + bip9: Some(Bip9SoftforkInfo { + status: Bip9SoftforkStatus::Defined, + bit: None, + start_time: 0, + timeout: 0, + since: 0, + statistics: None, + }), + } + } + + fn chain_lock_at(height: u32) -> ChainLock { + ChainLock { + block_height: height, + block_hash: BlockHash::from_byte_array([0u8; 32]), + signature: [0u8; 96].into(), + } + } + + /// If `get_fork_info("mn_rr")` returns `Ok(None)`, the method must yield + /// `InitializationForkNotActive`. + #[test] + fn v0_fork_info_none_returns_fork_not_active() { + let mut platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc(); + let mut mock_rpc = MockCoreRPCLike::new(); + mock_rpc.expect_get_fork_info().returning(|_| Ok(None)); + platform.core_rpc = mock_rpc; + + let err = platform + .platform + .initial_core_height_and_time_v0(None) + .expect_err("expected error"); + + match err { + Error::Execution(ExecutionError::InitializationForkNotActive(_)) => {} + other => panic!("expected InitializationForkNotActive, got {:?}", other), + } + } + + /// If the fork exists but is not active, the method must yield + /// `InitializationForkNotActive`. + #[test] + fn v0_fork_inactive_returns_fork_not_active() { + let mut platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc(); + let mut mock_rpc = MockCoreRPCLike::new(); + mock_rpc + .expect_get_fork_info() + .returning(|_| Ok(Some(inactive_fork()))); + platform.core_rpc = mock_rpc; + + let err = platform + .platform + .initial_core_height_and_time_v0(None) + .expect_err("expected error"); + + match err { + Error::Execution(ExecutionError::InitializationForkNotActive(_)) => {} + other => panic!("expected InitializationForkNotActive, got {:?}", other), + } + } + + /// If `requested` height is AFTER the best chain lock, yield + /// `InitializationHeightIsNotLocked`. + #[test] + fn v0_requested_above_chain_lock_returns_height_not_locked() { + let mut platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc(); + let mut mock_rpc = MockCoreRPCLike::new(); + mock_rpc + .expect_get_fork_info() + .returning(|_| Ok(Some(active_fork(10)))); + mock_rpc + .expect_get_best_chain_lock() + .returning(|| Ok(chain_lock_at(50))); + platform.core_rpc = mock_rpc; + + let err = platform + .platform + .initial_core_height_and_time_v0(Some(100)) + .expect_err("expected error"); + + match err { + Error::Execution(ExecutionError::InitializationHeightIsNotLocked { + initial_height, + chain_lock_height, + }) => { + assert_eq!(initial_height, 100); + assert_eq!(chain_lock_height, 50); + } + other => panic!("expected InitializationHeightIsNotLocked, got {:?}", other), + } + } + + /// Happy path: active fork, no requested height, chain lock far above fork. + /// Must return `(mn_rr_fork_height, block_time)`. + #[test] + fn v0_default_path_returns_fork_height_and_block_time() { + let mut platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc(); + let mut mock_rpc = MockCoreRPCLike::new(); + mock_rpc + .expect_get_fork_info() + .returning(|_| Ok(Some(active_fork(10)))); + mock_rpc + .expect_get_best_chain_lock() + .returning(|| Ok(chain_lock_at(100))); + mock_rpc + .expect_get_block_time_from_height() + .returning(|_| Ok(1_000_000)); + platform.core_rpc = mock_rpc; + + let (height, time) = platform + .platform + .initial_core_height_and_time_v0(None) + .expect("happy path must succeed"); + + assert_eq!(height, 10, "must fall back to mn_rr fork height"); + assert_eq!(time, 1_000_000); + } + + /// When the requested height is EXACTLY equal to the chain lock height, we + /// must NOT take the error path — the condition is `initial_height <= chain_lock_height`. + #[test] + fn v0_requested_equal_to_chain_lock_is_accepted() { + let mut platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc(); + let mut mock_rpc = MockCoreRPCLike::new(); + mock_rpc + .expect_get_fork_info() + .returning(|_| Ok(Some(active_fork(1)))); + mock_rpc + .expect_get_best_chain_lock() + .returning(|| Ok(chain_lock_at(42))); + mock_rpc + .expect_get_block_time_from_height() + .returning(|_| Ok(500)); + platform.core_rpc = mock_rpc; + + let (height, time) = platform + .platform + .initial_core_height_and_time_v0(Some(42)) + .expect("equality boundary must succeed"); + + assert_eq!(height, 42); + assert_eq!(time, 500); + } + + /// If the block time returned by core is in the future (> current system time), + /// `InitializationGenesisTimeInFuture` must be returned. + #[test] + fn v0_genesis_time_in_future_returns_dedicated_error() { + let mut platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc(); + let mut mock_rpc = MockCoreRPCLike::new(); + mock_rpc + .expect_get_fork_info() + .returning(|_| Ok(Some(active_fork(1)))); + mock_rpc + .expect_get_best_chain_lock() + .returning(|| Ok(chain_lock_at(50))); + // Block time ~2100 AD in ms — well into the future. + mock_rpc + .expect_get_block_time_from_height() + .returning(|_| Ok(4_102_444_800_000)); + platform.core_rpc = mock_rpc; + + let err = platform + .platform + .initial_core_height_and_time_v0(Some(10)) + .expect_err("expected genesis-time-in-future error"); + + match err { + Error::Execution(ExecutionError::InitializationGenesisTimeInFuture { + initial_height, + .. + }) => { + assert_eq!(initial_height, 10); + } + other => panic!( + "expected InitializationGenesisTimeInFuture, got {:?}", + other + ), + } + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_vote_polls_end/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_vote_polls_end/v0/mod.rs index 4677e64f3f5..1d1b4186e9f 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_vote_polls_end/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/clean_up_after_vote_polls_end/v0/mod.rs @@ -59,3 +59,100 @@ where } } } + +#[cfg(test)] +mod tests { + use crate::test::helpers::setup::TestPlatformBuilder; + use dpp::block::block_info::BlockInfo; + use dpp::version::PlatformVersion; + use std::collections::BTreeMap; + + /// Empty vote_polls map yields zero contested polls, so the function takes + /// the `else` branch and returns `Ok(())` without touching storage. + #[test] + fn v0_empty_vote_polls_returns_ok_without_cleanup() { + let platform_version = PlatformVersion::latest(); + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + + let transaction = platform.drive.grove.start_transaction(); + let block_info = BlockInfo { + time_ms: 1_234, + height: 1, + core_height: 1, + epoch: Default::default(), + }; + let empty: BTreeMap> = BTreeMap::new(); + + platform + .clean_up_after_vote_polls_end_v0( + &block_info, + &empty, + false, + Some(&transaction), + platform_version, + ) + .expect("empty map must be a no-op"); + } + + /// A `BTreeMap` where timestamps map to *empty* vectors still has no contested + /// polls after the drain-style iteration, so we again take the else branch. + /// This guards against a regression where an empty inner vec is mishandled. + #[test] + fn v0_vote_polls_with_empty_timestamps_is_noop() { + let platform_version = PlatformVersion::latest(); + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + + let transaction = platform.drive.grove.start_transaction(); + let block_info = BlockInfo { + time_ms: 1_000, + height: 1, + core_height: 1, + epoch: Default::default(), + }; + + let mut vote_polls: BTreeMap> = BTreeMap::new(); + vote_polls.insert(100, Vec::new()); + vote_polls.insert(200, Vec::new()); + + platform + .clean_up_after_vote_polls_end_v0( + &block_info, + &vote_polls, + false, + Some(&transaction), + platform_version, + ) + .expect("map of empty buckets must be a no-op"); + } + + /// The testnet cleanup flag does NOT influence the empty-path (otherwise an + /// empty input could inadvertently do work). Verify both flag values succeed + /// identically on empty input. + #[test] + fn v0_testnet_flag_has_no_effect_on_empty_input() { + let platform_version = PlatformVersion::latest(); + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + + let transaction = platform.drive.grove.start_transaction(); + let block_info = BlockInfo::default_with_time(0); + let empty: BTreeMap> = BTreeMap::new(); + + for flag in [false, true] { + platform + .clean_up_after_vote_polls_end_v0( + &block_info, + &empty, + flag, + Some(&transaction), + platform_version, + ) + .expect("empty input must succeed regardless of testnet flag"); + } + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/remove_votes_for_removed_masternodes/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/remove_votes_for_removed_masternodes/v0/mod.rs index 59d00b5b6e0..be42633ee75 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/remove_votes_for_removed_masternodes/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/remove_votes_for_removed_masternodes/v0/mod.rs @@ -42,3 +42,78 @@ where Ok(()) } } + +#[cfg(test)] +mod tests { + use crate::test::helpers::setup::TestPlatformBuilder; + use dpp::block::block_info::BlockInfo; + use dpp::version::PlatformVersion; + + /// When `last_committed` and `block_platform_state` are identical clones, there + /// are no masternode list changes, so the method must early-return `Ok(())` + /// without touching storage. + #[test] + fn v0_noop_when_no_removed_masternodes() { + let platform_version = PlatformVersion::latest(); + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + + let state = platform.state.load(); + let a = state.as_ref().clone(); + let b = state.as_ref().clone(); + drop(state); + + let transaction = platform.drive.grove.start_transaction(); + let block_info = BlockInfo { + time_ms: 1_000, + height: 1, + core_height: 1, + epoch: Default::default(), + }; + + platform + .remove_votes_for_removed_masternodes_v0( + &block_info, + &a, + &b, + Some(&transaction), + platform_version, + ) + .expect("no changes must be a no-op success"); + } + + /// The method must still succeed when the platform state has no masternodes at + /// all (genesis with empty masternode list) — this is the canonical empty case + /// for the "no removed masternodes" branch. + #[test] + fn v0_ok_on_empty_state_cloned_thrice() { + let platform_version = PlatformVersion::latest(); + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + + let transaction = platform.drive.grove.start_transaction(); + let block_info = BlockInfo { + time_ms: 1_000, + height: 1, + core_height: 1, + epoch: Default::default(), + }; + + let state = platform.state.load(); + for _ in 0..3 { + let a = state.as_ref().clone(); + let b = state.as_ref().clone(); + platform + .remove_votes_for_removed_masternodes_v0( + &block_info, + &a, + &b, + Some(&transaction), + platform_version, + ) + .expect("must remain OK across repeated calls with no diff"); + } + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/voting/run_dao_platform_events/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/voting/run_dao_platform_events/v0/mod.rs index 57fbe635b22..6647e9f31eb 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/voting/run_dao_platform_events/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/voting/run_dao_platform_events/v0/mod.rs @@ -41,3 +41,67 @@ where Ok(()) } } + +#[cfg(test)] +mod tests { + use crate::test::helpers::setup::TestPlatformBuilder; + use dpp::block::block_info::BlockInfo; + use dpp::version::PlatformVersion; + + /// On a genesis-state platform with no masternode changes and no ended vote + /// polls, `run_dao_platform_events_v0` must return `Ok(())` — both internal + /// calls (remove_votes, check_for_ended_vote_polls) must succeed with + /// no-op behavior. + #[test] + fn v0_is_ok_with_no_state_change_on_empty_platform() { + let platform_version = PlatformVersion::latest(); + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + + let state = platform.state.load(); + let a = state.as_ref().clone(); + let b = state.as_ref().clone(); + drop(state); + + let transaction = platform.drive.grove.start_transaction(); + let block_info = BlockInfo { + time_ms: 1_234, + height: 1, + core_height: 1, + epoch: Default::default(), + }; + + platform + .run_dao_platform_events_v0(&block_info, &a, &b, Some(&transaction), platform_version) + .expect("must succeed with no state changes"); + } + + /// Repeated invocation with no state change must remain Ok — the two + /// delegated operations are idempotent in the empty case. + #[test] + fn v0_idempotent_on_empty_platform() { + let platform_version = PlatformVersion::latest(); + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + + let state = platform.state.load(); + let transaction = platform.drive.grove.start_transaction(); + let block_info = BlockInfo::default_with_time(0); + + for _ in 0..3 { + let a = state.as_ref().clone(); + let b = state.as_ref().clone(); + platform + .run_dao_platform_events_v0( + &block_info, + &a, + &b, + Some(&transaction), + platform_version, + ) + .expect("idempotent"); + } + } +} From c9944a776ab59d8461499a20020ac0cf74493f52 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 23 Apr 2026 22:04:21 +0800 Subject: [PATCH 5/5] test: address CodeRabbit review comments on PR #3528 - serialize.rs V0 contract test: assert V0 precondition explicitly so the NotSupported branch is always exercised (no vacuous pass). - serialize.rs v0->v1 fallback: require fallback to recover the document rather than accept any non-version-mismatch error. - prune_shielded_pool_anchors tests: derive pruning-boundary heights from event_constants so tests fail if the targeted branch becomes unreachable under version drift. - clear_drive_block_cache test: directly verify unblock by blocking the global cache first, asserting GlobalCacheIsBlocked is returned, then asserting Ok after clear_drive_block_cache_v0. - update_core_info docstring: rewrite to describe actual behavior (idempotent short-circuit with is_init_chain=false). - gather_epoch_info error assertion: match ExecutionError::DriveIncoherence variant instead of substring-matching the display text. - fetch_reward_shares_list_for_masternode: honest doc comment describing the empty-list branch across distinct owners. - initial_core_height: pin current behavior for requested < fork_height (documents the doc-vs-impl invariant gap). - encrypted_notes test: derive unaligned start_index from chunk size so test is robust to version constant changes. - contested_resource_identity_votes: require empty errors + populated Votes response on acceptance-boundary tests. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/rs-dpp/src/document/v0/serialize.rs | 42 +++++++------ .../prune_shielded_pool_anchors/v0/mod.rs | 63 ++++++++++++++----- .../clear_drive_block_cache/v0/mod.rs | 62 ++++++++++++++++-- .../update_core_info/v0/mod.rs | 16 ++--- .../epoch/gather_epoch_info/v0/mod.rs | 18 ++++-- .../v0/mod.rs | 12 ++-- .../initial_core_height/v0/mod.rs | 43 +++++++++++++ .../query/shielded/encrypted_notes/v0/mod.rs | 10 ++- .../v0/mod.rs | 37 +++++++---- 9 files changed, 231 insertions(+), 72 deletions(-) diff --git a/packages/rs-dpp/src/document/v0/serialize.rs b/packages/rs-dpp/src/document/v0/serialize.rs index ab5e3249ffe..15a18140593 100644 --- a/packages/rs-dpp/src/document/v0/serialize.rs +++ b/packages/rs-dpp/src/document/v0/serialize.rs @@ -2528,7 +2528,10 @@ mod tests { fn serialize_specific_version_rejects_v2_for_v0_contract() { // V0 contracts always force serialize_v0, so feature_version 2 is // rejected with NotSupported before reaching the version dispatch. - let platform_version = PlatformVersion::latest(); + // Use `PlatformVersion::first()` so the fixture is guaranteed to load + // as a V0 contract — without this, the test could pass vacuously if + // the dashpay contract began deserializing as a non-V0 variant. + let platform_version = PlatformVersion::first(); let (contract, type_name) = dashpay_contract_and_type(platform_version); let document_type = contract .document_type_for_name(&type_name) @@ -2539,14 +2542,19 @@ mod tests { .expect("expected random document"); let crate::document::Document::V0(doc_v0) = &document; - if matches!(&contract, DataContract::V0(_)) { - let err = doc_v0 - .serialize_specific_version(document_type, &contract, 2) - .expect_err("V0 contract should reject v2"); - match err { - ProtocolError::NotSupported(_) => {} - other => panic!("expected NotSupported, got {:?}", other), - } + // Precondition: the fixture must actually be a V0 contract, otherwise + // the NotSupported branch we intend to exercise would never be hit. + assert!( + matches!(&contract, DataContract::V0(_)), + "fixture must be a V0 contract to exercise the V0-gated NotSupported branch" + ); + + let err = doc_v0 + .serialize_specific_version(document_type, &contract, 2) + .expect_err("V0 contract should reject v2"); + match err { + ProtocolError::NotSupported(_) => {} + other => panic!("expected NotSupported, got {:?}", other), } } @@ -2581,17 +2589,11 @@ mod tests { // Overwrite the varint-1 prefix with varint-0. v1_bytes[0] = 0; - // from_bytes will dispatch to v0, which may fail, but the implementation - // then retries via v1. Either way, it must not return UnknownVersionMismatch. - let result = DocumentV0::from_bytes(&v1_bytes, document_type, platform_version); - match result { - Ok(recovered) => assert_eq!(recovered, doc_v0), - Err(e) => { - // If v1-decode also fails, we get the original v0 error — not a - // version mismatch, since the prefix was 0. - assert!(!matches!(e, ProtocolError::UnknownVersionMismatch { .. })); - } - } + // from_bytes dispatches to v0, which fails on the mismatched layout, + // then retries via v1 — the fallback must recover the original document. + let recovered = DocumentV0::from_bytes(&v1_bytes, document_type, platform_version) + .expect("v0-prefixed v1 payload must fall back to v1 deserialization"); + assert_eq!(recovered, doc_v0); } // ================================================================ diff --git a/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/prune_shielded_pool_anchors/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/prune_shielded_pool_anchors/v0/mod.rs index 2ee3e913e08..c19e87e0660 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/prune_shielded_pool_anchors/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/prune_shielded_pool_anchors/v0/mod.rs @@ -48,6 +48,31 @@ mod tests { use crate::test::helpers::setup::TestPlatformBuilder; use dpp::version::PlatformVersion; + /// Returns `(retention_blocks, pruning_interval)` from the version's + /// event_constants so tests derive branch-entry/branch-skip heights from + /// the live configuration rather than hardcoded literals. + fn shielded_anchor_pruning_parameters(platform_version: &PlatformVersion) -> (u64, u64) { + let event_constants = &platform_version + .drive_abci + .validation_and_processing + .event_constants; + ( + event_constants.shielded_anchor_retention_blocks, + event_constants.shielded_anchor_pruning_interval, + ) + } + + /// Smallest block height that satisfies both + /// `block_height.is_multiple_of(pruning_interval)` AND + /// `block_height > retention_blocks`, so that the delegated prune path is + /// reached. + fn block_height_on_pruning_boundary_after_retention(platform_version: &PlatformVersion) -> u64 { + let (retention_blocks, pruning_interval) = + shielded_anchor_pruning_parameters(platform_version); + assert!(pruning_interval > 0, "pruning interval must be non-zero"); + ((retention_blocks / pruning_interval) + 1) * pruning_interval + } + /// When `block_height` is not a multiple of `pruning_interval`, the method /// must return `Ok(())` without touching storage. #[test] @@ -58,13 +83,18 @@ mod tests { .set_genesis_state(); let transaction = platform.drive.grove.start_transaction(); - // Pick a height that is guaranteed not to be a multiple of any realistic interval >= 2: - // height = 1 is only a multiple of 1, so for intervals >= 2 this early-returns. - // If `shielded_anchor_pruning_interval` is 1 in this version, the branch is - // not exercised — that is acceptable; the call must still succeed. + let (_, pruning_interval) = shielded_anchor_pruning_parameters(platform_version); + assert!( + pruning_interval > 1, + "this test requires a pruning_interval > 1 so a non-boundary height exists" + ); + // `pruning_interval - 1` is guaranteed not to be a multiple of + // `pruning_interval`, so the early-return branch fires. + let block_height = pruning_interval - 1; + platform - .prune_shielded_pool_anchors_v0(1, &transaction, platform_version) - .expect("prune must succeed at height 1"); + .prune_shielded_pool_anchors_v0(block_height, &transaction, platform_version) + .expect("prune must succeed off the pruning interval boundary"); } /// When `block_height <= retention_blocks`, the method returns early (no cutoff @@ -83,9 +113,9 @@ mod tests { .expect("prune must succeed at height 0"); } - /// Calling the pruner with a very large block height on an empty state should - /// not fail — there is simply nothing to remove, and the delegated drive - /// method is expected to treat this as a no-op. + /// Calling the pruner at the first boundary past the retention window on an + /// empty state should succeed — there is simply nothing to remove, and the + /// delegated drive method is expected to treat this as a no-op. #[test] fn v0_large_height_on_empty_state_is_ok() { let platform_version = PlatformVersion::latest(); @@ -94,12 +124,13 @@ mod tests { .set_genesis_state(); let transaction = platform.drive.grove.start_transaction(); - // Choose a height that is almost certainly a multiple of the pruning - // interval regardless of version: a power-of-two large enough that, - // combined with retention, the cutoff is positive. + // Derived from `event_constants` so the prune branch is actually + // exercised regardless of how the version constants drift. + let block_height = block_height_on_pruning_boundary_after_retention(platform_version); + platform - .prune_shielded_pool_anchors_v0(1_048_576, &transaction, platform_version) - .expect("prune must succeed on empty state at large height"); + .prune_shielded_pool_anchors_v0(block_height, &transaction, platform_version) + .expect("prune must succeed on empty state past retention"); } /// Pruning is idempotent on an empty state: calling it twice must not panic @@ -112,9 +143,11 @@ mod tests { .set_genesis_state(); let transaction = platform.drive.grove.start_transaction(); + let block_height = block_height_on_pruning_boundary_after_retention(platform_version); + for _ in 0..3 { platform - .prune_shielded_pool_anchors_v0(1_048_576, &transaction, platform_version) + .prune_shielded_pool_anchors_v0(block_height, &transaction, platform_version) .expect("prune must be idempotent"); } } diff --git a/packages/rs-drive-abci/src/execution/platform_events/block_start/clear_drive_block_cache/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/block_start/clear_drive_block_cache/v0/mod.rs index cab42fced01..cbb06ba391d 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/block_start/clear_drive_block_cache/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/block_start/clear_drive_block_cache/v0/mod.rs @@ -49,10 +49,64 @@ mod tests { } } - /// After `clear_drive_block_cache_v0`, the protocol_versions_counter's - /// global cache must be unblocked (ready for reads). We exercise this by - /// calling the method and then confirming a subsequent call continues to - /// succeed — which it can't do if the write lock became poisoned. + /// After `clear_drive_block_cache_v0`, the `protocol_versions_counter`'s + /// global cache must be unblocked (ready for reads). We prove this directly + /// by first blocking the global cache, observing that a read returns the + /// `GlobalCacheIsBlocked` error, then calling `clear_drive_block_cache_v0` + /// and observing that the same read now succeeds. + #[test] + fn v0_unblocks_globally_blocked_protocol_versions_cache() { + use drive::error::cache::CacheError; + use drive::error::Error as DriveError; + + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + + // Block the global cache so the read path would normally error. + platform + .drive + .cache + .protocol_versions_counter + .write() + .block_global_cache(); + + // Sanity check: while blocked, `get` must return GlobalCacheIsBlocked. + // Scope the read guard so it stays alive as long as the borrowed + // `Option<&u64>` inside the Result. + { + let guard = platform.drive.cache.protocol_versions_counter.read(); + let blocked_result = guard.get(&1u32); + assert!( + matches!( + &blocked_result, + Err(DriveError::Cache(CacheError::GlobalCacheIsBlocked)) + ), + "precondition: blocked cache must return GlobalCacheIsBlocked, got {:?}", + blocked_result + ); + } + + // Now clear the block cache — this must also unblock the global cache. + platform.clear_drive_block_cache_v0(); + + // Post-condition: `get` must now succeed (returning Ok regardless of + // whether the key is present) — any Err would mean the cache is still + // blocked. + { + let guard = platform.drive.cache.protocol_versions_counter.read(); + let unblocked_result = guard.get(&1u32); + assert!( + unblocked_result.is_ok(), + "clear_drive_block_cache_v0 must leave the global cache unblocked, got {:?}", + unblocked_result + ); + } + } + + /// After `clear_drive_block_cache_v0`, the `protocol_versions_counter` + /// write lock must still be usable (not poisoned) and further reads must + /// succeed. Complements the direct-unblock test above. #[test] fn v0_leaves_cache_usable_after_call() { let platform = TestPlatformBuilder::new() diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_core_info/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_core_info/v0/mod.rs index b78a0bc141a..91564d2db1c 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_core_info/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_core_info/v0/mod.rs @@ -116,17 +116,13 @@ mod tests { .expect("same-height non-init must short-circuit OK"); } - /// Even with the same core height, `is_init_chain = true` must bypass the - /// early return — but since we can't easily mock the downstream RPC, we - /// only assert the path is reachable (it will try to call core_rpc and - /// panic in the mock; we catch only the short-circuit case above). - /// This test simply confirms the same-height-short-circuit is governed - /// ONLY by `is_init_chain = false`: setting it to `true` with matching - /// heights must NOT early-return — which we observe indirectly by the - /// fact that the same-height non-init test above passes without touching - /// the mock RPC at all. + /// The same-height early-return short-circuit must be idempotent: calling + /// `update_core_info_v0` repeatedly with the same `core_block_height` and + /// `is_init_chain = false` must succeed every time without ever reaching + /// the downstream `update_masternode_list` / `update_quorum_info` paths + /// (which would fail because the mock Core RPC has no expectations set). #[test] - fn v0_short_circuit_is_tied_to_is_init_chain_false() { + fn v0_short_circuit_is_idempotent_when_not_init_chain() { // Re-confirm that both conditions together are required by running // the short-circuit case twice; idempotency guards against flakiness. let platform_version = PlatformVersion::latest(); diff --git a/packages/rs-drive-abci/src/execution/platform_events/epoch/gather_epoch_info/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/epoch/gather_epoch_info/v0/mod.rs index 50aab439a21..d91981a02ea 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/epoch/gather_epoch_info/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/epoch/gather_epoch_info/v0/mod.rs @@ -47,6 +47,8 @@ impl Platform { #[cfg(test)] mod tests { + use crate::error::execution::ExecutionError; + use crate::error::Error; use crate::platform_types::block_proposal::v0::BlockProposal; use crate::test::helpers::setup::TestPlatformBuilder; use dpp::version::PlatformVersion; @@ -121,12 +123,16 @@ mod tests { .gather_epoch_info_v0(&proposal, &transaction, &platform_state, platform_version) .expect_err("must fail when genesis time is missing"); - let msg = err.to_string().to_lowercase(); - assert!( - msg.contains("genesis"), - "error must mention genesis time: {}", - msg - ); + match err { + Error::Execution(ExecutionError::DriveIncoherence(msg)) => { + assert!( + msg.to_lowercase().contains("genesis"), + "DriveIncoherence message must mention genesis time, got: {}", + msg + ); + } + other => panic!("expected DriveIncoherence, got: {:?}", other), + } } /// When platform_state has NO `last_committed_block_time_ms`, the epoch info diff --git a/packages/rs-drive-abci/src/execution/platform_events/fee_pool_outwards_distribution/fetch_reward_shares_list_for_masternode/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/fee_pool_outwards_distribution/fetch_reward_shares_list_for_masternode/v0/mod.rs index cc8d6341b6e..d31eb13138a 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/fee_pool_outwards_distribution/fetch_reward_shares_list_for_masternode/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/fee_pool_outwards_distribution/fetch_reward_shares_list_for_masternode/v0/mod.rs @@ -106,11 +106,15 @@ mod tests { assert!(docs.is_empty(), "unknown owner must yield empty list"); } - /// Different owner ids on an empty state must all return empty lists — - /// the query is by `$ownerId`, so the owner bytes must actually matter - /// for the query shape (not cached across calls). + /// Across distinct owner ids on a fresh (empty) state, the query must + /// consistently return an empty `Vec`. This does not prove that owner + /// bytes flow into the query key — genesis has no reward-share documents, + /// so every owner is indistinguishable at the result level — but it does + /// exercise the empty-list branch for several different inputs and + /// guarantees the function is callable per-owner without internal state + /// leaking between calls. #[test] - fn v0_different_owners_all_return_empty_on_fresh_state() { + fn v0_empty_list_branch_holds_across_distinct_owners_on_fresh_state() { let platform_version = PlatformVersion::latest(); let platform = TestPlatformBuilder::new() .build_with_mock_rpc() diff --git a/packages/rs-drive-abci/src/execution/platform_events/initialization/initial_core_height/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/initialization/initial_core_height/v0/mod.rs index dec8d5cf85c..6a2336b2185 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/initialization/initial_core_height/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/initialization/initial_core_height/v0/mod.rs @@ -276,6 +276,49 @@ mod tests { assert_eq!(time, 500); } + /// Pins the currently-observable behavior when `requested < + /// mn_rr_fork_height`. The function docstring (lines 24-26) says this + /// should fail with an error, but the v0 implementation does not enforce + /// that invariant — it only checks `initial_height <= chain_lock_height`. + /// + /// This test locks in the actual behavior as-is so that any future change + /// (either tightening the implementation or relaxing the docstring) is a + /// conscious, reviewed decision rather than an accidental regression. + /// See discussion on PR #3528 for the docs-vs-impl gap. + #[test] + fn v0_requested_below_fork_height_is_currently_accepted() { + let mut platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc(); + let mut mock_rpc = MockCoreRPCLike::new(); + mock_rpc + .expect_get_fork_info() + .returning(|_| Ok(Some(active_fork(10)))); + mock_rpc + .expect_get_best_chain_lock() + .returning(|| Ok(chain_lock_at(50))); + mock_rpc + .expect_get_block_time_from_height() + .returning(|_| Ok(1_000)); + platform.core_rpc = mock_rpc; + + // requested = 9 is strictly below the mn_rr fork height (10). + let result = platform.platform.initial_core_height_and_time_v0(Some(9)); + + // Today the implementation accepts this; the docstring suggests it + // should not. Assert the current observable behavior (acceptance) + // rather than silently ignore the disagreement. + assert!( + result.is_ok(), + "v0 currently accepts requested < mn_rr_fork_height (see docstring \ + vs. impl gap); update this test when production behavior changes, \ + got: {:?}", + result + ); + let (height, _time) = result.unwrap(); + assert_eq!(height, 9); + } + /// If the block time returned by core is in the future (> current system time), /// `InitializationGenesisTimeInFuture` must be returned. #[test] diff --git a/packages/rs-drive-abci/src/query/shielded/encrypted_notes/v0/mod.rs b/packages/rs-drive-abci/src/query/shielded/encrypted_notes/v0/mod.rs index 1a8afbb7766..c2e71f52093 100644 --- a/packages/rs-drive-abci/src/query/shielded/encrypted_notes/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/encrypted_notes/v0/mod.rs @@ -164,10 +164,18 @@ mod tests { #[test] fn test_v0_non_aligned_start_index_errors() { // Non-aligned start_index branch: returns InvalidArgument directly. + // Derive the unaligned value from the versioned chunk size so this + // test never degrades into a vacuous check if the constant is later + // tuned to 1 or 5. let (platform, state, version) = setup_platform(None, Network::Testnet, None); + let chunk = max_chunk_size(version); + assert!( + chunk > 1, + "test requires a chunk size > 1 so an unaligned start_index exists" + ); let request = GetShieldedEncryptedNotesRequestV0 { - start_index: 5, // not aligned to chunk size + start_index: chunk - 1, // not aligned to chunk size count: 10, prove: false, }; diff --git a/packages/rs-drive-abci/src/query/voting/contested_resource_identity_votes/v0/mod.rs b/packages/rs-drive-abci/src/query/voting/contested_resource_identity_votes/v0/mod.rs index 5190fe26b25..31a1ed4694e 100644 --- a/packages/rs-drive-abci/src/query/voting/contested_resource_identity_votes/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/voting/contested_resource_identity_votes/v0/mod.rs @@ -442,18 +442,23 @@ mod tests { .query_contested_resource_identity_votes_v0(request, &state, version) .expect("expected query to succeed"); - // Should not hit the "offset out of bounds" error. - let has_offset_err = result.errors.iter().any(|e| { - matches!( - e, - QueryError::InvalidArgument(msg) if msg.contains("offset out of bounds") - ) - }); + // `u16::MAX` is the exact upper boundary accepted by the offset check + // — it must neither surface any validation error nor drop the + // response payload. Tightening beyond "no offset error" prevents the + // test from silently passing if a different validation error starts + // firing at this boundary. assert!( - !has_offset_err, - "u16::MAX should not trigger offset bounds error: {:?}", + result.errors.is_empty(), + "u16::MAX offset must be accepted without validation errors: {:?}", result.errors ); + assert!(matches!( + result.data, + Some(GetContestedResourceIdentityVotesResponseV0 { + result: Some(get_contested_resource_identity_votes_response_v0::Result::Votes(_)), + metadata: Some(_), + }) + )); } #[test] @@ -490,7 +495,8 @@ mod tests { #[test] fn test_query_contested_resource_identity_votes_valid_start_at_empty_state() { // Valid 32-byte start_at poll identifier + empty state → success with - // empty result set. + // an *empty* result set. Assert the inner fields so the test does not + // silently pass if the query started returning entries on empty state. use dapi_grpc::platform::v0::get_contested_resource_identity_votes_request::get_contested_resource_identity_votes_request_v0::StartAtVotePollIdInfo; let (platform, state, version) = setup_platform(None, Network::Testnet, None); @@ -515,9 +521,16 @@ mod tests { assert!(matches!( result.data, Some(GetContestedResourceIdentityVotesResponseV0 { - result: Some(get_contested_resource_identity_votes_response_v0::Result::Votes(_)), + result: Some( + get_contested_resource_identity_votes_response_v0::Result::Votes( + get_contested_resource_identity_votes_response_v0::ContestedResourceIdentityVotes { + contested_resource_identity_votes, + finished_results, + }, + ), + ), metadata: Some(_), - }) + }) if contested_resource_identity_votes.is_empty() && finished_results )); }