miden_objects/conversion/
batch.rs1use alloc::collections::BTreeMap;
2use alloc::format;
3use alloc::sync::Arc;
4use alloc::vec::Vec;
5
6use miden_protocol::Word;
7use miden_protocol::account::{AccountId, AccountUpdateDetails};
8use miden_protocol::batch::{BatchAccountUpdate, ProposedBatch, ProvenBatch};
9use miden_protocol::block::BlockNumber;
10use miden_protocol::note::{NoteId, NoteInclusionProof};
11use miden_protocol::transaction::{
12 InputNoteCommitment,
13 InputNotes,
14 OrderedTransactionHeaders,
15 OutputNote,
16 ProvenTransaction,
17 TransactionHeader,
18};
19use miden_protocol::vm::ExecutionProof;
20
21use super::{MessageDecodeExt, required};
22use crate::{ConversionError, ConversionResultExt, proto};
23
24impl From<&BatchAccountUpdate> for proto::transaction::BatchAccountUpdate {
25 fn from(value: &BatchAccountUpdate) -> Self {
26 Self {
27 account_id: Some(value.account_id().into()),
28 initial_state_commitment: Some(value.initial_state_commitment().into()),
29 final_state_commitment: Some(value.final_state_commitment().into()),
30 details: Some(value.details().into()),
31 }
32 }
33}
34
35impl TryFrom<proto::transaction::BatchAccountUpdate> for BatchAccountUpdate {
36 type Error = ConversionError;
37
38 fn try_from(value: proto::transaction::BatchAccountUpdate) -> Result<Self, Self::Error> {
39 let decoder = value.decoder();
40 let account_id = required!(decoder, value.account_id)?;
41 let initial_state_commitment = required!(decoder, value.initial_state_commitment)?;
42 let final_state_commitment = required!(decoder, value.final_state_commitment)?;
43 let details: AccountUpdateDetails = required!(decoder, value.details)?;
44 Self::new(account_id, initial_state_commitment, final_state_commitment, details)
45 .map_err(ConversionError::new)
46 }
47}
48
49impl From<&ProposedBatch> for proto::transaction::ProposedBatch {
50 fn from(value: &ProposedBatch) -> Self {
51 let (transactions, reference_block_header, partial_blockchain, note_proofs, ..) =
52 value.clone().into_parts();
53 Self {
54 transactions: transactions.iter().map(|tx| tx.as_ref().into()).collect(),
55 reference_block_header: Some(reference_block_header.into()),
56 partial_blockchain: Some((&partial_blockchain).into()),
57 unauthenticated_note_proofs: note_proofs.iter().map(Into::into).collect(),
58 }
59 }
60}
61
62impl From<ProposedBatch> for proto::transaction::ProposedBatch {
63 fn from(value: ProposedBatch) -> Self {
64 Self::from(&value)
65 }
66}
67
68pub fn decode_proposed_batch(
72 value: proto::transaction::ProposedBatch,
73 proof_security_level: u32,
74) -> Result<ProposedBatch, ConversionError> {
75 let decoder = value.decoder();
76 let transactions = value
77 .transactions
78 .into_iter()
79 .enumerate()
80 .map(|(index, tx)| {
81 ProvenTransaction::try_from(tx)
82 .map(Arc::new)
83 .context(format!("transactions[{index}]"))
84 })
85 .collect::<Result<Vec<_>, _>>()?;
86 let reference_block_header = required!(decoder, value.reference_block_header)?;
87 let partial_blockchain = required!(decoder, value.partial_blockchain)?;
88
89 let mut note_proofs = BTreeMap::new();
90 let mut previous_note_id = None;
91 for (index, proof) in value.unauthenticated_note_proofs.into_iter().enumerate() {
92 let (note_id, proof) = <(NoteId, NoteInclusionProof)>::try_from(&proof)
93 .context(format!("unauthenticated_note_proofs[{index}]"))?;
94 if previous_note_id.is_some_and(|previous| note_id <= previous) {
95 return Err(ConversionError::message(
96 "unauthenticated note proofs must have unique, ascending note IDs",
97 )
98 .context(format!("unauthenticated_note_proofs[{index}].note_id")));
99 }
100 previous_note_id = Some(note_id);
101 note_proofs.insert(note_id, proof);
102 }
103
104 ProposedBatch::new(
105 transactions,
106 reference_block_header,
107 partial_blockchain,
108 note_proofs,
109 proof_security_level,
110 )
111 .map_err(ConversionError::new)
112}
113
114impl From<&ProvenBatch> for proto::transaction::ProvenBatch {
115 fn from(value: &ProvenBatch) -> Self {
116 Self {
117 reference_block_commitment: Some(value.reference_block_commitment().into()),
118 reference_block_num: Some(value.reference_block_num().into()),
119 account_updates: value.account_updates().values().map(Into::into).collect(),
120 input_notes: value.input_notes().iter().map(Into::into).collect(),
121 output_notes: value.output_notes().iter().map(Into::into).collect(),
122 expiration_block_num: Some(value.batch_expiration_block_num().into()),
123 transactions: value.transactions().as_slice().iter().map(Into::into).collect(),
124 proof: Some(value.proof().into()),
125 }
126 }
127}
128
129impl From<ProvenBatch> for proto::transaction::ProvenBatch {
130 fn from(value: ProvenBatch) -> Self {
131 Self::from(&value)
132 }
133}
134
135struct DecodedProvenBatch {
136 reference_block_commitment: Word,
137 reference_block_num: BlockNumber,
138 account_updates: BTreeMap<AccountId, BatchAccountUpdate>,
139 input_notes: InputNotes<InputNoteCommitment>,
140 output_notes: Vec<OutputNote>,
141 expiration_block_num: BlockNumber,
142 transactions: Vec<TransactionHeader>,
143 proof: ExecutionProof,
144}
145
146impl DecodedProvenBatch {
147 fn decode(value: proto::transaction::ProvenBatch) -> Result<Self, ConversionError> {
148 let decoder = value.decoder();
149 let reference_block_commitment = required!(decoder, value.reference_block_commitment)?;
150 let reference_block_num =
151 required!(decoder, value.reference_block_num).context("reference_block_num")?;
152 let expiration_block_num =
153 required!(decoder, value.expiration_block_num).context("expiration_block_num")?;
154
155 let mut account_updates = BTreeMap::new();
156 let mut previous_account_id = None;
157 for (index, update) in value.account_updates.into_iter().enumerate() {
158 let update = BatchAccountUpdate::try_from(update)
159 .context(format!("account_updates[{index}]"))?;
160 if previous_account_id.is_some_and(|previous| update.account_id() <= previous) {
161 return Err(ConversionError::message(
162 "account updates must have unique, ascending account IDs",
163 )
164 .context(format!("account_updates[{index}].account_id")));
165 }
166 previous_account_id = Some(update.account_id());
167 account_updates.insert(update.account_id(), update);
168 }
169
170 let input_notes = value
171 .input_notes
172 .into_iter()
173 .enumerate()
174 .map(|(index, note)| {
175 InputNoteCommitment::try_from(note).context(format!("input_notes[{index}]"))
176 })
177 .collect::<Result<Vec<_>, _>>()?;
178 let input_notes = InputNotes::new_unchecked(input_notes);
179
180 let output_notes = value
181 .output_notes
182 .into_iter()
183 .enumerate()
184 .map(|(index, note)| {
185 OutputNote::try_from(note).context(format!("output_notes[{index}]"))
186 })
187 .collect::<Result<Vec<_>, _>>()?;
188
189 let transactions = value
190 .transactions
191 .into_iter()
192 .enumerate()
193 .map(|(index, tx)| {
194 TransactionHeader::try_from(tx).context(format!("transactions[{index}]"))
195 })
196 .collect::<Result<Vec<_>, _>>()?;
197 let proof = required!(decoder, value.proof)?;
198
199 Ok(Self {
200 reference_block_commitment,
201 reference_block_num,
202 account_updates,
203 input_notes,
204 output_notes,
205 expiration_block_num,
206 transactions,
207 proof,
208 })
209 }
210
211 fn into_domain(self) -> Result<ProvenBatch, ConversionError> {
212 ProvenBatch::new(
213 self.reference_block_commitment,
214 self.reference_block_num,
215 self.account_updates.into_values(),
216 self.input_notes,
217 self.output_notes,
218 self.expiration_block_num,
219 OrderedTransactionHeaders::new_unchecked(self.transactions),
220 self.proof,
221 )
222 .map_err(ConversionError::new)
223 }
224}
225
226pub fn decode_standalone_proven_batch(
229 value: proto::transaction::ProvenBatch,
230) -> Result<ProvenBatch, ConversionError> {
231 DecodedProvenBatch::decode(value)?.into_domain()
232}
233
234pub fn decode_proven_batch(
236 value: proto::transaction::ProvenBatch,
237 proposed: &ProposedBatch,
238) -> Result<ProvenBatch, ConversionError> {
239 let decoded = DecodedProvenBatch::decode(value)?;
240 let expected_header = proposed.reference_block_header();
241 if decoded.reference_block_num != expected_header.block_num() {
242 return Err(ConversionError::message("reference block number does not match proposal")
243 .context("reference_block_num"));
244 }
245 if decoded.reference_block_commitment != expected_header.commitment() {
246 return Err(ConversionError::message("reference block commitment does not match proposal")
247 .context("reference_block_commitment"));
248 }
249 if decoded.account_updates != *proposed.account_updates() {
250 return Err(ConversionError::message("account updates do not match proposal")
251 .context("account_updates"));
252 }
253 if !decoded.input_notes.iter().eq(proposed.input_notes().iter()) {
254 return Err(
255 ConversionError::message("input notes do not match proposal").context("input_notes")
256 );
257 }
258 if decoded.output_notes != proposed.output_notes() {
259 return Err(
260 ConversionError::message("output notes do not match proposal").context("output_notes")
261 );
262 }
263 if decoded.expiration_block_num != proposed.batch_expiration_block_num() {
264 return Err(ConversionError::message("expiration block does not match proposal")
265 .context("expiration_block_num"));
266 }
267 if decoded.transactions.as_slice() != proposed.transaction_headers().as_slice() {
268 return Err(ConversionError::message("transaction headers do not match proposal")
269 .context("transactions"));
270 }
271
272 decoded.into_domain()
273}