Skip to content
Merged
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
11 changes: 7 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@

### Breaking Changes

* [BREAKING][behavior][rpc] The `GetAccount` response no longer carries one SMT opening per requested storage map key. A slot queried with specific keys now comes back as a single partial SMT covering all of them, alongside the original unhashed keys, so the client requires a node that speaks this format ([#2360](https://github.com/0xMiden/rust-sdk/issues/2360)).
* [BREAKING][type][rust] `StorageMapEntries::EntriesWithProofs(Vec<SmtProof>)` is replaced by `StorageMapEntries::PartialMap { map_keys, partial_smt }`, which carries the values only inside the tree: read one by hashing its raw key and calling `PartialSmt::get_value`. The enum also gained a `LimitExceeded` variant and `AccountStorageMapDetails::too_many_entries` was removed in its favor ([#2360](https://github.com/0xMiden/rust-sdk/issues/2360)).
* [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][rpc] The `GetAccount` response no longer carries one SMT opening per requested storage map key. A slot queried with specific keys now comes back as a single partial SMT covering all of them, alongside the original unhashed keys, so the client requires a node that speaks this format ([#2362](https://github.com/0xMiden/rust-sdk/pull/2362)).
* [BREAKING][type][rust] `StorageMapEntries::EntriesWithProofs(Vec<SmtProof>)` is replaced by `StorageMapEntries::PartialMap { map_keys, partial_smt }`, which carries the values only inside the tree: read one by hashing its raw key and calling `PartialSmt::get_value`. The enum also gained a `LimitExceeded` variant and `AccountStorageMapDetails::too_many_entries` was removed in its favor ([#2362](https://github.com/0xMiden/rust-sdk/pull/2362)).
* [BREAKING][type][rust] `SyncedNote` splits the content it carries into two fields, `details: Option<NoteDetails>` and `attachments: NoteAttachments`, replacing the previous `content: Option<ResolvedNoteContent>`; `SyncedNote::new` takes them as separate arguments. `ResolvedNoteContent` is removed. Attachments are no longer optional, a note whose metadata advertises none carries an empty set, so "no attachments" and "attachments not resolved" are no longer the same value ([#2431](https://github.com/0xMiden/rust-sdk/pull/2431)).
* [BREAKING][param][rust] `NoteObserver::observe` takes `&NoteAttachments` instead of `Option<&NoteAttachments>`. A note that carries no attachments is reported with an empty set ([#2431](https://github.com/0xMiden/rust-sdk/pull/2431)).
* [BREAKING][removal][rust] `impl TryFrom<proto::rpc::AccountResponse> for AccountProof` is removed. A partial map can only be checked against the keys that were requested, which that conversion has no way to receive, so it rejected every response answering a specific-key query. Use `proto::rpc::account_response::AccountDetails::into_domain` with the `AccountStorageRequirements` the request was built from ([#2360](https://github.com/0xMiden/rust-sdk/issues/2360)).
* [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][removal][rust] `impl TryFrom<proto::rpc::AccountResponse> for AccountProof` is removed. A partial map can only be checked against the keys that were requested, which that conversion has no way to receive, so it rejected every response answering a specific-key query. Use `proto::rpc::account_response::AccountDetails::into_domain` with the `AccountStorageRequirements` the request was built from ([#2431](https://github.com/0xMiden/rust-sdk/issues/2431)).
* [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 — served from the local store first, 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)).
* [BREAKING][removal][rust] `AccountSmtForest` is now generic over the forest storage `BackendReader`, with updates additionally requiring `Backend`, and is constructed per store operation. The in-memory root-staging API (`stage_roots`, `commit_roots`, `discard_roots`, `replace_roots`, `get_roots`) and the node-insertion helpers were removed. Trees are addressed by account ID and storage slot name rather than by root: `miden_client::store` now exports only `AccountSmtForest` and `AccountUpdate`, with lineage identifiers, update batches and their miden-crypto types kept internal ([#2333](https://github.com/0xMiden/rust-sdk/pull/2333)).
Expand Down Expand Up @@ -59,6 +60,8 @@
* [FEATURE][store] Added the `miden-bench store` subcommand, which measures how the `SQLite` store methods scale with the number of notes and accounts and reports the growth between the smallest and the largest size. It seeds its own throwaway databases and needs no node, and `make bench-store` runs the same sweep CI does ([#2364](https://github.com/0xMiden/rust-sdk/pull/2364)).
* [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 while reading untracked public accounts from the network ([#2362](https://github.com/0xMiden/rust-sdk/pull/2362)).
* [store] Added schema version 2 (`0002_index_tuning.sql`), which indexes `code_commitment` on `latest_account_headers`, `historical_account_headers` and `foreign_account_code`, leads the `input_notes` consumption index with `consumer_account_id`, carries `nullifier` in the `input_notes` state index, narrows the `transactions` status index to pending rows, and drops the `transactions.block_num` column, whose value is already part of the serialized details. Stores built at version 1 are migrated when they are opened ([#2364](https://github.com/0xMiden/rust-sdk/pull/2364)).
* [store] The SQLite store's schema is now built from append-only migrations under `crates/sqlite-store/src/migrations/`, starting with the frozen `0001_init.sql`. Opening a store verifies its schema against a fingerprint derived by replaying the migrations, before migrating and then again for each version an upgrade builds, while it is still uncommitted. A version that builds an unexpected schema is rejected there, so the upgrade is rolled back and the store is left as it was. A pinned snapshot of the fingerprints and a CI job together reject any pull request that modifies an existing migration file ([#2346](https://github.com/0xMiden/rust-sdk/issues/2346)).
* [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)).
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 @@ -166,10 +166,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 @@ -211,11 +211,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 @@ -576,20 +575,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
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 @@ -574,30 +574,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 @@ -84,6 +90,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
Loading
Loading