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