diff --git a/packages/rs-drive-abci/src/platform_types/validator_set/v0/mod.rs b/packages/rs-drive-abci/src/platform_types/validator_set/v0/mod.rs index 6a882c1a7d3..c4582a4b83a 100644 --- a/packages/rs-drive-abci/src/platform_types/validator_set/v0/mod.rs +++ b/packages/rs-drive-abci/src/platform_types/validator_set/v0/mod.rs @@ -308,3 +308,242 @@ impl ValidatorSetMethodsV0 for ValidatorSetV0 { }) } } + +#[cfg(test)] +mod tests { + use super::*; + use dpp::bls_signatures::{Bls12381G2Impl, SecretKey}; + use dpp::dashcore::hashes::Hash; + use dpp::dashcore::{ProTxHash, PubkeyHash, QuorumHash}; + use rand::rngs::StdRng; + use rand::SeedableRng; + + fn make_validator(pro_tx_hash: ProTxHash, is_banned: bool) -> ValidatorV0 { + let mut rng = StdRng::seed_from_u64(1); + let public_key = Some(SecretKey::::random(&mut rng).public_key()); + ValidatorV0 { + pro_tx_hash, + public_key, + node_ip: "192.168.0.1".to_string(), + node_id: PubkeyHash::from_slice(&[7; 20]).unwrap(), + core_port: 10, + platform_http_port: 20, + platform_p2p_port: 30, + is_banned, + } + } + + fn make_set( + quorum_hash_seed: u8, + core_height: u32, + threshold_seed: u64, + members: BTreeMap, + ) -> ValidatorSetV0 { + let mut rng = StdRng::seed_from_u64(threshold_seed); + let threshold_public_key = SecretKey::::random(&mut rng).public_key(); + ValidatorSetV0 { + quorum_hash: QuorumHash::from_slice(&[quorum_hash_seed; 32]).unwrap(), + quorum_index: Some(1), + core_height, + members, + threshold_public_key, + } + } + + #[test] + fn test_update_difference_quorum_hash_mismatch() { + let pro_tx_hash = ProTxHash::from_slice(&[9; 32]).unwrap(); + let mut members_a = BTreeMap::new(); + members_a.insert(pro_tx_hash, make_validator(pro_tx_hash, false)); + + let mut members_b = BTreeMap::new(); + members_b.insert(pro_tx_hash, make_validator(pro_tx_hash, false)); + + let lhs = make_set(1, 100, 1, members_a); + let rhs = make_set(2, 100, 1, members_b); + + let err = lhs.update_difference(&rhs).unwrap_err(); + assert!( + matches!(err, Error::Execution(ExecutionError::CorruptedCachedState(ref msg)) if msg.contains("quorum hash")) + ); + } + + #[test] + fn test_update_difference_core_height_mismatch() { + let pro_tx_hash = ProTxHash::from_slice(&[9; 32]).unwrap(); + let mut members_a = BTreeMap::new(); + members_a.insert(pro_tx_hash, make_validator(pro_tx_hash, false)); + + let mut members_b = BTreeMap::new(); + members_b.insert(pro_tx_hash, make_validator(pro_tx_hash, false)); + + let lhs = make_set(1, 100, 1, members_a); + let rhs = make_set(1, 200, 1, members_b); + + let err = lhs.update_difference(&rhs).unwrap_err(); + assert!( + matches!(err, Error::Execution(ExecutionError::CorruptedCachedState(ref msg)) if msg.contains("core height")) + ); + } + + #[test] + fn test_update_difference_threshold_public_key_mismatch() { + let pro_tx_hash = ProTxHash::from_slice(&[9; 32]).unwrap(); + let mut members_a = BTreeMap::new(); + members_a.insert(pro_tx_hash, make_validator(pro_tx_hash, false)); + + let mut members_b = BTreeMap::new(); + members_b.insert(pro_tx_hash, make_validator(pro_tx_hash, false)); + + // Same quorum_hash & core_height but distinct threshold keys (different seeds) + let lhs = make_set(1, 100, 1, members_a); + let rhs = make_set(1, 100, 2, members_b); + + let err = lhs.update_difference(&rhs).unwrap_err(); + assert!( + matches!(err, Error::Execution(ExecutionError::CorruptedCachedState(ref msg)) if msg.contains("threshold public key")) + ); + } + + #[test] + fn test_update_difference_missing_member() { + // lhs has a member that is not present in rhs -> error + let pro_tx_hash_a = ProTxHash::from_slice(&[9; 32]).unwrap(); + let pro_tx_hash_b = ProTxHash::from_slice(&[10; 32]).unwrap(); + let mut members_a = BTreeMap::new(); + members_a.insert(pro_tx_hash_a, make_validator(pro_tx_hash_a, false)); + + let mut members_b = BTreeMap::new(); + members_b.insert(pro_tx_hash_b, make_validator(pro_tx_hash_b, false)); + + let lhs = make_set(1, 100, 1, members_a); + // Same threshold key as lhs by reusing seed 1 + let rhs_temp = make_set(1, 100, 1, members_b); + // force threshold keys equal by copying + let rhs = ValidatorSetV0 { + threshold_public_key: lhs.threshold_public_key, + ..rhs_temp + }; + + let err = lhs.update_difference(&rhs).unwrap_err(); + assert!( + matches!(err, Error::Execution(ExecutionError::CorruptedCachedState(ref msg)) if msg.contains("does not contain all same members")) + ); + } + + #[test] + fn test_update_difference_identical_sets_emit_for_non_banned_old_members() { + // When rhs members are structurally equal to lhs members, the code + // takes the "else" branch that emits an update for each non-banned + // old-state validator. + let pro_tx_hash = ProTxHash::from_slice(&[9; 32]).unwrap(); + let mut members_a = BTreeMap::new(); + members_a.insert(pro_tx_hash, make_validator(pro_tx_hash, false)); + + let lhs = make_set(1, 100, 1, members_a.clone()); + let rhs = ValidatorSetV0 { + threshold_public_key: lhs.threshold_public_key, + ..make_set(1, 100, 1, members_a) + }; + + let update = lhs.update_difference(&rhs).expect("expected ok"); + assert_eq!(update.validator_updates.len(), 1); + assert_eq!( + update.validator_updates[0].pro_tx_hash, + pro_tx_hash.to_byte_array().to_vec() + ); + assert_eq!(update.quorum_hash.len(), 32); + } + + #[test] + fn test_update_difference_identical_but_banned_emits_nothing() { + // Both sets have identical, banned validator => banned short-circuits + // to None in the equal branch. + let pro_tx_hash = ProTxHash::from_slice(&[9; 32]).unwrap(); + let mut members_a = BTreeMap::new(); + members_a.insert(pro_tx_hash, make_validator(pro_tx_hash, true)); + + let lhs = make_set(1, 100, 1, members_a.clone()); + let rhs = ValidatorSetV0 { + threshold_public_key: lhs.threshold_public_key, + ..make_set(1, 100, 1, members_a) + }; + + let update = lhs.update_difference(&rhs).expect("expected ok"); + assert!(update.validator_updates.is_empty()); + } + + #[test] + fn test_update_difference_banned_new_is_skipped() { + // new_validator_state differs (is_banned=true) -> the function takes the + // "different" branch and emits None because banned. + let pro_tx_hash = ProTxHash::from_slice(&[9; 32]).unwrap(); + let mut members_old = BTreeMap::new(); + members_old.insert(pro_tx_hash, make_validator(pro_tx_hash, false)); + + let mut members_new = BTreeMap::new(); + members_new.insert(pro_tx_hash, make_validator(pro_tx_hash, true)); + + let lhs = make_set(1, 100, 1, members_old); + let rhs = ValidatorSetV0 { + threshold_public_key: lhs.threshold_public_key, + ..make_set(1, 100, 1, members_new) + }; + + let update = lhs.update_difference(&rhs).expect("expected ok"); + assert!(update.validator_updates.is_empty()); + } + + #[test] + fn test_to_update_excludes_banned_validators() { + let pro_tx_hash_a = ProTxHash::from_slice(&[9; 32]).unwrap(); + let pro_tx_hash_b = ProTxHash::from_slice(&[10; 32]).unwrap(); + let mut members = BTreeMap::new(); + members.insert(pro_tx_hash_a, make_validator(pro_tx_hash_a, false)); + members.insert(pro_tx_hash_b, make_validator(pro_tx_hash_b, true)); + + let set = make_set(1, 100, 1, members); + + let update = set.to_update(); + assert_eq!(update.validator_updates.len(), 1); + assert_eq!( + update.validator_updates[0].pro_tx_hash, + pro_tx_hash_a.to_byte_array().to_vec() + ); + // quorum hash round-trips + assert_eq!(update.quorum_hash, vec![1u8; 32]); + } + + #[test] + fn test_to_update_owned_excludes_banned_validators() { + let pro_tx_hash_a = ProTxHash::from_slice(&[9; 32]).unwrap(); + let pro_tx_hash_b = ProTxHash::from_slice(&[10; 32]).unwrap(); + let mut members = BTreeMap::new(); + members.insert(pro_tx_hash_a, make_validator(pro_tx_hash_a, true)); + members.insert(pro_tx_hash_b, make_validator(pro_tx_hash_b, false)); + + let set = make_set(3, 100, 1, members); + + let update = set.to_update_owned(); + assert_eq!(update.validator_updates.len(), 1); + assert_eq!( + update.validator_updates[0].pro_tx_hash, + pro_tx_hash_b.to_byte_array().to_vec() + ); + assert_eq!(update.quorum_hash, vec![3u8; 32]); + } + + #[test] + fn test_to_update_all_banned_produces_no_updates() { + let pro_tx_hash = ProTxHash::from_slice(&[9; 32]).unwrap(); + let mut members = BTreeMap::new(); + members.insert(pro_tx_hash, make_validator(pro_tx_hash, true)); + + let set = make_set(4, 100, 1, members); + + let update = set.to_update(); + assert!(update.validator_updates.is_empty()); + // threshold_public_key is still present + assert!(update.threshold_public_key.is_some()); + } +} diff --git a/packages/rs-drive-abci/src/query/shielded/anchors/mod.rs b/packages/rs-drive-abci/src/query/shielded/anchors/mod.rs index f899099ceb7..19f0fa4ee30 100644 --- a/packages/rs-drive-abci/src/query/shielded/anchors/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/anchors/mod.rs @@ -52,3 +52,89 @@ impl Platform { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::query::tests::setup_platform; + use dapi_grpc::platform::v0::get_shielded_anchors_request::GetShieldedAnchorsRequestV0; + use dapi_grpc::platform::v0::get_shielded_anchors_response::get_shielded_anchors_response_v0; + use dpp::dashcore::Network; + + #[test] + fn test_query_shielded_anchors_with_none_version_returns_decoding_error() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetShieldedAnchorsRequest { version: None }; + + let result = platform + .query_shielded_anchors(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::DecodingError(msg)] if msg.contains("could not decode shielded anchors query") + )); + } + + #[test] + fn test_query_shielded_anchors_empty_state_returns_empty_list() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetShieldedAnchorsRequest { + version: Some(RequestVersion::V0(GetShieldedAnchorsRequestV0 { + prove: false, + })), + }; + + let result = platform + .query_shielded_anchors(request, &state, version) + .expect("expected query to succeed"); + + assert!(result.errors.is_empty(), "expected no errors"); + let response = result.data.expect("expected response data"); + let inner = match response.version { + Some(ResponseVersion::V0(v)) => v, + _ => panic!("expected v0 response"), + }; + match inner.result { + Some(get_shielded_anchors_response_v0::Result::Anchors(anchors)) => { + assert!( + anchors.anchors.is_empty(), + "expected no anchors in fresh state" + ); + } + other => panic!("expected Anchors result, got {:?}", other), + } + assert!(inner.metadata.is_some(), "expected metadata present"); + } + + #[test] + fn test_query_shielded_anchors_empty_state_proof() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetShieldedAnchorsRequest { + version: Some(RequestVersion::V0(GetShieldedAnchorsRequestV0 { + prove: true, + })), + }; + + let result = platform + .query_shielded_anchors(request, &state, version) + .expect("expected query to succeed"); + + assert!(result.errors.is_empty(), "expected no errors for proof"); + let response = result.data.expect("expected response data"); + let inner = match response.version { + Some(ResponseVersion::V0(v)) => v, + _ => panic!("expected v0 response"), + }; + match inner.result { + Some(get_shielded_anchors_response_v0::Result::Proof(proof)) => { + assert!(!proof.grovedb_proof.is_empty(), "expected non-empty proof"); + } + other => panic!("expected Proof result, got {:?}", other), + } + assert!(inner.metadata.is_some(), "expected metadata present"); + } +} diff --git a/packages/rs-drive-abci/src/query/shielded/encrypted_notes/mod.rs b/packages/rs-drive-abci/src/query/shielded/encrypted_notes/mod.rs index 6f1a44bff5b..53d8d232815 100644 --- a/packages/rs-drive-abci/src/query/shielded/encrypted_notes/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/encrypted_notes/mod.rs @@ -63,3 +63,154 @@ impl Platform { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::query::tests::setup_platform; + use dapi_grpc::platform::v0::get_shielded_encrypted_notes_request::GetShieldedEncryptedNotesRequestV0; + use dapi_grpc::platform::v0::get_shielded_encrypted_notes_response::get_shielded_encrypted_notes_response_v0; + use dpp::dashcore::Network; + + #[test] + fn test_query_shielded_encrypted_notes_with_none_version_returns_decoding_error() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetShieldedEncryptedNotesRequest { version: None }; + + let result = platform + .query_shielded_encrypted_notes(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::DecodingError(msg)] if msg.contains("could not decode shielded encrypted notes query") + )); + } + + #[test] + fn test_query_shielded_encrypted_notes_non_aligned_start_index_rejected() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + // chunk size is `max_encrypted_notes_per_query` (2048 in v1). Anything + // that isn't a multiple of that should be rejected as unaligned. + let request = GetShieldedEncryptedNotesRequest { + version: Some(RequestVersion::V0(GetShieldedEncryptedNotesRequestV0 { + start_index: 1, + count: 16, + prove: false, + })), + }; + + let result = platform + .query_shielded_encrypted_notes(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_query_shielded_encrypted_notes_empty_state_returns_empty_entries() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + // start_index 0 is always chunk-aligned. Fresh state has no notes, so + // the non-proved branch should loop once, find None, and return empty. + let request = GetShieldedEncryptedNotesRequest { + version: Some(RequestVersion::V0(GetShieldedEncryptedNotesRequestV0 { + start_index: 0, + count: 8, + prove: false, + })), + }; + + let result = platform + .query_shielded_encrypted_notes(request, &state, version) + .expect("expected query to succeed"); + + assert!(result.errors.is_empty(), "expected no errors"); + let response = result.data.expect("expected response data"); + let inner = match response.version { + Some(ResponseVersion::V0(v)) => v, + _ => panic!("expected v0 response"), + }; + match inner.result { + Some(get_shielded_encrypted_notes_response_v0::Result::EncryptedNotes(notes)) => { + assert!( + notes.entries.is_empty(), + "expected no notes in fresh shielded pool" + ); + } + other => panic!("expected EncryptedNotes result, got {:?}", other), + } + assert!(inner.metadata.is_some(), "expected metadata present"); + } + + #[test] + fn test_query_shielded_encrypted_notes_count_zero_uses_max() { + // When count == 0, the limit falls back to `max_encrypted_notes_per_query`. + // This exercises the "count == 0 || count > max" branch of the limit + // derivation without needing notes to be stored. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetShieldedEncryptedNotesRequest { + version: Some(RequestVersion::V0(GetShieldedEncryptedNotesRequestV0 { + start_index: 0, + count: 0, + prove: false, + })), + }; + + let result = platform + .query_shielded_encrypted_notes(request, &state, version) + .expect("expected query to succeed"); + + assert!( + result.errors.is_empty(), + "count=0 should be accepted (treated as max); errors: {:?}", + result.errors + ); + let response = result.data.expect("expected response data"); + let inner = match response.version { + Some(ResponseVersion::V0(v)) => v, + _ => panic!("expected v0 response"), + }; + // Fresh state still returns empty entries for the non-proof branch. + assert!(matches!( + inner.result, + Some(get_shielded_encrypted_notes_response_v0::Result::EncryptedNotes(_)) + )); + } + + #[test] + fn test_query_shielded_encrypted_notes_count_exceeds_max_clamped() { + 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 = GetShieldedEncryptedNotesRequest { + version: Some(RequestVersion::V0(GetShieldedEncryptedNotesRequestV0 { + start_index: 0, + count: max + 100, + prove: false, + })), + }; + + let result = platform + .query_shielded_encrypted_notes(request, &state, version) + .expect("expected query to succeed"); + + // Over-max count should be silently clamped to max, not errored out. + assert!( + result.errors.is_empty(), + "count > max should be clamped, not rejected; errors: {:?}", + result.errors + ); + } +} diff --git a/packages/rs-drive-abci/src/query/shielded/most_recent_anchor/mod.rs b/packages/rs-drive-abci/src/query/shielded/most_recent_anchor/mod.rs index b2be7839d25..bbaf361f587 100644 --- a/packages/rs-drive-abci/src/query/shielded/most_recent_anchor/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/most_recent_anchor/mod.rs @@ -65,3 +65,92 @@ impl Platform { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::query::tests::setup_platform; + use dapi_grpc::platform::v0::get_most_recent_shielded_anchor_request::GetMostRecentShieldedAnchorRequestV0; + use dapi_grpc::platform::v0::get_most_recent_shielded_anchor_response::get_most_recent_shielded_anchor_response_v0; + use dpp::dashcore::Network; + + #[test] + fn test_query_most_recent_shielded_anchor_with_none_version_returns_decoding_error() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetMostRecentShieldedAnchorRequest { version: None }; + + let result = platform + .query_most_recent_shielded_anchor(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::DecodingError(msg)] if msg.contains("could not decode most recent shielded anchor query") + )); + } + + #[test] + fn test_query_most_recent_shielded_anchor_empty_state_returns_zero_anchor() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetMostRecentShieldedAnchorRequest { + version: Some(RequestVersion::V0(GetMostRecentShieldedAnchorRequestV0 { + prove: false, + })), + }; + + let result = platform + .query_most_recent_shielded_anchor(request, &state, version) + .expect("expected query to succeed"); + + assert!(result.errors.is_empty(), "expected no errors"); + let response = result.data.expect("expected response data"); + let inner = match response.version { + Some(ResponseVersion::V0(v)) => v, + _ => panic!("expected v0 response"), + }; + match inner.result { + Some(get_most_recent_shielded_anchor_response_v0::Result::Anchor(anchor)) => { + // Fresh init: the most-recent-anchor subtree is empty, so the + // implementation returns a 32-byte zero vec sentinel. + assert_eq!(anchor.len(), 32, "expected 32-byte anchor sentinel"); + assert!( + anchor.iter().all(|&b| b == 0), + "expected all-zero anchor sentinel" + ); + } + other => panic!("expected Anchor result, got {:?}", other), + } + assert!(inner.metadata.is_some(), "expected metadata present"); + } + + #[test] + fn test_query_most_recent_shielded_anchor_empty_state_proof() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetMostRecentShieldedAnchorRequest { + version: Some(RequestVersion::V0(GetMostRecentShieldedAnchorRequestV0 { + prove: true, + })), + }; + + let result = platform + .query_most_recent_shielded_anchor(request, &state, version) + .expect("expected query to succeed"); + + assert!(result.errors.is_empty(), "expected no errors for proof"); + let response = result.data.expect("expected response data"); + let inner = match response.version { + Some(ResponseVersion::V0(v)) => v, + _ => panic!("expected v0 response"), + }; + match inner.result { + Some(get_most_recent_shielded_anchor_response_v0::Result::Proof(proof)) => { + assert!(!proof.grovedb_proof.is_empty(), "expected non-empty proof"); + } + other => panic!("expected Proof result, got {:?}", other), + } + assert!(inner.metadata.is_some(), "expected metadata present"); + } +} diff --git a/packages/rs-drive-abci/src/query/shielded/nullifiers/mod.rs b/packages/rs-drive-abci/src/query/shielded/nullifiers/mod.rs index 096a9820122..ecdf07a6c51 100644 --- a/packages/rs-drive-abci/src/query/shielded/nullifiers/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/nullifiers/mod.rs @@ -59,3 +59,59 @@ impl Platform { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::query::tests::setup_platform; + use dapi_grpc::platform::v0::get_shielded_nullifiers_request::GetShieldedNullifiersRequestV0; + use dapi_grpc::platform::v0::get_shielded_nullifiers_response::get_shielded_nullifiers_response_v0; + use dpp::dashcore::Network; + + #[test] + fn missing_version_returns_decoding_error() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetShieldedNullifiersRequest { version: None }; + + let result = platform + .query_shielded_nullifiers(request, &state, version) + .expect("expected query to succeed with validation errors"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::DecodingError(msg)] if msg.contains("shielded nullifiers") + )); + } + + #[test] + fn dispatcher_wraps_v0_response() { + // The dispatcher must wrap the inner V0 response with Version::V0. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetShieldedNullifiersRequest { + version: Some(RequestVersion::V0(GetShieldedNullifiersRequestV0 { + nullifiers: vec![vec![0x11u8; 32]], + prove: false, + })), + }; + + let result = platform + .query_shielded_nullifiers(request, &state, version) + .expect("expected query to succeed"); + assert!(result.errors.is_empty()); + + let response = result.data.expect("expected response data"); + let inner = match response.version { + Some(ResponseVersion::V0(v0)) => v0, + other => panic!("expected ResponseVersion::V0, got {:?}", other), + }; + match inner.result { + Some(get_shielded_nullifiers_response_v0::Result::NullifierStatuses(statuses)) => { + assert_eq!(statuses.entries.len(), 1); + assert!(!statuses.entries[0].is_spent); + } + other => panic!("expected NullifierStatuses, got {:?}", other), + } + } +} diff --git a/packages/rs-drive-abci/src/query/shielded/nullifiers/v0/mod.rs b/packages/rs-drive-abci/src/query/shielded/nullifiers/v0/mod.rs index b0fb644f87c..cb9f12bff57 100644 --- a/packages/rs-drive-abci/src/query/shielded/nullifiers/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/nullifiers/v0/mod.rs @@ -121,3 +121,142 @@ 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 nullifier(byte: u8) -> Vec { + vec![byte; 32] + } + + #[test] + fn empty_nullifier_list_returns_invalid_argument() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetShieldedNullifiersRequestV0 { + nullifiers: vec![], + prove: false, + }; + + let result = platform + .query_shielded_nullifiers_v0(request, &state, version) + .expect("expected query to succeed with validation errors"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::InvalidArgument(msg)] if msg.contains("must not be empty") + )); + } + + #[test] + fn invalid_nullifier_length_returns_invalid_argument() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + // Second nullifier has only 16 bytes, should be rejected by per-item validation. + let request = GetShieldedNullifiersRequestV0 { + nullifiers: vec![nullifier(1), vec![0u8; 16]], + prove: false, + }; + + let result = platform + .query_shielded_nullifiers_v0(request, &state, version) + .expect("expected query to succeed with validation errors"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::InvalidArgument(msg)] + if msg.contains("index 1") && msg.contains("32 bytes") && msg.contains("16") + )); + } + + #[test] + fn too_many_nullifiers_returns_invalid_limit() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let max = version.drive_abci.query.max_returned_elements as usize; + let nullifiers = (0..=max).map(|i| nullifier(i as u8)).collect::>(); + + let request = GetShieldedNullifiersRequestV0 { + nullifiers, + prove: false, + }; + + let result = platform + .query_shielded_nullifiers_v0(request, &state, version) + .expect("expected query to succeed with validation errors"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::Query(QuerySyntaxError::InvalidLimit(msg))] if msg.contains("maximum is") + )); + } + + #[test] + fn unspent_nullifier_returns_is_spent_false() { + // With an empty shielded pool (no insertions), every queried nullifier must + // come back with is_spent = false, and the set of returned statuses must + // match the input order. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let queried = vec![nullifier(0xAA), nullifier(0xBB), nullifier(0xCC)]; + let request = GetShieldedNullifiersRequestV0 { + nullifiers: queried.clone(), + prove: false, + }; + + let result = platform + .query_shielded_nullifiers_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(result.errors.is_empty(), "expected no validation errors"); + let response = result.data.expect("expected response data"); + assert!(response.metadata.is_some()); + + match response.result { + Some(get_shielded_nullifiers_response_v0::Result::NullifierStatuses(statuses)) => { + assert_eq!(statuses.entries.len(), queried.len()); + for (entry, expected) in statuses.entries.iter().zip(queried.iter()) { + assert_eq!(&entry.nullifier, expected); + assert!( + !entry.is_spent, + "expected nullifier {:?} to not be spent in empty pool", + entry.nullifier + ); + } + } + other => panic!("expected NullifierStatuses, got {:?}", other), + } + } + + #[test] + fn prove_branch_returns_proof_bytes() { + // With prove=true, the handler should produce a response whose result is the + // Proof variant and metadata is populated. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetShieldedNullifiersRequestV0 { + nullifiers: vec![nullifier(0x01)], + prove: true, + }; + + let result = platform + .query_shielded_nullifiers_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(result.errors.is_empty()); + let response = result.data.expect("expected response data"); + match response.result { + Some(get_shielded_nullifiers_response_v0::Result::Proof(proof)) => { + assert!( + !proof.grovedb_proof.is_empty(), + "proof bytes should not be empty" + ); + } + other => panic!("expected Proof result, got {:?}", other), + } + assert!(response.metadata.is_some(), "expected metadata"); + } +} diff --git a/packages/rs-drive-abci/src/query/shielded/nullifiers_branch_state/mod.rs b/packages/rs-drive-abci/src/query/shielded/nullifiers_branch_state/mod.rs index f50032376dc..51cd7ee7c17 100644 --- a/packages/rs-drive-abci/src/query/shielded/nullifiers_branch_state/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/nullifiers_branch_state/mod.rs @@ -60,3 +60,51 @@ impl Platform { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::query::tests::setup_platform; + use dapi_grpc::platform::v0::get_nullifiers_branch_state_request::GetNullifiersBranchStateRequestV0; + use dpp::dashcore::Network; + + #[test] + fn missing_version_returns_decoding_error() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetNullifiersBranchStateRequest { version: None }; + + let result = platform + .query_nullifiers_branch_state(request, &state, version) + .expect("expected query to succeed with validation errors"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::DecodingError(msg)] + if msg.contains("nullifiers branch state") + )); + } + + #[test] + fn invalid_input_bubbles_up_through_dispatcher() { + // Confirm that when the v0 handler returns Err (e.g. for out-of-range depth), + // the dispatcher propagates it as an Err rather than swallowing it. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetNullifiersBranchStateRequest { + version: Some(RequestVersion::V0(GetNullifiersBranchStateRequestV0 { + pool_type: 0, + pool_identifier: vec![], + key: vec![0u8; 32], + depth: 2, // below min_depth = 6 + checkpoint_height: 0, + })), + }; + + let err = platform + .query_nullifiers_branch_state(request, &state, version) + .expect_err("expected error from out-of-range depth"); + + assert!(matches!(err, Error::Drive(_))); + } +} diff --git a/packages/rs-drive-abci/src/query/shielded/nullifiers_branch_state/v0/mod.rs b/packages/rs-drive-abci/src/query/shielded/nullifiers_branch_state/v0/mod.rs index 322ac20bcc4..daada70ea16 100644 --- a/packages/rs-drive-abci/src/query/shielded/nullifiers_branch_state/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/nullifiers_branch_state/v0/mod.rs @@ -39,3 +39,219 @@ impl Platform { Ok(QueryValidationResult::new_with_data(response)) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::query::tests::setup_platform; + use dpp::dashcore::Network; + use drive::drive::{Checkpoint, CheckpointInfo}; + use drive::error::drive::DriveError as DriveInnerError; + use drive::grovedb::GroveDb; + use std::collections::BTreeMap; + use std::sync::Arc; + + fn install_checkpoint( + platform: &crate::test::helpers::setup::TempPlatform, + height: u64, + ) { + let checkpoint_path = platform + .config + .db_path + .join("checkpoints") + .join(height.to_string()); + std::fs::create_dir_all(checkpoint_path.parent().unwrap()) + .expect("expected to create checkpoints dir"); + platform + .drive + .grove + .create_checkpoint(&checkpoint_path) + .expect("expected to create checkpoint"); + let checkpoint_db = + GroveDb::open(&checkpoint_path).expect("expected to open checkpoint db"); + let checkpoint = Checkpoint::new(checkpoint_db, checkpoint_path); + + let mut checkpoints_map = BTreeMap::new(); + checkpoints_map.insert( + height, + CheckpointInfo::new(height * 10, Arc::new(checkpoint)), + ); + platform.drive.checkpoints.store(Arc::new(checkpoints_map)); + } + + #[test] + fn depth_below_min_returns_invalid_input_error() { + // nullifiers_query_min_depth is 6, so depth=3 is out of range and must + // bubble up as a Drive InvalidInput error. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetNullifiersBranchStateRequestV0 { + pool_type: 0, + pool_identifier: vec![], + key: vec![0u8; 32], + depth: 3, + checkpoint_height: 0, + }; + + let err = platform + .query_nullifiers_branch_state_v0(request, &state, version) + .expect_err("expected depth-range error"); + + let is_invalid_input = matches!( + err, + Error::Drive(drive::error::Error::Drive(DriveInnerError::InvalidInput( + ref msg + ))) if msg.contains("depth") + ); + assert!(is_invalid_input, "unexpected error: {err:?}"); + } + + #[test] + fn depth_above_max_returns_invalid_input_error() { + // nullifiers_query_max_depth is 10, so depth=11 is out of range. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetNullifiersBranchStateRequestV0 { + pool_type: 0, + pool_identifier: vec![], + key: vec![0u8; 32], + depth: 11, + checkpoint_height: 0, + }; + + let err = platform + .query_nullifiers_branch_state_v0(request, &state, version) + .expect_err("expected depth-range error"); + + assert!(matches!( + err, + Error::Drive(drive::error::Error::Drive(DriveInnerError::InvalidInput(_))) + )); + } + + #[test] + fn unsupported_token_pool_type_returns_not_supported_error() { + // Pool types 1 and 2 are reserved for token shielded pools and must error + // out with NotSupported, exposing the nullifiers_path_for_pool match arm. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetNullifiersBranchStateRequestV0 { + pool_type: 1, + pool_identifier: vec![], + key: vec![0u8; 32], + depth: 6, + checkpoint_height: 0, + }; + + let err = platform + .query_nullifiers_branch_state_v0(request, &state, version) + .expect_err("expected unsupported pool type error"); + + assert!(matches!( + err, + Error::Drive(drive::error::Error::Drive(DriveInnerError::NotSupported(_))) + )); + } + + #[test] + fn unknown_pool_type_returns_invalid_input_error() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetNullifiersBranchStateRequestV0 { + pool_type: 999, + pool_identifier: vec![], + key: vec![0u8; 32], + depth: 6, + checkpoint_height: 0, + }; + + let err = platform + .query_nullifiers_branch_state_v0(request, &state, version) + .expect_err("expected unknown pool type error"); + + assert!(matches!( + err, + Error::Drive(drive::error::Error::Drive(DriveInnerError::InvalidInput( + ref msg + ))) if msg.contains("Unknown pool type") + )); + } + + #[test] + fn missing_checkpoint_returns_checkpoint_not_found_error() { + // No checkpoints configured, so the requested checkpoint_height cannot be + // resolved and the Drive layer must bubble up CheckpointNotFound. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetNullifiersBranchStateRequestV0 { + pool_type: 0, + pool_identifier: vec![], + key: vec![0u8; 32], + depth: 6, + checkpoint_height: 42, + }; + + let err = platform + .query_nullifiers_branch_state_v0(request, &state, version) + .expect_err("expected checkpoint missing error"); + + assert!(matches!( + err, + Error::Drive(drive::error::Error::Drive(DriveInnerError::CheckpointNotFound(h))) if h == 42 + )); + } + + #[test] + fn pool_identifier_empty_is_normalized_to_none() { + // When pool_identifier is empty and no checkpoint exists, the handler + // must still translate it to `None` before reaching the drive layer, + // which then errors with CheckpointNotFound rather than a path error. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetNullifiersBranchStateRequestV0 { + pool_type: 0, + // Explicitly empty – exercises the `is_empty() -> None` branch. + pool_identifier: vec![], + key: vec![0u8; 32], + depth: 6, + checkpoint_height: 7, + }; + + let err = platform + .query_nullifiers_branch_state_v0(request, &state, version) + .expect_err("expected checkpoint missing error"); + + assert!(matches!( + err, + Error::Drive(drive::error::Error::Drive( + DriveInnerError::CheckpointNotFound(_) + )) + )); + } + + #[test] + fn branch_query_on_empty_tree_surfaces_merk_error() { + // When a checkpoint exists but the nullifiers tree is still empty, the + // underlying grovedb merk returns "branch_query cannot be performed on + // an empty tree". This documents that the drive-abci layer surfaces it + // verbatim as a Drive error rather than silently producing an empty proof. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + install_checkpoint(&platform, 1); + + let request = GetNullifiersBranchStateRequestV0 { + pool_type: 0, + pool_identifier: vec![], + key: vec![0u8; 32], + depth: 6, + checkpoint_height: 1, + }; + + let err = platform + .query_nullifiers_branch_state_v0(request, &state, version) + .expect_err("expected merk error on empty tree"); + + // The precise inner variant is a grovedb/merk implementation detail; we + // just assert it bubbles up as Error::Drive rather than being swallowed. + assert!(matches!(err, Error::Drive(_)), "unexpected error: {err:?}"); + } +} diff --git a/packages/rs-drive-abci/src/query/shielded/nullifiers_trunk_state/mod.rs b/packages/rs-drive-abci/src/query/shielded/nullifiers_trunk_state/mod.rs index 728b32f9131..aedaf2c1380 100644 --- a/packages/rs-drive-abci/src/query/shielded/nullifiers_trunk_state/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/nullifiers_trunk_state/mod.rs @@ -60,3 +60,27 @@ impl Platform { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::query::tests::setup_platform; + use dpp::dashcore::Network; + + #[test] + fn missing_version_returns_decoding_error() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetNullifiersTrunkStateRequest { version: None }; + + let result = platform + .query_nullifiers_trunk_state(request, &state, version) + .expect("expected query to succeed with validation errors"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::DecodingError(msg)] + if msg.contains("nullifiers trunk state") + )); + } +} diff --git a/packages/rs-drive-abci/src/query/shielded/nullifiers_trunk_state/v0/mod.rs b/packages/rs-drive-abci/src/query/shielded/nullifiers_trunk_state/v0/mod.rs index 2b754d850d5..741d6e1df4c 100644 --- a/packages/rs-drive-abci/src/query/shielded/nullifiers_trunk_state/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/nullifiers_trunk_state/v0/mod.rs @@ -42,3 +42,136 @@ impl Platform { Ok(QueryValidationResult::new_with_data(response)) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::error::query::QueryError; + use crate::query::tests::setup_platform; + use dpp::dashcore::Network; + use drive::drive::{Checkpoint, CheckpointInfo}; + use drive::error::drive::DriveError as DriveInnerError; + use drive::grovedb::GroveDb; + use std::collections::BTreeMap; + use std::sync::Arc; + + #[test] + fn no_checkpoint_returns_validation_errors() { + // Without any registered checkpoint, prove_nullifiers_trunk_query returns + // a NoCheckpointsAvailable drive error, which the check_validation_result_with_data + // macro funnels into the validation errors list. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetNullifiersTrunkStateRequestV0 { + pool_type: 0, + pool_identifier: vec![], + }; + + let result = platform + .query_nullifiers_trunk_state_v0(request, &state, version) + .expect("should return Ok with validation errors, not Err"); + + assert!( + !result.errors.is_empty(), + "expected validation errors when no checkpoint is available" + ); + assert!(matches!( + result.errors[0], + QueryError::Drive(drive::error::Error::Drive( + DriveInnerError::NoCheckpointsAvailable + )) + )); + } + + #[test] + fn unsupported_token_pool_type_returns_validation_errors() { + // pool_type=2 hits the NotSupported arm of nullifiers_path_for_pool. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetNullifiersTrunkStateRequestV0 { + pool_type: 2, + pool_identifier: vec![7u8; 32], + }; + + let result = platform + .query_nullifiers_trunk_state_v0(request, &state, version) + .expect("should return Ok with validation errors, not Err"); + + assert!( + !result.errors.is_empty(), + "expected validation errors for unsupported pool" + ); + assert!(matches!( + result.errors[0], + QueryError::Drive(drive::error::Error::Drive(DriveInnerError::NotSupported(_))) + )); + } + + #[test] + fn unknown_pool_type_returns_validation_errors() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetNullifiersTrunkStateRequestV0 { + pool_type: 1234, + pool_identifier: vec![], + }; + + let result = platform + .query_nullifiers_trunk_state_v0(request, &state, version) + .expect("should return Ok with validation errors, not Err"); + + assert!(!result.errors.is_empty()); + assert!(matches!( + result.errors[0], + QueryError::Drive(drive::error::Error::Drive(DriveInnerError::InvalidInput(_))) + )); + } + + #[test] + fn with_checkpoint_returns_proof_and_metadata() { + // Create a checkpoint from the current (empty) state and wire it up so + // both the drive-layer prove call and response_proof_v0 succeed. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let checkpoint_path = platform.config.db_path.join("checkpoints").join("1"); + std::fs::create_dir_all(checkpoint_path.parent().unwrap()) + .expect("expected to create checkpoints dir"); + platform + .drive + .grove + .create_checkpoint(&checkpoint_path) + .expect("expected to create checkpoint"); + + let checkpoint_db = + GroveDb::open(&checkpoint_path).expect("expected to open checkpoint db"); + let checkpoint = Checkpoint::new(checkpoint_db, checkpoint_path); + + let mut checkpoints_map = BTreeMap::new(); + checkpoints_map.insert(1u64, CheckpointInfo::new(1000, Arc::new(checkpoint))); + platform.drive.checkpoints.store(Arc::new(checkpoints_map)); + + let mut checkpoint_states = BTreeMap::new(); + checkpoint_states.insert(1u64, state.clone()); + platform + .checkpoint_platform_states + .store(Arc::new(checkpoint_states)); + + let request = GetNullifiersTrunkStateRequestV0 { + pool_type: 0, + pool_identifier: vec![], + }; + + let result = platform + .query_nullifiers_trunk_state_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(result.errors.is_empty()); + let response = result.data.expect("expected response data"); + let proof = response.proof.expect("expected proof present"); + assert!( + !proof.grovedb_proof.is_empty(), + "expected non-empty trunk proof bytes" + ); + assert!(response.metadata.is_some()); + } +} diff --git a/packages/rs-drive-abci/src/query/shielded/pool_state/mod.rs b/packages/rs-drive-abci/src/query/shielded/pool_state/mod.rs index 3ea9d875763..9853f4a3daa 100644 --- a/packages/rs-drive-abci/src/query/shielded/pool_state/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/pool_state/mod.rs @@ -59,3 +59,87 @@ impl Platform { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::query::tests::setup_platform; + use dapi_grpc::platform::v0::get_shielded_pool_state_request::GetShieldedPoolStateRequestV0; + use dapi_grpc::platform::v0::get_shielded_pool_state_response::get_shielded_pool_state_response_v0; + use dpp::dashcore::Network; + + #[test] + fn test_query_shielded_pool_state_with_none_version_returns_decoding_error() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetShieldedPoolStateRequest { version: None }; + + let result = platform + .query_shielded_pool_state(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::DecodingError(msg)] if msg.contains("could not decode shielded pool state query") + )); + } + + #[test] + fn test_query_shielded_pool_state_empty_state_returns_zero_balance() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetShieldedPoolStateRequest { + version: Some(RequestVersion::V0(GetShieldedPoolStateRequestV0 { + prove: false, + })), + }; + + let result = platform + .query_shielded_pool_state(request, &state, version) + .expect("expected query to succeed"); + + assert!(result.errors.is_empty(), "expected no errors"); + let response = result.data.expect("expected response data"); + let inner = match response.version { + Some(ResponseVersion::V0(v)) => v, + _ => panic!("expected v0 response"), + }; + match inner.result { + Some(get_shielded_pool_state_response_v0::Result::TotalBalance(balance)) => { + // Freshly initialized state: no credits have been shielded. + assert_eq!(balance, 0, "expected zero total balance on fresh state"); + } + other => panic!("expected TotalBalance result, got {:?}", other), + } + assert!(inner.metadata.is_some(), "expected metadata present"); + } + + #[test] + fn test_query_shielded_pool_state_empty_state_proof() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetShieldedPoolStateRequest { + version: Some(RequestVersion::V0(GetShieldedPoolStateRequestV0 { + prove: true, + })), + }; + + let result = platform + .query_shielded_pool_state(request, &state, version) + .expect("expected query to succeed"); + + assert!(result.errors.is_empty(), "expected no errors for proof"); + let response = result.data.expect("expected response data"); + let inner = match response.version { + Some(ResponseVersion::V0(v)) => v, + _ => panic!("expected v0 response"), + }; + match inner.result { + Some(get_shielded_pool_state_response_v0::Result::Proof(proof)) => { + assert!(!proof.grovedb_proof.is_empty(), "expected non-empty proof"); + } + other => panic!("expected Proof result, got {:?}", other), + } + assert!(inner.metadata.is_some(), "expected metadata present"); + } +} diff --git a/packages/rs-drive-abci/src/query/shielded/recent_compacted_nullifier_changes/mod.rs b/packages/rs-drive-abci/src/query/shielded/recent_compacted_nullifier_changes/mod.rs index 61d778b2682..589693e55da 100644 --- a/packages/rs-drive-abci/src/query/shielded/recent_compacted_nullifier_changes/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/recent_compacted_nullifier_changes/mod.rs @@ -65,3 +65,58 @@ impl Platform { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::query::tests::setup_platform; + use dapi_grpc::platform::v0::get_recent_compacted_nullifier_changes_request::GetRecentCompactedNullifierChangesRequestV0; + use dapi_grpc::platform::v0::get_recent_compacted_nullifier_changes_response::get_recent_compacted_nullifier_changes_response_v0; + use dpp::dashcore::Network; + + #[test] + fn missing_version_returns_decoding_error() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetRecentCompactedNullifierChangesRequest { version: None }; + + let result = platform + .query_recent_compacted_nullifier_changes(request, &state, version) + .expect("expected query to succeed with validation errors"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::DecodingError(msg)] + if msg.contains("recent compacted nullifier changes") + )); + } + + #[test] + fn dispatcher_wraps_v0_response() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetRecentCompactedNullifierChangesRequest { + version: Some(RequestVersion::V0( + GetRecentCompactedNullifierChangesRequestV0 { + start_block_height: 0, + prove: false, + }, + )), + }; + + let result = platform + .query_recent_compacted_nullifier_changes(request, &state, version) + .expect("expected query to succeed"); + assert!(result.errors.is_empty()); + + let response = result.data.expect("expected response data"); + let inner = match response.version { + Some(ResponseVersion::V0(v0)) => v0, + other => panic!("expected ResponseVersion::V0, got {:?}", other), + }; + assert!(matches!( + inner.result, + Some(get_recent_compacted_nullifier_changes_response_v0::Result::CompactedNullifierUpdateEntries(_)) + )); + } +} diff --git a/packages/rs-drive-abci/src/query/shielded/recent_compacted_nullifier_changes/v0/mod.rs b/packages/rs-drive-abci/src/query/shielded/recent_compacted_nullifier_changes/v0/mod.rs index 9d91eb7ce93..a544e7ce08d 100644 --- a/packages/rs-drive-abci/src/query/shielded/recent_compacted_nullifier_changes/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/recent_compacted_nullifier_changes/v0/mod.rs @@ -74,3 +74,109 @@ impl Platform { Ok(QueryValidationResult::new_with_data(response)) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::query::tests::setup_platform; + use dpp::dashcore::Network; + + #[test] + fn empty_state_non_prove_returns_empty_entries() { + // With no compactions stored, the fetch returns an empty vector and the + // handler must wrap it in CompactedNullifierUpdateEntries with an empty + // list and populated metadata. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetRecentCompactedNullifierChangesRequestV0 { + start_block_height: 0, + prove: false, + }; + + let result = platform + .query_recent_compacted_nullifier_changes_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(result.errors.is_empty()); + let response = result.data.expect("expected response data"); + match response.result { + Some( + get_recent_compacted_nullifier_changes_response_v0::Result::CompactedNullifierUpdateEntries( + entries, + ), + ) => { + assert!( + entries.compacted_block_changes.is_empty(), + "expected empty compacted block changes on a fresh platform" + ); + } + other => panic!( + "expected CompactedNullifierUpdateEntries result, got {:?}", + other + ), + } + assert!(response.metadata.is_some()); + } + + #[test] + fn empty_state_prove_returns_proof_bytes() { + // The prove branch should produce non-empty proof bytes even for an + // empty shielded pool – the proof encodes the absence. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetRecentCompactedNullifierChangesRequestV0 { + start_block_height: 0, + prove: true, + }; + + let result = platform + .query_recent_compacted_nullifier_changes_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(result.errors.is_empty()); + let response = result.data.expect("expected response data"); + match response.result { + Some(get_recent_compacted_nullifier_changes_response_v0::Result::Proof(proof)) => { + assert!( + !proof.grovedb_proof.is_empty(), + "expected proof bytes for empty compacted pool" + ); + } + other => panic!("expected Proof result, got {:?}", other), + } + assert!(response.metadata.is_some()); + } + + #[test] + fn large_start_block_height_returns_empty_entries() { + // Querying well beyond any stored block should return an empty list + // without error. This exercises the "start > everything" branch of the + // compacted nullifier fetch. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetRecentCompactedNullifierChangesRequestV0 { + start_block_height: u64::MAX - 1, + prove: false, + }; + + let result = platform + .query_recent_compacted_nullifier_changes_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(result.errors.is_empty()); + let response = result.data.expect("expected response data"); + match response.result { + Some( + get_recent_compacted_nullifier_changes_response_v0::Result::CompactedNullifierUpdateEntries( + entries, + ), + ) => { + assert!(entries.compacted_block_changes.is_empty()); + } + other => panic!( + "expected CompactedNullifierUpdateEntries result, got {:?}", + other + ), + } + } +} diff --git a/packages/rs-drive-abci/src/query/shielded/recent_nullifier_changes/mod.rs b/packages/rs-drive-abci/src/query/shielded/recent_nullifier_changes/mod.rs index 8409234c17d..836e6efff11 100644 --- a/packages/rs-drive-abci/src/query/shielded/recent_nullifier_changes/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/recent_nullifier_changes/mod.rs @@ -63,3 +63,57 @@ impl Platform { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::query::tests::setup_platform; + use dapi_grpc::platform::v0::get_recent_nullifier_changes_request::GetRecentNullifierChangesRequestV0; + use dapi_grpc::platform::v0::get_recent_nullifier_changes_response::get_recent_nullifier_changes_response_v0; + use dpp::dashcore::Network; + + #[test] + fn missing_version_returns_decoding_error() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetRecentNullifierChangesRequest { version: None }; + + let result = platform + .query_recent_nullifier_changes(request, &state, version) + .expect("expected query to succeed with validation errors"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::DecodingError(msg)] + if msg.contains("recent nullifier changes") + )); + } + + #[test] + fn dispatcher_wraps_v0_response() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetRecentNullifierChangesRequest { + version: Some(RequestVersion::V0(GetRecentNullifierChangesRequestV0 { + start_height: 0, + prove: true, + })), + }; + + let result = platform + .query_recent_nullifier_changes(request, &state, version) + .expect("expected query to succeed"); + assert!(result.errors.is_empty()); + + let response = result.data.expect("expected response data"); + let inner = match response.version { + Some(ResponseVersion::V0(v0)) => v0, + other => panic!("expected ResponseVersion::V0, got {:?}", other), + }; + assert!(matches!( + inner.result, + Some(get_recent_nullifier_changes_response_v0::Result::Proof(_)) + )); + assert!(inner.metadata.is_some()); + } +} diff --git a/packages/rs-drive-abci/src/query/shielded/recent_nullifier_changes/v0/mod.rs b/packages/rs-drive-abci/src/query/shielded/recent_nullifier_changes/v0/mod.rs index 4c952187b81..931cb70137f 100644 --- a/packages/rs-drive-abci/src/query/shielded/recent_nullifier_changes/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/recent_nullifier_changes/v0/mod.rs @@ -69,3 +69,97 @@ impl Platform { Ok(QueryValidationResult::new_with_data(response)) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::query::tests::setup_platform; + use dpp::dashcore::Network; + + #[test] + fn empty_state_non_prove_returns_empty_entries() { + // An empty platform has no recent nullifier changes, so the non-prove + // branch must return an empty NullifierUpdateEntries and populated metadata. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetRecentNullifierChangesRequestV0 { + start_height: 0, + prove: false, + }; + + let result = platform + .query_recent_nullifier_changes_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(result.errors.is_empty()); + let response = result.data.expect("expected response data"); + match response.result { + Some(get_recent_nullifier_changes_response_v0::Result::NullifierUpdateEntries( + entries, + )) => { + assert!( + entries.block_changes.is_empty(), + "expected no block changes on a fresh platform" + ); + } + other => panic!("expected NullifierUpdateEntries, got {:?}", other), + } + assert!(response.metadata.is_some()); + } + + #[test] + fn empty_state_prove_returns_proof_bytes() { + // Prove branch on empty state must yield a non-empty absence proof and + // populated metadata. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetRecentNullifierChangesRequestV0 { + start_height: 0, + prove: true, + }; + + let result = platform + .query_recent_nullifier_changes_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(result.errors.is_empty()); + let response = result.data.expect("expected response data"); + match response.result { + Some(get_recent_nullifier_changes_response_v0::Result::Proof(proof)) => { + assert!( + !proof.grovedb_proof.is_empty(), + "expected proof bytes for empty recent-changes pool" + ); + } + other => panic!("expected Proof result, got {:?}", other), + } + assert!(response.metadata.is_some()); + } + + #[test] + fn large_start_height_returns_empty_entries() { + // A start height beyond anything stored should return empty entries + // without erroring – exercises the range-from-start-height branch. + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetRecentNullifierChangesRequestV0 { + start_height: u64::MAX - 1, + prove: false, + }; + + let result = platform + .query_recent_nullifier_changes_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(result.errors.is_empty()); + let response = result.data.expect("expected response data"); + match response.result { + Some(get_recent_nullifier_changes_response_v0::Result::NullifierUpdateEntries( + entries, + )) => { + assert!(entries.block_changes.is_empty()); + } + other => panic!("expected NullifierUpdateEntries, got {:?}", other), + } + } +} 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 16a057a4b6b..98926279cb8 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 @@ -71,3 +71,36 @@ impl Platform { Ok(QueryValidationResult::new_with_data(response)) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::query::tests::setup_platform; + use dpp::dashcore::Network; + + #[test] + fn test_query_current_quorums_info_empty_state() { + // On an initialized platform with no validator sets, the response + // should contain empty quorum_hashes and validator_sets, with the + // default all-zeroes current_quorum_hash. + let (platform, state, _version) = setup_platform(None, Network::Testnet, None); + + let request = GetCurrentQuorumsInfoRequestV0 {}; + + let result = platform + .query_current_quorums_info_v0(request, &state) + .expect("expected query to succeed"); + + let data = result.into_data().expect("expected data"); + + assert!(data.quorum_hashes.is_empty()); + assert!(data.validator_sets.is_empty()); + assert_eq!(data.current_quorum_hash.len(), 32); + // all-zeros default quorum hash + assert!(data.current_quorum_hash.iter().all(|b| *b == 0)); + // default proposer pro_tx_hash is all zero bytes at genesis + assert_eq!(data.last_block_proposer.len(), 32); + assert!(data.last_block_proposer.iter().all(|b| *b == 0)); + assert!(data.metadata.is_some()); + } +} diff --git a/packages/rs-drive-abci/src/query/system/epoch_infos/v0/mod.rs b/packages/rs-drive-abci/src/query/system/epoch_infos/v0/mod.rs index 1c6ebf335bf..87e80861e7e 100644 --- a/packages/rs-drive-abci/src/query/system/epoch_infos/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/system/epoch_infos/v0/mod.rs @@ -154,4 +154,71 @@ mod tests { }) if epoch_infos.is_empty() )); } + + #[test] + fn test_query_epoch_infos_start_epoch_too_high() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetEpochsInfoRequestV0 { + start_epoch: Some(u16::MAX as u32), + count: 1, + ascending: true, + prove: false, + }; + + let result = platform + .query_epoch_infos_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::InvalidArgument(msg)] if msg.contains("start epoch too high") + )); + } + + #[test] + fn test_query_epoch_infos_count_too_high() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + // start 10, plus count u32::MAX-10 overflows u16::MAX cleanly via start+count check. + let request = GetEpochsInfoRequestV0 { + start_epoch: Some(10), + count: u16::MAX as u32, + ascending: true, + prove: false, + }; + + let result = platform + .query_epoch_infos_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::InvalidArgument(msg)] if msg.contains("count too high") + )); + } + + #[test] + fn test_query_empty_epoch_infos_proof() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetEpochsInfoRequestV0 { + start_epoch: None, + count: 3, + ascending: true, + prove: true, + }; + + let result = platform + .query_epoch_infos_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.data, + Some(GetEpochsInfoResponseV0 { + result: Some(get_epochs_info_response_v0::Result::Proof(_)), + metadata: Some(_), + }) + )); + } } diff --git a/packages/rs-drive-abci/src/query/system/finalized_epoch_infos/v0/mod.rs b/packages/rs-drive-abci/src/query/system/finalized_epoch_infos/v0/mod.rs index cb0b2b9c3a4..55d2b056d34 100644 --- a/packages/rs-drive-abci/src/query/system/finalized_epoch_infos/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/system/finalized_epoch_infos/v0/mod.rs @@ -145,3 +145,126 @@ impl Platform { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::query::tests::setup_platform; + use dpp::dashcore::Network; + + #[test] + fn test_query_finalized_epoch_infos_start_out_of_range() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetFinalizedEpochInfosRequestV0 { + start_epoch_index: (u16::MAX as u32) + 1, + start_epoch_index_included: true, + end_epoch_index: 10, + end_epoch_index_included: true, + prove: false, + }; + + let result = platform + .query_finalized_epoch_infos_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::InvalidArgument(msg)] if msg.contains("start_epoch_index") + )); + } + + #[test] + fn test_query_finalized_epoch_infos_end_out_of_range() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetFinalizedEpochInfosRequestV0 { + start_epoch_index: 0, + start_epoch_index_included: true, + end_epoch_index: (u16::MAX as u32) + 1, + end_epoch_index_included: true, + prove: false, + }; + + let result = platform + .query_finalized_epoch_infos_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::InvalidArgument(msg)] if msg.contains("end_epoch_index") + )); + } + + #[test] + fn test_query_finalized_epoch_infos_equal_indexes_without_both_included() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetFinalizedEpochInfosRequestV0 { + start_epoch_index: 3, + start_epoch_index_included: true, + end_epoch_index: 3, + end_epoch_index_included: false, + prove: false, + }; + + let result = platform + .query_finalized_epoch_infos_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::InvalidArgument(msg)] if msg.contains("both boundaries must be included") + )); + } + + #[test] + fn test_query_empty_finalized_epoch_infos() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetFinalizedEpochInfosRequestV0 { + start_epoch_index: 0, + start_epoch_index_included: true, + end_epoch_index: 5, + end_epoch_index_included: true, + prove: false, + }; + + let result = platform + .query_finalized_epoch_infos_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.data, + Some(GetFinalizedEpochInfosResponseV0 { + result: Some(get_finalized_epoch_infos_response_v0::Result::Epochs(FinalizedEpochInfos { finalized_epoch_infos })), + metadata: Some(_), + }) if finalized_epoch_infos.is_empty() + )); + } + + #[test] + fn test_query_empty_finalized_epoch_infos_proof() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetFinalizedEpochInfosRequestV0 { + start_epoch_index: 0, + start_epoch_index_included: true, + end_epoch_index: 5, + end_epoch_index_included: true, + prove: true, + }; + + let result = platform + .query_finalized_epoch_infos_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.data, + Some(GetFinalizedEpochInfosResponseV0 { + result: Some(get_finalized_epoch_infos_response_v0::Result::Proof(_)), + metadata: Some(_), + }) + )); + } +} diff --git a/packages/rs-drive-abci/src/query/system/path_elements/v0/mod.rs b/packages/rs-drive-abci/src/query/system/path_elements/v0/mod.rs index fa17331417a..7debdf8e6ee 100644 --- a/packages/rs-drive-abci/src/query/system/path_elements/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/system/path_elements/v0/mod.rs @@ -137,4 +137,62 @@ mod tests { assert_eq!(amount, 100); } + + #[test] + fn test_query_path_elements_exceeds_max_returned_elements() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let platform_version = PlatformVersion::latest(); + let max = platform_version.drive_abci.query.max_returned_elements as usize; + + // Build a keys list of length max+1 (trivial bytes, content irrelevant) + let keys: Vec> = (0..(max + 1)).map(|i| vec![i as u8]).collect(); + + let request = GetPathElementsRequestV0 { + path: vec![vec![RootTree::Misc as u8]], + keys, + prove: false, + }; + + let result = platform + .query_path_elements_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::Query( + drive::error::query::QuerySyntaxError::InvalidLimit(_) + )] + )); + } + + #[test] + fn test_query_path_elements_proof_branch() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let platform_version = PlatformVersion::latest(); + + platform + .drive + .add_to_system_credits(100, None, platform_version) + .expect("expected to add credits"); + + let request = GetPathElementsRequestV0 { + path: vec![vec![RootTree::Misc as u8]], + keys: vec![TOTAL_SYSTEM_CREDITS_STORAGE_KEY.to_vec()], + prove: true, + }; + + let result = platform + .query_path_elements_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.data, + Some(GetPathElementsResponseV0 { + result: Some(get_path_elements_response_v0::Result::Proof(_)), + metadata: Some(_), + }) + )); + } } diff --git a/packages/rs-drive-abci/src/query/system/status/v0/mod.rs b/packages/rs-drive-abci/src/query/system/status/v0/mod.rs index 31225e8b948..2a82e2601cc 100644 --- a/packages/rs-drive-abci/src/query/system/status/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/system/status/v0/mod.rs @@ -73,3 +73,65 @@ impl Platform { Ok(QueryValidationResult::new_with_data(response)) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::query::tests::setup_platform; + use dpp::dashcore::Network; + + #[test] + fn test_query_partial_status_default_state() { + let (platform, state, _version) = setup_platform(None, Network::Testnet, None); + + let request = GetStatusRequestV0 {}; + + let result = platform + .query_partial_status_v0(request, &state) + .expect("expected query to succeed"); + + let data = result.into_data().expect("expected data"); + + // Version structure must be populated with Drive protocol info + let version = data.version.expect("expected version"); + let protocol = version.protocol.expect("expected protocol section"); + let drive_proto = protocol.drive.expect("expected drive protocol section"); + assert_eq!( + drive_proto.latest, + PlatformVersion::latest().protocol_version + ); + assert_eq!( + drive_proto.current, + state.current_protocol_version_in_consensus() + ); + + // Software version must be the crate version string + let software = version.software.expect("expected software section"); + assert_eq!(software.drive, Some(env!("CARGO_PKG_VERSION").to_string())); + assert_eq!(software.dapi, "".to_string()); + + // Chain info reports last committed core height and empty placeholders + let chain = data.chain.expect("expected chain section"); + assert!(!chain.catching_up); + assert!(chain.latest_block_hash.is_empty()); + assert!(chain.latest_app_hash.is_empty()); + assert_eq!(chain.latest_block_height, 0); + assert_eq!( + chain.core_chain_locked_height, + Some(state.last_committed_core_height()) + ); + + // Time info includes epoch and defaults elsewhere + let time = data.time.expect("expected time section"); + assert_eq!(time.local, 0); + assert_eq!( + time.epoch, + Some(state.last_committed_block_epoch().index as u32) + ); + + // Optional sections are None at genesis + assert!(data.node.is_none()); + assert!(data.network.is_none()); + assert!(data.state_sync.is_none()); + } +} diff --git a/packages/rs-drive-abci/src/query/system/version_upgrade_vote_status/v0/mod.rs b/packages/rs-drive-abci/src/query/system/version_upgrade_vote_status/v0/mod.rs index 9481b6718f9..510b64c580f 100644 --- a/packages/rs-drive-abci/src/query/system/version_upgrade_vote_status/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/system/version_upgrade_vote_status/v0/mod.rs @@ -425,4 +425,69 @@ mod tests { assert_eq!(upgrade.len(), 1); assert_eq!(upgrade.get(&validator_pro_tx_hash), Some(1).as_ref()); } + + #[test] + fn test_query_upgrade_vote_status_bad_start_hash_length() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + // 8 bytes instead of 32 + let request = GetProtocolVersionUpgradeVoteStatusRequestV0 { + start_pro_tx_hash: vec![0; 8], + count: 5, + prove: false, + }; + + let result = platform + .query_version_upgrade_vote_status_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::InvalidArgument(msg)] if msg.contains("start_pro_tx_hash not 32 bytes long") + )); + } + + #[test] + fn test_query_upgrade_vote_status_count_too_high() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetProtocolVersionUpgradeVoteStatusRequestV0 { + start_pro_tx_hash: vec![], + count: u16::MAX as u32, + prove: false, + }; + + let result = platform + .query_version_upgrade_vote_status_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::InvalidArgument(msg)] if msg.contains("count too high") + )); + } + + #[test] + fn test_query_upgrade_vote_status_empty_start_hash() { + // empty start_pro_tx_hash should map to None and succeed + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetProtocolVersionUpgradeVoteStatusRequestV0 { + start_pro_tx_hash: vec![], + count: 5, + prove: false, + }; + + let result = platform + .query_version_upgrade_vote_status_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.data, + Some(GetProtocolVersionUpgradeVoteStatusResponseV0 { + result: Some(get_protocol_version_upgrade_vote_status_response_v0::Result::Versions(VersionSignals { version_signals })), + metadata: Some(_), + }) if version_signals.is_empty() + )); + } } 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 2661193bca7..8558d683d03 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 @@ -199,3 +199,91 @@ impl Platform { Ok(QueryValidationResult::new_with_data(response)) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::query::tests::setup_platform; + use dpp::dashcore::Network; + + #[test] + fn test_query_contested_resource_identity_votes_invalid_identity_id() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetContestedResourceIdentityVotesRequestV0 { + identity_id: vec![0; 8], + 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 must be a valid identifier") + )); + } + + #[test] + fn test_query_contested_resource_identity_votes_offset_out_of_bounds() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + // offset > u16::MAX triggers InvalidArgument("offset out of bounds") + let request = GetContestedResourceIdentityVotesRequestV0 { + identity_id: vec![0; 32], + limit: None, + offset: Some((u16::MAX as u32) + 1), + 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("offset out of bounds") + )); + } + + #[test] + fn test_query_contested_resource_identity_votes_empty() { + 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: None, + prove: false, + }; + + let result = platform + .query_contested_resource_identity_votes_v0(request, &state, version) + .expect("expected query to succeed"); + + // empty platform should return the Votes variant with no entries and finished_results=true + assert!(matches!( + result.data, + Some(GetContestedResourceIdentityVotesResponseV0 { + 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 + )); + } +} diff --git a/packages/rs-drive-abci/src/query/voting/contested_resource_vote_state/v0/mod.rs b/packages/rs-drive-abci/src/query/voting/contested_resource_vote_state/v0/mod.rs index 402ddda326f..bf66857aa75 100644 --- a/packages/rs-drive-abci/src/query/voting/contested_resource_vote_state/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/voting/contested_resource_vote_state/v0/mod.rs @@ -257,3 +257,62 @@ impl Platform { Ok(QueryValidationResult::new_with_data(response)) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::query::tests::setup_platform; + use dpp::dashcore::Network; + + #[test] + fn test_query_contested_resource_vote_state_invalid_contract_id() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetContestedResourceVoteStateRequestV0 { + contract_id: vec![0; 8], + document_type_name: "x".to_string(), + index_name: "x".to_string(), + index_values: vec![], + result_type: 0, + allow_include_locked_and_abstaining_vote_tally: false, + start_at_identifier_info: None, + count: None, + prove: false, + }; + + let result = platform + .query_contested_resource_vote_state_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::InvalidArgument(msg)] if msg.contains("contract_id must be a valid identifier") + )); + } + + #[test] + fn test_query_contested_resource_vote_state_contract_not_found() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetContestedResourceVoteStateRequestV0 { + contract_id: vec![0; 32], + document_type_name: "x".to_string(), + index_name: "x".to_string(), + index_values: vec![], + result_type: 0, + allow_include_locked_and_abstaining_vote_tally: false, + start_at_identifier_info: None, + count: None, + prove: false, + }; + + let result = platform + .query_contested_resource_vote_state_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::Query(QuerySyntaxError::DataContractNotFound(_))] + )); + } +} diff --git a/packages/rs-drive-abci/src/query/voting/contested_resource_voters_for_identity/v0/mod.rs b/packages/rs-drive-abci/src/query/voting/contested_resource_voters_for_identity/v0/mod.rs index 7b673cf1e45..ade2a13d48f 100644 --- a/packages/rs-drive-abci/src/query/voting/contested_resource_voters_for_identity/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/voting/contested_resource_voters_for_identity/v0/mod.rs @@ -249,3 +249,88 @@ impl Platform { Ok(QueryValidationResult::new_with_data(response)) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::query::tests::setup_platform; + use dpp::dashcore::Network; + + #[test] + fn test_query_voters_invalid_contract_id() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetContestedResourceVotersForIdentityRequestV0 { + contract_id: vec![0; 8], + document_type_name: "x".to_string(), + index_name: "x".to_string(), + index_values: vec![], + contestant_id: vec![0; 32], + start_at_identifier_info: None, + count: None, + order_ascending: true, + prove: false, + }; + + let result = platform + .query_contested_resource_voters_for_identity_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::InvalidArgument(msg)] if msg.contains("contract_id must be a valid identifier") + )); + } + + #[test] + fn test_query_voters_invalid_contestant_id() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetContestedResourceVotersForIdentityRequestV0 { + contract_id: vec![0; 32], + document_type_name: "x".to_string(), + index_name: "x".to_string(), + index_values: vec![], + contestant_id: vec![0; 8], + start_at_identifier_info: None, + count: None, + order_ascending: true, + prove: false, + }; + + let result = platform + .query_contested_resource_voters_for_identity_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::InvalidArgument(msg)] if msg.contains("voter_id must be a valid identifier") + )); + } + + #[test] + fn test_query_voters_contract_not_found() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetContestedResourceVotersForIdentityRequestV0 { + contract_id: vec![0; 32], + document_type_name: "x".to_string(), + index_name: "x".to_string(), + index_values: vec![], + contestant_id: vec![0; 32], + start_at_identifier_info: None, + count: None, + order_ascending: true, + prove: false, + }; + + let result = platform + .query_contested_resource_voters_for_identity_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::Query(QuerySyntaxError::DataContractNotFound(_))] + )); + } +} diff --git a/packages/rs-drive-abci/src/query/voting/contested_resources/v0/mod.rs b/packages/rs-drive-abci/src/query/voting/contested_resources/v0/mod.rs index 68faf1aeb43..9446a9eb0a2 100644 --- a/packages/rs-drive-abci/src/query/voting/contested_resources/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/voting/contested_resources/v0/mod.rs @@ -236,3 +236,62 @@ impl Platform { Ok(QueryValidationResult::new_with_data(response)) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::query::tests::setup_platform; + use dpp::dashcore::Network; + + #[test] + fn test_query_contested_resources_invalid_contract_id() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetContestedResourcesRequestV0 { + contract_id: vec![0; 8], + document_type_name: "x".to_string(), + index_name: "x".to_string(), + start_index_values: vec![], + end_index_values: vec![], + start_at_value_info: None, + count: None, + order_ascending: true, + prove: false, + }; + + let result = platform + .query_contested_resources_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::InvalidArgument(msg)] if msg.contains("contract_id must be a valid identifier") + )); + } + + #[test] + fn test_query_contested_resources_contract_not_found() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetContestedResourcesRequestV0 { + contract_id: vec![0; 32], + document_type_name: "x".to_string(), + index_name: "x".to_string(), + start_index_values: vec![], + end_index_values: vec![], + start_at_value_info: None, + count: None, + order_ascending: true, + prove: false, + }; + + let result = platform + .query_contested_resources_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::Query(QuerySyntaxError::DataContractNotFound(_))] + )); + } +} diff --git a/packages/rs-drive-abci/src/query/voting/vote_polls_by_end_date_query/v0/mod.rs b/packages/rs-drive-abci/src/query/voting/vote_polls_by_end_date_query/v0/mod.rs index 4819dbfe4f5..994e5c90479 100644 --- a/packages/rs-drive-abci/src/query/voting/vote_polls_by_end_date_query/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/voting/vote_polls_by_end_date_query/v0/mod.rs @@ -217,3 +217,175 @@ impl Platform { Ok(QueryValidationResult::new_with_data(response)) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::query::tests::setup_platform; + use dapi_grpc::platform::v0::get_vote_polls_by_end_date_request::get_vote_polls_by_end_date_request_v0::{ + EndAtTimeInfo, StartAtTimeInfo, + }; + use dpp::dashcore::Network; + + #[test] + fn test_query_vote_polls_by_end_date_start_after_end() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetVotePollsByEndDateRequestV0 { + start_time_info: Some(StartAtTimeInfo { + start_time_ms: 1000, + start_time_included: true, + }), + end_time_info: Some(EndAtTimeInfo { + end_time_ms: 500, + end_time_included: true, + }), + limit: None, + offset: None, + ascending: true, + prove: false, + }; + + let result = platform + .query_vote_polls_by_end_date_query_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::InvalidArgument(msg)] if msg.contains("must be before end time") + )); + } + + #[test] + fn test_query_vote_polls_by_end_date_equal_times_without_both_included() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetVotePollsByEndDateRequestV0 { + start_time_info: Some(StartAtTimeInfo { + start_time_ms: 1000, + start_time_included: true, + }), + end_time_info: Some(EndAtTimeInfo { + end_time_ms: 1000, + end_time_included: false, + }), + limit: None, + offset: None, + ascending: true, + prove: false, + }; + + let result = platform + .query_vote_polls_by_end_date_query_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::InvalidArgument(msg)] if msg.contains("start and end times must be included") + )); + } + + #[test] + fn test_query_vote_polls_by_end_date_limit_zero() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetVotePollsByEndDateRequestV0 { + start_time_info: None, + end_time_info: None, + limit: Some(0), + offset: None, + ascending: true, + prove: false, + }; + + let result = platform + .query_vote_polls_by_end_date_query_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::InvalidArgument(msg)] if msg.contains("out of bounds of") + )); + } + + #[test] + fn test_query_vote_polls_by_end_date_offset_out_of_bounds() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetVotePollsByEndDateRequestV0 { + start_time_info: None, + end_time_info: None, + limit: None, + offset: Some((u16::MAX as u32) + 1), + ascending: true, + prove: false, + }; + + let result = platform + .query_vote_polls_by_end_date_query_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::InvalidArgument(msg)] if msg.contains("offset out of bounds") + )); + } + + #[test] + fn test_query_vote_polls_by_end_date_prove_with_offset() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetVotePollsByEndDateRequestV0 { + start_time_info: None, + end_time_info: None, + limit: None, + offset: Some(5), + ascending: true, + prove: true, + }; + + let result = platform + .query_vote_polls_by_end_date_query_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.errors.as_slice(), + [QueryError::Query( + QuerySyntaxError::RequestingProofWithOffset(_) + )] + )); + } + + #[test] + fn test_query_vote_polls_by_end_date_empty() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let request = GetVotePollsByEndDateRequestV0 { + start_time_info: None, + end_time_info: None, + limit: None, + offset: None, + ascending: true, + prove: false, + }; + + let result = platform + .query_vote_polls_by_end_date_query_v0(request, &state, version) + .expect("expected query to succeed"); + + assert!(matches!( + result.data, + Some(GetVotePollsByEndDateResponseV0 { + result: Some( + get_vote_polls_by_end_date_response_v0::Result::VotePollsByTimestamps( + get_vote_polls_by_end_date_response_v0::SerializedVotePollsByTimestamps { + vote_polls_by_timestamps, + finished_results, + }, + ), + ), + metadata: Some(_), + }) if vote_polls_by_timestamps.is_empty() && finished_results + )); + } +} diff --git a/packages/rs-drive/src/prove/prove_multiple_state_transition_results/v0/mod.rs b/packages/rs-drive/src/prove/prove_multiple_state_transition_results/v0/mod.rs index 7f377a4a7b5..a4ab50716ed 100644 --- a/packages/rs-drive/src/prove/prove_multiple_state_transition_results/v0/mod.rs +++ b/packages/rs-drive/src/prove/prove_multiple_state_transition_results/v0/mod.rs @@ -165,3 +165,542 @@ impl Drive { ) } } + +#[cfg(test)] +mod tests { + use crate::drive::identity::{IdentityDriveQuery, IdentityProveRequestType}; + use crate::query::identity_token_balance_drive_query::IdentityTokenBalanceDriveQuery; + use crate::query::identity_token_info_drive_query::IdentityTokenInfoDriveQuery; + use crate::query::token_status_drive_query::TokenStatusDriveQuery; + use crate::query::{IdentityBasedVoteDriveQuery, SingleDocumentDriveQuery}; + use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; + use dpp::block::block_info::BlockInfo; + use dpp::identifier::Identifier; + use dpp::identity::accessors::IdentityGettersV0; + use dpp::identity::Identity; + use dpp::version::PlatformVersion; + + // ---------------- Helpers --------------------------------------------- + + fn empty_query_set<'a>() -> ( + &'a [IdentityDriveQuery], + &'a [([u8; 32], Option)], + &'a [SingleDocumentDriveQuery], + &'a [IdentityBasedVoteDriveQuery], + &'a [IdentityTokenBalanceDriveQuery], + &'a [IdentityTokenInfoDriveQuery], + &'a [TokenStatusDriveQuery], + ) { + (&[], &[], &[], &[], &[], &[], &[]) + } + + fn insert_identity(drive: &crate::drive::Drive, seed: u64) -> Identity { + let platform_version = PlatformVersion::latest(); + let identity = Identity::random_identity(3, Some(seed), platform_version) + .expect("expected a random identity"); + drive + .add_new_identity( + identity.clone(), + false, + &BlockInfo::default(), + true, + None, + platform_version, + ) + .expect("expected to insert identity"); + identity + } + + // ---------------- Empty input ---------------------------------------- + + /// Calling with no queries at all means there are zero path queries to + /// merge — `PathQuery::merge` errors with "InvalidInput" because it + /// requires at least one. This is the documented error path for a + /// completely empty request. + #[test] + fn empty_input_returns_invalid_input_merge_error() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + let (ids, cids, docs, votes, tbals, tinfos, tstat) = empty_query_set(); + + let err = drive + .prove_multiple_state_transition_results_v0( + ids, + cids, + docs, + votes, + tbals, + tinfos, + tstat, + None, + platform_version, + ) + .expect_err("expected merge error for empty input"); + + let msg = format!("{:?}", err); + assert!( + msg.contains("merge function requires at least 1 path query") + || msg.contains("InvalidInput"), + "expected merge error, got: {msg}" + ); + } + + // ---------------- Single balance query -------------------------------- + + #[test] + fn single_balance_query_produces_non_empty_proof_that_verifies() { + use grovedb::GroveDb; + + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let identity = insert_identity(&drive, 42); + + let identity_queries = vec![IdentityDriveQuery { + identity_id: identity.id().to_buffer(), + prove_request_type: IdentityProveRequestType::Balance, + }]; + + let proof = drive + .prove_multiple_state_transition_results_v0( + &identity_queries, + &[], + &[], + &[], + &[], + &[], + &[], + None, + platform_version, + ) + .expect("expected to produce a proof"); + + assert!(!proof.is_empty(), "proof should not be empty"); + + // Verify against the balance query. + let pq = crate::drive::Drive::balance_for_identity_id_query(identity.id().to_buffer()); + let (_root, items) = + GroveDb::verify_query(proof.as_slice(), &pq, &platform_version.drive.grove_version) + .expect("expected proof to verify"); + assert_eq!(items.len(), 1); + } + + // ---------------- Deterministic output -------------------------------- + + /// Invoking twice with the same inputs (and no writes in between) must + /// yield byte-identical proofs. This protects against incidental + /// nondeterminism (e.g. iterating over a HashMap) inside the aggregator. + #[test] + fn deterministic_proof_for_equal_inputs() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let identity_a = insert_identity(&drive, 1); + let identity_b = insert_identity(&drive, 2); + + let identity_queries = vec![ + IdentityDriveQuery { + identity_id: identity_a.id().to_buffer(), + prove_request_type: IdentityProveRequestType::Balance, + }, + IdentityDriveQuery { + identity_id: identity_b.id().to_buffer(), + prove_request_type: IdentityProveRequestType::Revision, + }, + ]; + + let first = drive + .prove_multiple_state_transition_results_v0( + &identity_queries, + &[], + &[], + &[], + &[], + &[], + &[], + None, + platform_version, + ) + .expect("first proof"); + let second = drive + .prove_multiple_state_transition_results_v0( + &identity_queries, + &[], + &[], + &[], + &[], + &[], + &[], + None, + platform_version, + ) + .expect("second proof"); + + assert_eq!( + first, second, + "proofs must be deterministic for equal inputs" + ); + } + + // ---------------- Non-existent identity -> absence proof -------------- + + /// A Balance query for an identity that does not exist must still + /// produce a verifiable proof (absence proof). This exercises the + /// `Balance` arm of the `IdentityProveRequestType` match independently. + #[test] + fn nonexistent_identity_still_produces_verifiable_proof() { + use grovedb::GroveDb; + + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let missing_id = [0xAAu8; 32]; + let identity_queries = vec![IdentityDriveQuery { + identity_id: missing_id, + prove_request_type: IdentityProveRequestType::Balance, + }]; + + let proof = drive + .prove_multiple_state_transition_results_v0( + &identity_queries, + &[], + &[], + &[], + &[], + &[], + &[], + None, + platform_version, + ) + .expect("absence proof must still succeed"); + + assert!(!proof.is_empty()); + + let pq = crate::drive::Drive::balance_for_identity_id_query(missing_id); + let (_root, items) = + GroveDb::verify_query(proof.as_slice(), &pq, &platform_version.drive.grove_version) + .expect("absence proof must verify"); + // Balance item should come back empty / None since the identity does + // not exist. + assert!( + items.is_empty() || items.iter().all(|(_, _, v)| v.is_none()), + "expected no balance items for a non-existent identity, got {:?}", + items + ); + } + + // ---------------- All four IdentityProveRequestType arms -------------- + + /// Exercises the full request-type match by proving four identities, + /// each with a different `IdentityProveRequestType` variant, ensuring + /// each arm of the match in `prove_multiple_state_transition_results_v0` + /// is hit. We only assert that the proof is non-empty and unique. + #[test] + fn covers_all_identity_prove_request_type_variants() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let i1 = insert_identity(&drive, 101); + let i2 = insert_identity(&drive, 202); + let i3 = insert_identity(&drive, 303); + let i4 = insert_identity(&drive, 404); + + let identity_queries = vec![ + IdentityDriveQuery { + identity_id: i1.id().to_buffer(), + prove_request_type: IdentityProveRequestType::FullIdentity, + }, + IdentityDriveQuery { + identity_id: i2.id().to_buffer(), + prove_request_type: IdentityProveRequestType::Balance, + }, + IdentityDriveQuery { + identity_id: i3.id().to_buffer(), + prove_request_type: IdentityProveRequestType::Keys, + }, + IdentityDriveQuery { + identity_id: i4.id().to_buffer(), + prove_request_type: IdentityProveRequestType::Revision, + }, + ]; + + let proof = drive + .prove_multiple_state_transition_results_v0( + &identity_queries, + &[], + &[], + &[], + &[], + &[], + &[], + None, + platform_version, + ) + .expect("proof should be produced"); + + assert!(!proof.is_empty()); + // A single-query balance proof baseline should differ from this + // combined proof (different queries -> different bytes). + let solo = drive + .prove_multiple_state_transition_results_v0( + &[IdentityDriveQuery { + identity_id: i2.id().to_buffer(), + prove_request_type: IdentityProveRequestType::Balance, + }], + &[], + &[], + &[], + &[], + &[], + &[], + None, + platform_version, + ) + .expect("solo proof"); + assert_ne!( + proof, solo, + "combined proof must differ from solo balance proof" + ); + } + + // ---------------- Non-historical contract ids ------------------------- + + /// Non-historical contract query: provide a single contract_id flagged + /// non-historical. This exercises the `Left(contract_id)` branch of the + /// partition and the `fetch_non_historical_contracts_query` path. + #[test] + fn non_historical_contract_id_produces_verifiable_proof() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + // Absent contract; we only care that a proof is produced. + let contract_id = [0xC1u8; 32]; + + let proof = drive + .prove_multiple_state_transition_results_v0( + &[], + &[(contract_id, Some(false))], + &[], + &[], + &[], + &[], + &[], + None, + platform_version, + ) + .expect("non-historical contract proof"); + assert!(!proof.is_empty()); + } + + /// Historical contract query: provide a single contract_id flagged + /// historical. This exercises the `Right(contract_id)` branch. + #[test] + fn historical_contract_id_produces_verifiable_proof() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let contract_id = [0xC2u8; 32]; + + let proof = drive + .prove_multiple_state_transition_results_v0( + &[], + &[(contract_id, Some(true))], + &[], + &[], + &[], + &[], + &[], + None, + platform_version, + ) + .expect("historical contract proof"); + assert!(!proof.is_empty()); + } + + // ---------------- Mixed contract historicity ------------------------- + + /// Both historical and non-historical contracts in the same call cover + /// the both-non-empty branches (two separate path queries appended). + #[test] + fn mixed_historical_and_non_historical_contract_ids_succeeds() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let c_nonhist = [0xC3u8; 32]; + let c_hist = [0xC4u8; 32]; + + let proof = drive + .prove_multiple_state_transition_results_v0( + &[], + &[(c_nonhist, Some(false)), (c_hist, Some(true))], + &[], + &[], + &[], + &[], + &[], + None, + platform_version, + ) + .expect("mixed contract proof"); + assert!(!proof.is_empty()); + } + + // ---------------- Contract historical=None default-path --------------- + + /// Passing `None` for `historical` must default to non-historical + /// (the `.unwrap_or(false)` branch). + #[test] + fn contract_id_with_none_historical_defaults_to_non_historical() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let c = [0xC5u8; 32]; + + // None should be treated the same as Some(false). + let none_proof = drive + .prove_multiple_state_transition_results_v0( + &[], + &[(c, None)], + &[], + &[], + &[], + &[], + &[], + None, + platform_version, + ) + .expect("none-historical"); + let false_proof = drive + .prove_multiple_state_transition_results_v0( + &[], + &[(c, Some(false))], + &[], + &[], + &[], + &[], + &[], + None, + platform_version, + ) + .expect("explicit false-historical"); + + assert_eq!( + none_proof, false_proof, + "historical=None must be equivalent to historical=Some(false)" + ); + } + + // ---------------- Token queries --------------------------------------- + + /// Exercise each of the three token query arms (balance / info / status) + /// with absent tokens. We just need to prove these branches execute and + /// their `construct_path_query` calls feed the merge correctly. + #[test] + fn covers_token_balance_info_and_status_arms() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let token_id = Identifier::from([0x77u8; 32]); + let identity_id = Identifier::from([0x88u8; 32]); + + let balance_q = vec![IdentityTokenBalanceDriveQuery { + identity_id, + token_id, + }]; + let info_q = vec![IdentityTokenInfoDriveQuery { + identity_id, + token_id, + }]; + let status_q = vec![TokenStatusDriveQuery { token_id }]; + + let proof = drive + .prove_multiple_state_transition_results_v0( + &[], + &[], + &[], + &[], + &balance_q, + &info_q, + &status_q, + None, + platform_version, + ) + .expect("all three token arms should produce a proof"); + assert!(!proof.is_empty()); + } + + // ---------------- Combined request covering many branches ----------- + + /// A maximally combined call covers nearly every `if !<...>.is_empty()` + /// branch of the aggregator in a single invocation. This is the + /// multi-result happy path. + #[test] + fn combined_multi_query_happy_path() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let identity = insert_identity(&drive, 7); + let ids = vec![IdentityDriveQuery { + identity_id: identity.id().to_buffer(), + prove_request_type: IdentityProveRequestType::Balance, + }]; + let contracts = vec![([0xA1u8; 32], Some(false)), ([0xA2u8; 32], Some(true))]; + let token_id = Identifier::from([0x33u8; 32]); + let tbals = vec![IdentityTokenBalanceDriveQuery { + identity_id: identity.id(), + token_id, + }]; + let tinfos = vec![IdentityTokenInfoDriveQuery { + identity_id: identity.id(), + token_id, + }]; + let tstat = vec![TokenStatusDriveQuery { token_id }]; + + let proof = drive + .prove_multiple_state_transition_results_v0( + &ids, + &contracts, + &[], + &[], + &tbals, + &tinfos, + &tstat, + None, + platform_version, + ) + .expect("combined proof must succeed"); + assert!(!proof.is_empty()); + } + + // ---------------- Public dispatcher routing -------------------------- + + /// Route the same call through the public `prove_multiple_state_transition_results` + /// entry point to confirm wiring to `_v0`. Not a version-dispatch test — + /// we only check the happy path produces a non-empty proof. + #[test] + fn public_entrypoint_routes_through_v0() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let identity = insert_identity(&drive, 77); + let ids = vec![IdentityDriveQuery { + identity_id: identity.id().to_buffer(), + prove_request_type: IdentityProveRequestType::Revision, + }]; + + let proof = drive + .prove_multiple_state_transition_results( + &ids, + &[], + &[], + &[], + &[], + &[], + &[], + None, + platform_version, + ) + .expect("public entrypoint happy path"); + assert!(!proof.is_empty()); + } +} diff --git a/packages/rs-drive/src/util/operations/mod.rs b/packages/rs-drive/src/util/operations/mod.rs index f67ada2efa3..ca7ba02d711 100644 --- a/packages/rs-drive/src/util/operations/mod.rs +++ b/packages/rs-drive/src/util/operations/mod.rs @@ -5,3 +5,6 @@ mod apply_partial_batch_low_level_drive_operations; mod commit_transaction; mod drop_cache; mod rollback_transaction; + +#[cfg(test)] +mod tests; diff --git a/packages/rs-drive/src/util/operations/tests.rs b/packages/rs-drive/src/util/operations/tests.rs new file mode 100644 index 00000000000..4c21d6e112a --- /dev/null +++ b/packages/rs-drive/src/util/operations/tests.rs @@ -0,0 +1,417 @@ +//! Inline tests for the `util/operations` module. +//! +//! These tests exercise the real logic of the batch application helpers, +//! the transaction commit / rollback helpers, and `drop_cache`. +//! +//! Per project test-writing guidance we do **not** include version-dispatch +//! tests here — every test exercises meaningful behaviour. + +use crate::error::Error; +use crate::fees::op::{FunctionOp, HashFunction, LowLevelDriveOperation}; +use crate::util::batch::grovedb_op_batch::GroveDbOpBatchV0Methods; +use crate::util::batch::GroveDbOpBatch; +use crate::util::test_helpers::setup::{setup_drive, setup_drive_with_initial_state_structure}; +use dpp::version::PlatformVersion; +use grovedb::batch::QualifiedGroveDbOp; +use grovedb::Element; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/// Produces a deterministic `FunctionOperation` for leftover-partition testing. +fn function_op() -> LowLevelDriveOperation { + LowLevelDriveOperation::FunctionOperation(FunctionOp::new_with_round_count( + HashFunction::Sha256, + 3, + )) +} + +// --------------------------------------------------------------------------- +// apply_batch_low_level_drive_operations_v0 +// --------------------------------------------------------------------------- + +/// Empty batch → no grove ops, nothing to apply, no leftovers. Must succeed. +#[test] +fn apply_batch_low_level_empty_batch_is_ok_and_drive_operations_stay_empty() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + let mut drive_operations: Vec = vec![]; + + drive + .apply_batch_low_level_drive_operations_v0( + None, + None, + vec![], + &mut drive_operations, + &platform_version.drive, + ) + .expect("empty batch should succeed"); + + // An empty batch must produce no drive_operations output since + // apply_batch_grovedb_operations is skipped and there are no leftovers. + assert!(drive_operations.is_empty()); +} + +/// Pure non-grove leftover ops must bypass grove and be appended to +/// `drive_operations` verbatim. +#[test] +fn apply_batch_low_level_only_leftovers_skips_grove_and_appends_them() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let batch_ops = vec![function_op(), function_op()]; + let mut drive_operations: Vec = vec![]; + + drive + .apply_batch_low_level_drive_operations_v0( + None, + None, + batch_ops, + &mut drive_operations, + &platform_version.drive, + ) + .expect("should succeed with only non-grove ops"); + + assert_eq!(drive_operations.len(), 2); + assert!(drive_operations + .iter() + .all(|op| matches!(op, LowLevelDriveOperation::FunctionOperation(_)))); +} + +/// Mixed batch: grove ops must be applied, non-grove ops must appear in +/// `drive_operations` at the end. +#[test] +fn apply_batch_low_level_mixed_ops_applies_grove_and_preserves_leftovers() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + // Two grove insertions into an existing root subtree (balances) and one + // non-grove op. We use the balances root subtree (key 0x02 at depth 0) + // because `setup_drive_with_initial_state_structure` created it. Instead + // of guessing paths, we build grove ops against an already-existing + // subtree by inserting at a *newly created* subtree first. + let batch_ops = vec![ + LowLevelDriveOperation::GroveOperation(QualifiedGroveDbOp::insert_or_replace_op( + vec![], + vec![0xfe], + Element::empty_tree(), + )), + function_op(), + LowLevelDriveOperation::GroveOperation(QualifiedGroveDbOp::insert_or_replace_op( + vec![vec![0xfe]], + vec![0x01], + Element::new_item(vec![1, 2, 3]), + )), + ]; + let mut drive_operations: Vec = vec![]; + + drive + .apply_batch_low_level_drive_operations_v0( + None, + None, + batch_ops, + &mut drive_operations, + &platform_version.drive, + ) + .expect("mixed batch should apply grove ops and preserve leftovers"); + + // Expect exactly one appended leftover (the function op). + assert_eq!( + drive_operations + .iter() + .filter(|op| matches!(op, LowLevelDriveOperation::FunctionOperation(_))) + .count(), + 1, + "function op should be preserved as a leftover" + ); + // Grove application itself records CalculatedCostOperation entries. Make + // sure we observe at least one cost entry from the grove apply call. + assert!( + drive_operations + .iter() + .any(|op| matches!(op, LowLevelDriveOperation::CalculatedCostOperation(_))), + "expected at least one CalculatedCostOperation from grove apply" + ); +} + +/// Inserted value must be readable back, proving the batch was materialized. +#[test] +fn apply_batch_low_level_actually_persists_grove_inserts() { + use crate::util::grove_operations::DirectQueryType; + use grovedb_path::SubtreePath; + + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let batch_ops = vec![ + LowLevelDriveOperation::GroveOperation(QualifiedGroveDbOp::insert_or_replace_op( + vec![], + vec![0xfd], + Element::empty_tree(), + )), + LowLevelDriveOperation::GroveOperation(QualifiedGroveDbOp::insert_or_replace_op( + vec![vec![0xfd]], + b"hello".to_vec(), + Element::new_item(b"world".to_vec()), + )), + ]; + let mut drive_operations: Vec = vec![]; + + drive + .apply_batch_low_level_drive_operations_v0( + None, + None, + batch_ops, + &mut drive_operations, + &platform_version.drive, + ) + .expect("persist batch"); + + // Read the value back through Drive's typed grove helper. + let path: Vec> = vec![vec![0xfd]]; + let path_refs: Vec<&[u8]> = path.iter().map(|v| v.as_slice()).collect(); + let subtree_path = SubtreePath::from(path_refs.as_slice()); + + let mut cost_ops: Vec = vec![]; + let fetched = drive + .grove_get_raw_optional_item( + subtree_path, + b"hello", + DirectQueryType::StatefulDirectQuery, + None, + &mut cost_ops, + &platform_version.drive, + ) + .expect("grove get must succeed"); + + assert_eq!( + fetched, + Some(b"world".to_vec()), + "inserted value must be readable back after batch apply" + ); +} + +// --------------------------------------------------------------------------- +// apply_batch_grovedb_operations_v0 — error path: empty batch +// --------------------------------------------------------------------------- + +/// A non-empty wrapping batch with no actual grove ops inside the GroveDbOpBatch +/// funnelled through `apply_batch_grovedb_operations` (applied branch) must +/// return `BatchIsEmpty` — this proves the error bubble-through works. +#[test] +fn apply_batch_grovedb_empty_ops_returns_batch_is_empty_error() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + let mut drive_operations: Vec = vec![]; + + let err = drive + .apply_batch_grovedb_operations( + None, + None, + GroveDbOpBatch::new(), + &mut drive_operations, + &platform_version.drive, + ) + .expect_err("empty batch must error in applied branch"); + + let msg = format!("{:?}", err); + assert!( + msg.contains("BatchIsEmpty"), + "expected BatchIsEmpty, got: {msg}" + ); +} + +// --------------------------------------------------------------------------- +// apply_partial_batch_low_level_drive_operations_v0 (deprecated but still +// part of the covered tree — exercise it at least once) +// --------------------------------------------------------------------------- + +/// Partial batch with only non-grove leftovers: the inner apply returns an +/// empty-batch error. The `#[deprecated]` function still routes through this +/// path, so we assert that error reaches the caller. +#[allow(deprecated)] +#[test] +fn apply_partial_batch_low_level_only_leftovers_surfaces_empty_batch_error() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + let mut drive_operations: Vec = vec![]; + + let err = drive + .apply_partial_batch_low_level_drive_operations_v0( + None, + None, + vec![function_op()], + |_cost, _ops_by_level| Ok(vec![]), + &mut drive_operations, + &platform_version.drive, + ) + .expect_err("expected inner BatchIsEmpty error to surface"); + + let msg = format!("{:?}", err); + assert!( + msg.contains("BatchIsEmpty"), + "expected BatchIsEmpty, got: {msg}" + ); +} + +// --------------------------------------------------------------------------- +// commit_transaction_v0 / rollback_transaction_v0 +// --------------------------------------------------------------------------- + +/// Starting a fresh transaction and committing it with `commit_transaction_v0` +/// must be OK even when no operations were performed. +#[test] +fn commit_transaction_v0_succeeds_on_empty_transaction() { + let drive = setup_drive_with_initial_state_structure(None); + let tx = drive.grove.start_transaction(); + drive + .commit_transaction_v0(tx) + .expect("commit of an untouched transaction must succeed"); +} + +/// Writes inside a transaction must disappear after `rollback_transaction_v0`. +#[test] +fn rollback_transaction_v0_discards_uncommitted_changes() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let tx = drive.grove.start_transaction(); + + // Build ops inside the transaction. + let batch_ops = vec![ + LowLevelDriveOperation::GroveOperation(QualifiedGroveDbOp::insert_or_replace_op( + vec![], + vec![0xfb], + Element::empty_tree(), + )), + LowLevelDriveOperation::GroveOperation(QualifiedGroveDbOp::insert_or_replace_op( + vec![vec![0xfb]], + b"k".to_vec(), + Element::new_item(b"v".to_vec()), + )), + ]; + let mut drive_operations: Vec = vec![]; + drive + .apply_batch_low_level_drive_operations_v0( + None, + Some(&tx), + batch_ops, + &mut drive_operations, + &platform_version.drive, + ) + .expect("apply in transaction"); + + // Roll back the transaction before it is committed. + drive + .rollback_transaction_v0(&tx) + .expect("rollback should succeed"); + + // Committing the rolled-back (hence empty) transaction must still succeed. + drive + .commit_transaction_v0(tx) + .expect("commit of a rolled-back transaction should succeed"); + + // After rollback + commit, trying to apply a new batch that would depend + // on the rolled-back subtree existing should fail (the subtree is gone). + // + // We verify this indirectly by attempting an insertion into the subtree + // path we *thought* we created. With consistency verification enabled, + // grovedb will error because the parent tree does not exist. + let bad_batch = vec![LowLevelDriveOperation::GroveOperation( + QualifiedGroveDbOp::insert_or_replace_op( + vec![vec![0xfb]], + b"k2".to_vec(), + Element::new_item(b"v2".to_vec()), + ), + )]; + let mut drive_operations: Vec = vec![]; + let res = drive.apply_batch_low_level_drive_operations_v0( + None, + None, + bad_batch, + &mut drive_operations, + &platform_version.drive, + ); + assert!( + res.is_err(), + "rolled back subtree should no longer be present; expected apply to fail" + ); +} + +// --------------------------------------------------------------------------- +// drop_cache_v0 +// --------------------------------------------------------------------------- + +/// After setting a non-default genesis_time_ms in the cache, `drop_cache_v0` +/// must reset it back to the config's default (None for a bare drive). +#[test] +fn drop_cache_v0_resets_genesis_time_and_protocol_versions() { + let drive = setup_drive(None); + + // Mutate the caches to non-default values. + { + let mut g = drive.cache.genesis_time_ms.write(); + *g = Some(123_456_789); + } + + // Sanity check that we actually wrote something. + assert_eq!(*drive.cache.genesis_time_ms.read(), Some(123_456_789)); + + drive.drop_cache_v0(); + + // The default for a bare Drive is None. + assert_eq!( + *drive.cache.genesis_time_ms.read(), + None, + "drop_cache should reset genesis_time_ms back to config default" + ); + // protocol_versions_counter cache should have been replaced by a new + // empty cache — we don't make assumptions about its internals beyond + // that it's readable. + let _ = drive.cache.protocol_versions_counter.read(); +} + +// --------------------------------------------------------------------------- +// apply_batch_low_level_drive_operations (top-level, non-v0) — exercise +// the real logic once through the public API. Not a version-dispatch test. +// --------------------------------------------------------------------------- + +/// Calling through the public entry point `apply_batch_low_level_drive_operations` +/// with a mixed batch must succeed (proves wiring between the public +/// function and `_v0`). +#[test] +fn public_apply_batch_low_level_drive_operations_routes_through_v0() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let batch_ops = vec![ + LowLevelDriveOperation::GroveOperation(QualifiedGroveDbOp::insert_or_replace_op( + vec![], + vec![0xfa], + Element::empty_tree(), + )), + LowLevelDriveOperation::GroveOperation(QualifiedGroveDbOp::insert_or_replace_op( + vec![vec![0xfa]], + b"x".to_vec(), + Element::new_item(b"y".to_vec()), + )), + ]; + let mut drive_operations: Vec = vec![]; + + let res = drive.apply_batch_low_level_drive_operations( + None, + None, + batch_ops, + &mut drive_operations, + &platform_version.drive, + ); + // On the latest platform version, we route to v0 and the call succeeds. + match res { + Ok(()) => {} + Err(Error::Drive(crate::error::drive::DriveError::UnknownVersionMismatch { .. })) => { + panic!("unexpected version mismatch on latest platform version"); + } + Err(e) => panic!("unexpected error: {:?}", e), + } +}