Skip to main content

ab_archiving/
reconstructor.rs

1use crate::archiver::{Segment, SegmentItem};
2use ab_core_primitives::block::BlockNumber;
3use ab_core_primitives::pieces::Piece;
4use ab_core_primitives::segments::{
5    ArchivedHistorySegment, LastArchivedBlock, LocalSegmentIndex, RecordedHistorySegment,
6    SegmentHeader,
7};
8use ab_erasure_coding::{ErasureCoding, ErasureCodingError, ShardsBitmap};
9use alloc::vec::Vec;
10use core::{array, mem};
11use parity_scale_codec::Decode;
12
13/// Reconstructor-related instantiation error
14#[derive(Debug, Clone, PartialEq, thiserror::Error)]
15pub enum ReconstructorError {
16    /// Error during data shards reconstruction
17    #[error("Error during data shards reconstruction: {0}")]
18    DataShardsReconstruction(#[from] ErasureCodingError),
19    /// Not enough shards
20    #[error("Not enough shards: {num_shards}")]
21    NotEnoughShards { num_shards: usize },
22    /// Segment size is not bigger than record size
23    #[error("Error during segment decoding: {0}")]
24    SegmentDecoding(parity_scale_codec::Error),
25    /// Incorrect segment order, each next segment must have monotonically increasing segment index
26    #[error(
27        "Incorrect segment order, expected index {expected_segment_index}, actual \
28        {actual_segment_index}"
29    )]
30    IncorrectSegmentOrder {
31        expected_segment_index: LocalSegmentIndex,
32        actual_segment_index: LocalSegmentIndex,
33    },
34}
35
36/// Data structure that contains information reconstructed from given segment (potentially using
37/// information from segments that were added previously)
38#[derive(Debug, Default, Clone, Eq, PartialEq)]
39pub struct ReconstructedContents {
40    /// Segment header stored in a segment
41    pub segment_header: Option<SegmentHeader>,
42    /// Reconstructed encoded blocks with their block numbers
43    pub blocks: Vec<(BlockNumber, Vec<u8>)>,
44}
45
46/// Reconstructor helps to retrieve blocks from archived pieces.
47#[derive(Debug, Clone)]
48pub struct Reconstructor {
49    /// Erasure coding data structure
50    erasure_coding: ErasureCoding,
51    /// Index of the last segment added to reconstructor
52    last_segment_index: Option<LocalSegmentIndex>,
53    /// Partially reconstructed block waiting for more data
54    partial_block: Option<Vec<u8>>,
55}
56
57impl Reconstructor {
58    /// Create a new instance
59    pub fn new(erasure_coding: ErasureCoding) -> Self {
60        Self {
61            erasure_coding,
62            last_segment_index: None,
63            partial_block: None,
64        }
65    }
66
67    /// Given a set of pieces of a segment of the archived history (any half of all pieces are
68    /// required to be present, the rest will be recovered automatically due to use of erasure
69    /// coding if needed), reconstructs and returns the segment itself.
70    ///
71    /// Does not modify the internal state of the reconstructor.
72    pub fn reconstruct_segment(
73        &self,
74        // TODO: Improve API to not use `Option` anymore
75        segment_pieces: &[Option<Piece>],
76    ) -> Result<Segment, ReconstructorError> {
77        if segment_pieces.len() < ArchivedHistorySegment::NUM_PIECES {
78            return Err(ReconstructorError::NotEnoughShards {
79                num_shards: segment_pieces.len(),
80            });
81        }
82        let mut segment_data = RecordedHistorySegment::new_boxed();
83
84        if !segment_pieces
85            .iter()
86            .zip(segment_data.iter_mut())
87            .all(|(maybe_piece, record)| {
88                if let Some(piece) = maybe_piece {
89                    // Fancy way to insert value to avoid going through stack (if naive
90                    // dereferencing is used) and potentially causing stack overflow as the result
91                    record.copy_from_slice(&*piece.record);
92                    true
93                } else {
94                    false
95                }
96            })
97        {
98            // If not all data pieces are available, need to reconstruct data shards using erasure
99            // coding
100            let (source_segment_pieces, parity_segment_pieces) =
101                segment_pieces.split_at(RecordedHistorySegment::NUM_RAW_RECORDS);
102
103            let mut source_present = ShardsBitmap::none();
104            for (index, (output_record, maybe_source_piece)) in segment_data
105                .iter_mut()
106                .zip(source_segment_pieces)
107                .enumerate()
108            {
109                if let Some(input_piece) = maybe_source_piece {
110                    // Fancy way to insert value to avoid going through stack (if naive
111                    // dereferencing is used) and potentially causing stack overflow as the result
112                    output_record.copy_from_slice(&*input_piece.record);
113                    source_present.set(index);
114                }
115            }
116
117            // Parity records are read-only here, so missing ones simply have no memory
118            let parity_records = array::from_fn(|index| {
119                parity_segment_pieces[index]
120                    .as_ref()
121                    .map(|input_piece| &input_piece.record)
122            });
123
124            self.erasure_coding.recover_source_scattered(
125                {
126                    let records: &mut [_; RecordedHistorySegment::NUM_RAW_RECORDS] =
127                        segment_data.as_mut();
128                    let mut records = records.iter_mut();
129
130                    array::from_fn::<_, { RecordedHistorySegment::NUM_RAW_RECORDS }, _>(|_| {
131                        records
132                            .next()
133                            .expect("Number of records matches the array size; qed")
134                    })
135                },
136                &source_present,
137                parity_records,
138            )?;
139        }
140
141        let segment = Segment::decode(&mut AsRef::<[u8]>::as_ref(segment_data.as_ref()))
142            .map_err(ReconstructorError::SegmentDecoding)?;
143
144        Ok(segment)
145    }
146
147    /// Given a set of pieces of a segment of the archived history (any half of all pieces are
148    /// required to be present, the rest will be recovered automatically due to use of erasure
149    /// coding if needed), reconstructs and returns segment header and a list of encoded blocks with
150    /// corresponding block numbers.
151    ///
152    /// It is possible to start with any segment, but when next segment is pushed, it needs to
153    /// follow the previous one or else error will be returned.
154    pub fn add_segment(
155        &mut self,
156        segment_pieces: &[Option<Piece>],
157    ) -> Result<ReconstructedContents, ReconstructorError> {
158        let segment = self.reconstruct_segment(segment_pieces)?;
159
160        let mut reconstructed_contents = ReconstructedContents::default();
161        let mut next_block_number = BlockNumber::ZERO;
162        let mut partial_block = self.partial_block.take().unwrap_or_default();
163
164        for segment_item in segment.items {
165            #[expect(
166                clippy::rest_pattern_accessible_field,
167                reason = "Do not need other fields"
168            )]
169            match segment_item {
170                SegmentItem::Padding => {
171                    // Doesn't contain anything
172                }
173                SegmentItem::Block { bytes, .. } => {
174                    if !partial_block.is_empty() {
175                        reconstructed_contents
176                            .blocks
177                            .push((next_block_number, mem::take(&mut partial_block)));
178
179                        next_block_number += BlockNumber::ONE;
180                    }
181
182                    reconstructed_contents
183                        .blocks
184                        .push((next_block_number, Vec::from(bytes)));
185
186                    next_block_number += BlockNumber::ONE;
187                }
188                SegmentItem::BlockStart { bytes, .. } => {
189                    if !partial_block.is_empty() {
190                        reconstructed_contents
191                            .blocks
192                            .push((next_block_number, mem::take(&mut partial_block)));
193
194                        next_block_number += BlockNumber::ONE;
195                    }
196
197                    partial_block = Vec::from(bytes);
198                }
199                SegmentItem::BlockContinuation { bytes, .. } => {
200                    if partial_block.is_empty() {
201                        // This is continuation from previous segment, we don't have the beginning
202                        // of the block to continue.
203                        continue;
204                    }
205
206                    partial_block.extend_from_slice(&bytes);
207                }
208                SegmentItem::ParentSegmentHeader(segment_header) => {
209                    let segment_index = segment_header.index.as_inner();
210
211                    if let Some(last_segment_index) = self.last_segment_index
212                        && last_segment_index != segment_index
213                    {
214                        return Err(ReconstructorError::IncorrectSegmentOrder {
215                            expected_segment_index: last_segment_index + LocalSegmentIndex::ONE,
216                            actual_segment_index: segment_index + LocalSegmentIndex::ONE,
217                        });
218                    }
219
220                    self.last_segment_index
221                        .replace(segment_index + LocalSegmentIndex::ONE);
222
223                    let LastArchivedBlock {
224                        number,
225                        archived_progress,
226                    } = segment_header.last_archived_block;
227
228                    reconstructed_contents
229                        .segment_header
230                        .replace(segment_header);
231
232                    match archived_progress.partial() {
233                        None => {
234                            reconstructed_contents
235                                .blocks
236                                .push((next_block_number, mem::take(&mut partial_block)));
237
238                            next_block_number = number.as_inner() + BlockNumber::ONE;
239                        }
240                        Some(_bytes) => {
241                            next_block_number = number.as_inner();
242
243                            if partial_block.is_empty() {
244                                // Will not be able to recover full block, bump right away.
245                                next_block_number += BlockNumber::ONE;
246                            }
247                        }
248                    }
249                }
250            }
251        }
252
253        if !partial_block.is_empty() {
254            self.partial_block.replace(partial_block);
255        }
256
257        if self.last_segment_index.is_none() {
258            self.last_segment_index.replace(LocalSegmentIndex::ZERO);
259        }
260
261        Ok(reconstructed_contents)
262    }
263}