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