Skip to main content

reth_provider/providers/static_file/
writer.rs

1use super::{
2    manager::StaticFileProviderInner, metrics::StaticFileProviderMetrics, StaticFileProvider,
3};
4use crate::providers::static_file::metrics::StaticFileProviderOperation;
5use alloy_consensus::BlockHeader;
6use alloy_primitives::{BlockHash, BlockNumber, TxNumber, U256};
7use parking_lot::{lock_api::RwLockWriteGuard, RawRwLock, RwLock};
8use reth_codecs::Compact;
9use reth_db::models::{AccountBeforeTx, StorageBeforeTx};
10use reth_db_api::models::CompactU256;
11use reth_nippy_jar::{NippyJar, NippyJarError, NippyJarWriter};
12use reth_node_types::NodePrimitives;
13use reth_primitives_traits::FastInstant as Instant;
14use reth_static_file_types::{
15    ChangesetOffset, ChangesetOffsetReader, ChangesetOffsetWriter, SegmentHeader,
16    SegmentRangeInclusive, StaticFileSegment,
17};
18use reth_storage_errors::provider::{ProviderError, ProviderResult, StaticFileWriterError};
19use std::{
20    borrow::Borrow,
21    cmp::Ordering,
22    fmt::Debug,
23    path::{Path, PathBuf},
24    sync::{Arc, Weak},
25};
26use tracing::{debug, instrument};
27
28/// Represents different pruning strategies for various static file segments.
29#[derive(Debug, Clone, Copy)]
30enum PruneStrategy {
31    /// Prune headers by number of blocks to delete.
32    Headers {
33        /// Number of blocks to delete.
34        num_blocks: u64,
35    },
36    /// Prune transactions by number of rows and last block.
37    Transactions {
38        /// Number of transaction rows to delete.
39        num_rows: u64,
40        /// The last block number after pruning.
41        last_block: BlockNumber,
42    },
43    /// Prune receipts by number of rows and last block.
44    Receipts {
45        /// Number of receipt rows to delete.
46        num_rows: u64,
47        /// The last block number after pruning.
48        last_block: BlockNumber,
49    },
50    /// Prune transaction senders by number of rows and last block.
51    TransactionSenders {
52        /// Number of transaction sender rows to delete.
53        num_rows: u64,
54        /// The last block number after pruning.
55        last_block: BlockNumber,
56    },
57    /// Prune account changesets to a target block number.
58    AccountChangeSets {
59        /// The target block number to prune to.
60        last_block: BlockNumber,
61    },
62    /// Prune storage changesets to a target block number.
63    StorageChangeSets {
64        /// The target block number to prune to.
65        last_block: BlockNumber,
66    },
67}
68
69/// Static file writers for every known [`StaticFileSegment`].
70///
71/// WARNING: Trying to use more than one writer for the same segment type **will result in a
72/// deadlock**.
73#[derive(Debug)]
74pub(crate) struct StaticFileWriters<N> {
75    headers: RwLock<Option<StaticFileProviderRW<N>>>,
76    transactions: RwLock<Option<StaticFileProviderRW<N>>>,
77    receipts: RwLock<Option<StaticFileProviderRW<N>>>,
78    transaction_senders: RwLock<Option<StaticFileProviderRW<N>>>,
79    account_change_sets: RwLock<Option<StaticFileProviderRW<N>>>,
80    storage_change_sets: RwLock<Option<StaticFileProviderRW<N>>>,
81}
82
83impl<N> Default for StaticFileWriters<N> {
84    fn default() -> Self {
85        Self {
86            headers: Default::default(),
87            transactions: Default::default(),
88            receipts: Default::default(),
89            transaction_senders: Default::default(),
90            account_change_sets: Default::default(),
91            storage_change_sets: Default::default(),
92        }
93    }
94}
95
96impl<N: NodePrimitives> StaticFileWriters<N> {
97    pub(crate) fn get_or_create(
98        &self,
99        segment: StaticFileSegment,
100        create_fn: impl FnOnce() -> ProviderResult<StaticFileProviderRW<N>>,
101    ) -> ProviderResult<StaticFileProviderRWRefMut<'_, N>> {
102        let mut write_guard = match segment {
103            StaticFileSegment::Headers => self.headers.write(),
104            StaticFileSegment::Transactions => self.transactions.write(),
105            StaticFileSegment::Receipts => self.receipts.write(),
106            StaticFileSegment::TransactionSenders => self.transaction_senders.write(),
107            StaticFileSegment::AccountChangeSets => self.account_change_sets.write(),
108            StaticFileSegment::StorageChangeSets => self.storage_change_sets.write(),
109        };
110
111        if write_guard.is_none() {
112            *write_guard = Some(create_fn()?);
113        }
114
115        Ok(StaticFileProviderRWRefMut(write_guard))
116    }
117
118    /// Drops the cached writer for a segment before destructive segment-level operations.
119    pub(crate) fn remove(&self, segment: StaticFileSegment) {
120        let mut write_guard = match segment {
121            StaticFileSegment::Headers => self.headers.write(),
122            StaticFileSegment::Transactions => self.transactions.write(),
123            StaticFileSegment::Receipts => self.receipts.write(),
124            StaticFileSegment::TransactionSenders => self.transaction_senders.write(),
125            StaticFileSegment::AccountChangeSets => self.account_change_sets.write(),
126            StaticFileSegment::StorageChangeSets => self.storage_change_sets.write(),
127        };
128
129        *write_guard = None;
130    }
131
132    #[instrument(
133        name = "StaticFileWriters::commit",
134        level = "debug",
135        target = "providers::static_file",
136        skip_all
137    )]
138    pub(crate) fn commit(&self) -> ProviderResult<()> {
139        debug!(target: "providers::static_file", "Committing all static file segments");
140
141        for writer_lock in [
142            &self.headers,
143            &self.transactions,
144            &self.receipts,
145            &self.transaction_senders,
146            &self.account_change_sets,
147            &self.storage_change_sets,
148        ] {
149            let mut writer = writer_lock.write();
150            if let Some(writer) = writer.as_mut() {
151                writer.commit()?;
152            }
153        }
154
155        debug!(target: "providers::static_file", "Committed all static file segments");
156        Ok(())
157    }
158
159    pub(crate) fn has_unwind_queued(&self) -> bool {
160        for writer_lock in [
161            &self.headers,
162            &self.transactions,
163            &self.receipts,
164            &self.transaction_senders,
165            &self.account_change_sets,
166            &self.storage_change_sets,
167        ] {
168            let writer = writer_lock.read();
169            if let Some(writer) = writer.as_ref() &&
170                writer.will_prune_on_commit()
171            {
172                return true
173            }
174        }
175        false
176    }
177
178    /// Finalizes all writers by committing their configuration to disk and updating indices.
179    ///
180    /// Must be called after `sync_all` was called on individual writers.
181    /// Returns an error if any writer has prune queued.
182    #[instrument(
183        name = "StaticFileWriters::finalize",
184        level = "debug",
185        target = "providers::static_file",
186        skip_all
187    )]
188    pub(crate) fn finalize(&self) -> ProviderResult<()> {
189        debug!(target: "providers::static_file", "Finalizing all static file segments into disk");
190
191        for writer_lock in [
192            &self.headers,
193            &self.transactions,
194            &self.receipts,
195            &self.transaction_senders,
196            &self.account_change_sets,
197            &self.storage_change_sets,
198        ] {
199            let mut writer = writer_lock.write();
200            if let Some(writer) = writer.as_mut() {
201                writer.finalize()?;
202            }
203        }
204
205        debug!(target: "providers::static_file", "Finalized all static file segments into disk");
206        Ok(())
207    }
208}
209
210/// Mutable reference to a [`StaticFileProviderRW`] behind a [`RwLockWriteGuard`].
211#[derive(Debug)]
212pub struct StaticFileProviderRWRefMut<'a, N>(
213    pub(crate) RwLockWriteGuard<'a, RawRwLock, Option<StaticFileProviderRW<N>>>,
214);
215
216impl<N> std::ops::DerefMut for StaticFileProviderRWRefMut<'_, N> {
217    fn deref_mut(&mut self) -> &mut Self::Target {
218        // This is always created by [`StaticFileWriters::get_or_create`]
219        self.0.as_mut().expect("static file writer provider should be init")
220    }
221}
222
223impl<N> std::ops::Deref for StaticFileProviderRWRefMut<'_, N> {
224    type Target = StaticFileProviderRW<N>;
225
226    fn deref(&self) -> &Self::Target {
227        // This is always created by [`StaticFileWriters::get_or_create`]
228        self.0.as_ref().expect("static file writer provider should be init")
229    }
230}
231
232#[derive(Debug)]
233/// Extends `StaticFileProvider` with writing capabilities
234pub struct StaticFileProviderRW<N> {
235    /// Reference back to the provider. We need [Weak] here because [`StaticFileProviderRW`] is
236    /// stored in a [`reth_primitives_traits::dashmap::DashMap`] inside the parent
237    /// [`StaticFileProvider`].which is an [Arc]. If we were to use an [Arc] here, we would
238    /// create a reference cycle.
239    reader: Weak<StaticFileProviderInner<N>>,
240    /// A [`NippyJarWriter`] instance.
241    writer: NippyJarWriter<SegmentHeader>,
242    /// Path to opened file.
243    data_path: PathBuf,
244    /// Reusable buffer for encoding appended data.
245    buf: Vec<u8>,
246    /// Metrics.
247    metrics: Option<Arc<StaticFileProviderMetrics>>,
248    /// On commit, contains the pruning strategy to apply for the segment.
249    prune_on_commit: Option<PruneStrategy>,
250    /// Whether `sync_all()` has been called. Used by `finalize()` to avoid redundant syncs.
251    synced: bool,
252    /// Changeset offsets sidecar writer (only for changeset segments).
253    changeset_offsets: Option<ChangesetOffsetWriter>,
254    /// Current block's changeset offset being written.
255    current_changeset_offset: Option<ChangesetOffset>,
256}
257
258impl<N: NodePrimitives> StaticFileProviderRW<N> {
259    /// Creates a new [`StaticFileProviderRW`] for a [`StaticFileSegment`].
260    ///
261    /// Before use, transaction based segments should ensure the block end range is the expected
262    /// one, and heal if not. For more check `Self::ensure_end_range_consistency`.
263    pub fn new(
264        segment: StaticFileSegment,
265        block: BlockNumber,
266        reader: Weak<StaticFileProviderInner<N>>,
267        metrics: Option<Arc<StaticFileProviderMetrics>>,
268    ) -> ProviderResult<Self> {
269        let (writer, data_path) = Self::open(segment, block, reader.clone(), metrics.clone())?;
270
271        // Create writer WITHOUT sidecar first - we'll add it after healing
272        let mut writer = Self {
273            writer,
274            data_path,
275            buf: Vec::with_capacity(100),
276            reader,
277            metrics,
278            prune_on_commit: None,
279            synced: false,
280            changeset_offsets: None,
281            current_changeset_offset: None,
282        };
283
284        // Run NippyJar healing BEFORE setting up changeset sidecar
285        // This may reduce rows, which affects valid sidecar offsets
286        writer.ensure_end_range_consistency()?;
287
288        // Now set up changeset sidecar with post-heal header values
289        if segment.is_change_based() {
290            writer.heal_changeset_sidecar()?;
291        }
292
293        Ok(writer)
294    }
295
296    fn open(
297        segment: StaticFileSegment,
298        block: u64,
299        reader: Weak<StaticFileProviderInner<N>>,
300        metrics: Option<Arc<StaticFileProviderMetrics>>,
301    ) -> ProviderResult<(NippyJarWriter<SegmentHeader>, PathBuf)> {
302        let start = Instant::now();
303
304        let static_file_provider = Self::upgrade_provider_to_strong_reference(&reader);
305
306        let block_range = static_file_provider.find_fixed_range(segment, block);
307        let (jar, path) = match static_file_provider.get_segment_provider_for_block(
308            segment,
309            block_range.start(),
310            None,
311        ) {
312            Ok(provider) => (
313                NippyJar::load(provider.data_path()).map_err(ProviderError::other)?,
314                provider.data_path().into(),
315            ),
316            Err(ProviderError::MissingStaticFileBlock(_, _)) => {
317                let path = static_file_provider.directory().join(segment.filename(&block_range));
318                (create_jar(segment, &path, block_range), path)
319            }
320            Err(err) => return Err(err),
321        };
322
323        let result = match NippyJarWriter::new(jar) {
324            Ok(writer) => Ok((writer, path)),
325            Err(NippyJarError::FrozenJar) => {
326                // This static file has been frozen, so we should
327                Err(ProviderError::FinalizedStaticFile(segment, block))
328            }
329            Err(e) => Err(ProviderError::other(e)),
330        }?;
331
332        if let Some(metrics) = &metrics {
333            metrics.record_segment_operation(
334                segment,
335                StaticFileProviderOperation::OpenWriter,
336                Some(start.elapsed()),
337            );
338        }
339
340        Ok(result)
341    }
342
343    /// If a file level healing happens, we need to update the end range on the
344    /// [`SegmentHeader`].
345    ///
346    /// However, for transaction based segments, the block end range has to be found and healed
347    /// externally.
348    ///
349    /// Check [`reth_nippy_jar::NippyJarChecker`] &
350    /// [`NippyJarWriter`] for more on healing.
351    fn ensure_end_range_consistency(&mut self) -> ProviderResult<()> {
352        // If we have lost rows (in this run or previous), we need to update the [SegmentHeader].
353        let expected_rows = if self.user_header().segment().is_headers() {
354            self.user_header().block_len().unwrap_or_default()
355        } else {
356            self.user_header().tx_len().unwrap_or_default()
357        };
358        let actual_rows = self.writer.rows() as u64;
359        let pruned_rows = expected_rows.saturating_sub(actual_rows);
360        if pruned_rows > 0 {
361            self.user_header_mut().prune(pruned_rows);
362        }
363
364        debug!(
365            target: "providers::static_file",
366            segment = ?self.writer.user_header().segment(),
367            path = ?self.data_path,
368            pruned_rows,
369            "Ensuring end range consistency"
370        );
371
372        self.writer.commit().map_err(ProviderError::other)?;
373
374        // Updates the [SnapshotProvider] manager
375        self.update_index()?;
376        Ok(())
377    }
378
379    /// Returns `true` if the writer will prune on commit.
380    pub const fn will_prune_on_commit(&self) -> bool {
381        self.prune_on_commit.is_some()
382    }
383
384    /// Heals the changeset offset sidecar after `NippyJar` healing.
385    ///
386    /// This must be called AFTER `ensure_end_range_consistency()` which may reduce rows.
387    /// Performs three-way consistency check between header, `NippyJar` rows, and sidecar file:
388    /// - Validates sidecar offsets don't point past actual `NippyJar` rows
389    /// - Heals header if sidecar was truncated during interrupted prune
390    /// - Truncates sidecar if offsets point past healed `NippyJar` data
391    fn heal_changeset_sidecar(&mut self) -> ProviderResult<()> {
392        let csoff_path = self.data_path.with_extension("csoff");
393
394        // Step 1: Read all three sources of truth
395        let header_claims_blocks = self.writer.user_header().changeset_offsets_len();
396        let actual_nippy_rows = self.writer.rows() as u64;
397
398        // Get actual sidecar file size (may differ from header after crash)
399        let actual_sidecar_blocks = if csoff_path.exists() {
400            let file_len = reth_fs_util::metadata(&csoff_path).map_err(ProviderError::other)?.len();
401            // Remove partial records from crash mid-write
402            let aligned_len = file_len - (file_len % 16);
403            aligned_len / 16
404        } else {
405            0
406        };
407
408        // Fresh segment or no sidecar data - nothing to heal
409        if header_claims_blocks == 0 && actual_sidecar_blocks == 0 {
410            self.changeset_offsets =
411                Some(ChangesetOffsetWriter::new(&csoff_path, 0).map_err(ProviderError::other)?);
412            return Ok(());
413        }
414
415        // Step 2: Validate sidecar offsets against actual NippyJar state
416        let valid_blocks = if actual_sidecar_blocks > 0 {
417            let reader = ChangesetOffsetReader::new(&csoff_path, actual_sidecar_blocks)
418                .map_err(ProviderError::other)?;
419
420            // Find last block where offset + num_changes <= actual_nippy_rows
421            // This correctly handles rows=0 with offset=0, num_changes=0 (empty blocks)
422            let mut valid = 0u64;
423            for i in 0..actual_sidecar_blocks {
424                if let Some(offset) = reader.get(i).map_err(ProviderError::other)? {
425                    if offset.offset() + offset.num_changes() <= actual_nippy_rows {
426                        valid = i + 1;
427                    } else {
428                        // This block points past EOF - stop here
429                        break;
430                    }
431                }
432            }
433            valid
434        } else {
435            0
436        };
437
438        // Step 3: Determine correct state from synced files (source of truth)
439        // Header is the commit marker - never enlarge, only shrink
440        let correct_blocks = valid_blocks.min(header_claims_blocks);
441
442        // Step 4: Heal if header doesn't match validated truth
443        let mut needs_header_commit = false;
444
445        if correct_blocks != header_claims_blocks || actual_sidecar_blocks != correct_blocks {
446            tracing::warn!(
447                target: "reth::static_file",
448                path = %csoff_path.display(),
449                header_claims = header_claims_blocks,
450                sidecar_has = actual_sidecar_blocks,
451                valid_blocks = correct_blocks,
452                actual_rows = actual_nippy_rows,
453                "Three-way healing: syncing header, sidecar, and NippyJar state"
454            );
455
456            // Truncate sidecar file if it has invalid blocks
457            if actual_sidecar_blocks > correct_blocks {
458                use std::fs::OpenOptions;
459                let file = OpenOptions::new()
460                    .write(true)
461                    .open(&csoff_path)
462                    .map_err(ProviderError::other)?;
463                file.set_len(correct_blocks * 16).map_err(ProviderError::other)?;
464                file.sync_all().map_err(ProviderError::other)?;
465
466                tracing::debug!(
467                    target: "reth::static_file",
468                    "Truncated sidecar from {} to {} blocks",
469                    actual_sidecar_blocks,
470                    correct_blocks
471                );
472            }
473
474            // Update header to match validated truth (can only shrink, never enlarge)
475            if correct_blocks < header_claims_blocks {
476                // Blocks were removed - use prune() to update both block_range and
477                // changeset_offsets_len atomically
478                let blocks_removed = header_claims_blocks - correct_blocks;
479                self.writer.user_header_mut().prune(blocks_removed);
480
481                tracing::debug!(
482                    target: "reth::static_file",
483                    "Updated header: removed {} blocks (changeset_offsets_len: {} -> {})",
484                    blocks_removed,
485                    header_claims_blocks,
486                    correct_blocks
487                );
488
489                needs_header_commit = true;
490            }
491        } else {
492            tracing::debug!(
493                target: "reth::static_file",
494                path = %csoff_path.display(),
495                blocks = correct_blocks,
496                "Changeset sidecar consistent, no healing needed"
497            );
498        }
499
500        // Open sidecar writer with corrected count (won't error now that sizes match)
501        let csoff_writer = ChangesetOffsetWriter::new(&csoff_path, correct_blocks)
502            .map_err(ProviderError::other)?;
503
504        self.changeset_offsets = Some(csoff_writer);
505
506        // Commit healed header if needed (after sidecar writer is set up)
507        if needs_header_commit {
508            self.writer.commit().map_err(ProviderError::other)?;
509
510            tracing::info!(
511                target: "reth::static_file",
512                path = %csoff_path.display(),
513                blocks = correct_blocks,
514                "Committed healed changeset offset header"
515            );
516        }
517
518        Ok(())
519    }
520
521    /// Flushes the current changeset offset (if any) to the `.csoff` sidecar file.
522    ///
523    /// This is idempotent - safe to call multiple times. After flushing, the current offset
524    /// is cleared to prevent duplicate writes.
525    ///
526    /// This must be called before committing or syncing to ensure the last block's offset
527    /// is persisted, since `increment_block()` only writes the *previous* block's offset.
528    fn flush_current_changeset_offset(&mut self) -> ProviderResult<()> {
529        if !self.writer.user_header().segment().is_change_based() {
530            return Ok(());
531        }
532
533        if let Some(offset) = self.current_changeset_offset.take() &&
534            let Some(writer) = &mut self.changeset_offsets
535        {
536            writer.append(&offset).map_err(ProviderError::other)?;
537        }
538        Ok(())
539    }
540
541    /// Syncs all data (rows, offsets, and changeset offsets sidecar) to disk.
542    ///
543    /// This does NOT commit the configuration. Call [`Self::finalize`] after to write the
544    /// configuration and mark the writer as clean.
545    ///
546    /// Returns an error if prune is queued (use [`Self::commit`] instead).
547    pub fn sync_all(&mut self) -> ProviderResult<()> {
548        if self.prune_on_commit.is_some() {
549            return Err(StaticFileWriterError::FinalizeWithPruneQueued.into());
550        }
551
552        // Write the final block's offset and sync the sidecar for changeset segments
553        self.flush_current_changeset_offset()?;
554        if let Some(writer) = &mut self.changeset_offsets {
555            writer.sync().map_err(ProviderError::other)?;
556            // Update the header with the actual number of offsets written
557            self.writer.user_header_mut().set_changeset_offsets_len(writer.len());
558        }
559
560        if self.writer.is_dirty() {
561            self.writer.sync_all().map_err(ProviderError::other)?;
562        }
563        self.synced = true;
564        Ok(())
565    }
566
567    /// Commits configuration to disk and updates the reader index.
568    ///
569    /// If `sync_all()` was not called, this will call it first to ensure data is persisted.
570    ///
571    /// Returns an error if prune is queued (use [`Self::commit`] instead).
572    #[instrument(
573        name = "StaticFileProviderRW::finalize",
574        level = "debug",
575        target = "providers::static_file",
576        skip_all
577    )]
578    pub fn finalize(&mut self) -> ProviderResult<()> {
579        if self.prune_on_commit.is_some() {
580            return Err(StaticFileWriterError::FinalizeWithPruneQueued.into());
581        }
582        if self.writer.is_dirty() {
583            if !self.synced {
584                // Must call self.sync_all() to flush changeset offsets and update
585                // the header's changeset_offsets_len, not just the inner writer
586                self.sync_all()?;
587            }
588
589            self.writer.finalize().map_err(ProviderError::other)?;
590            self.update_index()?;
591        }
592        self.synced = false;
593        Ok(())
594    }
595
596    /// Commits configuration changes to disk and updates the reader index with the new changes.
597    #[instrument(
598        name = "StaticFileProviderRW::commit",
599        level = "debug",
600        target = "providers::static_file",
601        skip_all
602    )]
603    pub fn commit(&mut self) -> ProviderResult<()> {
604        let start = Instant::now();
605
606        // Truncates the data file if instructed to.
607        if let Some(strategy) = self.prune_on_commit.take() {
608            debug!(
609                target: "providers::static_file",
610                segment = ?self.writer.user_header().segment(),
611                "Pruning data on commit"
612            );
613            match strategy {
614                PruneStrategy::Headers { num_blocks } => self.prune_header_data(num_blocks)?,
615                PruneStrategy::Transactions { num_rows, last_block } => {
616                    self.prune_transaction_data(num_rows, last_block)?
617                }
618                PruneStrategy::Receipts { num_rows, last_block } => {
619                    self.prune_receipt_data(num_rows, last_block)?
620                }
621                PruneStrategy::TransactionSenders { num_rows, last_block } => {
622                    self.prune_transaction_sender_data(num_rows, last_block)?
623                }
624                PruneStrategy::AccountChangeSets { last_block } => {
625                    self.prune_account_changeset_data(last_block)?
626                }
627                PruneStrategy::StorageChangeSets { last_block } => {
628                    self.prune_storage_changeset_data(last_block)?
629                }
630            }
631        }
632
633        // For changeset segments, flush and sync the sidecar file before committing the main file.
634        // This ensures crash consistency: the sidecar is durable before the header references it.
635        self.flush_current_changeset_offset()?;
636        if let Some(writer) = &mut self.changeset_offsets {
637            writer.sync().map_err(ProviderError::other)?;
638            // Update the header with the actual number of offsets written
639            self.writer.user_header_mut().set_changeset_offsets_len(writer.len());
640        }
641
642        if self.writer.is_dirty() {
643            debug!(
644                target: "providers::static_file",
645                segment = ?self.writer.user_header().segment(),
646                "Committing writer to disk"
647            );
648
649            // Commits offsets and new user_header to disk
650            self.writer.commit().map_err(ProviderError::other)?;
651
652            if let Some(metrics) = &self.metrics {
653                metrics.record_segment_operation(
654                    self.writer.user_header().segment(),
655                    StaticFileProviderOperation::CommitWriter,
656                    Some(start.elapsed()),
657                );
658            }
659
660            debug!(
661                target: "providers::static_file",
662                segment = ?self.writer.user_header().segment(),
663                path = ?self.data_path,
664                duration = ?start.elapsed(),
665                "Committed writer to disk"
666            );
667
668            self.update_index()?;
669        }
670
671        Ok(())
672    }
673
674    /// Commits configuration changes to disk and updates the reader index with the new changes.
675    ///
676    /// CAUTION: does not call `sync_all` on the files.
677    #[cfg(feature = "test-utils")]
678    pub fn commit_without_sync_all(&mut self) -> ProviderResult<()> {
679        let start = Instant::now();
680
681        debug!(
682            target: "providers::static_file",
683            segment = ?self.writer.user_header().segment(),
684            "Committing writer to disk (without sync)"
685        );
686
687        // Commits offsets and new user_header to disk
688        self.writer.commit_without_sync_all().map_err(ProviderError::other)?;
689
690        if let Some(metrics) = &self.metrics {
691            metrics.record_segment_operation(
692                self.writer.user_header().segment(),
693                StaticFileProviderOperation::CommitWriter,
694                Some(start.elapsed()),
695            );
696        }
697
698        debug!(
699            target: "providers::static_file",
700            segment = ?self.writer.user_header().segment(),
701            path = ?self.data_path,
702            duration = ?start.elapsed(),
703            "Committed writer to disk (without sync)"
704        );
705
706        self.update_index()?;
707
708        Ok(())
709    }
710
711    /// Updates the `self.reader` internal index.
712    fn update_index(&self) -> ProviderResult<()> {
713        let segment = self.writer.user_header().segment();
714
715        // We find the maximum block of the segment by checking this writer's last block.
716        //
717        // However if there's no block range (because there's no data), we try to calculate it by
718        // subtracting 1 from the expected block start, resulting on the last block of the
719        // previous file — but only if that file actually exists. If the previous file doesn't
720        // exist (e.g. first-ever file for a segment starting past range boundary), there's
721        // nothing to index.
722        let segment_max_block = self
723            .writer
724            .user_header()
725            .block_range()
726            .as_ref()
727            .map(|block_range| block_range.end())
728            .or_else(|| {
729                let expected_start = self.writer.user_header().expected_block_start();
730                if expected_start <= self.reader().genesis_block_number() {
731                    return None;
732                }
733
734                let prev_block = expected_start - 1;
735                let prev_range = self.reader().find_fixed_range(segment, prev_block);
736                let prev_path = self.reader().directory().join(segment.filename(&prev_range));
737                prev_path.exists().then_some(prev_block)
738            });
739
740        self.reader().update_index(segment, segment_max_block)
741    }
742
743    /// Ensures that the writer is positioned at the specified block number.
744    ///
745    /// If the writer is positioned at a greater block number than the specified one, the writer
746    /// will NOT be unwound and the error will be returned.
747    pub fn ensure_at_block(&mut self, advance_to: BlockNumber) -> ProviderResult<()> {
748        let current_block = if let Some(current_block_number) = self.current_block_number() {
749            current_block_number
750        } else {
751            // A fresh file does not necessarily start at block 0: a writer opened for a
752            // pruned segment may be positioned on a later fixed range, in which case its
753            // first block is the expected start of that range.
754            let first_block = self.writer.user_header().expected_block_start();
755            self.increment_block(first_block)?;
756            first_block
757        };
758
759        match current_block.cmp(&advance_to) {
760            Ordering::Less => {
761                for block in current_block + 1..=advance_to {
762                    self.increment_block(block)?;
763                }
764            }
765            Ordering::Equal => {}
766            Ordering::Greater => {
767                return Err(ProviderError::UnexpectedStaticFileBlockNumber(
768                    self.writer.user_header().segment(),
769                    current_block,
770                    advance_to,
771                ));
772            }
773        }
774
775        Ok(())
776    }
777
778    /// Allows to increment the [`SegmentHeader`] end block. It will commit the current static file,
779    /// and create the next one if we are past the end range.
780    pub fn increment_block(&mut self, expected_block_number: BlockNumber) -> ProviderResult<()> {
781        let segment = self.writer.user_header().segment();
782
783        self.check_next_block_number(expected_block_number)?;
784
785        let start = Instant::now();
786        if let Some(last_block) = self.writer.user_header().block_end() {
787            // We have finished the previous static file and must freeze it
788            if last_block == self.writer.user_header().expected_block_end() {
789                // Commits offsets and new user_header to disk
790                self.commit()?;
791
792                // Opens the new static file
793                let (writer, data_path) =
794                    Self::open(segment, last_block + 1, self.reader.clone(), self.metrics.clone())?;
795                self.writer = writer;
796                self.data_path = data_path.clone();
797
798                // Update changeset offsets writer for the new file (starts empty)
799                if segment.is_change_based() {
800                    let csoff_path = data_path.with_extension("csoff");
801                    self.changeset_offsets = Some(
802                        ChangesetOffsetWriter::new(&csoff_path, 0).map_err(ProviderError::other)?,
803                    );
804                }
805
806                *self.writer.user_header_mut() = SegmentHeader::new(
807                    self.reader().find_fixed_range(segment, last_block + 1),
808                    None,
809                    None,
810                    segment,
811                );
812            }
813        }
814
815        self.writer.user_header_mut().increment_block();
816
817        // Handle changeset offset tracking for changeset segments
818        if segment.is_change_based() {
819            // Write previous block's offset if we have one
820            if let Some(offset) = self.current_changeset_offset.take() &&
821                let Some(writer) = &mut self.changeset_offsets
822            {
823                writer.append(&offset).map_err(ProviderError::other)?;
824            }
825            // Start tracking new block's offset
826            let new_offset = self.writer.rows() as u64;
827            self.current_changeset_offset = Some(ChangesetOffset::new(new_offset, 0));
828        }
829
830        if let Some(metrics) = &self.metrics {
831            metrics.record_segment_operation(
832                segment,
833                StaticFileProviderOperation::IncrementBlock,
834                Some(start.elapsed()),
835            );
836        }
837
838        Ok(())
839    }
840
841    /// Returns the current block number of the static file writer.
842    pub fn current_block_number(&self) -> Option<u64> {
843        self.writer.user_header().block_end()
844    }
845
846    /// Returns a block number that is one next to the current tip of static files.
847    pub fn next_block_number(&self) -> u64 {
848        // The next static file block number can be found by checking the one after block_end.
849        // However, if it's a new file that hasn't been added any data, its block range will
850        // actually be None. In that case, the next block will be found on `expected_block_start`.
851        self.writer
852            .user_header()
853            .block_end()
854            .map(|b| b + 1)
855            .unwrap_or_else(|| self.writer.user_header().expected_block_start())
856    }
857
858    /// Verifies if the incoming block number matches the next expected block number
859    /// for a static file. This ensures data continuity when adding new blocks.
860    fn check_next_block_number(&self, expected_block_number: u64) -> ProviderResult<()> {
861        let next_static_file_block = self.next_block_number();
862
863        if expected_block_number != next_static_file_block {
864            return Err(ProviderError::UnexpectedStaticFileBlockNumber(
865                self.writer.user_header().segment(),
866                expected_block_number,
867                next_static_file_block,
868            ))
869        }
870        Ok(())
871    }
872
873    /// Truncates account changesets to the given block. It deletes and loads an older static file
874    /// if the block goes beyond the start of the current block range.
875    ///
876    /// # Note
877    /// Commits to the configuration file at the end
878    fn truncate_changesets(&mut self, last_block: u64) -> ProviderResult<()> {
879        let segment = self.writer.user_header().segment();
880        debug_assert!(segment.is_change_based());
881
882        // Get the current block range
883        let current_block_end = self
884            .writer
885            .user_header()
886            .block_end()
887            .ok_or(ProviderError::MissingStaticFileBlock(segment, 0))?;
888
889        // If we're already at or before the target block, nothing to do
890        if current_block_end <= last_block {
891            return Ok(())
892        }
893
894        // Navigate to the correct file if the target block is in a previous file
895        let mut expected_block_start = self.writer.user_header().expected_block_start();
896        while last_block < expected_block_start && expected_block_start > 0 {
897            self.delete_current_and_open_previous()?;
898            expected_block_start = self.writer.user_header().expected_block_start();
899        }
900
901        // Find the number of rows to keep (up to and including last_block)
902        let blocks_to_keep = if last_block >= expected_block_start {
903            last_block - expected_block_start + 1
904        } else {
905            0
906        };
907
908        // Read changeset offsets from sidecar file to find where to truncate
909        let csoff_path = self.data_path.with_extension("csoff");
910        let changeset_offsets_len = self.writer.user_header().changeset_offsets_len();
911
912        // Flush any pending changeset offset before reading the sidecar
913        self.flush_current_changeset_offset()?;
914
915        let rows_to_keep = if blocks_to_keep == 0 {
916            0
917        } else if blocks_to_keep >= changeset_offsets_len {
918            // Keep all rows in this file
919            self.writer.rows() as u64
920        } else {
921            // Read offset for the block after last_block from sidecar.
922            // Use committed length from header, ignoring any uncommitted records
923            // that may exist in the file after a crash.
924            let reader = ChangesetOffsetReader::new(&csoff_path, changeset_offsets_len)
925                .map_err(ProviderError::other)?;
926            if let Some(next_offset) = reader.get(blocks_to_keep).map_err(ProviderError::other)? {
927                next_offset.offset()
928            } else {
929                // If we can't read the offset, keep all rows
930                self.writer.rows() as u64
931            }
932        };
933
934        let total_rows = self.writer.rows() as u64;
935        let rows_to_delete = total_rows.saturating_sub(rows_to_keep);
936
937        if rows_to_delete > 0 {
938            // Calculate the number of blocks to prune
939            let current_block_end = self
940                .writer
941                .user_header()
942                .block_end()
943                .ok_or(ProviderError::MissingStaticFileBlock(segment, 0))?;
944            let blocks_to_remove = current_block_end - last_block;
945
946            // Update segment header - for changesets, prune expects number of blocks, not rows
947            self.writer.user_header_mut().prune(blocks_to_remove);
948
949            // Prune the actual rows
950            self.writer.prune_rows(rows_to_delete as usize).map_err(ProviderError::other)?;
951        }
952
953        // Update the block range
954        self.writer.user_header_mut().set_block_range(expected_block_start, last_block);
955
956        // Sync changeset offsets to match the new block range
957        self.writer.user_header_mut().sync_changeset_offsets();
958
959        // Truncate the sidecar file to match the new block count
960        if let Some(writer) = &mut self.changeset_offsets {
961            writer.truncate(blocks_to_keep).map_err(ProviderError::other)?;
962        }
963
964        // Clear current changeset offset tracking since we've pruned
965        self.current_changeset_offset = None;
966
967        // Commits new changes to disk
968        self.commit()?;
969
970        Ok(())
971    }
972
973    /// Truncates a number of rows from disk. It deletes and loads an older static file if block
974    /// goes beyond the start of the current block range.
975    ///
976    /// **`last_block`** should be passed only with transaction based segments.
977    ///
978    /// # Note
979    /// Commits to the configuration file at the end.
980    fn truncate(&mut self, num_rows: u64, last_block: Option<u64>) -> ProviderResult<()> {
981        let mut remaining_rows = num_rows;
982        let segment = self.writer.user_header().segment();
983        while remaining_rows > 0 {
984            let len = if segment.is_block_based() {
985                self.writer.user_header().block_len().unwrap_or_default()
986            } else {
987                self.writer.user_header().tx_len().unwrap_or_default()
988            };
989
990            if remaining_rows >= len {
991                // If there's more rows to delete than this static file contains, then just
992                // delete the whole file and go to the next static file
993                let block_start = self.writer.user_header().expected_block_start();
994
995                // We only delete the file if it's NOT the first static file AND:
996                // * it's a Header segment  OR
997                // * it's a tx-based segment AND `last_block` is lower than the first block of this
998                //   file's block range. Otherwise, having no rows simply means that this block
999                //   range has no transactions, but the file should remain.
1000                if block_start != 0 &&
1001                    (segment.is_headers() || last_block.is_some_and(|b| b < block_start))
1002                {
1003                    self.delete_current_and_open_previous()?;
1004                } else {
1005                    // Update `SegmentHeader`
1006                    self.writer.user_header_mut().prune(len);
1007                    self.writer.prune_rows(len as usize).map_err(ProviderError::other)?;
1008                    break
1009                }
1010
1011                remaining_rows -= len;
1012            } else {
1013                // Update `SegmentHeader`
1014                self.writer.user_header_mut().prune(remaining_rows);
1015
1016                // Truncate data
1017                self.writer.prune_rows(remaining_rows as usize).map_err(ProviderError::other)?;
1018                remaining_rows = 0;
1019            }
1020        }
1021
1022        // Only Transactions and Receipts
1023        if let Some(last_block) = last_block {
1024            let mut expected_block_start = self.writer.user_header().expected_block_start();
1025
1026            if num_rows == 0 {
1027                // Edge case for when we are unwinding a chain of empty blocks that goes across
1028                // files, and therefore, the only reference point to know which file
1029                // we are supposed to be at is `last_block`.
1030                while last_block < expected_block_start {
1031                    self.delete_current_and_open_previous()?;
1032                    expected_block_start = self.writer.user_header().expected_block_start();
1033                }
1034            }
1035            self.writer.user_header_mut().set_block_range(expected_block_start, last_block);
1036        }
1037
1038        // Commits new changes to disk.
1039        self.commit()?;
1040
1041        Ok(())
1042    }
1043
1044    /// Delete the current static file, and replace this provider writer with the previous static
1045    /// file.
1046    fn delete_current_and_open_previous(&mut self) -> Result<(), ProviderError> {
1047        let segment = self.user_header().segment();
1048        let current_path = self.data_path.clone();
1049        let (previous_writer, data_path) = Self::open(
1050            segment,
1051            self.writer.user_header().expected_block_start() - 1,
1052            self.reader.clone(),
1053            self.metrics.clone(),
1054        )?;
1055        self.writer = previous_writer;
1056        self.writer.set_dirty();
1057        self.data_path = data_path.clone();
1058
1059        // Delete the sidecar file for changeset segments before deleting the main jar
1060        if segment.is_change_based() {
1061            let csoff_path = current_path.with_extension("csoff");
1062            if csoff_path.exists() {
1063                std::fs::remove_file(&csoff_path).map_err(ProviderError::other)?;
1064            }
1065            // Re-initialize the changeset offsets writer for the previous file
1066            let new_csoff_path = data_path.with_extension("csoff");
1067            let committed_len = self.writer.user_header().changeset_offsets_len();
1068            self.changeset_offsets = Some(
1069                ChangesetOffsetWriter::new(&new_csoff_path, committed_len)
1070                    .map_err(ProviderError::other)?,
1071            );
1072        }
1073
1074        // Clear current changeset offset tracking since we're switching files
1075        self.current_changeset_offset = None;
1076
1077        NippyJar::<SegmentHeader>::load(&current_path)
1078            .map_err(ProviderError::other)?
1079            .delete()
1080            .map_err(ProviderError::other)?;
1081        Ok(())
1082    }
1083
1084    /// Appends column to static file.
1085    fn append_column<T: Compact>(&mut self, column: T) -> ProviderResult<()> {
1086        self.buf.clear();
1087        column.to_compact(&mut self.buf);
1088
1089        self.writer.append_column(Some(Ok(&self.buf))).map_err(ProviderError::other)?;
1090        Ok(())
1091    }
1092
1093    /// Appends to tx number-based static file.
1094    fn append_with_tx_number<V: Compact>(
1095        &mut self,
1096        tx_num: TxNumber,
1097        value: V,
1098    ) -> ProviderResult<()> {
1099        if let Some(range) = self.writer.user_header().tx_range() {
1100            let next_tx = range.end() + 1;
1101            if next_tx != tx_num {
1102                return Err(ProviderError::UnexpectedStaticFileTxNumber(
1103                    self.writer.user_header().segment(),
1104                    tx_num,
1105                    next_tx,
1106                ))
1107            }
1108            self.writer.user_header_mut().increment_tx();
1109        } else {
1110            self.writer.user_header_mut().set_tx_range(tx_num, tx_num);
1111        }
1112
1113        self.append_column(value)?;
1114
1115        Ok(())
1116    }
1117
1118    /// Appends change to changeset static file.
1119    fn append_change<V: Compact>(&mut self, change: &V) -> ProviderResult<()> {
1120        if let Some(ref mut offset) = self.current_changeset_offset {
1121            offset.increment_num_changes();
1122        }
1123        self.append_column(change)?;
1124        Ok(())
1125    }
1126
1127    /// Appends header to static file.
1128    ///
1129    /// It **CALLS** `increment_block()` since the number of headers is equal to the number of
1130    /// blocks.
1131    pub fn append_header(&mut self, header: &N::BlockHeader, hash: &BlockHash) -> ProviderResult<()>
1132    where
1133        N::BlockHeader: Compact,
1134    {
1135        self.append_header_with_td(header, U256::ZERO, hash)
1136    }
1137
1138    /// Appends header to static file with a specified total difficulty.
1139    ///
1140    /// It **CALLS** `increment_block()` since the number of headers is equal to the number of
1141    /// blocks.
1142    pub fn append_header_with_td(
1143        &mut self,
1144        header: &N::BlockHeader,
1145        total_difficulty: U256,
1146        hash: &BlockHash,
1147    ) -> ProviderResult<()>
1148    where
1149        N::BlockHeader: Compact,
1150    {
1151        let start = Instant::now();
1152        self.ensure_no_queued_prune()?;
1153
1154        debug_assert!(self.writer.user_header().segment() == StaticFileSegment::Headers);
1155
1156        self.increment_block(header.number())?;
1157
1158        self.append_column(header)?;
1159        self.append_column(CompactU256::from(total_difficulty))?;
1160        self.append_column(hash)?;
1161
1162        if let Some(metrics) = &self.metrics {
1163            metrics.record_segment_operation(
1164                StaticFileSegment::Headers,
1165                StaticFileProviderOperation::Append,
1166                Some(start.elapsed()),
1167            );
1168        }
1169
1170        Ok(())
1171    }
1172
1173    /// Appends header to static file without calling `increment_block`.
1174    /// This is useful for genesis blocks with non-zero block numbers.
1175    pub fn append_header_direct(
1176        &mut self,
1177        header: &N::BlockHeader,
1178        total_difficulty: U256,
1179        hash: &BlockHash,
1180    ) -> ProviderResult<()>
1181    where
1182        N::BlockHeader: Compact,
1183    {
1184        let start = Instant::now();
1185        self.ensure_no_queued_prune()?;
1186
1187        debug_assert!(self.writer.user_header().segment() == StaticFileSegment::Headers);
1188
1189        self.append_column(header)?;
1190        self.append_column(CompactU256::from(total_difficulty))?;
1191        self.append_column(hash)?;
1192
1193        if let Some(metrics) = &self.metrics {
1194            metrics.record_segment_operation(
1195                StaticFileSegment::Headers,
1196                StaticFileProviderOperation::Append,
1197                Some(start.elapsed()),
1198            );
1199        }
1200
1201        Ok(())
1202    }
1203
1204    /// Appends transaction to static file.
1205    ///
1206    /// It **DOES NOT CALL** `increment_block()`, it should be handled elsewhere. There might be
1207    /// empty blocks and this function wouldn't be called.
1208    pub fn append_transaction(&mut self, tx_num: TxNumber, tx: &N::SignedTx) -> ProviderResult<()>
1209    where
1210        N::SignedTx: Compact,
1211    {
1212        let start = Instant::now();
1213        self.ensure_no_queued_prune()?;
1214
1215        debug_assert!(self.writer.user_header().segment() == StaticFileSegment::Transactions);
1216        self.append_with_tx_number(tx_num, tx)?;
1217
1218        if let Some(metrics) = &self.metrics {
1219            metrics.record_segment_operation(
1220                StaticFileSegment::Transactions,
1221                StaticFileProviderOperation::Append,
1222                Some(start.elapsed()),
1223            );
1224        }
1225
1226        Ok(())
1227    }
1228
1229    /// Appends receipt to static file.
1230    ///
1231    /// It **DOES NOT** call `increment_block()`, it should be handled elsewhere. There might be
1232    /// empty blocks and this function wouldn't be called.
1233    pub fn append_receipt(&mut self, tx_num: TxNumber, receipt: &N::Receipt) -> ProviderResult<()>
1234    where
1235        N::Receipt: Compact,
1236    {
1237        let start = Instant::now();
1238        self.ensure_no_queued_prune()?;
1239
1240        debug_assert!(self.writer.user_header().segment() == StaticFileSegment::Receipts);
1241        self.append_with_tx_number(tx_num, receipt)?;
1242
1243        if let Some(metrics) = &self.metrics {
1244            metrics.record_segment_operation(
1245                StaticFileSegment::Receipts,
1246                StaticFileProviderOperation::Append,
1247                Some(start.elapsed()),
1248            );
1249        }
1250
1251        Ok(())
1252    }
1253
1254    /// Appends multiple receipts to the static file.
1255    pub fn append_receipts<I, R>(&mut self, receipts: I) -> ProviderResult<()>
1256    where
1257        I: Iterator<Item = Result<(TxNumber, R), ProviderError>>,
1258        R: Borrow<N::Receipt>,
1259        N::Receipt: Compact,
1260    {
1261        debug_assert!(self.writer.user_header().segment() == StaticFileSegment::Receipts);
1262
1263        let mut receipts_iter = receipts.into_iter().peekable();
1264        // If receipts are empty, we can simply return None
1265        if receipts_iter.peek().is_none() {
1266            return Ok(());
1267        }
1268
1269        let start = Instant::now();
1270        self.ensure_no_queued_prune()?;
1271
1272        // At this point receipts contains at least one receipt, so this would be overwritten.
1273        let mut count: u64 = 0;
1274
1275        for receipt_result in receipts_iter {
1276            let (tx_num, receipt) = receipt_result?;
1277            self.append_with_tx_number(tx_num, receipt.borrow())?;
1278            count += 1;
1279        }
1280
1281        if let Some(metrics) = &self.metrics {
1282            metrics.record_segment_operations(
1283                StaticFileSegment::Receipts,
1284                StaticFileProviderOperation::Append,
1285                count,
1286                Some(start.elapsed()),
1287            );
1288        }
1289
1290        Ok(())
1291    }
1292
1293    /// Appends transaction sender to static file.
1294    ///
1295    /// It **DOES NOT** call `increment_block()`, it should be handled elsewhere. There might be
1296    /// empty blocks and this function wouldn't be called.
1297    pub fn append_transaction_sender(
1298        &mut self,
1299        tx_num: TxNumber,
1300        sender: &alloy_primitives::Address,
1301    ) -> ProviderResult<()> {
1302        let start = Instant::now();
1303        self.ensure_no_queued_prune()?;
1304
1305        debug_assert!(self.writer.user_header().segment() == StaticFileSegment::TransactionSenders);
1306        self.append_with_tx_number(tx_num, sender)?;
1307
1308        if let Some(metrics) = &self.metrics {
1309            metrics.record_segment_operation(
1310                StaticFileSegment::TransactionSenders,
1311                StaticFileProviderOperation::Append,
1312                Some(start.elapsed()),
1313            );
1314        }
1315
1316        Ok(())
1317    }
1318
1319    /// Appends multiple transaction senders to the static file.
1320    pub fn append_transaction_senders<I>(&mut self, senders: I) -> ProviderResult<()>
1321    where
1322        I: Iterator<Item = (TxNumber, alloy_primitives::Address)>,
1323    {
1324        debug_assert!(self.writer.user_header().segment() == StaticFileSegment::TransactionSenders);
1325
1326        let mut senders_iter = senders.into_iter().peekable();
1327        // If senders are empty, we can simply return
1328        if senders_iter.peek().is_none() {
1329            return Ok(());
1330        }
1331
1332        let start = Instant::now();
1333        self.ensure_no_queued_prune()?;
1334
1335        // At this point senders contains at least one sender, so this would be overwritten.
1336        let mut count: u64 = 0;
1337        for (tx_num, sender) in senders_iter {
1338            self.append_with_tx_number(tx_num, sender)?;
1339            count += 1;
1340        }
1341
1342        if let Some(metrics) = &self.metrics {
1343            metrics.record_segment_operations(
1344                StaticFileSegment::TransactionSenders,
1345                StaticFileProviderOperation::Append,
1346                count,
1347                Some(start.elapsed()),
1348            );
1349        }
1350
1351        Ok(())
1352    }
1353
1354    /// Appends a block changeset to the static file.
1355    ///
1356    /// It **CALLS** `increment_block()`.
1357    ///
1358    /// Returns the current number of changesets in the file, if any.
1359    pub fn append_account_changeset(
1360        &mut self,
1361        mut changeset: Vec<AccountBeforeTx>,
1362        block_number: u64,
1363    ) -> ProviderResult<()> {
1364        debug_assert!(self.writer.user_header().segment() == StaticFileSegment::AccountChangeSets);
1365        let start = Instant::now();
1366
1367        self.increment_block(block_number)?;
1368        self.ensure_no_queued_prune()?;
1369
1370        // first sort the changeset by address
1371        changeset.sort_by_key(|change| change.address);
1372
1373        let mut count: u64 = 0;
1374
1375        for change in changeset {
1376            self.append_change(&change)?;
1377            count += 1;
1378        }
1379
1380        if let Some(metrics) = &self.metrics {
1381            metrics.record_segment_operations(
1382                StaticFileSegment::AccountChangeSets,
1383                StaticFileProviderOperation::Append,
1384                count,
1385                Some(start.elapsed()),
1386            );
1387        }
1388
1389        Ok(())
1390    }
1391
1392    /// Starts a block account changeset that will be appended one entry at a time.
1393    ///
1394    /// Callers must append entries sorted by address and keep the writer open until the block is
1395    /// complete so the changeset offset sidecar is finalized with the correct row count.
1396    pub fn begin_account_changeset(&mut self, block_number: u64) -> ProviderResult<()> {
1397        debug_assert!(self.writer.user_header().segment() == StaticFileSegment::AccountChangeSets);
1398
1399        self.increment_block(block_number)?;
1400        self.ensure_no_queued_prune()
1401    }
1402
1403    /// Appends one account changeset entry to the current block.
1404    ///
1405    /// [`Self::begin_account_changeset`] must be called first.
1406    pub fn append_account_changeset_entry(
1407        &mut self,
1408        change: AccountBeforeTx,
1409    ) -> ProviderResult<()> {
1410        debug_assert!(self.writer.user_header().segment() == StaticFileSegment::AccountChangeSets);
1411        if self.current_changeset_offset.is_none() {
1412            return Err(ProviderError::other(StaticFileWriterError::new(
1413                "account changeset stream must be started before appending entries",
1414            )))
1415        }
1416
1417        self.append_change(&change)
1418    }
1419
1420    /// Appends a block storage changeset to the static file.
1421    ///
1422    /// It **CALLS** `increment_block()`.
1423    pub fn append_storage_changeset(
1424        &mut self,
1425        mut changeset: Vec<StorageBeforeTx>,
1426        block_number: u64,
1427    ) -> ProviderResult<()> {
1428        debug_assert!(self.writer.user_header().segment() == StaticFileSegment::StorageChangeSets);
1429        let start = Instant::now();
1430
1431        self.increment_block(block_number)?;
1432        self.ensure_no_queued_prune()?;
1433
1434        // sort by address + storage key
1435        changeset.sort_by_key(|change| (change.address, change.key));
1436
1437        let mut count: u64 = 0;
1438        for change in changeset {
1439            self.append_change(&change)?;
1440            count += 1;
1441        }
1442
1443        if let Some(metrics) = &self.metrics {
1444            metrics.record_segment_operations(
1445                StaticFileSegment::StorageChangeSets,
1446                StaticFileProviderOperation::Append,
1447                count,
1448                Some(start.elapsed()),
1449            );
1450        }
1451
1452        Ok(())
1453    }
1454
1455    /// Starts a block storage changeset that will be appended one entry at a time.
1456    ///
1457    /// Callers must append entries sorted by address and storage key and keep the writer open until
1458    /// the block is complete so the changeset offset sidecar is finalized with the correct row
1459    /// count.
1460    pub fn begin_storage_changeset(&mut self, block_number: u64) -> ProviderResult<()> {
1461        debug_assert!(self.writer.user_header().segment() == StaticFileSegment::StorageChangeSets);
1462
1463        self.increment_block(block_number)?;
1464        self.ensure_no_queued_prune()
1465    }
1466
1467    /// Appends one storage changeset entry to the current block.
1468    ///
1469    /// [`Self::begin_storage_changeset`] must be called first.
1470    pub fn append_storage_changeset_entry(
1471        &mut self,
1472        change: StorageBeforeTx,
1473    ) -> ProviderResult<()> {
1474        debug_assert!(self.writer.user_header().segment() == StaticFileSegment::StorageChangeSets);
1475        if self.current_changeset_offset.is_none() {
1476            return Err(ProviderError::other(StaticFileWriterError::new(
1477                "storage changeset stream must be started before appending entries",
1478            )))
1479        }
1480
1481        self.append_change(&change)
1482    }
1483
1484    /// Adds an instruction to prune `to_delete` transactions during commit.
1485    ///
1486    /// Note: `last_block` refers to the block the unwinds ends at.
1487    pub fn prune_transactions(
1488        &mut self,
1489        to_delete: u64,
1490        last_block: BlockNumber,
1491    ) -> ProviderResult<()> {
1492        debug_assert_eq!(self.writer.user_header().segment(), StaticFileSegment::Transactions);
1493        self.queue_prune(PruneStrategy::Transactions { num_rows: to_delete, last_block })
1494    }
1495
1496    /// Adds an instruction to prune `to_delete` receipts during commit.
1497    ///
1498    /// Note: `last_block` refers to the block the unwinds ends at.
1499    pub fn prune_receipts(
1500        &mut self,
1501        to_delete: u64,
1502        last_block: BlockNumber,
1503    ) -> ProviderResult<()> {
1504        debug_assert_eq!(self.writer.user_header().segment(), StaticFileSegment::Receipts);
1505        self.queue_prune(PruneStrategy::Receipts { num_rows: to_delete, last_block })
1506    }
1507
1508    /// Adds an instruction to prune `to_delete` transaction senders during commit.
1509    ///
1510    /// Note: `last_block` refers to the block the unwinds ends at.
1511    pub fn prune_transaction_senders(
1512        &mut self,
1513        to_delete: u64,
1514        last_block: BlockNumber,
1515    ) -> ProviderResult<()> {
1516        debug_assert_eq!(
1517            self.writer.user_header().segment(),
1518            StaticFileSegment::TransactionSenders
1519        );
1520        self.queue_prune(PruneStrategy::TransactionSenders { num_rows: to_delete, last_block })
1521    }
1522
1523    /// Adds an instruction to prune `to_delete` headers during commit.
1524    pub fn prune_headers(&mut self, to_delete: u64) -> ProviderResult<()> {
1525        debug_assert_eq!(self.writer.user_header().segment(), StaticFileSegment::Headers);
1526        self.queue_prune(PruneStrategy::Headers { num_blocks: to_delete })
1527    }
1528
1529    /// Adds an instruction to prune changesets until the given block.
1530    pub fn prune_account_changesets(&mut self, last_block: u64) -> ProviderResult<()> {
1531        debug_assert_eq!(self.writer.user_header().segment(), StaticFileSegment::AccountChangeSets);
1532        self.queue_prune(PruneStrategy::AccountChangeSets { last_block })
1533    }
1534
1535    /// Adds an instruction to prune storage changesets until the given block.
1536    pub fn prune_storage_changesets(&mut self, last_block: u64) -> ProviderResult<()> {
1537        debug_assert_eq!(self.writer.user_header().segment(), StaticFileSegment::StorageChangeSets);
1538        self.queue_prune(PruneStrategy::StorageChangeSets { last_block })
1539    }
1540
1541    /// Adds an instruction to prune elements during commit using the specified strategy.
1542    fn queue_prune(&mut self, strategy: PruneStrategy) -> ProviderResult<()> {
1543        self.ensure_no_queued_prune()?;
1544        self.prune_on_commit = Some(strategy);
1545        Ok(())
1546    }
1547
1548    /// Returns Error if there is a pruning instruction that needs to be applied.
1549    fn ensure_no_queued_prune(&self) -> ProviderResult<()> {
1550        if self.prune_on_commit.is_some() {
1551            return Err(ProviderError::other(StaticFileWriterError::new(
1552                "Pruning should be committed before appending or pruning more data",
1553            )));
1554        }
1555        Ok(())
1556    }
1557
1558    /// Removes the last `to_delete` transactions from the data file.
1559    fn prune_transaction_data(
1560        &mut self,
1561        to_delete: u64,
1562        last_block: BlockNumber,
1563    ) -> ProviderResult<()> {
1564        let start = Instant::now();
1565
1566        debug_assert!(self.writer.user_header().segment() == StaticFileSegment::Transactions);
1567
1568        self.truncate(to_delete, Some(last_block))?;
1569
1570        if let Some(metrics) = &self.metrics {
1571            metrics.record_segment_operation(
1572                StaticFileSegment::Transactions,
1573                StaticFileProviderOperation::Prune,
1574                Some(start.elapsed()),
1575            );
1576        }
1577
1578        Ok(())
1579    }
1580
1581    /// Prunes the last `to_delete` account changesets from the data file.
1582    fn prune_account_changeset_data(&mut self, last_block: BlockNumber) -> ProviderResult<()> {
1583        let start = Instant::now();
1584
1585        debug_assert!(self.writer.user_header().segment() == StaticFileSegment::AccountChangeSets);
1586
1587        self.truncate_changesets(last_block)?;
1588
1589        if let Some(metrics) = &self.metrics {
1590            metrics.record_segment_operation(
1591                StaticFileSegment::AccountChangeSets,
1592                StaticFileProviderOperation::Prune,
1593                Some(start.elapsed()),
1594            );
1595        }
1596
1597        Ok(())
1598    }
1599
1600    /// Prunes the last storage changesets from the data file.
1601    fn prune_storage_changeset_data(&mut self, last_block: BlockNumber) -> ProviderResult<()> {
1602        let start = Instant::now();
1603
1604        debug_assert!(self.writer.user_header().segment() == StaticFileSegment::StorageChangeSets);
1605
1606        self.truncate_changesets(last_block)?;
1607
1608        if let Some(metrics) = &self.metrics {
1609            metrics.record_segment_operation(
1610                StaticFileSegment::StorageChangeSets,
1611                StaticFileProviderOperation::Prune,
1612                Some(start.elapsed()),
1613            );
1614        }
1615
1616        Ok(())
1617    }
1618
1619    /// Prunes the last `to_delete` receipts from the data file.
1620    fn prune_receipt_data(
1621        &mut self,
1622        to_delete: u64,
1623        last_block: BlockNumber,
1624    ) -> ProviderResult<()> {
1625        let start = Instant::now();
1626
1627        debug_assert!(self.writer.user_header().segment() == StaticFileSegment::Receipts);
1628
1629        self.truncate(to_delete, Some(last_block))?;
1630
1631        if let Some(metrics) = &self.metrics {
1632            metrics.record_segment_operation(
1633                StaticFileSegment::Receipts,
1634                StaticFileProviderOperation::Prune,
1635                Some(start.elapsed()),
1636            );
1637        }
1638
1639        Ok(())
1640    }
1641
1642    /// Prunes the last `to_delete` transaction senders from the data file.
1643    fn prune_transaction_sender_data(
1644        &mut self,
1645        to_delete: u64,
1646        last_block: BlockNumber,
1647    ) -> ProviderResult<()> {
1648        let start = Instant::now();
1649
1650        debug_assert!(self.writer.user_header().segment() == StaticFileSegment::TransactionSenders);
1651
1652        self.truncate(to_delete, Some(last_block))?;
1653
1654        if let Some(metrics) = &self.metrics {
1655            metrics.record_segment_operation(
1656                StaticFileSegment::TransactionSenders,
1657                StaticFileProviderOperation::Prune,
1658                Some(start.elapsed()),
1659            );
1660        }
1661
1662        Ok(())
1663    }
1664
1665    /// Prunes the last `to_delete` headers from the data file.
1666    fn prune_header_data(&mut self, to_delete: u64) -> ProviderResult<()> {
1667        let start = Instant::now();
1668
1669        debug_assert!(self.writer.user_header().segment() == StaticFileSegment::Headers);
1670
1671        self.truncate(to_delete, None)?;
1672
1673        if let Some(metrics) = &self.metrics {
1674            metrics.record_segment_operation(
1675                StaticFileSegment::Headers,
1676                StaticFileProviderOperation::Prune,
1677                Some(start.elapsed()),
1678            );
1679        }
1680
1681        Ok(())
1682    }
1683
1684    /// Returns a [`StaticFileProvider`] associated with this writer.
1685    pub fn reader(&self) -> StaticFileProvider<N> {
1686        Self::upgrade_provider_to_strong_reference(&self.reader)
1687    }
1688
1689    /// Upgrades a weak reference of [`StaticFileProviderInner`] to a strong reference
1690    /// [`StaticFileProvider`].
1691    ///
1692    /// # Panics
1693    ///
1694    /// Panics if the parent [`StaticFileProvider`] is fully dropped while the child writer is still
1695    /// active. In reality, it's impossible to detach the [`StaticFileProviderRW`] from the
1696    /// [`StaticFileProvider`].
1697    fn upgrade_provider_to_strong_reference(
1698        provider: &Weak<StaticFileProviderInner<N>>,
1699    ) -> StaticFileProvider<N> {
1700        provider.upgrade().map(StaticFileProvider).expect("StaticFileProvider is dropped")
1701    }
1702
1703    /// Helper function to access [`SegmentHeader`].
1704    pub const fn user_header(&self) -> &SegmentHeader {
1705        self.writer.user_header()
1706    }
1707
1708    /// Helper function to access a mutable reference to [`SegmentHeader`].
1709    pub const fn user_header_mut(&mut self) -> &mut SegmentHeader {
1710        self.writer.user_header_mut()
1711    }
1712
1713    /// Helper function to override block range for testing.
1714    #[cfg(any(test, feature = "test-utils"))]
1715    pub const fn set_block_range(&mut self, block_range: std::ops::RangeInclusive<BlockNumber>) {
1716        self.writer.user_header_mut().set_block_range(*block_range.start(), *block_range.end())
1717    }
1718
1719    /// Helper function to override block range for testing.
1720    #[cfg(any(test, feature = "test-utils"))]
1721    pub const fn inner(&mut self) -> &mut NippyJarWriter<SegmentHeader> {
1722        &mut self.writer
1723    }
1724}
1725
1726fn create_jar(
1727    segment: StaticFileSegment,
1728    path: &Path,
1729    expected_block_range: SegmentRangeInclusive,
1730) -> NippyJar<SegmentHeader> {
1731    let mut jar = NippyJar::new(
1732        segment.columns(),
1733        path,
1734        SegmentHeader::new(expected_block_range, None, None, segment),
1735    );
1736
1737    // Transaction and Receipt already have the compression scheme used natively in its encoding.
1738    // (zstd-dictionary)
1739    if segment.is_headers() {
1740        jar = jar.with_lz4();
1741    }
1742
1743    jar
1744}