Skip to main content

espresso_node/persistence/
fs.rs

1use std::{
2    collections::{BTreeMap, HashSet},
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, Leaf, Leaf2, NetworkConfig, Payload, PubKey, RegisteredValidatorMap,
18    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, QuorumProposal, QuorumProposalWrapper,
34        QuorumProposalWrapperLegacy, VidCommitment, VidDisperseShare, VidDisperseShare0,
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, QuorumCertificate, 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        let migration_path = path.join("migration");
179        let migrated = if migration_path.is_file() {
180            let bytes = fs::read(&migration_path).context(format!(
181                "unable to read migration from {}",
182                migration_path.display()
183            ))?;
184            bincode::deserialize(&bytes).context("malformed migration file")?
185        } else {
186            HashSet::new()
187        };
188
189        Ok(Persistence {
190            inner: Arc::new(RwLock::new(Inner {
191                path,
192                migrated,
193                view_retention,
194            })),
195            metrics: Arc::new(PersistenceMetricsValue::default()),
196        })
197    }
198
199    async fn reset(self) -> anyhow::Result<()> {
200        todo!()
201    }
202}
203
204/// File system backed persistence.
205#[derive(Clone, Debug)]
206pub struct Persistence {
207    // We enforce mutual exclusion on access to the data source, as the current file system
208    // implementation does not support transaction isolation for concurrent reads and writes. We can
209    // improve this in the future by switching to a SQLite-based file system implementation.
210    inner: Arc<RwLock<Inner>>,
211    /// A reference to the metrics trait
212    metrics: Arc<PersistenceMetricsValue>,
213}
214
215#[derive(Debug)]
216struct Inner {
217    path: PathBuf,
218    view_retention: u64,
219    migrated: HashSet<String>,
220}
221
222impl Inner {
223    fn config_path(&self) -> PathBuf {
224        self.path.join("hotshot.cfg")
225    }
226
227    fn migration(&self) -> PathBuf {
228        self.path.join("migration")
229    }
230
231    fn voted_view_path(&self) -> PathBuf {
232        self.path.join("highest_voted_view")
233    }
234
235    fn restart_view_path(&self) -> PathBuf {
236        self.path.join("restart_view")
237    }
238
239    /// Path to a directory containing decided leaves.
240    fn decided_leaf_path(&self) -> PathBuf {
241        self.path.join("decided_leaves")
242    }
243
244    fn decided_leaf2_path(&self) -> PathBuf {
245        self.path.join("decided_leaves2")
246    }
247
248    /// The path from previous versions where there was only a single file for anchor leaves.
249    fn legacy_anchor_leaf_path(&self) -> PathBuf {
250        self.path.join("anchor_leaf")
251    }
252
253    fn vid_dir_path(&self) -> PathBuf {
254        self.path.join("vid")
255    }
256
257    fn vid2_dir_path(&self) -> PathBuf {
258        self.path.join("vid2")
259    }
260
261    fn da_dir_path(&self) -> PathBuf {
262        self.path.join("da")
263    }
264
265    fn drb_dir_path(&self) -> PathBuf {
266        self.path.join("drb")
267    }
268
269    fn da2_dir_path(&self) -> PathBuf {
270        self.path.join("da2")
271    }
272
273    fn quorum_proposals_dir_path(&self) -> PathBuf {
274        self.path.join("quorum_proposals")
275    }
276
277    fn quorum_proposals2_dir_path(&self) -> PathBuf {
278        self.path.join("quorum_proposals2")
279    }
280
281    fn upgrade_certificate_dir_path(&self) -> PathBuf {
282        self.path.join("upgrade_certificate")
283    }
284
285    fn stake_table_dir_path(&self) -> PathBuf {
286        self.path.join("stake_table")
287    }
288
289    fn next_epoch_qc(&self) -> PathBuf {
290        self.path.join("next_epoch_quorum_certificate")
291    }
292
293    fn eqc(&self) -> PathBuf {
294        self.path.join("eqc")
295    }
296
297    fn high_qc2(&self) -> PathBuf {
298        self.path.join("high_qc2")
299    }
300
301    fn libp2p_dht_path(&self) -> PathBuf {
302        self.path.join("libp2p_dht")
303    }
304    fn epoch_drb_result_dir_path(&self) -> PathBuf {
305        self.path.join("epoch_drb_result")
306    }
307
308    fn epoch_root_block_header_dir_path(&self) -> PathBuf {
309        self.path.join("epoch_root_block_header")
310    }
311
312    fn finalized_state_cert_dir_path(&self) -> PathBuf {
313        self.path.join("finalized_state_cert")
314    }
315
316    fn state_cert_dir_path(&self) -> PathBuf {
317        self.path.join("state_cert")
318    }
319
320    fn decided_cert2_dir_path(&self) -> PathBuf {
321        self.path.join("decided_cert2")
322    }
323
324    /// cert2 is only persisted for the view that is directly finalized
325    /// (the newest leaf in a decided chain). Ancestor views finalized
326    /// indirectly have no cert2 file on disk
327    /// for those, this returns `Ok(None)`
328    fn load_cert2(&self, view: ViewNumber) -> anyhow::Result<Option<Certificate2<SeqTypes>>> {
329        let file_path = self
330            .decided_cert2_dir_path()
331            .join(view.u64().to_string())
332            .with_extension("bin");
333        if !file_path.is_file() {
334            return Ok(None);
335        }
336        let bytes = fs::read(&file_path).context("read cert2")?;
337        Ok(Some(
338            bincode::deserialize(&bytes).context("deserialize cert2")?,
339        ))
340    }
341
342    fn update_migration(&mut self) -> anyhow::Result<()> {
343        let path = self.migration();
344        let bytes = bincode::serialize(&self.migrated)?;
345
346        self.replace(
347            &path,
348            |_| Ok(true),
349            |mut file| {
350                file.write_all(&bytes)?;
351                Ok(())
352            },
353        )
354    }
355
356    /// Overwrite a file if a condition is met.
357    ///
358    /// The file at `path`, if it exists, is opened in read mode and passed to `pred`. If `pred`
359    /// returns `true`, or if there was no existing file, then `write` is called to update the
360    /// contents of the file. `write` receives a truncated file open in write mode and sets the
361    /// contents of the file.
362    ///
363    /// The final replacement of the original file is atomic; that is, `path` will be modified only
364    /// if the entire update succeeds.
365    fn replace(
366        &mut self,
367        path: &Path,
368        pred: impl FnOnce(File) -> anyhow::Result<bool>,
369        write: impl FnOnce(File) -> anyhow::Result<()>,
370    ) -> anyhow::Result<()> {
371        if path.is_file() {
372            // If there is an existing file, check if it is suitable to replace. Note that this
373            // check is not atomic with respect to the subsequent write at the file system level,
374            // but this object is the only one which writes to this file, and we have a mutable
375            // reference, so this should be safe.
376            if !pred(File::open(path)?)? {
377                // If we are not overwriting the file, we are done and consider the whole operation
378                // successful.
379                return Ok(());
380            }
381        }
382
383        // Either there is no existing file or we have decided to overwrite the file. Write the new
384        // contents into a temporary file so we can update `path` atomically using `rename`.
385        let mut swap_path = path.to_owned();
386        swap_path.set_extension("swp");
387        let swap = OpenOptions::new()
388            .write(true)
389            .truncate(true)
390            .create(true)
391            .open(&swap_path)?;
392        write(swap)?;
393
394        // Now we can replace the original file.
395        fs::rename(swap_path, path)?;
396
397        Ok(())
398    }
399
400    fn collect_garbage(
401        &mut self,
402        decided_view: ViewNumber,
403        prune_intervals: &[RangeInclusive<ViewNumber>],
404    ) -> anyhow::Result<()> {
405        let prune_view = ViewNumber::new(decided_view.saturating_sub(self.view_retention));
406
407        self.prune_files(self.da2_dir_path(), prune_view, None, prune_intervals)?;
408        self.prune_files(self.vid2_dir_path(), prune_view, None, prune_intervals)?;
409        self.prune_files(
410            self.quorum_proposals2_dir_path(),
411            prune_view,
412            None,
413            prune_intervals,
414        )?;
415        self.prune_files(
416            self.state_cert_dir_path(),
417            prune_view,
418            None,
419            prune_intervals,
420        )?;
421        self.prune_files(
422            self.decided_cert2_dir_path(),
423            prune_view,
424            None,
425            prune_intervals,
426        )?;
427
428        // Save the most recent leaf as it will be our anchor point if the node restarts.
429        self.prune_files(
430            self.decided_leaf2_path(),
431            prune_view,
432            Some(decided_view),
433            prune_intervals,
434        )?;
435
436        Ok(())
437    }
438
439    fn prune_files(
440        &mut self,
441        dir_path: PathBuf,
442        prune_view: ViewNumber,
443        keep_decided_view: Option<ViewNumber>,
444        prune_intervals: &[RangeInclusive<ViewNumber>],
445    ) -> anyhow::Result<()> {
446        if !dir_path.is_dir() {
447            return Ok(());
448        }
449
450        for (file_view, path) in view_files(dir_path)? {
451            // If the view is the anchor view, keep it no matter what.
452            if let Some(decided_view) = keep_decided_view
453                && decided_view == file_view
454            {
455                continue;
456            }
457            // Otherwise, delete it if it is time to prune this view _or_ if the given intervals,
458            // which we've already successfully processed, contain the view; in this case we simply
459            // don't need it anymore.
460            if file_view < prune_view || prune_intervals.iter().any(|i| i.contains(&file_view)) {
461                fs::remove_file(&path)?;
462            }
463        }
464
465        Ok(())
466    }
467
468    fn parse_decided_leaf(
469        &self,
470        bytes: &[u8],
471    ) -> anyhow::Result<(Leaf2, CertificatePair<SeqTypes>)> {
472        // Old versions of the software did not store the next epoch QC. Without knowing which
473        // version this file was created with, we can simply try parsing both ways and then
474        // reconstruct a certificate pair with or without the next epoch QC.
475        match bincode::deserialize(bytes) {
476            Ok((leaf, cert)) => Ok((leaf, cert)),
477            Err(err) => {
478                tracing::info!(
479                    "error parsing decided leaf, maybe file was created without next epoch QC? \
480                     {err}"
481                );
482                let (leaf, qc) =
483                    bincode::deserialize::<(Leaf2, QuorumCertificate2<SeqTypes>)>(bytes)
484                        .context("parsing decided leaf")?;
485                Ok((leaf, CertificatePair::non_epoch_change(qc)))
486            },
487        }
488    }
489
490    /// Generate events based on persisted decided leaves.
491    ///
492    /// Returns a list of closed intervals of views which can be safely deleted, as all leaves
493    /// within these view ranges have been processed by the event consumer.
494    async fn generate_decide_events(
495        &mut self,
496        view: ViewNumber,
497        deciding_qc: Option<Arc<CertificatePair<SeqTypes>>>,
498        consumer: &impl EventConsumer,
499    ) -> anyhow::Result<Vec<RangeInclusive<ViewNumber>>> {
500        // Generate a decide event for each leaf, to be processed by the event consumer. We make a
501        // separate event for each leaf because it is possible we have non-consecutive leaves in our
502        // storage, which would not be valid as a single decide with a single leaf chain.
503        let mut leaves = BTreeMap::new();
504        for (v, path) in view_files(self.decided_leaf2_path())? {
505            if v > view {
506                continue;
507            }
508
509            let bytes =
510                fs::read(&path).context(format!("reading decided leaf {}", path.display()))?;
511            let (mut leaf, cert) = self.parse_decided_leaf(&bytes)?;
512
513            // Include the VID share if available.
514            let vid_proposal = self.load_vid_share(v)?;
515            if vid_proposal.is_none() {
516                tracing::debug!(?v, "VID share not available at decide");
517            }
518            let vid_share = vid_proposal.as_ref().map(|proposal| proposal.data.clone());
519
520            // Move the state cert to the finalized dir if it exists.
521            let state_cert = self.store_finalized_state_cert(v)?;
522
523            // Fill in the full block payload using the DA proposals we had persisted.
524            if let Some(proposal) = self.load_da_proposal(v)? {
525                let payload = Payload::from_bytes(
526                    &proposal.data.encoded_transactions,
527                    &proposal.data.metadata,
528                );
529                leaf.fill_block_payload_unchecked(payload);
530            } else {
531                tracing::debug!(?v, "DA proposal not available at decide");
532            }
533
534            let info = LeafInfo {
535                leaf,
536                vid_share,
537                state_cert,
538                // Note: the following fields are not used in Decide event processing, and should be
539                // removed. For now, we just default them.
540                state: Default::default(),
541                delta: Default::default(),
542            };
543
544            leaves.insert(v, (info, cert));
545        }
546
547        // The invariant is that the oldest existing leaf in the `anchor_leaf` table -- if there is
548        // one -- was always included in the _previous_ decide event...but not removed from the
549        // database, because we always persist the most recent anchor leaf.
550        if let Some((oldest_view, _)) = leaves.first_key_value() {
551            // The only exception is when the oldest leaf is the genesis leaf; then there was no
552            // previous decide event.
553            if *oldest_view > ViewNumber::genesis() {
554                leaves.pop_first();
555            }
556        }
557
558        let mut intervals = vec![];
559        let mut current_interval = None;
560        for (view, (leaf, cert)) in leaves {
561            let height = leaf.leaf.block_header().block_number();
562
563            let event = if leaf.leaf.block_header().version() >= versions::NEW_PROTOCOL_VERSION {
564                let cert2 = self.load_cert2(view)?;
565                // One event per view. cert2 is only stored for the
566                // directly finalized view
567                // ancestors get `cert2: None`,
568                // which is what update() expects for indirectly decided leaves.
569                CoordinatorEvent::NewDecide {
570                    leaf_infos: vec![leaf],
571                    cert1: cert.qc().clone(),
572                    cert2,
573                }
574            } else {
575                let deciding_qc = deciding_qc
576                    .as_ref()
577                    .filter(|qc| qc.view_number() == cert.view_number() + 1)
578                    .cloned();
579                CoordinatorEvent::LegacyEvent(Event {
580                    view_number: view,
581                    event: EventType::Decide {
582                        committing_qc: Arc::new(cert),
583                        deciding_qc,
584                        leaf_chain: Arc::new(vec![leaf]),
585                        block_size: None,
586                    },
587                })
588            };
589            consumer.handle_event(&event).await?;
590
591            if let Some((start, end, current_height)) = current_interval.as_mut() {
592                if height == *current_height + 1 {
593                    // If we have a chain of consecutive leaves, extend the current interval of
594                    // views which are safe to delete.
595                    *current_height += 1;
596                    *end = view;
597                } else {
598                    // Otherwise, end the current interval and start a new one.
599                    intervals.push(*start..=*end);
600                    current_interval = Some((view, view, height));
601                }
602            } else {
603                // Start a new interval.
604                current_interval = Some((view, view, height));
605            }
606        }
607        if let Some((start, end, _)) = current_interval {
608            intervals.push(start..=end);
609        }
610
611        Ok(intervals)
612    }
613
614    fn load_da_proposal(
615        &self,
616        view: ViewNumber,
617    ) -> anyhow::Result<Option<Proposal<SeqTypes, DaProposal2<SeqTypes>>>> {
618        let dir_path = self.da2_dir_path();
619
620        let file_path = dir_path.join(view.u64().to_string()).with_extension("txt");
621
622        if !file_path.exists() {
623            return Ok(None);
624        }
625
626        let da_bytes = fs::read(file_path)?;
627
628        let da_proposal: Proposal<SeqTypes, DaProposal2<SeqTypes>> =
629            bincode::deserialize(&da_bytes)?;
630        Ok(Some(da_proposal))
631    }
632
633    fn load_vid_share(
634        &self,
635        view: ViewNumber,
636    ) -> anyhow::Result<Option<Proposal<SeqTypes, VidDisperseShare<SeqTypes>>>> {
637        let dir_path = self.vid2_dir_path();
638
639        let file_path = dir_path.join(view.u64().to_string()).with_extension("txt");
640
641        if !file_path.exists() {
642            return Ok(None);
643        }
644
645        let vid_share_bytes = fs::read(file_path)?;
646        let vid_share: Proposal<SeqTypes, VidDisperseShare<SeqTypes>> =
647            bincode::deserialize(&vid_share_bytes)?;
648        Ok(Some(vid_share))
649    }
650
651    fn load_anchor_leaf(&self) -> anyhow::Result<Option<(Leaf2, CertificatePair<SeqTypes>)>> {
652        tracing::info!("Checking `Leaf2` to load the anchor leaf.");
653        if self.decided_leaf2_path().is_dir() {
654            let mut anchor: Option<(Leaf2, CertificatePair<SeqTypes>)> = None;
655
656            // Return the latest decided leaf.
657            for (_, path) in view_files(self.decided_leaf2_path())? {
658                let bytes =
659                    fs::read(&path).context(format!("reading decided leaf {}", path.display()))?;
660                let (leaf, cert) = self.parse_decided_leaf(&bytes)?;
661                if let Some((anchor_leaf, _)) = &anchor {
662                    if leaf.view_number() > anchor_leaf.view_number() {
663                        anchor = Some((leaf, cert));
664                    }
665                } else {
666                    anchor = Some((leaf, cert));
667                }
668            }
669
670            return Ok(anchor);
671        }
672
673        tracing::warn!(
674            "Failed to find an anchor leaf in `Leaf2` storage. Checking legacy `Leaf` storage. \
675             This is very likely to fail."
676        );
677        if self.legacy_anchor_leaf_path().is_file() {
678            // We may have an old version of storage, where there is just a single file for the
679            // anchor leaf. Read it and return the contents.
680            let mut file = BufReader::new(File::open(self.legacy_anchor_leaf_path())?);
681
682            // The first 8 bytes just contain the height of the leaf. We can skip this.
683            file.seek(SeekFrom::Start(8)).context("seek")?;
684            let bytes = file
685                .bytes()
686                .collect::<Result<Vec<_>, _>>()
687                .context("read")?;
688            let (leaf2, qc2): (Leaf2, QuorumCertificate2<SeqTypes>) =
689                bincode::deserialize(&bytes).context("deserialize")?;
690            let cert_pair = CertificatePair::new(qc2, None);
691            return Ok(Some((leaf2, cert_pair)));
692        }
693
694        Ok(None)
695    }
696
697    fn store_finalized_state_cert(
698        &mut self,
699        view: ViewNumber,
700    ) -> anyhow::Result<Option<LightClientStateUpdateCertificateV2<SeqTypes>>> {
701        let dir_path = self.state_cert_dir_path();
702        let file_path = dir_path.join(view.u64().to_string()).with_extension("txt");
703
704        if !file_path.exists() {
705            return Ok(None);
706        }
707
708        let bytes = fs::read(&file_path)?;
709
710        let state_cert: LightClientStateUpdateCertificateV2<SeqTypes> =
711            bincode::deserialize(&bytes).or_else(|err_v2| {
712                tracing::info!(
713                    error = %err_v2,
714                    path = %file_path.display(),
715                    "Failed to deserialize state certificate, attempting with v1"
716                );
717
718                bincode::deserialize::<LightClientStateUpdateCertificateV1<SeqTypes>>(&bytes)
719                    .map(Into::into)
720                    .with_context(|| {
721                        format!(
722                            "Failed to deserialize with both v2 and v1 from file '{}'. error: \
723                             {err_v2}",
724                            file_path.display()
725                        )
726                    })
727            })?;
728
729        let epoch = state_cert.epoch.u64();
730        let finalized_dir_path = self.finalized_state_cert_dir_path();
731        fs::create_dir_all(&finalized_dir_path).context("creating finalized state cert dir")?;
732
733        let finalized_file_path = finalized_dir_path
734            .join(epoch.to_string())
735            .with_extension("txt");
736
737        self.replace(
738            &finalized_file_path,
739            |_| Ok(true),
740            |mut file| {
741                file.write_all(&bytes)?;
742                Ok(())
743            },
744        )
745        .context(format!(
746            "finalizing light client state update certificate file for epoch {epoch:?}"
747        ))?;
748
749        Ok(Some(state_cert))
750    }
751}
752
753#[async_trait]
754impl SequencerPersistence for Persistence {
755    async fn load_config(&self) -> anyhow::Result<Option<NetworkConfig>> {
756        let inner = self.inner.read().await;
757        let path = inner.config_path();
758        if !path.is_file() {
759            tracing::info!("config not found at {}", path.display());
760            return Ok(None);
761        }
762        tracing::info!("loading config from {}", path.display());
763
764        let bytes =
765            fs::read(&path).context(format!("unable to read config from {}", path.display()))?;
766        let json = serde_json::from_slice(&bytes).context("config file is not valid JSON")?;
767        let json = migrate_network_config(json).context("migration of network config failed")?;
768        let config = serde_json::from_value(json).context("malformed config file")?;
769        Ok(Some(config))
770    }
771
772    async fn save_config(&self, cfg: &NetworkConfig) -> anyhow::Result<()> {
773        let inner = self.inner.write().await;
774        let path = inner.config_path();
775        tracing::info!("saving config to {}", path.display());
776        Ok(cfg.to_file(path.display().to_string())?)
777    }
778
779    async fn load_latest_acted_view(&self) -> anyhow::Result<Option<ViewNumber>> {
780        let inner = self.inner.read().await;
781        let path = inner.voted_view_path();
782        if !path.is_file() {
783            return Ok(None);
784        }
785        let bytes = fs::read(inner.voted_view_path())?
786            .try_into()
787            .map_err(|bytes| anyhow!("malformed voted view file: {bytes:?}"))?;
788        Ok(Some(ViewNumber::new(u64::from_le_bytes(bytes))))
789    }
790
791    async fn load_restart_view(&self) -> anyhow::Result<Option<ViewNumber>> {
792        let inner = self.inner.read().await;
793        let path = inner.restart_view_path();
794        if !path.is_file() {
795            return Ok(None);
796        }
797        let bytes = fs::read(path)?
798            .try_into()
799            .map_err(|bytes| anyhow!("malformed restart view file: {bytes:?}"))?;
800        Ok(Some(ViewNumber::new(u64::from_le_bytes(bytes))))
801    }
802
803    async fn persist_decided_leaves(
804        &self,
805        _view: ViewNumber,
806        leaf_chain: impl IntoIterator<Item = (&LeafInfo<SeqTypes>, CertificatePair<SeqTypes>)> + Send,
807        _deciding_qc: Option<Arc<CertificatePair<SeqTypes>>>,
808        _consumer: &(impl EventConsumer + 'static),
809    ) -> anyhow::Result<()> {
810        let mut inner = self.inner.write().await;
811        let path = inner.decided_leaf2_path();
812
813        // Ensure the anchor leaf directory exists.
814        fs::create_dir_all(&path).context("creating anchor leaf directory")?;
815
816        // Earlier versions stored only a single decided leaf in a regular file. If our storage is
817        // still on this version, migrate to a directory structure storing (possibly) many leaves.
818        let legacy_path = inner.legacy_anchor_leaf_path();
819        if !path.is_dir() && legacy_path.is_file() {
820            tracing::info!("migrating to multi-leaf storage");
821
822            // Move the existing data into the new directory.
823            let (leaf, qc) = inner
824                .load_anchor_leaf()?
825                .context("anchor leaf file exists but unable to load contents")?;
826            let view = leaf.view_number().u64();
827            let bytes = bincode::serialize(&(leaf, qc))?;
828            let new_file = path.join(view.to_string()).with_extension("txt");
829            inner
830                .replace(
831                    &new_file,
832                    |_| Ok(true),
833                    |mut file| {
834                        file.write_all(&bytes)?;
835                        Ok(())
836                    },
837                )
838                .context(format!("writing anchor leaf file {view}"))?;
839
840            // Now we can remove the old file.
841            fs::remove_file(&legacy_path).context("removing legacy anchor leaf file")?;
842        }
843
844        for (info, cert) in leaf_chain {
845            let view = info.leaf.view_number().u64();
846            let file_path = path.join(view.to_string()).with_extension("txt");
847            inner.replace(
848                &file_path,
849                |_| {
850                    // Don't overwrite an existing leaf, but warn about it as this is likely not
851                    // intended behavior from HotShot.
852                    tracing::warn!(view, "duplicate decided leaf");
853                    Ok(false)
854                },
855                |mut file| {
856                    let bytes = bincode::serialize(&(&info.leaf, cert))?;
857                    file.write_all(&bytes)?;
858                    Ok(())
859                },
860            )?;
861        }
862
863        Ok(())
864    }
865
866    async fn process_decided_events(
867        &self,
868        view: ViewNumber,
869        deciding_qc: Option<Arc<CertificatePair<SeqTypes>>>,
870        consumer: &(impl EventConsumer + 'static),
871    ) -> anyhow::Result<Option<ViewNumber>> {
872        // Started before the lock acquisition: this pass holds the exclusive write lock, so the
873        // metric must include the wait to reflect how long appends can block behind it.
874        let now = Instant::now();
875        // On error, GC does not run over the failed range, so the leaves stay on disk and are
876        // retried; no data is lost.
877        let intervals = self
878            .inner
879            .write()
880            .await
881            .generate_decide_events(view, deciding_qc, consumer)
882            .await?;
883
884        // Highest view we generated an event for; unprocessed leaves stay on disk (the cursor).
885        let processed = intervals.iter().map(|i| *i.end()).max();
886
887        // Best-effort GC; runs again at the next decide.
888        let res = self.inner.write().await.collect_garbage(view, &intervals);
889        if let Err(err) = res {
890            tracing::warn!(?view, "GC failed: {err:#}");
891        }
892        self.metrics
893            .internal_process_decided_events_duration
894            .add_point(now.elapsed().as_secs_f64());
895
896        Ok(processed)
897    }
898
899    async fn load_anchor_leaf(&self) -> anyhow::Result<Option<(Leaf2, CertificatePair<SeqTypes>)>> {
900        self.inner.read().await.load_anchor_leaf()
901    }
902
903    async fn load_da_proposal(
904        &self,
905        view: ViewNumber,
906    ) -> anyhow::Result<Option<Proposal<SeqTypes, DaProposal2<SeqTypes>>>> {
907        self.inner.read().await.load_da_proposal(view)
908    }
909
910    async fn load_vid_share(
911        &self,
912        view: ViewNumber,
913    ) -> anyhow::Result<Option<Proposal<SeqTypes, VidDisperseShare<SeqTypes>>>> {
914        self.inner.read().await.load_vid_share(view)
915    }
916
917    async fn append_vid(
918        &self,
919        proposal: &Proposal<SeqTypes, VidDisperseShare<SeqTypes>>,
920    ) -> anyhow::Result<()> {
921        let mut inner = self.inner.write().await;
922        let view_number = proposal.data.view_number().u64();
923        let dir_path = inner.vid2_dir_path();
924
925        fs::create_dir_all(dir_path.clone()).context("failed to create vid dir")?;
926
927        let file_path = dir_path.join(view_number.to_string()).with_extension("txt");
928        inner.replace(
929            &file_path,
930            |_| {
931                // Don't overwrite an existing share, but warn about it as this is likely not intended
932                // behavior from HotShot.
933                tracing::warn!(view_number, "duplicate VID share");
934                Ok(false)
935            },
936            |mut file| {
937                let proposal_bytes = bincode::serialize(proposal).context("serialize proposal")?;
938                let now = Instant::now();
939                file.write_all(&proposal_bytes)?;
940                self.metrics
941                    .internal_append_vid_duration
942                    .add_point(now.elapsed().as_secs_f64());
943                Ok(())
944            },
945        )
946    }
947
948    async fn append_da(
949        &self,
950        proposal: &Proposal<SeqTypes, DaProposal<SeqTypes>>,
951        _vid_commit: VidCommitment,
952    ) -> anyhow::Result<()> {
953        let mut inner = self.inner.write().await;
954        let view_number = proposal.data.view_number().u64();
955        let dir_path = inner.da_dir_path();
956
957        fs::create_dir_all(dir_path.clone()).context("failed to create da dir")?;
958
959        let file_path = dir_path.join(view_number.to_string()).with_extension("txt");
960        inner.replace(
961            &file_path,
962            |_| {
963                // Don't overwrite an existing proposal, but warn about it as this is likely not
964                // intended behavior from HotShot.
965                tracing::warn!(view_number, "duplicate DA proposal");
966                Ok(false)
967            },
968            |mut file| {
969                let proposal_bytes = bincode::serialize(&proposal).context("serialize proposal")?;
970                let now = Instant::now();
971                file.write_all(&proposal_bytes)?;
972                self.metrics
973                    .internal_append_da_duration
974                    .add_point(now.elapsed().as_secs_f64());
975                Ok(())
976            },
977        )
978    }
979    async fn record_action(
980        &self,
981        view: ViewNumber,
982        _epoch: Option<EpochNumber>,
983        action: HotShotAction,
984    ) -> anyhow::Result<()> {
985        // Todo Remove this after https://github.com/EspressoSystems/espresso-network/issues/1931
986        if !matches!(action, HotShotAction::Propose | HotShotAction::Vote) {
987            return Ok(());
988        }
989        let mut inner = self.inner.write().await;
990        let path = &inner.voted_view_path();
991        inner.replace(
992            path,
993            |mut file| {
994                let mut bytes = vec![];
995                file.read_to_end(&mut bytes)?;
996                let bytes = bytes
997                    .try_into()
998                    .map_err(|bytes| anyhow!("malformed voted view file: {bytes:?}"))?;
999                let saved_view = ViewNumber::new(u64::from_le_bytes(bytes));
1000
1001                // Overwrite the file if the saved view is older than the new view.
1002                Ok(saved_view < view)
1003            },
1004            |mut file| {
1005                file.write_all(&view.u64().to_le_bytes())?;
1006                Ok(())
1007            },
1008        )?;
1009
1010        if matches!(action, HotShotAction::Vote) {
1011            let restart_view_path = &inner.restart_view_path();
1012            let restart_view = view + 1;
1013            inner.replace(
1014                restart_view_path,
1015                |mut file| {
1016                    let mut bytes = vec![];
1017                    file.read_to_end(&mut bytes)?;
1018                    let bytes = bytes
1019                        .try_into()
1020                        .map_err(|bytes| anyhow!("malformed voted view file: {bytes:?}"))?;
1021                    let saved_view = ViewNumber::new(u64::from_le_bytes(bytes));
1022
1023                    // Overwrite the file if the saved view is older than the new view.
1024                    Ok(saved_view < restart_view)
1025                },
1026                |mut file| {
1027                    file.write_all(&restart_view.u64().to_le_bytes())?;
1028                    Ok(())
1029                },
1030            )?;
1031        }
1032        Ok(())
1033    }
1034
1035    async fn append_quorum_proposal2(
1036        &self,
1037        proposal: &Proposal<SeqTypes, QuorumProposalWrapper<SeqTypes>>,
1038    ) -> anyhow::Result<()> {
1039        let mut inner = self.inner.write().await;
1040        let view_number = proposal.data.view_number().u64();
1041        let dir_path = inner.quorum_proposals2_dir_path();
1042
1043        fs::create_dir_all(dir_path.clone()).context("failed to create proposals dir")?;
1044
1045        let file_path = dir_path.join(view_number.to_string()).with_extension("txt");
1046        inner.replace(
1047            &file_path,
1048            |_| {
1049                // Always overwrite the previous file
1050                Ok(true)
1051            },
1052            |mut file| {
1053                let proposal_bytes = bincode::serialize(&proposal).context("serialize proposal")?;
1054                let now = Instant::now();
1055                file.write_all(&proposal_bytes)?;
1056                self.metrics
1057                    .internal_append_quorum2_duration
1058                    .add_point(now.elapsed().as_secs_f64());
1059                Ok(())
1060            },
1061        )
1062    }
1063
1064    async fn append_cert2(
1065        &self,
1066        view: ViewNumber,
1067        cert2: Certificate2<SeqTypes>,
1068    ) -> anyhow::Result<()> {
1069        let mut inner = self.inner.write().await;
1070        let dir_path = inner.decided_cert2_dir_path();
1071        fs::create_dir_all(dir_path.clone()).context("failed to create decided_cert2 dir")?;
1072        let file_path = dir_path.join(view.u64().to_string()).with_extension("bin");
1073        inner.replace(
1074            &file_path,
1075            |_| Ok(true),
1076            |mut file| {
1077                let bytes = bincode::serialize(&cert2).context("serialize cert2")?;
1078                file.write_all(&bytes)?;
1079                Ok(())
1080            },
1081        )
1082    }
1083
1084    async fn load_cert2(&self, view: ViewNumber) -> anyhow::Result<Option<Certificate2<SeqTypes>>> {
1085        let inner = self.inner.read().await;
1086        let dir_path = inner.decided_cert2_dir_path();
1087        let file_path = dir_path.join(view.u64().to_string()).with_extension("bin");
1088        if !file_path.is_file() {
1089            return Ok(None);
1090        }
1091        let bytes = fs::read(&file_path).context("read cert2")?;
1092        Ok(Some(
1093            bincode::deserialize(&bytes).context("deserialize cert2")?,
1094        ))
1095    }
1096    async fn load_quorum_proposals(
1097        &self,
1098    ) -> anyhow::Result<BTreeMap<ViewNumber, Proposal<SeqTypes, QuorumProposalWrapper<SeqTypes>>>>
1099    {
1100        let inner = self.inner.read().await;
1101
1102        // First, get the proposal directory.
1103        let dir_path = inner.quorum_proposals2_dir_path();
1104        if !dir_path.is_dir() {
1105            return Ok(Default::default());
1106        }
1107
1108        // Read quorum proposals from every data file in this directory.
1109        let mut map = BTreeMap::new();
1110        for (view, path) in view_files(&dir_path)? {
1111            let proposal_bytes = fs::read(path)?;
1112            let Some(proposal) = bincode::deserialize::<
1113                Proposal<SeqTypes, QuorumProposalWrapper<SeqTypes>>,
1114            >(&proposal_bytes)
1115            .or_else(|error| {
1116                bincode::deserialize::<Proposal<SeqTypes, QuorumProposalWrapperLegacy<SeqTypes>>>(
1117                    &proposal_bytes,
1118                )
1119                .map(convert_proposal)
1120                .inspect_err(|err_v3| {
1121                    // At this point, if the file contents are invalid, it is most likely an
1122                    // error rather than a miscellaneous file somehow ending up in the
1123                    // directory. However, we continue on, because it is better to collect as
1124                    // many proposals as we can rather than letting one bad proposal cause the
1125                    // entire operation to fail, and it is still possible that this was just
1126                    // some unintended file whose name happened to match the naming convention.
1127
1128                    tracing::warn!(
1129                        ?view,
1130                        %error,
1131                        error_v3 = %err_v3,
1132                        "ignoring malformed quorum proposal file"
1133                    );
1134                })
1135            })
1136            .ok() else {
1137                continue;
1138            };
1139
1140            let proposal2 = convert_proposal(proposal);
1141
1142            // Push to the map and we're done.
1143            map.insert(view, proposal2);
1144        }
1145
1146        Ok(map)
1147    }
1148
1149    async fn load_quorum_proposal(
1150        &self,
1151        view: ViewNumber,
1152    ) -> anyhow::Result<Proposal<SeqTypes, QuorumProposalWrapper<SeqTypes>>> {
1153        let inner = self.inner.read().await;
1154        let dir_path = inner.quorum_proposals2_dir_path();
1155        let file_path = dir_path.join(view.to_string()).with_extension("txt");
1156        let bytes = fs::read(file_path)?;
1157        let proposal: Proposal<SeqTypes, QuorumProposalWrapper<SeqTypes>> =
1158            bincode::deserialize(&bytes).or_else(|error| {
1159                bincode::deserialize::<Proposal<SeqTypes, QuorumProposalWrapperLegacy<SeqTypes>>>(
1160                    &bytes,
1161                )
1162                .map(convert_proposal)
1163                .context(format!(
1164                    "Failed to deserialize quorum proposal for view {view:?}: {error}."
1165                ))
1166            })?;
1167        Ok(proposal)
1168    }
1169
1170    async fn load_upgrade_certificate(
1171        &self,
1172    ) -> anyhow::Result<Option<UpgradeCertificate<SeqTypes>>> {
1173        let inner = self.inner.read().await;
1174        let path = inner.upgrade_certificate_dir_path();
1175        if !path.is_file() {
1176            return Ok(None);
1177        }
1178        let bytes = fs::read(&path).context("read")?;
1179        Ok(Some(
1180            bincode::deserialize(&bytes).context("deserialize upgrade certificate")?,
1181        ))
1182    }
1183
1184    async fn store_upgrade_certificate(
1185        &self,
1186        decided_upgrade_certificate: Option<UpgradeCertificate<SeqTypes>>,
1187    ) -> anyhow::Result<()> {
1188        let mut inner = self.inner.write().await;
1189        let path = &inner.upgrade_certificate_dir_path();
1190        let certificate = match decided_upgrade_certificate {
1191            Some(cert) => cert,
1192            None => return Ok(()),
1193        };
1194        inner.replace(
1195            path,
1196            |_| {
1197                // Always overwrite the previous file.
1198                Ok(true)
1199            },
1200            |mut file| {
1201                let bytes =
1202                    bincode::serialize(&certificate).context("serializing upgrade certificate")?;
1203                file.write_all(&bytes)?;
1204                Ok(())
1205            },
1206        )
1207    }
1208
1209    async fn store_next_epoch_quorum_certificate(
1210        &self,
1211        high_qc: NextEpochQuorumCertificate2<SeqTypes>,
1212    ) -> anyhow::Result<()> {
1213        let mut inner = self.inner.write().await;
1214        let path = &inner.next_epoch_qc();
1215
1216        inner.replace(
1217            path,
1218            |_| {
1219                // Always overwrite the previous file.
1220                Ok(true)
1221            },
1222            |mut file| {
1223                let bytes = bincode::serialize(&high_qc).context("serializing next epoch qc")?;
1224                file.write_all(&bytes)?;
1225                Ok(())
1226            },
1227        )
1228    }
1229
1230    async fn load_next_epoch_quorum_certificate(
1231        &self,
1232    ) -> anyhow::Result<Option<NextEpochQuorumCertificate2<SeqTypes>>> {
1233        let inner = self.inner.read().await;
1234        let path = inner.next_epoch_qc();
1235        if !path.is_file() {
1236            return Ok(None);
1237        }
1238        let bytes = fs::read(&path).context("read")?;
1239        Ok(Some(
1240            bincode::deserialize(&bytes).context("deserialize next epoch qc")?,
1241        ))
1242    }
1243
1244    async fn store_eqc(
1245        &self,
1246        high_qc: QuorumCertificate2<SeqTypes>,
1247        next_epoch_high_qc: NextEpochQuorumCertificate2<SeqTypes>,
1248    ) -> anyhow::Result<()> {
1249        let mut inner = self.inner.write().await;
1250        let path = &inner.eqc();
1251
1252        inner.replace(
1253            path,
1254            |_| {
1255                // Always overwrite the previous file.
1256                Ok(true)
1257            },
1258            |mut file| {
1259                let bytes = bincode::serialize(&(high_qc, next_epoch_high_qc))
1260                    .context("serializing next epoch qc")?;
1261                file.write_all(&bytes)?;
1262                Ok(())
1263            },
1264        )
1265    }
1266
1267    async fn load_eqc(
1268        &self,
1269    ) -> Option<(
1270        QuorumCertificate2<SeqTypes>,
1271        NextEpochQuorumCertificate2<SeqTypes>,
1272    )> {
1273        let inner = self.inner.read().await;
1274        let path = inner.eqc();
1275        if !path.is_file() {
1276            return None;
1277        }
1278        let bytes = fs::read(&path).ok()?;
1279
1280        bincode::deserialize(&bytes).ok()
1281    }
1282
1283    async fn append_high_qc2(&self, high_qc: QuorumCertificate2<SeqTypes>) -> anyhow::Result<()> {
1284        let mut inner = self.inner.write().await;
1285        let path = &inner.high_qc2();
1286        let view = high_qc.view_number();
1287        inner.replace(
1288            path,
1289            |mut file| {
1290                // Overwrite only when the new lock is newer. The whole replace
1291                // runs under the inner write lock, so this compare-and-set is
1292                // atomic and a stale concurrent write cannot regress the lock.
1293                let mut bytes = vec![];
1294                file.read_to_end(&mut bytes)?;
1295                let existing: QuorumCertificate2<SeqTypes> =
1296                    bincode::deserialize(&bytes).context("deserializing existing high_qc2")?;
1297                Ok(existing.view_number() < view)
1298            },
1299            |mut file| {
1300                let bytes = bincode::serialize(&high_qc).context("serializing high_qc2")?;
1301                file.write_all(&bytes)?;
1302                Ok(())
1303            },
1304        )
1305    }
1306
1307    async fn load_high_qc2(&self) -> anyhow::Result<Option<QuorumCertificate2<SeqTypes>>> {
1308        let inner = self.inner.read().await;
1309        let path = inner.high_qc2();
1310        if !path.is_file() {
1311            return Ok(None);
1312        }
1313        let bytes = fs::read(&path).context("reading high_qc2")?;
1314        Ok(Some(
1315            bincode::deserialize(&bytes).context("deserializing high_qc2")?,
1316        ))
1317    }
1318
1319    async fn append_da2(
1320        &self,
1321        proposal: &Proposal<SeqTypes, DaProposal2<SeqTypes>>,
1322        _vid_commit: VidCommitment,
1323    ) -> anyhow::Result<()> {
1324        let mut inner = self.inner.write().await;
1325        let view_number = proposal.data.view_number().u64();
1326        let dir_path = inner.da2_dir_path();
1327
1328        fs::create_dir_all(dir_path.clone()).context("failed to create da dir")?;
1329
1330        let file_path = dir_path.join(view_number.to_string()).with_extension("txt");
1331        inner.replace(
1332            &file_path,
1333            |_| {
1334                // Don't overwrite an existing proposal, but warn about it as this is likely not
1335                // intended behavior from HotShot.
1336                tracing::warn!(view_number, "duplicate DA proposal");
1337                Ok(false)
1338            },
1339            |mut file| {
1340                let proposal_bytes = bincode::serialize(&proposal).context("serialize proposal")?;
1341                let now = Instant::now();
1342                file.write_all(&proposal_bytes)?;
1343                self.metrics
1344                    .internal_append_da2_duration
1345                    .add_point(now.elapsed().as_secs_f64());
1346                Ok(())
1347            },
1348        )
1349    }
1350
1351    async fn append_proposal2(
1352        &self,
1353        proposal: &Proposal<SeqTypes, QuorumProposalWrapper<SeqTypes>>,
1354    ) -> anyhow::Result<()> {
1355        self.append_quorum_proposal2(proposal).await
1356    }
1357
1358    async fn migrate_anchor_leaf(&self) -> anyhow::Result<()> {
1359        let mut inner = self.inner.write().await;
1360
1361        if inner.migrated.contains("anchor_leaf") {
1362            tracing::info!("decided leaves already migrated");
1363            return Ok(());
1364        }
1365
1366        let new_leaf_dir = inner.decided_leaf2_path();
1367
1368        fs::create_dir_all(new_leaf_dir.clone()).context("failed to create anchor leaf 2  dir")?;
1369
1370        let old_leaf_dir = inner.decided_leaf_path();
1371        if !old_leaf_dir.is_dir() {
1372            return Ok(());
1373        }
1374
1375        tracing::warn!("migrating decided leaves..");
1376        for entry in fs::read_dir(old_leaf_dir)? {
1377            let entry = entry?;
1378            let path = entry.path();
1379
1380            let Some(file) = path.file_stem().and_then(|n| n.to_str()) else {
1381                continue;
1382            };
1383            let Ok(view) = file.parse::<u64>() else {
1384                continue;
1385            };
1386
1387            let bytes =
1388                fs::read(&path).context(format!("reading decided leaf {}", path.display()))?;
1389            let (leaf, qc) = bincode::deserialize::<(Leaf, QuorumCertificate<SeqTypes>)>(&bytes)
1390                .context(format!("parsing decided leaf {}", path.display()))?;
1391
1392            let leaf2: Leaf2 = leaf.into();
1393            let cert = CertificatePair::non_epoch_change(qc.to_qc2());
1394
1395            let new_leaf_path = new_leaf_dir.join(view.to_string()).with_extension("txt");
1396
1397            inner.replace(
1398                &new_leaf_path,
1399                |_| {
1400                    tracing::warn!(view, "duplicate decided leaf");
1401                    Ok(false)
1402                },
1403                |mut file| {
1404                    let bytes = bincode::serialize(&(&leaf2.clone(), cert))?;
1405                    file.write_all(&bytes)?;
1406                    Ok(())
1407                },
1408            )?;
1409
1410            if view % 100 == 0 {
1411                tracing::info!(view, "decided leaves migration progress");
1412            }
1413        }
1414
1415        inner.migrated.insert("anchor_leaf".to_string());
1416        inner.update_migration()?;
1417        tracing::warn!("successfully migrated decided leaves");
1418        Ok(())
1419    }
1420    async fn migrate_da_proposals(&self) -> anyhow::Result<()> {
1421        let mut inner = self.inner.write().await;
1422
1423        if inner.migrated.contains("da_proposal") {
1424            tracing::info!("da proposals already migrated");
1425            return Ok(());
1426        }
1427
1428        let new_da_dir = inner.da2_dir_path();
1429
1430        fs::create_dir_all(new_da_dir.clone()).context("failed to create da proposals 2 dir")?;
1431
1432        let old_da_dir = inner.da_dir_path();
1433        if !old_da_dir.is_dir() {
1434            return Ok(());
1435        }
1436
1437        tracing::warn!("migrating da proposals..");
1438
1439        for entry in fs::read_dir(old_da_dir)? {
1440            let entry = entry?;
1441            let path = entry.path();
1442
1443            let Some(file) = path.file_stem().and_then(|n| n.to_str()) else {
1444                continue;
1445            };
1446            let Ok(view) = file.parse::<u64>() else {
1447                continue;
1448            };
1449
1450            let bytes =
1451                fs::read(&path).context(format!("reading da proposal {}", path.display()))?;
1452            let proposal = bincode::deserialize::<Proposal<SeqTypes, DaProposal<SeqTypes>>>(&bytes)
1453                .context(format!("parsing da proposal {}", path.display()))?;
1454
1455            let new_da_path = new_da_dir.join(view.to_string()).with_extension("txt");
1456
1457            let proposal2: Proposal<SeqTypes, DaProposal2<SeqTypes>> = convert_proposal(proposal);
1458
1459            inner.replace(
1460                &new_da_path,
1461                |_| {
1462                    tracing::warn!(view, "duplicate DA proposal 2");
1463                    Ok(false)
1464                },
1465                |mut file| {
1466                    let bytes = bincode::serialize(&proposal2)?;
1467                    file.write_all(&bytes)?;
1468                    Ok(())
1469                },
1470            )?;
1471
1472            if view % 100 == 0 {
1473                tracing::info!(view, "DA proposals migration progress");
1474            }
1475        }
1476
1477        inner.migrated.insert("da_proposal".to_string());
1478        inner.update_migration()?;
1479        tracing::warn!("successfully migrated da proposals");
1480        Ok(())
1481    }
1482    async fn migrate_vid_shares(&self) -> anyhow::Result<()> {
1483        let mut inner = self.inner.write().await;
1484
1485        if inner.migrated.contains("vid_share") {
1486            tracing::info!("vid shares already migrated");
1487            return Ok(());
1488        }
1489
1490        let new_vid_dir = inner.vid2_dir_path();
1491
1492        fs::create_dir_all(new_vid_dir.clone()).context("failed to create vid shares 2 dir")?;
1493
1494        let old_vid_dir = inner.vid_dir_path();
1495        if !old_vid_dir.is_dir() {
1496            return Ok(());
1497        }
1498
1499        tracing::warn!("migrating vid shares..");
1500
1501        for entry in fs::read_dir(old_vid_dir)? {
1502            let entry = entry?;
1503            let path = entry.path();
1504
1505            let Some(file) = path.file_stem().and_then(|n| n.to_str()) else {
1506                continue;
1507            };
1508            let Ok(view) = file.parse::<u64>() else {
1509                continue;
1510            };
1511
1512            let bytes = fs::read(&path).context(format!("reading vid share {}", path.display()))?;
1513            let proposal =
1514                bincode::deserialize::<Proposal<SeqTypes, VidDisperseShare0<SeqTypes>>>(&bytes)
1515                    .context(format!("parsing vid share {}", path.display()))?;
1516
1517            let new_vid_path = new_vid_dir.join(view.to_string()).with_extension("txt");
1518
1519            let proposal2: Proposal<SeqTypes, VidDisperseShare<SeqTypes>> =
1520                convert_proposal(proposal);
1521
1522            inner.replace(
1523                &new_vid_path,
1524                |_| {
1525                    tracing::warn!(view, "duplicate VID share ");
1526                    Ok(false)
1527                },
1528                |mut file| {
1529                    let bytes = bincode::serialize(&proposal2)?;
1530                    file.write_all(&bytes)?;
1531                    Ok(())
1532                },
1533            )?;
1534
1535            if view % 100 == 0 {
1536                tracing::info!(view, "VID shares migration progress");
1537            }
1538        }
1539
1540        inner.migrated.insert("vid_share".to_string());
1541        inner.update_migration()?;
1542        tracing::warn!("successfully migrated vid shares");
1543        Ok(())
1544    }
1545
1546    async fn migrate_quorum_proposals(&self) -> anyhow::Result<()> {
1547        let mut inner = self.inner.write().await;
1548
1549        if inner.migrated.contains("quorum_proposals") {
1550            tracing::info!("quorum proposals already migrated");
1551            return Ok(());
1552        }
1553
1554        let new_quorum_proposals_dir = inner.quorum_proposals2_dir_path();
1555
1556        fs::create_dir_all(new_quorum_proposals_dir.clone())
1557            .context("failed to create quorum proposals 2 dir")?;
1558
1559        let old_quorum_proposals_dir = inner.quorum_proposals_dir_path();
1560        if !old_quorum_proposals_dir.is_dir() {
1561            tracing::info!("no existing quorum proposals found for migration");
1562            return Ok(());
1563        }
1564
1565        tracing::warn!("migrating quorum proposals..");
1566        for entry in fs::read_dir(old_quorum_proposals_dir)? {
1567            let entry = entry?;
1568            let path = entry.path();
1569
1570            let Some(file) = path.file_stem().and_then(|n| n.to_str()) else {
1571                continue;
1572            };
1573            let Ok(view) = file.parse::<u64>() else {
1574                continue;
1575            };
1576
1577            let bytes =
1578                fs::read(&path).context(format!("reading quorum proposal {}", path.display()))?;
1579            let proposal =
1580                bincode::deserialize::<Proposal<SeqTypes, QuorumProposal<SeqTypes>>>(&bytes)
1581                    .context(format!("parsing quorum proposal {}", path.display()))?;
1582
1583            let new_file_path = new_quorum_proposals_dir
1584                .join(view.to_string())
1585                .with_extension("txt");
1586
1587            let proposal2: Proposal<SeqTypes, QuorumProposalWrapper<SeqTypes>> =
1588                convert_proposal(proposal);
1589
1590            inner.replace(
1591                &new_file_path,
1592                |_| {
1593                    tracing::warn!(view, "duplicate Quorum proposal2 ");
1594                    Ok(false)
1595                },
1596                |mut file| {
1597                    let bytes = bincode::serialize(&proposal2)?;
1598                    file.write_all(&bytes)?;
1599                    Ok(())
1600                },
1601            )?;
1602
1603            if view % 100 == 0 {
1604                tracing::info!(view, "Quorum proposals migration progress");
1605            }
1606        }
1607
1608        inner.migrated.insert("quorum_proposals".to_string());
1609        inner.update_migration()?;
1610        tracing::warn!("successfully migrated quorum proposals");
1611        Ok(())
1612    }
1613    async fn migrate_quorum_certificates(&self) -> anyhow::Result<()> {
1614        Ok(())
1615    }
1616
1617    async fn migrate_x25519_keys(&self) -> anyhow::Result<()> {
1618        let mut inner = self.inner.write().await;
1619
1620        if inner.migrated.contains("x25519_keys") {
1621            tracing::info!("x25519_keys migration already complete");
1622            return Ok(());
1623        }
1624
1625        let path = inner.stake_table_dir_path();
1626        if !path.is_dir() {
1627            inner.migrated.insert("x25519_keys".to_string());
1628            inner.update_migration()?;
1629            return Ok(());
1630        }
1631
1632        tracing::warn!("migrating stake tables to add x25519 key fields...");
1633
1634        for (epoch, file_path) in epoch_files(&path)? {
1635            let bytes = fs::read(&file_path).with_context(|| {
1636                format!("failed to read stake table file at {}", file_path.display())
1637            })?;
1638
1639            let (stake, needs_rewrite) = deserialize_stake_table(&bytes).with_context(|| {
1640                format!(
1641                    "failed to deserialize stake table at {}",
1642                    file_path.display()
1643                )
1644            })?;
1645
1646            if !needs_rewrite {
1647                continue;
1648            }
1649
1650            // Atomic write: write to temp, then rename
1651            let new_bytes = bincode::serialize(&stake)?;
1652            let tmp_path = file_path.with_extension("txt.tmp");
1653            fs::write(&tmp_path, new_bytes)?;
1654            fs::rename(&tmp_path, &file_path)?;
1655
1656            tracing::info!(?epoch, "migrated stake table");
1657        }
1658
1659        // Also migrate validators JSON files (used by store_all_validators)
1660        let validators_dir = path.join("validators");
1661        if validators_dir.is_dir() {
1662            type LegacyJsonMap = indexmap::IndexMap<Address, RegisteredValidatorNoX25519>;
1663
1664            for entry in fs::read_dir(&validators_dir)? {
1665                let entry = entry?;
1666                let file_path = entry.path();
1667                if file_path.extension().is_some_and(|ext| ext == "json") {
1668                    let content = fs::read_to_string(&file_path)?;
1669
1670                    // Try current format first
1671                    if serde_json::from_str::<RegisteredValidatorMap>(&content).is_ok() {
1672                        continue;
1673                    }
1674
1675                    // Migrate from legacy format (no x25519_key/p2p_addr)
1676                    let legacy: LegacyJsonMap =
1677                        serde_json::from_str(&content).with_context(|| {
1678                            format!(
1679                                "failed to deserialize validators at {} (tried both formats)",
1680                                file_path.display()
1681                            )
1682                        })?;
1683
1684                    let migrated: RegisteredValidatorMap = legacy
1685                        .into_iter()
1686                        .map(|(addr, v)| (addr, v.migrate()))
1687                        .collect();
1688
1689                    // Atomic write: write to temp, then rename
1690                    let new_json = serde_json::to_string_pretty(&migrated)?;
1691                    let tmp_path = file_path.with_extension("json.tmp");
1692                    fs::write(&tmp_path, new_json)?;
1693                    fs::rename(&tmp_path, &file_path)?;
1694
1695                    tracing::info!(?file_path, "migrated validators file");
1696                }
1697            }
1698        }
1699
1700        inner.migrated.insert("x25519_keys".to_string());
1701        inner.update_migration()?;
1702        tracing::warn!("x25519_keys migration complete");
1703        Ok(())
1704    }
1705
1706    async fn store_drb_input(&self, drb_input: DrbInput) -> anyhow::Result<()> {
1707        if let Ok(loaded_drb_input) = self.load_drb_input(drb_input.epoch).await {
1708            if loaded_drb_input.difficulty_level != drb_input.difficulty_level {
1709                tracing::error!("Overwriting {loaded_drb_input:?} in storage with {drb_input:?}");
1710            } else if loaded_drb_input.iteration >= drb_input.iteration {
1711                anyhow::bail!(
1712                    "DrbInput in storage {:?} is more recent than {:?}, refusing to update",
1713                    loaded_drb_input,
1714                    drb_input
1715                )
1716            }
1717        }
1718
1719        let mut inner = self.inner.write().await;
1720        let dir_path = inner.drb_dir_path();
1721
1722        fs::create_dir_all(dir_path.clone()).context("failed to create drb dir")?;
1723
1724        let drb_input_bytes =
1725            bincode::serialize(&drb_input).context("failed to serialize drb_input")?;
1726
1727        let file_path = dir_path
1728            .join(drb_input.epoch.to_string())
1729            .with_extension("bin");
1730
1731        inner.replace(
1732            &file_path,
1733            |_| {
1734                // Always overwrite the previous file.
1735                Ok(true)
1736            },
1737            |mut file| {
1738                file.write_all(&drb_input_bytes).context(format!(
1739                    "writing epoch drb_input file for epoch {:?} at {:?}",
1740                    drb_input.epoch, file_path
1741                ))
1742            },
1743        )
1744    }
1745
1746    async fn load_drb_input(&self, epoch: u64) -> anyhow::Result<DrbInput> {
1747        let inner = self.inner.read().await;
1748        let path = &inner.drb_dir_path();
1749        let file_path = path.join(epoch.to_string()).with_extension("bin");
1750        let bytes = fs::read(&file_path).context("read")?;
1751        Ok(bincode::deserialize(&bytes)
1752            .context(format!("failed to deserialize DrbInput for epoch {epoch}"))?)
1753    }
1754
1755    async fn store_drb_result(
1756        &self,
1757        epoch: EpochNumber,
1758        drb_result: DrbResult,
1759    ) -> anyhow::Result<()> {
1760        let mut inner = self.inner.write().await;
1761        let dir_path = inner.epoch_drb_result_dir_path();
1762
1763        fs::create_dir_all(dir_path.clone()).context("failed to create epoch drb result dir")?;
1764
1765        let drb_result_bytes = bincode::serialize(&drb_result).context("serialize drb result")?;
1766
1767        let file_path = dir_path.join(epoch.to_string()).with_extension("txt");
1768
1769        inner.replace(
1770            &file_path,
1771            |_| {
1772                // Always overwrite the previous file.
1773                Ok(true)
1774            },
1775            |mut file| {
1776                file.write_all(&drb_result_bytes)
1777                    .context(format!("writing epoch drb result file for epoch {epoch:?}"))
1778            },
1779        )
1780    }
1781
1782    async fn store_epoch_root(
1783        &self,
1784        epoch: EpochNumber,
1785        block_header: <SeqTypes as NodeType>::BlockHeader,
1786    ) -> anyhow::Result<()> {
1787        let mut inner = self.inner.write().await;
1788        let dir_path = inner.epoch_root_block_header_dir_path();
1789
1790        fs::create_dir_all(dir_path.clone())
1791            .context("failed to create epoch root block header dir")?;
1792
1793        let block_header_bytes =
1794            bincode::serialize(&block_header).context("serialize block header")?;
1795
1796        let file_path = dir_path.join(epoch.to_string()).with_extension("txt");
1797        inner
1798            .replace(
1799                &file_path,
1800                |_| Ok(true),
1801                |mut file| {
1802                    file.write_all(&block_header_bytes)?;
1803                    Ok(())
1804                },
1805            )
1806            .context(format!(
1807                "writing epoch root block header file for epoch {epoch:?}"
1808            ))?;
1809
1810        Ok(())
1811    }
1812
1813    async fn add_state_cert(
1814        &self,
1815        state_cert: LightClientStateUpdateCertificateV2<SeqTypes>,
1816    ) -> anyhow::Result<()> {
1817        let mut inner = self.inner.write().await;
1818        // let epoch = state_cert.epoch;
1819        let view = state_cert.light_client_state.view_number;
1820        let dir_path = inner.state_cert_dir_path();
1821
1822        fs::create_dir_all(dir_path.clone())
1823            .context("failed to create light client state update certificate dir")?;
1824
1825        let bytes = bincode::serialize(&state_cert)
1826            .context("serialize light client state update certificate")?;
1827
1828        let file_path = dir_path.join(view.to_string()).with_extension("txt");
1829        inner
1830            .replace(
1831                &file_path,
1832                |_| Ok(true),
1833                |mut file| {
1834                    file.write_all(&bytes)?;
1835                    Ok(())
1836                },
1837            )
1838            .context(format!(
1839                "writing light client state update certificate file for view {view:?}"
1840            ))?;
1841
1842        Ok(())
1843    }
1844
1845    async fn load_start_epoch_info(&self) -> anyhow::Result<Vec<InitializerEpochInfo<SeqTypes>>> {
1846        let inner = self.inner.read().await;
1847        let drb_dir_path = inner.epoch_drb_result_dir_path();
1848        let block_header_dir_path = inner.epoch_root_block_header_dir_path();
1849
1850        let mut result = Vec::new();
1851
1852        if !drb_dir_path.is_dir() {
1853            return Ok(Vec::new());
1854        }
1855        for (epoch, path) in epoch_files(drb_dir_path)? {
1856            let bytes =
1857                fs::read(&path).context(format!("reading epoch drb result {}", path.display()))?;
1858            let drb_result = bincode::deserialize::<DrbResult>(&bytes)
1859                .context(format!("parsing epoch drb result {}", path.display()))?;
1860
1861            let block_header_path = block_header_dir_path
1862                .join(epoch.to_string())
1863                .with_extension("txt");
1864            let block_header = if block_header_path.is_file() {
1865                let bytes = fs::read(&block_header_path).context(format!(
1866                    "reading epoch root block header {}",
1867                    block_header_path.display()
1868                ))?;
1869                Some(
1870                    bincode::deserialize::<<SeqTypes as NodeType>::BlockHeader>(&bytes).context(
1871                        format!(
1872                            "parsing epoch root block header {}",
1873                            block_header_path.display()
1874                        ),
1875                    )?,
1876                )
1877            } else {
1878                None
1879            };
1880
1881            result.push(InitializerEpochInfo::<SeqTypes> {
1882                epoch,
1883                drb_result,
1884                block_header,
1885            });
1886        }
1887
1888        result.sort_by_key(|a| a.epoch);
1889
1890        // Keep only the most recent epochs
1891        let start = result
1892            .len()
1893            .saturating_sub(RECENT_STAKE_TABLES_LIMIT as usize);
1894        let recent = result[start..].to_vec();
1895
1896        Ok(recent)
1897    }
1898
1899    async fn load_state_cert(
1900        &self,
1901    ) -> anyhow::Result<Option<LightClientStateUpdateCertificateV2<SeqTypes>>> {
1902        let inner = self.inner.read().await;
1903        let dir_path = inner.finalized_state_cert_dir_path();
1904
1905        if !dir_path.is_dir() {
1906            return Ok(None);
1907        }
1908
1909        let mut result: Option<LightClientStateUpdateCertificateV2<SeqTypes>> = None;
1910
1911        for (epoch, path) in epoch_files(dir_path)? {
1912            if result.as_ref().is_some_and(|cert| epoch <= cert.epoch) {
1913                continue;
1914            }
1915            let bytes = fs::read(&path).context(format!(
1916                "reading light client state update certificate {}",
1917                path.display()
1918            ))?;
1919            let cert =
1920                bincode::deserialize::<LightClientStateUpdateCertificateV2<SeqTypes>>(&bytes)
1921                    .or_else(|error| {
1922                        tracing::info!(
1923                            %error,
1924                            path = %path.display(),
1925                            "Failed to deserialize LightClientStateUpdateCertificateV2"
1926                        );
1927
1928                        bincode::deserialize::<LightClientStateUpdateCertificateV1<SeqTypes>>(
1929                            &bytes,
1930                        )
1931                        .map(Into::into)
1932                        .with_context(|| {
1933                            format!(
1934                                "Failed to deserialize with v1 and v2. path='{}'. error: {error}",
1935                                path.display()
1936                            )
1937                        })
1938                    })?;
1939
1940            result = Some(cert);
1941        }
1942
1943        Ok(result)
1944    }
1945
1946    async fn get_state_cert_by_epoch(
1947        &self,
1948        epoch: u64,
1949    ) -> anyhow::Result<Option<LightClientStateUpdateCertificateV2<SeqTypes>>> {
1950        let inner = self.inner.read().await;
1951        let dir_path = inner.finalized_state_cert_dir_path();
1952
1953        let file_path = dir_path.join(epoch.to_string()).with_extension("txt");
1954
1955        if !file_path.exists() {
1956            return Ok(None);
1957        }
1958
1959        let bytes = fs::read(&file_path).context(format!(
1960            "reading light client state update certificate {}",
1961            file_path.display()
1962        ))?;
1963
1964        let cert = bincode::deserialize::<LightClientStateUpdateCertificateV2<SeqTypes>>(&bytes)
1965            .or_else(|error| {
1966                tracing::info!(
1967                    %error,
1968                    path = %file_path.display(),
1969                    "Failed to deserialize LightClientStateUpdateCertificateV2"
1970                );
1971
1972                bincode::deserialize::<LightClientStateUpdateCertificateV1<SeqTypes>>(&bytes)
1973                    .map(Into::into)
1974                    .with_context(|| {
1975                        format!(
1976                            "Failed to deserialize with v1 and v2. path='{}'. error: {error}",
1977                            file_path.display()
1978                        )
1979                    })
1980            })?;
1981
1982        Ok(Some(cert))
1983    }
1984
1985    async fn insert_state_cert(
1986        &self,
1987        epoch: u64,
1988        cert: LightClientStateUpdateCertificateV2<SeqTypes>,
1989    ) -> anyhow::Result<()> {
1990        let inner = self.inner.read().await;
1991        let dir_path = inner.finalized_state_cert_dir_path();
1992
1993        fs::create_dir_all(&dir_path)
1994            .context(format!("creating state cert dir {}", dir_path.display()))?;
1995
1996        let file_path = dir_path.join(epoch.to_string()).with_extension("txt");
1997        let bytes = bincode::serialize(&cert)
1998            .context("serializing light client state update certificate")?;
1999
2000        fs::write(&file_path, bytes).context(format!(
2001            "writing light client state update certificate {}",
2002            file_path.display()
2003        ))?;
2004
2005        Ok(())
2006    }
2007
2008    fn enable_metrics(&mut self, _metrics: &dyn Metrics) {
2009        // todo!()
2010    }
2011}
2012
2013#[async_trait]
2014impl MembershipPersistence for Persistence {
2015    async fn load_stake(&self, epoch: EpochNumber) -> anyhow::Result<Option<StakeTuple>> {
2016        let inner = self.inner.read().await;
2017        let path = &inner.stake_table_dir_path();
2018        let file_path = path.join(epoch.to_string()).with_extension("txt");
2019
2020        if !file_path.exists() {
2021            return Ok(None);
2022        }
2023
2024        let bytes = fs::read(&file_path).with_context(|| {
2025            format!("failed to read stake table file at {}", file_path.display())
2026        })?;
2027
2028        let (stake, _needs_rewrite) = deserialize_stake_table(&bytes).with_context(|| {
2029            format!(
2030                "failed to deserialize stake table at {}",
2031                file_path.display()
2032            )
2033        })?;
2034        Ok(Some(stake))
2035    }
2036
2037    async fn load_latest_stake(&self, limit: u64) -> anyhow::Result<Option<Vec<IndexedStake>>> {
2038        let limit = limit as usize;
2039        let inner = self.inner.read().await;
2040        let path = &inner.stake_table_dir_path();
2041        let sorted_files = epoch_files(path)?
2042            .sorted_by(|(e1, _), (e2, _)| e2.cmp(e1))
2043            .take(limit);
2044        let mut validator_sets: Vec<IndexedStake> = Vec::new();
2045
2046        for (epoch, file_path) in sorted_files {
2047            let bytes = fs::read(&file_path).with_context(|| {
2048                format!("failed to read stake table file at {}", file_path.display())
2049            })?;
2050
2051            let (stake, _needs_rewrite) = deserialize_stake_table(&bytes).with_context(|| {
2052                format!(
2053                    "failed to deserialize stake table at {}",
2054                    file_path.display()
2055                )
2056            })?;
2057            validator_sets.push((epoch, (stake.0, stake.1), stake.2));
2058        }
2059
2060        Ok(Some(validator_sets))
2061    }
2062
2063    async fn store_stake(
2064        &self,
2065        epoch: EpochNumber,
2066        stake: AuthenticatedValidatorMap,
2067        block_reward: Option<RewardAmount>,
2068        stake_table_hash: Option<StakeTableHash>,
2069    ) -> anyhow::Result<()> {
2070        let mut inner = self.inner.write().await;
2071        let dir_path = &inner.stake_table_dir_path();
2072
2073        fs::create_dir_all(dir_path.clone()).context("failed to create stake table dir")?;
2074
2075        let file_path = dir_path.join(epoch.to_string()).with_extension("txt");
2076
2077        inner.replace(
2078            &file_path,
2079            |_| {
2080                // Always overwrite the previous file.
2081                Ok(true)
2082            },
2083            |mut file| {
2084                let data: StakeTuple = (stake, block_reward, stake_table_hash);
2085                let bytes =
2086                    bincode::serialize(&data).context("serializing combined stake table")?;
2087                file.write_all(&bytes)?;
2088                Ok(())
2089            },
2090        )
2091    }
2092
2093    /// store stake table events upto the l1 block
2094    async fn store_events(
2095        &self,
2096        to_l1_block: u64,
2097        events: Vec<(EventKey, StakeTableEvent)>,
2098    ) -> anyhow::Result<()> {
2099        let mut inner = self.inner.write().await;
2100        let dir_path = &inner.stake_table_dir_path();
2101        let events_dir = dir_path.join("events");
2102
2103        fs::create_dir_all(events_dir.clone()).context("failed to create events dir")?;
2104        // Read the last l1 finalized for which events has been stored
2105        let last_l1_finalized_path = events_dir.join("last_l1_finalized").with_extension("bin");
2106
2107        // check if the last l1 events is higher than the incoming one
2108        if last_l1_finalized_path.exists() {
2109            let bytes = fs::read(&last_l1_finalized_path).with_context(|| {
2110                format!("Failed to read file at path: {last_l1_finalized_path:?}")
2111            })?;
2112            let mut buf = [0; 8];
2113            bytes
2114                .as_slice()
2115                .read_exact(&mut buf[..8])
2116                .with_context(|| {
2117                    format!("Failed to read 8 bytes from file at path: {last_l1_finalized_path:?}")
2118                })?;
2119            let persisted_l1_block = u64::from_le_bytes(buf);
2120            if persisted_l1_block > to_l1_block {
2121                tracing::debug!(?persisted_l1_block, ?to_l1_block, "stored l1 is greater");
2122                return Ok(());
2123            }
2124        }
2125
2126        // stores each event in a separate file
2127        // this can cause performance issue when, for example, reading all the files
2128        // However, the plan is to remove file system completely in future
2129        for (event_key, event) in events {
2130            let (block_number, event_index) = event_key;
2131            // file name is like block_index.json
2132            let filename = format!("{block_number}_{event_index}");
2133            let file_path = events_dir.join(filename).with_extension("json");
2134
2135            if file_path.exists() {
2136                continue;
2137            }
2138
2139            inner
2140                .replace(
2141                    &file_path,
2142                    |_| Ok(true),
2143                    |file| {
2144                        let writer = BufWriter::new(file);
2145
2146                        serde_json::to_writer_pretty(writer, &event)?;
2147                        Ok(())
2148                    },
2149                )
2150                .context("Failed to write event to file")?;
2151        }
2152
2153        // update the l1 block for which we have processed events
2154        inner.replace(
2155            &last_l1_finalized_path,
2156            |_| Ok(true),
2157            |mut file| {
2158                let bytes = to_l1_block.to_le_bytes();
2159
2160                file.write_all(&bytes)?;
2161                tracing::debug!("updated l1 finalized ={to_l1_block:?}");
2162                Ok(())
2163            },
2164        )
2165    }
2166
2167    /// Loads all events from persistent storage up to the specified L1 block.
2168    ///
2169    /// # Returns
2170    ///
2171    /// Returns a tuple containing:
2172    /// - `Option<u64>` - The queried L1 block for which all events have been successfully fetched.
2173    /// - `Vec<(EventKey, StakeTableEvent)>` - A list of events, where each entry is a tuple of the event key
2174    /// event key is (l1 block number, log index)
2175    ///   and the corresponding StakeTable event.
2176    ///
2177    async fn load_events(
2178        &self,
2179        from_l1_block: u64,
2180        to_l1_block: u64,
2181    ) -> anyhow::Result<(
2182        Option<EventsPersistenceRead>,
2183        Vec<(EventKey, StakeTableEvent)>,
2184    )> {
2185        let inner = self.inner.read().await;
2186        let dir_path = inner.stake_table_dir_path();
2187        let events_dir = dir_path.join("events");
2188
2189        // check if we have any events in storage
2190        // we can do this by checking last l1 finalized block for which we processed events
2191        let last_l1_finalized_path = events_dir.join("last_l1_finalized").with_extension("bin");
2192
2193        if !last_l1_finalized_path.exists() || !events_dir.exists() {
2194            return Ok((None, Vec::new()));
2195        }
2196
2197        let mut events = Vec::new();
2198
2199        let bytes = fs::read(&last_l1_finalized_path)
2200            .with_context(|| format!("Failed to read file at path: {last_l1_finalized_path:?}"))?;
2201        let mut buf = [0; 8];
2202        bytes
2203            .as_slice()
2204            .read_exact(&mut buf[..8])
2205            .with_context(|| {
2206                format!("Failed to read 8 bytes from file at path: {last_l1_finalized_path:?}")
2207            })?;
2208
2209        let last_processed_l1_block = u64::from_le_bytes(buf);
2210
2211        // Determine the L1 block for querying events.
2212        // If the last stored L1 block is greater than the requested block, limit the query to the requested block.
2213        // Otherwise, query up to the last stored block.
2214        let query_l1_block = if last_processed_l1_block > to_l1_block {
2215            to_l1_block
2216        } else {
2217            last_processed_l1_block
2218        };
2219
2220        for entry in fs::read_dir(&events_dir).context("events directory")? {
2221            let entry = entry?;
2222            let path = entry.path();
2223
2224            if !entry.file_type()?.is_file() {
2225                continue;
2226            }
2227
2228            if path
2229                .extension()
2230                .context(format!("extension for path={path:?}"))?
2231                != "json"
2232            {
2233                continue;
2234            }
2235
2236            let filename = path
2237                .file_stem()
2238                .and_then(|f| f.to_str())
2239                .unwrap_or_default();
2240
2241            let parts: Vec<&str> = filename.split('_').collect();
2242            if parts.len() != 2 {
2243                continue;
2244            }
2245
2246            let block_number = parts[0].parse::<u64>()?;
2247            let log_index = parts[1].parse::<u64>()?;
2248
2249            if block_number < from_l1_block || block_number > query_l1_block {
2250                continue;
2251            }
2252
2253            let file =
2254                File::open(&path).context(format!("Failed to open event file. path={path:?}"))?;
2255            let reader = BufReader::new(file);
2256
2257            let event: StakeTableEvent = serde_json::from_reader(reader)
2258                .context(format!("Failed to deserialize event at path={path:?}"))?;
2259
2260            events.push(((block_number, log_index), event));
2261        }
2262
2263        events.sort_by_key(|(key, _)| *key);
2264
2265        if query_l1_block == to_l1_block {
2266            Ok((Some(EventsPersistenceRead::Complete), events))
2267        } else {
2268            Ok((
2269                Some(EventsPersistenceRead::UntilL1Block(query_l1_block)),
2270                events,
2271            ))
2272        }
2273    }
2274
2275    async fn delete_stake_tables(&self) -> anyhow::Result<()> {
2276        let inner = self.inner.write().await;
2277        let events_dir = inner.stake_table_dir_path().join("events");
2278        if events_dir.exists() {
2279            fs::remove_dir_all(&events_dir)
2280                .with_context(|| format!("Failed to remove events dir: {events_dir:?}"))?;
2281        }
2282        let validators_dir = inner.stake_table_dir_path().join("validators");
2283        if validators_dir.exists() {
2284            fs::remove_dir_all(&validators_dir)
2285                .with_context(|| format!("Failed to remove validators dir: {validators_dir:?}"))?;
2286        }
2287        let drb_dir = inner.epoch_drb_result_dir_path();
2288        if drb_dir.exists() {
2289            fs::remove_dir_all(&drb_dir)
2290                .with_context(|| format!("Failed to remove epoch DRB result dir: {drb_dir:?}"))?;
2291        }
2292        Ok(())
2293    }
2294
2295    async fn store_all_validators(
2296        &self,
2297        epoch: EpochNumber,
2298        all_validators: RegisteredValidatorMap,
2299    ) -> anyhow::Result<()> {
2300        let mut inner = self.inner.write().await;
2301        let dir_path = inner.stake_table_dir_path();
2302        let validators_dir = dir_path.join("validators");
2303
2304        // Ensure validators directory exists
2305        fs::create_dir_all(&validators_dir)
2306            .with_context(|| format!("Failed to create validators dir: {validators_dir:?}"))?;
2307
2308        // Path = validators/epoch_<number>.json
2309        let file_path = validators_dir.join(format!("epoch_{epoch}.json"));
2310
2311        inner
2312            .replace(
2313                &file_path,
2314                |_| Ok(true),
2315                |file| {
2316                    let writer = BufWriter::new(file);
2317
2318                    serde_json::to_writer_pretty(writer, &all_validators).with_context(|| {
2319                        format!("Failed to serialize validators for epoch {epoch}")
2320                    })?;
2321                    Ok(())
2322                },
2323            )
2324            .with_context(|| format!("Failed to write validator file: {file_path:?}"))?;
2325
2326        Ok(())
2327    }
2328
2329    async fn load_all_validators(
2330        &self,
2331        epoch: EpochNumber,
2332        offset: u64,
2333        limit: u64,
2334    ) -> anyhow::Result<Vec<RegisteredValidator<PubKey>>> {
2335        let inner = self.inner.read().await;
2336        let dir_path = inner.stake_table_dir_path();
2337        let validators_dir = dir_path.join("validators");
2338        let file_path = validators_dir.join(format!("epoch_{epoch}.json"));
2339
2340        if !file_path.exists() {
2341            bail!("Validator file not found for epoch {epoch}");
2342        }
2343
2344        let file = File::open(&file_path)
2345            .with_context(|| format!("Failed to open validator file: {file_path:?}"))?;
2346        let reader = BufReader::new(file);
2347
2348        let map: RegisteredValidatorMap = serde_json::from_reader(reader).with_context(|| {
2349            format!("Failed to deserialize validators at {file_path:?}. epoch = {epoch}")
2350        })?;
2351
2352        let mut values: Vec<RegisteredValidator<PubKey>> = map.into_values().collect();
2353        values.sort_by_key(|v| v.account);
2354
2355        let start = offset as usize;
2356        let end = (start + limit as usize).min(values.len());
2357
2358        if start >= values.len() {
2359            return Ok(vec![]);
2360        }
2361
2362        Ok(values[start..end].to_vec())
2363    }
2364}
2365
2366#[async_trait]
2367impl DhtPersistentStorage for Persistence {
2368    /// Save the DHT to the file on disk
2369    ///
2370    /// # Errors
2371    /// - If we fail to serialize the records
2372    /// - If we fail to write the serialized records to the file
2373    async fn save(&self, records: Vec<SerializableRecord>) -> anyhow::Result<()> {
2374        // Bincode-serialize the records
2375        let to_save =
2376            bincode::serialize(&records).with_context(|| "failed to serialize records")?;
2377
2378        // Get the path to save the file to
2379        let path = self.inner.read().await.libp2p_dht_path();
2380
2381        // Create the directory if it doesn't exist
2382        fs::create_dir_all(path.parent().with_context(|| "directory had no parent")?)
2383            .with_context(|| "failed to create directory")?;
2384
2385        // Get a write lock on the inner struct
2386        let mut inner = self.inner.write().await;
2387
2388        // Save the file, replacing the previous one if it exists
2389        inner
2390            .replace(
2391                &path,
2392                |_| {
2393                    // Always overwrite the previous file
2394                    Ok(true)
2395                },
2396                |mut file| {
2397                    file.write_all(&to_save)
2398                        .with_context(|| "failed to write records to file")?;
2399                    Ok(())
2400                },
2401            )
2402            .with_context(|| "failed to save records to file")?;
2403
2404        Ok(())
2405    }
2406
2407    /// Load the DHT from the file on disk
2408    ///
2409    /// # Errors
2410    /// - If we fail to read the file
2411    /// - If we fail to deserialize the records
2412    async fn load(&self) -> anyhow::Result<Vec<SerializableRecord>> {
2413        // Read the contents of the file
2414        let contents = std::fs::read(self.inner.read().await.libp2p_dht_path())
2415            .with_context(|| "Failed to read records from file")?;
2416
2417        // Deserialize the contents
2418        let records: Vec<SerializableRecord> =
2419            bincode::deserialize(&contents).with_context(|| "Failed to deserialize records")?;
2420
2421        Ok(records)
2422    }
2423}
2424
2425/// Get all paths under `dir` whose name is of the form <view number>.txt.
2426fn view_files(
2427    dir: impl AsRef<Path>,
2428) -> anyhow::Result<impl Iterator<Item = (ViewNumber, PathBuf)>> {
2429    Ok(fs::read_dir(dir.as_ref())?.filter_map(move |entry| {
2430        let dir = dir.as_ref().display();
2431        let entry = entry.ok()?;
2432        if !entry.file_type().ok()?.is_file() {
2433            tracing::debug!(%dir, ?entry, "ignoring non-file in data directory");
2434            return None;
2435        }
2436        let path = entry.path();
2437        // Most view-keyed files use a `.txt` extension; cert2 files use `.bin`. Both hold bincode.
2438        let ext = path.extension()?;
2439        if ext != "txt" && ext != "bin" {
2440            tracing::debug!(%dir, ?entry, "ignoring file with unrecognized extension in data directory");
2441            return None;
2442        }
2443        let file_name = path.file_stem()?;
2444        let Ok(view_number) = file_name.to_string_lossy().parse::<u64>() else {
2445            tracing::debug!(%dir, ?file_name, "ignoring extraneous file in data directory");
2446            return None;
2447        };
2448        Some((ViewNumber::new(view_number), entry.path().to_owned()))
2449    }))
2450}
2451
2452/// Get all paths under `dir` whose name is of the form <epoch number>.txt.
2453/// Should probably be made generic and merged with view_files.
2454fn epoch_files(
2455    dir: impl AsRef<Path>,
2456) -> anyhow::Result<impl Iterator<Item = (EpochNumber, PathBuf)>> {
2457    Ok(fs::read_dir(dir.as_ref())?.filter_map(move |entry| {
2458        let dir = dir.as_ref().display();
2459        let entry = entry.ok()?;
2460        if !entry.file_type().ok()?.is_file() {
2461            tracing::debug!(%dir, ?entry, "ignoring non-file in data directory");
2462            return None;
2463        }
2464        let path = entry.path();
2465        if path.extension()? != "txt" {
2466            tracing::debug!(%dir, ?entry, "ignoring non-text file in data directory");
2467            return None;
2468        }
2469        let file_name = path.file_stem()?;
2470        let Ok(epoch_number) = file_name.to_string_lossy().parse::<u64>() else {
2471            tracing::debug!(%dir, ?file_name, "ignoring extraneous file in data directory");
2472            return None;
2473        };
2474        Some((EpochNumber::new(epoch_number), entry.path().to_owned()))
2475    }))
2476}
2477
2478#[cfg(test)]
2479mod test {
2480    use std::marker::PhantomData;
2481
2482    use committable::{Commitment, CommitmentBoundsArkless, Committable};
2483    use espresso_types::{Header, Leaf, NodeState, PubKey, ValidatedState};
2484    use hotshot::types::SignatureKey;
2485    use hotshot_example_types::node_types::TEST_VERSIONS;
2486    use hotshot_query_service::testing::mocks::MOCK_UPGRADE;
2487    use hotshot_types::{
2488        data::QuorumProposal2,
2489        light_client::LightClientState,
2490        simple_certificate::QuorumCertificate,
2491        simple_vote::{QuorumData, Vote2Data},
2492        traits::{EncodeBytes, block_contents::GENESIS_VID_NUM_STORAGE_NODES},
2493        vid::advz::advz_scheme,
2494    };
2495    use jf_advz::VidScheme;
2496    use serde_json::json;
2497    use tempfile::TempDir;
2498
2499    use super::*;
2500    use crate::{BLSPubKey, persistence::tests::TestablePersistence};
2501
2502    #[async_trait]
2503    impl TestablePersistence for Persistence {
2504        type Storage = TempDir;
2505
2506        async fn tmp_storage() -> Self::Storage {
2507            TempDir::new().unwrap()
2508        }
2509
2510        fn options(storage: &Self::Storage) -> impl PersistenceOptions<Persistence = Self> {
2511            Options::new(storage.path().into())
2512        }
2513    }
2514
2515    #[test]
2516    fn test_config_migrations_add_builder_urls() {
2517        let before = json!({
2518            "config": {
2519                "builder_url": "https://test:8080",
2520                "start_proposing_view": 1,
2521                "stop_proposing_view": 2,
2522                "start_voting_view": 1,
2523                "stop_voting_view": 2,
2524                "start_proposing_time": 1,
2525                "stop_proposing_time": 2,
2526                "start_voting_time": 1,
2527                "stop_voting_time": 2
2528            }
2529        });
2530        let after = json!({
2531            "config": {
2532                "builder_urls": ["https://test:8080"],
2533                "start_proposing_view": 1,
2534                "stop_proposing_view": 2,
2535                "start_voting_view": 1,
2536                "stop_voting_view": 2,
2537                "start_proposing_time": 1,
2538                "stop_proposing_time": 2,
2539                "start_voting_time": 1,
2540                "stop_voting_time": 2,
2541                "epoch_height": 0,
2542                "drb_difficulty": 0,
2543                "drb_upgrade_difficulty": 0,
2544                "da_committees": [],
2545            }
2546        });
2547
2548        assert_eq!(migrate_network_config(before).unwrap(), after);
2549    }
2550
2551    #[test]
2552    fn test_config_migrations_existing_builder_urls() {
2553        let before = json!({
2554            "config": {
2555                "builder_urls": ["https://test:8080", "https://test:8081"],
2556                "start_proposing_view": 1,
2557                "stop_proposing_view": 2,
2558                "start_voting_view": 1,
2559                "stop_voting_view": 2,
2560                "start_proposing_time": 1,
2561                "stop_proposing_time": 2,
2562                "start_voting_time": 1,
2563                "stop_voting_time": 2,
2564                "epoch_height": 0,
2565                "drb_difficulty": 0,
2566                "drb_upgrade_difficulty": 0,
2567                "da_committees": [],
2568            }
2569        });
2570
2571        assert_eq!(migrate_network_config(before.clone()).unwrap(), before);
2572    }
2573
2574    #[test]
2575    fn test_config_migrations_add_upgrade_params() {
2576        let before = json!({
2577            "config": {
2578                "builder_urls": ["https://test:8080", "https://test:8081"]
2579            }
2580        });
2581        let after = json!({
2582            "config": {
2583                "builder_urls": ["https://test:8080", "https://test:8081"],
2584                "start_proposing_view": 9007199254740991u64,
2585                "stop_proposing_view": 0,
2586                "start_voting_view": 9007199254740991u64,
2587                "stop_voting_view": 0,
2588                "start_proposing_time": 9007199254740991u64,
2589                "stop_proposing_time": 0,
2590                "start_voting_time": 9007199254740991u64,
2591                "stop_voting_time": 0,
2592                "epoch_height": 0,
2593                "drb_difficulty": 0,
2594                "drb_upgrade_difficulty": 0,
2595                "da_committees": [],
2596            }
2597        });
2598
2599        assert_eq!(migrate_network_config(before).unwrap(), after);
2600    }
2601
2602    #[test]
2603    fn test_config_migrations_existing_upgrade_params() {
2604        let before = json!({
2605            "config": {
2606                "builder_urls": ["https://test:8080", "https://test:8081"],
2607                "start_proposing_view": 1,
2608                "stop_proposing_view": 2,
2609                "start_voting_view": 1,
2610                "stop_voting_view": 2,
2611                "start_proposing_time": 1,
2612                "stop_proposing_time": 2,
2613                "start_voting_time": 1,
2614                "stop_voting_time": 2,
2615                "epoch_height": 0,
2616                "drb_difficulty": 0,
2617                "drb_upgrade_difficulty": 0,
2618                "da_committees": [],
2619            }
2620        });
2621
2622        assert_eq!(migrate_network_config(before.clone()).unwrap(), before);
2623    }
2624
2625    #[test_log::test(tokio::test(flavor = "multi_thread"))]
2626    pub async fn test_consensus_migration() {
2627        let rows = 300;
2628        let tmp = Persistence::tmp_storage().await;
2629        let mut opt = Persistence::options(&tmp);
2630        let storage = opt.create().await.unwrap();
2631
2632        let inner = storage.inner.read().await;
2633
2634        let decided_leaves_path = inner.decided_leaf_path();
2635        fs::create_dir_all(decided_leaves_path.clone()).expect("failed to create proposals dir");
2636
2637        let qp_dir_path = inner.quorum_proposals_dir_path();
2638        fs::create_dir_all(qp_dir_path.clone()).expect("failed to create proposals dir");
2639
2640        let state_cert_dir_path = inner.state_cert_dir_path();
2641        fs::create_dir_all(state_cert_dir_path.clone()).expect("failed to create state cert dir");
2642        drop(inner);
2643
2644        assert!(storage.load_state_cert().await.unwrap().is_none());
2645
2646        for i in 0..rows {
2647            let view = ViewNumber::new(i);
2648            let validated_state = ValidatedState::default();
2649            let instance_state = NodeState::default();
2650
2651            let (pubkey, privkey) = BLSPubKey::generated_from_seed_indexed([0; 32], i);
2652            let (payload, metadata) =
2653                Payload::from_transactions([], &validated_state, &instance_state)
2654                    .await
2655                    .unwrap();
2656
2657            let payload_bytes = payload.encode();
2658
2659            let block_header = Header::genesis(
2660                &instance_state,
2661                payload.clone(),
2662                &metadata,
2663                TEST_VERSIONS.test.base,
2664            );
2665
2666            let state_cert = LightClientStateUpdateCertificateV2::<SeqTypes> {
2667                epoch: EpochNumber::new(i),
2668                light_client_state: LightClientState {
2669                    view_number: i,
2670                    block_height: i,
2671                    block_comm_root: Default::default(),
2672                },
2673                next_stake_table_state: Default::default(),
2674                signatures: vec![], // filling arbitrary value
2675                auth_root: Default::default(),
2676            };
2677            assert!(storage.add_state_cert(state_cert).await.is_ok());
2678
2679            let null_quorum_data = QuorumData {
2680                leaf_commit: Commitment::<Leaf>::default_commitment_no_preimage(),
2681            };
2682
2683            let justify_qc = QuorumCertificate::new(
2684                null_quorum_data.clone(),
2685                null_quorum_data.commit(),
2686                view,
2687                None,
2688                PhantomData,
2689            );
2690
2691            let quorum_proposal = QuorumProposal {
2692                block_header,
2693                view_number: view,
2694                justify_qc: justify_qc.clone(),
2695                upgrade_certificate: None,
2696                proposal_certificate: None,
2697            };
2698
2699            let quorum_proposal_signature =
2700                BLSPubKey::sign(&privkey, &bincode::serialize(&quorum_proposal).unwrap())
2701                    .expect("Failed to sign quorum proposal");
2702
2703            let proposal = Proposal {
2704                data: quorum_proposal.clone(),
2705                signature: quorum_proposal_signature,
2706                _pd: PhantomData::<SeqTypes>,
2707            };
2708
2709            let mut leaf = Leaf::from_quorum_proposal(&quorum_proposal);
2710            leaf.fill_block_payload(
2711                payload,
2712                GENESIS_VID_NUM_STORAGE_NODES,
2713                TEST_VERSIONS.test.base,
2714            )
2715            .unwrap();
2716
2717            let mut inner = storage.inner.write().await;
2718
2719            tracing::debug!("inserting decided leaves");
2720            let file_path = decided_leaves_path
2721                .join(view.to_string())
2722                .with_extension("txt");
2723
2724            tracing::debug!("inserting decided leaves");
2725
2726            inner
2727                .replace(
2728                    &file_path,
2729                    |_| Ok(true),
2730                    |mut file| {
2731                        let bytes = bincode::serialize(&(&leaf.clone(), justify_qc))?;
2732                        file.write_all(&bytes)?;
2733                        Ok(())
2734                    },
2735                )
2736                .expect("replace decided leaves");
2737
2738            let file_path = qp_dir_path.join(view.to_string()).with_extension("txt");
2739
2740            tracing::debug!("inserting qc for {view}");
2741
2742            inner
2743                .replace(
2744                    &file_path,
2745                    |_| Ok(true),
2746                    |mut file| {
2747                        let proposal_bytes =
2748                            bincode::serialize(&proposal).context("serialize proposal")?;
2749
2750                        file.write_all(&proposal_bytes)?;
2751                        Ok(())
2752                    },
2753                )
2754                .unwrap();
2755
2756            drop(inner);
2757            let disperse = advz_scheme(GENESIS_VID_NUM_STORAGE_NODES)
2758                .disperse(payload_bytes.clone())
2759                .unwrap();
2760
2761            let vid = VidDisperseShare0::<SeqTypes> {
2762                view_number: ViewNumber::new(i),
2763                payload_commitment: Default::default(),
2764                share: disperse.shares[0].clone(),
2765                common: disperse.common,
2766                recipient_key: pubkey,
2767            };
2768
2769            let (payload, metadata) =
2770                Payload::from_transactions([], &ValidatedState::default(), &NodeState::default())
2771                    .await
2772                    .unwrap();
2773
2774            let da = DaProposal::<SeqTypes> {
2775                encoded_transactions: payload.encode(),
2776                metadata,
2777                view_number: ViewNumber::new(i),
2778            };
2779
2780            let block_payload_signature =
2781                BLSPubKey::sign(&privkey, &payload_bytes).expect("Failed to sign block payload");
2782
2783            let da_proposal = Proposal {
2784                data: da,
2785                signature: block_payload_signature,
2786                _pd: Default::default(),
2787            };
2788
2789            tracing::debug!("inserting vid for {view}");
2790            storage
2791                .append_vid(&convert_proposal(vid.to_proposal(&privkey).unwrap()))
2792                .await
2793                .unwrap();
2794
2795            tracing::debug!("inserting da for {view}");
2796            storage
2797                .append_da(&da_proposal, VidCommitment::V0(disperse.commit))
2798                .await
2799                .unwrap();
2800        }
2801
2802        storage.migrate_storage().await.unwrap();
2803        let inner = storage.inner.read().await;
2804        let decided_leaves = fs::read_dir(inner.decided_leaf2_path()).unwrap();
2805        let decided_leaves_count = decided_leaves
2806            .filter_map(Result::ok)
2807            .filter(|e| e.path().is_file())
2808            .count();
2809        assert_eq!(
2810            decided_leaves_count, rows as usize,
2811            "decided leaves count does not match",
2812        );
2813
2814        let da_proposals = fs::read_dir(inner.da2_dir_path()).unwrap();
2815        let da_proposals_count = da_proposals
2816            .filter_map(Result::ok)
2817            .filter(|e| e.path().is_file())
2818            .count();
2819        assert_eq!(
2820            da_proposals_count, rows as usize,
2821            "da proposals does not match",
2822        );
2823
2824        let vids = fs::read_dir(inner.vid2_dir_path()).unwrap();
2825        let vids_count = vids
2826            .filter_map(Result::ok)
2827            .filter(|e| e.path().is_file())
2828            .count();
2829        assert_eq!(vids_count, rows as usize, "vid shares count does not match",);
2830
2831        let qps = fs::read_dir(inner.quorum_proposals2_dir_path()).unwrap();
2832        let qps_count = qps
2833            .filter_map(Result::ok)
2834            .filter(|e| e.path().is_file())
2835            .count();
2836        assert_eq!(
2837            qps_count, rows as usize,
2838            "quorum proposals count does not match",
2839        );
2840
2841        let state_certs = fs::read_dir(inner.state_cert_dir_path()).unwrap();
2842        let state_cert_count = state_certs
2843            .filter_map(Result::ok)
2844            .filter(|e| e.path().is_file())
2845            .count();
2846        assert_eq!(
2847            state_cert_count, rows as usize,
2848            "light client state update certificate count does not match",
2849        );
2850
2851        // Reinitialize the file system persistence using the same path.
2852        // re run the consensus migration.
2853        // No changes will occur, as the migration has already been completed.
2854        let storage = opt.create().await.unwrap();
2855        storage.migrate_storage().await.unwrap();
2856
2857        let inner = storage.inner.read().await;
2858        let decided_leaves = fs::read_dir(inner.decided_leaf2_path()).unwrap();
2859        let decided_leaves_count = decided_leaves
2860            .filter_map(Result::ok)
2861            .filter(|e| e.path().is_file())
2862            .count();
2863        assert_eq!(
2864            decided_leaves_count, rows as usize,
2865            "decided leaves count does not match",
2866        );
2867    }
2868
2869    #[test_log::test(tokio::test(flavor = "multi_thread"))]
2870    async fn test_load_quorum_proposals_invalid_extension() {
2871        let tmp = Persistence::tmp_storage().await;
2872        let storage = Persistence::connect(&tmp).await;
2873
2874        // Generate a couple of valid quorum proposals.
2875        let leaf = Leaf2::genesis(&Default::default(), &NodeState::mock(), MOCK_UPGRADE.base).await;
2876        let privkey = PubKey::generated_from_seed_indexed([0; 32], 1).1;
2877        let signature = PubKey::sign(&privkey, &[]).unwrap();
2878        let mut quorum_proposal = Proposal {
2879            data: QuorumProposalWrapper::<SeqTypes> {
2880                proposal: QuorumProposal2::<SeqTypes> {
2881                    epoch: None,
2882                    block_header: leaf.block_header().clone(),
2883                    view_number: ViewNumber::genesis(),
2884                    justify_qc: QuorumCertificate2::genesis(
2885                        &Default::default(),
2886                        &NodeState::mock(),
2887                        TEST_VERSIONS.test,
2888                    )
2889                    .await,
2890                    upgrade_certificate: None,
2891                    view_change_evidence: None,
2892                    next_drb_result: None,
2893                    next_epoch_justify_qc: None,
2894                    state_cert: None,
2895                },
2896            },
2897            signature,
2898            _pd: Default::default(),
2899        };
2900
2901        // Store quorum proposals.
2902        let quorum_proposal1 = quorum_proposal.clone();
2903        storage
2904            .append_quorum_proposal2(&quorum_proposal1)
2905            .await
2906            .unwrap();
2907        quorum_proposal.data.proposal.view_number = ViewNumber::new(1);
2908        let quorum_proposal2 = quorum_proposal.clone();
2909        storage
2910            .append_quorum_proposal2(&quorum_proposal2)
2911            .await
2912            .unwrap();
2913
2914        // Change one of the file extensions. It can happen that we end up with files with the wrong
2915        // extension if, for example, the node is killed before cleaning up a swap file.
2916        fs::rename(
2917            tmp.path().join("quorum_proposals2/1.txt"),
2918            tmp.path().join("quorum_proposals2/1.swp"),
2919        )
2920        .unwrap();
2921
2922        // Loading should simply ignore the unrecognized extension.
2923        assert_eq!(
2924            storage.load_quorum_proposals().await.unwrap(),
2925            [(ViewNumber::genesis(), quorum_proposal1)]
2926                .into_iter()
2927                .collect::<BTreeMap<_, _>>()
2928        );
2929    }
2930
2931    #[test_log::test(tokio::test(flavor = "multi_thread"))]
2932    async fn test_cert2_persisted_as_bin() {
2933        let tmp = Persistence::tmp_storage().await;
2934        let storage = Persistence::connect(&tmp).await;
2935        let view = ViewNumber::new(7);
2936        let leaf = Leaf2::genesis(&Default::default(), &NodeState::mock(), MOCK_UPGRADE.base).await;
2937        let data = Vote2Data {
2938            leaf_commit: leaf.commit(),
2939            epoch: EpochNumber::new(1),
2940            block_number: leaf.height(),
2941        };
2942        let cert2 = Certificate2::new(data.clone(), data.commit(), view, None, PhantomData);
2943
2944        storage.append_cert2(view, cert2.clone()).await.unwrap();
2945
2946        assert!(tmp.path().join("decided_cert2/7.bin").is_file());
2947        assert!(!tmp.path().join("decided_cert2/7.txt").exists());
2948        assert_eq!(storage.load_cert2(view).await.unwrap(), Some(cert2));
2949    }
2950
2951    #[test_log::test(tokio::test(flavor = "multi_thread"))]
2952    async fn test_load_quorum_proposals_malformed_data() {
2953        let tmp = Persistence::tmp_storage().await;
2954        let storage = Persistence::connect(&tmp).await;
2955
2956        // Generate a valid quorum proposal.
2957        let leaf: Leaf2 = Leaf::genesis(&Default::default(), &NodeState::mock(), MOCK_UPGRADE.base)
2958            .await
2959            .into();
2960        let privkey = PubKey::generated_from_seed_indexed([0; 32], 1).1;
2961        let signature = PubKey::sign(&privkey, &[]).unwrap();
2962        let quorum_proposal = Proposal {
2963            data: QuorumProposalWrapper::<SeqTypes> {
2964                proposal: QuorumProposal2::<SeqTypes> {
2965                    epoch: None,
2966                    block_header: leaf.block_header().clone(),
2967                    view_number: ViewNumber::new(1),
2968                    justify_qc: QuorumCertificate2::genesis(
2969                        &Default::default(),
2970                        &NodeState::mock(),
2971                        TEST_VERSIONS.test,
2972                    )
2973                    .await,
2974                    upgrade_certificate: None,
2975                    view_change_evidence: None,
2976                    next_drb_result: None,
2977                    next_epoch_justify_qc: None,
2978                    state_cert: None,
2979                },
2980            },
2981            signature,
2982            _pd: Default::default(),
2983        };
2984
2985        // First store an invalid quorum proposal.
2986        fs::create_dir_all(tmp.path().join("quorum_proposals2")).unwrap();
2987        fs::write(
2988            tmp.path().join("quorum_proposals2/0.txt"),
2989            "invalid data".as_bytes(),
2990        )
2991        .unwrap();
2992
2993        // Store valid quorum proposal.
2994        storage
2995            .append_quorum_proposal2(&quorum_proposal)
2996            .await
2997            .unwrap();
2998
2999        // Loading should ignore the invalid data and return the valid proposal.
3000        assert_eq!(
3001            storage.load_quorum_proposals().await.unwrap(),
3002            [(ViewNumber::new(1), quorum_proposal)]
3003                .into_iter()
3004                .collect::<BTreeMap<_, _>>()
3005        );
3006    }
3007
3008    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3009    async fn test_store_events_empty() {
3010        let tmp = Persistence::tmp_storage().await;
3011        let mut opt = Persistence::options(&tmp);
3012        let storage = opt.create().await.unwrap();
3013
3014        assert_eq!(storage.load_events(0, 100).await.unwrap(), (None, vec![]));
3015
3016        // Storing an empty events list still updates the latest L1 block.
3017        for i in 1..=2 {
3018            tracing::info!(i, "update l1 height");
3019            storage.store_events(i, vec![]).await.unwrap();
3020            assert_eq!(
3021                storage.load_events(0, 100).await.unwrap(),
3022                (Some(EventsPersistenceRead::UntilL1Block(i)), vec![])
3023            );
3024        }
3025    }
3026
3027    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3028    async fn test_migrate_x25519_keys() {
3029        use std::collections::HashMap;
3030
3031        use alloy::primitives::{Address, U256};
3032        use indexmap::IndexMap;
3033
3034        use crate::persistence::RegisteredValidatorNoX25519;
3035
3036        let tmp = Persistence::tmp_storage().await;
3037        let mut opt = Persistence::options(&tmp);
3038        let storage = opt.create().await.unwrap();
3039
3040        let addr = Address::random();
3041        let legacy_validator = RegisteredValidatorNoX25519 {
3042            account: addr,
3043            stake_table_key: BLSPubKey::generated_from_seed_indexed([0u8; 32], 0).0,
3044            state_ver_key: hotshot_types::light_client::StateVerKey::default(),
3045            stake: U256::from(1000),
3046            commission: 100,
3047            delegators: HashMap::new(),
3048            authenticated: true,
3049        };
3050
3051        // Serialize bincode stake table in legacy format (no x25519 fields)
3052        let mut legacy_map: IndexMap<Address, RegisteredValidatorNoX25519> = IndexMap::new();
3053        legacy_map.insert(addr, legacy_validator);
3054
3055        type LegacyTuple = (
3056            IndexMap<Address, RegisteredValidatorNoX25519>,
3057            Option<RewardAmount>,
3058            Option<StakeTableHash>,
3059        );
3060        let legacy_data: LegacyTuple = (legacy_map, None, None);
3061        let bytes = bincode::serialize(&legacy_data).unwrap();
3062
3063        let inner = storage.inner.read().await;
3064        let path = inner.stake_table_dir_path();
3065        drop(inner);
3066        fs::create_dir_all(&path).unwrap();
3067        fs::write(path.join("1.txt"), &bytes).unwrap();
3068
3069        // Also create a JSON validators file without x25519 fields
3070        let json_validator = RegisteredValidatorNoX25519 {
3071            account: addr,
3072            stake_table_key: BLSPubKey::generated_from_seed_indexed([0u8; 32], 0).0,
3073            state_ver_key: hotshot_types::light_client::StateVerKey::default(),
3074            stake: U256::from(2000),
3075            commission: 200,
3076            delegators: HashMap::new(),
3077            authenticated: true,
3078        };
3079        let mut json_map: IndexMap<Address, RegisteredValidatorNoX25519> = IndexMap::new();
3080        json_map.insert(addr, json_validator);
3081        let validators_dir = path.join("validators");
3082        fs::create_dir_all(&validators_dir).unwrap();
3083        let json_path = validators_dir.join("epoch_1.json");
3084        fs::write(&json_path, serde_json::to_string_pretty(&json_map).unwrap()).unwrap();
3085
3086        // Run migration
3087        storage.migrate_x25519_keys().await.unwrap();
3088
3089        // Bincode file should now be loadable as current format
3090        let result = storage.load_stake(EpochNumber::new(1)).await.unwrap();
3091        assert!(result.is_some());
3092        let (validators, reward, hash) = result.unwrap();
3093        assert_eq!(validators.len(), 1);
3094        let v = validators.get(&addr).unwrap();
3095        assert_eq!(v.stake, U256::from(1000));
3096        assert!(v.x25519_key.is_none());
3097        assert!(v.p2p_addr.is_none());
3098        assert!(reward.is_none());
3099        assert!(hash.is_none());
3100
3101        // JSON file should now be loadable as RegisteredValidatorMap
3102        let json_content = fs::read_to_string(&json_path).unwrap();
3103        let parsed: espresso_types::RegisteredValidatorMap =
3104            serde_json::from_str(&json_content).unwrap();
3105        assert_eq!(parsed.len(), 1);
3106        let json_v = parsed.get(&addr).unwrap();
3107        assert_eq!(json_v.stake, U256::from(2000));
3108        assert!(json_v.x25519_key.is_none());
3109
3110        // Idempotent: running again is a no-op
3111        storage.migrate_x25519_keys().await.unwrap();
3112        let result2 = storage.load_stake(EpochNumber::new(1)).await;
3113        assert!(result2.is_ok());
3114    }
3115
3116    fn write_legacy_stake_file(
3117        path: &std::path::Path,
3118        epoch: u64,
3119        validator: RegisteredValidatorPreOption,
3120    ) {
3121        use indexmap::IndexMap;
3122
3123        let mut map: IndexMap<Address, RegisteredValidatorPreOption> = IndexMap::new();
3124        map.insert(validator.account, validator);
3125        type PreOptionTuple = (
3126            IndexMap<Address, RegisteredValidatorPreOption>,
3127            Option<RewardAmount>,
3128            Option<StakeTableHash>,
3129        );
3130        let data: PreOptionTuple = (map, None, None);
3131        let bytes = bincode::serialize(&data).unwrap();
3132        fs::create_dir_all(path).unwrap();
3133        fs::write(path.join(format!("{epoch}.txt")), &bytes).unwrap();
3134    }
3135
3136    fn pre_option_validator(seed: u8, stake: u64) -> RegisteredValidatorPreOption {
3137        use std::collections::HashMap;
3138
3139        use alloy::primitives::U256;
3140
3141        RegisteredValidatorPreOption {
3142            account: Address::random(),
3143            stake_table_key: BLSPubKey::generated_from_seed_indexed([seed; 32], 0).0,
3144            state_ver_key: hotshot_types::light_client::StateVerKey::default(),
3145            stake: U256::from(stake),
3146            commission: 0,
3147            delegators: HashMap::new(),
3148            authenticated: true,
3149            x25519_key: None,
3150            p2p_addr: None,
3151        }
3152    }
3153
3154    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3155    async fn test_load_stake_legacy_storage() {
3156        let tmp = Persistence::tmp_storage().await;
3157        let mut opt = Persistence::options(&tmp);
3158        let storage = opt.create().await.unwrap();
3159
3160        let v1 = pre_option_validator(1, 100);
3161        let v2 = pre_option_validator(2, 200);
3162        let v1_addr = v1.account;
3163        let v2_addr = v2.account;
3164
3165        let path = {
3166            let inner = storage.inner.read().await;
3167            inner.stake_table_dir_path()
3168        };
3169        write_legacy_stake_file(&path, 1, v1);
3170        write_legacy_stake_file(&path, 2, v2);
3171
3172        let (loaded1, ..) = storage
3173            .load_stake(EpochNumber::new(1))
3174            .await
3175            .unwrap()
3176            .unwrap();
3177        assert_eq!(loaded1.len(), 1);
3178        assert!(loaded1.get(&v1_addr).unwrap().stake_table_key.is_some());
3179
3180        let (loaded2, ..) = storage
3181            .load_stake(EpochNumber::new(2))
3182            .await
3183            .unwrap()
3184            .unwrap();
3185        assert_eq!(loaded2.len(), 1);
3186        assert!(loaded2.get(&v2_addr).unwrap().stake_table_key.is_some());
3187
3188        let latest = storage.load_latest_stake(10).await.unwrap().unwrap();
3189        assert_eq!(latest.len(), 2);
3190        let epochs: Vec<_> = latest.iter().map(|(e, ..)| *e).collect();
3191        assert!(epochs.contains(&EpochNumber::new(1)));
3192        assert!(epochs.contains(&EpochNumber::new(2)));
3193    }
3194
3195    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3196    async fn test_load_stake_mixed_storage() {
3197        use indexmap::IndexMap;
3198
3199        let tmp = Persistence::tmp_storage().await;
3200        let mut opt = Persistence::options(&tmp);
3201        let storage = opt.create().await.unwrap();
3202
3203        let legacy_v = pre_option_validator(3, 300);
3204        let legacy_addr = legacy_v.account;
3205        let path = {
3206            let inner = storage.inner.read().await;
3207            inner.stake_table_dir_path()
3208        };
3209        write_legacy_stake_file(&path, 5, legacy_v);
3210
3211        let current_v = espresso_types::v0_3::AuthenticatedValidator::mock();
3212        let current_addr = current_v.account;
3213        let mut current_map = IndexMap::new();
3214        current_map.insert(current_addr, current_v);
3215        storage
3216            .store_stake(EpochNumber::new(6), current_map, None, None)
3217            .await
3218            .unwrap();
3219
3220        let latest = storage.load_latest_stake(10).await.unwrap().unwrap();
3221        assert_eq!(latest.len(), 2);
3222        let by_epoch: std::collections::HashMap<_, _> = latest
3223            .into_iter()
3224            .map(|(e, (map, _), _)| (e, map))
3225            .collect();
3226        assert!(
3227            by_epoch
3228                .get(&EpochNumber::new(5))
3229                .unwrap()
3230                .contains_key(&legacy_addr)
3231        );
3232        assert!(
3233            by_epoch
3234                .get(&EpochNumber::new(6))
3235                .unwrap()
3236                .contains_key(&current_addr)
3237        );
3238    }
3239
3240    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3241    async fn test_migrate_x25519_keys_no_stake_dir() {
3242        let tmp = Persistence::tmp_storage().await;
3243        let mut opt = Persistence::options(&tmp);
3244        let storage = opt.create().await.unwrap();
3245
3246        // No stake_table dir exists. Migration should succeed.
3247        storage.migrate_x25519_keys().await.unwrap();
3248
3249        // Create stake_table dir with garbage data.
3250        let inner = storage.inner.read().await;
3251        let path = inner.stake_table_dir_path();
3252        drop(inner);
3253        fs::create_dir_all(&path).unwrap();
3254        fs::write(path.join("1.txt"), b"garbage").unwrap();
3255
3256        // If migration was properly marked done, this is a no-op.
3257        storage.migrate_x25519_keys().await.unwrap();
3258    }
3259
3260    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3261    async fn test_store_all_validators_authenticated_and_unauthenticated() {
3262        use std::collections::HashMap;
3263
3264        use alloy::primitives::{Address, U256};
3265        use espresso_types::v0_3::RegisteredValidator;
3266        use indexmap::IndexMap;
3267
3268        let tmp = Persistence::tmp_storage().await;
3269        let mut opt = Persistence::options(&tmp);
3270        let storage = opt.create().await.unwrap();
3271
3272        // Create an authenticated validator
3273        let authenticated_validator = RegisteredValidator {
3274            account: Address::random(),
3275            stake_table_key: Some(BLSPubKey::generated_from_seed_indexed([0u8; 32], 0).0),
3276            state_ver_key: Some(hotshot_types::light_client::StateVerKey::default()),
3277            stake: U256::from(1000),
3278            commission: 100,
3279            delegators: HashMap::new(),
3280            authenticated: true,
3281            x25519_key: None,
3282            p2p_addr: None,
3283        };
3284
3285        // Create an unauthenticated validator
3286        let unauthenticated_validator = RegisteredValidator {
3287            account: Address::random(),
3288            stake_table_key: Some(BLSPubKey::generated_from_seed_indexed([0u8; 32], 1).0),
3289            state_ver_key: Some(hotshot_types::light_client::StateVerKey::default()),
3290            stake: U256::from(2000),
3291            commission: 200,
3292            delegators: HashMap::new(),
3293            authenticated: false,
3294            x25519_key: None,
3295            p2p_addr: None,
3296        };
3297
3298        let mut validators: IndexMap<Address, RegisteredValidator<BLSPubKey>> = IndexMap::new();
3299        validators.insert(
3300            authenticated_validator.account,
3301            authenticated_validator.clone(),
3302        );
3303        validators.insert(
3304            unauthenticated_validator.account,
3305            unauthenticated_validator.clone(),
3306        );
3307
3308        // Store both validators
3309        storage
3310            .store_all_validators(EpochNumber::new(1), validators)
3311            .await
3312            .unwrap();
3313
3314        // Load and verify
3315        let loaded = storage
3316            .load_all_validators(EpochNumber::new(1), 0, 100)
3317            .await
3318            .unwrap();
3319        assert_eq!(loaded.len(), 2);
3320
3321        // Find each validator and verify authenticated state is preserved
3322        let loaded_auth = loaded
3323            .iter()
3324            .find(|v| v.account == authenticated_validator.account)
3325            .unwrap();
3326        assert!(
3327            loaded_auth.authenticated,
3328            "authenticated validator should remain authenticated"
3329        );
3330
3331        let loaded_unauth = loaded
3332            .iter()
3333            .find(|v| v.account == unauthenticated_validator.account)
3334            .unwrap();
3335        assert!(
3336            !loaded_unauth.authenticated,
3337            "unauthenticated validator should remain unauthenticated"
3338        );
3339    }
3340}