1#![allow(clippy::items_after_statements)]
2
3use std::collections::{BTreeMap, BTreeSet};
4use std::num::NonZeroUsize;
5use std::rc::Rc;
6use std::string::String;
7use std::vec::Vec;
8
9use miden_client::Word;
10use miden_client::block::BlockHeader;
11use miden_client::crypto::{Forest, InOrderIndex, MmrPeaks};
12use miden_client::note::BlockNumber;
13use miden_client::store::{BlockRelevance, PartialBlockchainFilter, StoreError};
14use miden_client::utils::{Deserializable, Serializable};
15use rusqlite::types::Value;
16use rusqlite::{Connection, OptionalExtension, Transaction, params, params_from_iter};
17
18use super::SqliteStore;
19use crate::sql_error::SqlResultExt;
20use crate::{insert_sql, subst};
21
22struct SerializedBlockHeaderData {
23 block_num: u32,
24 header: Vec<u8>,
25 partial_blockchain_peaks: Vec<u8>,
26 has_client_notes: bool,
27}
28struct SerializedBlockHeaderParts {
29 _block_num: u64,
30 header: Vec<u8>,
31 _partial_blockchain_peaks: Vec<u8>,
32 has_client_notes: bool,
33}
34
35struct SerializedPartialBlockchainNodeData {
36 id: i64,
37 node: String,
38}
39struct SerializedPartialBlockchainNodeParts {
40 id: u64,
41 node: String,
42}
43
44fn partial_blockchain_filter_to_query(filter: &PartialBlockchainFilter) -> String {
48 let base = String::from("SELECT id, node FROM partial_blockchain_nodes");
49 match filter {
50 PartialBlockchainFilter::All => base,
51 PartialBlockchainFilter::List(_) => format!("{base} WHERE id IN rarray(?)"),
52 }
53}
54
55impl SqliteStore {
56 pub(crate) fn insert_block_header(
57 conn: &mut Connection,
58 block_header: &BlockHeader,
59 partial_blockchain_peaks: &MmrPeaks,
60 has_client_notes: bool,
61 ) -> Result<(), StoreError> {
62 let tx = conn.transaction().into_store_error()?;
63
64 Self::insert_block_header_tx(
65 &tx,
66 block_header,
67 partial_blockchain_peaks,
68 has_client_notes,
69 )?;
70
71 tx.commit().into_store_error()?;
72 Ok(())
73 }
74
75 pub(crate) fn get_block_headers(
76 conn: &mut Connection,
77 block_numbers: &BTreeSet<BlockNumber>,
78 ) -> Result<Vec<(BlockHeader, BlockRelevance)>, StoreError> {
79 let block_number_list = block_numbers
80 .iter()
81 .map(|block_number| Value::Integer(i64::from(block_number.as_u32())))
82 .collect::<Vec<Value>>();
83
84 const QUERY: &str = "SELECT block_num, header, partial_blockchain_peaks, has_client_notes FROM block_headers WHERE block_num IN rarray(?)";
85
86 conn.prepare(QUERY)
87 .into_store_error()?
88 .query_map(params![Rc::new(block_number_list)], parse_block_headers_columns)
89 .into_store_error()?
90 .map(|result| {
91 let serialized_block_header_parts: SerializedBlockHeaderParts =
92 result.into_store_error()?;
93 parse_block_header(&serialized_block_header_parts)
94 })
95 .collect()
96 }
97
98 pub(crate) fn get_tracked_block_headers(
99 conn: &mut Connection,
100 ) -> Result<Vec<BlockHeader>, StoreError> {
101 const QUERY: &str = "SELECT block_num, header, partial_blockchain_peaks, has_client_notes FROM block_headers WHERE has_client_notes=true";
102 conn.prepare(QUERY)
103 .into_store_error()?
104 .query_map(params![], parse_block_headers_columns)
105 .into_store_error()?
106 .map(|result| {
107 let serialized_block_header_parts: SerializedBlockHeaderParts =
108 result.into_store_error()?;
109 parse_block_header(&serialized_block_header_parts).map(|(block, _)| block)
110 })
111 .collect()
112 }
113
114 pub(crate) fn get_partial_blockchain_nodes(
115 conn: &mut Connection,
116 filter: &PartialBlockchainFilter,
117 ) -> Result<BTreeMap<InOrderIndex, Word>, StoreError> {
118 let mut params = Vec::new();
119 if let PartialBlockchainFilter::List(ids) = &filter {
120 let id_values = ids
121 .iter()
122 .map(|id| Value::Integer(i64::try_from(id.inner()).expect("id is a valid i64")))
124 .collect::<Vec<_>>();
125
126 params.push(Rc::new(id_values));
127 }
128
129 conn.prepare(&partial_blockchain_filter_to_query(filter))
130 .into_store_error()?
131 .query_map(params_from_iter(params), parse_partial_blockchain_nodes_columns)
132 .into_store_error()?
133 .map(|result| {
134 let serialized_partial_blockchain_node_parts: SerializedPartialBlockchainNodeParts =
135 result.into_store_error()?;
136 parse_partial_blockchain_nodes(&serialized_partial_blockchain_node_parts)
137 })
138 .collect()
139 }
140
141 pub(crate) fn get_partial_blockchain_peaks_by_block_num(
142 conn: &mut Connection,
143 block_num: BlockNumber,
144 ) -> Result<MmrPeaks, StoreError> {
145 const QUERY: &str =
146 "SELECT partial_blockchain_peaks FROM block_headers WHERE block_num = ?";
147
148 let partial_blockchain_peaks: Option<Vec<u8>> = conn
149 .prepare(QUERY)
150 .into_store_error()?
151 .query_row(params![block_num.as_u32()], |row| row.get::<_, Vec<u8>>(0))
152 .optional()
153 .into_store_error()?;
154
155 if let Some(partial_blockchain_peaks) = partial_blockchain_peaks {
156 return parse_partial_blockchain_peaks(block_num.as_u32(), &partial_blockchain_peaks);
157 }
158
159 Ok(MmrPeaks::new(Forest::empty(), vec![])?)
160 }
161
162 pub fn insert_partial_blockchain_nodes(
163 conn: &mut Connection,
164 nodes: &[(InOrderIndex, Word)],
165 ) -> Result<(), StoreError> {
166 let tx = conn.transaction().into_store_error()?;
167
168 Self::insert_partial_blockchain_nodes_tx(&tx, nodes)?;
169 tx.commit().into_store_error()?;
170 Ok(())
171 }
172
173 pub(crate) fn insert_partial_blockchain_nodes_tx(
175 tx: &Transaction<'_>,
176 nodes: &[(InOrderIndex, Word)],
177 ) -> Result<(), StoreError> {
178 for (index, node) in nodes {
179 insert_partial_blockchain_node(tx, *index, *node)?;
180 }
181 Ok(())
182 }
183
184 pub(crate) fn insert_block_header_tx(
189 tx: &Transaction<'_>,
190 block_header: &BlockHeader,
191 partial_blockchain_peaks: &MmrPeaks,
192 has_client_notes: bool,
193 ) -> Result<(), StoreError> {
194 let partial_blockchain_peaks = partial_blockchain_peaks.peaks().to_vec();
195 let SerializedBlockHeaderData {
196 block_num,
197 header,
198 partial_blockchain_peaks,
199 has_client_notes,
200 } = serialize_block_header(block_header, &partial_blockchain_peaks, has_client_notes);
201 const QUERY: &str = insert_sql!(
202 block_headers {
203 block_num,
204 header,
205 partial_blockchain_peaks,
206 has_client_notes,
207 } | IGNORE
208 );
209 tx.execute(QUERY, params![block_num, header, partial_blockchain_peaks, has_client_notes])
210 .into_store_error()?;
211
212 set_block_header_has_client_notes(tx, u64::from(block_num), has_client_notes)?;
213 Ok(())
214 }
215
216 pub fn prune_irrelevant_blocks(conn: &mut Connection) -> Result<(), StoreError> {
219 let tx = conn.transaction().into_store_error()?;
220 let genesis: u32 = BlockNumber::GENESIS.as_u32();
221
222 let sync_block: Option<u32> = tx
223 .query_row("SELECT block_num FROM state_sync LIMIT 1", [], |r| r.get(0))
224 .optional()
225 .into_store_error()?;
226
227 if let Some(sync_height) = sync_block {
228 tx.execute(
229 r"
230 DELETE FROM block_headers
231 WHERE has_client_notes = 0
232 AND block_num > ?1
233 AND block_num < ?2
234 ",
235 rusqlite::params![genesis, sync_height],
236 )
237 .into_store_error()?;
238 }
239
240 tx.commit().into_store_error()
241 }
242}
243
244fn insert_partial_blockchain_node(
249 tx: &Transaction<'_>,
250 id: InOrderIndex,
251 node: Word,
252) -> Result<(), StoreError> {
253 let SerializedPartialBlockchainNodeData { id, node } =
254 serialize_partial_blockchain_node(id, node);
255 const QUERY: &str = insert_sql!(partial_blockchain_nodes { id, node } | IGNORE);
256 tx.execute(QUERY, params![id, node]).into_store_error()?;
257 Ok(())
258}
259
260fn parse_partial_blockchain_peaks(forest: u32, peaks_nodes: &[u8]) -> Result<MmrPeaks, StoreError> {
261 let mmr_peaks_nodes = Vec::<Word>::read_from_bytes(peaks_nodes)?;
262
263 MmrPeaks::new(
264 Forest::new(usize::try_from(forest).expect("u64 should fit in usize")),
265 mmr_peaks_nodes,
266 )
267 .map_err(StoreError::MmrError)
268}
269
270fn serialize_block_header(
271 block_header: &BlockHeader,
272 partial_blockchain_peaks: &[Word],
273 has_client_notes: bool,
274) -> SerializedBlockHeaderData {
275 let block_num = block_header.block_num();
276 let header = block_header.to_bytes();
277 let partial_blockchain_peaks = partial_blockchain_peaks.to_bytes();
278
279 SerializedBlockHeaderData {
280 block_num: block_num.as_u32(),
281 header,
282 partial_blockchain_peaks,
283 has_client_notes,
284 }
285}
286
287fn parse_block_headers_columns(
288 row: &rusqlite::Row<'_>,
289) -> Result<SerializedBlockHeaderParts, rusqlite::Error> {
290 let block_num: u32 = row.get(0)?;
291 let header: Vec<u8> = row.get(1)?;
292 let partial_blockchain_peaks: Vec<u8> = row.get(2)?;
293 let has_client_notes: bool = row.get(3)?;
294
295 Ok(SerializedBlockHeaderParts {
296 _block_num: u64::from(block_num),
297 header,
298 _partial_blockchain_peaks: partial_blockchain_peaks,
299 has_client_notes,
300 })
301}
302
303fn parse_block_header(
304 serialized_block_header_parts: &SerializedBlockHeaderParts,
305) -> Result<(BlockHeader, BlockRelevance), StoreError> {
306 Ok((
307 BlockHeader::read_from_bytes(&serialized_block_header_parts.header)?,
308 serialized_block_header_parts.has_client_notes.into(),
309 ))
310}
311
312fn serialize_partial_blockchain_node(
313 id: InOrderIndex,
314 node: Word,
315) -> SerializedPartialBlockchainNodeData {
316 let id = i64::try_from(id.inner()).expect("id is a valid i64");
317 let node = node.to_hex();
318 SerializedPartialBlockchainNodeData { id, node }
319}
320
321fn parse_partial_blockchain_nodes_columns(
322 row: &rusqlite::Row<'_>,
323) -> Result<SerializedPartialBlockchainNodeParts, rusqlite::Error> {
324 let id: u64 = row.get(0)?;
325 let node = row.get(1)?;
326 Ok(SerializedPartialBlockchainNodeParts { id, node })
327}
328
329fn parse_partial_blockchain_nodes(
330 serialized_partial_blockchain_node_parts: &SerializedPartialBlockchainNodeParts,
331) -> Result<(InOrderIndex, Word), StoreError> {
332 let id = InOrderIndex::new(
333 NonZeroUsize::new(
334 usize::try_from(serialized_partial_blockchain_node_parts.id)
335 .expect("id is u64, should not fail"),
336 )
337 .unwrap(),
338 );
339 let node: Word = Word::try_from(&serialized_partial_blockchain_node_parts.node)?;
340 Ok((id, node))
341}
342
343pub(crate) fn set_block_header_has_client_notes(
344 tx: &Transaction<'_>,
345 block_num: u64,
346 has_client_notes: bool,
347) -> Result<(), StoreError> {
348 const QUERY: &str = "\
350 UPDATE block_headers
351 SET has_client_notes=?
352 WHERE block_num=? AND has_client_notes=FALSE;";
353 tx.execute(QUERY, params![has_client_notes, block_num]).into_store_error()?;
354 Ok(())
355}
356
357#[cfg(test)]
358mod test {
359 use std::vec::Vec;
360
361 use miden_client::block::BlockHeader;
362 use miden_client::crypto::{Forest, MmrPeaks};
363 use miden_client::store::Store;
364 use miden_lib::transaction::TransactionKernel;
365
366 use crate::SqliteStore;
367 use crate::tests::create_test_store;
368
369 async fn insert_dummy_block_headers(store: &mut SqliteStore) -> Vec<BlockHeader> {
370 let block_headers: Vec<BlockHeader> = (0..5)
371 .map(|block_num| {
372 BlockHeader::mock(block_num, None, None, &[], TransactionKernel.to_commitment())
373 })
374 .collect();
375
376 let block_headers_clone = block_headers.clone();
377 store
378 .interact_with_connection(move |conn| {
379 let tx = conn.transaction().unwrap();
380 let dummy_peaks = MmrPeaks::new(Forest::empty(), Vec::new()).unwrap();
381 (0..5).for_each(|block_num| {
382 SqliteStore::insert_block_header_tx(
383 &tx,
384 &block_headers_clone[block_num],
385 &dummy_peaks,
386 false,
387 )
388 .unwrap();
389 });
390 tx.commit().unwrap();
391 Ok(())
392 })
393 .await
394 .unwrap();
395
396 block_headers
397 }
398
399 #[tokio::test]
400 async fn insert_and_get_block_headers_by_number() {
401 let mut store = create_test_store().await;
402 let block_headers = insert_dummy_block_headers(&mut store).await;
403
404 let block_header = Store::get_block_header_by_num(&store, 3.into()).await.unwrap().unwrap();
405 assert_eq!(block_headers[3], block_header.0);
406 }
407
408 #[tokio::test]
409 async fn insert_and_get_block_headers_by_list() {
410 let mut store = create_test_store().await;
411 let mock_block_headers = insert_dummy_block_headers(&mut store).await;
412
413 let block_headers: Vec<BlockHeader> =
414 Store::get_block_headers(&store, &[1.into(), 3.into()].into_iter().collect())
415 .await
416 .unwrap()
417 .into_iter()
418 .map(|(block_header, _has_notes)| block_header)
419 .collect();
420 assert_eq!(
421 &[mock_block_headers[1].clone(), mock_block_headers[3].clone()],
422 &block_headers[..]
423 );
424 }
425}