Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
* [rust] Added `PartialBlockchainUpdates::block_headers_to_store`, which narrows the staged headers to the ones a sync must persist: those marked as relevant, genesis, and the block at the sync height. `block_headers` still yields all staged headers ([#2297](https://github.com/0xMiden/rust-sdk/pull/2297)).
* [rust] State sync now authenticates every relevant note block but only persists block headers and MMR authentication nodes for blocks containing notes that remain unspent or that a `NoteObserver` explicitly marks as relevant ([#2297](https://github.com/0xMiden/rust-sdk/pull/2297)).
* [rust,store] `BatchBuilder` now stacks in-batch account state as a `PartialAccount` updated with each transaction's `AccountPatch` instead of reconstructing the full `Account` after every push. Witnesses for keys no prior in-batch transaction touched are served by the new `Store::vault_asset_witnesses_after_patch` and `Store::storage_map_witness_after_patch` methods, which stage the accumulated patch onto the store's committed Merkle forest without persisting it (default implementations return `UnsupportedOperation`; `SqliteStore` implements them) ([#2277](https://github.com/0xMiden/rust-sdk/pull/2277)).
* [rust,store] Single-transaction execution (`Client::execute_transaction`, `Client::validate_request`) no longer reconstructs the full `Account`: request validation now checks balances against the vault asset list fetched via the new `Store::get_account_assets`, and everything else works from the minimal partial account. Executor vault witnesses (including emptiness proofs for assets being added) are served by the new `Store::get_vault_asset_witnesses`, which `SqliteStore` answers directly from its in-memory Merkle forest instead of rebuilding the vault. Both new `Store` methods have vault-reconstruction default implementations, so existing store backends keep working unchanged ([#2277](https://github.com/0xMiden/rust-sdk/pull/2277)).
* [cli] `account --list`, `account show` and `call` no longer load full accounts from the store: faucet token symbols and decimals are read from the faucet's token config storage slot, and the `call` existence check uses the account header ([#TBD](https://github.com/0xMiden/rust-sdk/pull/TBD)).

## 0.16.0-alpha.1 (2026-07-17)

Expand Down
43 changes: 27 additions & 16 deletions bin/miden-cli/src/commands/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use miden_client::account::{
StorageSlotContent,
};
use miden_client::address::{Address, AddressInterface, NetworkId, RoutingParameters};
use miden_client::asset::Asset;
use miden_client::asset::{Asset, TokenSymbol};
use miden_client::rpc::{GrpcClient, NodeRpcClient, VerifyingRpcClient};
use miden_client::transaction::{AccountComponentInterface, AccountInterface};
use miden_client::utils::base_units_to_tokens;
Expand Down Expand Up @@ -112,10 +112,10 @@ async fn list_accounts<AUTH>(client: Client<AUTH>) -> Result<(), CliError> {
for (acc, _acc_seed) in &accounts {
let reader = client.account_reader(acc.id());
let status = reader.status().await?.to_string();
let token_symbol = get_faucet_component(&client, acc.id())
let token_symbol = get_faucet_token_info(&client, acc.id())
.await
.ok()
.map(|faucet| faucet.symbol().to_string());
.map(|(symbol, _)| symbol.to_string());

table.add_row(vec![
acc.id().to_hex(),
Expand Down Expand Up @@ -175,11 +175,10 @@ async fn show_account<AUTH>(
Asset::Fungible(fungible_asset) => {
let faucet_id = fungible_asset.faucet_id();
let asset_amount = fungible_asset.amount();
let (faucet, amount) = match get_faucet_component(client, faucet_id).await {
Ok(faucet_component) => (
faucet_component.symbol().to_string(),
base_units_to_tokens(asset_amount, faucet_component.decimals()),
),
let (faucet, amount) = match get_faucet_token_info(client, faucet_id).await {
Ok((symbol, decimals)) => {
(symbol.to_string(), base_units_to_tokens(asset_amount, decimals))
},
Err(_) => (faucet_id.prefix().to_hex(), asset_amount.as_u64().to_string()),
};
("Fungible Asset", faucet, amount)
Expand Down Expand Up @@ -275,20 +274,32 @@ fn print_summary_table(account: &Account, network_id: NetworkId, token_symbol: O
println!("{table}\n");
}

/// Loads the tracked account for `account_id` and reconstructs its [`FungibleFaucet`] component.
/// Reads the faucet's token symbol and decimals from its token config storage slot, without
/// loading the full account.
///
/// # Errors
/// Returns an error if the account is not tracked by the client or its faucet metadata can't be
/// read.
async fn get_faucet_component<AUTH>(
/// Returns an error if the account is not tracked by the client, has no token config slot (i.e.
/// is not a fungible faucet), or the token config can't be decoded.
async fn get_faucet_token_info<AUTH>(
client: &Client<AUTH>,
account_id: AccountId,
) -> Result<FungibleFaucet, CliError> {
let account = client.get_account(account_id).await?.ok_or_else(|| {
CliError::Input(format!("account {account_id} not tracked by the client"))
) -> Result<(TokenSymbol, u8), CliError> {
let token_config = client
.account_reader(account_id)
.get_storage_item(FungibleFaucet::token_config_slot().clone())
.await?;

// Token config word layout: `[token_supply, max_supply, decimals, symbol]` (see
// `FungibleFaucet::token_config_slot_value`).
let [_token_supply, _max_supply, decimals, symbol] = *token_config;
let symbol = TokenSymbol::try_from(symbol).map_err(|err| {
CliError::Input(format!("failed to decode token symbol of faucet {account_id}: {err}"))
})?;
let decimals = u8::try_from(decimals.as_canonical_u64()).map_err(|err| {
CliError::Input(format!("failed to decode token decimals of faucet {account_id}: {err}"))
})?;

faucet_component_from_account(&account)
Ok((symbol, decimals))
}

/// Reconstructs the [`FungibleFaucet`] component from a materialized [`Account`].
Expand Down
3 changes: 2 additions & 1 deletion bin/miden-cli/src/commands/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ impl CallCmd {
})?;

let account_id = parse_account_id(&client, account_str).await?;
client.try_get_account(account_id).await?;
// Ensure the account is tracked before executing against it; only the header is needed.
client.account_reader(account_id).header().await?;

let package = load_package(&self.package)?;

Expand Down
30 changes: 8 additions & 22 deletions crates/rust-client/src/store/data_store/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ use miden_protocol::account::{
};
use miden_protocol::asset::{AssetId, AssetWitness};
use miden_protocol::block::{BlockHeader, BlockNumber};
use miden_protocol::crypto::merkle::MerklePath;
use miden_protocol::crypto::merkle::mmr::{InOrderIndex, MmrPeaks, PartialMmr};
use miden_protocol::crypto::merkle::{MerkleError, MerklePath};
use miden_protocol::note::{NoteScript, NoteScriptRoot};
use miden_protocol::transaction::{AccountInputs, PartialBlockchain};
use miden_protocol::vm::FutureMaybeSend;
Expand Down Expand Up @@ -331,27 +331,13 @@ impl DataStore for ClientDataStore {
return Ok(witnesses);
}

let mut asset_witnesses = vec![];
for asset_id in asset_ids.iter().copied() {
match self.store.get_account_asset(account_id, asset_id).await {
Ok(Some((_, asset_witness))) => asset_witnesses.push(asset_witness),
Ok(None) | Err(StoreError::MerkleStoreError(MerkleError::RootNotInStore(_))) => {
let vault = self.store.get_account_vault(account_id).await?;

if vault.root() != vault_root {
return Err(DataStoreError::other("Vault root mismatch"));
}

asset_witnesses.push(vault.open(asset_id));
},
Err(err) => {
return Err(DataStoreError::other_with_source(
"Failed to get account asset",
err,
));
},
}
}
let asset_witnesses = self
.store
.get_vault_asset_witnesses(account_id, vault_root, asset_ids.clone())
.await
.map_err(|err| {
DataStoreError::other_with_source("failed to get vault asset witnesses", err)
})?;

self.cache
.insert_vault_asset_witnesses(vault_root, &asset_ids, &asset_witnesses);
Expand Down
36 changes: 36 additions & 0 deletions crates/rust-client/src/store/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ use miden_protocol::account::{
use miden_protocol::address::Address;
use miden_protocol::asset::{Asset, AssetId, AssetVault, AssetWitness};
use miden_protocol::block::{BlockHeader, BlockNumber};
use miden_protocol::crypto::merkle::MerkleError;
use miden_protocol::crypto::merkle::mmr::{Forest, InOrderIndex, MmrPeaks, PartialMmr};
use miden_protocol::errors::AccountError;
use miden_protocol::note::{NoteDetailsCommitment, NoteId, NoteScript, NoteTag, Nullifier};
Expand Down Expand Up @@ -627,6 +628,41 @@ pub trait Store: Send + Sync {
/// Retrieves the asset vault for a specific account.
async fn get_account_vault(&self, account_id: AccountId) -> Result<AssetVault, StoreError>;

/// Retrieves all assets in the account's vault as a plain list, without building the vault's
/// Merkle tree.
///
/// Prefer this over [`Store::get_account_vault`] when only asset values are needed (e.g.
/// balance checks): it avoids hashing every asset into an SMT.
///
/// The default implementation of this method uses [`Store::get_account_vault`].
async fn get_account_assets(&self, account_id: AccountId) -> Result<Vec<Asset>, StoreError> {
Ok(self.get_account_vault(account_id).await?.assets().collect())
}

/// Returns vault asset witnesses for `asset_ids` against the account's vault with root
/// `vault_root`. An asset absent from the vault yields an emptiness proof rather than an
/// error, which the executor needs when an asset is being added to the vault.
///
/// The default implementation reconstructs the vault via [`Store::get_account_vault`] and
/// opens each witness from it; backends that keep an in-memory Merkle forest (e.g.
/// `SqliteStore`) override it to open the witnesses directly, without materializing the
/// vault.
async fn get_vault_asset_witnesses(
&self,
account_id: AccountId,
vault_root: Word,
asset_ids: BTreeSet<AssetId>,
) -> Result<Vec<AssetWitness>, StoreError> {
let vault = self.get_account_vault(account_id).await?;
if vault.root() != vault_root {
return Err(StoreError::MerkleStoreError(MerkleError::ConflictingRoots {
expected_root: vault_root,
actual_root: vault.root(),
}));
}
Ok(asset_ids.into_iter().map(|asset_id| vault.open(asset_id)).collect())
}

/// Retrieves a specific asset (by vault id) from the account's vault along with its Merkle
/// witness.
///
Expand Down
18 changes: 18 additions & 0 deletions crates/rust-client/src/store/smt_forest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,24 @@ impl AccountSmtForest {
Ok((asset, witness))
}

/// Opens vault asset witnesses for `asset_ids` against `vault_root`.
///
/// Unlike [`Self::get_asset_and_witness`], an absent key is not an error: its witness is an
/// emptiness proof, which the executor needs when an asset is being added to the vault.
pub fn open_vault_asset_witnesses(
&self,
vault_root: Word,
asset_ids: impl IntoIterator<Item = AssetId>,
) -> Result<Vec<AssetWitness>, StoreError> {
asset_ids
.into_iter()
.map(|asset_id| {
let proof = self.forest.open(vault_root, asset_id.hash().into())?;
Ok(AssetWitness::new(proof, [asset_id])?)
})
.collect()
}

/// Retrieves the storage map witness for a specific map item.
pub fn get_storage_map_item_witness(
&self,
Expand Down
Loading
Loading