Skip to main content

drive_proof_verifier/proof/
token_pre_programmed_distributions.rs

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