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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
### Fixes

* [FIX][cli] `miden-client import` now rejects invocations without a file path instead of silently succeeding ([#2450](https://github.com/0xMiden/rust-sdk/pull/2450)).
* [FIX][rust] `StateSync::validate_account_proof` now returns a `ChainValidationError` when a `get_account` proof carries no account details, instead of panicking. The RPC layer allows the details to be absent, so a malformed or malicious node response could crash the client mid-sync ([#2502](https://github.com/0xMiden/rust-sdk/pull/2502)).
* [FIX][rust] `TransactionRequestBuilder::build_swap` and `build_pswap_create` now reject a zero-amount asset on either side of the exchange. A zero requested asset produced a payback P2ID note carrying nothing, and a zero offered asset produced a note whose consumer pays and receives nothing ([#2459](https://github.com/0xMiden/rust-sdk/pull/2459)).

## 0.16.0 (2026-09-07)
Expand Down
42 changes: 31 additions & 11 deletions crates/rust-client/src/sync/state_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -988,11 +988,6 @@ impl StateSync {
///
/// Must only be called when the local commitment for the account is known to differ from the
/// network's, so an equal nonce always means a genuine fork.
///
/// # Panics
///
/// Panics if the node response omits account details, since that would mean the account is not
/// public.
async fn sync_public_account(
&self,
account_id: AccountId,
Expand Down Expand Up @@ -1067,11 +1062,9 @@ impl StateSync {
/// - the proof is for a different block than the sync target.
/// - the witness is for a different account than the requested one.
/// - the witness does not open under the sync target header's account root.
///
/// # Panics
///
/// Panics if the proof carries no account details, since this is only called for public
/// accounts and the node always returns details for them.
/// - the proof carries no account details. This is only called for public accounts, for which
/// the node is expected to return details, but a malformed or malicious response can omit
/// them, so it is rejected rather than trusted as an invariant.
fn validate_account_proof(
Comment thread
erhnysr marked this conversation as resolved.
proof: AccountProof,
proof_block_num: BlockNumber,
Expand Down Expand Up @@ -1108,7 +1101,11 @@ impl StateSync {
))
})?;

Ok(details.expect("node returned no details for a public account"))
details.ok_or_else(|| {
ClientError::ChainValidationError(format!(
"get_account returned no details for public account {account_id}"
))
})
}

/// Builds a [`PublicAccountUpdate::Patch`] by fetching incremental storage map and vault
Expand Down Expand Up @@ -1825,6 +1822,29 @@ mod tests {
assert!(matches!(result, Err(ClientError::ChainValidationError(_))));
}

/// `validate_account_proof` rejects a proof that carries no account details rather than
/// panicking, since a malformed or malicious node response can omit them.
#[tokio::test]
async fn validate_account_proof_rejects_missing_details() {
let mut builder = MockChainBuilder::new();
let account = builder.add_existing_mock_account(miden_testing::Auth::IncrNonce).unwrap();
let rpc_api = MockRpcApi::new(builder.build().unwrap());
let chain_tip_header = rpc_api.mock_chain.read().latest_block_header();

// An otherwise honest proof, but with the account details stripped.
let (proof_block_num, proof) = get_account_proof(&rpc_api, account.id()).await;
let (witness, _details) = proof.into_parts();
let proof = AccountProof::new(witness, None).unwrap();
let result = StateSync::validate_account_proof(
proof,
proof_block_num,
account.id(),
&chain_tip_header,
);

assert!(matches!(result, Err(ClientError::ChainValidationError(_))));
}

/// `validate_account_proof` rejects a proof reported for a block other than the sync target.
#[tokio::test]
async fn validate_account_proof_rejects_wrong_block() {
Expand Down
Loading