Skip to main content

espresso_node/persistence/
fs.rs

1use std::{
2    collections::BTreeMap,
3    fs::{self, File, OpenOptions},
4    io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write},
5    ops::RangeInclusive,
6    path::{Path, PathBuf},
7    sync::Arc,
8    time::Instant,
9};
10
11use alloy::primitives::Address;
12use anyhow::{Context, anyhow, bail};
13use async_lock::RwLock;
14use async_trait::async_trait;
15use clap::Parser;
16use espresso_types::{
17    AuthenticatedValidatorMap, Header, Leaf2, NetworkConfig, Payload, PubKey,
18    RegisteredValidatorMap, SeqTypes, StakeTableHash,
19    traits::{EventsPersistenceRead, MembershipPersistence, StakeTuple},
20    v0::traits::{EventConsumer, PersistenceOptions, SequencerPersistence, StateCertPersistence},
21    v0_3::{
22        AuthenticatedValidator, EventKey, IndexedStake, RegisteredValidator, RewardAmount,
23        StakeTableEvent,
24    },
25};
26use hotshot_new_protocol::message::Certificate2;
27use hotshot_types::{
28    data::{
29        DaProposal, DaProposal2, EpochNumber, QuorumProposalWrapper, QuorumProposalWrapperLegacy,
30        VidCommitment, VidDisperseShare,
31    },
32    drb::{DrbInput, DrbResult},
33    event::{HotShotAction, LeafInfo},
34    initializer::InitializerEpochInfo,
35    message::{Proposal, convert_proposal},
36    new_protocol::CoordinatorEvent,
37    simple_certificate::{
38        CertificatePair, LightClientStateUpdateCertificateV1, LightClientStateUpdateCertificateV2,
39        NextEpochQuorumCertificate2, QuorumCertificate2, UpgradeCertificate,
40    },
41    simple_vote::LockView,
42    traits::{
43        block_contents::{BlockHeader, BlockPayload},
44        metrics::Metrics,
45        node_implementation::NodeType,
46    },
47    vote::HasViewNumber,
48};
49use itertools::Itertools;
50
51use super::{
52    RegisteredValidatorNoX25519, RegisteredValidatorPreOption, RegisteredValidatorPreSchnorrOption,
53};
54use crate::{
55    RECENT_STAKE_TABLES_LIMIT, ViewNumber,
56    persistence::{
57        migrate_network_config,
58        persistence_metrics::PersistenceMetricsValue,
59        storage_probe::{self, StorageProbe},
60    },
61};
62
63/// Deserialize a stake table from bytes, trying current and legacy formats.
64/// Returns (stake_tuple, needs_rewrite) where needs_rewrite=true means legacy format was used.
65fn deserialize_stake_table(bytes: &[u8]) -> anyhow::Result<(StakeTuple, bool)> {
66    // Try current format (both keys as Option<KEY>).
67    if let Ok(stake) = bincode::deserialize::<StakeTuple>(bytes) {
68        return Ok((stake, false));
69    }
70
71    // Pre-Schnorr-Option: stake_table_key as Option<KEY>, state_ver_key as raw KEY.
72    type PreSchnorrMap = indexmap::IndexMap<Address, RegisteredValidatorPreSchnorrOption>;
73    type PreSchnorrTuple = (PreSchnorrMap, Option<RewardAmount>, Option<StakeTableHash>);
74    if let Ok(pre_schnorr) = bincode::deserialize::<PreSchnorrTuple>(bytes) {
75        let migrated: AuthenticatedValidatorMap = pre_schnorr
76            .0
77            .into_iter()
78            .map(|(addr, v)| {
79                let registered = v.migrate();
80                let authenticated = AuthenticatedValidator::try_from(registered)?;
81                Ok((addr, authenticated))
82            })
83            .collect::<anyhow::Result<_>>()?;
84        return Ok(((migrated, pre_schnorr.1, pre_schnorr.2), true));
85    }
86
87    // Pre-Option: both keys raw, x25519/p2p fields present.
88    type PreOptionMap = indexmap::IndexMap<Address, RegisteredValidatorPreOption>;
89    type PreOptionTuple = (PreOptionMap, Option<RewardAmount>, Option<StakeTableHash>);
90    if let Ok(pre_option) = bincode::deserialize::<PreOptionTuple>(bytes) {
91        let migrated: AuthenticatedValidatorMap = pre_option
92            .0
93            .into_iter()
94            .map(|(addr, v)| {
95                let registered = v.migrate();
96                let authenticated = AuthenticatedValidator::try_from(registered)?;
97                Ok((addr, authenticated))
98            })
99            .collect::<anyhow::Result<_>>()?;
100        return Ok(((migrated, pre_option.1, pre_option.2), true));
101    }
102
103    // Pre-x25519: RegisteredValidator without x25519_key/p2p_addr.
104    type LegacyMap = indexmap::IndexMap<Address, RegisteredValidatorNoX25519>;
105    type LegacyTuple = (LegacyMap, Option<RewardAmount>, Option<StakeTableHash>);
106    let legacy: LegacyTuple = bincode::deserialize(bytes)
107        .context("failed to deserialize stake table (tried current and legacy formats)")?;
108    let migrated: AuthenticatedValidatorMap = legacy
109        .0
110        .into_iter()
111        .map(|(addr, v)| {
112            let registered = v.migrate();
113            (
114                addr,
115                AuthenticatedValidator::try_from(registered)
116                    .expect("stake tables only contain authenticated validators"),
117            )
118        })
119        .collect();
120    Ok(((migrated, legacy.1, legacy.2), true))
121}
122
123/// Options for file system backed persistence.
124#[derive(Parser, Clone, Debug)]
125pub struct Options {
126    /// Storage path for persistent data.
127    #[clap(long, env = "ESPRESSO_NODE_STORAGE_PATH")]
128    pub(crate) path: PathBuf,
129
130    /// Number of views to retain in consensus storage before data that hasn't been archived is
131    /// garbage collected.
132    ///
133    /// The longer this is, the more certain that all data will eventually be archived, even if
134    /// there are temporary problems with archive storage or partially missing data. This can be set
135    /// very large, as most data is garbage collected as soon as it is finalized by consensus. This
136    /// setting only applies to views which never get decided (ie forks in consensus) and views for
137    /// which this node is partially offline. These should be exceptionally rare.
138    ///
139    /// The default of 130000 views equates to approximately 3 days (259200 seconds) at an average
140    /// view time of 2s.
141    #[clap(
142        long,
143        env = "ESPRESSO_NODE_CONSENSUS_VIEW_RETENTION",
144        default_value = "130000"
145    )]
146    pub(crate) consensus_view_retention: u64,
147
148    #[clap(skip)]
149    pub(crate) consensus_only: bool,
150}
151
152impl Default for Options {
153    fn default() -> Self {
154        Self::parse_from(std::iter::empty::<String>())
155    }
156}
157
158impl Options {
159    pub fn new(path: PathBuf) -> Self {
160        Self {
161            path,
162            consensus_view_retention: 130000,
163            consensus_only: false,
164        }
165    }
166
167    pub(crate) fn path(&self) -> &Path {
168        &self.path
169    }
170}
171
172#[async_trait]
173impl PersistenceOptions for Options {
174    type Persistence = Persistence;
175
176    fn set_view_retention(&mut self, view_retention: u64) {
177        self.consensus_view_retention = view_retention;
178    }
179
180    fn set_consensus_only(&mut self) {
181        self.consensus_only = true;
182    }
183
184    async fn create(&mut self) -> anyhow::Result<Self::Persistence> {
185        let path = self.path.clone();
186        let view_retention = self.consensus_view_retention;
187        // Fail fast if the data directory can't be created: every writer would fail on it later
188        // anyway, and the fsync probe below needs it to exist.
189        fs::create_dir_all(&path).context("creating storage directory")?;
190        let probe = storage_probe::probe(&path, None).await?;
191
192        Ok(Persistence {
193            inner: Arc::new(RwLock::new(Inner {
194                path,
195                view_retention,
196            })),
197            metrics: Arc::new(PersistenceMetricsValue::default()),
198            probe,
199            consensus_only: self.consensus_only,
200        })
201    }
202
203    async fn reset(self) -> anyhow::Result<()> {
204        todo!()
205    }
206}
207
208/// File system backed persistence.
209#[derive(Clone, Debug)]
210pub struct Persistence {
211    // We enforce mutual exclusion on access to the data source, as the current file system
212    // implementation does not support transaction isolation for concurrent reads and writes. We can
213    // improve this in the future by switching to a SQLite-based file system implementation.
214    inner: Arc<RwLock<Inner>>,
215    /// A reference to the metrics trait
216    metrics: Arc<PersistenceMetricsValue>,
217    /// Startup findings about the filesystem backing the data directory.
218    probe: StorageProbe,
219    consensus_only: bool,
220}
221
222#[derive(Debug)]
223struct Inner {
224    path: PathBuf,
225    view_retention: u64,
226}
227
228impl Inner {
229    fn config_path(&self) -> PathBuf {
230        self.path.join("hotshot.cfg")
231    }
232
233    fn voted_view_path(&self) -> PathBuf {
234        self.path.join("highest_voted_view")
235    }
236
237    fn restart_view_path(&self) -> PathBuf {
238        self.path.join("restart_view")
239    }
240
241    fn decided_leaf2_path(&self) -> PathBuf {
242        self.path.join("decided_leaves2")
243    }
244
245    /// The path from previous versions where there was only a single file for anchor leaves.
246    fn legacy_anchor_leaf_path(&self) -> PathBuf {
247        self.path.join("anchor_leaf")
248    }
249
250    fn vid2_dir_path(&self) -> PathBuf {
251        self.path.join("vid2")
252    }
253
254    fn da_dir_path(&self) -> PathBuf {
255        self.path.join("da")
256    }
257
258    fn drb_dir_path(&self) -> PathBuf {
259        self.path.join("drb")
260    }
261
262    fn da2_dir_path(&self) -> PathBuf {
263        self.path.join("da2")
264    }
265
266    fn quorum_proposals2_dir_path(&self) -> PathBuf {
267        self.path.join("quorum_proposals2")
268    }
269
270    fn upgrade_certificate_dir_path(&self) -> PathBuf {
271        self.path.join("upgrade_certificate")
272    }
273
274    fn stake_table_dir_path(&self) -> PathBuf {
275        self.path.join("stake_table")
276    }
277
278    fn next_epoch_qc(&self) -> PathBuf {
279        self.path.join("next_epoch_quorum_certificate")
280    }
281
282    fn eqc(&self) -> PathBuf {
283        self.path.join("eqc")
284    }
285
286    fn high_qc2(&self) -> PathBuf {
287        self.path.join("high_qc2")
288    }
289
290    fn boundary_qc2(&self) -> PathBuf {
291        self.path.join("boundary_qc2")
292    }
293
294    fn epoch_drb_result_dir_path(&self) -> PathBuf {
295        self.path.join("epoch_drb_result")
296    }
297
298    fn epoch_root_block_header_dir_path(&self) -> PathBuf {
299        self.path.join("epoch_root_block_header")
300    }
301
302    fn finalized_state_cert_dir_path(&self) -> PathBuf {
303        self.path.join("finalized_state_cert")
304    }
305
306    fn state_cert_dir_path(&self) -> PathBuf {
307        self.path.join("state_cert")
308    }
309
310    fn decided_cert2_dir_path(&self) -> PathBuf {
311        self.path.join("decided_cert2")
312    }
313
314    /// cert2 is only persisted for the view that is directly finalized
315    /// (the newest leaf in a decided chain). Ancestor views finalized
316    /// indirectly have no cert2 file on disk
317    /// for those, this returns `Ok(None)`
318    fn load_cert2(&self, view: ViewNumber) -> anyhow::Result<Option<Certificate2<SeqTypes>>> {
319        let file_path = self
320            .decided_cert2_dir_path()
321            .join(view.u64().to_string())
322            .with_extension("bin");
323        if !file_path.is_file() {
324            return Ok(None);
325        }
326        let bytes = fs::read(&file_path).context("read cert2")?;
327        Ok(Some(
328            bincode::deserialize(&bytes).context("deserialize cert2")?,
329        ))
330    }
331
332    fn append_cert2(
333        &mut self,
334        view: ViewNumber,
335        cert2: &Certificate2<SeqTypes>,
336    ) -> anyhow::Result<()> {
337        let dir_path = self.decided_cert2_dir_path();
338        fs::create_dir_all(&dir_path).context("failed to create decided_cert2 dir")?;
339        let file_path = dir_path.join(view.u64().to_string()).with_extension("bin");
340        self.replace(
341            &file_path,
342            |_| Ok(true),
343            |mut file| {
344                let bytes = bincode::serialize(cert2).context("serialize cert2")?;
345                file.write_all(&bytes)?;
346                Ok(())
347            },
348        )
349    }
350
351    /// Overwrite a file if a condition is met.
352    ///
353    /// The file at `path`, if it exists, is opened in read mode and passed to `pred`. If `pred`
354    /// returns `true`, or if there was no existing file, then `write` is called to update the
355    /// contents of the file. `write` receives a truncated file open in write mode and sets the
356    /// contents of the file.
357    ///
358    /// The final replacement of the original file is atomic; that is, `path` will be modified only
359    /// if the entire update succeeds.
360    fn replace(
361        &mut self,
362        path: &Path,
363        pred: impl FnOnce(File) -> anyhow::Result<bool>,
364        write: impl FnOnce(File) -> anyhow::Result<()>,
365    ) -> anyhow::Result<()> {
366        if path.is_file() {
367            // If there is an existing file, check if it is suitable to replace. Note that this
368            // check is not atomic with respect to the subsequent write at the file system level,
369            // but this object is the only one which writes to this file, and we have a mutable
370            // reference, so this should be safe.
371            if !pred(File::open(path)?)? {
372                // If we are not overwriting the file, we are done and consider the whole operation
373                // successful.
374                return Ok(());
375            }
376        }
377
378        // Either there is no existing file or we have decided to overwrite the file. Write the new
379        // contents into a temporary file so we can update `path` atomically using `rename`.
380        let mut swap_path = path.to_owned();
381        swap_path.set_extension("swp");
382        let swap = OpenOptions::new()
383            .write(true)
384            .truncate(true)
385            .create(true)
386            .open(&swap_path)?;
387        write(swap)?;
388
389        // Now we can replace the original file.
390        fs::rename(swap_path, path)?;
391
392        Ok(())
393    }
394
395    fn collect_garbage(
396        &mut self,
397        decided_view: ViewNumber,
398        prune_intervals: &[RangeInclusive<ViewNumber>],
399    ) -> anyhow::Result<()> {
400        let prune_view = ViewNumber::new(decided_view.saturating_sub(self.view_retention));
401
402        self.prune_files(self.da2_dir_path(), prune_view, None, prune_intervals)?;
403        self.prune_files(self.vid2_dir_path(), prune_view, None, prune_intervals)?;
404        self.prune_files(
405            self.quorum_proposals2_dir_path(),
406            prune_view,
407            None,
408            prune_intervals,
409        )?;
410        self.prune_files(
411            self.state_cert_dir_path(),
412            prune_view,
413            None,
414            prune_intervals,
415        )?;
416        self.prune_files(
417            self.decided_cert2_dir_path(),
418            prune_view,
419            Some(decided_view),
420            prune_intervals,
421        )?;
422
423        // Save the most recent leaf as it will be our anchor point if the node restarts.
424        self.prune_files(
425            self.decided_leaf2_path(),
426            prune_view,
427            Some(decided_view),
428            prune_intervals,
429        )?;
430
431        Ok(())
432    }
433
434    fn prune_files(
435        &mut self,
436        dir_path: PathBuf,
437        prune_view: ViewNumber,
438        keep_decided_view: Option<ViewNumber>,
439        prune_intervals: &[RangeInclusive<ViewNumber>],
440    ) -> anyhow::Result<()> {
441        if !dir_path.is_dir() {
442            return Ok(());
443        }
444
445        for (file_view, path) in view_files(dir_path)? {
446            // If the view is the anchor view, keep it no matter what.
447            if let Some(decided_view) = keep_decided_view
448                && decided_view == file_view
449            {
450                continue;
451            }
452            // Otherwise, delete it if it is time to prune this view _or_ if the given intervals,
453            // which we've already successfully processed, contain the view; in this case we simply
454            // don't need it anymore.
455            if file_view < prune_view || prune_intervals.iter().any(|i| i.contains(&file_view)) {
456                fs::remove_file(&path)?;
457            }
458        }
459
460        Ok(())
461    }
462
463    fn parse_decided_leaf(
464        &self,
465        bytes: &[u8],
466    ) -> anyhow::Result<(Leaf2, CertificatePair<SeqTypes>)> {
467        // Old versions of the software did not store the next epoch QC. Without knowing which
468        // version this file was created with, we can simply try parsing both ways and then
469        // reconstruct a certificate pair with or without the next epoch QC.
470        match bincode::deserialize(bytes) {
471            Ok((leaf, cert)) => Ok((leaf, cert)),
472            Err(err) => {
473                tracing::info!(
474                    "error parsing decided leaf, maybe file was created without next epoch QC? \
475                     {err}"
476                );
477                let (leaf, qc) =
478                    bincode::deserialize::<(Leaf2, QuorumCertificate2<SeqTypes>)>(bytes)
479                        .context("parsing decided leaf")?;
480                Ok((leaf, CertificatePair::non_epoch_change(qc)))
481            },
482        }
483    }
484
485    /// Generate events based on persisted decided leaves.
486    ///
487    /// Returns a list of closed intervals of views which can be safely deleted, as all leaves
488    /// within these view ranges have been processed by the event consumer.
489    async fn generate_decide_events(
490        &mut self,
491        view: ViewNumber,
492        consumer: &impl EventConsumer,
493    ) -> anyhow::Result<Vec<RangeInclusive<ViewNumber>>> {
494        // Generate a decide event for each leaf, to be processed by the event consumer. We make a
495        // separate event for each leaf because it is possible we have non-consecutive leaves in our
496        // storage, which would not be valid as a single decide with a single leaf chain.
497        let mut leaves = BTreeMap::new();
498        for (v, path) in view_files(self.decided_leaf2_path())? {
499            if v > view {
500                continue;
501            }
502
503            let bytes =
504                fs::read(&path).context(format!("reading decided leaf {}", path.display()))?;
505            let (mut leaf, cert) = self.parse_decided_leaf(&bytes)?;
506
507            // Include the VID share if available.
508            let vid_proposal = self.load_vid_share(v)?;
509            if vid_proposal.is_none() {
510                tracing::debug!(?v, "VID share not available at decide");
511            }
512            let vid_share = vid_proposal.as_ref().map(|proposal| proposal.data.clone());
513
514            // Move the state cert to the finalized dir if it exists.
515            let state_cert = self.store_finalized_state_cert(v)?;
516
517            // Fill in the full block payload using the DA proposals we had persisted.
518            if let Some(proposal) = self.load_da_proposal(v)? {
519                let payload = Payload::from_bytes(
520                    &proposal.data.encoded_transactions,
521                    &proposal.data.metadata,
522                );
523                leaf.fill_block_payload_unchecked(payload);
524            } else {
525                tracing::debug!(?v, "DA proposal not available at decide");
526            }
527
528            let info = LeafInfo {
529                leaf,
530                vid_share,
531                state_cert,
532                // Note: the following fields are not used in Decide event processing, and should be
533                // removed. For now, we just default them.
534                state: Default::default(),
535                delta: Default::default(),
536            };
537
538            leaves.insert(v, (info, cert));
539        }
540
541        // The invariant is that the oldest existing leaf in the `anchor_leaf` table -- if there is
542        // one -- was always included in the _previous_ decide event...but not removed from the
543        // database, because we always persist the most recent anchor leaf.
544        if let Some((oldest_view, _)) = leaves.first_key_value() {
545            // The only exception is when the oldest leaf is the genesis leaf; then there was no
546            // previous decide event.
547            if *oldest_view > ViewNumber::genesis() {
548                leaves.pop_first();
549            }
550        }
551
552        let mut intervals = vec![];
553        let mut current_interval = None;
554        for (view, (leaf, cert)) in leaves {
555            let height = leaf.leaf.block_header().block_number();
556
557            // cert2 is stored only for the directly finalized view; ancestors get `None`, which
558            // is what update() expects for indirectly decided leaves.
559            let event = CoordinatorEvent::NewDecide {
560                leaf_infos: vec![leaf],
561                cert1: cert.qc().clone(),
562                cert2: self.load_cert2(view)?,
563            };
564            consumer.handle_event(&event).await?;
565
566            if let Some((start, end, current_height)) = current_interval.as_mut() {
567                if height == *current_height + 1 {
568                    // If we have a chain of consecutive leaves, extend the current interval of
569                    // views which are safe to delete.
570                    *current_height += 1;
571                    *end = view;
572                } else {
573                    // Otherwise, end the current interval and start a new one.
574                    intervals.push(*start..=*end);
575                    current_interval = Some((view, view, height));
576                }
577            } else {
578                // Start a new interval.
579                current_interval = Some((view, view, height));
580            }
581        }
582        if let Some((start, end, _)) = current_interval {
583            intervals.push(start..=end);
584        }
585
586        Ok(intervals)
587    }
588
589    fn skip_decide_events(
590        &mut self,
591        view: ViewNumber,
592    ) -> anyhow::Result<Vec<RangeInclusive<ViewNumber>>> {
593        for (leaf_view, _) in view_files(self.decided_leaf2_path())? {
594            if leaf_view <= view {
595                self.store_finalized_state_cert(leaf_view)?;
596            }
597        }
598        Ok(Vec::from([ViewNumber::genesis()..=view]))
599    }
600
601    fn load_da_proposal(
602        &self,
603        view: ViewNumber,
604    ) -> anyhow::Result<Option<Proposal<SeqTypes, DaProposal2<SeqTypes>>>> {
605        let dir_path = self.da2_dir_path();
606
607        let file_path = dir_path.join(view.u64().to_string()).with_extension("txt");
608
609        if !file_path.exists() {
610            return Ok(None);
611        }
612
613        let da_bytes = fs::read(file_path)?;
614
615        let da_proposal: Proposal<SeqTypes, DaProposal2<SeqTypes>> =
616            bincode::deserialize(&da_bytes)?;
617        Ok(Some(da_proposal))
618    }
619
620    fn load_vid_share(
621        &self,
622        view: ViewNumber,
623    ) -> anyhow::Result<Option<Proposal<SeqTypes, VidDisperseShare<SeqTypes>>>> {
624        let dir_path = self.vid2_dir_path();
625
626        let file_path = dir_path.join(view.u64().to_string()).with_extension("txt");
627
628        if !file_path.exists() {
629            return Ok(None);
630        }
631
632        let vid_share_bytes = fs::read(file_path)?;
633        let vid_share: Proposal<SeqTypes, VidDisperseShare<SeqTypes>> =
634            bincode::deserialize(&vid_share_bytes)?;
635        Ok(Some(vid_share))
636    }
637
638    fn load_anchor_leaf(&self) -> anyhow::Result<Option<(Leaf2, CertificatePair<SeqTypes>)>> {
639        tracing::info!("Checking `Leaf2` to load the anchor leaf.");
640        if self.decided_leaf2_path().is_dir() {
641            let mut anchor: Option<(Leaf2, CertificatePair<SeqTypes>)> = None;
642
643            // Return the latest decided leaf.
644            for (_, path) in view_files(self.decided_leaf2_path())? {
645                let bytes =
646                    fs::read(&path).context(format!("reading decided leaf {}", path.display()))?;
647                let (leaf, cert) = self.parse_decided_leaf(&bytes)?;
648                if let Some((anchor_leaf, _)) = &anchor {
649                    if leaf.view_number() > anchor_leaf.view_number() {
650                        anchor = Some((leaf, cert));
651                    }
652                } else {
653                    anchor = Some((leaf, cert));
654                }
655            }
656
657            return Ok(anchor);
658        }
659
660        tracing::warn!(
661            "Failed to find an anchor leaf in `Leaf2` storage. Checking legacy `Leaf` storage. \
662             This is very likely to fail."
663        );
664        if self.legacy_anchor_leaf_path().is_file() {
665            // We may have an old version of storage, where there is just a single file for the
666            // anchor leaf. Read it and return the contents.
667            let mut file = BufReader::new(File::open(self.legacy_anchor_leaf_path())?);
668
669            // The first 8 bytes just contain the height of the leaf. We can skip this.
670            file.seek(SeekFrom::Start(8)).context("seek")?;
671            let bytes = file
672                .bytes()
673                .collect::<Result<Vec<_>, _>>()
674                .context("read")?;
675            let (leaf2, qc2): (Leaf2, QuorumCertificate2<SeqTypes>) =
676                bincode::deserialize(&bytes).context("deserialize")?;
677            let cert_pair = CertificatePair::new(qc2, None);
678            return Ok(Some((leaf2, cert_pair)));
679        }
680
681        Ok(None)
682    }
683
684    fn store_finalized_state_cert(
685        &mut self,
686        view: ViewNumber,
687    ) -> anyhow::Result<Option<LightClientStateUpdateCertificateV2<SeqTypes>>> {
688        let dir_path = self.state_cert_dir_path();
689        let file_path = dir_path.join(view.u64().to_string()).with_extension("txt");
690
691        if !file_path.exists() {
692            return Ok(None);
693        }
694
695        let bytes = fs::read(&file_path)?;
696
697        let state_cert: LightClientStateUpdateCertificateV2<SeqTypes> =
698            bincode::deserialize(&bytes).or_else(|err_v2| {
699                tracing::info!(
700                    error = %err_v2,
701                    path = %file_path.display(),
702                    "Failed to deserialize state certificate, attempting with v1"
703                );
704
705                bincode::deserialize::<LightClientStateUpdateCertificateV1<SeqTypes>>(&bytes)
706                    .map(Into::into)
707                    .with_context(|| {
708                        format!(
709                            "Failed to deserialize with both v2 and v1 from file '{}'. error: \
710                             {err_v2}",
711                            file_path.display()
712                        )
713                    })
714            })?;
715
716        let epoch = state_cert.epoch.u64();
717        let finalized_dir_path = self.finalized_state_cert_dir_path();
718        fs::create_dir_all(&finalized_dir_path).context("creating finalized state cert dir")?;
719
720        let finalized_file_path = finalized_dir_path
721            .join(epoch.to_string())
722            .with_extension("txt");
723
724        self.replace(
725            &finalized_file_path,
726            |_| Ok(true),
727            |mut file| {
728                file.write_all(&bytes)?;
729                Ok(())
730            },
731        )
732        .context(format!(
733            "finalizing light client state update certificate file for epoch {epoch:?}"
734        ))?;
735
736        Ok(Some(state_cert))
737    }
738}
739
740#[async_trait]
741impl SequencerPersistence for Persistence {
742    async fn load_config(&self) -> anyhow::Result<Option<NetworkConfig>> {
743        let inner = self.inner.read().await;
744        let path = inner.config_path();
745        if !path.is_file() {
746            tracing::info!("config not found at {}", path.display());
747            return Ok(None);
748        }
749        tracing::info!("loading config from {}", path.display());
750
751        let bytes =
752            fs::read(&path).context(format!("unable to read config from {}", path.display()))?;
753        let json = serde_json::from_slice(&bytes).context("config file is not valid JSON")?;
754        let json = migrate_network_config(json).context("migration of network config failed")?;
755        let config = serde_json::from_value(json).context("malformed config file")?;
756        Ok(Some(config))
757    }
758
759    async fn save_config(&self, cfg: &NetworkConfig) -> anyhow::Result<()> {
760        let inner = self.inner.write().await;
761        let path = inner.config_path();
762        tracing::info!("saving config to {}", path.display());
763        Ok(cfg.to_file(path.display().to_string())?)
764    }
765
766    async fn load_latest_acted_view(&self) -> anyhow::Result<Option<ViewNumber>> {
767        let inner = self.inner.read().await;
768        let path = inner.voted_view_path();
769        if !path.is_file() {
770            return Ok(None);
771        }
772        let bytes = fs::read(inner.voted_view_path())?
773            .try_into()
774            .map_err(|bytes| anyhow!("malformed voted view file: {bytes:?}"))?;
775        Ok(Some(ViewNumber::new(u64::from_le_bytes(bytes))))
776    }
777
778    async fn load_restart_view(&self) -> anyhow::Result<Option<ViewNumber>> {
779        let inner = self.inner.read().await;
780        let path = inner.restart_view_path();
781        if !path.is_file() {
782            return Ok(None);
783        }
784        let bytes = fs::read(path)?
785            .try_into()
786            .map_err(|bytes| anyhow!("malformed restart view file: {bytes:?}"))?;
787        Ok(Some(ViewNumber::new(u64::from_le_bytes(bytes))))
788    }
789
790    async fn persist_decided_leaves(
791        &self,
792        leaf_chain: impl IntoIterator<Item = (&LeafInfo<SeqTypes>, CertificatePair<SeqTypes>)> + Send,
793        cert2: Option<&Certificate2<SeqTypes>>,
794        _consumer: &(impl EventConsumer + 'static),
795    ) -> anyhow::Result<()> {
796        let leaf_chain: Vec<_> = leaf_chain.into_iter().collect();
797        let mut inner = self.inner.write().await;
798        // Written before the leaves and under the same lock as the decide replay, so the replay
799        // can never find the newest leaf on disk without its cert2. Keyed by that leaf's view,
800        // which a re-vote's cert2 is later than.
801        let newest = leaf_chain
802            .iter()
803            .map(|(info, _)| info.leaf.view_number())
804            .max();
805        if let Some(cert2) = cert2
806            && newest.is_none()
807        {
808            tracing::warn!(view = %cert2.view_number, "cert2 without a decided leaf; not stored");
809        }
810        if let Some(cert2) = cert2
811            && let Some(newest) = newest
812        {
813            inner.append_cert2(newest, cert2)?;
814        }
815        let path = inner.decided_leaf2_path();
816
817        // Ensure the anchor leaf directory exists.
818        fs::create_dir_all(&path).context("creating anchor leaf directory")?;
819
820        // Earlier versions stored only a single decided leaf in a regular file. If our storage is
821        // still on this version, migrate to a directory structure storing (possibly) many leaves.
822        let legacy_path = inner.legacy_anchor_leaf_path();
823        if !path.is_dir() && legacy_path.is_file() {
824            tracing::info!("migrating to multi-leaf storage");
825
826            // Move the existing data into the new directory.
827            let (leaf, qc) = inner
828                .load_anchor_leaf()?
829                .context("anchor leaf file exists but unable to load contents")?;
830            let view = leaf.view_number().u64();
831            let bytes = bincode::serialize(&(leaf, qc))?;
832            let new_file = path.join(view.to_string()).with_extension("txt");
833            inner
834                .replace(
835                    &new_file,
836                    |_| Ok(true),
837                    |mut file| {
838                        file.write_all(&bytes)?;
839                        Ok(())
840                    },
841                )
842                .context(format!("writing anchor leaf file {view}"))?;
843
844            // Now we can remove the old file.
845            fs::remove_file(&legacy_path).context("removing legacy anchor leaf file")?;
846        }
847
848        for (info, cert) in leaf_chain {
849            let view = info.leaf.view_number().u64();
850            let file_path = path.join(view.to_string()).with_extension("txt");
851            inner.replace(
852                &file_path,
853                |_| {
854                    // Don't overwrite an existing leaf, but warn about it as this is likely not
855                    // intended behavior from HotShot.
856                    tracing::warn!(view, "duplicate decided leaf");
857                    Ok(false)
858                },
859                |mut file| {
860                    let bytes = bincode::serialize(&(&info.leaf, cert))?;
861                    file.write_all(&bytes)?;
862                    Ok(())
863                },
864            )?;
865        }
866
867        Ok(())
868    }
869
870    async fn process_decided_events(
871        &self,
872        view: ViewNumber,
873        consumer: &(impl EventConsumer + 'static),
874    ) -> anyhow::Result<Option<ViewNumber>> {
875        // Started before the lock acquisition: this pass holds the exclusive write lock, so the
876        // metric must include the wait to reflect how long appends can block behind it.
877        let now = Instant::now();
878        // On error, GC does not run over the failed range, so the leaves stay on disk and are
879        // retried; no data is lost.
880        let mut inner = self.inner.write().await;
881        let intervals = if self.consensus_only {
882            inner.skip_decide_events(view)?
883        } else {
884            inner.generate_decide_events(view, consumer).await?
885        };
886        drop(inner);
887
888        // Highest view we generated an event for; unprocessed leaves stay on disk (the cursor).
889        let processed = intervals.iter().map(|i| *i.end()).max();
890
891        // Best-effort GC; runs again at the next decide.
892        let res = self.inner.write().await.collect_garbage(view, &intervals);
893        if let Err(err) = res {
894            tracing::warn!(?view, "GC failed: {err:#}");
895        }
896        self.metrics
897            .internal_process_decided_events_duration
898            .add_point(now.elapsed().as_secs_f64());
899
900        Ok(processed)
901    }
902
903    async fn load_anchor_leaf(&self) -> anyhow::Result<Option<(Leaf2, CertificatePair<SeqTypes>)>> {
904        self.inner.read().await.load_anchor_leaf()
905    }
906
907    async fn load_da_proposal(
908        &self,
909        view: ViewNumber,
910    ) -> anyhow::Result<Option<Proposal<SeqTypes, DaProposal2<SeqTypes>>>> {
911        self.inner.read().await.load_da_proposal(view)
912    }
913
914    async fn load_vid_share(
915        &self,
916        view: ViewNumber,
917    ) -> anyhow::Result<Option<Proposal<SeqTypes, VidDisperseShare<SeqTypes>>>> {
918        self.inner.read().await.load_vid_share(view)
919    }
920
921    async fn append_vid(
922        &self,
923        proposal: &Proposal<SeqTypes, VidDisperseShare<SeqTypes>>,
924    ) -> anyhow::Result<()> {
925        let mut inner = self.inner.write().await;
926        let view_number = proposal.data.view_number().u64();
927        let dir_path = inner.vid2_dir_path();
928
929        fs::create_dir_all(dir_path.clone()).context("failed to create vid dir")?;
930
931        let file_path = dir_path.join(view_number.to_string()).with_extension("txt");
932        inner.replace(
933            &file_path,
934            |_| {
935                // Don't overwrite an existing share, but warn about it as this is likely not intended
936                // behavior from HotShot.
937                tracing::warn!(view_number, "duplicate VID share");
938                Ok(false)
939            },
940            |mut file| {
941                let proposal_bytes = bincode::serialize(proposal).context("serialize proposal")?;
942                let now = Instant::now();
943                file.write_all(&proposal_bytes)?;
944                self.metrics
945                    .internal_append_vid_duration
946                    .add_point(now.elapsed().as_secs_f64());
947                Ok(())
948            },
949        )
950    }
951
952    async fn append_da(
953        &self,
954        proposal: &Proposal<SeqTypes, DaProposal<SeqTypes>>,
955        _vid_commit: VidCommitment,
956    ) -> anyhow::Result<()> {
957        let mut inner = self.inner.write().await;
958        let view_number = proposal.data.view_number().u64();
959        let dir_path = inner.da_dir_path();
960
961        fs::create_dir_all(dir_path.clone()).context("failed to create da dir")?;
962
963        let file_path = dir_path.join(view_number.to_string()).with_extension("txt");
964        inner.replace(
965            &file_path,
966            |_| {
967                // Don't overwrite an existing proposal, but warn about it as this is likely not
968                // intended behavior from HotShot.
969                tracing::warn!(view_number, "duplicate DA proposal");
970                Ok(false)
971            },
972            |mut file| {
973                let proposal_bytes = bincode::serialize(&proposal).context("serialize proposal")?;
974                let now = Instant::now();
975                file.write_all(&proposal_bytes)?;
976                self.metrics
977                    .internal_append_da_duration
978                    .add_point(now.elapsed().as_secs_f64());
979                Ok(())
980            },
981        )
982    }
983    async fn record_action(&self, view: ViewNumber, action: HotShotAction) -> anyhow::Result<()> {
984        // Todo Remove this after https://github.com/EspressoSystems/espresso-network/issues/1931
985        //
986        // A timeout vote counts: a restarted node must not vote2 at a view it
987        // sent a timeout vote for.
988        if !matches!(
989            action,
990            HotShotAction::Propose | HotShotAction::Vote | HotShotAction::TimeoutVote
991        ) {
992            return Ok(());
993        }
994        let mut inner = self.inner.write().await;
995        let path = &inner.voted_view_path();
996        inner.replace(
997            path,
998            |mut file| {
999                let mut bytes = vec![];
1000                file.read_to_end(&mut bytes)?;
1001                let bytes = bytes
1002                    .try_into()
1003                    .map_err(|bytes| anyhow!("malformed voted view file: {bytes:?}"))?;
1004                let saved_view = ViewNumber::new(u64::from_le_bytes(bytes));
1005
1006                // Overwrite the file if the saved view is older than the new view.
1007                Ok(saved_view < view)
1008            },
1009            |mut file| {
1010                file.write_all(&view.u64().to_le_bytes())?;
1011                Ok(())
1012            },
1013        )?;
1014
1015        if matches!(action, HotShotAction::Vote) {
1016            let restart_view_path = &inner.restart_view_path();
1017            let restart_view = view + 1;
1018            inner.replace(
1019                restart_view_path,
1020                |mut file| {
1021                    let mut bytes = vec![];
1022                    file.read_to_end(&mut bytes)?;
1023                    let bytes = bytes
1024                        .try_into()
1025                        .map_err(|bytes| anyhow!("malformed voted view file: {bytes:?}"))?;
1026                    let saved_view = ViewNumber::new(u64::from_le_bytes(bytes));
1027
1028                    // Overwrite the file if the saved view is older than the new view.
1029                    Ok(saved_view < restart_view)
1030                },
1031                |mut file| {
1032                    file.write_all(&restart_view.u64().to_le_bytes())?;
1033                    Ok(())
1034                },
1035            )?;
1036        }
1037        Ok(())
1038    }
1039
1040    async fn append_quorum_proposal2(
1041        &self,
1042        proposal: &Proposal<SeqTypes, QuorumProposalWrapper<SeqTypes>>,
1043    ) -> anyhow::Result<()> {
1044        let mut inner = self.inner.write().await;
1045        let view_number = proposal.data.view_number().u64();
1046        let dir_path = inner.quorum_proposals2_dir_path();
1047
1048        fs::create_dir_all(dir_path.clone()).context("failed to create proposals dir")?;
1049
1050        let file_path = dir_path.join(view_number.to_string()).with_extension("txt");
1051        inner.replace(
1052            &file_path,
1053            |_| {
1054                // Always overwrite the previous file
1055                Ok(true)
1056            },
1057            |mut file| {
1058                let proposal_bytes = bincode::serialize(&proposal).context("serialize proposal")?;
1059                let now = Instant::now();
1060                file.write_all(&proposal_bytes)?;
1061                self.metrics
1062                    .internal_append_quorum2_duration
1063                    .add_point(now.elapsed().as_secs_f64());
1064                Ok(())
1065            },
1066        )
1067    }
1068
1069    async fn append_cert2(
1070        &self,
1071        view: ViewNumber,
1072        cert2: Certificate2<SeqTypes>,
1073    ) -> anyhow::Result<()> {
1074        self.inner.write().await.append_cert2(view, &cert2)
1075    }
1076
1077    async fn load_cert2(&self, view: ViewNumber) -> anyhow::Result<Option<Certificate2<SeqTypes>>> {
1078        let inner = self.inner.read().await;
1079        let dir_path = inner.decided_cert2_dir_path();
1080        let file_path = dir_path.join(view.u64().to_string()).with_extension("bin");
1081        if !file_path.is_file() {
1082            return Ok(None);
1083        }
1084        let bytes = fs::read(&file_path).context("read cert2")?;
1085        Ok(Some(
1086            bincode::deserialize(&bytes).context("deserialize cert2")?,
1087        ))
1088    }
1089    async fn load_quorum_proposals(
1090        &self,
1091    ) -> anyhow::Result<BTreeMap<ViewNumber, Proposal<SeqTypes, QuorumProposalWrapper<SeqTypes>>>>
1092    {
1093        let inner = self.inner.read().await;
1094
1095        // First, get the proposal directory.
1096        let dir_path = inner.quorum_proposals2_dir_path();
1097        if !dir_path.is_dir() {
1098            return Ok(Default::default());
1099        }
1100
1101        // Read quorum proposals from every data file in this directory.
1102        let mut map = BTreeMap::new();
1103        for (view, path) in view_files(&dir_path)? {
1104            let proposal_bytes = fs::read(path)?;
1105            let Some(proposal) = bincode::deserialize::<
1106                Proposal<SeqTypes, QuorumProposalWrapper<SeqTypes>>,
1107            >(&proposal_bytes)
1108            .or_else(|error| {
1109                bincode::deserialize::<Proposal<SeqTypes, QuorumProposalWrapperLegacy<SeqTypes>>>(
1110                    &proposal_bytes,
1111                )
1112                .map(convert_proposal)
1113                .inspect_err(|err_v3| {
1114                    // At this point, if the file contents are invalid, it is most likely an
1115                    // error rather than a miscellaneous file somehow ending up in the
1116                    // directory. However, we continue on, because it is better to collect as
1117                    // many proposals as we can rather than letting one bad proposal cause the
1118                    // entire operation to fail, and it is still possible that this was just
1119                    // some unintended file whose name happened to match the naming convention.
1120
1121                    tracing::warn!(
1122                        ?view,
1123                        %error,
1124                        error_v3 = %err_v3,
1125                        "ignoring malformed quorum proposal file"
1126                    );
1127                })
1128            })
1129            .ok() else {
1130                continue;
1131            };
1132
1133            let proposal2 = convert_proposal(proposal);
1134
1135            // Push to the map and we're done.
1136            map.insert(view, proposal2);
1137        }
1138
1139        Ok(map)
1140    }
1141
1142    async fn load_quorum_proposal(
1143        &self,
1144        view: ViewNumber,
1145    ) -> anyhow::Result<Proposal<SeqTypes, QuorumProposalWrapper<SeqTypes>>> {
1146        let inner = self.inner.read().await;
1147        let dir_path = inner.quorum_proposals2_dir_path();
1148        let file_path = dir_path.join(view.to_string()).with_extension("txt");
1149        let bytes = fs::read(file_path)?;
1150        let proposal: Proposal<SeqTypes, QuorumProposalWrapper<SeqTypes>> =
1151            bincode::deserialize(&bytes).or_else(|error| {
1152                bincode::deserialize::<Proposal<SeqTypes, QuorumProposalWrapperLegacy<SeqTypes>>>(
1153                    &bytes,
1154                )
1155                .map(convert_proposal)
1156                .context(format!(
1157                    "Failed to deserialize quorum proposal for view {view:?}: {error}."
1158                ))
1159            })?;
1160        Ok(proposal)
1161    }
1162
1163    async fn load_upgrade_certificate(
1164        &self,
1165    ) -> anyhow::Result<Option<UpgradeCertificate<SeqTypes>>> {
1166        let inner = self.inner.read().await;
1167        let path = inner.upgrade_certificate_dir_path();
1168        if !path.is_file() {
1169            return Ok(None);
1170        }
1171        let bytes = fs::read(&path).context("read")?;
1172        Ok(Some(
1173            bincode::deserialize(&bytes).context("deserialize upgrade certificate")?,
1174        ))
1175    }
1176
1177    async fn store_upgrade_certificate(
1178        &self,
1179        decided_upgrade_certificate: Option<UpgradeCertificate<SeqTypes>>,
1180    ) -> anyhow::Result<()> {
1181        let mut inner = self.inner.write().await;
1182        let path = &inner.upgrade_certificate_dir_path();
1183        let certificate = match decided_upgrade_certificate {
1184            Some(cert) => cert,
1185            None => return Ok(()),
1186        };
1187        inner.replace(
1188            path,
1189            |_| {
1190                // Always overwrite the previous file.
1191                Ok(true)
1192            },
1193            |mut file| {
1194                let bytes =
1195                    bincode::serialize(&certificate).context("serializing upgrade certificate")?;
1196                file.write_all(&bytes)?;
1197                Ok(())
1198            },
1199        )
1200    }
1201
1202    async fn append_next_epoch_high_qc2(
1203        &self,
1204        next_epoch_high_qc: NextEpochQuorumCertificate2<SeqTypes>,
1205    ) -> anyhow::Result<()> {
1206        let mut inner = self.inner.write().await;
1207        let path = &inner.next_epoch_qc();
1208        let view = next_epoch_high_qc.view_number();
1209        inner.replace(
1210            path,
1211            |mut file| {
1212                // Overwrite only when the new QC is newer. The whole replace runs under the inner
1213                // write lock, so this compare-and-set is atomic and a stale concurrent write cannot
1214                // regress the stored view (mirrors `append_high_qc2`).
1215                let mut bytes = vec![];
1216                file.read_to_end(&mut bytes)?;
1217                let existing: NextEpochQuorumCertificate2<SeqTypes> = bincode::deserialize(&bytes)
1218                    .context("deserializing existing next epoch high_qc2")?;
1219                Ok(existing.view_number() < view)
1220            },
1221            |mut file| {
1222                let bytes = bincode::serialize(&next_epoch_high_qc)
1223                    .context("serializing next epoch high_qc2")?;
1224                file.write_all(&bytes)?;
1225                Ok(())
1226            },
1227        )
1228    }
1229
1230    async fn store_eqc(
1231        &self,
1232        high_qc: QuorumCertificate2<SeqTypes>,
1233        next_epoch_high_qc: NextEpochQuorumCertificate2<SeqTypes>,
1234    ) -> anyhow::Result<()> {
1235        let mut inner = self.inner.write().await;
1236        let path = &inner.eqc();
1237
1238        inner.replace(
1239            path,
1240            |_| {
1241                // Always overwrite the previous file.
1242                Ok(true)
1243            },
1244            |mut file| {
1245                let bytes = bincode::serialize(&(high_qc, next_epoch_high_qc))
1246                    .context("serializing next epoch qc")?;
1247                file.write_all(&bytes)?;
1248                Ok(())
1249            },
1250        )
1251    }
1252
1253    async fn load_eqc(
1254        &self,
1255    ) -> Option<(
1256        QuorumCertificate2<SeqTypes>,
1257        NextEpochQuorumCertificate2<SeqTypes>,
1258    )> {
1259        let inner = self.inner.read().await;
1260        let path = inner.eqc();
1261        if !path.is_file() {
1262            return None;
1263        }
1264        let bytes = fs::read(&path).ok()?;
1265
1266        bincode::deserialize(&bytes).ok()
1267    }
1268
1269    async fn append_high_qc2(&self, high_qc: QuorumCertificate2<SeqTypes>) -> anyhow::Result<()> {
1270        let mut inner = self.inner.write().await;
1271        let path = &inner.high_qc2();
1272        // Locks are ordered by epoch, then view.
1273        let lock = LockView::of(&high_qc);
1274        inner.replace(
1275            path,
1276            |mut file| {
1277                // Overwrite only when the new lock is newer. The whole replace
1278                // runs under the inner write lock, so this compare-and-set is
1279                // atomic and a stale concurrent write cannot regress the lock.
1280                let mut bytes = vec![];
1281                file.read_to_end(&mut bytes)?;
1282                let existing: QuorumCertificate2<SeqTypes> =
1283                    bincode::deserialize(&bytes).context("deserializing existing high_qc2")?;
1284                Ok(LockView::of(&existing) < lock)
1285            },
1286            |mut file| {
1287                let bytes = bincode::serialize(&high_qc).context("serializing high_qc2")?;
1288                file.write_all(&bytes)?;
1289                Ok(())
1290            },
1291        )
1292    }
1293
1294    async fn load_high_qc2(&self) -> anyhow::Result<Option<QuorumCertificate2<SeqTypes>>> {
1295        let inner = self.inner.read().await;
1296        let path = inner.high_qc2();
1297        if !path.is_file() {
1298            return Ok(None);
1299        }
1300        let bytes = fs::read(&path).context("reading high_qc2")?;
1301        Ok(Some(
1302            bincode::deserialize(&bytes).context("deserializing high_qc2")?,
1303        ))
1304    }
1305
1306    async fn append_boundary_qc2(&self, qc: QuorumCertificate2<SeqTypes>) -> anyhow::Result<()> {
1307        let mut inner = self.inner.write().await;
1308        let path = &inner.boundary_qc2();
1309        let lock = LockView::of(&qc);
1310        inner.replace(
1311            path,
1312            |mut file| {
1313                // Overwrite only when the new certificate is later, under the
1314                // inner write lock, as for `high_qc2`.
1315                let mut bytes = vec![];
1316                file.read_to_end(&mut bytes)?;
1317                let existing: QuorumCertificate2<SeqTypes> =
1318                    bincode::deserialize(&bytes).context("deserializing existing boundary_qc2")?;
1319                Ok(LockView::of(&existing) < lock)
1320            },
1321            |mut file| {
1322                let bytes = bincode::serialize(&qc).context("serializing boundary_qc2")?;
1323                file.write_all(&bytes)?;
1324                Ok(())
1325            },
1326        )
1327    }
1328
1329    async fn load_boundary_qc2(&self) -> anyhow::Result<Option<QuorumCertificate2<SeqTypes>>> {
1330        let inner = self.inner.read().await;
1331        let path = inner.boundary_qc2();
1332        if !path.is_file() {
1333            return Ok(None);
1334        }
1335        let bytes = fs::read(&path).context("reading boundary_qc2")?;
1336        Ok(Some(
1337            bincode::deserialize(&bytes).context("deserializing boundary_qc2")?,
1338        ))
1339    }
1340
1341    async fn append_da2(
1342        &self,
1343        proposal: &Proposal<SeqTypes, DaProposal2<SeqTypes>>,
1344        _vid_commit: VidCommitment,
1345    ) -> anyhow::Result<()> {
1346        if self.consensus_only {
1347            return Ok(());
1348        }
1349        let mut inner = self.inner.write().await;
1350        let view_number = proposal.data.view_number().u64();
1351        let dir_path = inner.da2_dir_path();
1352
1353        fs::create_dir_all(dir_path.clone()).context("failed to create da dir")?;
1354
1355        let file_path = dir_path.join(view_number.to_string()).with_extension("txt");
1356        inner.replace(
1357            &file_path,
1358            |_| {
1359                // Don't overwrite an existing proposal, but warn about it as this is likely not
1360                // intended behavior from HotShot.
1361                tracing::warn!(view_number, "duplicate DA proposal");
1362                Ok(false)
1363            },
1364            |mut file| {
1365                let proposal_bytes = bincode::serialize(&proposal).context("serialize proposal")?;
1366                let now = Instant::now();
1367                file.write_all(&proposal_bytes)?;
1368                self.metrics
1369                    .internal_append_da2_duration
1370                    .add_point(now.elapsed().as_secs_f64());
1371                Ok(())
1372            },
1373        )
1374    }
1375
1376    async fn store_drb_input(&self, drb_input: DrbInput) -> anyhow::Result<()> {
1377        if let Ok(loaded_drb_input) = self.load_drb_input(drb_input.epoch).await {
1378            if loaded_drb_input.difficulty_level != drb_input.difficulty_level {
1379                tracing::error!("Overwriting {loaded_drb_input:?} in storage with {drb_input:?}");
1380            } else if loaded_drb_input.iteration >= drb_input.iteration {
1381                anyhow::bail!(
1382                    "DrbInput in storage {:?} is more recent than {:?}, refusing to update",
1383                    loaded_drb_input,
1384                    drb_input
1385                )
1386            }
1387        }
1388
1389        let mut inner = self.inner.write().await;
1390        let dir_path = inner.drb_dir_path();
1391
1392        fs::create_dir_all(dir_path.clone()).context("failed to create drb dir")?;
1393
1394        let drb_input_bytes =
1395            bincode::serialize(&drb_input).context("failed to serialize drb_input")?;
1396
1397        let file_path = dir_path
1398            .join(drb_input.epoch.to_string())
1399            .with_extension("bin");
1400
1401        inner.replace(
1402            &file_path,
1403            |_| {
1404                // Always overwrite the previous file.
1405                Ok(true)
1406            },
1407            |mut file| {
1408                file.write_all(&drb_input_bytes).context(format!(
1409                    "writing epoch drb_input file for epoch {:?} at {:?}",
1410                    drb_input.epoch, file_path
1411                ))
1412            },
1413        )
1414    }
1415
1416    async fn load_drb_input(&self, epoch: u64) -> anyhow::Result<DrbInput> {
1417        let inner = self.inner.read().await;
1418        let path = &inner.drb_dir_path();
1419        let file_path = path.join(epoch.to_string()).with_extension("bin");
1420        let bytes = fs::read(&file_path).context("read")?;
1421        Ok(bincode::deserialize(&bytes)
1422            .context(format!("failed to deserialize DrbInput for epoch {epoch}"))?)
1423    }
1424
1425    async fn store_drb_result(
1426        &self,
1427        epoch: EpochNumber,
1428        drb_result: DrbResult,
1429    ) -> anyhow::Result<()> {
1430        let mut inner = self.inner.write().await;
1431        let dir_path = inner.epoch_drb_result_dir_path();
1432
1433        fs::create_dir_all(dir_path.clone()).context("failed to create epoch drb result dir")?;
1434
1435        let drb_result_bytes = bincode::serialize(&drb_result).context("serialize drb result")?;
1436
1437        let file_path = dir_path.join(epoch.to_string()).with_extension("txt");
1438
1439        inner.replace(
1440            &file_path,
1441            |_| {
1442                // Always overwrite the previous file.
1443                Ok(true)
1444            },
1445            |mut file| {
1446                file.write_all(&drb_result_bytes)
1447                    .context(format!("writing epoch drb result file for epoch {epoch:?}"))
1448            },
1449        )
1450    }
1451
1452    async fn add_state_cert(
1453        &self,
1454        state_cert: LightClientStateUpdateCertificateV2<SeqTypes>,
1455    ) -> anyhow::Result<()> {
1456        let mut inner = self.inner.write().await;
1457        // let epoch = state_cert.epoch;
1458        let view = state_cert.light_client_state.view_number;
1459        let dir_path = inner.state_cert_dir_path();
1460
1461        fs::create_dir_all(dir_path.clone())
1462            .context("failed to create light client state update certificate dir")?;
1463
1464        let bytes = bincode::serialize(&state_cert)
1465            .context("serialize light client state update certificate")?;
1466
1467        let file_path = dir_path.join(view.to_string()).with_extension("txt");
1468        inner
1469            .replace(
1470                &file_path,
1471                |_| Ok(true),
1472                |mut file| {
1473                    file.write_all(&bytes)?;
1474                    Ok(())
1475                },
1476            )
1477            .context(format!(
1478                "writing light client state update certificate file for view {view:?}"
1479            ))?;
1480
1481        Ok(())
1482    }
1483
1484    async fn load_start_epoch_info(&self) -> anyhow::Result<Vec<InitializerEpochInfo<SeqTypes>>> {
1485        let inner = self.inner.read().await;
1486        let drb_dir_path = inner.epoch_drb_result_dir_path();
1487        let block_header_dir_path = inner.epoch_root_block_header_dir_path();
1488
1489        let mut result = Vec::new();
1490
1491        if !drb_dir_path.is_dir() {
1492            return Ok(Vec::new());
1493        }
1494        for (epoch, path) in epoch_files(drb_dir_path)? {
1495            let bytes =
1496                fs::read(&path).context(format!("reading epoch drb result {}", path.display()))?;
1497            let drb_result = bincode::deserialize::<DrbResult>(&bytes)
1498                .context(format!("parsing epoch drb result {}", path.display()))?;
1499
1500            let block_header_path = block_header_dir_path
1501                .join(epoch.to_string())
1502                .with_extension("txt");
1503            let block_header = if block_header_path.is_file() {
1504                let bytes = fs::read(&block_header_path).context(format!(
1505                    "reading epoch root block header {}",
1506                    block_header_path.display()
1507                ))?;
1508                Some(
1509                    bincode::deserialize::<<SeqTypes as NodeType>::BlockHeader>(&bytes).context(
1510                        format!(
1511                            "parsing epoch root block header {}",
1512                            block_header_path.display()
1513                        ),
1514                    )?,
1515                )
1516            } else {
1517                None
1518            };
1519
1520            result.push(InitializerEpochInfo::<SeqTypes> {
1521                epoch,
1522                drb_result,
1523                block_header,
1524            });
1525        }
1526
1527        result.sort_by_key(|a| a.epoch);
1528
1529        // Keep only the most recent epochs
1530        let start = result
1531            .len()
1532            .saturating_sub(RECENT_STAKE_TABLES_LIMIT as usize);
1533        let recent = result[start..].to_vec();
1534
1535        Ok(recent)
1536    }
1537
1538    async fn load_state_cert(
1539        &self,
1540    ) -> anyhow::Result<Option<LightClientStateUpdateCertificateV2<SeqTypes>>> {
1541        let inner = self.inner.read().await;
1542        let dir_path = inner.finalized_state_cert_dir_path();
1543
1544        if !dir_path.is_dir() {
1545            return Ok(None);
1546        }
1547
1548        let mut result: Option<LightClientStateUpdateCertificateV2<SeqTypes>> = None;
1549
1550        for (epoch, path) in epoch_files(dir_path)? {
1551            if result.as_ref().is_some_and(|cert| epoch <= cert.epoch) {
1552                continue;
1553            }
1554            let bytes = fs::read(&path).context(format!(
1555                "reading light client state update certificate {}",
1556                path.display()
1557            ))?;
1558            let cert =
1559                bincode::deserialize::<LightClientStateUpdateCertificateV2<SeqTypes>>(&bytes)
1560                    .or_else(|error| {
1561                        tracing::info!(
1562                            %error,
1563                            path = %path.display(),
1564                            "Failed to deserialize LightClientStateUpdateCertificateV2"
1565                        );
1566
1567                        bincode::deserialize::<LightClientStateUpdateCertificateV1<SeqTypes>>(
1568                            &bytes,
1569                        )
1570                        .map(Into::into)
1571                        .with_context(|| {
1572                            format!(
1573                                "Failed to deserialize with v1 and v2. path='{}'. error: {error}",
1574                                path.display()
1575                            )
1576                        })
1577                    })?;
1578
1579            result = Some(cert);
1580        }
1581
1582        Ok(result)
1583    }
1584
1585    fn enable_metrics(&mut self, metrics: &dyn Metrics) {
1586        self.metrics = Arc::new(PersistenceMetricsValue::new(metrics));
1587        self.probe.register(&*metrics.subgroup("disk".into()));
1588    }
1589}
1590
1591#[async_trait]
1592impl StateCertPersistence for Persistence {
1593    async fn get_state_cert_by_epoch(
1594        &self,
1595        epoch: u64,
1596    ) -> anyhow::Result<Option<LightClientStateUpdateCertificateV2<SeqTypes>>> {
1597        let inner = self.inner.read().await;
1598        let dir_path = inner.finalized_state_cert_dir_path();
1599
1600        let file_path = dir_path.join(epoch.to_string()).with_extension("txt");
1601
1602        if !file_path.exists() {
1603            return Ok(None);
1604        }
1605
1606        let bytes = fs::read(&file_path).context(format!(
1607            "reading light client state update certificate {}",
1608            file_path.display()
1609        ))?;
1610
1611        let cert = bincode::deserialize::<LightClientStateUpdateCertificateV2<SeqTypes>>(&bytes)
1612            .or_else(|error| {
1613                tracing::info!(
1614                    %error,
1615                    path = %file_path.display(),
1616                    "Failed to deserialize LightClientStateUpdateCertificateV2"
1617                );
1618
1619                bincode::deserialize::<LightClientStateUpdateCertificateV1<SeqTypes>>(&bytes)
1620                    .map(Into::into)
1621                    .with_context(|| {
1622                        format!(
1623                            "Failed to deserialize with v1 and v2. path='{}'. error: {error}",
1624                            file_path.display()
1625                        )
1626                    })
1627            })?;
1628
1629        Ok(Some(cert))
1630    }
1631
1632    async fn insert_state_cert(
1633        &self,
1634        epoch: u64,
1635        cert: LightClientStateUpdateCertificateV2<SeqTypes>,
1636    ) -> anyhow::Result<()> {
1637        let inner = self.inner.read().await;
1638        let dir_path = inner.finalized_state_cert_dir_path();
1639
1640        fs::create_dir_all(&dir_path)
1641            .context(format!("creating state cert dir {}", dir_path.display()))?;
1642
1643        let file_path = dir_path.join(epoch.to_string()).with_extension("txt");
1644        let bytes = bincode::serialize(&cert)
1645            .context("serializing light client state update certificate")?;
1646
1647        fs::write(&file_path, bytes).context(format!(
1648            "writing light client state update certificate {}",
1649            file_path.display()
1650        ))?;
1651
1652        Ok(())
1653    }
1654}
1655
1656#[async_trait]
1657impl MembershipPersistence for Persistence {
1658    async fn load_stake(&self, epoch: EpochNumber) -> anyhow::Result<Option<StakeTuple>> {
1659        let inner = self.inner.read().await;
1660        let path = &inner.stake_table_dir_path();
1661        let file_path = path.join(epoch.to_string()).with_extension("txt");
1662
1663        if !file_path.exists() {
1664            return Ok(None);
1665        }
1666
1667        let bytes = fs::read(&file_path).with_context(|| {
1668            format!("failed to read stake table file at {}", file_path.display())
1669        })?;
1670
1671        let (stake, _needs_rewrite) = deserialize_stake_table(&bytes).with_context(|| {
1672            format!(
1673                "failed to deserialize stake table at {}",
1674                file_path.display()
1675            )
1676        })?;
1677        Ok(Some(stake))
1678    }
1679
1680    async fn load_drb_result(&self, epoch: EpochNumber) -> anyhow::Result<Option<DrbResult>> {
1681        let inner = self.inner.read().await;
1682        let file_path = inner
1683            .epoch_drb_result_dir_path()
1684            .join(epoch.to_string())
1685            .with_extension("txt");
1686
1687        if !file_path.is_file() {
1688            return Ok(None);
1689        }
1690
1691        let bytes = fs::read(&file_path)
1692            .context(format!("reading epoch drb result {}", file_path.display()))?;
1693        let drb_result = bincode::deserialize::<DrbResult>(&bytes)
1694            .context(format!("parsing epoch drb result {}", file_path.display()))?;
1695        Ok(Some(drb_result))
1696    }
1697
1698    async fn load_epoch_root(&self, epoch: EpochNumber) -> anyhow::Result<Option<Header>> {
1699        let inner = self.inner.read().await;
1700        let file_path = inner
1701            .epoch_root_block_header_dir_path()
1702            .join(epoch.to_string())
1703            .with_extension("txt");
1704
1705        if !file_path.is_file() {
1706            return Ok(None);
1707        }
1708
1709        let bytes = fs::read(&file_path).context(format!(
1710            "reading epoch root block header {}",
1711            file_path.display()
1712        ))?;
1713        let header = bincode::deserialize::<Header>(&bytes).context(format!(
1714            "parsing epoch root block header {}",
1715            file_path.display()
1716        ))?;
1717        Ok(Some(header))
1718    }
1719
1720    async fn store_epoch_root(
1721        &self,
1722        epoch: EpochNumber,
1723        block_header: Header,
1724    ) -> anyhow::Result<()> {
1725        let mut inner = self.inner.write().await;
1726        let dir_path = inner.epoch_root_block_header_dir_path();
1727
1728        fs::create_dir_all(dir_path.clone())
1729            .context("failed to create epoch root block header dir")?;
1730
1731        let block_header_bytes =
1732            bincode::serialize(&block_header).context("serialize block header")?;
1733
1734        let file_path = dir_path.join(epoch.to_string()).with_extension("txt");
1735        inner
1736            .replace(
1737                &file_path,
1738                |_| Ok(true),
1739                |mut file| {
1740                    file.write_all(&block_header_bytes)?;
1741                    Ok(())
1742                },
1743            )
1744            .context(format!(
1745                "writing epoch root block header file for epoch {epoch:?}"
1746            ))?;
1747
1748        Ok(())
1749    }
1750
1751    async fn load_latest_stake(&self, limit: u64) -> anyhow::Result<Option<Vec<IndexedStake>>> {
1752        let limit = limit as usize;
1753        let inner = self.inner.read().await;
1754        let path = &inner.stake_table_dir_path();
1755        let sorted_files = epoch_files(path)?
1756            .sorted_by(|(e1, _), (e2, _)| e2.cmp(e1))
1757            .take(limit);
1758        let mut validator_sets: Vec<IndexedStake> = Vec::new();
1759
1760        for (epoch, file_path) in sorted_files {
1761            let bytes = fs::read(&file_path).with_context(|| {
1762                format!("failed to read stake table file at {}", file_path.display())
1763            })?;
1764
1765            let (stake, _needs_rewrite) = deserialize_stake_table(&bytes).with_context(|| {
1766                format!(
1767                    "failed to deserialize stake table at {}",
1768                    file_path.display()
1769                )
1770            })?;
1771            validator_sets.push((epoch, (stake.0, stake.1), stake.2));
1772        }
1773
1774        Ok(Some(validator_sets))
1775    }
1776
1777    async fn store_stake(
1778        &self,
1779        epoch: EpochNumber,
1780        stake: AuthenticatedValidatorMap,
1781        block_reward: Option<RewardAmount>,
1782        stake_table_hash: Option<StakeTableHash>,
1783    ) -> anyhow::Result<()> {
1784        let mut inner = self.inner.write().await;
1785        let dir_path = &inner.stake_table_dir_path();
1786
1787        fs::create_dir_all(dir_path.clone()).context("failed to create stake table dir")?;
1788
1789        let file_path = dir_path.join(epoch.to_string()).with_extension("txt");
1790
1791        inner.replace(
1792            &file_path,
1793            |_| {
1794                // Always overwrite the previous file.
1795                Ok(true)
1796            },
1797            |mut file| {
1798                let data: StakeTuple = (stake, block_reward, stake_table_hash);
1799                let bytes =
1800                    bincode::serialize(&data).context("serializing combined stake table")?;
1801                file.write_all(&bytes)?;
1802                Ok(())
1803            },
1804        )
1805    }
1806
1807    /// store stake table events upto the l1 block
1808    async fn store_events(
1809        &self,
1810        to_l1_block: u64,
1811        events: Vec<(EventKey, StakeTableEvent)>,
1812    ) -> anyhow::Result<()> {
1813        let mut inner = self.inner.write().await;
1814        let dir_path = &inner.stake_table_dir_path();
1815        let events_dir = dir_path.join("events");
1816
1817        fs::create_dir_all(events_dir.clone()).context("failed to create events dir")?;
1818        // Read the last l1 finalized for which events has been stored
1819        let last_l1_finalized_path = events_dir.join("last_l1_finalized").with_extension("bin");
1820
1821        // check if the last l1 events is higher than the incoming one
1822        if last_l1_finalized_path.exists() {
1823            let bytes = fs::read(&last_l1_finalized_path).with_context(|| {
1824                format!("Failed to read file at path: {last_l1_finalized_path:?}")
1825            })?;
1826            let mut buf = [0; 8];
1827            bytes
1828                .as_slice()
1829                .read_exact(&mut buf[..8])
1830                .with_context(|| {
1831                    format!("Failed to read 8 bytes from file at path: {last_l1_finalized_path:?}")
1832                })?;
1833            let persisted_l1_block = u64::from_le_bytes(buf);
1834            if persisted_l1_block > to_l1_block {
1835                tracing::debug!(?persisted_l1_block, ?to_l1_block, "stored l1 is greater");
1836                return Ok(());
1837            }
1838        }
1839
1840        // stores each event in a separate file
1841        // this can cause performance issue when, for example, reading all the files
1842        // However, the plan is to remove file system completely in future
1843        for (event_key, event) in events {
1844            let (block_number, event_index) = event_key;
1845            // file name is like block_index.json
1846            let filename = format!("{block_number}_{event_index}");
1847            let file_path = events_dir.join(filename).with_extension("json");
1848
1849            if file_path.exists() {
1850                continue;
1851            }
1852
1853            inner
1854                .replace(
1855                    &file_path,
1856                    |_| Ok(true),
1857                    |file| {
1858                        let writer = BufWriter::new(file);
1859
1860                        serde_json::to_writer_pretty(writer, &event)?;
1861                        Ok(())
1862                    },
1863                )
1864                .context("Failed to write event to file")?;
1865        }
1866
1867        // update the l1 block for which we have processed events
1868        inner.replace(
1869            &last_l1_finalized_path,
1870            |_| Ok(true),
1871            |mut file| {
1872                let bytes = to_l1_block.to_le_bytes();
1873
1874                file.write_all(&bytes)?;
1875                tracing::debug!("updated l1 finalized ={to_l1_block:?}");
1876                Ok(())
1877            },
1878        )
1879    }
1880
1881    /// Loads all events from persistent storage up to the specified L1 block.
1882    ///
1883    /// # Returns
1884    ///
1885    /// Returns a tuple containing:
1886    /// - `Option<u64>` - The queried L1 block for which all events have been successfully fetched.
1887    /// - `Vec<(EventKey, StakeTableEvent)>` - A list of events, where each entry is a tuple of the event key
1888    /// event key is (l1 block number, log index)
1889    ///   and the corresponding StakeTable event.
1890    ///
1891    async fn load_events(
1892        &self,
1893        from_l1_block: u64,
1894        to_l1_block: u64,
1895    ) -> anyhow::Result<(
1896        Option<EventsPersistenceRead>,
1897        Vec<(EventKey, StakeTableEvent)>,
1898    )> {
1899        let inner = self.inner.read().await;
1900        let dir_path = inner.stake_table_dir_path();
1901        let events_dir = dir_path.join("events");
1902
1903        // check if we have any events in storage
1904        // we can do this by checking last l1 finalized block for which we processed events
1905        let last_l1_finalized_path = events_dir.join("last_l1_finalized").with_extension("bin");
1906
1907        if !last_l1_finalized_path.exists() || !events_dir.exists() {
1908            return Ok((None, Vec::new()));
1909        }
1910
1911        let mut events = Vec::new();
1912
1913        let bytes = fs::read(&last_l1_finalized_path)
1914            .with_context(|| format!("Failed to read file at path: {last_l1_finalized_path:?}"))?;
1915        let mut buf = [0; 8];
1916        bytes
1917            .as_slice()
1918            .read_exact(&mut buf[..8])
1919            .with_context(|| {
1920                format!("Failed to read 8 bytes from file at path: {last_l1_finalized_path:?}")
1921            })?;
1922
1923        let last_processed_l1_block = u64::from_le_bytes(buf);
1924
1925        // Determine the L1 block for querying events.
1926        // If the last stored L1 block is greater than the requested block, limit the query to the requested block.
1927        // Otherwise, query up to the last stored block.
1928        let query_l1_block = if last_processed_l1_block > to_l1_block {
1929            to_l1_block
1930        } else {
1931            last_processed_l1_block
1932        };
1933
1934        for entry in fs::read_dir(&events_dir).context("events directory")? {
1935            let entry = entry?;
1936            let path = entry.path();
1937
1938            if !entry.file_type()?.is_file() {
1939                continue;
1940            }
1941
1942            if path
1943                .extension()
1944                .context(format!("extension for path={path:?}"))?
1945                != "json"
1946            {
1947                continue;
1948            }
1949
1950            let filename = path
1951                .file_stem()
1952                .and_then(|f| f.to_str())
1953                .unwrap_or_default();
1954
1955            let parts: Vec<&str> = filename.split('_').collect();
1956            if parts.len() != 2 {
1957                continue;
1958            }
1959
1960            let block_number = parts[0].parse::<u64>()?;
1961            let log_index = parts[1].parse::<u64>()?;
1962
1963            if block_number < from_l1_block || block_number > query_l1_block {
1964                continue;
1965            }
1966
1967            let file =
1968                File::open(&path).context(format!("Failed to open event file. path={path:?}"))?;
1969            let reader = BufReader::new(file);
1970
1971            let event: StakeTableEvent = serde_json::from_reader(reader)
1972                .context(format!("Failed to deserialize event at path={path:?}"))?;
1973
1974            events.push(((block_number, log_index), event));
1975        }
1976
1977        events.sort_by_key(|(key, _)| *key);
1978
1979        if query_l1_block == to_l1_block {
1980            Ok((Some(EventsPersistenceRead::Complete), events))
1981        } else {
1982            Ok((
1983                Some(EventsPersistenceRead::UntilL1Block(query_l1_block)),
1984                events,
1985            ))
1986        }
1987    }
1988
1989    async fn delete_stake_tables(&self) -> anyhow::Result<()> {
1990        let inner = self.inner.write().await;
1991        let events_dir = inner.stake_table_dir_path().join("events");
1992        if events_dir.exists() {
1993            fs::remove_dir_all(&events_dir)
1994                .with_context(|| format!("Failed to remove events dir: {events_dir:?}"))?;
1995        }
1996        let validators_dir = inner.stake_table_dir_path().join("validators");
1997        if validators_dir.exists() {
1998            fs::remove_dir_all(&validators_dir)
1999                .with_context(|| format!("Failed to remove validators dir: {validators_dir:?}"))?;
2000        }
2001        let drb_dir = inner.epoch_drb_result_dir_path();
2002        if drb_dir.exists() {
2003            fs::remove_dir_all(&drb_dir)
2004                .with_context(|| format!("Failed to remove epoch DRB result dir: {drb_dir:?}"))?;
2005        }
2006        Ok(())
2007    }
2008
2009    async fn store_all_validators(
2010        &self,
2011        epoch: EpochNumber,
2012        all_validators: RegisteredValidatorMap,
2013    ) -> anyhow::Result<()> {
2014        let mut inner = self.inner.write().await;
2015        let dir_path = inner.stake_table_dir_path();
2016        let validators_dir = dir_path.join("validators");
2017
2018        // Ensure validators directory exists
2019        fs::create_dir_all(&validators_dir)
2020            .with_context(|| format!("Failed to create validators dir: {validators_dir:?}"))?;
2021
2022        // Path = validators/epoch_<number>.json
2023        let file_path = validators_dir.join(format!("epoch_{epoch}.json"));
2024
2025        inner
2026            .replace(
2027                &file_path,
2028                |_| Ok(true),
2029                |file| {
2030                    let writer = BufWriter::new(file);
2031
2032                    serde_json::to_writer_pretty(writer, &all_validators).with_context(|| {
2033                        format!("Failed to serialize validators for epoch {epoch}")
2034                    })?;
2035                    Ok(())
2036                },
2037            )
2038            .with_context(|| format!("Failed to write validator file: {file_path:?}"))?;
2039
2040        Ok(())
2041    }
2042
2043    async fn load_all_validators(
2044        &self,
2045        epoch: EpochNumber,
2046        offset: u64,
2047        limit: u64,
2048    ) -> anyhow::Result<Vec<RegisteredValidator<PubKey>>> {
2049        let inner = self.inner.read().await;
2050        let dir_path = inner.stake_table_dir_path();
2051        let validators_dir = dir_path.join("validators");
2052        let file_path = validators_dir.join(format!("epoch_{epoch}.json"));
2053
2054        if !file_path.exists() {
2055            bail!("Validator file not found for epoch {epoch}");
2056        }
2057
2058        let file = File::open(&file_path)
2059            .with_context(|| format!("Failed to open validator file: {file_path:?}"))?;
2060        let reader = BufReader::new(file);
2061
2062        let map: RegisteredValidatorMap = serde_json::from_reader(reader).with_context(|| {
2063            format!("Failed to deserialize validators at {file_path:?}. epoch = {epoch}")
2064        })?;
2065
2066        let mut values: Vec<RegisteredValidator<PubKey>> = map.into_values().collect();
2067        values.sort_by_key(|v| v.account);
2068
2069        let start = offset as usize;
2070        let end = (start + limit as usize).min(values.len());
2071
2072        if start >= values.len() {
2073            return Ok(vec![]);
2074        }
2075
2076        Ok(values[start..end].to_vec())
2077    }
2078}
2079
2080/// Get all paths under `dir` whose name is of the form <view number>.txt.
2081fn view_files(
2082    dir: impl AsRef<Path>,
2083) -> anyhow::Result<impl Iterator<Item = (ViewNumber, PathBuf)>> {
2084    Ok(fs::read_dir(dir.as_ref())?.filter_map(move |entry| {
2085        let dir = dir.as_ref().display();
2086        let entry = entry.ok()?;
2087        if !entry.file_type().ok()?.is_file() {
2088            tracing::debug!(%dir, ?entry, "ignoring non-file in data directory");
2089            return None;
2090        }
2091        let path = entry.path();
2092        // Most view-keyed files use a `.txt` extension; cert2 files use `.bin`. Both hold bincode.
2093        let ext = path.extension()?;
2094        if ext != "txt" && ext != "bin" {
2095            tracing::debug!(%dir, ?entry, "ignoring file with unrecognized extension in data directory");
2096            return None;
2097        }
2098        let file_name = path.file_stem()?;
2099        let Ok(view_number) = file_name.to_string_lossy().parse::<u64>() else {
2100            tracing::debug!(%dir, ?file_name, "ignoring extraneous file in data directory");
2101            return None;
2102        };
2103        Some((ViewNumber::new(view_number), entry.path().to_owned()))
2104    }))
2105}
2106
2107/// Get all paths under `dir` whose name is of the form <epoch number>.txt.
2108/// Should probably be made generic and merged with view_files.
2109fn epoch_files(
2110    dir: impl AsRef<Path>,
2111) -> anyhow::Result<impl Iterator<Item = (EpochNumber, PathBuf)>> {
2112    Ok(fs::read_dir(dir.as_ref())?.filter_map(move |entry| {
2113        let dir = dir.as_ref().display();
2114        let entry = entry.ok()?;
2115        if !entry.file_type().ok()?.is_file() {
2116            tracing::debug!(%dir, ?entry, "ignoring non-file in data directory");
2117            return None;
2118        }
2119        let path = entry.path();
2120        if path.extension()? != "txt" {
2121            tracing::debug!(%dir, ?entry, "ignoring non-text file in data directory");
2122            return None;
2123        }
2124        let file_name = path.file_stem()?;
2125        let Ok(epoch_number) = file_name.to_string_lossy().parse::<u64>() else {
2126            tracing::debug!(%dir, ?file_name, "ignoring extraneous file in data directory");
2127            return None;
2128        };
2129        Some((EpochNumber::new(epoch_number), entry.path().to_owned()))
2130    }))
2131}
2132
2133#[cfg(test)]
2134mod test {
2135    use std::marker::PhantomData;
2136
2137    use committable::Committable;
2138    use espresso_types::{Leaf, NodeState, PubKey};
2139    use hotshot_example_types::node_types::TEST_VERSIONS;
2140    use hotshot_query_service::{metrics::PrometheusMetrics, testing::mocks::MOCK_UPGRADE};
2141    use hotshot_types::{
2142        data::QuorumProposal2, simple_vote::Vote2Data, traits::signature_key::SignatureKey,
2143    };
2144    use serde_json::json;
2145    use tempfile::TempDir;
2146
2147    use super::*;
2148    use crate::{BLSPubKey, persistence::tests::TestablePersistence};
2149
2150    #[async_trait]
2151    impl TestablePersistence for Persistence {
2152        type Storage = TempDir;
2153
2154        async fn tmp_storage() -> Self::Storage {
2155            TempDir::new().unwrap()
2156        }
2157
2158        fn options(storage: &Self::Storage) -> impl PersistenceOptions<Persistence = Self> {
2159            Options::new(storage.path().into())
2160        }
2161    }
2162
2163    #[test]
2164    fn test_config_migrations_add_builder_urls() {
2165        let before = json!({
2166            "config": {
2167                "builder_url": "https://test:8080",
2168                "start_proposing_view": 1,
2169                "stop_proposing_view": 2,
2170                "start_voting_view": 1,
2171                "stop_voting_view": 2,
2172                "start_proposing_time": 1,
2173                "stop_proposing_time": 2,
2174                "start_voting_time": 1,
2175                "stop_voting_time": 2
2176            }
2177        });
2178        let after = json!({
2179            "config": {
2180                "builder_urls": ["https://test:8080"],
2181                "start_proposing_view": 1,
2182                "stop_proposing_view": 2,
2183                "start_voting_view": 1,
2184                "stop_voting_view": 2,
2185                "start_proposing_time": 1,
2186                "stop_proposing_time": 2,
2187                "start_voting_time": 1,
2188                "stop_voting_time": 2,
2189                "epoch_height": 0,
2190                "drb_difficulty": 0,
2191                "drb_upgrade_difficulty": 0,
2192                "da_committees": [],
2193            }
2194        });
2195
2196        assert_eq!(migrate_network_config(before).unwrap(), after);
2197    }
2198
2199    #[test]
2200    fn test_config_migrations_existing_builder_urls() {
2201        let before = json!({
2202            "config": {
2203                "builder_urls": ["https://test:8080", "https://test:8081"],
2204                "start_proposing_view": 1,
2205                "stop_proposing_view": 2,
2206                "start_voting_view": 1,
2207                "stop_voting_view": 2,
2208                "start_proposing_time": 1,
2209                "stop_proposing_time": 2,
2210                "start_voting_time": 1,
2211                "stop_voting_time": 2,
2212                "epoch_height": 0,
2213                "drb_difficulty": 0,
2214                "drb_upgrade_difficulty": 0,
2215                "da_committees": [],
2216            }
2217        });
2218
2219        assert_eq!(migrate_network_config(before.clone()).unwrap(), before);
2220    }
2221
2222    #[test]
2223    fn test_config_migrations_add_upgrade_params() {
2224        let before = json!({
2225            "config": {
2226                "builder_urls": ["https://test:8080", "https://test:8081"]
2227            }
2228        });
2229        let after = json!({
2230            "config": {
2231                "builder_urls": ["https://test:8080", "https://test:8081"],
2232                "start_proposing_view": 9007199254740991u64,
2233                "stop_proposing_view": 0,
2234                "start_voting_view": 9007199254740991u64,
2235                "stop_voting_view": 0,
2236                "start_proposing_time": 9007199254740991u64,
2237                "stop_proposing_time": 0,
2238                "start_voting_time": 9007199254740991u64,
2239                "stop_voting_time": 0,
2240                "epoch_height": 0,
2241                "drb_difficulty": 0,
2242                "drb_upgrade_difficulty": 0,
2243                "da_committees": [],
2244            }
2245        });
2246
2247        assert_eq!(migrate_network_config(before).unwrap(), after);
2248    }
2249
2250    #[test]
2251    fn test_config_migrations_existing_upgrade_params() {
2252        let before = json!({
2253            "config": {
2254                "builder_urls": ["https://test:8080", "https://test:8081"],
2255                "start_proposing_view": 1,
2256                "stop_proposing_view": 2,
2257                "start_voting_view": 1,
2258                "stop_voting_view": 2,
2259                "start_proposing_time": 1,
2260                "stop_proposing_time": 2,
2261                "start_voting_time": 1,
2262                "stop_voting_time": 2,
2263                "epoch_height": 0,
2264                "drb_difficulty": 0,
2265                "drb_upgrade_difficulty": 0,
2266                "da_committees": [],
2267            }
2268        });
2269
2270        assert_eq!(migrate_network_config(before.clone()).unwrap(), before);
2271    }
2272
2273    #[test_log::test(tokio::test(flavor = "multi_thread"))]
2274    async fn test_load_quorum_proposals_invalid_extension() {
2275        let tmp = Persistence::tmp_storage().await;
2276        let storage = Persistence::connect(&tmp).await;
2277
2278        // Generate a couple of valid quorum proposals.
2279        let leaf = Leaf2::genesis(&Default::default(), &NodeState::mock(), MOCK_UPGRADE.base).await;
2280        let privkey = PubKey::generated_from_seed_indexed([0; 32], 1).1;
2281        let signature = PubKey::sign(&privkey, &[]).unwrap();
2282        let mut quorum_proposal = Proposal {
2283            data: QuorumProposalWrapper::<SeqTypes> {
2284                proposal: QuorumProposal2::<SeqTypes> {
2285                    epoch: None,
2286                    block_header: leaf.block_header().clone(),
2287                    view_number: ViewNumber::genesis(),
2288                    justify_qc: QuorumCertificate2::genesis(
2289                        &Default::default(),
2290                        &NodeState::mock(),
2291                        TEST_VERSIONS.test,
2292                    )
2293                    .await,
2294                    upgrade_certificate: None,
2295                    view_change_evidence: None,
2296                    next_drb_result: None,
2297                    next_epoch_justify_qc: None,
2298                    state_cert: None,
2299                },
2300            },
2301            signature,
2302            _pd: Default::default(),
2303        };
2304
2305        // Store quorum proposals.
2306        let quorum_proposal1 = quorum_proposal.clone();
2307        storage
2308            .append_quorum_proposal2(&quorum_proposal1)
2309            .await
2310            .unwrap();
2311        quorum_proposal.data.proposal.view_number = ViewNumber::new(1);
2312        let quorum_proposal2 = quorum_proposal.clone();
2313        storage
2314            .append_quorum_proposal2(&quorum_proposal2)
2315            .await
2316            .unwrap();
2317
2318        // Change one of the file extensions. It can happen that we end up with files with the wrong
2319        // extension if, for example, the node is killed before cleaning up a swap file.
2320        fs::rename(
2321            tmp.path().join("quorum_proposals2/1.txt"),
2322            tmp.path().join("quorum_proposals2/1.swp"),
2323        )
2324        .unwrap();
2325
2326        // Loading should simply ignore the unrecognized extension.
2327        assert_eq!(
2328            storage.load_quorum_proposals().await.unwrap(),
2329            [(ViewNumber::genesis(), quorum_proposal1)]
2330                .into_iter()
2331                .collect::<BTreeMap<_, _>>()
2332        );
2333    }
2334
2335    #[test_log::test(tokio::test(flavor = "multi_thread"))]
2336    async fn test_cert2_persisted_as_bin() {
2337        let tmp = Persistence::tmp_storage().await;
2338        let storage = Persistence::connect(&tmp).await;
2339        let view = ViewNumber::new(7);
2340        let leaf = Leaf2::genesis(&Default::default(), &NodeState::mock(), MOCK_UPGRADE.base).await;
2341        let data = Vote2Data {
2342            leaf_commit: leaf.commit(),
2343            epoch: EpochNumber::new(1),
2344            block_number: leaf.height(),
2345        };
2346        let cert2 = Certificate2::new(data.clone(), data.commit(), view, None, PhantomData);
2347
2348        storage.append_cert2(view, cert2.clone()).await.unwrap();
2349
2350        assert!(tmp.path().join("decided_cert2/7.bin").is_file());
2351        assert!(!tmp.path().join("decided_cert2/7.txt").exists());
2352        assert_eq!(storage.load_cert2(view).await.unwrap(), Some(cert2));
2353    }
2354
2355    #[test_log::test(tokio::test(flavor = "multi_thread"))]
2356    async fn test_load_quorum_proposals_malformed_data() {
2357        let tmp = Persistence::tmp_storage().await;
2358        let storage = Persistence::connect(&tmp).await;
2359
2360        // Generate a valid quorum proposal.
2361        let leaf: Leaf2 = Leaf::genesis(&Default::default(), &NodeState::mock(), MOCK_UPGRADE.base)
2362            .await
2363            .into();
2364        let privkey = PubKey::generated_from_seed_indexed([0; 32], 1).1;
2365        let signature = PubKey::sign(&privkey, &[]).unwrap();
2366        let quorum_proposal = Proposal {
2367            data: QuorumProposalWrapper::<SeqTypes> {
2368                proposal: QuorumProposal2::<SeqTypes> {
2369                    epoch: None,
2370                    block_header: leaf.block_header().clone(),
2371                    view_number: ViewNumber::new(1),
2372                    justify_qc: QuorumCertificate2::genesis(
2373                        &Default::default(),
2374                        &NodeState::mock(),
2375                        TEST_VERSIONS.test,
2376                    )
2377                    .await,
2378                    upgrade_certificate: None,
2379                    view_change_evidence: None,
2380                    next_drb_result: None,
2381                    next_epoch_justify_qc: None,
2382                    state_cert: None,
2383                },
2384            },
2385            signature,
2386            _pd: Default::default(),
2387        };
2388
2389        // First store an invalid quorum proposal.
2390        fs::create_dir_all(tmp.path().join("quorum_proposals2")).unwrap();
2391        fs::write(
2392            tmp.path().join("quorum_proposals2/0.txt"),
2393            "invalid data".as_bytes(),
2394        )
2395        .unwrap();
2396
2397        // Store valid quorum proposal.
2398        storage
2399            .append_quorum_proposal2(&quorum_proposal)
2400            .await
2401            .unwrap();
2402
2403        // Loading should ignore the invalid data and return the valid proposal.
2404        assert_eq!(
2405            storage.load_quorum_proposals().await.unwrap(),
2406            [(ViewNumber::new(1), quorum_proposal)]
2407                .into_iter()
2408                .collect::<BTreeMap<_, _>>()
2409        );
2410    }
2411
2412    #[test_log::test(tokio::test(flavor = "multi_thread"))]
2413    async fn test_store_events_empty() {
2414        let tmp = Persistence::tmp_storage().await;
2415        let mut opt = Persistence::options(&tmp);
2416        let storage = opt.create().await.unwrap();
2417
2418        assert_eq!(storage.load_events(0, 100).await.unwrap(), (None, vec![]));
2419
2420        // Storing an empty events list still updates the latest L1 block.
2421        for i in 1..=2 {
2422            tracing::info!(i, "update l1 height");
2423            storage.store_events(i, vec![]).await.unwrap();
2424            assert_eq!(
2425                storage.load_events(0, 100).await.unwrap(),
2426                (Some(EventsPersistenceRead::UntilL1Block(i)), vec![])
2427            );
2428        }
2429    }
2430
2431    #[test_log::test(tokio::test(flavor = "multi_thread"))]
2432    async fn test_store_all_validators_authenticated_and_unauthenticated() {
2433        use std::collections::HashMap;
2434
2435        use alloy::primitives::{Address, U256};
2436        use espresso_types::v0_3::RegisteredValidator;
2437        use indexmap::IndexMap;
2438
2439        let tmp = Persistence::tmp_storage().await;
2440        let mut opt = Persistence::options(&tmp);
2441        let storage = opt.create().await.unwrap();
2442
2443        // Create an authenticated validator
2444        let authenticated_validator = RegisteredValidator {
2445            account: Address::random(),
2446            stake_table_key: Some(BLSPubKey::generated_from_seed_indexed([0u8; 32], 0).0),
2447            state_ver_key: Some(hotshot_types::light_client::StateVerKey::default()),
2448            stake: U256::from(1000),
2449            commission: 100,
2450            delegators: HashMap::new(),
2451            authenticated: true,
2452            x25519_key: None,
2453            p2p_addr: None,
2454        };
2455
2456        // Create an unauthenticated validator
2457        let unauthenticated_validator = RegisteredValidator {
2458            account: Address::random(),
2459            stake_table_key: Some(BLSPubKey::generated_from_seed_indexed([0u8; 32], 1).0),
2460            state_ver_key: Some(hotshot_types::light_client::StateVerKey::default()),
2461            stake: U256::from(2000),
2462            commission: 200,
2463            delegators: HashMap::new(),
2464            authenticated: false,
2465            x25519_key: None,
2466            p2p_addr: None,
2467        };
2468
2469        let mut validators: IndexMap<Address, RegisteredValidator<BLSPubKey>> = IndexMap::new();
2470        validators.insert(
2471            authenticated_validator.account,
2472            authenticated_validator.clone(),
2473        );
2474        validators.insert(
2475            unauthenticated_validator.account,
2476            unauthenticated_validator.clone(),
2477        );
2478
2479        // Store both validators
2480        storage
2481            .store_all_validators(EpochNumber::new(1), validators)
2482            .await
2483            .unwrap();
2484
2485        // Load and verify
2486        let loaded = storage
2487            .load_all_validators(EpochNumber::new(1), 0, 100)
2488            .await
2489            .unwrap();
2490        assert_eq!(loaded.len(), 2);
2491
2492        // Find each validator and verify authenticated state is preserved
2493        let loaded_auth = loaded
2494            .iter()
2495            .find(|v| v.account == authenticated_validator.account)
2496            .unwrap();
2497        assert!(
2498            loaded_auth.authenticated,
2499            "authenticated validator should remain authenticated"
2500        );
2501
2502        let loaded_unauth = loaded
2503            .iter()
2504            .find(|v| v.account == unauthenticated_validator.account)
2505            .unwrap();
2506        assert!(
2507            !loaded_unauth.authenticated,
2508            "unauthenticated validator should remain unauthenticated"
2509        );
2510    }
2511
2512    fn write_legacy_stake_file(
2513        path: &std::path::Path,
2514        epoch: u64,
2515        validator: RegisteredValidatorPreOption,
2516    ) {
2517        use indexmap::IndexMap;
2518
2519        let mut map: IndexMap<Address, RegisteredValidatorPreOption> = IndexMap::new();
2520        map.insert(validator.account, validator);
2521        type PreOptionTuple = (
2522            IndexMap<Address, RegisteredValidatorPreOption>,
2523            Option<RewardAmount>,
2524            Option<StakeTableHash>,
2525        );
2526        let data: PreOptionTuple = (map, None, None);
2527        let bytes = bincode::serialize(&data).unwrap();
2528        fs::create_dir_all(path).unwrap();
2529        fs::write(path.join(format!("{epoch}.txt")), &bytes).unwrap();
2530    }
2531
2532    fn pre_option_validator(seed: u8, stake: u64) -> RegisteredValidatorPreOption {
2533        use std::collections::HashMap;
2534
2535        use alloy::primitives::U256;
2536
2537        RegisteredValidatorPreOption {
2538            account: Address::random(),
2539            stake_table_key: BLSPubKey::generated_from_seed_indexed([seed; 32], 0).0,
2540            state_ver_key: hotshot_types::light_client::StateVerKey::default(),
2541            stake: U256::from(stake),
2542            commission: 0,
2543            delegators: HashMap::new(),
2544            authenticated: true,
2545            x25519_key: None,
2546            p2p_addr: None,
2547        }
2548    }
2549
2550    #[test_log::test(tokio::test(flavor = "multi_thread"))]
2551    async fn test_load_stake_legacy_storage() {
2552        let tmp = Persistence::tmp_storage().await;
2553        let mut opt = Persistence::options(&tmp);
2554        let storage = opt.create().await.unwrap();
2555
2556        let v1 = pre_option_validator(1, 100);
2557        let v2 = pre_option_validator(2, 200);
2558        let v1_addr = v1.account;
2559        let v2_addr = v2.account;
2560
2561        let path = {
2562            let inner = storage.inner.read().await;
2563            inner.stake_table_dir_path()
2564        };
2565        write_legacy_stake_file(&path, 1, v1);
2566        write_legacy_stake_file(&path, 2, v2);
2567
2568        let (loaded1, ..) = storage
2569            .load_stake(EpochNumber::new(1))
2570            .await
2571            .unwrap()
2572            .unwrap();
2573        assert_eq!(loaded1.len(), 1);
2574        assert!(loaded1.get(&v1_addr).unwrap().stake_table_key.is_some());
2575
2576        let (loaded2, ..) = storage
2577            .load_stake(EpochNumber::new(2))
2578            .await
2579            .unwrap()
2580            .unwrap();
2581        assert_eq!(loaded2.len(), 1);
2582        assert!(loaded2.get(&v2_addr).unwrap().stake_table_key.is_some());
2583
2584        let latest = storage.load_latest_stake(10).await.unwrap().unwrap();
2585        assert_eq!(latest.len(), 2);
2586        let epochs: Vec<_> = latest.iter().map(|(e, ..)| *e).collect();
2587        assert!(epochs.contains(&EpochNumber::new(1)));
2588        assert!(epochs.contains(&EpochNumber::new(2)));
2589    }
2590
2591    #[test_log::test(tokio::test(flavor = "multi_thread"))]
2592    async fn test_load_stake_mixed_storage() {
2593        use indexmap::IndexMap;
2594
2595        let tmp = Persistence::tmp_storage().await;
2596        let mut opt = Persistence::options(&tmp);
2597        let storage = opt.create().await.unwrap();
2598
2599        let legacy_v = pre_option_validator(3, 300);
2600        let legacy_addr = legacy_v.account;
2601        let path = {
2602            let inner = storage.inner.read().await;
2603            inner.stake_table_dir_path()
2604        };
2605        write_legacy_stake_file(&path, 5, legacy_v);
2606
2607        let current_v = espresso_types::v0_3::AuthenticatedValidator::mock();
2608        let current_addr = current_v.account;
2609        let mut current_map = IndexMap::new();
2610        current_map.insert(current_addr, current_v);
2611        storage
2612            .store_stake(EpochNumber::new(6), current_map, None, None)
2613            .await
2614            .unwrap();
2615
2616        let latest = storage.load_latest_stake(10).await.unwrap().unwrap();
2617        assert_eq!(latest.len(), 2);
2618        let by_epoch: std::collections::HashMap<_, _> = latest
2619            .into_iter()
2620            .map(|(e, (map, _), _)| (e, map))
2621            .collect();
2622        assert!(
2623            by_epoch
2624                .get(&EpochNumber::new(5))
2625                .unwrap()
2626                .contains_key(&legacy_addr)
2627        );
2628        assert!(
2629            by_epoch
2630                .get(&EpochNumber::new(6))
2631                .unwrap()
2632                .contains_key(&current_addr)
2633        );
2634    }
2635
2636    /// The probe is taken in `create()` and only reaches the exported registry through
2637    /// `enable_metrics`, mirroring the sqlite backend's `test_storage_probe_reaches_exported_registry`.
2638    #[test_log::test(tokio::test(flavor = "multi_thread"))]
2639    async fn test_storage_probe_reaches_exported_registry() {
2640        let tmp = Persistence::tmp_storage().await;
2641        let mut persistence = Persistence::options(&tmp).create().await.unwrap();
2642
2643        let metrics = PrometheusMetrics::default();
2644        persistence.enable_metrics(&*Metrics::subgroup(&metrics, "consensus".to_string()));
2645
2646        let exported = metrics.export().unwrap();
2647        assert!(exported.contains("consensus_disk_info"), "{exported}");
2648        assert!(exported.contains("backend=\"fs\""), "{exported}");
2649        assert!(
2650            exported.contains("consensus_disk_fsync_micros"),
2651            "{exported}"
2652        );
2653    }
2654}