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: PartialEq + 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
192fn quorum_read<T, Q>(
199 providers: &[Registration],
200 threshold: usize,
201 query: Q,
202) -> Result<T, ChainSourceError>
203where
204 T: PartialEq + Clone,
205 Q: Fn(&DynProvider) -> Result<T, ChainSourceError>,
206{
207 let mut per_group: Vec<(&str, T)> = Vec::new();
209 for reg in providers {
210 let group = reg.independence_group.as_str();
211 if per_group.iter().any(|(existing, _)| *existing == group) {
212 continue; }
214 if let Ok(answer) = query(&*reg.provider) {
215 per_group.push((group, answer));
216 }
217 }
218
219 for (_, candidate) in &per_group {
221 let agreeing = per_group.iter().filter(|(_, a)| a == candidate).count();
222 if agreeing >= threshold {
223 return Ok(candidate.clone());
224 }
225 }
226 Err(ChainSourceError::NoProvider)
227}
228
229impl ChainSource for TrustedView<'_> {
233 type Error = ChainSourceError;
234
235 fn coin_record(&self, coin_id: Bytes32) -> Result<Option<CoinRecord>, Self::Error> {
236 self.custody_read(move |p| p.coin_record(coin_id))
237 }
238
239 fn coin_records_by_puzzle_hash(
240 &self,
241 puzzle_hash: Bytes32,
242 include_spent: bool,
243 ) -> Result<Vec<CoinRecord>, Self::Error> {
244 self.custody_read(move |p| p.coin_records_by_puzzle_hash(puzzle_hash, include_spent))
245 }
246
247 fn coin_records_by_parent(
248 &self,
249 parent_coin_id: Bytes32,
250 ) -> Result<Vec<CoinRecord>, Self::Error> {
251 self.custody_read(move |p| p.coin_records_by_parent(parent_coin_id))
252 }
253
254 fn coin_spend(&self, coin_id: Bytes32) -> Result<Option<CoinSpend>, Self::Error> {
255 self.custody_read(move |p| p.coin_spend(coin_id))
256 }
257
258 fn resolve_singleton_lineage(
259 &self,
260 launcher_id: Bytes32,
261 ) -> Result<Option<SingletonLineage>, Self::Error> {
262 self.custody_read(move |p| p.resolve_singleton_lineage(launcher_id))
266 }
267
268 fn peak_height(&self) -> Result<Option<u32>, Self::Error> {
269 self.custody_read(|p| p.peak_height())
270 }
271
272 fn block_timestamp(&self, height: u32) -> Result<Option<u64>, Self::Error> {
273 self.custody_read(move |p| p.block_timestamp(height))
274 }
275}
276
277impl ChainSource for DiscoveryView<'_> {
278 type Error = ChainSourceError;
279
280 fn coin_record(&self, coin_id: Bytes32) -> Result<Option<CoinRecord>, Self::Error> {
281 self.discovery_read(move |p| p.coin_record(coin_id))
282 }
283
284 fn coin_records_by_puzzle_hash(
285 &self,
286 puzzle_hash: Bytes32,
287 include_spent: bool,
288 ) -> Result<Vec<CoinRecord>, Self::Error> {
289 self.discovery_read(move |p| p.coin_records_by_puzzle_hash(puzzle_hash, include_spent))
290 }
291
292 fn coin_records_by_parent(
293 &self,
294 parent_coin_id: Bytes32,
295 ) -> Result<Vec<CoinRecord>, Self::Error> {
296 self.discovery_read(move |p| p.coin_records_by_parent(parent_coin_id))
297 }
298
299 fn coin_spend(&self, coin_id: Bytes32) -> Result<Option<CoinSpend>, Self::Error> {
300 self.discovery_read(move |p| p.coin_spend(coin_id))
301 }
302
303 fn resolve_singleton_lineage(
304 &self,
305 launcher_id: Bytes32,
306 ) -> Result<Option<SingletonLineage>, Self::Error> {
307 self.discovery_read(move |p| p.resolve_singleton_lineage(launcher_id))
308 }
309
310 fn peak_height(&self) -> Result<Option<u32>, Self::Error> {
311 self.discovery_read(|p| p.peak_height())
312 }
313
314 fn block_timestamp(&self, height: u32) -> Result<Option<u64>, Self::Error> {
315 self.discovery_read(move |p| p.block_timestamp(height))
316 }
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322 use chia_protocol::Coin;
323 use dig_chainsource_interface::MockChainSource;
324
325 use crate::provider_registry::providers::{CoinsetProvider, CustomProvider, LocalNodeProvider};
326
327 fn coin_id(byte: u8) -> Bytes32 {
328 Coin::new(Bytes32::new([byte; 32]), Bytes32::new([byte; 32]), 1).coin_id()
329 }
330
331 fn record_for(id: Bytes32) -> CoinRecord {
332 CoinRecord {
333 coin: Coin::new(id, Bytes32::new([0x22; 32]), 1),
334 confirmed_height: Some(100),
335 spent_height: None,
336 timestamp: Some(1_700_000_000),
337 coinbase: false,
338 }
339 }
340
341 fn mock_with(id: Bytes32) -> MockChainSource {
342 MockChainSource::new().with_coin(id, record_for(id))
343 }
344
345 #[test]
348 fn pure_public_quorum_without_optin_fails_closed_for_custody() {
349 let id = coin_id(0x01);
350 let registry = ProviderRegistry::new()
353 .register(
354 Box::new(CoinsetProvider::new("coinset-a", 10, mock_with(id))),
355 None,
356 "coinset.org",
357 )
358 .register(
359 Box::new(CustomProvider::new("mirror-b", 20, mock_with(id))),
360 None,
361 "mirror.example",
362 );
363
364 let result = registry.trusted().coin_record(id);
365 assert_eq!(
366 result,
367 Err(ChainSourceError::NoProvider),
368 "pure-public custody must fail closed without allow_public_quorum_custody"
369 );
370 }
371
372 #[test]
375 fn operator_trusted_local_node_satisfies_custody() {
376 let id = coin_id(0x02);
377 let registry = ProviderRegistry::new().register(
378 Box::new(LocalNodeProvider::new("local", 0, mock_with(id))),
379 None, "local-node",
381 );
382
383 let record = registry.trusted().coin_record(id).unwrap();
384 assert_eq!(record, Some(record_for(id)));
385 }
386
387 #[test]
388 fn local_node_defaults_to_trusted() {
389 assert_eq!(
390 TrustLevel::default_for(ProviderKind::LocalNode),
391 TrustLevel::Trusted
392 );
393 assert_eq!(
394 TrustLevel::default_for(ProviderKind::PublicOracle),
395 TrustLevel::Untrusted
396 );
397 }
398
399 #[test]
402 fn quorum_including_trusted_member_satisfies_custody() {
403 let id = coin_id(0x03);
404 let registry = ProviderRegistry::new()
405 .register(
406 Box::new(LocalNodeProvider::new("local", 0, mock_with(id))),
407 None, "local-node",
409 )
410 .register(
411 Box::new(CoinsetProvider::new("coinset", 10, mock_with(id))),
412 None, "coinset.org",
414 );
415
416 let record = registry.trusted().coin_record(id).unwrap();
418 assert_eq!(record, Some(record_for(id)));
419 }
420
421 #[test]
422 fn trusted_source_error_fails_closed_not_public_fallback() {
423 let id = coin_id(0x04);
424 let registry = ProviderRegistry::new()
425 .register(
426 Box::new(LocalNodeProvider::new(
427 "local",
428 0,
429 MockChainSource::new().fail_with(ChainSourceError::Timeout),
430 )),
431 None,
432 "local-node",
433 )
434 .register(
435 Box::new(CoinsetProvider::new("coinset", 10, mock_with(id))),
436 None,
437 "coinset.org",
438 );
439
440 assert_eq!(
442 registry.trusted().coin_record(id),
443 Err(ChainSourceError::Timeout)
444 );
445 }
446
447 #[test]
450 fn optin_two_independent_groups_agree_satisfies_custody() {
451 let id = coin_id(0x05);
452 let registry = ProviderRegistry::new()
453 .allow_public_quorum_custody(true)
454 .register(
455 Box::new(CoinsetProvider::new("coinset", 10, mock_with(id))),
456 None,
457 "coinset.org",
458 )
459 .register(
460 Box::new(CustomProvider::new("mirror", 20, mock_with(id))),
461 None,
462 "mirror.example",
463 );
464
465 assert_eq!(
466 registry.trusted().coin_record(id).unwrap(),
467 Some(record_for(id))
468 );
469 }
470
471 #[test]
472 fn optin_single_group_fails_closed() {
473 let id = coin_id(0x06);
474 let registry = ProviderRegistry::new()
476 .allow_public_quorum_custody(true)
477 .register(
478 Box::new(CoinsetProvider::new("coinset", 10, mock_with(id))),
479 None,
480 "coinset.org",
481 );
482
483 assert_eq!(
484 registry.trusted().coin_record(id),
485 Err(ChainSourceError::NoProvider)
486 );
487 }
488
489 #[test]
490 fn optin_two_providers_same_group_fails_closed() {
491 let id = coin_id(0x07);
492 let registry = ProviderRegistry::new()
494 .allow_public_quorum_custody(true)
495 .register(
496 Box::new(CoinsetProvider::new("coinset-a", 10, mock_with(id))),
497 None,
498 "coinset.org",
499 )
500 .register(
501 Box::new(CoinsetProvider::new("coinset-b", 20, mock_with(id))),
502 None,
503 "coinset.org", );
505
506 assert_eq!(
507 registry.trusted().coin_record(id),
508 Err(ChainSourceError::NoProvider)
509 );
510 }
511
512 #[test]
513 fn optin_two_groups_disagree_fails_closed() {
514 let id = coin_id(0x08);
515 let registry = ProviderRegistry::new()
517 .allow_public_quorum_custody(true)
518 .register(
519 Box::new(CoinsetProvider::new("coinset", 10, mock_with(id))),
520 None,
521 "coinset.org",
522 )
523 .register(
524 Box::new(CustomProvider::new("empty", 20, MockChainSource::new())),
525 None,
526 "mirror.example",
527 );
528
529 assert_eq!(
531 registry.trusted().coin_record(id),
532 Err(ChainSourceError::NoProvider)
533 );
534 }
535
536 #[test]
539 fn discovery_view_returns_single_provider_answer() {
540 let id = coin_id(0x09);
541 let registry = ProviderRegistry::new().register(
542 Box::new(CoinsetProvider::new("coinset", 10, mock_with(id))),
543 None,
544 "coinset.org",
545 );
546
547 assert_eq!(
549 registry.any().coin_record(id).unwrap(),
550 Some(record_for(id))
551 );
552 assert_eq!(
554 registry.trusted().coin_record(id),
555 Err(ChainSourceError::NoProvider)
556 );
557 }
558
559 #[test]
560 fn every_read_method_flows_through_both_views() {
561 let ph = Bytes32::new([0x22; 32]);
562 let parent = Coin::new(Bytes32::new([0x01; 32]), ph, 1);
563 let parent_id = parent.coin_id();
564 let child = Coin::new(parent_id, ph, 1);
565 let launcher = Bytes32::new([0x77; 32]);
566
567 let source = MockChainSource::new()
568 .with_coin(parent_id, record_for(parent_id))
569 .with_coin(child.coin_id(), {
570 let mut r = record_for(child.coin_id());
571 r.coin = child;
572 r
573 })
574 .with_spend(
575 parent_id,
576 chia_protocol::CoinSpend::new(
577 parent,
578 chia_protocol::Program::from(vec![1]),
579 chia_protocol::Program::from(vec![0x80]),
580 ),
581 )
582 .with_lineage(launcher, SingletonLineage::single(launcher))
583 .with_timestamp(100, 1_700_000_000)
584 .with_peak(555);
585
586 let registry = ProviderRegistry::new().register(
587 Box::new(LocalNodeProvider::new("local", 0, source)),
588 None, "local-node",
590 );
591
592 let custody = registry.trusted();
594 assert!(!custody
595 .coin_records_by_puzzle_hash(ph, true)
596 .unwrap()
597 .is_empty());
598 assert!(!custody
599 .coin_records_by_parent(parent_id)
600 .unwrap()
601 .is_empty());
602 assert!(custody.coin_spend(parent_id).unwrap().is_some());
603 assert_eq!(custody.peak_height().unwrap(), Some(555));
604 assert_eq!(custody.block_timestamp(100).unwrap(), Some(1_700_000_000));
605 assert_eq!(
606 custody.resolve_singleton_lineage(launcher).unwrap(),
607 Some(SingletonLineage::single(launcher))
608 );
609
610 let discovery = registry.any();
612 assert!(discovery.coin_record(parent_id).unwrap().is_some());
613 assert!(!discovery
614 .coin_records_by_puzzle_hash(ph, false)
615 .unwrap()
616 .is_empty());
617 assert!(!discovery
618 .coin_records_by_parent(parent_id)
619 .unwrap()
620 .is_empty());
621 assert!(discovery.coin_spend(parent_id).unwrap().is_some());
622 assert_eq!(discovery.peak_height().unwrap(), Some(555));
623 assert_eq!(discovery.block_timestamp(100).unwrap(), Some(1_700_000_000));
624 assert!(discovery
625 .resolve_singleton_lineage(launcher)
626 .unwrap()
627 .is_some());
628 }
629
630 #[test]
631 fn discovery_falls_through_failing_providers_to_a_responder() {
632 let id = coin_id(0x0B);
633 let registry = ProviderRegistry::new()
634 .register(
635 Box::new(CoinsetProvider::new(
636 "down",
637 0,
638 MockChainSource::new().fail_with(ChainSourceError::Timeout),
639 )),
640 None,
641 "down",
642 )
643 .register(
644 Box::new(CoinsetProvider::new("up", 10, mock_with(id))),
645 None,
646 "up",
647 );
648 assert_eq!(
649 registry.any().coin_record(id).unwrap(),
650 Some(record_for(id))
651 );
652 }
653
654 #[test]
655 fn empty_registry_fails_closed_everywhere() {
656 let registry = ProviderRegistry::new();
657 let id = coin_id(0x0A);
658 assert_eq!(
659 registry.trusted().coin_record(id),
660 Err(ChainSourceError::NoProvider)
661 );
662 assert_eq!(
663 registry.any().coin_record(id),
664 Err(ChainSourceError::NoProvider)
665 );
666 }
667}