miden_objects/transaction/
proven_tx.rs

1use alloc::{string::ToString, vec::Vec};
2
3use super::{InputNote, ToInputNoteCommitments};
4use crate::{
5    ACCOUNT_UPDATE_MAX_SIZE, ProvenTransactionError,
6    account::delta::AccountUpdateDetails,
7    block::BlockNumber,
8    note::NoteHeader,
9    transaction::{
10        AccountId, Digest, InputNotes, Nullifier, OutputNote, OutputNotes, TransactionId,
11    },
12    utils::serde::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable},
13    vm::ExecutionProof,
14};
15
16// PROVEN TRANSACTION
17// ================================================================================================
18
19/// Result of executing and proving a transaction. Contains all the data required to verify that a
20/// transaction was executed correctly.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct ProvenTransaction {
23    /// A unique identifier for the transaction, see [TransactionId] for additional details.
24    id: TransactionId,
25
26    /// Account update data.
27    account_update: TxAccountUpdate,
28
29    /// Committed details of all notes consumed by the transaction.
30    input_notes: InputNotes<InputNoteCommitment>,
31
32    /// Notes created by the transaction. For private notes, this will contain only note headers,
33    /// while for public notes this will also contain full note details.
34    output_notes: OutputNotes,
35
36    /// [`BlockNumber`] of the transaction's reference block.
37    ref_block_num: BlockNumber,
38
39    /// The block commitment of the transaction's reference block.
40    ref_block_commitment: Digest,
41
42    /// The block number by which the transaction will expire, as defined by the executed scripts.
43    expiration_block_num: BlockNumber,
44
45    /// A STARK proof that attests to the correct execution of the transaction.
46    proof: ExecutionProof,
47}
48
49impl ProvenTransaction {
50    /// Returns unique identifier of this transaction.
51    pub fn id(&self) -> TransactionId {
52        self.id
53    }
54
55    /// Returns ID of the account against which this transaction was executed.
56    pub fn account_id(&self) -> AccountId {
57        self.account_update.account_id()
58    }
59
60    /// Returns the account update details.
61    pub fn account_update(&self) -> &TxAccountUpdate {
62        &self.account_update
63    }
64
65    /// Returns a reference to the notes consumed by the transaction.
66    pub fn input_notes(&self) -> &InputNotes<InputNoteCommitment> {
67        &self.input_notes
68    }
69
70    /// Returns a reference to the notes produced by the transaction.
71    pub fn output_notes(&self) -> &OutputNotes {
72        &self.output_notes
73    }
74
75    /// Returns the proof of the transaction.
76    pub fn proof(&self) -> &ExecutionProof {
77        &self.proof
78    }
79
80    /// Returns the number of the reference block the transaction was executed against.
81    pub fn ref_block_num(&self) -> BlockNumber {
82        self.ref_block_num
83    }
84
85    /// Returns the commitment of the block transaction was executed against.
86    pub fn ref_block_commitment(&self) -> Digest {
87        self.ref_block_commitment
88    }
89
90    /// Returns an iterator of the headers of unauthenticated input notes in this transaction.
91    pub fn unauthenticated_notes(&self) -> impl Iterator<Item = &NoteHeader> {
92        self.input_notes.iter().filter_map(|note| note.header())
93    }
94
95    /// Returns the block number at which the transaction will expire.
96    pub fn expiration_block_num(&self) -> BlockNumber {
97        self.expiration_block_num
98    }
99
100    /// Returns an iterator over the nullifiers of all input notes in this transaction.
101    ///
102    /// This includes both authenticated and unauthenticated notes.
103    pub fn nullifiers(&self) -> impl Iterator<Item = Nullifier> + '_ {
104        self.input_notes.iter().map(InputNoteCommitment::nullifier)
105    }
106
107    // HELPER METHODS
108    // --------------------------------------------------------------------------------------------
109
110    /// Validates the transaction.
111    ///
112    /// # Errors
113    ///
114    /// Returns an error if:
115    /// - The size of the serialized account update exceeds [`ACCOUNT_UPDATE_MAX_SIZE`].
116    /// - The transaction was executed against a _new_ on-chain account and its account ID does not
117    ///   match the ID in the account update.
118    /// - The transaction was executed against a _new_ on-chain account and its commitment does not
119    ///   match the final state commitment of the account update.
120    /// - The transaction was executed against a private account and the account update is _not_ of
121    ///   type [`AccountUpdateDetails::Private`].
122    /// - The transaction was executed against an on-chain account and the update is of type
123    ///   [`AccountUpdateDetails::Private`].
124    /// - The transaction was executed against an _existing_ on-chain account and the update is of
125    ///   type [`AccountUpdateDetails::New`].
126    /// - The transaction creates a _new_ on-chain account and the update is of type
127    ///   [`AccountUpdateDetails::Delta`].
128    fn validate(self) -> Result<Self, ProvenTransactionError> {
129        // If the account is on-chain, then the account update details must be present.
130        if self.account_id().is_onchain() {
131            self.account_update.validate()?;
132
133            let is_new_account =
134                self.account_update.initial_state_commitment() == Digest::default();
135            match self.account_update.details() {
136                AccountUpdateDetails::Private => {
137                    return Err(ProvenTransactionError::OnChainAccountMissingDetails(
138                        self.account_id(),
139                    ));
140                },
141                AccountUpdateDetails::New(account) => {
142                    if !is_new_account {
143                        return Err(
144                            ProvenTransactionError::ExistingOnChainAccountRequiresDeltaDetails(
145                                self.account_id(),
146                            ),
147                        );
148                    }
149                    if account.id() != self.account_id() {
150                        return Err(ProvenTransactionError::AccountIdMismatch {
151                            tx_account_id: self.account_id(),
152                            details_account_id: account.id(),
153                        });
154                    }
155                    if account.commitment() != self.account_update.final_state_commitment() {
156                        return Err(ProvenTransactionError::AccountFinalCommitmentMismatch {
157                            tx_final_commitment: self.account_update.final_state_commitment(),
158                            details_commitment: account.commitment(),
159                        });
160                    }
161                },
162                AccountUpdateDetails::Delta(_) => {
163                    if is_new_account {
164                        return Err(ProvenTransactionError::NewOnChainAccountRequiresFullDetails(
165                            self.account_id(),
166                        ));
167                    }
168                },
169            }
170        } else if !self.account_update.is_private() {
171            return Err(ProvenTransactionError::PrivateAccountWithDetails(self.account_id()));
172        }
173
174        Ok(self)
175    }
176}
177
178impl Serializable for ProvenTransaction {
179    fn write_into<W: ByteWriter>(&self, target: &mut W) {
180        self.account_update.write_into(target);
181        self.input_notes.write_into(target);
182        self.output_notes.write_into(target);
183        self.ref_block_num.write_into(target);
184        self.ref_block_commitment.write_into(target);
185        self.expiration_block_num.write_into(target);
186        self.proof.write_into(target);
187    }
188}
189
190impl Deserializable for ProvenTransaction {
191    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
192        let account_update = TxAccountUpdate::read_from(source)?;
193
194        let input_notes = <InputNotes<InputNoteCommitment>>::read_from(source)?;
195        let output_notes = OutputNotes::read_from(source)?;
196
197        let ref_block_num = BlockNumber::read_from(source)?;
198        let ref_block_commitment = Digest::read_from(source)?;
199        let expiration_block_num = BlockNumber::read_from(source)?;
200        let proof = ExecutionProof::read_from(source)?;
201
202        let id = TransactionId::new(
203            account_update.initial_state_commitment(),
204            account_update.final_state_commitment(),
205            input_notes.commitment(),
206            output_notes.commitment(),
207        );
208
209        let proven_transaction = Self {
210            id,
211            account_update,
212            input_notes,
213            output_notes,
214            ref_block_num,
215            ref_block_commitment,
216            expiration_block_num,
217            proof,
218        };
219
220        proven_transaction
221            .validate()
222            .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
223    }
224}
225
226// PROVEN TRANSACTION BUILDER
227// ================================================================================================
228
229/// Builder for a proven transaction.
230#[derive(Clone, Debug)]
231pub struct ProvenTransactionBuilder {
232    /// ID of the account that the transaction was executed against.
233    account_id: AccountId,
234
235    /// The commitment of the account before the transaction was executed.
236    initial_account_commitment: Digest,
237
238    /// The commitment of the account after the transaction was executed.
239    final_account_commitment: Digest,
240
241    /// State changes to the account due to the transaction.
242    account_update_details: AccountUpdateDetails,
243
244    /// List of [InputNoteCommitment]s of all consumed notes by the transaction.
245    input_notes: Vec<InputNoteCommitment>,
246
247    /// List of [OutputNote]s of all notes created by the transaction.
248    output_notes: Vec<OutputNote>,
249
250    /// [`BlockNumber`] of the transaction's reference block.
251    ref_block_num: BlockNumber,
252
253    /// Block [Digest] of the transaction's reference block.
254    ref_block_commitment: Digest,
255
256    /// The block number by which the transaction will expire, as defined by the executed scripts.
257    expiration_block_num: BlockNumber,
258
259    /// A STARK proof that attests to the correct execution of the transaction.
260    proof: ExecutionProof,
261}
262
263impl ProvenTransactionBuilder {
264    // CONSTRUCTOR
265    // --------------------------------------------------------------------------------------------
266
267    /// Returns a [ProvenTransactionBuilder] used to build a [ProvenTransaction].
268    pub fn new(
269        account_id: AccountId,
270        initial_account_commitment: Digest,
271        final_account_commitment: Digest,
272        ref_block_num: BlockNumber,
273        ref_block_commitment: Digest,
274        expiration_block_num: BlockNumber,
275        proof: ExecutionProof,
276    ) -> Self {
277        Self {
278            account_id,
279            initial_account_commitment,
280            final_account_commitment,
281            account_update_details: AccountUpdateDetails::Private,
282            input_notes: Vec::new(),
283            output_notes: Vec::new(),
284            ref_block_num,
285            ref_block_commitment,
286            expiration_block_num,
287            proof,
288        }
289    }
290
291    // PUBLIC ACCESSORS
292    // --------------------------------------------------------------------------------------------
293
294    /// Sets the account's update details.
295    pub fn account_update_details(mut self, details: AccountUpdateDetails) -> Self {
296        self.account_update_details = details;
297        self
298    }
299
300    /// Add notes consumed by the transaction.
301    pub fn add_input_notes<I, T>(mut self, notes: I) -> Self
302    where
303        I: IntoIterator<Item = T>,
304        T: Into<InputNoteCommitment>,
305    {
306        self.input_notes.extend(notes.into_iter().map(|note| note.into()));
307        self
308    }
309
310    /// Add notes produced by the transaction.
311    pub fn add_output_notes<T>(mut self, notes: T) -> Self
312    where
313        T: IntoIterator<Item = OutputNote>,
314    {
315        self.output_notes.extend(notes);
316        self
317    }
318
319    /// Builds the [`ProvenTransaction`].
320    ///
321    /// # Errors
322    ///
323    /// Returns an error if:
324    /// - The total number of input notes is greater than
325    ///   [`MAX_INPUT_NOTES_PER_TX`](crate::constants::MAX_INPUT_NOTES_PER_TX).
326    /// - The vector of input notes contains duplicates.
327    /// - The total number of output notes is greater than
328    ///   [`MAX_OUTPUT_NOTES_PER_TX`](crate::constants::MAX_OUTPUT_NOTES_PER_TX).
329    /// - The vector of output notes contains duplicates.
330    /// - The size of the serialized account update exceeds [`ACCOUNT_UPDATE_MAX_SIZE`].
331    /// - The transaction was executed against a _new_ on-chain account and its account ID does not
332    ///   match the ID in the account update.
333    /// - The transaction was executed against a _new_ on-chain account and its commitment does not
334    ///   match the final state commitment of the account update.
335    /// - The transaction was executed against a private account and the account update is _not_ of
336    ///   type [`AccountUpdateDetails::Private`].
337    /// - The transaction was executed against an on-chain account and the update is of type
338    ///   [`AccountUpdateDetails::Private`].
339    /// - The transaction was executed against an _existing_ on-chain account and the update is of
340    ///   type [`AccountUpdateDetails::New`].
341    /// - The transaction creates a _new_ on-chain account and the update is of type
342    ///   [`AccountUpdateDetails::Delta`].
343    pub fn build(self) -> Result<ProvenTransaction, ProvenTransactionError> {
344        let input_notes =
345            InputNotes::new(self.input_notes).map_err(ProvenTransactionError::InputNotesError)?;
346        let output_notes = OutputNotes::new(self.output_notes)
347            .map_err(ProvenTransactionError::OutputNotesError)?;
348        let id = TransactionId::new(
349            self.initial_account_commitment,
350            self.final_account_commitment,
351            input_notes.commitment(),
352            output_notes.commitment(),
353        );
354        let account_update = TxAccountUpdate::new(
355            self.account_id,
356            self.initial_account_commitment,
357            self.final_account_commitment,
358            self.account_update_details,
359        );
360
361        let proven_transaction = ProvenTransaction {
362            id,
363            account_update,
364            input_notes,
365            output_notes,
366            ref_block_num: self.ref_block_num,
367            ref_block_commitment: self.ref_block_commitment,
368            expiration_block_num: self.expiration_block_num,
369            proof: self.proof,
370        };
371
372        proven_transaction.validate()
373    }
374}
375
376// TRANSACTION ACCOUNT UPDATE
377// ================================================================================================
378
379/// Describes the changes made to the account state resulting from a transaction execution.
380#[derive(Debug, Clone, PartialEq, Eq)]
381pub struct TxAccountUpdate {
382    /// ID of the account updated by a transaction.
383    account_id: AccountId,
384
385    /// The commitment of the account before a transaction was executed.
386    ///
387    /// Set to `Digest::default()` for new accounts.
388    init_state_commitment: Digest,
389
390    /// The commitment of the account state after a transaction was executed.
391    final_state_commitment: Digest,
392
393    /// A set of changes which can be applied the account's state prior to the transaction to
394    /// get the account state after the transaction. For private accounts this is set to
395    /// [AccountUpdateDetails::Private].
396    details: AccountUpdateDetails,
397}
398
399impl TxAccountUpdate {
400    /// Returns a new [TxAccountUpdate] instantiated from the specified components.
401    pub const fn new(
402        account_id: AccountId,
403        init_state_commitment: Digest,
404        final_state_commitment: Digest,
405        details: AccountUpdateDetails,
406    ) -> Self {
407        Self {
408            account_id,
409            init_state_commitment,
410            final_state_commitment,
411            details,
412        }
413    }
414
415    /// Returns the ID of the updated account.
416    pub fn account_id(&self) -> AccountId {
417        self.account_id
418    }
419
420    /// Returns the commitment of the account's initial state.
421    pub fn initial_state_commitment(&self) -> Digest {
422        self.init_state_commitment
423    }
424
425    /// Returns the commitment of the account's after a transaction was executed.
426    pub fn final_state_commitment(&self) -> Digest {
427        self.final_state_commitment
428    }
429
430    /// Returns the description of the updates for public accounts.
431    ///
432    /// These descriptions can be used to build the new account state from the previous account
433    /// state.
434    pub fn details(&self) -> &AccountUpdateDetails {
435        &self.details
436    }
437
438    /// Returns `true` if the account update details are for a private account.
439    pub fn is_private(&self) -> bool {
440        self.details.is_private()
441    }
442
443    /// Validates the following properties of the account update:
444    ///
445    /// - The size of the serialized account update does not exceed [`ACCOUNT_UPDATE_MAX_SIZE`].
446    pub fn validate(&self) -> Result<(), ProvenTransactionError> {
447        let account_update_size = self.details().get_size_hint();
448        if account_update_size > ACCOUNT_UPDATE_MAX_SIZE as usize {
449            Err(ProvenTransactionError::AccountUpdateSizeLimitExceeded {
450                account_id: self.account_id(),
451                update_size: account_update_size,
452            })
453        } else {
454            Ok(())
455        }
456    }
457}
458
459impl Serializable for TxAccountUpdate {
460    fn write_into<W: ByteWriter>(&self, target: &mut W) {
461        self.account_id.write_into(target);
462        self.init_state_commitment.write_into(target);
463        self.final_state_commitment.write_into(target);
464        self.details.write_into(target);
465    }
466}
467
468impl Deserializable for TxAccountUpdate {
469    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
470        Ok(Self {
471            account_id: AccountId::read_from(source)?,
472            init_state_commitment: Digest::read_from(source)?,
473            final_state_commitment: Digest::read_from(source)?,
474            details: AccountUpdateDetails::read_from(source)?,
475        })
476    }
477}
478
479// INPUT NOTE COMMITMENT
480// ================================================================================================
481
482/// The commitment to an input note.
483///
484/// For notes authenticated by the transaction kernel, the commitment consists only of the note's
485/// nullifier. For notes whose authentication is delayed to batch/block kernels, the commitment
486/// also includes full note header (i.e., note ID and metadata).
487#[derive(Debug, Clone, PartialEq, Eq)]
488pub struct InputNoteCommitment {
489    nullifier: Nullifier,
490    header: Option<NoteHeader>,
491}
492
493impl InputNoteCommitment {
494    /// Returns the nullifier of the input note committed to by this commitment.
495    pub fn nullifier(&self) -> Nullifier {
496        self.nullifier
497    }
498
499    /// Returns the header of the input committed to by this commitment.
500    ///
501    /// Note headers are present only for notes whose presence in the change has not yet been
502    /// authenticated.
503    pub fn header(&self) -> Option<&NoteHeader> {
504        self.header.as_ref()
505    }
506
507    /// Returns true if this commitment is for a note whose presence in the chain has been
508    /// authenticated.
509    ///
510    /// Authenticated notes are represented solely by their nullifiers and are missing the note
511    /// header.
512    pub fn is_authenticated(&self) -> bool {
513        self.header.is_none()
514    }
515}
516
517impl From<InputNote> for InputNoteCommitment {
518    fn from(note: InputNote) -> Self {
519        Self::from(&note)
520    }
521}
522
523impl From<&InputNote> for InputNoteCommitment {
524    fn from(note: &InputNote) -> Self {
525        match note {
526            InputNote::Authenticated { note, .. } => Self {
527                nullifier: note.nullifier(),
528                header: None,
529            },
530            InputNote::Unauthenticated { note } => Self {
531                nullifier: note.nullifier(),
532                header: Some(*note.header()),
533            },
534        }
535    }
536}
537
538impl From<Nullifier> for InputNoteCommitment {
539    fn from(nullifier: Nullifier) -> Self {
540        Self { nullifier, header: None }
541    }
542}
543
544impl ToInputNoteCommitments for InputNoteCommitment {
545    fn nullifier(&self) -> Nullifier {
546        self.nullifier
547    }
548
549    fn note_commitment(&self) -> Option<Digest> {
550        self.header.map(|header| header.commitment())
551    }
552}
553
554// SERIALIZATION
555// ------------------------------------------------------------------------------------------------
556
557impl Serializable for InputNoteCommitment {
558    fn write_into<W: ByteWriter>(&self, target: &mut W) {
559        self.nullifier.write_into(target);
560        self.header.write_into(target);
561    }
562}
563
564impl Deserializable for InputNoteCommitment {
565    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
566        let nullifier = Nullifier::read_from(source)?;
567        let header = <Option<NoteHeader>>::read_from(source)?;
568
569        Ok(Self { nullifier, header })
570    }
571}
572
573// TESTS
574// ================================================================================================
575
576#[cfg(test)]
577mod tests {
578    use alloc::collections::BTreeMap;
579
580    use anyhow::Context;
581    use miden_verifier::ExecutionProof;
582    use vm_core::utils::Deserializable;
583    use winter_air::proof::Proof;
584    use winter_rand_utils::rand_array;
585
586    use super::ProvenTransaction;
587    use crate::{
588        ACCOUNT_UPDATE_MAX_SIZE, Digest, EMPTY_WORD, ONE, ProvenTransactionError, ZERO,
589        account::{
590            AccountDelta, AccountId, AccountIdVersion, AccountStorageDelta, AccountStorageMode,
591            AccountType, AccountVaultDelta, StorageMapDelta, delta::AccountUpdateDetails,
592        },
593        block::BlockNumber,
594        testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
595        transaction::{ProvenTransactionBuilder, TxAccountUpdate},
596        utils::Serializable,
597    };
598
599    fn check_if_sync<T: Sync>() {}
600    fn check_if_send<T: Send>() {}
601
602    /// [ProvenTransaction] being Sync is part of its public API and changing it is backwards
603    /// incompatible.
604    #[test]
605    fn test_proven_transaction_is_sync() {
606        check_if_sync::<ProvenTransaction>();
607    }
608
609    /// [ProvenTransaction] being Send is part of its public API and changing it is backwards
610    /// incompatible.
611    #[test]
612    fn test_proven_transaction_is_send() {
613        check_if_send::<ProvenTransaction>();
614    }
615
616    #[test]
617    fn account_update_size_limit_not_exceeded() {
618        // A small delta does not exceed the limit.
619        let storage_delta = AccountStorageDelta::from_iters(
620            [1, 2, 3, 4],
621            [(2, [ONE, ONE, ONE, ONE]), (3, [ONE, ONE, ZERO, ONE])],
622            [],
623        );
624        let delta =
625            AccountDelta::new(storage_delta, AccountVaultDelta::default(), Some(ONE)).unwrap();
626        let details = AccountUpdateDetails::Delta(delta);
627        TxAccountUpdate::new(
628            AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap(),
629            Digest::new(EMPTY_WORD),
630            Digest::new(EMPTY_WORD),
631            details,
632        )
633        .validate()
634        .unwrap();
635    }
636
637    #[test]
638    fn account_update_size_limit_exceeded() {
639        let mut map = BTreeMap::new();
640        // The number of entries in the map required to exceed the limit.
641        // We divide by each entry's size which consists of a key (digest) and a value (word), both
642        // 32 bytes in size.
643        let required_entries = ACCOUNT_UPDATE_MAX_SIZE / (2 * 32);
644        for _ in 0..required_entries {
645            map.insert(Digest::new(rand_array()), rand_array());
646        }
647        let storage_delta = StorageMapDelta::new(map);
648
649        // A delta that exceeds the limit returns an error.
650        let storage_delta = AccountStorageDelta::from_iters([], [], [(4, storage_delta)]);
651        let delta =
652            AccountDelta::new(storage_delta, AccountVaultDelta::default(), Some(ONE)).unwrap();
653        let details = AccountUpdateDetails::Delta(delta);
654        let details_size = details.get_size_hint();
655
656        let err = TxAccountUpdate::new(
657            AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap(),
658            Digest::new(EMPTY_WORD),
659            Digest::new(EMPTY_WORD),
660            details,
661        )
662        .validate()
663        .unwrap_err();
664
665        assert!(
666            matches!(err, ProvenTransactionError::AccountUpdateSizeLimitExceeded { update_size, .. } if update_size == details_size)
667        );
668    }
669
670    #[test]
671    fn test_proven_tx_serde_roundtrip() -> anyhow::Result<()> {
672        let account_id = AccountId::dummy(
673            [1; 15],
674            AccountIdVersion::Version0,
675            AccountType::FungibleFaucet,
676            AccountStorageMode::Private,
677        );
678        let initial_account_commitment =
679            [2; 32].try_into().expect("failed to create initial account commitment");
680        let final_account_commitment =
681            [3; 32].try_into().expect("failed to create final account commitment");
682        let ref_block_num = BlockNumber::from(1);
683        let ref_block_commitment = Digest::default();
684        let expiration_block_num = BlockNumber::from(2);
685        let proof = ExecutionProof::new(Proof::new_dummy(), Default::default());
686
687        let tx = ProvenTransactionBuilder::new(
688            account_id,
689            initial_account_commitment,
690            final_account_commitment,
691            ref_block_num,
692            ref_block_commitment,
693            expiration_block_num,
694            proof,
695        )
696        .build()
697        .context("failed to build proven transaction")?;
698
699        let deserialized = ProvenTransaction::read_from_bytes(&tx.to_bytes()).unwrap();
700
701        assert_eq!(tx, deserialized);
702
703        Ok(())
704    }
705}