Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -1173,7 +1173,7 @@ impl<'a> TryFrom<&'a DocumentQuery> for DriveDocumentQuery<'a> {
/// produced by the SDK's typical WhereClause builders, so a
/// rejection here flags an unsupported caller construction at the
/// wire boundary rather than silently dropping the value.
fn where_clause_to_proto(clause: WhereClause) -> Result<ProtoWhereClause, Error> {
pub(crate) fn where_clause_to_proto(clause: WhereClause) -> Result<ProtoWhereClause, Error> {
Ok(ProtoWhereClause {
field: clause.field,
operator: where_operator_to_proto(clause.operator) as i32,
Expand All @@ -1185,7 +1185,7 @@ fn where_clause_to_proto(clause: WhereClause) -> Result<ProtoWhereClause, Error>
})
}

fn order_clause_to_proto(clause: OrderClause) -> ProtoOrderClause {
pub(crate) fn order_clause_to_proto(clause: OrderClause) -> ProtoOrderClause {
// Drive's `OrderClause` carries a plain `field: String` —
// emit the field-target variant of the wire's `target` oneof.
// The aggregate-target variant (`ORDER BY COUNT(*)`) is
Expand Down
1 change: 1 addition & 0 deletions packages/dash-platform-queries/src/documents/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub(crate) mod average_proof_helpers;
pub mod chained_document_query;
pub mod composite_document_query;
pub(crate) mod count_proof_helpers;
pub mod document_average;
pub mod document_count;
Expand Down
421 changes: 421 additions & 0 deletions packages/rs-drive-abci/src/query/document_query/v1/tests.rs

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions packages/rs-drive-proof-verifier/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ pub use error::Error;
pub use proof::chained_document::{
verify_chained_documents_proof as verify_chained_documents_tenderdash_proof, ChainedDocuments,
};
pub use proof::composite_document::{
verify_composite_documents_proof as verify_composite_documents_tenderdash_proof,
CompositeDocuments,
};
// Re-export the per-sub-query result of a composite query at the
// crate root, paralleling `SplitCountEntry` below, so SDK consumers can
// name it without depending on rs-drive directly.
pub use drive::query::drive_composite_document_query::SubQueryResult as CompositeSubQueryResult;
pub use proof::document_count::{
verify_aggregate_count_proof, verify_carrier_aggregate_count_proof,
verify_distinct_count_proof, verify_point_lookup_count_proof,
Expand Down
4 changes: 4 additions & 0 deletions packages/rs-drive-proof-verifier/src/proof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
/// proofs — the inner indexOnly page and the outer by-ids fetch derived
/// from its proven values — bound to one quorum-signed root.
pub mod chained_document;
/// Verified composite-document result: a page plus the sub-queries
/// derived from it (joins, lookups, counts, siblings), ONE merged
/// grovedb proof bound to one quorum-signed root.
pub mod composite_document;
/// Verified average result. Holds the `(count, sum)` pair recovered
/// from a `CountSumTree` / PCPS proof; client divides to obtain the
/// average. Lights up alongside grovedb PR 670's
Expand Down
123 changes: 123 additions & 0 deletions packages/rs-drive-proof-verifier/src/proof/composite_document.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
//! Verified **composite document** results: a page plus the
//! sub-queries derived from it, answered as ONE merged grovedb proof.
//!
//! The server proves the limited page and every sub-query (by-id
//! joins, indexed lookups, grouped counts, siblings) as one merged
//! path query. The verifier
//! ([`DriveCompositeDocumentQuery::verify_composite_documents_proof`])
//! bootstraps the page from the proof with a subset pass, re-derives
//! every sub-query's `IN` clause from the PROVEN page (or the proven
//! earlier sub-query it binds), rebuilds the same merged query, verifies
//! it in one authoritative pass, and routes the proved entries back to
//! their components — refusing unclaimed entries, dangling joins and
//! derivation divergence. This module's [`FromProof`] impl composes
//! that with the tenderdash signature binding of the single root.
//!
//! There is deliberately **no unproven decoder with verification
//! semantics** here: an unproven composite response is free to
//! fabricate any sub-result, which is precisely what the surface exists
//! to prevent. [`CompositeDocuments`] can still be built from a trusted
//! node's unproven wire by the SDK if it chooses, but the canonical
//! path proves.

use crate::error::MapGroveDbError;
use crate::verify::verify_tenderdash_proof;
use crate::{ContextProvider, Error, FromProof};
use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata};
use dapi_grpc::platform::VersionedGrpcResponse;
use dpp::dashcore::Network;
use dpp::document::Document;
use dpp::version::PlatformVersion;
use drive::query::drive_composite_document_query::{DriveCompositeDocumentQuery, SubQueryResult};
use drive::verify::RootHash;

/// The verified result of a composite document query.
#[derive(Debug, Clone, PartialEq, Default)]
pub struct CompositeDocuments {
/// The page, exactly as the page query alone would return it.
pub page_documents: Vec<Document>,
/// One result per sub-query, in request order: a by-id join's
/// documents in first-appearance order of their ids among the
/// source documents, a lookup's or sibling's in query order, or one
/// count per derived value that has a count tree (a value without
/// an entry counts zero).
pub sub_results: Vec<SubQueryResult>,
}

/// Verify a composite query's single merged proof and bind its root
/// hash to the quorum signature.
///
/// The merk-level composition (bootstrap subset pass on the page,
/// re-derivation of every sub-query, authoritative full verification,
/// routing with the completeness checks) lives in rs-drive's
/// [`DriveCompositeDocumentQuery::verify_composite_documents_proof`];
/// this wrapper adds the [`verify_tenderdash_proof`] binding — the root
/// hash the proof commits to is only an attested fact once it is tied
/// to the quorum-signed app hash, and this function exists so the
/// composition can never be skipped by accident.
pub fn verify_composite_documents_proof(
query: &DriveCompositeDocumentQuery,
proof: &Proof,
mtd: &ResponseMetadata,
platform_version: &PlatformVersion,
provider: &dyn ContextProvider,
) -> Result<(RootHash, CompositeDocuments), Error> {
let (root_hash, result) = query
.verify_composite_documents_proof(&proof.grovedb_proof, platform_version)
.map_drive_error(proof, mtd)?;

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

Ok((
root_hash,
CompositeDocuments {
page_documents: result.page_documents,
sub_results: result.sub_results,
},
))
}

impl<'dq, Q> FromProof<Q> for CompositeDocuments
where
Q: TryInto<DriveCompositeDocumentQuery<'dq>> + Clone + 'dq,
Q::Error: std::fmt::Display,
{
type Request = Q;
type Response = GetDocumentsResponse;

fn maybe_from_proof_with_metadata<'a, I: Into<Self::Request>, O: Into<Self::Response>>(
request: I,
response: O,
_network: Network,
platform_version: &PlatformVersion,
provider: &'a dyn ContextProvider,
) -> Result<(Option<Self>, ResponseMetadata, Proof), Error>
where
Self: 'a,
{
let request: Self::Request = request.into();
let response: Self::Response = response.into();

let query: DriveCompositeDocumentQuery<'dq> =
request
.clone()
.try_into()
.map_err(|e: Q::Error| Error::RequestError {
error: e.to_string(),
})?;

// The standard envelope carries the single MERGED proof, and
// the proof alone is enough: the verifier bootstraps the page
// from it via a subset pass and re-derives the rest.
let proof = response.proof().or(Err(Error::NoProofInResult))?;
let mtd = response.metadata().or(Err(Error::EmptyResponseMetadata))?;

let (_root_hash, composite) =
verify_composite_documents_proof(&query, proof, mtd, platform_version, provider)?;

// An empty page is a valid, proven "nothing here" — surface it
// as Some(empty) rather than None so callers can tell it apart
// from a missing object.
Ok((Some(composite), mtd.clone(), proof.clone()))
}
}
85 changes: 85 additions & 0 deletions packages/rs-sdk/src/mock/requests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -863,3 +863,88 @@ impl MockResponse for drive_proof_verifier::ChainedDocuments {
}
}
}

/// Wire shape for one `CompositeDocuments` sub-result mock round-trip:
/// `(is_documents, documents, count triples)`, only one side populated.
type MockCompositeSubResult = (bool, Vec<Vec<u8>>, DocumentSplitCountTriples);

/// Wire shape for `CompositeDocuments` mock round-trip: the page as a
/// per-document CBOR list, then one entry per sub-query.
type MockCompositeShape = (Vec<Vec<u8>>, Vec<MockCompositeSubResult>);

impl MockResponse for drive_proof_verifier::CompositeDocuments {
/// The page and every documents sub-result as per-document CBOR,
/// count sub-results as `(in_key, key, count)` triples, all
/// bincode-framed in request order — list order IS the answer
/// (page order, a join's first-appearance order), so a map-shaped
/// encoding would destroy it.
fn mock_serialize(&self, _sdk: &MockDashPlatformSdk) -> Vec<u8> {
let bincode_config = standard();
let encode = |documents: &[Document]| -> Vec<Vec<u8>> {
documents
.iter()
.map(|d| d.to_cbor().expect("encode document"))
.collect()
};
let shape: MockCompositeShape = (
encode(&self.page_documents),
self.sub_results
.iter()
.map(|result| match result {
drive_proof_verifier::CompositeSubQueryResult::Documents(documents) => {
(true, encode(documents), Vec::new())
}
drive_proof_verifier::CompositeSubQueryResult::Counts(entries) => (
false,
Vec::new(),
entries
.iter()
.map(|e| (e.in_key.clone(), e.key.clone(), e.count))
.collect(),
),
})
.collect(),
);
bincode::encode_to_vec(shape, bincode_config).expect("encode CompositeDocuments")
}

fn mock_deserialize(sdk: &MockDashPlatformSdk, buf: &[u8]) -> Self
where
Self: Sized,
{
let bincode_config = standard();
let ((page, sub_results), _): (MockCompositeShape, _) =
bincode::decode_from_slice(buf, bincode_config).expect("decode CompositeDocuments");
let decode = |bufs: Vec<Vec<u8>>| -> Vec<Document> {
bufs.into_iter()
.map(|b| {
Document::from_cbor(&b, None, None, sdk.version()).expect("decode document")
})
.collect()
};
drive_proof_verifier::CompositeDocuments {
page_documents: decode(page),
sub_results: sub_results
.into_iter()
.map(|(is_documents, documents, triples)| {
if is_documents {
drive_proof_verifier::CompositeSubQueryResult::Documents(decode(documents))
} else {
drive_proof_verifier::CompositeSubQueryResult::Counts(
triples
.into_iter()
.map(
|(in_key, key, count)| drive_proof_verifier::SplitCountEntry {
in_key,
key,
count,
},
)
.collect(),
)
}
})
.collect(),
}
}
}
6 changes: 5 additions & 1 deletion packages/rs-sdk/src/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ pub use dash_context_provider::ContextProvider;
#[cfg(feature = "mocks")]
pub use dash_context_provider::MockContextProvider;
pub use documents::chained_document_query::ChainedDocumentQuery;
pub use documents::composite_document_query::{
CompositeBinding, CompositeBindingSource, CompositeDocumentQuery, CompositeSubQuery,
CompositeSubQueryKind,
};
pub use documents::document_history_query::DocumentHistoryQuery;
pub use documents::document_query::DocumentQuery;
/// Sdk-bound constructors for [`DocumentQuery`]. Must be in scope to call
Expand All @@ -42,7 +46,7 @@ pub use dpp::{
prelude::{DataContract, Identifier, Identity, IdentityPublicKey, Revision},
};
pub use drive::query::DriveDocumentQuery;
pub use drive_proof_verifier::ChainedDocuments;
pub use drive_proof_verifier::{ChainedDocuments, CompositeDocuments, CompositeSubQueryResult};
pub use rs_dapi_client as dapi;
pub use {
fetch::Fetch,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//! Sdk-bound half of the composite document query surface: the rich →
//! wire encoding. The transport-free query type itself
//! ([`CompositeDocumentQuery`]) lives in `dash-platform-queries`.

use dapi_grpc::platform::v0 as platform_proto;
use dapi_grpc::platform::v0::GetDocumentsRequest;
use dash_platform_queries::documents::composite_document_query::CompositeDocumentQuery;
use dpp::version::TryFromPlatformVersioned;

use crate::Error;

/// Encode a [`CompositeDocumentQuery`] onto the wire.
///
/// The [`Fetch`](crate::platform::Fetch) trampoline for
/// [`drive_proof_verifier::CompositeDocuments`] splits `Query =
/// CompositeDocumentQuery` (rich, what `FromProof` binds to) from
/// `Request = GetDocumentsRequest` (wire); this impl is the rich→wire
/// step.
impl crate::platform::Query<platform_proto::GetDocumentsRequest> for CompositeDocumentQuery {
fn query(
&self,
settings: &crate::platform::QuerySettings<'_>,
) -> Result<platform_proto::GetDocumentsRequest, Error> {
GetDocumentsRequest::try_from_platform_versioned(self.clone(), settings.protocol_version)
.map_err(Error::from)
}
}

// `CompositeDocumentQuery` does not implement `TransportRequest` (the
// wire form is `GetDocumentsRequest`), so the blanket `Query<T> for T`
// does not apply — provide the identity impl explicitly, same as
// `DocumentQuery`'s, so the fetch trampoline can use it both as the
// user-supplied `Q` and as the rich `Self::Query`.
impl crate::platform::Query<CompositeDocumentQuery> for CompositeDocumentQuery {
fn query(
&self,
settings: &crate::platform::QuerySettings<'_>,
) -> Result<CompositeDocumentQuery, Error> {
if !settings.prove {
tracing::warn!(request= ?self, "sending query without proof, ensure data is trusted");
}
Ok(self.clone())
}
}
5 changes: 5 additions & 0 deletions packages/rs-sdk/src/platform/documents/fetch_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,8 @@ impl Fetch for drive_proof_verifier::ChainedDocuments {
type Query = dash_platform_queries::documents::chained_document_query::ChainedDocumentQuery;
type Request = dapi_grpc::platform::v0::GetDocumentsRequest;
}

impl Fetch for drive_proof_verifier::CompositeDocuments {
type Query = dash_platform_queries::documents::composite_document_query::CompositeDocumentQuery;
type Request = dapi_grpc::platform::v0::GetDocumentsRequest;
}
7 changes: 4 additions & 3 deletions packages/rs-sdk/src/platform/documents/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
//! bindings, the contract-fetching constructor, and transition builders.

pub use dash_platform_queries::documents::{
chained_document_query, document_average, document_count, document_having_entries,
document_history_query, document_query, document_ranked_entries, document_split_averages,
document_split_counts, document_split_sums, document_sum,
chained_document_query, composite_document_query, document_average, document_count,
document_having_entries, document_history_query, document_query, document_ranked_entries,
document_split_averages, document_split_counts, document_split_sums, document_sum,
};

pub mod chained_document_query_sdk;
pub mod composite_document_query_sdk;
pub mod document_query_sdk;
mod fetch_bindings;
pub mod transitions;
Expand Down
Loading