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_prover::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 )?;
189
190 let proven_batch =
192 LocalBatchProver::new(MIN_PROOF_SECURITY_LEVEL).prove(proposed_batch.clone())?;
193
194 let mut updates: Vec<TransactionStoreUpdate> = Vec::with_capacity(len);
196 let block_num = self
197 .client
198 .rpc_api
199 .submit_proven_batch(proven_batch, proposed_batch, transaction_inputs)
200 .await?;
201
202 for tx_result in &tx_results {
204 let update =
205 self.client.get_transaction_store_update(tx_result, block_num).await.map_err(
206 |source| BatchBuilderError::BatchSubmittedButUpdateBuildFailed {
207 block_num,
208 source,
209 },
210 )?;
211 updates.push(update);
212 }
213
214 if let Err(source) = self.client.store.apply_transaction_batch(updates).await {
216 return Err(ClientError::from(BatchBuilderError::BatchSubmittedButApplyFailed {
217 block_num,
218 source,
219 }));
220 }
221
222 Ok(block_num)
223 }
224
225 pub async fn push(
233 mut self,
234 account_id: AccountId,
235 req: TransactionRequest,
236 ) -> Result<Self, ClientError> {
237 for note_id in req.input_note_ids() {
239 if self.consumed_input_notes.contains(¬e_id) {
240 return Err(ClientError::from(BatchBuilderError::DuplicateInputNote(note_id)));
241 }
242 }
243
244 let tx_result =
246 execute_transaction_for_batch(self.client, &mut self.data_store, account_id, req)
247 .await?;
248 let tx_inputs = tx_result.executed_transaction().tx_inputs().clone();
249 let proven_tx = self.client.prove_transaction(&tx_result).await?;
250
251 for note in tx_result.consumed_notes().iter() {
253 self.consumed_input_notes.insert(note.id());
254 }
255 self.pushed_txs.push(PushedTx {
256 proven_tx: Arc::new(proven_tx),
257 transaction_inputs: tx_inputs,
258 tx_result,
259 });
260 Ok(self)
261 }
262}
263
264async fn execute_transaction_for_batch<AUTH>(
267 client: &Client<AUTH>,
268 data_store: &mut InMemoryBatchDataStore,
269 account_id: AccountId,
270 transaction_request: TransactionRequest,
271) -> Result<TransactionResult, ClientError>
272where
273 AUTH: TransactionAuthenticator + Sync + 'static,
274{
275 let mut account = if let Some(account) = data_store.get_account(account_id) {
276 account.clone()
277 } else {
278 let record = client
279 .store
280 .get_account(account_id)
281 .await?
282 .ok_or(ClientError::AccountDataNotFound(account_id))?;
283 if record.is_locked() {
284 return Err(ClientError::AccountLocked(account_id));
285 }
286 let account: Account = record.try_into()?;
287 account
288 };
289
290 let account_id = account.id();
291 let prep = client.prepare_transaction(&account, transaction_request).await?;
292
293 data_store.register_note_scripts(prep.output_note_scripts());
294 for fpi_account in &prep.foreign_account_inputs {
295 data_store.mast_store().load_account_code(fpi_account.code());
296 }
297 data_store.register_foreign_account_inputs(prep.foreign_account_inputs);
298
299 data_store.mast_store().load_account_code(account.code());
300
301 let mut notes = prep.notes;
302 if prep.ignore_invalid_notes {
303 notes = client
304 .get_valid_input_notes(&account, notes, prep.tx_args.clone(), &prep.output_recipients)
305 .await?;
306 }
307
308 let executed_transaction = client
309 .build_executor(data_store)?
310 .execute_transaction(account_id, prep.block_num, notes, prep.tx_args)
311 .await?;
312
313 account
315 .apply_delta(executed_transaction.account_delta())
316 .map_err(ClientError::AccountError)?;
317 data_store.cache_account(account_id, account);
318
319 validate_executed_transaction(&executed_transaction, &prep.output_recipients)?;
320 TransactionResult::new(executed_transaction, prep.future_notes)
321}