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};
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::account::delta::AccountUpdateDetails;
30use miden_protocol::account::{
31 Account,
32 AccountCode,
33 AccountId,
34 AccountStorage,
35 AccountStorageHeader,
36 NonFungibleDeltaAction,
37 StorageMap,
38 StorageMapKey,
39 StorageSlot,
40 StorageSlotContent,
41 StorageSlotName,
42 StorageSlotType,
43};
44use miden_protocol::asset::{Asset, AssetVault, AssetVaultKey, FungibleAsset};
45use miden_protocol::block::{BlockAccountUpdate, BlockNumber};
46use miden_protocol::utils::serde::{Deserializable, Serializable};
47use miden_protocol::{Felt, Word};
48use miden_standards::account::auth::NetworkAccount;
49
50use crate::COMPONENT;
51use crate::db::models::conv::{SqlTypeConvert, nonce_to_raw_sql, raw_sql_to_nonce};
52#[cfg(test)]
53use crate::db::models::vec_raw_try_into;
54use crate::db::{AccountVaultValue, schema};
55use crate::errors::DatabaseError;
56
57mod at_block;
58pub(crate) use at_block::select_account_header_with_storage_header_at_block;
59
60mod delta;
61use delta::{
62 AccountStateForInsert,
63 PartialAccountState,
64 apply_storage_delta,
65 select_latest_vault_assets,
66 select_minimal_account_state_headers,
67 select_vault_balances_by_vault_keys,
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
559#[cfg(test)]
580pub(crate) fn select_all_accounts(
581 conn: &mut SqliteConnection,
582) -> Result<Vec<AccountInfo>, DatabaseError> {
583 let raw = SelectDsl::select(schema::accounts::table, AccountSummaryRaw::as_select())
584 .filter(schema::accounts::is_latest.eq(true))
585 .order_by(schema::accounts::block_num.asc())
586 .load::<AccountSummaryRaw>(conn)?;
587
588 let summaries: Vec<AccountSummary> = vec_raw_try_into(raw)?;
589
590 let account_infos = summaries
592 .into_iter()
593 .map(|summary| {
594 let account_id = summary.account_id;
595 let details = select_full_account(conn, account_id).ok();
596 AccountInfo { summary, details }
597 })
598 .collect();
599
600 Ok(account_infos)
601}
602
603#[derive(Debug, Clone, PartialEq, Eq)]
604pub struct StorageMapValue {
605 pub block_num: BlockNumber,
606 pub slot_name: StorageSlotName,
607 pub key: StorageMapKey,
608 pub value: Word,
609}
610
611#[derive(Debug, Clone, PartialEq, Eq)]
612pub struct StorageMapValuesPage {
613 pub last_block_included: BlockNumber,
615 pub values: Vec<StorageMapValue>,
617}
618
619impl StorageMapValue {
620 pub fn from_raw_row(row: StorageMapValueRow) -> Result<Self, DatabaseError> {
621 let (block_num, slot_name, key, value) = row;
622 Ok(Self {
623 block_num: BlockNumber::from_raw_sql(block_num)?,
624 slot_name: StorageSlotName::from_raw_sql(slot_name)?,
625 key: StorageMapKey::read_from_bytes(&key)?,
626 value: Word::read_from_bytes(&value)?,
627 })
628 }
629}
630
631pub(crate) fn select_account_storage_map_values_paged(
673 conn: &mut SqliteConnection,
674 account_id: AccountId,
675 block_range: RangeInclusive<BlockNumber>,
676 limit: usize,
677) -> Result<StorageMapValuesPage, DatabaseError> {
678 use schema::account_storage_map_values as t;
679
680 if !account_id.is_public() {
681 return Err(DatabaseError::AccountNotPublic(account_id));
682 }
683
684 if block_range.is_empty() {
685 return Err(DatabaseError::InvalidBlockRange {
686 from: *block_range.start(),
687 to: *block_range.end(),
688 });
689 }
690
691 let raw: Vec<StorageMapValueRow> =
692 SelectDsl::select(t::table, (t::block_num, t::slot_name, t::key, t::value))
693 .filter(
694 t::account_id
695 .eq(account_id.to_bytes())
696 .and(t::block_num.ge(block_range.start().to_raw_sql()))
697 .and(t::block_num.le(block_range.end().to_raw_sql())),
698 )
699 .order(t::block_num.asc())
700 .limit(i64::try_from(limit + 1).expect("limit fits within i64"))
701 .load(conn)?;
702
703 let (last_block_included, values) = if let Some(&(last_block_num, ..)) = raw.last()
706 && raw.len() > limit
707 {
708 let values = raw
709 .into_iter()
710 .take_while(|(bn, ..)| *bn != last_block_num)
711 .map(StorageMapValue::from_raw_row)
712 .collect::<Result<Vec<_>, DatabaseError>>()?;
713
714 let last_block_included = values.last().map_or(*block_range.start(), |v| v.block_num);
715
716 (last_block_included, values)
717 } else {
718 (
719 *block_range.end(),
720 raw.into_iter()
721 .map(StorageMapValue::from_raw_row)
722 .collect::<Result<Vec<_>, _>>()?,
723 )
724 };
725
726 Ok(StorageMapValuesPage { last_block_included, values })
727}
728
729pub(crate) fn select_latest_account_storage(
735 conn: &mut SqliteConnection,
736 account_id: AccountId,
737) -> Result<AccountStorage, DatabaseError> {
738 let (storage_header, map_entries_by_slot) =
739 select_latest_account_storage_components(conn, account_id)?;
740 let slots =
742 Result::<Vec<_>, DatabaseError>::from_iter(storage_header.slots().map(|slot_header| {
743 let slot = match slot_header.slot_type() {
744 StorageSlotType::Value => {
745 StorageSlot::with_value(slot_header.name().clone(), slot_header.value())
747 },
748 StorageSlotType::Map => {
749 let entries =
751 map_entries_by_slot.get(slot_header.name()).cloned().unwrap_or_default();
752 let storage_map = StorageMap::with_entries(entries.into_iter())?;
753 StorageSlot::with_map(slot_header.name().clone(), storage_map)
754 },
755 };
756 Ok(slot)
757 }))?;
758
759 Ok(AccountStorage::new(slots)?)
760}
761
762pub(crate) fn select_latest_account_storage_components(
764 conn: &mut SqliteConnection,
765 account_id: AccountId,
766) -> Result<StorageHeaderWithEntries, DatabaseError> {
767 let account_id_bytes = account_id.to_bytes();
768
769 let storage_blob: Option<Vec<u8>> =
771 SelectDsl::select(schema::accounts::table, schema::accounts::storage_header)
772 .filter(schema::accounts::account_id.eq(&account_id_bytes))
773 .filter(schema::accounts::is_latest.eq(true))
774 .first(conn)
775 .optional()?
776 .flatten();
777
778 let header = match storage_blob {
779 Some(blob) => AccountStorageHeader::read_from_bytes(&blob)?,
780 None => AccountStorageHeader::new(Vec::new())?,
781 };
782
783 let entries = select_latest_storage_map_entries_all(conn, &account_id)?;
784 Ok((header, entries))
785}
786
787fn select_latest_storage_map_entries_all(
789 conn: &mut SqliteConnection,
790 account_id: &AccountId,
791) -> Result<HashMap<StorageSlotName, BTreeMap<StorageMapKey, Word>>, DatabaseError> {
792 use schema::account_storage_map_values as t;
793
794 let map_values: Vec<(String, Vec<u8>, Vec<u8>)> =
795 SelectDsl::select(t::table, (t::slot_name, t::key, t::value))
796 .filter(t::account_id.eq(&account_id.to_bytes()))
797 .filter(t::is_latest.eq(true))
798 .load(conn)?;
799
800 group_storage_map_entries(map_values)
801}
802
803fn select_latest_storage_map_entries_for_slots(
804 conn: &mut SqliteConnection,
805 account_id: &AccountId,
806 slot_names: &[StorageSlotName],
807) -> Result<HashMap<StorageSlotName, BTreeMap<StorageMapKey, Word>>, DatabaseError> {
808 use schema::account_storage_map_values as t;
809
810 if slot_names.is_empty() {
811 return Ok(HashMap::new());
812 }
813
814 if let [slot_name] = slot_names {
815 let entries = select_latest_storage_map_entries_for_slot(conn, account_id, slot_name)?;
816 if entries.is_empty() {
817 return Ok(HashMap::new());
818 }
819
820 let mut map_entries = HashMap::new();
821 map_entries.insert(slot_name.clone(), entries);
822 return Ok(map_entries);
823 }
824
825 let slot_names = Vec::from_iter(slot_names.iter().cloned().map(StorageSlotName::to_raw_sql));
826 let map_values: Vec<(String, Vec<u8>, Vec<u8>)> =
827 SelectDsl::select(t::table, (t::slot_name, t::key, t::value))
828 .filter(t::account_id.eq(&account_id.to_bytes()))
829 .filter(t::is_latest.eq(true))
830 .filter(t::slot_name.eq_any(slot_names))
831 .load(conn)?;
832
833 group_storage_map_entries(map_values)
834}
835
836fn select_latest_storage_map_entries_for_slot(
837 conn: &mut SqliteConnection,
838 account_id: &AccountId,
839 slot_name: &StorageSlotName,
840) -> Result<BTreeMap<StorageMapKey, Word>, DatabaseError> {
841 use schema::account_storage_map_values as t;
842
843 let map_values: Vec<(String, Vec<u8>, Vec<u8>)> =
844 SelectDsl::select(t::table, (t::slot_name, t::key, t::value))
845 .filter(t::account_id.eq(&account_id.to_bytes()))
846 .filter(t::is_latest.eq(true))
847 .filter(t::slot_name.eq(slot_name.clone().to_raw_sql()))
848 .load(conn)?;
849
850 Ok(group_storage_map_entries(map_values)?.remove(slot_name).unwrap_or_default())
851}
852
853fn group_storage_map_entries(
854 map_values: Vec<(String, Vec<u8>, Vec<u8>)>,
855) -> Result<HashMap<StorageSlotName, BTreeMap<StorageMapKey, Word>>, DatabaseError> {
856 let mut map_entries_by_slot: HashMap<StorageSlotName, BTreeMap<StorageMapKey, Word>> =
857 HashMap::new();
858 for (slot_name_str, key_bytes, value_bytes) in map_values {
859 let slot_name: StorageSlotName = slot_name_str.parse().map_err(|_| {
860 DatabaseError::DataCorrupted(format!("Invalid slot name: {slot_name_str}"))
861 })?;
862 let key = StorageMapKey::read_from_bytes(&key_bytes)?;
863 let value = Word::read_from_bytes(&value_bytes)?;
864 map_entries_by_slot.entry(slot_name).or_default().insert(key, value);
865 }
866
867 Ok(map_entries_by_slot)
868}
869
870#[derive(Queryable, Selectable)]
874#[diesel(table_name = crate::db::schema::account_vault_assets)]
875#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
876pub struct AccountVaultUpdateRaw {
877 pub vault_key: Vec<u8>,
878 pub asset: Option<Vec<u8>>,
879 pub block_num: i64,
880}
881
882impl TryFrom<AccountVaultUpdateRaw> for AccountVaultValue {
883 type Error = DatabaseError;
884
885 fn try_from(raw: AccountVaultUpdateRaw) -> Result<Self, Self::Error> {
886 let vault_key = AssetVaultKey::try_from(Word::read_from_bytes(&raw.vault_key)?)?;
887 let asset = raw.asset.map(|bytes| Asset::read_from_bytes(&bytes)).transpose()?;
888 let block_num = BlockNumber::from_raw_sql(raw.block_num)?;
889
890 Ok(AccountVaultValue { block_num, vault_key, asset })
891 }
892}
893
894#[derive(Debug, Clone, PartialEq, Eq, Selectable, Queryable, QueryableByName)]
895#[diesel(table_name = schema::accounts)]
896#[diesel(check_for_backend(Sqlite))]
897pub struct AccountSummaryRaw {
898 account_id: Vec<u8>, account_commitment: Vec<u8>, block_num: i64, }
902
903impl TryInto<AccountSummary> for AccountSummaryRaw {
904 type Error = DatabaseError;
905 fn try_into(self) -> Result<AccountSummary, Self::Error> {
906 let account_id = AccountId::read_from_bytes(&self.account_id[..])?;
907 let account_commitment = Word::read_from_bytes(&self.account_commitment[..])?;
908 let block_num = BlockNumber::from_raw_sql(self.block_num)?;
909
910 Ok(AccountSummary {
911 account_id,
912 account_commitment,
913 block_num,
914 })
915 }
916}
917
918pub(crate) fn insert_account_vault_asset(
927 conn: &mut SqliteConnection,
928 account_id: AccountId,
929 block_num: BlockNumber,
930 vault_key: AssetVaultKey,
931 asset: Option<Asset>,
932) -> Result<usize, DatabaseError> {
933 let record = AccountAssetRowInsert::new(&account_id, &vault_key, block_num, asset, true);
934
935 diesel::Connection::transaction(conn, |conn| {
936 let vault_key: Word = vault_key.into();
939 let vault_key_bytes = vault_key.to_bytes();
940 let account_id_bytes = account_id.to_bytes();
941 let update_count = diesel::update(schema::account_vault_assets::table)
942 .filter(
943 schema::account_vault_assets::account_id
944 .eq(account_id_bytes)
945 .and(schema::account_vault_assets::vault_key.eq(vault_key_bytes))
946 .and(schema::account_vault_assets::is_latest.eq(true)),
947 )
948 .set(schema::account_vault_assets::is_latest.eq(false))
949 .execute(conn)?;
950
951 let insert_count = diesel::insert_into(schema::account_vault_assets::table)
953 .values(record)
954 .execute(conn)?;
955
956 Ok(update_count + insert_count)
957 })
958}
959
960pub(crate) fn insert_account_storage_map_value(
969 conn: &mut SqliteConnection,
970 account_id: AccountId,
971 block_num: BlockNumber,
972 slot_name: StorageSlotName,
973 key: StorageMapKey,
974 value: Word,
975) -> Result<usize, DatabaseError> {
976 let account_id = account_id.to_bytes();
977 let key = key.to_bytes();
978 let value = value.to_bytes();
979 let slot_name = slot_name.to_raw_sql();
980 let block_num = block_num.to_raw_sql();
981
982 let update_count = diesel::update(schema::account_storage_map_values::table)
983 .filter(
984 schema::account_storage_map_values::account_id
985 .eq(&account_id)
986 .and(schema::account_storage_map_values::slot_name.eq(&slot_name))
987 .and(schema::account_storage_map_values::key.eq(&key))
988 .and(schema::account_storage_map_values::is_latest.eq(true)),
989 )
990 .set(schema::account_storage_map_values::is_latest.eq(false))
991 .execute(conn)?;
992
993 let record = AccountStorageMapRowInsert {
994 account_id,
995 key,
996 value,
997 slot_name,
998 block_num,
999 is_latest: true,
1000 };
1001 let insert_count = diesel::insert_into(schema::account_storage_map_values::table)
1002 .values(record)
1003 .execute(conn)?;
1004
1005 Ok(update_count + insert_count)
1006}
1007
1008type PendingStorageInserts = Vec<(AccountId, StorageSlotName, StorageMapKey, Word)>;
1009type PendingAssetInserts = Vec<(AccountId, AssetVaultKey, Option<Asset>)>;
1010
1011fn prepare_full_account_update(
1012 update: &BlockAccountUpdate,
1013 account: Account,
1014) -> Result<(AccountStateForInsert, PendingStorageInserts, PendingAssetInserts), DatabaseError> {
1015 let account_id = account.id();
1016
1017 if account.to_commitment() != update.final_state_commitment() {
1019 return Err(DatabaseError::AccountCommitmentsMismatch {
1020 calculated: account.to_commitment(),
1021 expected: update.final_state_commitment(),
1022 });
1023 }
1024
1025 let mut storage = Vec::new();
1027 for slot in account.storage().slots() {
1028 if let StorageSlotContent::Map(storage_map) = slot.content() {
1029 for (key, value) in storage_map.entries() {
1030 storage.push((account_id, slot.name().clone(), *key, *value));
1031 }
1032 }
1033 }
1034
1035 let mut assets = Vec::new();
1037 for asset in account.vault().assets() {
1038 let should_insert = match asset {
1040 Asset::Fungible(fungible) => fungible.amount().as_u64() > 0,
1041 Asset::NonFungible(_) => true,
1042 };
1043 if should_insert {
1044 assets.push((account_id, asset.vault_key(), Some(asset)));
1045 }
1046 }
1047
1048 Ok((AccountStateForInsert::FullAccount(account), storage, assets))
1049}
1050
1051fn prepare_partial_account_update(
1053 conn: &mut SqliteConnection,
1054 update: &BlockAccountUpdate,
1055 account_id: AccountId,
1056 delta: &miden_protocol::account::delta::AccountDelta,
1057) -> Result<(AccountStateForInsert, PendingStorageInserts, PendingAssetInserts), DatabaseError> {
1058 let state_headers = select_minimal_account_state_headers(conn, account_id)?;
1062
1063 let vault_keys =
1066 Vec::from_iter(delta.vault().fungible().iter().map(|(vault_key, _)| *vault_key));
1067 let prev_balances = select_vault_balances_by_vault_keys(conn, account_id, &vault_keys)?;
1068
1069 let mut assets = Vec::new();
1071
1072 for (vault_key, amount_delta) in delta.vault().fungible().iter() {
1074 let faucet_id = vault_key.faucet_id();
1075 let callback_flag = vault_key.callback_flag();
1076 let prev_amount = prev_balances.get(&vault_key.to_word()).copied().unwrap_or(0);
1077 let prev_asset = FungibleAsset::new(faucet_id, prev_amount)?.with_callbacks(callback_flag);
1078 let amount_abs = amount_delta.unsigned_abs();
1079 let delta = FungibleAsset::new(faucet_id, amount_abs)?.with_callbacks(callback_flag);
1080 let new_balance = if *amount_delta < 0 {
1081 prev_asset.sub(delta)?
1082 } else {
1083 prev_asset.add(delta)?
1084 };
1085 let update_or_remove = if new_balance.amount().as_u64() == 0 {
1086 None
1087 } else {
1088 Some(Asset::from(new_balance))
1089 };
1090 assets.push((account_id, new_balance.vault_key(), update_or_remove));
1091 }
1092
1093 for (asset, delta_action) in delta.vault().non_fungible().iter() {
1095 let asset_update = match delta_action {
1096 NonFungibleDeltaAction::Add => Some(Asset::NonFungible(*asset)),
1097 NonFungibleDeltaAction::Remove => None,
1098 };
1099 assets.push((account_id, asset.vault_key(), asset_update));
1100 }
1101
1102 let mut storage = Vec::new();
1105 for (slot_name, map_delta) in delta.storage().maps() {
1106 for (key, value) in map_delta.entries() {
1107 storage.push((account_id, slot_name.clone(), *key, *value));
1108 }
1109 }
1110
1111 let slot_names = Vec::from_iter(delta.storage().maps().filter_map(|(slot_name, map_delta)| {
1113 if map_delta.is_empty() {
1114 None
1115 } else {
1116 Some(slot_name.clone())
1117 }
1118 }));
1119
1120 let map_entries = select_latest_storage_map_entries_for_slots(conn, &account_id, &slot_names)?;
1121
1122 let new_storage_header =
1124 apply_storage_delta(&state_headers.storage_header, delta.storage(), &map_entries)?;
1125
1126 let new_vault_root = {
1128 let assets = select_latest_vault_assets(conn, account_id)?;
1129 let mut vault = AssetVault::new(&assets)?;
1130 vault.apply_delta(delta.vault())?;
1131 vault.root()
1132 };
1133
1134 let new_nonce_value = state_headers
1136 .nonce
1137 .as_canonical_u64()
1138 .checked_add(delta.nonce_delta().as_canonical_u64())
1139 .ok_or_else(|| {
1140 DatabaseError::DataCorrupted(format!("Nonce overflow for account {account_id}"))
1141 })?;
1142 let new_nonce = Felt::new_unchecked(new_nonce_value);
1143
1144 let account_state = PartialAccountState {
1146 nonce: new_nonce,
1147 code_commitment: state_headers.code_commitment,
1148 storage_header: new_storage_header,
1149 vault_root: new_vault_root,
1150 };
1151
1152 let account_header = miden_protocol::account::AccountHeader::new(
1153 account_id,
1154 account_state.nonce,
1155 account_state.vault_root,
1156 account_state.storage_header.to_commitment(),
1157 account_state.code_commitment,
1158 );
1159
1160 if account_header.to_commitment() != update.final_state_commitment() {
1161 return Err(DatabaseError::AccountCommitmentsMismatch {
1162 calculated: account_header.to_commitment(),
1163 expected: update.final_state_commitment(),
1164 });
1165 }
1166
1167 Ok((AccountStateForInsert::PartialState(account_state), storage, assets))
1168}
1169
1170pub(crate) fn select_network_accounts_subset(
1174 conn: &mut SqliteConnection,
1175 account_ids: &[AccountId],
1176) -> Result<HashSet<AccountId>, DatabaseError> {
1177 QueryParamAccountIdLimit::check(account_ids.len())?;
1178 let id_bytes: Vec<Vec<u8>> =
1179 account_ids.iter().map(miden_crypto::utils::Serializable::to_bytes).collect();
1180
1181 let rows: Vec<Vec<u8>> =
1182 SelectDsl::select(schema::accounts::table, schema::accounts::account_id)
1183 .filter(
1184 schema::accounts::account_id
1185 .eq_any(&id_bytes)
1186 .and(
1187 schema::accounts::network_account_type
1188 .eq(NetworkAccountType::Network.to_raw_sql()),
1189 )
1190 .and(schema::accounts::is_latest.eq(true)),
1191 )
1192 .load::<Vec<u8>>(conn)
1193 .map_err(DatabaseError::Diesel)?;
1194
1195 rows.into_iter()
1196 .map(|bytes| {
1197 AccountId::read_from_bytes(&bytes).map_err(DatabaseError::DeserializationError)
1198 })
1199 .collect()
1200}
1201
1202#[miden_instrument(
1204 target = COMPONENT,
1205 skip_all,
1206 err,
1207)]
1208pub(crate) fn upsert_accounts(
1209 conn: &mut SqliteConnection,
1210 accounts: &[BlockAccountUpdate],
1211 block_num: BlockNumber,
1212) -> Result<usize, DatabaseError> {
1213 let mut count = 0;
1214 for update in accounts {
1215 let account_id = update.account_id();
1216 let account_id_bytes = account_id.to_bytes();
1217
1218 let existing: Option<(i64, i32)> = QueryDsl::select(
1221 schema::accounts::table.filter(
1222 schema::accounts::account_id
1223 .eq(&account_id_bytes)
1224 .and(schema::accounts::is_latest.eq(true)),
1225 ),
1226 (schema::accounts::created_at_block, schema::accounts::network_account_type),
1227 )
1228 .first(conn)
1229 .optional()
1230 .map_err(DatabaseError::Diesel)?;
1231
1232 let created_at_block = match existing {
1233 Some((raw, _)) => BlockNumber::from_raw_sql(raw)?,
1234 None => block_num,
1235 };
1236
1237 let (account_state, pending_storage_inserts, pending_asset_inserts) = match update.details()
1242 {
1243 AccountUpdateDetails::Private => (AccountStateForInsert::Private, vec![], vec![]),
1244
1245 AccountUpdateDetails::Delta(delta) if delta.is_full_state() => {
1247 let account = Account::try_from(delta)
1248 .expect("Delta to full account always works for full state deltas");
1249 debug_assert_eq!(account_id, account.id());
1250
1251 prepare_full_account_update(update, account)?
1252 },
1253
1254 AccountUpdateDetails::Delta(delta) => {
1256 prepare_partial_account_update(conn, update, account_id, delta)?
1257 },
1258 };
1259
1260 let network_account_type = match existing {
1263 Some((_, raw)) => NetworkAccountType::from_raw_sql(raw)?,
1264 None => match &account_state {
1265 AccountStateForInsert::FullAccount(account)
1266 if NetworkAccount::new(account.clone()).is_ok() =>
1267 {
1268 NetworkAccountType::Network
1269 },
1270 _ => NetworkAccountType::None,
1271 },
1272 };
1273
1274 if let AccountStateForInsert::FullAccount(ref account) = account_state {
1276 let code = account.code();
1277 let code_value = AccountCodeRowInsert {
1278 code_commitment: code.commitment().to_bytes(),
1279 code: code.to_bytes(),
1280 };
1281 diesel::insert_into(schema::account_codes::table)
1282 .values(&code_value)
1283 .on_conflict(schema::account_codes::code_commitment)
1284 .do_nothing()
1285 .execute(conn)?;
1286 }
1287
1288 diesel::update(schema::accounts::table)
1290 .filter(
1291 schema::accounts::account_id
1292 .eq(&account_id_bytes)
1293 .and(schema::accounts::is_latest.eq(true)),
1294 )
1295 .set(schema::accounts::is_latest.eq(false))
1296 .execute(conn)?;
1297
1298 let account_value = match &account_state {
1299 AccountStateForInsert::Private => AccountRowInsert::new_private(
1300 account_id,
1301 network_account_type,
1302 update.final_state_commitment(),
1303 block_num,
1304 created_at_block,
1305 ),
1306 AccountStateForInsert::FullAccount(account) => AccountRowInsert::new_from_account(
1307 account_id,
1308 network_account_type,
1309 update.final_state_commitment(),
1310 block_num,
1311 created_at_block,
1312 account,
1313 ),
1314 AccountStateForInsert::PartialState(state) => AccountRowInsert::new_from_partial(
1315 account_id,
1316 network_account_type,
1317 update.final_state_commitment(),
1318 block_num,
1319 created_at_block,
1320 state,
1321 ),
1322 };
1323
1324 diesel::insert_into(schema::accounts::table)
1325 .values(&account_value)
1326 .on_conflict((schema::accounts::account_id, schema::accounts::block_num))
1327 .do_update()
1328 .set(&account_value)
1329 .execute(conn)?;
1330
1331 for (acc_id, slot_name, key, value) in pending_storage_inserts {
1333 insert_account_storage_map_value(conn, acc_id, block_num, slot_name, key, value)?;
1334 }
1335
1336 for (acc_id, vault_key, update) in pending_asset_inserts {
1337 insert_account_vault_asset(conn, acc_id, block_num, vault_key, update)?;
1338 }
1339
1340 count += 1;
1341 }
1342
1343 Ok(count)
1344}
1345
1346#[derive(Insertable, Debug, Clone)]
1347#[diesel(table_name = schema::account_codes)]
1348pub(crate) struct AccountCodeRowInsert {
1349 pub(crate) code_commitment: Vec<u8>,
1350 pub(crate) code: Vec<u8>,
1351}
1352
1353#[derive(Insertable, AsChangeset, Debug, Clone)]
1354#[diesel(table_name = schema::accounts)]
1355pub(crate) struct AccountRowInsert {
1356 pub(crate) account_id: Vec<u8>,
1357 pub(crate) network_account_type: i32,
1358 pub(crate) block_num: i64,
1359 pub(crate) account_commitment: Vec<u8>,
1360 pub(crate) code_commitment: Option<Vec<u8>>,
1361 pub(crate) nonce: Option<i64>,
1362 pub(crate) storage_header: Option<Vec<u8>>,
1363 pub(crate) vault_root: Option<Vec<u8>>,
1364 pub(crate) is_latest: bool,
1365 pub(crate) created_at_block: i64,
1366}
1367
1368impl AccountRowInsert {
1369 pub(crate) fn new_private(
1371 account_id: AccountId,
1372 network_account_type: NetworkAccountType,
1373 account_commitment: Word,
1374 block_num: BlockNumber,
1375 created_at_block: BlockNumber,
1376 ) -> Self {
1377 Self {
1378 account_id: account_id.to_bytes(),
1379 network_account_type: network_account_type.to_raw_sql(),
1380 account_commitment: account_commitment.to_bytes(),
1381 block_num: block_num.to_raw_sql(),
1382 nonce: None,
1383 code_commitment: None,
1384 storage_header: None,
1385 vault_root: None,
1386 is_latest: true,
1387 created_at_block: created_at_block.to_raw_sql(),
1388 }
1389 }
1390
1391 fn new_from_account(
1393 account_id: AccountId,
1394 network_account_type: NetworkAccountType,
1395 account_commitment: Word,
1396 block_num: BlockNumber,
1397 created_at_block: BlockNumber,
1398 account: &Account,
1399 ) -> Self {
1400 Self {
1401 account_id: account_id.to_bytes(),
1402 network_account_type: network_account_type.to_raw_sql(),
1403 account_commitment: account_commitment.to_bytes(),
1404 block_num: block_num.to_raw_sql(),
1405 nonce: Some(nonce_to_raw_sql(account.nonce())),
1406 code_commitment: Some(account.code().commitment().to_bytes()),
1407 storage_header: Some(account.storage().to_header().to_bytes()),
1408 vault_root: Some(account.vault().root().to_bytes()),
1409 is_latest: true,
1410 created_at_block: created_at_block.to_raw_sql(),
1411 }
1412 }
1413
1414 fn new_from_partial(
1416 account_id: AccountId,
1417 network_account_type: NetworkAccountType,
1418 account_commitment: Word,
1419 block_num: BlockNumber,
1420 created_at_block: BlockNumber,
1421 state: &PartialAccountState,
1422 ) -> Self {
1423 Self {
1424 account_id: account_id.to_bytes(),
1425 network_account_type: network_account_type.to_raw_sql(),
1426 account_commitment: account_commitment.to_bytes(),
1427 block_num: block_num.to_raw_sql(),
1428 nonce: Some(nonce_to_raw_sql(state.nonce)),
1429 code_commitment: Some(state.code_commitment.to_bytes()),
1430 storage_header: Some(state.storage_header.to_bytes()),
1431 vault_root: Some(state.vault_root.to_bytes()),
1432 is_latest: true,
1433 created_at_block: created_at_block.to_raw_sql(),
1434 }
1435 }
1436}
1437
1438#[derive(Insertable, AsChangeset, Debug, Clone)]
1439#[diesel(table_name = schema::account_vault_assets)]
1440pub(crate) struct AccountAssetRowInsert {
1441 pub(crate) account_id: Vec<u8>,
1442 pub(crate) block_num: i64,
1443 pub(crate) vault_key: Vec<u8>,
1444 pub(crate) asset: Option<Vec<u8>>,
1445 pub(crate) is_latest: bool,
1446}
1447
1448impl AccountAssetRowInsert {
1449 pub(crate) fn new(
1450 account_id: &AccountId,
1451 vault_key: &AssetVaultKey,
1452 block_num: BlockNumber,
1453 asset: Option<Asset>,
1454 is_latest: bool,
1455 ) -> Self {
1456 let account_id = account_id.to_bytes();
1457 let vault_key: Word = (*vault_key).into();
1458 let vault_key = vault_key.to_bytes();
1459 let block_num = block_num.to_raw_sql();
1460 let asset = asset.map(|asset| asset.to_bytes());
1461 Self {
1462 account_id,
1463 block_num,
1464 vault_key,
1465 asset,
1466 is_latest,
1467 }
1468 }
1469}
1470
1471#[derive(Insertable, AsChangeset, Debug, Clone)]
1472#[diesel(table_name = schema::account_storage_map_values)]
1473pub(crate) struct AccountStorageMapRowInsert {
1474 pub(crate) account_id: Vec<u8>,
1475 pub(crate) block_num: i64,
1476 pub(crate) slot_name: String,
1477 pub(crate) key: Vec<u8>,
1478 pub(crate) value: Vec<u8>,
1479 pub(crate) is_latest: bool,
1480}
1481
1482pub const HISTORICAL_BLOCK_RETENTION: u32 = 50;
1489
1490#[miden_instrument(
1499 target = COMPONENT,
1500 skip_all,
1501 err,
1502 fields(
1503 cutoff_block,
1504 ),
1505)]
1506pub(crate) fn prune_history(
1507 conn: &mut SqliteConnection,
1508 chain_tip: BlockNumber,
1509) -> Result<(usize, usize, usize), DatabaseError> {
1510 let cutoff_block = i64::from(chain_tip.as_u32().saturating_sub(HISTORICAL_BLOCK_RETENTION));
1511 tracing::Span::current().record("cutoff_block", cutoff_block);
1512 let vault_deleted = prune_account_vault_assets(conn, cutoff_block)?;
1513 let storage_deleted = prune_account_storage_map_values(conn, cutoff_block)?;
1514 let codes_deleted = prune_account_codes(conn, cutoff_block)?;
1515
1516 Ok((vault_deleted, storage_deleted, codes_deleted))
1517}
1518
1519#[miden_instrument(
1520 target = COMPONENT,
1521 skip_all,
1522 err,
1523 fields(
1524 cutoff_block,
1525 ),
1526)]
1527fn prune_account_vault_assets(
1528 conn: &mut SqliteConnection,
1529 cutoff_block: i64,
1530) -> Result<usize, DatabaseError> {
1531 diesel::delete(
1532 schema::account_vault_assets::table.filter(
1533 schema::account_vault_assets::block_num
1534 .lt(cutoff_block)
1535 .and(schema::account_vault_assets::is_latest.eq(false)),
1536 ),
1537 )
1538 .execute(conn)
1539 .map_err(DatabaseError::Diesel)
1540}
1541
1542#[miden_instrument(
1543 target = COMPONENT,
1544 skip_all,
1545 err,
1546 fields(
1547 cutoff_block,
1548 ),
1549)]
1550fn prune_account_storage_map_values(
1551 conn: &mut SqliteConnection,
1552 cutoff_block: i64,
1553) -> Result<usize, DatabaseError> {
1554 diesel::delete(
1555 schema::account_storage_map_values::table.filter(
1556 schema::account_storage_map_values::block_num
1557 .lt(cutoff_block)
1558 .and(schema::account_storage_map_values::is_latest.eq(false)),
1559 ),
1560 )
1561 .execute(conn)
1562 .map_err(DatabaseError::Diesel)
1563}
1564
1565#[miden_instrument(
1576 target = COMPONENT,
1577 skip_all,
1578 err,
1579 fields(
1580 cutoff_block,
1581 ),
1582)]
1583fn prune_account_codes(
1584 conn: &mut SqliteConnection,
1585 cutoff_block: i64,
1586) -> Result<usize, DatabaseError> {
1587 use diesel::sql_types::BigInt;
1588
1589 diesel::sql_query(
1590 "DELETE FROM account_codes \
1591 WHERE code_commitment NOT IN ( \
1592 SELECT DISTINCT code_commitment \
1593 FROM ( \
1594 SELECT code_commitment \
1595 FROM accounts INDEXED BY idx_accounts_prune_code \
1596 WHERE code_commitment IS NOT NULL \
1597 AND block_num >= ?1 \
1598 UNION ALL \
1599 SELECT code_commitment \
1600 FROM accounts INDEXED BY idx_accounts_latest_code_commitment \
1601 WHERE code_commitment IS NOT NULL \
1602 AND is_latest = 1 \
1603 ) \
1604 )",
1605 )
1606 .bind::<BigInt, _>(cutoff_block)
1607 .execute(conn)
1608 .map_err(DatabaseError::Diesel)
1609}