Skip to main content

drive/query/
moderated_join.rs

1//! Joins through a `refersTo: moderatedDocument` property: the removal
2//! records a join proves beside the documents it joins.
3//!
4//! A `moderatedDocument` reference promises its target is in state or was
5//! removed by the contract's moderators, on the record: the document type
6//! keeps a removal record for every document a moderator deletes, and its
7//! documents leave state no other way. So a by-id join off such a property
8//! (chained or composite) proves, beside the by-ids fetch of the joined
9//! documents, the removal records of the same ids, one more component of the
10//! one merged proof: [`removals_path_query`]. A joined id with no document is
11//! then reported with its proven record ([`pair_missing_with_removals`]), and
12//! an id with neither is refused, as a missing `permanentDocument` target is:
13//! corrupted state on the server, an invalid proof in the verifier.
14//!
15//! The component names every joined id, not only the ones missing a
16//! document: the verifier builds the merged query before it knows which are
17//! missing, and grovedb proves each queried key present or absent, so a
18//! prover can pass neither a removed document off as live nor a live one off
19//! as removed.
20
21use crate::drive::contract::moderation::types::{
22    ContractDocumentRemovalEntry, ContractDocumentRemovalsQuery, ContractDocumentRemovalsSelection,
23};
24use crate::drive::Drive;
25use crate::error::proof::ProofError;
26use crate::error::Error;
27#[cfg(feature = "server")]
28use crate::fees::op::LowLevelDriveOperation;
29#[cfg(feature = "server")]
30use crate::query::is_absent_path;
31use dpp::identifier::Identifier;
32#[cfg(feature = "server")]
33use dpp::version::PlatformVersion;
34#[cfg(feature = "server")]
35use grovedb::query_result_type::QueryResultType;
36#[cfg(feature = "server")]
37use grovedb::TransactionArg;
38use grovedb::{Element, PathQuery};
39use std::collections::BTreeMap;
40
41/// The removal records of `document_ids` within one document type of one
42/// contract, each proved present or absent: the removals query by ids
43/// ([`Drive::contract_document_removals_query`]), unlimited, as the by-ids
44/// fetch of the documents it sits beside is (the ids bound it), and walking in
45/// `left_to_right` so it merges with the components it is proven beside (its
46/// selection does not depend on the direction). The ids are queried in
47/// canonical byte order, so the prover and a verifier that collected them in
48/// any order build the same query.
49pub fn removals_path_query(
50    contract_id: Identifier,
51    document_type_name: &str,
52    document_ids: &[Identifier],
53    left_to_right: bool,
54) -> PathQuery {
55    let mut ids = document_ids.to_vec();
56    ids.sort();
57    ids.dedup();
58    let mut path_query = Drive::contract_document_removals_query(
59        contract_id.to_buffer(),
60        &ContractDocumentRemovalsQuery {
61            document_type_name: document_type_name.to_string(),
62            selection: ContractDocumentRemovalsSelection::DocumentIds(ids),
63        },
64    );
65    path_query.query.limit = None;
66    path_query.query.query.left_to_right = left_to_right;
67    path_query
68}
69
70/// Decodes the proved (or fetched) entries of a [`removals_path_query`], by
71/// document id. A malformed record, or one proved twice, is a corrupted
72/// proof.
73pub fn decode_removals(
74    entries: impl IntoIterator<Item = (Vec<u8>, Element)>,
75) -> Result<BTreeMap<Identifier, ContractDocumentRemovalEntry>, Error> {
76    let mut removals = BTreeMap::new();
77    for (key, element) in entries {
78        let entry =
79            ContractDocumentRemovalEntry::from_key_element(&key, &element).map_err(|reason| {
80                Error::Proof(ProofError::CorruptedProof(format!(
81                    "a joined document's removal record does not decode: {reason}"
82                )))
83            })?;
84        let document_id = entry.document_id;
85        if removals.insert(document_id, entry).is_some() {
86            return Err(Error::Proof(ProofError::CorruptedProof(format!(
87                "the removal record of joined document {document_id} is carried twice"
88            ))));
89        }
90    }
91    Ok(removals)
92}
93
94/// The records of the joined ids `missing` that have no document, in their
95/// order, from `removals`. Each must have a record that stands, unrestored: a
96/// restored record describes a document that is live again. An id without
97/// one means the target left state without a moderator's recorded removal,
98/// which a `moderatedDocument` target can not, so it is refused.
99pub fn pair_missing_with_removals(
100    missing: &[Identifier],
101    removals: &BTreeMap<Identifier, ContractDocumentRemovalEntry>,
102) -> Result<Vec<ContractDocumentRemovalEntry>, Error> {
103    missing
104        .iter()
105        .map(|document_id| match removals.get(document_id) {
106            Some(entry) if !entry.removal.is_restored() => Ok(entry.clone()),
107            Some(_) => Err(Error::Proof(ProofError::CorruptedProof(format!(
108                "joined document {document_id} is missing although its removal record says a \
109                 moderator restored it"
110            )))),
111            None => Err(Error::Proof(ProofError::CorruptedProof(format!(
112                "joined document {document_id} is missing without a removal record: a \
113                 moderatedDocument reference resolves to its document or to the record of its \
114                 removal"
115            )))),
116        })
117        .collect()
118}
119
120/// Fetches, without a proof, the entries of a [`removals_path_query`]: the
121/// server's materialized join reads the very query the proof covers. A
122/// document type without a removal records tree yet answers with none.
123#[cfg(feature = "server")]
124pub(crate) fn fetch_removals(
125    drive: &Drive,
126    path_query: &PathQuery,
127    transaction: TransactionArg,
128    drive_operations: &mut Vec<LowLevelDriveOperation>,
129    platform_version: &PlatformVersion,
130) -> Result<BTreeMap<Identifier, ContractDocumentRemovalEntry>, Error> {
131    let results = match drive.grove_get_path_query(
132        path_query,
133        transaction,
134        QueryResultType::QueryKeyElementPairResultType,
135        drive_operations,
136        &platform_version.drive,
137    ) {
138        Err(error) if is_absent_path(&error) => {
139            return Ok(BTreeMap::new());
140        }
141        other => other?.0,
142    };
143    decode_removals(results.to_key_elements())
144}