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
61fn deserialize_stake_table(bytes: &[u8]) -> anyhow::Result<(StakeTuple, bool)> {
64 if let Ok(stake) = bincode::deserialize::<StakeTuple>(bytes) {
66 return Ok((stake, false));
67 }
68
69 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 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 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#[derive(Parser, Clone, Debug)]
123pub struct Options {
124 #[clap(long, env = "ESPRESSO_NODE_STORAGE_PATH")]
126 pub(crate) path: PathBuf,
127
128 #[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#[derive(Clone, Debug)]
206pub struct Persistence {
207 inner: Arc<RwLock<Inner>>,
211 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 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 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 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 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 !pred(File::open(path)?)? {
377 return Ok(());
380 }
381 }
382
383 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 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 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 let Some(decided_view) = keep_decided_view
453 && decided_view == file_view
454 {
455 continue;
456 }
457 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 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 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 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 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 let state_cert = self.store_finalized_state_cert(v)?;
522
523 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 state: Default::default(),
541 delta: Default::default(),
542 };
543
544 leaves.insert(v, (info, cert));
545 }
546
547 if let Some((oldest_view, _)) = leaves.first_key_value() {
551 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 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 *current_height += 1;
596 *end = view;
597 } else {
598 intervals.push(*start..=*end);
600 current_interval = Some((view, view, height));
601 }
602 } else {
603 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 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 let mut file = BufReader::new(File::open(self.legacy_anchor_leaf_path())?);
681
682 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 fs::create_dir_all(&path).context("creating anchor leaf directory")?;
815
816 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 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 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 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 let now = Instant::now();
875 let intervals = self
878 .inner
879 .write()
880 .await
881 .generate_decide_events(view, deciding_qc, consumer)
882 .await?;
883
884 let processed = intervals.iter().map(|i| *i.end()).max();
886
887 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 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 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 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 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 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 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 let dir_path = inner.quorum_proposals2_dir_path();
1104 if !dir_path.is_dir() {
1105 return Ok(Default::default());
1106 }
1107
1108 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 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 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 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 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 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 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 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 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 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 if serde_json::from_str::<RegisteredValidatorMap>(&content).is_ok() {
1672 continue;
1673 }
1674
1675 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 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 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 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 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 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 }
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 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 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 let last_l1_finalized_path = events_dir.join("last_l1_finalized").with_extension("bin");
2106
2107 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 for (event_key, event) in events {
2130 let (block_number, event_index) = event_key;
2131 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 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 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 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 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 fs::create_dir_all(&validators_dir)
2306 .with_context(|| format!("Failed to create validators dir: {validators_dir:?}"))?;
2307
2308 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 async fn save(&self, records: Vec<SerializableRecord>) -> anyhow::Result<()> {
2374 let to_save =
2376 bincode::serialize(&records).with_context(|| "failed to serialize records")?;
2377
2378 let path = self.inner.read().await.libp2p_dht_path();
2380
2381 fs::create_dir_all(path.parent().with_context(|| "directory had no parent")?)
2383 .with_context(|| "failed to create directory")?;
2384
2385 let mut inner = self.inner.write().await;
2387
2388 inner
2390 .replace(
2391 &path,
2392 |_| {
2393 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 async fn load(&self) -> anyhow::Result<Vec<SerializableRecord>> {
2413 let contents = std::fs::read(self.inner.read().await.libp2p_dht_path())
2415 .with_context(|| "Failed to read records from file")?;
2416
2417 let records: Vec<SerializableRecord> =
2419 bincode::deserialize(&contents).with_context(|| "Failed to deserialize records")?;
2420
2421 Ok(records)
2422 }
2423}
2424
2425fn 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 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
2452fn 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![], 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 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 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 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 fs::rename(
2917 tmp.path().join("quorum_proposals2/1.txt"),
2918 tmp.path().join("quorum_proposals2/1.swp"),
2919 )
2920 .unwrap();
2921
2922 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 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 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 storage
2995 .append_quorum_proposal2(&quorum_proposal)
2996 .await
2997 .unwrap();
2998
2999 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 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 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 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 storage.migrate_x25519_keys().await.unwrap();
3088
3089 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 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 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(¤t_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 storage.migrate_x25519_keys().await.unwrap();
3248
3249 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 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 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 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 storage
3310 .store_all_validators(EpochNumber::new(1), validators)
3311 .await
3312 .unwrap();
3313
3314 let loaded = storage
3316 .load_all_validators(EpochNumber::new(1), 0, 100)
3317 .await
3318 .unwrap();
3319 assert_eq!(loaded.len(), 2);
3320
3321 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}