miden_protocol/transaction/
tx_summary.rs1use 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#[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 pub(crate) const VERSION: u8 = 1;
48
49 const VERSION_IDX: usize = 0;
52 const METADATA_IDX: usize = 1;
53 const USER_PARAMS_IDX: usize = 2;
54
55 pub const NUM_ELEMENTS: usize =
59 Self::USER_PARAMS_IDX + TransactionSummaryUserParams::NUM_ELEMENTS + 4 * WORD_SIZE;
60
61 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 pub fn account_delta(&self) -> &AccountDelta {
93 &self.account_delta
94 }
95
96 pub fn input_notes(&self) -> &InputNotes<InputNote> {
98 &self.input_notes
99 }
100
101 pub fn output_notes(&self) -> &RawOutputNotes {
103 &self.output_notes
104 }
105
106 pub fn block_number(&self) -> BlockNumber {
108 self.block_number
109 }
110
111 pub fn block_commitment(&self) -> Word {
113 self.block_commitment
114 }
115
116 pub fn expiration_delta(&self) -> u16 {
118 self.expiration_delta
119 }
120
121 pub fn metadata(&self) -> TransactionSummaryMetadata {
123 TransactionSummaryMetadata::new(self.block_number, self.expiration_delta)
124 }
125
126 pub fn user_params(&self) -> TransactionSummaryUserParams {
128 self.user_params
129 }
130
131 pub fn to_elements(&self) -> Vec<Felt> {
148 <Self as SequentialCommit>::to_elements(self)
149 }
150
151 pub fn to_commitment(&self) -> Word {
155 <Self as SequentialCommit>::to_commitment(self)
156 }
157
158 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
206const _: () = 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
279pub struct TransactionSummaryMetadata {
280 block_number: BlockNumber,
281 expiration_delta: u16,
282}
283
284impl TransactionSummaryMetadata {
285 const EXPIRATION_DELTA_SHIFT: u32 = u32::BITS;
290
291 const NUM_BITS: u32 = Self::EXPIRATION_DELTA_SHIFT + u16::BITS;
293
294 pub fn new(block_number: BlockNumber, expiration_delta: u16) -> Self {
299 Self { block_number, expiration_delta }
300 }
301
302 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 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 pub fn block_number(&self) -> BlockNumber {
325 self.block_number
326 }
327
328 pub fn expiration_delta(&self) -> u16 {
330 self.expiration_delta
331 }
332
333 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 Felt::try_from(packed).expect("packed metadata should fit in felt")
340 }
341}
342
343#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
356pub struct TransactionSummaryUserParams {
357 elements: [Felt; Self::NUM_ELEMENTS],
358}
359
360impl TransactionSummaryUserParams {
361 pub const NUM_ELEMENTS: usize = 6;
366
367 pub fn new(elements: [Felt; Self::NUM_ELEMENTS]) -> Self {
372 Self { elements }
373 }
374
375 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#[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 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 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}