miden_client/transaction/batch/
mod.rs1mod data_store;
50mod error;
51
52use alloc::collections::{BTreeMap, BTreeSet};
53use alloc::sync::Arc;
54use alloc::vec::Vec;
55
56pub(crate) use data_store::InMemoryBatchDataStore;
57pub use error::BatchBuilderError;
58use miden_protocol::MIN_PROOF_SECURITY_LEVEL;
59use miden_protocol::account::{Account, AccountId};
60use miden_protocol::batch::ProposedBatch;
61use miden_protocol::block::{BlockHeader, BlockNumber};
62use miden_protocol::note::NoteId;
63use miden_protocol::transaction::{PartialBlockchain, ProvenTransaction, TransactionInputs};
64use miden_tx::auth::TransactionAuthenticator;
65use miden_tx_batch::{BatchExecutor, LocalBatchProver};
66
67use crate::store::data_store::build_partial_mmr_with_paths;
68use crate::transaction::{
69 TransactionRequest,
70 TransactionResult,
71 TransactionStoreUpdate,
72 validate_executed_transaction,
73};
74use crate::{Client, ClientError};
75
76pub(crate) struct PushedTx {
80 pub(crate) proven_tx: Arc<ProvenTransaction>,
81 pub(crate) transaction_inputs: TransactionInputs,
82 pub(crate) tx_result: TransactionResult,
83}
84
85pub struct BatchBuilder<'c, AUTH> {
89 pub(crate) client: &'c Client<AUTH>,
90 pub(crate) data_store: InMemoryBatchDataStore,
91 pub(crate) pushed_txs: Vec<PushedTx>,
92 pub(crate) consumed_input_notes: BTreeSet<NoteId>,
93}
94
95impl<AUTH> BatchBuilder<'_, AUTH> {
96 pub fn len(&self) -> usize {
98 self.pushed_txs.len()
99 }
100
101 pub fn is_empty(&self) -> bool {
103 self.pushed_txs.is_empty()
104 }
105}
106
107impl<AUTH> BatchBuilder<'_, AUTH>
108where
109 AUTH: TransactionAuthenticator + Sync + 'static,
110{
111 pub async fn submit(self) -> Result<BlockNumber, ClientError> {
118 let ref_block_num = self
121 .pushed_txs
122 .iter()
123 .map(|p| p.proven_tx.ref_block_num())
124 .max()
125 .ok_or(BatchBuilderError::Empty)?;
126
127 let lower_refs: BTreeSet<BlockNumber> = self
128 .pushed_txs
129 .iter()
130 .map(|p| p.proven_tx.ref_block_num())
131 .filter(|&r| r < ref_block_num)
132 .collect();
133
134 let store = self.client.store.clone();
135
136 let (ref_block_header, _) = store
138 .get_block_header_by_num(ref_block_num)
139 .await
140 .map_err(ClientError::StoreError)?
141 .ok_or_else(|| {
142 ClientError::StoreError(crate::store::StoreError::BlockHeaderNotFound(
143 ref_block_num,
144 ))
145 })?;
146
147 let fetched =
149 store.get_block_headers(&lower_refs).await.map_err(ClientError::StoreError)?;
150 let authenticated_blocks: Vec<BlockHeader> =
151 fetched.into_iter().map(|(header, _)| header).collect();
152 let fetched_nums: BTreeSet<BlockNumber> =
153 authenticated_blocks.iter().map(BlockHeader::block_num).collect();
154 if let Some(&missing) = lower_refs.difference(&fetched_nums).next() {
155 return Err(ClientError::StoreError(crate::store::StoreError::BlockHeaderNotFound(
156 missing,
157 )));
158 }
159
160 let current_peaks =
163 store.get_current_blockchain_peaks().await.map_err(ClientError::StoreError)?;
164 let partial_mmr =
165 build_partial_mmr_with_paths(&store, current_peaks, &authenticated_blocks).await?;
166 let partial_blockchain = PartialBlockchain::new(partial_mmr, authenticated_blocks)?;
167
168 let len = self.pushed_txs.len();
171 let mut proven_txs: Vec<Arc<ProvenTransaction>> = Vec::with_capacity(len);
172 let mut transaction_inputs: Vec<TransactionInputs> = Vec::with_capacity(len);
173 let mut tx_results: Vec<TransactionResult> = Vec::with_capacity(len);
174 for pushed in self.pushed_txs {
175 proven_txs.push(pushed.proven_tx);
176 transaction_inputs.push(pushed.transaction_inputs);
177 tx_results.push(pushed.tx_result);
178 }
179
180 let unauthenticated_note_proofs = BTreeMap::new();
183 let proposed_batch = ProposedBatch::new(
184 proven_txs,
185 ref_block_header,
186 partial_blockchain,
187 unauthenticated_note_proofs,
188 MIN_PROOF_SECURITY_LEVEL,
189 )?;
190
191 let executed_batch = BatchExecutor::new().execute(proposed_batch.clone())?;
193 let proven_batch = LocalBatchProver::new().prove(executed_batch)?;
194
195 let mut updates: Vec<TransactionStoreUpdate> = Vec::with_capacity(len);
197 let block_num = self
198 .client
199 .rpc_api
200 .submit_proven_batch(proven_batch, proposed_batch, transaction_inputs)
201 .await?;
202
203 for tx_result in &tx_results {
205 let update =
206 self.client.get_transaction_store_update(tx_result, block_num).await.map_err(
207 |source| BatchBuilderError::BatchSubmittedButUpdateBuildFailed {
208 block_num,
209 source,
210 },
211 )?;
212 updates.push(update);
213 }
214
215 if let Err(source) = self.client.store.apply_transaction_batch(updates).await {
217 return Err(ClientError::from(BatchBuilderError::BatchSubmittedButApplyFailed {
218 block_num,
219 source,
220 }));
221 }
222
223 Ok(block_num)
224 }
225
226 pub async fn push(
234 mut self,
235 account_id: AccountId,
236 req: TransactionRequest,
237 ) -> Result<Self, ClientError> {
238 for note_id in req.input_note_ids() {
240 if self.consumed_input_notes.contains(¬e_id) {
241 return Err(ClientError::from(BatchBuilderError::DuplicateInputNote(note_id)));
242 }
243 }
244
245 let tx_result =
247 execute_transaction_for_batch(self.client, &mut self.data_store, account_id, req)
248 .await?;
249 let tx_inputs = tx_result.executed_transaction().tx_inputs().clone();
250 let proven_tx = self.client.prove_transaction(&tx_result).await?;
251
252 for note in tx_result.consumed_notes().iter() {
254 self.consumed_input_notes.insert(note.id());
255 }
256 self.pushed_txs.push(PushedTx {
257 proven_tx: Arc::new(proven_tx),
258 transaction_inputs: tx_inputs,
259 tx_result,
260 });
261 Ok(self)
262 }
263}
264
265async fn execute_transaction_for_batch<AUTH>(
268 client: &Client<AUTH>,
269 data_store: &mut InMemoryBatchDataStore,
270 account_id: AccountId,
271 transaction_request: TransactionRequest,
272) -> Result<TransactionResult, ClientError>
273where
274 AUTH: TransactionAuthenticator + Sync + 'static,
275{
276 let mut account = if let Some(account) = data_store.get_account(account_id) {
277 account.clone()
278 } else {
279 let record = client
280 .store
281 .get_account(account_id)
282 .await?
283 .ok_or(ClientError::AccountDataNotFound(account_id))?;
284 if record.is_locked() {
285 return Err(ClientError::AccountLocked(account_id));
286 }
287 let account: Account = record.try_into()?;
288 account
289 };
290
291 let account_id = account.id();
292 let prep = client.prepare_transaction(&account, transaction_request).await?;
293
294 data_store.register_note_scripts(prep.output_note_scripts());
295 for fpi_account in &prep.foreign_account_inputs {
296 data_store.mast_store().load_account_code(fpi_account.code());
297 }
298 data_store.register_foreign_account_inputs(prep.foreign_account_inputs);
299
300 data_store.mast_store().load_account_code(account.code());
301
302 let mut notes = prep.notes;
303 if prep.ignore_invalid_notes {
304 notes = client
305 .get_valid_input_notes(&account, notes, prep.tx_args.clone(), &prep.output_recipients)
306 .await?;
307 }
308
309 let executed_transaction = client
310 .build_executor(data_store)?
311 .execute_transaction(account_id, prep.block_num, notes, prep.tx_args)
312 .await?;
313
314 let patch = executed_transaction.account_patch();
316 let account = if patch.is_full_state() {
317 Account::try_from(patch).map_err(ClientError::AccountError)?
318 } else {
319 account.apply_patch(patch).map_err(ClientError::AccountError)?;
320 account
321 };
322 data_store.cache_account(account_id, account);
323
324 validate_executed_transaction(&executed_transaction, &prep.output_recipients)?;
325 TransactionResult::new(executed_transaction, prep.future_notes)
326}