Skip to main content

drive_proof_verifier/proof/
data_contracts_by_range.rs

1use crate::error::MapGroveDbError;
2use crate::types::data_contracts_by_range::DataContractsByRange;
3use crate::verify::{supported_grovedb_proof_bytes, verify_tenderdash_proof};
4use crate::{ContextProvider, Error, FromProof};
5use dapi_grpc::platform::v0::get_data_contracts_by_range_request::get_data_contracts_by_range_request_v0::Start;
6use dapi_grpc::platform::v0::{
7    get_data_contracts_by_range_request, GetDataContractsByRangeRequest,
8    GetDataContractsResponse, Proof, ResponseMetadata,
9};
10use dapi_grpc::platform::VersionedGrpcResponse;
11use dpp::dashcore::Network;
12use dpp::version::PlatformVersion;
13use drive::drive::Drive;
14
15/// The page a `getDataContractsByRange` request asked for, in the verifier's terms.
16struct PageParameters {
17    start_at: Option<([u8; 32], bool)>,
18    limit: u16,
19    ids_only: bool,
20}
21
22/// Reads the page parameters back from the request, applying the same defaults and bounds
23/// as the server so that the verifier rebuilds the exact query that was proved: an omitted
24/// limit is the protocol page cap (`max_returned_elements`), which is also the upper bound.
25fn page_parameters(
26    request: GetDataContractsByRangeRequest,
27    platform_version: &PlatformVersion,
28) -> Result<PageParameters, Error> {
29    let get_data_contracts_by_range_request::Version::V0(v0) =
30        request.version.ok_or(Error::EmptyVersion)?;
31
32    let max_returned_elements = platform_version.drive_abci.query.max_returned_elements;
33    let limit = match v0.limit {
34        None => max_returned_elements,
35        Some(requested) => match u16::try_from(requested) {
36            Ok(limit) if (1..=max_returned_elements).contains(&limit) => limit,
37            _ => {
38                return Err(Error::RequestError {
39                    error: format!(
40                        "limit {requested} is out of bounds, it must be between 1 and {max_returned_elements}"
41                    ),
42                })
43            }
44        },
45    };
46
47    let start_at = match v0.start {
48        None => None,
49        Some(Start::StartAfter(cursor)) => {
50            Some((contract_id_from_cursor(cursor, "start_after")?, false))
51        }
52        Some(Start::StartAt(cursor)) => Some((contract_id_from_cursor(cursor, "start_at")?, true)),
53    };
54
55    Ok(PageParameters {
56        start_at,
57        limit,
58        ids_only: v0.ids_only,
59    })
60}
61
62fn contract_id_from_cursor(cursor: Vec<u8>, field: &str) -> Result<[u8; 32], Error> {
63    cursor
64        .try_into()
65        .map_err(|cursor: Vec<u8>| Error::RequestError {
66            error: format!(
67                "{field} must be a 32 byte contract id, got {} bytes",
68                cursor.len()
69            ),
70        })
71}
72
73impl FromProof<GetDataContractsByRangeRequest> for DataContractsByRange {
74    type Request = GetDataContractsByRangeRequest;
75    type Response = GetDataContractsResponse;
76
77    fn maybe_from_proof_with_metadata<'a, I: Into<Self::Request>, O: Into<Self::Response>>(
78        request: I,
79        response: O,
80        _network: Network,
81        platform_version: &PlatformVersion,
82        provider: &'a dyn ContextProvider,
83    ) -> Result<(Option<Self>, ResponseMetadata, Proof), Error>
84    where
85        Self: Sized + 'a,
86    {
87        let request: Self::Request = request.into();
88        let response: Self::Response = response.into();
89
90        let PageParameters {
91            start_at,
92            limit,
93            ids_only,
94        } = page_parameters(request, platform_version)?;
95
96        let metadata = response
97            .metadata()
98            .or(Err(Error::EmptyResponseMetadata))?
99            .clone();
100        let proof = response.proof_owned().or(Err(Error::NoProofInResult))?;
101
102        let (root_hash, page) = Drive::verify_contracts_by_range(
103            supported_grovedb_proof_bytes(&proof)?,
104            start_at,
105            limit,
106            ids_only,
107            platform_version,
108        )
109        .map_drive_error(&proof, &metadata)?;
110
111        verify_tenderdash_proof(&proof, &metadata, &root_hash, provider)?;
112
113        // An empty page is a proved result in its own right: no contract exists at or after
114        // the cursor. It is never `None`.
115        let page: DataContractsByRange = page.into_iter().collect();
116
117        Ok((Some(page), metadata, proof))
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use dapi_grpc::platform::v0::get_data_contracts_by_range_request::{
125        GetDataContractsByRangeRequestV0, Version as ReqVersion,
126    };
127    use dapi_grpc::platform::v0::get_data_contracts_response::{
128        get_data_contracts_response_v0::Result as RespResult, GetDataContractsResponseV0,
129        Version as RespVersion,
130    };
131    use dash_context_provider::ContextProviderError;
132    use dpp::data_contract::TokenConfiguration;
133    use dpp::prelude::{CoreBlockHeight, DataContract, Identifier};
134    use std::sync::Arc;
135
136    /// Context provider that panics if called: every test here fails on the request before
137    /// proof verification, so the provider must stay unreachable.
138    struct UnreachableProvider;
139
140    impl ContextProvider for UnreachableProvider {
141        fn get_data_contract(
142            &self,
143            _id: &Identifier,
144            _pv: &PlatformVersion,
145        ) -> Result<Option<Arc<DataContract>>, ContextProviderError> {
146            panic!("context provider should not be called")
147        }
148
149        fn get_token_configuration(
150            &self,
151            _id: &Identifier,
152        ) -> Result<Option<TokenConfiguration>, ContextProviderError> {
153            panic!("context provider should not be called")
154        }
155
156        fn get_quorum_public_key(
157            &self,
158            _qt: u32,
159            _qh: [u8; 32],
160            _h: u32,
161        ) -> Result<[u8; 48], ContextProviderError> {
162            panic!("context provider should not be called")
163        }
164
165        fn get_platform_activation_height(&self) -> Result<CoreBlockHeight, ContextProviderError> {
166            panic!("context provider should not be called")
167        }
168    }
169
170    fn pv() -> &'static PlatformVersion {
171        PlatformVersion::latest()
172    }
173
174    fn response_with_proof_and_metadata() -> GetDataContractsResponse {
175        GetDataContractsResponse {
176            version: Some(RespVersion::V0(GetDataContractsResponseV0 {
177                result: Some(RespResult::Proof(Proof::default())),
178                metadata: Some(ResponseMetadata::default()),
179            })),
180        }
181    }
182
183    fn request(v0: GetDataContractsByRangeRequestV0) -> GetDataContractsByRangeRequest {
184        GetDataContractsByRangeRequest {
185            version: Some(ReqVersion::V0(v0)),
186        }
187    }
188
189    fn page_v0(limit: Option<u32>, start: Option<Start>) -> GetDataContractsByRangeRequestV0 {
190        GetDataContractsByRangeRequestV0 {
191            limit,
192            start,
193            ids_only: false,
194            prove: true,
195        }
196    }
197
198    fn from_proof_error(request: GetDataContractsByRangeRequest) -> Error {
199        <DataContractsByRange as FromProof<_>>::maybe_from_proof(
200            request,
201            response_with_proof_and_metadata(),
202            Network::Testnet,
203            pv(),
204            &UnreachableProvider,
205        )
206        .unwrap_err()
207    }
208
209    #[test]
210    fn should_fail_with_empty_version_when_request_has_no_version() {
211        let err = from_proof_error(GetDataContractsByRangeRequest { version: None });
212        assert!(matches!(err, Error::EmptyVersion), "got: {err:?}");
213    }
214
215    #[test]
216    fn should_reject_start_after_that_is_not_a_contract_id() {
217        let err = from_proof_error(request(page_v0(
218            None,
219            Some(Start::StartAfter(vec![0u8; 8])),
220        )));
221        assert!(
222            matches!(err, Error::RequestError { ref error } if error.contains("start_after")),
223            "got: {err:?}"
224        );
225    }
226
227    #[test]
228    fn should_reject_zero_limit() {
229        let err = from_proof_error(request(page_v0(Some(0), None)));
230        assert!(matches!(err, Error::RequestError { .. }), "got: {err:?}");
231    }
232
233    #[test]
234    fn should_reject_limit_above_the_page_cap() {
235        let above_cap = pv().drive_abci.query.max_returned_elements as u32 + 1;
236        let err = from_proof_error(request(page_v0(Some(above_cap), None)));
237        assert!(matches!(err, Error::RequestError { .. }), "got: {err:?}");
238    }
239}