From fb3cc74855f167b386a4ba862e493591eec66e0c Mon Sep 17 00:00:00 2001 From: ricomateo Date: Tue, 18 Aug 2026 14:17:53 -0300 Subject: [PATCH 01/14] fix(rust-client): require a CryptoRng in the client and default to ChaCha20Rng --- CHANGELOG.md | 1 + bin/integration-tests/src/tests/config.rs | 10 +-- bin/miden-bench/src/config.rs | 9 +-- bin/miden-cli/tests/cli.rs | 9 +-- crates/rust-client/Cargo.toml | 2 +- crates/rust-client/src/builder.rs | 70 +++++++++++++++---- crates/rust-client/src/lib.rs | 35 +++++++--- .../testing/miden-client-tests/src/tests.rs | 39 +---------- .../miden-client-tests/src/tests/batch.rs | 7 -- .../miden-client-tests/src/tests/transport.rs | 14 ---- 10 files changed, 86 insertions(+), 110 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94473f4945..89579cd4f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Breaking Changes +* [BREAKING][type][rust] `ClientBuilder::rng` now requires `CryptoRng + Send + Sync`, the marker trait `ClientFeltRng` is renamed to `ClientCryptoRng` with its bound changed from `FeltRng` to `CryptoRng`, and a client with no RNG configured now defaults to an OS-seeded `ChaCha20Rng` instead of a `RandomCoin`. `RandomCoin` is no longer accepted, since it is not a `CryptoRng`; pass a `rand` CSPRNG or drop the `rng()` call to take the default. `Client::rng()` still returns a `FeltRng`, so `draw_word` and `generate_serial_number` call sites are unaffected. For reproducible randomness, seed a `ChaCha20Rng` with `from_seed`; note that `CryptoRng` constrains the generator's algorithm and not the entropy of its seed ([#2204](https://github.com/0xMiden/rust-sdk/issues/2204)). * [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)). * [BREAKING][type][rust] `rpc::domain::transaction::TransactionRecord` gained a non-public field, so it can no longer be constructed with a struct literal outside the crate ([#2300](https://github.com/0xMiden/rust-sdk/pull/2300)). diff --git a/bin/integration-tests/src/tests/config.rs b/bin/integration-tests/src/tests/config.rs index eb553b4e7b..c73b3e2ed7 100644 --- a/bin/integration-tests/src/tests/config.rs +++ b/bin/integration-tests/src/tests/config.rs @@ -5,8 +5,8 @@ use std::str::FromStr; use std::sync::Arc; use anyhow::{Context, Result}; +use miden_client::RemoteTransactionProver; use miden_client::builder::ClientBuilder; -use miden_client::crypto::RandomCoin; use miden_client::grpc_support::{DEVNET_PROVER_ENDPOINT, TESTNET_PROVER_ENDPOINT}; use miden_client::note_transport::grpc::GrpcNoteTransportClient; use miden_client::note_transport::{ @@ -15,9 +15,7 @@ use miden_client::note_transport::{ }; use miden_client::rpc::{Endpoint, GrpcClient, VerifyingRpcClient}; use miden_client::testing::common::{FilesystemKeyStore, TestClient, create_test_store_path}; -use miden_client::{Felt, RemoteTransactionProver}; use miden_client_sqlite_store::ClientBuilderSqliteExt; -use rand::RngExt; use uuid::Uuid; const NETWORK_DEVNET: &str = "devnet"; @@ -134,11 +132,6 @@ impl ClientConfig { ) -> Result<(ClientBuilder, FilesystemKeyStore)> { let (rpc_endpoint, rpc_timeout, store_config, auth_path) = self.as_parts(); - let mut rng = rand::rng(); - let coin_seed: [u64; 4] = rng.random(); - - let rng = RandomCoin::new(coin_seed.map(Felt::new_unchecked).into()); - let keystore = FilesystemKeyStore::new(auth_path.clone()).with_context(|| { format!("failed to create keystore at path: {}", auth_path.to_string_lossy()) })?; @@ -148,7 +141,6 @@ impl ClientConfig { let mut builder = ClientBuilder::new() .rpc(rpc_client) - .rng(Box::new(rng)) .sqlite_store(store_config) .authenticator(Arc::new(keystore.clone())) .tx_discard_delta(None); diff --git a/bin/miden-bench/src/config.rs b/bin/miden-bench/src/config.rs index 06a165be00..e1630a449f 100644 --- a/bin/miden-bench/src/config.rs +++ b/bin/miden-bench/src/config.rs @@ -1,13 +1,11 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; +use miden_client::Client; use miden_client::builder::ClientBuilder; -use miden_client::crypto::RandomCoin; use miden_client::keystore::FilesystemKeyStore; use miden_client::rpc::{Endpoint, GrpcClient, VerifyingRpcClient}; -use miden_client::{Client, Felt}; use miden_client_sqlite_store::ClientBuilderSqliteExt; -use rand::RngExt; /// Default store directory name, created in the current working directory. pub const DEFAULT_STORE_DIR: &str = "miden-bench-store"; @@ -43,13 +41,8 @@ pub async fn create_client( let keystore_path = store_path.join("keystore"); std::fs::create_dir_all(&keystore_path)?; - let mut rng = rand::rng(); - let coin_seed: [u64; 4] = rng.random(); - let rng_coin = RandomCoin::new(coin_seed.map(Felt::new_unchecked).into()); - let client = ClientBuilder::new() .rpc(Arc::new(VerifyingRpcClient::new(GrpcClient::new(endpoint, 30_000)))) - .rng(Box::new(rng_coin)) .sqlite_store(sqlite_path) .filesystem_keystore(keystore_path.to_str().expect("keystore path should be valid UTF-8"))? .tx_discard_delta(None) diff --git a/bin/miden-cli/tests/cli.rs b/bin/miden-cli/tests/cli.rs index 87f645bb8a..09b825373b 100644 --- a/bin/miden-cli/tests/cli.rs +++ b/bin/miden-cli/tests/cli.rs @@ -12,7 +12,6 @@ use miden_client::account::{AccountId, AccountType, FaucetMetadata}; use miden_client::address::{Address, NetworkId}; use miden_client::auth::TransactionAuthenticator; use miden_client::builder::ClientBuilder; -use miden_client::crypto::RandomCoin; use miden_client::keystore::Keystore; use miden_client::note::NoteId; use miden_client::note_transport::NOTE_TRANSPORT_TESTNET_ENDPOINT; @@ -24,7 +23,7 @@ use miden_client::testing::common::{ create_test_store_path, }; use miden_client::utils::Serializable; -use miden_client::{self, Client, Felt}; +use miden_client::{self, Client}; use miden_client_cli::MIDEN_DIR; use miden_client_cli::config::Network; use miden_client_sqlite_store::SqliteStore; @@ -1598,16 +1597,10 @@ async fn create_rust_client_with_store_path( std::sync::Arc::new(sqlite_store) }; - let mut rng = rand::rng(); - let coin_seed: [u64; 4] = rng.random(); - - let rng = Box::new(RandomCoin::new(coin_seed.map(Felt::new_unchecked).into())); - let keystore = FilesystemKeyStore::new(temp_dir())?; let client = ClientBuilder::new() .grpc_client(&endpoint, Some(10_000)) - .rng(rng) .store(store) .authenticator(Arc::new(keystore.clone())) .build() diff --git a/crates/rust-client/Cargo.toml b/crates/rust-client/Cargo.toml index 540f2e3e7c..a9c0a72f44 100644 --- a/crates/rust-client/Cargo.toml +++ b/crates/rust-client/Cargo.toml @@ -91,6 +91,7 @@ hex = { workspace = true } prost = { features = ["derive"], workspace = true } prost-types = { version = "0.14" } rand = { workspace = true } +rand_chacha = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } tempfile = { optional = true, workspace = true } @@ -121,7 +122,6 @@ tonic-prost-build = { version = "0.14" } miden-protocol = { default-features = false, features = ["testing"], workspace = true } miden-standards = { features = ["testing"], workspace = true } miden-testing = { default-features = false, workspace = true } -rand_chacha = { workspace = true } tokio = { workspace = true } [lints] diff --git a/crates/rust-client/src/builder.rs b/crates/rust-client/src/builder.rs index 8ec03f875c..bba41b3297 100644 --- a/crates/rust-client/src/builder.rs +++ b/crates/rust-client/src/builder.rs @@ -5,10 +5,10 @@ use alloc::vec::Vec; use miden_protocol::assembly::{DefaultSourceManager, SourceManagerSync}; use miden_protocol::block::BlockNumber; -use miden_protocol::crypto::rand::RandomCoin; -use miden_protocol::{Felt, MAX_TX_EXECUTION_CYCLES, MIN_TX_EXECUTION_CYCLES}; +use miden_protocol::{MAX_TX_EXECUTION_CYCLES, MIN_TX_EXECUTION_CYCLES}; use miden_tx::{ExecutionOptions, LocalTransactionProver}; -use rand::RngExt; +use rand::SeedableRng; +use rand_chacha::ChaCha20Rng; #[cfg(any(feature = "tonic", feature = "std"))] use crate::alloc::string::ToString; @@ -85,9 +85,9 @@ pub trait StoreFactory { /// - **Store** ([`Store`]): Provides persistence for accounts, notes, and transaction history. /// Configure via [`store()`](Self::store). /// -/// - **RNG** ([`FeltRng`](miden_protocol::crypto::rand::FeltRng)): Provides randomness for -/// generating keys, serial numbers, and other cryptographic operations. If not provided, a random -/// seed-based RNG is created automatically. Configure via [`rng()`](Self::rng). +/// - **RNG** ([`ClientCryptoRng`](crate::ClientCryptoRng)): Provides randomness for generating +/// keys, serial numbers, and other cryptographic operations. If not provided, an OS-seeded +/// `ChaCha20Rng` is created automatically. Configure via [`rng()`](Self::rng). /// /// - **Authenticator** ([`TransactionAuthenticator`](miden_tx::auth::TransactionAuthenticator)): /// Handles transaction signing when signatures are requested from within the VM. Configure via @@ -338,6 +338,53 @@ where } /// Optionally provide a custom RNG. + /// + /// The generator must implement [`CryptoRng`](rand::CryptoRng), because it backs key + /// generation, account seeds and the ephemeral key and nonce used to seal transaction inputs. + /// If not set, an OS-seeded `ChaCha20Rng` is used. + /// + /// Seed a `ChaCha20Rng` explicitly to make a client's randomness reproducible. Note that + /// `CryptoRng` constrains the generator's algorithm and not the entropy of its seed, so a + /// low-entropy seed still yields predictable keys. + /// + /// ``` + /// # use miden_client::builder::ClientBuilder; + /// # use miden_client::keystore::FilesystemKeyStore; + /// use rand::SeedableRng; + /// use rand_chacha::ChaCha20Rng; + /// + /// let builder = + /// ClientBuilder::::new().rng(Box::new(ChaCha20Rng::from_seed([7u8; 32]))); + /// ``` + /// + /// A generator that is not a `CryptoRng` is rejected at compile time, even when it implements + /// [`FeltRng`](miden_protocol::crypto::rand::FeltRng): + /// + /// ```compile_fail + /// # use miden_client::builder::ClientBuilder; + /// # use miden_client::keystore::FilesystemKeyStore; + /// use miden_protocol::crypto::rand::FeltRng; + /// use miden_protocol::{Felt, Word}; + /// + /// struct FixedRng(u64); + /// + /// impl rand::TryRng for FixedRng { + /// type Error = core::convert::Infallible; + /// fn try_next_u32(&mut self) -> Result { Ok(self.0 as u32) } + /// fn try_next_u64(&mut self) -> Result { Ok(self.0) } + /// fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Self::Error> { + /// dest.fill(self.0 as u8); + /// Ok(()) + /// } + /// } + /// + /// impl FeltRng for FixedRng { + /// fn draw_element(&mut self) -> Felt { Felt::new_unchecked(self.0) } + /// fn draw_word(&mut self) -> Word { Word::new([self.draw_element(); 4]) } + /// } + /// + /// let builder = ClientBuilder::::new().rng(Box::new(FixedRng(7))); + /// ``` #[must_use] pub fn rng(mut self, rng: ClientRngBox) -> Self { self.rng = Some(rng); @@ -473,14 +520,9 @@ where )); }; - // Use the provided RNG, or create a default one. - let rng = if let Some(user_rng) = self.rng { - user_rng - } else { - let mut seed_rng = rand::rng(); - let coin_seed: [u64; 4] = seed_rng.random(); - Box::new(RandomCoin::new(coin_seed.map(Felt::new_unchecked).into())) - }; + // Use the provided RNG, or create a default one seeded from the OS. + let rng: ClientRngBox = + self.rng.unwrap_or_else(|| Box::new(ChaCha20Rng::from_rng(&mut rand::rng()))); // Set default prover if not provided let tx_prover: Arc = diff --git a/crates/rust-client/src/lib.rs b/crates/rust-client/src/lib.rs index 566175d1bc..f5628aea10 100644 --- a/crates/rust-client/src/lib.rs +++ b/crates/rust-client/src/lib.rs @@ -367,7 +367,7 @@ use miden_protocol::block::BlockNumber; use miden_protocol::crypto::merkle::mmr::PartialMmr; use miden_protocol::crypto::rand::FeltRng; use miden_tx::auth::TransactionAuthenticator; -use rand::{TryCryptoRng, TryRng}; +use rand::{CryptoRng, RngExt, TryCryptoRng, TryRng}; use rpc::NodeRpcClient; use store::Store; @@ -544,7 +544,7 @@ impl Client { // CLIENT RNG // ================================================================================================ -// NOTE: The idea of having `ClientRng` is to enforce `Send` and `Sync` over `FeltRng`. +// NOTE: The idea of having `ClientRng` is to enforce `Send` and `Sync` over the supplied RNG. // This allows `Client`` to be `Send` and `Sync`. There may be users that would want to use clients // with !Send/!Sync RNGs. For this we have two options: // @@ -553,13 +553,17 @@ impl Client { // these bounds. (similar to TransactionAuthenticator) /// Marker trait for RNGs that can be shared across threads and used by the client. -pub trait ClientFeltRng: FeltRng + Send + Sync {} -impl ClientFeltRng for T where T: FeltRng + Send + Sync {} +/// +/// The [`CryptoRng`] bound is required because the client's RNG backs key generation, account +/// seeds and the ephemeral key and nonce used to seal transaction inputs. Note that `CryptoRng` +/// constrains the generator's algorithm, not the entropy of its seed. +pub trait ClientCryptoRng: CryptoRng + Send + Sync {} +impl ClientCryptoRng for T where T: CryptoRng + Send + Sync {} /// Boxed RNG trait object used by the client. -pub type ClientRngBox = Box; +pub type ClientRngBox = Box; -/// A wrapper around a [`FeltRng`] that implements the [`TryRng`] trait. +/// A wrapper around a [`CryptoRng`] that implements the [`TryRng`] and [`FeltRng`] traits. /// This allows the user to pass their own generic RNG so that it's used by the client. pub struct ClientRng(ClientRngBox); @@ -590,18 +594,27 @@ impl TryRng for ClientRng { } } -// The client's RNG already backs key and serial-number generation, so callers are required to -// supply cryptographically secure randomness. Asserting it here lets the RNG drive primitives that -// demand a `CryptoRng`, such as sealing transaction inputs. +// Holds because the inner generator is a `CryptoRng` and the delegation above is infallible. impl TryCryptoRng for ClientRng {} impl FeltRng for ClientRng { fn draw_element(&mut self) -> Felt { - self.0.draw_element() + // Rejection sampling: `Felt::new` rejects any `u64` at or beyond the field modulus, which + // keeps the result uniform over the field. The rejection probability is about 2^-32. + loop { + if let Ok(felt) = Felt::new(self.0.random::()) { + return felt; + } + } } fn draw_word(&mut self) -> Word { - self.0.draw_word() + Word::new([ + self.draw_element(), + self.draw_element(), + self.draw_element(), + self.draw_element(), + ]) } } diff --git a/crates/testing/miden-client-tests/src/tests.rs b/crates/testing/miden-client-tests/src/tests.rs index 26d389f1a3..2f7d97ec9e 100644 --- a/crates/testing/miden-client-tests/src/tests.rs +++ b/crates/testing/miden-client-tests/src/tests.rs @@ -128,7 +128,7 @@ use miden_standards::testing::note::NoteBuilder; use miden_standards::tx_script::SendNotesTransactionScriptError; use miden_testing::{MockChain, MockChainBuilder, MockTransactionInput}; use rand::rngs::StdRng; -use rand::{Rng, RngExt, SeedableRng}; +use rand::{Rng, SeedableRng}; use rstest::rstest; mod batch; @@ -1414,15 +1414,12 @@ async fn input_note_reader_finds_externally_consumed_notes() { chain.prove_next_block().unwrap(); // Build a client backed by this chain. - let rng = - RandomCoin::new(rand::random::<[u64; 4]>().map(|v| Felt::new_unchecked(v >> 1)).into()); let keystore_path = std::env::temp_dir(); let keystore = FilesystemKeyStore::new(keystore_path).unwrap(); let mock_rpc = MockRpcApi::new(chain); let mut client = ClientBuilder::new() .rpc(Arc::new(mock_rpc)) - .rng(Box::new(rng)) .sqlite_store(create_test_store_path()) .authenticator(Arc::new(keystore)) .tx_discard_delta(None) @@ -1521,14 +1518,11 @@ async fn import_by_id_already_consumed_note_is_findable_by_id() { chain.prove_next_block().unwrap(); // Build a client backed by this chain. This client never saw the note before. - let rng = - RandomCoin::new(rand::random::<[u64; 4]>().map(|v| Felt::new_unchecked(v >> 1)).into()); let keystore = FilesystemKeyStore::new(std::env::temp_dir()).unwrap(); let mock_rpc = MockRpcApi::new(chain); let mut client = ClientBuilder::new() .rpc(Arc::new(mock_rpc)) - .rng(Box::new(rng)) .sqlite_store(create_test_store_path()) .authenticator(Arc::new(keystore)) .tx_discard_delta(None) @@ -1622,14 +1616,11 @@ async fn setup_prunable_block_scenario( chain.add_pending_executed_transaction(&tx).unwrap(); chain.prove_next_block().unwrap(); - let rng = - RandomCoin::new(rand::random::<[u64; 4]>().map(|v| Felt::new_unchecked(v >> 1)).into()); let keystore = FilesystemKeyStore::new(std::env::temp_dir()).unwrap(); let mock_rpc = MockRpcApi::new(chain); let mut client = ClientBuilder::new() .rpc(Arc::new(mock_rpc.clone())) - .rng(Box::new(rng)) .sqlite_store(create_test_store_path()) .authenticator(Arc::new(keystore)) .tx_discard_delta(None) @@ -3308,15 +3299,10 @@ async fn pswap_cancel_test() { async fn create_pswap_test_client( mock_rpc_api: &MockRpcApi, ) -> (MockClient, FilesystemKeyStore) { - let mut seed_rng = rand::rng(); - let coin_seed: [u64; 4] = seed_rng.random(); - let rng = RandomCoin::new(coin_seed.map(|v| Felt::new_unchecked(v >> 1)).into()); - let keystore = FilesystemKeyStore::new(temp_dir()).unwrap(); let mut client = ClientBuilder::new() .rpc(Arc::new(mock_rpc_api.clone())) - .rng(Box::new(rng)) .sqlite_store(create_test_store_path()) .authenticator(Arc::new(keystore.clone())) .tx_discard_delta(None) @@ -4279,13 +4265,9 @@ async fn import_watched_account_by_id_rejects_already_tracked_native_account() { let account_id = account.id(); let rpc_api = MockRpcApi::new(mock_chain_builder.build().unwrap()); let arc_rpc_api = Arc::new(rpc_api); - let mut rng = rand::rng(); - let coin_seed: [u64; 4] = rng.random(); - let rng = RandomCoin::new(coin_seed.map(|v| Felt::new_unchecked(v >> 1)).into()); let keystore = FilesystemKeyStore::new(temp_dir()).unwrap(); let mut client = ClientBuilder::new() .rpc(arc_rpc_api) - .rng(Box::new(rng)) .sqlite_store(create_test_store_path()) .authenticator(Arc::new(keystore)) .build() @@ -4591,12 +4573,9 @@ async fn sync_stores_private_note_attachments() { let rpc_api = Arc::new(MockRpcApi::new(mock_chain)); rpc_api.register_private_note_attachments(private_note.id(), attachments.clone()); - let rng = - RandomCoin::new(rand::random::<[u64; 4]>().map(|v| Felt::new_unchecked(v >> 1)).into()); let keystore = FilesystemKeyStore::new(std::env::temp_dir()).unwrap(); let mut client = ClientBuilder::new() .rpc(rpc_api) - .rng(Box::new(rng)) .sqlite_store(create_test_store_path()) .authenticator(Arc::new(keystore)) .tx_discard_delta(None) @@ -4734,16 +4713,11 @@ async fn sync_large_public_account() { // 4. Build a client and add the ORIGINAL (pre-tx) account. // The pre-tx commitment differs from on-chain, which triggers sync. - let mut rng = rand::rng(); - let coin_seed: [u64; 4] = rng.random(); - let rng = RandomCoin::new(coin_seed.map(|v| Felt::new_unchecked(v >> 1)).into()); - let keystore_path = temp_dir(); let keystore = FilesystemKeyStore::new(keystore_path).unwrap(); let mut client = ClientBuilder::new() .rpc(arc_rpc_api) - .rng(Box::new(rng)) .sqlite_store(create_test_store_path()) .authenticator(Arc::new(keystore)) .build() @@ -4799,10 +4773,6 @@ async fn prepare_offline_bootstrap_inserts_mock_chain_genesis() { use miden_protocol::crypto::merkle::smt::Smt; use miden_protocol::transaction::TransactionKernel; - let mut rng_seed = rand::rng(); - let coin_seed: [u64; 4] = rng_seed.random(); - let rng = RandomCoin::new(coin_seed.map(Felt::new_unchecked).into()); - let reference_rpc = MockRpcApi::default(); let (expected_genesis, _) = reference_rpc .get_block_header_by_number(Some(BlockNumber::GENESIS), false) @@ -4815,7 +4785,6 @@ async fn prepare_offline_bootstrap_inserts_mock_chain_genesis() { let mut client = ClientBuilder::new() .rpc(Arc::new(MockRpcApi::default())) .sqlite_store(create_test_store_path()) - .rng(Box::new(rng)) .authenticator(Arc::new(keystore)) .build() .await @@ -4871,11 +4840,6 @@ pub async fn seed_mock_transaction_encryption_key(client: &mut MockClient (ClientBuilder, MockRpcApi, FilesystemKeyStore) { - let mut rng = rand::rng(); - let coin_seed: [u64; 4] = rng.random(); - - let rng = RandomCoin::new(coin_seed.map(|v| Felt::new_unchecked(v >> 1)).into()); - let keystore_path = temp_dir(); let keystore = FilesystemKeyStore::new(keystore_path).unwrap(); @@ -4884,7 +4848,6 @@ pub async fn create_test_client_builder() let builder = ClientBuilder::new() .rpc(arc_rpc_api) - .rng(Box::new(rng)) .sqlite_store(create_test_store_path()) .authenticator(Arc::new(keystore.clone())) .tx_discard_delta(None); diff --git a/crates/testing/miden-client-tests/src/tests/batch.rs b/crates/testing/miden-client-tests/src/tests/batch.rs index a9c2a011df..8813340004 100644 --- a/crates/testing/miden-client-tests/src/tests/batch.rs +++ b/crates/testing/miden-client-tests/src/tests/batch.rs @@ -26,8 +26,6 @@ use miden_client::transaction::{ TransactionStoreUpdate, }; use miden_client_sqlite_store::ClientBuilderSqliteExt; -use miden_protocol::Felt; -use miden_protocol::crypto::rand::RandomCoin; use miden_testing::{Auth, MockChainBuilder, MockTransactionInput}; use crate::tests::{create_test_client, seed_mock_transaction_encryption_key}; @@ -156,13 +154,10 @@ async fn apply_transaction_batch_rolls_back_on_mid_batch_failure() { let mock_chain = chain_builder.build().unwrap(); // Build a client backed by the mock chain. - let rng = - RandomCoin::new(rand::random::<[u64; 4]>().map(|v| Felt::new_unchecked(v >> 1)).into()); let keystore = FilesystemKeyStore::new(std::env::temp_dir()).unwrap(); let rpc_api = MockRpcApi::new(mock_chain); let mut client = ClientBuilder::new() .rpc(Arc::new(rpc_api.clone())) - .rng(Box::new(rng)) .sqlite_store(create_test_store_path()) .authenticator(Arc::new(keystore)) .tx_discard_delta(None) @@ -430,12 +425,10 @@ async fn batch_builder_submits_txs_across_multiple_accounts() { let account_id_b = account_b.id(); let mock_chain = chain_builder.build().unwrap(); - let rng = RandomCoin::new(rand::random::<[u64; 4]>().map(Felt::new_unchecked).into()); let keystore = FilesystemKeyStore::new(std::env::temp_dir()).unwrap(); let rpc_api = MockRpcApi::new(mock_chain); let mut client = ClientBuilder::new() .rpc(Arc::new(rpc_api.clone())) - .rng(Box::new(rng)) .sqlite_store(create_test_store_path()) .authenticator(Arc::new(keystore)) .tx_discard_delta(None) diff --git a/crates/testing/miden-client-tests/src/tests/transport.rs b/crates/testing/miden-client-tests/src/tests/transport.rs index 56b54d9e6a..d3be57e1a0 100644 --- a/crates/testing/miden-client-tests/src/tests/transport.rs +++ b/crates/testing/miden-client-tests/src/tests/transport.rs @@ -40,7 +40,6 @@ use miden_protocol::utils::serde::Serializable; use miden_standards::note::P2idNote; use miden_standards::testing::note::NoteBuilder; use miden_testing::{Auth, MockChainBuilder, MockTransactionInput}; -use rand::RngExt; use crate::tests::{ create_test_client_builder, @@ -139,11 +138,8 @@ async fn transport_recovers_attachments() { let mock_node = Arc::new(RwLock::new(MockNoteTransportNode::new())); let keystore = FilesystemKeyStore::new(temp_dir()).unwrap(); - let rng = - RandomCoin::new(rand::random::<[u64; 4]>().map(|v| Felt::new_unchecked(v >> 1)).into()); let mut client = ClientBuilder::new() .rpc(rpc_api.clone()) - .rng(Box::new(rng)) .sqlite_store(create_test_store_path()) .authenticator(Arc::new(keystore)) .note_transport(Arc::new(MockNoteTransportApi::new(mock_node.clone()))) @@ -538,16 +534,11 @@ async fn fetch_private_notes_finds_note_committed_at_sync_height() { let arc_rpc_api = Arc::new(rpc_api); let transport_client = MockNoteTransportApi::new(mock_transport_node.clone()); - let mut rng = rand::rng(); - let coin_seed: [u64; 4] = rng.random(); - let rng = RandomCoin::new(coin_seed.map(|v| Felt::new_unchecked(v >> 1)).into()); - let keystore_path = temp_dir(); let keystore = FilesystemKeyStore::new(keystore_path.clone()).unwrap(); let builder: ClientBuilder = ClientBuilder::new() .rpc(arc_rpc_api) - .rng(Box::new(rng)) .sqlite_store(create_test_store_path()) .authenticator(Arc::new(keystore)) .tx_discard_delta(None) @@ -995,16 +986,11 @@ async fn committed_private_note_recipient( let arc_rpc_api = Arc::new(rpc_api); let transport_client = MockNoteTransportApi::new(mock_transport_node.clone()); - let mut rng = rand::rng(); - let coin_seed: [u64; 4] = rng.random(); - let rng = RandomCoin::new(coin_seed.map(|v| Felt::new_unchecked(v >> 1)).into()); - let keystore_path = temp_dir(); let keystore = FilesystemKeyStore::new(keystore_path.clone()).unwrap(); let builder: ClientBuilder = ClientBuilder::new() .rpc(arc_rpc_api) - .rng(Box::new(rng)) .sqlite_store(create_test_store_path()) .authenticator(Arc::new(keystore)) .tx_discard_delta(None) From e2f2ad123c0fa435406c82257bae7a33a13579df Mon Sep 17 00:00:00 2001 From: ricomateo Date: Tue, 18 Aug 2026 15:31:59 -0300 Subject: [PATCH 02/14] fix(rust-client): draw secret keys and input sealing from a separate OS-seeded RNG --- CHANGELOG.md | 1 + bin/miden-cli/src/commands/new_account.rs | 2 +- crates/rust-client/src/builder.rs | 30 ++++++++++++------ crates/rust-client/src/lib.rs | 31 ++++++++++++++++--- crates/rust-client/src/test_utils/common.rs | 2 +- .../rust-client/src/transaction/batch/mod.rs | 2 +- crates/rust-client/src/transaction/mod.rs | 2 +- .../testing/miden-client-tests/src/tests.rs | 4 +-- 8 files changed, 54 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89579cd4f8..44616a1e3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Breaking Changes +* [BREAKING][behavior][rust] The client now keeps two random number generators. `Client::rng` covers note serial numbers, script arguments and account seeds, and is the one `ClientBuilder::rng` overrides; the new `Client::secure_rng` covers secret keys, is always seeded from the operating system and cannot be overridden. Transaction input sealing draws its ephemeral key and nonce from the secure generator, so seeding `ClientBuilder::rng` for a reproducible run can no longer replay a sealing nonce or a key. Callers generating secret keys must switch from `client.rng()` to `client.secure_rng()` ([#2204](https://github.com/0xMiden/rust-sdk/issues/2204)). * [BREAKING][type][rust] `ClientBuilder::rng` now requires `CryptoRng + Send + Sync`, the marker trait `ClientFeltRng` is renamed to `ClientCryptoRng` with its bound changed from `FeltRng` to `CryptoRng`, and a client with no RNG configured now defaults to an OS-seeded `ChaCha20Rng` instead of a `RandomCoin`. `RandomCoin` is no longer accepted, since it is not a `CryptoRng`; pass a `rand` CSPRNG or drop the `rng()` call to take the default. `Client::rng()` still returns a `FeltRng`, so `draw_word` and `generate_serial_number` call sites are unaffected. For reproducible randomness, seed a `ChaCha20Rng` with `from_seed`; note that `CryptoRng` constrains the generator's algorithm and not the entropy of its seed ([#2204](https://github.com/0xMiden/rust-sdk/issues/2204)). * [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)). diff --git a/bin/miden-cli/src/commands/new_account.rs b/bin/miden-cli/src/commands/new_account.rs index 236fd2ce73..4fd10816e1 100644 --- a/bin/miden-cli/src/commands/new_account.rs +++ b/bin/miden-cli/src/commands/new_account.rs @@ -541,7 +541,7 @@ async fn create_client_account( None } else { debug!("Adding default Falcon auth component"); - let kp = AuthSecretKey::new_falcon512_poseidon2_with_rng(client.rng()); + let kp = AuthSecretKey::new_falcon512_poseidon2_with_rng(client.secure_rng()); builder = builder.with_component(AuthSingleSig::new(Approver::new( kp.public_key().to_commitment(), AuthSchemeId::Falcon512Poseidon2, diff --git a/crates/rust-client/src/builder.rs b/crates/rust-client/src/builder.rs index bba41b3297..6c258cf25a 100644 --- a/crates/rust-client/src/builder.rs +++ b/crates/rust-client/src/builder.rs @@ -85,9 +85,10 @@ pub trait StoreFactory { /// - **Store** ([`Store`]): Provides persistence for accounts, notes, and transaction history. /// Configure via [`store()`](Self::store). /// -/// - **RNG** ([`ClientCryptoRng`](crate::ClientCryptoRng)): Provides randomness for generating -/// keys, serial numbers, and other cryptographic operations. If not provided, an OS-seeded -/// `ChaCha20Rng` is created automatically. Configure via [`rng()`](Self::rng). +/// - **RNG** ([`ClientCryptoRng`](crate::ClientCryptoRng)): Provides randomness for note serial +/// numbers, script arguments and account seeds. If not provided, an OS-seeded `ChaCha20Rng` is +/// created automatically. Configure via [`rng()`](Self::rng). Secret keys and transaction input +/// sealing use a separate, non-overridable OS-seeded generator. /// /// - **Authenticator** ([`TransactionAuthenticator`](miden_tx::auth::TransactionAuthenticator)): /// Handles transaction signing when signatures are requested from within the VM. Configure via @@ -337,15 +338,19 @@ where self } - /// Optionally provide a custom RNG. + /// Optionally provide a custom RNG for note serial numbers and script arguments. /// - /// The generator must implement [`CryptoRng`](rand::CryptoRng), because it backs key - /// generation, account seeds and the ephemeral key and nonce used to seal transaction inputs. - /// If not set, an OS-seeded `ChaCha20Rng` is used. + /// This does **not** affect secret keys, account seeds or the sealing of transaction inputs. + /// Those draw from a separate generator that is always seeded from the operating system and + /// cannot be overridden, so seeding this one for a reproducible run cannot replay a key or a + /// sealing nonce. See [`Client::secure_rng`](crate::Client::secure_rng). /// - /// Seed a `ChaCha20Rng` explicitly to make a client's randomness reproducible. Note that - /// `CryptoRng` constrains the generator's algorithm and not the entropy of its seed, so a - /// low-entropy seed still yields predictable keys. + /// If not set, an OS-seeded `ChaCha20Rng` is used here too. Seed a `ChaCha20Rng` explicitly to + /// make serial numbers reproducible; predicting them reveals a private note's recipient and + /// nullifier, so this trades away the caller's own privacy. + /// + /// The generator must still implement [`CryptoRng`](rand::CryptoRng). Note that `CryptoRng` + /// constrains the generator's algorithm and not the entropy of its seed. /// /// ``` /// # use miden_client::builder::ClientBuilder; @@ -524,6 +529,10 @@ where let rng: ClientRngBox = self.rng.unwrap_or_else(|| Box::new(ChaCha20Rng::from_rng(&mut rand::rng()))); + // The secure RNG is never caller-supplied: it backs secret keys, account seeds and the + // sealing of transaction inputs, which must stay unpredictable even when `rng` is seeded. + let secure_rng: ClientRngBox = Box::new(ChaCha20Rng::from_rng(&mut rand::rng())); + // Set default prover if not provided let tx_prover: Arc = self.tx_prover.unwrap_or_else(|| Arc::new(LocalTransactionProver::default())); @@ -566,6 +575,7 @@ where Ok(Client { store, rng: ClientRng::new(rng), + secure_rng: ClientRng::new(secure_rng), rpc_api, tx_prover, authenticator: self.authenticator, diff --git a/crates/rust-client/src/lib.rs b/crates/rust-client/src/lib.rs index f5628aea10..29fb6678ea 100644 --- a/crates/rust-client/src/lib.rs +++ b/crates/rust-client/src/lib.rs @@ -387,9 +387,16 @@ use crate::transaction::TransactionProver; pub struct Client { /// The client's store, which provides a way to write and read entities to provide persistence. store: Arc, - /// An instance of [`FeltRng`] which provides randomness tools for generating new keys, - /// serial numbers, etc. + /// Randomness for values whose predictability costs only the caller's own privacy, such as + /// note serial numbers, script arguments and account seeds. This is the generator a caller + /// may override through [`ClientBuilder::rng`](crate::builder::ClientBuilder::rng), so it may + /// be seeded for reproducibility. rng: ClientRng, + /// Randomness for values whose predictability would harm someone other than the caller, + /// namely secret keys and the ephemeral key and nonce that seal transaction inputs. Always + /// seeded from the operating system and never overridable, so a caller replaying a seeded + /// `rng` cannot replay a sealing nonce or a key. + secure_rng: ClientRng, /// An instance of [`NodeRpcClient`] which provides a way for the client to connect to the /// Miden node. rpc_api: Arc, @@ -481,12 +488,28 @@ where note::NoteScreener::new(self.store.clone(), self.rpc_api.clone()) } - /// Returns a reference to the client's random number generator. This can be used to generate - /// randomness for various purposes such as serial numbers, keys, etc. + /// Returns the client's general-purpose random number generator, used for note serial numbers + /// and script arguments. + /// + /// This generator can be replaced with + /// [`ClientBuilder::rng`](crate::builder::ClientBuilder::rng), so a caller may have seeded + /// it for reproducibility. Account seeds belong here too: they grind a public account ID, so + /// predicting one costs the caller's own privacy and nothing more. Use + /// [`Client::secure_rng`] for secret keys. pub fn rng(&mut self) -> &mut ClientRng { &mut self.rng } + /// Returns the client's secure random number generator, seeded from the operating system and + /// never overridable. + /// + /// Use this for secret keys. It is also what the client uses internally to seal transaction + /// inputs, so an ephemeral key and nonce stay unpredictable even when the caller seeds + /// [`Client::rng`] for reproducible runs. + pub fn secure_rng(&mut self) -> &mut ClientRng { + &mut self.secure_rng + } + pub fn prover(&self) -> Arc { self.tx_prover.clone() } diff --git a/crates/rust-client/src/test_utils/common.rs b/crates/rust-client/src/test_utils/common.rs index f166736588..e87a487819 100644 --- a/crates/rust-client/src/test_utils/common.rs +++ b/crates/rust-client/src/test_utils/common.rs @@ -629,7 +629,7 @@ pub async fn insert_account_with_custom_component( let mut init_seed = [0u8; 32]; client.rng().fill_bytes(&mut init_seed); - let key_pair = AuthSecretKey::new_falcon512_poseidon2_with_rng(client.rng()); + let key_pair = AuthSecretKey::new_falcon512_poseidon2_with_rng(client.secure_rng()); let pub_key = key_pair.public_key(); let account = AccountBuilder::new(init_seed) diff --git a/crates/rust-client/src/transaction/batch/mod.rs b/crates/rust-client/src/transaction/batch/mod.rs index 25b5190feb..be435b18b2 100644 --- a/crates/rust-client/src/transaction/batch/mod.rs +++ b/crates/rust-client/src/transaction/batch/mod.rs @@ -207,7 +207,7 @@ where .iter() .zip(transaction_inputs) .map(|(tx_id, inputs)| { - seal_transaction_inputs(&mut self.client.rng, &key, *tx_id, &inputs) + seal_transaction_inputs(&mut self.client.secure_rng, &key, *tx_id, &inputs) }) .collect::, _>>()?; diff --git a/crates/rust-client/src/transaction/mod.rs b/crates/rust-client/src/transaction/mod.rs index 7f77a197ce..6c562b219c 100644 --- a/crates/rust-client/src/transaction/mod.rs +++ b/crates/rust-client/src/transaction/mod.rs @@ -538,7 +538,7 @@ where let tx_id = proven_transaction.id(); let key = self.transaction_encryption_key().await?; let sealed_inputs = - seal_transaction_inputs(&mut self.rng, &key, tx_id, &transaction_inputs.into())?; + seal_transaction_inputs(&mut self.secure_rng, &key, tx_id, &transaction_inputs.into())?; let result = self.rpc_api.submit_proven_transaction(proven_transaction, sealed_inputs).await; if let Err(err) = &result { diff --git a/crates/testing/miden-client-tests/src/tests.rs b/crates/testing/miden-client-tests/src/tests.rs index 2f7d97ec9e..a42ed3ed9c 100644 --- a/crates/testing/miden-client-tests/src/tests.rs +++ b/crates/testing/miden-client-tests/src/tests.rs @@ -4945,7 +4945,7 @@ async fn insert_new_wallet( visibility: AccountType, keystore: &FilesystemKeyStore, ) -> Result { - let key_pair = AuthSecretKey::new_falcon512_poseidon2_with_rng(client.rng()); + let key_pair = AuthSecretKey::new_falcon512_poseidon2_with_rng(client.secure_rng()); let pub_key = key_pair.public_key(); let mut init_seed = [0u8; 32]; @@ -5001,7 +5001,7 @@ async fn insert_new_fungible_faucet( visibility: AccountType, keystore: &FilesystemKeyStore, ) -> Result { - let key_pair = AuthSecretKey::new_falcon512_poseidon2_with_rng(client.rng()); + let key_pair = AuthSecretKey::new_falcon512_poseidon2_with_rng(client.secure_rng()); let pub_key = key_pair.public_key(); // we need to use an initial seed to create the wallet account From 42aea8b9d66ea384656d58f9d7ca50d4ab10bcf3 Mon Sep 17 00:00:00 2001 From: ricomateo Date: Tue, 18 Aug 2026 15:59:39 -0300 Subject: [PATCH 03/14] refactor(rust-client): remove RandomCoin in favor of ChaCha20Rng --- CHANGELOG.md | 1 + Cargo.lock | 2 + bin/integration-tests/Cargo.toml | 1 + .../src/tests/custom_transaction.rs | 12 +-- crates/rust-client/src/lib.rs | 41 ++++++---- crates/rust-client/src/transaction/mod.rs | 8 +- .../src/transaction/request/mod.rs | 14 ++-- crates/testing/miden-client-tests/Cargo.toml | 1 + .../testing/miden-client-tests/src/tests.rs | 75 ++++++++----------- .../miden-client-tests/src/tests/transport.rs | 54 ++++++------- 10 files changed, 103 insertions(+), 106 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44616a1e3c..48f28b5877 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Breaking Changes +* [BREAKING][removal][rust] The `miden_client::crypto::RandomCoin` re-export is gone; the client no longer uses `RandomCoin` anywhere. Use a `rand` CSPRNG such as `ChaCha20Rng`, and the new `miden_client::crypto::draw_felt` / `draw_word` helpers where a random `Felt` or `Word` is needed from a generator that does not implement `FeltRng` (for example a note serial number: `.serial_number(draw_word(&mut rng))` in place of `.generate_serial_number(&mut rng)`). The `FeltRng` re-export remains, and `Client::rng` still implements it ([#2204](https://github.com/0xMiden/rust-sdk/issues/2204)). * [BREAKING][behavior][rust] The client now keeps two random number generators. `Client::rng` covers note serial numbers, script arguments and account seeds, and is the one `ClientBuilder::rng` overrides; the new `Client::secure_rng` covers secret keys, is always seeded from the operating system and cannot be overridden. Transaction input sealing draws its ephemeral key and nonce from the secure generator, so seeding `ClientBuilder::rng` for a reproducible run can no longer replay a sealing nonce or a key. Callers generating secret keys must switch from `client.rng()` to `client.secure_rng()` ([#2204](https://github.com/0xMiden/rust-sdk/issues/2204)). * [BREAKING][type][rust] `ClientBuilder::rng` now requires `CryptoRng + Send + Sync`, the marker trait `ClientFeltRng` is renamed to `ClientCryptoRng` with its bound changed from `FeltRng` to `CryptoRng`, and a client with no RNG configured now defaults to an OS-seeded `ChaCha20Rng` instead of a `RandomCoin`. `RandomCoin` is no longer accepted, since it is not a `CryptoRng`; pass a `rand` CSPRNG or drop the `rng()` call to take the default. `Client::rng()` still returns a `FeltRng`, so `draw_word` and `generate_serial_number` call sites are unaffected. For reproducible randomness, seed a `ChaCha20Rng` with `from_seed`; note that `CryptoRng` constrains the generator's algorithm and not the entropy of its seed ([#2204](https://github.com/0xMiden/rust-sdk/issues/2204)). * [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)). diff --git a/Cargo.lock b/Cargo.lock index dc8cf825ad..cb51b2ec0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2524,6 +2524,7 @@ dependencies = [ "miden-protocol", "num_cpus", "rand 0.10.1", + "rand_chacha 0.10.0", "regex", "serde", "serde_json", @@ -2562,6 +2563,7 @@ dependencies = [ "miden-standards", "miden-testing", "rand 0.10.1", + "rand_chacha 0.10.0", "rstest", "tempfile", "tokio", diff --git a/bin/integration-tests/Cargo.toml b/bin/integration-tests/Cargo.toml index 9c3850c98b..3ef93f7984 100644 --- a/bin/integration-tests/Cargo.toml +++ b/bin/integration-tests/Cargo.toml @@ -31,6 +31,7 @@ async-trait = { workspace = true } clap = { features = ["derive", "env"], workspace = true } num_cpus = { version = "1.0" } rand = { workspace = true } +rand_chacha = { workspace = true } regex = { workspace = true } serde = { workspace = true } serde_json = { features = ["arbitrary_precision"], workspace = true } diff --git a/bin/integration-tests/src/tests/custom_transaction.rs b/bin/integration-tests/src/tests/custom_transaction.rs index 7e2b258f80..f79010c486 100644 --- a/bin/integration-tests/src/tests/custom_transaction.rs +++ b/bin/integration-tests/src/tests/custom_transaction.rs @@ -2,7 +2,7 @@ use anyhow::{Context, Result}; use miden_client::account::{AccountId, AccountType}; use miden_client::asset::FungibleAsset; use miden_client::auth::RPO_FALCON_SCHEME_ID; -use miden_client::crypto::{FeltRng, MerkleStore, MerkleTree, NodeIndex, Poseidon2, RandomCoin}; +use miden_client::crypto::{MerkleStore, MerkleTree, NodeIndex, Poseidon2, draw_word}; use miden_client::note::{ Note, NoteAssets, @@ -22,6 +22,8 @@ use miden_client::transaction::{ }; use miden_client::utils::{Deserializable, Serializable}; use miden_client::{Felt, Word, ZERO}; +use rand::SeedableRng; +use rand_chacha::ChaCha20Rng; use crate::tests::config::ClientConfig; @@ -276,7 +278,7 @@ pub async fn test_onchain_notes_sync_with_tag(client_config: ClientConfig) -> Re "; let note_script = client_1.code_builder().compile_note_script(note_script)?; let inputs = NoteStorage::new(vec![])?; - let serial_num = client_1.rng().draw_word(); + let serial_num = draw_word(client_1.rng()); let note_metadata = PartialNoteMetadata::new(basic_account_1.id(), NoteType::Public) .with_tag(NoteTag::with_account_target(basic_account_1.id())); let note_assets = NoteAssets::new(vec![])?; @@ -320,7 +322,7 @@ async fn mint_custom_note( target_account_id: AccountId, ) -> Result { // Prepare transaction - let mut random_coin = RandomCoin::new(Default::default()); + let mut random_coin = ChaCha20Rng::seed_from_u64(0); let note = create_custom_note(client, faucet_account_id, target_account_id, &mut random_coin)?; let transaction_request = @@ -337,7 +339,7 @@ fn create_custom_note( client: &TestClient, faucet_account_id: AccountId, target_account_id: AccountId, - rng: &mut RandomCoin, + rng: &mut impl rand::Rng, ) -> Result { let mem_addr: u32 = 1000; @@ -356,7 +358,7 @@ fn create_custom_note( let inputs = NoteStorage::new(vec![target_account_id.suffix(), target_account_id.prefix().as_felt()]) .context("failed to create note inputs")?; - let serial_num = rng.draw_word(); + let serial_num = draw_word(rng); let note_metadata = PartialNoteMetadata::new(faucet_account_id, NoteType::Private) .with_tag(NoteTag::with_account_target(target_account_id)); let note_assets = NoteAssets::new(vec![ diff --git a/crates/rust-client/src/lib.rs b/crates/rust-client/src/lib.rs index 29fb6678ea..a319fbff2e 100644 --- a/crates/rust-client/src/lib.rs +++ b/crates/rust-client/src/lib.rs @@ -287,7 +287,29 @@ pub mod crypto { NodeIndex, SparseMerklePath, }; - pub use miden_protocol::crypto::rand::{FeltRng, RandomCoin}; + pub use miden_protocol::crypto::rand::FeltRng; + + /// Draws a field element uniformly at random from `rng`. + /// + /// Uses rejection sampling: [`Felt::new`] rejects any `u64` at or beyond the field modulus, + /// which keeps the result uniform over the field. The rejection probability is about 2^-32. + pub fn draw_felt(rng: &mut impl rand::Rng) -> crate::Felt { + use rand::RngExt; + + loop { + if let Ok(felt) = crate::Felt::new(rng.random::()) { + return felt; + } + } + } + + /// Draws a [`Word`](crate::Word) uniformly at random from `rng`. + /// + /// Use this for note serial numbers when building notes from a plain [`rand`] generator, which + /// does not implement [`FeltRng`]. + pub fn draw_word(rng: &mut impl rand::Rng) -> crate::Word { + crate::Word::new([draw_felt(rng), draw_felt(rng), draw_felt(rng), draw_felt(rng)]) + } } /// Provides types for working with addresses within the Miden network. @@ -367,7 +389,7 @@ use miden_protocol::block::BlockNumber; use miden_protocol::crypto::merkle::mmr::PartialMmr; use miden_protocol::crypto::rand::FeltRng; use miden_tx::auth::TransactionAuthenticator; -use rand::{CryptoRng, RngExt, TryCryptoRng, TryRng}; +use rand::{CryptoRng, TryCryptoRng, TryRng}; use rpc::NodeRpcClient; use store::Store; @@ -622,22 +644,11 @@ impl TryCryptoRng for ClientRng {} impl FeltRng for ClientRng { fn draw_element(&mut self) -> Felt { - // Rejection sampling: `Felt::new` rejects any `u64` at or beyond the field modulus, which - // keeps the result uniform over the field. The rejection probability is about 2^-32. - loop { - if let Ok(felt) = Felt::new(self.0.random::()) { - return felt; - } - } + crypto::draw_felt(&mut self.0) } fn draw_word(&mut self) -> Word { - Word::new([ - self.draw_element(), - self.draw_element(), - self.draw_element(), - self.draw_element(), - ]) + crypto::draw_word(&mut self.0) } } diff --git a/crates/rust-client/src/transaction/mod.rs b/crates/rust-client/src/transaction/mod.rs index 6c562b219c..7daefea97a 100644 --- a/crates/rust-client/src/transaction/mod.rs +++ b/crates/rust-client/src/transaction/mod.rs @@ -1430,7 +1430,6 @@ mod tests { use miden_protocol::account::auth::AuthSecretKey; use miden_protocol::account::{AccountBuilder, AccountComponent, AccountId, AccountType}; use miden_protocol::asset::FungibleAsset; - use miden_protocol::crypto::rand::RandomCoin; use miden_protocol::note::{Note, NoteType}; use miden_protocol::testing::account_id::{ ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET, @@ -1441,6 +1440,8 @@ mod tests { use miden_standards::account::auth::{Approver, AuthSingleSig, FeeConversionInfo, NoAuth}; use miden_standards::account::wallets::BasicWallet; use miden_standards::note::P2idNote; + use rand::SeedableRng; + use rand_chacha::ChaCha20Rng; use super::{ Account, @@ -1452,20 +1453,21 @@ mod tests { }; use crate::ClientError; use crate::auth::AuthSchemeId; + use crate::crypto::draw_word; use crate::transaction::TransactionRequestError; fn own_note_with_sender(sender: AccountId) -> Note { let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap(); let target_id = AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap(); - let mut rng = RandomCoin::new(Word::default()); + let mut rng = ChaCha20Rng::seed_from_u64(0); P2idNote::builder() .sender(sender) .target(target_id) .asset(FungibleAsset::new(faucet_id, 100).unwrap()) .note_type(NoteType::Public) - .generate_serial_number(&mut rng) + .serial_number(draw_word(&mut rng)) .build() .expect("note creation failed") .into() diff --git a/crates/rust-client/src/transaction/request/mod.rs b/crates/rust-client/src/transaction/request/mod.rs index 6ef31db58c..005044a051 100644 --- a/crates/rust-client/src/transaction/request/mod.rs +++ b/crates/rust-client/src/transaction/request/mod.rs @@ -599,7 +599,6 @@ mod tests { StorageSlotName, }; use miden_protocol::asset::FungibleAsset; - use miden_protocol::crypto::rand::{FeltRng, RandomCoin}; use miden_protocol::note::{NoteTag, NoteType}; use miden_protocol::testing::account_id::{ ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET, @@ -611,8 +610,11 @@ mod tests { use miden_standards::note::P2idNote; use miden_standards::testing::account_component::MockAccountComponent; use miden_tx::utils::serde::{Deserializable, Serializable}; + use rand::SeedableRng; + use rand_chacha::ChaCha20Rng; use super::{TransactionRequest, TransactionRequestBuilder}; + use crate::crypto::draw_word; use crate::rpc::domain::account::AccountStorageRequirements; use crate::transaction::ForeignAccount; @@ -646,7 +648,7 @@ mod tests { let target_id = AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap(); let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap(); - let mut rng = RandomCoin::new(Word::default()); + let mut rng = ChaCha20Rng::seed_from_u64(0); let mut notes = vec![]; for i in 0..6 { @@ -655,7 +657,7 @@ mod tests { .target(target_id) .assets(vec![FungibleAsset::new(faucet_id, 100 + i).unwrap()]) .note_type(NoteType::Private) - .generate_serial_number(&mut rng) + .serial_number(draw_word(&mut rng)) .build() .expect("note creation failed"); notes.push(note.into()); @@ -663,7 +665,7 @@ mod tests { let mut advice_vec: Vec<(Word, Vec)> = vec![]; for i in 0u32..10 { - advice_vec.push((rng.draw_word(), vec![Felt::from(i)])); + advice_vec.push((draw_word(&mut rng), vec![Felt::from(i)])); } let account = AccountBuilder::new(Default::default()) @@ -694,8 +696,8 @@ mod tests { ForeignAccount::private(&account).unwrap(), ]) .own_output_notes(vec![notes.pop().unwrap(), notes.pop().unwrap()]) - .script_arg(rng.draw_word()) - .auth_arg(rng.draw_word()) + .script_arg(draw_word(&mut rng)) + .auth_arg(draw_word(&mut rng)) .expected_ntx_scripts(vec![notes.first().unwrap().recipient().script().clone()]) .build() .unwrap(); diff --git a/crates/testing/miden-client-tests/Cargo.toml b/crates/testing/miden-client-tests/Cargo.toml index 2d33807245..a8799c2f36 100644 --- a/crates/testing/miden-client-tests/Cargo.toml +++ b/crates/testing/miden-client-tests/Cargo.toml @@ -13,6 +13,7 @@ miden-protocol = { default-features = false, features = ["testing"], miden-standards = { features = ["testing"], workspace = true } miden-testing = { default-features = false, workspace = true } rand = { workspace = true } +rand_chacha = { workspace = true } rstest = { workspace = true } tempfile = { workspace = true } tokio = { workspace = true } diff --git a/crates/testing/miden-client-tests/src/tests.rs b/crates/testing/miden-client-tests/src/tests.rs index a42ed3ed9c..27db4827b2 100644 --- a/crates/testing/miden-client-tests/src/tests.rs +++ b/crates/testing/miden-client-tests/src/tests.rs @@ -18,6 +18,7 @@ use miden_client::auth::{ RPO_FALCON_SCHEME_ID, }; use miden_client::builder::ClientBuilder; +use miden_client::crypto::draw_word; use miden_client::keystore::{FilesystemKeyStore, Keystore}; use miden_client::note::{BlockNumber, NetworkAccountTarget, NoteExecutionHint}; use miden_client::pswap::PswapLineageState; @@ -84,7 +85,7 @@ use miden_protocol::account::{ use miden_protocol::asset::{Asset, AssetAmount, AssetId, FungibleAsset, TokenSymbol}; use miden_protocol::crypto::dsa::eddsa_25519_sha512::KeyExchangeKey; use miden_protocol::crypto::merkle::MerklePath; -use miden_protocol::crypto::rand::{FeltRng, RandomCoin}; +use miden_protocol::crypto::rand::FeltRng; use miden_protocol::note::{ Note, NoteAssets, @@ -129,6 +130,7 @@ use miden_standards::tx_script::SendNotesTransactionScriptError; use miden_testing::{MockChain, MockChainBuilder, MockTransactionInput}; use rand::rngs::StdRng; use rand::{Rng, SeedableRng}; +use rand_chacha::ChaCha20Rng; use rstest::rstest; mod batch; @@ -1559,22 +1561,16 @@ async fn setup_prunable_block_scenario( let mut builder = MockChainBuilder::new(); let mock_account = builder.add_existing_mock_account(miden_testing::Auth::IncrNonce).unwrap(); - let note_first = NoteBuilder::new( - mock_account.id(), - RandomCoin::new([0, 0, 0, 0].map(Felt::new_unchecked).into()), - ) - .note_type(NoteType::Public) - .tag(NoteTag::new(0).into()) - .build() - .unwrap(); - let note_second = NoteBuilder::new( - mock_account.id(), - RandomCoin::new([0, 0, 0, 1].map(Felt::new_unchecked).into()), - ) - .note_type(NoteType::Public) - .tag(NoteTag::new(0).into()) - .build() - .unwrap(); + let note_first = NoteBuilder::new(mock_account.id(), ChaCha20Rng::seed_from_u64(0)) + .note_type(NoteType::Public) + .tag(NoteTag::new(0).into()) + .build() + .unwrap(); + let note_second = NoteBuilder::new(mock_account.id(), ChaCha20Rng::seed_from_u64(1)) + .note_type(NoteType::Public) + .tag(NoteTag::new(0).into()) + .build() + .unwrap(); let spawn_note_1 = builder.add_spawn_note(std::slice::from_ref(¬e_first)).unwrap(); let spawn_note_2 = builder.add_spawn_note(std::slice::from_ref(¬e_second)).unwrap(); @@ -2282,15 +2278,12 @@ async fn note_screening_reports_only_the_account_bound_by_the_note() { let mut records = Vec::with_capacity(NOTE_COUNT); let mut expected_ids = BTreeSet::new(); for i in 0..NOTE_COUNT { - let note = NoteBuilder::new( - faucet_id, - RandomCoin::new([i as u64, 0, 0, 0].map(Felt::new_unchecked).into()), - ) - .script(script.clone()) - .note_storage([target.suffix(), target.prefix().as_felt()]) - .unwrap() - .build() - .unwrap(); + let note = NoteBuilder::new(faucet_id, ChaCha20Rng::seed_from_u64(i as u64)) + .script(script.clone()) + .note_storage([target.suffix(), target.prefix().as_felt()]) + .unwrap() + .build() + .unwrap(); expected_ids.insert(note.id()); let metadata = *note.metadata(); @@ -4532,14 +4525,14 @@ async fn sync_stores_private_note_attachments() { // 2. Build a PRIVATE P2ID note carrying a NetworkAccountTarget attachment. let ntx_target = NetworkAccountTarget::new(target.id(), NoteExecutionHint::Always).unwrap(); let attachments = NoteAttachments::new(vec![ntx_target.into()]).unwrap(); - let mut note_rng = RandomCoin::new([1, 2, 3, 4].map(Felt::new_unchecked).into()); + let mut note_rng = ChaCha20Rng::seed_from_u64(1234); let private_note = P2idNote::builder() .sender(sender.id()) .target(target.id()) .asset(note_asset) .note_type(NoteType::Private) .attachments(attachments.clone().into_vec()) - .generate_serial_number(&mut note_rng) + .serial_number(draw_word(&mut note_rng)) .build() .unwrap() .into(); @@ -4861,23 +4854,17 @@ pub async fn create_prebuilt_mock_chain() -> MockChain { .add_existing_mock_account(miden_testing::Auth::IncrNonce) .unwrap(); - let note_first = NoteBuilder::new( - mock_account.id(), - RandomCoin::new([0, 0, 0, 0].map(Felt::new_unchecked).into()), - ) - .note_type(NoteType::Public) - .tag(NoteTag::new(0).into()) - .build() - .unwrap(); + let note_first = NoteBuilder::new(mock_account.id(), ChaCha20Rng::seed_from_u64(0)) + .note_type(NoteType::Public) + .tag(NoteTag::new(0).into()) + .build() + .unwrap(); - let note_second = NoteBuilder::new( - mock_account.id(), - RandomCoin::new([0, 0, 0, 1].map(Felt::new_unchecked).into()), - ) - .note_type(NoteType::Public) - .tag(NoteTag::new(0).into()) - .build() - .unwrap(); + let note_second = NoteBuilder::new(mock_account.id(), ChaCha20Rng::seed_from_u64(1)) + .note_type(NoteType::Public) + .tag(NoteTag::new(0).into()) + .build() + .unwrap(); let spawn_note_1 = mock_chain_builder.add_spawn_note(std::slice::from_ref(¬e_first)).unwrap(); let spawn_note_2 = diff --git a/crates/testing/miden-client-tests/src/tests/transport.rs b/crates/testing/miden-client-tests/src/tests/transport.rs index d3be57e1a0..9ca8070c17 100644 --- a/crates/testing/miden-client-tests/src/tests/transport.rs +++ b/crates/testing/miden-client-tests/src/tests/transport.rs @@ -24,7 +24,6 @@ use miden_client::testing::note_transport::{ }; use miden_client::utils::RwLock; use miden_client_sqlite_store::ClientBuilderSqliteExt; -use miden_protocol::Felt; use miden_protocol::account::{ AccountId, AccountIdVersion, @@ -33,13 +32,14 @@ use miden_protocol::account::{ }; use miden_protocol::asset::{Asset, FungibleAsset}; use miden_protocol::block::BlockNumber; -use miden_protocol::crypto::rand::RandomCoin; use miden_protocol::note::NoteType as ProtocolNoteType; use miden_protocol::transaction::RawOutputNote; use miden_protocol::utils::serde::Serializable; use miden_standards::note::P2idNote; use miden_standards::testing::note::NoteBuilder; use miden_testing::{Auth, MockChainBuilder, MockTransactionInput}; +use rand::SeedableRng; +use rand_chacha::ChaCha20Rng; use crate::tests::{ create_test_client_builder, @@ -105,15 +105,12 @@ async fn transport_recovers_attachments() { let target = mock_chain_builder.add_existing_wallet(Auth::IncrNonce).unwrap(); let ntx_target = NetworkAccountTarget::new(target.id(), NoteExecutionHint::Always).unwrap(); - let private_note = NoteBuilder::new( - sender.id(), - RandomCoin::new([1, 2, 3, 4].map(Felt::new_unchecked).into()), - ) - .note_type(ProtocolNoteType::Private) - .tag(NoteTag::new(0).into()) - .attachment(ntx_target) - .build() - .unwrap(); + let private_note = NoteBuilder::new(sender.id(), ChaCha20Rng::seed_from_u64(1234)) + .note_type(ProtocolNoteType::Private) + .tag(NoteTag::new(0).into()) + .attachment(ntx_target) + .build() + .unwrap(); let attachments = private_note.attachments().clone(); let spawn_note = @@ -494,14 +491,11 @@ async fn fetch_private_notes_finds_note_committed_at_sync_height() { .add_existing_mock_account(miden_testing::Auth::IncrNonce) .unwrap(); - let private_note = NoteBuilder::new( - mock_account.id(), - RandomCoin::new([1, 2, 3, 4].map(Felt::new_unchecked).into()), - ) - .note_type(ProtocolNoteType::Private) - .tag(NoteTag::new(0).into()) - .build() - .unwrap(); + let private_note = NoteBuilder::new(mock_account.id(), ChaCha20Rng::seed_from_u64(1234)) + .note_type(ProtocolNoteType::Private) + .tag(NoteTag::new(0).into()) + .build() + .unwrap(); let spawn_note = mock_chain_builder.add_spawn_note(std::slice::from_ref(&private_note)).unwrap(); @@ -918,14 +912,11 @@ pub async fn create_test_user_with_transport( /// distinct notes. Lets a test seed the mock transport with notes whose tag and relative ordering /// it controls, independent of any recipient's auto-registered account tag. fn private_note_with_tag(account: AccountId, tag: NoteTag, seed: u64) -> Note { - NoteBuilder::new( - account, - RandomCoin::new([seed, seed + 1, seed + 2, seed + 3].map(Felt::new_unchecked).into()), - ) - .note_type(ProtocolNoteType::Private) - .tag(tag.into()) - .build() - .unwrap() + NoteBuilder::new(account, ChaCha20Rng::seed_from_u64(seed)) + .note_type(ProtocolNoteType::Private) + .tag(tag.into()) + .build() + .unwrap() } /// Build a chain with a private note (tag 0) committed at block 1, advance @@ -944,12 +935,9 @@ async fn committed_private_note_recipient( .add_existing_mock_account(miden_testing::Auth::IncrNonce) .unwrap(); - let mut note_builder = NoteBuilder::new( - mock_account.id(), - RandomCoin::new([1, 2, 3, 4].map(Felt::new_unchecked).into()), - ) - .note_type(ProtocolNoteType::Private) - .tag(NoteTag::new(0).into()); + let mut note_builder = NoteBuilder::new(mock_account.id(), ChaCha20Rng::seed_from_u64(1234)) + .note_type(ProtocolNoteType::Private) + .tag(NoteTag::new(0).into()); if with_unserved_attachment { let ntx_target = NetworkAccountTarget::new(mock_account.id(), NoteExecutionHint::Always).unwrap(); From 98e77fd13a5e4ee4938eae6f5d9fd90ae35f8521 Mon Sep 17 00:00:00 2001 From: ricomateo Date: Tue, 18 Aug 2026 17:09:29 -0300 Subject: [PATCH 04/14] refactor(rust-client): use explicit branching for the default RNG --- crates/rust-client/src/builder.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/rust-client/src/builder.rs b/crates/rust-client/src/builder.rs index 6c258cf25a..6c93ce79c8 100644 --- a/crates/rust-client/src/builder.rs +++ b/crates/rust-client/src/builder.rs @@ -525,9 +525,12 @@ where )); }; - // Use the provided RNG, or create a default one seeded from the OS. - let rng: ClientRngBox = - self.rng.unwrap_or_else(|| Box::new(ChaCha20Rng::from_rng(&mut rand::rng()))); + // Use the provided RNG, or create a default one. + let rng: ClientRngBox = if let Some(user_rng) = self.rng { + user_rng + } else { + Box::new(ChaCha20Rng::from_rng(&mut rand::rng())) + }; // The secure RNG is never caller-supplied: it backs secret keys, account seeds and the // sealing of transaction inputs, which must stay unpredictable even when `rng` is seeded. From a69138b0e8c3e196be2781689d331c8a71d33933 Mon Sep 17 00:00:00 2001 From: ricomateo Date: Tue, 18 Aug 2026 17:11:57 -0300 Subject: [PATCH 05/14] docs(rust-client): correct which RNG covers account seeds --- crates/rust-client/src/builder.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/crates/rust-client/src/builder.rs b/crates/rust-client/src/builder.rs index 6c93ce79c8..e35eb4cfdf 100644 --- a/crates/rust-client/src/builder.rs +++ b/crates/rust-client/src/builder.rs @@ -338,12 +338,15 @@ where self } - /// Optionally provide a custom RNG for note serial numbers and script arguments. + /// Optionally provide a custom RNG for note serial numbers, script arguments and account + /// seeds. /// - /// This does **not** affect secret keys, account seeds or the sealing of transaction inputs. - /// Those draw from a separate generator that is always seeded from the operating system and - /// cannot be overridden, so seeding this one for a reproducible run cannot replay a key or a - /// sealing nonce. See [`Client::secure_rng`](crate::Client::secure_rng). + /// This does **not** affect secret keys or the sealing of transaction inputs. Those draw + /// from a separate generator that is always seeded from the operating system and cannot be + /// overridden, so seeding this one for a reproducible run cannot replay a key or a sealing + /// nonce. Account seeds do belong here: they grind a public account ID, so predicting one + /// costs the caller's own privacy and nothing more. See + /// [`Client::secure_rng`](crate::Client::secure_rng). /// /// If not set, an OS-seeded `ChaCha20Rng` is used here too. Seed a `ChaCha20Rng` explicitly to /// make serial numbers reproducible; predicting them reveals a private note's recipient and @@ -532,8 +535,8 @@ where Box::new(ChaCha20Rng::from_rng(&mut rand::rng())) }; - // The secure RNG is never caller-supplied: it backs secret keys, account seeds and the - // sealing of transaction inputs, which must stay unpredictable even when `rng` is seeded. + // The secure RNG is never caller-supplied: it backs secret keys and the sealing of + // transaction inputs, which must stay unpredictable even when `rng` is seeded. let secure_rng: ClientRngBox = Box::new(ChaCha20Rng::from_rng(&mut rand::rng())); // Set default prover if not provided From f2c72b72e5914a5fbc7e390a7e6641f4c7c4514a Mon Sep 17 00:00:00 2001 From: ricomateo Date: Tue, 18 Aug 2026 17:50:26 -0300 Subject: [PATCH 06/14] docs(rust-client): fix broken intra-doc link in draw_felt --- crates/rust-client/src/lib.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/rust-client/src/lib.rs b/crates/rust-client/src/lib.rs index a319fbff2e..b65a2b0167 100644 --- a/crates/rust-client/src/lib.rs +++ b/crates/rust-client/src/lib.rs @@ -291,8 +291,9 @@ pub mod crypto { /// Draws a field element uniformly at random from `rng`. /// - /// Uses rejection sampling: [`Felt::new`] rejects any `u64` at or beyond the field modulus, - /// which keeps the result uniform over the field. The rejection probability is about 2^-32. + /// Uses rejection sampling: [`Felt::new`](crate::Felt::new) rejects any `u64` at or beyond + /// the field modulus, which keeps the result uniform over the field. The rejection + /// probability is about 2^-32. pub fn draw_felt(rng: &mut impl rand::Rng) -> crate::Felt { use rand::RngExt; From 4efc3f3c870568735de93aad403dd6fb1d679578 Mon Sep 17 00:00:00 2001 From: ricomateo Date: Tue, 18 Aug 2026 18:32:37 -0300 Subject: [PATCH 07/14] chore: improve docs --- crates/rust-client/src/builder.rs | 59 ++++++++----------------------- crates/rust-client/src/lib.rs | 37 ++++++------------- 2 files changed, 26 insertions(+), 70 deletions(-) diff --git a/crates/rust-client/src/builder.rs b/crates/rust-client/src/builder.rs index e35eb4cfdf..f94e492e74 100644 --- a/crates/rust-client/src/builder.rs +++ b/crates/rust-client/src/builder.rs @@ -86,9 +86,8 @@ pub trait StoreFactory { /// Configure via [`store()`](Self::store). /// /// - **RNG** ([`ClientCryptoRng`](crate::ClientCryptoRng)): Provides randomness for note serial -/// numbers, script arguments and account seeds. If not provided, an OS-seeded `ChaCha20Rng` is -/// created automatically. Configure via [`rng()`](Self::rng). Secret keys and transaction input -/// sealing use a separate, non-overridable OS-seeded generator. +/// numbers, script arguments and account seeds. If not provided, a random seed-based RNG is +/// created automatically. Configure via [`rng()`](Self::rng). /// /// - **Authenticator** ([`TransactionAuthenticator`](miden_tx::auth::TransactionAuthenticator)): /// Handles transaction signing when signatures are requested from within the VM. Configure via @@ -339,21 +338,14 @@ where } /// Optionally provide a custom RNG for note serial numbers, script arguments and account - /// seeds. + /// seeds. Defaults to an OS-seeded `ChaCha20Rng`. /// - /// This does **not** affect secret keys or the sealing of transaction inputs. Those draw - /// from a separate generator that is always seeded from the operating system and cannot be - /// overridden, so seeding this one for a reproducible run cannot replay a key or a sealing - /// nonce. Account seeds do belong here: they grind a public account ID, so predicting one - /// costs the caller's own privacy and nothing more. See - /// [`Client::secure_rng`](crate::Client::secure_rng). + /// Secret keys and transaction input sealing are unaffected: they draw from a separate, + /// non-overridable generator, so seeding this one for a reproducible run cannot replay a key + /// or a sealing nonce. See [`Client::secure_rng`](crate::Client::secure_rng). /// - /// If not set, an OS-seeded `ChaCha20Rng` is used here too. Seed a `ChaCha20Rng` explicitly to - /// make serial numbers reproducible; predicting them reveals a private note's recipient and - /// nullifier, so this trades away the caller's own privacy. - /// - /// The generator must still implement [`CryptoRng`](rand::CryptoRng). Note that `CryptoRng` - /// constrains the generator's algorithm and not the entropy of its seed. + /// The [`CryptoRng`](rand::CryptoRng) bound rules out non-cryptographic generators. It + /// constrains the algorithm only, not the entropy of a seed. /// /// ``` /// # use miden_client::builder::ClientBuilder; @@ -361,37 +353,16 @@ where /// use rand::SeedableRng; /// use rand_chacha::ChaCha20Rng; /// - /// let builder = - /// ClientBuilder::::new().rng(Box::new(ChaCha20Rng::from_seed([7u8; 32]))); + /// ClientBuilder::::new().rng(Box::new(ChaCha20Rng::from_seed([7u8; 32]))); /// ``` /// - /// A generator that is not a `CryptoRng` is rejected at compile time, even when it implements - /// [`FeltRng`](miden_protocol::crypto::rand::FeltRng): - /// /// ```compile_fail /// # use miden_client::builder::ClientBuilder; /// # use miden_client::keystore::FilesystemKeyStore; - /// use miden_protocol::crypto::rand::FeltRng; - /// use miden_protocol::{Felt, Word}; - /// - /// struct FixedRng(u64); - /// - /// impl rand::TryRng for FixedRng { - /// type Error = core::convert::Infallible; - /// fn try_next_u32(&mut self) -> Result { Ok(self.0 as u32) } - /// fn try_next_u64(&mut self) -> Result { Ok(self.0) } - /// fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Self::Error> { - /// dest.fill(self.0 as u8); - /// Ok(()) - /// } - /// } - /// - /// impl FeltRng for FixedRng { - /// fn draw_element(&mut self) -> Felt { Felt::new_unchecked(self.0) } - /// fn draw_word(&mut self) -> Word { Word::new([self.draw_element(); 4]) } - /// } - /// - /// let builder = ClientBuilder::::new().rng(Box::new(FixedRng(7))); + /// use rand::SeedableRng; + /// use rand::rngs::SmallRng; + /// + /// ClientBuilder::::new().rng(Box::new(SmallRng::seed_from_u64(7))); /// ``` #[must_use] pub fn rng(mut self, rng: ClientRngBox) -> Self { @@ -535,8 +506,8 @@ where Box::new(ChaCha20Rng::from_rng(&mut rand::rng())) }; - // The secure RNG is never caller-supplied: it backs secret keys and the sealing of - // transaction inputs, which must stay unpredictable even when `rng` is seeded. + // Create a separate, secure RNG for the sealing of transaction inputs and secret key + // generation. let secure_rng: ClientRngBox = Box::new(ChaCha20Rng::from_rng(&mut rand::rng())); // Set default prover if not provided diff --git a/crates/rust-client/src/lib.rs b/crates/rust-client/src/lib.rs index b65a2b0167..6711624257 100644 --- a/crates/rust-client/src/lib.rs +++ b/crates/rust-client/src/lib.rs @@ -410,15 +410,12 @@ use crate::transaction::TransactionProver; pub struct Client { /// The client's store, which provides a way to write and read entities to provide persistence. store: Arc, - /// Randomness for values whose predictability costs only the caller's own privacy, such as - /// note serial numbers, script arguments and account seeds. This is the generator a caller - /// may override through [`ClientBuilder::rng`](crate::builder::ClientBuilder::rng), so it may - /// be seeded for reproducibility. + /// The client's random number generator for non-secret values: note serial + /// numbers, script arguments, account seeds, etc. The caller can override it, + /// so it must not be used for secret keys; see [`Client::secure_rng`] for those. rng: ClientRng, - /// Randomness for values whose predictability would harm someone other than the caller, - /// namely secret keys and the ephemeral key and nonce that seal transaction inputs. Always - /// seeded from the operating system and never overridable, so a caller replaying a seeded - /// `rng` cannot replay a sealing nonce or a key. + /// The client's random number generator for secret values: secret keys and the + /// ephemeral key and nonce that seal transaction inputs. secure_rng: ClientRng, /// An instance of [`NodeRpcClient`] which provides a way for the client to connect to the /// Miden node. @@ -511,24 +508,16 @@ where note::NoteScreener::new(self.store.clone(), self.rpc_api.clone()) } - /// Returns the client's general-purpose random number generator, used for note serial numbers - /// and script arguments. - /// - /// This generator can be replaced with - /// [`ClientBuilder::rng`](crate::builder::ClientBuilder::rng), so a caller may have seeded - /// it for reproducibility. Account seeds belong here too: they grind a public account ID, so - /// predicting one costs the caller's own privacy and nothing more. Use - /// [`Client::secure_rng`] for secret keys. + /// Returns a reference to the client's random number generator. This can be used to generate + /// randomness for non-secret values such as serial numbers, script arguments, etc. + /// Use [`Client::secure_rng`] for generating randomness for secret values. pub fn rng(&mut self) -> &mut ClientRng { &mut self.rng } - /// Returns the client's secure random number generator, seeded from the operating system and - /// never overridable. - /// - /// Use this for secret keys. It is also what the client uses internally to seal transaction - /// inputs, so an ephemeral key and nonce stay unpredictable even when the caller seeds - /// [`Client::rng`] for reproducible runs. + /// Returns a reference to the client's secure random number generator. This can be used to + /// generate randomness for secret values such as account keys, and the nonces that seal + /// transaction inputs. pub fn secure_rng(&mut self) -> &mut ClientRng { &mut self.secure_rng } @@ -599,10 +588,6 @@ impl Client { // these bounds. (similar to TransactionAuthenticator) /// Marker trait for RNGs that can be shared across threads and used by the client. -/// -/// The [`CryptoRng`] bound is required because the client's RNG backs key generation, account -/// seeds and the ephemeral key and nonce used to seal transaction inputs. Note that `CryptoRng` -/// constrains the generator's algorithm, not the entropy of its seed. pub trait ClientCryptoRng: CryptoRng + Send + Sync {} impl ClientCryptoRng for T where T: CryptoRng + Send + Sync {} From 2e5ebc7189c554ddbfeb7c49b37b2cfe4aafd152 Mon Sep 17 00:00:00 2001 From: ricomateo Date: Tue, 18 Aug 2026 19:03:56 -0300 Subject: [PATCH 08/14] chore: improve changelog --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48f28b5877..89892cb98e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,9 @@ ### Breaking Changes -* [BREAKING][removal][rust] The `miden_client::crypto::RandomCoin` re-export is gone; the client no longer uses `RandomCoin` anywhere. Use a `rand` CSPRNG such as `ChaCha20Rng`, and the new `miden_client::crypto::draw_felt` / `draw_word` helpers where a random `Felt` or `Word` is needed from a generator that does not implement `FeltRng` (for example a note serial number: `.serial_number(draw_word(&mut rng))` in place of `.generate_serial_number(&mut rng)`). The `FeltRng` re-export remains, and `Client::rng` still implements it ([#2204](https://github.com/0xMiden/rust-sdk/issues/2204)). -* [BREAKING][behavior][rust] The client now keeps two random number generators. `Client::rng` covers note serial numbers, script arguments and account seeds, and is the one `ClientBuilder::rng` overrides; the new `Client::secure_rng` covers secret keys, is always seeded from the operating system and cannot be overridden. Transaction input sealing draws its ephemeral key and nonce from the secure generator, so seeding `ClientBuilder::rng` for a reproducible run can no longer replay a sealing nonce or a key. Callers generating secret keys must switch from `client.rng()` to `client.secure_rng()` ([#2204](https://github.com/0xMiden/rust-sdk/issues/2204)). -* [BREAKING][type][rust] `ClientBuilder::rng` now requires `CryptoRng + Send + Sync`, the marker trait `ClientFeltRng` is renamed to `ClientCryptoRng` with its bound changed from `FeltRng` to `CryptoRng`, and a client with no RNG configured now defaults to an OS-seeded `ChaCha20Rng` instead of a `RandomCoin`. `RandomCoin` is no longer accepted, since it is not a `CryptoRng`; pass a `rand` CSPRNG or drop the `rng()` call to take the default. `Client::rng()` still returns a `FeltRng`, so `draw_word` and `generate_serial_number` call sites are unaffected. For reproducible randomness, seed a `ChaCha20Rng` with `from_seed`; note that `CryptoRng` constrains the generator's algorithm and not the entropy of its seed ([#2204](https://github.com/0xMiden/rust-sdk/issues/2204)). +* [BREAKING][removal][rust] Removed the `miden_client::crypto::RandomCoin` re-export. Use a `rand` CSPRNG such as `ChaCha20Rng`, plus the new `miden_client::crypto::draw_felt` / `draw_word` helpers where a `Felt` or `Word` is needed from a generator that does not implement `FeltRng` ([#2414](https://github.com/0xMiden/rust-sdk/pull/2414)). +* [BREAKING][behavior][rust] Added `Client::secure_rng`, a second random number generator intended for secret values: secret keys, and the ephemeral key and nonce that seal transaction inputs. It is seeded from the operating system and cannot be overridden. The existing `Client::rng` is kept for non-secret values (note serial numbers, script arguments, account seeds); it stays the one `ClientBuilder::rng` overrides, so a caller may have seeded it and its output can be predictable. Callers generating secret keys must switch from `client.rng()` to `client.secure_rng()` ([#2414](https://github.com/0xMiden/rust-sdk/pull/2414)). +* [BREAKING][type][rust] `ClientBuilder::rng` now requires `CryptoRng + Send + Sync`, and the marker trait `ClientFeltRng` is renamed to `ClientCryptoRng`. A client with no RNG configured defaults to an OS-seeded `ChaCha20Rng` instead of a `RandomCoin`, which is no longer accepted since it is not a `CryptoRng`; pass a `rand` CSPRNG or drop the `rng()` call. `Client::rng()` still returns a `FeltRng`, so `draw_word` and `generate_serial_number` call sites are unaffected ([#2414](https://github.com/0xMiden/rust-sdk/pull/2414)). * [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)). * [BREAKING][type][rust] `rpc::domain::transaction::TransactionRecord` gained a non-public field, so it can no longer be constructed with a struct literal outside the crate ([#2300](https://github.com/0xMiden/rust-sdk/pull/2300)). From 9e408fee42c562aeb4875a5dd79afb8e16d17ead Mon Sep 17 00:00:00 2001 From: ricomateo Date: Tue, 18 Aug 2026 19:11:49 -0300 Subject: [PATCH 09/14] chore: shorten doc comment --- crates/rust-client/src/builder.rs | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/crates/rust-client/src/builder.rs b/crates/rust-client/src/builder.rs index f94e492e74..c629f8a4a1 100644 --- a/crates/rust-client/src/builder.rs +++ b/crates/rust-client/src/builder.rs @@ -338,31 +338,20 @@ where } /// Optionally provide a custom RNG for note serial numbers, script arguments and account - /// seeds. Defaults to an OS-seeded `ChaCha20Rng`. + /// seeds. Defaults to an OS-seeded `ChaCha20Rng`. Secret keys and transaction input sealing + /// use a separate, non-overridable generator; see + /// [`Client::secure_rng`](crate::Client::secure_rng). /// - /// Secret keys and transaction input sealing are unaffected: they draw from a separate, - /// non-overridable generator, so seeding this one for a reproducible run cannot replay a key - /// or a sealing nonce. See [`Client::secure_rng`](crate::Client::secure_rng). - /// - /// The [`CryptoRng`](rand::CryptoRng) bound rules out non-cryptographic generators. It - /// constrains the algorithm only, not the entropy of a seed. - /// - /// ``` - /// # use miden_client::builder::ClientBuilder; - /// # use miden_client::keystore::FilesystemKeyStore; - /// use rand::SeedableRng; - /// use rand_chacha::ChaCha20Rng; - /// - /// ClientBuilder::::new().rng(Box::new(ChaCha20Rng::from_seed([7u8; 32]))); - /// ``` + /// The [`CryptoRng`](rand::CryptoRng) bound rejects generators that only implement + /// [`FeltRng`](miden_protocol::crypto::rand::FeltRng), such as `RandomCoin`: /// /// ```compile_fail /// # use miden_client::builder::ClientBuilder; /// # use miden_client::keystore::FilesystemKeyStore; - /// use rand::SeedableRng; - /// use rand::rngs::SmallRng; + /// use miden_protocol::Word; + /// use miden_protocol::crypto::rand::RandomCoin; /// - /// ClientBuilder::::new().rng(Box::new(SmallRng::seed_from_u64(7))); + /// ClientBuilder::::new().rng(Box::new(RandomCoin::new(Word::default()))); /// ``` #[must_use] pub fn rng(mut self, rng: ClientRngBox) -> Self { From a440cdb386e4ef96f6b1c6d534504ef9d269586b Mon Sep 17 00:00:00 2001 From: ricomateo Date: Wed, 19 Aug 2026 10:18:08 -0300 Subject: [PATCH 10/14] test(rust-client): add trybuild test asserting ClientBuilder::rng rejects a non-CryptoRng --- Cargo.lock | 1 + crates/rust-client/Cargo.toml | 1 + crates/rust-client/src/builder.rs | 22 +++++++++ .../tests/ui/rng_requires_cryptorng.rs | 45 +++++++++++++++++++ .../tests/ui/rng_requires_cryptorng.stderr | 15 +++++++ 5 files changed, 84 insertions(+) create mode 100644 crates/rust-client/tests/ui/rng_requires_cryptorng.rs create mode 100644 crates/rust-client/tests/ui/rng_requires_cryptorng.stderr diff --git a/Cargo.lock b/Cargo.lock index cb51b2ec0a..56f0b4b621 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2463,6 +2463,7 @@ dependencies = [ "tonic-prost-build", "tonic-web-wasm-client", "tracing", + "trybuild", "uuid", ] diff --git a/crates/rust-client/Cargo.toml b/crates/rust-client/Cargo.toml index a9c0a72f44..f2d10e8a43 100644 --- a/crates/rust-client/Cargo.toml +++ b/crates/rust-client/Cargo.toml @@ -123,6 +123,7 @@ miden-protocol = { default-features = false, features = ["testing"], workspace miden-standards = { features = ["testing"], workspace = true } miden-testing = { default-features = false, workspace = true } tokio = { workspace = true } +trybuild = { version = "1.0" } [lints] workspace = true diff --git a/crates/rust-client/src/builder.rs b/crates/rust-client/src/builder.rs index c629f8a4a1..f1809c20df 100644 --- a/crates/rust-client/src/builder.rs +++ b/crates/rust-client/src/builder.rs @@ -611,3 +611,25 @@ impl ClientBuilder { Ok(self.authenticator(Arc::new(keystore))) } } + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + /// Checks that [`ClientBuilder::rng`] rejects a generator that is not a `CryptoRng`. + /// + /// The driver lives here rather than in `tests/` because `make test` runs `--lib` only. + /// + /// The expected diagnostic is snapshotted in `tests/ui/*.stderr`. It quotes the compiler + /// verbatim, so a `rand` or `rustc` upgrade can reword it; regenerate with + /// `TRYBUILD=overwrite cargo test -p miden-client --features "testing std" --lib ui`. + /// + /// `cargo shear` reports the fixture as an unlinked file and suggests deleting it. That is a + /// false positive: trybuild loads it as data rather than as a cargo target. The warning does + /// not fail `make lint`. + #[test] + fn ui() { + trybuild::TestCases::new().compile_fail("tests/ui/*.rs"); + } +} diff --git a/crates/rust-client/tests/ui/rng_requires_cryptorng.rs b/crates/rust-client/tests/ui/rng_requires_cryptorng.rs new file mode 100644 index 0000000000..514f782d6f --- /dev/null +++ b/crates/rust-client/tests/ui/rng_requires_cryptorng.rs @@ -0,0 +1,45 @@ +//! Do not delete. `cargo shear` reports this file as unlinked because trybuild loads it as +//! data rather than as a cargo target; the test that drives it is `builder::tests::ui`. +//! +//! `ClientBuilder::rng` must reject a generator that is not a `CryptoRng`, even when it +//! implements `FeltRng`. See `Client::secure_rng` for why the bound is there. + +use miden_client::builder::ClientBuilder; +use miden_client::keystore::FilesystemKeyStore; +use miden_protocol::crypto::rand::FeltRng; +use miden_protocol::{Felt, Word}; + +/// A fully predictable generator. It implements `TryRng` (and so `Rng`) and `FeltRng`, but +/// deliberately not `TryCryptoRng`. +struct FixedRng(u64); + +impl rand::TryRng for FixedRng { + type Error = core::convert::Infallible; + + fn try_next_u32(&mut self) -> Result { + Ok(self.0 as u32) + } + + fn try_next_u64(&mut self) -> Result { + Ok(self.0) + } + + fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Self::Error> { + dest.fill(self.0 as u8); + Ok(()) + } +} + +impl FeltRng for FixedRng { + fn draw_element(&mut self) -> Felt { + Felt::new_unchecked(self.0) + } + + fn draw_word(&mut self) -> Word { + Word::new([self.draw_element(); 4]) + } +} + +fn main() { + let _ = ClientBuilder::::new().rng(Box::new(FixedRng(7))); +} diff --git a/crates/rust-client/tests/ui/rng_requires_cryptorng.stderr b/crates/rust-client/tests/ui/rng_requires_cryptorng.stderr new file mode 100644 index 0000000000..990b9751cc --- /dev/null +++ b/crates/rust-client/tests/ui/rng_requires_cryptorng.stderr @@ -0,0 +1,15 @@ +error[E0277]: the trait bound `FixedRng: ClientCryptoRng` is not satisfied + --> tests/ui/rng_requires_cryptorng.rs:44:60 + | +44 | let _ = ClientBuilder::::new().rng(Box::new(FixedRng(7))); + | ^^^^^^^^^^^^^^^^^^^^^ unsatisfied trait bound + | +help: the trait `DerefMut` is not implemented for `FixedRng` + --> tests/ui/rng_requires_cryptorng.rs:14:1 + | +14 | struct FixedRng(u64); + | ^^^^^^^^^^^^^^^ + = note: required for `FixedRng` to implement `TryCryptoRng` + = note: required for `FixedRng` to implement `CryptoRng` + = note: required for `FixedRng` to implement `ClientCryptoRng` + = note: required for the cast from `Box` to `Box<(dyn ClientCryptoRng + 'static)>` From 18cf8d95020c1fc9cac13169db8636bfcd3d10ac Mon Sep 17 00:00:00 2001 From: ricomateo Date: Wed, 19 Aug 2026 11:01:27 -0300 Subject: [PATCH 11/14] chore: remove doc test --- crates/rust-client/src/builder.rs | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/crates/rust-client/src/builder.rs b/crates/rust-client/src/builder.rs index f1809c20df..20f2a7ac72 100644 --- a/crates/rust-client/src/builder.rs +++ b/crates/rust-client/src/builder.rs @@ -338,21 +338,9 @@ where } /// Optionally provide a custom RNG for note serial numbers, script arguments and account - /// seeds. Defaults to an OS-seeded `ChaCha20Rng`. Secret keys and transaction input sealing + /// seeds. Defaults to `ChaCha20Rng`. Secret keys and transaction input sealing /// use a separate, non-overridable generator; see /// [`Client::secure_rng`](crate::Client::secure_rng). - /// - /// The [`CryptoRng`](rand::CryptoRng) bound rejects generators that only implement - /// [`FeltRng`](miden_protocol::crypto::rand::FeltRng), such as `RandomCoin`: - /// - /// ```compile_fail - /// # use miden_client::builder::ClientBuilder; - /// # use miden_client::keystore::FilesystemKeyStore; - /// use miden_protocol::Word; - /// use miden_protocol::crypto::rand::RandomCoin; - /// - /// ClientBuilder::::new().rng(Box::new(RandomCoin::new(Word::default()))); - /// ``` #[must_use] pub fn rng(mut self, rng: ClientRngBox) -> Self { self.rng = Some(rng); From 47b7d75ba99be12471ef5dd06790430dfe257c28 Mon Sep 17 00:00:00 2001 From: ricomateo Date: Wed, 19 Aug 2026 12:24:42 -0300 Subject: [PATCH 12/14] chore(rust-client): silence the cargo-shear warning for trybuild fixtures --- crates/rust-client/Cargo.toml | 3 +++ crates/rust-client/src/builder.rs | 4 ---- crates/rust-client/tests/ui/rng_requires_cryptorng.rs | 3 +-- crates/rust-client/tests/ui/rng_requires_cryptorng.stderr | 8 ++++---- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/crates/rust-client/Cargo.toml b/crates/rust-client/Cargo.toml index f2d10e8a43..1f5eb5f9d2 100644 --- a/crates/rust-client/Cargo.toml +++ b/crates/rust-client/Cargo.toml @@ -24,6 +24,9 @@ ignored = ["getrandom", "prost-types", "tonic-prost"] # getrandom is used through rand's wasm backend selection. The other entries are used by # generated proto bindings and std-only keystore code that shear does not fully resolve. ignored = ["getrandom", "prost-types", "serde", "serde_json", "tempfile", "tonic-prost"] +# trybuild loads the UI fixtures as data rather than as cargo targets, so shear sees them +# as unreachable files and suggests deleting them. +ignored-paths = ["tests/ui/*.rs"] [lib] crate-type = ["lib"] diff --git a/crates/rust-client/src/builder.rs b/crates/rust-client/src/builder.rs index 20f2a7ac72..a60eda72e5 100644 --- a/crates/rust-client/src/builder.rs +++ b/crates/rust-client/src/builder.rs @@ -612,10 +612,6 @@ mod tests { /// The expected diagnostic is snapshotted in `tests/ui/*.stderr`. It quotes the compiler /// verbatim, so a `rand` or `rustc` upgrade can reword it; regenerate with /// `TRYBUILD=overwrite cargo test -p miden-client --features "testing std" --lib ui`. - /// - /// `cargo shear` reports the fixture as an unlinked file and suggests deleting it. That is a - /// false positive: trybuild loads it as data rather than as a cargo target. The warning does - /// not fail `make lint`. #[test] fn ui() { trybuild::TestCases::new().compile_fail("tests/ui/*.rs"); diff --git a/crates/rust-client/tests/ui/rng_requires_cryptorng.rs b/crates/rust-client/tests/ui/rng_requires_cryptorng.rs index 514f782d6f..50e3fbd042 100644 --- a/crates/rust-client/tests/ui/rng_requires_cryptorng.rs +++ b/crates/rust-client/tests/ui/rng_requires_cryptorng.rs @@ -1,5 +1,4 @@ -//! Do not delete. `cargo shear` reports this file as unlinked because trybuild loads it as -//! data rather than as a cargo target; the test that drives it is `builder::tests::ui`. +//! Do not delete: this fixture is driven by the `builder::tests::ui` test. //! //! `ClientBuilder::rng` must reject a generator that is not a `CryptoRng`, even when it //! implements `FeltRng`. See `Client::secure_rng` for why the bound is there. diff --git a/crates/rust-client/tests/ui/rng_requires_cryptorng.stderr b/crates/rust-client/tests/ui/rng_requires_cryptorng.stderr index 990b9751cc..e6c5fd62ac 100644 --- a/crates/rust-client/tests/ui/rng_requires_cryptorng.stderr +++ b/crates/rust-client/tests/ui/rng_requires_cryptorng.stderr @@ -1,13 +1,13 @@ error[E0277]: the trait bound `FixedRng: ClientCryptoRng` is not satisfied - --> tests/ui/rng_requires_cryptorng.rs:44:60 + --> tests/ui/rng_requires_cryptorng.rs:43:60 | -44 | let _ = ClientBuilder::::new().rng(Box::new(FixedRng(7))); +43 | let _ = ClientBuilder::::new().rng(Box::new(FixedRng(7))); | ^^^^^^^^^^^^^^^^^^^^^ unsatisfied trait bound | help: the trait `DerefMut` is not implemented for `FixedRng` - --> tests/ui/rng_requires_cryptorng.rs:14:1 + --> tests/ui/rng_requires_cryptorng.rs:13:1 | -14 | struct FixedRng(u64); +13 | struct FixedRng(u64); | ^^^^^^^^^^^^^^^ = note: required for `FixedRng` to implement `TryCryptoRng` = note: required for `FixedRng` to implement `CryptoRng` From e3b4748adb007c94e5679eced4a8534b18f11afd Mon Sep 17 00:00:00 2001 From: ricomateo Date: Thu, 20 Aug 2026 15:47:50 -0300 Subject: [PATCH 13/14] chore: move draw_felt and draw_word functions to rng.rs --- .../src/tests/custom_transaction.rs | 3 +- crates/rust-client/src/lib.rs | 28 ++----------------- crates/rust-client/src/rng.rs | 22 +++++++++++++++ crates/rust-client/src/transaction/mod.rs | 2 +- .../src/transaction/request/mod.rs | 2 +- .../testing/miden-client-tests/src/tests.rs | 2 +- 6 files changed, 30 insertions(+), 29 deletions(-) create mode 100644 crates/rust-client/src/rng.rs diff --git a/bin/integration-tests/src/tests/custom_transaction.rs b/bin/integration-tests/src/tests/custom_transaction.rs index f79010c486..cb0c7f180f 100644 --- a/bin/integration-tests/src/tests/custom_transaction.rs +++ b/bin/integration-tests/src/tests/custom_transaction.rs @@ -2,7 +2,7 @@ use anyhow::{Context, Result}; use miden_client::account::{AccountId, AccountType}; use miden_client::asset::FungibleAsset; use miden_client::auth::RPO_FALCON_SCHEME_ID; -use miden_client::crypto::{MerkleStore, MerkleTree, NodeIndex, Poseidon2, draw_word}; +use miden_client::crypto::{MerkleStore, MerkleTree, NodeIndex, Poseidon2}; use miden_client::note::{ Note, NoteAssets, @@ -12,6 +12,7 @@ use miden_client::note::{ NoteType, PartialNoteMetadata, }; +use miden_client::rng::draw_word; use miden_client::store::{NoteFilter, TransactionFilter}; use miden_client::testing::common::*; use miden_client::transaction::{ diff --git a/crates/rust-client/src/lib.rs b/crates/rust-client/src/lib.rs index 6711624257..76cd5f642b 100644 --- a/crates/rust-client/src/lib.rs +++ b/crates/rust-client/src/lib.rs @@ -132,6 +132,7 @@ pub mod transaction; pub mod utils; pub mod builder; +pub mod rng; #[cfg(feature = "testing")] mod test_utils; @@ -288,29 +289,6 @@ pub mod crypto { SparseMerklePath, }; pub use miden_protocol::crypto::rand::FeltRng; - - /// Draws a field element uniformly at random from `rng`. - /// - /// Uses rejection sampling: [`Felt::new`](crate::Felt::new) rejects any `u64` at or beyond - /// the field modulus, which keeps the result uniform over the field. The rejection - /// probability is about 2^-32. - pub fn draw_felt(rng: &mut impl rand::Rng) -> crate::Felt { - use rand::RngExt; - - loop { - if let Ok(felt) = crate::Felt::new(rng.random::()) { - return felt; - } - } - } - - /// Draws a [`Word`](crate::Word) uniformly at random from `rng`. - /// - /// Use this for note serial numbers when building notes from a plain [`rand`] generator, which - /// does not implement [`FeltRng`]. - pub fn draw_word(rng: &mut impl rand::Rng) -> crate::Word { - crate::Word::new([draw_felt(rng), draw_felt(rng), draw_felt(rng), draw_felt(rng)]) - } } /// Provides types for working with addresses within the Miden network. @@ -630,11 +608,11 @@ impl TryCryptoRng for ClientRng {} impl FeltRng for ClientRng { fn draw_element(&mut self) -> Felt { - crypto::draw_felt(&mut self.0) + rng::draw_felt(&mut self.0) } fn draw_word(&mut self) -> Word { - crypto::draw_word(&mut self.0) + rng::draw_word(&mut self.0) } } diff --git a/crates/rust-client/src/rng.rs b/crates/rust-client/src/rng.rs new file mode 100644 index 0000000000..ec43b6d0ea --- /dev/null +++ b/crates/rust-client/src/rng.rs @@ -0,0 +1,22 @@ +use rand::RngExt; + +/// Draws a field element uniformly at random from `rng`. +/// +/// Uses rejection sampling: [`Felt::new`](crate::Felt::new) rejects any `u64` at or beyond +/// the field modulus, which keeps the result uniform over the field. The rejection +/// probability is about 2^-32. +pub fn draw_felt(rng: &mut impl rand::Rng) -> crate::Felt { + loop { + if let Ok(felt) = crate::Felt::new(rng.random::()) { + return felt; + } + } +} + +/// Draws a [`Word`](crate::Word) uniformly at random from `rng`. +/// +/// Use this for note serial numbers when building notes from a plain [`rand`] generator, which +/// does not implement [`FeltRng`]. +pub fn draw_word(rng: &mut impl rand::Rng) -> crate::Word { + crate::Word::new([draw_felt(rng), draw_felt(rng), draw_felt(rng), draw_felt(rng)]) +} diff --git a/crates/rust-client/src/transaction/mod.rs b/crates/rust-client/src/transaction/mod.rs index 5e79443e1d..c6880ba832 100644 --- a/crates/rust-client/src/transaction/mod.rs +++ b/crates/rust-client/src/transaction/mod.rs @@ -1444,7 +1444,7 @@ mod tests { }; use crate::ClientError; use crate::auth::AuthSchemeId; - use crate::crypto::draw_word; + use crate::rng::draw_word; use crate::transaction::TransactionRequestError; fn own_note_with_sender(sender: AccountId) -> Note { diff --git a/crates/rust-client/src/transaction/request/mod.rs b/crates/rust-client/src/transaction/request/mod.rs index 005044a051..4a06881432 100644 --- a/crates/rust-client/src/transaction/request/mod.rs +++ b/crates/rust-client/src/transaction/request/mod.rs @@ -614,7 +614,7 @@ mod tests { use rand_chacha::ChaCha20Rng; use super::{TransactionRequest, TransactionRequestBuilder}; - use crate::crypto::draw_word; + use crate::rng::draw_word; use crate::rpc::domain::account::AccountStorageRequirements; use crate::transaction::ForeignAccount; diff --git a/crates/testing/miden-client-tests/src/tests.rs b/crates/testing/miden-client-tests/src/tests.rs index 56ac0b9a01..943d5fd28c 100644 --- a/crates/testing/miden-client-tests/src/tests.rs +++ b/crates/testing/miden-client-tests/src/tests.rs @@ -18,10 +18,10 @@ use miden_client::auth::{ RPO_FALCON_SCHEME_ID, }; use miden_client::builder::ClientBuilder; -use miden_client::crypto::draw_word; use miden_client::keystore::{FilesystemKeyStore, Keystore}; use miden_client::note::{BlockNumber, NetworkAccountTarget, NoteExecutionHint}; use miden_client::pswap::PswapLineageState; +use miden_client::rng::draw_word; use miden_client::rpc::NodeRpcClient; use miden_client::rpc::encryption::TransactionEncryptionKey; use miden_client::store::input_note_states::ConsumedAuthenticatedLocalNoteState; From b36b001e66f8c1438f1a2ddf47d91fa2abacc1d1 Mon Sep 17 00:00:00 2001 From: ricomateo Date: Thu, 20 Aug 2026 16:08:05 -0300 Subject: [PATCH 14/14] chore: fix intra-doc link --- crates/rust-client/src/rng.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rust-client/src/rng.rs b/crates/rust-client/src/rng.rs index ec43b6d0ea..925111412f 100644 --- a/crates/rust-client/src/rng.rs +++ b/crates/rust-client/src/rng.rs @@ -16,7 +16,7 @@ pub fn draw_felt(rng: &mut impl rand::Rng) -> crate::Felt { /// Draws a [`Word`](crate::Word) uniformly at random from `rng`. /// /// Use this for note serial numbers when building notes from a plain [`rand`] generator, which -/// does not implement [`FeltRng`]. +/// does not implement [`FeltRng`](crate::Felt). pub fn draw_word(rng: &mut impl rand::Rng) -> crate::Word { crate::Word::new([draw_felt(rng), draw_felt(rng), draw_felt(rng), draw_felt(rng)]) }