dig_chainsource_interface/source.rs
1//! The [`ChainSource`] trait — the ONE canonical, reads-only interface for consulting Chia chain
2//! state, and [`ChainSourceProvider`], the self-describing form a registry composes.
3//!
4//! The trait is deliberately **synchronous and object-safe**: every method takes `&self` and
5//! returns a plain `Result`, so `Box<dyn ChainSource<Error = _>>` works and an async backend can
6//! present a blocking facade at the aggregator boundary (see the async->sync bridge note in the
7//! crate docs). It is **reads-only** — there is NO broadcast/push/submit method anywhere in this
8//! crate, by design (SPEC §1).
9
10use chia_protocol::{Bytes32, CoinSpend};
11
12use crate::lineage::SingletonLineage;
13use crate::provider::ProviderInfo;
14use crate::record::CoinRecord;
15
16/// A reads-only view of Chia chain state — the single canonical contract every provider implements
17/// and every consumer depends on.
18///
19/// ## Fail-closed `None` vs `Err` (the crux)
20///
21/// Every fallible method distinguishes two outcomes that consumers MUST treat differently:
22/// - `Ok(None)` / an empty `Vec` — the source reliably answered and the thing genuinely does not
23/// exist (an unlaunched singleton, an unspent coin, a melted lineage). Safe to act on.
24/// - `Err(_)` — the source could NOT reliably answer (transport, timeout, malformed, unsupported).
25/// The answer is unknown; the consumer MUST fail closed and never treat it as an absence.
26///
27/// ## Reads only
28///
29/// This trait cannot broadcast, push, or submit anything to the chain — there is no such method,
30/// deliberately. It is a pure reader; write/spend paths live entirely outside this crate.
31pub trait ChainSource {
32 /// The source's own transport/parse error. [`crate::ChainSourceError`] is the recommended type
33 /// for registry participants, but any `Display` error is accepted.
34 type Error: core::fmt::Display;
35
36 /// Reads the coin with id `coin_id`.
37 ///
38 /// `Ok(None)` = the coin genuinely does not exist per this source; `Err(_)` = could not answer.
39 fn coin_record(&self, coin_id: Bytes32) -> Result<Option<CoinRecord>, Self::Error>;
40
41 /// Reads all coins paying to `puzzle_hash`, optionally including already-spent coins.
42 ///
43 /// An empty `Vec` = no matching coins; `Err(_)` = could not answer.
44 fn coin_records_by_puzzle_hash(
45 &self,
46 puzzle_hash: Bytes32,
47 include_spent: bool,
48 ) -> Result<Vec<CoinRecord>, Self::Error>;
49
50 /// Reads all coins whose parent is `parent_coin_id` — the direct children created by spending
51 /// that coin.
52 ///
53 /// An empty `Vec` = no known children; `Err(_)` = could not answer.
54 fn coin_records_by_parent(
55 &self,
56 parent_coin_id: Bytes32,
57 ) -> Result<Vec<CoinRecord>, Self::Error>;
58
59 /// Reads the spend that SPENT `coin_id` (the spend whose input coin is `coin_id`).
60 ///
61 /// `Ok(None)` = `coin_id` is unspent or unknown; `Err(_)` = could not answer.
62 fn coin_spend(&self, coin_id: Bytes32) -> Result<Option<CoinSpend>, Self::Error>;
63
64 /// Reads the spend that CREATED `coin_id` — i.e. the spend of `coin_id`'s parent — by resolving
65 /// the coin's parent and reading that parent's spend. This is the **money-critical parent-walk
66 /// primitive**.
67 ///
68 /// A coin's `puzzle_hash` is attacker-chosen: anyone can pay a coin whose puzzle hash equals a
69 /// victim singleton's outer hash, so a bare `launcher_id ==` check is spoofable. Authenticating
70 /// a coin as a genuine singleton requires walking `parent_spend` back toward the real launcher,
71 /// where each hop is proven from the parent's actual reveal+solution. A spoofed curried-puzzle
72 /// coin has no genuine recreation parent-spend, so the walk fails closed rather than trusting
73 /// the cheap equality. Consumers MUST walk this primitive, never trust puzzle-hash equality.
74 ///
75 /// The default implementation composes [`coin_record`](Self::coin_record) +
76 /// [`coin_spend`](Self::coin_spend); a source may override it with a direct query. `Ok(None)` =
77 /// the parent is unspent/unknown (a gap → fail closed mid-walk); `Err(_)` = could not answer.
78 fn parent_spend(&self, coin_id: Bytes32) -> Result<Option<CoinSpend>, Self::Error> {
79 let Some(record) = self.coin_record(coin_id)? else {
80 return Ok(None);
81 };
82 self.coin_spend(record.coin.parent_coin_info)
83 }
84
85 /// Resolves the singleton launched at `launcher_id` to its authenticated [`SingletonLineage`]
86 /// (every coin id from launcher to current tip).
87 ///
88 /// This MUST be a genuine forward walk from the launcher to its tip — each coin the singleton
89 /// recreation of the previous — NEVER an echo of a caller-supplied coin, or membership becomes
90 /// meaningless (SPEC §4, the money-critical requirement). `Ok(None)` = the launcher never
91 /// existed or the singleton has been fully melted; `Err(_)` = could not answer.
92 fn resolve_singleton_lineage(
93 &self,
94 launcher_id: Bytes32,
95 ) -> Result<Option<SingletonLineage>, Self::Error>;
96
97 /// Reads the current peak (fully-synced) block height, if the source tracks one.
98 ///
99 /// `Ok(None)` = the source does not expose a peak; `Err(_)` = could not answer.
100 fn peak_height(&self) -> Result<Option<u32>, Self::Error>;
101
102 /// Reads the Unix timestamp of the block at `height`.
103 ///
104 /// `Ok(None)` = no such block or no timestamp index; `Err(_)` = could not answer.
105 fn block_timestamp(&self, height: u32) -> Result<Option<u64>, Self::Error>;
106}
107
108/// A [`ChainSource`] that describes itself to the aggregating registry.
109///
110/// The registry uses [`provider_info`](Self::provider_info) to order and trust-weight providers
111/// (see [`ProviderInfo`]).
112pub trait ChainSourceProvider: ChainSource {
113 /// This provider's registration descriptor (identity, kind, priority, trust posture).
114 fn provider_info(&self) -> ProviderInfo;
115}