Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
Comment thread
igamigo marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Changes

- Added type signatures where missing throughout the protocol and standards Miden Assembly libraries
- `LocalTransactionProver` now leaves precompile claims deferred for the batch prover to settle, instead of proving them per transaction ([#3851](https://github.com/0xMiden/protocol/pull/3851)).
Comment thread
PhilippGackstatter marked this conversation as resolved.

## v0.17.0-pre.1 (2026-09-05)

Expand Down
5 changes: 1 addition & 4 deletions crates/miden-objects/src/decoded/transaction/batch/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,7 @@ fn proposal_proofs_are_only_checked_by_explicit_verification() {
assert!(
matches!(
error_source::<miden_protocol::errors::ProposedBatchError>(&error),
Some(
miden_protocol::errors::ProposedBatchError::TransactionVerificationFailed { .. }
| miden_protocol::errors::ProposedBatchError::IncompleteTransactionProof { .. }
)
Some(miden_protocol::errors::ProposedBatchError::TransactionVerificationFailed { .. })
),
"{error}"
);
Expand Down
69 changes: 19 additions & 50 deletions crates/miden-protocol/src/batch/proposed_batch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,11 +341,14 @@ impl ProposedBatch {
/// Creates a new [`ProposedBatch`] from the provided parts, verifying every transaction's
/// execution proof against the transaction kernel.
///
/// Transactions whose precompile claims are still outstanding are accepted: verification checks
/// that their deferred witness matches their VM proof, and the batch prover settles the claims
/// of all transactions in the batch with a single precompile proof.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: LocalBatchProver does not yet merge or prove transaction precompile wires, and the batch kernel still ignores transactions. Fine if we intend to merge before this goes in a release, otherwise we could describe this as planned settlement here and in the changelog.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm working on a follow-up PR that I intend to stack on this one that implements merging the precompiles during batch building, so I would skip describing the planned settlement here.

///
/// # Errors
///
/// Returns an error for any of the batch-validation conditions documented on `new_batch_inner`,
/// if a transaction's proof fails to verify or does not meet `proof_security_level`, or if the
/// proof has an outstanding precompile obligation.
/// or if a transaction's proof fails to verify or does not meet `proof_security_level`.
pub fn new(
transactions: Vec<Arc<ProvenTransaction>>,
reference_block_header: BlockHeader,
Expand All @@ -362,17 +365,14 @@ impl ProposedBatch {

let verifier = TransactionVerifier::new(proof_security_level);
for tx in batch.transactions() {
let verification_outcome = verifier.verify(tx).map_err(|source| {
// The outcome may carry an outstanding precompile obligation, which the batch prover
// settles for all transactions at once.
let _verification_outcome = verifier.verify(tx).map_err(|source| {
Comment thread
PhilippGackstatter marked this conversation as resolved.
ProposedBatchError::TransactionVerificationFailed {
transaction_id: tx.id(),
source,
}
})?;
if !verification_outcome.is_complete() {
return Err(ProposedBatchError::IncompleteTransactionProof {
transaction_id: tx.id(),
});
}
}

Ok(batch)
Expand Down Expand Up @@ -511,13 +511,6 @@ impl Deserializable for ProposedBatch {
.map(Arc::new)
.collect::<Vec<Arc<ProvenTransaction>>>();

if let Some(tx) = transactions.iter().find(|tx| !tx.proof().is_complete()) {
return Err(DeserializationError::InvalidValue(format!(
"transaction {} has an outstanding precompile obligation",
tx.id()
)));
}

let block_header = BlockHeader::read_from(source)?;
let partial_blockchain = PartialBlockchain::read_from(source)?;
let unauthenticated_note_proofs =
Expand Down Expand Up @@ -546,9 +539,14 @@ mod tests {
use crate::Word;
use crate::account::{AccountType, AccountUpdateDetails};
use crate::transaction::{InputNoteCommitment, OutputNote, ProvenTransaction, TxAccountUpdate};

#[test]
fn proposed_batch_serialization() -> anyhow::Result<()> {
use crate::vm::ExecutionProof;

/// A proposed batch round-trips whether or not its transactions still owe precompile work,
/// since settling that work is the batch prover's job.
#[rstest::rstest]
#[case::complete(crate::testing::dummy_execution_proof())]
#[case::deferred(crate::testing::dummy_deferred_execution_proof())]
fn proposed_batch_serialization(#[case] proof: ExecutionProof) -> anyhow::Result<()> {
// create partial blockchain with 3 blocks - i.e., 2 peaks
let mut mmr = Mmr::default();
for i in 0..3 {
Expand All @@ -575,7 +573,6 @@ mod tests {
let block_num = reference_block_header.block_num();
let block_ref = reference_block_header.commitment();
let expiration_block_num = reference_block_header.block_num() + 1;
let proof = crate::testing::dummy_execution_proof();

let account_update = TxAccountUpdate::new(
account_id,
Expand All @@ -587,7 +584,7 @@ mod tests {
.context("failed to build account update")?;

let tx = ProvenTransaction::new(
account_update.clone(),
account_update,
Vec::<InputNoteCommitment>::new(),
Vec::<OutputNote>::new(),
block_num,
Expand All @@ -599,8 +596,8 @@ mod tests {

let batch = ProposedBatch::new_unverified(
vec![Arc::new(tx)],
reference_block_header.clone(),
partial_blockchain.clone(),
reference_block_header,
partial_blockchain,
BTreeMap::new(),
)
.context("failed to propose batch")?;
Expand All @@ -620,34 +617,6 @@ mod tests {
assert_eq!(batch.input_notes, batch2.input_notes);
assert_eq!(batch.output_notes, batch2.output_notes);

let tx = ProvenTransaction::new(
account_update,
Vec::<InputNoteCommitment>::new(),
Vec::<OutputNote>::new(),
block_num,
block_ref,
expiration_block_num,
crate::testing::dummy_deferred_execution_proof(),
)
.context("failed to build deferred proven transaction")?;
let transaction_id = tx.id();
let batch = ProposedBatch::new_unverified(
vec![Arc::new(tx)],
reference_block_header,
partial_blockchain,
BTreeMap::new(),
)
.context("failed to propose deferred batch")?;

let error = ProposedBatch::read_from_bytes(&batch.to_bytes()).unwrap_err();
let expected_error =
format!("transaction {transaction_id} has an outstanding precompile obligation");
assert_matches::assert_matches!(
error,
DeserializationError::InvalidValue(message)
if message == expected_error
);

Ok(())
}
}
3 changes: 0 additions & 3 deletions crates/miden-protocol/src/errors/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1351,9 +1351,6 @@ pub enum ProposedBatchError {
source: TransactionVerifierError,
},

#[error("transaction {transaction_id} has an outstanding precompile obligation")]
IncompleteTransactionProof { transaction_id: TransactionId },

#[error(
"transaction batch has {0} input notes but at most {MAX_INPUT_NOTES_PER_BATCH} are allowed"
)]
Expand Down
81 changes: 75 additions & 6 deletions crates/miden-testing/tests/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ mod scripts;
mod standards;
mod wallet;

use miden_protocol::Word;
use std::iter;
use std::sync::Arc;

use miden_protocol::account::AccountId;
use miden_protocol::asset::FungibleAsset;
use miden_protocol::batch::ProposedBatch;
use miden_protocol::crypto::utils::Serializable;
use miden_protocol::errors::TransactionVerifierError;
use miden_protocol::note::{
Expand All @@ -22,17 +25,42 @@ use miden_protocol::note::{
use miden_protocol::testing::account_id::ACCOUNT_ID_SENDER;
use miden_protocol::transaction::{ExecutedTransaction, ProvenTransaction, TransactionVerifier};
use miden_protocol::utils::serde::Deserializable;
#[cfg(test)]
use miden_protocol::vm::VerificationOutcome;
use miden_protocol::{MIN_PROOF_SECURITY_LEVEL, Word};
use miden_standards::code_builder::CodeBuilder;
use miden_testing::{Auth, MockChain};
use miden_tx::{LocalTransactionProver, Prover};
use rstest::rstest;

// HELPER FUNCTIONS
// ================================================================================================

#[cfg(test)]
pub async fn prove_and_verify_transaction(
pub async fn prove_and_verify_transaction_deferred(
executed_transaction: ExecutedTransaction,
) -> Result<(), TransactionVerifierError> {
let (_, outcome) = prove_and_verify_transaction(executed_transaction).await?;
assert!(!outcome.is_complete());
Ok(())
}

/// Proves `executed_transaction` locally, round-trips it and verifies it.
#[cfg(test)]
pub async fn prove_and_verify_transaction_complete(
executed_transaction: ExecutedTransaction,
) -> Result<(), TransactionVerifierError> {
let (_, outcome) = prove_and_verify_transaction(executed_transaction).await?;
assert!(outcome.is_complete());
Ok(())
}

/// Proves `executed_transaction` locally, round-trips it and verifies it, returning the proven
/// transaction together with its verification outcome.
#[cfg(test)]
pub async fn prove_and_verify_transaction(
executed_transaction: ExecutedTransaction,
) -> Result<(ProvenTransaction, VerificationOutcome), TransactionVerifierError> {
use miden_protocol::transaction::TransactionHeader;

let executed_transaction_id = executed_transaction.id();
Expand All @@ -54,10 +82,51 @@ pub async fn prove_and_verify_transaction(
let verifier = TransactionVerifier::new(miden_protocol::MIN_PROOF_SECURITY_LEVEL);

let outcome = verifier.verify(&proven_transaction)?;
assert!(
outcome.is_complete(),
"the local transaction prover must settle precompile work"
);

Ok((proven_transaction, outcome))
}

/// The local prover leaves precompile claims for the batch prover, so a transaction that
/// authenticates with ECDSA verifies while its precompile obligation is still outstanding. Falcon
/// is the control: it verifies in-circuit and uses no precompile, so its proof is complete.
///
/// Both must also pass `ProposedBatch::new`, which verifies the proof of every transaction it
/// batches.
#[rstest]
#[case::ecdsa(Auth::basic_ecdsa(), false)]
#[case::falcon(Auth::basic_falcon(), true)]
#[tokio::test]
async fn prove_and_verify_defers_precompile_claims(
#[case] auth: Auth,
#[case] is_complete: bool,
) -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let account = builder.add_existing_wallet(auth)?;
let note = builder.add_p2any_note(account.id(), NoteType::Public, [])?;
let mock_chain = builder.build()?;

let executed = mock_chain
.build_transaction(account.id())
.authenticated_input_note(note.id())
.build()?
.execute()
.await?;

let (proven_transaction, outcome) = prove_and_verify_transaction(executed).await?;
assert_eq!(outcome.is_complete(), is_complete);

let transactions = vec![Arc::new(proven_transaction)];
let (batch_reference_block, partial_blockchain, unauthenticated_note_proofs) = mock_chain
.get_batch_inputs(transactions.iter().map(|tx| tx.ref_block_num()), iter::empty())?;

let batch = ProposedBatch::new(
transactions,
batch_reference_block,
partial_blockchain,
unauthenticated_note_proofs,
MIN_PROOF_SECURITY_LEVEL,
)?;
assert_eq!(batch.transactions().len(), 1);

Ok(())
}
Expand Down
4 changes: 2 additions & 2 deletions crates/miden-testing/tests/scripts/faucet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ use miden_testing::{
};
use rand::RngExt;

use crate::{get_note_with_fungible_asset_and_script, prove_and_verify_transaction};
use crate::{get_note_with_fungible_asset_and_script, prove_and_verify_transaction_complete};

// Shared test utilities for faucet tests
// ================================================================================================
Expand Down Expand Up @@ -666,7 +666,7 @@ async fn prove_burning_fungible_asset_on_existing_faucet_succeeds() -> anyhow::R
assert_eq!(executed_transaction.input_notes().get_note(0).id(), note.id());

// Prove, serialize/deserialize and verify the transaction
prove_and_verify_transaction(executed_transaction.clone()).await?;
prove_and_verify_transaction_complete(executed_transaction.clone()).await?;

Ok(())
}
Expand Down
6 changes: 2 additions & 4 deletions crates/miden-testing/tests/scripts/fee_sponsorship.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,7 @@ fn setup(reclaim_height: Option<BlockNumber>, reclaimer: Reclaimer) -> anyhow::R
let mut rng = RandomCoin::new(Word::empty());

let mut builder = MockChain::builder();
// The happy-path test proves this account's transaction. Use an auth scheme whose proof does
// not contain settled precompile work.
let network_account = builder.add_existing_wallet(Auth::basic_falcon())?;
let network_account = builder.add_existing_wallet(Auth::basic_ecdsa())?;
let sponsor = builder.add_existing_wallet(Auth::basic_ecdsa())?;
let stranger = builder.add_existing_wallet(Auth::basic_ecdsa())?;

Expand Down Expand Up @@ -151,7 +149,7 @@ async fn network_account_consumes_sponsorship_with_feature_note() -> anyhow::Res
"the network account should receive the sponsored fee",
);

crate::prove_and_verify_transaction(executed).await?;
crate::prove_and_verify_transaction_deferred(executed).await?;

Ok(())
}
Expand Down
8 changes: 5 additions & 3 deletions crates/miden-testing/tests/scripts/p2id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use miden_standards::errors::standards::ERR_P2ID_TARGET_ACCT_MISMATCH;
use miden_standards::note::{P2idNote, P2idNoteStorage};
use miden_testing::{Auth, MockChain, assert_transaction_executor_error};

use crate::prove_and_verify_transaction;
use crate::prove_and_verify_transaction_complete;

/// We test the Pay to script with 2 assets to test the loop inside the script.
/// So we create a note containing two assets that can only be consumed by the target account.
Expand Down Expand Up @@ -142,7 +142,7 @@ async fn prove_consume_note_with_new_account() -> anyhow::Result<()> {
executed_transaction.final_account().to_commitment(),
target_account_after.to_commitment()
);
prove_and_verify_transaction(executed_transaction).await?;
prove_and_verify_transaction_complete(executed_transaction).await?;
Ok(())
}

Expand Down Expand Up @@ -183,7 +183,9 @@ async fn prove_consume_multiple_notes() -> anyhow::Result<()> {
let resulting_asset = account.vault().assets().next().unwrap();
assert_eq!(resulting_asset.unwrap_fungible().amount().as_u64(), 123);

Ok(prove_and_verify_transaction(executed_transaction).await?)
prove_and_verify_transaction_complete(executed_transaction).await?;

Ok(())
}

/// Consumes two existing notes and creates two other notes in the same transaction
Expand Down
8 changes: 4 additions & 4 deletions crates/miden-testing/tests/scripts/swap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use miden_standards::code_builder::CodeBuilder;
use miden_standards::note::P2idNote;
use miden_testing::{Auth, MockChain};

use crate::prove_and_verify_transaction;
use crate::prove_and_verify_transaction_complete;

/// Creates a SWAP note from the transaction script and proves and verifies the transaction.
#[tokio::test]
Expand Down Expand Up @@ -83,7 +83,7 @@ pub async fn prove_send_swap_note() -> anyhow::Result<()> {

let swap_output_note = create_swap_note_tx.output_notes().iter().next().unwrap();
assert_eq!(swap_output_note.assets().iter().next().unwrap(), &offered_asset);
assert!(prove_and_verify_transaction(create_swap_note_tx).await.is_ok());
assert!(prove_and_verify_transaction_complete(create_swap_note_tx).await.is_ok());

Ok(())
}
Expand Down Expand Up @@ -151,11 +151,11 @@ async fn consume_swap_note_private_payback_note() -> anyhow::Result<()> {

assert!(sender_account.vault().assets().any(|asset| asset == requested_asset));

prove_and_verify_transaction(consume_swap_note_tx)
prove_and_verify_transaction_complete(consume_swap_note_tx)
.await
.context("failed to prove/verify consume_swap_note_tx")?;

prove_and_verify_transaction(consume_payback_tx)
prove_and_verify_transaction_complete(consume_payback_tx)
.await
.context("failed to prove/verify consume_payback_tx")?;

Expand Down
3 changes: 3 additions & 0 deletions crates/miden-tx/src/errors/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use miden_protocol::errors::{
use miden_protocol::note::{NoteId, PartialNoteMetadata};
use miden_protocol::transaction::{TransactionEventId, TransactionSummary};
use miden_protocol::{Felt, Word};
use miden_prover::ProverError;
use thiserror::Error;

// NOTE EXECUTION ERROR
Expand Down Expand Up @@ -164,6 +165,8 @@ pub enum TransactionProverError {
// case, the diagnostic is lost if the execution error is not explicitly unwrapped.
#[error("failed to execute transaction kernel program:\n{}", PrintDiagnostic::new(.0))]
TransactionProgramExecutionFailed(ExecutionError),
#[error("failed to generate transaction proof")]
TransactionProofGenerationFailed(#[source] ProverError),
/// Custom error variant for errors not covered by the other variants.
#[error("{error_msg}")]
Other {
Expand Down
Loading
Loading