reth_static_file_types/
segment.rs

1use crate::{BlockNumber, Compression};
2use alloy_primitives::TxNumber;
3use derive_more::Display;
4use serde::{Deserialize, Serialize};
5use std::{ops::RangeInclusive, str::FromStr};
6use strum::{AsRefStr, EnumIter, EnumString};
7
8#[derive(
9    Debug,
10    Copy,
11    Clone,
12    Eq,
13    PartialEq,
14    Hash,
15    Ord,
16    PartialOrd,
17    Deserialize,
18    Serialize,
19    EnumString,
20    EnumIter,
21    AsRefStr,
22    Display,
23)]
24#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
25/// Segment of the data that can be moved to static files.
26pub enum StaticFileSegment {
27    #[strum(serialize = "headers")]
28    /// Static File segment responsible for the `CanonicalHeaders`, `Headers`,
29    /// `HeaderTerminalDifficulties` tables.
30    Headers,
31    #[strum(serialize = "transactions")]
32    /// Static File segment responsible for the `Transactions` table.
33    Transactions,
34    #[strum(serialize = "receipts")]
35    /// Static File segment responsible for the `Receipts` table.
36    Receipts,
37}
38
39impl StaticFileSegment {
40    /// Returns the segment as a string.
41    pub const fn as_str(&self) -> &'static str {
42        match self {
43            Self::Headers => "headers",
44            Self::Transactions => "transactions",
45            Self::Receipts => "receipts",
46        }
47    }
48
49    /// Returns the default configuration of the segment.
50    pub const fn config(&self) -> SegmentConfig {
51        SegmentConfig { compression: Compression::Lz4 }
52    }
53
54    /// Returns the number of columns for the segment
55    pub const fn columns(&self) -> usize {
56        match self {
57            Self::Headers => 3,
58            Self::Transactions | Self::Receipts => 1,
59        }
60    }
61
62    /// Returns the default file name for the provided segment and range.
63    pub fn filename(&self, block_range: &SegmentRangeInclusive) -> String {
64        // ATTENTION: if changing the name format, be sure to reflect those changes in
65        // [`Self::parse_filename`].
66        format!("static_file_{}_{}_{}", self.as_ref(), block_range.start(), block_range.end())
67    }
68
69    /// Returns file name for the provided segment and range, alongside filters, compression.
70    pub fn filename_with_configuration(
71        &self,
72        compression: Compression,
73        block_range: &SegmentRangeInclusive,
74    ) -> String {
75        let prefix = self.filename(block_range);
76
77        let filters_name = "none".to_string();
78
79        // ATTENTION: if changing the name format, be sure to reflect those changes in
80        // [`Self::parse_filename`.]
81        format!("{prefix}_{}_{}", filters_name, compression.as_ref())
82    }
83
84    /// Parses a filename into a `StaticFileSegment` and its expected block range.
85    ///
86    /// The filename is expected to follow the format:
87    /// "`static_file`_{segment}_{`block_start`}_{`block_end`}". This function checks
88    /// for the correct prefix ("`static_file`"), and then parses the segment and the inclusive
89    /// ranges for blocks. It ensures that the start of each range is less than or equal to the
90    /// end.
91    ///
92    /// # Returns
93    /// - `Some((segment, block_range))` if parsing is successful and all conditions are met.
94    /// - `None` if any condition fails, such as an incorrect prefix, parsing error, or invalid
95    ///   range.
96    ///
97    /// # Note
98    /// This function is tightly coupled with the naming convention defined in [`Self::filename`].
99    /// Any changes in the filename format in `filename` should be reflected here.
100    pub fn parse_filename(name: &str) -> Option<(Self, SegmentRangeInclusive)> {
101        let mut parts = name.split('_');
102        if !(parts.next() == Some("static") && parts.next() == Some("file")) {
103            return None
104        }
105
106        let segment = Self::from_str(parts.next()?).ok()?;
107        let (block_start, block_end) = (parts.next()?.parse().ok()?, parts.next()?.parse().ok()?);
108
109        if block_start > block_end {
110            return None
111        }
112
113        Some((segment, SegmentRangeInclusive::new(block_start, block_end)))
114    }
115
116    /// Returns `true` if the segment is `StaticFileSegment::Headers`.
117    pub const fn is_headers(&self) -> bool {
118        matches!(self, Self::Headers)
119    }
120
121    /// Returns `true` if the segment is `StaticFileSegment::Receipts`.
122    pub const fn is_receipts(&self) -> bool {
123        matches!(self, Self::Receipts)
124    }
125
126    /// Returns `true` if the segment is `StaticFileSegment::Receipts` or
127    /// `StaticFileSegment::Transactions`.
128    pub const fn is_tx_based(&self) -> bool {
129        matches!(self, Self::Receipts | Self::Transactions)
130    }
131}
132
133/// A segment header that contains information common to all segments. Used for storage.
134#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Clone)]
135pub struct SegmentHeader {
136    /// Defines the expected block range for a static file segment. This attribute is crucial for
137    /// scenarios where the file contains no data, allowing for a representation beyond a
138    /// simple `start..=start` range. It ensures clarity in differentiating between an empty file
139    /// and a file with a single block numbered 0.
140    expected_block_range: SegmentRangeInclusive,
141    /// Block range of data on the static file segment
142    block_range: Option<SegmentRangeInclusive>,
143    /// Transaction range of data of the static file segment
144    tx_range: Option<SegmentRangeInclusive>,
145    /// Segment type
146    segment: StaticFileSegment,
147}
148
149impl SegmentHeader {
150    /// Returns [`SegmentHeader`].
151    pub const fn new(
152        expected_block_range: SegmentRangeInclusive,
153        block_range: Option<SegmentRangeInclusive>,
154        tx_range: Option<SegmentRangeInclusive>,
155        segment: StaticFileSegment,
156    ) -> Self {
157        Self { expected_block_range, block_range, tx_range, segment }
158    }
159
160    /// Returns the static file segment kind.
161    pub const fn segment(&self) -> StaticFileSegment {
162        self.segment
163    }
164
165    /// Returns the block range.
166    pub const fn block_range(&self) -> Option<&SegmentRangeInclusive> {
167        self.block_range.as_ref()
168    }
169
170    /// Returns the transaction range.
171    pub const fn tx_range(&self) -> Option<&SegmentRangeInclusive> {
172        self.tx_range.as_ref()
173    }
174
175    /// The expected block start of the segment.
176    pub const fn expected_block_start(&self) -> BlockNumber {
177        self.expected_block_range.start()
178    }
179
180    /// The expected block end of the segment.
181    pub const fn expected_block_end(&self) -> BlockNumber {
182        self.expected_block_range.end()
183    }
184
185    /// Returns the first block number of the segment.
186    pub fn block_start(&self) -> Option<BlockNumber> {
187        self.block_range.as_ref().map(|b| b.start())
188    }
189
190    /// Returns the last block number of the segment.
191    pub fn block_end(&self) -> Option<BlockNumber> {
192        self.block_range.as_ref().map(|b| b.end())
193    }
194
195    /// Returns the first transaction number of the segment.
196    pub fn tx_start(&self) -> Option<TxNumber> {
197        self.tx_range.as_ref().map(|t| t.start())
198    }
199
200    /// Returns the last transaction number of the segment.
201    pub fn tx_end(&self) -> Option<TxNumber> {
202        self.tx_range.as_ref().map(|t| t.end())
203    }
204
205    /// Number of transactions.
206    pub fn tx_len(&self) -> Option<u64> {
207        self.tx_range.as_ref().map(|r| (r.end() + 1) - r.start())
208    }
209
210    /// Number of blocks.
211    pub fn block_len(&self) -> Option<u64> {
212        self.block_range.as_ref().map(|r| (r.end() + 1) - r.start())
213    }
214
215    /// Increments block end range depending on segment
216    pub fn increment_block(&mut self) -> BlockNumber {
217        if let Some(block_range) = &mut self.block_range {
218            block_range.end += 1;
219            block_range.end
220        } else {
221            self.block_range = Some(SegmentRangeInclusive::new(
222                self.expected_block_start(),
223                self.expected_block_start(),
224            ));
225            self.expected_block_start()
226        }
227    }
228
229    /// Increments tx end range depending on segment
230    pub fn increment_tx(&mut self) {
231        match self.segment {
232            StaticFileSegment::Headers => (),
233            StaticFileSegment::Transactions | StaticFileSegment::Receipts => {
234                if let Some(tx_range) = &mut self.tx_range {
235                    tx_range.end += 1;
236                } else {
237                    self.tx_range = Some(SegmentRangeInclusive::new(0, 0));
238                }
239            }
240        }
241    }
242
243    /// Removes `num` elements from end of tx or block range.
244    pub fn prune(&mut self, num: u64) {
245        match self.segment {
246            StaticFileSegment::Headers => {
247                if let Some(range) = &mut self.block_range {
248                    if num > range.end - range.start {
249                        self.block_range = None;
250                    } else {
251                        range.end = range.end.saturating_sub(num);
252                    }
253                };
254            }
255            StaticFileSegment::Transactions | StaticFileSegment::Receipts => {
256                if let Some(range) = &mut self.tx_range {
257                    if num > range.end - range.start {
258                        self.tx_range = None;
259                    } else {
260                        range.end = range.end.saturating_sub(num);
261                    }
262                };
263            }
264        };
265    }
266
267    /// Sets a new `block_range`.
268    pub fn set_block_range(&mut self, block_start: BlockNumber, block_end: BlockNumber) {
269        if let Some(block_range) = &mut self.block_range {
270            block_range.start = block_start;
271            block_range.end = block_end;
272        } else {
273            self.block_range = Some(SegmentRangeInclusive::new(block_start, block_end))
274        }
275    }
276
277    /// Sets a new `tx_range`.
278    pub fn set_tx_range(&mut self, tx_start: TxNumber, tx_end: TxNumber) {
279        if let Some(tx_range) = &mut self.tx_range {
280            tx_range.start = tx_start;
281            tx_range.end = tx_end;
282        } else {
283            self.tx_range = Some(SegmentRangeInclusive::new(tx_start, tx_end))
284        }
285    }
286
287    /// Returns the row offset which depends on whether the segment is block or transaction based.
288    pub fn start(&self) -> Option<u64> {
289        match self.segment {
290            StaticFileSegment::Headers => self.block_start(),
291            StaticFileSegment::Transactions | StaticFileSegment::Receipts => self.tx_start(),
292        }
293    }
294}
295
296/// Configuration used on the segment.
297#[derive(Debug, Clone, Copy)]
298pub struct SegmentConfig {
299    /// Compression used on the segment
300    pub compression: Compression,
301}
302
303/// Helper type to handle segment transaction and block INCLUSIVE ranges.
304///
305/// They can be modified on a hot loop, which makes the `std::ops::RangeInclusive` a poor fit.
306#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Clone, Copy)]
307pub struct SegmentRangeInclusive {
308    start: u64,
309    end: u64,
310}
311
312impl SegmentRangeInclusive {
313    /// Creates a new [`SegmentRangeInclusive`]
314    pub const fn new(start: u64, end: u64) -> Self {
315        Self { start, end }
316    }
317
318    /// Start of the inclusive range
319    pub const fn start(&self) -> u64 {
320        self.start
321    }
322
323    /// End of the inclusive range
324    pub const fn end(&self) -> u64 {
325        self.end
326    }
327}
328
329impl std::fmt::Display for SegmentRangeInclusive {
330    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
331        write!(f, "{}..={}", self.start, self.end)
332    }
333}
334
335impl From<RangeInclusive<u64>> for SegmentRangeInclusive {
336    fn from(value: RangeInclusive<u64>) -> Self {
337        Self { start: *value.start(), end: *value.end() }
338    }
339}
340
341impl From<&SegmentRangeInclusive> for RangeInclusive<u64> {
342    fn from(value: &SegmentRangeInclusive) -> Self {
343        value.start()..=value.end()
344    }
345}
346
347impl From<SegmentRangeInclusive> for RangeInclusive<u64> {
348    fn from(value: SegmentRangeInclusive) -> Self {
349        (&value).into()
350    }
351}
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356
357    #[test]
358    fn test_filename() {
359        let test_vectors = [
360            (StaticFileSegment::Headers, 2..=30, "static_file_headers_2_30", None),
361            (StaticFileSegment::Receipts, 30..=300, "static_file_receipts_30_300", None),
362            (
363                StaticFileSegment::Transactions,
364                1_123_233..=11_223_233,
365                "static_file_transactions_1123233_11223233",
366                None,
367            ),
368            (
369                StaticFileSegment::Headers,
370                2..=30,
371                "static_file_headers_2_30_none_lz4",
372                Some(Compression::Lz4),
373            ),
374            (
375                StaticFileSegment::Headers,
376                2..=30,
377                "static_file_headers_2_30_none_zstd",
378                Some(Compression::Zstd),
379            ),
380            (
381                StaticFileSegment::Headers,
382                2..=30,
383                "static_file_headers_2_30_none_zstd-dict",
384                Some(Compression::ZstdWithDictionary),
385            ),
386        ];
387
388        for (segment, block_range, filename, compression) in test_vectors {
389            let block_range: SegmentRangeInclusive = block_range.into();
390            if let Some(compression) = compression {
391                assert_eq!(
392                    segment.filename_with_configuration(compression, &block_range),
393                    filename
394                );
395            } else {
396                assert_eq!(segment.filename(&block_range), filename);
397            }
398
399            assert_eq!(StaticFileSegment::parse_filename(filename), Some((segment, block_range)));
400        }
401
402        assert_eq!(StaticFileSegment::parse_filename("static_file_headers_2"), None);
403        assert_eq!(StaticFileSegment::parse_filename("static_file_headers_"), None);
404    }
405}