miden_protocol/batch/proposed_batch.rs
1use alloc::collections::btree_map::Entry;
2use alloc::collections::{BTreeMap, BTreeSet};
3use alloc::sync::Arc;
4use alloc::vec::Vec;
5
6use crate::account::AccountId;
7use crate::batch::note_tracker::{NoteTracker, TrackerOutput};
8use crate::batch::{BatchAccountUpdate, BatchId};
9use crate::block::{BlockHeader, BlockNumber};
10use crate::errors::ProposedBatchError;
11use crate::note::{NoteId, NoteInclusionProof};
12use crate::transaction::{
13 InputNoteCommitment,
14 InputNotes,
15 OrderedTransactionHeaders,
16 OutputNote,
17 PartialBlockchain,
18 ProvenTransaction,
19 TransactionHeader,
20 TransactionVerifier,
21};
22use crate::utils::serde::{
23 ByteReader,
24 ByteWriter,
25 Deserializable,
26 DeserializationError,
27 Serializable,
28};
29use crate::{MAX_ACCOUNTS_PER_BATCH, MAX_INPUT_NOTES_PER_BATCH, MAX_OUTPUT_NOTES_PER_BATCH};
30
31/// A proposed batch of transactions with all necessary data to validate it.
32///
33/// See [`ProposedBatch::new`] for what a proposed batch expects and guarantees.
34///
35/// This type is fairly large, so consider boxing it.
36#[derive(Debug, Clone)]
37pub struct ProposedBatch {
38 /// The transactions of this batch.
39 transactions: Vec<Arc<ProvenTransaction>>,
40 /// The header of the reference block that this batch is proposed for.
41 reference_block_header: BlockHeader,
42 /// The partial blockchain used to authenticate:
43 /// - all unauthenticated notes that can be authenticated,
44 /// - all block commitments referenced by the transactions in the batch.
45 partial_blockchain: PartialBlockchain,
46 /// The note inclusion proofs for unauthenticated notes that were consumed in the batch which
47 /// can be authenticated.
48 unauthenticated_note_proofs: BTreeMap<NoteId, NoteInclusionProof>,
49 /// The ID of the batch, which is a cryptographic commitment to the transactions in the batch.
50 id: BatchId,
51 /// A map from account ID's updated in this batch to the aggregated update from all
52 /// transaction's that touched the account.
53 account_updates: BTreeMap<AccountId, BatchAccountUpdate>,
54 /// The block number at which the batch will expire. This is the minimum of all transaction's
55 /// expiration block number.
56 batch_expiration_block_num: BlockNumber,
57 /// The input note commitment of the transaction batch. This consists of all authenticated
58 /// notes that transactions in the batch consume as well as unauthenticated notes whose
59 /// authentication is delayed to the block kernel. These are sorted by
60 /// [`InputNoteCommitment::nullifier`].
61 input_notes: InputNotes<InputNoteCommitment>,
62 /// The output notes of this batch. This consists of all notes created by transactions in the
63 /// batch that are not consumed within the same batch. These are sorted by
64 /// [`OutputNote::id`].
65 output_notes: Vec<OutputNote>,
66}
67
68impl ProposedBatch {
69 // CONSTRUCTORS
70 // --------------------------------------------------------------------------------------------
71
72 /// Creates a new [`ProposedBatch`] from the provided parts.
73 ///
74 /// # Inputs
75 ///
76 /// - The given transactions must be correctly ordered. That is, if two transactions A and B
77 /// update the same account in this order, meaning A's initial account state commitment
78 /// matches the account state before any transactions are executed and B's initial account
79 /// state commitment matches the final account state commitment of A, then A must come before
80 /// B.
81 /// - The partial blockchain's hashed peaks must match the reference block's `chain_commitment`
82 /// and it must contain all block headers:
83 /// - that are referenced by note inclusion proofs in `unauthenticated_note_proofs`.
84 /// - that are referenced by a transaction in the batch.
85 /// - The `unauthenticated_note_proofs` should contain [`NoteInclusionProof`]s for any
86 /// unauthenticated note consumed by the transaction's in the batch which can be
87 /// authenticated. This means it is not required that every unauthenticated note has an entry
88 /// in this map for two reasons.
89 /// - Unauthenticated note authentication can be delayed to the block kernel.
90 /// - Another transaction in the batch creates an output note matching an unauthenticated
91 /// input note, in which case inclusion in the chain does not need to be proven.
92 /// - The reference block of a batch must satisfy the following requirement: Its block number
93 /// must be greater or equal to the highest block number referenced by any transaction. This
94 /// is not verified explicitly, but will implicitly cause an error during the validation that
95 /// each reference block of a transaction is in the partial blockchain.
96 ///
97 /// # Errors
98 ///
99 /// Returns an error if:
100 ///
101 /// - The number of input notes exceeds [`MAX_INPUT_NOTES_PER_BATCH`].
102 /// - Note that unauthenticated notes that are created in the same batch do not count. Any
103 /// other input notes, unauthenticated or not, do count.
104 /// - The number of output notes exceeds [`MAX_OUTPUT_NOTES_PER_BATCH`].
105 /// - Note that output notes that are consumed in the same batch as unauthenticated input
106 /// notes do not count.
107 /// - Any note is consumed more than once.
108 /// - Any note is created more than once.
109 /// - An unauthenticated note is consumed before it is created (as determined by the order in
110 /// which transactions are given).
111 /// - The number of account updates exceeds [`MAX_ACCOUNTS_PER_BATCH`].
112 /// - Note that any number of transactions against the same account count as one update.
113 /// - The partial blockchains chain length does not match the block header's block number. This
114 /// means the partial blockchain should not contain the block header itself as it is added to
115 /// the MMR in the batch kernel.
116 /// - The partial blockchains hashed peaks do not match the block header's chain commitment.
117 /// - The reference block of any transaction is not in the partial blockchain.
118 /// - The note inclusion proof for an unauthenticated note fails to verify.
119 /// - The block referenced by a note inclusion proof for an unauthenticated note is missing from
120 /// the partial blockchain.
121 /// - The transactions in the proposed batch which update the same account are not correctly
122 /// ordered.
123 /// - The provided list of transactions is empty. An empty batch is pointless and would
124 /// potentially result in the same [`BatchId`] for two empty batches which would mean batch
125 /// IDs are no longer unique.
126 /// - There are duplicate transactions.
127 /// - If any transaction's expiration block number is less than or equal to the batch's
128 /// reference block.
129 fn new_batch_inner(
130 transactions: Vec<Arc<ProvenTransaction>>,
131 reference_block_header: BlockHeader,
132 partial_blockchain: PartialBlockchain,
133 unauthenticated_note_proofs: BTreeMap<NoteId, NoteInclusionProof>,
134 ) -> Result<Self, ProposedBatchError> {
135 // Check for empty or duplicate transactions.
136 // --------------------------------------------------------------------------------------------
137
138 if transactions.is_empty() {
139 return Err(ProposedBatchError::EmptyTransactionBatch);
140 }
141
142 let mut transaction_set = BTreeSet::new();
143 for tx in transactions.iter() {
144 if !transaction_set.insert(tx.id()) {
145 return Err(ProposedBatchError::DuplicateTransaction { transaction_id: tx.id() });
146 }
147 }
148
149 // Verify block header and partial blockchain match.
150 // --------------------------------------------------------------------------------------------
151
152 if partial_blockchain.chain_length() != reference_block_header.block_num() {
153 return Err(ProposedBatchError::InconsistentChainLength {
154 expected: reference_block_header.block_num(),
155 actual: partial_blockchain.chain_length(),
156 });
157 }
158
159 let hashed_peaks = partial_blockchain.peaks().hash_peaks();
160 if hashed_peaks != reference_block_header.chain_commitment() {
161 return Err(ProposedBatchError::InconsistentChainRoot {
162 expected: reference_block_header.chain_commitment(),
163 actual: hashed_peaks,
164 });
165 }
166
167 // Verify all block references from the transactions are in the partial blockchain, except
168 // for the batch's reference block.
169 //
170 // Note that some block X is only added to the blockchain by block X + 1. This
171 // is because block X cannot compute its own block commitment and thus cannot add
172 // itself to the chain. So, more generally, a block is added to the blockchain by its child
173 // block.
174 //
175 // The reference block of a batch may be the latest block in the chain and, as mentioned,
176 // the block is not yet part of the blockchain, so its inclusion cannot be proven.
177 // Since the inclusion cannot be proven, the batch kernel instead commits to this reference
178 // block's commitment as a public input, which means the block kernel will prove
179 // this block's inclusion when including this batch and verifying its ZK proof.
180 //
181 // Finally, note that we don't verify anything cryptographically here. We have previously
182 // verified that the chain commitment of the batch's reference block matches the hashed
183 // peaks of the `PartialBlockchain`. This means the provided blockchain is consistent with
184 // the batch's reference block and that all blocks contained in the blockchain are
185 // consistent, too. So, as long as each transaction's reference block (number and
186 // commitment) is contained in the partial blockchain, we know the transaction's
187 // block header is consistent with the batch's reference block, too.
188 // --------------------------------------------------------------------------------------------
189
190 for tx in transactions.iter() {
191 // Differentiate between validation against the batch's reference block or a block from
192 // the chain (see above).
193 if reference_block_header.block_num() == tx.ref_block_num() {
194 if reference_block_header.commitment() != tx.ref_block_commitment() {
195 return Err(ProposedBatchError::TransactionReferenceBlockCommitmentMismatch {
196 transaction_id: tx.id(),
197 block_num: tx.ref_block_num(),
198 actual_block_commitment: tx.ref_block_commitment(),
199 expected_block_commitment: reference_block_header.commitment(),
200 });
201 }
202 } else {
203 let block_header =
204 partial_blockchain.get_block(tx.ref_block_num()).ok_or_else(|| {
205 ProposedBatchError::MissingTransactionReferenceBlock {
206 transaction_id: tx.id(),
207 block_num: tx.ref_block_num(),
208 }
209 })?;
210
211 if block_header.commitment() != tx.ref_block_commitment() {
212 return Err(ProposedBatchError::TransactionReferenceBlockCommitmentMismatch {
213 transaction_id: tx.id(),
214 block_num: tx.ref_block_num(),
215 actual_block_commitment: tx.ref_block_commitment(),
216 expected_block_commitment: block_header.commitment(),
217 });
218 }
219 }
220 }
221
222 // Aggregate individual tx-level account updates into a batch-level account update - one per
223 // account.
224 // --------------------------------------------------------------------------------------------
225
226 // Populate batch output notes and updated accounts.
227 let mut account_updates = BTreeMap::<AccountId, BatchAccountUpdate>::new();
228 for tx in transactions.iter() {
229 // Merge account updates so that state transitions A->B->C become A->C.
230 match account_updates.entry(tx.account_id()) {
231 Entry::Vacant(vacant) => {
232 let batch_account_update = BatchAccountUpdate::from_transaction(tx);
233 vacant.insert(batch_account_update);
234 },
235 Entry::Occupied(occupied) => {
236 // This returns an error if the transactions are not correctly ordered, e.g. if
237 // B comes before A.
238 occupied.into_mut().merge_proven_tx(tx).map_err(|source| {
239 ProposedBatchError::AccountUpdateError {
240 account_id: tx.account_id(),
241 source,
242 }
243 })?;
244 },
245 };
246 }
247
248 if account_updates.len() > MAX_ACCOUNTS_PER_BATCH {
249 return Err(ProposedBatchError::TooManyAccountUpdates(account_updates.len()));
250 }
251
252 // Check that all transaction's expiration block numbers are greater than the reference
253 // block.
254 // --------------------------------------------------------------------------------------------
255
256 let mut batch_expiration_block_num = BlockNumber::from(u32::MAX);
257 for tx in transactions.iter() {
258 if tx.expiration_block_num() <= reference_block_header.block_num() {
259 return Err(ProposedBatchError::ExpiredTransaction {
260 transaction_id: tx.id(),
261 transaction_expiration_num: tx.expiration_block_num(),
262 reference_block_num: reference_block_header.block_num(),
263 });
264 }
265
266 // The expiration block of the batch is the minimum of all transaction's expiration
267 // block.
268 batch_expiration_block_num = batch_expiration_block_num.min(tx.expiration_block_num());
269 }
270
271 // Check for duplicates in input notes.
272 // --------------------------------------------------------------------------------------------
273
274 // Check for duplicate input notes both within a transaction and across transactions.
275 // This also includes authenticated notes, as the transaction kernel doesn't check for
276 // duplicates.
277 let mut input_note_map = BTreeMap::new();
278
279 for tx in transactions.iter() {
280 for note in tx.input_notes() {
281 let nullifier = note.nullifier();
282 if let Some(first_transaction_id) = input_note_map.insert(nullifier, tx.id()) {
283 return Err(ProposedBatchError::DuplicateInputNote {
284 note_nullifier: nullifier,
285 first_transaction_id,
286 second_transaction_id: tx.id(),
287 });
288 }
289 }
290 }
291
292 // Create input and output note set of the batch.
293 // --------------------------------------------------------------------------------------------
294
295 // Check for duplicate output notes and remove all output notes from the batch output note
296 // set that are consumed by transactions.
297 let mut tracker = NoteTracker::new(
298 &partial_blockchain,
299 &reference_block_header,
300 &unauthenticated_note_proofs,
301 );
302 for tx in transactions.iter() {
303 tracker.push(tx.as_ref()).map_err(ProposedBatchError::from)?;
304 }
305 let TrackerOutput { input_notes, output_notes, .. } =
306 tracker.finalize().map_err(ProposedBatchError::from)?;
307
308 // Collect the remaining (non-erased) output notes into the final set of output notes.
309 let output_notes: Vec<OutputNote> =
310 output_notes.into_values().map(|(_, output_note)| output_note).collect();
311
312 if input_notes.len() > MAX_INPUT_NOTES_PER_BATCH {
313 return Err(ProposedBatchError::TooManyInputNotes(input_notes.len()));
314 }
315 // SAFETY: This is safe as we have checked for duplicates and the max number of input notes
316 // in a batch.
317 let input_notes = InputNotes::new_unchecked(input_notes);
318
319 if output_notes.len() > MAX_OUTPUT_NOTES_PER_BATCH {
320 return Err(ProposedBatchError::TooManyOutputNotes(output_notes.len()));
321 }
322
323 // Compute batch ID.
324 // --------------------------------------------------------------------------------------------
325
326 let id = BatchId::from_transactions(transactions.iter().map(AsRef::as_ref));
327
328 Ok(Self {
329 id,
330 transactions,
331 reference_block_header,
332 partial_blockchain,
333 unauthenticated_note_proofs,
334 account_updates,
335 batch_expiration_block_num,
336 input_notes,
337 output_notes,
338 })
339 }
340
341 /// Creates a new [`ProposedBatch`] from the provided parts, verifying every transaction's
342 /// execution proof against the transaction kernel.
343 ///
344 /// # Errors
345 ///
346 /// Returns an error for any of the batch-validation conditions documented on `new_batch_inner`,
347 /// or if a transaction's proof fails to verify or does not meet `proof_security_level`.
348 pub fn new(
349 transactions: Vec<Arc<ProvenTransaction>>,
350 reference_block_header: BlockHeader,
351 partial_blockchain: PartialBlockchain,
352 unauthenticated_note_proofs: BTreeMap<NoteId, NoteInclusionProof>,
353 proof_security_level: u32,
354 ) -> Result<Self, ProposedBatchError> {
355 let batch = Self::new_batch_inner(
356 transactions,
357 reference_block_header,
358 partial_blockchain,
359 unauthenticated_note_proofs,
360 )?;
361
362 let verifier = TransactionVerifier::new(proof_security_level);
363 for tx in batch.transactions() {
364 verifier.verify(tx).map_err(|source| {
365 ProposedBatchError::TransactionVerificationFailed {
366 transaction_id: tx.id(),
367 source,
368 }
369 })?;
370 }
371
372 Ok(batch)
373 }
374
375 /// Creates a new [`ProposedBatch`] **without verifying the transactions' execution proofs**.
376 ///
377 /// Runs the same batch validation as [`Self::new`] but skips proof verification. Exposed for
378 /// tests that build batches from mock transactions carrying dummy proofs.
379 #[cfg(any(test, feature = "testing"))]
380 pub fn new_unverified(
381 transactions: Vec<Arc<ProvenTransaction>>,
382 reference_block_header: BlockHeader,
383 partial_blockchain: PartialBlockchain,
384 unauthenticated_note_proofs: BTreeMap<NoteId, NoteInclusionProof>,
385 ) -> Result<Self, ProposedBatchError> {
386 Self::new_batch_inner(
387 transactions,
388 reference_block_header,
389 partial_blockchain,
390 unauthenticated_note_proofs,
391 )
392 }
393
394 // PUBLIC ACCESSORS
395 // --------------------------------------------------------------------------------------------
396
397 /// Returns a slice of the [`ProvenTransaction`]s in the batch.
398 pub fn transactions(&self) -> &[Arc<ProvenTransaction>] {
399 &self.transactions
400 }
401
402 /// Returns the ordered set of transactions in the batch.
403 pub fn transaction_headers(&self) -> OrderedTransactionHeaders {
404 // SAFETY: This constructs an ordered set in the order of the transactions in the batch.
405 OrderedTransactionHeaders::new_unchecked(
406 self.transactions
407 .iter()
408 .map(AsRef::as_ref)
409 .map(TransactionHeader::from)
410 .collect(),
411 )
412 }
413
414 /// Returns the map of account IDs mapped to their [`BatchAccountUpdate`]s.
415 ///
416 /// If an account was updated by multiple transactions, the [`BatchAccountUpdate`] is the result
417 /// of merging the individual updates.
418 ///
419 /// For example, suppose an account's state before this batch is `A` and the batch contains two
420 /// transactions that updated it. Applying the first transaction results in intermediate state
421 /// `B`, and applying the second one results in state `C`. Then the returned update represents
422 /// the state transition from `A` to `C`.
423 pub fn account_updates(&self) -> &BTreeMap<AccountId, BatchAccountUpdate> {
424 &self.account_updates
425 }
426
427 /// The ID of this batch. See [`BatchId`] for details on how it is computed.
428 pub fn id(&self) -> BatchId {
429 self.id
430 }
431
432 /// Returns the header of the reference block this batch is proposed for.
433 pub fn reference_block_header(&self) -> &BlockHeader {
434 &self.reference_block_header
435 }
436
437 /// Returns the block number at which the batch will expire.
438 pub fn batch_expiration_block_num(&self) -> BlockNumber {
439 self.batch_expiration_block_num
440 }
441
442 /// Returns the [`InputNotes`] of this batch.
443 pub fn input_notes(&self) -> &InputNotes<InputNoteCommitment> {
444 &self.input_notes
445 }
446
447 /// Returns the output notes of the batch.
448 ///
449 /// This is the aggregation of all output notes by the transactions in the batch, except the
450 /// ones that were consumed within the batch itself.
451 pub fn output_notes(&self) -> &[OutputNote] {
452 &self.output_notes
453 }
454
455 /// Consumes the proposed batch and returns its underlying parts.
456 #[allow(clippy::type_complexity)]
457 pub fn into_parts(
458 self,
459 ) -> (
460 Vec<Arc<ProvenTransaction>>,
461 BlockHeader,
462 PartialBlockchain,
463 BTreeMap<NoteId, NoteInclusionProof>,
464 BatchId,
465 BTreeMap<AccountId, BatchAccountUpdate>,
466 InputNotes<InputNoteCommitment>,
467 Vec<OutputNote>,
468 BlockNumber,
469 ) {
470 (
471 self.transactions,
472 self.reference_block_header,
473 self.partial_blockchain,
474 self.unauthenticated_note_proofs,
475 self.id,
476 self.account_updates,
477 self.input_notes,
478 self.output_notes,
479 self.batch_expiration_block_num,
480 )
481 }
482}
483
484// SERIALIZATION
485// ================================================================================================
486
487impl Serializable for ProposedBatch {
488 fn write_into<W: ByteWriter>(&self, target: &mut W) {
489 self.transactions
490 .iter()
491 .map(|tx| tx.as_ref().clone())
492 .collect::<Vec<ProvenTransaction>>()
493 .write_into(target);
494
495 self.reference_block_header.write_into(target);
496 self.partial_blockchain.write_into(target);
497 self.unauthenticated_note_proofs.write_into(target);
498 }
499}
500
501impl Deserializable for ProposedBatch {
502 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
503 let transactions = Vec::<ProvenTransaction>::read_from(source)?
504 .into_iter()
505 .map(Arc::new)
506 .collect::<Vec<Arc<ProvenTransaction>>>();
507
508 let block_header = BlockHeader::read_from(source)?;
509 let partial_blockchain = PartialBlockchain::read_from(source)?;
510 let unauthenticated_note_proofs =
511 BTreeMap::<NoteId, NoteInclusionProof>::read_from(source)?;
512
513 // Reconstruct structurally without verifying the transactions' proofs.
514 ProposedBatch::new_batch_inner(
515 transactions,
516 block_header,
517 partial_blockchain,
518 unauthenticated_note_proofs,
519 )
520 .map_err(|source| {
521 DeserializationError::UnknownError(format!("failed to create proposed batch: {source}"))
522 })
523 }
524}
525
526#[cfg(test)]
527mod tests {
528 use anyhow::Context;
529 use miden_crypto::merkle::mmr::{Mmr, PartialMmr};
530 use miden_crypto::rand::test_utils::rand_value;
531 use miden_verifier::ExecutionProof;
532
533 use super::*;
534 use crate::Word;
535 use crate::account::{AccountType, AccountUpdateDetails};
536 use crate::transaction::{InputNoteCommitment, OutputNote, ProvenTransaction, TxAccountUpdate};
537
538 #[test]
539 fn proposed_batch_serialization() -> anyhow::Result<()> {
540 // create partial blockchain with 3 blocks - i.e., 2 peaks
541 let mut mmr = Mmr::default();
542 for i in 0..3 {
543 let block_header = BlockHeader::mock(i, None, None, &[], Word::empty());
544 mmr.add(block_header.commitment())
545 .expect("mmr leaf count exceeds forest leaf bound");
546 }
547 let partial_mmr: PartialMmr = mmr.peaks().into();
548 let partial_blockchain = PartialBlockchain::new(partial_mmr, Vec::new()).unwrap();
549
550 let chain_commitment = partial_blockchain.peaks().hash_peaks();
551 let note_root = rand_value::<Word>();
552 let tx_kernel_commitment = rand_value::<Word>();
553 let reference_block_header = BlockHeader::mock(
554 3,
555 Some(chain_commitment),
556 Some(note_root),
557 &[],
558 tx_kernel_commitment,
559 );
560
561 let account_id =
562 AccountId::builder().account_type(AccountType::Private).build_with_seed([1; 32]);
563 let initial_account_commitment =
564 [2; 32].try_into().expect("failed to create initial account commitment");
565 let final_account_commitment =
566 [3; 32].try_into().expect("failed to create final account commitment");
567 let account_patch_commitment =
568 [4; 32].try_into().expect("failed to create account patch commitment");
569 let block_num = reference_block_header.block_num();
570 let block_ref = reference_block_header.commitment();
571 let expiration_block_num = reference_block_header.block_num() + 1;
572 let proof = ExecutionProof::new_dummy();
573
574 let account_update = TxAccountUpdate::new(
575 account_id,
576 initial_account_commitment,
577 final_account_commitment,
578 account_patch_commitment,
579 AccountUpdateDetails::Private,
580 )
581 .context("failed to build account update")?;
582
583 let tx = ProvenTransaction::new(
584 account_update,
585 Vec::<InputNoteCommitment>::new(),
586 Vec::<OutputNote>::new(),
587 block_num,
588 block_ref,
589 expiration_block_num,
590 proof,
591 )
592 .context("failed to build proven transaction")?;
593
594 let batch = ProposedBatch::new_unverified(
595 vec![Arc::new(tx)],
596 reference_block_header,
597 partial_blockchain,
598 BTreeMap::new(),
599 )
600 .context("failed to propose batch")?;
601
602 let encoded_batch = batch.to_bytes();
603
604 let batch2 = ProposedBatch::read_from_bytes(&encoded_batch)
605 .context("failed to deserialize proposed batch")?;
606
607 assert_eq!(batch.transactions(), batch2.transactions());
608 assert_eq!(batch.reference_block_header, batch2.reference_block_header);
609 assert_eq!(batch.partial_blockchain, batch2.partial_blockchain);
610 assert_eq!(batch.unauthenticated_note_proofs, batch2.unauthenticated_note_proofs);
611 assert_eq!(batch.id, batch2.id);
612 assert_eq!(batch.account_updates, batch2.account_updates);
613 assert_eq!(batch.batch_expiration_block_num, batch2.batch_expiration_block_num);
614 assert_eq!(batch.input_notes, batch2.input_notes);
615 assert_eq!(batch.output_notes, batch2.output_notes);
616
617 Ok(())
618 }
619}