Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
523347b
Add ntx data store impl and grpc client/server changes
sergerad Jan 15, 2026
5f071e3
Update get_storage_map_witness
sergerad Jan 16, 2026
5af082a
Changelog
sergerad Jan 16, 2026
9b49133
Add partial storage logic
sergerad Jan 16, 2026
eb26ce2
Merge branch 'next' of github.com:0xMiden/miden-node into sergerad-nt…
sergerad Jan 16, 2026
8f3c1c6
Comment and collect nits
sergerad Jan 16, 2026
220385f
RM account id branch
sergerad Jan 18, 2026
a517beb
Simplify datastore other errs
sergerad Jan 18, 2026
d706d3f
Rename request variable
sergerad Jan 18, 2026
66e94cd
Fix more variable names
sergerad Jan 18, 2026
71571bd
Fill details request with todos
sergerad Jan 18, 2026
4f01a6d
Register slot names in ntx data store
sergerad Jan 20, 2026
6c4463c
Use arc mutex
sergerad Jan 20, 2026
00c352d
Add temporary snippet with todo
sergerad Jan 20, 2026
a124390
Fix details request fields
sergerad Jan 20, 2026
9ea0855
Merge branch 'next' of github.com:0xMiden/miden-node into sergerad-nt…
sergerad Jan 20, 2026
bbc38bd
Merge branch 'next' of github.com:0xMiden/miden-node into sergerad-nt…
sergerad Jan 21, 2026
801cda0
Add ntx builder client fns for store witness endpoints and integrate
sergerad Jan 21, 2026
d4b3a77
Replace try_from
sergerad Jan 21, 2026
0ce950b
Vault root only
sergerad Jan 21, 2026
705ba3a
Return account inputs
sergerad Jan 21, 2026
a9c4d3a
Fix error message for witness not found
sergerad Jan 21, 2026
5c034ec
RM instrument in ntx
sergerad Jan 21, 2026
360d875
Register foreign account storage slots
sergerad Jan 21, 2026
8b68b2e
Merge branch 'next' of github.com:0xMiden/miden-node into sergerad-nt…
sergerad Jan 21, 2026
5873fb8
Treat native/foreign the same
sergerad Jan 21, 2026
89b9725
Pass ref block
sergerad Jan 21, 2026
2e066ed
Explainer comment for slot name cache
sergerad Jan 21, 2026
828ac4d
RM account request
sergerad Jan 21, 2026
2763610
Doc comment
sergerad Jan 21, 2026
f068918
Use account header vault root
sergerad Jan 21, 2026
7bd5258
add register_storage_map_slots fn
sergerad Jan 21, 2026
a8781ea
Misc cleanup
sergerad Jan 21, 2026
a6d5b97
Lock out of loop
sergerad Jan 22, 2026
0cbf820
Rename reference header
sergerad Jan 22, 2026
f7eacbe
Fix details request
sergerad Jan 22, 2026
9448396
Expand get_account_inputs comment
sergerad Jan 22, 2026
75a17f0
Reprhase build_minimal_foreign_account fn
sergerad Jan 22, 2026
c9c658b
RM unused variants
sergerad Jan 22, 2026
e133005
chore: update comment
bobbinth Jan 22, 2026
fd3c646
chore: fix lints
bobbinth Jan 22, 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
- Improve DB query performance for account queries ([#1496](https://github.com/0xMiden/miden-node/pull/1496).
- Limit number of storage map keys in `GetAccount` requests ([#1517](https://github.com/0xMiden/miden-node/pull/1517)).
- The network monitor now marks the chain as unhealthy if it fails to create new blocks ([#1512](https://github.com/0xMiden/miden-node/pull/1512)).
- Add support for foreign accounts to `NtxDataStore` and add `GetAccount` endpoint to NTX Builder gRPC store client ([#1521](https://github.com/0xMiden/miden-node/pull/1521)).
- Block producer now detects if it is desync'd from the store's chain tip and aborts ([#1520](https://github.com/0xMiden/miden-node/pull/1520)).
- Pin tool versions in CI ([#1523](https://github.com/0xMiden/miden-node/pull/1523)).
- Add `GetVaultAssetWitnesses` and `GetStorageMapWitness` RPC endpoints to store ([#1529](https://github.com/0xMiden/miden-node/pull/1529)).
Expand Down
155 changes: 104 additions & 51 deletions crates/ntx-builder/src/actor/execute.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::collections::BTreeSet;
use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;

use miden_node_proto::clients::ValidatorClient;
use miden_node_proto::generated::{self as proto};
Expand All @@ -8,9 +9,11 @@ use miden_protocol::Word;
use miden_protocol::account::{
Account,
AccountId,
AccountStorageHeader,
PartialAccount,
StorageMapWitness,
StorageSlotContent,
StorageSlotName,
StorageSlotType,
};
use miden_protocol::asset::{AssetVaultKey, AssetWitness};
use miden_protocol::block::{BlockHeader, BlockNumber};
Expand Down Expand Up @@ -45,6 +48,7 @@ use miden_tx::{
TransactionMastStore,
TransactionProverError,
};
use tokio::sync::Mutex;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Because we know this lock is low-contention and also not held across awaits, I think an std::sync::Mutex would be lower overhead and overall faster

use tokio::task::JoinError;
use tracing::{Instrument, instrument};

Expand Down Expand Up @@ -222,7 +226,7 @@ impl NtxContext {

match Box::pin(checker.check_notes_consumability(
data_store.account.id(),
data_store.reference_header.block_num(),
data_store.reference_block.block_num(),
notes,
TransactionArgs::default(),
))
Expand Down Expand Up @@ -256,7 +260,7 @@ impl NtxContext {

Box::pin(executor.execute_transaction(
data_store.account.id(),
data_store.reference_header.block_num(),
data_store.reference_block.block_num(),
notes,
TransactionArgs::default(),
))
Expand Down Expand Up @@ -322,20 +326,42 @@ impl NtxContext {
/// This is sufficient for executing a network transaction.
struct NtxDataStore {
account: Account,
reference_header: BlockHeader,
reference_block: BlockHeader,
chain_mmr: PartialBlockchain,
mast_store: TransactionMastStore,
/// Store client for retrieving note scripts.
store: StoreClient,
/// LRU cache for storing retrieved note scripts to avoid repeated store calls.
script_cache: LruCache<Word, NoteScript>,
/// Mapping of storage map roots to storage slot names observed during various calls.
///
/// The registered slot names are subsequently used to retrieve storage map witnesses from the
/// store. We need this because the store interface (and the underling SMT forest) use storage
/// slot names, but the `DataStore` interface works with tree roots. To get around this problem
/// we populate this map when:
/// - The the native account is loaded (in `get_transaction_inputs()`).
/// - When a foreign account is loaded (in `get_foreign_account_inputs`).
///
/// The assumption here are:
/// - Once an account is loaded, the mapping between `(account_id, map_root)` and slot names do
/// not change. This is always the case.
/// - New storage slots created during transaction execution will not be accesses in the same
/// transaction. The mechanism for adding new storage slots is not implemented yet, but the
/// plan for it is consistent with this assumption.
///
/// One nuance worth mentioning: it is possible that there could be a root collision where an
/// account has two storage maps with the same root. In this case, the map will contain only a
/// single entry with the storage slot name that was added last. Thus, technically, requests
/// to the store could be "wrong", but given that two identical maps have identical witnesses
/// this does not cause issues in practice.
storage_slots: Arc<Mutex<BTreeMap<(AccountId, Word), StorageSlotName>>>,
Comment on lines 336 to 357
Copy link
Contributor

Choose a reason for hiding this comment

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

Expanded this comment. cc @Mirko-von-Leipzig.

}

impl NtxDataStore {
/// Creates a new `NtxDataStore` with default cache size.
fn new(
account: Account,
reference_header: BlockHeader,
reference_block: BlockHeader,
chain_mmr: PartialBlockchain,
store: StoreClient,
script_cache: LruCache<Word, NoteScript>,
Expand All @@ -345,11 +371,28 @@ impl NtxDataStore {

Self {
account,
reference_header,
reference_block,
chain_mmr,
mast_store,
store,
script_cache,
storage_slots: Arc::new(Mutex::new(BTreeMap::default())),
}
}

/// Registers storage map slot names for the given account ID and storage header.
///
/// These slot names are subsequently used to query for storage map witnesses against the store.
async fn register_storage_map_slots(
&self,
account_id: AccountId,
storage_header: &AccountStorageHeader,
) {
let mut storage_slots = self.storage_slots.lock().await;
for slot_header in storage_header.slots() {
if let StorageSlotType::Map = slot_header.slot_type() {
storage_slots.insert((account_id, slot_header.value()), slot_header.name().clone());
}
}
}
}
Expand All @@ -366,53 +409,63 @@ impl DataStore for NtxDataStore {
return Err(DataStoreError::AccountNotFound(account_id));
}

// The latest supplied reference block must match the current reference block.
match ref_blocks.last().copied() {
Some(reference) if reference == self.reference_header.block_num() => {},

Some(reference) if reference == self.reference_block.block_num() => {},
Some(other) => return Err(DataStoreError::BlockNotFound(other)),
None => return Err(DataStoreError::other("no reference block requested")),
}

let partial_account = PartialAccount::from(&self.account);
// Register slot names from the native account for later use.
self.register_storage_map_slots(account_id, &self.account.storage().to_header())
.await;

Ok((partial_account, self.reference_header.clone(), self.chain_mmr.clone()))
let partial_account = PartialAccount::from(&self.account);
Ok((partial_account, self.reference_block.clone(), self.chain_mmr.clone()))
}
}

fn get_foreign_account_inputs(
&self,
foreign_account_id: AccountId,
_ref_block: BlockNumber,
ref_block: BlockNumber,
) -> impl FutureMaybeSend<Result<AccountInputs, DataStoreError>> {
async move { Err(DataStoreError::AccountNotFound(foreign_account_id)) }
async move {
debug_assert_eq!(ref_block, self.reference_block.block_num());

// Get foreign account inputs from store.
let account_inputs =
self.store.get_account_inputs(foreign_account_id, ref_block).await.map_err(
|err| DataStoreError::other_with_source("failed to get account inputs", err),
)?;

// Register slot names from the foreign account for later use.
self.register_storage_map_slots(foreign_account_id, account_inputs.storage().header())
.await;

Ok(account_inputs)
Comment on lines +433 to +446
Copy link
Contributor

Choose a reason for hiding this comment

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

Following up on the conversation we had during the call, I actually don't think we need to add foreign account code to the MAST store in this function (i.e., nothing needs to change). The reason for this is that transaction host does this after calling DataStore::get_foreign_account_inputs().

Technically, I think we didn't have to do this in the validator either - but in the validator, we are planning to change how re-execution works and so, I'd leave it as is.

cc @igamigo to confirm.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Looking at the code, I think this is right. But also, not sure why the validator was erroring during client integration FPI tests then (which prompted #1561).

Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh actually it's because it never even calls DataStore::get_foreign_account_inputs().

Although then I wonder how you plan on doing away with adding the MAST forest in the validator.

}
}

fn get_vault_asset_witnesses(
&self,
account_id: AccountId,
vault_root: Word,
_vault_root: Word,
vault_keys: BTreeSet<AssetVaultKey>,
) -> impl FutureMaybeSend<Result<Vec<AssetWitness>, DataStoreError>> {
async move {
if self.account.id() != account_id {
return Err(DataStoreError::AccountNotFound(account_id));
}
let ref_block = self.reference_block.block_num();

if self.account.vault().root() != vault_root {
return Err(DataStoreError::Other {
error_msg: "vault root mismatch".into(),
source: None,
});
}
// Get vault asset witnesses from the store.
let witnesses = self
.store
.get_vault_asset_witnesses(account_id, vault_keys, Some(ref_block))
.await
.map_err(|err| {
DataStoreError::other_with_source("failed to get vault asset witnesses", err)
})?;

Result::<Vec<_>, _>::from_iter(vault_keys.into_iter().map(|vault_key| {
AssetWitness::new(self.account.vault().open(vault_key).into()).map_err(|err| {
DataStoreError::Other {
error_msg: "failed to open vault asset tree".into(),
source: Some(Box::new(err)),
}
})
}))
Ok(witnesses)
}
}

Expand All @@ -423,27 +476,27 @@ impl DataStore for NtxDataStore {
map_key: Word,
) -> impl FutureMaybeSend<Result<StorageMapWitness, DataStoreError>> {
async move {
if self.account.id() != account_id {
return Err(DataStoreError::AccountNotFound(account_id));
}

let mut map_witness = None;
for slot in self.account.storage().slots() {
if let StorageSlotContent::Map(map) = slot.content() {
if map.root() == map_root {
map_witness = Some(map.open(&map_key));
}
}
}
// The slot name that corresponds to the given account ID and map root must have been
// registered during previous calls of this data store.
let storage_slots = self.storage_slots.lock().await;
let Some(slot_name) = storage_slots.get(&(account_id, map_root)) else {
return Err(DataStoreError::other(
"requested storage slot has not been registered",
));
};
Comment on lines +479 to +486
Copy link
Collaborator

Choose a reason for hiding this comment

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

Are we essentially relying on knowing the order of operations of the caller of data store?

Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: Seems like you can drop the lock by this point


let ref_block = self.reference_block.block_num();

// Get storage map witness from the store.
let witness = self
.store
.get_storage_map_witness(account_id, slot_name.clone(), map_key, Some(ref_block))
.await
.map_err(|err| {
DataStoreError::other_with_source("failed to get storage map witness", err)
})?;

if let Some(map_witness) = map_witness {
Ok(map_witness)
} else {
Err(DataStoreError::Other {
error_msg: "account storage does not contain the expected root".into(),
source: None,
})
}
Ok(witness)
}
}

Expand Down
Loading