diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/mod.rs index 3fbb22bf596..256ca3b4718 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/mod.rs @@ -190,7 +190,14 @@ mod tests { mod contests_requests_query { use super::*; + use crate::config::PlatformConfig; use dapi_grpc::platform::v0::get_contested_resources_request::get_contested_resources_request_v0; + use dpp::dashcore::Network; + use dpp::data_contract::TokenConfiguration; + use dpp::prelude::CoreBlockHeight; + use drive::config::DEFAULT_QUERY_LIMIT; + use drive_proof_verifier::types::{ContestedResources, VotePollsGroupedByTimestamp}; + use drive_proof_verifier::{ContextProvider, ContextProviderError, FromProof}; #[tokio::test] async fn test_not_proved_contests_request() { @@ -393,6 +400,234 @@ mod tests { assert_eq!(contests.len(), 2); } + /// Serves the DPNS contract and no quorum keys, so `FromProof` checks the Drive part + /// of a proof and then stops at the quorum key the signature check asks for + struct DpnsOnlyContextProvider(Arc); + + impl ContextProvider for DpnsOnlyContextProvider { + fn get_data_contract( + &self, + id: &Identifier, + _platform_version: &PlatformVersion, + ) -> Result>, ContextProviderError> { + Ok((*id == self.0.id()).then(|| Arc::clone(&self.0))) + } + + fn get_token_configuration( + &self, + _token_id: &Identifier, + ) -> Result, ContextProviderError> { + Ok(None) + } + + fn get_quorum_public_key( + &self, + _quorum_type: u32, + _quorum_hash: [u8; 32], + _core_chain_locked_height: u32, + ) -> Result<[u8; 48], ContextProviderError> { + Err(ContextProviderError::Config( + "no quorum keys in this test".to_string(), + )) + } + + fn get_platform_activation_height( + &self, + ) -> Result { + Ok(1) + } + } + + fn assert_drive_part_of_proof_verified(error: drive_proof_verifier::Error) { + assert!( + matches!( + error, + drive_proof_verifier::Error::QuorumKeyUnavailable { .. } + ), + "expected the Drive part of the proof to verify and the signature check to \ + stop at the quorum key, got {error:?}" + ); + } + + /// Drive answers a proved request naming no limit with a page of the node's + /// `default_query_limit` and proves that page. On a node keeping the default (100), + /// the proof of a full page verifies only as that page + #[tokio::test] + async fn should_verify_a_full_default_page_of_contests_requested_with_no_limit() { + verify_101_contests_requested_with_no_limit(DEFAULT_QUERY_LIMIT).await; + } + + /// On a node configured with a larger default page, every contest fits it and the + /// proof verifies only as the whole range + #[tokio::test] + async fn should_verify_every_contest_requested_with_no_limit_from_a_node_with_a_larger_default( + ) { + verify_101_contests_requested_with_no_limit(200).await; + } + + /// Creates 101 contests on a node whose `default_query_limit` is + /// `node_default_query_limit`, and checks that `FromProof` verifies the Drive part of + /// the proofs of a contested resources request and a vote polls by end date request + /// naming no limit + async fn verify_101_contests_requested_with_no_limit(node_default_query_limit: u16) { + let platform_version = PlatformVersion::latest(); + let mut config = PlatformConfig::default_testnet(); + config.drive.default_query_limit = node_default_query_limit; + let mut platform = TestPlatformBuilder::new() + .with_config(config) + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_genesis_state(); + + let platform_state = platform.state.load(); + + // One contest more than the standard default page (100). The suffix leaves out the letters + // DPNS normalizes to digits (i, l and o), which would merge two names into one contest + const SUFFIX_LETTERS: &[u8] = b"abcdefghjkmnpqrstuvwxyz"; + let mut dpns_contract = None; + for n in 0..=DEFAULT_QUERY_LIMIT as u64 { + let letter = |i: u64| SUFFIX_LETTERS[i as usize % SUFFIX_LETTERS.len()] as char; + let name = format!( + "contested{}{}", + letter(n / SUFFIX_LETTERS.len() as u64), + letter(n) + ); + let (_, _, contract) = create_dpns_identity_name_contest( + &mut platform, + &platform_state, + 1_000 + n, + &name, + platform_version, + ) + .await; + dpns_contract = Some(contract); + } + let dpns_contract = dpns_contract.expect("expected the dpns contract"); + let provider = DpnsOnlyContextProvider(Arc::clone(&dpns_contract)); + + let domain = dpns_contract + .document_type_for_name("domain") + .expect("expected the domain document type"); + let index_name = "parentNameAndLabel".to_string(); + let dash_encoded = bincode::encode_to_vec( + Value::Text("dash".to_string()), + bincode::config::standard() + .with_big_endian() + .with_no_limit(), + ) + .expect("expected to encode value"); + + let request = GetContestedResourcesRequest { + version: Some(get_contested_resources_request::Version::V0( + GetContestedResourcesRequestV0 { + contract_id: dpns_contract.id().to_vec(), + document_type_name: domain.name().clone(), + index_name: index_name.clone(), + start_index_values: vec![dash_encoded], + end_index_values: vec![], + start_at_value_info: None, + count: None, + order_ascending: true, + prove: true, + }, + )), + }; + let response = platform + .query_contested_resources(request.clone(), &platform_state, platform_version) + .expect("expected to execute query") + .into_data() + .expect("expected query to be valid"); + + let Some(get_contested_resources_response::Version::V0( + GetContestedResourcesResponseV0 { + result: Some(get_contested_resources_response_v0::Result::Proof(proof)), + .. + }, + )) = response.version.clone() + else { + panic!("expected a proof") + }; + + // Drive proved a page of the node's default: checked against the whole range and + // against a page of the standard default, only the check matching that page holds + let contests_count = DEFAULT_QUERY_LIMIT as usize + 1; + let mut query = ResolvedVotePollsByDocumentTypeQuery { + contract: DataContractResolvedInfo::BorrowedDataContract( + dpns_contract.as_ref(), + ), + document_type_name: domain.name(), + index_name: &index_name, + start_index_values: &vec!["dash".into()], + end_index_values: &vec![], + limit: None, + order_ascending: true, + start_at_value: &None, + }; + let whole_range = + query.verify_contests_proof(proof.grovedb_proof.as_ref(), platform_version); + query.limit = Some(DEFAULT_QUERY_LIMIT); + let standard_page = + query.verify_contests_proof(proof.grovedb_proof.as_ref(), platform_version); + if (node_default_query_limit as usize) < contests_count { + whole_range + .expect_err("expected the proof of a full page to fail with no limit"); + let (_, contests) = + standard_page.expect("expected the proof to verify as a default page"); + assert_eq!(contests.len(), DEFAULT_QUERY_LIMIT as usize); + } else { + let (_, contests) = + whole_range.expect("expected the proof to verify as the whole range"); + assert_eq!(contests.len(), contests_count); + standard_page + .expect_err("expected the proof of every contest to fail as a 100 page"); + } + + let error = >::maybe_from_proof_with_metadata( + request, + response, + Network::Testnet, + platform_version, + &provider, + ) + .expect_err("expected no quorum key"); + assert_drive_part_of_proof_verified(error); + + // The same contests read by end date: they all end at once + let request = GetVotePollsByEndDateRequest { + version: Some(get_vote_polls_by_end_date_request::Version::V0( + GetVotePollsByEndDateRequestV0 { + start_time_info: None, + end_time_info: None, + limit: None, + offset: None, + ascending: true, + prove: true, + }, + )), + }; + let response = platform + .query_vote_polls_by_end_date_query( + request.clone(), + &platform_state, + platform_version, + ) + .expect("expected to execute query") + .into_data() + .expect("expected query to be valid"); + + let error = >::maybe_from_proof_with_metadata( + request, + response, + Network::Testnet, + platform_version, + &provider, + ) + .expect_err("expected no quorum key"); + assert_drive_part_of_proof_verified(error); + } + #[tokio::test] async fn test_empty_string_start_index_value() { let platform_version = PlatformVersion::latest(); diff --git a/packages/rs-drive-proof-verifier/src/proof.rs b/packages/rs-drive-proof-verifier/src/proof.rs index 8f9d5cdc49b..a47798d9d41 100644 --- a/packages/rs-drive-proof-verifier/src/proof.rs +++ b/packages/rs-drive-proof-verifier/src/proof.rs @@ -98,9 +98,15 @@ use drive::error::proof::ProofError; use drive::grovedb::GroveTrunkQueryResult; use drive::query::contested_resource_votes_given_by_identity_query::ContestedResourceVotesGivenByIdentityQuery; use drive::query::proposer_block_count_query::ProposerQueryType; -use drive::query::vote_poll_contestant_votes_query::ContestedDocumentVotePollVotesDriveQuery; -use drive::query::vote_poll_vote_state_query::ContestedDocumentVotePollDriveQuery; -use drive::query::vote_polls_by_document_type_query::VotePollsByDocumentTypeQuery; +use drive::query::vote_poll_contestant_votes_query::{ + ContestedDocumentVotePollVotesDriveQuery, ResolvedContestedDocumentVotePollVotesDriveQuery, +}; +use drive::query::vote_poll_vote_state_query::{ + ContestedDocumentVotePollDriveQuery, ResolvedContestedDocumentVotePollDriveQuery, +}; +use drive::query::vote_polls_by_document_type_query::{ + ResolvedVotePollsByDocumentTypeQuery, VotePollsByDocumentTypeQuery, +}; use drive::query::{DriveDocumentQuery, VotePollsByEndDateDriveQuery}; use indexmap::IndexMap; use std::array::TryFromSliceError; @@ -1988,10 +1994,16 @@ impl FromProof for ContestedResources { // Parse response to read proof and metadata let proof = response.proof().or(Err(Error::NoProofInResult))?; let mtd = response.metadata().or(Err(Error::EmptyResponseMetadata))?; + let grovedb_proof = supported_grovedb_proof_bytes(proof)?; - let (root_hash, items) = resolved_request - .verify_contests_proof(supported_grovedb_proof_bytes(proof)?, platform_version) - .map_drive_error(proof, mtd)?; + let (root_hash, items) = verify_page_drive_proves(resolved_request.limit, |limit| { + ResolvedVotePollsByDocumentTypeQuery { + limit, + ..resolved_request.clone() + } + .verify_contests_proof(grovedb_proof, platform_version) + }) + .map_drive_error(proof, mtd)?; verify_tenderdash_proof(proof, mtd, &root_hash, provider)?; @@ -2030,12 +2042,16 @@ impl FromProof for Contenders { // Parse response to read proof and metadata let proof = response.proof().or(Err(Error::NoProofInResult))?; let mtd = response.metadata().or(Err(Error::EmptyResponseMetadata))?; + let grovedb_proof = supported_grovedb_proof_bytes(proof)?; - let (root_hash, contested_resource_vote_state) = resolved_request - .verify_vote_poll_vote_state_proof( - supported_grovedb_proof_bytes(proof)?, - platform_version, - ) + let (root_hash, contested_resource_vote_state) = + verify_page_drive_proves(resolved_request.limit, |limit| { + ResolvedContestedDocumentVotePollDriveQuery { + limit, + ..resolved_request.clone() + } + .verify_vote_poll_vote_state_proof(grovedb_proof, platform_version) + }) .map_drive_error(proof, mtd)?; verify_tenderdash_proof(proof, mtd, &root_hash, provider)?; @@ -2085,10 +2101,16 @@ impl FromProof for Voters { // Parse response to read proof and metadata let proof = response.proof().or(Err(Error::NoProofInResult))?; let mtd = response.metadata().or(Err(Error::EmptyResponseMetadata))?; + let grovedb_proof = supported_grovedb_proof_bytes(proof)?; - let (root_hash, voters) = resolved_request - .verify_vote_poll_votes_proof(supported_grovedb_proof_bytes(proof)?, platform_version) - .map_drive_error(proof, mtd)?; + let (root_hash, voters) = verify_page_drive_proves(resolved_request.limit, |limit| { + ResolvedContestedDocumentVotePollVotesDriveQuery { + limit, + ..resolved_request.clone() + } + .verify_vote_poll_votes_proof(grovedb_proof, platform_version) + }) + .map_drive_error(proof, mtd)?; verify_tenderdash_proof(proof, mtd, &root_hash, provider)?; @@ -2125,14 +2147,21 @@ impl FromProof for ResourceV let proof = response.proof().or(Err(Error::NoProofInResult))?; let mtd = response.metadata().or(Err(Error::EmptyResponseMetadata))?; + let grovedb_proof = supported_grovedb_proof_bytes(proof)?; + let contract_provider_fn = provider.as_contract_lookup_fn(platform_version); - let (root_hash, voters) = drive_query + let (root_hash, voters) = verify_page_drive_proves(drive_query.limit, |limit| { + ContestedResourceVotesGivenByIdentityQuery { + limit, + ..drive_query.clone() + } .verify_identity_votes_given_proof::>( - supported_grovedb_proof_bytes(proof)?, + grovedb_proof, &contract_provider_fn, platform_version, ) - .map_drive_error(proof, mtd)?; + }) + .map_drive_error(proof, mtd)?; verify_tenderdash_proof(proof, mtd, &root_hash, provider)?; @@ -2169,12 +2198,16 @@ impl FromProof for VotePollsGroupedByTim let proof = response.proof().or(Err(Error::NoProofInResult))?; let mtd = response.metadata().or(Err(Error::EmptyResponseMetadata))?; - let (root_hash, vote_polls) = drive_query - .verify_vote_polls_by_end_date_proof::>( - supported_grovedb_proof_bytes(proof)?, - platform_version, - ) - .map_drive_error(proof, mtd)?; + let grovedb_proof = supported_grovedb_proof_bytes(proof)?; + + let (root_hash, vote_polls) = verify_page_drive_proves(drive_query.limit, |limit| { + VotePollsByEndDateDriveQuery { + limit, + ..drive_query.clone() + } + .verify_vote_polls_by_end_date_proof::>(grovedb_proof, platform_version) + }) + .map_drive_error(proof, mtd)?; verify_tenderdash_proof(proof, mtd, &root_hash, provider)?;