Skip to content
Open
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Breaking Changes

* [BREAKING][removal][rust] Removed `Client::try_get_account`. Use `Client::get_account` and handle the `None` case, or `Client::account_reader` for existence checks and single-field reads that don't need the full materialized account ([#2362](https://github.com/0xMiden/rust-sdk/pull/2362)).
* [BREAKING][behavior][rust] Foreign `AccountInputs` keep a fetched asset list only when it hashes to the account header's vault root; otherwise (omitted because unchanged, capped as oversize, or malformed) they carry a root-only partial vault, and any assets the foreign code reads are resolved during execution as per-asset witnesses — everything local first (the store's per-asset reads, then the full local vault), falling back to fetching the vault via RPC at the transaction reference block and verifying it against the required root ([#2417](https://github.com/0xMiden/rust-sdk/pull/2417)).
* [BREAKING][behavior][rust] Foreign `AccountInputs` likewise keep a fetched storage-map entry list only when it hashes to the slot's root in the storage header; otherwise (capped as oversize, or malformed) the map is carried root-only and any keys the foreign code reads are resolved during execution as lazy per-key witnesses, instead of syncing an oversized map's full history from genesis before executing ([#2417](https://github.com/0xMiden/rust-sdk/pull/2417)).
* [BREAKING][arch][store] The account SMT forest now persists in SQLite (new `forest_trees`, `forest_entries`, `forest_subtrees` and `forest_revision` tables) through a `LargeSmtForest` backend scoped to the store's own transaction, so forest mutations commit or roll back atomically with the account tables and opening the store no longer rebuilds the forest from account data. Tree inner nodes are persisted as packed subtree blobs, so witness reads load a single leaf plus eight blobs instead of rebuilding the account's tree, making their cost independent of the account's map size at the price of a larger store file. Tree updates are computed path-locally from the persisted leaves and subtree blobs, so committed update cost scales with the size of the change set rather than with the map size. Existing stores are not compatible and must be recreated ([#2333](https://github.com/0xMiden/rust-sdk/pull/2333)).
Expand Down Expand Up @@ -41,6 +42,8 @@
* [FEATURE][rust,store] Added `NoteFilter::ScriptRoots` to query input notes by their note script root directly at the store level, without loading and screening unrelated notes. The filter doesn't apply to output notes: querying output notes with it returns an empty list ([#2335](https://github.com/0xMiden/rust-sdk/pull/2335)).
* [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] Single-transaction execution no longer reconstructs the full `Account`: the client works from the minimal partial account, and request validation checks balances against the vault asset list fetched via the new `Store::get_account_assets` (exposed as `AccountReader::assets`). 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 Merkle forest instead of rebuilding the vault ([#2362](https://github.com/0xMiden/rust-sdk/pull/2362)).
* [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 `call` checks the account header, importing untracked accounts from the node on demand ([#2362](https://github.com/0xMiden/rust-sdk/pull/2362)).
* [rust] `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 at the in-batch state are built by replaying the batch's writes onto committed-state proofs, so any `Store` backend supports batches through the witness methods it already implements ([#2277](https://github.com/0xMiden/rust-sdk/pull/2277)).
* [FEATURE][cli] Added `account --inspect <ID>[:<PROCEDURE>]` to list the procedures an account exposes, grouped into resolved procedures (with their names and signatures) and unresolved ones (listed by MAST root). Names and signatures are resolved from the `.masp` packages in the configured packages directory plus any passed via `--package` (`-p`). `--verbose` prints each procedure's MASM disassembly. ([#2312](https://github.com/0xMiden/rust-sdk/issues/2312)).
* Improved the output of the `miden-client init` command when a configuration already exists ([#2357](https://github.com/0xMiden/rust-sdk/pull/2357)).
Expand Down
5 changes: 4 additions & 1 deletion bin/integration-tests/src/tests/fpi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -854,7 +854,10 @@ pub(crate) async fn deploy_foreign_account(

// NOTE: We get the new account state here since the first transaction updates the nonce from
// to 1
let foreign_account: Account = client.try_get_account(foreign_account_id).await?;
let foreign_account: Account = client
.get_account(foreign_account_id)
.await?
.with_context(|| format!("account {foreign_account_id} should be tracked"))?;

Ok((foreign_account, proc_root))
}
42 changes: 26 additions & 16 deletions bin/miden-cli/src/commands/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,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::domain::account::GetAccountRequest;
use miden_client::rpc::{GrpcClient, NodeRpcClient, VerifyingRpcClient};
use miden_client::transaction::{AccountComponentInterface, AccountInterface};
Expand Down Expand Up @@ -159,10 +159,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 @@ -204,11 +204,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 @@ -569,20 +568,31 @@ 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.
///
/// # 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
11 changes: 10 additions & 1 deletion bin/miden-cli/src/commands/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,16 @@ impl CallCmd {
})?;

let account_id = parse_account_id(&client, account_str).await?;
client.try_get_account(account_id).await?;
// Untracked accounts are imported from the node before executing against them; private
// accounts have no public state to import and fail here.
if client.account_reader(account_id).header().await.is_err() {
client.import_account_by_id(account_id).await.map_err(|err| {
CliError::InvalidArgument(format!(
"Account {account_id} is not tracked and could not be imported from the \
node: {err}"
))
})?;
}

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

Expand Down
12 changes: 12 additions & 0 deletions crates/rust-client/src/account/account_reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,18 @@ impl AccountReader {
// VAULT ACCESS
// --------------------------------------------------------------------------------------------

/// Retrieves all assets in the account's vault as a plain list, without building the vault's
/// Merkle tree.
///
/// To load the entire vault, use
/// [`Client::get_account_vault`](crate::Client::get_account_vault).
pub async fn assets(&self) -> Result<Vec<Asset>, ClientError> {
self.store
.get_account_assets(self.account_id)
.await
.map_err(ClientError::StoreError)
}

/// Retrieves the balance of a fungible asset in the account's vault.
///
/// Returns [`AssetAmount::ZERO`] if the asset is not present in the vault or if the asset is
Expand Down
19 changes: 3 additions & 16 deletions crates/rust-client/src/account/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -559,30 +559,17 @@ impl<AUTH> Client<AUTH> {

/// Retrieves the full [`Account`] object from the store, returning `None` if not found.
///
/// This method loads the complete account state including vault, storage, and code.
///
/// For lazy access that fetches only the data you need, use
/// This method loads the complete account state including vault, storage, and code
/// including building the vault's Merkle tree. For lazy access that fetches only the data
/// you need (existence checks, single fields, storage items), use
/// [`Client::account_reader`] instead.
///
/// Use [`Client::try_get_account`] if you want to error when the account is not found.
pub async fn get_account(&self, account_id: AccountId) -> Result<Option<Account>, ClientError> {
match self.store.get_account(account_id).await? {
Some(record) => Ok(Some(record.try_into()?)),
None => Ok(None),
}
}

/// Retrieves the full [`Account`] object from the store, erroring if not found.
///
/// This method loads the complete account state including vault, storage, and code.
///
/// Use [`Client::get_account`] if you want to handle missing accounts gracefully.
pub async fn try_get_account(&self, account_id: AccountId) -> Result<Account, ClientError> {
self.get_account(account_id)
.await?
.ok_or(ClientError::AccountDataNotFound(account_id))
}

/// Creates an [`AccountReader`] for lazy access to account data.
///
/// The `AccountReader` provides lazy access to account state - each method call
Expand Down
10 changes: 9 additions & 1 deletion crates/rust-client/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ use core::fmt;
use miden_protocol::Word;
use miden_protocol::account::AccountId;
use miden_protocol::crypto::merkle::MerkleError;
pub use miden_protocol::errors::{AccountError, AccountIdError, AssetError, NetworkIdError};
pub use miden_protocol::errors::{
AccountError,
AccountIdError,
AccountPatchError,
AssetError,
NetworkIdError,
};
use miden_protocol::errors::{
NoteError,
PartialBlockchainError,
Expand Down Expand Up @@ -79,6 +85,8 @@ pub enum ClientError {
AccountAlreadyTracked(AccountId),
#[error("account error")]
AccountError(#[from] AccountError),
#[error("account patch error")]
AccountPatchError(#[from] AccountPatchError),
#[error("account {0} is locked because the local state may be out of date with the network")]
AccountLocked(AccountId),
#[error(
Expand Down
28 changes: 7 additions & 21 deletions crates/rust-client/src/store/data_store/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -404,27 +404,13 @@ impl DataStore for ClientDataStore {
return Ok(witnesses);
}

let mut asset_witnesses = Vec::with_capacity(asset_ids.len());
for asset_id in asset_ids.iter().copied() {
match self.store.get_account_asset(account_id, asset_id).await {
Ok(Some((_, witness))) if witness.proof().compute_root() == vault_root => {
asset_witnesses.push(witness);
},
Ok(_) => {
asset_witnesses.clear();
break;
},
Err(err) => {
tracing::debug!(
%account_id,
%err,
"asset witness not available locally, will try the full vault"
);
asset_witnesses.clear();
break;
},
}
}
let mut 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)
})?;

// Fall back to the full local vault — an absent asset still needs a non-membership
// witness, which only the vault itself can produce — and lastly to an RPC vault fetch,
Expand Down
2 changes: 2 additions & 0 deletions crates/rust-client/src/store/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ pub enum StoreError {
VaultKeyNotTracked(AssetId, Word),
#[error("failed to parse word")]
WordError(#[from] WordError),
#[error("operation `{0}` is not supported by this store backend")]
UnsupportedOperation(&'static str),
}

impl From<StoreError> for DataStoreError {
Expand Down
39 changes: 39 additions & 0 deletions crates/rust-client/src/store/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,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::{
Expand Down Expand Up @@ -632,6 +633,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 Expand Up @@ -708,6 +744,9 @@ pub trait Store: Send + Sync {
}
}

// IN-BATCH (STAGED) WITNESSES
// --------------------------------------------------------------------------------------------

// PARTIAL ACCOUNTS
// --------------------------------------------------------------------------------------------

Expand Down
26 changes: 26 additions & 0 deletions crates/rust-client/src/store/smt_forest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,32 @@ impl<B: BackendReader> AccountSmtForest<B> {
Ok((asset, witness))
}

/// Retrieves vault asset witnesses for the given vault keys.
///
/// Unlike [`Self::get_asset_and_witness`], keys absent from the vault are served too: their
/// witness is an emptiness proof, which the executor needs when an asset is being added to
/// the vault.
///
/// The proofs are opened against the latest tree of the account's vault lineage, after
/// verifying that its root matches `expected_vault_root`.
pub fn open_vault_asset_witnesses(
&self,
account_id: AccountId,
expected_vault_root: Word,
asset_ids: impl IntoIterator<Item = AssetId>,
) -> Result<Vec<AssetWitness>, StoreError> {
let lineage = vault_lineage_id(account_id);
let tree = self.verified_latest_tree(lineage, expected_vault_root)?;

asset_ids
.into_iter()
.map(|asset_id| {
let proof = self.forest.open(tree, asset_id.hash().into()).map_err(forest_error)?;
Ok(AssetWitness::new(proof, [asset_id])?)
})
.collect()
}

/// Retrieves the storage map witness for a specific map item.
///
/// The proof is opened against the latest tree of the map's lineage, after verifying that
Expand Down
4 changes: 1 addition & 3 deletions crates/rust-client/src/transaction/batch/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -306,9 +306,7 @@ where
None => account_reader.partial_account().await?,
};

let prep = client
.prepare_transaction(account.code_interface(), transaction_request)
.await?;
let prep = client.prepare_transaction_for_batch(&account, transaction_request).await?;

data_store.register_note_scripts(prep.output_note_scripts());
for fpi_account in &prep.foreign_account_inputs {
Expand Down
Loading
Loading