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>
197 where
198 T: QuorumComparable,
199 Q: Fn(&DynProvider) -> Result<T, ChainSourceError>,
200 {
201 let mut last_error = ChainSourceError::NoProvider;
202 for reg in self.registry.by_priority() {
203 match query(&*reg.provider) {
204 Ok(value) => match value.validate_bound() {
205 Ok(()) => return Ok(value),
206 Err(error) => last_error = error,
207 },
208 Err(error) => last_error = error,
209 }
210 }
211 Err(last_error)
212 }
213}
214
215trait QuorumComparable {
225 fn quorum_eq(&self, other: &Self) -> bool;
227
228 fn validate_bound(&self) -> Result<(), ChainSourceError> {
232 Ok(())
233 }
234}
235
236macro_rules! quorum_eq_via_partial_eq {
238 ($($t:ty),+ $(,)?) => {
239 $(impl QuorumComparable for $t {
240 fn quorum_eq(&self, other: &Self) -> bool {
241 self == other
242 }
243 })+
244 };
245}
246
247quorum_eq_via_partial_eq!(
248 Option<CoinRecord>,
249 Option<CoinSpend>,
250 Option<SingletonLineage>,
251 Option<u32>,
252 Option<u64>,
253);
254
255impl QuorumComparable for Vec<CoinRecord> {
256 fn quorum_eq(&self, other: &Self) -> bool {
257 canonical_order(self) == canonical_order(other)
258 }
259
260 fn validate_bound(&self) -> Result<(), ChainSourceError> {
261 if self.len() > MAX_QUORUM_RECORDS {
262 return Err(ChainSourceError::TooManyRecords {
263 count: self.len(),
264 limit: MAX_QUORUM_RECORDS,
265 });
266 }
267 Ok(())
268 }
269}
270
271fn canonical_order(records: &[CoinRecord]) -> Vec<(Bytes32, &CoinRecord)> {
280 let mut keyed: Vec<(Bytes32, &CoinRecord)> =
281 records.iter().map(|r| (r.coin.coin_id(), r)).collect();
282 keyed.sort_by(|a, b| a.0.as_ref().cmp(b.0.as_ref()));
283 keyed
284}
285
286fn quorum_read<T, Q>(
294 providers: &[Registration],
295 threshold: usize,
296 query: Q,
297) -> Result<T, ChainSourceError>
298where
299 T: QuorumComparable + Clone,
300 Q: Fn(&DynProvider) -> Result<T, ChainSourceError>,
301{
302 let mut per_group: Vec<(&str, T)> = Vec::new();
304 for reg in providers {
305 let group = reg.independence_group.as_str();
306 if per_group.iter().any(|(existing, _)| *existing == group) {
307 continue; }
309 if let Ok(answer) = query(&*reg.provider) {
310 if answer.validate_bound().is_err() {
316 continue;
317 }
318 per_group.push((group, answer));
319 }
320 }
321
322 for (_, candidate) in &per_group {
324 let agreeing = per_group
325 .iter()
326 .filter(|(_, a)| a.quorum_eq(candidate))
327 .count();
328 if agreeing >= threshold {
329 return Ok(candidate.clone());
330 }
331 }
332 Err(ChainSourceError::NoProvider)
333}
334
335impl ChainSource for TrustedView<'_> {
339 type Error = ChainSourceError;
340
341 fn coin_record(&self, coin_id: Bytes32) -> Result<Option<CoinRecord>, Self::Error> {
342 self.custody_read(move |p| p.coin_record(coin_id))
343 }
344
345 fn coin_records_by_puzzle_hash(
346 &self,
347 puzzle_hash: Bytes32,
348 include_spent: bool,
349 ) -> Result<Vec<CoinRecord>, Self::Error> {
350 self.custody_read(move |p| p.coin_records_by_puzzle_hash(puzzle_hash, include_spent))
351 }
352
353 fn coin_records_by_parent(
354 &self,
355 parent_coin_id: Bytes32,
356 ) -> Result<Vec<CoinRecord>, Self::Error> {
357 self.custody_read(move |p| p.coin_records_by_parent(parent_coin_id))
358 }
359
360 fn coin_spend(&self, coin_id: Bytes32) -> Result<Option<CoinSpend>, Self::Error> {
361 self.custody_read(move |p| p.coin_spend(coin_id))
362 }
363
364 fn resolve_singleton_lineage(
365 &self,
366 launcher_id: Bytes32,
367 ) -> Result<Option<SingletonLineage>, Self::Error> {
368 self.custody_read(move |p| p.resolve_singleton_lineage(launcher_id))
372 }
373
374 fn peak_height(&self) -> Result<Option<u32>, Self::Error> {
375 self.custody_read(|p| p.peak_height())
376 }
377
378 fn block_timestamp(&self, height: u32) -> Result<Option<u64>, Self::Error> {
379 self.custody_read(move |p| p.block_timestamp(height))
380 }
381}
382
383impl ChainSource for DiscoveryView<'_> {
384 type Error = ChainSourceError;
385
386 fn coin_record(&self, coin_id: Bytes32) -> Result<Option<CoinRecord>, Self::Error> {
387 self.discovery_read(move |p| p.coin_record(coin_id))
388 }
389
390 fn coin_records_by_puzzle_hash(
391 &self,
392 puzzle_hash: Bytes32,
393 include_spent: bool,
394 ) -> Result<Vec<CoinRecord>, Self::Error> {
395 self.discovery_read(move |p| p.coin_records_by_puzzle_hash(puzzle_hash, include_spent))
396 }
397
398 fn coin_records_by_parent(
399 &self,
400 parent_coin_id: Bytes32,
401 ) -> Result<Vec<CoinRecord>, Self::Error> {
402 self.discovery_read(move |p| p.coin_records_by_parent(parent_coin_id))
403 }
404
405 fn coin_spend(&self, coin_id: Bytes32) -> Result<Option<CoinSpend>, Self::Error> {
406 self.discovery_read(move |p| p.coin_spend(coin_id))
407 }
408
409 fn resolve_singleton_lineage(
410 &self,
411 launcher_id: Bytes32,
412 ) -> Result<Option<SingletonLineage>, Self::Error> {
413 self.discovery_read(move |p| p.resolve_singleton_lineage(launcher_id))
414 }
415
416 fn peak_height(&self) -> Result<Option<u32>, Self::Error> {
417 self.discovery_read(|p| p.peak_height())
418 }
419
420 fn block_timestamp(&self, height: u32) -> Result<Option<u64>, Self::Error> {
421 self.discovery_read(move |p| p.block_timestamp(height))
422 }
423}
424
425#[cfg(test)]
426mod tests {
427 use super::*;
428 use chia_protocol::Coin;
429 use dig_chainsource_interface::MockChainSource;
430
431 use crate::provider_registry::providers::{CoinsetProvider, CustomProvider, LocalNodeProvider};
432
433 fn coin_id(byte: u8) -> Bytes32 {
434 Coin::new(Bytes32::new([byte; 32]), Bytes32::new([byte; 32]), 1).coin_id()
435 }
436
437 fn record_for(id: Bytes32) -> CoinRecord {
438 CoinRecord {
439 coin: Coin::new(id, Bytes32::new([0x22; 32]), 1),
440 confirmed_height: Some(100),
441 spent_height: None,
442 timestamp: Some(1_700_000_000),
443 coinbase: false,
444 }
445 }
446
447 fn mock_with(id: Bytes32) -> MockChainSource {
448 MockChainSource::new().with_coin(id, record_for(id))
449 }
450
451 #[test]
454 fn pure_public_quorum_without_optin_fails_closed_for_custody() {
455 let id = coin_id(0x01);
456 let registry = ProviderRegistry::new()
459 .register(
460 Box::new(CoinsetProvider::new("coinset-a", 10, mock_with(id))),
461 None,
462 "coinset.org",
463 )
464 .register(
465 Box::new(CustomProvider::new("mirror-b", 20, mock_with(id))),
466 None,
467 "mirror.example",
468 );
469
470 let result = registry.trusted().coin_record(id);
471 assert_eq!(
472 result,
473 Err(ChainSourceError::NoProvider),
474 "pure-public custody must fail closed without allow_public_quorum_custody"
475 );
476 }
477
478 #[test]
481 fn operator_trusted_local_node_satisfies_custody() {
482 let id = coin_id(0x02);
483 let registry = ProviderRegistry::new().register(
484 Box::new(LocalNodeProvider::new("local", 0, mock_with(id))),
485 None, "local-node",
487 );
488
489 let record = registry.trusted().coin_record(id).unwrap();
490 assert_eq!(record, Some(record_for(id)));
491 }
492
493 #[test]
494 fn local_node_defaults_to_trusted() {
495 assert_eq!(
496 TrustLevel::default_for(ProviderKind::LocalNode),
497 TrustLevel::Trusted
498 );
499 assert_eq!(
500 TrustLevel::default_for(ProviderKind::PublicOracle),
501 TrustLevel::Untrusted
502 );
503 }
504
505 #[test]
508 fn quorum_including_trusted_member_satisfies_custody() {
509 let id = coin_id(0x03);
510 let registry = ProviderRegistry::new()
511 .register(
512 Box::new(LocalNodeProvider::new("local", 0, mock_with(id))),
513 None, "local-node",
515 )
516 .register(
517 Box::new(CoinsetProvider::new("coinset", 10, mock_with(id))),
518 None, "coinset.org",
520 );
521
522 let record = registry.trusted().coin_record(id).unwrap();
524 assert_eq!(record, Some(record_for(id)));
525 }
526
527 #[test]
528 fn trusted_source_error_fails_closed_not_public_fallback() {
529 let id = coin_id(0x04);
530 let registry = ProviderRegistry::new()
531 .register(
532 Box::new(LocalNodeProvider::new(
533 "local",
534 0,
535 MockChainSource::new().fail_with(ChainSourceError::Timeout),
536 )),
537 None,
538 "local-node",
539 )
540 .register(
541 Box::new(CoinsetProvider::new("coinset", 10, mock_with(id))),
542 None,
543 "coinset.org",
544 );
545
546 assert_eq!(
548 registry.trusted().coin_record(id),
549 Err(ChainSourceError::Timeout)
550 );
551 }
552
553 #[test]
556 fn optin_two_independent_groups_agree_satisfies_custody() {
557 let id = coin_id(0x05);
558 let registry = ProviderRegistry::new()
559 .allow_public_quorum_custody(true)
560 .register(
561 Box::new(CoinsetProvider::new("coinset", 10, mock_with(id))),
562 None,
563 "coinset.org",
564 )
565 .register(
566 Box::new(CustomProvider::new("mirror", 20, mock_with(id))),
567 None,
568 "mirror.example",
569 );
570
571 assert_eq!(
572 registry.trusted().coin_record(id).unwrap(),
573 Some(record_for(id))
574 );
575 }
576
577 #[test]
578 fn optin_single_group_fails_closed() {
579 let id = coin_id(0x06);
580 let registry = ProviderRegistry::new()
582 .allow_public_quorum_custody(true)
583 .register(
584 Box::new(CoinsetProvider::new("coinset", 10, mock_with(id))),
585 None,
586 "coinset.org",
587 );
588
589 assert_eq!(
590 registry.trusted().coin_record(id),
591 Err(ChainSourceError::NoProvider)
592 );
593 }
594
595 #[test]
596 fn optin_two_providers_same_group_fails_closed() {
597 let id = coin_id(0x07);
598 let registry = ProviderRegistry::new()
600 .allow_public_quorum_custody(true)
601 .register(
602 Box::new(CoinsetProvider::new("coinset-a", 10, mock_with(id))),
603 None,
604 "coinset.org",
605 )
606 .register(
607 Box::new(CoinsetProvider::new("coinset-b", 20, mock_with(id))),
608 None,
609 "coinset.org", );
611
612 assert_eq!(
613 registry.trusted().coin_record(id),
614 Err(ChainSourceError::NoProvider)
615 );
616 }
617
618 #[test]
619 fn optin_two_groups_disagree_fails_closed() {
620 let id = coin_id(0x08);
621 let registry = ProviderRegistry::new()
623 .allow_public_quorum_custody(true)
624 .register(
625 Box::new(CoinsetProvider::new("coinset", 10, mock_with(id))),
626 None,
627 "coinset.org",
628 )
629 .register(
630 Box::new(CustomProvider::new("empty", 20, MockChainSource::new())),
631 None,
632 "mirror.example",
633 );
634
635 assert_eq!(
637 registry.trusted().coin_record(id),
638 Err(ChainSourceError::NoProvider)
639 );
640 }
641
642 struct FixedListSource {
647 records: Vec<CoinRecord>,
648 }
649
650 impl ChainSource for FixedListSource {
651 type Error = ChainSourceError;
652
653 fn coin_record(&self, _coin_id: Bytes32) -> Result<Option<CoinRecord>, Self::Error> {
654 Ok(None)
655 }
656
657 fn coin_records_by_puzzle_hash(
658 &self,
659 _puzzle_hash: Bytes32,
660 _include_spent: bool,
661 ) -> Result<Vec<CoinRecord>, Self::Error> {
662 Ok(self.records.clone())
663 }
664
665 fn coin_records_by_parent(
666 &self,
667 _parent_coin_id: Bytes32,
668 ) -> Result<Vec<CoinRecord>, Self::Error> {
669 Ok(self.records.clone())
670 }
671
672 fn coin_spend(&self, _coin_id: Bytes32) -> Result<Option<CoinSpend>, Self::Error> {
673 Ok(None)
674 }
675
676 fn resolve_singleton_lineage(
677 &self,
678 _launcher_id: Bytes32,
679 ) -> Result<Option<SingletonLineage>, Self::Error> {
680 Ok(None)
681 }
682
683 fn peak_height(&self) -> Result<Option<u32>, Self::Error> {
684 Ok(None)
685 }
686
687 fn block_timestamp(&self, _height: u32) -> Result<Option<u64>, Self::Error> {
688 Ok(None)
689 }
690 }
691
692 fn list_source(records: Vec<CoinRecord>) -> FixedListSource {
693 FixedListSource { records }
694 }
695
696 #[test]
697 fn quorum_agrees_on_same_record_set_in_different_order() {
698 let ph = Bytes32::new([0x22; 32]);
699 let a = record_for(coin_id(0x0A));
700 let b = record_for(coin_id(0x0B));
701
702 let registry = ProviderRegistry::new()
705 .allow_public_quorum_custody(true)
706 .register(
707 Box::new(CoinsetProvider::new(
708 "coinset",
709 10,
710 list_source(vec![a.clone(), b.clone()]),
711 )),
712 None,
713 "coinset.org",
714 )
715 .register(
716 Box::new(CustomProvider::new(
717 "mirror",
718 20,
719 list_source(vec![b.clone(), a.clone()]),
720 )),
721 None,
722 "mirror.example",
723 );
724
725 let records = registry
726 .trusted()
727 .coin_records_by_puzzle_hash(ph, false)
728 .expect("honest sources agreeing on a set must satisfy the quorum");
729 assert_eq!(records.len(), 2);
730 assert!(records.contains(&a) && records.contains(&b));
731 }
732
733 #[test]
734 fn quorum_still_fails_closed_on_genuinely_different_record_sets() {
735 let ph = Bytes32::new([0x22; 32]);
736 let a = record_for(coin_id(0x0A));
737 let b = record_for(coin_id(0x0B));
738 let c = record_for(coin_id(0x0C));
739
740 let registry = ProviderRegistry::new()
743 .allow_public_quorum_custody(true)
744 .register(
745 Box::new(CoinsetProvider::new(
746 "coinset",
747 10,
748 list_source(vec![a.clone(), b]),
749 )),
750 None,
751 "coinset.org",
752 )
753 .register(
754 Box::new(CustomProvider::new("mirror", 20, list_source(vec![c, a]))),
755 None,
756 "mirror.example",
757 );
758
759 assert_eq!(
760 registry.trusted().coin_records_by_puzzle_hash(ph, false),
761 Err(ChainSourceError::NoProvider),
762 "genuinely different record sets must fail closed"
763 );
764 }
765
766 fn oversized_flood(ph: Bytes32) -> Vec<CoinRecord> {
768 let flood: Vec<CoinRecord> = (0..=MAX_QUORUM_RECORDS as u32)
769 .map(|i| {
770 let coin = Coin::new(Bytes32::new([0x01; 32]), ph, u64::from(i) + 1);
771 let mut r = record_for(coin.coin_id());
772 r.coin = coin;
773 r
774 })
775 .collect();
776 assert!(flood.len() > MAX_QUORUM_RECORDS);
777 flood
778 }
779
780 #[test]
784 fn quorum_fails_closed_when_all_sources_exceed_the_record_cap() {
785 let ph = Bytes32::new([0x22; 32]);
786 let flood = oversized_flood(ph);
787
788 let registry = ProviderRegistry::new()
789 .allow_public_quorum_custody(true)
790 .register(
791 Box::new(CoinsetProvider::new(
792 "coinset",
793 10,
794 list_source(flood.clone()),
795 )),
796 None,
797 "coinset.org",
798 )
799 .register(
800 Box::new(CustomProvider::new("mirror", 20, list_source(flood))),
801 None,
802 "mirror.example",
803 );
804
805 assert_eq!(
807 registry.trusted().coin_records_by_puzzle_hash(ph, false),
808 Err(ChainSourceError::NoProvider),
809 "when every source floods, custody must fail closed"
810 );
811 }
812
813 #[test]
818 fn oversized_quorum_member_is_skipped_not_fatal() {
819 let ph = Bytes32::new([0x22; 32]);
820 let a = record_for(coin_id(0x0A));
821 let b = record_for(coin_id(0x0B));
822 let flood = oversized_flood(ph);
823
824 let registry = ProviderRegistry::new()
825 .allow_public_quorum_custody(true)
826 .register(
827 Box::new(CoinsetProvider::new(
828 "coinset",
829 10,
830 list_source(vec![a.clone(), b.clone()]),
831 )),
832 None,
833 "coinset.org",
834 )
835 .register(
836 Box::new(CustomProvider::new(
837 "mirror",
838 20,
839 list_source(vec![b.clone(), a.clone()]),
840 )),
841 None,
842 "mirror.example",
843 )
844 .register(
845 Box::new(CustomProvider::new("flooder", 30, list_source(flood))),
846 None,
847 "flooder.example",
848 );
849
850 let records = registry
851 .trusted()
852 .coin_records_by_puzzle_hash(ph, false)
853 .expect("an honest 2-group agreement must still satisfy the quorum");
854 assert_eq!(records.len(), 2, "the flood must not enter the tally");
855 assert!(records.contains(&a) && records.contains(&b));
856 }
857
858 #[test]
861 fn discovery_view_returns_single_provider_answer() {
862 let id = coin_id(0x09);
863 let registry = ProviderRegistry::new().register(
864 Box::new(CoinsetProvider::new("coinset", 10, mock_with(id))),
865 None,
866 "coinset.org",
867 );
868
869 assert_eq!(
871 registry.any().coin_record(id).unwrap(),
872 Some(record_for(id))
873 );
874 assert_eq!(
876 registry.trusted().coin_record(id),
877 Err(ChainSourceError::NoProvider)
878 );
879 }
880
881 #[test]
884 fn discovery_rejects_oversized_untrusted_response() {
885 let ph = Bytes32::new([0x22; 32]);
886 let flood = oversized_flood(ph);
887
888 let registry = ProviderRegistry::new().register(
889 Box::new(CoinsetProvider::new("coinset", 10, list_source(flood))),
890 None,
891 "coinset.org",
892 );
893
894 assert!(
895 matches!(
896 registry.any().coin_records_by_puzzle_hash(ph, false),
897 Err(ChainSourceError::TooManyRecords { count, limit })
898 if count == MAX_QUORUM_RECORDS + 1 && limit == MAX_QUORUM_RECORDS
899 ),
900 "an over-cap discovery answer must be rejected as TooManyRecords"
901 );
902 }
903
904 #[test]
907 fn discovery_passes_within_cap_response() {
908 let ph = Bytes32::new([0x22; 32]);
909 let a = record_for(coin_id(0x0A));
910 let b = record_for(coin_id(0x0B));
911
912 let registry = ProviderRegistry::new().register(
913 Box::new(CoinsetProvider::new(
914 "coinset",
915 10,
916 list_source(vec![a.clone(), b.clone()]),
917 )),
918 None,
919 "coinset.org",
920 );
921
922 let records = registry
923 .any()
924 .coin_records_by_puzzle_hash(ph, false)
925 .expect("a within-cap discovery answer must pass through");
926 assert_eq!(records.len(), 2);
927 assert!(records.contains(&a) && records.contains(&b));
928 }
929
930 #[test]
931 fn every_read_method_flows_through_both_views() {
932 let ph = Bytes32::new([0x22; 32]);
933 let parent = Coin::new(Bytes32::new([0x01; 32]), ph, 1);
934 let parent_id = parent.coin_id();
935 let child = Coin::new(parent_id, ph, 1);
936 let launcher = Bytes32::new([0x77; 32]);
937
938 let source = MockChainSource::new()
939 .with_coin(parent_id, record_for(parent_id))
940 .with_coin(child.coin_id(), {
941 let mut r = record_for(child.coin_id());
942 r.coin = child;
943 r
944 })
945 .with_spend(
946 parent_id,
947 chia_protocol::CoinSpend::new(
948 parent,
949 chia_protocol::Program::from(vec![1]),
950 chia_protocol::Program::from(vec![0x80]),
951 ),
952 )
953 .with_lineage(launcher, SingletonLineage::single(launcher))
954 .with_timestamp(100, 1_700_000_000)
955 .with_peak(555);
956
957 let registry = ProviderRegistry::new().register(
958 Box::new(LocalNodeProvider::new("local", 0, source)),
959 None, "local-node",
961 );
962
963 let custody = registry.trusted();
965 assert!(!custody
966 .coin_records_by_puzzle_hash(ph, true)
967 .unwrap()
968 .is_empty());
969 assert!(!custody
970 .coin_records_by_parent(parent_id)
971 .unwrap()
972 .is_empty());
973 assert!(custody.coin_spend(parent_id).unwrap().is_some());
974 assert_eq!(custody.peak_height().unwrap(), Some(555));
975 assert_eq!(custody.block_timestamp(100).unwrap(), Some(1_700_000_000));
976 assert_eq!(
977 custody.resolve_singleton_lineage(launcher).unwrap(),
978 Some(SingletonLineage::single(launcher))
979 );
980
981 let discovery = registry.any();
983 assert!(discovery.coin_record(parent_id).unwrap().is_some());
984 assert!(!discovery
985 .coin_records_by_puzzle_hash(ph, false)
986 .unwrap()
987 .is_empty());
988 assert!(!discovery
989 .coin_records_by_parent(parent_id)
990 .unwrap()
991 .is_empty());
992 assert!(discovery.coin_spend(parent_id).unwrap().is_some());
993 assert_eq!(discovery.peak_height().unwrap(), Some(555));
994 assert_eq!(discovery.block_timestamp(100).unwrap(), Some(1_700_000_000));
995 assert!(discovery
996 .resolve_singleton_lineage(launcher)
997 .unwrap()
998 .is_some());
999 }
1000
1001 #[test]
1002 fn discovery_falls_through_failing_providers_to_a_responder() {
1003 let id = coin_id(0x0B);
1004 let registry = ProviderRegistry::new()
1005 .register(
1006 Box::new(CoinsetProvider::new(
1007 "down",
1008 0,
1009 MockChainSource::new().fail_with(ChainSourceError::Timeout),
1010 )),
1011 None,
1012 "down",
1013 )
1014 .register(
1015 Box::new(CoinsetProvider::new("up", 10, mock_with(id))),
1016 None,
1017 "up",
1018 );
1019 assert_eq!(
1020 registry.any().coin_record(id).unwrap(),
1021 Some(record_for(id))
1022 );
1023 }
1024
1025 #[test]
1026 fn empty_registry_fails_closed_everywhere() {
1027 let registry = ProviderRegistry::new();
1028 let id = coin_id(0x0A);
1029 assert_eq!(
1030 registry.trusted().coin_record(id),
1031 Err(ChainSourceError::NoProvider)
1032 );
1033 assert_eq!(
1034 registry.any().coin_record(id),
1035 Err(ChainSourceError::NoProvider)
1036 );
1037 }
1038}