Skip to main content

drive_proof_verifier/proof/
token_pre_programmed_distributions.rs

1use crate::error::MapGroveDbError;
2use crate::verify::verify_tenderdash_proof;
3use crate::{types::TokenPreProgrammedDistributions, ContextProvider, Error};
4use dapi_grpc::platform::v0::{
5    get_token_pre_programmed_distributions_request, GetTokenPreProgrammedDistributionsRequest,
6    GetTokenPreProgrammedDistributionsResponse, Proof, ResponseMetadata,
7};
8use dapi_grpc::platform::VersionedGrpcResponse;
9use dpp::dashcore::Network;
10use dpp::prelude::Identifier;
11use dpp::version::PlatformVersion;
12use drive::drive::tokens::distribution::queries::QueryPreProgrammedDistributionStartAt;
13use drive::drive::Drive;
14
15use super::FromProof;
16
17impl FromProof<GetTokenPreProgrammedDistributionsRequest> for TokenPreProgrammedDistributions {
18    type Request = GetTokenPreProgrammedDistributionsRequest;
19    type Response = GetTokenPreProgrammedDistributionsResponse;
20
21    fn maybe_from_proof_with_metadata<'a, I: Into<Self::Request>, O: Into<Self::Response>>(
22        request: I,
23        response: O,
24        _network: Network,
25        platform_version: &PlatformVersion,
26        provider: &'a dyn ContextProvider,
27    ) -> Result<(Option<Self>, ResponseMetadata, Proof), Error>
28    where
29        Self: Sized + 'a,
30    {
31        let request: Self::Request = request.into();
32        let response: Self::Response = response.into();
33
34        let get_token_pre_programmed_distributions_request::Version::V0(req_v0) =
35            request.version.ok_or(Error::EmptyVersion)?;
36
37        let token_id: [u8; 32] =
38            req_v0
39                .token_id
40                .as_slice()
41                .try_into()
42                .map_err(|_| Error::RequestError {
43                    error: "token_id must be 32 bytes".into(),
44                })?;
45
46        let start_at = match req_v0.start_at_info {
47            Some(start_at_info) => {
48                let start_at_recipient = match start_at_info.start_recipient {
49                    Some(recipient_bytes) => {
50                        let recipient_id =
51                            Identifier::from_bytes(&recipient_bytes).map_err(|_| {
52                                Error::RequestError {
53                                    error: "start_recipient must be 32 bytes".into(),
54                                }
55                            })?;
56                        // Default to inclusive: if omitted the start recipient is included.
57                        let included = start_at_info.start_recipient_included.unwrap_or(true);
58                        Some((recipient_id, included))
59                    }
60                    None => None,
61                };
62
63                Some(QueryPreProgrammedDistributionStartAt {
64                    start_at_time: start_at_info.start_time_ms,
65                    start_at_recipient,
66                })
67            }
68            None => None,
69        };
70
71        let limit = req_v0
72            .limit
73            .map(|l| {
74                u16::try_from(l).map_err(|_| Error::RequestError {
75                    error: "limit exceeds u16::MAX".into(),
76                })
77            })
78            .transpose()?;
79
80        let metadata = response
81            .metadata()
82            .or(Err(Error::EmptyResponseMetadata))?
83            .clone();
84
85        let proof = response.proof_owned().or(Err(Error::NoProofInResult))?;
86
87        let (root_hash, result): ([u8; 32], TokenPreProgrammedDistributions) =
88            Drive::verify_token_pre_programmed_distributions(
89                &proof.grovedb_proof,
90                token_id,
91                start_at,
92                limit,
93                false,
94                platform_version,
95            )
96            .map_drive_error(&proof, &metadata)?;
97
98        verify_tenderdash_proof(&proof, &metadata, &root_hash, provider)?;
99
100        if result.0.is_empty() {
101            Ok((None, metadata, proof))
102        } else {
103            Ok((Some(result), metadata, proof))
104        }
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use crate::FromProof;
112    use dapi_grpc::platform::v0::get_token_pre_programmed_distributions_request::{
113        get_token_pre_programmed_distributions_request_v0::StartAtInfo,
114        GetTokenPreProgrammedDistributionsRequestV0, Version as ReqVersion,
115    };
116    use dapi_grpc::platform::v0::get_token_pre_programmed_distributions_response::{
117        get_token_pre_programmed_distributions_response_v0::Result as RespResult,
118        GetTokenPreProgrammedDistributionsResponseV0, Version as RespVersion,
119    };
120    use dash_context_provider::ContextProviderError;
121    use dpp::data_contract::TokenConfiguration;
122    use dpp::prelude::{CoreBlockHeight, DataContract};
123    use std::sync::Arc;
124
125    struct UnreachableProvider;
126
127    impl ContextProvider for UnreachableProvider {
128        fn get_data_contract(
129            &self,
130            _id: &Identifier,
131            _pv: &PlatformVersion,
132        ) -> Result<Option<Arc<DataContract>>, ContextProviderError> {
133            panic!("should not be called")
134        }
135        fn get_token_configuration(
136            &self,
137            _id: &Identifier,
138        ) -> Result<Option<TokenConfiguration>, ContextProviderError> {
139            panic!("should not be called")
140        }
141        fn get_quorum_public_key(
142            &self,
143            _qt: u32,
144            _qh: [u8; 32],
145            _h: u32,
146        ) -> Result<[u8; 48], ContextProviderError> {
147            panic!("should not be called")
148        }
149        fn get_platform_activation_height(&self) -> Result<CoreBlockHeight, ContextProviderError> {
150            panic!("should not be called")
151        }
152    }
153
154    fn pv() -> &'static PlatformVersion {
155        PlatformVersion::latest()
156    }
157
158    fn response_with_proof() -> GetTokenPreProgrammedDistributionsResponse {
159        GetTokenPreProgrammedDistributionsResponse {
160            version: Some(RespVersion::V0(
161                GetTokenPreProgrammedDistributionsResponseV0 {
162                    result: Some(RespResult::Proof(Proof::default())),
163                    metadata: Some(ResponseMetadata::default()),
164                },
165            )),
166        }
167    }
168
169    fn req_v0_defaults() -> GetTokenPreProgrammedDistributionsRequestV0 {
170        GetTokenPreProgrammedDistributionsRequestV0 {
171            token_id: vec![0u8; 32],
172            start_at_info: None,
173            limit: None,
174            prove: true,
175        }
176    }
177
178    #[test]
179    fn empty_version_when_request_has_no_version() {
180        let request = GetTokenPreProgrammedDistributionsRequest { version: None };
181        let response = response_with_proof();
182        let err = <TokenPreProgrammedDistributions as FromProof<_>>::maybe_from_proof(
183            request,
184            response,
185            Network::Testnet,
186            pv(),
187            &UnreachableProvider,
188        )
189        .unwrap_err();
190        assert!(matches!(err, Error::EmptyVersion), "got: {err:?}");
191    }
192
193    #[test]
194    fn request_error_when_token_id_wrong_length() {
195        let request = GetTokenPreProgrammedDistributionsRequest {
196            version: Some(ReqVersion::V0(
197                GetTokenPreProgrammedDistributionsRequestV0 {
198                    token_id: vec![0u8; 5], // invalid
199                    ..req_v0_defaults()
200                },
201            )),
202        };
203        let response = response_with_proof();
204        let err = <TokenPreProgrammedDistributions as FromProof<_>>::maybe_from_proof(
205            request,
206            response,
207            Network::Testnet,
208            pv(),
209            &UnreachableProvider,
210        )
211        .unwrap_err();
212        match err {
213            Error::RequestError { error } => assert!(error.contains("token_id"), "got: {error}"),
214            other => panic!("expected RequestError, got: {other:?}"),
215        }
216    }
217
218    #[test]
219    fn request_error_when_start_recipient_wrong_length() {
220        let request = GetTokenPreProgrammedDistributionsRequest {
221            version: Some(ReqVersion::V0(
222                GetTokenPreProgrammedDistributionsRequestV0 {
223                    token_id: vec![1u8; 32],
224                    start_at_info: Some(StartAtInfo {
225                        start_time_ms: 10_000,
226                        start_recipient: Some(vec![1u8; 16]), // not 32 bytes
227                        start_recipient_included: Some(true),
228                    }),
229                    limit: None,
230                    prove: true,
231                },
232            )),
233        };
234        let response = response_with_proof();
235        let err = <TokenPreProgrammedDistributions as FromProof<_>>::maybe_from_proof(
236            request,
237            response,
238            Network::Testnet,
239            pv(),
240            &UnreachableProvider,
241        )
242        .unwrap_err();
243        match err {
244            Error::RequestError { error } => {
245                assert!(error.contains("start_recipient"), "got: {error}")
246            }
247            other => panic!("expected RequestError, got: {other:?}"),
248        }
249    }
250
251    #[test]
252    fn request_error_when_limit_exceeds_u16_max() {
253        let request = GetTokenPreProgrammedDistributionsRequest {
254            version: Some(ReqVersion::V0(
255                GetTokenPreProgrammedDistributionsRequestV0 {
256                    token_id: vec![2u8; 32],
257                    start_at_info: None,
258                    limit: Some(u32::MAX), // exceeds u16::MAX
259                    prove: true,
260                },
261            )),
262        };
263        let response = response_with_proof();
264        let err = <TokenPreProgrammedDistributions as FromProof<_>>::maybe_from_proof(
265            request,
266            response,
267            Network::Testnet,
268            pv(),
269            &UnreachableProvider,
270        )
271        .unwrap_err();
272        match err {
273            Error::RequestError { error } => {
274                assert!(error.contains("limit"), "got: {error}");
275                assert!(error.contains("u16"), "got: {error}");
276            }
277            other => panic!("expected RequestError, got: {other:?}"),
278        }
279    }
280
281    #[test]
282    fn empty_response_metadata_when_metadata_missing() {
283        let request = GetTokenPreProgrammedDistributionsRequest {
284            version: Some(ReqVersion::V0(req_v0_defaults())),
285        };
286        // No version ⇒ metadata() errors ⇒ EmptyResponseMetadata.
287        let response = GetTokenPreProgrammedDistributionsResponse { version: None };
288        let err = <TokenPreProgrammedDistributions as FromProof<_>>::maybe_from_proof(
289            request,
290            response,
291            Network::Testnet,
292            pv(),
293            &UnreachableProvider,
294        )
295        .unwrap_err();
296        assert!(matches!(err, Error::EmptyResponseMetadata), "got: {err:?}");
297    }
298
299    #[test]
300    fn no_proof_in_result_when_result_missing() {
301        let request = GetTokenPreProgrammedDistributionsRequest {
302            version: Some(ReqVersion::V0(req_v0_defaults())),
303        };
304        let response = GetTokenPreProgrammedDistributionsResponse {
305            version: Some(RespVersion::V0(
306                GetTokenPreProgrammedDistributionsResponseV0 {
307                    result: None,
308                    metadata: Some(ResponseMetadata::default()),
309                },
310            )),
311        };
312        let err = <TokenPreProgrammedDistributions as FromProof<_>>::maybe_from_proof(
313            request,
314            response,
315            Network::Testnet,
316            pv(),
317            &UnreachableProvider,
318        )
319        .unwrap_err();
320        assert!(matches!(err, Error::NoProofInResult), "got: {err:?}");
321    }
322
323    #[test]
324    fn limit_at_u16_max_does_not_error_on_conversion() {
325        // Using limit = u16::MAX as u32 — conversion must succeed.
326        //
327        // To stop deterministically BEFORE Drive verification and the provider,
328        // we use a response whose `version = None`. Control flow in
329        // `maybe_from_proof_with_metadata` is:
330        //   version -> token_id -> start_at -> limit conversion -> metadata -> proof -> drive
331        // So the limit conversion runs first; if it wrongly rejected u16::MAX,
332        // we'd see `Error::RequestError { "limit exceeds u16::MAX" }`.
333        // Instead, the conversion should succeed and we should fall through to
334        // the deterministic `EmptyResponseMetadata` branch.
335        let request = GetTokenPreProgrammedDistributionsRequest {
336            version: Some(ReqVersion::V0(
337                GetTokenPreProgrammedDistributionsRequestV0 {
338                    token_id: vec![3u8; 32],
339                    start_at_info: None,
340                    limit: Some(u16::MAX as u32),
341                    prove: true,
342                },
343            )),
344        };
345        // response.version = None ⇒ metadata() errors ⇒ EmptyResponseMetadata,
346        // which fires BEFORE proof_owned() or Drive verification, and well
347        // before the provider could ever be consulted.
348        let response = GetTokenPreProgrammedDistributionsResponse { version: None };
349        let err = <TokenPreProgrammedDistributions as FromProof<_>>::maybe_from_proof(
350            request,
351            response,
352            Network::Testnet,
353            pv(),
354            &UnreachableProvider,
355        )
356        .unwrap_err();
357        assert!(
358            matches!(err, Error::EmptyResponseMetadata),
359            "expected EmptyResponseMetadata (proves limit conversion succeeded), got: {err:?}"
360        );
361    }
362}