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::{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
24pub(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 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 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 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#[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#[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 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 let h: Option<u32> = row.get(0)?;
199 Ok(h.map(BlockHeight::from))
200 })
201}
202
203#[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#[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 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 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 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}