Skip to main content

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