Skip to main content

miden_protocol/transaction/
tx_header.rs

1use alloc::collections::BTreeSet;
2use alloc::string::ToString;
3use alloc::vec::Vec;
4
5use crate::Word;
6use crate::errors::TransactionHeaderError;
7use crate::note::NoteHeader;
8use crate::transaction::{
9    AccountId,
10    ExecutedTransaction,
11    InputNoteCommitment,
12    InputNotes,
13    ProvenTransaction,
14    RawOutputNotes,
15    TransactionId,
16};
17use crate::utils::serde::{
18    ByteReader,
19    ByteWriter,
20    Deserializable,
21    DeserializationError,
22    Serializable,
23};
24
25/// A transaction header derived from a
26/// [`ProvenTransaction`](crate::transaction::ProvenTransaction).
27///
28/// The header is essentially a direct copy of the transaction's public commitments, in particular
29/// the initial and final account state commitment as well as all nullifiers of consumed notes and
30/// all note IDs of created notes. While account updates may be aggregated and notes may be erased
31/// as part of batch and block building, the header retains the original transaction's data.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct TransactionHeader {
34    id: TransactionId,
35    account_id: AccountId,
36    initial_state_commitment: Word,
37    final_state_commitment: Word,
38    input_notes: InputNotes<InputNoteCommitment>,
39    output_notes: Vec<NoteHeader>,
40}
41
42impl TransactionHeader {
43    // CONSTRUCTORS
44    // --------------------------------------------------------------------------------------------
45
46    /// Constructs a new [`TransactionHeader`] from the provided parameters.
47    ///
48    /// The [`TransactionId`] is computed from the provided parameters, committing to the initial
49    /// and final account commitments and the input and output note commitments.
50    ///
51    /// The input notes and output notes must be in the same order as they appeared in the
52    /// transaction that this header represents, otherwise an incorrect ID will be computed.
53    ///
54    /// Note that this cannot validate that the [`AccountId`] is valid with respect to the other
55    /// data. This must be validated outside of this type.
56    ///
57    /// # Errors
58    ///
59    /// Returns an error if the input notes contain duplicate nullifiers, the output notes contain
60    /// duplicate note IDs, or an unauthenticated input note is also created by the transaction.
61    /// Authenticated input note commitments do not carry note IDs, so overlap involving those notes
62    /// cannot be detected from a transaction header.
63    pub fn new(
64        account_id: AccountId,
65        initial_state_commitment: Word,
66        final_state_commitment: Word,
67        input_notes: InputNotes<InputNoteCommitment>,
68        output_notes: Vec<NoteHeader>,
69    ) -> Result<Self, TransactionHeaderError> {
70        let mut input_nullifiers = BTreeSet::new();
71        for input_note in &input_notes {
72            if !input_nullifiers.insert(input_note.nullifier()) {
73                return Err(TransactionHeaderError::DuplicateInputNote(input_note.nullifier()));
74            }
75        }
76
77        let mut output_note_ids = BTreeSet::new();
78        for output_note in &output_notes {
79            if !output_note_ids.insert(output_note.id()) {
80                return Err(TransactionHeaderError::DuplicateOutputNote(output_note.id()));
81            }
82        }
83
84        for input_note in input_notes.iter().filter_map(InputNoteCommitment::header) {
85            if output_note_ids.contains(&input_note.id()) {
86                return Err(TransactionHeaderError::NoteCreatedAndConsumed(input_note.id()));
87            }
88        }
89
90        let input_notes_commitment = input_notes.commitment();
91        let output_notes_commitment = RawOutputNotes::compute_commitment(output_notes.iter());
92
93        let id = TransactionId::new(
94            initial_state_commitment,
95            final_state_commitment,
96            input_notes_commitment,
97            output_notes_commitment,
98        );
99
100        Ok(Self {
101            id,
102            account_id,
103            initial_state_commitment,
104            final_state_commitment,
105            input_notes,
106            output_notes,
107        })
108    }
109
110    /// Constructs a new [`TransactionHeader`] from the provided parameters.
111    ///
112    /// # Warning
113    ///
114    /// This does not validate the internal consistency of the data. Prefer [`Self::new`] whenever
115    /// possible.
116    pub(crate) fn new_unchecked(
117        id: TransactionId,
118        account_id: AccountId,
119        initial_state_commitment: Word,
120        final_state_commitment: Word,
121        input_notes: InputNotes<InputNoteCommitment>,
122        output_notes: Vec<NoteHeader>,
123    ) -> Self {
124        Self {
125            id,
126            account_id,
127            initial_state_commitment,
128            final_state_commitment,
129            input_notes,
130            output_notes,
131        }
132    }
133
134    // PUBLIC ACCESSORS
135    // --------------------------------------------------------------------------------------------
136
137    /// Returns the unique identifier of this transaction.
138    pub fn id(&self) -> TransactionId {
139        self.id
140    }
141
142    /// Returns the ID of the account against which this transaction was executed.
143    pub fn account_id(&self) -> AccountId {
144        self.account_id
145    }
146
147    /// Returns a commitment to the state of the account before this update is applied.
148    ///
149    /// This is equal to [`Word::empty()`] for new accounts.
150    pub fn initial_state_commitment(&self) -> Word {
151        self.initial_state_commitment
152    }
153
154    /// Returns a commitment to the state of the account after this update is applied.
155    pub fn final_state_commitment(&self) -> Word {
156        self.final_state_commitment
157    }
158
159    /// Returns a reference to the consumed notes of the transaction.
160    ///
161    /// The returned input note commitments have the same order as the transaction to which the
162    /// header belongs.
163    ///
164    /// Note that the note may have been erased at the batch or block level, so it may not be
165    /// present there.
166    pub fn input_notes(&self) -> &InputNotes<InputNoteCommitment> {
167        &self.input_notes
168    }
169
170    /// Returns a reference to the ID and metadata of the output notes created by the transaction.
171    ///
172    /// The returned output note data has the same order as the transaction to which the header
173    /// belongs.
174    ///
175    /// Note that the note may have been erased at the batch or block level, so it may not be
176    /// present there.
177    pub fn output_notes(&self) -> &[NoteHeader] {
178        &self.output_notes
179    }
180}
181
182impl From<&ProvenTransaction> for TransactionHeader {
183    /// Constructs a [`TransactionHeader`] from a [`ProvenTransaction`].
184    fn from(tx: &ProvenTransaction) -> Self {
185        // SAFETY: The data in a proven transaction is guaranteed to be internally consistent and so
186        // we can skip the consistency checks by the `new` constructor.
187        TransactionHeader::new_unchecked(
188            tx.id(),
189            tx.account_id(),
190            tx.account_update().initial_state_commitment(),
191            tx.account_update().final_state_commitment(),
192            tx.input_notes().clone(),
193            tx.output_notes().iter().map(|note| *note.header()).collect(),
194        )
195    }
196}
197
198impl From<&ExecutedTransaction> for TransactionHeader {
199    /// Constructs a [`TransactionHeader`] from a [`ExecutedTransaction`].
200    fn from(tx: &ExecutedTransaction) -> Self {
201        TransactionHeader::new_unchecked(
202            tx.id(),
203            tx.account_id(),
204            tx.initial_account().initial_commitment(),
205            tx.final_account().to_commitment(),
206            tx.input_notes().to_commitments(),
207            tx.output_notes().iter().map(|n| *n.header()).collect(),
208        )
209    }
210}
211
212// SERIALIZATION
213// ================================================================================================
214
215impl Serializable for TransactionHeader {
216    fn write_into<W: ByteWriter>(&self, target: &mut W) {
217        let Self {
218            id: _,
219            account_id,
220            initial_state_commitment,
221            final_state_commitment,
222            input_notes,
223            output_notes,
224        } = self;
225
226        account_id.write_into(target);
227        initial_state_commitment.write_into(target);
228        final_state_commitment.write_into(target);
229        input_notes.write_into(target);
230        output_notes.write_into(target);
231    }
232}
233
234impl Deserializable for TransactionHeader {
235    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
236        let account_id = <AccountId>::read_from(source)?;
237        let initial_state_commitment = <Word>::read_from(source)?;
238        let final_state_commitment = <Word>::read_from(source)?;
239        let input_notes = <InputNotes<InputNoteCommitment>>::read_from(source)?;
240        let output_notes = <Vec<NoteHeader>>::read_from(source)?;
241
242        Self::new(
243            account_id,
244            initial_state_commitment,
245            final_state_commitment,
246            input_notes,
247            output_notes,
248        )
249        .map_err(|error| DeserializationError::InvalidValue(error.to_string()))
250    }
251}
252
253// TESTS
254// ================================================================================================
255
256#[cfg(test)]
257mod tests {
258    use assert_matches::assert_matches;
259
260    use super::TransactionHeader;
261    use crate::Word;
262    use crate::account::AccountId;
263    use crate::errors::TransactionHeaderError;
264    use crate::note::Note;
265    use crate::testing::account_id::ACCOUNT_ID_PRIVATE_SENDER;
266    use crate::transaction::{InputNoteCommitment, InputNotes, TransactionId};
267    use crate::utils::serde::{Deserializable, DeserializationError, Serializable};
268
269    fn account_id() -> AccountId {
270        AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap()
271    }
272
273    #[test]
274    fn rejects_duplicate_input_notes() {
275        let note = Note::mock_noop(Word::empty());
276        let input =
277            InputNoteCommitment::from_parts_unchecked(note.nullifier(), Some(*note.header()));
278        let inputs = InputNotes::new_unchecked(vec![input.clone(), input]);
279
280        let error = TransactionHeader::new(
281            account_id(),
282            Word::from([1_u32, 2, 3, 4]),
283            Word::from([5_u32, 6, 7, 8]),
284            inputs,
285            vec![],
286        )
287        .unwrap_err();
288
289        assert_matches!(
290            error,
291            TransactionHeaderError::DuplicateInputNote(nullifier)
292                if nullifier == note.nullifier()
293        );
294    }
295
296    #[test]
297    fn rejects_duplicate_output_notes() {
298        let note = Note::mock_noop(Word::empty());
299
300        let error = TransactionHeader::new(
301            account_id(),
302            Word::from([1_u32, 2, 3, 4]),
303            Word::from([5_u32, 6, 7, 8]),
304            InputNotes::default(),
305            vec![*note.header(), *note.header()],
306        )
307        .unwrap_err();
308
309        assert_matches!(
310            error,
311            TransactionHeaderError::DuplicateOutputNote(note_id) if note_id == note.id()
312        );
313    }
314
315    #[test]
316    fn rejects_note_created_and_consumed() {
317        let note = Note::mock_noop(Word::empty());
318        let input =
319            InputNoteCommitment::from_parts_unchecked(note.nullifier(), Some(*note.header()));
320
321        let error = TransactionHeader::new(
322            account_id(),
323            Word::from([1_u32, 2, 3, 4]),
324            Word::from([5_u32, 6, 7, 8]),
325            InputNotes::new(vec![input]).unwrap(),
326            vec![*note.header()],
327        )
328        .unwrap_err();
329
330        assert_matches!(
331            error,
332            TransactionHeaderError::NoteCreatedAndConsumed(note_id) if note_id == note.id()
333        );
334    }
335
336    #[test]
337    fn deserialization_rejects_duplicate_output_notes() {
338        let note = Note::mock_noop(Word::empty());
339        let invalid_header = TransactionHeader::new_unchecked(
340            TransactionId::new(Word::empty(), Word::empty(), Word::empty(), Word::empty()),
341            account_id(),
342            Word::from([1_u32, 2, 3, 4]),
343            Word::from([5_u32, 6, 7, 8]),
344            InputNotes::default(),
345            vec![*note.header(), *note.header()],
346        );
347
348        let error = TransactionHeader::read_from_bytes(&invalid_header.to_bytes()).unwrap_err();
349
350        assert_matches!(
351            error,
352            DeserializationError::InvalidValue(message)
353                if message
354                    == format!(
355                        "output note {} appears twice in the transaction header",
356                        note.id()
357                    )
358        );
359    }
360}