Skip to content

Commit 45852dd

Browse files
fix(sdk): verify no-limit vote queries against the page Drive proves (#5483)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 5fa100e commit 45852dd

2 files changed

Lines changed: 291 additions & 23 deletions

File tree

  • packages
    • rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote
    • rs-drive-proof-verifier/src

‎packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/mod.rs‎

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,14 @@ mod tests {
190190

191191
mod contests_requests_query {
192192
use super::*;
193+
use crate::config::PlatformConfig;
193194
use dapi_grpc::platform::v0::get_contested_resources_request::get_contested_resources_request_v0;
195+
use dpp::dashcore::Network;
196+
use dpp::data_contract::TokenConfiguration;
197+
use dpp::prelude::CoreBlockHeight;
198+
use drive::config::DEFAULT_QUERY_LIMIT;
199+
use drive_proof_verifier::types::{ContestedResources, VotePollsGroupedByTimestamp};
200+
use drive_proof_verifier::{ContextProvider, ContextProviderError, FromProof};
194201

195202
#[tokio::test]
196203
async fn test_not_proved_contests_request() {
@@ -393,6 +400,234 @@ mod tests {
393400
assert_eq!(contests.len(), 2);
394401
}
395402

403+
/// Serves the DPNS contract and no quorum keys, so `FromProof` checks the Drive part
404+
/// of a proof and then stops at the quorum key the signature check asks for
405+
struct DpnsOnlyContextProvider(Arc<DataContract>);
406+
407+
impl ContextProvider for DpnsOnlyContextProvider {
408+
fn get_data_contract(
409+
&self,
410+
id: &Identifier,
411+
_platform_version: &PlatformVersion,
412+
) -> Result<Option<Arc<DataContract>>, ContextProviderError> {
413+
Ok((*id == self.0.id()).then(|| Arc::clone(&self.0)))
414+
}
415+
416+
fn get_token_configuration(
417+
&self,
418+
_token_id: &Identifier,
419+
) -> Result<Option<TokenConfiguration>, ContextProviderError> {
420+
Ok(None)
421+
}
422+
423+
fn get_quorum_public_key(
424+
&self,
425+
_quorum_type: u32,
426+
_quorum_hash: [u8; 32],
427+
_core_chain_locked_height: u32,
428+
) -> Result<[u8; 48], ContextProviderError> {
429+
Err(ContextProviderError::Config(
430+
"no quorum keys in this test".to_string(),
431+
))
432+
}
433+
434+
fn get_platform_activation_height(
435+
&self,
436+
) -> Result<CoreBlockHeight, ContextProviderError> {
437+
Ok(1)
438+
}
439+
}
440+
441+
fn assert_drive_part_of_proof_verified(error: drive_proof_verifier::Error) {
442+
assert!(
443+
matches!(
444+
error,
445+
drive_proof_verifier::Error::QuorumKeyUnavailable { .. }
446+
),
447+
"expected the Drive part of the proof to verify and the signature check to \
448+
stop at the quorum key, got {error:?}"
449+
);
450+
}
451+
452+
/// Drive answers a proved request naming no limit with a page of the node's
453+
/// `default_query_limit` and proves that page. On a node keeping the default (100),
454+
/// the proof of a full page verifies only as that page
455+
#[tokio::test]
456+
async fn should_verify_a_full_default_page_of_contests_requested_with_no_limit() {
457+
verify_101_contests_requested_with_no_limit(DEFAULT_QUERY_LIMIT).await;
458+
}
459+
460+
/// On a node configured with a larger default page, every contest fits it and the
461+
/// proof verifies only as the whole range
462+
#[tokio::test]
463+
async fn should_verify_every_contest_requested_with_no_limit_from_a_node_with_a_larger_default(
464+
) {
465+
verify_101_contests_requested_with_no_limit(200).await;
466+
}
467+
468+
/// Creates 101 contests on a node whose `default_query_limit` is
469+
/// `node_default_query_limit`, and checks that `FromProof` verifies the Drive part of
470+
/// the proofs of a contested resources request and a vote polls by end date request
471+
/// naming no limit
472+
async fn verify_101_contests_requested_with_no_limit(node_default_query_limit: u16) {
473+
let platform_version = PlatformVersion::latest();
474+
let mut config = PlatformConfig::default_testnet();
475+
config.drive.default_query_limit = node_default_query_limit;
476+
let mut platform = TestPlatformBuilder::new()
477+
.with_config(config)
478+
.with_latest_protocol_version()
479+
.build_with_mock_rpc()
480+
.set_genesis_state();
481+
482+
let platform_state = platform.state.load();
483+
484+
// One contest more than the standard default page (100). The suffix leaves out the letters
485+
// DPNS normalizes to digits (i, l and o), which would merge two names into one contest
486+
const SUFFIX_LETTERS: &[u8] = b"abcdefghjkmnpqrstuvwxyz";
487+
let mut dpns_contract = None;
488+
for n in 0..=DEFAULT_QUERY_LIMIT as u64 {
489+
let letter = |i: u64| SUFFIX_LETTERS[i as usize % SUFFIX_LETTERS.len()] as char;
490+
let name = format!(
491+
"contested{}{}",
492+
letter(n / SUFFIX_LETTERS.len() as u64),
493+
letter(n)
494+
);
495+
let (_, _, contract) = create_dpns_identity_name_contest(
496+
&mut platform,
497+
&platform_state,
498+
1_000 + n,
499+
&name,
500+
platform_version,
501+
)
502+
.await;
503+
dpns_contract = Some(contract);
504+
}
505+
let dpns_contract = dpns_contract.expect("expected the dpns contract");
506+
let provider = DpnsOnlyContextProvider(Arc::clone(&dpns_contract));
507+
508+
let domain = dpns_contract
509+
.document_type_for_name("domain")
510+
.expect("expected the domain document type");
511+
let index_name = "parentNameAndLabel".to_string();
512+
let dash_encoded = bincode::encode_to_vec(
513+
Value::Text("dash".to_string()),
514+
bincode::config::standard()
515+
.with_big_endian()
516+
.with_no_limit(),
517+
)
518+
.expect("expected to encode value");
519+
520+
let request = GetContestedResourcesRequest {
521+
version: Some(get_contested_resources_request::Version::V0(
522+
GetContestedResourcesRequestV0 {
523+
contract_id: dpns_contract.id().to_vec(),
524+
document_type_name: domain.name().clone(),
525+
index_name: index_name.clone(),
526+
start_index_values: vec![dash_encoded],
527+
end_index_values: vec![],
528+
start_at_value_info: None,
529+
count: None,
530+
order_ascending: true,
531+
prove: true,
532+
},
533+
)),
534+
};
535+
let response = platform
536+
.query_contested_resources(request.clone(), &platform_state, platform_version)
537+
.expect("expected to execute query")
538+
.into_data()
539+
.expect("expected query to be valid");
540+
541+
let Some(get_contested_resources_response::Version::V0(
542+
GetContestedResourcesResponseV0 {
543+
result: Some(get_contested_resources_response_v0::Result::Proof(proof)),
544+
..
545+
},
546+
)) = response.version.clone()
547+
else {
548+
panic!("expected a proof")
549+
};
550+
551+
// Drive proved a page of the node's default: checked against the whole range and
552+
// against a page of the standard default, only the check matching that page holds
553+
let contests_count = DEFAULT_QUERY_LIMIT as usize + 1;
554+
let mut query = ResolvedVotePollsByDocumentTypeQuery {
555+
contract: DataContractResolvedInfo::BorrowedDataContract(
556+
dpns_contract.as_ref(),
557+
),
558+
document_type_name: domain.name(),
559+
index_name: &index_name,
560+
start_index_values: &vec!["dash".into()],
561+
end_index_values: &vec![],
562+
limit: None,
563+
order_ascending: true,
564+
start_at_value: &None,
565+
};
566+
let whole_range =
567+
query.verify_contests_proof(proof.grovedb_proof.as_ref(), platform_version);
568+
query.limit = Some(DEFAULT_QUERY_LIMIT);
569+
let standard_page =
570+
query.verify_contests_proof(proof.grovedb_proof.as_ref(), platform_version);
571+
if (node_default_query_limit as usize) < contests_count {
572+
whole_range
573+
.expect_err("expected the proof of a full page to fail with no limit");
574+
let (_, contests) =
575+
standard_page.expect("expected the proof to verify as a default page");
576+
assert_eq!(contests.len(), DEFAULT_QUERY_LIMIT as usize);
577+
} else {
578+
let (_, contests) =
579+
whole_range.expect("expected the proof to verify as the whole range");
580+
assert_eq!(contests.len(), contests_count);
581+
standard_page
582+
.expect_err("expected the proof of every contest to fail as a 100 page");
583+
}
584+
585+
let error = <ContestedResources as FromProof<GetContestedResourcesRequest>>::maybe_from_proof_with_metadata(
586+
request,
587+
response,
588+
Network::Testnet,
589+
platform_version,
590+
&provider,
591+
)
592+
.expect_err("expected no quorum key");
593+
assert_drive_part_of_proof_verified(error);
594+
595+
// The same contests read by end date: they all end at once
596+
let request = GetVotePollsByEndDateRequest {
597+
version: Some(get_vote_polls_by_end_date_request::Version::V0(
598+
GetVotePollsByEndDateRequestV0 {
599+
start_time_info: None,
600+
end_time_info: None,
601+
limit: None,
602+
offset: None,
603+
ascending: true,
604+
prove: true,
605+
},
606+
)),
607+
};
608+
let response = platform
609+
.query_vote_polls_by_end_date_query(
610+
request.clone(),
611+
&platform_state,
612+
platform_version,
613+
)
614+
.expect("expected to execute query")
615+
.into_data()
616+
.expect("expected query to be valid");
617+
618+
let error = <VotePollsGroupedByTimestamp as FromProof<
619+
GetVotePollsByEndDateRequest,
620+
>>::maybe_from_proof_with_metadata(
621+
request,
622+
response,
623+
Network::Testnet,
624+
platform_version,
625+
&provider,
626+
)
627+
.expect_err("expected no quorum key");
628+
assert_drive_part_of_proof_verified(error);
629+
}
630+
396631
#[tokio::test]
397632
async fn test_empty_string_start_index_value() {
398633
let platform_version = PlatformVersion::latest();

‎packages/rs-drive-proof-verifier/src/proof.rs‎

Lines changed: 56 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,15 @@ use drive::error::proof::ProofError;
9898
use drive::grovedb::GroveTrunkQueryResult;
9999
use drive::query::contested_resource_votes_given_by_identity_query::ContestedResourceVotesGivenByIdentityQuery;
100100
use drive::query::proposer_block_count_query::ProposerQueryType;
101-
use drive::query::vote_poll_contestant_votes_query::ContestedDocumentVotePollVotesDriveQuery;
102-
use drive::query::vote_poll_vote_state_query::ContestedDocumentVotePollDriveQuery;
103-
use drive::query::vote_polls_by_document_type_query::VotePollsByDocumentTypeQuery;
101+
use drive::query::vote_poll_contestant_votes_query::{
102+
ContestedDocumentVotePollVotesDriveQuery, ResolvedContestedDocumentVotePollVotesDriveQuery,
103+
};
104+
use drive::query::vote_poll_vote_state_query::{
105+
ContestedDocumentVotePollDriveQuery, ResolvedContestedDocumentVotePollDriveQuery,
106+
};
107+
use drive::query::vote_polls_by_document_type_query::{
108+
ResolvedVotePollsByDocumentTypeQuery, VotePollsByDocumentTypeQuery,
109+
};
104110
use drive::query::{DriveDocumentQuery, VotePollsByEndDateDriveQuery};
105111
use indexmap::IndexMap;
106112
use std::array::TryFromSliceError;
@@ -1988,10 +1994,16 @@ impl FromProof<platform::GetContestedResourcesRequest> for ContestedResources {
19881994
// Parse response to read proof and metadata
19891995
let proof = response.proof().or(Err(Error::NoProofInResult))?;
19901996
let mtd = response.metadata().or(Err(Error::EmptyResponseMetadata))?;
1997+
let grovedb_proof = supported_grovedb_proof_bytes(proof)?;
19911998

1992-
let (root_hash, items) = resolved_request
1993-
.verify_contests_proof(supported_grovedb_proof_bytes(proof)?, platform_version)
1994-
.map_drive_error(proof, mtd)?;
1999+
let (root_hash, items) = verify_page_drive_proves(resolved_request.limit, |limit| {
2000+
ResolvedVotePollsByDocumentTypeQuery {
2001+
limit,
2002+
..resolved_request.clone()
2003+
}
2004+
.verify_contests_proof(grovedb_proof, platform_version)
2005+
})
2006+
.map_drive_error(proof, mtd)?;
19952007

19962008
verify_tenderdash_proof(proof, mtd, &root_hash, provider)?;
19972009

@@ -2030,12 +2042,16 @@ impl FromProof<platform::GetContestedResourceVoteStateRequest> for Contenders {
20302042
// Parse response to read proof and metadata
20312043
let proof = response.proof().or(Err(Error::NoProofInResult))?;
20322044
let mtd = response.metadata().or(Err(Error::EmptyResponseMetadata))?;
2045+
let grovedb_proof = supported_grovedb_proof_bytes(proof)?;
20332046

2034-
let (root_hash, contested_resource_vote_state) = resolved_request
2035-
.verify_vote_poll_vote_state_proof(
2036-
supported_grovedb_proof_bytes(proof)?,
2037-
platform_version,
2038-
)
2047+
let (root_hash, contested_resource_vote_state) =
2048+
verify_page_drive_proves(resolved_request.limit, |limit| {
2049+
ResolvedContestedDocumentVotePollDriveQuery {
2050+
limit,
2051+
..resolved_request.clone()
2052+
}
2053+
.verify_vote_poll_vote_state_proof(grovedb_proof, platform_version)
2054+
})
20392055
.map_drive_error(proof, mtd)?;
20402056

20412057
verify_tenderdash_proof(proof, mtd, &root_hash, provider)?;
@@ -2085,10 +2101,16 @@ impl FromProof<GetContestedResourceVotersForIdentityRequest> for Voters {
20852101
// Parse response to read proof and metadata
20862102
let proof = response.proof().or(Err(Error::NoProofInResult))?;
20872103
let mtd = response.metadata().or(Err(Error::EmptyResponseMetadata))?;
2104+
let grovedb_proof = supported_grovedb_proof_bytes(proof)?;
20882105

2089-
let (root_hash, voters) = resolved_request
2090-
.verify_vote_poll_votes_proof(supported_grovedb_proof_bytes(proof)?, platform_version)
2091-
.map_drive_error(proof, mtd)?;
2106+
let (root_hash, voters) = verify_page_drive_proves(resolved_request.limit, |limit| {
2107+
ResolvedContestedDocumentVotePollVotesDriveQuery {
2108+
limit,
2109+
..resolved_request.clone()
2110+
}
2111+
.verify_vote_poll_votes_proof(grovedb_proof, platform_version)
2112+
})
2113+
.map_drive_error(proof, mtd)?;
20922114

20932115
verify_tenderdash_proof(proof, mtd, &root_hash, provider)?;
20942116

@@ -2125,14 +2147,21 @@ impl FromProof<platform::GetContestedResourceIdentityVotesRequest> for ResourceV
21252147
let proof = response.proof().or(Err(Error::NoProofInResult))?;
21262148
let mtd = response.metadata().or(Err(Error::EmptyResponseMetadata))?;
21272149

2150+
let grovedb_proof = supported_grovedb_proof_bytes(proof)?;
2151+
21282152
let contract_provider_fn = provider.as_contract_lookup_fn(platform_version);
2129-
let (root_hash, voters) = drive_query
2153+
let (root_hash, voters) = verify_page_drive_proves(drive_query.limit, |limit| {
2154+
ContestedResourceVotesGivenByIdentityQuery {
2155+
limit,
2156+
..drive_query.clone()
2157+
}
21302158
.verify_identity_votes_given_proof::<Vec<_>>(
2131-
supported_grovedb_proof_bytes(proof)?,
2159+
grovedb_proof,
21322160
&contract_provider_fn,
21332161
platform_version,
21342162
)
2135-
.map_drive_error(proof, mtd)?;
2163+
})
2164+
.map_drive_error(proof, mtd)?;
21362165

21372166
verify_tenderdash_proof(proof, mtd, &root_hash, provider)?;
21382167

@@ -2169,12 +2198,16 @@ impl FromProof<platform::GetVotePollsByEndDateRequest> for VotePollsGroupedByTim
21692198
let proof = response.proof().or(Err(Error::NoProofInResult))?;
21702199
let mtd = response.metadata().or(Err(Error::EmptyResponseMetadata))?;
21712200

2172-
let (root_hash, vote_polls) = drive_query
2173-
.verify_vote_polls_by_end_date_proof::<Vec<(_, _)>>(
2174-
supported_grovedb_proof_bytes(proof)?,
2175-
platform_version,
2176-
)
2177-
.map_drive_error(proof, mtd)?;
2201+
let grovedb_proof = supported_grovedb_proof_bytes(proof)?;
2202+
2203+
let (root_hash, vote_polls) = verify_page_drive_proves(drive_query.limit, |limit| {
2204+
VotePollsByEndDateDriveQuery {
2205+
limit,
2206+
..drive_query.clone()
2207+
}
2208+
.verify_vote_polls_by_end_date_proof::<Vec<(_, _)>>(grovedb_proof, platform_version)
2209+
})
2210+
.map_drive_error(proof, mtd)?;
21782211

21792212
verify_tenderdash_proof(proof, mtd, &root_hash, provider)?;
21802213

0 commit comments

Comments
 (0)