miden_client_sqlite_store/
chain_data.rs

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
44// PARTIAL BLOCKCHAIN NODE FILTER HELPERS
45// --------------------------------------------------------------------------------------------
46
47fn 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            // SAFETY: d.inner() is a usize casted to u64, should not fail.
123                .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    /// Inserts a list of MMR authentication nodes to the Partial Blockchain nodes table.
174    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    /// Inserts a block header using a [`rusqlite::Transaction`].
185    ///
186    /// If the block header exists and `has_client_notes` is `true` then the `has_client_notes`
187    /// column is updated to `true` to signify that the block now contains a relevant note.
188    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    /// Removes block headers that do not contain any client notes and aren't the genesis or last
217    /// block.
218    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
244// HELPERS
245// ================================================================================================
246
247/// Inserts a node represented by its in-order index and the node value.
248fn 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    // Only update to change has_client_notes to true if it was false previously
349    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}