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