1use chia_protocol::{Bytes32, CoinSpend};
11use dig_chainsource_interface::{
12 ChainSource, ChainSourceError, ChainSourceProvider, CoinRecord, ProviderKind, SingletonLineage,
13};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum TrustLevel {
19 Trusted,
21 Untrusted,
23}
24
25impl TrustLevel {
26 pub fn default_for(kind: ProviderKind) -> Self {
29 match kind {
30 ProviderKind::LocalNode => Self::Trusted,
31 ProviderKind::PublicOracle | ProviderKind::DigPeers | ProviderKind::Custom => {
32 Self::Untrusted
33 }
34 }
35 }
36}
37
38const PUBLIC_QUORUM_THRESHOLD: usize = 2;
40
41const MAX_QUORUM_RECORDS: usize = 100_000;
52
53type DynProvider = dyn ChainSourceProvider<Error = ChainSourceError>;
55
56struct Registration {
59 provider: Box<DynProvider>,
60 trust: TrustLevel,
61 independence_group: String,
62}
63
64#[derive(Default)]
67pub struct ProviderRegistry {
68 providers: Vec<Registration>,
69 allow_public_quorum_custody: bool,
70}
71
72impl ProviderRegistry {
73 pub fn new() -> Self {
76 Self::default()
77 }
78
79 pub fn allow_public_quorum_custody(mut self, allow: bool) -> Self {
86 self.allow_public_quorum_custody = allow;
87 if allow {
88 log::warn!(
89 "chia-query registry: pure-public-quorum custody ENABLED — custody reads may be \
90 satisfied by {PUBLIC_QUORUM_THRESHOLD} independent public sources with NO \
91 operator-trusted source. Reduced assurance vs a trusted local node."
92 );
93 }
94 self
95 }
96
97 pub fn register(
101 mut self,
102 provider: Box<DynProvider>,
103 trust_override: Option<TrustLevel>,
104 independence_group: impl Into<String>,
105 ) -> Self {
106 let trust = trust_override
107 .unwrap_or_else(|| TrustLevel::default_for(provider.provider_info().kind));
108 self.providers.push(Registration {
109 provider,
110 trust,
111 independence_group: independence_group.into(),
112 });
113 self
114 }
115
116 pub fn trusted(&self) -> TrustedView<'_> {
122 TrustedView { registry: self }
123 }
124
125 pub fn any(&self) -> DiscoveryView<'_> {
130 DiscoveryView { registry: self }
131 }
132
133 fn by_priority(&self) -> Vec<&Registration> {
135 let mut ordered: Vec<&Registration> = self.providers.iter().collect();
136 ordered.sort_by_key(|reg| reg.provider.provider_info().priority);
137 ordered
138 }
139}
140
141pub struct TrustedView<'a> {
143 registry: &'a ProviderRegistry,
144}
145
146impl TrustedView<'_> {
147 fn custody_read<T, Q>(&self, query: Q) -> Result<T, ChainSourceError>
150 where
151 T: QuorumComparable + Clone,
152 Q: Fn(&DynProvider) -> Result<T, ChainSourceError>,
153 {
154 let trusted: Vec<&Registration> = self
155 .registry
156 .by_priority()
157 .into_iter()
158 .filter(|reg| reg.trust == TrustLevel::Trusted)
159 .collect();
160
161 if !trusted.is_empty() {
163 let mut last_error = ChainSourceError::NoProvider;
164 for reg in trusted {
165 match query(&*reg.provider) {
166 Ok(value) => return Ok(value),
167 Err(error) => last_error = error,
168 }
169 }
170 return Err(last_error);
172 }
173
174 if !self.registry.allow_public_quorum_custody {
176 return Err(ChainSourceError::NoProvider);
177 }
178 quorum_read(&self.registry.providers, PUBLIC_QUORUM_THRESHOLD, query)
179 }
180}
181
182pub struct DiscoveryView<'a> {
184 registry: &'a ProviderRegistry,
185}
186
187impl DiscoveryView<'_> {
188 fn discovery_read<T, Q>(&self, query: Q) -> Result<T, ChainSourceError>
190 where
191 Q: Fn(&DynProvider) -> Result<T, ChainSourceError>,
192 {
193 let mut last_error = ChainSourceError::NoProvider;
194 for reg in self.registry.by_priority() {
195 match query(&*reg.provider) {
196 Ok(value) => return Ok(value),
197 Err(error) => last_error = error,
198 }
199 }
200 Err(last_error)
201 }
202}
203
204trait QuorumComparable {
214 fn quorum_eq(&self, other: &Self) -> bool;
216
217 fn validate_bound(&self) -> Result<(), ChainSourceError> {
221 Ok(())
222 }
223}
224
225macro_rules! quorum_eq_via_partial_eq {
227 ($($t:ty),+ $(,)?) => {
228 $(impl QuorumComparable for $t {
229 fn quorum_eq(&self, other: &Self) -> bool {
230 self == other
231 }
232 })+
233 };
234}
235
236quorum_eq_via_partial_eq!(
237 Option<CoinRecord>,
238 Option<CoinSpend>,
239 Option<SingletonLineage>,
240 Option<u32>,
241 Option<u64>,
242);
243
244impl QuorumComparable for Vec<CoinRecord> {
245 fn quorum_eq(&self, other: &Self) -> bool {
246 canonical_order(self) == canonical_order(other)
247 }
248
249 fn validate_bound(&self) -> Result<(), ChainSourceError> {
250 if self.len() > MAX_QUORUM_RECORDS {
251 return Err(ChainSourceError::Malformed(format!(
252 "untrusted source returned {} coin records, exceeding the {MAX_QUORUM_RECORDS} cap",
253 self.len()
254 )));
255 }
256 Ok(())
257 }
258}
259
260fn canonical_order(records: &[CoinRecord]) -> Vec<(Bytes32, &CoinRecord)> {
269 let mut keyed: Vec<(Bytes32, &CoinRecord)> =
270 records.iter().map(|r| (r.coin.coin_id(), r)).collect();
271 keyed.sort_by(|a, b| a.0.as_ref().cmp(b.0.as_ref()));
272 keyed
273}
274
275fn quorum_read<T, Q>(
283 providers: &[Registration],
284 threshold: usize,
285 query: Q,
286) -> Result<T, ChainSourceError>
287where
288 T: QuorumComparable + Clone,
289 Q: Fn(&DynProvider) -> Result<T, ChainSourceError>,
290{
291 let mut per_group: Vec<(&str, T)> = Vec::new();
293 for reg in providers {
294 let group = reg.independence_group.as_str();
295 if per_group.iter().any(|(existing, _)| *existing == group) {
296 continue; }
298 if let Ok(answer) = query(&*reg.provider) {
299 if answer.validate_bound().is_err() {
305 continue;
306 }
307 per_group.push((group, answer));
308 }
309 }
310
311 for (_, candidate) in &per_group {
313 let agreeing = per_group
314 .iter()
315 .filter(|(_, a)| a.quorum_eq(candidate))
316 .count();
317 if agreeing >= threshold {
318 return Ok(candidate.clone());
319 }
320 }
321 Err(ChainSourceError::NoProvider)
322}
323
324impl ChainSource for TrustedView<'_> {
328 type Error = ChainSourceError;
329
330 fn coin_record(&self, coin_id: Bytes32) -> Result<Option<CoinRecord>, Self::Error> {
331 self.custody_read(move |p| p.coin_record(coin_id))
332 }
333
334 fn coin_records_by_puzzle_hash(
335 &self,
336 puzzle_hash: Bytes32,
337 include_spent: bool,
338 ) -> Result<Vec<CoinRecord>, Self::Error> {
339 self.custody_read(move |p| p.coin_records_by_puzzle_hash(puzzle_hash, include_spent))
340 }
341
342 fn coin_records_by_parent(
343 &self,
344 parent_coin_id: Bytes32,
345 ) -> Result<Vec<CoinRecord>, Self::Error> {
346 self.custody_read(move |p| p.coin_records_by_parent(parent_coin_id))
347 }
348
349 fn coin_spend(&self, coin_id: Bytes32) -> Result<Option<CoinSpend>, Self::Error> {
350 self.custody_read(move |p| p.coin_spend(coin_id))
351 }
352
353 fn resolve_singleton_lineage(
354 &self,
355 launcher_id: Bytes32,
356 ) -> Result<Option<SingletonLineage>, Self::Error> {
357 self.custody_read(move |p| p.resolve_singleton_lineage(launcher_id))
361 }
362
363 fn peak_height(&self) -> Result<Option<u32>, Self::Error> {
364 self.custody_read(|p| p.peak_height())
365 }
366
367 fn block_timestamp(&self, height: u32) -> Result<Option<u64>, Self::Error> {
368 self.custody_read(move |p| p.block_timestamp(height))
369 }
370}
371
372impl ChainSource for DiscoveryView<'_> {
373 type Error = ChainSourceError;
374
375 fn coin_record(&self, coin_id: Bytes32) -> Result<Option<CoinRecord>, Self::Error> {
376 self.discovery_read(move |p| p.coin_record(coin_id))
377 }
378
379 fn coin_records_by_puzzle_hash(
380 &self,
381 puzzle_hash: Bytes32,
382 include_spent: bool,
383 ) -> Result<Vec<CoinRecord>, Self::Error> {
384 self.discovery_read(move |p| p.coin_records_by_puzzle_hash(puzzle_hash, include_spent))
385 }
386
387 fn coin_records_by_parent(
388 &self,
389 parent_coin_id: Bytes32,
390 ) -> Result<Vec<CoinRecord>, Self::Error> {
391 self.discovery_read(move |p| p.coin_records_by_parent(parent_coin_id))
392 }
393
394 fn coin_spend(&self, coin_id: Bytes32) -> Result<Option<CoinSpend>, Self::Error> {
395 self.discovery_read(move |p| p.coin_spend(coin_id))
396 }
397
398 fn resolve_singleton_lineage(
399 &self,
400 launcher_id: Bytes32,
401 ) -> Result<Option<SingletonLineage>, Self::Error> {
402 self.discovery_read(move |p| p.resolve_singleton_lineage(launcher_id))
403 }
404
405 fn peak_height(&self) -> Result<Option<u32>, Self::Error> {
406 self.discovery_read(|p| p.peak_height())
407 }
408
409 fn block_timestamp(&self, height: u32) -> Result<Option<u64>, Self::Error> {
410 self.discovery_read(move |p| p.block_timestamp(height))
411 }
412}
413
414#[cfg(test)]
415mod tests {
416 use super::*;
417 use chia_protocol::Coin;
418 use dig_chainsource_interface::MockChainSource;
419
420 use crate::provider_registry::providers::{CoinsetProvider, CustomProvider, LocalNodeProvider};
421
422 fn coin_id(byte: u8) -> Bytes32 {
423 Coin::new(Bytes32::new([byte; 32]), Bytes32::new([byte; 32]), 1).coin_id()
424 }
425
426 fn record_for(id: Bytes32) -> CoinRecord {
427 CoinRecord {
428 coin: Coin::new(id, Bytes32::new([0x22; 32]), 1),
429 confirmed_height: Some(100),
430 spent_height: None,
431 timestamp: Some(1_700_000_000),
432 coinbase: false,
433 }
434 }
435
436 fn mock_with(id: Bytes32) -> MockChainSource {
437 MockChainSource::new().with_coin(id, record_for(id))
438 }
439
440 #[test]
443 fn pure_public_quorum_without_optin_fails_closed_for_custody() {
444 let id = coin_id(0x01);
445 let registry = ProviderRegistry::new()
448 .register(
449 Box::new(CoinsetProvider::new("coinset-a", 10, mock_with(id))),
450 None,
451 "coinset.org",
452 )
453 .register(
454 Box::new(CustomProvider::new("mirror-b", 20, mock_with(id))),
455 None,
456 "mirror.example",
457 );
458
459 let result = registry.trusted().coin_record(id);
460 assert_eq!(
461 result,
462 Err(ChainSourceError::NoProvider),
463 "pure-public custody must fail closed without allow_public_quorum_custody"
464 );
465 }
466
467 #[test]
470 fn operator_trusted_local_node_satisfies_custody() {
471 let id = coin_id(0x02);
472 let registry = ProviderRegistry::new().register(
473 Box::new(LocalNodeProvider::new("local", 0, mock_with(id))),
474 None, "local-node",
476 );
477
478 let record = registry.trusted().coin_record(id).unwrap();
479 assert_eq!(record, Some(record_for(id)));
480 }
481
482 #[test]
483 fn local_node_defaults_to_trusted() {
484 assert_eq!(
485 TrustLevel::default_for(ProviderKind::LocalNode),
486 TrustLevel::Trusted
487 );
488 assert_eq!(
489 TrustLevel::default_for(ProviderKind::PublicOracle),
490 TrustLevel::Untrusted
491 );
492 }
493
494 #[test]
497 fn quorum_including_trusted_member_satisfies_custody() {
498 let id = coin_id(0x03);
499 let registry = ProviderRegistry::new()
500 .register(
501 Box::new(LocalNodeProvider::new("local", 0, mock_with(id))),
502 None, "local-node",
504 )
505 .register(
506 Box::new(CoinsetProvider::new("coinset", 10, mock_with(id))),
507 None, "coinset.org",
509 );
510
511 let record = registry.trusted().coin_record(id).unwrap();
513 assert_eq!(record, Some(record_for(id)));
514 }
515
516 #[test]
517 fn trusted_source_error_fails_closed_not_public_fallback() {
518 let id = coin_id(0x04);
519 let registry = ProviderRegistry::new()
520 .register(
521 Box::new(LocalNodeProvider::new(
522 "local",
523 0,
524 MockChainSource::new().fail_with(ChainSourceError::Timeout),
525 )),
526 None,
527 "local-node",
528 )
529 .register(
530 Box::new(CoinsetProvider::new("coinset", 10, mock_with(id))),
531 None,
532 "coinset.org",
533 );
534
535 assert_eq!(
537 registry.trusted().coin_record(id),
538 Err(ChainSourceError::Timeout)
539 );
540 }
541
542 #[test]
545 fn optin_two_independent_groups_agree_satisfies_custody() {
546 let id = coin_id(0x05);
547 let registry = ProviderRegistry::new()
548 .allow_public_quorum_custody(true)
549 .register(
550 Box::new(CoinsetProvider::new("coinset", 10, mock_with(id))),
551 None,
552 "coinset.org",
553 )
554 .register(
555 Box::new(CustomProvider::new("mirror", 20, mock_with(id))),
556 None,
557 "mirror.example",
558 );
559
560 assert_eq!(
561 registry.trusted().coin_record(id).unwrap(),
562 Some(record_for(id))
563 );
564 }
565
566 #[test]
567 fn optin_single_group_fails_closed() {
568 let id = coin_id(0x06);
569 let registry = ProviderRegistry::new()
571 .allow_public_quorum_custody(true)
572 .register(
573 Box::new(CoinsetProvider::new("coinset", 10, mock_with(id))),
574 None,
575 "coinset.org",
576 );
577
578 assert_eq!(
579 registry.trusted().coin_record(id),
580 Err(ChainSourceError::NoProvider)
581 );
582 }
583
584 #[test]
585 fn optin_two_providers_same_group_fails_closed() {
586 let id = coin_id(0x07);
587 let registry = ProviderRegistry::new()
589 .allow_public_quorum_custody(true)
590 .register(
591 Box::new(CoinsetProvider::new("coinset-a", 10, mock_with(id))),
592 None,
593 "coinset.org",
594 )
595 .register(
596 Box::new(CoinsetProvider::new("coinset-b", 20, mock_with(id))),
597 None,
598 "coinset.org", );
600
601 assert_eq!(
602 registry.trusted().coin_record(id),
603 Err(ChainSourceError::NoProvider)
604 );
605 }
606
607 #[test]
608 fn optin_two_groups_disagree_fails_closed() {
609 let id = coin_id(0x08);
610 let registry = ProviderRegistry::new()
612 .allow_public_quorum_custody(true)
613 .register(
614 Box::new(CoinsetProvider::new("coinset", 10, mock_with(id))),
615 None,
616 "coinset.org",
617 )
618 .register(
619 Box::new(CustomProvider::new("empty", 20, MockChainSource::new())),
620 None,
621 "mirror.example",
622 );
623
624 assert_eq!(
626 registry.trusted().coin_record(id),
627 Err(ChainSourceError::NoProvider)
628 );
629 }
630
631 struct FixedListSource {
636 records: Vec<CoinRecord>,
637 }
638
639 impl ChainSource for FixedListSource {
640 type Error = ChainSourceError;
641
642 fn coin_record(&self, _coin_id: Bytes32) -> Result<Option<CoinRecord>, Self::Error> {
643 Ok(None)
644 }
645
646 fn coin_records_by_puzzle_hash(
647 &self,
648 _puzzle_hash: Bytes32,
649 _include_spent: bool,
650 ) -> Result<Vec<CoinRecord>, Self::Error> {
651 Ok(self.records.clone())
652 }
653
654 fn coin_records_by_parent(
655 &self,
656 _parent_coin_id: Bytes32,
657 ) -> Result<Vec<CoinRecord>, Self::Error> {
658 Ok(self.records.clone())
659 }
660
661 fn coin_spend(&self, _coin_id: Bytes32) -> Result<Option<CoinSpend>, Self::Error> {
662 Ok(None)
663 }
664
665 fn resolve_singleton_lineage(
666 &self,
667 _launcher_id: Bytes32,
668 ) -> Result<Option<SingletonLineage>, Self::Error> {
669 Ok(None)
670 }
671
672 fn peak_height(&self) -> Result<Option<u32>, Self::Error> {
673 Ok(None)
674 }
675
676 fn block_timestamp(&self, _height: u32) -> Result<Option<u64>, Self::Error> {
677 Ok(None)
678 }
679 }
680
681 fn list_source(records: Vec<CoinRecord>) -> FixedListSource {
682 FixedListSource { records }
683 }
684
685 #[test]
686 fn quorum_agrees_on_same_record_set_in_different_order() {
687 let ph = Bytes32::new([0x22; 32]);
688 let a = record_for(coin_id(0x0A));
689 let b = record_for(coin_id(0x0B));
690
691 let registry = ProviderRegistry::new()
694 .allow_public_quorum_custody(true)
695 .register(
696 Box::new(CoinsetProvider::new(
697 "coinset",
698 10,
699 list_source(vec![a.clone(), b.clone()]),
700 )),
701 None,
702 "coinset.org",
703 )
704 .register(
705 Box::new(CustomProvider::new(
706 "mirror",
707 20,
708 list_source(vec![b.clone(), a.clone()]),
709 )),
710 None,
711 "mirror.example",
712 );
713
714 let records = registry
715 .trusted()
716 .coin_records_by_puzzle_hash(ph, false)
717 .expect("honest sources agreeing on a set must satisfy the quorum");
718 assert_eq!(records.len(), 2);
719 assert!(records.contains(&a) && records.contains(&b));
720 }
721
722 #[test]
723 fn quorum_still_fails_closed_on_genuinely_different_record_sets() {
724 let ph = Bytes32::new([0x22; 32]);
725 let a = record_for(coin_id(0x0A));
726 let b = record_for(coin_id(0x0B));
727 let c = record_for(coin_id(0x0C));
728
729 let registry = ProviderRegistry::new()
732 .allow_public_quorum_custody(true)
733 .register(
734 Box::new(CoinsetProvider::new(
735 "coinset",
736 10,
737 list_source(vec![a.clone(), b]),
738 )),
739 None,
740 "coinset.org",
741 )
742 .register(
743 Box::new(CustomProvider::new("mirror", 20, list_source(vec![c, a]))),
744 None,
745 "mirror.example",
746 );
747
748 assert_eq!(
749 registry.trusted().coin_records_by_puzzle_hash(ph, false),
750 Err(ChainSourceError::NoProvider),
751 "genuinely different record sets must fail closed"
752 );
753 }
754
755 fn oversized_flood(ph: Bytes32) -> Vec<CoinRecord> {
757 let flood: Vec<CoinRecord> = (0..=MAX_QUORUM_RECORDS as u32)
758 .map(|i| {
759 let coin = Coin::new(Bytes32::new([0x01; 32]), ph, u64::from(i) + 1);
760 let mut r = record_for(coin.coin_id());
761 r.coin = coin;
762 r
763 })
764 .collect();
765 assert!(flood.len() > MAX_QUORUM_RECORDS);
766 flood
767 }
768
769 #[test]
773 fn quorum_fails_closed_when_all_sources_exceed_the_record_cap() {
774 let ph = Bytes32::new([0x22; 32]);
775 let flood = oversized_flood(ph);
776
777 let registry = ProviderRegistry::new()
778 .allow_public_quorum_custody(true)
779 .register(
780 Box::new(CoinsetProvider::new(
781 "coinset",
782 10,
783 list_source(flood.clone()),
784 )),
785 None,
786 "coinset.org",
787 )
788 .register(
789 Box::new(CustomProvider::new("mirror", 20, list_source(flood))),
790 None,
791 "mirror.example",
792 );
793
794 assert_eq!(
796 registry.trusted().coin_records_by_puzzle_hash(ph, false),
797 Err(ChainSourceError::NoProvider),
798 "when every source floods, custody must fail closed"
799 );
800 }
801
802 #[test]
807 fn oversized_quorum_member_is_skipped_not_fatal() {
808 let ph = Bytes32::new([0x22; 32]);
809 let a = record_for(coin_id(0x0A));
810 let b = record_for(coin_id(0x0B));
811 let flood = oversized_flood(ph);
812
813 let registry = ProviderRegistry::new()
814 .allow_public_quorum_custody(true)
815 .register(
816 Box::new(CoinsetProvider::new(
817 "coinset",
818 10,
819 list_source(vec![a.clone(), b.clone()]),
820 )),
821 None,
822 "coinset.org",
823 )
824 .register(
825 Box::new(CustomProvider::new(
826 "mirror",
827 20,
828 list_source(vec![b.clone(), a.clone()]),
829 )),
830 None,
831 "mirror.example",
832 )
833 .register(
834 Box::new(CustomProvider::new("flooder", 30, list_source(flood))),
835 None,
836 "flooder.example",
837 );
838
839 let records = registry
840 .trusted()
841 .coin_records_by_puzzle_hash(ph, false)
842 .expect("an honest 2-group agreement must still satisfy the quorum");
843 assert_eq!(records.len(), 2, "the flood must not enter the tally");
844 assert!(records.contains(&a) && records.contains(&b));
845 }
846
847 #[test]
850 fn discovery_view_returns_single_provider_answer() {
851 let id = coin_id(0x09);
852 let registry = ProviderRegistry::new().register(
853 Box::new(CoinsetProvider::new("coinset", 10, mock_with(id))),
854 None,
855 "coinset.org",
856 );
857
858 assert_eq!(
860 registry.any().coin_record(id).unwrap(),
861 Some(record_for(id))
862 );
863 assert_eq!(
865 registry.trusted().coin_record(id),
866 Err(ChainSourceError::NoProvider)
867 );
868 }
869
870 #[test]
871 fn every_read_method_flows_through_both_views() {
872 let ph = Bytes32::new([0x22; 32]);
873 let parent = Coin::new(Bytes32::new([0x01; 32]), ph, 1);
874 let parent_id = parent.coin_id();
875 let child = Coin::new(parent_id, ph, 1);
876 let launcher = Bytes32::new([0x77; 32]);
877
878 let source = MockChainSource::new()
879 .with_coin(parent_id, record_for(parent_id))
880 .with_coin(child.coin_id(), {
881 let mut r = record_for(child.coin_id());
882 r.coin = child;
883 r
884 })
885 .with_spend(
886 parent_id,
887 chia_protocol::CoinSpend::new(
888 parent,
889 chia_protocol::Program::from(vec![1]),
890 chia_protocol::Program::from(vec![0x80]),
891 ),
892 )
893 .with_lineage(launcher, SingletonLineage::single(launcher))
894 .with_timestamp(100, 1_700_000_000)
895 .with_peak(555);
896
897 let registry = ProviderRegistry::new().register(
898 Box::new(LocalNodeProvider::new("local", 0, source)),
899 None, "local-node",
901 );
902
903 let custody = registry.trusted();
905 assert!(!custody
906 .coin_records_by_puzzle_hash(ph, true)
907 .unwrap()
908 .is_empty());
909 assert!(!custody
910 .coin_records_by_parent(parent_id)
911 .unwrap()
912 .is_empty());
913 assert!(custody.coin_spend(parent_id).unwrap().is_some());
914 assert_eq!(custody.peak_height().unwrap(), Some(555));
915 assert_eq!(custody.block_timestamp(100).unwrap(), Some(1_700_000_000));
916 assert_eq!(
917 custody.resolve_singleton_lineage(launcher).unwrap(),
918 Some(SingletonLineage::single(launcher))
919 );
920
921 let discovery = registry.any();
923 assert!(discovery.coin_record(parent_id).unwrap().is_some());
924 assert!(!discovery
925 .coin_records_by_puzzle_hash(ph, false)
926 .unwrap()
927 .is_empty());
928 assert!(!discovery
929 .coin_records_by_parent(parent_id)
930 .unwrap()
931 .is_empty());
932 assert!(discovery.coin_spend(parent_id).unwrap().is_some());
933 assert_eq!(discovery.peak_height().unwrap(), Some(555));
934 assert_eq!(discovery.block_timestamp(100).unwrap(), Some(1_700_000_000));
935 assert!(discovery
936 .resolve_singleton_lineage(launcher)
937 .unwrap()
938 .is_some());
939 }
940
941 #[test]
942 fn discovery_falls_through_failing_providers_to_a_responder() {
943 let id = coin_id(0x0B);
944 let registry = ProviderRegistry::new()
945 .register(
946 Box::new(CoinsetProvider::new(
947 "down",
948 0,
949 MockChainSource::new().fail_with(ChainSourceError::Timeout),
950 )),
951 None,
952 "down",
953 )
954 .register(
955 Box::new(CoinsetProvider::new("up", 10, mock_with(id))),
956 None,
957 "up",
958 );
959 assert_eq!(
960 registry.any().coin_record(id).unwrap(),
961 Some(record_for(id))
962 );
963 }
964
965 #[test]
966 fn empty_registry_fails_closed_everywhere() {
967 let registry = ProviderRegistry::new();
968 let id = coin_id(0x0A);
969 assert_eq!(
970 registry.trusted().coin_record(id),
971 Err(ChainSourceError::NoProvider)
972 );
973 assert_eq!(
974 registry.any().coin_record(id),
975 Err(ChainSourceError::NoProvider)
976 );
977 }
978}