kona_comp/
channel_out.rs

1//! Contains the `ChannelOut` primitive for Optimism.
2
3use crate::{ChannelCompressor, CompressorError};
4use alloc::{vec, vec::Vec};
5use kona_genesis::RollupConfig;
6use kona_protocol::{Batch, ChannelId, Frame};
7use rand::{RngCore, SeedableRng, rngs::SmallRng};
8
9/// The frame overhead.
10const FRAME_V0_OVERHEAD: usize = 23;
11
12/// An error returned by the [ChannelOut] when adding single batches.
13#[derive(Debug, Clone, PartialEq, thiserror::Error)]
14pub enum ChannelOutError {
15    /// The channel is closed.
16    #[error("The channel is already closed")]
17    ChannelClosed,
18    /// The max frame size is too small.
19    #[error("The max frame size is too small")]
20    MaxFrameSizeTooSmall,
21    /// Missing compressed batch data.
22    #[error("Missing compressed batch data")]
23    MissingData,
24    /// An error from compression.
25    #[error("Error from compression")]
26    Compression(#[from] CompressorError),
27    /// An error encoding the `Batch`.
28    #[error("Error encoding the batch")]
29    BatchEncoding,
30    /// The encoded batch exceeds the max RLP bytes per channel.
31    #[error("The encoded batch exceeds the max RLP bytes per channel")]
32    ExceedsMaxRlpBytesPerChannel,
33}
34
35/// [ChannelOut] constructs a channel from compressed, encoded batch data.
36#[allow(missing_debug_implementations)]
37pub struct ChannelOut<'a, C>
38where
39    C: ChannelCompressor,
40{
41    /// The unique identifier for the channel.
42    pub id: ChannelId,
43    /// A reference to the [RollupConfig] used to
44    /// check the max RLP bytes per channel when
45    /// encoding and accepting the
46    pub config: &'a RollupConfig,
47    /// The rlp length of the channel.
48    pub rlp_length: u64,
49    /// Whether the channel is closed.
50    pub closed: bool,
51    /// The frame number.
52    pub frame_number: u16,
53    /// The compressor.
54    pub compressor: C,
55}
56
57impl<'a, C> ChannelOut<'a, C>
58where
59    C: ChannelCompressor,
60{
61    /// Creates a new [ChannelOut] with the given [ChannelId].
62    pub const fn new(id: ChannelId, config: &'a RollupConfig, compressor: C) -> Self {
63        Self { id, config, rlp_length: 0, frame_number: 0, closed: false, compressor }
64    }
65
66    /// Resets the [ChannelOut] to its initial state.
67    pub fn reset(&mut self) {
68        self.rlp_length = 0;
69        self.frame_number = 0;
70        self.closed = false;
71        self.compressor.reset();
72        // `getrandom` isn't available for wasm and risc targets
73        // Thread-based RNGs are not available for no_std
74        // So we must use a seeded RNG.
75        let mut small_rng = SmallRng::seed_from_u64(43);
76        SmallRng::fill_bytes(&mut small_rng, &mut self.id);
77    }
78
79    /// Accepts the given [Batch] data into the [ChannelOut], compressing it
80    /// into frames.
81    pub fn add_batch(&mut self, batch: Batch) -> Result<(), ChannelOutError> {
82        if self.closed {
83            return Err(ChannelOutError::ChannelClosed);
84        }
85
86        // Encode the batch.
87        let mut buf = vec![];
88        batch.encode(&mut buf).map_err(|_| ChannelOutError::BatchEncoding)?;
89
90        // Validate that the RLP length is within the channel's limits.
91        let max_rlp_bytes_per_channel = self.config.max_rlp_bytes_per_channel(batch.timestamp());
92        if self.rlp_length + buf.len() as u64 > max_rlp_bytes_per_channel {
93            return Err(ChannelOutError::ExceedsMaxRlpBytesPerChannel);
94        }
95
96        self.compressor.write(&buf)?;
97
98        Ok(())
99    }
100
101    /// Returns the total amount of rlp-encoded input bytes.
102    pub const fn input_bytes(&self) -> u64 {
103        self.rlp_length
104    }
105
106    /// Returns the number of bytes ready to be output to a frame.
107    pub fn ready_bytes(&self) -> usize {
108        self.compressor.len()
109    }
110
111    /// Flush the internal compressor.
112    pub fn flush(&mut self) -> Result<(), ChannelOutError> {
113        self.compressor.flush()?;
114        Ok(())
115    }
116
117    /// Closes the channel if not already closed.
118    pub const fn close(&mut self) {
119        self.closed = true;
120    }
121
122    /// Outputs a [Frame] from the [ChannelOut].
123    pub fn output_frame(&mut self, max_size: usize) -> Result<Frame, ChannelOutError> {
124        if max_size < FRAME_V0_OVERHEAD {
125            return Err(ChannelOutError::MaxFrameSizeTooSmall);
126        }
127
128        // Construct an empty frame.
129        let mut frame =
130            Frame { id: self.id, number: self.frame_number, is_last: self.closed, data: vec![] };
131
132        let mut max_size = max_size - FRAME_V0_OVERHEAD;
133        if max_size > self.ready_bytes() {
134            max_size = self.ready_bytes();
135        }
136
137        // Read `max_size` bytes from the compressed data.
138        let mut data = Vec::with_capacity(max_size);
139        self.compressor.read(&mut data).map_err(ChannelOutError::Compression)?;
140        frame.data.extend_from_slice(data.as_slice());
141
142        // Update the compressed data.
143        self.frame_number += 1;
144        Ok(frame)
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151    use crate::{CompressorWriter, test_utils::MockCompressor};
152    use kona_protocol::{SingleBatch, SpanBatch};
153
154    #[test]
155    fn test_output_frame_max_size_too_small() {
156        let config = RollupConfig::default();
157        let mut channel = ChannelOut::new(ChannelId::default(), &config, MockCompressor::default());
158        assert_eq!(channel.output_frame(0), Err(ChannelOutError::MaxFrameSizeTooSmall));
159    }
160
161    #[test]
162    fn test_channel_out_output_frame_no_data() {
163        let config = RollupConfig::default();
164        let mut channel = ChannelOut::new(
165            ChannelId::default(),
166            &config,
167            MockCompressor { read_error: true, compressed: Some(Default::default()) },
168        );
169        let err = channel.output_frame(FRAME_V0_OVERHEAD).unwrap_err();
170        assert_eq!(err, ChannelOutError::Compression(CompressorError::Full));
171    }
172
173    #[test]
174    fn test_channel_out_output() {
175        let config = RollupConfig::default();
176        let mut channel = ChannelOut::new(
177            ChannelId::default(),
178            &config,
179            MockCompressor { compressed: Some(Default::default()), ..Default::default() },
180        );
181        let frame = channel.output_frame(FRAME_V0_OVERHEAD).unwrap();
182        assert_eq!(frame.id, ChannelId::default());
183        assert_eq!(frame.number, 0);
184        assert!(!frame.is_last);
185    }
186
187    #[test]
188    fn test_channel_out_reset() {
189        let config = RollupConfig::default();
190        let mut channel = ChannelOut {
191            id: ChannelId::default(),
192            config: &config,
193            rlp_length: 10,
194            closed: true,
195            frame_number: 11,
196            compressor: MockCompressor::default(),
197        };
198        channel.reset();
199        assert_eq!(channel.rlp_length, 0);
200        assert_eq!(channel.frame_number, 0);
201        // The odds of a randomized channel id being equal to the
202        // default are so astronomically low, this test will always pass.
203        // The randomized [u8; 16] is about 1/255^16.
204        assert!(channel.id != ChannelId::default());
205        assert!(!channel.closed);
206    }
207
208    #[test]
209    fn test_channel_out_ready_bytes_empty() {
210        let config = RollupConfig::default();
211        let channel = ChannelOut::new(ChannelId::default(), &config, MockCompressor::default());
212        assert_eq!(channel.ready_bytes(), 0);
213    }
214
215    #[test]
216    fn test_channel_out_ready_bytes_some() {
217        let config = RollupConfig::default();
218        let mut channel = ChannelOut::new(ChannelId::default(), &config, MockCompressor::default());
219        channel.compressor.write(&[1, 2, 3]).unwrap();
220        assert_eq!(channel.ready_bytes(), 3);
221    }
222
223    #[test]
224    fn test_channel_out_close() {
225        let config = RollupConfig::default();
226        let mut channel = ChannelOut::new(ChannelId::default(), &config, MockCompressor::default());
227        assert!(!channel.closed);
228
229        channel.close();
230        assert!(channel.closed);
231    }
232
233    #[test]
234    fn test_channel_out_add_batch_closed() {
235        let config = RollupConfig::default();
236        let mut channel = ChannelOut::new(ChannelId::default(), &config, MockCompressor::default());
237        channel.close();
238
239        let batch = Batch::Single(SingleBatch::default());
240        assert_eq!(channel.add_batch(batch), Err(ChannelOutError::ChannelClosed));
241    }
242
243    #[test]
244    fn test_channel_out_empty_span_batch_decode_error() {
245        let config = RollupConfig::default();
246        let mut channel = ChannelOut::new(ChannelId::default(), &config, MockCompressor::default());
247
248        let batch = Batch::Span(SpanBatch::default());
249        assert_eq!(channel.add_batch(batch), Err(ChannelOutError::BatchEncoding));
250    }
251
252    #[test]
253    fn test_channel_out_max_rlp_bytes_per_channel() {
254        let config = RollupConfig::default();
255        let mut channel = ChannelOut::new(ChannelId::default(), &config, MockCompressor::default());
256
257        let batch = Batch::Single(SingleBatch::default());
258        channel.rlp_length = config.max_rlp_bytes_per_channel(batch.timestamp());
259
260        assert_eq!(channel.add_batch(batch), Err(ChannelOutError::ExceedsMaxRlpBytesPerChannel));
261    }
262
263    #[test]
264    fn test_channel_out_add_batch() {
265        let config = RollupConfig::default();
266        let mut channel = ChannelOut::new(ChannelId::default(), &config, MockCompressor::default());
267
268        let batch = Batch::Single(SingleBatch::default());
269        assert_eq!(channel.add_batch(batch), Ok(()));
270    }
271}