1use crate::{
2 Error,
3 model::{ChainPoint, CommitSourceStatus},
4 recorded::RecordedError,
5};
6use bitcoin::{BlockHash, Txid, block::Header};
7use std::{
8 net::SocketAddr,
9 path::{Path, PathBuf},
10 sync::{
11 Arc, Mutex, MutexGuard,
12 atomic::{AtomicBool, Ordering},
13 },
14 time::{Duration, Instant},
15};
16use urma_chain::{decode, observation::Chain};
17use urma_runtime::{
18 node::{Node, NodeConfig},
19 p2p::{Anchor, HeaderBootstrap, P2pProvider},
20};
21
22pub trait ChainSource: Send + Sync {
28 fn genesis(&self) -> Result<BlockHash, Error>;
29 fn tip(&self) -> Result<ChainPoint, Error>;
30 fn header(&self, height: u64) -> Result<Header, Error>;
31 fn block(&self, height: u64) -> Result<Vec<u8>, Error>;
32 fn chain_peers(&self) -> Result<u64, Error> {
34 Ok(0)
35 }
36 fn chain_target(&self) -> Option<u64> {
39 None
40 }
41}
42
43pub trait CommitSource {
47 fn chain(&self) -> Chain;
48 fn fetch(&self, txid: Txid, cancelled: &AtomicBool) -> Result<Option<Vec<u8>>, Error>;
49 fn probe(&self) -> Option<Result<(), Error>> {
52 None
53 }
54}
55
56pub(crate) fn check_cancelled(cancelled: &AtomicBool) -> Result<(), Error> {
57 if cancelled.load(Ordering::Acquire) {
58 return Err(Error::Busy("dependency acquisition cancelled".into()));
59 }
60 Ok(())
61}
62
63pub fn verify_parent(chain: Chain, txid: Txid, raw: &[u8]) -> Result<(), Error> {
66 let transaction = decode::transaction(raw, chain)
67 .map_err(|error| Error::Invalid(format!("commit parent bytes do not decode: {error}")))?;
68 if transaction.compute_txid() != txid {
69 return Err(Error::Invalid(
70 "commit parent bytes do not match the requested TXID".into(),
71 ));
72 }
73 Ok(())
74}
75
76pub struct RuntimeCommitSource {
80 pub node: Arc<Node>,
81}
82
83impl CommitSource for RuntimeCommitSource {
84 fn chain(&self) -> Chain {
85 self.node.chain()
86 }
87
88 fn fetch(&self, txid: Txid, cancelled: &AtomicBool) -> Result<Option<Vec<u8>>, Error> {
97 check_cancelled(cancelled)?;
98 fetch_from_node(&self.node, txid)
99 }
100}
101
102pub struct LocalRpcCommitSource {
106 chain: Chain,
107 rpc_url: String,
108 cookie_file: PathBuf,
109 probe_interval: Duration,
110 verified: Mutex<Option<(Vec<u8>, Arc<Node>)>>,
111 probed: Mutex<Option<Instant>>,
112}
113
114impl LocalRpcCommitSource {
115 pub const PROBE_INTERVAL: Duration = Duration::from_secs(10);
116
117 pub fn new(chain: Chain, rpc_url: String, cookie_file: PathBuf) -> Self {
118 Self {
119 chain,
120 rpc_url,
121 cookie_file,
122 probe_interval: Self::PROBE_INTERVAL,
123 verified: Mutex::new(None),
124 probed: Mutex::new(None),
125 }
126 }
127
128 pub fn with_probe_interval(mut self, interval: Duration) -> Self {
129 self.probe_interval = interval;
130 self
131 }
132
133 fn node(&self) -> Result<Arc<Node>, Error> {
134 let cookie = std::fs::read(&self.cookie_file).map_err(|_| {
135 Error::CommitSource("cookie file missing: litecoind not running?".into())
136 })?;
137 let mut verified = self
138 .verified
139 .lock()
140 .map_err(|_| Error::Invalid("local RPC commit source poisoned".into()))?;
141 if let Some((seen, node)) = verified.as_ref() {
142 if *seen == cookie {
143 return Ok(Arc::clone(node));
144 }
145 }
146 *verified = None;
147 let node = Node::connect(NodeConfig {
148 chain: self.chain,
149 rpc_url: self.rpc_url.clone(),
150 cookie_file: self.cookie_file.clone(),
151 })
152 .and_then(|node| node.require_txindex().map(|()| Arc::new(node)))
153 .map_err(|error| Error::CommitSource(error.to_string()))?;
154 *verified = Some((cookie, Arc::clone(&node)));
155 Ok(node)
156 }
157
158 fn probe_due(&self) -> Result<bool, Error> {
159 let mut probed = self
160 .probed
161 .lock()
162 .map_err(|_| Error::Invalid("local RPC probe clock poisoned".into()))?;
163 let due = probed.is_none_or(|last| last.elapsed() >= self.probe_interval);
164 if due {
165 *probed = Some(Instant::now());
166 }
167 Ok(due)
168 }
169}
170
171impl CommitSource for LocalRpcCommitSource {
172 fn chain(&self) -> Chain {
173 self.chain
174 }
175
176 fn fetch(&self, txid: Txid, cancelled: &AtomicBool) -> Result<Option<Vec<u8>>, Error> {
177 check_cancelled(cancelled)?;
178 let node = self.node()?;
179 fetch_from_node(&node, txid)
180 }
181
182 fn probe(&self) -> Option<Result<(), Error>> {
183 match self.probe_due() {
184 Ok(false) => None,
185 Ok(true) => Some(self.node().and_then(|node| {
186 node.call("getblockcount", &[])
187 .map(|_| ())
188 .map_err(|error| Error::CommitSource(error.to_string()))
189 })),
190 Err(error) => Some(Err(error)),
191 }
192 }
193}
194
195fn fetch_from_node(node: &Node, txid: Txid) -> Result<Option<Vec<u8>>, Error> {
196 let result = match node.call(
197 "getrawtransaction",
198 &[serde_json::json!(txid), serde_json::json!(false)],
199 ) {
200 Ok(result) => result,
201 Err(error) if error.is_missing_transaction() => return Ok(None),
202 Err(error) => return Err(Error::CommitSource(error.to_string())),
203 };
204 let encoded = result
205 .as_str()
206 .ok_or_else(|| Error::Invalid("commit source must return transaction hex".into()))?;
207 if encoded.len() > 16_000_000 || encoded.len() % 2 != 0 {
208 return Err(Error::Invalid(
209 "commit source hex exceeds capacity or has odd length".into(),
210 ));
211 }
212 let raw = hex::decode(encoded)
213 .map_err(|error| Error::Invalid(format!("commit source hex: {error}")))?;
214 Ok(Some(raw))
215}
216
217pub struct PublicCommitSource {
221 chain: Chain,
222 cache_dir: PathBuf,
223 state: Mutex<PublicState>,
224}
225
226struct PublicState {
227 node: Option<Arc<Node>>,
228 failure: Option<(Instant, String)>,
229}
230
231impl PublicCommitSource {
232 const RETRY: Duration = Duration::from_secs(60);
233
234 pub fn new(chain: Chain, cache_dir: PathBuf) -> Self {
235 Self {
236 chain,
237 cache_dir,
238 state: Mutex::new(PublicState {
239 node: None,
240 failure: None,
241 }),
242 }
243 }
244
245 fn node(&self) -> Result<Arc<Node>, Error> {
246 let mut state = self
247 .state
248 .lock()
249 .map_err(|_| Error::Invalid("public commit source poisoned".into()))?;
250 if let Some(node) = state.node.as_ref() {
251 return Ok(Arc::clone(node));
252 }
253 if let Some((failed, message)) = state.failure.as_ref() {
254 if failed.elapsed() < Self::RETRY {
255 return Err(Error::CommitSource(message.clone()));
256 }
257 }
258 match Node::public_pinned(self.chain, &self.cache_dir) {
259 Ok(node) => {
260 let node = Arc::new(node);
261 state.node = Some(Arc::clone(&node));
262 state.failure = None;
263 Ok(node)
264 }
265 Err(error) => {
266 let message = error.to_string();
267 state.failure = Some((Instant::now(), message.clone()));
268 Err(Error::CommitSource(message))
269 }
270 }
271 }
272}
273
274impl CommitSource for PublicCommitSource {
275 fn chain(&self) -> Chain {
276 self.chain
277 }
278
279 fn fetch(&self, txid: Txid, cancelled: &AtomicBool) -> Result<Option<Vec<u8>>, Error> {
280 check_cancelled(cancelled)?;
281 let node = self.node()?;
282 fetch_from_node(&node, txid)
283 }
284}
285
286pub struct CommitSources {
290 chain: Chain,
291 entries: Vec<(String, Box<dyn CommitSource + Send + Sync>)>,
292 errors: Mutex<Vec<Option<RecordedError>>>,
293}
294
295impl CommitSources {
296 pub fn new(chain: Chain) -> Self {
297 Self {
298 chain,
299 entries: Vec::new(),
300 errors: Mutex::new(Vec::new()),
301 }
302 }
303
304 pub fn push(&mut self, label: String, source: Box<dyn CommitSource + Send + Sync>) {
305 self.entries.push((label, source));
306 }
307
308 pub fn is_empty(&self) -> bool {
309 self.entries.is_empty()
310 }
311
312 pub fn status(&self) -> Result<Vec<CommitSourceStatus>, Error> {
313 for (index, entry) in self.entries.iter().enumerate() {
314 if let Some(probed) = entry.1.probe() {
315 self.record(index, probed.err().map(|error| RecordedError::now(&error)))?;
316 }
317 }
318 let errors = self.errors()?;
319 Ok(self
320 .entries
321 .iter()
322 .zip(errors.iter())
323 .map(|(entry, error)| {
324 let (last_error, last_error_at) = RecordedError::split(error.clone());
325 let mut status = CommitSourceStatus::default();
326 status.label = entry.0.clone();
327 status.last_error = last_error;
328 status.last_error_at = last_error_at;
329 status
330 })
331 .collect())
332 }
333
334 pub(crate) fn source_chains(&self) -> Vec<Chain> {
335 self.entries.iter().map(|entry| entry.1.chain()).collect()
336 }
337
338 fn errors(&self) -> Result<MutexGuard<'_, Vec<Option<RecordedError>>>, Error> {
339 let mut errors = self
340 .errors
341 .lock()
342 .map_err(|_| Error::Invalid("commit source status poisoned".into()))?;
343 errors.resize(self.entries.len(), None);
344 Ok(errors)
345 }
346
347 fn verified(&self, txid: Txid, raw: Option<Vec<u8>>) -> Result<Option<Vec<u8>>, Error> {
348 let Some(raw) = raw else {
349 return Ok(None);
350 };
351 verify_parent(self.chain, txid, &raw)?;
352 Ok(Some(raw))
353 }
354
355 fn record(&self, index: usize, error: Option<RecordedError>) -> Result<(), Error> {
356 let mut errors = self.errors()?;
357 let slot = errors
358 .get_mut(index)
359 .ok_or_else(|| Error::Invalid("commit source status out of range".into()))?;
360 *slot = error;
361 Ok(())
362 }
363}
364
365impl CommitSource for CommitSources {
366 fn chain(&self) -> Chain {
367 self.chain
368 }
369
370 fn fetch(&self, txid: Txid, cancelled: &AtomicBool) -> Result<Option<Vec<u8>>, Error> {
371 let mut last = None;
372 let mut failures = 0;
373 for (index, entry) in self.entries.iter().enumerate() {
374 check_cancelled(cancelled)?;
375 let fetched = entry
376 .1
377 .fetch(txid, cancelled)
378 .and_then(|raw| self.verified(txid, raw));
379 check_cancelled(cancelled)?;
380 match fetched {
381 Ok(Some(raw)) => {
382 self.record(index, None)?;
383 return Ok(Some(raw));
384 }
385 Ok(None) => self.record(index, None)?,
386 Err(error) => {
387 self.record(index, Some(RecordedError::now(&error)))?;
388 failures += 1;
389 last = Some(error);
390 }
391 }
392 }
393 let Some(error) = last else {
394 return Ok(None);
395 };
396 if failures == self.entries.len() {
397 return Err(error);
398 }
399 Ok(None)
400 }
401}
402
403pub struct LitecoinSource {
408 chain: Chain,
409 provider: P2pProvider,
410}
411
412impl LitecoinSource {
413 pub fn open(
414 chain: Chain,
415 cache: &Path,
416 anchor: Anchor,
417 peers: Vec<SocketAddr>,
418 ) -> Result<Self, Error> {
419 if !matches!(chain, Chain::LitecoinTestnet | Chain::LitecoinMainnet) {
420 return Err(Error::Invalid(
421 "Litecoin source requires a Litecoin network".into(),
422 ));
423 }
424 Ok(Self {
425 chain,
426 provider: P2pProvider::with_peers(chain, cache, anchor, peers).map_err(source_error)?,
427 })
428 }
429
430 pub fn open_bootstrap(
436 cache: &Path,
437 bootstrap: HeaderBootstrap,
438 peers: Vec<SocketAddr>,
439 ) -> Result<Self, Error> {
440 let chain = bootstrap.chain();
441 if !matches!(chain, Chain::LitecoinTestnet | Chain::LitecoinMainnet) {
442 return Err(Error::Invalid(
443 "Litecoin source requires a Litecoin network".into(),
444 ));
445 }
446 Ok(Self {
447 chain,
448 provider: P2pProvider::with_bootstrap(cache, bootstrap, peers).map_err(source_error)?,
449 })
450 }
451}
452
453fn source_error(e: urma_runtime::error::Error) -> Error {
454 Error::Source(Box::new(e))
455}
456
457impl ChainSource for LitecoinSource {
458 fn genesis(&self) -> Result<BlockHash, Error> {
459 Ok(self
460 .chain
461 .genesis()
462 .map_err(|e| Error::Invalid(e.to_string()))?
463 .0)
464 }
465 fn tip(&self) -> Result<ChainPoint, Error> {
466 let (height, header) = self.provider.verified_tip().map_err(source_error)?;
467 Ok(ChainPoint {
468 height,
469 hash: header.block_hash(),
470 })
471 }
472 fn header(&self, height: u64) -> Result<Header, Error> {
473 self.provider.verified_header(height).map_err(source_error)
474 }
475 fn block(&self, height: u64) -> Result<Vec<u8>, Error> {
476 Ok(self
477 .provider
478 .verified_block(height)
479 .map_err(source_error)?
480 .raw()
481 .to_vec())
482 }
483 fn chain_peers(&self) -> Result<u64, Error> {
484 u64::try_from(self.provider.progress().peers_connected)
485 .map_err(|error| Error::Invalid(format!("chain peer count: {error}")))
486 }
487 fn chain_target(&self) -> Option<u64> {
488 self.provider.target_height()
489 }
490}