Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
4ff0970
for account queries, now return partials too
drahnr Dec 1, 2025
5ee1043
drop all tables as part of migration
drahnr Dec 1, 2025
8eca359
externalize storage and vault blobs to separate tables
drahnr Dec 1, 2025
e7f17ed
trailing .
drahnr Dec 1, 2025
c8b43ab
smt forest
drahnr Dec 1, 2025
19164be
changset, should go away after rebase
drahnr Dec 1, 2025
8eb49af
improve
drahnr Dec 1, 2025
6725461
TODO and deprecation
drahnr Dec 1, 2025
ee65a88
account queries
drahnr Dec 1, 2025
741df6f
yes
drahnr Dec 2, 2025
6a64077
why
drahnr Dec 2, 2025
9d5806e
y
drahnr Dec 2, 2025
2964a93
y
drahnr Dec 2, 2025
9416a63
review comments
drahnr Dec 2, 2025
ccc2d63
sanitize comments
drahnr Dec 2, 2025
66ea831
remove
drahnr Dec 4, 2025
1c4f8b1
cleanup
drahnr Dec 4, 2025
80e0393
fix queries with _at suffix
drahnr Dec 4, 2025
e7bf1aa
cleanup
drahnr Dec 4, 2025
dad90e7
simplify
drahnr Dec 4, 2025
e441245
fix
drahnr Dec 4, 2025
0a319d1
cleanup
drahnr Dec 4, 2025
8897939
remove dead code
drahnr Dec 4, 2025
7d7fefc
add test
drahnr Dec 5, 2025
0f53fa9
add block exists helper
drahnr Dec 5, 2025
7400134
Merge remote-tracking branch 'origin' into bernhard-integrate-smtforest
drahnr Dec 9, 2025
0e2d871
simplify
drahnr Dec 9, 2025
f78103e
better docs
drahnr Dec 9, 2025
ea05b01
split long function in State
drahnr Dec 9, 2025
3cd457a
better
drahnr Dec 9, 2025
c8f0eb1
clippy et al
drahnr Dec 9, 2025
ed7224e
review
drahnr Dec 10, 2025
6336f41
address review comments
drahnr Dec 10, 2025
a1173f7
Revert "address review comments"
drahnr Dec 11, 2025
36470a5
improve
drahnr Dec 11, 2025
928fdb4
yes
drahnr Dec 18, 2025
5de3936
tuple ticks
drahnr Dec 18, 2025
f5b4898
Merge remote-tracking branch 'origin' into bernhard-integrate-smtforest
drahnr Dec 18, 2025
ca5ef9a
docs
drahnr Dec 18, 2025
17fd95b
from_iter
drahnr Dec 18, 2025
22f3ca9
simplify
drahnr Dec 18, 2025
3ee1884
docs
drahnr Dec 18, 2025
eaf7242
undo
drahnr Dec 18, 2025
72126e1
one more enum
drahnr Dec 18, 2025
be9071b
docs
drahnr Dec 18, 2025
a0f8fc9
unneces
drahnr Dec 18, 2025
88c058b
simplify
drahnr Dec 18, 2025
b84f25f
misleading
drahnr Dec 18, 2025
25b5550
bound
drahnr Dec 18, 2025
31dacdd
fmt
drahnr Dec 18, 2025
55f4a46
changelog
drahnr Dec 18, 2025
bf67ce8
0 ->1; 1->0
drahnr Dec 19, 2025
0c0e32b
avoid full paths
drahnr Dec 19, 2025
ec4318e
fn
drahnr Dec 19, 2025
4bfee30
refactor, simplify
drahnr Dec 19, 2025
b8d2e66
yuk
drahnr Dec 19, 2025
e453faa
remoe useless comment
drahnr Dec 19, 2025
f6d1ce1
shorthandg pu
drahnr Dec 19, 2025
b1f9cf6
delete unused
drahnr Dec 19, 2025
d2d9e8c
review
drahnr Dec 20, 2025
2781db8
simplify
drahnr Dec 22, 2025
b0e537c
Merge remote-tracking branch 'origin' into bernhard-integrate-smtforest
drahnr Dec 22, 2025
d6b31ef
minor
drahnr Dec 22, 2025
9c859fc
fmt
drahnr Dec 23, 2025
b016495
Merge branch 'next' into bernhard-integrate-smtforest
bobbinth Dec 27, 2025
579b9dc
chore: fix merge conflicts
bobbinth Dec 27, 2025
3336edb
chore: fix test
bobbinth Dec 27, 2025
2aa8c8b
chore: minor formatting changes
bobbinth Dec 27, 2025
354d586
chore: move InnerForest module
bobbinth Dec 27, 2025
a96def0
chore: refactor SMT forest initialization
bobbinth Dec 27, 2025
e8cdad1
chore: re-organize account queries
bobbinth Dec 27, 2025
3009bf7
nope
drahnr Dec 29, 2025
3110962
fix inconsistency
drahnr Dec 29, 2025
53cb5e8
faster
drahnr Dec 29, 2025
0cc0c61
undue changes
drahnr Dec 29, 2025
ac7b8f9
move fn to innerforest
drahnr Dec 29, 2025
369db2f
y
drahnr Dec 29, 2025
6cd1033
another
drahnr Dec 29, 2025
7613624
test re-review
drahnr Dec 29, 2025
e04ff10
fuckup
drahnr Dec 29, 2025
9d8c220
update
drahnr Dec 29, 2025
5ed1a4f
sync docs
drahnr Dec 29, 2025
3346d9f
cleanup
drahnr Dec 29, 2025
dbbc1eb
fmt
drahnr Dec 29, 2025
c5a199a
Merge branch 'bernhard-db-schema-queries' into bernhard-integrate-smt…
drahnr Dec 30, 2025
c712ba4
Merge branch 'bernhard-db-schema-queries' into bernhard-integrate-smt…
drahnr Jan 4, 2026
d9a666f
review
drahnr Jan 6, 2026
29b840c
remove dead code
drahnr Jan 6, 2026
1b22a34
chore: minor rename
bobbinth Jan 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 47 additions & 5 deletions crates/proto/src/domain/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ impl From<AccountId> for proto::account::AccountId {
// ACCOUNT UPDATE
// ================================================================================================

// TODO should be called `AccountStateRef` or so
#[derive(Debug, PartialEq)]
pub struct AccountSummary {
pub account_id: AccountId,
Expand All @@ -86,6 +87,7 @@ impl From<&AccountSummary> for proto::account::AccountSummary {
}
}

// TODO #[deprecated(note = "avoid this type, details will be `None` always!")]
#[derive(Debug, PartialEq)]
pub struct AccountInfo {
pub summary: AccountSummary,
Expand Down Expand Up @@ -375,6 +377,27 @@ impl AccountVaultDetails {
}
}

/// Creates `AccountVaultDetails` from vault entries (key-value pairs).
///
/// This is useful when entries have been fetched directly from the database
/// rather than extracted from an `AssetVault`.
///
/// The entries are `(vault_key, asset)` pairs where `asset` is a Word representation.
pub fn from_entries(entries: Vec<(Word, Word)>) -> Result<Self, miden_objects::AssetError> {
let too_many_assets = entries.len() > Self::MAX_RETURN_ENTRIES;

if too_many_assets {
return Ok(Self::too_many());
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for this PR, but we should probably have a way not to read all assets from the database in case the number of assets is too big. So, as a future optimization, we could also store num_assets in the accounts table (next to vault_root) - though, it will require updating this filed as the number of assets changes, which will not be trivial.

Copy link
Contributor Author

@drahnr drahnr Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be easier to add a subquery with LIMIT (n+1) clause where n is our actual desired limit to mitigate large~ish memory overhead.


let assets = entries
.into_iter()
.map(|(_key, asset_word)| Asset::try_from(asset_word))
.collect::<Result<Vec<_>, _>>()?;

Ok(Self { too_many_assets: false, assets })
}

fn too_many() -> Self {
Self {
too_many_assets: true,
Expand Down Expand Up @@ -418,16 +441,20 @@ impl From<AccountVaultDetails> for proto::rpc_store::AccountVaultDetails {
pub struct AccountStorageMapDetails {
pub slot_index: u8,
pub too_many_entries: bool,
// TODO the following is only for the case when _all_ entries are included
// TODO for partials, we also need to provide merkle proofs / a partial SMT with inner nodes
// Reason: if all leaf values are included, one can reconstruct the entire SMT, if just one
// is missing one cannot
pub map_entries: Vec<(Word, Word)>,
}

impl AccountStorageMapDetails {
const MAX_RETURN_ENTRIES: usize = 1000;
pub const MAX_RETURN_ENTRIES: usize = 1000;

pub fn new(slot_index: u8, slot_data: SlotData, storage_map: &StorageMap) -> Self {
match slot_data {
SlotData::All => Self::from_all_entries(slot_index, storage_map),
SlotData::MapKeys(keys) => Self::from_specific_keys(slot_index, &keys[..], storage_map),
SlotData::MapKeys(_keys) => Self::from_all_entries(slot_index, storage_map), /* TODO use from_specific_keys */
}
}

Expand All @@ -444,12 +471,27 @@ impl AccountStorageMapDetails {
}
}

fn from_specific_keys(slot_index: u8, keys: &[Word], storage_map: &StorageMap) -> Self {
// TODO this is
#[allow(dead_code)]
fn from_specific_keys(slot_index: u8, keys: &[Word], _storage_map: &StorageMap) -> Self {
if keys.len() > Self::MAX_RETURN_ENTRIES {
Self::too_many_entries(slot_index)
} else {
// TODO For now, we return all entries instead of specific keys with proofs
Self::from_all_entries(slot_index, storage_map)
todo!("construct a partial SMT / set of key values")
}
}

/// Creates an `AccountStorageMapDetails` from already-queried entries (e.g., from database).
/// This is useful when entries have been fetched directly rather than extracted from a
/// `StorageMap`.
pub fn from_entries(slot_index: u8, map_entries: Vec<(Word, Word)>) -> Self {
let too_many_entries = map_entries.len() > Self::MAX_RETURN_ENTRIES;
let map_entries = if too_many_entries { Vec::new() } else { map_entries };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to one of the above comments: we should probably optimize this in the future to avoid reading full maps from the database when there are too many entries - but that would be even more tricky than doing this for the asset vault.


Self {
slot_index,
too_many_entries,
map_entries,
}
}

Expand Down
12 changes: 12 additions & 0 deletions crates/store/src/db/migrations/2025062000000_setup/down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-- Drop all tables in reverse order of creation (respecting foreign key dependencies)
DROP TABLE IF EXISTS transactions;
DROP TABLE IF EXISTS nullifiers;
DROP TABLE IF EXISTS account_vault_headers;
DROP TABLE IF EXISTS account_vault_assets;
DROP TABLE IF EXISTS account_storage_map_values;
DROP TABLE IF EXISTS note_scripts;
DROP TABLE IF EXISTS notes;
DROP TABLE IF EXISTS account_storage_headers;
DROP TABLE IF EXISTS accounts;
DROP TABLE IF EXISTS account_codes;
DROP TABLE IF EXISTS block_headers;
42 changes: 38 additions & 4 deletions crates/store/src/db/migrations/2025062000000_setup/up.sql
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,15 @@ CREATE TABLE accounts (
block_num INTEGER NOT NULL,
account_commitment BLOB NOT NULL,
code_commitment BLOB,
storage BLOB,
vault BLOB,
nonce INTEGER,
is_latest BOOLEAN NOT NULL DEFAULT 0, -- Indicates if this is the latest state for this account_id

PRIMARY KEY (account_id, block_num),
CONSTRAINT all_null_or_none_null CHECK
(
(code_commitment IS NOT NULL AND storage IS NOT NULL AND vault IS NOT NULL AND nonce IS NOT NULL)
(code_commitment IS NOT NULL AND nonce IS NOT NULL)
OR
(code_commitment IS NULL AND storage IS NULL AND vault IS NULL AND nonce IS NULL)
(code_commitment IS NULL AND nonce IS NULL)
)
) WITHOUT ROWID;

Expand All @@ -40,6 +38,26 @@ CREATE INDEX idx_accounts_block_num ON accounts(block_num);
-- Index for joining with account_codes
CREATE INDEX idx_accounts_code_commitment ON accounts(code_commitment) WHERE code_commitment IS NOT NULL;

-- Table to store storage slot headers (slot types and commitments)
CREATE TABLE account_storage_headers (
account_id BLOB NOT NULL,
block_num INTEGER NOT NULL,
slot_index INTEGER NOT NULL,
slot_type INTEGER NOT NULL, -- 0=Map, 1=Value (as per StorageSlotType)
slot_commitment BLOB NOT NULL,
is_latest BOOLEAN NOT NULL DEFAULT 0,

PRIMARY KEY (account_id, block_num, slot_index),
CONSTRAINT slot_index_is_u8 CHECK (slot_index BETWEEN 0 AND 0xFF),
CONSTRAINT slot_type_in_enum CHECK (slot_type BETWEEN 0 AND 1),
FOREIGN KEY (account_id, block_num) REFERENCES accounts(account_id, block_num) ON DELETE CASCADE
) WITHOUT ROWID;

-- Index for joining with accounts table
CREATE INDEX idx_account_storage_headers_account_block ON account_storage_headers(account_id, block_num);
-- Index for querying latest state
CREATE INDEX idx_account_storage_headers_latest ON account_storage_headers(account_id, is_latest) WHERE is_latest = 1;

CREATE TABLE notes (
committed_at INTEGER NOT NULL, -- Block number when the note was committed
batch_index INTEGER NOT NULL, -- Index of batch in block, starting from 0
Expand Down Expand Up @@ -122,6 +140,22 @@ CREATE INDEX idx_vault_assets_account_block ON account_vault_assets(account_id,
-- Index for querying latest assets
CREATE INDEX idx_vault_assets_latest ON account_vault_assets(account_id, is_latest) WHERE is_latest = 1;

-- Table to store vault headers (vault root commitments)
CREATE TABLE account_vault_headers (
account_id BLOB NOT NULL,
block_num INTEGER NOT NULL,
vault_root BLOB NOT NULL,
is_latest BOOLEAN NOT NULL DEFAULT 0,

PRIMARY KEY (account_id, block_num),
FOREIGN KEY (account_id, block_num) REFERENCES accounts(account_id, block_num) ON DELETE CASCADE
) WITHOUT ROWID;

-- Index for joining with accounts table
CREATE INDEX idx_account_vault_headers_account_block ON account_vault_headers(account_id, block_num);
-- Index for querying latest state
CREATE INDEX idx_account_vault_headers_latest ON account_vault_headers(account_id, is_latest) WHERE is_latest = 1;

CREATE TABLE nullifiers (
nullifier BLOB NOT NULL,
nullifier_prefix INTEGER NOT NULL,
Expand Down
94 changes: 93 additions & 1 deletion crates/store/src/db/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,7 @@ impl Db {
}

/// Loads all the account commitments from the DB.
// TODO add a variant with block_num as arg
#[instrument(level = "debug", target = COMPONENT, skip_all, ret(level = "debug"), err)]
pub async fn select_all_account_commitments(&self) -> Result<Vec<(AccountId, Word)>> {
self.transact("read all account commitments", move |conn| {
Expand Down Expand Up @@ -433,6 +434,77 @@ impl Db {
.await
}

/// Reconstructs account storage at a specific block from the database
///
/// This method queries the decomposed storage tables and reconstructs the full
/// `AccountStorage` with SMT backing for Map slots.
#[instrument(level = "debug", target = COMPONENT, skip_all, ret(level = "debug"), err)]
pub async fn select_account_storage_at_block(
&self,
account_id: AccountId,
block_num: BlockNumber,
) -> Result<miden_objects::account::AccountStorage> {
self.transact("Get account storage at block", move |conn| {
queries::select_account_storage_at_block(conn, account_id, block_num)
})
.await
}

/// Gets the latest account storage from the database
///
/// Uses the `is_latest` flag for efficient querying.
#[instrument(level = "debug", target = COMPONENT, skip_all, ret(level = "debug"), err)]
pub async fn select_latest_account_storage(
&self,
account_id: AccountId,
) -> Result<miden_objects::account::AccountStorage> {
self.transact("Get latest account storage", move |conn| {
queries::select_latest_account_storage(conn, account_id)
})
.await
}

/// Queries vault assets at a specific block
#[instrument(level = "debug", target = COMPONENT, skip_all, ret(level = "debug"), err)]
pub async fn select_account_vault_at_block(
&self,
account_id: AccountId,
block_num: BlockNumber,
) -> Result<Vec<(Word, Word)>> {
self.transact("Get account vault at block", move |conn| {
queries::select_account_vault_at_block(conn, account_id, block_num)
})
.await
}

/// Queries the account code for a specific account at a specific block number.
///
/// Returns `None` if the account doesn't exist at that block or has no code.
pub async fn select_account_code_at_block(
&self,
account_id: AccountId,
block_num: BlockNumber,
) -> Result<Option<Vec<u8>>> {
self.transact("Get account code at block", move |conn| {
queries::select_account_code_at_block(conn, account_id, block_num)
})
.await
}

/// Queries the account header for a specific account at a specific block number.
///
/// Returns `None` if the account doesn't exist at that block.
pub async fn select_account_header_at_block(
&self,
account_id: AccountId,
block_num: BlockNumber,
) -> Result<Option<miden_objects::account::AccountHeader>> {
self.transact("Get account header at block", move |conn| {
queries::select_account_header_at_block(conn, account_id, block_num)
})
.await
}

#[instrument(level = "debug", target = COMPONENT, skip_all, ret(level = "debug"), err)]
pub async fn get_state_sync(
&self,
Expand Down Expand Up @@ -531,7 +603,7 @@ impl Db {
.await
}

/// Selects storage map values for syncing storage maps for a specific account ID.
/// Selects storage map values for syncing storage maps for a specific account ID
///
/// The returned values are the latest known values up to `block_range.end()`, and no values
/// earlier than `block_range.start()` are returned.
Expand All @@ -546,6 +618,26 @@ impl Db {
.await
}

/// Selects specific storage map keys at a specific block from the DB
///
/// This method is optimized for querying specific keys without deserializing the entire
/// account, which is much faster for historical queries.
#[instrument(level = "debug", target = COMPONENT, skip_all, ret(level = "debug"), err)]
pub async fn select_storage_map_keys_at_block(
&self,
account_id: AccountId,
block_num: BlockNumber,
slot_index: u8,
keys: Vec<Word>,
) -> Result<Vec<(Word, Word)>> {
self.transact("select storage map keys at block", move |conn| {
models::queries::select_storage_map_keys_at_block(
conn, account_id, block_num, slot_index, &keys,
)
})
.await
}

/// Runs database optimization.
#[instrument(level = "debug", target = COMPONENT, skip_all, err)]
pub async fn optimize(&self) -> Result<(), DatabaseError> {
Expand Down
27 changes: 25 additions & 2 deletions crates/store/src/db/models/conv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ use std::any::type_name;

use miden_node_proto::domain::account::{NetworkAccountError, NetworkAccountPrefix};
use miden_objects::Felt;
use miden_objects::account::StorageSlotType;
use miden_objects::block::BlockNumber;
use miden_objects::note::{NoteExecutionMode, NoteTag};

Expand Down Expand Up @@ -116,6 +117,28 @@ impl SqlTypeConvert for NoteTag {
}
}

impl SqlTypeConvert for StorageSlotType {
type Raw = i32;
type Error = DatabaseTypeConversionError;

#[inline(always)]
fn from_raw_sql(raw: Self::Raw) -> Result<Self, Self::Error> {
match raw {
0 => Ok(StorageSlotType::Map),
1 => Ok(StorageSlotType::Value),
_ => Err(DatabaseTypeConversionError(type_name::<StorageSlotType>())),
}
}

#[inline(always)]
fn to_raw_sql(self) -> Self::Raw {
match self {
StorageSlotType::Map => 0,
StorageSlotType::Value => 1,
}
}
}

// Raw type conversions - eventually introduce wrapper types
// ===========================================================

Expand All @@ -130,9 +153,9 @@ pub(crate) fn nullifier_prefix_to_raw_sql(prefix: u16) -> i32 {
}

#[inline(always)]
pub(crate) fn raw_sql_to_nonce(raw: i64) -> u64 {
pub(crate) fn raw_sql_to_nonce(raw: i64) -> Felt {
debug_assert!(raw >= 0);
raw as u64
Felt::new(raw as u64)
}
#[inline(always)]
pub(crate) fn nonce_to_raw_sql(nonce: Felt) -> i64 {
Expand Down
Loading
Loading