Skip to main content

miden_objects/decoded/transaction/
batch.rs

1use miden_protobuf::unwrap_infallible;
2pub use proto::transaction::DecodedBatchAccountUpdate as BatchAccountUpdate;
3
4use crate::decoded::VerificationError;
5use crate::{BuildUnchecked, Verify, proto};
6
7#[cfg(test)]
8mod tests;
9
10impl Verify for BatchAccountUpdate {
11    type Verified = miden_protocol::batch::BatchAccountUpdate;
12    type Error = VerificationError;
13    fn verify(self) -> Result<Self::Verified, Self::Error> {
14        Ok(Self::Verified::new(
15            self.account_id.verify()?,
16            self.initial_state_commitment,
17            self.final_state_commitment,
18            self.details.verify()?,
19        )?)
20    }
21}
22
23pub use proto::transaction::DecodedProposedBatch as ProposedBatch;
24
25/// Verifies transaction proofs and batch consistency, not trust in the supplied reference chain.
26impl crate::VerifyWith<u32> for ProposedBatch {
27    type Verified = miden_protocol::batch::ProposedBatch;
28    type Error = VerificationError;
29    fn verify_with(self, proof_security_level: u32) -> Result<Self::Verified, Self::Error> {
30        let transactions = self
31            .transactions
32            .into_iter()
33            .map(|tx| tx.build_unchecked().map(alloc::sync::Arc::new))
34            .collect::<Result<_, _>>()?;
35        let header = self.reference_block_header.build_unchecked()?;
36        let chain = self.partial_blockchain.build_unchecked()?;
37        let mut proofs = alloc::collections::BTreeMap::new();
38        let mut previous = None;
39        for proof in self.unauthenticated_note_proofs {
40            let (id, proof) = proof.verify()?;
41            if previous.is_some_and(|previous| id <= previous) {
42                return Err(ProposedBatchError::ProofOrder.into());
43            }
44            previous = Some(id);
45            proofs.insert(id, proof);
46        }
47        Ok(Self::Verified::new(transactions, header, chain, proofs, proof_security_level)?)
48    }
49}
50
51#[derive(Debug, thiserror::Error)]
52pub enum ProposedBatchError {
53    #[error("unauthenticated note proofs must have unique, ascending note IDs")]
54    ProofOrder,
55}
56
57pub use proto::transaction::DecodedProvenBatch as ProvenBatch;
58
59/// Checks local batch invariants, but not proof validity, note aggregation, or transaction
60/// ordering.
61impl crate::BuildUnchecked for ProvenBatch {
62    type Output = miden_protocol::batch::ProvenBatch;
63    type Error = VerificationError;
64    fn build_unchecked(self) -> Result<Self::Output, Self::Error> {
65        let mut previous = None;
66        let mut updates = alloc::vec::Vec::new();
67        for update in self.account_updates {
68            let update = update.verify()?;
69            if previous.is_some_and(|previous| update.account_id() <= previous) {
70                return Err(ProvenBatchError::AccountOrder.into());
71            }
72            previous = Some(update.account_id());
73            updates.push(update);
74        }
75        let inputs = self
76            .input_notes
77            .into_iter()
78            .map(BuildUnchecked::build_unchecked)
79            .collect::<Result<_, _>>()?;
80        let outputs =
81            self.output_notes.into_iter().map(Verify::verify).collect::<Result<_, _>>()?;
82        let transactions = self
83            .transactions
84            .into_iter()
85            .map(BuildUnchecked::build_unchecked)
86            .collect::<Result<_, _>>()?;
87        Ok(Self::Output::new(
88            self.reference_block_commitment,
89            unwrap_infallible(self.reference_block_num.verify()),
90            updates,
91            miden_protocol::transaction::InputNotes::new_unchecked(inputs),
92            outputs,
93            unwrap_infallible(self.expiration_block_num.verify()),
94            miden_protocol::transaction::OrderedTransactionHeaders::new_unchecked(transactions),
95            self.proof,
96        )?)
97    }
98}
99
100#[derive(Debug, thiserror::Error)]
101pub enum ProvenBatchError {
102    #[error("account updates must have unique, ascending account IDs")]
103    AccountOrder,
104    #[error("{0} does not match proposal")]
105    ProposalMismatch(&'static str),
106}
107
108/// Checks all fields duplicated from an already-verified proposal. The batch execution proof
109/// still needs verification by the consuming service; this only establishes proposal agreement.
110impl crate::VerifyWith<&miden_protocol::batch::ProposedBatch> for ProvenBatch {
111    type Verified = miden_protocol::batch::ProvenBatch;
112    type Error = VerificationError;
113    fn verify_with(
114        self,
115        proposed: &miden_protocol::batch::ProposedBatch,
116    ) -> Result<Self::Verified, Self::Error> {
117        let batch = self.build_unchecked()?;
118        let header = proposed.reference_block_header();
119        let mismatch = if batch.reference_block_num() != header.block_num() {
120            Some("reference block number")
121        } else if batch.reference_block_commitment() != header.commitment() {
122            Some("reference block commitment")
123        } else if batch.account_updates() != proposed.account_updates() {
124            Some("account updates")
125        } else if !batch.input_notes().iter().eq(proposed.input_notes().iter()) {
126            Some("input notes")
127        } else if batch.output_notes() != proposed.output_notes() {
128            Some("output notes")
129        } else if batch.batch_expiration_block_num() != proposed.batch_expiration_block_num() {
130            Some("expiration block")
131        } else if batch.transactions().as_slice() != proposed.transaction_headers().as_slice() {
132            Some("transaction headers")
133        } else {
134            None
135        };
136        if let Some(mismatch) = mismatch {
137            return Err(ProvenBatchError::ProposalMismatch(mismatch).into());
138        }
139        Ok(batch)
140    }
141}