Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
7 changes: 4 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,14 @@
- Remove `trait AccountTreeStorage` ([#1352](https://github.com/0xMiden/miden-node/issues/1352)).
- [BREAKING] `SubmitProvenTransaction` now **requires** that the network's genesis commitment is set in the request's `ACCEPT` header ([#1298](https://github.com/0xMiden/miden-node/pull/1298), [#1436](https://github.com/0xMiden/miden-node/pull/1436)).
- Add `S` generic to `NullifierTree` to allow usage with `LargeSmt`s ([#1353](https://github.com/0xMiden/miden-node/issues/1353)).
- Removed internal errors from the `miden-network-monitor` ([#1424](https://github.com/0xMiden/miden-node/pull/1424)).
- Track network transactions latency in `miden-network-monitor` ([#1430](https://github.com/0xMiden/miden-node/pull/1430)).
- Refactor account table and introduce tracking forest ([#1394](https://github.com/0xMiden/miden-node/pull/1394)).
- [BREAKING] Re-organized RPC protobuf schema to be independent of internal schema ([#1401](https://github.com/0xMiden/miden-node/pull/1401)).
- Increased the maximum query limit for the store ([#1443](https://github.com/0xMiden/miden-node/pull/1443)).
- Removed internal errors from the `miden-network-monitor` ([#1424](https://github.com/0xMiden/miden-node/pull/1424)).
- [BREAKING] Added block signing capabilities to Validator component and updated gensis bootstrap to sign blocks with configured signer ([#1426](https://github.com/0xMiden/miden-node/pull/1426)).
- Track network transactions latency in `miden-network-monitor` ([#1430](https://github.com/0xMiden/miden-node/pull/1430)).
- Reduced default block interval from `5s` to `2s` ([#1438](https://github.com/0xMiden/miden-node/pull/1438)).
- Increased retained account tree history from 33 to 100 blocks to account for the reduced block interval ([#1438](https://github.com/0xMiden/miden-node/pull/1438)).
- Increased the maximum query limit for the store ([#1443](https://github.com/0xMiden/miden-node/pull/1443)).
- [BREAKING] Migrated to version `v0.20` of the VM ([#1476](https://github.com/0xMiden/miden-node/pull/1476)).
- [BREAKING] Change account in database representation ([#1481](https://github.com/0xMiden/miden-node/pull/1481)).

Expand Down
1 change: 1 addition & 0 deletions crates/proto/src/domain/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ impl From<AccountId> for proto::account::AccountId {

// ACCOUNT UPDATE
// ================================================================================================

#[derive(Debug, PartialEq)]
pub struct AccountSummary {
pub account_id: AccountId,
Expand Down
2 changes: 1 addition & 1 deletion crates/store/benches/account_tree_historical.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::hint::black_box;

use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
use miden_node_store::AccountTreeWithHistory;
use miden_node_store::accounts::AccountTreeWithHistory;
use miden_protocol::Word;
use miden_protocol::account::AccountId;
use miden_protocol::block::BlockNumber;
Expand Down
2 changes: 1 addition & 1 deletion crates/store/src/accounts/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ mod account_tree_with_history_tests {
/// Helper function to create an `AccountTree` from entries using the new API
fn create_account_tree(
entries: impl IntoIterator<Item = (AccountId, Word)>,
) -> AccountTree<LargeSmt<MemoryStorage>> {
) -> InMemoryAccountTree {
let smt_entries = entries
.into_iter()
.map(|(id, commitment)| (account_id_to_smt_key(id), commitment));
Expand Down
8 changes: 3 additions & 5 deletions crates/store/src/db/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,7 @@ impl TransactionRecord {
self,
note_records: Vec<NoteRecord>,
) -> proto::rpc::TransactionRecord {
let output_notes: Vec<proto::note::NoteSyncRecord> =
note_records.into_iter().map(Into::into).collect();
let output_notes = Vec::from_iter(note_records.into_iter().map(Into::into));

proto::rpc::TransactionRecord {
header: Some(proto::transaction::TransactionHeader {
Expand Down Expand Up @@ -323,7 +322,7 @@ impl Db {

/// Loads all the nullifiers from the DB.
#[instrument(level = "debug", target = COMPONENT, skip_all, ret(level = "debug"), err)]
pub async fn select_all_nullifiers(&self) -> Result<Vec<NullifierInfo>> {
pub(crate) async fn select_all_nullifiers(&self) -> Result<Vec<NullifierInfo>> {
self.transact("all nullifiers", move |conn| {
let nullifiers = queries::select_all_nullifiers(conn)?;
Ok(nullifiers)
Expand Down Expand Up @@ -392,7 +391,7 @@ impl Db {
.await
}

/// TODO marked for removal, replace with paged version
/// TODO marked for removal, replace with paged version.
#[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 All @@ -402,7 +401,6 @@ impl Db {
}

/// Returns all account IDs that have public state.
#[allow(dead_code)] // Will be used by InnerForest in next PR
#[instrument(level = "debug", target = COMPONENT, skip_all, ret(level = "debug"), err)]
pub async fn select_all_public_account_ids(&self) -> Result<Vec<AccountId>> {
self.transact("read all public account IDs", move |conn| {
Expand Down
1 change: 0 additions & 1 deletion crates/store/src/db/models/queries/accounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,6 @@ pub(crate) fn select_all_account_commitments(
/// ORDER BY
/// block_num ASC
/// ```
#[allow(dead_code)] // Will be used by InnerForest in next PR
pub(crate) fn select_all_public_account_ids(
conn: &mut SqliteConnection,
) -> Result<Vec<AccountId>, DatabaseError> {
Expand Down
273 changes: 273 additions & 0 deletions crates/store/src/inner_forest/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
use std::collections::BTreeMap;

use miden_protocol::account::delta::{AccountDelta, AccountStorageDelta, AccountVaultDelta};
use miden_protocol::account::{AccountId, NonFungibleDeltaAction, StorageSlotName};
use miden_protocol::asset::{Asset, FungibleAsset};
use miden_protocol::block::BlockNumber;
use miden_protocol::crypto::merkle::EmptySubtreeRoots;
use miden_protocol::crypto::merkle::smt::{SMT_DEPTH, SmtForest};
use miden_protocol::{EMPTY_WORD, Word};

#[cfg(test)]
mod tests;

// INNER FOREST
// ================================================================================================

/// Container for forest-related state that needs to be updated atomically.
pub(crate) struct InnerForest {
/// `SmtForest` for efficient account storage reconstruction.
/// Populated during block import with storage and vault SMTs.
forest: SmtForest,

/// Maps (`account_id`, `slot_name`, `block_num`) to SMT root.
/// Populated during block import for all storage map slots.
storage_map_roots: BTreeMap<(AccountId, StorageSlotName, BlockNumber), Word>,

/// Maps (`account_id`, `block_num`) to vault SMT root.
/// Tracks asset vault versions across all blocks with structural sharing.
vault_roots: BTreeMap<(AccountId, BlockNumber), Word>,
}

impl InnerForest {
pub(crate) fn new() -> Self {
Self {
forest: SmtForest::new(),
storage_map_roots: BTreeMap::new(),
vault_roots: BTreeMap::new(),
}
}

// HELPERS
// --------------------------------------------------------------------------------------------

/// Returns the root of an empty SMT.
const fn empty_smt_root() -> Word {
*EmptySubtreeRoots::entry(SMT_DEPTH, 0)
}

/// Retrieves the most recent vault SMT root for an account.
///
/// Returns the latest vault root entry regardless of block number.
/// Used when applying incremental deltas where we always want the previous state.
fn get_latest_vault_root(&self, account_id: AccountId) -> Word {
self.vault_roots
.range((account_id, BlockNumber::GENESIS)..)
.take_while(|((id, _), _)| *id == account_id)
.last()
.map_or_else(Self::empty_smt_root, |(_, root)| *root)
}
Comment on lines +49 to +59
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's mention in the doc comments that if a vault root for the specified account cannot be found, we return a root of an empty tree.

Also, looking at the implementation, I wonder if a better backing structure would be BTreeMap<AccountId, (BlockNumber, Word)>. This would make getting the last vault root much simpler.

Getting root for a specific block number should be pretty simple as well (we could use a binary search or even just a linear scan since we know that the number of entries will be pretty small per account ID).


/// Retrieves the vault SMT root for an account at or before the given block.
///
/// Finds the most recent vault root entry for the account, since vault state persists
/// across blocks where no changes occur.
//
// TODO: a fallback to DB lookup is required once pruning lands.
// Currently returns empty root which would be incorrect
#[cfg(test)]
fn get_vault_root(&self, account_id: AccountId, block_num: BlockNumber) -> Word {
self.vault_roots
.range((account_id, BlockNumber::GENESIS)..=(account_id, block_num))
.next_back()
.map_or_else(Self::empty_smt_root, |(_, root)| *root)
}

/// Retrieves the most recent storage map SMT root for an account slot.
///
/// Returns the latest storage root entry regardless of block number.
/// Used when applying incremental deltas where we always want the previous state.
fn get_latest_storage_map_root(
&self,
account_id: AccountId,
slot_name: &StorageSlotName,
) -> Word {
self.storage_map_roots
.range((account_id, slot_name.clone(), BlockNumber::GENESIS)..)
.take_while(|((id, name, _), _)| *id == account_id && name == slot_name)
.last()
.map_or_else(Self::empty_smt_root, |(_, root)| *root)
}

// PUBLIC INTERFACE
// --------------------------------------------------------------------------------------------

/// Applies account updates from a block to the forest.
///
/// Iterates through account updates and applies each delta to the forest.
/// Private accounts should be filtered out before calling this method.
///
/// # Arguments
///
/// * `block_num` - Block number for which these updates apply
/// * `account_updates` - Iterator of `AccountDelta` for public accounts
pub(crate) fn apply_block_updates(
&mut self,
block_num: BlockNumber,
account_updates: impl IntoIterator<Item = AccountDelta>,
) {
for delta in account_updates {
self.update_account(block_num, &delta);

tracing::debug!(
target: crate::COMPONENT,
account_id = %delta.id(),
%block_num,
is_full_state = delta.is_full_state(),
"Updated forest with account delta"
);
}
}

/// Updates the forest with account vault and storage changes from a delta.
///
/// Unified interface for updating all account state in the forest, handling both full-state
/// deltas (new accounts or reconstruction from DB) and partial deltas (incremental updates
/// during block application).
///
/// Full-state deltas (`delta.is_full_state() == true`) populate the forest from scratch using
/// an empty SMT root. Partial deltas apply changes on top of the previous block's state.
pub(crate) fn update_account(&mut self, block_num: BlockNumber, delta: &AccountDelta) {
let account_id = delta.id();
let is_full_state = delta.is_full_state();

if !delta.vault().is_empty() {
self.update_account_vault(block_num, account_id, delta.vault(), is_full_state);
}

if !delta.storage().is_empty() {
self.update_account_storage(block_num, account_id, delta.storage(), is_full_state);
}
}

// PRIVATE METHODS
// --------------------------------------------------------------------------------------------

/// Updates the forest with vault changes from a delta.
///
/// Processes both fungible and non-fungible asset changes, building entries for the vault SMT
/// and tracking the new root.
fn update_account_vault(
&mut self,
block_num: BlockNumber,
account_id: AccountId,
vault_delta: &AccountVaultDelta,
is_full_state: bool,
) {
let prev_root = if is_full_state {
Self::empty_smt_root()
} else {
self.get_latest_vault_root(account_id)
};
Comment on lines +157 to +161
Copy link
Contributor

Choose a reason for hiding this comment

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

This could be just:

let prev_root = self.get_latest_vault_root(account_id);


let mut entries = Vec::new();

// Process fungible assets
for (faucet_id, amount_delta) in vault_delta.fungible().iter() {
let key: Word =
FungibleAsset::new(*faucet_id, 0).expect("valid faucet id").vault_key().into();

let new_amount = if is_full_state {
// For full-state deltas, amount is the absolute value
(*amount_delta).try_into().expect("full-state amount should be non-negative")
} else {
// For partial deltas, amount is a change that must be applied to previous balance.
//
// TODO: SmtForest only exposes `fn open()` which computes a full Merkle
// proof. We only need the leaf, so a direct `fn get()` method would be faster.
let prev_amount = self
.forest
.open(prev_root, key)
.ok()
.and_then(|proof| proof.get(&key))
.and_then(|word| FungibleAsset::try_from(word).ok())
.map_or(0, |asset| asset.amount());

let new_balance = i128::from(prev_amount) + i128::from(*amount_delta);
u64::try_from(new_balance.max(0)).expect("balance fits in u64")
Copy link
Contributor

Choose a reason for hiding this comment

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

If new_balance somehow ends up negative, we should catch this. A panic could be fine for now, but ideally, it'd be an error.

};

let value = if new_amount == 0 {
EMPTY_WORD
} else {
let asset: Asset = FungibleAsset::new(*faucet_id, new_amount)
.expect("valid fungible asset")
.into();
Word::from(asset)
};
entries.push((key, value));
}

// Process non-fungible assets
for (asset, action) in vault_delta.non_fungible().iter() {
let value = match action {
NonFungibleDeltaAction::Add => Word::from(Asset::NonFungible(*asset)),
NonFungibleDeltaAction::Remove => EMPTY_WORD,
};
entries.push((asset.vault_key().into(), value));
}

if entries.is_empty() {
return;
}

let updated_root = self
.forest
.batch_insert(prev_root, entries.iter().copied())
.expect("forest insertion should succeed");

self.vault_roots.insert((account_id, block_num), updated_root);

tracing::debug!(
target: crate::COMPONENT,
%account_id,
%block_num,
vault_entries = entries.len(),
"Updated vault in forest"
);
}

/// Updates the forest with storage map changes from a delta.
///
/// Processes storage map slot deltas, building SMTs for each modified slot
/// and tracking the new roots.
fn update_account_storage(
&mut self,
block_num: BlockNumber,
account_id: AccountId,
storage_delta: &AccountStorageDelta,
is_full_state: bool,
) {
for (slot_name, map_delta) in storage_delta.maps() {
let prev_root = if is_full_state {
Self::empty_smt_root()
} else {
self.get_latest_storage_map_root(account_id, slot_name)
};
Comment on lines +242 to +246
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, this could probably be just:

let prev_root = self.get_latest_storage_map_root(account_id, slot_name);


let entries: Vec<_> =
map_delta.entries().iter().map(|(key, value)| ((*key).into(), *value)).collect();

if entries.is_empty() {
continue;
}

let updated_root = self
.forest
.batch_insert(prev_root, entries.iter().copied())
.expect("forest insertion should succeed");

self.storage_map_roots
.insert((account_id, slot_name.clone(), block_num), updated_root);

tracing::debug!(
target: crate::COMPONENT,
%account_id,
%block_num,
?slot_name,
entries = entries.len(),
"Updated storage map in forest"
);
}
}
}
Loading