1mod migrations;
2mod models;
3mod schema;
4
5use std::num::NonZeroUsize;
6use std::path::{Path, PathBuf};
7
8use diesel::SqliteConnection;
9use diesel::dsl::{count_star, exists};
10use diesel::prelude::*;
11use miden_node_db::{DatabaseError, Db, SqlTypeConvert};
12use miden_node_utils::tracing::miden_instrument;
13use miden_protocol::block::{BlockHeader, BlockNumber};
14use miden_protocol::transaction::TransactionId;
15use miden_protocol::utils::serde::{Deserializable, Serializable};
16
17use crate::db::migrations::{bootstrap_database, migrate_database, verify_latest_schema};
18use crate::db::models::{BlockHeaderRowInsert, ValidatedTransactionRowInsert};
19use crate::tx_validation::ValidatedTransaction;
20use crate::{COMPONENT, LOG_TARGET};
21
22#[miden_instrument(
24 target = COMPONENT,
25 skip_all,
26)]
27pub async fn load(database_filepath: PathBuf) -> Result<Db, DatabaseError> {
28 load_with_pool_size(database_filepath, miden_node_db::default_connection_pool_size()).await
29}
30
31#[miden_instrument(
34 target = COMPONENT,
35 skip_all,
36)]
37pub async fn load_with_pool_size(
38 database_filepath: PathBuf,
39 connection_pool_size: NonZeroUsize,
40) -> Result<Db, DatabaseError> {
41 verify_latest_schema(&database_filepath)?;
42
43 open_with_pool_size(&database_filepath, connection_pool_size)
44}
45
46#[miden_instrument(
48 target = COMPONENT,
49 skip_all,
50)]
51pub async fn setup(database_filepath: PathBuf) -> Result<Db, DatabaseError> {
52 setup_with_pool_size(database_filepath, miden_node_db::default_connection_pool_size()).await
53}
54
55#[miden_instrument(
57 target = COMPONENT,
58 skip_all,
59)]
60pub async fn setup_with_pool_size(
61 database_filepath: PathBuf,
62 connection_pool_size: NonZeroUsize,
63) -> Result<Db, DatabaseError> {
64 bootstrap_database(&database_filepath)?;
65
66 open_with_pool_size(&database_filepath, connection_pool_size)
67}
68
69#[miden_instrument(
71 target = COMPONENT,
72 skip_all,
73)]
74pub fn migrate(database_filepath: impl AsRef<Path>) -> Result<(), DatabaseError> {
75 migrate_database(database_filepath.as_ref())?;
76 Ok(())
77}
78
79fn open_with_pool_size(
80 database_filepath: &Path,
81 connection_pool_size: NonZeroUsize,
82) -> Result<Db, DatabaseError> {
83 let db = Db::new_with_pool_size(database_filepath, connection_pool_size)?;
84 tracing::info!(
85 target: LOG_TARGET,
86 sqlite= %database_filepath.display(),
87 connection_pool_size = %connection_pool_size,
88 "Connected to the database"
89 );
90 Ok(db)
91}
92
93#[miden_instrument(
95 target = COMPONENT,
96 skip_all,
97 fields(
98 tx_id = %tx_info.tx_id(),
99 ),
100 err,
101)]
102pub(crate) fn insert_transaction(
103 conn: &mut SqliteConnection,
104 tx_info: &ValidatedTransaction,
105) -> Result<usize, DatabaseError> {
106 let row = ValidatedTransactionRowInsert::new(tx_info);
107 let count = diesel::insert_into(schema::validated_transactions::table)
108 .values(row)
109 .on_conflict_do_nothing()
110 .execute(conn)?;
111 Ok(count)
112}
113
114#[miden_instrument(
126 target = COMPONENT,
127 skip(conn),
128 err,
129)]
130pub(crate) fn transaction_exists(
131 conn: &mut SqliteConnection,
132 tx_id: TransactionId,
133) -> Result<bool, DatabaseError> {
134 let exists = diesel::select(exists(
135 schema::validated_transactions::table
136 .filter(schema::validated_transactions::id.eq(tx_id.to_bytes())),
137 ))
138 .get_result::<bool>(conn)?;
139 Ok(exists)
140}
141
142#[miden_instrument(
156 target = COMPONENT,
157 skip(conn),
158 err,
159)]
160pub(crate) fn find_unvalidated_transactions(
161 conn: &mut SqliteConnection,
162 tx_ids: &[TransactionId],
163) -> Result<Vec<TransactionId>, DatabaseError> {
164 let mut unvalidated_tx_ids = Vec::new();
165 for tx_id in tx_ids {
166 let exists = diesel::select(exists(
168 schema::validated_transactions::table
169 .filter(schema::validated_transactions::id.eq(tx_id.to_bytes())),
170 ))
171 .get_result::<bool>(conn)?;
172 if !exists {
174 unvalidated_tx_ids.push(*tx_id);
175 }
176 }
177 Ok(unvalidated_tx_ids)
178}
179
180#[miden_instrument(
185 target = COMPONENT,
186 skip(conn, header),
187 err,
188)]
189pub fn upsert_block_header(
190 conn: &mut SqliteConnection,
191 header: &BlockHeader,
192) -> Result<(), DatabaseError> {
193 let row = BlockHeaderRowInsert {
194 block_num: header.block_num().to_raw_sql(),
195 block_header: header.to_bytes(),
196 };
197 diesel::replace_into(schema::block_headers::table).values(row).execute(conn)?;
198 Ok(())
199}
200
201#[miden_instrument(
205 target = COMPONENT,
206 skip(conn),
207 err,
208)]
209pub fn load_chain_tip(conn: &mut SqliteConnection) -> Result<Option<BlockHeader>, DatabaseError> {
210 let row = schema::block_headers::table
211 .order(schema::block_headers::block_num.desc())
212 .select(schema::block_headers::block_header)
213 .first::<Vec<u8>>(conn)
214 .optional()?;
215
216 row.map(|bytes| {
217 BlockHeader::read_from_bytes(&bytes)
218 .map_err(|err| DatabaseError::deserialization("BlockHeader", err))
219 })
220 .transpose()
221}
222
223#[miden_instrument(
227 target = COMPONENT,
228 skip(conn),
229 err,
230)]
231pub fn load_block_header(
232 conn: &mut SqliteConnection,
233 block_num: BlockNumber,
234) -> Result<Option<BlockHeader>, DatabaseError> {
235 let row = schema::block_headers::table
236 .filter(schema::block_headers::block_num.eq(block_num.to_raw_sql()))
237 .select(schema::block_headers::block_header)
238 .first::<Vec<u8>>(conn)
239 .optional()?;
240
241 row.map(|bytes| {
242 BlockHeader::read_from_bytes(&bytes)
243 .map_err(|err| DatabaseError::deserialization("BlockHeader", err))
244 })
245 .transpose()
246}
247
248#[miden_instrument(
250 target = COMPONENT,
251 skip(conn),
252 err,
253)]
254pub fn count_validated_transactions(conn: &mut SqliteConnection) -> Result<i64, DatabaseError> {
255 let count = schema::validated_transactions::table.select(count_star()).first::<i64>(conn)?;
256 Ok(count)
257}
258
259#[miden_instrument(
261 target = COMPONENT,
262 skip(conn),
263 err,
264)]
265pub fn count_signed_blocks(conn: &mut SqliteConnection) -> Result<i64, DatabaseError> {
266 let count = schema::block_headers::table.select(count_star()).first::<i64>(conn)?;
267 Ok(count)
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
275 fn migrate_rejects_missing_database() {
276 let temp_dir = tempfile::tempdir().expect("failed to create temp directory");
277 let db_path = temp_dir.path().join("validator.sqlite3");
278
279 let err = migrate(db_path.clone()).expect_err("missing database should fail");
280
281 assert!(matches!(err, DatabaseError::Migration(_)), "unexpected error: {err:?}");
282 assert!(!db_path.exists());
283 }
284
285 #[tokio::test]
286 async fn setup_creates_database_that_load_accepts() {
287 let temp_dir = tempfile::tempdir().expect("failed to create temp directory");
288 let db_path = temp_dir.path().join("validator.sqlite3");
289
290 setup(db_path.clone()).await.expect("setup should bootstrap the database");
291 load(db_path).await.expect("load should accept a bootstrapped database");
292 }
293
294 #[tokio::test]
295 async fn transaction_exists_detects_validated_transactions() {
296 use miden_protocol::Word;
297
298 let temp_dir = tempfile::tempdir().expect("failed to create temp directory");
299 let db = setup(temp_dir.path().join("validator.sqlite3")).await.unwrap();
300
301 let validated_id = TransactionId::from_raw(Word::try_from([1u64, 2, 3, 4]).unwrap());
302 let unknown_id = TransactionId::from_raw(Word::try_from([5u64, 6, 7, 8]).unwrap());
303
304 let row = ValidatedTransactionRowInsert {
307 id: validated_id.to_bytes(),
308 block_num: 0,
309 account_id: vec![],
310 account_delta: vec![],
311 input_notes: vec![],
312 output_notes: vec![],
313 initial_account_hash: vec![],
314 final_account_hash: vec![],
315 fee: vec![],
316 };
317 db.transact("insert_row", move |conn| -> Result<usize, DatabaseError> {
318 Ok(diesel::insert_into(schema::validated_transactions::table)
319 .values(row)
320 .execute(conn)?)
321 })
322 .await
323 .unwrap();
324
325 let validated_exists = db
326 .query("transaction_exists", move |conn| transaction_exists(conn, validated_id))
327 .await
328 .unwrap();
329 assert!(validated_exists, "an inserted transaction id should be reported as existing");
330
331 let unknown_exists = db
332 .query("transaction_exists", move |conn| transaction_exists(conn, unknown_id))
333 .await
334 .unwrap();
335 assert!(!unknown_exists, "an unknown transaction id should not be reported as existing");
336 }
337}