1use 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
26pub(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 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 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 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#[cfg(feature = "unstable")]
100#[derive(Clone, Copy, Debug, PartialEq, Eq)]
101pub struct BlockMeta {
102 pub height: BlockHeight,
104 pub block_hash: BlockHash,
106 pub block_time: u32,
108 pub sapling_outputs_count: u32,
110 pub orchard_actions_count: u32,
112}
113
114#[cfg(feature = "unstable")]
115impl BlockMeta {
116 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#[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 {
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 let h: Option<u32> = row.get(0)?;
205 Ok(h.map(BlockHeight::from))
206 })
207}
208
209#[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#[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 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 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 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}