1pub mod block;
2pub mod connect;
3pub mod frames;
4pub mod ordering;
5pub mod plurality;
6pub mod pool;
7pub mod translate;
8
9#[cfg(test)]
10mod corroboration_tests;
11#[cfg(test)]
12pub(crate) mod test_support;
13
14use std::net::SocketAddr;
15use std::time::Duration;
16
17use chia_consensus::consensus_constants::ConsensusConstants;
18use chia_protocol::{
19 Bytes32, CoinStateFilters, FullBlock as ProtoFullBlock, RejectAdditionsRequest, RejectBlock,
20 RejectHeaderRequest, RejectRemovalsRequest, RequestAdditions, RequestBlock, RequestBlockHeader,
21 RequestFeeEstimates, RequestRemovals, RespondAdditions, RespondBlock, RespondBlockHeader,
22 RespondFeeEstimates, RespondRemovals, SpendBundle as ProtoBundle,
23};
24use chia_wallet_sdk::client::Peer;
25use chia_wallet_sdk::types::{MAINNET_CONSTANTS, TESTNET11_CONSTANTS};
26use tokio_tungstenite::Connector;
27
28use crate::types::*;
29
30use crate::NetworkType;
31use plurality::CORROBORATION_FLOOR;
32pub use pool::PeerRequirement;
33use pool::{CorroborationReadiness, PeerPool};
34
35#[derive(Debug, Clone, PartialEq, Eq)]
46pub enum OptAnswer<T> {
47 Found(T),
56 UncorroboratedFound(T),
62 CorroboratedAbsent,
64 UncorroboratedAbsent,
70}
71
72fn corroborated(readiness: CorroborationReadiness, agreed: usize) -> bool {
95 matches!(readiness, CorroborationReadiness::Armed { .. }) && agreed >= CORROBORATION_FLOOR
96}
97
98#[cfg(test)]
109impl PeerBackend {
110 pub(crate) fn for_tests() -> Self {
111 Self {
112 pool: pool::PeerPool::for_tests(0),
113 network: NetworkType::Mainnet,
114 request_timeout: Duration::from_millis(1),
115 }
116 }
117}
118
119pub struct PeerBackend {
120 pool: PeerPool,
121 network: NetworkType,
122 request_timeout: Duration,
123}
124
125impl PeerBackend {
126 pub async fn new(
127 network: crate::NetworkType,
128 tls: Connector,
129 max_peers: usize,
130 requirement: PeerRequirement,
131 connect_timeout: Duration,
132 request_timeout: Duration,
133 ) -> Result<Self, ChiaQueryError> {
134 let pool = PeerPool::new(network, tls, max_peers, requirement, connect_timeout).await?;
135 Ok(Self {
136 pool,
137 network,
138 request_timeout,
139 })
140 }
141
142 pub fn constants(&self) -> &ConsensusConstants {
144 match self.network {
145 NetworkType::Mainnet => &MAINNET_CONSTANTS,
146 NetworkType::Testnet11 => &TESTNET11_CONSTANTS,
147 }
148 }
149
150 fn genesis_challenge(&self) -> Bytes32 {
154 self.constants().genesis_challenge
155 }
156
157 pub async fn has_peers(&self) -> bool {
158 self.pool.has_peers().await
159 }
160
161 pub async fn peer_count(&self) -> usize {
163 self.pool.peer_count().await
164 }
165
166 pub async fn independent_peer_count(&self) -> usize {
168 self.pool.independent_peer_count().await
169 }
170
171 pub async fn corroboration_readiness(&self, asked: SocketAddr) -> pool::CorroborationReadiness {
174 self.pool.corroboration_readiness(asked).await
175 }
176
177 pub async fn subscribe_frames(&self, capacity: usize) -> frames::FrameSubscription {
182 self.pool.subscribe_frames(capacity).await
183 }
184
185 async fn pick(&self) -> Result<(Peer, SocketAddr), ChiaQueryError> {
190 self.pool.maintain().await;
197
198 self.pool
199 .select_peer()
200 .await
201 .ok_or_else(|| ChiaQueryError::PeerConnection("no peers available".into()))
202 }
203
204 pub async fn try_get_coin_record_by_name(
210 &self,
211 name: &str,
212 ) -> Result<CoinRecord, ChiaQueryError> {
213 let (peer, addr) = self.pick().await?;
214 let res = self.do_get_coin_record_by_name(&peer, name).await;
215 if res.is_err() {
216 self.pool.eject_peer(addr).await;
217 }
218 res
219 }
220
221 pub async fn try_get_coin_record_by_name_opt(
229 &self,
230 name: &str,
231 ) -> Result<OptAnswer<CoinRecord>, ChiaQueryError> {
232 self.read_opt_corroborated(|peer| async move {
233 self.do_get_coin_record_by_name_opt(&peer, name).await
234 })
235 .await
236 }
237
238 async fn read_opt_corroborated<T, F, Fut>(
277 &self,
278 read: F,
279 ) -> Result<OptAnswer<T>, ChiaQueryError>
280 where
281 T: ChainClaim,
282 F: Fn(Peer) -> Fut,
283 Fut: std::future::Future<Output = Result<Option<T>, ChiaQueryError>>,
284 {
285 let (peer, addr) = self.pick().await?;
286 let first = match read(peer).await {
287 Ok(v) => v,
288 Err(e) => {
289 self.pool.eject_peer(addr).await;
290 return Err(e);
291 }
292 };
293
294 match first {
295 Some(found) => self.corroborate_presence(found, addr, &read).await,
296 None => self.corroborate_absence(addr, &read).await,
297 }
298 }
299
300 async fn corroborate_presence<T, F, Fut>(
302 &self,
303 found: T,
304 addr: SocketAddr,
305 read: &F,
306 ) -> Result<OptAnswer<T>, ChiaQueryError>
307 where
308 T: ChainClaim,
309 F: Fn(Peer) -> Fut,
310 Fut: std::future::Future<Output = Result<Option<T>, ChiaQueryError>>,
311 {
312 let readiness = self.pool.corroboration_readiness(addr).await;
317
318 let corroborators = self.pool.select_corroborating_peers(addr).await;
319 if corroborators.is_empty() {
320 log::debug!("presence reported by {addr} has no independent corroborator available");
321 return Ok(OptAnswer::UncorroboratedFound(found));
322 }
323
324 let answers =
328 futures_util::future::join_all(corroborators.into_iter().map(|(peer, peer_addr)| {
329 let answer = read(peer);
330 async move { (peer_addr, answer.await) }
331 }))
332 .await;
333
334 let claim = found.chain_claim();
335 let mut agreed = 0usize;
336 let mut disagreement: Option<String> = None;
337 let mut failed: Vec<SocketAddr> = Vec::new();
338
339 for (peer_addr, answer) in answers {
340 match answer {
341 Ok(Some(other)) if other.chain_claim() == claim => agreed += 1,
342 Ok(Some(other)) => {
343 disagreement.get_or_insert_with(|| {
344 format!(
345 "peer {addr} claims `{claim}`, peer {peer_addr} claims `{}`",
346 other.chain_claim()
347 )
348 });
349 }
350 Ok(None) => {
351 disagreement.get_or_insert_with(|| {
352 format!("peer {addr} reports present, peer {peer_addr} reports absent")
353 });
354 }
355 Err(e) => {
356 log::debug!("corroborator {peer_addr} failed: {e}");
357 failed.push(peer_addr);
358 }
359 }
360 }
361
362 for peer_addr in failed {
365 self.pool.eject_peer(peer_addr).await;
366 }
367
368 if let Some(detail) = disagreement {
372 return Err(ChiaQueryError::SourcesDisagree(detail));
373 }
374
375 if !corroborated(readiness, agreed) {
376 log::debug!(
377 "presence reported by {addr} drew {agreed} agreeing voices with the pool \
378 {readiness:?}; below the floor of {CORROBORATION_FLOOR}"
379 );
380 return Ok(OptAnswer::UncorroboratedFound(found));
381 }
382 Ok(OptAnswer::Found(found))
383 }
384
385 async fn corroborate_absence<T, F, Fut>(
391 &self,
392 addr: SocketAddr,
393 read: &F,
394 ) -> Result<OptAnswer<T>, ChiaQueryError>
395 where
396 T: ChainClaim,
397 F: Fn(Peer) -> Fut,
398 Fut: std::future::Future<Output = Result<Option<T>, ChiaQueryError>>,
399 {
400 let readiness = self.pool.corroboration_readiness(addr).await;
401
402 let corroborators = self.pool.select_corroborating_peers(addr).await;
403 if corroborators.is_empty() {
404 log::debug!("absence reported by {addr} has no independent corroborator available");
405 return Ok(OptAnswer::UncorroboratedAbsent);
406 }
407
408 let answers =
409 futures_util::future::join_all(corroborators.into_iter().map(|(peer, peer_addr)| {
410 let answer = read(peer);
411 async move { (peer_addr, answer.await) }
412 }))
413 .await;
414
415 let mut agreed = 0usize;
416 let mut disagreement: Option<String> = None;
417 let mut failed: Vec<SocketAddr> = Vec::new();
418
419 for (peer_addr, answer) in answers {
420 match answer {
421 Ok(None) => agreed += 1,
422 Ok(Some(_)) => {
423 disagreement.get_or_insert_with(|| {
424 format!("peer {addr} reports absent, peer {peer_addr} reports present")
425 });
426 }
427 Err(e) => {
428 log::debug!("corroborator {peer_addr} failed: {e}");
429 failed.push(peer_addr);
430 }
431 }
432 }
433
434 for peer_addr in failed {
437 self.pool.eject_peer(peer_addr).await;
438 }
439
440 if let Some(detail) = disagreement {
441 return Err(ChiaQueryError::SourcesDisagree(detail));
442 }
443
444 if !corroborated(readiness, agreed) {
445 log::debug!(
446 "absence reported by {addr} drew {agreed} agreeing voices with the pool \
447 {readiness:?}; below the floor of {CORROBORATION_FLOOR}"
448 );
449 return Ok(OptAnswer::UncorroboratedAbsent);
450 }
451 Ok(OptAnswer::CorroboratedAbsent)
452 }
453
454 pub async fn try_get_coin_spend_opt(
461 &self,
462 coin_id: &str,
463 ) -> Result<OptAnswer<CoinSpend>, ChiaQueryError> {
464 self.read_opt_corroborated(|peer| async move {
465 self.do_get_coin_spend_opt(&peer, coin_id).await
466 })
467 .await
468 }
469
470 pub async fn try_get_coin_records_by_puzzle_hash(
471 &self,
472 puzzle_hash: &str,
473 start_height: Option<u32>,
474 end_height: Option<u32>,
475 include_spent: bool,
476 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
477 let (peer, addr) = self.pick().await?;
478 let res = self
479 .do_puzzle_hash_query(
480 &peer,
481 &[puzzle_hash],
482 start_height,
483 end_height,
484 include_spent,
485 false,
486 )
487 .await;
488 if res.is_err() {
489 self.pool.eject_peer(addr).await;
490 }
491 res
492 }
493
494 pub async fn try_get_coin_records_by_puzzle_hashes(
495 &self,
496 puzzle_hashes: &[String],
497 start_height: Option<u32>,
498 end_height: Option<u32>,
499 include_spent: bool,
500 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
501 let hashes: Vec<&str> = puzzle_hashes.iter().map(String::as_str).collect();
502 let (peer, addr) = self.pick().await?;
503 let res = self
504 .do_puzzle_hash_query(
505 &peer,
506 &hashes,
507 start_height,
508 end_height,
509 include_spent,
510 false,
511 )
512 .await;
513 if res.is_err() {
514 self.pool.eject_peer(addr).await;
515 }
516 res
517 }
518
519 pub async fn try_get_coin_records_by_hint(
520 &self,
521 hint: &str,
522 start_height: Option<u32>,
523 end_height: Option<u32>,
524 include_spent: bool,
525 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
526 let (peer, addr) = self.pick().await?;
527 let res = self
528 .do_puzzle_hash_query(
529 &peer,
530 &[hint],
531 start_height,
532 end_height,
533 include_spent,
534 true,
535 )
536 .await;
537 if res.is_err() {
538 self.pool.eject_peer(addr).await;
539 }
540 res
541 }
542
543 pub async fn try_get_coin_records_by_hints(
544 &self,
545 hints: &[String],
546 start_height: Option<u32>,
547 end_height: Option<u32>,
548 include_spent: bool,
549 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
550 let hs: Vec<&str> = hints.iter().map(String::as_str).collect();
551 let (peer, addr) = self.pick().await?;
552 let res = self
553 .do_puzzle_hash_query(&peer, &hs, start_height, end_height, include_spent, true)
554 .await;
555 if res.is_err() {
556 self.pool.eject_peer(addr).await;
557 }
558 res
559 }
560
561 pub async fn try_get_coin_records_by_names(
562 &self,
563 names: &[String],
564 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
565 let (peer, addr) = self.pick().await?;
566 let res = self.do_coin_ids_query(&peer, names).await;
567 if res.is_err() {
568 self.pool.eject_peer(addr).await;
569 }
570 res
571 }
572
573 pub async fn try_get_puzzle_and_solution(
574 &self,
575 coin_id: &str,
576 height: u32,
577 ) -> Result<CoinSpend, ChiaQueryError> {
578 let (peer, addr) = self.pick().await?;
579 let res = self
580 .do_get_puzzle_and_solution(&peer, coin_id, height)
581 .await;
582 if res.is_err() {
583 self.pool.eject_peer(addr).await;
584 }
585 res
586 }
587
588 pub async fn try_get_fee_estimate(
589 &self,
590 target_times: &[u64],
591 ) -> Result<FeeEstimate, ChiaQueryError> {
592 let (peer, addr) = self.pick().await?;
593 let res = self.do_get_fee_estimate(&peer, target_times).await;
594 if res.is_err() {
595 self.pool.eject_peer(addr).await;
596 }
597 res
598 }
599
600 pub async fn try_push_tx(&self, bundle: &SpendBundle) -> Result<TxStatus, ChiaQueryError> {
601 let (peer, addr) = self.pick().await?;
602 let res = self.do_push_tx(&peer, bundle).await;
603 if res.is_err() {
604 self.pool.eject_peer(addr).await;
605 }
606 res
607 }
608
609 pub async fn try_get_block_record_by_height(
612 &self,
613 height: u32,
614 ) -> Result<BlockRecord, ChiaQueryError> {
615 let (peer, addr) = self.pick().await?;
616 let res = self.do_get_block_record_by_height(&peer, height).await;
617 if res.is_err() {
618 self.pool.eject_peer(addr).await;
619 }
620 res
621 }
622
623 #[allow(dead_code)]
629 pub async fn try_get_additions_and_removals(
630 &self,
631 height: u32,
632 header_hash: &str,
633 ) -> Result<AdditionsAndRemovals, ChiaQueryError> {
634 let (peer, addr) = self.pick().await?;
635 let res = self
636 .do_get_additions_and_removals(&peer, height, header_hash)
637 .await;
638 if res.is_err() {
639 self.pool.eject_peer(addr).await;
640 }
641 res
642 }
643
644 pub async fn try_get_children(
647 &self,
648 parent_id: &str,
649 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
650 let (peer, addr) = self.pick().await?;
651 let res = self.do_get_children(&peer, parent_id).await;
652 if res.is_err() {
653 self.pool.eject_peer(addr).await;
654 }
655 res
656 }
657
658 pub async fn try_get_block_by_height(
661 &self,
662 height: u32,
663 ) -> Result<serde_json::Value, ChiaQueryError> {
664 let (peer, addr) = self.pick().await?;
665 let res = self.do_get_block_by_height(&peer, height).await;
666 if res.is_err() {
667 self.pool.eject_peer(addr).await;
668 }
669 res
670 }
671
672 pub async fn try_get_additions_and_removals_from_block(
675 &self,
676 height: u32,
677 ) -> Result<AdditionsAndRemovals, ChiaQueryError> {
678 let (peer, addr) = self.pick().await?;
679 let res = self
680 .do_get_additions_and_removals_from_block(&peer, height)
681 .await;
682 if res.is_err() {
683 self.pool.eject_peer(addr).await;
684 }
685 res
686 }
687
688 pub async fn try_get_block_spends_by_height(
691 &self,
692 height: u32,
693 ) -> Result<Vec<CoinSpend>, ChiaQueryError> {
694 let (peer, addr) = self.pick().await?;
695 let res = self.do_get_block_spends(&peer, height).await;
696 if res.is_err() {
697 self.pool.eject_peer(addr).await;
698 }
699 res
700 }
701
702 pub async fn try_get_block_spends_with_conditions(
705 &self,
706 height: u32,
707 ) -> Result<Vec<CoinSpendWithConditions>, ChiaQueryError> {
708 let (peer, addr) = self.pick().await?;
709 let proto_block = self.fetch_full_block(&peer, height).await;
710 if proto_block.is_err() {
711 self.pool.eject_peer(addr).await;
712 }
713 let proto_block = proto_block?;
714 block::block_spends_with_conditions(&proto_block, self.constants())
715 }
716
717 pub async fn try_get_puzzle_and_solution_auto(
720 &self,
721 coin_id: &str,
722 ) -> Result<CoinSpend, ChiaQueryError> {
723 let (peer, addr) = self.pick().await?;
725 let id = translate::parse_bytes32(coin_id)?;
726
727 let state_resp = tokio::time::timeout(self.request_timeout, {
728 peer.request_coin_state(vec![id], None, self.genesis_challenge(), false)
729 })
730 .await
731 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
732 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
733 .map_err(|_| ChiaQueryError::PeerRejection("coin state rejected".into()))?;
734
735 let cs = state_resp
736 .coin_states
737 .first()
738 .ok_or_else(|| ChiaQueryError::PeerRejection("coin not found".into()))?;
739 let spent_height = cs
740 .spent_height
741 .ok_or_else(|| ChiaQueryError::PeerRejection("coin is not spent".into()))?;
742
743 let res = self
744 .do_get_puzzle_and_solution(&peer, coin_id, spent_height)
745 .await;
746 if res.is_err() {
747 self.pool.eject_peer(addr).await;
748 }
749 res
750 }
751
752 pub async fn try_get_block_records(
755 &self,
756 start: u32,
757 end: u32,
758 ) -> Result<Vec<BlockRecord>, ChiaQueryError> {
759 let mut records = Vec::with_capacity((end - start) as usize);
760 for height in start..end {
761 records.push(self.try_get_block_record_by_height(height).await?);
762 }
763 Ok(records)
764 }
765
766 pub async fn try_get_blocks_range(
769 &self,
770 start: u32,
771 end: u32,
772 ) -> Result<Vec<serde_json::Value>, ChiaQueryError> {
773 let mut blocks = Vec::with_capacity((end - start) as usize);
774 for height in start..end {
775 blocks.push(self.try_get_block_by_height(height).await?);
776 }
777 Ok(blocks)
778 }
779
780 pub fn network_info(&self) -> NetworkInfo {
783 let c = self.constants();
784 NetworkInfo {
785 network_name: self.network.network_id().to_string(),
786 network_prefix: match self.network {
787 NetworkType::Mainnet => "xch".to_string(),
788 NetworkType::Testnet11 => "txch".to_string(),
789 },
790 genesis_challenge: format!("0x{}", hex::encode(c.genesis_challenge)),
791 }
792 }
793
794 pub fn aggsig_additional_data(&self) -> String {
797 format!(
798 "0x{}",
799 hex::encode(self.constants().agg_sig_me_additional_data)
800 )
801 }
802
803 pub fn peak_height(&self) -> u32 {
806 self.pool.peak_height()
807 }
808
809 async fn do_get_coin_record_by_name(
814 &self,
815 peer: &Peer,
816 name: &str,
817 ) -> Result<CoinRecord, ChiaQueryError> {
818 let coin_id = translate::parse_bytes32(name)?;
819
820 let response = tokio::time::timeout(self.request_timeout, {
821 peer.request_coin_state(vec![coin_id], None, self.genesis_challenge(), false)
822 })
823 .await
824 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
825 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
826 .map_err(|_| ChiaQueryError::PeerRejection("coin state request rejected".into()))?;
827
828 response
829 .coin_states
830 .first()
831 .map(translate::coin_state_to_record)
832 .ok_or_else(|| ChiaQueryError::PeerRejection("coin not found".into()))
833 }
834
835 async fn do_get_coin_record_by_name_opt(
838 &self,
839 peer: &Peer,
840 name: &str,
841 ) -> Result<Option<CoinRecord>, ChiaQueryError> {
842 let coin_id = translate::parse_bytes32(name)?;
843
844 let response = tokio::time::timeout(self.request_timeout, {
845 peer.request_coin_state(vec![coin_id], None, self.genesis_challenge(), false)
846 })
847 .await
848 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
849 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
850 .map_err(|_| ChiaQueryError::PeerRejection("coin state request rejected".into()))?;
851
852 Ok(response
857 .coin_states
858 .first()
859 .map(translate::coin_state_to_record))
860 }
861
862 async fn do_get_coin_spend_opt(
865 &self,
866 peer: &Peer,
867 coin_id: &str,
868 ) -> Result<Option<CoinSpend>, ChiaQueryError> {
869 let id = translate::parse_bytes32(coin_id)?;
870
871 let state_resp = tokio::time::timeout(self.request_timeout, {
872 peer.request_coin_state(vec![id], None, self.genesis_challenge(), false)
873 })
874 .await
875 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
876 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
877 .map_err(|_| ChiaQueryError::PeerRejection("coin state rejected".into()))?;
878
879 let Some(cs) = state_resp.coin_states.first() else {
881 return Ok(None);
882 };
883 let Some(spent_height) = cs.spent_height else {
884 return Ok(None);
885 };
886
887 let spend = self
888 .do_get_puzzle_and_solution(peer, coin_id, spent_height)
889 .await?;
890
891 let spend = CoinSpend {
898 coin: Coin::from_protocol(&cs.coin),
899 ..spend
900 };
901 Ok(Some(spend))
902 }
903
904 async fn do_puzzle_hash_query(
905 &self,
906 peer: &Peer,
907 hashes: &[&str],
908 start_height: Option<u32>,
909 end_height: Option<u32>,
910 include_spent: bool,
911 include_hinted: bool,
912 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
913 let puzzle_hashes: Vec<Bytes32> = hashes
914 .iter()
915 .map(|h| translate::parse_bytes32(h))
916 .collect::<Result<_, _>>()?;
917
918 let filters = CoinStateFilters {
919 include_spent,
920 include_unspent: true,
921 include_hinted,
922 min_amount: 0,
923 };
924
925 let mut all_states = Vec::new();
926 let mut prev_height: Option<u32> = None;
932 let mut prev_header = self.genesis_challenge();
933
934 loop {
935 let response = tokio::time::timeout(self.request_timeout, {
936 peer.request_puzzle_state(
937 puzzle_hashes.clone(),
938 prev_height,
939 prev_header,
940 filters.clone(),
941 false,
942 )
943 })
944 .await
945 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
946 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
947 .map_err(|_| ChiaQueryError::PeerRejection("puzzle state request rejected".into()))?;
948
949 all_states.extend(response.coin_states.iter().cloned());
950
951 if response.is_finished {
952 break;
953 }
954 prev_height = Some(response.height);
955 prev_header = response.header_hash;
956 }
957
958 let records: Vec<CoinRecord> = all_states
960 .iter()
961 .filter(|cs| {
962 let h = cs.created_height.unwrap_or(0);
963 let above_start = start_height.is_none_or(|s| h >= s);
964 let below_end = end_height.is_none_or(|e| h <= e);
965 above_start && below_end
966 })
967 .map(translate::coin_state_to_record)
968 .collect();
969
970 Ok(records)
971 }
972
973 async fn do_coin_ids_query(
974 &self,
975 peer: &Peer,
976 names: &[String],
977 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
978 let ids: Vec<Bytes32> = names
979 .iter()
980 .map(|n| translate::parse_bytes32(n))
981 .collect::<Result<_, _>>()?;
982
983 let response = tokio::time::timeout(self.request_timeout, {
984 peer.request_coin_state(ids, None, self.genesis_challenge(), false)
985 })
986 .await
987 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
988 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
989 .map_err(|_| ChiaQueryError::PeerRejection("coin state request rejected".into()))?;
990
991 Ok(translate::coin_states_to_records(&response.coin_states))
992 }
993
994 async fn do_get_puzzle_and_solution(
995 &self,
996 peer: &Peer,
997 coin_id: &str,
998 height: u32,
999 ) -> Result<CoinSpend, ChiaQueryError> {
1000 let id = translate::parse_bytes32(coin_id)?;
1001
1002 let response = tokio::time::timeout(self.request_timeout, {
1003 peer.request_puzzle_and_solution(id, height)
1004 })
1005 .await
1006 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
1007 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
1008 .map_err(|_| ChiaQueryError::PeerRejection("puzzle solution rejected".into()))?;
1009
1010 Ok(translate::make_coin_spend(
1011 &chia_protocol::Coin {
1017 parent_coin_info: response.coin_name,
1018 puzzle_hash: Bytes32::default(),
1019 amount: 0,
1020 },
1021 &response.puzzle,
1022 &response.solution,
1023 ))
1024 }
1025
1026 async fn do_get_fee_estimate(
1027 &self,
1028 peer: &Peer,
1029 target_times: &[u64],
1030 ) -> Result<FeeEstimate, ChiaQueryError> {
1031 let request = RequestFeeEstimates {
1032 time_targets: target_times.to_vec(),
1033 };
1034
1035 let response: RespondFeeEstimates =
1036 tokio::time::timeout(self.request_timeout, peer.request_infallible(request))
1037 .await
1038 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
1039 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?;
1040
1041 let estimates: Vec<f64> = response
1042 .estimates
1043 .estimates
1044 .iter()
1045 .map(|e| e.estimated_fee_rate.mojos_per_clvm_cost as f64)
1046 .collect();
1047
1048 Ok(translate::make_fee_estimate(
1049 estimates,
1050 target_times.to_vec(),
1051 ))
1052 }
1053
1054 async fn do_push_tx(
1055 &self,
1056 peer: &Peer,
1057 bundle: &SpendBundle,
1058 ) -> Result<TxStatus, ChiaQueryError> {
1059 let proto = to_protocol_spend_bundle(bundle)?;
1060
1061 let ack = tokio::time::timeout(self.request_timeout, peer.send_transaction(proto))
1062 .await
1063 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
1064 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?;
1065
1066 Ok(translate::ack_to_tx_status(ack.status))
1067 }
1068
1069 async fn do_get_block_by_height(
1072 &self,
1073 peer: &Peer,
1074 height: u32,
1075 ) -> Result<serde_json::Value, ChiaQueryError> {
1076 let proto_block = self.fetch_full_block(peer, height).await?;
1077 serde_json::to_value(&proto_block)
1078 .map_err(|e| ChiaQueryError::PeerConnection(format!("serialize block: {e}")))
1079 }
1080
1081 async fn do_get_additions_and_removals_from_block(
1084 &self,
1085 peer: &Peer,
1086 height: u32,
1087 ) -> Result<AdditionsAndRemovals, ChiaQueryError> {
1088 let proto_block = self.fetch_full_block(peer, height).await?;
1089 block::block_additions_and_removals(&proto_block, height, self.constants())
1090 }
1091
1092 async fn do_get_block_spends(
1095 &self,
1096 peer: &Peer,
1097 height: u32,
1098 ) -> Result<Vec<CoinSpend>, ChiaQueryError> {
1099 let proto_block = self.fetch_full_block(peer, height).await?;
1100 block::block_spends(&proto_block, self.constants())
1101 }
1102
1103 async fn fetch_full_block(
1106 &self,
1107 peer: &Peer,
1108 height: u32,
1109 ) -> Result<ProtoFullBlock, ChiaQueryError> {
1110 let response = tokio::time::timeout(self.request_timeout, {
1111 peer.request_fallible::<RespondBlock, RejectBlock, _>(RequestBlock {
1112 height,
1113 include_transaction_block: true,
1114 })
1115 })
1116 .await
1117 .map_err(|_| ChiaQueryError::PeerConnection("block request timed out".into()))?
1118 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
1119 .map_err(|_| ChiaQueryError::PeerRejection("block request rejected".into()))?;
1120
1121 Ok(response.block)
1122 }
1123
1124 async fn do_get_block_record_by_height(
1127 &self,
1128 peer: &Peer,
1129 height: u32,
1130 ) -> Result<BlockRecord, ChiaQueryError> {
1131 let response = tokio::time::timeout(self.request_timeout, {
1132 peer.request_fallible::<RespondBlockHeader, RejectHeaderRequest, _>(
1133 RequestBlockHeader { height },
1134 )
1135 })
1136 .await
1137 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
1138 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
1139 .map_err(|_| ChiaQueryError::PeerRejection("header request rejected".into()))?;
1140
1141 Ok(translate::header_block_to_block_record(
1142 &response.header_block,
1143 ))
1144 }
1145
1146 async fn do_get_additions_and_removals(
1149 &self,
1150 peer: &Peer,
1151 height: u32,
1152 header_hash_hex: &str,
1153 ) -> Result<AdditionsAndRemovals, ChiaQueryError> {
1154 let header_hash = translate::parse_bytes32(header_hash_hex)?;
1155
1156 let (adds_result, rems_result) = tokio::join!(
1158 tokio::time::timeout(self.request_timeout, {
1159 peer.request_fallible::<RespondAdditions, RejectAdditionsRequest, _>(
1160 RequestAdditions {
1161 height,
1162 header_hash: Some(header_hash),
1163 puzzle_hashes: None,
1164 },
1165 )
1166 }),
1167 tokio::time::timeout(self.request_timeout, {
1168 peer.request_fallible::<RespondRemovals, RejectRemovalsRequest, _>(
1169 RequestRemovals {
1170 height,
1171 header_hash,
1172 coin_names: None,
1173 },
1174 )
1175 }),
1176 );
1177
1178 let adds = adds_result
1179 .map_err(|_| ChiaQueryError::PeerConnection("additions request timed out".into()))?
1180 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
1181 .map_err(|_| ChiaQueryError::PeerRejection("additions rejected".into()))?;
1182
1183 let rems = rems_result
1184 .map_err(|_| ChiaQueryError::PeerConnection("removals request timed out".into()))?
1185 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
1186 .map_err(|_| ChiaQueryError::PeerRejection("removals rejected".into()))?;
1187
1188 Ok(translate::additions_removals_to_response(
1189 &adds, &rems, height,
1190 ))
1191 }
1192
1193 async fn do_get_children(
1196 &self,
1197 peer: &Peer,
1198 parent_id: &str,
1199 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
1200 let coin_name = translate::parse_bytes32(parent_id)?;
1201
1202 let response = tokio::time::timeout(self.request_timeout, peer.request_children(coin_name))
1203 .await
1204 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
1205 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?;
1206
1207 Ok(translate::coin_states_to_records(&response.coin_states))
1208 }
1209}
1210
1211fn to_protocol_spend_bundle(bundle: &SpendBundle) -> Result<ProtoBundle, ChiaQueryError> {
1216 let coin_spends: Vec<chia_protocol::CoinSpend> = bundle
1217 .coin_spends
1218 .iter()
1219 .map(|cs| {
1220 Ok(chia_protocol::CoinSpend {
1221 coin: chia_protocol::Coin {
1222 parent_coin_info: translate::parse_bytes32(&cs.coin.parent_coin_info)?,
1223 puzzle_hash: translate::parse_bytes32(&cs.coin.puzzle_hash)?,
1224 amount: cs.coin.amount,
1225 },
1226 puzzle_reveal: chia_protocol::Program::from(chia_protocol::Bytes::from(
1227 translate::parse_hex(&cs.puzzle_reveal)?,
1228 )),
1229 solution: chia_protocol::Program::from(chia_protocol::Bytes::from(
1230 translate::parse_hex(&cs.solution)?,
1231 )),
1232 })
1233 })
1234 .collect::<Result<_, ChiaQueryError>>()?;
1235
1236 let sig_bytes = translate::parse_hex(&bundle.aggregated_signature)?;
1237 let sig_arr: [u8; 96] = sig_bytes
1238 .try_into()
1239 .map_err(|_| ChiaQueryError::InvalidRequest("signature must be 96 bytes".into()))?;
1240 let aggregated_signature = chia_bls::Signature::from_bytes(&sig_arr)
1241 .map_err(|e| ChiaQueryError::InvalidRequest(format!("bad BLS signature: {e}")))?;
1242
1243 Ok(ProtoBundle {
1244 coin_spends,
1245 aggregated_signature,
1246 })
1247}
1248
1249#[cfg(test)]
1250mod grading_tests {
1251 use super::*;
1252
1253 #[test]
1264 fn agreement_alone_does_not_corroborate_when_the_pool_was_not_armed() {
1265 let unarmed = CorroborationReadiness::Insufficient {
1266 corroborators: 1,
1267 required: CORROBORATION_FLOOR,
1268 };
1269
1270 assert!(
1271 !corroborated(unarmed, CORROBORATION_FLOOR),
1272 "a pool that held too few independent peers has corroborated nothing, however many \
1273 voices answered"
1274 );
1275 }
1276
1277 #[test]
1282 fn an_armed_pool_with_a_floor_of_agreement_corroborates() {
1283 let armed = CorroborationReadiness::Armed {
1284 corroborators: CORROBORATION_FLOOR,
1285 };
1286
1287 assert!(corroborated(armed, CORROBORATION_FLOOR));
1288 assert!(
1289 !corroborated(armed, CORROBORATION_FLOOR - 1),
1290 "an armed pool whose corroborators timed out has corroborated nothing"
1291 );
1292 }
1293}