Skip to main content

miden_protocol/transaction/
tx_summary.rs

1use alloc::vec::Vec;
2
3use crate::account::AccountDelta;
4use crate::crypto::SequentialCommit;
5use crate::errors::TransactionSummaryError;
6use crate::transaction::{InputNote, InputNotes, RawOutputNotes};
7use crate::utils::serde::{
8    ByteReader,
9    ByteWriter,
10    Deserializable,
11    DeserializationError,
12    Serializable,
13};
14use crate::{Felt, WORD_SIZE, Word};
15
16// TRANSACTION SUMMARY
17// ================================================================================================
18
19/// The summary of the changes that result from executing a transaction.
20///
21/// These are the account delta, the consumed and created notes, the commitment to the reference
22/// block, the transaction's expiration block delta and the user-defined parameters (see
23/// [`TransactionSummaryUserParams`]).
24///
25/// Because this data is intended to be signed, the user-defined parameters give an account's
26/// authentication procedure a way to bind arbitrary additional data to that signature, for example
27/// a salt providing replay protection or a maximum fee.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct TransactionSummary {
30    account_delta: AccountDelta,
31    input_notes: InputNotes<InputNote>,
32    output_notes: RawOutputNotes,
33    block_commitment: Word,
34    expiration_delta: u16,
35    user_params: TransactionSummaryUserParams,
36}
37
38impl TransactionSummary {
39    // CONSTANTS
40    // --------------------------------------------------------------------------------------------
41
42    /// The index of the expiration block delta in the commitment preimage, i.e. the number of
43    /// elements occupied by the four leading commitments.
44    const EXPIRATION_DELTA_IDX: usize = 4 * WORD_SIZE;
45
46    /// The number of elements in the preimage of a [`TransactionSummary`] commitment, i.e. the
47    /// length of the vector returned by [`TransactionSummary::to_elements`] (6 words).
48    ///
49    /// Must match `TX_SUMMARY_NUM_ELEMENTS` in the standard auth library
50    /// (crates/miden-standards/asm/standards/auth/mod.masm).
51    pub const NUM_ELEMENTS: usize =
52        Self::EXPIRATION_DELTA_IDX + 1 + TransactionSummaryUserParams::NUM_ELEMENTS;
53
54    // CONSTRUCTORS
55    // --------------------------------------------------------------------------------------------
56
57    /// Creates a new [`TransactionSummary`] from the provided parts.
58    pub fn new(
59        account_delta: AccountDelta,
60        input_notes: InputNotes<InputNote>,
61        output_notes: RawOutputNotes,
62        block_commitment: Word,
63        expiration_delta: u16,
64        user_params: TransactionSummaryUserParams,
65    ) -> Self {
66        Self {
67            account_delta,
68            input_notes,
69            output_notes,
70            block_commitment,
71            expiration_delta,
72            user_params,
73        }
74    }
75
76    // PUBLIC ACCESSORS
77    // --------------------------------------------------------------------------------------------
78
79    /// Returns the account delta of this transaction summary.
80    pub fn account_delta(&self) -> &AccountDelta {
81        &self.account_delta
82    }
83
84    /// Returns the input notes of this transaction summary.
85    pub fn input_notes(&self) -> &InputNotes<InputNote> {
86        &self.input_notes
87    }
88
89    /// Returns the output notes of this transaction summary.
90    pub fn output_notes(&self) -> &RawOutputNotes {
91        &self.output_notes
92    }
93
94    /// Returns the commitment to the reference block of this transaction summary.
95    pub fn block_commitment(&self) -> Word {
96        self.block_commitment
97    }
98
99    /// Returns the expiration block delta of the transaction, or 0 if it has not been set.
100    pub fn expiration_delta(&self) -> u16 {
101        self.expiration_delta
102    }
103
104    /// Returns the user-defined parameters bound by this transaction summary.
105    pub fn user_params(&self) -> TransactionSummaryUserParams {
106        self.user_params
107    }
108
109    /// Returns the elements this transaction summary commits to, i.e. the preimage of
110    /// [`TransactionSummary::to_commitment`].
111    ///
112    /// The returned vector contains [`TransactionSummary::NUM_ELEMENTS`] elements laid out as:
113    ///
114    /// ```text
115    /// [
116    ///     ACCOUNT_DELTA_COMMITMENT, INPUT_NOTES_COMMITMENT, OUTPUT_NOTES_COMMITMENT,
117    ///     BLOCK_COMMITMENT, [expiration_delta, user_param0, user_param1, user_param2],
118    ///     [user_param3, user_param4, user_param5, user_param6],
119    /// ]
120    /// ```
121    pub fn to_elements(&self) -> Vec<Felt> {
122        <Self as SequentialCommit>::to_elements(self)
123    }
124
125    /// Computes the commitment to the [`TransactionSummary`].
126    ///
127    /// This can be used to sign the transaction.
128    pub fn to_commitment(&self) -> Word {
129        <Self as SequentialCommit>::to_commitment(self)
130    }
131
132    // PARAMETER DECODING
133    // --------------------------------------------------------------------------------------------
134
135    /// Decodes the transaction's expiration block delta and its [`TransactionSummaryUserParams`]
136    /// from the preimage of a transaction summary commitment.
137    ///
138    /// `elements` must be a full preimage as returned by [`TransactionSummary::to_elements`]. The
139    /// four leading commitments are not decoded because they cannot be inverted: a caller
140    /// reconstructing a summary from a preimage must rebuild the committed data from its own state,
141    /// pass it to [`TransactionSummary::new`] alongside the decoded values and check the result
142    /// against [`TransactionSummary::to_commitment`].
143    ///
144    /// # Errors
145    ///
146    /// Returns an error if:
147    /// - `elements` does not contain exactly [`TransactionSummary::NUM_ELEMENTS`] elements.
148    /// - the expiration block delta element does not fit into a `u16`.
149    pub fn try_params_from_elements(
150        elements: &[Felt],
151    ) -> Result<(u16, TransactionSummaryUserParams), TransactionSummaryError> {
152        if elements.len() != Self::NUM_ELEMENTS {
153            return Err(TransactionSummaryError::InvalidPreimageLength {
154                actual: elements.len(),
155                expected: Self::NUM_ELEMENTS,
156            });
157        }
158
159        let expiration_delta_element = elements[Self::EXPIRATION_DELTA_IDX];
160        let expiration_delta =
161            u16::try_from(expiration_delta_element.as_canonical_u64()).map_err(|_| {
162                TransactionSummaryError::ExpirationDeltaTooLarge(expiration_delta_element)
163            })?;
164
165        let user_params = elements[Self::EXPIRATION_DELTA_IDX + 1..]
166            .try_into()
167            .expect("preimage length was validated above");
168
169        Ok((expiration_delta, TransactionSummaryUserParams::new(user_params)))
170    }
171}
172
173/// The auth library absorbs the preimage word by word, so its length must stay word-aligned.
174const _: () = assert!(TransactionSummary::NUM_ELEMENTS.is_multiple_of(WORD_SIZE));
175
176impl SequentialCommit for TransactionSummary {
177    type Commitment = Word;
178
179    fn to_elements(&self) -> Vec<Felt> {
180        let mut elements = Vec::with_capacity(Self::NUM_ELEMENTS);
181        elements.extend_from_slice(self.account_delta.to_commitment().as_elements());
182        elements.extend_from_slice(self.input_notes.commitment().as_elements());
183        elements.extend_from_slice(self.output_notes.commitment().as_elements());
184        elements.extend_from_slice(self.block_commitment.as_elements());
185        elements.push(Felt::from(self.expiration_delta));
186        elements.extend_from_slice(self.user_params.as_elements());
187        elements
188    }
189}
190
191impl Serializable for TransactionSummary {
192    fn write_into<W: ByteWriter>(&self, target: &mut W) {
193        self.account_delta.write_into(target);
194        self.input_notes.write_into(target);
195        self.output_notes.write_into(target);
196        self.block_commitment.write_into(target);
197        self.expiration_delta.write_into(target);
198        self.user_params.write_into(target);
199    }
200}
201
202impl Deserializable for TransactionSummary {
203    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
204        let account_delta = source.read()?;
205        let input_notes = source.read()?;
206        let output_notes = source.read()?;
207        let block_commitment = source.read()?;
208        let expiration_delta = source.read()?;
209        let user_params = source.read()?;
210
211        Ok(Self::new(
212            account_delta,
213            input_notes,
214            output_notes,
215            block_commitment,
216            expiration_delta,
217            user_params,
218        ))
219    }
220}
221
222// TRANSACTION SUMMARY USER PARAMS
223// ================================================================================================
224
225/// The user-defined parameters bound by a [`TransactionSummary`].
226///
227/// These are [`TransactionSummaryUserParams::NUM_ELEMENTS`] elements supplied by the account's
228/// authentication procedure when the summary is created.
229///
230/// The parameters are opaque: they are bound by the signature over the summary, but no meaning is
231/// enforced for any of them at the protocol level. Any semantics - using some of them as a salt for
232/// replay protection or binding a maximum fee, for example - must be implemented by the account
233/// component that supplies them.
234#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
235pub struct TransactionSummaryUserParams {
236    elements: [Felt; Self::NUM_ELEMENTS],
237}
238
239impl TransactionSummaryUserParams {
240    // CONSTANTS
241    // --------------------------------------------------------------------------------------------
242
243    /// The number of user-defined elements bound by a [`TransactionSummary`].
244    pub const NUM_ELEMENTS: usize = 7;
245
246    // CONSTRUCTORS
247    // --------------------------------------------------------------------------------------------
248
249    /// Creates new [`TransactionSummaryUserParams`] from the provided elements.
250    pub fn new(elements: [Felt; Self::NUM_ELEMENTS]) -> Self {
251        Self { elements }
252    }
253
254    // PUBLIC ACCESSORS
255    // --------------------------------------------------------------------------------------------
256
257    /// Returns the user-defined elements in the order in which they are hashed into the
258    /// [`TransactionSummary`] commitment.
259    pub fn as_elements(&self) -> &[Felt; Self::NUM_ELEMENTS] {
260        &self.elements
261    }
262}
263
264impl Serializable for TransactionSummaryUserParams {
265    fn write_into<W: ByteWriter>(&self, target: &mut W) {
266        self.elements.write_into(target);
267    }
268}
269
270impl Deserializable for TransactionSummaryUserParams {
271    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
272        Ok(Self::new(source.read()?))
273    }
274}
275
276// TESTS
277// ================================================================================================
278
279#[cfg(test)]
280mod tests {
281    use assert_matches::assert_matches;
282
283    use super::*;
284    use crate::ONE;
285    use crate::account::{AccountId, AccountStoragePatch, AccountVaultDelta};
286    use crate::testing::account_id::ACCOUNT_ID_PRIVATE_SENDER;
287
288    /// The expiration delta and user parameters used by the tests below.
289    const EXPIRATION_DELTA: u16 = 42;
290    const USER_PARAMS: [u32; TransactionSummaryUserParams::NUM_ELEMENTS] = [1, 2, 3, 4, 5, 6, 7];
291
292    /// Builds a transaction summary over an empty delta and no notes, binding the parameters above.
293    fn mock_summary() -> TransactionSummary {
294        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
295        let account_delta = AccountDelta::new(
296            account_id,
297            AccountStoragePatch::new(),
298            AccountVaultDelta::default(),
299            None,
300            ONE,
301        )
302        .unwrap();
303
304        TransactionSummary::new(
305            account_delta,
306            InputNotes::new(Vec::new()).unwrap(),
307            RawOutputNotes::new(Vec::new()).unwrap(),
308            Word::from([9u32, 10, 11, 12].map(Felt::from)),
309            EXPIRATION_DELTA,
310            TransactionSummaryUserParams::new(USER_PARAMS.map(Felt::from)),
311        )
312    }
313
314    #[test]
315    fn tx_summary_params_element_roundtrip() {
316        let summary = mock_summary();
317        let elements = summary.to_elements();
318
319        assert_eq!(elements.len(), TransactionSummary::NUM_ELEMENTS);
320        assert_eq!(
321            &elements[TransactionSummary::EXPIRATION_DELTA_IDX..],
322            [
323                Felt::from(EXPIRATION_DELTA),
324                Felt::from(USER_PARAMS[0]),
325                Felt::from(USER_PARAMS[1]),
326                Felt::from(USER_PARAMS[2]),
327                Felt::from(USER_PARAMS[3]),
328                Felt::from(USER_PARAMS[4]),
329                Felt::from(USER_PARAMS[5]),
330                Felt::from(USER_PARAMS[6]),
331            ]
332        );
333
334        let (expiration_delta, user_params) =
335            TransactionSummary::try_params_from_elements(&elements).unwrap();
336        assert_eq!(expiration_delta, EXPIRATION_DELTA);
337        assert_eq!(user_params, summary.user_params());
338    }
339
340    #[test]
341    fn tx_summary_serde_roundtrip() {
342        let summary = mock_summary();
343
344        let deserialized = TransactionSummary::read_from_bytes(&summary.to_bytes()).unwrap();
345        assert_eq!(deserialized, summary);
346    }
347
348    #[test]
349    fn tx_summary_params_reject_out_of_range_expiration_delta() {
350        let mut elements = mock_summary().to_elements();
351        elements[TransactionSummary::EXPIRATION_DELTA_IDX] = Felt::from(u16::MAX as u32 + 1);
352
353        assert_matches!(
354            TransactionSummary::try_params_from_elements(&elements),
355            Err(TransactionSummaryError::ExpirationDeltaTooLarge(_))
356        );
357    }
358
359    #[test]
360    fn tx_summary_params_reject_preimage_of_wrong_length() {
361        let mut elements = mock_summary().to_elements();
362        elements.pop();
363
364        assert_matches!(
365            TransactionSummary::try_params_from_elements(&elements),
366            Err(TransactionSummaryError::InvalidPreimageLength { actual, expected })
367                if actual == TransactionSummary::NUM_ELEMENTS - 1
368                    && expected == TransactionSummary::NUM_ELEMENTS
369        );
370    }
371}