Skip to main content

drive_proof_verifier/proof/
token_contract_info.rs

1use crate::error::MapGroveDbError;
2use crate::verify::verify_tenderdash_proof;
3use crate::{ContextProvider, Error, FromProof};
4use dapi_grpc::platform::v0::{
5    get_token_contract_info_request, GetTokenContractInfoRequest, GetTokenContractInfoResponse,
6    Proof, ResponseMetadata,
7};
8use dapi_grpc::platform::VersionedGrpcResponse;
9use dpp::dashcore::Network;
10use dpp::tokens::contract_info::TokenContractInfo;
11use dpp::version::PlatformVersion;
12use drive::drive::Drive;
13
14impl FromProof<GetTokenContractInfoRequest> for TokenContractInfo {
15    type Request = GetTokenContractInfoRequest;
16    type Response = GetTokenContractInfoResponse;
17
18    fn maybe_from_proof_with_metadata<'a, I: Into<Self::Request>, O: Into<Self::Response>>(
19        request: I,
20        response: O,
21        _network: Network,
22        platform_version: &PlatformVersion,
23        provider: &'a dyn ContextProvider,
24    ) -> Result<(Option<Self>, ResponseMetadata, Proof), Error>
25    where
26        Self: Sized + 'a,
27    {
28        let request: Self::Request = request.into();
29        let response: Self::Response = response.into();
30
31        let token_id = match request.version.ok_or(Error::EmptyVersion)? {
32            get_token_contract_info_request::Version::V0(v0) => {
33                <[u8; 32]>::try_from(v0.token_id).map_err(|_| Error::RequestError {
34                    error: "can't convert token_id to [u8; 32]".to_string(),
35                })?
36            }
37        };
38
39        let metadata = response
40            .metadata()
41            .or(Err(Error::EmptyResponseMetadata))?
42            .clone();
43
44        let proof = response.proof_owned().or(Err(Error::NoProofInResult))?;
45
46        let (root_hash, result) = Drive::verify_token_contract_info(
47            &proof.grovedb_proof,
48            token_id,
49            false,
50            platform_version,
51        )
52        .map_drive_error(&proof, &metadata)?;
53
54        verify_tenderdash_proof(&proof, &metadata, &root_hash, provider)?;
55
56        Ok((result, metadata, proof))
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63    use crate::FromProof;
64    use dapi_grpc::platform::v0::{
65        get_token_contract_info_request::{GetTokenContractInfoRequestV0, Version as ReqVersion},
66        get_token_contract_info_response::{
67            get_token_contract_info_response_v0::Result as RespResult,
68            GetTokenContractInfoResponseV0, Version as RespVersion,
69        },
70    };
71    use dash_context_provider::ContextProviderError;
72    use dpp::data_contract::TokenConfiguration;
73    use dpp::prelude::{CoreBlockHeight, DataContract, Identifier};
74    use std::sync::Arc;
75
76    /// Context provider that panics if called — tests should stop before proof
77    /// verification so the provider is unreachable.
78    struct UnreachableProvider;
79
80    impl ContextProvider for UnreachableProvider {
81        fn get_data_contract(
82            &self,
83            _id: &Identifier,
84            _pv: &PlatformVersion,
85        ) -> Result<Option<Arc<DataContract>>, ContextProviderError> {
86            panic!("context provider should not be called")
87        }
88
89        fn get_token_configuration(
90            &self,
91            _id: &Identifier,
92        ) -> Result<Option<TokenConfiguration>, ContextProviderError> {
93            panic!("context provider should not be called")
94        }
95
96        fn get_quorum_public_key(
97            &self,
98            _qt: u32,
99            _qh: [u8; 32],
100            _h: u32,
101        ) -> Result<[u8; 48], ContextProviderError> {
102            panic!("context provider should not be called")
103        }
104
105        fn get_platform_activation_height(&self) -> Result<CoreBlockHeight, ContextProviderError> {
106            panic!("context provider should not be called")
107        }
108    }
109
110    fn pv() -> &'static PlatformVersion {
111        PlatformVersion::latest()
112    }
113
114    fn response_with_proof_and_metadata() -> GetTokenContractInfoResponse {
115        GetTokenContractInfoResponse {
116            version: Some(RespVersion::V0(GetTokenContractInfoResponseV0 {
117                result: Some(RespResult::Proof(Proof::default())),
118                metadata: Some(ResponseMetadata::default()),
119            })),
120        }
121    }
122
123    #[test]
124    fn token_contract_info_empty_version_on_request_missing_version() {
125        let request = GetTokenContractInfoRequest { version: None };
126        let response = response_with_proof_and_metadata();
127        let err = <TokenContractInfo as FromProof<_>>::maybe_from_proof(
128            request,
129            response,
130            Network::Testnet,
131            pv(),
132            &UnreachableProvider,
133        )
134        .unwrap_err();
135        assert!(matches!(err, Error::EmptyVersion), "got: {err:?}");
136    }
137
138    #[test]
139    fn token_contract_info_request_error_when_token_id_wrong_length() {
140        // 16-byte token_id — not 32
141        let request = GetTokenContractInfoRequest {
142            version: Some(ReqVersion::V0(GetTokenContractInfoRequestV0 {
143                token_id: vec![0u8; 16],
144                prove: true,
145            })),
146        };
147        let response = response_with_proof_and_metadata();
148        let err = <TokenContractInfo as FromProof<_>>::maybe_from_proof(
149            request,
150            response,
151            Network::Testnet,
152            pv(),
153            &UnreachableProvider,
154        )
155        .unwrap_err();
156        match err {
157            Error::RequestError { error } => {
158                assert!(
159                    error.contains("token_id"),
160                    "error should mention token_id, got: {error}"
161                );
162            }
163            other => panic!("expected RequestError, got: {other:?}"),
164        }
165    }
166
167    #[test]
168    fn token_contract_info_request_error_when_token_id_too_long() {
169        let request = GetTokenContractInfoRequest {
170            version: Some(ReqVersion::V0(GetTokenContractInfoRequestV0 {
171                token_id: vec![1u8; 64], // too long
172                prove: true,
173            })),
174        };
175        let response = response_with_proof_and_metadata();
176        let err = <TokenContractInfo as FromProof<_>>::maybe_from_proof(
177            request,
178            response,
179            Network::Testnet,
180            pv(),
181            &UnreachableProvider,
182        )
183        .unwrap_err();
184        assert!(matches!(err, Error::RequestError { .. }), "got: {err:?}");
185    }
186
187    #[test]
188    fn token_contract_info_empty_response_metadata_when_metadata_missing() {
189        let request = GetTokenContractInfoRequest {
190            version: Some(ReqVersion::V0(GetTokenContractInfoRequestV0 {
191                token_id: vec![0u8; 32],
192                prove: true,
193            })),
194        };
195        let response = GetTokenContractInfoResponse {
196            version: Some(RespVersion::V0(GetTokenContractInfoResponseV0 {
197                result: Some(RespResult::Proof(Proof::default())),
198                metadata: None,
199            })),
200        };
201        let err = <TokenContractInfo as FromProof<_>>::maybe_from_proof(
202            request,
203            response,
204            Network::Testnet,
205            pv(),
206            &UnreachableProvider,
207        )
208        .unwrap_err();
209        assert!(matches!(err, Error::EmptyResponseMetadata), "got: {err:?}");
210    }
211
212    #[test]
213    fn token_contract_info_no_proof_when_response_has_no_result() {
214        let request = GetTokenContractInfoRequest {
215            version: Some(ReqVersion::V0(GetTokenContractInfoRequestV0 {
216                token_id: vec![0u8; 32],
217                prove: true,
218            })),
219        };
220        // result=None — proof is missing
221        let response = GetTokenContractInfoResponse {
222            version: Some(RespVersion::V0(GetTokenContractInfoResponseV0 {
223                result: None,
224                metadata: Some(ResponseMetadata::default()),
225            })),
226        };
227        let err = <TokenContractInfo as FromProof<_>>::maybe_from_proof(
228            request,
229            response,
230            Network::Testnet,
231            pv(),
232            &UnreachableProvider,
233        )
234        .unwrap_err();
235        assert!(matches!(err, Error::NoProofInResult), "got: {err:?}");
236    }
237
238    #[test]
239    fn token_contract_info_empty_response_metadata_when_response_is_default() {
240        // response.version = None. In `TokenContractInfo::maybe_from_proof`,
241        // `response.metadata()` is called BEFORE `response.proof_owned()`, and
242        // the derived `VersionedGrpcResponse` impl returns Err when version is
243        // None, which maps to `Error::EmptyResponseMetadata`. Ordering is
244        // deterministic, so this is the only acceptable outcome.
245        let request = GetTokenContractInfoRequest {
246            version: Some(ReqVersion::V0(GetTokenContractInfoRequestV0 {
247                token_id: vec![0u8; 32],
248                prove: true,
249            })),
250        };
251        let response = GetTokenContractInfoResponse::default();
252        let err = <TokenContractInfo as FromProof<_>>::maybe_from_proof(
253            request,
254            response,
255            Network::Testnet,
256            pv(),
257            &UnreachableProvider,
258        )
259        .unwrap_err();
260        assert!(matches!(err, Error::EmptyResponseMetadata), "got: {err:?}");
261    }
262}