Skip to main content

miden_protocol/batch/
output.rs

1use alloc::vec::Vec;
2
3use crate::block::BlockNumber;
4use crate::errors::BatchOutputError;
5use crate::vm::StackOutputs;
6use crate::{Felt, Word};
7
8// BATCH OUTPUTS
9// ================================================================================================
10
11/// The public outputs produced by the batch kernel.
12///
13/// This is the parsed, typed form of the kernel's output stack.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct BatchOutputs {
16    /// The commitment to the batch's input notes.
17    input_notes_commitment: Word,
18    /// The root of the batch's note tree (the [`BatchNoteTree`](crate::batch::BatchNoteTree)) over
19    /// the batch's output notes.
20    batch_note_tree_root: Word,
21    /// The block number at which the batch expires.
22    batch_expiration_block_num: BlockNumber,
23}
24
25impl BatchOutputs {
26    // OUTPUT STACK LAYOUT
27    // --------------------------------------------------------------------------------------------
28
29    /// The element index at which the input notes commitment word starts on the output stack.
30    pub const INPUT_NOTES_COMMITMENT_WORD_IDX: usize = 0;
31    /// The element index at which the batch note tree root word starts on the output stack.
32    pub const BATCH_NOTE_TREE_ROOT_WORD_IDX: usize = 4;
33    /// The element index at which the batch expiration block number is stored on the output stack.
34    pub const BATCH_EXPIRATION_BLOCK_NUM_ELEMENT_IDX: usize = 8;
35
36    // CONSTRUCTOR
37    // --------------------------------------------------------------------------------------------
38
39    /// Returns a new [`BatchOutputs`] instantiated from the provided data.
40    pub fn new(
41        input_notes_commitment: Word,
42        batch_note_tree_root: Word,
43        batch_expiration_block_num: BlockNumber,
44    ) -> Self {
45        Self {
46            input_notes_commitment,
47            batch_note_tree_root,
48            batch_expiration_block_num,
49        }
50    }
51
52    // PARSER
53    // --------------------------------------------------------------------------------------------
54
55    /// Parses the batch kernel's output stack into a [`BatchOutputs`].
56    ///
57    /// # Errors
58    ///
59    /// Returns [`BatchOutputError::OutputStackInvalid`] if:
60    /// - a required output word or element is missing from the stack;
61    /// - the cells following `batch_expiration_block_num` (positions 9..16) are not all zero.
62    ///
63    /// Returns [`BatchOutputError::ExpirationBlockNumberTooLarge`] if `batch_expiration_block_num`
64    /// does not fit into a `u32`.
65    pub fn parse(stack: &StackOutputs) -> Result<Self, BatchOutputError> {
66        let input_notes_commitment =
67            stack.get_word(Self::INPUT_NOTES_COMMITMENT_WORD_IDX).ok_or_else(|| {
68                BatchOutputError::OutputStackInvalid(
69                    "input notes commitment word missing from output stack".into(),
70                )
71            })?;
72        let batch_note_tree_root =
73            stack.get_word(Self::BATCH_NOTE_TREE_ROOT_WORD_IDX).ok_or_else(|| {
74                BatchOutputError::OutputStackInvalid(
75                    "batch note tree root word missing from output stack".into(),
76                )
77            })?;
78
79        let expiration_felt =
80            stack.get_element(Self::BATCH_EXPIRATION_BLOCK_NUM_ELEMENT_IDX).ok_or_else(|| {
81                BatchOutputError::OutputStackInvalid(
82                    "batch expiration block number missing from output stack".into(),
83                )
84            })?;
85
86        // Every cell after batch_expiration_block_num must be zero padding.
87        if stack[Self::BATCH_EXPIRATION_BLOCK_NUM_ELEMENT_IDX + 1..]
88            .iter()
89            .any(|&felt| felt != Felt::ZERO)
90        {
91            return Err(BatchOutputError::OutputStackInvalid(
92                "batch_expiration_block_num must be followed by zero padding".into(),
93            ));
94        }
95
96        let batch_expiration_block_num = u32::try_from(expiration_felt.as_canonical_u64())
97            .map_err(|_| BatchOutputError::ExpirationBlockNumberTooLarge(expiration_felt))?
98            .into();
99
100        Ok(Self::new(
101            input_notes_commitment,
102            batch_note_tree_root,
103            batch_expiration_block_num,
104        ))
105    }
106
107    // PUBLIC ACCESSORS
108    // --------------------------------------------------------------------------------------------
109
110    /// Returns the commitment to the batch's input notes.
111    pub fn input_notes_commitment(&self) -> Word {
112        self.input_notes_commitment
113    }
114
115    /// Returns the root of the batch's note tree.
116    pub fn batch_note_tree_root(&self) -> Word {
117        self.batch_note_tree_root
118    }
119
120    /// Returns the block number at which the batch expires.
121    pub fn batch_expiration_block_num(&self) -> BlockNumber {
122        self.batch_expiration_block_num
123    }
124
125    // CONVERSIONS
126    // --------------------------------------------------------------------------------------------
127
128    /// Encodes these [`BatchOutputs`] into the batch kernel's output stack.
129    ///
130    /// This is the inverse of [`BatchOutputs::parse`]; the resulting stack is laid out as:
131    ///
132    /// ```text
133    /// [INPUT_NOTES_COMMITMENT, BATCH_NOTE_TREE_ROOT, batch_expiration_block_num]
134    /// ```
135    pub fn into_stack_outputs(self) -> StackOutputs {
136        let mut outputs: Vec<Felt> = Vec::with_capacity(9);
137        outputs.extend_from_slice(self.input_notes_commitment.as_elements());
138        outputs.extend_from_slice(self.batch_note_tree_root.as_elements());
139        outputs.push(Felt::from(self.batch_expiration_block_num));
140
141        StackOutputs::new(&outputs).expect("number of stack outputs should be <= 16")
142    }
143}
144
145// TESTS
146// ================================================================================================
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn parse_returns_outputs_for_well_formed_stack() {
154        let input_notes_commitment =
155            Word::from([Felt::from(1u32), Felt::from(2u32), Felt::from(3u32), Felt::from(4u32)]);
156        let batch_note_tree_root =
157            Word::from([Felt::from(5u32), Felt::from(6u32), Felt::from(7u32), Felt::from(8u32)]);
158        let elements = [
159            Felt::from(1u32),
160            Felt::from(2u32),
161            Felt::from(3u32),
162            Felt::from(4u32),
163            Felt::from(5u32),
164            Felt::from(6u32),
165            Felt::from(7u32),
166            Felt::from(8u32),
167            Felt::from(1234u32),
168        ];
169        let stack = StackOutputs::new(&elements).unwrap();
170
171        let outputs = BatchOutputs::parse(&stack).unwrap();
172
173        assert_eq!(outputs.input_notes_commitment(), input_notes_commitment);
174        assert_eq!(outputs.batch_note_tree_root(), batch_note_tree_root);
175        assert_eq!(outputs.batch_expiration_block_num(), BlockNumber::from(1234u32));
176    }
177
178    #[test]
179    fn parse_rejects_non_zero_padding() {
180        // A valid 9-element output followed by a non-zero felt in the padding region (>= idx 9).
181        let elements = [
182            Felt::ZERO,
183            Felt::ZERO,
184            Felt::ZERO,
185            Felt::ZERO,
186            Felt::ZERO,
187            Felt::ZERO,
188            Felt::ZERO,
189            Felt::ZERO,
190            Felt::from(7u32),
191            Felt::from(1u32),
192        ];
193        let stack = StackOutputs::new(&elements).unwrap();
194
195        assert!(matches!(
196            BatchOutputs::parse(&stack),
197            Err(BatchOutputError::OutputStackInvalid(_))
198        ));
199    }
200
201    #[test]
202    fn parse_rejects_oversized_expiration() {
203        // An expiration value that does not fit into a u32.
204        let oversized = Felt::from(u32::MAX) + Felt::from(1u32);
205        let elements = [
206            Felt::ZERO,
207            Felt::ZERO,
208            Felt::ZERO,
209            Felt::ZERO,
210            Felt::ZERO,
211            Felt::ZERO,
212            Felt::ZERO,
213            Felt::ZERO,
214            oversized,
215        ];
216        let stack = StackOutputs::new(&elements).unwrap();
217
218        assert!(matches!(
219            BatchOutputs::parse(&stack),
220            Err(BatchOutputError::ExpirationBlockNumberTooLarge(_))
221        ));
222    }
223}