Skip to content

Commit 7c1f8be

Browse files
authored
feat: add per note consumption checker (#1928)
1 parent b8372fa commit 7c1f8be

10 files changed

Lines changed: 327 additions & 37 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## 0.11.5 (2025-10-01)
4+
5+
- Add new `can_consume` method to the `NoteConsumptionChecker` ([#1928](https://github.com/0xMiden/miden-base/pull/1928)).
6+
37
## 0.11.4 (2025-09-17)
48

59
- Updated `miden-vm` dependencies to `0.17.2` patch version. ([#1905](https://github.com/0xMiden/miden-base/pull/1905))

crates/miden-lib/asm/account_components/multisig_rpo_falcon_512.masm

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,6 @@ use.miden::tx
99

1010
# Auth Request Constants
1111

12-
# The event to request an authentication signature.
13-
const.AUTH_REQUEST=131087
14-
1512
# The event emitted when a signature is not found for a required signer.
1613
const.UNAUTHORIZED_EVENT=131102
1714

crates/miden-lib/asm/miden/auth/rpo_falcon512.masm

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,6 @@ use.std::crypto::dsa::rpo_falcon512
99
# The event to request an authentication signature.
1010
const.AUTH_REQUEST=131087
1111

12-
# The event emitted when a signature is not found for a required signer.
13-
const.UNAUTHORIZED_EVENT=131102
14-
1512
# The slot in this component's storage layout where the public key is stored.
1613
const.PUBLIC_KEY_SLOT=0
1714

@@ -74,8 +71,8 @@ end
7471

7572
#! Verify signatures for all required signers in a loop.
7673
#!
77-
#! This procedure iterates through the required number of signers, fetches their public keys
78-
#! from the provided account storage map slot, verifies their signatures against the transaction message,
74+
#! This procedure iterates through the required number of signers, fetches their public keys from
75+
#! the provided account storage map slot, verifies their signatures against the transaction message,
7976
#! and returns the number of successfully verified signatures.
8077
#!
8178
#! Inputs: [pub_key_slot_idx, num_of_approvers, MSG]

crates/miden-testing/src/kernel_tests/tx/test_account_interface.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ use miden_objects::testing::account_id::{
1414
ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE,
1515
ACCOUNT_ID_SENDER,
1616
};
17+
use miden_objects::transaction::InputNote;
1718
use miden_processor::ExecutionError;
1819
use miden_processor::crypto::RpoRandomCoin;
1920
use miden_tx::auth::UnreachableAuth;
2021
use miden_tx::{
2122
FailedNote,
2223
NoteConsumptionChecker,
2324
NoteConsumptionInfo,
25+
NoteConsumptionStatus,
2426
TransactionExecutor,
2527
TransactionExecutorError,
2628
};
@@ -285,3 +287,46 @@ async fn check_note_consumability_epilogue_failure() -> anyhow::Result<()> {
285287
);
286288
Ok(())
287289
}
290+
291+
#[tokio::test]
292+
async fn test_check_note_consumability_without_signatures() -> anyhow::Result<()> {
293+
let mut builder = MockChain::builder();
294+
295+
// Use basic auth which will cause epilogue failure when paired up with unreachable auth.
296+
let account = builder.add_existing_wallet(Auth::BasicAuth)?;
297+
298+
let successful_note = builder.add_p2id_note(
299+
ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE.try_into().unwrap(),
300+
account.id(),
301+
&[FungibleAsset::mock(10)],
302+
NoteType::Public,
303+
)?;
304+
305+
let mock_chain = builder.build()?;
306+
let notes = vec![successful_note.clone()];
307+
let tx_context = mock_chain
308+
.build_tx_context(TxContextInput::Account(account), &[], &notes)?
309+
.build()?;
310+
311+
let account_id = tx_context.account().id();
312+
let block_ref = tx_context.tx_inputs().block_header().block_num();
313+
let tx_args = tx_context.tx_args().clone();
314+
315+
// Use an auth that fails in order to force an epilogue failure when paired up with basic auth.
316+
let executor =
317+
TransactionExecutor::<'_, '_, _, UnreachableAuth>::new(&tx_context).with_tracing();
318+
let notes_checker = NoteConsumptionChecker::new(&executor);
319+
320+
let consumability_info: NoteConsumptionStatus = notes_checker
321+
.can_consume(
322+
account_id,
323+
block_ref,
324+
InputNote::Unauthenticated { note: successful_note },
325+
tx_args,
326+
)
327+
.await?;
328+
329+
assert_eq!(consumability_info, NoteConsumptionStatus::UnconsumableWithoutAuthorization);
330+
331+
Ok(())
332+
}

crates/miden-testing/tests/auth/multisig.rs

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use assert_matches::assert_matches;
12
use miden_lib::account::wallets::BasicWallet;
23
use miden_lib::errors::tx_kernel_errors::ERR_TX_ALREADY_EXECUTED;
34
use miden_objects::account::{
@@ -19,8 +20,13 @@ use miden_objects::transaction::OutputNote;
1920
use miden_objects::vm::AdviceMap;
2021
use miden_objects::{Felt, Hasher, Word};
2122
use miden_testing::{Auth, MockChainBuilder, assert_transaction_executor_error};
22-
use miden_tx::TransactionExecutorError;
2323
use miden_tx::auth::{BasicAuthenticator, SigningInputs, TransactionAuthenticator};
24+
use miden_tx::{
25+
NoteConsumptionChecker,
26+
NoteConsumptionStatus,
27+
TransactionExecutor,
28+
TransactionExecutorError,
29+
};
2430
use rand::SeedableRng;
2531
use rand_chacha::ChaCha20Rng;
2632

@@ -325,3 +331,102 @@ async fn test_multisig_replay_protection() -> anyhow::Result<()> {
325331

326332
Ok(())
327333
}
334+
335+
#[tokio::test]
336+
async fn test_check_note_consumability_multisig() -> anyhow::Result<()> {
337+
// Setup keys and authenticators
338+
let (_secret_keys, public_keys, authenticators) = setup_keys_and_authenticators(2, 2)?;
339+
340+
// Create multisig account
341+
let multisig_account = create_multisig_account(2, &public_keys, 10)?;
342+
343+
let mut mock_chain_builder =
344+
MockChainBuilder::with_accounts([multisig_account.clone()]).unwrap();
345+
346+
let p2id_note = mock_chain_builder.add_p2id_note(
347+
ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE.try_into().unwrap(),
348+
multisig_account.id(),
349+
&[FungibleAsset::mock(1)],
350+
NoteType::Public,
351+
)?;
352+
353+
let mock_chain = mock_chain_builder.build().unwrap();
354+
355+
let salt = Word::from([Felt::new(1); 4]);
356+
357+
// get the transaction context without signatures
358+
let tx_context_without_signatures = mock_chain
359+
.build_tx_context(multisig_account.id(), &[p2id_note.id()], &[])?
360+
.auth_args(salt)
361+
.build()?;
362+
363+
let block_ref = tx_context_without_signatures.tx_inputs().block_header().block_num();
364+
let tx_args = tx_context_without_signatures.tx_args();
365+
let tx_executor = TransactionExecutor::<'_, '_, _, BasicAuthenticator<ChaCha20Rng>>::new(
366+
&tx_context_without_signatures,
367+
);
368+
369+
let notes_checker = NoteConsumptionChecker::new(&tx_executor);
370+
371+
// this check should return `UnconsumableWithoutAuthorization` variant: the note is consumable,
372+
// but authentication is failing
373+
let unconsumable_without_authorization = notes_checker
374+
.can_consume(
375+
multisig_account.id(),
376+
block_ref,
377+
miden_objects::transaction::InputNote::Unauthenticated { note: p2id_note.clone() },
378+
tx_args.clone(),
379+
)
380+
.await?;
381+
assert_matches!(
382+
unconsumable_without_authorization,
383+
NoteConsumptionStatus::UnconsumableWithoutAuthorization
384+
);
385+
386+
// execute the transaction to get the summary
387+
let tx_summary = match tx_context_without_signatures.execute().await.unwrap_err() {
388+
TransactionExecutorError::Unauthorized(tx_effects) => tx_effects,
389+
error => panic!("expected abort with tx effects: {error:?}"),
390+
};
391+
392+
// Get signatures from both approvers
393+
let msg = tx_summary.as_ref().to_commitment();
394+
let tx_summary = SigningInputs::TransactionSummary(tx_summary);
395+
396+
let sig_1 = authenticators[0].get_signature(public_keys[0].into(), &tx_summary).await?;
397+
let sig_2 = authenticators[1].get_signature(public_keys[1].into(), &tx_summary).await?;
398+
399+
// Populate advice map with signatures
400+
let mut advice_map = AdviceMap::default();
401+
advice_map.insert(Hasher::merge(&[public_keys[0].into(), msg]), sig_1);
402+
advice_map.insert(Hasher::merge(&[public_keys[1].into(), msg]), sig_2);
403+
404+
// get the transaction context with signatures
405+
let tx_context_with_signatures = mock_chain
406+
.build_tx_context(multisig_account.id(), &[p2id_note.id()], &[])?
407+
.extend_expected_output_notes(vec![OutputNote::Full(p2id_note)])
408+
.extend_advice_map(advice_map.iter().map(|(k, v)| (*k, v.to_vec())))
409+
.auth_args(salt)
410+
.build()?;
411+
412+
let block_num = tx_context_with_signatures.tx_inputs().block_header().block_num();
413+
let notes = tx_context_with_signatures.tx_inputs().input_notes().clone();
414+
let tx_args = tx_context_with_signatures.tx_args().clone();
415+
416+
let mut tx_executor = TransactionExecutor::new(&tx_context_with_signatures)
417+
.with_source_manager(tx_context_with_signatures.source_manager());
418+
if let Some(authenticator) = tx_context_with_signatures.authenticator() {
419+
tx_executor = tx_executor.with_authenticator(authenticator);
420+
}
421+
422+
let notes_checker = NoteConsumptionChecker::new(&tx_executor);
423+
424+
// this check should return `Consumable` variant: we provided the signatures, so the transaction
425+
// should execute successfully.
426+
let consumable_with_authorization = notes_checker
427+
.can_consume(multisig_account.id(), block_num, notes.get_note(0).clone(), tx_args)
428+
.await?;
429+
assert_matches!(consumable_with_authorization, NoteConsumptionStatus::Consumable);
430+
431+
Ok(())
432+
}

crates/miden-testing/tests/auth/rpo_falcon_acl.rs

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ use core::slice;
33
use assert_matches::assert_matches;
44
use miden_lib::testing::account_component::MockAccountComponent;
55
use miden_lib::testing::note::NoteBuilder;
6-
use miden_lib::transaction::TransactionKernelError;
76
use miden_lib::utils::ScriptBuilder;
87
use miden_objects::account::{
98
AccountBuilder,
@@ -16,7 +15,6 @@ use miden_objects::account::{
1615
use miden_objects::testing::account_id::ACCOUNT_ID_SENDER;
1716
use miden_objects::transaction::OutputNote;
1817
use miden_objects::{Felt, FieldElement, Word};
19-
use miden_processor::ExecutionError;
2018
use miden_testing::{Auth, MockChain};
2119
use miden_tx::TransactionExecutorError;
2220

@@ -164,14 +162,7 @@ fn test_rpo_falcon_acl() -> anyhow::Result<()> {
164162

165163
let executed_tx_no_auth = tx_context_no_auth.execute_blocking();
166164

167-
assert_matches!(executed_tx_no_auth, Err(TransactionExecutorError::TransactionProgramExecutionFailed(
168-
execution_error
169-
)) => {
170-
assert_matches!(execution_error, ExecutionError::EventError { error, .. } => {
171-
let kernel_error = error.downcast_ref::<TransactionKernelError>().unwrap();
172-
assert_matches!(kernel_error, TransactionKernelError::MissingAuthenticator);
173-
})
174-
});
165+
assert_matches!(executed_tx_no_auth, Err(TransactionExecutorError::MissingAuthenticator));
175166

176167
// Test 4: Transaction WITHOUT authenticator calling non-trigger procedure (should succeed)
177168
let tx_context_no_trigger = mock_chain
@@ -256,14 +247,7 @@ fn test_rpo_falcon_acl_with_disallow_unauthorized_input_notes() -> anyhow::Resul
256247

257248
// This should fail with MissingAuthenticator error because input notes are being consumed
258249
// and allow_unauthorized_input_notes is false
259-
assert_matches!(executed_tx_no_auth, Err(TransactionExecutorError::TransactionProgramExecutionFailed(
260-
execution_error
261-
)) => {
262-
assert_matches!(execution_error, ExecutionError::EventError { error, .. } => {
263-
let kernel_error = error.downcast_ref::<TransactionKernelError>().unwrap();
264-
assert_matches!(kernel_error, TransactionKernelError::MissingAuthenticator);
265-
})
266-
});
250+
assert_matches!(executed_tx_no_auth, Err(TransactionExecutorError::MissingAuthenticator));
267251

268252
Ok(())
269253
}

crates/miden-tx/src/errors/mod.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,42 @@ pub enum NoteCheckerError {
3838
failed_note_index: usize,
3939
error: TransactionExecutorError,
4040
},
41+
42+
// new variants were created instead of modifying existing ones do decrease the number of
43+
// merge conflicts during the next release
44+
#[error("transaction preparation failed: {0}")]
45+
TransactionPreparation(#[source] TransactionExecutorError),
46+
#[error("transaction execution prologue failed: {0}")]
47+
PrologueExecution(#[source] TransactionExecutorError),
48+
}
49+
50+
// TRANSACTION CHECKER ERROR
51+
// ================================================================================================
52+
53+
#[derive(Debug, Error)]
54+
pub(crate) enum TransactionCheckerError {
55+
#[error("transaction preparation failed: {0}")]
56+
TransactionPreparation(#[source] TransactionExecutorError),
57+
#[error("transaction execution prologue failed: {0}")]
58+
PrologueExecution(#[source] TransactionExecutorError),
59+
#[error("transaction execution epilogue failed: {0}")]
60+
EpilogueExecution(#[source] TransactionExecutorError),
61+
#[error("transaction note execution failed on note index {failed_note_index}: {error}")]
62+
NoteExecution {
63+
failed_note_index: usize,
64+
error: TransactionExecutorError,
65+
},
66+
}
67+
68+
impl From<TransactionCheckerError> for TransactionExecutorError {
69+
fn from(error: TransactionCheckerError) -> Self {
70+
match error {
71+
TransactionCheckerError::TransactionPreparation(error) => error,
72+
TransactionCheckerError::PrologueExecution(error) => error,
73+
TransactionCheckerError::EpilogueExecution(error) => error,
74+
TransactionCheckerError::NoteExecution { error, .. } => error,
75+
}
76+
}
4177
}
4278

4379
// TRANSACTION EXECUTOR ERROR
@@ -101,6 +137,10 @@ pub enum TransactionExecutorError {
101137
// It is boxed to avoid triggering clippy::result_large_err for functions that return this type.
102138
#[error("transaction is unauthorized with summary {0:?}")]
103139
Unauthorized(Box<TransactionSummary>),
140+
#[error(
141+
"failed to respond to signature requested since no authenticator is assigned to the host"
142+
)]
143+
MissingAuthenticator,
104144
}
105145

106146
// TRANSACTION PROVER ERROR

crates/miden-tx/src/executor/mod.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,12 @@ mod data_store;
3535
pub use data_store::DataStore;
3636

3737
mod notes_checker;
38-
pub use notes_checker::{FailedNote, NoteConsumptionChecker, NoteConsumptionInfo};
38+
pub use notes_checker::{
39+
FailedNote,
40+
NoteConsumptionChecker,
41+
NoteConsumptionInfo,
42+
NoteConsumptionStatus,
43+
};
3944

4045
// TRANSACTION EXECUTOR
4146
// ================================================================================================
@@ -457,6 +462,9 @@ fn map_execution_error(exec_err: ExecutionError) -> TransactionExecutorError {
457462
tx_fee: *tx_fee,
458463
}
459464
},
465+
Some(TransactionKernelError::MissingAuthenticator) => {
466+
TransactionExecutorError::MissingAuthenticator
467+
},
460468
_ => TransactionExecutorError::TransactionProgramExecutionFailed(exec_err),
461469
}
462470
},

0 commit comments

Comments
 (0)