Skip to main content

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::{BlockDb, error::SqliteClientError};
11
12#[cfg(feature = "unstable")]
13use {
14    crate::{BlockHash, FsBlockDb, FsBlockDbError},
15    rusqlite::{Connection, OptionalExtension, named_params},
16    std::{
17        fs::File,
18        io::Read,
19        path::{Path, PathBuf},
20    },
21};
22
23pub mod init;
24pub mod migrations;
25
26/// Implements a traversal of `limit` blocks of the block cache database.
27///
28/// Starting at `from_height`, the `with_row` callback is invoked with each block retrieved from
29/// the backing store. If the `limit` value provided is `None`, all blocks are traversed up to the
30/// maximum height.
31pub(crate) fn blockdb_with_blocks<F, DbErrT>(
32    block_source: &BlockDb,
33    from_height: Option<BlockHeight>,
34    limit: Option<usize>,
35    mut with_row: F,
36) -> Result<(), Error<DbErrT, SqliteClientError>>
37where
38    F: FnMut(CompactBlock) -> Result<(), Error<DbErrT, SqliteClientError>>,
39{
40    fn to_chain_error<D, E: Into<SqliteClientError>>(err: E) -> Error<D, SqliteClientError> {
41        Error::BlockSource(err.into())
42    }
43
44    // Fetch the CompactBlocks we need to scan
45    let mut stmt_blocks = block_source
46        .0
47        .prepare(
48            "SELECT height, data FROM compactblocks
49            WHERE height >= ?
50            ORDER BY height ASC LIMIT ?",
51        )
52        .map_err(to_chain_error)?;
53
54    let mut rows = stmt_blocks
55        .query(params![
56            from_height.map_or(0u32, u32::from),
57            limit
58                .and_then(|l| u32::try_from(l).ok())
59                .unwrap_or(u32::MAX)
60        ])
61        .map_err(to_chain_error)?;
62
63    // Only look for the `from_height` in the scanned blocks if it is set.
64    let mut from_height_found = from_height.is_none();
65    while let Some(row) = rows.next().map_err(to_chain_error)? {
66        let height = BlockHeight::from_u32(row.get(0).map_err(to_chain_error)?);
67        if !from_height_found {
68            // We will only perform this check on the first row.
69            let from_height = from_height.expect("can only reach here if set");
70            if from_height != height {
71                return Err(to_chain_error(SqliteClientError::CacheMiss(from_height)));
72            } else {
73                from_height_found = true;
74            }
75        }
76
77        let data: Vec<u8> = row.get(1).map_err(to_chain_error)?;
78        let block = CompactBlock::decode(&data[..]).map_err(to_chain_error)?;
79        if block.height() != height {
80            return Err(to_chain_error(SqliteClientError::CorruptedData(format!(
81                "Block height {} did not match row's height field value {}",
82                block.height(),
83                height
84            ))));
85        }
86
87        with_row(block)?;
88    }
89
90    if !from_height_found {
91        let from_height = from_height.expect("can only reach here if set");
92        return Err(to_chain_error(SqliteClientError::CacheMiss(from_height)));
93    }
94
95    Ok(())
96}
97
98/// Data structure representing a row in the block metadata database.
99#[cfg(feature = "unstable")]
100#[derive(Clone, Copy, Debug, PartialEq, Eq)]
101pub struct BlockMeta {
102    /// The height of the block.
103    pub height: BlockHeight,
104    /// The hash of the block.
105    pub block_hash: BlockHash,
106    /// The block timestamp as a Unix epoch time.
107    pub block_time: u32,
108    /// The number of Sapling outputs in the block.
109    pub sapling_outputs_count: u32,
110    /// The number of Orchard actions in the block.
111    pub orchard_actions_count: u32,
112}
113
114#[cfg(feature = "unstable")]
115impl BlockMeta {
116    /// Returns the path to this block's file within the given blocks directory.
117    pub fn block_file_path<P: AsRef<Path>>(&self, blocks_dir: &P) -> PathBuf {
118        blocks_dir.as_ref().join(Path::new(&format!(
119            "{}-{}-compactblock",
120            self.height, self.block_hash
121        )))
122    }
123}
124
125/// Inserts a batch of rows into the block metadata database.
126#[cfg(feature = "unstable")]
127pub(crate) fn blockmetadb_insert(
128    conn: &Connection,
129    block_meta: &[BlockMeta],
130) -> Result<(), rusqlite::Error> {
131    let mut stmt_insert = conn.prepare(
132        "INSERT INTO compactblocks_meta (
133            height,
134            blockhash,
135            time,
136            sapling_outputs_count,
137            orchard_actions_count
138        )
139        VALUES (
140            :height,
141            :blockhash,
142            :time,
143            :sapling_outputs_count,
144            :orchard_actions_count
145        )
146        ON CONFLICT (height) DO UPDATE
147        SET blockhash = :blockhash,
148            time = :time,
149            sapling_outputs_count = :sapling_outputs_count,
150            orchard_actions_count = :orchard_actions_count",
151    )?;
152
153    conn.execute("BEGIN IMMEDIATE", [])?;
154    let result = block_meta
155        .iter()
156        .map(|m| {
157            stmt_insert.execute(named_params![
158                ":height": u32::from(m.height),
159                ":blockhash": &m.block_hash.0[..],
160                ":time": m.block_time,
161                ":sapling_outputs_count": m.sapling_outputs_count,
162                ":orchard_actions_count": m.orchard_actions_count,
163            ])
164        })
165        .collect::<Result<Vec<_>, _>>();
166    match result {
167        Ok(_) => {
168            conn.execute("COMMIT", [])?;
169            Ok(())
170        }
171        Err(error) => {
172            match conn.execute("ROLLBACK", []) {
173                Ok(_) => Err(error),
174                Err(e) =>
175                // Panicking here is probably the right thing to do, because it
176                // means the database is corrupt.
177                {
178                    panic!(
179                        "Rollback failed with error {e} while attempting to recover from error {error}; database is likely corrupt."
180                    )
181                }
182            }
183        }
184    }
185}
186
187#[cfg(feature = "unstable")]
188pub(crate) fn blockmetadb_truncate_to_height(
189    conn: &Connection,
190    block_height: BlockHeight,
191) -> Result<(), rusqlite::Error> {
192    conn.prepare("DELETE FROM compactblocks_meta WHERE height > ?")?
193        .execute(params![u32::from(block_height)])?;
194    Ok(())
195}
196
197#[cfg(feature = "unstable")]
198pub(crate) fn blockmetadb_get_max_cached_height(
199    conn: &Connection,
200) -> Result<Option<BlockHeight>, rusqlite::Error> {
201    conn.query_row("SELECT MAX(height) FROM compactblocks_meta", [], |row| {
202        // `SELECT MAX(_)` will always return a row, but it will return `null` if the
203        // table is empty, which has no integer type. We handle the optionality here.
204        let h: Option<u32> = row.get(0)?;
205        Ok(h.map(BlockHeight::from))
206    })
207}
208
209/// Returns the metadata for the block with the given height, if it exists in the database.
210#[cfg(feature = "unstable")]
211pub(crate) fn blockmetadb_find_block(
212    conn: &Connection,
213    height: BlockHeight,
214) -> Result<Option<BlockMeta>, rusqlite::Error> {
215    conn.query_row(
216        "SELECT blockhash, time, sapling_outputs_count, orchard_actions_count
217        FROM compactblocks_meta
218        WHERE height = ?",
219        [u32::from(height)],
220        |row| {
221            Ok(BlockMeta {
222                height,
223                block_hash: BlockHash::from_slice(&row.get::<_, Vec<_>>(0)?),
224                block_time: row.get(1)?,
225                sapling_outputs_count: row.get(2)?,
226                orchard_actions_count: row.get(3)?,
227            })
228        },
229    )
230    .optional()
231}
232
233/// Implements a traversal of `limit` blocks of the filesystem-backed
234/// block cache.
235///
236/// Starting at `from_height`, the `with_row` callback is invoked with each block retrieved from
237/// the backing store. If the `limit` value provided is `None`, all blocks are traversed up to the
238/// maximum height for which metadata is available.
239#[cfg(feature = "unstable")]
240pub(crate) fn fsblockdb_with_blocks<F, DbErrT>(
241    cache: &FsBlockDb,
242    from_height: Option<BlockHeight>,
243    limit: Option<usize>,
244    mut with_block: F,
245) -> Result<(), Error<DbErrT, FsBlockDbError>>
246where
247    F: FnMut(CompactBlock) -> Result<(), Error<DbErrT, FsBlockDbError>>,
248{
249    fn to_chain_error<D, E: Into<FsBlockDbError>>(err: E) -> Error<D, FsBlockDbError> {
250        Error::BlockSource(err.into())
251    }
252
253    // Fetch the CompactBlocks we need to scan
254    let mut stmt_blocks = cache
255        .conn
256        .prepare(
257            "SELECT height, blockhash, time, sapling_outputs_count, orchard_actions_count
258             FROM compactblocks_meta
259             WHERE height >= ?
260             ORDER BY height ASC LIMIT ?",
261        )
262        .map_err(to_chain_error)?;
263
264    let rows = stmt_blocks
265        .query_map(
266            params![
267                from_height.map_or(0u32, u32::from),
268                limit
269                    .and_then(|l| u32::try_from(l).ok())
270                    .unwrap_or(u32::MAX)
271            ],
272            |row| {
273                Ok(BlockMeta {
274                    height: BlockHeight::from_u32(row.get(0)?),
275                    block_hash: BlockHash::from_slice(&row.get::<_, Vec<_>>(1)?),
276                    block_time: row.get(2)?,
277                    sapling_outputs_count: row.get(3)?,
278                    orchard_actions_count: row.get(4)?,
279                })
280            },
281        )
282        .map_err(to_chain_error)?;
283
284    // Only look for the `from_height` in the scanned blocks if it is set.
285    let mut from_height_found = from_height.is_none();
286    for row_result in rows {
287        let cbr = row_result.map_err(to_chain_error)?;
288        if !from_height_found {
289            // We will only perform this check on the first row.
290            let from_height = from_height.expect("can only reach here if set");
291            if from_height != cbr.height {
292                return Err(to_chain_error(FsBlockDbError::CacheMiss(from_height)));
293            } else {
294                from_height_found = true;
295            }
296        }
297
298        let mut block_file =
299            File::open(cbr.block_file_path(&cache.blocks_dir)).map_err(to_chain_error)?;
300        let mut block_data = vec![];
301        block_file
302            .read_to_end(&mut block_data)
303            .map_err(to_chain_error)?;
304
305        let block = CompactBlock::decode(&block_data[..]).map_err(to_chain_error)?;
306
307        if block.height() != cbr.height {
308            return Err(to_chain_error(FsBlockDbError::CorruptedData(format!(
309                "Block height {} did not match row's height field value {}",
310                block.height(),
311                cbr.height
312            ))));
313        }
314
315        with_block(block)?;
316    }
317
318    if !from_height_found {
319        let from_height = from_height.expect("can only reach here if set");
320        return Err(to_chain_error(FsBlockDbError::CacheMiss(from_height)));
321    }
322
323    Ok(())
324}
325
326#[cfg(test)]
327mod tests {
328    use zcash_client_backend::data_api::testing::sapling::SaplingPoolTester;
329
330    use crate::testing;
331
332    #[cfg(feature = "orchard")]
333    use zcash_client_backend::data_api::testing::orchard::OrchardPoolTester;
334
335    #[test]
336    fn valid_chain_states_sapling() {
337        testing::pool::valid_chain_states::<SaplingPoolTester>()
338    }
339
340    #[test]
341    #[cfg(feature = "orchard")]
342    fn valid_chain_states_orchard() {
343        testing::pool::valid_chain_states::<OrchardPoolTester>()
344    }
345
346    #[test]
347    #[cfg(feature = "orchard")]
348    fn invalid_chain_cache_disconnected_sapling() {
349        testing::pool::invalid_chain_cache_disconnected::<SaplingPoolTester>()
350    }
351
352    #[test]
353    #[cfg(feature = "orchard")]
354    fn invalid_chain_cache_disconnected_orchard() {
355        testing::pool::invalid_chain_cache_disconnected::<OrchardPoolTester>()
356    }
357
358    #[test]
359    fn data_db_truncation_sapling() {
360        testing::pool::data_db_truncation::<SaplingPoolTester>()
361    }
362
363    #[test]
364    #[cfg(feature = "orchard")]
365    fn data_db_truncation_orchard() {
366        testing::pool::data_db_truncation::<OrchardPoolTester>()
367    }
368
369    #[test]
370    fn truncate_to_chain_state_sapling() {
371        testing::pool::truncate_to_chain_state::<SaplingPoolTester>()
372    }
373
374    #[test]
375    #[cfg(feature = "orchard")]
376    fn truncate_to_chain_state_orchard() {
377        testing::pool::truncate_to_chain_state::<OrchardPoolTester>()
378    }
379
380    #[test]
381    fn truncate_to_chain_state_below_birthday_sapling() {
382        testing::pool::truncate_to_chain_state_below_birthday::<SaplingPoolTester>()
383    }
384
385    #[test]
386    #[cfg(feature = "orchard")]
387    fn truncate_to_chain_state_below_birthday_orchard() {
388        testing::pool::truncate_to_chain_state_below_birthday::<OrchardPoolTester>()
389    }
390
391    #[test]
392    fn truncate_to_chain_state_above_scanned_sapling() {
393        testing::pool::truncate_to_chain_state_above_scanned::<SaplingPoolTester>()
394    }
395
396    #[test]
397    #[cfg(feature = "orchard")]
398    fn truncate_to_chain_state_above_scanned_orchard() {
399        testing::pool::truncate_to_chain_state_above_scanned::<OrchardPoolTester>()
400    }
401
402    #[test]
403    fn truncate_to_chain_state_commitment_tree_error_sapling() {
404        testing::pool::truncate_to_chain_state_commitment_tree_error::<SaplingPoolTester>()
405    }
406
407    #[test]
408    #[cfg(feature = "orchard")]
409    fn truncate_to_chain_state_commitment_tree_error_orchard() {
410        testing::pool::truncate_to_chain_state_commitment_tree_error::<OrchardPoolTester>()
411    }
412
413    #[test]
414    fn put_blocks_commitment_tree_error_sapling() {
415        testing::pool::put_blocks_commitment_tree_error::<SaplingPoolTester>()
416    }
417
418    #[test]
419    #[cfg(feature = "orchard")]
420    fn put_blocks_commitment_tree_error_orchard() {
421        testing::pool::put_blocks_commitment_tree_error::<OrchardPoolTester>()
422    }
423
424    #[test]
425    fn rewind_to_chain_state_deep_sapling() {
426        testing::pool::rewind_to_chain_state_deep::<SaplingPoolTester>()
427    }
428
429    #[test]
430    #[cfg(feature = "orchard")]
431    fn rewind_to_chain_state_deep_orchard() {
432        testing::pool::rewind_to_chain_state_deep::<OrchardPoolTester>()
433    }
434
435    #[test]
436    fn rewind_to_chain_state_shallow_sapling() {
437        testing::pool::rewind_to_chain_state_shallow::<SaplingPoolTester>()
438    }
439
440    #[test]
441    #[cfg(feature = "orchard")]
442    fn rewind_to_chain_state_shallow_orchard() {
443        testing::pool::rewind_to_chain_state_shallow::<OrchardPoolTester>()
444    }
445
446    #[test]
447    fn rewind_after_non_contiguous_scan_sapling() {
448        testing::pool::rewind_after_non_contiguous_scan::<SaplingPoolTester>()
449    }
450
451    #[test]
452    #[cfg(feature = "orchard")]
453    fn rewind_after_non_contiguous_scan_orchard() {
454        testing::pool::rewind_after_non_contiguous_scan::<OrchardPoolTester>()
455    }
456
457    #[test]
458    #[cfg(feature = "expensive-tests")]
459    #[cfg_attr(
460        feature = "ignore-expensive-tests",
461        ignore = "covered by the expensive-test CI matrix"
462    )]
463    fn stabilized_note_spendable_after_deep_rewind_sapling() {
464        testing::pool::stabilized_note_spendable_after_deep_rewind::<SaplingPoolTester>()
465    }
466
467    #[test]
468    #[cfg(all(feature = "orchard", feature = "expensive-tests"))]
469    #[cfg_attr(
470        feature = "ignore-expensive-tests",
471        ignore = "covered by the expensive-test CI matrix"
472    )]
473    fn stabilized_note_spendable_after_deep_rewind_orchard() {
474        testing::pool::stabilized_note_spendable_after_deep_rewind::<OrchardPoolTester>()
475    }
476
477    #[test]
478    #[cfg(feature = "expensive-tests")]
479    #[cfg_attr(
480        feature = "ignore-expensive-tests",
481        ignore = "covered by the expensive-test CI matrix"
482    )]
483    fn newly_discovered_notes_become_stabilized_sapling() {
484        testing::pool::newly_discovered_notes_become_stabilized::<SaplingPoolTester>()
485    }
486
487    #[test]
488    #[cfg(all(feature = "orchard", feature = "expensive-tests"))]
489    #[cfg_attr(
490        feature = "ignore-expensive-tests",
491        ignore = "covered by the expensive-test CI matrix"
492    )]
493    fn newly_discovered_notes_become_stabilized_orchard() {
494        testing::pool::newly_discovered_notes_become_stabilized::<OrchardPoolTester>()
495    }
496
497    #[test]
498    fn reorg_to_checkpoint_sapling() {
499        testing::pool::reorg_to_checkpoint::<SaplingPoolTester>()
500    }
501
502    #[test]
503    #[cfg(feature = "orchard")]
504    fn reorg_to_checkpoint_orchard() {
505        testing::pool::reorg_to_checkpoint::<OrchardPoolTester>()
506    }
507
508    #[test]
509    fn scan_cached_blocks_allows_blocks_out_of_order_sapling() {
510        testing::pool::scan_cached_blocks_allows_blocks_out_of_order::<SaplingPoolTester>()
511    }
512
513    #[test]
514    #[cfg(feature = "orchard")]
515    fn scan_cached_blocks_allows_blocks_out_of_order_orchard() {
516        testing::pool::scan_cached_blocks_allows_blocks_out_of_order::<OrchardPoolTester>()
517    }
518
519    #[test]
520    fn scan_cached_blocks_finds_received_notes_sapling() {
521        testing::pool::scan_cached_blocks_finds_received_notes::<SaplingPoolTester>()
522    }
523
524    #[test]
525    #[cfg(feature = "orchard")]
526    fn scan_cached_blocks_finds_received_notes_orchard() {
527        testing::pool::scan_cached_blocks_finds_received_notes::<OrchardPoolTester>()
528    }
529
530    #[test]
531    fn scan_cached_blocks_finds_change_notes_sapling() {
532        testing::pool::scan_cached_blocks_finds_change_notes::<SaplingPoolTester>()
533    }
534
535    #[test]
536    #[cfg(feature = "orchard")]
537    fn scan_cached_blocks_finds_change_notes_orchard() {
538        testing::pool::scan_cached_blocks_finds_change_notes::<OrchardPoolTester>()
539    }
540
541    #[test]
542    fn scan_cached_blocks_detects_spends_out_of_order_sapling() {
543        testing::pool::scan_cached_blocks_detects_spends_out_of_order::<SaplingPoolTester>()
544    }
545
546    #[test]
547    #[cfg(feature = "orchard")]
548    fn scan_cached_blocks_detects_spends_out_of_order_orchard() {
549        testing::pool::scan_cached_blocks_detects_spends_out_of_order::<OrchardPoolTester>()
550    }
551}