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
41type DynProvider = dyn ChainSourceProvider<Error = ChainSourceError>;
43
44struct Registration {
47 provider: Box<DynProvider>,
48 trust: TrustLevel,
49 independence_group: String,
50}
51
52#[derive(Default)]
55pub struct ProviderRegistry {
56 providers: Vec<Registration>,
57 allow_public_quorum_custody: bool,
58}
59
60impl ProviderRegistry {
61 pub fn new() -> Self {
64 Self::default()
65 }
66
67 pub fn allow_public_quorum_custody(mut self, allow: bool) -> Self {
74 self.allow_public_quorum_custody = allow;
75 if allow {
76 log::warn!(
77 "chia-query registry: pure-public-quorum custody ENABLED — custody reads may be \
78 satisfied by {PUBLIC_QUORUM_THRESHOLD} independent public sources with NO \
79 operator-trusted source. Reduced assurance vs a trusted local node."
80 );
81 }
82 self
83 }
84
85 pub fn register(
89 mut self,
90 provider: Box<DynProvider>,
91 trust_override: Option<TrustLevel>,
92 independence_group: impl Into<String>,
93 ) -> Self {
94 let trust = trust_override
95 .unwrap_or_else(|| TrustLevel::default_for(provider.provider_info().kind));
96 self.providers.push(Registration {
97 provider,
98 trust,
99 independence_group: independence_group.into(),
100 });
101 self
102 }
103
104 pub fn trusted(&self) -> TrustedView<'_> {
110 TrustedView { registry: self }
111 }
112
113 pub fn any(&self) -> DiscoveryView<'_> {
118 DiscoveryView { registry: self }
119 }
120
121 fn by_priority(&self) -> Vec<&Registration> {
123 let mut ordered: Vec<&Registration> = self.providers.iter().collect();
124 ordered.sort_by_key(|reg| reg.provider.provider_info().priority);
125 ordered
126 }
127}
128
129pub struct TrustedView<'a> {
131 registry: &'a ProviderRegistry,
132}
133
134impl TrustedView<'_> {
135 fn custody_read<T, Q>(&self, query: Q) -> Result<T, ChainSourceError>
138 where
139 T: QuorumComparable + Clone,
140 Q: Fn(&DynProvider) -> Result<T, ChainSourceError>,
141 {
142 let trusted: Vec<&Registration> = self
143 .registry
144 .by_priority()
145 .into_iter()
146 .filter(|reg| reg.trust == TrustLevel::Trusted)
147 .collect();
148
149 if !trusted.is_empty() {
151 let mut last_error = ChainSourceError::NoProvider;
152 for reg in trusted {
153 match query(&*reg.provider) {
154 Ok(value) => return Ok(value),
155 Err(error) => last_error = error,
156 }
157 }
158 return Err(last_error);
160 }
161
162 if !self.registry.allow_public_quorum_custody {
164 return Err(ChainSourceError::NoProvider);
165 }
166 quorum_read(&self.registry.providers, PUBLIC_QUORUM_THRESHOLD, query)
167 }
168}
169
170pub struct DiscoveryView<'a> {
172 registry: &'a ProviderRegistry,
173}
174
175impl DiscoveryView<'_> {
176 fn discovery_read<T, Q>(&self, query: Q) -> Result<T, ChainSourceError>
178 where
179 Q: Fn(&DynProvider) -> Result<T, ChainSourceError>,
180 {
181 let mut last_error = ChainSourceError::NoProvider;
182 for reg in self.registry.by_priority() {
183 match query(&*reg.provider) {
184 Ok(value) => return Ok(value),
185 Err(error) => last_error = error,
186 }
187 }
188 Err(last_error)
189 }
190}
191
192trait QuorumComparable {
202 fn quorum_eq(&self, other: &Self) -> bool;
204}
205
206macro_rules! quorum_eq_via_partial_eq {
208 ($($t:ty),+ $(,)?) => {
209 $(impl QuorumComparable for $t {
210 fn quorum_eq(&self, other: &Self) -> bool {
211 self == other
212 }
213 })+
214 };
215}
216
217quorum_eq_via_partial_eq!(
218 Option<CoinRecord>,
219 Option<CoinSpend>,
220 Option<SingletonLineage>,
221 Option<u32>,
222 Option<u64>,
223);
224
225impl QuorumComparable for Vec<CoinRecord> {
226 fn quorum_eq(&self, other: &Self) -> bool {
227 canonical_order(self) == canonical_order(other)
228 }
229}
230
231fn canonical_order(records: &[CoinRecord]) -> Vec<&CoinRecord> {
236 let mut ordered: Vec<&CoinRecord> = records.iter().collect();
237 ordered.sort_by(|a, b| a.coin.coin_id().as_ref().cmp(b.coin.coin_id().as_ref()));
238 ordered
239}
240
241fn quorum_read<T, Q>(
249 providers: &[Registration],
250 threshold: usize,
251 query: Q,
252) -> Result<T, ChainSourceError>
253where
254 T: QuorumComparable + Clone,
255 Q: Fn(&DynProvider) -> Result<T, ChainSourceError>,
256{
257 let mut per_group: Vec<(&str, T)> = Vec::new();
259 for reg in providers {
260 let group = reg.independence_group.as_str();
261 if per_group.iter().any(|(existing, _)| *existing == group) {
262 continue; }
264 if let Ok(answer) = query(&*reg.provider) {
265 per_group.push((group, answer));
266 }
267 }
268
269 for (_, candidate) in &per_group {
271 let agreeing = per_group
272 .iter()
273 .filter(|(_, a)| a.quorum_eq(candidate))
274 .count();
275 if agreeing >= threshold {
276 return Ok(candidate.clone());
277 }
278 }
279 Err(ChainSourceError::NoProvider)
280}
281
282impl ChainSource for TrustedView<'_> {
286 type Error = ChainSourceError;
287
288 fn coin_record(&self, coin_id: Bytes32) -> Result<Option<CoinRecord>, Self::Error> {
289 self.custody_read(move |p| p.coin_record(coin_id))
290 }
291
292 fn coin_records_by_puzzle_hash(
293 &self,
294 puzzle_hash: Bytes32,
295 include_spent: bool,
296 ) -> Result<Vec<CoinRecord>, Self::Error> {
297 self.custody_read(move |p| p.coin_records_by_puzzle_hash(puzzle_hash, include_spent))
298 }
299
300 fn coin_records_by_parent(
301 &self,
302 parent_coin_id: Bytes32,
303 ) -> Result<Vec<CoinRecord>, Self::Error> {
304 self.custody_read(move |p| p.coin_records_by_parent(parent_coin_id))
305 }
306
307 fn coin_spend(&self, coin_id: Bytes32) -> Result<Option<CoinSpend>, Self::Error> {
308 self.custody_read(move |p| p.coin_spend(coin_id))
309 }
310
311 fn resolve_singleton_lineage(
312 &self,
313 launcher_id: Bytes32,
314 ) -> Result<Option<SingletonLineage>, Self::Error> {
315 self.custody_read(move |p| p.resolve_singleton_lineage(launcher_id))
319 }
320
321 fn peak_height(&self) -> Result<Option<u32>, Self::Error> {
322 self.custody_read(|p| p.peak_height())
323 }
324
325 fn block_timestamp(&self, height: u32) -> Result<Option<u64>, Self::Error> {
326 self.custody_read(move |p| p.block_timestamp(height))
327 }
328}
329
330impl ChainSource for DiscoveryView<'_> {
331 type Error = ChainSourceError;
332
333 fn coin_record(&self, coin_id: Bytes32) -> Result<Option<CoinRecord>, Self::Error> {
334 self.discovery_read(move |p| p.coin_record(coin_id))
335 }
336
337 fn coin_records_by_puzzle_hash(
338 &self,
339 puzzle_hash: Bytes32,
340 include_spent: bool,
341 ) -> Result<Vec<CoinRecord>, Self::Error> {
342 self.discovery_read(move |p| p.coin_records_by_puzzle_hash(puzzle_hash, include_spent))
343 }
344
345 fn coin_records_by_parent(
346 &self,
347 parent_coin_id: Bytes32,
348 ) -> Result<Vec<CoinRecord>, Self::Error> {
349 self.discovery_read(move |p| p.coin_records_by_parent(parent_coin_id))
350 }
351
352 fn coin_spend(&self, coin_id: Bytes32) -> Result<Option<CoinSpend>, Self::Error> {
353 self.discovery_read(move |p| p.coin_spend(coin_id))
354 }
355
356 fn resolve_singleton_lineage(
357 &self,
358 launcher_id: Bytes32,
359 ) -> Result<Option<SingletonLineage>, Self::Error> {
360 self.discovery_read(move |p| p.resolve_singleton_lineage(launcher_id))
361 }
362
363 fn peak_height(&self) -> Result<Option<u32>, Self::Error> {
364 self.discovery_read(|p| p.peak_height())
365 }
366
367 fn block_timestamp(&self, height: u32) -> Result<Option<u64>, Self::Error> {
368 self.discovery_read(move |p| p.block_timestamp(height))
369 }
370}
371
372#[cfg(test)]
373mod tests {
374 use super::*;
375 use chia_protocol::Coin;
376 use dig_chainsource_interface::MockChainSource;
377
378 use crate::provider_registry::providers::{CoinsetProvider, CustomProvider, LocalNodeProvider};
379
380 fn coin_id(byte: u8) -> Bytes32 {
381 Coin::new(Bytes32::new([byte; 32]), Bytes32::new([byte; 32]), 1).coin_id()
382 }
383
384 fn record_for(id: Bytes32) -> CoinRecord {
385 CoinRecord {
386 coin: Coin::new(id, Bytes32::new([0x22; 32]), 1),
387 confirmed_height: Some(100),
388 spent_height: None,
389 timestamp: Some(1_700_000_000),
390 coinbase: false,
391 }
392 }
393
394 fn mock_with(id: Bytes32) -> MockChainSource {
395 MockChainSource::new().with_coin(id, record_for(id))
396 }
397
398 #[test]
401 fn pure_public_quorum_without_optin_fails_closed_for_custody() {
402 let id = coin_id(0x01);
403 let registry = ProviderRegistry::new()
406 .register(
407 Box::new(CoinsetProvider::new("coinset-a", 10, mock_with(id))),
408 None,
409 "coinset.org",
410 )
411 .register(
412 Box::new(CustomProvider::new("mirror-b", 20, mock_with(id))),
413 None,
414 "mirror.example",
415 );
416
417 let result = registry.trusted().coin_record(id);
418 assert_eq!(
419 result,
420 Err(ChainSourceError::NoProvider),
421 "pure-public custody must fail closed without allow_public_quorum_custody"
422 );
423 }
424
425 #[test]
428 fn operator_trusted_local_node_satisfies_custody() {
429 let id = coin_id(0x02);
430 let registry = ProviderRegistry::new().register(
431 Box::new(LocalNodeProvider::new("local", 0, mock_with(id))),
432 None, "local-node",
434 );
435
436 let record = registry.trusted().coin_record(id).unwrap();
437 assert_eq!(record, Some(record_for(id)));
438 }
439
440 #[test]
441 fn local_node_defaults_to_trusted() {
442 assert_eq!(
443 TrustLevel::default_for(ProviderKind::LocalNode),
444 TrustLevel::Trusted
445 );
446 assert_eq!(
447 TrustLevel::default_for(ProviderKind::PublicOracle),
448 TrustLevel::Untrusted
449 );
450 }
451
452 #[test]
455 fn quorum_including_trusted_member_satisfies_custody() {
456 let id = coin_id(0x03);
457 let registry = ProviderRegistry::new()
458 .register(
459 Box::new(LocalNodeProvider::new("local", 0, mock_with(id))),
460 None, "local-node",
462 )
463 .register(
464 Box::new(CoinsetProvider::new("coinset", 10, mock_with(id))),
465 None, "coinset.org",
467 );
468
469 let record = registry.trusted().coin_record(id).unwrap();
471 assert_eq!(record, Some(record_for(id)));
472 }
473
474 #[test]
475 fn trusted_source_error_fails_closed_not_public_fallback() {
476 let id = coin_id(0x04);
477 let registry = ProviderRegistry::new()
478 .register(
479 Box::new(LocalNodeProvider::new(
480 "local",
481 0,
482 MockChainSource::new().fail_with(ChainSourceError::Timeout),
483 )),
484 None,
485 "local-node",
486 )
487 .register(
488 Box::new(CoinsetProvider::new("coinset", 10, mock_with(id))),
489 None,
490 "coinset.org",
491 );
492
493 assert_eq!(
495 registry.trusted().coin_record(id),
496 Err(ChainSourceError::Timeout)
497 );
498 }
499
500 #[test]
503 fn optin_two_independent_groups_agree_satisfies_custody() {
504 let id = coin_id(0x05);
505 let registry = ProviderRegistry::new()
506 .allow_public_quorum_custody(true)
507 .register(
508 Box::new(CoinsetProvider::new("coinset", 10, mock_with(id))),
509 None,
510 "coinset.org",
511 )
512 .register(
513 Box::new(CustomProvider::new("mirror", 20, mock_with(id))),
514 None,
515 "mirror.example",
516 );
517
518 assert_eq!(
519 registry.trusted().coin_record(id).unwrap(),
520 Some(record_for(id))
521 );
522 }
523
524 #[test]
525 fn optin_single_group_fails_closed() {
526 let id = coin_id(0x06);
527 let registry = ProviderRegistry::new()
529 .allow_public_quorum_custody(true)
530 .register(
531 Box::new(CoinsetProvider::new("coinset", 10, mock_with(id))),
532 None,
533 "coinset.org",
534 );
535
536 assert_eq!(
537 registry.trusted().coin_record(id),
538 Err(ChainSourceError::NoProvider)
539 );
540 }
541
542 #[test]
543 fn optin_two_providers_same_group_fails_closed() {
544 let id = coin_id(0x07);
545 let registry = ProviderRegistry::new()
547 .allow_public_quorum_custody(true)
548 .register(
549 Box::new(CoinsetProvider::new("coinset-a", 10, mock_with(id))),
550 None,
551 "coinset.org",
552 )
553 .register(
554 Box::new(CoinsetProvider::new("coinset-b", 20, mock_with(id))),
555 None,
556 "coinset.org", );
558
559 assert_eq!(
560 registry.trusted().coin_record(id),
561 Err(ChainSourceError::NoProvider)
562 );
563 }
564
565 #[test]
566 fn optin_two_groups_disagree_fails_closed() {
567 let id = coin_id(0x08);
568 let registry = ProviderRegistry::new()
570 .allow_public_quorum_custody(true)
571 .register(
572 Box::new(CoinsetProvider::new("coinset", 10, mock_with(id))),
573 None,
574 "coinset.org",
575 )
576 .register(
577 Box::new(CustomProvider::new("empty", 20, MockChainSource::new())),
578 None,
579 "mirror.example",
580 );
581
582 assert_eq!(
584 registry.trusted().coin_record(id),
585 Err(ChainSourceError::NoProvider)
586 );
587 }
588
589 struct FixedListSource {
594 records: Vec<CoinRecord>,
595 }
596
597 impl ChainSource for FixedListSource {
598 type Error = ChainSourceError;
599
600 fn coin_record(&self, _coin_id: Bytes32) -> Result<Option<CoinRecord>, Self::Error> {
601 Ok(None)
602 }
603
604 fn coin_records_by_puzzle_hash(
605 &self,
606 _puzzle_hash: Bytes32,
607 _include_spent: bool,
608 ) -> Result<Vec<CoinRecord>, Self::Error> {
609 Ok(self.records.clone())
610 }
611
612 fn coin_records_by_parent(
613 &self,
614 _parent_coin_id: Bytes32,
615 ) -> Result<Vec<CoinRecord>, Self::Error> {
616 Ok(self.records.clone())
617 }
618
619 fn coin_spend(&self, _coin_id: Bytes32) -> Result<Option<CoinSpend>, Self::Error> {
620 Ok(None)
621 }
622
623 fn resolve_singleton_lineage(
624 &self,
625 _launcher_id: Bytes32,
626 ) -> Result<Option<SingletonLineage>, Self::Error> {
627 Ok(None)
628 }
629
630 fn peak_height(&self) -> Result<Option<u32>, Self::Error> {
631 Ok(None)
632 }
633
634 fn block_timestamp(&self, _height: u32) -> Result<Option<u64>, Self::Error> {
635 Ok(None)
636 }
637 }
638
639 fn list_source(records: Vec<CoinRecord>) -> FixedListSource {
640 FixedListSource { records }
641 }
642
643 #[test]
644 fn quorum_agrees_on_same_record_set_in_different_order() {
645 let ph = Bytes32::new([0x22; 32]);
646 let a = record_for(coin_id(0x0A));
647 let b = record_for(coin_id(0x0B));
648
649 let registry = ProviderRegistry::new()
652 .allow_public_quorum_custody(true)
653 .register(
654 Box::new(CoinsetProvider::new(
655 "coinset",
656 10,
657 list_source(vec![a.clone(), b.clone()]),
658 )),
659 None,
660 "coinset.org",
661 )
662 .register(
663 Box::new(CustomProvider::new(
664 "mirror",
665 20,
666 list_source(vec![b.clone(), a.clone()]),
667 )),
668 None,
669 "mirror.example",
670 );
671
672 let records = registry
673 .trusted()
674 .coin_records_by_puzzle_hash(ph, false)
675 .expect("honest sources agreeing on a set must satisfy the quorum");
676 assert_eq!(records.len(), 2);
677 assert!(records.contains(&a) && records.contains(&b));
678 }
679
680 #[test]
681 fn quorum_still_fails_closed_on_genuinely_different_record_sets() {
682 let ph = Bytes32::new([0x22; 32]);
683 let a = record_for(coin_id(0x0A));
684 let b = record_for(coin_id(0x0B));
685 let c = record_for(coin_id(0x0C));
686
687 let registry = ProviderRegistry::new()
690 .allow_public_quorum_custody(true)
691 .register(
692 Box::new(CoinsetProvider::new(
693 "coinset",
694 10,
695 list_source(vec![a.clone(), b]),
696 )),
697 None,
698 "coinset.org",
699 )
700 .register(
701 Box::new(CustomProvider::new("mirror", 20, list_source(vec![c, a]))),
702 None,
703 "mirror.example",
704 );
705
706 assert_eq!(
707 registry.trusted().coin_records_by_puzzle_hash(ph, false),
708 Err(ChainSourceError::NoProvider),
709 "genuinely different record sets must fail closed"
710 );
711 }
712
713 #[test]
716 fn discovery_view_returns_single_provider_answer() {
717 let id = coin_id(0x09);
718 let registry = ProviderRegistry::new().register(
719 Box::new(CoinsetProvider::new("coinset", 10, mock_with(id))),
720 None,
721 "coinset.org",
722 );
723
724 assert_eq!(
726 registry.any().coin_record(id).unwrap(),
727 Some(record_for(id))
728 );
729 assert_eq!(
731 registry.trusted().coin_record(id),
732 Err(ChainSourceError::NoProvider)
733 );
734 }
735
736 #[test]
737 fn every_read_method_flows_through_both_views() {
738 let ph = Bytes32::new([0x22; 32]);
739 let parent = Coin::new(Bytes32::new([0x01; 32]), ph, 1);
740 let parent_id = parent.coin_id();
741 let child = Coin::new(parent_id, ph, 1);
742 let launcher = Bytes32::new([0x77; 32]);
743
744 let source = MockChainSource::new()
745 .with_coin(parent_id, record_for(parent_id))
746 .with_coin(child.coin_id(), {
747 let mut r = record_for(child.coin_id());
748 r.coin = child;
749 r
750 })
751 .with_spend(
752 parent_id,
753 chia_protocol::CoinSpend::new(
754 parent,
755 chia_protocol::Program::from(vec![1]),
756 chia_protocol::Program::from(vec![0x80]),
757 ),
758 )
759 .with_lineage(launcher, SingletonLineage::single(launcher))
760 .with_timestamp(100, 1_700_000_000)
761 .with_peak(555);
762
763 let registry = ProviderRegistry::new().register(
764 Box::new(LocalNodeProvider::new("local", 0, source)),
765 None, "local-node",
767 );
768
769 let custody = registry.trusted();
771 assert!(!custody
772 .coin_records_by_puzzle_hash(ph, true)
773 .unwrap()
774 .is_empty());
775 assert!(!custody
776 .coin_records_by_parent(parent_id)
777 .unwrap()
778 .is_empty());
779 assert!(custody.coin_spend(parent_id).unwrap().is_some());
780 assert_eq!(custody.peak_height().unwrap(), Some(555));
781 assert_eq!(custody.block_timestamp(100).unwrap(), Some(1_700_000_000));
782 assert_eq!(
783 custody.resolve_singleton_lineage(launcher).unwrap(),
784 Some(SingletonLineage::single(launcher))
785 );
786
787 let discovery = registry.any();
789 assert!(discovery.coin_record(parent_id).unwrap().is_some());
790 assert!(!discovery
791 .coin_records_by_puzzle_hash(ph, false)
792 .unwrap()
793 .is_empty());
794 assert!(!discovery
795 .coin_records_by_parent(parent_id)
796 .unwrap()
797 .is_empty());
798 assert!(discovery.coin_spend(parent_id).unwrap().is_some());
799 assert_eq!(discovery.peak_height().unwrap(), Some(555));
800 assert_eq!(discovery.block_timestamp(100).unwrap(), Some(1_700_000_000));
801 assert!(discovery
802 .resolve_singleton_lineage(launcher)
803 .unwrap()
804 .is_some());
805 }
806
807 #[test]
808 fn discovery_falls_through_failing_providers_to_a_responder() {
809 let id = coin_id(0x0B);
810 let registry = ProviderRegistry::new()
811 .register(
812 Box::new(CoinsetProvider::new(
813 "down",
814 0,
815 MockChainSource::new().fail_with(ChainSourceError::Timeout),
816 )),
817 None,
818 "down",
819 )
820 .register(
821 Box::new(CoinsetProvider::new("up", 10, mock_with(id))),
822 None,
823 "up",
824 );
825 assert_eq!(
826 registry.any().coin_record(id).unwrap(),
827 Some(record_for(id))
828 );
829 }
830
831 #[test]
832 fn empty_registry_fails_closed_everywhere() {
833 let registry = ProviderRegistry::new();
834 let id = coin_id(0x0A);
835 assert_eq!(
836 registry.trusted().coin_record(id),
837 Err(ChainSourceError::NoProvider)
838 );
839 assert_eq!(
840 registry.any().coin_record(id),
841 Err(ChainSourceError::NoProvider)
842 );
843 }
844}