Skip to main content

miden_protocol/transaction/
tx_summary.rs

1use alloc::format;
2use alloc::vec::Vec;
3
4use crate::account::AccountDelta;
5use crate::block::BlockNumber;
6use crate::crypto::SequentialCommit;
7use crate::errors::TransactionSummaryError;
8use crate::transaction::{InputNote, InputNotes, RawOutputNotes};
9use crate::utils::serde::{
10    ByteReader,
11    ByteWriter,
12    Deserializable,
13    DeserializationError,
14    Serializable,
15};
16use crate::{Felt, WORD_SIZE, Word};
17
18// TRANSACTION SUMMARY
19// ================================================================================================
20
21/// The summary of the changes that result from executing a transaction.
22///
23/// These are the account delta, the consumed and created notes, the block the summary binds (see
24/// [`TransactionSummaryMetadata`]) together with that block's commitment, the transaction's
25/// expiration block delta and the user-defined parameters (see [`TransactionSummaryUserParams`]).
26///
27/// Because this data is intended to be signed, the user-defined parameters give an account's
28/// authentication procedure a way to bind arbitrary additional data to that signature, for example
29/// a salt providing replay protection or a maximum fee.
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct TransactionSummary {
32    account_delta: AccountDelta,
33    input_notes: InputNotes<InputNote>,
34    output_notes: RawOutputNotes,
35    block_number: BlockNumber,
36    block_commitment: Word,
37    expiration_delta: u16,
38    user_params: TransactionSummaryUserParams,
39}
40
41impl TransactionSummary {
42    // CONSTANTS
43    // --------------------------------------------------------------------------------------------
44
45    /// The layout version of the commitment preimage produced by
46    /// [`TransactionSummary::to_elements`].
47    pub(crate) const VERSION: u8 = 1;
48
49    /// The indices of the version, of the packed [`TransactionSummaryMetadata`] element and of the
50    /// first user parameter in the commitment preimage.
51    const VERSION_IDX: usize = 0;
52    const METADATA_IDX: usize = 1;
53    const USER_PARAMS_IDX: usize = 2;
54
55    /// The number of elements in the preimage of a [`TransactionSummary`] commitment, i.e. the
56    /// length of the vector returned by [`TransactionSummary::to_elements`]: the version, the
57    /// metadata element, the user parameters and the four commitment words.
58    pub const NUM_ELEMENTS: usize =
59        Self::USER_PARAMS_IDX + TransactionSummaryUserParams::NUM_ELEMENTS + 4 * WORD_SIZE;
60
61    // CONSTRUCTORS
62    // --------------------------------------------------------------------------------------------
63
64    /// Creates a new [`TransactionSummary`] from the provided parts.
65    ///
66    /// `block_commitment` must be the commitment of the block identified by `block_number`, which
67    /// is what the kernel guarantees for the summaries it builds.
68    pub fn new(
69        account_delta: AccountDelta,
70        input_notes: InputNotes<InputNote>,
71        output_notes: RawOutputNotes,
72        block_number: BlockNumber,
73        block_commitment: Word,
74        expiration_delta: u16,
75        user_params: TransactionSummaryUserParams,
76    ) -> Self {
77        Self {
78            account_delta,
79            input_notes,
80            output_notes,
81            block_number,
82            block_commitment,
83            expiration_delta,
84            user_params,
85        }
86    }
87
88    // PUBLIC ACCESSORS
89    // --------------------------------------------------------------------------------------------
90
91    /// Returns the account delta of this transaction summary.
92    pub fn account_delta(&self) -> &AccountDelta {
93        &self.account_delta
94    }
95
96    /// Returns the input notes of this transaction summary.
97    pub fn input_notes(&self) -> &InputNotes<InputNote> {
98        &self.input_notes
99    }
100
101    /// Returns the output notes of this transaction summary.
102    pub fn output_notes(&self) -> &RawOutputNotes {
103        &self.output_notes
104    }
105
106    /// Returns the number of the block bound by this transaction summary.
107    pub fn block_number(&self) -> BlockNumber {
108        self.block_number
109    }
110
111    /// Returns the commitment to the block bound by this transaction summary.
112    pub fn block_commitment(&self) -> Word {
113        self.block_commitment
114    }
115
116    /// Returns the expiration block delta of the transaction, or 0 if it has not been set.
117    pub fn expiration_delta(&self) -> u16 {
118        self.expiration_delta
119    }
120
121    /// Returns the metadata packed into the commitment preimage.
122    pub fn metadata(&self) -> TransactionSummaryMetadata {
123        TransactionSummaryMetadata::new(self.block_number, self.expiration_delta)
124    }
125
126    /// Returns the user-defined parameters bound by this transaction summary.
127    pub fn user_params(&self) -> TransactionSummaryUserParams {
128        self.user_params
129    }
130
131    /// Returns the elements this transaction summary commits to, i.e. the preimage of
132    /// [`TransactionSummary::to_commitment`].
133    ///
134    /// The returned vector contains [`TransactionSummary::NUM_ELEMENTS`] elements laid out as:
135    ///
136    /// ```text
137    /// [
138    ///     [version, metadata, user_param0, user_param1],
139    ///     [user_param2, user_param3, user_param4, user_param5],
140    ///     ACCOUNT_DELTA_COMMITMENT, INPUT_NOTES_COMMITMENT,
141    ///     OUTPUT_NOTES_COMMITMENT, BLOCK_COMMITMENT,
142    /// ]
143    /// ```
144    ///
145    /// The version comes first so that a reader encounters the layout version before anything that
146    /// depends on it.
147    pub fn to_elements(&self) -> Vec<Felt> {
148        <Self as SequentialCommit>::to_elements(self)
149    }
150
151    /// Computes the commitment to the [`TransactionSummary`].
152    ///
153    /// This can be used to sign the transaction.
154    pub fn to_commitment(&self) -> Word {
155        <Self as SequentialCommit>::to_commitment(self)
156    }
157
158    // PARAMETER DECODING
159    // --------------------------------------------------------------------------------------------
160
161    /// Decodes the [`TransactionSummaryMetadata`] and the [`TransactionSummaryUserParams`] from the
162    /// preimage of a transaction summary commitment.
163    ///
164    /// `elements` must be a full preimage as returned by [`TransactionSummary::to_elements`]. The
165    /// four commitments are not decoded because they cannot be inverted: a caller reconstructing a
166    /// summary from a preimage must rebuild the committed data from its own state, pass it to
167    /// [`TransactionSummary::new`] alongside the decoded values and check the result against
168    /// [`TransactionSummary::to_commitment`].
169    ///
170    /// # Errors
171    ///
172    /// Returns an error if:
173    /// - `elements` does not contain exactly [`TransactionSummary::NUM_ELEMENTS`] elements.
174    /// - the encoded version is not supported.
175    /// - the metadata element is not a valid [`TransactionSummaryMetadata`].
176    pub fn try_params_from_elements(
177        elements: &[Felt],
178    ) -> Result<(TransactionSummaryMetadata, TransactionSummaryUserParams), TransactionSummaryError>
179    {
180        if elements.len() != Self::NUM_ELEMENTS {
181            return Err(TransactionSummaryError::InvalidPreimageLength {
182                actual: elements.len(),
183                expected: Self::NUM_ELEMENTS,
184            });
185        }
186
187        let version = elements[Self::VERSION_IDX];
188        if version != Felt::from(Self::VERSION) {
189            return Err(TransactionSummaryError::UnsupportedVersion {
190                actual: version,
191                expected: Self::VERSION,
192            });
193        }
194
195        let metadata = TransactionSummaryMetadata::try_from_element(elements[Self::METADATA_IDX])?;
196
197        let user_params_end = Self::USER_PARAMS_IDX + TransactionSummaryUserParams::NUM_ELEMENTS;
198        let user_params = elements[Self::USER_PARAMS_IDX..user_params_end]
199            .try_into()
200            .expect("preimage length was validated above");
201
202        Ok((metadata, TransactionSummaryUserParams::new(user_params)))
203    }
204}
205
206/// The auth library absorbs the preimage word by word, so its length must stay word-aligned.
207const _: () = assert!(TransactionSummary::NUM_ELEMENTS.is_multiple_of(WORD_SIZE));
208
209impl SequentialCommit for TransactionSummary {
210    type Commitment = Word;
211
212    fn to_elements(&self) -> Vec<Felt> {
213        let mut elements = Vec::with_capacity(Self::NUM_ELEMENTS);
214        elements.push(Felt::from(Self::VERSION));
215        elements.push(self.metadata().to_element());
216        elements.extend_from_slice(self.user_params.as_elements());
217        elements.extend_from_slice(self.account_delta.to_commitment().as_elements());
218        elements.extend_from_slice(self.input_notes.commitment().as_elements());
219        elements.extend_from_slice(self.output_notes.commitment().as_elements());
220        elements.extend_from_slice(self.block_commitment.as_elements());
221        elements
222    }
223}
224
225impl Serializable for TransactionSummary {
226    fn write_into<W: ByteWriter>(&self, target: &mut W) {
227        Self::VERSION.write_into(target);
228        self.account_delta.write_into(target);
229        self.input_notes.write_into(target);
230        self.output_notes.write_into(target);
231        self.block_number.write_into(target);
232        self.block_commitment.write_into(target);
233        self.expiration_delta.write_into(target);
234        self.user_params.write_into(target);
235    }
236}
237
238impl Deserializable for TransactionSummary {
239    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
240        let version: u8 = source.read()?;
241        if version != Self::VERSION {
242            return Err(DeserializationError::InvalidValue(format!(
243                "transaction summary version is {version} but only version {} is supported",
244                Self::VERSION
245            )));
246        }
247
248        let account_delta = source.read()?;
249        let input_notes = source.read()?;
250        let output_notes = source.read()?;
251        let block_number = source.read()?;
252        let block_commitment = source.read()?;
253        let expiration_delta = source.read()?;
254        let user_params = source.read()?;
255
256        Ok(Self::new(
257            account_delta,
258            input_notes,
259            output_notes,
260            block_number,
261            block_commitment,
262            expiration_delta,
263            user_params,
264        ))
265    }
266}
267
268// TRANSACTION SUMMARY METADATA
269// ================================================================================================
270
271/// The metadata packed into a single element of a [`TransactionSummary`] commitment preimage.
272///
273/// It binds the number of the block whose commitment the summary contains and the transaction's
274/// expiration block delta.
275///
276/// The metadata encodes to a single element with the following layout:
277/// `[16 zero bits | expiration_delta (16 bits) | block_number (32 bits)]`
278#[derive(Debug, Clone, Copy, PartialEq, Eq)]
279pub struct TransactionSummaryMetadata {
280    block_number: BlockNumber,
281    expiration_delta: u16,
282}
283
284impl TransactionSummaryMetadata {
285    // CONSTANTS
286    // --------------------------------------------------------------------------------------------
287
288    /// The bit offset of the expiration delta, i.e. the width of the block number below it.
289    const EXPIRATION_DELTA_SHIFT: u32 = u32::BITS;
290
291    /// The number of bits the packed metadata occupies.
292    const NUM_BITS: u32 = Self::EXPIRATION_DELTA_SHIFT + u16::BITS;
293
294    // CONSTRUCTORS
295    // --------------------------------------------------------------------------------------------
296
297    /// Creates new metadata from the provided parts.
298    pub fn new(block_number: BlockNumber, expiration_delta: u16) -> Self {
299        Self { block_number, expiration_delta }
300    }
301
302    /// Decodes the metadata from its packed representation.
303    ///
304    /// # Errors
305    ///
306    /// Returns an error if `metadata` sets bits above the packed fields.
307    pub fn try_from_element(metadata: Felt) -> Result<Self, TransactionSummaryError> {
308        let packed = metadata.as_canonical_u64();
309        if packed >> Self::NUM_BITS != 0 {
310            return Err(TransactionSummaryError::MetadataOutOfRange(metadata));
311        }
312
313        // The `as` casts truncate to exactly the bits of the respective field.
314        let block_number = BlockNumber::from(packed as u32);
315        let expiration_delta = (packed >> Self::EXPIRATION_DELTA_SHIFT) as u16;
316
317        Ok(Self::new(block_number, expiration_delta))
318    }
319
320    // PUBLIC ACCESSORS
321    // --------------------------------------------------------------------------------------------
322
323    /// Returns the number of the block bound by the transaction summary.
324    pub fn block_number(&self) -> BlockNumber {
325        self.block_number
326    }
327
328    /// Returns the expiration block delta of the transaction, or 0 if it has not been set.
329    pub fn expiration_delta(&self) -> u16 {
330        self.expiration_delta
331    }
332
333    /// Returns the packed representation of the metadata.
334    pub fn to_element(&self) -> Felt {
335        let packed = (u64::from(self.expiration_delta) << Self::EXPIRATION_DELTA_SHIFT)
336            | u64::from(self.block_number.as_u32());
337
338        // The packed value occupies NUM_BITS bits, so it is always a canonical field element.
339        Felt::try_from(packed).expect("packed metadata should fit in felt")
340    }
341}
342
343// TRANSACTION SUMMARY USER PARAMS
344// ================================================================================================
345
346/// The user-defined parameters bound by a [`TransactionSummary`].
347///
348/// These are [`TransactionSummaryUserParams::NUM_ELEMENTS`] elements supplied by the account's
349/// authentication procedure when the summary is created.
350///
351/// The parameters are opaque: they are bound by the signature over the summary, but no meaning is
352/// enforced for any of them at the protocol level. Any semantics - using some of them as a salt for
353/// replay protection or binding a maximum fee, for example - must be implemented by the account
354/// component that supplies them.
355#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
356pub struct TransactionSummaryUserParams {
357    elements: [Felt; Self::NUM_ELEMENTS],
358}
359
360impl TransactionSummaryUserParams {
361    // CONSTANTS
362    // --------------------------------------------------------------------------------------------
363
364    /// The number of user-defined elements bound by a [`TransactionSummary`].
365    pub const NUM_ELEMENTS: usize = 6;
366
367    // CONSTRUCTORS
368    // --------------------------------------------------------------------------------------------
369
370    /// Creates new [`TransactionSummaryUserParams`] from the provided elements.
371    pub fn new(elements: [Felt; Self::NUM_ELEMENTS]) -> Self {
372        Self { elements }
373    }
374
375    // PUBLIC ACCESSORS
376    // --------------------------------------------------------------------------------------------
377
378    /// Returns the user-defined elements in the order in which they are hashed into the
379    /// [`TransactionSummary`] commitment.
380    pub fn as_elements(&self) -> &[Felt; Self::NUM_ELEMENTS] {
381        &self.elements
382    }
383}
384
385impl Serializable for TransactionSummaryUserParams {
386    fn write_into<W: ByteWriter>(&self, target: &mut W) {
387        self.elements.write_into(target);
388    }
389}
390
391impl Deserializable for TransactionSummaryUserParams {
392    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
393        Ok(Self::new(source.read()?))
394    }
395}
396
397// TESTS
398// ================================================================================================
399
400#[cfg(test)]
401mod tests {
402    use assert_matches::assert_matches;
403
404    use super::*;
405    use crate::ONE;
406    use crate::account::{AccountId, AccountStoragePatch, AccountVaultDelta};
407    use crate::testing::account_id::ACCOUNT_ID_PRIVATE_SENDER;
408
409    /// The block number, expiration delta and user parameters used by the tests below.
410    const BLOCK_NUMBER: u32 = 123;
411    const EXPIRATION_DELTA: u16 = 42;
412    const USER_PARAMS: [u32; TransactionSummaryUserParams::NUM_ELEMENTS] = [1, 2, 3, 4, 5, 6];
413
414    /// Builds a transaction summary over an empty delta and no notes, binding the parameters above.
415    fn mock_summary() -> TransactionSummary {
416        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
417        let account_delta = AccountDelta::new(
418            account_id,
419            AccountStoragePatch::new(),
420            AccountVaultDelta::default(),
421            None,
422            ONE,
423        )
424        .unwrap();
425
426        TransactionSummary::new(
427            account_delta,
428            InputNotes::new(Vec::new()).unwrap(),
429            RawOutputNotes::new(Vec::new()).unwrap(),
430            BlockNumber::from(BLOCK_NUMBER),
431            Word::from([9u32, 10, 11, 12].map(Felt::from)),
432            EXPIRATION_DELTA,
433            TransactionSummaryUserParams::new(USER_PARAMS.map(Felt::from)),
434        )
435    }
436
437    #[test]
438    fn tx_summary_params_element_roundtrip() -> anyhow::Result<()> {
439        let summary = mock_summary();
440        let elements = summary.to_elements();
441
442        assert_eq!(elements.len(), TransactionSummary::NUM_ELEMENTS);
443        assert_eq!(
444            &elements[..TransactionSummary::USER_PARAMS_IDX + USER_PARAMS.len()],
445            [
446                Felt::from(TransactionSummary::VERSION),
447                summary.metadata().to_element(),
448                Felt::from(USER_PARAMS[0]),
449                Felt::from(USER_PARAMS[1]),
450                Felt::from(USER_PARAMS[2]),
451                Felt::from(USER_PARAMS[3]),
452                Felt::from(USER_PARAMS[4]),
453                Felt::from(USER_PARAMS[5]),
454            ]
455        );
456
457        let (metadata, user_params) = TransactionSummary::try_params_from_elements(&elements)?;
458        assert_eq!(metadata, summary.metadata());
459        assert_eq!(user_params, summary.user_params());
460
461        Ok(())
462    }
463
464    #[test]
465    fn tx_summary_serde_roundtrip() -> anyhow::Result<()> {
466        let summary = mock_summary();
467
468        let deserialized = TransactionSummary::read_from_bytes(&summary.to_bytes())?;
469        assert_eq!(deserialized, summary);
470
471        Ok(())
472    }
473
474    #[rstest::rstest]
475    #[case::genesis(0, 0)]
476    #[case::typical(BLOCK_NUMBER, EXPIRATION_DELTA)]
477    #[case::maximum(u32::MAX, u16::MAX)]
478    fn tx_summary_metadata_roundtrip(
479        #[case] block_number: u32,
480        #[case] expiration_delta: u16,
481    ) -> anyhow::Result<()> {
482        let metadata =
483            TransactionSummaryMetadata::new(BlockNumber::from(block_number), expiration_delta);
484
485        let decoded = TransactionSummaryMetadata::try_from_element(metadata.to_element())?;
486        assert_eq!(decoded, metadata);
487        assert_eq!(decoded.block_number().as_u32(), block_number);
488        assert_eq!(decoded.expiration_delta(), expiration_delta);
489
490        Ok(())
491    }
492
493    #[test]
494    fn tx_summary_params_reject_unsupported_version() {
495        let unsupported_version = Felt::from(TransactionSummary::VERSION + 1);
496        let mut elements = mock_summary().to_elements();
497        elements[TransactionSummary::VERSION_IDX] = unsupported_version;
498
499        assert_matches!(
500            TransactionSummary::try_params_from_elements(&elements),
501            Err(TransactionSummaryError::UnsupportedVersion { actual, expected })
502                if actual == unsupported_version && expected == TransactionSummary::VERSION
503        );
504    }
505
506    #[test]
507    fn tx_summary_metadata_rejects_bits_above_packed_fields() {
508        let out_of_range = Felt::new_unchecked(1u64 << TransactionSummaryMetadata::NUM_BITS);
509
510        assert_matches!(
511            TransactionSummaryMetadata::try_from_element(out_of_range),
512            Err(TransactionSummaryError::MetadataOutOfRange(_))
513        );
514    }
515
516    #[test]
517    fn tx_summary_params_reject_preimage_of_wrong_length() {
518        let mut elements = mock_summary().to_elements();
519        elements.pop();
520
521        assert_matches!(
522            TransactionSummary::try_params_from_elements(&elements),
523            Err(TransactionSummaryError::InvalidPreimageLength { actual, expected })
524                if actual == TransactionSummary::NUM_ELEMENTS - 1
525                    && expected == TransactionSummary::NUM_ELEMENTS
526        );
527    }
528}