Skip to main content

zcash_client_backend/
sync.rs

1//! Implementation of the synchronization flow described in the crate root.
2//!
3//! This is currently a simple implementation that does not yet implement a few features:
4//!
5//! - Block batches are not downloaded in parallel with scanning.
6//! - Transactions are not enhanced once detected (that is, after an output is detected in
7//!   a transaction, the full transaction is not downloaded and scanned).
8//! - There is no mechanism for notifying the caller of progress updates.
9//! - There is no mechanism for interrupting the synchronization flow, other than ending
10//!   the process.
11
12#[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/// Scans the chain until the wallet is up-to-date.
59#[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    // 1) Download note commitment tree data from lightwalletd
81    // 2) Pass the commitment tree data to the database.
82    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    // 3) Download chain tip metadata from lightwalletd
110    // 4) Notify the wallet of the updated chain tip.
111    update_chain_tip(client, db_data).await?;
112
113    // Refresh UTXOs for the accounts in the wallet. We do this before we perform
114    // any shielded scanning, to ensure that we discover any UTXOs between the old
115    // fully-scanned height and the current chain tip.
116    #[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    // 5) Get the suggested scan ranges from the wallet database
129    let mut scan_ranges = db_data.suggest_scan_ranges().map_err(Error::Wallet)?;
130
131    // Store the handles to cached block deletions (which we spawn into separate
132    // tasks to allow us to continue downloading and scanning other ranges).
133    let mut block_deletions = vec![];
134
135    // 6) Run the following loop until the wallet's view of the chain tip as of
136    //    the previous wallet session is valid.
137    loop {
138        // If there is a range of blocks that needs to be verified, it will always
139        // be returned as the first element of the vector of suggested ranges.
140        match scan_ranges.first() {
141            Some(scan_range) if scan_range.priority() == ScanPriority::Verify => {
142                // Download the blocks in `scan_range` into the block source,
143                // overwriting any existing blocks in this range.
144                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                // Scan the downloaded blocks and check for scanning errors that
150                // indicate the wallet's chain tip is out of sync with blockchain
151                // history.
152                let scan_ranges_updated =
153                    scan_blocks(params, db_cache, db_data, &chain_state, scan_range).await?;
154
155                // Delete the now-scanned blocks, because keeping the entire chain
156                // in CompactBlock files on disk is horrendous for the filesystem.
157                block_deletions.push(db_cache.delete(scan_range.clone()));
158
159                if scan_ranges_updated {
160                    // The suggested scan ranges have been updated, so we re-request.
161                    scan_ranges = db_data.suggest_scan_ranges().map_err(Error::Wallet)?;
162                } else {
163                    // At this point, the cache and scanned data are locally
164                    // consistent (though not necessarily consistent with the
165                    // latest chain tip - this would be discovered the next time
166                    // this codepath is executed after new blocks are received) so
167                    // we can break out of the loop.
168                    break;
169                }
170            }
171            _ => {
172                // Nothing to verify; break out of the loop
173                break;
174            }
175        }
176    }
177
178    // 7) Loop over the remaining suggested scan ranges, retrieving the requested data
179    //    and calling `scan_cached_blocks` on each range.
180    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        // Limit the number of blocks we download and scan at any one time.
184        (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 the blocks in `scan_range` into the block source.
199        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        // Scan the downloaded blocks.
204        let scan_ranges_updated =
205            scan_blocks(params, db_cache, db_data, &chain_state, &scan_range).await?;
206
207        // Delete the now-scanned blocks.
208        block_deletions.push(db_cache.delete(scan_range));
209
210        if scan_ranges_updated {
211            // The suggested scan ranges have been updated (either due to a continuity
212            // error or because a higher priority range has been added).
213            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/// Scans the given block range and checks for scanning errors that indicate the wallet's
394/// chain tip is out of sync with blockchain history.
395///
396/// Returns `true` if scanning these blocks materially changed the suggested scan ranges.
397#[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            // Pick a height to rewind to, which must be at least one block before the
426            // height at which the error occurred, but may be an earlier height determined
427            // based on heuristics such as the platform, available bandwidth, size of
428            // recent CompactBlocks, etc.
429            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            // Rewind to the chosen height.
437            db_data
438                .truncate_to_height(rewind_height)
439                .map_err(Error::Wallet)?;
440
441            // Delete cached blocks from rewind_height onwards.
442            //
443            // This does imply that assumed-valid blocks will be re-downloaded, but it is
444            // also possible that in the intervening time, a chain reorg has occurred that
445            // orphaned some of those blocks.
446            db_cache
447                .truncate(rewind_height)
448                .await
449                .map_err(Error::Cache)?;
450
451            // The database was truncated, invalidating prior suggested ranges.
452            Ok(true)
453        }
454        Ok(_) => {
455            // If scanning these blocks caused a suggested range to be added that has a
456            // higher priority than the current range, invalidate the current ranges.
457            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/// Refreshes the given account's view of UTXOs that exist starting at the given height.
470///
471/// ## Note about UTXO tracking
472///
473/// (Extracted from [a comment in the Android SDK].)
474///
475/// We no longer clear UTXOs here, as `WalletDb::put_received_transparent_utxo` now uses
476/// an upsert instead of an insert. This means that now-spent UTXOs would previously have
477/// been deleted, but now are left in the database (like shielded notes).
478///
479/// Due to the fact that the `lightwalletd` query only returns _current_ UTXOs, we don't
480/// learn about recently-spent UTXOs here, so the transparent balance does not get updated
481/// here.
482///
483/// Instead, when a received shielded note is "enhanced" by downloading the full
484/// transaction, we mark any UTXOs spent in that transaction as spent in the database.
485/// This relies on two current properties:
486/// - UTXOs are only ever spent in shielding transactions.
487/// - At least one shielded note from each shielding transaction is always enhanced.
488///
489/// However, for greater reliability, we may want to alter the Data Access API to support
490/// "inferring spentness" from what is _not_ returned as a UTXO, or alternatively fetch
491/// TXOs from `lightwalletd` instead of just UTXOs.
492///
493/// [a comment in the Android SDK]: https://github.com/Electric-Coin-Company/zcash-android-wallet-sdk/blob/855204fc8ae4057fdac939f98df4aa38c8e662f1/sdk-lib/src/main/java/cash/z/ecc/android/sdk/block/processor/CompactBlockProcessor.kt#L979-L991
494#[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/// Errors that can occur while syncing.
567#[cfg(feature = "sync")]
568#[derive(Debug)]
569#[non_exhaustive]
570pub enum Error<CaErr, DbErr, TrErr> {
571    /// An error while interacting with a [`BlockCache`].
572    Cache(CaErr),
573    /// The lightwalletd server returned invalid information, and is misbehaving.
574    MisbehavingServer,
575    /// An error while scanning blocks.
576    Scan(ScanError),
577    /// An error while communicating with the lightwalletd server.
578    Server(tonic::Status),
579    /// An error while interacting with a wallet database via [`WalletRead`] or
580    /// [`WalletWrite`].
581    Wallet(DbErr),
582    /// An error while interacting with a wallet database via [`WalletCommitmentTrees`].
583    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}