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    /// Anchors an empty non-header segment so the next append starts at `block + 1`.
779    /// Preserves the file's expected range, since reopening derives the file name from it;
780    /// refuses segments containing blocks or rows.
781    pub fn initialize_pruned_anchor(&mut self, block: BlockNumber) -> ProviderResult<()> {
782        let header = self.user_header();
783        if header.segment().is_headers() ||
784            header.block_range().is_some() ||
785            self.writer.rows() != 0
786        {
787            return Err(ProviderError::other(StaticFileWriterError::new(
788                "pruned anchor requires a fresh non-header segment",
789            )))
790        }
791        if !header.expected_block_range().contains(block) {
792            return Err(ProviderError::other(StaticFileWriterError::new(
793                "pruned anchor is outside the static file range",
794            )))
795        }
796        self.writer.user_header_mut().set_block_range(block, block);
797        if self.user_header().segment().is_change_based() {
798            self.writer.user_header_mut().set_changeset_offsets_len(1);
799            self.current_changeset_offset = Some(ChangesetOffset::new(0, 0));
800        }
801        Ok(())
802    }
803
804    /// Allows to increment the [`SegmentHeader`] end block. It will commit the current static file,
805    /// and create the next one if we are past the end range.
806    pub fn increment_block(&mut self, expected_block_number: BlockNumber) -> ProviderResult<()> {
807        let segment = self.writer.user_header().segment();
808
809        self.check_next_block_number(expected_block_number)?;
810
811        let start = Instant::now();
812        if let Some(last_block) = self.writer.user_header().block_end() {
813            // We have finished the previous static file and must freeze it
814            if last_block == self.writer.user_header().expected_block_end() {
815                // Commits offsets and new user_header to disk
816                self.commit()?;
817
818                // Opens the new static file
819                let (writer, data_path) =
820                    Self::open(segment, last_block + 1, self.reader.clone(), self.metrics.clone())?;
821                self.writer = writer;
822                self.data_path = data_path.clone();
823
824                // Update changeset offsets writer for the new file (starts empty)
825                if segment.is_change_based() {
826                    let csoff_path = data_path.with_extension("csoff");
827                    self.changeset_offsets = Some(
828                        ChangesetOffsetWriter::new(&csoff_path, 0).map_err(ProviderError::other)?,
829                    );
830                }
831
832                *self.writer.user_header_mut() = SegmentHeader::new(
833                    self.reader().find_fixed_range(segment, last_block + 1),
834                    None,
835                    None,
836                    segment,
837                );
838            }
839        }
840
841        self.writer.user_header_mut().increment_block();
842
843        // Handle changeset offset tracking for changeset segments
844        if segment.is_change_based() {
845            // Write previous block's offset if we have one
846            if let Some(offset) = self.current_changeset_offset.take() &&
847                let Some(writer) = &mut self.changeset_offsets
848            {
849                writer.append(&offset).map_err(ProviderError::other)?;
850            }
851            // Start tracking new block's offset
852            let new_offset = self.writer.rows() as u64;
853            self.current_changeset_offset = Some(ChangesetOffset::new(new_offset, 0));
854        }
855
856        if let Some(metrics) = &self.metrics {
857            metrics.record_segment_operation(
858                segment,
859                StaticFileProviderOperation::IncrementBlock,
860                Some(start.elapsed()),
861            );
862        }
863
864        Ok(())
865    }
866
867    /// Returns the current block number of the static file writer.
868    pub fn current_block_number(&self) -> Option<u64> {
869        self.writer.user_header().block_end()
870    }
871
872    /// Returns a block number that is one next to the current tip of static files.
873    pub fn next_block_number(&self) -> u64 {
874        // The next static file block number can be found by checking the one after block_end.
875        // However, if it's a new file that hasn't been added any data, its block range will
876        // actually be None. In that case, the next block will be found on `expected_block_start`.
877        self.writer
878            .user_header()
879            .block_end()
880            .map(|b| b + 1)
881            .unwrap_or_else(|| self.writer.user_header().expected_block_start())
882    }
883
884    /// Verifies if the incoming block number matches the next expected block number
885    /// for a static file. This ensures data continuity when adding new blocks.
886    fn check_next_block_number(&self, expected_block_number: u64) -> ProviderResult<()> {
887        let next_static_file_block = self.next_block_number();
888
889        if expected_block_number != next_static_file_block {
890            return Err(ProviderError::UnexpectedStaticFileBlockNumber(
891                self.writer.user_header().segment(),
892                expected_block_number,
893                next_static_file_block,
894            ))
895        }
896        Ok(())
897    }
898
899    /// Truncates account changesets to the given block. It deletes and loads an older static file
900    /// if the block goes beyond the start of the current block range.
901    ///
902    /// # Note
903    /// Commits to the configuration file at the end
904    fn truncate_changesets(&mut self, last_block: u64) -> ProviderResult<()> {
905        let segment = self.writer.user_header().segment();
906        debug_assert!(segment.is_change_based());
907
908        // Get the current block range
909        let current_block_end = self
910            .writer
911            .user_header()
912            .block_end()
913            .ok_or(ProviderError::MissingStaticFileBlock(segment, 0))?;
914
915        // If we're already at or before the target block, nothing to do
916        if current_block_end <= last_block {
917            return Ok(())
918        }
919
920        self.ensure_above_pruned_anchor(last_block)?;
921
922        // Navigate to the correct file if the target block is in a previous file
923        let mut expected_block_start = self.writer.user_header().expected_block_start();
924        while last_block < expected_block_start && expected_block_start > 0 {
925            self.delete_current_and_open_previous()?;
926            expected_block_start = self.writer.user_header().expected_block_start();
927        }
928
929        // An anchor can start inside the file's fixed range, shifting the sidecar indices.
930        let block_start = self.writer.user_header().block_start().unwrap_or(expected_block_start);
931
932        // Find the number of rows to keep (up to and including last_block)
933        let blocks_to_keep =
934            if last_block >= block_start { last_block - block_start + 1 } else { 0 };
935
936        // Read changeset offsets from sidecar file to find where to truncate
937        let csoff_path = self.data_path.with_extension("csoff");
938        let changeset_offsets_len = self.writer.user_header().changeset_offsets_len();
939
940        // Flush any pending changeset offset before reading the sidecar
941        self.flush_current_changeset_offset()?;
942
943        let rows_to_keep = if blocks_to_keep == 0 {
944            0
945        } else if blocks_to_keep >= changeset_offsets_len {
946            // Keep all rows in this file
947            self.writer.rows() as u64
948        } else {
949            // Read offset for the block after last_block from sidecar.
950            // Use committed length from header, ignoring any uncommitted records
951            // that may exist in the file after a crash.
952            let reader = ChangesetOffsetReader::new(&csoff_path, changeset_offsets_len)
953                .map_err(ProviderError::other)?;
954            if let Some(next_offset) = reader.get(blocks_to_keep).map_err(ProviderError::other)? {
955                next_offset.offset()
956            } else {
957                // If we can't read the offset, keep all rows
958                self.writer.rows() as u64
959            }
960        };
961
962        let total_rows = self.writer.rows() as u64;
963        let rows_to_delete = total_rows.saturating_sub(rows_to_keep);
964
965        if rows_to_delete > 0 {
966            // Calculate the number of blocks to prune
967            let current_block_end = self
968                .writer
969                .user_header()
970                .block_end()
971                .ok_or(ProviderError::MissingStaticFileBlock(segment, 0))?;
972            let blocks_to_remove = current_block_end - last_block;
973
974            // Update segment header - for changesets, prune expects number of blocks, not rows
975            self.writer.user_header_mut().prune(blocks_to_remove);
976
977            // Prune the actual rows
978            self.writer.prune_rows(rows_to_delete as usize).map_err(ProviderError::other)?;
979        }
980
981        // Update the block range
982        self.writer.user_header_mut().set_block_range(block_start, last_block);
983
984        // Sync changeset offsets to match the new block range
985        self.writer.user_header_mut().sync_changeset_offsets();
986
987        // Truncate the sidecar file to match the new block count
988        if let Some(writer) = &mut self.changeset_offsets {
989            writer.truncate(blocks_to_keep).map_err(ProviderError::other)?;
990        }
991
992        // Clear current changeset offset tracking since we've pruned
993        self.current_changeset_offset = None;
994
995        // Commits new changes to disk
996        self.commit()?;
997
998        Ok(())
999    }
1000
1001    /// Truncates a number of rows from disk. It deletes and loads an older static file if block
1002    /// goes beyond the start of the current block range.
1003    ///
1004    /// **`last_block`** should be passed only with transaction based segments.
1005    ///
1006    /// # Note
1007    /// Commits to the configuration file at the end.
1008    fn truncate(&mut self, num_rows: u64, last_block: Option<u64>) -> ProviderResult<()> {
1009        if let Some(last_block) = last_block {
1010            self.ensure_above_pruned_anchor(last_block)?;
1011        }
1012        let mut remaining_rows = num_rows;
1013        let segment = self.writer.user_header().segment();
1014        while remaining_rows > 0 {
1015            let len = if segment.is_block_based() {
1016                self.writer.user_header().block_len().unwrap_or_default()
1017            } else {
1018                self.writer.user_header().tx_len().unwrap_or_default()
1019            };
1020
1021            if remaining_rows >= len {
1022                // If there's more rows to delete than this static file contains, then just
1023                // delete the whole file and go to the next static file
1024                let block_start = self.writer.user_header().expected_block_start();
1025
1026                // We only delete the file if it's NOT the first static file AND:
1027                // * it's a Header segment  OR
1028                // * it's a tx-based segment AND `last_block` is lower than the first block of this
1029                //   file's block range. Otherwise, having no rows simply means that this block
1030                //   range has no transactions, but the file should remain.
1031                if block_start != 0 &&
1032                    (segment.is_headers() || last_block.is_some_and(|b| b < block_start))
1033                {
1034                    self.delete_current_and_open_previous()?;
1035                } else {
1036                    // Update `SegmentHeader`
1037                    self.writer.user_header_mut().prune(len);
1038                    self.writer.prune_rows(len as usize).map_err(ProviderError::other)?;
1039                    break
1040                }
1041
1042                remaining_rows -= len;
1043            } else {
1044                // Update `SegmentHeader`
1045                self.writer.user_header_mut().prune(remaining_rows);
1046
1047                // Truncate data
1048                self.writer.prune_rows(remaining_rows as usize).map_err(ProviderError::other)?;
1049                remaining_rows = 0;
1050            }
1051        }
1052
1053        // Only Transactions and Receipts
1054        if let Some(last_block) = last_block {
1055            let mut expected_block_start = self.writer.user_header().expected_block_start();
1056
1057            if num_rows == 0 {
1058                // Edge case for when we are unwinding a chain of empty blocks that goes across
1059                // files, and therefore, the only reference point to know which file
1060                // we are supposed to be at is `last_block`.
1061                while last_block < expected_block_start {
1062                    self.delete_current_and_open_previous()?;
1063                    expected_block_start = self.writer.user_header().expected_block_start();
1064                }
1065            }
1066            // Preserve the anchor; the fixed file range may start before any retained history.
1067            let block_start =
1068                self.writer.user_header().block_start().unwrap_or(expected_block_start);
1069            self.writer.user_header_mut().set_block_range(block_start, last_block);
1070        }
1071
1072        // Commits new changes to disk.
1073        self.commit()?;
1074
1075        Ok(())
1076    }
1077
1078    /// Delete the current static file, and replace this provider writer with the previous static
1079    /// file.
1080    fn delete_current_and_open_previous(&mut self) -> Result<(), ProviderError> {
1081        let segment = self.user_header().segment();
1082        let current_path = self.data_path.clone();
1083        let (previous_writer, data_path) = Self::open(
1084            segment,
1085            self.writer.user_header().expected_block_start() - 1,
1086            self.reader.clone(),
1087            self.metrics.clone(),
1088        )?;
1089        self.writer = previous_writer;
1090        self.writer.set_dirty();
1091        self.data_path = data_path.clone();
1092
1093        // Delete the sidecar file for changeset segments before deleting the main jar
1094        if segment.is_change_based() {
1095            let csoff_path = current_path.with_extension("csoff");
1096            if csoff_path.exists() {
1097                std::fs::remove_file(&csoff_path).map_err(ProviderError::other)?;
1098            }
1099            // Re-initialize the changeset offsets writer for the previous file
1100            let new_csoff_path = data_path.with_extension("csoff");
1101            let committed_len = self.writer.user_header().changeset_offsets_len();
1102            self.changeset_offsets = Some(
1103                ChangesetOffsetWriter::new(&new_csoff_path, committed_len)
1104                    .map_err(ProviderError::other)?,
1105            );
1106        }
1107
1108        // Clear current changeset offset tracking since we're switching files
1109        self.current_changeset_offset = None;
1110
1111        NippyJar::<SegmentHeader>::load(&current_path)
1112            .map_err(ProviderError::other)?
1113            .delete()
1114            .map_err(ProviderError::other)?;
1115        Ok(())
1116    }
1117
1118    /// Errors if the segment is anchored above `last_block`, since nothing below the anchor is
1119    /// stored and the unwind cannot be applied. Must run before the unwind deletes any file.
1120    fn ensure_above_pruned_anchor(&self, last_block: BlockNumber) -> ProviderResult<()> {
1121        let header = self.writer.user_header();
1122        let lowest = self.reader().get_lowest_range_start(header.segment());
1123        // Before the first commit, the current file is the only source of the anchor,
1124        // including when it lies on a file boundary.
1125        let current = header
1126            .block_start()
1127            .filter(|start| lowest.is_none() || *start > header.expected_block_start());
1128        if current.into_iter().chain(lowest).any(|start| last_block < start) {
1129            return Err(ProviderError::other(StaticFileWriterError::new(
1130                "cannot unwind below the pruned anchor",
1131            )))
1132        }
1133        Ok(())
1134    }
1135
1136    /// Appends column to static file.
1137    fn append_column<T: Compact>(&mut self, column: T) -> ProviderResult<()> {
1138        self.buf.clear();
1139        column.to_compact(&mut self.buf);
1140
1141        self.writer.append_column(Some(Ok(&self.buf))).map_err(ProviderError::other)?;
1142        Ok(())
1143    }
1144
1145    /// Appends to tx number-based static file.
1146    fn append_with_tx_number<V: Compact>(
1147        &mut self,
1148        tx_num: TxNumber,
1149        value: V,
1150    ) -> ProviderResult<()> {
1151        if let Some(range) = self.writer.user_header().tx_range() {
1152            let next_tx = range.end() + 1;
1153            if next_tx != tx_num {
1154                return Err(ProviderError::UnexpectedStaticFileTxNumber(
1155                    self.writer.user_header().segment(),
1156                    tx_num,
1157                    next_tx,
1158                ))
1159            }
1160            self.writer.user_header_mut().increment_tx();
1161        } else {
1162            self.writer.user_header_mut().set_tx_range(tx_num, tx_num);
1163        }
1164
1165        self.append_column(value)?;
1166
1167        Ok(())
1168    }
1169
1170    /// Appends change to changeset static file.
1171    fn append_change<V: Compact>(&mut self, change: &V) -> ProviderResult<()> {
1172        if let Some(ref mut offset) = self.current_changeset_offset {
1173            offset.increment_num_changes();
1174        }
1175        self.append_column(change)?;
1176        Ok(())
1177    }
1178
1179    /// Appends header to static file.
1180    ///
1181    /// It **CALLS** `increment_block()` since the number of headers is equal to the number of
1182    /// blocks.
1183    pub fn append_header(&mut self, header: &N::BlockHeader, hash: &BlockHash) -> ProviderResult<()>
1184    where
1185        N::BlockHeader: Compact,
1186    {
1187        self.append_header_with_td(header, U256::ZERO, hash)
1188    }
1189
1190    /// Appends header to static file with a specified total difficulty.
1191    ///
1192    /// It **CALLS** `increment_block()` since the number of headers is equal to the number of
1193    /// blocks.
1194    pub fn append_header_with_td(
1195        &mut self,
1196        header: &N::BlockHeader,
1197        total_difficulty: U256,
1198        hash: &BlockHash,
1199    ) -> ProviderResult<()>
1200    where
1201        N::BlockHeader: Compact,
1202    {
1203        let start = Instant::now();
1204        self.ensure_no_queued_prune()?;
1205
1206        debug_assert!(self.writer.user_header().segment() == StaticFileSegment::Headers);
1207
1208        self.increment_block(header.number())?;
1209
1210        self.append_column(header)?;
1211        self.append_column(CompactU256::from(total_difficulty))?;
1212        self.append_column(hash)?;
1213
1214        if let Some(metrics) = &self.metrics {
1215            metrics.record_segment_operation(
1216                StaticFileSegment::Headers,
1217                StaticFileProviderOperation::Append,
1218                Some(start.elapsed()),
1219            );
1220        }
1221
1222        Ok(())
1223    }
1224
1225    /// Appends header to static file without calling `increment_block`.
1226    /// This is useful for genesis blocks with non-zero block numbers.
1227    pub fn append_header_direct(
1228        &mut self,
1229        header: &N::BlockHeader,
1230        total_difficulty: U256,
1231        hash: &BlockHash,
1232    ) -> ProviderResult<()>
1233    where
1234        N::BlockHeader: Compact,
1235    {
1236        let start = Instant::now();
1237        self.ensure_no_queued_prune()?;
1238
1239        debug_assert!(self.writer.user_header().segment() == StaticFileSegment::Headers);
1240
1241        self.append_column(header)?;
1242        self.append_column(CompactU256::from(total_difficulty))?;
1243        self.append_column(hash)?;
1244
1245        if let Some(metrics) = &self.metrics {
1246            metrics.record_segment_operation(
1247                StaticFileSegment::Headers,
1248                StaticFileProviderOperation::Append,
1249                Some(start.elapsed()),
1250            );
1251        }
1252
1253        Ok(())
1254    }
1255
1256    /// Appends transaction to static file.
1257    ///
1258    /// It **DOES NOT CALL** `increment_block()`, it should be handled elsewhere. There might be
1259    /// empty blocks and this function wouldn't be called.
1260    pub fn append_transaction(&mut self, tx_num: TxNumber, tx: &N::SignedTx) -> ProviderResult<()>
1261    where
1262        N::SignedTx: Compact,
1263    {
1264        let start = Instant::now();
1265        self.ensure_no_queued_prune()?;
1266
1267        debug_assert!(self.writer.user_header().segment() == StaticFileSegment::Transactions);
1268        self.append_with_tx_number(tx_num, tx)?;
1269
1270        if let Some(metrics) = &self.metrics {
1271            metrics.record_segment_operation(
1272                StaticFileSegment::Transactions,
1273                StaticFileProviderOperation::Append,
1274                Some(start.elapsed()),
1275            );
1276        }
1277
1278        Ok(())
1279    }
1280
1281    /// Appends receipt 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_receipt(&mut self, tx_num: TxNumber, receipt: &N::Receipt) -> ProviderResult<()>
1286    where
1287        N::Receipt: Compact,
1288    {
1289        let start = Instant::now();
1290        self.ensure_no_queued_prune()?;
1291
1292        debug_assert!(self.writer.user_header().segment() == StaticFileSegment::Receipts);
1293        self.append_with_tx_number(tx_num, receipt)?;
1294
1295        if let Some(metrics) = &self.metrics {
1296            metrics.record_segment_operation(
1297                StaticFileSegment::Receipts,
1298                StaticFileProviderOperation::Append,
1299                Some(start.elapsed()),
1300            );
1301        }
1302
1303        Ok(())
1304    }
1305
1306    /// Appends multiple receipts to the static file.
1307    pub fn append_receipts<I, R>(&mut self, receipts: I) -> ProviderResult<()>
1308    where
1309        I: Iterator<Item = Result<(TxNumber, R), ProviderError>>,
1310        R: Borrow<N::Receipt>,
1311        N::Receipt: Compact,
1312    {
1313        debug_assert!(self.writer.user_header().segment() == StaticFileSegment::Receipts);
1314
1315        let mut receipts_iter = receipts.into_iter().peekable();
1316        // If receipts are empty, we can simply return None
1317        if receipts_iter.peek().is_none() {
1318            return Ok(());
1319        }
1320
1321        let start = Instant::now();
1322        self.ensure_no_queued_prune()?;
1323
1324        // At this point receipts contains at least one receipt, so this would be overwritten.
1325        let mut count: u64 = 0;
1326
1327        for receipt_result in receipts_iter {
1328            let (tx_num, receipt) = receipt_result?;
1329            self.append_with_tx_number(tx_num, receipt.borrow())?;
1330            count += 1;
1331        }
1332
1333        if let Some(metrics) = &self.metrics {
1334            metrics.record_segment_operations(
1335                StaticFileSegment::Receipts,
1336                StaticFileProviderOperation::Append,
1337                count,
1338                Some(start.elapsed()),
1339            );
1340        }
1341
1342        Ok(())
1343    }
1344
1345    /// Appends transaction sender to static file.
1346    ///
1347    /// It **DOES NOT** call `increment_block()`, it should be handled elsewhere. There might be
1348    /// empty blocks and this function wouldn't be called.
1349    pub fn append_transaction_sender(
1350        &mut self,
1351        tx_num: TxNumber,
1352        sender: &alloy_primitives::Address,
1353    ) -> ProviderResult<()> {
1354        let start = Instant::now();
1355        self.ensure_no_queued_prune()?;
1356
1357        debug_assert!(self.writer.user_header().segment() == StaticFileSegment::TransactionSenders);
1358        self.append_with_tx_number(tx_num, sender)?;
1359
1360        if let Some(metrics) = &self.metrics {
1361            metrics.record_segment_operation(
1362                StaticFileSegment::TransactionSenders,
1363                StaticFileProviderOperation::Append,
1364                Some(start.elapsed()),
1365            );
1366        }
1367
1368        Ok(())
1369    }
1370
1371    /// Appends multiple transaction senders to the static file.
1372    pub fn append_transaction_senders<I>(&mut self, senders: I) -> ProviderResult<()>
1373    where
1374        I: Iterator<Item = (TxNumber, alloy_primitives::Address)>,
1375    {
1376        debug_assert!(self.writer.user_header().segment() == StaticFileSegment::TransactionSenders);
1377
1378        let mut senders_iter = senders.into_iter().peekable();
1379        // If senders are empty, we can simply return
1380        if senders_iter.peek().is_none() {
1381            return Ok(());
1382        }
1383
1384        let start = Instant::now();
1385        self.ensure_no_queued_prune()?;
1386
1387        // At this point senders contains at least one sender, so this would be overwritten.
1388        let mut count: u64 = 0;
1389        for (tx_num, sender) in senders_iter {
1390            self.append_with_tx_number(tx_num, sender)?;
1391            count += 1;
1392        }
1393
1394        if let Some(metrics) = &self.metrics {
1395            metrics.record_segment_operations(
1396                StaticFileSegment::TransactionSenders,
1397                StaticFileProviderOperation::Append,
1398                count,
1399                Some(start.elapsed()),
1400            );
1401        }
1402
1403        Ok(())
1404    }
1405
1406    /// Appends a block changeset to the static file.
1407    ///
1408    /// It **CALLS** `increment_block()`.
1409    ///
1410    /// Returns the current number of changesets in the file, if any.
1411    pub fn append_account_changeset(
1412        &mut self,
1413        mut changeset: Vec<AccountBeforeTx>,
1414        block_number: u64,
1415    ) -> ProviderResult<()> {
1416        debug_assert!(self.writer.user_header().segment() == StaticFileSegment::AccountChangeSets);
1417        let start = Instant::now();
1418
1419        self.increment_block(block_number)?;
1420        self.ensure_no_queued_prune()?;
1421
1422        // first sort the changeset by address
1423        changeset.sort_by_key(|change| change.address);
1424
1425        let mut count: u64 = 0;
1426
1427        for change in changeset {
1428            self.append_change(&change)?;
1429            count += 1;
1430        }
1431
1432        if let Some(metrics) = &self.metrics {
1433            metrics.record_segment_operations(
1434                StaticFileSegment::AccountChangeSets,
1435                StaticFileProviderOperation::Append,
1436                count,
1437                Some(start.elapsed()),
1438            );
1439        }
1440
1441        Ok(())
1442    }
1443
1444    /// Starts a block account changeset that will be appended one entry at a time.
1445    ///
1446    /// Callers must append entries sorted by address and keep the writer open until the block is
1447    /// complete so the changeset offset sidecar is finalized with the correct row count.
1448    pub fn begin_account_changeset(&mut self, block_number: u64) -> ProviderResult<()> {
1449        debug_assert!(self.writer.user_header().segment() == StaticFileSegment::AccountChangeSets);
1450
1451        self.increment_block(block_number)?;
1452        self.ensure_no_queued_prune()
1453    }
1454
1455    /// Appends one account changeset entry to the current block.
1456    ///
1457    /// [`Self::begin_account_changeset`] must be called first.
1458    pub fn append_account_changeset_entry(
1459        &mut self,
1460        change: AccountBeforeTx,
1461    ) -> ProviderResult<()> {
1462        debug_assert!(self.writer.user_header().segment() == StaticFileSegment::AccountChangeSets);
1463        if self.current_changeset_offset.is_none() {
1464            return Err(ProviderError::other(StaticFileWriterError::new(
1465                "account changeset stream must be started before appending entries",
1466            )))
1467        }
1468
1469        self.append_change(&change)
1470    }
1471
1472    /// Appends a block storage changeset to the static file.
1473    ///
1474    /// It **CALLS** `increment_block()`.
1475    pub fn append_storage_changeset(
1476        &mut self,
1477        mut changeset: Vec<StorageBeforeTx>,
1478        block_number: u64,
1479    ) -> ProviderResult<()> {
1480        debug_assert!(self.writer.user_header().segment() == StaticFileSegment::StorageChangeSets);
1481        let start = Instant::now();
1482
1483        self.increment_block(block_number)?;
1484        self.ensure_no_queued_prune()?;
1485
1486        // sort by address + storage key
1487        changeset.sort_by_key(|change| (change.address, change.key));
1488
1489        let mut count: u64 = 0;
1490        for change in changeset {
1491            self.append_change(&change)?;
1492            count += 1;
1493        }
1494
1495        if let Some(metrics) = &self.metrics {
1496            metrics.record_segment_operations(
1497                StaticFileSegment::StorageChangeSets,
1498                StaticFileProviderOperation::Append,
1499                count,
1500                Some(start.elapsed()),
1501            );
1502        }
1503
1504        Ok(())
1505    }
1506
1507    /// Starts a block storage changeset that will be appended one entry at a time.
1508    ///
1509    /// Callers must append entries sorted by address and storage key and keep the writer open until
1510    /// the block is complete so the changeset offset sidecar is finalized with the correct row
1511    /// count.
1512    pub fn begin_storage_changeset(&mut self, block_number: u64) -> ProviderResult<()> {
1513        debug_assert!(self.writer.user_header().segment() == StaticFileSegment::StorageChangeSets);
1514
1515        self.increment_block(block_number)?;
1516        self.ensure_no_queued_prune()
1517    }
1518
1519    /// Appends one storage changeset entry to the current block.
1520    ///
1521    /// [`Self::begin_storage_changeset`] must be called first.
1522    pub fn append_storage_changeset_entry(
1523        &mut self,
1524        change: StorageBeforeTx,
1525    ) -> ProviderResult<()> {
1526        debug_assert!(self.writer.user_header().segment() == StaticFileSegment::StorageChangeSets);
1527        if self.current_changeset_offset.is_none() {
1528            return Err(ProviderError::other(StaticFileWriterError::new(
1529                "storage changeset stream must be started before appending entries",
1530            )))
1531        }
1532
1533        self.append_change(&change)
1534    }
1535
1536    /// Adds an instruction to prune `to_delete` transactions during commit.
1537    ///
1538    /// Note: `last_block` refers to the block the unwinds ends at.
1539    pub fn prune_transactions(
1540        &mut self,
1541        to_delete: u64,
1542        last_block: BlockNumber,
1543    ) -> ProviderResult<()> {
1544        debug_assert_eq!(self.writer.user_header().segment(), StaticFileSegment::Transactions);
1545        self.queue_prune(PruneStrategy::Transactions { num_rows: to_delete, last_block })
1546    }
1547
1548    /// Adds an instruction to prune `to_delete` receipts during commit.
1549    ///
1550    /// Note: `last_block` refers to the block the unwinds ends at.
1551    pub fn prune_receipts(
1552        &mut self,
1553        to_delete: u64,
1554        last_block: BlockNumber,
1555    ) -> ProviderResult<()> {
1556        debug_assert_eq!(self.writer.user_header().segment(), StaticFileSegment::Receipts);
1557        self.queue_prune(PruneStrategy::Receipts { num_rows: to_delete, last_block })
1558    }
1559
1560    /// Adds an instruction to prune `to_delete` transaction senders during commit.
1561    ///
1562    /// Note: `last_block` refers to the block the unwinds ends at.
1563    pub fn prune_transaction_senders(
1564        &mut self,
1565        to_delete: u64,
1566        last_block: BlockNumber,
1567    ) -> ProviderResult<()> {
1568        debug_assert_eq!(
1569            self.writer.user_header().segment(),
1570            StaticFileSegment::TransactionSenders
1571        );
1572        self.queue_prune(PruneStrategy::TransactionSenders { num_rows: to_delete, last_block })
1573    }
1574
1575    /// Adds an instruction to prune `to_delete` headers during commit.
1576    pub fn prune_headers(&mut self, to_delete: u64) -> ProviderResult<()> {
1577        debug_assert_eq!(self.writer.user_header().segment(), StaticFileSegment::Headers);
1578        self.queue_prune(PruneStrategy::Headers { num_blocks: to_delete })
1579    }
1580
1581    /// Adds an instruction to prune changesets until the given block.
1582    pub fn prune_account_changesets(&mut self, last_block: u64) -> ProviderResult<()> {
1583        debug_assert_eq!(self.writer.user_header().segment(), StaticFileSegment::AccountChangeSets);
1584        self.queue_prune(PruneStrategy::AccountChangeSets { last_block })
1585    }
1586
1587    /// Adds an instruction to prune storage changesets until the given block.
1588    pub fn prune_storage_changesets(&mut self, last_block: u64) -> ProviderResult<()> {
1589        debug_assert_eq!(self.writer.user_header().segment(), StaticFileSegment::StorageChangeSets);
1590        self.queue_prune(PruneStrategy::StorageChangeSets { last_block })
1591    }
1592
1593    /// Adds an instruction to prune elements during commit using the specified strategy.
1594    fn queue_prune(&mut self, strategy: PruneStrategy) -> ProviderResult<()> {
1595        self.ensure_no_queued_prune()?;
1596        // Unwinds commit MDBX before static files, so reject invalid targets before queuing them.
1597        match strategy {
1598            PruneStrategy::Transactions { last_block, .. } |
1599            PruneStrategy::Receipts { last_block, .. } |
1600            PruneStrategy::TransactionSenders { last_block, .. } |
1601            PruneStrategy::AccountChangeSets { last_block } |
1602            PruneStrategy::StorageChangeSets { last_block } => {
1603                self.ensure_above_pruned_anchor(last_block)?;
1604            }
1605            PruneStrategy::Headers { .. } => {}
1606        }
1607        self.prune_on_commit = Some(strategy);
1608        Ok(())
1609    }
1610
1611    /// Returns Error if there is a pruning instruction that needs to be applied.
1612    fn ensure_no_queued_prune(&self) -> ProviderResult<()> {
1613        if self.prune_on_commit.is_some() {
1614            return Err(ProviderError::other(StaticFileWriterError::new(
1615                "Pruning should be committed before appending or pruning more data",
1616            )));
1617        }
1618        Ok(())
1619    }
1620
1621    /// Removes the last `to_delete` transactions from the data file.
1622    fn prune_transaction_data(
1623        &mut self,
1624        to_delete: u64,
1625        last_block: BlockNumber,
1626    ) -> ProviderResult<()> {
1627        let start = Instant::now();
1628
1629        debug_assert!(self.writer.user_header().segment() == StaticFileSegment::Transactions);
1630
1631        self.truncate(to_delete, Some(last_block))?;
1632
1633        if let Some(metrics) = &self.metrics {
1634            metrics.record_segment_operation(
1635                StaticFileSegment::Transactions,
1636                StaticFileProviderOperation::Prune,
1637                Some(start.elapsed()),
1638            );
1639        }
1640
1641        Ok(())
1642    }
1643
1644    /// Prunes the last `to_delete` account changesets from the data file.
1645    fn prune_account_changeset_data(&mut self, last_block: BlockNumber) -> ProviderResult<()> {
1646        let start = Instant::now();
1647
1648        debug_assert!(self.writer.user_header().segment() == StaticFileSegment::AccountChangeSets);
1649
1650        self.truncate_changesets(last_block)?;
1651
1652        if let Some(metrics) = &self.metrics {
1653            metrics.record_segment_operation(
1654                StaticFileSegment::AccountChangeSets,
1655                StaticFileProviderOperation::Prune,
1656                Some(start.elapsed()),
1657            );
1658        }
1659
1660        Ok(())
1661    }
1662
1663    /// Prunes the last storage changesets from the data file.
1664    fn prune_storage_changeset_data(&mut self, last_block: BlockNumber) -> ProviderResult<()> {
1665        let start = Instant::now();
1666
1667        debug_assert!(self.writer.user_header().segment() == StaticFileSegment::StorageChangeSets);
1668
1669        self.truncate_changesets(last_block)?;
1670
1671        if let Some(metrics) = &self.metrics {
1672            metrics.record_segment_operation(
1673                StaticFileSegment::StorageChangeSets,
1674                StaticFileProviderOperation::Prune,
1675                Some(start.elapsed()),
1676            );
1677        }
1678
1679        Ok(())
1680    }
1681
1682    /// Prunes the last `to_delete` receipts from the data file.
1683    fn prune_receipt_data(
1684        &mut self,
1685        to_delete: u64,
1686        last_block: BlockNumber,
1687    ) -> ProviderResult<()> {
1688        let start = Instant::now();
1689
1690        debug_assert!(self.writer.user_header().segment() == StaticFileSegment::Receipts);
1691
1692        self.truncate(to_delete, Some(last_block))?;
1693
1694        if let Some(metrics) = &self.metrics {
1695            metrics.record_segment_operation(
1696                StaticFileSegment::Receipts,
1697                StaticFileProviderOperation::Prune,
1698                Some(start.elapsed()),
1699            );
1700        }
1701
1702        Ok(())
1703    }
1704
1705    /// Prunes the last `to_delete` transaction senders from the data file.
1706    fn prune_transaction_sender_data(
1707        &mut self,
1708        to_delete: u64,
1709        last_block: BlockNumber,
1710    ) -> ProviderResult<()> {
1711        let start = Instant::now();
1712
1713        debug_assert!(self.writer.user_header().segment() == StaticFileSegment::TransactionSenders);
1714
1715        self.truncate(to_delete, Some(last_block))?;
1716
1717        if let Some(metrics) = &self.metrics {
1718            metrics.record_segment_operation(
1719                StaticFileSegment::TransactionSenders,
1720                StaticFileProviderOperation::Prune,
1721                Some(start.elapsed()),
1722            );
1723        }
1724
1725        Ok(())
1726    }
1727
1728    /// Prunes the last `to_delete` headers from the data file.
1729    fn prune_header_data(&mut self, to_delete: u64) -> ProviderResult<()> {
1730        let start = Instant::now();
1731
1732        debug_assert!(self.writer.user_header().segment() == StaticFileSegment::Headers);
1733
1734        self.truncate(to_delete, None)?;
1735
1736        if let Some(metrics) = &self.metrics {
1737            metrics.record_segment_operation(
1738                StaticFileSegment::Headers,
1739                StaticFileProviderOperation::Prune,
1740                Some(start.elapsed()),
1741            );
1742        }
1743
1744        Ok(())
1745    }
1746
1747    /// Returns a [`StaticFileProvider`] associated with this writer.
1748    pub fn reader(&self) -> StaticFileProvider<N> {
1749        Self::upgrade_provider_to_strong_reference(&self.reader)
1750    }
1751
1752    /// Upgrades a weak reference of [`StaticFileProviderInner`] to a strong reference
1753    /// [`StaticFileProvider`].
1754    ///
1755    /// # Panics
1756    ///
1757    /// Panics if the parent [`StaticFileProvider`] is fully dropped while the child writer is still
1758    /// active. In reality, it's impossible to detach the [`StaticFileProviderRW`] from the
1759    /// [`StaticFileProvider`].
1760    fn upgrade_provider_to_strong_reference(
1761        provider: &Weak<StaticFileProviderInner<N>>,
1762    ) -> StaticFileProvider<N> {
1763        provider.upgrade().map(StaticFileProvider).expect("StaticFileProvider is dropped")
1764    }
1765
1766    /// Helper function to access [`SegmentHeader`].
1767    pub const fn user_header(&self) -> &SegmentHeader {
1768        self.writer.user_header()
1769    }
1770
1771    /// Helper function to access a mutable reference to [`SegmentHeader`].
1772    pub const fn user_header_mut(&mut self) -> &mut SegmentHeader {
1773        self.writer.user_header_mut()
1774    }
1775
1776    /// Helper function to override block range for testing.
1777    #[cfg(any(test, feature = "test-utils"))]
1778    pub const fn set_block_range(&mut self, block_range: std::ops::RangeInclusive<BlockNumber>) {
1779        self.writer.user_header_mut().set_block_range(*block_range.start(), *block_range.end())
1780    }
1781
1782    /// Helper function to override block range for testing.
1783    #[cfg(any(test, feature = "test-utils"))]
1784    pub const fn inner(&mut self) -> &mut NippyJarWriter<SegmentHeader> {
1785        &mut self.writer
1786    }
1787}
1788
1789fn create_jar(
1790    segment: StaticFileSegment,
1791    path: &Path,
1792    expected_block_range: SegmentRangeInclusive,
1793) -> NippyJar<SegmentHeader> {
1794    let mut jar = NippyJar::new(
1795        segment.columns(),
1796        path,
1797        SegmentHeader::new(expected_block_range, None, None, segment),
1798    );
1799
1800    // Transaction and Receipt already have the compression scheme used natively in its encoding.
1801    // (zstd-dictionary)
1802    if segment.is_headers() {
1803        jar = jar.with_lz4();
1804    }
1805
1806    jar
1807}