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
25pub trait FromUnproved<Req> {
50 type Request;
52 type Response;
54
55 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 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 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 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 let response: platform::GetCurrentQuorumsInfoResponse = response.into();
176
177 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 let info = match response.version.ok_or(Error::EmptyVersion)? {
188 platform::get_current_quorums_info_response::Version::V0(v0) => {
189 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 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 let validator_sets =
205 v0.validator_sets
206 .into_iter()
207 .map(|vs| {
208 let quorum_hash =
210 parse_hash_32("validator_set.quorum_hash", &vs.quorum_hash)?;
211
212 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, 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"), core_port: 0, platform_http_port: 0, platform_p2p_port: 0, 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, 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 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 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 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); 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 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 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 if let Some(get_current_quorums_info_response::Version::V0(ref mut v0)) = response.version {
469 v0.quorum_hashes = vec![vec![0u8; 16]]; }
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 if let Some(get_current_quorums_info_response::Version::V0(ref mut v0)) = response.version {
496 v0.current_quorum_hash = vec![0u8; 10]; }
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 if let Some(get_current_quorums_info_response::Version::V0(ref mut v0)) = response.version {
523 v0.last_block_proposer = vec![0u8; 5]; }
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 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 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 assert_eq!(status.node.id, vec![10u8; 20]);
629 assert_eq!(status.node.pro_tx_hash, Some(vec![11u8; 32]));
630
631 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 assert_eq!(status.network.chain_id, "dash-testnet-1");
638 assert_eq!(status.network.peers_count, 25);
639 assert!(status.network.listening);
640
641 assert_eq!(status.time.local, 1700000000);
643 assert_eq!(status.time.block, Some(1699999900));
644 assert_eq!(status.time.epoch, Some(42));
645 }
646}