1use std::collections::{BTreeMap, HashMap, HashSet};
2use std::num::NonZeroUsize;
3use std::ops::RangeInclusive;
4
5use diesel::prelude::{Queryable, QueryableByName};
6use diesel::query_dsl::methods::SelectDsl;
7use diesel::sqlite::Sqlite;
8use diesel::{
9 AsChangeset,
10 BoolExpressionMethods,
11 ExpressionMethods,
12 Insertable,
13 JoinOnDsl,
14 NullableExpressionMethods,
15 OptionalExtension,
16 QueryDsl,
17 RunQueryDsl,
18 Selectable,
19 SelectableHelper,
20 SqliteConnection,
21};
22use miden_node_proto::domain::account::{AccountInfo, AccountSummary, AccountVaultDetails};
23use miden_node_utils::limiter::{
24 MAX_RESPONSE_PAYLOAD_BYTES,
25 QueryParamAccountIdLimit,
26 QueryParamLimiter,
27};
28use miden_node_utils::tracing::miden_instrument;
29use miden_protocol::Word;
30use miden_protocol::account::{
31 Account,
32 AccountCode,
33 AccountId,
34 AccountPatch,
35 AccountStorage,
36 AccountStorageHeader,
37 AccountUpdateDetails,
38 StorageMap,
39 StorageMapKey,
40 StorageMapPatchEntries,
41 StorageSlot,
42 StorageSlotContent,
43 StorageSlotName,
44 StorageSlotType,
45};
46use miden_protocol::asset::{Asset, AssetId, AssetVault};
47use miden_protocol::block::{BlockAccountUpdate, BlockNumber};
48use miden_protocol::utils::serde::{Deserializable, Serializable};
49use miden_standards::account::auth::NetworkAccount;
50
51use crate::COMPONENT;
52use crate::db::models::conv::{SqlTypeConvert, nonce_to_raw_sql, raw_sql_to_nonce};
53#[cfg(test)]
54use crate::db::models::vec_raw_try_into;
55use crate::db::{AccountVaultValue, schema};
56use crate::errors::DatabaseError;
57
58mod at_block;
59pub(crate) use at_block::select_account_header_with_storage_header_at_block;
60
61mod delta;
62use delta::{
63 AccountStateForInsert,
64 PartialAccountState,
65 apply_storage_patch,
66 select_latest_vault_assets,
67 select_minimal_account_state_headers,
68};
69
70#[cfg(test)]
71mod tests;
72
73type StorageMapValueRow = (i64, String, Vec<u8>, Vec<u8>);
74type StorageHeaderWithEntries =
75 (AccountStorageHeader, HashMap<StorageSlotName, BTreeMap<StorageMapKey, Word>>);
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub(crate) enum NetworkAccountType {
83 None,
85 Network,
87}
88
89pub(crate) fn select_account_code_by_commitment(
104 conn: &mut SqliteConnection,
105 code_commitment: Word,
106) -> Result<Option<Vec<u8>>, DatabaseError> {
107 use schema::account_codes;
108
109 let code_commitment_bytes = code_commitment.to_bytes();
110
111 let result: Option<Vec<u8>> = SelectDsl::select(
112 account_codes::table.filter(account_codes::code_commitment.eq(&code_commitment_bytes)),
113 account_codes::code,
114 )
115 .first(conn)
116 .optional()?;
117
118 Ok(result)
119}
120
121pub(crate) fn select_account(
144 conn: &mut SqliteConnection,
145 account_id: AccountId,
146) -> Result<AccountInfo, DatabaseError> {
147 let raw = SelectDsl::select(schema::accounts::table, AccountSummaryRaw::as_select())
148 .filter(schema::accounts::account_id.eq(account_id.to_bytes()))
149 .filter(schema::accounts::is_latest.eq(true))
150 .get_result::<AccountSummaryRaw>(conn)
151 .optional()?
152 .ok_or(DatabaseError::AccountNotFoundInDb(account_id))?;
153
154 let summary: AccountSummary = raw.try_into()?;
155
156 let details = if account_id.is_public() {
159 Some(select_full_account(conn, account_id)?)
160 } else {
161 None
162 };
163
164 Ok(AccountInfo { summary, details })
165}
166
167pub(crate) fn select_full_account(
183 conn: &mut SqliteConnection,
184 account_id: AccountId,
185) -> Result<Account, DatabaseError> {
186 let joined = schema::accounts::table.inner_join(schema::account_codes::table.on(
188 schema::accounts::code_commitment.eq(schema::account_codes::code_commitment.nullable()),
189 ));
190
191 let (nonce, code_bytes): (Option<i64>, Vec<u8>) =
192 SelectDsl::select(joined, (schema::accounts::nonce, schema::account_codes::code))
193 .filter(schema::accounts::account_id.eq(account_id.to_bytes()))
194 .filter(schema::accounts::is_latest.eq(true))
195 .get_result(conn)
196 .optional()?
197 .ok_or(DatabaseError::AccountNotFoundInDb(account_id))?;
198
199 let nonce = raw_sql_to_nonce(nonce.ok_or_else(|| {
200 DatabaseError::DataCorrupted(format!("No nonce found for account {account_id}"))
201 })?);
202
203 let code = AccountCode::read_from_bytes(&code_bytes)?;
204
205 let storage = select_latest_account_storage(conn, account_id)?;
207
208 let vault_entries: Vec<(Vec<u8>, Option<Vec<u8>>)> = SelectDsl::select(
210 schema::account_vault_assets::table,
211 (schema::account_vault_assets::vault_key, schema::account_vault_assets::asset),
212 )
213 .filter(schema::account_vault_assets::account_id.eq(account_id.to_bytes()))
214 .filter(schema::account_vault_assets::is_latest.eq(true))
215 .load(conn)?;
216
217 let mut assets = Vec::new();
218 for (_key_bytes, maybe_asset_bytes) in vault_entries {
219 if let Some(asset_bytes) = maybe_asset_bytes {
220 let asset = Asset::read_from_bytes(&asset_bytes)?;
221 assets.push(asset);
222 }
223 }
224
225 let vault = AssetVault::new(&assets)?;
226
227 Ok(Account::new(account_id, vault, storage, code, nonce, None)?)
228}
229
230#[derive(Debug)]
232pub struct AccountCommitmentsPage {
233 pub commitments: Vec<(AccountId, Word)>,
235 pub next_cursor: Option<AccountId>,
237}
238
239pub(crate) fn select_account_commitments_paged(
260 conn: &mut SqliteConnection,
261 page_size: NonZeroUsize,
262 after_account_id: Option<AccountId>,
263) -> Result<AccountCommitmentsPage, DatabaseError> {
264 #[expect(clippy::cast_possible_wrap)]
266 let limit = (page_size.get() + 1) as i64;
267
268 let mut query = SelectDsl::select(
269 schema::accounts::table,
270 (schema::accounts::account_id, schema::accounts::account_commitment),
271 )
272 .filter(schema::accounts::is_latest.eq(true))
273 .order_by(schema::accounts::account_id.asc())
274 .limit(limit)
275 .into_boxed();
276
277 if let Some(cursor) = after_account_id {
278 query = query.filter(schema::accounts::account_id.gt(cursor.to_bytes()));
279 }
280
281 let raw = query.load::<(Vec<u8>, Vec<u8>)>(conn)?;
282
283 let mut commitments = Result::<Vec<_>, DatabaseError>::from_iter(raw.into_iter().map(
284 |(ref account, ref commitment)| {
285 Ok((AccountId::read_from_bytes(account)?, Word::read_from_bytes(commitment)?))
286 },
287 ))?;
288
289 let next_cursor = if commitments.len() > page_size.get() {
291 commitments.pop(); commitments.last().map(|(id, _)| *id)
293 } else {
294 None
295 };
296
297 Ok(AccountCommitmentsPage { commitments, next_cursor })
298}
299
300#[derive(Debug)]
302pub struct PublicAccountIdsPage {
303 pub account_ids: Vec<AccountId>,
305 pub next_cursor: Option<AccountId>,
307}
308
309#[derive(Debug)]
311pub struct PublicAccountStateRoots {
312 pub account_id: AccountId,
313 pub vault_root: Word,
314 pub storage_header: AccountStorageHeader,
315}
316
317#[derive(Debug)]
319pub struct PublicAccountStateRootsPage {
320 pub accounts: Vec<PublicAccountStateRoots>,
322 pub next_cursor: Option<AccountId>,
324}
325
326pub(crate) fn select_public_account_ids_paged(
351 conn: &mut SqliteConnection,
352 page_size: NonZeroUsize,
353 after_account_id: Option<AccountId>,
354) -> Result<PublicAccountIdsPage, DatabaseError> {
355 #[expect(clippy::cast_possible_wrap)]
356 let limit = (page_size.get() + 1) as i64;
357
358 let mut query = SelectDsl::select(schema::accounts::table, schema::accounts::account_id)
359 .filter(schema::accounts::is_latest.eq(true))
360 .filter(schema::accounts::code_commitment.is_not_null())
361 .order_by(schema::accounts::account_id.asc())
362 .limit(limit)
363 .into_boxed();
364
365 if let Some(cursor) = after_account_id {
366 query = query.filter(schema::accounts::account_id.gt(cursor.to_bytes()));
367 }
368
369 let raw = query.load::<Vec<u8>>(conn)?;
370
371 let mut account_ids: Vec<AccountId> = Result::from_iter(raw.into_iter().map(|bytes| {
372 AccountId::read_from_bytes(&bytes).map_err(DatabaseError::DeserializationError)
373 }))?;
374
375 let next_cursor = if account_ids.len() > page_size.get() {
377 account_ids.pop(); account_ids.last().copied()
379 } else {
380 None
381 };
382
383 Ok(PublicAccountIdsPage { account_ids, next_cursor })
384}
385
386pub(crate) fn select_public_account_state_roots_paged(
413 conn: &mut SqliteConnection,
414 page_size: NonZeroUsize,
415 after_account_id: Option<AccountId>,
416) -> Result<PublicAccountStateRootsPage, DatabaseError> {
417 #[expect(clippy::cast_possible_wrap)]
418 let limit = (page_size.get() + 1) as i64;
419
420 let mut query = SelectDsl::select(
421 schema::accounts::table,
422 (
423 schema::accounts::account_id,
424 schema::accounts::vault_root,
425 schema::accounts::storage_header,
426 ),
427 )
428 .filter(schema::accounts::is_latest.eq(true))
429 .filter(schema::accounts::code_commitment.is_not_null())
430 .order_by(schema::accounts::account_id.asc())
431 .limit(limit)
432 .into_boxed();
433
434 if let Some(cursor) = after_account_id {
435 query = query.filter(schema::accounts::account_id.gt(cursor.to_bytes()));
436 }
437
438 let raw = query.load::<(Vec<u8>, Option<Vec<u8>>, Option<Vec<u8>>)>(conn)?;
439
440 let mut accounts: Vec<PublicAccountStateRoots> = Result::from_iter(raw.into_iter().map(
441 |(account_id_bytes, vault_root_bytes, storage_header_bytes)| {
442 let account_id = AccountId::read_from_bytes(&account_id_bytes)
443 .map_err(DatabaseError::DeserializationError)?;
444 let vault_root_bytes = vault_root_bytes.ok_or_else(|| {
445 DatabaseError::DataCorrupted(format!(
446 "public account {account_id} is missing a vault root"
447 ))
448 })?;
449 let storage_header_bytes = storage_header_bytes.ok_or_else(|| {
450 DatabaseError::DataCorrupted(format!(
451 "public account {account_id} is missing a storage header"
452 ))
453 })?;
454
455 Ok::<_, DatabaseError>(PublicAccountStateRoots {
456 account_id,
457 vault_root: Word::read_from_bytes(&vault_root_bytes)?,
458 storage_header: AccountStorageHeader::read_from_bytes(&storage_header_bytes)?,
459 })
460 },
461 ))?;
462
463 let next_cursor = if accounts.len() > page_size.get() {
465 accounts.pop();
466 accounts.last().map(|account| account.account_id)
467 } else {
468 None
469 };
470
471 Ok(PublicAccountStateRootsPage { accounts, next_cursor })
472}
473
474pub(crate) fn select_account_vault_assets(
502 conn: &mut SqliteConnection,
503 account_id: AccountId,
504 block_range: RangeInclusive<BlockNumber>,
505) -> Result<(BlockNumber, Vec<AccountVaultValue>), DatabaseError> {
506 use schema::account_vault_assets as t;
507 const ROW_OVERHEAD_BYTES: usize = 2 * size_of::<Word>() + size_of::<u32>(); const MAX_ROWS: usize = MAX_RESPONSE_PAYLOAD_BYTES / ROW_OVERHEAD_BYTES;
511
512 if !account_id.is_public() {
513 return Err(DatabaseError::AccountNotPublic(account_id));
514 }
515
516 if block_range.is_empty() {
517 return Err(DatabaseError::InvalidBlockRange {
518 from: *block_range.start(),
519 to: *block_range.end(),
520 });
521 }
522
523 let raw: Vec<(i64, Vec<u8>, Option<Vec<u8>>)> =
524 SelectDsl::select(t::table, (t::block_num, t::vault_key, t::asset))
525 .filter(
526 t::account_id
527 .eq(account_id.to_bytes())
528 .and(t::block_num.ge(block_range.start().to_raw_sql()))
529 .and(t::block_num.le(block_range.end().to_raw_sql())),
530 )
531 .order(t::block_num.asc())
532 .limit(i64::try_from(MAX_ROWS + 1).expect("should fit within i64"))
533 .load::<(i64, Vec<u8>, Option<Vec<u8>>)>(conn)?;
534
535 let (last_block_included, values) = if let Some(&(last_block_num, ..)) = raw.last()
538 && raw.len() > MAX_ROWS
539 {
540 let values = raw
541 .into_iter()
542 .take_while(|(bn, ..)| *bn != last_block_num)
543 .map(AccountVaultValue::from_raw_row)
544 .collect::<Result<Vec<_>, DatabaseError>>()?;
545
546 let last_block_included = values.last().map_or(*block_range.start(), |v| v.block_num);
547
548 (last_block_included, values)
549 } else {
550 (
551 *block_range.end(),
552 raw.into_iter().map(AccountVaultValue::from_raw_row).collect::<Result<_, _>>()?,
553 )
554 };
555
556 Ok((last_block_included, values))
557}
558
559pub(crate) fn select_account_vault_at_block(
577 conn: &mut SqliteConnection,
578 account_id: AccountId,
579 block_num: BlockNumber,
580) -> Result<Vec<Asset>, DatabaseError> {
581 use diesel::sql_types::{BigInt, Binary};
582
583 let account_id_bytes = account_id.to_bytes();
584 let block_num_sql = block_num.to_raw_sql();
585 let limit_sql =
586 i64::try_from(AccountVaultDetails::MAX_RETURN_ENTRIES + 1).expect("should fit within i64");
587
588 let entries: Vec<Option<Vec<u8>>> = diesel::sql_query(
589 r"
590 SELECT a.asset FROM account_vault_assets a
591 INNER JOIN (
592 SELECT vault_key, MAX(block_num) as max_block
593 FROM account_vault_assets
594 WHERE account_id = ? AND block_num <= ?
595 GROUP BY vault_key
596 ) latest ON a.vault_key = latest.vault_key AND a.block_num = latest.max_block
597 WHERE a.account_id = ?
598 LIMIT ?
599 ",
600 )
601 .bind::<Binary, _>(&account_id_bytes)
602 .bind::<BigInt, _>(block_num_sql)
603 .bind::<Binary, _>(&account_id_bytes)
604 .bind::<BigInt, _>(limit_sql)
605 .load::<AssetRow>(conn)?
606 .into_iter()
607 .map(|row| row.asset)
608 .collect();
609
610 let mut assets = Vec::new();
612 for asset_bytes in entries.into_iter().flatten() {
613 let asset = Asset::read_from_bytes(&asset_bytes)?;
614 assets.push(asset);
615 }
616
617 Ok(assets)
618}
619
620#[derive(QueryableByName)]
621struct AssetRow {
622 #[diesel(sql_type = diesel::sql_types::Nullable<diesel::sql_types::Binary>)]
623 asset: Option<Vec<u8>>,
624}
625
626#[cfg(test)]
647pub(crate) fn select_all_accounts(
648 conn: &mut SqliteConnection,
649) -> Result<Vec<AccountInfo>, DatabaseError> {
650 let raw = SelectDsl::select(schema::accounts::table, AccountSummaryRaw::as_select())
651 .filter(schema::accounts::is_latest.eq(true))
652 .order_by(schema::accounts::block_num.asc())
653 .load::<AccountSummaryRaw>(conn)?;
654
655 let summaries: Vec<AccountSummary> = vec_raw_try_into(raw)?;
656
657 let account_infos = summaries
659 .into_iter()
660 .map(|summary| {
661 let account_id = summary.account_id;
662 let details = select_full_account(conn, account_id).ok();
663 AccountInfo { summary, details }
664 })
665 .collect();
666
667 Ok(account_infos)
668}
669
670#[derive(Debug, Clone, PartialEq, Eq)]
671pub struct StorageMapValue {
672 pub block_num: BlockNumber,
673 pub slot_name: StorageSlotName,
674 pub key: StorageMapKey,
675 pub value: Word,
676}
677
678#[derive(Debug, Clone, PartialEq, Eq)]
679pub struct StorageMapValuesPage {
680 pub last_block_included: BlockNumber,
682 pub values: Vec<StorageMapValue>,
684}
685
686impl StorageMapValue {
687 pub fn from_raw_row(row: StorageMapValueRow) -> Result<Self, DatabaseError> {
688 let (block_num, slot_name, key, value) = row;
689 Ok(Self {
690 block_num: BlockNumber::from_raw_sql(block_num)?,
691 slot_name: StorageSlotName::from_raw_sql(slot_name)?,
692 key: StorageMapKey::read_from_bytes(&key)?,
693 value: Word::read_from_bytes(&value)?,
694 })
695 }
696}
697
698pub(crate) fn select_account_storage_map_values_paged(
740 conn: &mut SqliteConnection,
741 account_id: AccountId,
742 block_range: RangeInclusive<BlockNumber>,
743 limit: usize,
744) -> Result<StorageMapValuesPage, DatabaseError> {
745 use schema::account_storage_map_values as t;
746
747 if !account_id.is_public() {
748 return Err(DatabaseError::AccountNotPublic(account_id));
749 }
750
751 if block_range.is_empty() {
752 return Err(DatabaseError::InvalidBlockRange {
753 from: *block_range.start(),
754 to: *block_range.end(),
755 });
756 }
757
758 let raw: Vec<StorageMapValueRow> =
759 SelectDsl::select(t::table, (t::block_num, t::slot_name, t::key, t::value))
760 .filter(
761 t::account_id
762 .eq(account_id.to_bytes())
763 .and(t::block_num.ge(block_range.start().to_raw_sql()))
764 .and(t::block_num.le(block_range.end().to_raw_sql())),
765 )
766 .order(t::block_num.asc())
767 .limit(i64::try_from(limit + 1).expect("limit fits within i64"))
768 .load(conn)?;
769
770 let (last_block_included, values) = if let Some(&(last_block_num, ..)) = raw.last()
773 && raw.len() > limit
774 {
775 let values = raw
776 .into_iter()
777 .take_while(|(bn, ..)| *bn != last_block_num)
778 .map(StorageMapValue::from_raw_row)
779 .collect::<Result<Vec<_>, DatabaseError>>()?;
780
781 let last_block_included = values.last().map_or(*block_range.start(), |v| v.block_num);
782
783 (last_block_included, values)
784 } else {
785 (
786 *block_range.end(),
787 raw.into_iter()
788 .map(StorageMapValue::from_raw_row)
789 .collect::<Result<Vec<_>, _>>()?,
790 )
791 };
792
793 Ok(StorageMapValuesPage { last_block_included, values })
794}
795
796pub(crate) fn select_latest_account_storage(
802 conn: &mut SqliteConnection,
803 account_id: AccountId,
804) -> Result<AccountStorage, DatabaseError> {
805 let (storage_header, map_entries_by_slot) =
806 select_latest_account_storage_components(conn, account_id)?;
807 let slots =
809 Result::<Vec<_>, DatabaseError>::from_iter(storage_header.slots().map(|slot_header| {
810 let slot = match slot_header.slot_type() {
811 StorageSlotType::Value => {
812 StorageSlot::with_value(slot_header.name().clone(), slot_header.value())
814 },
815 StorageSlotType::Map => {
816 let entries =
818 map_entries_by_slot.get(slot_header.name()).cloned().unwrap_or_default();
819 let storage_map = StorageMap::with_entries(entries)?;
820 StorageSlot::with_map(slot_header.name().clone(), storage_map)
821 },
822 };
823 Ok(slot)
824 }))?;
825
826 Ok(AccountStorage::new(slots)?)
827}
828
829pub(crate) fn select_latest_account_storage_components(
831 conn: &mut SqliteConnection,
832 account_id: AccountId,
833) -> Result<StorageHeaderWithEntries, DatabaseError> {
834 let account_id_bytes = account_id.to_bytes();
835
836 let storage_blob: Option<Vec<u8>> =
838 SelectDsl::select(schema::accounts::table, schema::accounts::storage_header)
839 .filter(schema::accounts::account_id.eq(&account_id_bytes))
840 .filter(schema::accounts::is_latest.eq(true))
841 .first(conn)
842 .optional()?
843 .flatten();
844
845 let header = match storage_blob {
846 Some(blob) => AccountStorageHeader::read_from_bytes(&blob)?,
847 None => AccountStorageHeader::new(Vec::new())?,
848 };
849
850 let entries = select_latest_storage_map_entries_all(conn, &account_id)?;
851 Ok((header, entries))
852}
853
854fn select_latest_storage_map_entries_all(
856 conn: &mut SqliteConnection,
857 account_id: &AccountId,
858) -> Result<HashMap<StorageSlotName, BTreeMap<StorageMapKey, Word>>, DatabaseError> {
859 use schema::account_storage_map_values as t;
860
861 let map_values: Vec<(String, Vec<u8>, Vec<u8>)> =
862 SelectDsl::select(t::table, (t::slot_name, t::key, t::value))
863 .filter(t::account_id.eq(&account_id.to_bytes()))
864 .filter(t::is_latest.eq(true))
865 .load(conn)?;
866
867 group_storage_map_entries(map_values)
868}
869
870fn select_latest_storage_map_entries_for_slots(
871 conn: &mut SqliteConnection,
872 account_id: &AccountId,
873 slot_names: &[StorageSlotName],
874) -> Result<HashMap<StorageSlotName, BTreeMap<StorageMapKey, Word>>, DatabaseError> {
875 use schema::account_storage_map_values as t;
876
877 if slot_names.is_empty() {
878 return Ok(HashMap::new());
879 }
880
881 if let [slot_name] = slot_names {
882 let entries = select_latest_storage_map_entries_for_slot(conn, account_id, slot_name)?;
883 if entries.is_empty() {
884 return Ok(HashMap::new());
885 }
886
887 let mut map_entries = HashMap::new();
888 map_entries.insert(slot_name.clone(), entries);
889 return Ok(map_entries);
890 }
891
892 let slot_names = Vec::from_iter(slot_names.iter().cloned().map(StorageSlotName::to_raw_sql));
893 let map_values: Vec<(String, Vec<u8>, Vec<u8>)> =
894 SelectDsl::select(t::table, (t::slot_name, t::key, t::value))
895 .filter(t::account_id.eq(&account_id.to_bytes()))
896 .filter(t::is_latest.eq(true))
897 .filter(t::slot_name.eq_any(slot_names))
898 .load(conn)?;
899
900 group_storage_map_entries(map_values)
901}
902
903fn select_latest_storage_map_entries_for_slot(
904 conn: &mut SqliteConnection,
905 account_id: &AccountId,
906 slot_name: &StorageSlotName,
907) -> Result<BTreeMap<StorageMapKey, Word>, DatabaseError> {
908 use schema::account_storage_map_values as t;
909
910 let map_values: Vec<(String, Vec<u8>, Vec<u8>)> =
911 SelectDsl::select(t::table, (t::slot_name, t::key, t::value))
912 .filter(t::account_id.eq(&account_id.to_bytes()))
913 .filter(t::is_latest.eq(true))
914 .filter(t::slot_name.eq(slot_name.clone().to_raw_sql()))
915 .load(conn)?;
916
917 Ok(group_storage_map_entries(map_values)?.remove(slot_name).unwrap_or_default())
918}
919
920fn group_storage_map_entries(
921 map_values: Vec<(String, Vec<u8>, Vec<u8>)>,
922) -> Result<HashMap<StorageSlotName, BTreeMap<StorageMapKey, Word>>, DatabaseError> {
923 let mut map_entries_by_slot: HashMap<StorageSlotName, BTreeMap<StorageMapKey, Word>> =
924 HashMap::new();
925 for (slot_name_str, key_bytes, value_bytes) in map_values {
926 let slot_name: StorageSlotName = slot_name_str.parse().map_err(|_| {
927 DatabaseError::DataCorrupted(format!("Invalid slot name: {slot_name_str}"))
928 })?;
929 let key = StorageMapKey::read_from_bytes(&key_bytes)?;
930 let value = Word::read_from_bytes(&value_bytes)?;
931 map_entries_by_slot.entry(slot_name).or_default().insert(key, value);
932 }
933
934 Ok(map_entries_by_slot)
935}
936
937#[derive(Queryable, Selectable)]
941#[diesel(table_name = crate::db::schema::account_vault_assets)]
942#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
943pub struct AccountVaultUpdateRaw {
944 pub vault_key: Vec<u8>,
945 pub asset: Option<Vec<u8>>,
946 pub block_num: i64,
947}
948
949impl TryFrom<AccountVaultUpdateRaw> for AccountVaultValue {
950 type Error = DatabaseError;
951
952 fn try_from(raw: AccountVaultUpdateRaw) -> Result<Self, Self::Error> {
953 let vault_key = AssetId::try_from(Word::read_from_bytes(&raw.vault_key)?)?;
954 let asset = raw.asset.map(|bytes| Asset::read_from_bytes(&bytes)).transpose()?;
955 let block_num = BlockNumber::from_raw_sql(raw.block_num)?;
956
957 Ok(AccountVaultValue { block_num, vault_key, asset })
958 }
959}
960
961#[derive(Debug, Clone, PartialEq, Eq, Selectable, Queryable, QueryableByName)]
962#[diesel(table_name = schema::accounts)]
963#[diesel(check_for_backend(Sqlite))]
964pub struct AccountSummaryRaw {
965 account_id: Vec<u8>, account_commitment: Vec<u8>, block_num: i64, }
969
970impl TryInto<AccountSummary> for AccountSummaryRaw {
971 type Error = DatabaseError;
972 fn try_into(self) -> Result<AccountSummary, Self::Error> {
973 let account_id = AccountId::read_from_bytes(&self.account_id[..])?;
974 let account_commitment = Word::read_from_bytes(&self.account_commitment[..])?;
975 let block_num = BlockNumber::from_raw_sql(self.block_num)?;
976
977 Ok(AccountSummary {
978 account_id,
979 account_commitment,
980 block_num,
981 })
982 }
983}
984
985pub(crate) fn insert_account_vault_asset(
994 conn: &mut SqliteConnection,
995 account_id: AccountId,
996 block_num: BlockNumber,
997 vault_key: AssetId,
998 asset: Option<Asset>,
999) -> Result<usize, DatabaseError> {
1000 let record = AccountAssetRowInsert::new(&account_id, &vault_key, block_num, asset, true);
1001
1002 diesel::Connection::transaction(conn, |conn| {
1003 let vault_key: Word = vault_key.into();
1006 let vault_key_bytes = vault_key.to_bytes();
1007 let account_id_bytes = account_id.to_bytes();
1008 let update_count = diesel::update(schema::account_vault_assets::table)
1009 .filter(
1010 schema::account_vault_assets::account_id
1011 .eq(account_id_bytes)
1012 .and(schema::account_vault_assets::vault_key.eq(vault_key_bytes))
1013 .and(schema::account_vault_assets::is_latest.eq(true)),
1014 )
1015 .set(schema::account_vault_assets::is_latest.eq(false))
1016 .execute(conn)?;
1017
1018 let insert_count = diesel::insert_into(schema::account_vault_assets::table)
1020 .values(record)
1021 .execute(conn)?;
1022
1023 Ok(update_count + insert_count)
1024 })
1025}
1026
1027pub(crate) fn insert_account_storage_map_value(
1036 conn: &mut SqliteConnection,
1037 account_id: AccountId,
1038 block_num: BlockNumber,
1039 slot_name: StorageSlotName,
1040 key: StorageMapKey,
1041 value: Word,
1042) -> Result<usize, DatabaseError> {
1043 let account_id = account_id.to_bytes();
1044 let key = key.to_bytes();
1045 let value = value.to_bytes();
1046 let slot_name = slot_name.to_raw_sql();
1047 let block_num = block_num.to_raw_sql();
1048
1049 let update_count = diesel::update(schema::account_storage_map_values::table)
1050 .filter(
1051 schema::account_storage_map_values::account_id
1052 .eq(&account_id)
1053 .and(schema::account_storage_map_values::slot_name.eq(&slot_name))
1054 .and(schema::account_storage_map_values::key.eq(&key))
1055 .and(schema::account_storage_map_values::is_latest.eq(true)),
1056 )
1057 .set(schema::account_storage_map_values::is_latest.eq(false))
1058 .execute(conn)?;
1059
1060 let record = AccountStorageMapRowInsert {
1061 account_id,
1062 key,
1063 value,
1064 slot_name,
1065 block_num,
1066 is_latest: true,
1067 };
1068 let insert_count = diesel::insert_into(schema::account_storage_map_values::table)
1069 .values(record)
1070 .execute(conn)?;
1071
1072 Ok(update_count + insert_count)
1073}
1074
1075type PendingStorageInserts = Vec<(AccountId, StorageSlotName, StorageMapKey, Word)>;
1076type PendingAssetInserts = Vec<(AccountId, AssetId, Option<Asset>)>;
1077
1078fn prepare_full_account_update(
1079 update: &BlockAccountUpdate,
1080 account: Account,
1081) -> Result<(AccountStateForInsert, PendingStorageInserts, PendingAssetInserts), DatabaseError> {
1082 let account_id = account.id();
1083
1084 if account.to_commitment() != update.final_state_commitment() {
1086 return Err(DatabaseError::AccountCommitmentsMismatch {
1087 calculated: account.to_commitment(),
1088 expected: update.final_state_commitment(),
1089 });
1090 }
1091
1092 let mut storage = Vec::new();
1094 for slot in account.storage().slots() {
1095 if let StorageSlotContent::Map(storage_map) = slot.content() {
1096 for (key, value) in storage_map.entries() {
1097 storage.push((account_id, slot.name().clone(), *key, *value));
1098 }
1099 }
1100 }
1101
1102 let mut assets = Vec::new();
1104 for asset in account.vault().assets() {
1105 let should_insert = match asset {
1107 Asset::Fungible(fungible) => fungible.amount().as_u64() > 0,
1108 Asset::NonFungible(_) => true,
1109 };
1110 if should_insert {
1111 assets.push((account_id, asset.id(), Some(asset)));
1112 }
1113 }
1114
1115 Ok((AccountStateForInsert::FullAccount(account), storage, assets))
1116}
1117
1118fn prepare_partial_account_update(
1120 conn: &mut SqliteConnection,
1121 update: &BlockAccountUpdate,
1122 account_id: AccountId,
1123 patch: &AccountPatch,
1124) -> Result<(AccountStateForInsert, PendingStorageInserts, PendingAssetInserts), DatabaseError> {
1125 let state_headers = select_minimal_account_state_headers(conn, account_id)?;
1129
1130 let mut assets = Vec::new();
1133 for (vault_key, value) in patch.vault().iter() {
1134 let update_or_remove = if *value == Word::empty() {
1135 None
1136 } else {
1137 Some(Asset::from_id_and_value(*vault_key, *value)?)
1138 };
1139 assets.push((account_id, *vault_key, update_or_remove));
1140 }
1141
1142 let mut storage = Vec::new();
1145 for (slot_name, map_patch) in patch.storage().maps() {
1146 for (key, value) in map_patch.entries().into_iter().flat_map(StorageMapPatchEntries::as_map)
1147 {
1148 storage.push((account_id, slot_name.clone(), *key, *value));
1149 }
1150 }
1151
1152 let slot_names = Vec::from_iter(patch.storage().maps().filter_map(|(slot_name, map_patch)| {
1154 if map_patch.entries().is_none_or(StorageMapPatchEntries::is_empty) {
1155 None
1156 } else {
1157 Some(slot_name.clone())
1158 }
1159 }));
1160
1161 let map_entries = select_latest_storage_map_entries_for_slots(conn, &account_id, &slot_names)?;
1162
1163 let new_storage_header =
1165 apply_storage_patch(&state_headers.storage_header, patch.storage(), &map_entries)?;
1166
1167 let new_vault_root = {
1169 let assets = select_latest_vault_assets(conn, account_id)?;
1170 let mut vault = AssetVault::new(&assets)?;
1171 vault.apply_patch(patch.vault())?;
1172 vault.root()
1173 };
1174
1175 let new_nonce = patch.final_nonce().unwrap_or(state_headers.nonce);
1177
1178 let account_state = PartialAccountState {
1180 nonce: new_nonce,
1181 code_commitment: state_headers.code_commitment,
1182 storage_header: new_storage_header,
1183 vault_root: new_vault_root,
1184 };
1185
1186 let account_header = miden_protocol::account::AccountHeader::new(
1187 account_id,
1188 account_state.nonce,
1189 account_state.vault_root,
1190 account_state.storage_header.to_commitment(),
1191 account_state.code_commitment,
1192 );
1193
1194 if account_header.to_commitment() != update.final_state_commitment() {
1195 return Err(DatabaseError::AccountCommitmentsMismatch {
1196 calculated: account_header.to_commitment(),
1197 expected: update.final_state_commitment(),
1198 });
1199 }
1200
1201 Ok((AccountStateForInsert::PartialState(account_state), storage, assets))
1202}
1203
1204pub(crate) fn select_network_accounts_subset(
1208 conn: &mut SqliteConnection,
1209 account_ids: &[AccountId],
1210) -> Result<HashSet<AccountId>, DatabaseError> {
1211 QueryParamAccountIdLimit::check(account_ids.len())?;
1212 let id_bytes: Vec<Vec<u8>> =
1213 account_ids.iter().map(miden_crypto::utils::Serializable::to_bytes).collect();
1214
1215 let rows: Vec<Vec<u8>> =
1216 SelectDsl::select(schema::accounts::table, schema::accounts::account_id)
1217 .filter(
1218 schema::accounts::account_id
1219 .eq_any(&id_bytes)
1220 .and(
1221 schema::accounts::network_account_type
1222 .eq(NetworkAccountType::Network.to_raw_sql()),
1223 )
1224 .and(schema::accounts::is_latest.eq(true)),
1225 )
1226 .load::<Vec<u8>>(conn)
1227 .map_err(DatabaseError::Diesel)?;
1228
1229 rows.into_iter()
1230 .map(|bytes| {
1231 AccountId::read_from_bytes(&bytes).map_err(DatabaseError::DeserializationError)
1232 })
1233 .collect()
1234}
1235
1236#[miden_instrument(
1238 target = COMPONENT,
1239 skip_all,
1240 err,
1241)]
1242pub(crate) fn upsert_accounts(
1243 conn: &mut SqliteConnection,
1244 accounts: &[BlockAccountUpdate],
1245 block_num: BlockNumber,
1246) -> Result<usize, DatabaseError> {
1247 let mut count = 0;
1248 for update in accounts {
1249 let account_id = update.account_id();
1250 let account_id_bytes = account_id.to_bytes();
1251
1252 let existing: Option<(i64, i32)> = QueryDsl::select(
1255 schema::accounts::table.filter(
1256 schema::accounts::account_id
1257 .eq(&account_id_bytes)
1258 .and(schema::accounts::is_latest.eq(true)),
1259 ),
1260 (schema::accounts::created_at_block, schema::accounts::network_account_type),
1261 )
1262 .first(conn)
1263 .optional()
1264 .map_err(DatabaseError::Diesel)?;
1265
1266 let created_at_block = match existing {
1267 Some((raw, _)) => BlockNumber::from_raw_sql(raw)?,
1268 None => block_num,
1269 };
1270
1271 let (account_state, pending_storage_inserts, pending_asset_inserts) = match update.details()
1276 {
1277 AccountUpdateDetails::Private => (AccountStateForInsert::Private, vec![], vec![]),
1278
1279 AccountUpdateDetails::Public(patch) if patch.is_full_state() => {
1281 let account = Account::try_from(patch)
1282 .expect("Patch to full account always works for full state patches");
1283 debug_assert_eq!(account_id, account.id());
1284
1285 prepare_full_account_update(update, account)?
1286 },
1287
1288 AccountUpdateDetails::Public(patch) => {
1290 prepare_partial_account_update(conn, update, account_id, patch)?
1291 },
1292 };
1293
1294 let network_account_type = match existing {
1297 Some((_, raw)) => NetworkAccountType::from_raw_sql(raw)?,
1298 None => match &account_state {
1299 AccountStateForInsert::FullAccount(account)
1300 if NetworkAccount::new(account.clone()).is_ok() =>
1301 {
1302 NetworkAccountType::Network
1303 },
1304 _ => NetworkAccountType::None,
1305 },
1306 };
1307
1308 if let AccountStateForInsert::FullAccount(ref account) = account_state {
1310 let code = account.code();
1311 let code_value = AccountCodeRowInsert {
1312 code_commitment: code.commitment().to_bytes(),
1313 code: code.to_bytes(),
1314 };
1315 diesel::insert_into(schema::account_codes::table)
1316 .values(&code_value)
1317 .on_conflict(schema::account_codes::code_commitment)
1318 .do_nothing()
1319 .execute(conn)?;
1320 }
1321
1322 diesel::update(schema::accounts::table)
1324 .filter(
1325 schema::accounts::account_id
1326 .eq(&account_id_bytes)
1327 .and(schema::accounts::is_latest.eq(true)),
1328 )
1329 .set(schema::accounts::is_latest.eq(false))
1330 .execute(conn)?;
1331
1332 let account_value = match &account_state {
1333 AccountStateForInsert::Private => AccountRowInsert::new_private(
1334 account_id,
1335 network_account_type,
1336 update.final_state_commitment(),
1337 block_num,
1338 created_at_block,
1339 ),
1340 AccountStateForInsert::FullAccount(account) => AccountRowInsert::new_from_account(
1341 account_id,
1342 network_account_type,
1343 update.final_state_commitment(),
1344 block_num,
1345 created_at_block,
1346 account,
1347 ),
1348 AccountStateForInsert::PartialState(state) => AccountRowInsert::new_from_partial(
1349 account_id,
1350 network_account_type,
1351 update.final_state_commitment(),
1352 block_num,
1353 created_at_block,
1354 state,
1355 ),
1356 };
1357
1358 diesel::insert_into(schema::accounts::table)
1359 .values(&account_value)
1360 .on_conflict((schema::accounts::account_id, schema::accounts::block_num))
1361 .do_update()
1362 .set(&account_value)
1363 .execute(conn)?;
1364
1365 for (acc_id, slot_name, key, value) in pending_storage_inserts {
1367 insert_account_storage_map_value(conn, acc_id, block_num, slot_name, key, value)?;
1368 }
1369
1370 for (acc_id, vault_key, update) in pending_asset_inserts {
1371 insert_account_vault_asset(conn, acc_id, block_num, vault_key, update)?;
1372 }
1373
1374 count += 1;
1375 }
1376
1377 Ok(count)
1378}
1379
1380#[derive(Insertable, Debug, Clone)]
1381#[diesel(table_name = schema::account_codes)]
1382pub(crate) struct AccountCodeRowInsert {
1383 pub(crate) code_commitment: Vec<u8>,
1384 pub(crate) code: Vec<u8>,
1385}
1386
1387#[derive(Insertable, AsChangeset, Debug, Clone)]
1388#[diesel(table_name = schema::accounts)]
1389pub(crate) struct AccountRowInsert {
1390 pub(crate) account_id: Vec<u8>,
1391 pub(crate) network_account_type: i32,
1392 pub(crate) block_num: i64,
1393 pub(crate) account_commitment: Vec<u8>,
1394 pub(crate) code_commitment: Option<Vec<u8>>,
1395 pub(crate) nonce: Option<i64>,
1396 pub(crate) storage_header: Option<Vec<u8>>,
1397 pub(crate) vault_root: Option<Vec<u8>>,
1398 pub(crate) is_latest: bool,
1399 pub(crate) created_at_block: i64,
1400}
1401
1402impl AccountRowInsert {
1403 pub(crate) fn new_private(
1405 account_id: AccountId,
1406 network_account_type: NetworkAccountType,
1407 account_commitment: Word,
1408 block_num: BlockNumber,
1409 created_at_block: BlockNumber,
1410 ) -> Self {
1411 Self {
1412 account_id: account_id.to_bytes(),
1413 network_account_type: network_account_type.to_raw_sql(),
1414 account_commitment: account_commitment.to_bytes(),
1415 block_num: block_num.to_raw_sql(),
1416 nonce: None,
1417 code_commitment: None,
1418 storage_header: None,
1419 vault_root: None,
1420 is_latest: true,
1421 created_at_block: created_at_block.to_raw_sql(),
1422 }
1423 }
1424
1425 fn new_from_account(
1427 account_id: AccountId,
1428 network_account_type: NetworkAccountType,
1429 account_commitment: Word,
1430 block_num: BlockNumber,
1431 created_at_block: BlockNumber,
1432 account: &Account,
1433 ) -> Self {
1434 Self {
1435 account_id: account_id.to_bytes(),
1436 network_account_type: network_account_type.to_raw_sql(),
1437 account_commitment: account_commitment.to_bytes(),
1438 block_num: block_num.to_raw_sql(),
1439 nonce: Some(nonce_to_raw_sql(account.nonce())),
1440 code_commitment: Some(account.code().commitment().to_bytes()),
1441 storage_header: Some(account.storage().to_header().to_bytes()),
1442 vault_root: Some(account.vault().root().to_bytes()),
1443 is_latest: true,
1444 created_at_block: created_at_block.to_raw_sql(),
1445 }
1446 }
1447
1448 fn new_from_partial(
1450 account_id: AccountId,
1451 network_account_type: NetworkAccountType,
1452 account_commitment: Word,
1453 block_num: BlockNumber,
1454 created_at_block: BlockNumber,
1455 state: &PartialAccountState,
1456 ) -> Self {
1457 Self {
1458 account_id: account_id.to_bytes(),
1459 network_account_type: network_account_type.to_raw_sql(),
1460 account_commitment: account_commitment.to_bytes(),
1461 block_num: block_num.to_raw_sql(),
1462 nonce: Some(nonce_to_raw_sql(state.nonce)),
1463 code_commitment: Some(state.code_commitment.to_bytes()),
1464 storage_header: Some(state.storage_header.to_bytes()),
1465 vault_root: Some(state.vault_root.to_bytes()),
1466 is_latest: true,
1467 created_at_block: created_at_block.to_raw_sql(),
1468 }
1469 }
1470}
1471
1472#[derive(Insertable, AsChangeset, Debug, Clone)]
1473#[diesel(table_name = schema::account_vault_assets)]
1474pub(crate) struct AccountAssetRowInsert {
1475 pub(crate) account_id: Vec<u8>,
1476 pub(crate) block_num: i64,
1477 pub(crate) vault_key: Vec<u8>,
1478 pub(crate) asset: Option<Vec<u8>>,
1479 pub(crate) is_latest: bool,
1480}
1481
1482impl AccountAssetRowInsert {
1483 pub(crate) fn new(
1484 account_id: &AccountId,
1485 vault_key: &AssetId,
1486 block_num: BlockNumber,
1487 asset: Option<Asset>,
1488 is_latest: bool,
1489 ) -> Self {
1490 let account_id = account_id.to_bytes();
1491 let vault_key: Word = (*vault_key).into();
1492 let vault_key = vault_key.to_bytes();
1493 let block_num = block_num.to_raw_sql();
1494 let asset = asset.map(|asset| asset.to_bytes());
1495 Self {
1496 account_id,
1497 block_num,
1498 vault_key,
1499 asset,
1500 is_latest,
1501 }
1502 }
1503}
1504
1505#[derive(Insertable, AsChangeset, Debug, Clone)]
1506#[diesel(table_name = schema::account_storage_map_values)]
1507pub(crate) struct AccountStorageMapRowInsert {
1508 pub(crate) account_id: Vec<u8>,
1509 pub(crate) block_num: i64,
1510 pub(crate) slot_name: String,
1511 pub(crate) key: Vec<u8>,
1512 pub(crate) value: Vec<u8>,
1513 pub(crate) is_latest: bool,
1514}
1515
1516pub const HISTORICAL_BLOCK_RETENTION: u32 = 50;
1523
1524#[miden_instrument(
1533 target = COMPONENT,
1534 skip_all,
1535 err,
1536 fields(
1537 cutoff_block,
1538 ),
1539)]
1540pub(crate) fn prune_history(
1541 conn: &mut SqliteConnection,
1542 chain_tip: BlockNumber,
1543) -> Result<(usize, usize, usize), DatabaseError> {
1544 let cutoff_block = i64::from(chain_tip.as_u32().saturating_sub(HISTORICAL_BLOCK_RETENTION));
1545 tracing::Span::current().record("cutoff_block", cutoff_block);
1546 let vault_deleted = prune_account_vault_assets(conn, cutoff_block)?;
1547 let storage_deleted = prune_account_storage_map_values(conn, cutoff_block)?;
1548 let codes_deleted = prune_account_codes(conn, cutoff_block)?;
1549
1550 Ok((vault_deleted, storage_deleted, codes_deleted))
1551}
1552
1553#[miden_instrument(
1554 target = COMPONENT,
1555 skip_all,
1556 err,
1557 fields(
1558 cutoff_block,
1559 ),
1560)]
1561fn prune_account_vault_assets(
1562 conn: &mut SqliteConnection,
1563 cutoff_block: i64,
1564) -> Result<usize, DatabaseError> {
1565 diesel::delete(
1566 schema::account_vault_assets::table.filter(
1567 schema::account_vault_assets::block_num
1568 .lt(cutoff_block)
1569 .and(schema::account_vault_assets::is_latest.eq(false)),
1570 ),
1571 )
1572 .execute(conn)
1573 .map_err(DatabaseError::Diesel)
1574}
1575
1576#[miden_instrument(
1577 target = COMPONENT,
1578 skip_all,
1579 err,
1580 fields(
1581 cutoff_block,
1582 ),
1583)]
1584fn prune_account_storage_map_values(
1585 conn: &mut SqliteConnection,
1586 cutoff_block: i64,
1587) -> Result<usize, DatabaseError> {
1588 diesel::delete(
1589 schema::account_storage_map_values::table.filter(
1590 schema::account_storage_map_values::block_num
1591 .lt(cutoff_block)
1592 .and(schema::account_storage_map_values::is_latest.eq(false)),
1593 ),
1594 )
1595 .execute(conn)
1596 .map_err(DatabaseError::Diesel)
1597}
1598
1599#[miden_instrument(
1610 target = COMPONENT,
1611 skip_all,
1612 err,
1613 fields(
1614 cutoff_block,
1615 ),
1616)]
1617fn prune_account_codes(
1618 conn: &mut SqliteConnection,
1619 cutoff_block: i64,
1620) -> Result<usize, DatabaseError> {
1621 use diesel::sql_types::BigInt;
1622
1623 diesel::sql_query(
1624 "DELETE FROM account_codes \
1625 WHERE code_commitment NOT IN ( \
1626 SELECT DISTINCT code_commitment \
1627 FROM ( \
1628 SELECT code_commitment \
1629 FROM accounts INDEXED BY idx_accounts_prune_code \
1630 WHERE code_commitment IS NOT NULL \
1631 AND block_num >= ?1 \
1632 UNION ALL \
1633 SELECT code_commitment \
1634 FROM accounts INDEXED BY idx_accounts_latest_code_commitment \
1635 WHERE code_commitment IS NOT NULL \
1636 AND is_latest = 1 \
1637 ) \
1638 )",
1639 )
1640 .bind::<BigInt, _>(cutoff_block)
1641 .execute(conn)
1642 .map_err(DatabaseError::Diesel)
1643}