Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
* [BREAKING] Removed `toBech32Custom` and implemented custom id conversion for wasm derived class `NetworkId` ([#1612](https://github.com/0xMiden/miden-client/pull/1612)).
* [BREAKING] Remove `SecretKey` model and consolidated functionality into `AuthSecretKey` ([#1592](https://github.com/0xMiden/miden-client/issues/1380))
* Incremented the limits for various RPC calls to accommodate larger data sets ([#1621](https://github.com/0xMiden/miden-client/pull/1621)).
* [BREAKING] Replace `TransactionRequestBuilder::unauthenticated_input_notes` & `TransactionRequestBuilder::authenticated_input_notes` for `TransactionRequestBuilder::input_notes`, now the user passes a list of notes which the `Client` itself determines the authentication status of ([#1624](https://github.com/0xMiden/miden-client/issues/1624)).

## 0.12.5 (2025-12-01)

Expand Down
31 changes: 21 additions & 10 deletions bin/integration-tests/src/tests/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,7 @@ pub async fn test_sync_detail_values(client_config: ClientConfig) -> Result<()>
NoteType::Public,
client1.rng(),
)?;
let note_id = tx_request.expected_output_own_notes().pop().unwrap().id();
let note = tx_request.expected_output_own_notes().pop().unwrap();
execute_tx_and_sync(&mut client1, from_account_id, tx_request).await?;

// Second client sync should have new note
Expand All @@ -495,7 +495,7 @@ pub async fn test_sync_detail_values(client_config: ClientConfig) -> Result<()>
assert_eq!(new_details.updated_accounts.len(), 0);

// Consume the note with the second account
let tx_request = TransactionRequestBuilder::new().build_consume_notes(vec![note_id]).unwrap();
let tx_request = TransactionRequestBuilder::new().build_consume_notes(vec![note]).unwrap();
execute_tx_and_sync(&mut client2, to_account_id, tx_request).await?;

// First client sync should have a new nullifier as the note was consumed
Expand Down Expand Up @@ -715,11 +715,11 @@ pub async fn test_consume_multiple_expected_notes(client_config: ClientConfig) -

// Create and execute transactions
let tx_request_1 = TransactionRequestBuilder::new()
.authenticated_input_notes(client_owned_notes.iter().map(|note| (note.id(), None)))
.input_notes(client_owned_notes.iter().map(|note| (note.clone(), None)))
.build()?;

let tx_request_2 = TransactionRequestBuilder::new()
.unauthenticated_input_notes(unauth_owned_notes.iter().map(|note| ((*note).clone(), None)))
.input_notes(unauth_owned_notes.iter().map(|note| ((*note).clone(), None)))
.build()?;

let tx_id_1 = client.submit_new_transaction(to_account_ids[0], tx_request_1).await.unwrap();
Expand Down Expand Up @@ -816,7 +816,9 @@ pub async fn test_import_consumed_note_with_proof(client_config: ClientConfig) -
// Consume the note with the sender account

println!("Consuming Note...");
let tx_request = TransactionRequestBuilder::new().build_consume_notes(vec![note.id()]).unwrap();
let tx_request = TransactionRequestBuilder::new()
.build_consume_notes(vec![note.clone().try_into().unwrap()])
.unwrap();
execute_tx_and_sync(&mut client_1, from_account_id, tx_request).await?;

// Import the consumed note
Expand Down Expand Up @@ -881,7 +883,9 @@ pub async fn test_import_consumed_note_with_id(client_config: ClientConfig) -> R
// Consume the note with the sender account

println!("Consuming Note...");
let tx_request = TransactionRequestBuilder::new().build_consume_notes(vec![note.id()]).unwrap();
let tx_request = TransactionRequestBuilder::new()
.build_consume_notes(vec![note.clone().try_into().unwrap()])
.unwrap();
execute_tx_and_sync(&mut client_1, from_account_id, tx_request).await?;
client_2.sync_state().await.unwrap();

Expand Down Expand Up @@ -1012,7 +1016,9 @@ pub async fn test_discarded_transaction(client_config: ClientConfig) -> Result<(
.clone();

println!("Consuming Note...");
let tx_request = TransactionRequestBuilder::new().build_consume_notes(vec![note.id()]).unwrap();
let tx_request = TransactionRequestBuilder::new()
.build_consume_notes(vec![note.clone().try_into().unwrap()])
.unwrap();

// Consume the note in client 1 but dont submit it to the node
let transaction_result =
Expand Down Expand Up @@ -1390,10 +1396,10 @@ pub async fn test_unused_rpc_api(client_config: ClientConfig) -> Result<()> {
NoteType::Public,
client.rng(),
)?;
let note_id = tx_request.expected_output_own_notes().pop().unwrap().id();
let note = tx_request.expected_output_own_notes().pop().unwrap();
execute_tx_and_sync(&mut client, fungible_asset.faucet_id(), tx_request.clone()).await?;

let tx_request = TransactionRequestBuilder::new().build_consume_notes(vec![note_id])?;
let tx_request = TransactionRequestBuilder::new().build_consume_notes(vec![note.clone()])?;
execute_tx_and_sync(&mut client, first_basic_account.id(), tx_request).await?;

let nullifier = note.nullifier();
Expand Down Expand Up @@ -1483,7 +1489,12 @@ pub async fn test_ignore_invalid_notes(client_config: ClientConfig) -> Result<()
// Create a transaction to consume all 4 notes but ignore the invalid ones
let tx_request = TransactionRequestBuilder::new()
.ignore_invalid_input_notes()
.build_consume_notes(vec![note_1.id(), note_3.id(), note_2.id(), note_4.id()])?;
.build_consume_notes(vec![
note_1.clone(),
note_3.clone(),
note_2.clone(),
note_4.clone(),
])?;

execute_tx_and_sync(&mut client, account_id, tx_request).await?;

Expand Down
6 changes: 3 additions & 3 deletions bin/integration-tests/src/tests/custom_transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ pub async fn test_transaction_request(client_config: ClientConfig) -> Result<()>

// FAILURE ATTEMPT
let transaction_request = TransactionRequestBuilder::new()
.unauthenticated_input_notes(note_args_map.clone())
.input_notes(note_args_map.clone())
.custom_script(tx_script.clone())
.script_arg(Word::empty())
.extend_advice_map(advice_map.clone())
Expand All @@ -120,7 +120,7 @@ pub async fn test_transaction_request(client_config: ClientConfig) -> Result<()>

// SUCCESS EXECUTION
let transaction_request = TransactionRequestBuilder::new()
.unauthenticated_input_notes(note_args_map)
.input_notes(note_args_map)
.custom_script(tx_script)
.script_arg([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)].into())
.extend_advice_map(advice_map)
Expand Down Expand Up @@ -239,7 +239,7 @@ pub async fn test_merkle_store(client_config: ClientConfig) -> Result<()> {
let tx_script = client.script_builder().compile_tx_script(&code)?;

let transaction_request = TransactionRequestBuilder::new()
.unauthenticated_input_notes(note_args_map)
.input_notes(note_args_map)
.custom_script(tx_script)
.extend_advice_map(advice_map)
.extend_merkle_store(merkle_store.inner_nodes())
Expand Down
2 changes: 1 addition & 1 deletion bin/integration-tests/src/tests/network_transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ pub async fn test_recall_note_before_ntx_consumes_it(client_config: ClientConfig
client.apply_transaction(&bump_result, current_height).await?;

let tx_request = TransactionRequestBuilder::new()
.unauthenticated_input_notes(vec![(network_note, None)])
.input_notes(vec![(network_note, None)])
.build()?;

let consume_result = client.execute_transaction(native_account.id(), tx_request).await?;
Expand Down
5 changes: 3 additions & 2 deletions bin/integration-tests/src/tests/onchain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ pub async fn test_onchain_notes_flow(client_config: ClientConfig) -> Result<()>
.clone();
execute_tx_and_sync(&mut client_2, basic_wallet_1.id(), tx_request).await?;

let tx_request = TransactionRequestBuilder::new().build_consume_notes(vec![note.id()])?;
let tx_request = TransactionRequestBuilder::new().build_consume_notes(vec![note.clone()])?;
execute_tx_and_sync(&mut client_2, basic_wallet_1.id(), tx_request).await?;

// sync client 3 (basic account 2)
Expand Down Expand Up @@ -301,7 +301,8 @@ pub async fn test_onchain_accounts(client_config: ClientConfig) -> Result<()> {

// Consume the note
println!("Consuming note on second client...");
let tx_request = TransactionRequestBuilder::new().build_consume_notes(vec![notes[0].id()])?;
let tx_request = TransactionRequestBuilder::new()
.build_consume_notes(vec![notes[0].clone().try_into().unwrap()])?;
execute_tx_and_sync(&mut client_2, to_account_id, tx_request).await?;

// sync on first client
Expand Down
32 changes: 24 additions & 8 deletions bin/integration-tests/src/tests/swap_transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,17 +118,25 @@ pub async fn test_swap_fully_onchain(client_config: ClientConfig) -> Result<()>
client2.sync_state().await?;
println!("Consuming swap note on second client...");

let tx_request = TransactionRequestBuilder::new()
.build_consume_notes(vec![expected_output_notes[0].id()])?;
let note = client2
.get_input_note(expected_output_notes[0].id())
.await?
.unwrap()
.try_into()?;
let tx_request = TransactionRequestBuilder::new().build_consume_notes(vec![note])?;
Comment on lines +121 to +126
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned in #1112 (comment), this does make this scenario a bit more inconvenient. I guess we could add some other way of expressing notes via note IDs but maybe this defeats the original purpose. Nothing to do for now but just making a note. Would be nice to know @mmagician's opinion on this.

Copy link
Collaborator Author

@juan518munoz juan518munoz Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One possibility I can think of is wrapping the input notes the TransactionRequestBuilder receives in some sort of enum:

enum InputNote {
    /// InputNote to be used is assumed to be present
    /// on the store.
    Stored(NoteId)
    /// InputNote provided fully. May still be present
    /// on the store.
    Provided(Note)
}

#[derive(Clone, Debug)]
pub struct TransactionRequestBuilder {
    /// Notes to be consumed by the transaction.
    input_notes: Vec<InputNote>,
    ...
}

This way we can still take just NoteIds from the user, and fail in the case where the requested note is not present in our store.

What do you think?

Copy link
Collaborator Author

@juan518munoz juan518munoz Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following on the proposed enum, if we keep the current procedure of passing all full Notes regardless of if they are authenticated or not, we have a possible security flaw:

  1. NoteA with noteId 123 is on the client store as an authenticated note.
  2. Alice passes NoteB with noteId 123 as an input note.
  3. Client assumes that the note is authenticated, as the id is already present on the store.

The current implementation does not have this vulnerability, as we are using the notes retrieved from the store:

// Retrieve all input notes from the store.
let mut authenticated_note_records = self
    .store
    .get_input_notes(NoteFilter::List(transaction_request.get_input_note_ids()))
    .await?;

// Verify that none of the authenticated input notes are already consumed.
for note in authenticated_note_records.iter() {
    if note.is_consumed() {
        return Err(ClientError::TransactionRequestError(TransactionRequestError::InputNoteAlreadyConsumed(note.id())));
    }
}

// Only keep authenticated input notes from the store.
authenticated_note_records.retain(InputNoteRecord::is_authenticated);

But a future "optimization" might use the provided notes instead.

execute_tx_and_sync(&mut client2, account_b.id(), tx_request).await?;

// sync on client 1, we should get the missing payback note details.
// try consuming the received note with accountA, it should now have 25 ETH
client1.sync_state().await?;
println!("Consuming swap payback note on first client...");

let tx_request = TransactionRequestBuilder::new()
.build_consume_notes(vec![expected_payback_note_details[0].id()])?;
let note = client1
.get_input_note(expected_payback_note_details[0].id())
.await?
.unwrap()
.try_into()?;
let tx_request = TransactionRequestBuilder::new().build_consume_notes(vec![note])?;
execute_tx_and_sync(&mut client1, account_a.id(), tx_request).await?;

// At the end we should end up with
Expand Down Expand Up @@ -322,17 +330,25 @@ pub async fn test_swap_private(client_config: ClientConfig) -> Result<()> {
// consume swap note with accountB, and check that the vault changed appropriately
println!("Consuming swap note on second client...");

let tx_request = TransactionRequestBuilder::new()
.build_consume_notes(vec![expected_output_notes[0].id()])?;
let note = client2
.get_input_note(expected_output_notes[0].id())
.await?
.unwrap()
.try_into()?;
let tx_request = TransactionRequestBuilder::new().build_consume_notes(vec![note])?;
execute_tx_and_sync(&mut client2, account_b.id(), tx_request).await?;

// sync on client 1, we should get the missing payback note details.
// try consuming the received note with accountA, it should now have 25 ETH
client1.sync_state().await?;
println!("Consuming swap payback note on first client...");

let tx_request = TransactionRequestBuilder::new()
.build_consume_notes(vec![expected_payback_note_details[0].id()])?;
let note = client1
.get_input_note(expected_payback_note_details[0].id())
.await?
.unwrap()
.try_into()?;
let tx_request = TransactionRequestBuilder::new().build_consume_notes(vec![note])?;
execute_tx_and_sync(&mut client1, account_a.id(), tx_request).await?;

// At the end we should end up with
Expand Down
37 changes: 21 additions & 16 deletions bin/miden-cli/src/commands/new_transactions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ impl ConsumeNotesCmd {
let force = self.force;

let mut authenticated_notes = Vec::new();
let mut unauthenticated_notes = Vec::new();
let mut input_notes = Vec::new();

for note_id in &self.list_of_notes {
let note_record = get_input_note_with_id_prefix(&client, note_id)
Expand All @@ -320,17 +320,14 @@ impl ConsumeNotesCmd {

if note_record.is_authenticated() {
authenticated_notes.push(note_record.id());
} else {
unauthenticated_notes.push((
note_record.try_into().map_err(|err: NoteRecordError| {
CliError::Transaction(
err.into(),
"Failed to convert note record".to_string(),
)
})?,
None,
));
}

input_notes.push((
note_record.try_into().map_err(|err: NoteRecordError| {
CliError::Transaction(err.into(), "Failed to convert note record".to_string())
})?,
None,
));
}

let account_id =
Expand All @@ -339,20 +336,28 @@ impl ConsumeNotesCmd {
if authenticated_notes.is_empty() {
info!("No input note IDs provided, getting all notes consumable by {}", account_id);
let consumable_notes = client.get_consumable_notes(Some(account_id)).await?;

authenticated_notes.extend(consumable_notes.iter().map(|(note, _)| note.id()));
for (note_record, _) in consumable_notes {
input_notes.push((
note_record.try_into().map_err(|err: NoteRecordError| {
CliError::Transaction(
err.into(),
"Failed to convert note record".to_string(),
)
})?,
None,
));
}
}

if authenticated_notes.is_empty() && unauthenticated_notes.is_empty() {
if authenticated_notes.is_empty() && input_notes.is_empty() {
return Err(CliError::Transaction(
"No input notes were provided and the store does not contain any notes consumable by {account_id}".into(),
"Input notes check failed".to_string(),
));
}

let transaction_request = TransactionRequestBuilder::new()
.authenticated_input_notes(authenticated_notes.into_iter().map(|id| (id, None)))
.unauthenticated_input_notes(unauthenticated_notes)
.input_notes(input_notes)
.build()
.map_err(|err| {
CliError::Transaction(
Expand Down
8 changes: 0 additions & 8 deletions crates/rust-client/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,14 +190,6 @@ impl ClientError {
impl From<&TransactionRequestError> for Option<ErrorHint> {
fn from(err: &TransactionRequestError) -> Self {
match err {
TransactionRequestError::MissingAuthenticatedInputNote(note_id) => {
Some(ErrorHint {
message: format!(
"Note {note_id} was listed via `TransactionRequestBuilder::authenticated_input_notes(...)`, but the store lacks an authenticated `InputNoteRecord`. Import or sync the note so its record and authentication data are available before executing the request."
),
docs_url: Some(TROUBLESHOOTING_DOC),
})
},
TransactionRequestError::NoInputNotesNorAccountChange => Some(ErrorHint {
message: "Transactions must consume input notes or mutate tracked account state. Add at least one authenticated/unauthenticated input note or include an explicit account state update in the request.".to_string(),
docs_url: Some(TROUBLESHOOTING_DOC),
Expand Down
2 changes: 1 addition & 1 deletion crates/rust-client/src/note/note_screener.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ where
note: &Note,
) -> Result<Option<NoteRelevance>, NoteScreenerError> {
let transaction_request =
TransactionRequestBuilder::new().build_consume_notes(vec![note.id()])?;
TransactionRequestBuilder::new().build_consume_notes(vec![note.clone()])?;

let tx_script = transaction_request.build_transaction_script(
&AccountInterface::from(account),
Expand Down
15 changes: 6 additions & 9 deletions crates/rust-client/src/test_utils/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use anyhow::{Context, Result};
use miden_objects::account::auth::AuthSecretKey;
use miden_objects::account::{Account, AccountId, AccountStorageMode};
use miden_objects::asset::{Asset, FungibleAsset, TokenSymbol};
use miden_objects::note::{NoteId, NoteType};
use miden_objects::note::NoteType;
use miden_objects::transaction::{OutputNote, TransactionId};
use miden_objects::{Felt, FieldElement};
use rand::RngCore;
Expand Down Expand Up @@ -427,7 +427,7 @@ pub async fn consume_notes(
) -> TransactionId {
println!("Consuming Note...");
let tx_request = TransactionRequestBuilder::new()
.build_consume_notes(input_notes.iter().map(Note::id).collect())
.build_consume_notes(input_notes.to_vec())
.unwrap();
Box::pin(client.submit_new_transaction(account_id, tx_request)).await.unwrap()
}
Expand Down Expand Up @@ -457,22 +457,22 @@ pub async fn assert_account_has_single_asset(
pub async fn assert_note_cannot_be_consumed_twice(
client: &mut TestClient,
consuming_account_id: AccountId,
note_to_consume_id: NoteId,
note_to_consume: Note,
) {
// Check that we can't consume the P2ID note again
println!("Consuming Note...");

// Double-spend error expected to be received since we are consuming the same note
let tx_request = TransactionRequestBuilder::new()
.build_consume_notes(vec![note_to_consume_id])
.build_consume_notes(vec![note_to_consume.clone()])
.unwrap();

match Box::pin(client.submit_new_transaction(consuming_account_id, tx_request)).await {
Err(ClientError::TransactionRequestError(
TransactionRequestError::InputNoteAlreadyConsumed(_),
)) => {},
Ok(_) => panic!("Double-spend error: Note should not be consumable!"),
err => panic!("Unexpected error {:?} for note ID: {}", err, note_to_consume_id.to_hex()),
err => panic!("Unexpected error {:?} for note ID: {}", err, note_to_consume.id().to_hex()),
}
}

Expand Down Expand Up @@ -519,10 +519,7 @@ pub async fn execute_tx_and_consume_output_notes(

Box::pin(client.submit_new_transaction(executor, tx_request)).await.unwrap();

let tx_request = TransactionRequestBuilder::new()
.unauthenticated_input_notes(output_notes)
.build()
.unwrap();
let tx_request = TransactionRequestBuilder::new().input_notes(output_notes).build().unwrap();
Box::pin(client.submit_new_transaction(consumer, tx_request)).await.unwrap()
}

Expand Down
Loading