Skip to main content

drive_proof_verifier/proof/
contract_groups.rs

1//! Proof verification of the contract group queries.
2
3use crate::error::MapGroveDbError;
4use crate::types::contract_groups::{
5    members_limit_from_request, members_query_from_request, ContractGroupInfo,
6    ContractGroupMembersPage, ContractGroupMembershipsForContract,
7};
8use crate::verify::{supported_grovedb_proof_bytes, verify_tenderdash_proof};
9use crate::{ContextProvider, Error, FromProof};
10use dapi_grpc::platform::v0::{
11    get_contract_group_info_request, get_contract_group_members_request,
12    get_contract_groups_for_contract_request, GetContractGroupInfoRequest,
13    GetContractGroupInfoResponse, GetContractGroupMembersRequest, GetContractGroupMembersResponse,
14    GetContractGroupsForContractRequest, GetContractGroupsForContractResponse, Proof,
15    ResponseMetadata,
16};
17use dapi_grpc::platform::VersionedGrpcResponse;
18use dpp::dashcore::Network;
19use dpp::identifier::Identifier;
20use dpp::version::PlatformVersion;
21use drive::drive::Drive;
22
23fn identifier_from_request(bytes: Vec<u8>, what: &str) -> Result<Identifier, Error> {
24    Identifier::from_bytes(&bytes).map_err(|_| Error::RequestError {
25        error: format!(
26            "{what} must be a 32 byte identifier, got {} bytes",
27            bytes.len()
28        ),
29    })
30}
31
32impl FromProof<GetContractGroupInfoRequest> for ContractGroupInfo {
33    type Request = GetContractGroupInfoRequest;
34    type Response = GetContractGroupInfoResponse;
35
36    fn maybe_from_proof_with_metadata<'a, I: Into<Self::Request>, O: Into<Self::Response>>(
37        request: I,
38        response: O,
39        _network: Network,
40        platform_version: &PlatformVersion,
41        provider: &'a dyn ContextProvider,
42    ) -> Result<(Option<Self>, ResponseMetadata, Proof), Error>
43    where
44        Self: Sized + 'a,
45    {
46        let request: Self::Request = request.into();
47        let response: Self::Response = response.into();
48
49        let get_contract_group_info_request::Version::V0(v0) =
50            request.version.ok_or(Error::EmptyVersion)?;
51        let contract_group_id = identifier_from_request(v0.contract_group_id, "contract_group_id")?;
52
53        let metadata = response
54            .metadata()
55            .or(Err(Error::EmptyResponseMetadata))?
56            .clone();
57        let proof = response.proof_owned().or(Err(Error::NoProofInResult))?;
58
59        let (root_hash, info) = Drive::verify_contract_group_info(
60            supported_grovedb_proof_bytes(&proof)?,
61            contract_group_id,
62            platform_version,
63        )
64        .map_drive_error(&proof, &metadata)?;
65
66        verify_tenderdash_proof(&proof, &metadata, &root_hash, provider)?;
67
68        Ok((info, metadata, proof))
69    }
70}
71
72impl FromProof<GetContractGroupMembersRequest> for ContractGroupMembersPage {
73    type Request = GetContractGroupMembersRequest;
74    type Response = GetContractGroupMembersResponse;
75
76    fn maybe_from_proof_with_metadata<'a, I: Into<Self::Request>, O: Into<Self::Response>>(
77        request: I,
78        response: O,
79        _network: Network,
80        platform_version: &PlatformVersion,
81        provider: &'a dyn ContextProvider,
82    ) -> Result<(Option<Self>, ResponseMetadata, Proof), Error>
83    where
84        Self: Sized + 'a,
85    {
86        let request: Self::Request = request.into();
87        let response: Self::Response = response.into();
88
89        let get_contract_group_members_request::Version::V0(v0) =
90            request.version.ok_or(Error::EmptyVersion)?;
91        let contract_group_id = identifier_from_request(v0.contract_group_id, "contract_group_id")?;
92        let query = members_query_from_request(v0.members)?;
93        let limit = members_limit_from_request(v0.limit, platform_version)?;
94
95        let metadata = response
96            .metadata()
97            .or(Err(Error::EmptyResponseMetadata))?
98            .clone();
99        let proof = response.proof_owned().or(Err(Error::NoProofInResult))?;
100
101        let (root_hash, page) = Drive::verify_contract_group_members(
102            supported_grovedb_proof_bytes(&proof)?,
103            contract_group_id,
104            &query,
105            limit,
106            platform_version,
107        )
108        .map_drive_error(&proof, &metadata)?;
109
110        verify_tenderdash_proof(&proof, &metadata, &root_hash, provider)?;
111
112        // An absent group and a group with no members of that kind both prove as an empty
113        // page, so the page itself is always the answer.
114        Ok((Some(page), metadata, proof))
115    }
116}
117
118impl FromProof<GetContractGroupsForContractRequest> for ContractGroupMembershipsForContract {
119    type Request = GetContractGroupsForContractRequest;
120    type Response = GetContractGroupsForContractResponse;
121
122    fn maybe_from_proof_with_metadata<'a, I: Into<Self::Request>, O: Into<Self::Response>>(
123        request: I,
124        response: O,
125        _network: Network,
126        platform_version: &PlatformVersion,
127        provider: &'a dyn ContextProvider,
128    ) -> Result<(Option<Self>, ResponseMetadata, Proof), Error>
129    where
130        Self: Sized + 'a,
131    {
132        let request: Self::Request = request.into();
133        let response: Self::Response = response.into();
134
135        let get_contract_groups_for_contract_request::Version::V0(v0) =
136            request.version.ok_or(Error::EmptyVersion)?;
137        let contract_id = identifier_from_request(v0.contract_id, "contract_id")?;
138
139        let metadata = response
140            .metadata()
141            .or(Err(Error::EmptyResponseMetadata))?
142            .clone();
143        let proof = response.proof_owned().or(Err(Error::NoProofInResult))?;
144
145        let (root_hash, memberships) = Drive::verify_contract_group_memberships_for_contract(
146            supported_grovedb_proof_bytes(&proof)?,
147            contract_id,
148            platform_version,
149        )
150        .map_drive_error(&proof, &metadata)?;
151
152        verify_tenderdash_proof(&proof, &metadata, &root_hash, provider)?;
153
154        // A contract in no group proves as empty memberships, so they are always the answer.
155        Ok((Some(memberships), metadata, proof))
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    use dapi_grpc::platform::v0::get_contract_group_info_request::GetContractGroupInfoRequestV0;
163    use dapi_grpc::platform::v0::get_contract_group_info_response::{
164        get_contract_group_info_response_v0::Result as InfoResult, GetContractGroupInfoResponseV0,
165        Version as InfoResponseVersion,
166    };
167    use dapi_grpc::platform::v0::get_contract_group_members_request::{
168        ContractMembersQuery, GetContractGroupMembersRequestV0,
169    };
170    use dapi_grpc::platform::v0::get_contract_group_members_response::{
171        get_contract_group_members_response_v0::Result as MembersResult,
172        GetContractGroupMembersResponseV0, Version as MembersResponseVersion,
173    };
174    use dapi_grpc::platform::v0::get_contract_group_members_request::get_contract_group_members_request_v0::Members;
175    use dash_context_provider::ContextProviderError;
176    use dpp::data_contract::TokenConfiguration;
177    use dpp::prelude::{CoreBlockHeight, DataContract};
178    use std::sync::Arc;
179
180    /// Context provider that panics if called: every test here fails on the request before
181    /// proof verification, so the provider must stay unreachable.
182    struct UnreachableProvider;
183
184    impl ContextProvider for UnreachableProvider {
185        fn get_data_contract(
186            &self,
187            _id: &Identifier,
188            _pv: &PlatformVersion,
189        ) -> Result<Option<Arc<DataContract>>, ContextProviderError> {
190            panic!("context provider should not be called")
191        }
192
193        fn get_token_configuration(
194            &self,
195            _id: &Identifier,
196        ) -> Result<Option<TokenConfiguration>, ContextProviderError> {
197            panic!("context provider should not be called")
198        }
199
200        fn get_quorum_public_key(
201            &self,
202            _qt: u32,
203            _qh: [u8; 32],
204            _h: u32,
205        ) -> Result<[u8; 48], ContextProviderError> {
206            panic!("context provider should not be called")
207        }
208
209        fn get_platform_activation_height(&self) -> Result<CoreBlockHeight, ContextProviderError> {
210            panic!("context provider should not be called")
211        }
212    }
213
214    fn info_request(contract_group_id: Vec<u8>) -> GetContractGroupInfoRequest {
215        GetContractGroupInfoRequest {
216            version: Some(get_contract_group_info_request::Version::V0(
217                GetContractGroupInfoRequestV0 {
218                    contract_group_id,
219                    prove: true,
220                },
221            )),
222        }
223    }
224
225    fn info_response(result: Option<InfoResult>) -> GetContractGroupInfoResponse {
226        GetContractGroupInfoResponse {
227            version: Some(InfoResponseVersion::V0(GetContractGroupInfoResponseV0 {
228                result,
229                metadata: Some(ResponseMetadata::default()),
230            })),
231        }
232    }
233
234    fn members_request(
235        members: Option<Members>,
236        limit: Option<u32>,
237    ) -> GetContractGroupMembersRequest {
238        GetContractGroupMembersRequest {
239            version: Some(get_contract_group_members_request::Version::V0(
240                GetContractGroupMembersRequestV0 {
241                    contract_group_id: vec![1; 32],
242                    members,
243                    limit,
244                    prove: true,
245                },
246            )),
247        }
248    }
249
250    fn members_response_with_proof() -> GetContractGroupMembersResponse {
251        GetContractGroupMembersResponse {
252            version: Some(MembersResponseVersion::V0(
253                GetContractGroupMembersResponseV0 {
254                    result: Some(MembersResult::Proof(Proof::default())),
255                    metadata: Some(ResponseMetadata::default()),
256                },
257            )),
258        }
259    }
260
261    #[test]
262    fn info_should_fail_with_empty_version_when_request_has_no_version() {
263        let err = <ContractGroupInfo as FromProof<_>>::maybe_from_proof(
264            GetContractGroupInfoRequest { version: None },
265            info_response(Some(InfoResult::Proof(Proof::default()))),
266            Network::Testnet,
267            PlatformVersion::latest(),
268            &UnreachableProvider,
269        )
270        .unwrap_err();
271        assert!(matches!(err, Error::EmptyVersion), "got: {err:?}");
272    }
273
274    #[test]
275    fn info_should_reject_id_that_is_not_32_bytes() {
276        let err = <ContractGroupInfo as FromProof<_>>::maybe_from_proof(
277            info_request(vec![1; 5]),
278            info_response(Some(InfoResult::Proof(Proof::default()))),
279            Network::Testnet,
280            PlatformVersion::latest(),
281            &UnreachableProvider,
282        )
283        .unwrap_err();
284        assert!(matches!(err, Error::RequestError { .. }), "got: {err:?}");
285    }
286
287    #[test]
288    fn info_should_fail_without_proof_when_response_carries_none() {
289        let err = <ContractGroupInfo as FromProof<_>>::maybe_from_proof(
290            info_request(vec![1; 32]),
291            info_response(None),
292            Network::Testnet,
293            PlatformVersion::latest(),
294            &UnreachableProvider,
295        )
296        .unwrap_err();
297        assert!(matches!(err, Error::NoProofInResult), "got: {err:?}");
298    }
299
300    #[test]
301    fn members_should_reject_a_request_without_a_kind() {
302        let err = <ContractGroupMembersPage as FromProof<_>>::maybe_from_proof(
303            members_request(None, None),
304            members_response_with_proof(),
305            Network::Testnet,
306            PlatformVersion::latest(),
307            &UnreachableProvider,
308        )
309        .unwrap_err();
310        assert!(
311            matches!(&err, Error::RequestError { error } if error.contains("members")),
312            "got: {err:?}"
313        );
314    }
315
316    #[test]
317    fn members_should_reject_out_of_bounds_limits() {
318        let platform_version = PlatformVersion::latest();
319        let too_many = platform_version.drive_abci.query.max_returned_elements as u32 + 1;
320        for limit in [0, too_many] {
321            let err = <ContractGroupMembersPage as FromProof<_>>::maybe_from_proof(
322                members_request(
323                    Some(Members::Contracts(ContractMembersQuery {
324                        start_after: None,
325                    })),
326                    Some(limit),
327                ),
328                members_response_with_proof(),
329                Network::Testnet,
330                platform_version,
331                &UnreachableProvider,
332            )
333            .unwrap_err();
334            assert!(
335                matches!(&err, Error::RequestError { error } if error.contains("limit")),
336                "limit {limit}: {err:?}"
337            );
338        }
339    }
340
341    #[test]
342    fn members_should_reject_a_cursor_that_is_not_an_identifier() {
343        let err = <ContractGroupMembersPage as FromProof<_>>::maybe_from_proof(
344            members_request(
345                Some(Members::Contracts(ContractMembersQuery {
346                    start_after: Some(vec![1; 3]),
347                })),
348                None,
349            ),
350            members_response_with_proof(),
351            Network::Testnet,
352            PlatformVersion::latest(),
353            &UnreachableProvider,
354        )
355        .unwrap_err();
356        assert!(
357            matches!(&err, Error::RequestError { error } if error.contains("start_after")),
358            "got: {err:?}"
359        );
360    }
361}