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