1use std::sync::Arc;
22
23use chia_protocol::{Bytes32, CoinSpend, CoinState, CoinStateFilters, Program};
24use dig_chainsource_interface::{
25 ChainSource, ChainSourceError, ChainSourceProvider, CoinRecord, ProviderInfo, SingletonLineage,
26};
27use tokio::runtime::Handle;
28use tokio::sync::RwLock;
29
30use crate::bridge::run_blocking;
31use crate::cache::CoinStateCache;
32use crate::fetcher::CoinStateFetcher;
33
34#[derive(Clone)]
38pub struct ChiaPeerProvider {
39 fetcher: Arc<dyn CoinStateFetcher>,
40 cache: Arc<RwLock<CoinStateCache>>,
41 handle: Handle,
42 info: ProviderInfo,
43}
44
45impl ChiaPeerProvider {
46 pub fn new(
50 fetcher: Arc<dyn CoinStateFetcher>,
51 cache: Arc<RwLock<CoinStateCache>>,
52 handle: Handle,
53 info: ProviderInfo,
54 ) -> Self {
55 Self {
56 fetcher,
57 cache,
58 handle,
59 info,
60 }
61 }
62
63 fn coin_state(&self, coin_id: Bytes32) -> Result<Option<CoinState>, ChainSourceError> {
65 let fetcher = self.fetcher.clone();
66 let cache = self.cache.clone();
67 run_blocking(&self.handle, async move {
68 if let Some(cached) = cache.read().await.get(coin_id) {
69 return Ok(Some(cached));
70 }
71 let states = fetcher.coin_states(vec![coin_id], false).await?;
72 Ok::<_, crate::error::ChiaPeerError>(
73 states.into_iter().find(|s| s.coin.coin_id() == coin_id),
74 )
75 })?
76 .map_err(ChainSourceError::from)
77 }
78}
79
80impl ChainSource for ChiaPeerProvider {
81 type Error = ChainSourceError;
82
83 fn coin_record(&self, coin_id: Bytes32) -> Result<Option<CoinRecord>, Self::Error> {
84 let peak = self.peak_height()?;
85 Ok(self
86 .coin_state(coin_id)?
87 .map(CoinRecord::from_coin_state)
88 .map(|record| clamp_confirmed_to_peak(record, peak)))
89 }
90
91 fn coin_records_by_puzzle_hash(
92 &self,
93 puzzle_hash: Bytes32,
94 include_spent: bool,
95 ) -> Result<Vec<CoinRecord>, Self::Error> {
96 let fetcher = self.fetcher.clone();
97 let filters = CoinStateFilters {
98 include_spent,
99 include_unspent: true,
100 include_hinted: true,
101 min_amount: 0,
102 };
103 let states = run_blocking(&self.handle, async move {
104 fetcher
105 .puzzle_states(vec![puzzle_hash], filters, false)
106 .await
107 })?
108 .map_err(ChainSourceError::from)?;
109 let peak = self.peak_height()?;
110 Ok(states
111 .into_iter()
112 .map(CoinRecord::from_coin_state)
113 .map(|record| clamp_confirmed_to_peak(record, peak))
114 .collect())
115 }
116
117 fn coin_records_by_parent(
118 &self,
119 parent_coin_id: Bytes32,
120 ) -> Result<Vec<CoinRecord>, Self::Error> {
121 let fetcher = self.fetcher.clone();
122 let states = run_blocking(&self.handle, async move {
123 fetcher.children(parent_coin_id).await
124 })?
125 .map_err(ChainSourceError::from)?;
126 let peak = self.peak_height()?;
127 Ok(states
128 .into_iter()
129 .map(CoinRecord::from_coin_state)
130 .map(|record| clamp_confirmed_to_peak(record, peak))
131 .collect())
132 }
133
134 fn coin_spend(&self, coin_id: Bytes32) -> Result<Option<CoinSpend>, Self::Error> {
135 let Some(state) = self.coin_state(coin_id)? else {
138 return Ok(None);
139 };
140 let Some(spent_height) = state.spent_height else {
141 return Ok(None);
142 };
143 let fetcher = self.fetcher.clone();
144 let (puzzle, solution) = run_blocking(&self.handle, async move {
145 fetcher.puzzle_and_solution(coin_id, spent_height).await
146 })?
147 .map_err(ChainSourceError::from)?;
148
149 verify_reveal_matches(&puzzle, state.coin.puzzle_hash)?;
152 Ok(Some(CoinSpend::new(state.coin, puzzle, solution)))
153 }
154
155 fn resolve_singleton_lineage(
156 &self,
157 _launcher_id: Bytes32,
158 ) -> Result<Option<SingletonLineage>, Self::Error> {
159 Err(ChainSourceError::Unsupported(
160 "singleton lineage resolution is not provided by the light-client source; \
161 use an aggregating chain source",
162 ))
163 }
164
165 fn peak_height(&self) -> Result<Option<u32>, Self::Error> {
166 let cache = self.cache.clone();
167 let peak = run_blocking(&self.handle, async move { cache.read().await.peak() })?;
168 Ok(peak.map(|(height, _)| height))
169 }
170
171 fn block_timestamp(&self, _height: u32) -> Result<Option<u64>, Self::Error> {
172 Err(ChainSourceError::Unsupported(
173 "block timestamps are not indexed by the light-client source",
174 ))
175 }
176}
177
178impl ChainSourceProvider for ChiaPeerProvider {
179 fn provider_info(&self) -> ProviderInfo {
180 self.info.clone()
181 }
182}
183
184fn clamp_confirmed_to_peak(mut record: CoinRecord, peak: Option<u32>) -> CoinRecord {
200 if let (Some(confirmed), Some(peak)) = (record.confirmed_height, peak) {
201 if confirmed > peak {
202 record.confirmed_height = Some(peak);
203 }
204 }
205 record
206}
207
208fn verify_reveal_matches(puzzle: &Program, expected: Bytes32) -> Result<(), ChainSourceError> {
211 let actual: Bytes32 = chia::clvm_utils::tree_hash_from_bytes(puzzle.as_ref())
212 .map_err(|e| ChainSourceError::Malformed(format!("undecodable puzzle reveal: {e}")))?
213 .into();
214 if actual != expected {
215 return Err(ChainSourceError::Malformed(
216 "puzzle reveal does not hash to the coin's puzzle hash".into(),
217 ));
218 }
219 Ok(())
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225 use crate::error::ChiaPeerError;
226 use async_trait::async_trait;
227 use chia_protocol::{Coin, Program};
228 use dig_chainsource_interface::{ProviderId, ProviderKind};
229 use std::borrow::Cow;
230
231 #[derive(Default, Clone)]
234 struct MockFetcher {
235 coin_states: Vec<CoinState>,
236 fail: Option<ChiaPeerError>,
237 children: Vec<CoinState>,
238 puzzle_states: Vec<CoinState>,
239 reveal: Option<(Program, Program)>,
240 }
241
242 #[async_trait]
243 impl CoinStateFetcher for MockFetcher {
244 async fn coin_states(
245 &self,
246 _coin_ids: Vec<Bytes32>,
247 _subscribe: bool,
248 ) -> Result<Vec<CoinState>, ChiaPeerError> {
249 match &self.fail {
250 Some(e) => Err(e.clone()),
251 None => Ok(self.coin_states.clone()),
252 }
253 }
254 async fn puzzle_states(
255 &self,
256 _puzzle_hashes: Vec<Bytes32>,
257 _filters: CoinStateFilters,
258 _subscribe: bool,
259 ) -> Result<Vec<CoinState>, ChiaPeerError> {
260 match &self.fail {
261 Some(e) => Err(e.clone()),
262 None => Ok(self.puzzle_states.clone()),
263 }
264 }
265 async fn children(&self, _coin_id: Bytes32) -> Result<Vec<CoinState>, ChiaPeerError> {
266 match &self.fail {
267 Some(e) => Err(e.clone()),
268 None => Ok(self.children.clone()),
269 }
270 }
271 async fn puzzle_and_solution(
272 &self,
273 _coin_id: Bytes32,
274 _height: u32,
275 ) -> Result<(Program, Program), ChiaPeerError> {
276 if let Some(e) = &self.fail {
277 return Err(e.clone());
278 }
279 match &self.reveal {
280 Some(reveal) => Ok(reveal.clone()),
281 None => Err(ChiaPeerError::Rejected("no reveal".into())),
283 }
284 }
285 }
286
287 fn reveal_and_matching_puzzle_hash() -> (Program, Bytes32) {
290 let puzzle = Program::from(vec![1u8]);
291 let ph: Bytes32 = chia::clvm_utils::tree_hash_from_bytes(puzzle.as_ref())
292 .unwrap()
293 .into();
294 (puzzle, ph)
295 }
296
297 fn info() -> ProviderInfo {
298 ProviderInfo {
299 id: ProviderId(Cow::Borrowed("chia-peer-test")),
300 kind: ProviderKind::Custom,
301 priority: 20,
302 trustless: false,
303 }
304 }
305
306 fn provider_with(fetcher: MockFetcher) -> (tokio::runtime::Runtime, ChiaPeerProvider) {
307 provider_with_peak(fetcher, None)
308 }
309
310 fn provider_with_peak(
313 fetcher: MockFetcher,
314 peak: Option<u32>,
315 ) -> (tokio::runtime::Runtime, ChiaPeerProvider) {
316 let rt = tokio::runtime::Builder::new_multi_thread()
317 .worker_threads(1)
318 .enable_all()
319 .build()
320 .expect("multi-thread runtime");
321 let mut cache = CoinStateCache::new();
322 if let Some(height) = peak {
323 cache.set_peak(height, Bytes32::new([0xAB; 32]));
324 }
325 let provider = ChiaPeerProvider::new(
326 Arc::new(fetcher),
327 Arc::new(RwLock::new(cache)),
328 rt.handle().clone(),
329 info(),
330 );
331 (rt, provider)
332 }
333
334 fn call<T: Send>(f: impl FnOnce() -> T + Send) -> T {
336 std::thread::scope(|s| s.spawn(f).join().expect("thread panicked"))
337 }
338
339 fn coin(seed: u8) -> Coin {
340 Coin::new(Bytes32::new([seed; 32]), Bytes32::new([seed ^ 1; 32]), 1)
341 }
342
343 #[test]
346 fn coin_record_returns_some_for_a_known_coin() {
347 let c = coin(7);
348 let id = c.coin_id();
349 let fetcher = MockFetcher {
350 coin_states: vec![CoinState {
351 coin: c,
352 created_height: Some(100),
353 spent_height: None,
354 }],
355 ..Default::default()
356 };
357 let (_rt, provider) = provider_with(fetcher);
358 let record = call(move || provider.coin_record(id)).expect("read ok");
359 assert!(record.is_some());
360 assert_eq!(record.unwrap().confirmed_height, Some(100));
361 }
362
363 #[test]
368 fn live_fetched_coin_above_peak_reports_clamped_confirmed_height() {
369 let c = coin(11);
370 let id = c.coin_id();
371 let fetcher = MockFetcher {
372 coin_states: vec![CoinState {
373 coin: c,
374 created_height: Some(1_000_001), spent_height: None,
376 }],
377 ..Default::default()
378 };
379 let (_rt, provider) = provider_with_peak(fetcher, Some(1_000_000));
380 let record = call(move || provider.coin_record(id))
381 .expect("read ok")
382 .expect("coin present, never omitted");
383 assert_eq!(
384 record.confirmed_height,
385 Some(1_000_000),
386 "an above-peak live coin must clamp to the peak (0 confirmations), never overstate"
387 );
388 }
389
390 #[test]
393 fn live_fetched_coin_at_or_below_peak_is_unaffected() {
394 let c = coin(12);
395 let id = c.coin_id();
396 let fetcher = MockFetcher {
397 coin_states: vec![CoinState {
398 coin: c,
399 created_height: Some(900_000),
400 spent_height: None,
401 }],
402 ..Default::default()
403 };
404 let (_rt, provider) = provider_with_peak(fetcher, Some(1_000_000));
405 let record = call(move || provider.coin_record(id))
406 .expect("read ok")
407 .expect("coin present");
408 assert_eq!(record.confirmed_height, Some(900_000));
409 }
410
411 #[test]
413 fn discovery_reads_clamp_above_peak_confirmed_height() {
414 let fetcher = MockFetcher {
415 puzzle_states: vec![CoinState {
416 coin: coin(13),
417 created_height: Some(2_000_000),
418 spent_height: None,
419 }],
420 children: vec![CoinState {
421 coin: coin(14),
422 created_height: Some(2_000_000),
423 spent_height: None,
424 }],
425 ..Default::default()
426 };
427 let (_rt, provider) = provider_with_peak(fetcher, Some(1_000_000));
428 let ph = Bytes32::new([8; 32]);
429 let parent = Bytes32::new([9; 32]);
430 let p = provider.clone();
431 let by_ph = call(move || p.coin_records_by_puzzle_hash(ph, true)).unwrap();
432 assert_eq!(by_ph[0].confirmed_height, Some(1_000_000));
433 let by_parent = call(move || provider.coin_records_by_parent(parent)).unwrap();
434 assert_eq!(by_parent[0].confirmed_height, Some(1_000_000));
435 }
436
437 #[test]
438 fn coin_record_returns_none_for_provable_absence() {
439 let (_rt, provider) = provider_with(MockFetcher::default());
440 let id = coin(9).coin_id();
441 let record = call(move || provider.coin_record(id)).expect("read ok");
442 assert_eq!(record, None);
443 }
444
445 #[test]
446 fn transport_failure_is_err_never_false_absence() {
447 let fetcher = MockFetcher {
448 fail: Some(ChiaPeerError::Transport("socket reset".into())),
449 ..Default::default()
450 };
451 let (_rt, provider) = provider_with(fetcher);
452 let id = coin(3).coin_id();
453 let result = call(move || provider.coin_record(id));
454 assert!(
455 matches!(result, Err(ChainSourceError::Transport(_))),
456 "a transport failure MUST be Err, never Ok(None): {result:?}"
457 );
458 }
459
460 #[test]
461 fn coin_spend_of_unspent_coin_is_none() {
462 let c = coin(4);
463 let id = c.coin_id();
464 let fetcher = MockFetcher {
465 coin_states: vec![CoinState {
466 coin: c,
467 created_height: Some(10),
468 spent_height: None,
469 }],
470 ..Default::default()
471 };
472 let (_rt, provider) = provider_with(fetcher);
473 assert_eq!(call(move || provider.coin_spend(id)).unwrap(), None);
474 }
475
476 #[test]
477 fn coin_spend_of_spent_coin_assembles_from_real_coin() {
478 let (puzzle, ph) = reveal_and_matching_puzzle_hash();
479 let c = Coin::new(Bytes32::new([5; 32]), ph, 1);
480 let id = c.coin_id();
481 let fetcher = MockFetcher {
482 coin_states: vec![CoinState {
483 coin: c,
484 created_height: Some(10),
485 spent_height: Some(20),
486 }],
487 reveal: Some((puzzle, Program::from(vec![2u8]))),
488 ..Default::default()
489 };
490 let (_rt, provider) = provider_with(fetcher);
491 let spend = call(move || provider.coin_spend(id))
492 .unwrap()
493 .expect("spend");
494 assert_eq!(spend.coin, c);
495 }
496
497 #[test]
500 fn coin_spend_of_spent_coin_with_rejected_reveal_is_err_never_none() {
501 let c = coin(6);
502 let id = c.coin_id();
503 let fetcher = MockFetcher {
504 coin_states: vec![CoinState {
505 coin: c,
506 created_height: Some(10),
507 spent_height: Some(20),
508 }],
509 reveal: None, ..Default::default()
511 };
512 let (_rt, provider) = provider_with(fetcher);
513 let result = call(move || provider.coin_spend(id));
514 assert!(
515 matches!(result, Err(ChainSourceError::Transport(_))),
516 "a rejected reveal for a spent coin must be Err, never Ok(None): {result:?}"
517 );
518 }
519
520 #[test]
523 fn coin_spend_rejects_a_reveal_that_does_not_hash_to_the_coin() {
524 let c = coin(8); let id = c.coin_id();
526 let fetcher = MockFetcher {
527 coin_states: vec![CoinState {
528 coin: c,
529 created_height: Some(10),
530 spent_height: Some(20),
531 }],
532 reveal: Some((Program::from(vec![1u8]), Program::from(vec![2u8]))),
533 ..Default::default()
534 };
535 let (_rt, provider) = provider_with(fetcher);
536 let result = call(move || provider.coin_spend(id));
537 assert!(
538 matches!(result, Err(ChainSourceError::Malformed(_))),
539 "a mismatched reveal must be Malformed: {result:?}"
540 );
541 }
542
543 #[test]
544 fn records_by_puzzle_hash_and_parent_map_states() {
545 let fetcher = MockFetcher {
546 puzzle_states: vec![CoinState {
547 coin: coin(1),
548 created_height: Some(1),
549 spent_height: None,
550 }],
551 children: vec![CoinState {
552 coin: coin(2),
553 created_height: Some(2),
554 spent_height: None,
555 }],
556 ..Default::default()
557 };
558 let (_rt, provider) = provider_with(fetcher);
559 let ph = Bytes32::new([8; 32]);
560 let parent = Bytes32::new([9; 32]);
561 let p = provider.clone();
562 assert_eq!(
563 call(move || p.coin_records_by_puzzle_hash(ph, true))
564 .unwrap()
565 .len(),
566 1
567 );
568 assert_eq!(
569 call(move || provider.coin_records_by_parent(parent))
570 .unwrap()
571 .len(),
572 1
573 );
574 }
575
576 #[test]
577 fn lineage_and_timestamp_are_unsupported_not_false_absence() {
578 let (_rt, provider) = provider_with(MockFetcher::default());
579 let p = provider.clone();
580 assert!(matches!(
581 call(move || p.resolve_singleton_lineage(Bytes32::new([1; 32]))),
582 Err(ChainSourceError::Unsupported(_))
583 ));
584 assert!(matches!(
585 call(move || provider.block_timestamp(1)),
586 Err(ChainSourceError::Unsupported(_))
587 ));
588 }
589
590 #[test]
591 fn provider_info_is_reported() {
592 let (_rt, provider) = provider_with(MockFetcher::default());
593 assert_eq!(provider.provider_info().priority, 20);
594 assert_eq!(provider.peak_height().unwrap(), None);
595 }
596}