1use std::collections::{BTreeMap, BTreeSet, HashSet};
2use std::mem::size_of;
3use std::num::NonZeroUsize;
4use std::ops::{Deref, DerefMut, RangeInclusive};
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
7
8use anyhow::Context;
9use diesel::{Connection, SqliteConnection};
10use miden_crypto::dsa::ecdsa_k256_keccak::Signature;
11use miden_node_proto::domain::account::AccountInfo;
12use miden_node_utils::limiter::MAX_RESPONSE_PAYLOAD_BYTES;
13use miden_node_utils::tracing::miden_instrument;
14use miden_protocol::Word;
15use miden_protocol::account::{AccountHeader, AccountId, AccountStorageHeader, StorageMapKey};
16use miden_protocol::asset::{Asset, AssetVaultKey};
17use miden_protocol::block::{BlockHeader, BlockNoteIndex, BlockNumber, SignedBlock};
18use miden_protocol::crypto::merkle::SparseMerklePath;
19use miden_protocol::note::{
20 NoteAttachments,
21 NoteDetails,
22 NoteId,
23 NoteInclusionProof,
24 NoteMetadata,
25 NoteScript,
26 Nullifier,
27};
28use miden_protocol::transaction::TransactionHeader;
29use miden_protocol::utils::serde::Deserializable;
30use tokio::sync::oneshot;
31use tracing::info;
32
33use crate::db::migrations::{migrate_database, verify_latest_schema};
34use crate::db::models::conv::SqlTypeConvert;
35use crate::db::models::queries;
36pub use crate::db::models::queries::{
37 AccountCommitmentsPage,
38 NullifiersPage,
39 PublicAccountIdsPage,
40 PublicAccountStateRootsPage,
41};
42use crate::db::models::queries::{BlockHeaderCommitment, StorageMapValuesPage};
43use crate::errors::{DatabaseError, NoteSyncError};
44use crate::genesis::GenesisBlock;
45use crate::{COMPONENT, LOG_TARGET};
46
47const STORAGE_MAP_VALUE_PER_ROW_BYTES: usize =
48 2 * size_of::<Word>() + size_of::<u32>() + size_of::<u8>();
49
50fn default_storage_map_entries_limit() -> usize {
51 MAX_RESPONSE_PAYLOAD_BYTES / STORAGE_MAP_VALUE_PER_ROW_BYTES
52}
53
54mod migrations;
55#[cfg(test)]
56pub(crate) use migrations::bootstrap_database;
57
58#[cfg(test)]
59mod tests;
60
61pub(crate) mod models;
62
63pub(crate) mod schema;
68
69pub type Result<T, E = DatabaseError> = std::result::Result<T, E>;
70
71#[derive(Copy, Clone, Debug, PartialEq, Eq)]
73pub struct DatabaseOptions {
74 pub connection_pool_size: NonZeroUsize,
76}
77
78impl Default for DatabaseOptions {
79 fn default() -> Self {
80 Self {
81 connection_pool_size: miden_node_db::default_connection_pool_size(),
82 }
83 }
84}
85
86pub struct Db {
90 db: miden_node_db::Db,
91}
92
93impl Deref for Db {
94 type Target = miden_node_db::Db;
95
96 fn deref(&self) -> &Self::Target {
97 &self.db
98 }
99}
100
101impl DerefMut for Db {
102 fn deref_mut(&mut self) -> &mut Self::Target {
103 &mut self.db
104 }
105}
106
107#[derive(Debug, Clone)]
111pub struct AccountVaultValue {
112 pub block_num: BlockNumber,
113 pub vault_key: AssetVaultKey,
114 pub asset: Option<Asset>,
116}
117
118impl AccountVaultValue {
119 pub fn from_raw_row(row: (i64, Vec<u8>, Option<Vec<u8>>)) -> Result<Self, DatabaseError> {
120 let (block_num, vault_key, asset) = row;
121 let vault_key = Word::read_from_bytes(&vault_key)?;
122 Ok(Self {
123 block_num: BlockNumber::from_raw_sql(block_num)?,
124 vault_key: AssetVaultKey::try_from(vault_key)?,
125 asset: asset.map(|b| Asset::read_from_bytes(&b)).transpose()?,
126 })
127 }
128}
129
130#[derive(Debug, PartialEq)]
131pub struct NullifierInfo {
132 pub nullifier: Nullifier,
133 pub block_num: BlockNumber,
134}
135
136impl PartialEq<(Nullifier, BlockNumber)> for NullifierInfo {
137 fn eq(&self, (nullifier, block_num): &(Nullifier, BlockNumber)) -> bool {
138 &self.nullifier == nullifier && &self.block_num == block_num
139 }
140}
141
142#[derive(Debug, PartialEq)]
143pub struct TransactionRecord {
144 pub block_num: BlockNumber,
145 pub header: TransactionHeader,
146 pub output_note_proofs: Vec<NoteSyncRecord>,
149 pub consumed_note_refs: Vec<(Nullifier, NoteId)>,
152}
153
154#[derive(Debug, Clone, PartialEq)]
155pub struct NoteRecord {
156 pub block_num: BlockNumber,
157 pub note_index: BlockNoteIndex,
158 pub note_id: Word,
159 pub metadata: NoteMetadata,
160 pub details: Option<NoteDetails>,
161 pub attachments: NoteAttachments,
162 pub inclusion_path: SparseMerklePath,
163}
164
165#[derive(Debug, PartialEq)]
166pub struct NoteSyncUpdate {
167 pub notes: Vec<NoteSyncRecord>,
168 pub block_header: BlockHeader,
169}
170
171#[derive(Debug, Clone, PartialEq)]
172pub struct NoteSyncRecord {
173 pub block_num: BlockNumber,
174 pub note_index: BlockNoteIndex,
175 pub note_id: NoteId,
176 pub metadata: NoteMetadata,
177 pub inclusion_path: SparseMerklePath,
178}
179
180impl From<NoteRecord> for NoteSyncRecord {
181 fn from(note: NoteRecord) -> Self {
182 Self {
183 block_num: note.block_num,
184 note_index: note.note_index,
185 note_id: NoteId::from_raw(note.note_id),
186 metadata: note.metadata,
187 inclusion_path: note.inclusion_path,
188 }
189 }
190}
191
192impl Db {
193 #[miden_instrument(
195 target = COMPONENT,
196 name = "store.database.bootstrap",
197 skip_all,
198 fields(path=%database_filepath.display())
199 err,
200 )]
201 pub fn bootstrap(database_filepath: PathBuf, genesis: GenesisBlock) -> anyhow::Result<()> {
202 migrations::bootstrap_database(&database_filepath)
203 .context("failed to bootstrap database schema")?;
204
205 let mut conn: SqliteConnection = diesel::sqlite::SqliteConnection::establish(
206 database_filepath.to_str().context("database filepath is invalid")?,
207 )
208 .context("failed to open a database connection")?;
209
210 miden_node_db::configure_connection_on_creation(&mut conn)?;
211
212 let genesis_block = genesis.into_inner();
214 conn.transaction(move |conn| models::queries::apply_block(conn, &genesis_block, &[]))
215 .context("failed to insert genesis block")?;
216 Ok(())
217 }
218
219 #[miden_instrument(
221 target = COMPONENT,
222 skip_all,
223 )]
224 pub async fn load(database_filepath: PathBuf) -> Result<Self, DatabaseError> {
225 Self::load_with_pool_size(database_filepath, miden_node_db::default_connection_pool_size())
226 .await
227 }
228
229 #[miden_instrument(
232 target = COMPONENT,
233 skip_all,
234 )]
235 pub async fn load_with_pool_size(
236 database_filepath: PathBuf,
237 connection_pool_size: NonZeroUsize,
238 ) -> Result<Self, DatabaseError> {
239 verify_latest_schema(&database_filepath)?;
240
241 let db = miden_node_db::Db::new_with_pool_size(&database_filepath, connection_pool_size)?;
242 info!(
243 target: LOG_TARGET,
244 sqlite= %database_filepath.display(),
245 connection_pool_size = %connection_pool_size,
246 "Connected to the database"
247 );
248
249 Ok(Self { db })
250 }
251
252 #[miden_instrument(
254 target = COMPONENT,
255 skip_all,
256 )]
257 pub fn migrate(database_filepath: impl AsRef<Path>) -> Result<(), DatabaseError> {
258 migrate_database(database_filepath.as_ref())?;
259 Ok(())
260 }
261
262 #[miden_instrument(
264 level = "debug",
265 target = COMPONENT,
266 skip_all,
267 err,
268 )]
269 pub async fn select_nullifiers_paged(
270 &self,
271 page_size: std::num::NonZeroUsize,
272 after_nullifier: Option<Nullifier>,
273 ) -> Result<NullifiersPage> {
274 self.transact("read nullifiers paged", move |conn| {
275 queries::select_nullifiers_paged(conn, page_size, after_nullifier)
276 })
277 .await
278 }
279
280 #[miden_instrument(
282 level = "debug",
283 target = COMPONENT,
284 skip_all,
285 fields(
286 prefix_len,
287 prefixes = nullifier_prefixes.len(),
288 ),
289 err,
290 )]
291 pub async fn select_nullifiers_by_prefix(
292 &self,
293 prefix_len: u32,
294 nullifier_prefixes: Vec<u32>,
295 block_range: RangeInclusive<BlockNumber>,
296 ) -> Result<(Vec<NullifierInfo>, BlockNumber)> {
297 assert_eq!(prefix_len, 16, "Only 16-bit prefixes are supported");
298
299 self.transact("nullifieres by prefix", move |conn| {
300 let nullifier_prefixes =
301 Vec::from_iter(nullifier_prefixes.into_iter().map(|prefix| prefix as u16));
302 queries::select_nullifiers_by_prefix(
303 conn,
304 prefix_len as u8,
305 &nullifier_prefixes[..],
306 block_range,
307 )
308 })
309 .await
310 }
311
312 #[miden_instrument(
316 level = "debug",
317 target = COMPONENT,
318 skip_all,
319 err,
320 )]
321 pub async fn select_block_header_by_block_num(
322 &self,
323 maybe_block_number: Option<BlockNumber>,
324 ) -> Result<Option<BlockHeader>> {
325 self.transact("block headers by block number", move |conn| {
326 let val = queries::select_block_header_by_block_num(conn, maybe_block_number)?;
327 Ok(val)
328 })
329 .await
330 }
331
332 #[miden_instrument(
334 level = "debug",
335 target = COMPONENT,
336 skip_all,
337 err,
338 )]
339 pub async fn select_block_header_and_signature_by_block_num(
340 &self,
341 block_number: BlockNumber,
342 ) -> Result<Option<(BlockHeader, Signature)>> {
343 self.transact("block headers and signature by block number", move |conn| {
344 let val = queries::select_block_header_and_signature_by_block_num(conn, block_number)?;
345 Ok(val)
346 })
347 .await
348 }
349
350 #[miden_instrument(
352 level = "debug",
353 target = COMPONENT,
354 skip_all,
355 err,
356 )]
357 pub async fn select_block_headers(
358 &self,
359 blocks: impl Iterator<Item = BlockNumber> + Send + 'static,
360 ) -> Result<Vec<BlockHeader>> {
361 self.transact("block headers from given block numbers", move |conn| {
362 let raw = queries::select_block_headers(conn, blocks)?;
363 Ok(raw)
364 })
365 .await
366 }
367
368 #[miden_instrument(
370 level = "debug",
371 target = COMPONENT,
372 skip_all,
373 err,
374 )]
375 pub async fn select_all_block_header_commitments(&self) -> Result<Vec<BlockHeaderCommitment>> {
376 self.transact("all block headers", |conn| {
377 let raw = queries::select_all_block_header_commitments(conn)?;
378 Ok(raw)
379 })
380 .await
381 }
382
383 #[miden_instrument(
385 level = "debug",
386 target = COMPONENT,
387 skip_all,
388 err,
389 )]
390 pub async fn select_account_commitments_paged(
391 &self,
392 page_size: std::num::NonZeroUsize,
393 after_account_id: Option<AccountId>,
394 ) -> Result<AccountCommitmentsPage> {
395 self.transact("read account commitments paged", move |conn| {
396 queries::select_account_commitments_paged(conn, page_size, after_account_id)
397 })
398 .await
399 }
400
401 #[miden_instrument(
403 level = "debug",
404 target = COMPONENT,
405 skip_all,
406 err,
407 )]
408 pub async fn select_public_account_ids_paged(
409 &self,
410 page_size: std::num::NonZeroUsize,
411 after_account_id: Option<AccountId>,
412 ) -> Result<PublicAccountIdsPage> {
413 self.transact("read public account IDs paged", move |conn| {
414 queries::select_public_account_ids_paged(conn, page_size, after_account_id)
415 })
416 .await
417 }
418
419 #[miden_instrument(
421 level = "debug",
422 target = COMPONENT,
423 skip_all,
424 err,
425 )]
426 pub async fn select_public_account_state_roots_paged(
427 &self,
428 page_size: std::num::NonZeroUsize,
429 after_account_id: Option<AccountId>,
430 ) -> Result<PublicAccountStateRootsPage> {
431 self.transact("read public account state roots paged", move |conn| {
432 queries::select_public_account_state_roots_paged(conn, page_size, after_account_id)
433 })
434 .await
435 }
436
437 #[miden_instrument(
439 level = "debug",
440 target = COMPONENT,
441 skip_all,
442 err,
443 )]
444 pub async fn select_account(&self, id: AccountId) -> Result<AccountInfo> {
445 self.transact("Get account details", move |conn| queries::select_account(conn, id))
446 .await
447 }
448
449 #[miden_instrument(
451 level = "debug",
452 target = COMPONENT,
453 skip_all,
454 err,
455 )]
456 pub async fn select_network_accounts_subset(
457 &self,
458 account_ids: Vec<AccountId>,
459 ) -> Result<HashSet<AccountId>> {
460 self.transact("Filter network accounts subset", move |conn| {
461 queries::select_network_accounts_subset(conn, &account_ids)
462 })
463 .await
464 }
465
466 #[miden_instrument(
470 target = COMPONENT,
471 skip_all,
472 )]
473 pub async fn select_account_code_by_commitment(
474 &self,
475 code_commitment: Word,
476 ) -> Result<Option<Vec<u8>>> {
477 self.transact("Get account code by commitment", move |conn| {
478 queries::select_account_code_by_commitment(conn, code_commitment)
479 })
480 .await
481 }
482
483 #[miden_instrument(
488 target = COMPONENT,
489 skip_all,
490 )]
491 pub async fn select_account_header_with_storage_header_at_block(
492 &self,
493 account_id: AccountId,
494 block_num: BlockNumber,
495 ) -> Result<Option<(AccountHeader, AccountStorageHeader)>> {
496 self.transact("Get account header with storage header at block", move |conn| {
497 queries::select_account_header_with_storage_header_at_block(conn, account_id, block_num)
498 })
499 .await
500 }
501
502 #[miden_instrument(
503 level = "debug",
504 target = COMPONENT,
505 skip_all,
506 err,
507 )]
508 pub async fn get_note_sync_multi(
509 &self,
510 block_range: RangeInclusive<BlockNumber>,
511 note_tags: Arc<[u32]>,
512 ) -> Result<Vec<NoteSyncUpdate>, NoteSyncError> {
513 self.transact("notes sync task", move |conn| {
514 queries::get_note_sync_multi(conn, ¬e_tags, block_range, MAX_RESPONSE_PAYLOAD_BYTES)
515 })
516 .await
517 }
518
519 #[miden_instrument(
522 level = "debug",
523 target = COMPONENT,
524 skip_all,
525 err,
526 )]
527 pub async fn select_notes_by_id(&self, note_ids: Vec<NoteId>) -> Result<Vec<NoteRecord>> {
528 self.transact("note by id", move |conn| {
529 queries::select_notes_by_id(conn, note_ids.as_slice())
530 })
531 .await
532 }
533
534 #[miden_instrument(
536 level = "debug",
537 target = COMPONENT,
538 skip_all,
539 err,
540 )]
541 pub async fn select_existing_note_commitments(
542 &self,
543 note_commitments: Vec<Word>,
544 ) -> Result<HashSet<Word>> {
545 self.transact("note by commitment", move |conn| {
546 queries::select_existing_note_commitments(conn, note_commitments.as_slice())
547 })
548 .await
549 }
550
551 #[miden_instrument(
553 level = "debug",
554 target = COMPONENT,
555 skip_all,
556 err,
557 )]
558 pub async fn select_note_inclusion_proofs(
559 &self,
560 note_commitments: BTreeSet<Word>,
561 ) -> Result<BTreeMap<NoteId, NoteInclusionProof>> {
562 self.transact("block note inclusion proofs by commitment", move |conn| {
563 models::queries::select_note_inclusion_proofs(conn, ¬e_commitments)
564 })
565 .await
566 }
567
568 #[miden_instrument(
574 target = COMPONENT,
575 skip_all,
576 err,
577 )]
578 pub async fn apply_block(
579 &self,
580 allow_acquire: oneshot::Sender<()>,
581 acquire_done: oneshot::Receiver<()>,
582 signed_block: SignedBlock,
583 notes: Vec<(NoteRecord, Option<Nullifier>)>,
584 ) -> Result<()> {
585 self.transact("apply block", move |conn| -> Result<()> {
586 models::queries::apply_block(conn, &signed_block, ¬es)?;
587
588 {
591 let _span = tracing::info_span!(target: COMPONENT, "acquire_write_lock").entered();
592 if allow_acquire.send(()).is_err() {
593 tracing::warn!(target: COMPONENT, "failed to send notification for successful block application, potential deadlock");
594 }
595 }
596
597 models::queries::prune_history(conn, signed_block.header().block_num())?;
598
599 let _span =
600 tracing::info_span!(target: COMPONENT, "acquire_done_lock").entered();
601 acquire_done.blocking_recv()?;
602
603 Ok(())
604 })
605 .await
606 }
607
608 pub(crate) async fn select_storage_map_sync_values(
613 &self,
614 account_id: AccountId,
615 block_range: RangeInclusive<BlockNumber>,
616 entries_limit: Option<usize>,
617 ) -> Result<StorageMapValuesPage> {
618 let entries_limit = entries_limit.unwrap_or_else(default_storage_map_entries_limit);
619
620 self.transact("select storage map sync values", move |conn| {
621 models::queries::select_account_storage_map_values_paged(
622 conn,
623 account_id,
624 block_range,
625 entries_limit,
626 )
627 })
628 .await
629 }
630
631 #[miden_instrument(
640 target = COMPONENT,
641 skip_all,
642 )]
643 pub(crate) async fn reconstruct_storage_map_from_db(
644 &self,
645 account_id: AccountId,
646 slot_name: miden_protocol::account::StorageSlotName,
647 block_num: BlockNumber,
648 entries_limit: Option<usize>,
649 ) -> Result<miden_node_proto::domain::account::AccountStorageMapDetails> {
650 use miden_node_proto::domain::account::{AccountStorageMapDetails, StorageMapEntries};
651 use miden_protocol::EMPTY_WORD;
652
653 let mut values = Vec::new();
656 let mut block_range_start = BlockNumber::GENESIS;
657 let entries_limit = entries_limit.unwrap_or_else(default_storage_map_entries_limit);
658
659 let mut page = self
660 .select_storage_map_sync_values(
661 account_id,
662 block_range_start..=block_num,
663 Some(entries_limit),
664 )
665 .await?;
666
667 values.extend(page.values);
668 let mut last_block_included = page.last_block_included;
669
670 if values.is_empty() && last_block_included == block_range_start {
673 return Ok(AccountStorageMapDetails::limit_exceeded(slot_name));
674 }
675
676 loop {
677 if page.last_block_included == block_num || page.last_block_included < block_range_start
678 {
679 break;
680 }
681
682 block_range_start = page.last_block_included.child();
683 page = self
684 .select_storage_map_sync_values(
685 account_id,
686 block_range_start..=block_num,
687 Some(entries_limit),
688 )
689 .await?;
690
691 if page.last_block_included <= last_block_included {
692 return Ok(AccountStorageMapDetails::limit_exceeded(slot_name));
693 }
694
695 last_block_included = page.last_block_included;
696 values.extend(page.values);
697 }
698
699 if page.last_block_included != block_num {
700 return Ok(AccountStorageMapDetails::limit_exceeded(slot_name));
701 }
702
703 let mut latest_values = BTreeMap::<StorageMapKey, Word>::new();
705 for value in values {
706 if value.slot_name == slot_name {
707 let raw_key = value.key;
708 latest_values.insert(raw_key, value.value);
709 }
710 }
711
712 latest_values.retain(|_, v| *v != EMPTY_WORD);
714
715 if latest_values.len() > AccountStorageMapDetails::MAX_RETURN_ENTRIES {
716 return Ok(AccountStorageMapDetails::limit_exceeded(slot_name));
717 }
718
719 let entries = Vec::from_iter(latest_values.into_iter());
720 Ok(AccountStorageMapDetails {
721 slot_name,
722 entries: StorageMapEntries::AllEntries(entries),
723 })
724 }
725
726 pub async fn get_account_vault_sync(
727 &self,
728 account_id: AccountId,
729 block_range: RangeInclusive<BlockNumber>,
730 ) -> Result<(BlockNumber, Vec<AccountVaultValue>)> {
731 self.transact("account vault sync", move |conn| {
732 queries::select_account_vault_assets(conn, account_id, block_range)
733 })
734 .await
735 }
736
737 pub async fn select_note_script_by_root(&self, root: Word) -> Result<Option<NoteScript>> {
739 self.transact("note script by root", move |conn| {
740 queries::select_note_script_by_root(conn, root)
741 })
742 .await
743 }
744
745 pub async fn select_transactions_records(
752 &self,
753 account_ids: Vec<AccountId>,
754 block_range: RangeInclusive<BlockNumber>,
755 ) -> Result<(BlockNumber, Vec<TransactionRecord>)> {
756 self.transact("full transactions records", move |conn| {
757 queries::select_transactions_records(conn, &account_ids, block_range)
758 })
759 .await
760 }
761}