Skip to main content

zcash_client_sqlite/
testing.rs

1//! Test-support utilities exposed under the `test-dependencies` feature: an in-memory
2//! [`db::TestDbFactory`] / [`db::TestDb`] wallet for the `zcash_client_backend` testing framework, a
3//! [`BlockCache`] compact-block source, and the [`highest_rooted_orchard_checkpoint`] commitment-tree
4//! helper. Consumed by this crate's own tests and, through the feature, by downstream crates' tests.
5
6use prost::Message;
7use rusqlite::params;
8
9use zcash_client_backend::{
10    data_api::testing::{CacheInsertionResult, NoteCommitments, TestCache},
11    proto::compact_formats::CompactBlock,
12};
13use zcash_protocol::TxId;
14#[cfg(feature = "orchard")]
15use {
16    shardtree::{error::ShardTreeError, store::ShardStore},
17    zcash_client_backend::data_api::WalletCommitmentTrees,
18    zcash_protocol::consensus::BlockHeight,
19};
20
21use crate::{chain::init::init_cache_database, error::SqliteClientError};
22
23use super::BlockDb;
24
25#[cfg(all(test, feature = "unstable"))]
26#[cfg(all(test, feature = "unstable"))]
27use std::io::Write;
28#[cfg(all(test, feature = "unstable"))]
29use {
30    crate::{
31        FsBlockDb, FsBlockDbError,
32        chain::{BlockMeta, init::init_blockmeta_db},
33    },
34    std::fs::File,
35    tempfile::TempDir,
36};
37
38pub mod db;
39// The shielded-pool testers are used only by this crate's own in-crate tests, not by external
40// consumers of the exposed harness, so they stay test-only and keep their heavier test-only
41// dependencies (proptest, incrementalmerkletree-testing) out of the `test-dependencies` build.
42#[cfg(test)]
43pub(crate) mod pool;
44
45/// An in-memory compact-block cache backed by a temporary [`BlockDb`], implementing the
46/// `zcash_client_backend` testing framework's [`TestCache`].
47pub struct BlockCache {
48    db_cache: BlockDb,
49}
50
51impl BlockCache {
52    /// Creates an empty cache over a fresh in-memory block database.
53    pub fn new() -> Self {
54        let db_cache = BlockDb::from_connection(rusqlite::Connection::open_in_memory().unwrap());
55        init_cache_database(&db_cache).unwrap();
56
57        BlockCache { db_cache }
58    }
59}
60
61impl Default for BlockCache {
62    fn default() -> Self {
63        Self::new()
64    }
65}
66
67/// The result of inserting a compact block into a [`BlockCache`]: the block's transaction ids and
68/// the note commitments it added.
69pub struct BlockCacheInsertionResult {
70    txids: Vec<TxId>,
71    #[allow(dead_code)]
72    note_commitments: NoteCommitments,
73}
74
75impl BlockCacheInsertionResult {
76    #[allow(dead_code)]
77    pub(crate) fn note_commitments(&self) -> &NoteCommitments {
78        &self.note_commitments
79    }
80}
81
82impl CacheInsertionResult for BlockCacheInsertionResult {
83    fn txids(&self) -> &[TxId] {
84        &self.txids[..]
85    }
86}
87
88impl TestCache for BlockCache {
89    type BsError = SqliteClientError;
90    type BlockSource = BlockDb;
91    type InsertResult = BlockCacheInsertionResult;
92
93    fn block_source(&self) -> &Self::BlockSource {
94        &self.db_cache
95    }
96
97    fn insert(&mut self, cb: &CompactBlock) -> Self::InsertResult {
98        let cb_bytes = cb.encode_to_vec();
99        let note_commitments = NoteCommitments::from_compact_block(cb);
100        self.db_cache
101            .0
102            .execute(
103                "INSERT INTO compactblocks (height, data) VALUES (?, ?)",
104                params![u32::from(cb.height()), cb_bytes,],
105            )
106            .unwrap();
107
108        BlockCacheInsertionResult {
109            txids: cb.vtx.iter().map(|tx| tx.txid()).collect(),
110            note_commitments,
111        }
112    }
113
114    fn truncate_to_height(&mut self, height: zcash_protocol::consensus::BlockHeight) {
115        self.db_cache
116            .0
117            .execute(
118                "DELETE FROM compactblocks WHERE height > ?",
119                params![u32::from(height)],
120            )
121            .unwrap();
122    }
123}
124
125/// The highest checkpoint at or below `from` whose Orchard commitment-tree root is available, or
126/// `None` if there is none at or below `from`. Right after scanning, the tip checkpoint is not yet
127/// rooted, so a spend anchors to the newest settled checkpoint below it (every note mined at or
128/// before that height is still witnessable there).
129#[cfg(feature = "orchard")]
130pub fn highest_rooted_orchard_checkpoint<W>(db: &mut W, from: BlockHeight) -> Option<BlockHeight>
131where
132    W: zcash_client_backend::data_api::WalletCommitmentTrees,
133{
134    db.with_orchard_tree_mut::<_, _, ShardTreeError<<W as WalletCommitmentTrees>::Error>>(|tree| {
135        // Take the highest checkpoint id at or below `from` directly from the checkpoint set,
136        // rather than probing the tree at every height down from `from`.
137        let store = tree.store();
138        let count = store.checkpoint_count().map_err(ShardTreeError::Storage)?;
139        let mut highest: Option<BlockHeight> = None;
140        store
141            .for_each_checkpoint(count, |id, _| {
142                if *id <= from {
143                    highest = Some(highest.map_or(*id, |h| h.max(*id)));
144                }
145                Ok(())
146            })
147            .map_err(ShardTreeError::Storage)?;
148        Ok(highest)
149    })
150    .expect("queries the Orchard tree")
151}
152
153#[cfg(all(test, feature = "unstable"))]
154pub(crate) struct FsBlockCache {
155    fsblockdb_root: TempDir,
156    db_meta: FsBlockDb,
157}
158
159#[cfg(all(test, feature = "unstable"))]
160impl FsBlockCache {
161    pub(crate) fn new() -> Self {
162        let fsblockdb_root = tempfile::tempdir().unwrap();
163        let mut db_meta = FsBlockDb::for_path(&fsblockdb_root).unwrap();
164        init_blockmeta_db(&mut db_meta).unwrap();
165
166        FsBlockCache {
167            fsblockdb_root,
168            db_meta,
169        }
170    }
171}
172
173/// The result of inserting a compact block into an [`FsBlockCache`]: the block's transaction ids and
174/// its on-disk block metadata.
175#[cfg(all(test, feature = "unstable"))]
176#[derive(Debug)]
177pub struct FsBlockCacheInsertionResult {
178    txids: Vec<TxId>,
179    pub(crate) block_meta: BlockMeta,
180}
181
182#[cfg(all(test, feature = "unstable"))]
183impl CacheInsertionResult for FsBlockCacheInsertionResult {
184    fn txids(&self) -> &[TxId] {
185        &self.txids[..]
186    }
187}
188
189#[cfg(all(test, feature = "unstable"))]
190impl TestCache for FsBlockCache {
191    type BsError = FsBlockDbError;
192    type BlockSource = FsBlockDb;
193    type InsertResult = FsBlockCacheInsertionResult;
194
195    fn block_source(&self) -> &Self::BlockSource {
196        &self.db_meta
197    }
198
199    fn insert(&mut self, cb: &CompactBlock) -> Self::InsertResult {
200        let txids = cb.vtx.iter().map(|tx| tx.txid()).collect();
201        let block_meta = BlockMeta {
202            height: cb.height(),
203            block_hash: cb.hash(),
204            block_time: cb.time,
205            sapling_outputs_count: cb.vtx.iter().map(|tx| tx.outputs.len() as u32).sum(),
206            orchard_actions_count: cb.vtx.iter().map(|tx| tx.actions.len() as u32).sum(),
207        };
208
209        let blocks_dir = self.fsblockdb_root.as_ref().join("blocks");
210        let block_path = block_meta.block_file_path(&blocks_dir);
211
212        File::create(block_path)
213            .unwrap()
214            .write_all(&cb.encode_to_vec())
215            .unwrap();
216
217        FsBlockCacheInsertionResult { txids, block_meta }
218    }
219
220    fn truncate_to_height(&mut self, height: zcash_protocol::consensus::BlockHeight) {
221        self.db_meta.truncate_to_height(height).unwrap()
222    }
223}