1#![allow(clippy::items_after_statements)]
2
3use std::collections::{BTreeMap, BTreeSet};
4use std::num::NonZeroUsize;
5use std::rc::Rc;
6use std::vec::Vec;
7
8use miden_client::Word;
9use miden_client::block::BlockHeader;
10use miden_client::crypto::{Forest, InOrderIndex, MmrPeaks};
11use miden_client::note::BlockNumber;
12use miden_client::store::{BlockRelevance, PartialBlockchainFilter, StoreError};
13use miden_client::utils::{Deserializable, Serializable};
14use rusqlite::types::Value;
15use rusqlite::{Connection, OptionalExtension, Transaction, params, params_from_iter};
16
17use super::SqliteStore;
18use crate::sql_error::SqlResultExt;
19use crate::{insert_sql, subst};
20
21struct SerializedBlockHeaderData {
22 block_num: u32,
23 header: Vec<u8>,
24 has_client_notes: bool,
25}
26struct SerializedBlockHeaderParts {
27 _block_num: u64,
28 header: Vec<u8>,
29 has_client_notes: bool,
30}
31
32struct SerializedPartialBlockchainNodeData {
33 id: i64,
34 node: Vec<u8>,
35}
36struct SerializedPartialBlockchainNodeParts {
37 id: u64,
38 node: Vec<u8>,
39}
40
41impl SqliteStore {
42 pub(crate) fn get_block_headers(
43 conn: &mut Connection,
44 block_numbers: &BTreeSet<BlockNumber>,
45 ) -> Result<Vec<(BlockHeader, BlockRelevance)>, StoreError> {
46 let block_number_list = block_numbers
47 .iter()
48 .map(|block_number| Value::Integer(i64::from(block_number.as_u32())))
49 .collect::<Vec<Value>>();
50
51 const QUERY: &str = "SELECT block_num, header, has_client_notes FROM block_headers WHERE block_num IN rarray(?)";
52
53 conn.prepare(QUERY)
54 .into_store_error()?
55 .query_map(params![Rc::new(block_number_list)], parse_block_headers_columns)
56 .into_store_error()?
57 .map(|result| {
58 let serialized_block_header_parts: SerializedBlockHeaderParts =
59 result.into_store_error()?;
60 parse_block_header(&serialized_block_header_parts)
61 })
62 .collect()
63 }
64
65 pub(crate) fn get_tracked_block_headers(
66 conn: &mut Connection,
67 ) -> Result<Vec<BlockHeader>, StoreError> {
68 const QUERY: &str = "SELECT block_num, header, has_client_notes FROM block_headers WHERE has_client_notes=true";
69 conn.prepare(QUERY)
70 .into_store_error()?
71 .query_map(params![], parse_block_headers_columns)
72 .into_store_error()?
73 .map(|result| {
74 let serialized_block_header_parts: SerializedBlockHeaderParts =
75 result.into_store_error()?;
76 parse_block_header(&serialized_block_header_parts).map(|(block, _)| block)
77 })
78 .collect()
79 }
80
81 pub(crate) fn get_tracked_block_header_numbers(
82 conn: &mut Connection,
83 ) -> Result<BTreeSet<usize>, StoreError> {
84 const QUERY: &str = "SELECT block_num FROM block_headers WHERE has_client_notes=true";
85 conn.prepare(QUERY)
86 .into_store_error()?
87 .query_map(params![], |row| row.get::<_, u32>(0))
88 .into_store_error()?
89 .map(|result| {
90 let block_num: u32 = result.into_store_error()?;
91 Ok(block_num as usize)
92 })
93 .collect()
94 }
95
96 pub(crate) fn get_partial_blockchain_nodes(
97 conn: &mut Connection,
98 filter: &PartialBlockchainFilter,
99 ) -> Result<BTreeMap<InOrderIndex, Word>, StoreError> {
100 match filter {
101 PartialBlockchainFilter::All => query_partial_blockchain_nodes(
102 conn,
103 "SELECT id, node FROM partial_blockchain_nodes",
104 params![],
105 ),
106
107 PartialBlockchainFilter::List(ids) if ids.is_empty() => Ok(BTreeMap::new()),
108 PartialBlockchainFilter::List(ids) => {
109 let id_values = ids
110 .iter()
111 .map(|id| Value::Integer(i64::try_from(id.inner()).expect("id is a valid i64")))
112 .collect::<Vec<_>>();
113
114 query_partial_blockchain_nodes(
115 conn,
116 "SELECT id, node FROM partial_blockchain_nodes WHERE id IN rarray(?)",
117 params_from_iter([Rc::new(id_values)]),
118 )
119 },
120
121 PartialBlockchainFilter::Forest(forest) if forest.is_empty() => Ok(BTreeMap::new()),
122 PartialBlockchainFilter::Forest(forest) => {
123 let max_index = i64::try_from(forest.rightmost_in_order_index().inner())
124 .expect("id is a valid i64");
125
126 query_partial_blockchain_nodes(
127 conn,
128 "SELECT id, node FROM partial_blockchain_nodes WHERE id <= ?",
129 params![max_index],
130 )
131 },
132 }
133 }
134
135 pub(crate) fn get_current_blockchain_peaks(
136 conn: &mut Connection,
137 ) -> Result<MmrPeaks, StoreError> {
138 const QUERY: &str =
139 "SELECT block_num, partial_blockchain_peaks FROM blockchain_checkpoint LIMIT 1";
140
141 let row: Option<(u32, Vec<u8>)> = conn
142 .prepare(QUERY)
143 .into_store_error()?
144 .query_row(params![], |row| Ok((row.get(0)?, row.get(1)?)))
145 .optional()
146 .into_store_error()?;
147
148 match row {
149 Some((block_num, peaks_bytes)) if !peaks_bytes.is_empty() => {
150 parse_partial_blockchain_peaks(block_num, &peaks_bytes)
151 },
152 _ => Ok(MmrPeaks::new(Forest::empty(), vec![])?),
153 }
154 }
155
156 pub(crate) fn insert_block_header(
157 conn: &mut Connection,
158 block_header: &BlockHeader,
159 nodes: &[(InOrderIndex, Word)],
160 has_client_notes: bool,
161 ) -> Result<(), StoreError> {
162 let tx = conn.transaction().into_store_error()?;
163
164 Self::insert_block_header_tx(&tx, block_header, has_client_notes)?;
165 Self::insert_partial_blockchain_nodes_tx(&tx, nodes)?;
166 tx.commit().into_store_error()?;
167 Ok(())
168 }
169
170 pub(crate) fn insert_partial_blockchain_nodes_tx(
172 tx: &Transaction<'_>,
173 nodes: &[(InOrderIndex, Word)],
174 ) -> Result<(), StoreError> {
175 for (index, node) in nodes {
176 insert_partial_blockchain_node(tx, *index, *node)?;
177 }
178 Ok(())
179 }
180
181 pub(crate) fn insert_block_header_tx(
186 tx: &Transaction<'_>,
187 block_header: &BlockHeader,
188 has_client_notes: bool,
189 ) -> Result<(), StoreError> {
190 let SerializedBlockHeaderData { block_num, header, has_client_notes } =
191 serialize_block_header(block_header, has_client_notes);
192 const QUERY: &str =
193 insert_sql!(block_headers { block_num, header, has_client_notes } | IGNORE);
194 tx.execute(QUERY, params![block_num, header, has_client_notes])
195 .into_store_error()?;
196
197 set_block_header_has_client_notes(tx, u64::from(block_num), has_client_notes)?;
198 Ok(())
199 }
200
201 pub fn prune_irrelevant_blocks(
209 conn: &mut Connection,
210 blocks_to_untrack: &[BlockNumber],
211 node_indices_to_remove: &[InOrderIndex],
212 ) -> Result<(), StoreError> {
213 let tx = conn.transaction().into_store_error()?;
214
215 if !node_indices_to_remove.is_empty() {
217 let id_values = node_indices_to_remove
218 .iter()
219 .map(|id| Value::Integer(i64::try_from(id.inner()).expect("id is a valid i64")))
220 .collect::<Vec<_>>();
221
222 tx.execute(
223 "DELETE FROM partial_blockchain_nodes WHERE id IN rarray(?)",
224 params![Rc::new(id_values)],
225 )
226 .into_store_error()?;
227 }
228
229 if !blocks_to_untrack.is_empty() {
231 let block_values = blocks_to_untrack
232 .iter()
233 .map(|b| Value::Integer(i64::from(b.as_u32())))
234 .collect::<Vec<_>>();
235
236 tx.execute(
237 "UPDATE block_headers SET has_client_notes = 0 WHERE block_num IN rarray(?)",
238 params![Rc::new(block_values)],
239 )
240 .into_store_error()?;
241 }
242
243 let genesis: u32 = BlockNumber::GENESIS.as_u32();
245
246 let sync_block: Option<u32> = tx
247 .query_row("SELECT block_num FROM blockchain_checkpoint LIMIT 1", [], |r| r.get(0))
248 .optional()
249 .into_store_error()?;
250
251 if let Some(sync_height) = sync_block {
252 tx.execute(
253 "DELETE FROM block_headers \
254 WHERE has_client_notes = 0 \
255 AND block_num > ?1 \
256 AND block_num < ?2",
257 rusqlite::params![genesis, sync_height],
258 )
259 .into_store_error()?;
260 }
261
262 tx.commit().into_store_error()
263 }
264}
265
266fn insert_partial_blockchain_node(
271 tx: &Transaction<'_>,
272 id: InOrderIndex,
273 node: Word,
274) -> Result<(), StoreError> {
275 let SerializedPartialBlockchainNodeData { id, node } =
276 serialize_partial_blockchain_node(id, node);
277 const QUERY: &str = insert_sql!(partial_blockchain_nodes { id, node } | IGNORE);
278 tx.execute(QUERY, params![id, node]).into_store_error()?;
279 Ok(())
280}
281
282fn query_partial_blockchain_nodes<P: rusqlite::Params>(
283 conn: &mut Connection,
284 sql: &str,
285 params: P,
286) -> Result<BTreeMap<InOrderIndex, Word>, StoreError> {
287 let mut stmt = conn.prepare_cached(sql).into_store_error()?;
288
289 stmt.query_map(params, parse_partial_blockchain_nodes_columns)
290 .into_store_error()?
291 .map(|row_res| {
292 let parts: SerializedPartialBlockchainNodeParts = row_res.into_store_error()?;
293 parse_partial_blockchain_nodes(&parts)
294 })
295 .collect()
296}
297
298fn parse_partial_blockchain_peaks(forest: u32, peaks_nodes: &[u8]) -> Result<MmrPeaks, StoreError> {
299 let mmr_peaks_nodes = Vec::<Word>::read_from_bytes(peaks_nodes)?;
300
301 let forest_size = usize::try_from(forest).expect("u64 should fit in usize");
302 let forest = Forest::new(forest_size).map_err(|err| {
303 StoreError::DataDeserializationError(
304 miden_protocol::utils::serde::DeserializationError::InvalidValue(format!(
305 "invalid forest size {forest_size}: {err}"
306 )),
307 )
308 })?;
309 MmrPeaks::new(forest, mmr_peaks_nodes).map_err(StoreError::MmrError)
310}
311
312fn serialize_block_header(
313 block_header: &BlockHeader,
314 has_client_notes: bool,
315) -> SerializedBlockHeaderData {
316 let block_num = block_header.block_num();
317 let header = block_header.to_bytes();
318
319 SerializedBlockHeaderData {
320 block_num: block_num.as_u32(),
321 header,
322 has_client_notes,
323 }
324}
325
326fn parse_block_headers_columns(
327 row: &rusqlite::Row<'_>,
328) -> Result<SerializedBlockHeaderParts, rusqlite::Error> {
329 let block_num: u32 = row.get(0)?;
330 let header: Vec<u8> = row.get(1)?;
331 let has_client_notes: bool = row.get(2)?;
332
333 Ok(SerializedBlockHeaderParts {
334 _block_num: u64::from(block_num),
335 header,
336 has_client_notes,
337 })
338}
339
340fn parse_block_header(
341 serialized_block_header_parts: &SerializedBlockHeaderParts,
342) -> Result<(BlockHeader, BlockRelevance), StoreError> {
343 Ok((
344 BlockHeader::read_from_bytes(&serialized_block_header_parts.header)?,
345 serialized_block_header_parts.has_client_notes.into(),
346 ))
347}
348
349fn serialize_partial_blockchain_node(
350 id: InOrderIndex,
351 node: Word,
352) -> SerializedPartialBlockchainNodeData {
353 let id = i64::try_from(id.inner()).expect("id is a valid i64");
354 let node = node.to_bytes();
355 SerializedPartialBlockchainNodeData { id, node }
356}
357
358fn parse_partial_blockchain_nodes_columns(
359 row: &rusqlite::Row<'_>,
360) -> Result<SerializedPartialBlockchainNodeParts, rusqlite::Error> {
361 let id: u64 = row.get(0)?;
362 let node = row.get(1)?;
363 Ok(SerializedPartialBlockchainNodeParts { id, node })
364}
365
366fn parse_partial_blockchain_nodes(
367 serialized_partial_blockchain_node_parts: &SerializedPartialBlockchainNodeParts,
368) -> Result<(InOrderIndex, Word), StoreError> {
369 let id = InOrderIndex::new(
370 NonZeroUsize::new(
371 usize::try_from(serialized_partial_blockchain_node_parts.id)
372 .expect("id is u64, should not fail"),
373 )
374 .unwrap(),
375 );
376 let node: Word = Word::read_from_bytes(&serialized_partial_blockchain_node_parts.node)?;
377 Ok((id, node))
378}
379
380pub(crate) fn set_block_header_has_client_notes(
381 tx: &Transaction<'_>,
382 block_num: u64,
383 has_client_notes: bool,
384) -> Result<(), StoreError> {
385 const QUERY: &str = "\
387 UPDATE block_headers
388 SET has_client_notes=?
389 WHERE block_num=? AND has_client_notes=FALSE;";
390 tx.execute(QUERY, params![has_client_notes, block_num]).into_store_error()?;
391 Ok(())
392}
393
394#[cfg(test)]
395mod test {
396 use std::collections::{BTreeMap, BTreeSet};
397 use std::vec::Vec;
398
399 use miden_client::Word;
400 use miden_client::block::BlockHeader;
401 use miden_client::crypto::{Forest, InOrderIndex, MmrPeaks};
402 use miden_client::note::BlockNumber;
403 use miden_client::store::{PartialBlockchainFilter, Store};
404 use miden_client::utils::Serializable;
405 use miden_protocol::crypto::merkle::mmr::Mmr;
406 use miden_protocol::transaction::TransactionKernel;
407 use rusqlite::params;
408
409 use crate::SqliteStore;
410 use crate::tests::create_test_store;
411
412 async fn insert_dummy_block_headers(store: &mut SqliteStore) -> Vec<BlockHeader> {
413 let block_headers: Vec<BlockHeader> = (0..5)
414 .map(|block_num| {
415 BlockHeader::mock(block_num, None, None, &[], TransactionKernel.to_commitment())
416 })
417 .collect();
418
419 let block_headers_clone = block_headers.clone();
420 store
421 .interact_with_connection(move |conn| {
422 let tx = conn.transaction().unwrap();
423 (0..5).for_each(|block_num| {
424 SqliteStore::insert_block_header_tx(
425 &tx,
426 &block_headers_clone[block_num],
427 false,
428 )
429 .unwrap();
430 });
431 tx.commit().unwrap();
432 Ok(())
433 })
434 .await
435 .unwrap();
436
437 block_headers
438 }
439
440 #[tokio::test]
441 async fn insert_and_get_block_headers_by_number() {
442 let mut store = create_test_store().await;
443 let block_headers = insert_dummy_block_headers(&mut store).await;
444
445 let block_header = Store::get_block_header_by_num(&store, 3.into()).await.unwrap().unwrap();
446 assert_eq!(block_headers[3], block_header.0);
447 }
448
449 #[tokio::test]
450 async fn insert_and_get_block_headers_by_list() {
451 let mut store = create_test_store().await;
452 let mock_block_headers = insert_dummy_block_headers(&mut store).await;
453
454 let block_headers: Vec<BlockHeader> =
455 Store::get_block_headers(&store, &[1.into(), 3.into()].into_iter().collect())
456 .await
457 .unwrap()
458 .into_iter()
459 .map(|(block_header, _has_notes)| block_header)
460 .collect();
461 assert_eq!(
462 &[mock_block_headers[1].clone(), mock_block_headers[3].clone()],
463 &block_headers[..]
464 );
465 }
466
467 #[tokio::test]
470 async fn insert_block_header_stores_header_and_nodes() {
471 let store = create_test_store().await;
472 const TOTAL_BLOCKS: usize = 8;
473 let tx_kernel = TransactionKernel.to_commitment();
474
475 let headers: Vec<BlockHeader> = (0..TOTAL_BLOCKS)
476 .map(|n| BlockHeader::mock(u32::try_from(n).unwrap(), None, None, &[], tx_kernel))
477 .collect();
478 let mut mmr = Mmr::default();
479 for header in &headers {
480 mmr.add(header.commitment()).expect("valid MMR append");
481 }
482
483 let tracked: BTreeSet<usize> = [5].into();
484 let auth_nodes = collect_auth_nodes(&mmr, &headers, &tracked);
485 let header = headers[5].clone();
486
487 Store::insert_block_header(&store, &header, &auth_nodes, true).await.unwrap();
488
489 let stored = Store::get_block_headers(&store, &[5.into()].into_iter().collect())
491 .await
492 .unwrap();
493 assert_eq!(stored.len(), 1);
494 assert_eq!(stored[0].0, header);
495
496 let tracked = Store::get_tracked_block_header_numbers(&store).await.unwrap();
497 assert_eq!(tracked, [5].into());
498
499 let stored_nodes =
501 Store::get_partial_blockchain_nodes(&store, PartialBlockchainFilter::All)
502 .await
503 .unwrap();
504 let expected: BTreeMap<InOrderIndex, Word> = auth_nodes.iter().copied().collect();
505 assert_eq!(stored_nodes, expected);
506 }
507
508 #[tokio::test]
511 async fn insert_block_header_rolls_back_header_when_nodes_fail() {
512 let store = create_test_store().await;
513 let header = BlockHeader::mock(5, None, None, &[], TransactionKernel.to_commitment());
514 let nodes = [(InOrderIndex::from_leaf_pos(5), header.commitment())];
516
517 store
519 .interact_with_connection(|conn| {
520 conn.execute("DROP TABLE partial_blockchain_nodes", []).unwrap();
521 Ok(())
522 })
523 .await
524 .unwrap();
525
526 let result = Store::insert_block_header(&store, &header, &nodes, true).await;
527 assert!(result.is_err(), "node insert must fail against the dropped table");
528
529 let stored = Store::get_block_headers(&store, &[5.into()].into_iter().collect())
531 .await
532 .unwrap();
533 assert!(stored.is_empty(), "header must roll back when the node insert fails");
534 }
535
536 #[tokio::test]
538 async fn partial_mmr_reconstructs_after_multiple_prune() {
539 let store = create_test_store().await;
543 const TOTAL_BLOCKS: usize = 7300;
544
545 let tx_kernel_commitment = TransactionKernel.to_commitment();
546 let block_headers: Vec<BlockHeader> = (0..TOTAL_BLOCKS)
547 .map(|block_num| {
548 BlockHeader::mock(
549 u32::try_from(block_num).unwrap(),
550 None,
551 None,
552 &[],
553 tx_kernel_commitment,
554 )
555 })
556 .collect();
557
558 let mut mmr = Mmr::default();
559 for header in &block_headers {
560 mmr.add(header.commitment()).expect("valid MMR append");
561 }
562
563 let mut tracked_set: BTreeSet<usize> = (0..(TOTAL_BLOCKS - 1)).step_by(97).collect();
564 tracked_set.insert(TOTAL_BLOCKS - 2);
565 let tracked_blocks: Vec<usize> = tracked_set.iter().copied().collect();
566
567 let mut tracked_nodes: BTreeMap<InOrderIndex, Word> = BTreeMap::new();
568 for &block_num in &tracked_blocks {
569 let header = &block_headers[block_num];
570 tracked_nodes.insert(InOrderIndex::from_leaf_pos(block_num), header.commitment());
571
572 let proof = mmr.open(block_num).expect("valid proof");
573 let mut idx = InOrderIndex::from_leaf_pos(block_num);
574 for node in proof.merkle_path().nodes() {
575 tracked_nodes.insert(idx.sibling(), *node);
576 idx = idx.parent();
577 }
578 }
579 let tracked_nodes: Vec<(InOrderIndex, Word)> = tracked_nodes.into_iter().collect();
580
581 let peaks_by_block: Vec<MmrPeaks> = (0..TOTAL_BLOCKS)
582 .map(|block_num| {
583 mmr.peaks_at(Forest::new(block_num).expect("valid forest"))
584 .expect("valid peaks")
585 })
586 .collect();
587
588 store
590 .interact_with_connection(move |conn| {
591 let tx = conn.transaction().unwrap();
592 for (block_num, block_header) in block_headers.iter().enumerate() {
593 let has_notes = tracked_set.contains(&block_num);
594 SqliteStore::insert_block_header_tx(&tx, block_header, has_notes).unwrap();
595 }
596
597 SqliteStore::insert_partial_blockchain_nodes_tx(&tx, &tracked_nodes).unwrap();
598 tx.commit().unwrap();
599 Ok(())
600 })
601 .await
602 .unwrap();
603
604 let prune_heights = [
605 TOTAL_BLOCKS / 5,
606 (TOTAL_BLOCKS * 2) / 5,
607 (TOTAL_BLOCKS * 3) / 5,
608 TOTAL_BLOCKS - 1,
609 ];
610
611 let mut previous_remaining: Option<i64> = None;
615 for height in prune_heights {
616 let height_i64 = i64::try_from(height).expect("fits in i64");
617 let peaks_bytes = peaks_by_block[height].peaks().to_vec().to_bytes();
618
619 store
621 .interact_with_connection(move |conn| {
622 conn.execute(
623 "UPDATE blockchain_checkpoint SET block_num = ?, partial_blockchain_peaks = ?",
624 params![height_i64, peaks_bytes],
625 )
626 .unwrap();
627 Ok(())
628 })
629 .await
630 .unwrap();
631
632 store.untrack_and_prune_irrelevant_blocks(&[], &[]).await.unwrap();
634
635 let remaining_headers: i64 = store
637 .interact_with_connection(|conn| {
638 let count = conn
639 .query_row("SELECT COUNT(*) FROM block_headers", [], |row| row.get(0))
640 .unwrap();
641 Ok(count)
642 })
643 .await
644 .unwrap();
645 if let Some(previous) = previous_remaining {
646 assert!(remaining_headers < previous);
647 } else {
648 assert!(remaining_headers < i64::try_from(TOTAL_BLOCKS).unwrap());
649 }
650 previous_remaining = Some(remaining_headers);
651 }
652
653 let partial_mmr = Store::get_current_partial_mmr(&store).await.unwrap();
655 assert_eq!(partial_mmr.peaks().hash_peaks(), mmr.peaks().hash_peaks());
656
657 for block_num in tracked_blocks {
658 let partial_proof = partial_mmr.open(block_num).expect("partial mmr query succeeds");
659 assert!(partial_proof.is_some());
660 assert_eq!(
661 partial_proof.unwrap().merkle_path(),
662 mmr.open(block_num).unwrap().merkle_path()
663 );
664 }
665 }
666
667 fn collect_auth_nodes(
669 mmr: &Mmr,
670 block_headers: &[BlockHeader],
671 tracked: &BTreeSet<usize>,
672 ) -> Vec<(InOrderIndex, Word)> {
673 let mut nodes: BTreeMap<InOrderIndex, Word> = BTreeMap::new();
674 for &block_num in tracked {
675 nodes.insert(
676 InOrderIndex::from_leaf_pos(block_num),
677 block_headers[block_num].commitment(),
678 );
679 let proof = mmr.open(block_num).expect("valid proof");
680 let mut idx = InOrderIndex::from_leaf_pos(block_num);
681 for node in proof.merkle_path().nodes() {
682 nodes.insert(idx.sibling(), *node);
683 idx = idx.parent();
684 }
685 }
686 nodes.into_iter().collect()
687 }
688
689 #[tokio::test]
692 async fn prune_irrelevant_blocks_removes_redundant_auth_nodes() {
693 let store = create_test_store().await;
694 const TOTAL_BLOCKS: usize = 16;
695 let tx_kernel = TransactionKernel.to_commitment();
696
697 let headers: Vec<BlockHeader> = (0..TOTAL_BLOCKS)
698 .map(|n| BlockHeader::mock(u32::try_from(n).unwrap(), None, None, &[], tx_kernel))
699 .collect();
700 let mut mmr = Mmr::default();
701 for h in &headers {
702 mmr.add(h.commitment()).expect("valid MMR append");
703 }
704
705 let tracked: BTreeSet<usize> = [3, 10].into();
707 let auth_nodes = collect_auth_nodes(&mmr, &headers, &tracked);
708 let tip_peaks_bytes = mmr
709 .peaks_at(Forest::new(TOTAL_BLOCKS - 1).expect("valid forest"))
710 .unwrap()
711 .peaks()
712 .to_vec()
713 .to_bytes();
714
715 let headers_clone = headers.clone();
717 store
718 .interact_with_connection(move |conn| {
719 let tx = conn.transaction().unwrap();
720 for (i, header) in headers_clone.iter().enumerate().take(TOTAL_BLOCKS) {
721 SqliteStore::insert_block_header_tx(&tx, header, tracked.contains(&i)).unwrap();
722 }
723 SqliteStore::insert_partial_blockchain_nodes_tx(&tx, &auth_nodes).unwrap();
724 tx.execute(
725 "UPDATE blockchain_checkpoint SET block_num = ?, partial_blockchain_peaks = ?",
726 params![i64::try_from(TOTAL_BLOCKS - 1).unwrap(), tip_peaks_bytes],
727 )
728 .unwrap();
729 tx.commit().unwrap();
730 Ok(())
731 })
732 .await
733 .unwrap();
734
735 let mut partial_mmr = Store::get_current_partial_mmr(&store).await.unwrap();
737 let removed: Vec<InOrderIndex> =
738 partial_mmr.untrack(3).into_iter().map(|(idx, _)| idx).collect();
739 assert!(!removed.is_empty(), "untracking should remove at least one node");
740
741 store
742 .untrack_and_prune_irrelevant_blocks(&[BlockNumber::from(3u32)], &removed)
743 .await
744 .unwrap();
745
746 let rebuilt = Store::get_current_partial_mmr(&store).await.unwrap();
748 assert_eq!(rebuilt.peaks().hash_peaks(), mmr.peaks().hash_peaks());
749
750 let proof_10 = rebuilt.open(10).expect("open succeeds");
751 assert!(proof_10.is_some(), "block 10 should still be provable");
752
753 let proof_3 = rebuilt.open(3).expect("open succeeds");
754 assert!(proof_3.is_none(), "block 3 should no longer be provable");
755 }
756}