miden_protocol/transaction/
tx_header.rs1use 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#[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 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 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 pub fn id(&self) -> TransactionId {
139 self.id
140 }
141
142 pub fn account_id(&self) -> AccountId {
144 self.account_id
145 }
146
147 pub fn initial_state_commitment(&self) -> Word {
151 self.initial_state_commitment
152 }
153
154 pub fn final_state_commitment(&self) -> Word {
156 self.final_state_commitment
157 }
158
159 pub fn input_notes(&self) -> &InputNotes<InputNoteCommitment> {
167 &self.input_notes
168 }
169
170 pub fn output_notes(&self) -> &[NoteHeader] {
178 &self.output_notes
179 }
180}
181
182impl From<&ProvenTransaction> for TransactionHeader {
183 fn from(tx: &ProvenTransaction) -> Self {
185 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 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
212impl 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#[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}