drive_proof_verifier/
unproved.rs

1use crate::types::evonode_status::EvoNodeStatus;
2use crate::types::CurrentQuorumsInfo;
3use crate::Error;
4use dapi_grpc::platform::v0::ResponseMetadata;
5use dapi_grpc::platform::v0::{self as platform};
6use dapi_grpc::tonic::async_trait;
7use dpp::bls_signatures::PublicKey as BlsPublicKey;
8use dpp::core_types::validator::v0::ValidatorV0;
9use dpp::core_types::validator_set::v0::ValidatorSetV0;
10use dpp::core_types::validator_set::ValidatorSet;
11use dpp::dashcore::hashes::Hash;
12use dpp::dashcore::{Network, ProTxHash, PubkeyHash, QuorumHash};
13use dpp::version::PlatformVersion;
14use std::collections::BTreeMap;
15
16/// Trait for parsing unproved responses from the Platform.
17///
18/// This trait defines methods for extracting data from responses received from the Platform
19/// without the need for cryptographic proof validation. It is primarily used for scenarios where
20/// the proof data is not available or not required, and only the data itself is needed.
21///
22/// ## Associated Types
23///
24/// - `Request`: The type of the request sent to the server. This represents the format of the
25///   data that the platform expects when making a query.
26/// - `Response`: The type of the response received from the server. This represents the format of
27///   the data returned by the platform after executing the query.
28///
29/// ## Methods
30///
31/// - `maybe_from_unproved`: Parses the response to retrieve the requested object, if any.
32/// - `maybe_from_unproved_with_metadata`: Parses the response to retrieve the requested object
33///   along with response metadata, if any.
34/// - `from_unproved`: Retrieves the requested object from the response, returning an error if the
35///   object is not found.
36/// - `from_unproved_with_metadata`: Retrieves the requested object from the response along with
37///   metadata, returning an error if the object is not found.
38///
39/// ```
40pub trait FromUnproved<Req> {
41    /// Request type for which this trait is implemented.
42    type Request;
43    /// Response type for which this trait is implemented.
44    type Response;
45
46    /// Parse the received response and retrieve the requested object, if any.
47    ///
48    /// # Arguments
49    ///
50    /// * `request`: The request sent to the server.
51    /// * `response`: The response received from the server.
52    /// * `network`: The network we are using (Mainnet/Testnet/Devnet/Regtest).
53    /// * `platform_version`: The platform version that should be used.
54    ///
55    /// # Returns
56    ///
57    /// * `Ok(Some(object))` when the requested object was found in the response.
58    /// * `Ok(None)` when the requested object was not found.
59    /// * `Err(Error)` when parsing fails or data is invalid.
60    fn maybe_from_unproved<I: Into<Self::Request>, O: Into<Self::Response>>(
61        request: I,
62        response: O,
63        network: Network,
64        platform_version: &PlatformVersion,
65    ) -> Result<Option<Self>, Error>
66    where
67        Self: Sized,
68    {
69        Self::maybe_from_unproved_with_metadata(request, response, network, platform_version)
70            .map(|maybe_result| maybe_result.0)
71    }
72
73    /// Parse the received response and retrieve the requested object along with metadata, if any.
74    ///
75    /// # Arguments
76    ///
77    /// * `request`: The request sent to the server.
78    /// * `response`: The response received from the server.
79    /// * `network`: The network we are using (Mainnet/Testnet/Devnet/Regtest).
80    /// * `platform_version`: The platform version that should be used.
81    ///
82    /// # Returns
83    ///
84    /// * `Ok((Some(object), metadata))` when the requested object was found.
85    /// * `Ok((None, metadata))` when the requested object was not found.
86    /// * `Err(Error)` when parsing fails or data is invalid.
87    fn maybe_from_unproved_with_metadata<I: Into<Self::Request>, O: Into<Self::Response>>(
88        request: I,
89        response: O,
90        network: Network,
91        platform_version: &PlatformVersion,
92    ) -> Result<(Option<Self>, ResponseMetadata), Error>
93    where
94        Self: Sized;
95
96    /// Retrieve the requested object from the response.
97    ///
98    /// # Arguments
99    ///
100    /// * `request`: The request sent to the server.
101    /// * `response`: The response received from the server.
102    /// * `network`: The network we are using.
103    /// * `platform_version`: The platform version that should be used.
104    ///
105    /// # Returns
106    ///
107    /// * `Ok(object)` when the requested object was found.
108    /// * `Err(Error::NotFound)` when the requested object was not found.
109    /// * `Err(Error)` when parsing fails or data is invalid.
110    fn from_unproved<I: Into<Self::Request>, O: Into<Self::Response>>(
111        request: I,
112        response: O,
113        network: Network,
114        platform_version: &PlatformVersion,
115    ) -> Result<Self, Error>
116    where
117        Self: Sized,
118    {
119        Self::maybe_from_unproved(request, response, network, platform_version)?
120            .ok_or(Error::NotFound)
121    }
122
123    /// Retrieve the requested object from the response along with metadata.
124    ///
125    /// # Arguments
126    ///
127    /// * `request`: The request sent to the server.
128    /// * `response`: The response received from the server.
129    /// * `network`: The network we are using.
130    /// * `platform_version`: The platform version that should be used.
131    ///
132    /// # Returns
133    ///
134    /// * `Ok((object, metadata))` when the requested object was found.
135    /// * `Err(Error::NotFound)` when the requested object was not found.
136    /// * `Err(Error)` when parsing fails or data is invalid.
137    fn from_unproved_with_metadata<I: Into<Self::Request>, O: Into<Self::Response>>(
138        request: I,
139        response: O,
140        network: Network,
141        platform_version: &PlatformVersion,
142    ) -> Result<(Self, ResponseMetadata), Error>
143    where
144        Self: Sized,
145    {
146        let (main_item, response_metadata) =
147            Self::maybe_from_unproved_with_metadata(request, response, network, platform_version)?;
148        Ok((main_item.ok_or(Error::NotFound)?, response_metadata))
149    }
150}
151
152impl FromUnproved<platform::GetCurrentQuorumsInfoRequest> for CurrentQuorumsInfo {
153    type Request = platform::GetCurrentQuorumsInfoRequest;
154    type Response = platform::GetCurrentQuorumsInfoResponse;
155
156    fn maybe_from_unproved_with_metadata<I: Into<Self::Request>, O: Into<Self::Response>>(
157        _request: I,
158        response: O,
159        _network: Network,
160        _platform_version: &PlatformVersion,
161    ) -> Result<(Option<Self>, ResponseMetadata), Error>
162    where
163        Self: Sized,
164    {
165        // Convert the response into a GetCurrentQuorumsInfoResponse
166        let response: platform::GetCurrentQuorumsInfoResponse = response.into();
167
168        // Extract metadata from the response
169        let metadata = match &response.version {
170            Some(platform::get_current_quorums_info_response::Version::V0(ref v0)) => {
171                v0.metadata.clone()
172            }
173            None => None,
174        }
175        .ok_or(Error::EmptyResponseMetadata)?;
176
177        // Parse response based on the version field
178        let info = match response.version.ok_or(Error::EmptyVersion)? {
179            platform::get_current_quorums_info_response::Version::V0(v0) => {
180                // Extract quorum hashes
181                let quorum_hashes = v0
182                    .quorum_hashes
183                    .into_iter()
184                    .map(|q_hash| {
185                        let mut q_hash_array = [0u8; 32];
186                        if q_hash.len() != 32 {
187                            return Err(Error::ProtocolError {
188                                error: "Invalid quorum_hash length".to_string(),
189                            });
190                        }
191                        q_hash_array.copy_from_slice(&q_hash);
192                        Ok(q_hash_array)
193                    })
194                    .collect::<Result<Vec<[u8; 32]>, Error>>()?;
195
196                // Extract current quorum hash
197                let mut current_quorum_hash = [0u8; 32];
198                if v0.current_quorum_hash.len() != 32 {
199                    return Err(Error::ProtocolError {
200                        error: "Invalid current_quorum_hash length".to_string(),
201                    });
202                }
203                current_quorum_hash.copy_from_slice(&v0.current_quorum_hash);
204
205                let mut last_block_proposer = [0u8; 32];
206                if v0.last_block_proposer.len() != 32 {
207                    return Err(Error::ProtocolError {
208                        error: "Invalid last_block_proposer length".to_string(),
209                    });
210                }
211                last_block_proposer.copy_from_slice(&v0.last_block_proposer);
212
213                // Extract validator sets
214                let validator_sets =
215                    v0.validator_sets
216                        .into_iter()
217                        .map(|vs| {
218                            // Parse the ValidatorSetV0
219                            let mut quorum_hash = [0u8; 32];
220                            quorum_hash.copy_from_slice(&vs.quorum_hash);
221
222                            // Parse ValidatorV0 members
223                            let members = vs
224                                .members
225                                .into_iter()
226                                .map(|member| {
227                                    let pro_tx_hash = ProTxHash::from_slice(&member.pro_tx_hash)
228                                        .map_err(|_| Error::ProtocolError {
229                                            error: "Invalid ProTxHash format".to_string(),
230                                        })?;
231                                    let validator = ValidatorV0 {
232                                        pro_tx_hash,
233                                        public_key: None, // Assuming it's not provided here
234                                        node_ip: member.node_ip,
235                                        node_id: PubkeyHash::from_slice(&[0; 20]).expect("expected to make pub key hash from 20 byte empty array"), // Placeholder, since not provided
236                                        core_port: 0, // Placeholder, since not provided
237                                        platform_http_port: 0, // Placeholder, since not provided
238                                        platform_p2p_port: 0, // Placeholder, since not provided
239                                        is_banned: member.is_banned,
240                                    };
241                                    Ok((pro_tx_hash, validator))
242                                })
243                                .collect::<Result<BTreeMap<ProTxHash, ValidatorV0>, Error>>()?;
244
245                            Ok(ValidatorSet::V0(ValidatorSetV0 {
246                                quorum_hash: QuorumHash::from_slice(quorum_hash.as_slice())
247                                    .map_err(|_| Error::ProtocolError {
248                                        error: "Invalid Quorum Hash format".to_string(),
249                                    })?,
250                                quorum_index: None, // Assuming it's not provided here
251                                core_height: vs.core_height,
252                                members,
253                                threshold_public_key: BlsPublicKey::try_from(
254                                    vs.threshold_public_key.as_slice(),
255                                )
256                                .map_err(|_| Error::ProtocolError {
257                                    error: "Invalid BlsPublicKey format".to_string(),
258                                })?,
259                            }))
260                        })
261                        .collect::<Result<Vec<ValidatorSet>, Error>>()?;
262
263                // Create the CurrentQuorumsInfo struct
264                Ok::<CurrentQuorumsInfo, Error>(CurrentQuorumsInfo {
265                    quorum_hashes,
266                    current_quorum_hash,
267                    validator_sets,
268                    last_block_proposer,
269                    last_platform_block_height: metadata.height,
270                    last_core_block_height: metadata.core_chain_locked_height,
271                })
272            }
273        }?;
274
275        Ok((Some(info), metadata))
276    }
277}
278
279#[async_trait]
280impl FromUnproved<platform::GetStatusRequest> for EvoNodeStatus {
281    type Request = platform::GetStatusRequest;
282    type Response = platform::GetStatusResponse;
283
284    fn maybe_from_unproved_with_metadata<I: Into<Self::Request>, O: Into<Self::Response>>(
285        _request: I,
286        response: O,
287        _network: Network,
288        _platform_version: &PlatformVersion,
289    ) -> Result<(Option<Self>, ResponseMetadata), Error>
290    where
291        Self: Sized,
292    {
293        let status = Self::try_from(response.into())?;
294        // we use default response metadata, as this request does not return any metadata
295        Ok((Some(status), Default::default()))
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302    use dapi_grpc::platform::v0::{
303        get_current_quorums_info_response, get_status_response, ResponseMetadata,
304    };
305    use dpp::bls_signatures::{Bls12381G2Impl, SecretKey};
306    use dpp::version::PlatformVersion;
307
308    /// Generate a valid BLS public key as compressed bytes (48 bytes) from a
309    /// deterministic secret key derived from the given seed byte.
310    fn generate_valid_bls_public_key_bytes(seed: u8) -> Vec<u8> {
311        let mut secret_bytes = [0u8; 32];
312        secret_bytes[31] = seed.max(1); // ensure nonzero
313        let sk: SecretKey<Bls12381G2Impl> =
314            SecretKey::<Bls12381G2Impl>::from_be_bytes(&secret_bytes)
315                .into_option()
316                .expect("valid secret key");
317        sk.public_key().0.to_compressed().to_vec()
318    }
319
320    /// Helper: build a valid GetCurrentQuorumsInfoResponse with one quorum hash,
321    /// one validator set with one member, and metadata.
322    fn build_valid_quorums_info_response() -> platform::GetCurrentQuorumsInfoResponse {
323        let quorum_hash = vec![1u8; 32];
324        let current_quorum_hash = vec![2u8; 32];
325        let last_block_proposer = vec![3u8; 32];
326        let pro_tx_hash = vec![4u8; 32];
327        let threshold_public_key = generate_valid_bls_public_key_bytes(42);
328
329        let member = get_current_quorums_info_response::ValidatorV0 {
330            pro_tx_hash: pro_tx_hash.clone(),
331            node_ip: "127.0.0.1".to_string(),
332            is_banned: false,
333        };
334
335        let validator_set = get_current_quorums_info_response::ValidatorSetV0 {
336            quorum_hash: quorum_hash.clone(),
337            core_height: 100,
338            members: vec![member],
339            threshold_public_key,
340        };
341
342        let v0 = get_current_quorums_info_response::GetCurrentQuorumsInfoResponseV0 {
343            quorum_hashes: vec![quorum_hash],
344            current_quorum_hash,
345            validator_sets: vec![validator_set],
346            last_block_proposer,
347            metadata: Some(ResponseMetadata {
348                height: 500,
349                core_chain_locked_height: 200,
350                epoch: 10,
351                time_ms: 1234567890,
352                protocol_version: 1,
353                chain_id: "dash-testnet-1".to_string(),
354            }),
355        };
356
357        platform::GetCurrentQuorumsInfoResponse {
358            version: Some(get_current_quorums_info_response::Version::V0(v0)),
359        }
360    }
361
362    /// Helper: build a valid GetStatusResponse V0 with all inner fields populated.
363    fn build_valid_status_response() -> platform::GetStatusResponse {
364        use dapi_grpc::platform::v0::get_status_response::get_status_response_v0;
365
366        let software = get_status_response_v0::version::Software {
367            dapi: "1.0.0".to_string(),
368            drive: Some("2.0.0".to_string()),
369            tenderdash: Some("0.14.0".to_string()),
370        };
371
372        let tenderdash_protocol =
373            get_status_response_v0::version::protocol::Tenderdash { p2p: 8, block: 11 };
374
375        let drive_protocol = get_status_response_v0::version::protocol::Drive {
376            latest: 5,
377            current: 4,
378            next_epoch: 5,
379        };
380
381        let protocol = get_status_response_v0::version::Protocol {
382            tenderdash: Some(tenderdash_protocol),
383            drive: Some(drive_protocol),
384        };
385
386        let version = get_status_response_v0::Version {
387            software: Some(software),
388            protocol: Some(protocol),
389        };
390
391        let time = get_status_response_v0::Time {
392            local: 1700000000,
393            block: Some(1699999900),
394            genesis: Some(1690000000),
395            epoch: Some(42),
396        };
397
398        let node = get_status_response_v0::Node {
399            id: vec![10u8; 20],
400            pro_tx_hash: Some(vec![11u8; 32]),
401        };
402
403        let chain = get_status_response_v0::Chain {
404            catching_up: false,
405            latest_block_hash: vec![20u8; 32],
406            latest_app_hash: vec![21u8; 32],
407            latest_block_height: 1000,
408            earliest_block_hash: vec![22u8; 32],
409            earliest_app_hash: vec![23u8; 32],
410            earliest_block_height: 1,
411            max_peer_block_height: 1001,
412            core_chain_locked_height: Some(500),
413        };
414
415        let network = get_status_response_v0::Network {
416            chain_id: "dash-testnet-1".to_string(),
417            peers_count: 25,
418            listening: true,
419        };
420
421        let state_sync = get_status_response_v0::StateSync {
422            total_synced_time: 3600,
423            remaining_time: 120,
424            total_snapshots: 5,
425            chunk_process_avg_time: 50,
426            snapshot_height: 900,
427            snapshot_chunks_count: 100,
428            backfilled_blocks: 800,
429            backfill_blocks_total: 1000,
430        };
431
432        let v0 = get_status_response::GetStatusResponseV0 {
433            version: Some(version),
434            node: Some(node),
435            chain: Some(chain),
436            network: Some(network),
437            state_sync: Some(state_sync),
438            time: Some(time),
439        };
440
441        platform::GetStatusResponse {
442            version: Some(get_status_response::Version::V0(v0)),
443        }
444    }
445
446    #[test]
447    fn test_current_quorums_info_valid_response() {
448        let request = platform::GetCurrentQuorumsInfoRequest { version: None };
449        let response = build_valid_quorums_info_response();
450        let platform_version = PlatformVersion::latest();
451
452        let result = CurrentQuorumsInfo::maybe_from_unproved_with_metadata(
453            request,
454            response,
455            Network::Testnet,
456            platform_version,
457        );
458
459        let (maybe_info, metadata) = result.expect("should parse valid response");
460        let info = maybe_info.expect("should contain CurrentQuorumsInfo");
461
462        assert_eq!(info.quorum_hashes.len(), 1);
463        assert_eq!(info.quorum_hashes[0], [1u8; 32]);
464        assert_eq!(info.current_quorum_hash, [2u8; 32]);
465        assert_eq!(info.last_block_proposer, [3u8; 32]);
466        assert_eq!(info.validator_sets.len(), 1);
467        assert_eq!(info.last_platform_block_height, 500);
468        assert_eq!(info.last_core_block_height, 200);
469        assert_eq!(metadata.height, 500);
470        assert_eq!(metadata.core_chain_locked_height, 200);
471    }
472
473    #[test]
474    fn test_current_quorums_info_invalid_quorum_hash_length() {
475        let mut response = build_valid_quorums_info_response();
476
477        // Inject an invalid quorum_hash that is not 32 bytes
478        if let Some(get_current_quorums_info_response::Version::V0(ref mut v0)) = response.version {
479            v0.quorum_hashes = vec![vec![0u8; 16]]; // 16 bytes instead of 32
480        }
481
482        let request = platform::GetCurrentQuorumsInfoRequest { version: None };
483        let platform_version = PlatformVersion::latest();
484
485        let result = CurrentQuorumsInfo::maybe_from_unproved_with_metadata(
486            request,
487            response,
488            Network::Testnet,
489            platform_version,
490        );
491
492        let err = result.expect_err("should fail for invalid quorum_hash length");
493        let err_string = err.to_string();
494        assert!(
495            err_string.contains("Invalid quorum_hash length"),
496            "unexpected error: {err_string}"
497        );
498    }
499
500    #[test]
501    fn test_current_quorums_info_invalid_current_quorum_hash_length() {
502        let mut response = build_valid_quorums_info_response();
503
504        // Inject an invalid current_quorum_hash that is not 32 bytes
505        if let Some(get_current_quorums_info_response::Version::V0(ref mut v0)) = response.version {
506            v0.current_quorum_hash = vec![0u8; 10]; // 10 bytes instead of 32
507        }
508
509        let request = platform::GetCurrentQuorumsInfoRequest { version: None };
510        let platform_version = PlatformVersion::latest();
511
512        let result = CurrentQuorumsInfo::maybe_from_unproved_with_metadata(
513            request,
514            response,
515            Network::Testnet,
516            platform_version,
517        );
518
519        let err = result.expect_err("should fail for invalid current_quorum_hash length");
520        let err_string = err.to_string();
521        assert!(
522            err_string.contains("Invalid current_quorum_hash length"),
523            "unexpected error: {err_string}"
524        );
525    }
526
527    #[test]
528    fn test_current_quorums_info_invalid_last_block_proposer() {
529        let mut response = build_valid_quorums_info_response();
530
531        // Inject an invalid last_block_proposer that is not 32 bytes
532        if let Some(get_current_quorums_info_response::Version::V0(ref mut v0)) = response.version {
533            v0.last_block_proposer = vec![0u8; 5]; // 5 bytes instead of 32
534        }
535
536        let request = platform::GetCurrentQuorumsInfoRequest { version: None };
537        let platform_version = PlatformVersion::latest();
538
539        let result = CurrentQuorumsInfo::maybe_from_unproved_with_metadata(
540            request,
541            response,
542            Network::Testnet,
543            platform_version,
544        );
545
546        let err = result.expect_err("should fail for invalid last_block_proposer length");
547        let err_string = err.to_string();
548        assert!(
549            err_string.contains("Invalid last_block_proposer length"),
550            "unexpected error: {err_string}"
551        );
552    }
553
554    #[test]
555    fn test_current_quorums_info_none_metadata() {
556        let mut response = build_valid_quorums_info_response();
557
558        // Remove metadata from the response
559        if let Some(get_current_quorums_info_response::Version::V0(ref mut v0)) = response.version {
560            v0.metadata = None;
561        }
562
563        let request = platform::GetCurrentQuorumsInfoRequest { version: None };
564        let platform_version = PlatformVersion::latest();
565
566        let result = CurrentQuorumsInfo::maybe_from_unproved_with_metadata(
567            request,
568            response,
569            Network::Testnet,
570            platform_version,
571        );
572
573        let err = result.expect_err("should fail when metadata is missing");
574        let err_string = err.to_string();
575        assert!(
576            err_string.contains("empty response metadata"),
577            "unexpected error: {err_string}"
578        );
579    }
580
581    #[test]
582    fn test_evo_node_status_valid_response() {
583        let request = platform::GetStatusRequest { version: None };
584        let response = build_valid_status_response();
585        let platform_version = PlatformVersion::latest();
586
587        let result = EvoNodeStatus::maybe_from_unproved_with_metadata(
588            request,
589            response,
590            Network::Testnet,
591            platform_version,
592        );
593
594        let (maybe_status, _metadata) = result.expect("should parse valid status response");
595        let status = maybe_status.expect("should contain EvoNodeStatus");
596
597        // Verify version fields
598        let software = status.version.software.as_ref().unwrap();
599        assert_eq!(software.dapi, "1.0.0");
600        assert_eq!(software.drive.as_deref(), Some("2.0.0"));
601        assert_eq!(software.tenderdash.as_deref(), Some("0.14.0"));
602
603        let protocol = status.version.protocol.as_ref().unwrap();
604        let td = protocol.tenderdash.as_ref().unwrap();
605        assert_eq!(td.p2p, 8);
606        assert_eq!(td.block, 11);
607        let drv = protocol.drive.as_ref().unwrap();
608        assert_eq!(drv.latest, 5);
609        assert_eq!(drv.current, 4);
610        assert_eq!(drv.next_epoch, 5);
611
612        // Verify node fields
613        assert_eq!(status.node.id, vec![10u8; 20]);
614        assert_eq!(status.node.pro_tx_hash, Some(vec![11u8; 32]));
615
616        // Verify chain fields
617        assert!(!status.chain.catching_up);
618        assert_eq!(status.chain.latest_block_height, 1000);
619        assert_eq!(status.chain.core_chain_locked_height, Some(500));
620
621        // Verify network fields
622        assert_eq!(status.network.chain_id, "dash-testnet-1");
623        assert_eq!(status.network.peers_count, 25);
624        assert!(status.network.listening);
625
626        // Verify time fields
627        assert_eq!(status.time.local, 1700000000);
628        assert_eq!(status.time.block, Some(1699999900));
629        assert_eq!(status.time.epoch, Some(42));
630    }
631}