diff --git a/packages/dash-platform-queries/src/documents/document_query.rs b/packages/dash-platform-queries/src/documents/document_query.rs index 523a1364de..113e6e1e47 100644 --- a/packages/dash-platform-queries/src/documents/document_query.rs +++ b/packages/dash-platform-queries/src/documents/document_query.rs @@ -30,6 +30,7 @@ use dpp::{ prelude::{DataContract, Identifier}, InvalidVectorSizeError, ProtocolError, }; +use drive::config::DEFAULT_QUERY_LIMIT; use drive::query::drive_document_ranked_query::mode_detection::ranked_order_key; use drive::query::{ DriveDocumentQuery, HavingAggregate, HavingAggregateFunction, HavingClause, HavingOperator, @@ -788,10 +789,28 @@ impl<'a> TryFrom<&'a DocumentQuery> for DriveDocumentQuery<'a> { ) .map_err(Error::Drive)?; - let limit = if request.limit != 0 { - Some(request.limit as u16) - } else { - None + // Mirror the limit contract of the server's + // `DriveDocumentQuery::from_typed_clauses` exactly: `0` (this + // struct's "unset" sentinel — V0's `limit: 0`, V1's + // `limit: None`) falls back to the server default, and anything + // above `DEFAULT_QUERY_LIMIT` (the `config.default_query_limit` + // every deployed server runs with) is refused with the server's + // own `QuerySyntaxError::InvalidLimit` rather than truncated or + // passed through. A `u16::try_from` alone would not do: limits + // 101..=65535 fit a `u16` but the server refuses them, so a raw + // `DriveDocumentQuery` carrying one would verify a proof no + // honest server could have produced. + let limit = match request.limit { + 0 => Some(DEFAULT_QUERY_LIMIT), + limit if limit > u32::from(DEFAULT_QUERY_LIMIT) => { + return Err(Error::Drive(drive::error::Error::Query( + drive::error::query::QuerySyntaxError::InvalidLimit(format!( + "limit {} greater than max limit {}", + limit, DEFAULT_QUERY_LIMIT + )), + ))); + } + limit => Some(limit as u16), }; let (start_at, start_at_included) = match request.start.as_ref() { diff --git a/packages/dash-platform-queries/tests/document_limit_lowering.rs b/packages/dash-platform-queries/tests/document_limit_lowering.rs new file mode 100644 index 0000000000..9f027c7c57 --- /dev/null +++ b/packages/dash-platform-queries/tests/document_limit_lowering.rs @@ -0,0 +1,66 @@ +//! The `DocumentQuery` → `DriveDocumentQuery` lowering must reproduce +//! the server's limit contract exactly: `SizedQuery::limit` is +//! proof-sensitive, so any divergence lets an untrusted transport pair +//! a request the server would refuse (or answer differently) with a +//! genuine proof produced for another query. + +use std::sync::Arc; + +use dash_platform_queries::documents::document_query::DocumentQuery; +use dash_platform_queries::Error; +use dpp::prelude::DataContract; +use dpp::tests::fixtures::get_data_contract_fixture; +use dpp::version::PlatformVersion; +use drive::query::DriveDocumentQuery; + +fn test_contract() -> Arc { + let platform_version = PlatformVersion::latest(); + Arc::new( + get_data_contract_fixture(None, 0, platform_version.protocol_version).data_contract_owned(), + ) +} + +/// The lowering mirrors `DriveDocumentQuery::from_typed_clauses`' +/// limit contract exactly: `0` = unset → the concrete server default +/// (`Some(DEFAULT_QUERY_LIMIT)`, never `None` — `None` is unbounded in +/// `SizedQuery`, which no honest server produces), +/// `1..=DEFAULT_QUERY_LIMIT` passes through, and anything above the +/// cap — including 101..=65535, which fits a `u16` but is +/// server-invalid, and 65537, which the old `as u16` cast silently +/// truncated to 1 — is refused with the server's `InvalidLimit`. +#[test] +fn limit_cap_mirrors_the_server() { + let contract = test_contract(); + let query = |limit: u32| { + DocumentQuery::new(Arc::clone(&contract), "niceDocument") + .expect("document type exists") + .with_limit(limit) + }; + + let unset_query = query(0); + let unset = DriveDocumentQuery::try_from(&unset_query).expect("limit 0 is the unset sentinel"); + assert_eq!( + unset.limit, + Some(100), + "0 must lower to the concrete server default, not unbounded" + ); + + let at_cap_query = query(100); + let at_cap = + DriveDocumentQuery::try_from(&at_cap_query).expect("the server serves limits up to 100"); + assert_eq!(at_cap.limit, Some(100)); + + for limit in [101u32, 65_535, 65_537, u32::MAX] { + let error = DriveDocumentQuery::try_from(&query(limit)) + .expect_err("a limit the server refuses must not reach a DriveDocumentQuery"); + assert!( + matches!( + &error, + Error::Drive(drive::error::Error::Query( + drive::error::query::QuerySyntaxError::InvalidLimit(message) + )) if message.contains("greater than max limit 100") + ), + "expected the server's InvalidLimit for limit {limit}: {error}" + ); + } +}