zcash_client_sqlite/
chain.rs

1//! Functions for enforcing chain validity and handling chain reorgs.
2
3use prost::Message;
4use rusqlite::params;
5
6use zcash_protocol::consensus::BlockHeight;
7
8use zcash_client_backend::{data_api::chain::error::Error, proto::compact_formats::CompactBlock};
9
10use crate::{error::SqliteClientError, BlockDb};
11
12#[cfg(feature = "unstable")]
13use {
14    crate::{BlockHash, FsBlockDb, FsBlockDbError},
15    rusqlite::Connection,
16    std::fs::File,
17    std::io::Read,
18    std::path::{Path, PathBuf},
19};
20
21pub mod init;
22pub mod migrations;
23
24/// Implements a traversal of `limit` blocks of the block cache database.
25///
26/// Starting at `from_height`, the `with_row` callback is invoked with each block retrieved from
27/// the backing store. If the `limit` value provided is `None`, all blocks are traversed up to the
28/// maximum height.
29pub(crate) fn blockdb_with_blocks<F, DbErrT>(
30    block_source: &BlockDb,
31    from_height: Option<BlockHeight>,
32    limit: Option<usize>,
33    mut with_row: F,
34) -> Result<(), Error<DbErrT, SqliteClientError>>
35where
36    F: FnMut(CompactBlock) -> Result<(), Error<DbErrT, SqliteClientError>>,
37{
38    fn to_chain_error<D, E: Into<SqliteClientError>>(err: E) -> Error<D, SqliteClientError> {
39        Error::BlockSource(err.into())
40    }
41
42    // Fetch the CompactBlocks we need to scan
43    let mut stmt_blocks = block_source
44        .0
45        .prepare(
46            "SELECT height, data FROM compactblocks
47            WHERE height >= ?
48            ORDER BY height ASC LIMIT ?",
49        )
50        .map_err(to_chain_error)?;
51
52    let mut rows = stmt_blocks
53        .query(params![
54            from_height.map_or(0u32, u32::from),
55            limit
56                .and_then(|l| u32::try_from(l).ok())
57                .unwrap_or(u32::MAX)
58        ])
59        .map_err(to_chain_error)?;
60
61    // Only look for the `from_height` in the scanned blocks if it is set.
62    let mut from_height_found = from_height.is_none();
63    while let Some(row) = rows.next().map_err(to_chain_error)? {
64        let height = BlockHeight::from_u32(row.get(0).map_err(to_chain_error)?);
65        if !from_height_found {
66            // We will only perform this check on the first row.
67            let from_height = from_height.expect("can only reach here if set");
68            if from_height != height {
69                return Err(to_chain_error(SqliteClientError::CacheMiss(from_height)));
70            } else {
71                from_height_found = true;
72            }
73        }
74
75        let data: Vec<u8> = row.get(1).map_err(to_chain_error)?;
76        let block = CompactBlock::decode(&data[..]).map_err(to_chain_error)?;
77        if block.height() != height {
78            return Err(to_chain_error(SqliteClientError::CorruptedData(format!(
79                "Block height {} did not match row's height field value {}",
80                block.height(),
81                height
82            ))));
83        }
84
85        with_row(block)?;
86    }
87
88    if !from_height_found {
89        let from_height = from_height.expect("can only reach here if set");
90        return Err(to_chain_error(SqliteClientError::CacheMiss(from_height)));
91    }
92
93    Ok(())
94}
95
96/// Data structure representing a row in the block metadata database.
97#[cfg(feature = "unstable")]
98#[derive(Clone, Copy, Debug, PartialEq, Eq)]
99pub struct BlockMeta {
100    pub height: BlockHeight,
101    pub block_hash: BlockHash,
102    pub block_time: u32,
103    pub sapling_outputs_count: u32,
104    pub orchard_actions_count: u32,
105}
106
107#[cfg(feature = "unstable")]
108impl BlockMeta {
109    pub fn block_file_path<P: AsRef<Path>>(&self, blocks_dir: &P) -> PathBuf {
110        blocks_dir.as_ref().join(Path::new(&format!(
111            "{}-{}-compactblock",
112            self.height, self.block_hash
113        )))
114    }
115}
116
117/// Inserts a batch of rows into the block metadata database.
118#[cfg(feature = "unstable")]
119pub(crate) fn blockmetadb_insert(
120    conn: &Connection,
121    block_meta: &[BlockMeta],
122) -> Result<(), rusqlite::Error> {
123    use rusqlite::named_params;
124
125    let mut stmt_insert = conn.prepare(
126        "INSERT INTO compactblocks_meta (
127            height,
128            blockhash,
129            time,
130            sapling_outputs_count,
131            orchard_actions_count
132        )
133        VALUES (
134            :height,
135            :blockhash,
136            :time,
137            :sapling_outputs_count,
138            :orchard_actions_count
139        )
140        ON CONFLICT (height) DO UPDATE
141        SET blockhash = :blockhash,
142            time = :time,
143            sapling_outputs_count = :sapling_outputs_count,
144            orchard_actions_count = :orchard_actions_count",
145    )?;
146
147    conn.execute("BEGIN IMMEDIATE", [])?;
148    let result = block_meta
149        .iter()
150        .map(|m| {
151            stmt_insert.execute(named_params![
152                ":height": u32::from(m.height),
153                ":blockhash": &m.block_hash.0[..],
154                ":time": m.block_time,
155                ":sapling_outputs_count": m.sapling_outputs_count,
156                ":orchard_actions_count": m.orchard_actions_count,
157            ])
158        })
159        .collect::<Result<Vec<_>, _>>();
160    match result {
161        Ok(_) => {
162            conn.execute("COMMIT", [])?;
163            Ok(())
164        }
165        Err(error) => {
166            match conn.execute("ROLLBACK", []) {
167                Ok(_) => Err(error),
168                Err(e) =>
169                    // Panicking here is probably the right thing to do, because it
170                    // means the database is corrupt.
171                    panic!(
172                        "Rollback failed with error {} while attempting to recover from error {}; database is likely corrupt.",
173                        e,
174                        error
175                    )
176            }
177        }
178    }
179}
180
181#[cfg(feature = "unstable")]
182pub(crate) fn blockmetadb_truncate_to_height(
183    conn: &Connection,
184    block_height: BlockHeight,
185) -> Result<(), rusqlite::Error> {
186    conn.prepare("DELETE FROM compactblocks_meta WHERE height > ?")?
187        .execute(params![u32::from(block_height)])?;
188    Ok(())
189}
190
191#[cfg(feature = "unstable")]
192pub(crate) fn blockmetadb_get_max_cached_height(
193    conn: &Connection,
194) -> Result<Option<BlockHeight>, rusqlite::Error> {
195    conn.query_row("SELECT MAX(height) FROM compactblocks_meta", [], |row| {
196        // `SELECT MAX(_)` will always return a row, but it will return `null` if the
197        // table is empty, which has no integer type. We handle the optionality here.
198        let h: Option<u32> = row.get(0)?;
199        Ok(h.map(BlockHeight::from))
200    })
201}
202
203/// Returns the metadata for the block with the given height, if it exists in the database.
204#[cfg(feature = "unstable")]
205pub(crate) fn blockmetadb_find_block(
206    conn: &Connection,
207    height: BlockHeight,
208) -> Result<Option<BlockMeta>, rusqlite::Error> {
209    use rusqlite::OptionalExtension;
210
211    conn.query_row(
212        "SELECT blockhash, time, sapling_outputs_count, orchard_actions_count
213        FROM compactblocks_meta
214        WHERE height = ?",
215        [u32::from(height)],
216        |row| {
217            Ok(BlockMeta {
218                height,
219                block_hash: BlockHash::from_slice(&row.get::<_, Vec<_>>(0)?),
220                block_time: row.get(1)?,
221                sapling_outputs_count: row.get(2)?,
222                orchard_actions_count: row.get(3)?,
223            })
224        },
225    )
226    .optional()
227}
228
229/// Implements a traversal of `limit` blocks of the filesystem-backed
230/// block cache.
231///
232/// Starting at `from_height`, the `with_row` callback is invoked with each block retrieved from
233/// the backing store. If the `limit` value provided is `None`, all blocks are traversed up to the
234/// maximum height for which metadata is available.
235#[cfg(feature = "unstable")]
236pub(crate) fn fsblockdb_with_blocks<F, DbErrT>(
237    cache: &FsBlockDb,
238    from_height: Option<BlockHeight>,
239    limit: Option<usize>,
240    mut with_block: F,
241) -> Result<(), Error<DbErrT, FsBlockDbError>>
242where
243    F: FnMut(CompactBlock) -> Result<(), Error<DbErrT, FsBlockDbError>>,
244{
245    fn to_chain_error<D, E: Into<FsBlockDbError>>(err: E) -> Error<D, FsBlockDbError> {
246        Error::BlockSource(err.into())
247    }
248
249    // Fetch the CompactBlocks we need to scan
250    let mut stmt_blocks = cache
251        .conn
252        .prepare(
253            "SELECT height, blockhash, time, sapling_outputs_count, orchard_actions_count
254             FROM compactblocks_meta
255             WHERE height >= ?
256             ORDER BY height ASC LIMIT ?",
257        )
258        .map_err(to_chain_error)?;
259
260    let rows = stmt_blocks
261        .query_map(
262            params![
263                from_height.map_or(0u32, u32::from),
264                limit
265                    .and_then(|l| u32::try_from(l).ok())
266                    .unwrap_or(u32::MAX)
267            ],
268            |row| {
269                Ok(BlockMeta {
270                    height: BlockHeight::from_u32(row.get(0)?),
271                    block_hash: BlockHash::from_slice(&row.get::<_, Vec<_>>(1)?),
272                    block_time: row.get(2)?,
273                    sapling_outputs_count: row.get(3)?,
274                    orchard_actions_count: row.get(4)?,
275                })
276            },
277        )
278        .map_err(to_chain_error)?;
279
280    // Only look for the `from_height` in the scanned blocks if it is set.
281    let mut from_height_found = from_height.is_none();
282    for row_result in rows {
283        let cbr = row_result.map_err(to_chain_error)?;
284        if !from_height_found {
285            // We will only perform this check on the first row.
286            let from_height = from_height.expect("can only reach here if set");
287            if from_height != cbr.height {
288                return Err(to_chain_error(FsBlockDbError::CacheMiss(from_height)));
289            } else {
290                from_height_found = true;
291            }
292        }
293
294        let mut block_file =
295            File::open(cbr.block_file_path(&cache.blocks_dir)).map_err(to_chain_error)?;
296        let mut block_data = vec![];
297        block_file
298            .read_to_end(&mut block_data)
299            .map_err(to_chain_error)?;
300
301        let block = CompactBlock::decode(&block_data[..]).map_err(to_chain_error)?;
302
303        if block.height() != cbr.height {
304            return Err(to_chain_error(FsBlockDbError::CorruptedData(format!(
305                "Block height {} did not match row's height field value {}",
306                block.height(),
307                cbr.height
308            ))));
309        }
310
311        with_block(block)?;
312    }
313
314    if !from_height_found {
315        let from_height = from_height.expect("can only reach here if set");
316        return Err(to_chain_error(FsBlockDbError::CacheMiss(from_height)));
317    }
318
319    Ok(())
320}
321
322#[cfg(test)]
323mod tests {
324    use zcash_client_backend::data_api::testing::sapling::SaplingPoolTester;
325
326    use crate::testing;
327
328    #[cfg(feature = "orchard")]
329    use zcash_client_backend::data_api::testing::orchard::OrchardPoolTester;
330
331    #[test]
332    fn valid_chain_states_sapling() {
333        testing::pool::valid_chain_states::<SaplingPoolTester>()
334    }
335
336    #[test]
337    #[cfg(feature = "orchard")]
338    fn valid_chain_states_orchard() {
339        testing::pool::valid_chain_states::<OrchardPoolTester>()
340    }
341
342    #[test]
343    #[cfg(feature = "orchard")]
344    fn invalid_chain_cache_disconnected_sapling() {
345        testing::pool::invalid_chain_cache_disconnected::<SaplingPoolTester>()
346    }
347
348    #[test]
349    #[cfg(feature = "orchard")]
350    fn invalid_chain_cache_disconnected_orchard() {
351        testing::pool::invalid_chain_cache_disconnected::<OrchardPoolTester>()
352    }
353
354    #[test]
355    fn data_db_truncation_sapling() {
356        testing::pool::data_db_truncation::<SaplingPoolTester>()
357    }
358
359    #[test]
360    #[cfg(feature = "orchard")]
361    fn data_db_truncation_orchard() {
362        testing::pool::data_db_truncation::<OrchardPoolTester>()
363    }
364
365    #[test]
366    fn reorg_to_checkpoint_sapling() {
367        testing::pool::reorg_to_checkpoint::<SaplingPoolTester>()
368    }
369
370    #[test]
371    #[cfg(feature = "orchard")]
372    fn reorg_to_checkpoint_orchard() {
373        testing::pool::reorg_to_checkpoint::<OrchardPoolTester>()
374    }
375
376    #[test]
377    fn scan_cached_blocks_allows_blocks_out_of_order_sapling() {
378        testing::pool::scan_cached_blocks_allows_blocks_out_of_order::<SaplingPoolTester>()
379    }
380
381    #[test]
382    #[cfg(feature = "orchard")]
383    fn scan_cached_blocks_allows_blocks_out_of_order_orchard() {
384        testing::pool::scan_cached_blocks_allows_blocks_out_of_order::<OrchardPoolTester>()
385    }
386
387    #[test]
388    fn scan_cached_blocks_finds_received_notes_sapling() {
389        testing::pool::scan_cached_blocks_finds_received_notes::<SaplingPoolTester>()
390    }
391
392    #[test]
393    #[cfg(feature = "orchard")]
394    fn scan_cached_blocks_finds_received_notes_orchard() {
395        testing::pool::scan_cached_blocks_finds_received_notes::<OrchardPoolTester>()
396    }
397
398    #[test]
399    fn scan_cached_blocks_finds_change_notes_sapling() {
400        testing::pool::scan_cached_blocks_finds_change_notes::<SaplingPoolTester>()
401    }
402
403    #[test]
404    #[cfg(feature = "orchard")]
405    fn scan_cached_blocks_finds_change_notes_orchard() {
406        testing::pool::scan_cached_blocks_finds_change_notes::<OrchardPoolTester>()
407    }
408
409    #[test]
410    fn scan_cached_blocks_detects_spends_out_of_order_sapling() {
411        testing::pool::scan_cached_blocks_detects_spends_out_of_order::<SaplingPoolTester>()
412    }
413
414    #[test]
415    #[cfg(feature = "orchard")]
416    fn scan_cached_blocks_detects_spends_out_of_order_orchard() {
417        testing::pool::scan_cached_blocks_detects_spends_out_of_order::<OrchardPoolTester>()
418    }
419}