1#[cfg(feature = "sync")]
13use {
14 crate::{
15 data_api::{
16 WalletCommitmentTrees, WalletRead, WalletWrite,
17 chain::{
18 BlockCache, ChainState, CommitmentTreeRoot, error::Error as ChainError,
19 scan_cached_blocks,
20 },
21 scanning::{ScanPriority, ScanRange},
22 },
23 proto::service::{self, BlockId, compact_tx_streamer_client::CompactTxStreamerClient},
24 scanning::ScanError,
25 },
26 futures_util::TryStreamExt,
27 shardtree::error::ShardTreeError,
28 std::fmt,
29 subtle::ConditionallySelectable,
30 tonic::{
31 body::Body as TonicBody,
32 client::GrpcService,
33 codegen::{Body, Bytes, StdError},
34 },
35 tracing::{debug, info},
36 zcash_primitives::merkle_tree::HashSer,
37 zcash_protocol::consensus::{BlockHeight, Parameters},
38};
39
40#[cfg(all(feature = "sync", feature = "orchard"))]
41use orchard::tree::MerkleHashOrchard;
42
43#[cfg(all(feature = "sync", feature = "transparent-inputs"))]
44use {
45 crate::wallet::WalletTransparentOutput,
46 ::transparent::{
47 address::Script,
48 bundle::{OutPoint, TxOut},
49 },
50 zcash_keys::encoding::AddressCodec as _,
51 zcash_protocol::value::Zatoshis,
52 zcash_script::script,
53};
54
55#[cfg(feature = "sync-decryptor")]
56pub mod decryptor;
57
58#[cfg(feature = "sync")]
60pub async fn run<P, ChT, CaT, DbT>(
61 client: &mut CompactTxStreamerClient<ChT>,
62 params: &P,
63 db_cache: &CaT,
64 db_data: &mut DbT,
65 batch_size: u32,
66) -> Result<(), Error<CaT::Error, <DbT as WalletRead>::Error, <DbT as WalletCommitmentTrees>::Error>>
67where
68 P: Parameters + Send + 'static,
69 ChT: GrpcService<TonicBody>,
70 ChT::Error: Into<StdError>,
71 ChT::ResponseBody: Body<Data = Bytes> + Send + 'static,
72 <ChT::ResponseBody as Body>::Error: Into<StdError> + Send,
73 CaT: BlockCache,
74 CaT::Error: std::error::Error + Send + Sync + 'static,
75 DbT: WalletWrite + WalletCommitmentTrees,
76 <DbT as WalletRead>::AccountId: ConditionallySelectable + Default + Send + Sync + 'static,
77 <DbT as WalletRead>::Error: std::error::Error + Send + Sync + 'static,
78 <DbT as WalletCommitmentTrees>::Error: std::error::Error + Send + Sync + 'static,
79{
80 update_subtree_roots(client, db_data).await?;
83
84 while running(client, params, db_cache, db_data, batch_size).await? {}
85
86 Ok(())
87}
88
89#[cfg(feature = "sync")]
90async fn running<P, ChT, CaT, DbT, TrErr>(
91 client: &mut CompactTxStreamerClient<ChT>,
92 params: &P,
93 db_cache: &CaT,
94 db_data: &mut DbT,
95 batch_size: u32,
96) -> Result<bool, Error<CaT::Error, <DbT as WalletRead>::Error, TrErr>>
97where
98 P: Parameters + Send + 'static,
99 ChT: GrpcService<TonicBody>,
100 ChT::Error: Into<StdError>,
101 ChT::ResponseBody: Body<Data = Bytes> + Send + 'static,
102 <ChT::ResponseBody as Body>::Error: Into<StdError> + Send,
103 CaT: BlockCache,
104 CaT::Error: std::error::Error + Send + Sync + 'static,
105 DbT: WalletWrite,
106 <DbT as WalletRead>::AccountId: ConditionallySelectable + Default + Send + Sync + 'static,
107 <DbT as WalletRead>::Error: std::error::Error + Send + Sync + 'static,
108{
109 update_chain_tip(client, db_data).await?;
112
113 #[cfg(feature = "transparent-inputs")]
117 for account_id in db_data.get_account_ids().map_err(Error::Wallet)? {
118 let start_height = db_data
119 .utxo_query_height(account_id)
120 .map_err(Error::Wallet)?;
121 info!(
122 "Refreshing UTXOs for {:?} from height {}",
123 account_id, start_height,
124 );
125 refresh_utxos(params, client, db_data, account_id, start_height).await?;
126 }
127
128 let mut scan_ranges = db_data.suggest_scan_ranges().map_err(Error::Wallet)?;
130
131 let mut block_deletions = vec![];
134
135 loop {
138 match scan_ranges.first() {
141 Some(scan_range) if scan_range.priority() == ScanPriority::Verify => {
142 download_blocks(client, db_cache, scan_range).await?;
145
146 let chain_state =
147 download_chain_state(client, scan_range.block_range().start - 1).await?;
148
149 let scan_ranges_updated =
153 scan_blocks(params, db_cache, db_data, &chain_state, scan_range).await?;
154
155 block_deletions.push(db_cache.delete(scan_range.clone()));
158
159 if scan_ranges_updated {
160 scan_ranges = db_data.suggest_scan_ranges().map_err(Error::Wallet)?;
162 } else {
163 break;
169 }
170 }
171 _ => {
172 break;
174 }
175 }
176 }
177
178 let scan_ranges = db_data.suggest_scan_ranges().map_err(Error::Wallet)?;
181 debug!("Suggested ranges: {:?}", scan_ranges);
182 for scan_range in scan_ranges.into_iter().flat_map(|r| {
183 (0..).scan(r, |acc, _| {
185 if acc.is_empty() {
186 None
187 } else if let Some((cur, next)) = acc.split_at(acc.block_range().start + batch_size) {
188 *acc = next;
189 Some(cur)
190 } else {
191 let cur = acc.clone();
192 let end = acc.block_range().end;
193 *acc = ScanRange::from_parts(end..end, acc.priority());
194 Some(cur)
195 }
196 })
197 }) {
198 download_blocks(client, db_cache, &scan_range).await?;
200
201 let chain_state = download_chain_state(client, scan_range.block_range().start - 1).await?;
202
203 let scan_ranges_updated =
205 scan_blocks(params, db_cache, db_data, &chain_state, &scan_range).await?;
206
207 block_deletions.push(db_cache.delete(scan_range));
209
210 if scan_ranges_updated {
211 info!("Waiting for cached blocks to be deleted...");
214 for deletion in block_deletions {
215 deletion.await.map_err(Error::Cache)?;
216 }
217 return Ok(true);
218 }
219 }
220
221 info!("Waiting for cached blocks to be deleted...");
222 for deletion in block_deletions {
223 deletion.await.map_err(Error::Cache)?;
224 }
225 Ok(false)
226}
227
228#[cfg(feature = "sync")]
229async fn download_subtree_roots<ChT, H>(
230 client: &mut CompactTxStreamerClient<ChT>,
231 protocol: service::ShieldedProtocol,
232) -> Result<Vec<CommitmentTreeRoot<H>>, tonic::Status>
233where
234 ChT: GrpcService<TonicBody>,
235 ChT::Error: Into<StdError>,
236 ChT::ResponseBody: Body<Data = Bytes> + Send + 'static,
237 <ChT::ResponseBody as Body>::Error: Into<StdError> + Send,
238 H: HashSer,
239{
240 let mut request = service::GetSubtreeRootsArg::default();
241 request.set_shielded_protocol(protocol);
242
243 client
244 .get_subtree_roots(request)
245 .await?
246 .into_inner()
247 .and_then(|root| async move {
248 let root_hash = H::read(&root.root_hash[..])?;
249 Ok(CommitmentTreeRoot::from_parts(
250 BlockHeight::from_u32(root.completing_block_height as u32),
251 root_hash,
252 ))
253 })
254 .try_collect()
255 .await
256}
257
258#[cfg(feature = "sync")]
259async fn update_subtree_roots<ChT, DbT, CaErr, DbErr>(
260 client: &mut CompactTxStreamerClient<ChT>,
261 db_data: &mut DbT,
262) -> Result<(), Error<CaErr, DbErr, <DbT as WalletCommitmentTrees>::Error>>
263where
264 ChT: GrpcService<TonicBody>,
265 ChT::Error: Into<StdError>,
266 ChT::ResponseBody: Body<Data = Bytes> + Send + 'static,
267 <ChT::ResponseBody as Body>::Error: Into<StdError> + Send,
268 DbT: WalletCommitmentTrees,
269 <DbT as WalletCommitmentTrees>::Error: std::error::Error + Send + Sync + 'static,
270{
271 let sapling_roots: Vec<CommitmentTreeRoot<sapling::Node>> =
272 download_subtree_roots(client, service::ShieldedProtocol::Sapling).await?;
273
274 info!("Sapling tree has {} subtrees", sapling_roots.len());
275 db_data
276 .put_sapling_subtree_roots(0, &sapling_roots)
277 .map_err(Error::WalletTrees)?;
278
279 #[cfg(feature = "orchard")]
280 {
281 let orchard_roots: Vec<CommitmentTreeRoot<MerkleHashOrchard>> =
282 download_subtree_roots(client, service::ShieldedProtocol::Orchard).await?;
283
284 info!("Orchard tree has {} subtrees", orchard_roots.len());
285 db_data
286 .put_orchard_subtree_roots(0, &orchard_roots)
287 .map_err(Error::WalletTrees)?;
288
289 let ironwood_roots: Vec<CommitmentTreeRoot<MerkleHashOrchard>> =
290 download_subtree_roots(client, service::ShieldedProtocol::Ironwood).await?;
291
292 info!("Ironwood tree has {} subtrees", ironwood_roots.len());
293 db_data
294 .put_ironwood_subtree_roots(0, &ironwood_roots)
295 .map_err(Error::WalletTrees)?;
296 }
297
298 Ok(())
299}
300
301#[cfg(feature = "sync")]
302async fn update_chain_tip<ChT, DbT, CaErr, TrErr>(
303 client: &mut CompactTxStreamerClient<ChT>,
304 db_data: &mut DbT,
305) -> Result<(), Error<CaErr, <DbT as WalletRead>::Error, TrErr>>
306where
307 ChT: GrpcService<TonicBody>,
308 ChT::Error: Into<StdError>,
309 ChT::ResponseBody: Body<Data = Bytes> + Send + 'static,
310 <ChT::ResponseBody as Body>::Error: Into<StdError> + Send,
311 DbT: WalletWrite,
312 <DbT as WalletRead>::Error: std::error::Error + Send + Sync + 'static,
313{
314 let tip_height: BlockHeight = client
315 .get_latest_block(service::ChainSpec::default())
316 .await?
317 .get_ref()
318 .height
319 .try_into()
320 .map_err(|_| Error::MisbehavingServer)?;
321
322 info!("Latest block height is {}", tip_height);
323 db_data
324 .update_chain_tip(tip_height)
325 .map_err(Error::Wallet)?;
326
327 Ok(())
328}
329
330#[cfg(feature = "sync")]
331async fn download_blocks<ChT, CaT, DbErr, TrErr>(
332 client: &mut CompactTxStreamerClient<ChT>,
333 db_cache: &CaT,
334 scan_range: &ScanRange,
335) -> Result<(), Error<CaT::Error, DbErr, TrErr>>
336where
337 ChT: GrpcService<TonicBody>,
338 ChT::Error: Into<StdError>,
339 ChT::ResponseBody: Body<Data = Bytes> + Send + 'static,
340 <ChT::ResponseBody as Body>::Error: Into<StdError> + Send,
341 CaT: BlockCache,
342 CaT::Error: std::error::Error + Send + Sync + 'static,
343{
344 info!("Fetching {}", scan_range);
345 let mut start = service::BlockId::default();
346 start.height = scan_range.block_range().start.into();
347 let mut end = service::BlockId::default();
348 end.height = (scan_range.block_range().end - 1).into();
349 let range = service::BlockRange {
350 start: Some(start),
351 end: Some(end),
352 pool_types: vec![],
353 };
354 let compact_blocks = client
355 .get_block_range(range)
356 .await?
357 .into_inner()
358 .try_collect::<Vec<_>>()
359 .await?;
360
361 db_cache
362 .insert(compact_blocks)
363 .await
364 .map_err(Error::Cache)?;
365
366 Ok(())
367}
368
369#[cfg(feature = "sync")]
370async fn download_chain_state<ChT, CaErr, DbErr, TrErr>(
371 client: &mut CompactTxStreamerClient<ChT>,
372 block_height: BlockHeight,
373) -> Result<ChainState, Error<CaErr, DbErr, TrErr>>
374where
375 ChT: GrpcService<TonicBody>,
376 ChT::Error: Into<StdError>,
377 ChT::ResponseBody: Body<Data = Bytes> + Send + 'static,
378 <ChT::ResponseBody as Body>::Error: Into<StdError> + Send,
379{
380 let tree_state = client
381 .get_tree_state(BlockId {
382 height: block_height.into(),
383 hash: vec![],
384 })
385 .await?;
386
387 tree_state
388 .into_inner()
389 .to_chain_state()
390 .map_err(|_| Error::MisbehavingServer)
391}
392
393#[cfg(feature = "sync")]
398async fn scan_blocks<P, CaT, DbT, TrErr>(
399 params: &P,
400 db_cache: &CaT,
401 db_data: &mut DbT,
402 initial_chain_state: &ChainState,
403 scan_range: &ScanRange,
404) -> Result<bool, Error<CaT::Error, <DbT as WalletRead>::Error, TrErr>>
405where
406 P: Parameters + Send + 'static,
407 CaT: BlockCache,
408 CaT::Error: std::error::Error + Send + Sync + 'static,
409 DbT: WalletWrite,
410 <DbT as WalletRead>::AccountId: ConditionallySelectable + Default + Send + Sync + 'static,
411 <DbT as WalletRead>::Error: std::error::Error + Send + Sync + 'static,
412{
413 info!("Scanning {}", scan_range);
414 let scan_result = scan_cached_blocks(
415 params,
416 db_cache,
417 db_data,
418 scan_range.block_range().start,
419 initial_chain_state,
420 scan_range.len(),
421 );
422
423 match scan_result {
424 Err(ChainError::Scan(err)) if err.is_continuity_error() => {
425 let rewind_height = err.at_height().saturating_sub(10);
430 info!(
431 "Chain reorg detected at {}, rewinding to {}",
432 err.at_height(),
433 rewind_height,
434 );
435
436 db_data
438 .truncate_to_height(rewind_height)
439 .map_err(Error::Wallet)?;
440
441 db_cache
447 .truncate(rewind_height)
448 .await
449 .map_err(Error::Cache)?;
450
451 Ok(true)
453 }
454 Ok(_) => {
455 let latest_ranges = db_data.suggest_scan_ranges().map_err(Error::Wallet)?;
458
459 Ok(if let Some(range) = latest_ranges.first() {
460 range.priority() > scan_range.priority()
461 } else {
462 false
463 })
464 }
465 Err(e) => Err(e.into()),
466 }
467}
468
469#[cfg(all(feature = "sync", feature = "transparent-inputs"))]
495async fn refresh_utxos<P, ChT, DbT, CaErr, TrErr>(
496 params: &P,
497 client: &mut CompactTxStreamerClient<ChT>,
498 db_data: &mut DbT,
499 account_id: <DbT as WalletRead>::AccountId,
500 start_height: BlockHeight,
501) -> Result<(), Error<CaErr, <DbT as WalletRead>::Error, TrErr>>
502where
503 P: Parameters + Send + 'static,
504 ChT: GrpcService<TonicBody>,
505 ChT::Error: Into<StdError>,
506 ChT::ResponseBody: Body<Data = Bytes> + Send + 'static,
507 <ChT::ResponseBody as Body>::Error: Into<StdError> + Send,
508 DbT: WalletWrite,
509 <DbT as WalletRead>::Error: std::error::Error + Send + Sync + 'static,
510{
511 let request = service::GetAddressUtxosArg {
512 addresses: db_data
513 .get_transparent_receivers(account_id, true, true)
514 .map_err(Error::Wallet)?
515 .into_keys()
516 .map(|addr| addr.encode(params))
517 .collect(),
518 start_height: start_height.into(),
519 max_entries: 0,
520 };
521
522 if request.addresses.is_empty() {
523 info!("{:?} has no transparent receivers", account_id);
524 } else {
525 client
526 .get_address_utxos_stream(request)
527 .await?
528 .into_inner()
529 .map_err(Error::Server)
530 .and_then(|reply| async move {
531 WalletTransparentOutput::from_parts(
532 OutPoint::new(
533 reply.txid[..]
534 .try_into()
535 .map_err(|_| Error::MisbehavingServer)?,
536 reply
537 .index
538 .try_into()
539 .map_err(|_| Error::MisbehavingServer)?,
540 ),
541 TxOut::new(
542 Zatoshis::from_nonnegative_i64(reply.value_zat)
543 .map_err(|_| Error::MisbehavingServer)?,
544 Script(script::Code(reply.script)),
545 ),
546 Some(
547 BlockHeight::try_from(reply.height)
548 .map_err(|_| Error::MisbehavingServer)?,
549 ),
550 Some(account_id),
551 None,
552 None,
553 )
554 .ok_or(Error::MisbehavingServer)
555 })
556 .try_for_each(|output| {
557 let res = db_data.put_received_transparent_utxo(&output).map(|_| ());
558 async move { res.map_err(Error::Wallet) }
559 })
560 .await?;
561 }
562
563 Ok(())
564}
565
566#[cfg(feature = "sync")]
568#[derive(Debug)]
569#[non_exhaustive]
570pub enum Error<CaErr, DbErr, TrErr> {
571 Cache(CaErr),
573 MisbehavingServer,
575 Scan(ScanError),
577 Server(tonic::Status),
579 Wallet(DbErr),
582 WalletTrees(ShardTreeError<TrErr>),
584}
585
586#[cfg(feature = "sync")]
587impl<CaErr, DbErr, TrErr> fmt::Display for Error<CaErr, DbErr, TrErr>
588where
589 CaErr: fmt::Display,
590 DbErr: fmt::Display,
591 TrErr: fmt::Display,
592{
593 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
594 match self {
595 Error::Cache(e) => write!(f, "Error while interacting with block cache: {e}"),
596 Error::MisbehavingServer => write!(f, "lightwalletd server is misbehaving"),
597 Error::Scan(e) => write!(f, "Error while scanning blocks: {e}"),
598 Error::Server(e) => {
599 write!(f, "Error while communicating with lightwalletd server: {e}")
600 }
601 Error::Wallet(e) => write!(f, "Error while interacting with wallet database: {e}"),
602 Error::WalletTrees(e) => write!(
603 f,
604 "Error while interacting with wallet commitment trees: {e}"
605 ),
606 }
607 }
608}
609
610#[cfg(feature = "sync")]
611impl<CaErr, DbErr, TrErr> std::error::Error for Error<CaErr, DbErr, TrErr>
612where
613 CaErr: std::error::Error,
614 DbErr: std::error::Error,
615 TrErr: std::error::Error,
616{
617}
618
619#[cfg(feature = "sync")]
620impl<CaErr, DbErr, TrErr> From<ChainError<DbErr, CaErr>> for Error<CaErr, DbErr, TrErr> {
621 fn from(e: ChainError<DbErr, CaErr>) -> Self {
622 match e {
623 ChainError::Wallet(e) => Error::Wallet(e),
624 ChainError::BlockSource(e) => Error::Cache(e),
625 ChainError::Scan(e) => Error::Scan(e),
626 }
627 }
628}
629
630#[cfg(feature = "sync")]
631impl<CaErr, DbErr, TrErr> From<tonic::Status> for Error<CaErr, DbErr, TrErr> {
632 fn from(status: tonic::Status) -> Self {
633 Error::Server(status)
634 }
635}
636
637#[cfg(all(test, feature = "sync", feature = "orchard"))]
638mod tests {
639 use std::{
640 convert::Infallible,
641 future::{Ready, ready},
642 sync::{
643 Arc,
644 atomic::{AtomicUsize, Ordering},
645 },
646 task::{Context, Poll},
647 };
648
649 use tonic::{
650 body::Body as TonicBody,
651 codegen::{Service, http::Request, http::Response},
652 };
653 use zcash_protocol::consensus::Network;
654
655 use crate::{
656 data_api::testing::MockWalletDb,
657 proto::service::compact_tx_streamer_client::CompactTxStreamerClient,
658 };
659
660 use super::update_subtree_roots;
661
662 #[derive(Clone)]
663 struct CountingGrpcService(Arc<AtomicUsize>);
664
665 impl Service<Request<TonicBody>> for CountingGrpcService {
666 type Response = Response<TonicBody>;
667 type Error = Infallible;
668 type Future = Ready<Result<Self::Response, Self::Error>>;
669
670 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
671 Poll::Ready(Ok(()))
672 }
673
674 fn call(&mut self, _request: Request<TonicBody>) -> Self::Future {
675 self.0.fetch_add(1, Ordering::Relaxed);
676 ready(Ok(Response::builder()
677 .header("content-type", "application/grpc")
678 .header("grpc-status", "0")
679 .body(TonicBody::empty())
680 .unwrap()))
681 }
682 }
683
684 #[test]
685 fn subtree_root_sync_requests_all_pools() {
686 let request_count = Arc::new(AtomicUsize::new(0));
687 let mut client =
688 CompactTxStreamerClient::new(CountingGrpcService(Arc::clone(&request_count)));
689 let mut wallet = MockWalletDb::new(Network::TestNetwork);
690
691 tokio::runtime::Runtime::new().unwrap().block_on(async {
692 update_subtree_roots::<_, _, Infallible, Infallible>(&mut client, &mut wallet)
693 .await
694 .unwrap();
695 });
696
697 assert_eq!(request_count.load(Ordering::Relaxed), 3);
698 }
699}