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