1use std::{
2 collections::BTreeMap,
3 future::Future,
4 path::PathBuf,
5 str::FromStr,
6 sync::Arc,
7 time::{Duration, Instant},
8};
9
10use alloy::primitives::Address;
11use anyhow::{Context, bail};
12use async_trait::async_trait;
13use clap::Parser;
14use committable::Committable;
15use derivative::Derivative;
16use derive_more::derive::{From, Into};
17use espresso_types::{
18 AuthenticatedValidatorMap, BackoffParams, BlockMerkleTree, FeeMerkleTree, Leaf, Leaf2,
19 NetworkConfig, Payload, PubKey, Ratio, RegisteredValidatorMap, StakeTableHash, parse_duration,
20 parse_size,
21 traits::{EventsPersistenceRead, MembershipPersistence, StakeTuple},
22 v0::traits::{EventConsumer, PersistenceOptions, SequencerPersistence, StateCatchup},
23 v0_3::{
24 AuthenticatedValidator, EventKey, IndexedStake, RegisteredValidator, RewardAmount,
25 StakeTableEvent,
26 },
27};
28use futures::stream::StreamExt;
29use hotshot::InitializerEpochInfo;
30use hotshot_libp2p_networking::network::behaviours::dht::store::persistent::{
31 DhtPersistentStorage, SerializableRecord,
32};
33use hotshot_new_protocol::message::Certificate2;
34use hotshot_query_service::{
35 data_source::{
36 Transaction as _, VersionedDataSource,
37 storage::{
38 SerializableRetry,
39 pruning::PrunerCfg,
40 sql::{
41 Config, Db, Read, SerializableRetryConfig, SqlStorage, StorageConnectionType,
42 Transaction, Write, include_migrations, query_as, syntax_helpers::MAX_FN,
43 },
44 },
45 },
46 fetching::{
47 Provider,
48 request::{PayloadRequest, VidCommonRequest},
49 },
50 merklized_state::MerklizedState,
51 serializable_retry,
52};
53use hotshot_types::{
54 data::{
55 DaProposal, DaProposal2, EpochNumber, QuorumProposal, QuorumProposalWrapper,
56 QuorumProposalWrapperLegacy, VidCommitment, VidCommon, VidDisperseShare, VidDisperseShare0,
57 },
58 drb::{DrbInput, DrbResult},
59 event::{Event, EventType, HotShotAction, LeafInfo},
60 message::{Proposal, convert_proposal},
61 new_protocol::CoordinatorEvent,
62 simple_certificate::{
63 CertificatePair, LightClientStateUpdateCertificateV1, LightClientStateUpdateCertificateV2,
64 NextEpochQuorumCertificate2, QuorumCertificate, QuorumCertificate2, UpgradeCertificate,
65 },
66 traits::{
67 block_contents::{BlockHeader, BlockPayload},
68 metrics::Metrics,
69 },
70 vote::HasViewNumber,
71};
72use indexmap::IndexMap;
73use itertools::Itertools;
74use sqlx::{Executor, QueryBuilder, Row, query};
75
76use crate::{
77 NodeType, RECENT_STAKE_TABLES_LIMIT, SeqTypes, ViewNumber,
78 catchup::SqlStateCatchup,
79 persistence::{migrate_network_config, persistence_metrics::PersistenceMetricsValue},
80};
81
82#[derive(Parser, Clone, Derivative)]
84#[derivative(Debug)]
85pub struct PostgresOptions {
86 #[clap(long, env = "ESPRESSO_NODE_POSTGRES_HOST")]
88 pub(crate) host: Option<String>,
89
90 #[clap(long, env = "ESPRESSO_NODE_POSTGRES_PORT")]
92 pub(crate) port: Option<u16>,
93
94 #[clap(long, env = "ESPRESSO_NODE_POSTGRES_DATABASE")]
96 pub(crate) database: Option<String>,
97
98 #[clap(long, env = "ESPRESSO_NODE_POSTGRES_USER")]
100 pub(crate) user: Option<String>,
101
102 #[clap(long, env = "ESPRESSO_NODE_POSTGRES_PASSWORD")]
104 #[derivative(Debug = "ignore")]
106 pub(crate) password: Option<String>,
107
108 #[clap(long, env = "ESPRESSO_NODE_POSTGRES_USE_TLS")]
110 pub(crate) use_tls: bool,
111
112 #[clap(
119 long,
120 env = "ESPRESSO_NODE_POSTGRES_NO_DEFERRABLE",
121 default_value_t = true
122 )]
123 pub(crate) no_deferrable: bool,
124}
125
126impl Default for PostgresOptions {
127 fn default() -> Self {
128 Self::parse_from(std::iter::empty::<String>())
129 }
130}
131
132#[derive(Parser, Clone, Derivative, Default, From, Into)]
133#[derivative(Debug)]
134pub struct SqliteOptions {
135 #[clap(
138 long,
139 env = "ESPRESSO_NODE_STORAGE_PATH",
140 value_parser = build_sqlite_path
141 )]
142 pub(crate) path: PathBuf,
143}
144
145pub fn build_sqlite_path(path: &str) -> anyhow::Result<PathBuf> {
146 let sub_dir = PathBuf::from_str(path)?.join("sqlite");
147
148 if !sub_dir.exists() {
150 std::fs::create_dir_all(&sub_dir)
151 .with_context(|| format!("failed to create directory: {sub_dir:?}"))?;
152 }
153
154 Ok(sub_dir.join("database"))
155}
156
157#[derive(Parser, Clone, Derivative, From, Into)]
159#[derivative(Debug)]
160pub struct Options {
161 #[cfg(not(feature = "embedded-db"))]
162 #[clap(flatten)]
163 pub(crate) postgres_options: PostgresOptions,
164
165 #[cfg(feature = "embedded-db")]
166 #[clap(flatten)]
167 pub(crate) sqlite_options: SqliteOptions,
168
169 #[derivative(Debug = "ignore")]
182 pub(crate) uri: Option<String>,
183
184 #[clap(long, env = "ESPRESSO_NODE_DATABASE_PRUNE")]
193 pub(crate) prune: bool,
194
195 #[clap(flatten)]
197 pub(crate) pruning: PruningOptions,
198
199 #[clap(flatten)]
201 pub(crate) consensus_pruning: ConsensusPruningOptions,
202
203 #[clap(flatten)]
205 pub(crate) serializable_retry: SerializableRetryOptions,
206
207 #[clap(long, env = "ESPRESSO_NODE_FETCH_RATE_LIMIT")]
209 pub(crate) fetch_rate_limit: Option<usize>,
210
211 #[clap(long, env = "ESPRESSO_NODE_ACTIVE_FETCH_DELAY", value_parser = parse_duration)]
213 pub(crate) active_fetch_delay: Option<Duration>,
214
215 #[clap(long, env = "ESPRESSO_NODE_CHUNK_FETCH_DELAY", value_parser = parse_duration)]
217 pub(crate) chunk_fetch_delay: Option<Duration>,
218
219 #[clap(long, env = "ESPRESSO_NODE_SYNC_STATUS_CHUNK_SIZE")]
222 pub(crate) sync_status_chunk_size: Option<usize>,
223
224 #[clap(long, env = "ESPRESSO_NODE_SYNC_STATUS_TTL", value_parser = parse_duration)]
226 pub(crate) sync_status_ttl: Option<Duration>,
227
228 #[clap(long, env = "ESPRESSO_NODE_PROACTIVE_SCAN_CHUNK_SIZE")]
230 pub(crate) proactive_scan_chunk_size: Option<usize>,
231
232 #[clap(long, env = "ESPRESSO_NODE_PROACTIVE_SCAN_INTERVAL", value_parser = parse_duration)]
234 pub(crate) proactive_scan_interval: Option<Duration>,
235
236 #[clap(long, env = "ESPRESSO_NODE_DISABLE_PROACTIVE_FETCHING")]
238 pub(crate) disable_proactive_fetching: bool,
239
240 #[clap(long, env = "ESPRESSO_NODE_ARCHIVE", conflicts_with = "prune")]
247 pub(crate) archive: bool,
248
249 #[clap(
251 long,
252 env = "ESPRESSO_NODE_LIGHTWEIGHT",
253 default_value_t = false,
254 conflicts_with = "archive"
255 )]
256 pub(crate) lightweight: bool,
257
258 #[clap(long, env = "ESPRESSO_NODE_DATABASE_IDLE_CONNECTION_TIMEOUT", value_parser = parse_duration, default_value = "10m")]
263 pub(crate) idle_connection_timeout: Duration,
264
265 #[clap(long, env = "ESPRESSO_NODE_DATABASE_CONNECTION_TIMEOUT", value_parser = parse_duration, default_value = "30m")]
272 pub(crate) connection_timeout: Duration,
273
274 #[clap(long, env = "ESPRESSO_NODE_DATABASE_SLOW_STATEMENT_THRESHOLD", value_parser = parse_duration, default_value = "1s")]
275 pub(crate) slow_statement_threshold: Duration,
276
277 #[clap(long, env = "ESPRESSO_NODE_DATABASE_STATEMENT_TIMEOUT", value_parser = parse_duration, default_value = "10m")]
282 pub(crate) statement_timeout: Duration,
283
284 #[clap(
290 long,
291 env = "ESPRESSO_NODE_DATABASE_MIN_CONNECTIONS",
292 default_value = "0"
293 )]
294 pub(crate) min_connections: u32,
295
296 #[cfg(not(feature = "embedded-db"))]
299 #[clap(long, env = "ESPRESSO_NODE_DATABASE_QUERY_MIN_CONNECTIONS", default_value = None)]
300 pub(crate) query_min_connections: Option<u32>,
301
302 #[clap(
307 long,
308 env = "ESPRESSO_NODE_DATABASE_MAX_CONNECTIONS",
309 default_value = "25"
310 )]
311 pub(crate) max_connections: u32,
312
313 #[cfg(not(feature = "embedded-db"))]
316 #[clap(long, env = "ESPRESSO_NODE_DATABASE_QUERY_MAX_CONNECTIONS", default_value = None)]
317 pub(crate) query_max_connections: Option<u32>,
318
319 #[clap(skip)]
327 pub(crate) pool: Option<sqlx::Pool<Db>>,
328}
329
330impl Default for Options {
331 fn default() -> Self {
332 Self::parse_from(std::iter::empty::<String>())
333 }
334}
335
336#[cfg(not(feature = "embedded-db"))]
337impl From<PostgresOptions> for Config {
338 fn from(opt: PostgresOptions) -> Self {
339 let mut cfg = Config::default();
340
341 if let Some(host) = opt.host {
342 cfg = cfg.host(host);
343 }
344
345 if let Some(port) = opt.port {
346 cfg = cfg.port(port);
347 }
348
349 if let Some(database) = &opt.database {
350 cfg = cfg.database(database);
351 }
352
353 if let Some(user) = &opt.user {
354 cfg = cfg.user(user);
355 }
356
357 if let Some(password) = &opt.password {
358 cfg = cfg.password(password);
359 }
360
361 if opt.use_tls {
362 cfg = cfg.tls();
363 }
364
365 cfg = cfg.max_connections(20);
366 cfg = cfg.idle_connection_timeout(Duration::from_secs(120));
367 cfg = cfg.connection_timeout(Duration::from_secs(10240));
368 cfg = cfg.slow_statement_threshold(Duration::from_secs(1));
369 cfg = cfg.statement_timeout(Duration::from_secs(600)); hotshot_query_service::data_source::storage::sql::set_no_deferrable_on_read(
372 opt.no_deferrable,
373 );
374
375 cfg
376 }
377}
378
379#[cfg(feature = "embedded-db")]
380impl From<SqliteOptions> for Config {
381 fn from(opt: SqliteOptions) -> Self {
382 let mut cfg = Config::default();
383
384 cfg = cfg.db_path(opt.path);
385
386 cfg = cfg.max_connections(20);
387 cfg = cfg.idle_connection_timeout(Duration::from_secs(120));
388 cfg = cfg.connection_timeout(Duration::from_secs(10240));
389 cfg = cfg.slow_statement_threshold(Duration::from_secs(2));
390 cfg = cfg.statement_timeout(Duration::from_secs(600));
391 cfg
392 }
393}
394
395#[cfg(not(feature = "embedded-db"))]
396impl From<PostgresOptions> for Options {
397 fn from(opt: PostgresOptions) -> Self {
398 Options {
399 postgres_options: opt,
400 max_connections: 20,
401 idle_connection_timeout: Duration::from_secs(120),
402 connection_timeout: Duration::from_secs(10240),
403 slow_statement_threshold: Duration::from_secs(1),
404 statement_timeout: Duration::from_secs(600),
405 ..Default::default()
406 }
407 }
408}
409
410#[cfg(feature = "embedded-db")]
411impl From<SqliteOptions> for Options {
412 fn from(opt: SqliteOptions) -> Self {
413 Options {
414 sqlite_options: opt,
415 max_connections: 5,
416 idle_connection_timeout: Duration::from_secs(120),
417 connection_timeout: Duration::from_secs(10240),
418 slow_statement_threshold: Duration::from_secs(1),
419 uri: None,
420 statement_timeout: Duration::from_secs(600),
421 prune: false,
422 pruning: Default::default(),
423 consensus_pruning: Default::default(),
424 fetch_rate_limit: None,
425 active_fetch_delay: None,
426 chunk_fetch_delay: None,
427 sync_status_chunk_size: None,
428 sync_status_ttl: None,
429 proactive_scan_chunk_size: None,
430 proactive_scan_interval: None,
431 disable_proactive_fetching: false,
432 archive: false,
433 lightweight: false,
434 min_connections: 0,
435 pool: None,
436 serializable_retry: SerializableRetryOptions::default(),
437 }
438 }
439}
440impl TryFrom<&Options> for Config {
441 type Error = anyhow::Error;
442
443 fn try_from(opt: &Options) -> Result<Self, Self::Error> {
444 let mut cfg = match &opt.uri {
445 Some(uri) => uri.parse()?,
446 None => Self::default(),
447 };
448
449 if let Some(pool) = &opt.pool {
450 cfg = cfg.pool(pool.clone());
451 }
452
453 cfg = cfg.max_connections(opt.max_connections);
454 cfg = cfg.idle_connection_timeout(opt.idle_connection_timeout);
455 cfg = cfg.min_connections(opt.min_connections);
456
457 #[cfg(not(feature = "embedded-db"))]
458 {
459 cfg =
460 cfg.query_max_connections(opt.query_max_connections.unwrap_or(opt.max_connections));
461 cfg =
462 cfg.query_min_connections(opt.query_min_connections.unwrap_or(opt.min_connections));
463
464 hotshot_query_service::data_source::storage::sql::set_no_deferrable_on_read(
465 opt.postgres_options.no_deferrable,
466 );
467 }
468
469 cfg = cfg.connection_timeout(opt.connection_timeout);
470 cfg = cfg.slow_statement_threshold(opt.slow_statement_threshold);
471 cfg = cfg.statement_timeout(opt.statement_timeout);
472
473 #[cfg(not(feature = "embedded-db"))]
474 {
475 cfg = cfg.migrations(include_migrations!(
476 "$CARGO_MANIFEST_DIR/api/migrations/postgres"
477 ));
478
479 let pg_options = &opt.postgres_options;
480
481 if let Some(host) = &pg_options.host {
482 cfg = cfg.host(host.clone());
483 }
484
485 if let Some(port) = pg_options.port {
486 cfg = cfg.port(port);
487 }
488
489 if let Some(database) = &pg_options.database {
490 cfg = cfg.database(database);
491 }
492
493 if let Some(user) = &pg_options.user {
494 cfg = cfg.user(user);
495 }
496
497 if let Some(password) = &pg_options.password {
498 cfg = cfg.password(password);
499 }
500
501 if pg_options.use_tls {
502 cfg = cfg.tls();
503 }
504 }
505
506 #[cfg(feature = "embedded-db")]
507 {
508 cfg = cfg.migrations(include_migrations!(
509 "$CARGO_MANIFEST_DIR/api/migrations/sqlite"
510 ));
511
512 cfg = cfg.db_path(opt.sqlite_options.path.clone());
513 }
514
515 if opt.prune {
516 cfg = cfg.pruner_cfg(PrunerCfg::from(opt.pruning))?;
517 }
518 if opt.archive {
519 cfg = cfg.archive();
520 }
521
522 cfg = cfg.serializable_retry(opt.serializable_retry.to_retry_config());
523
524 Ok(cfg)
525 }
526}
527
528#[derive(Parser, Clone, Copy, Debug)]
530pub struct PruningOptions {
531 #[clap(long, env = "ESPRESSO_NODE_PRUNER_PRUNING_THRESHOLD", value_parser = parse_size)]
535 pub(crate) pruning_threshold: Option<u64>,
536
537 #[clap(
540 long,
541 env = "ESPRESSO_NODE_PRUNER_MINIMUM_RETENTION",
542 value_parser = parse_duration,
543 )]
544 pub(crate) minimum_retention: Option<Duration>,
545
546 #[clap(
549 long,
550 env = "ESPRESSO_NODE_PRUNER_STATE_MINIMUM_RETENTION",
551 value_parser = parse_duration,
552 )]
553 state_minimum_retention: Option<Duration>,
554
555 #[clap(
558 long,
559 env = "ESPRESSO_NODE_PRUNER_TARGET_RETENTION",
560 value_parser = parse_duration,
561 )]
562 pub(crate) target_retention: Option<Duration>,
563
564 #[clap(
567 long,
568 env = "ESPRESSO_NODE_PRUNER_STATE_TARGET_RETENTION",
569 value_parser = parse_duration,
570 )]
571 state_target_retention: Option<Duration>,
572
573 #[clap(long, env = "ESPRESSO_NODE_PRUNER_BATCH_SIZE")]
576 pub(crate) batch_size: Option<u64>,
577
578 #[clap(long, env = "ESPRESSO_NODE_PRUNER_MAX_USAGE")]
584 pub(crate) max_usage: Option<u16>,
585
586 #[clap(
588 long,
589 env = "ESPRESSO_NODE_PRUNER_INTERVAL",
590 value_parser = parse_duration,
591 )]
592 pub(crate) interval: Option<Duration>,
593
594 #[clap(long, env = "ESPRESSO_NODE_PRUNER_INCREMENTAL_VACUUM_PAGES")]
598 pub(crate) pages: Option<u64>,
599}
600
601impl Default for PruningOptions {
602 fn default() -> Self {
603 Self::parse_from(std::iter::empty::<String>())
604 }
605}
606
607impl From<PruningOptions> for PrunerCfg {
608 fn from(opt: PruningOptions) -> Self {
609 let mut cfg = PrunerCfg::new();
610 if let Some(threshold) = opt.pruning_threshold {
611 cfg = cfg.with_pruning_threshold(threshold);
612 }
613 if let Some(min) = opt.minimum_retention {
614 cfg = cfg.with_minimum_retention(min);
615 }
616 if let Some(min) = opt.state_minimum_retention {
617 cfg = cfg.with_state_minimum_retention(min);
618 }
619 if let Some(target) = opt.target_retention {
620 cfg = cfg.with_target_retention(target);
621 }
622 if let Some(target) = opt.state_target_retention {
623 cfg = cfg.with_state_target_retention(target);
624 }
625 if let Some(batch) = opt.batch_size {
626 cfg = cfg.with_batch_size(batch);
627 }
628 if let Some(max) = opt.max_usage {
629 cfg = cfg.with_max_usage(max);
630 }
631 if let Some(interval) = opt.interval {
632 cfg = cfg.with_interval(interval);
633 }
634
635 if let Some(pages) = opt.pages {
636 cfg = cfg.with_incremental_vacuum_pages(pages)
637 }
638
639 cfg = cfg.with_state_tables(vec![
640 BlockMerkleTree::state_type().to_string(),
641 FeeMerkleTree::state_type().to_string(),
642 ]);
643
644 cfg
645 }
646}
647
648#[derive(Parser, Clone, Copy, Debug)]
650pub struct ConsensusPruningOptions {
651 #[clap(
667 name = "TARGET_RETENTION",
668 long = "consensus-storage-target-retention",
669 env = "ESPRESSO_NODE_CONSENSUS_STORAGE_TARGET_RETENTION",
670 default_value = "302000"
671 )]
672 pub(crate) target_retention: u64,
673
674 #[clap(
685 name = "MINIMUM_RETENTION",
686 long = "consensus-storage-minimum-retention",
687 env = "ESPRESSO_NODE_CONSENSUS_STORAGE_MINIMUM_RETENTION",
688 default_value = "130000"
689 )]
690 pub(crate) minimum_retention: u64,
691
692 #[clap(
697 name = "TARGET_USAGE",
698 long = "consensus-storage-target-usage",
699 env = "ESPRESSO_NODE_CONSENSUS_STORAGE_TARGET_USAGE",
700 default_value = "1000000000"
701 )]
702 pub(crate) target_usage: u64,
703}
704
705impl Default for ConsensusPruningOptions {
706 fn default() -> Self {
707 Self::parse_from(std::iter::empty::<String>())
708 }
709}
710
711#[derive(Parser, Clone, Copy, Debug)]
714pub struct SerializableRetryOptions {
715 #[clap(
717 long = "serializable-retry-max",
718 env = "ESPRESSO_NODE_SERIALIZABLE_RETRY_MAX",
719 default_value = "100"
720 )]
721 retry_max: u32,
722
723 #[clap(
725 long = "serializable-backoff-base",
726 env = "ESPRESSO_NODE_SERIALIZABLE_BACKOFF_BASE",
727 default_value = "10ms",
728 value_parser = parse_duration
729 )]
730 backoff_base: Duration,
731
732 #[clap(
734 long = "serializable-backoff-max",
735 env = "ESPRESSO_NODE_SERIALIZABLE_BACKOFF_MAX",
736 default_value = "500ms",
737 value_parser = parse_duration
738 )]
739 backoff_max: Duration,
740
741 #[clap(
743 long = "serializable-backoff-factor",
744 env = "ESPRESSO_NODE_SERIALIZABLE_BACKOFF_FACTOR",
745 default_value = "2"
746 )]
747 backoff_factor: u32,
748
749 #[clap(
751 long = "serializable-backoff-jitter",
752 env = "ESPRESSO_NODE_SERIALIZABLE_BACKOFF_JITTER",
753 default_value = "5:10"
754 )]
755 backoff_jitter: Ratio,
756
757 #[clap(
761 long = "serializable-pg-stat-diag",
762 env = "ESPRESSO_NODE_SERIALIZABLE_PG_STAT_DIAG"
763 )]
764 pg_stat_diag: bool,
765}
766
767impl Default for SerializableRetryOptions {
768 fn default() -> Self {
769 Self::parse_from(std::iter::empty::<String>())
770 }
771}
772
773impl SerializableRetryOptions {
774 fn to_retry_config(self) -> SerializableRetryConfig {
776 SerializableRetryConfig::new(
777 self.backoff_base,
778 self.backoff_max,
779 self.backoff_factor,
780 self.backoff_jitter.into(),
781 self.retry_max,
782 self.pg_stat_diag,
783 )
784 }
785}
786
787#[async_trait]
788impl PersistenceOptions for Options {
789 type Persistence = Persistence;
790
791 fn set_view_retention(&mut self, view_retention: u64) {
792 self.consensus_pruning.target_retention = view_retention;
793 self.consensus_pruning.minimum_retention = view_retention;
794 }
795
796 async fn create(&mut self) -> anyhow::Result<Self::Persistence> {
797 let config = (&*self).try_into()?;
798 let persistence = Persistence {
799 db: SqlStorage::connect(config, StorageConnectionType::Sequencer).await?,
800 gc_opt: self.consensus_pruning,
801 internal_metrics: PersistenceMetricsValue::default(),
802 };
803 persistence.migrate_quorum_proposal_leaf_hashes().await?;
804 self.pool = Some(persistence.db.pool());
805
806 #[cfg(not(feature = "embedded-db"))]
807 {
808 let registry = super::migrations::hash_bigint_migrations();
809 let db = persistence.db.clone();
810 tokio::spawn(registry.run_all_migrations(db));
811 }
812
813 Ok(persistence)
814 }
815
816 async fn reset(self) -> anyhow::Result<()> {
817 SqlStorage::connect(
818 Config::try_from(&self)?.reset_schema(),
819 StorageConnectionType::Sequencer,
820 )
821 .await?;
822 Ok(())
823 }
824}
825
826#[derive(Debug, Clone, Copy)]
827pub enum DataMigration {
828 X25519Keys,
829}
830
831impl DataMigration {
832 pub fn as_str(&self) -> &'static str {
833 match self {
834 Self::X25519Keys => "x25519_keys",
835 }
836 }
837}
838
839#[derive(Clone, Debug)]
841pub struct Persistence {
842 db: SqlStorage,
843 gc_opt: ConsensusPruningOptions,
844 internal_metrics: PersistenceMetricsValue,
846}
847
848const PG_SERIALIZATION_FAILURE_CODE: &str = "40001";
851
852pub(crate) const DECIDE_GAP_FILL_HORIZON: u64 = 20;
856
857pub(crate) fn within_gap_fill_horizon(view: u64, watermark: u64) -> bool {
861 view.saturating_sub(1) + DECIDE_GAP_FILL_HORIZON > watermark
862}
863
864#[derive(Debug)]
865struct DecidedLeaf {
866 info: LeafInfo<SeqTypes>,
867 cert: CertificatePair<SeqTypes>,
868}
869
870fn decide_events_from_chain(
871 mut chain: Vec<DecidedLeaf>,
872 cert2: Option<Certificate2<SeqTypes>>,
873 deciding_qc: Option<Arc<CertificatePair<SeqTypes>>>,
874) -> Vec<CoordinatorEvent<SeqTypes>> {
875 let split_idx = chain
876 .iter()
877 .position(|leaf| leaf.info.leaf.block_header().version() < versions::NEW_PROTOCOL_VERSION)
878 .unwrap_or(chain.len());
879 let legacy_leaves = chain.split_off(split_idx);
880 let new_leaves = chain;
881
882 let mut events = Vec::with_capacity(2);
883 if !legacy_leaves.is_empty() {
884 let committing_qc = legacy_leaves[0].cert.clone();
885 let deciding_qc = new_leaves
886 .is_empty()
887 .then_some(deciding_qc)
888 .flatten()
889 .filter(|qc| qc.view_number() == committing_qc.view_number() + 1);
890 let view_number = legacy_leaves[0].info.leaf.view_number();
891 let leaf_chain = legacy_leaves
892 .into_iter()
893 .map(|leaf| leaf.info)
894 .collect::<Vec<_>>();
895
896 events.push(CoordinatorEvent::LegacyEvent(Event {
897 view_number,
898 event: EventType::Decide {
899 leaf_chain: Arc::new(leaf_chain),
900 committing_qc: Arc::new(committing_qc),
901 deciding_qc,
902 block_size: None,
903 },
904 }));
905 }
906
907 if new_leaves.is_empty() && cert2.is_some() {
908 tracing::warn!(
909 "decide_events_from_chain called with cert2 but no new-protocol leaves; cert2 will be \
910 dropped"
911 );
912 }
913
914 if !new_leaves.is_empty() {
915 let cert1 = new_leaves[0].cert.qc().clone();
921 let leaf_infos = new_leaves.into_iter().map(|leaf| leaf.info).collect();
922
923 events.push(CoordinatorEvent::NewDecide {
924 leaf_infos,
925 cert1,
926 cert2,
927 });
928 }
929
930 events
931}
932
933impl Persistence {
934 async fn serializable_retry<F, Fut, T>(&self, op: &'static str, f: F) -> anyhow::Result<T>
936 where
937 T: Send,
938 F: Fn() -> Fut + Send + Sync,
939 Fut: Future<Output = anyhow::Result<T>> + Send,
940 {
941 self.db.serializable_retry(op, f).await
942 }
943
944 async fn migrate_quorum_proposal_leaf_hashes(&self) -> anyhow::Result<()> {
951 serializable_retry!(self, || async {
952 let mut tx = self.db.write().await?;
953
954 let mut proposals = tx.fetch("SELECT * FROM quorum_proposals");
955
956 let mut updates = vec![];
957 while let Some(row) = proposals.next().await {
958 let row = row?;
959
960 let hash: Option<String> = row.try_get("leaf_hash")?;
961 if hash.is_none() {
962 let view: i64 = row.try_get("view")?;
963 let data: Vec<u8> = row.try_get("data")?;
964 let proposal: Proposal<SeqTypes, QuorumProposal<SeqTypes>> =
965 bincode::deserialize(&data)?;
966 let leaf = Leaf::from_quorum_proposal(&proposal.data);
967 let leaf_hash = Committable::commit(&leaf);
968 tracing::info!(view, %leaf_hash, "populating quorum proposal leaf hash");
969 updates.push((view, leaf_hash.to_string()));
970 }
971 }
972 drop(proposals);
973
974 tx.upsert("quorum_proposals", ["view", "leaf_hash"], ["view"], updates)
975 .await?;
976
977 tx.commit().await
978 })
979 .await
980 }
981
982 async fn is_migration_complete(&self, name: &str, table_name: &str) -> anyhow::Result<bool> {
983 serializable_retry!(self, || async {
984 let mut tx = self.db.read().await?;
985 let (completed,): (bool,) = query_as(
986 "SELECT completed FROM data_migrations WHERE name = $1 AND table_name = $2",
987 )
988 .bind(name)
989 .bind(table_name)
990 .fetch_one(tx.as_mut())
991 .await
992 .context("migration tracking row missing - schema may be out of sync")?;
993 Ok(completed)
994 })
995 .await
996 }
997
998 async fn mark_migration_complete(
999 tx: &mut Transaction<Write>,
1000 name: &str,
1001 table_name: &str,
1002 migrated_rows: usize,
1003 ) -> anyhow::Result<()> {
1004 tx.execute(
1005 query(
1006 "UPDATE data_migrations SET completed = true, migrated_rows = $1 WHERE name = $2 \
1007 AND table_name = $3",
1008 )
1009 .bind(migrated_rows as i64)
1010 .bind(name)
1011 .bind(table_name),
1012 )
1013 .await?;
1014 Ok(())
1015 }
1016
1017 async fn load_processed_view(&self) -> anyhow::Result<Option<ViewNumber>> {
1019 Ok(self
1020 .db
1021 .read()
1022 .await?
1023 .fetch_optional("SELECT last_processed_view FROM event_stream WHERE id = 1 LIMIT 1")
1024 .await?
1025 .map(|row| ViewNumber::new(row.get::<i64, _>("last_processed_view") as u64)))
1026 }
1027
1028 async fn generate_decide_events(
1029 &self,
1030 deciding_qc: Option<Arc<CertificatePair<SeqTypes>>>,
1031 consumer: &impl EventConsumer,
1032 ) -> anyhow::Result<()> {
1033 let mut last_processed_view: Option<i64> = serializable_retry!(self, || async {
1034 Ok(self
1035 .db
1036 .read()
1037 .await?
1038 .fetch_optional("SELECT last_processed_view FROM event_stream WHERE id = 1 LIMIT 1")
1039 .await?
1040 .map(|row| row.get("last_processed_view")))
1041 })
1042 .await?;
1043 let mut last_processed_height: Option<u64> = match last_processed_view {
1046 Some(view) => {
1047 serializable_retry!(self, || async {
1048 self.db
1049 .read()
1050 .await?
1051 .fetch_optional(
1052 query("SELECT leaf FROM anchor_leaf2 WHERE view = $1").bind(view),
1053 )
1054 .await?
1055 .map(|row| -> anyhow::Result<u64> {
1056 let leaf_data: Vec<u8> = row.get("leaf");
1057 let leaf = bincode::deserialize::<Leaf2>(&leaf_data)?;
1058 Ok(leaf.block_header().block_number())
1059 })
1060 .transpose()
1061 })
1062 .await?
1063 },
1064 None => None,
1065 };
1066 loop {
1067 let Some((
1078 from_view,
1079 to_view,
1080 leaves,
1081 final_qc,
1082 mut vid_shares,
1083 mut da_proposals,
1084 state_certs,
1085 cert2,
1086 )) = serializable_retry!(self, || async {
1087 let mut tx = self.db.read().await?;
1088
1089 let from_view = match last_processed_view {
1095 Some(v) => v + 1,
1096 None => 0,
1097 };
1098 tracing::debug!(?from_view, "generate decide event");
1099
1100 let (watermark,): (Option<i64>,) = query_as("SELECT max(view) FROM anchor_leaf2")
1102 .fetch_one(tx.as_mut())
1103 .await?;
1104
1105 let mut parent = last_processed_height;
1106 let mut rows = query(
1107 "SELECT leaf, qc, next_epoch_qc FROM anchor_leaf2 WHERE view >= $1 ORDER BY \
1108 view",
1109 )
1110 .bind(from_view)
1111 .fetch(tx.as_mut());
1112 let mut leaves: Vec<(Leaf2, CertificatePair<SeqTypes>)> = vec![];
1113 let mut final_qc = None;
1114 while let Some(row) = rows.next().await {
1115 let row = match row {
1116 Ok(row) => row,
1117 Err(err) => {
1118 if err.as_database_error().is_some_and(|e| {
1119 e.code().as_deref() == Some(PG_SERIALIZATION_FAILURE_CODE)
1120 }) {
1121 drop(rows);
1122 return Err(anyhow::Error::from(err));
1123 }
1124 tracing::warn!("error loading row: {err:#}");
1127 break;
1128 },
1129 };
1130
1131 let leaf_data: Vec<u8> = row.get("leaf");
1132 let leaf = bincode::deserialize::<Leaf2>(&leaf_data)?;
1133 let qc_data: Vec<u8> = row.get("qc");
1134 let qc = bincode::deserialize::<QuorumCertificate2<SeqTypes>>(&qc_data)?;
1135 let next_epoch_qc = match row.get::<Option<Vec<u8>>, _>("next_epoch_qc") {
1136 Some(bytes) => Some(bincode::deserialize::<
1137 NextEpochQuorumCertificate2<SeqTypes>,
1138 >(&bytes)?),
1139 None => None,
1140 };
1141 let height = leaf.block_header().block_number();
1142
1143 if let Some(parent) = parent
1147 && height != parent + 1
1148 {
1149 if !leaves.is_empty() {
1150 tracing::debug!(
1151 height,
1152 parent,
1153 "ending decide event at non-consecutive leaf"
1154 );
1155 break;
1156 }
1157 let row_view = leaf.view_number().u64();
1163 if height > parent + 1
1164 && leaf.block_header().version() >= versions::NEW_PROTOCOL_VERSION
1165 && watermark
1166 .is_some_and(|w| within_gap_fill_horizon(row_view, w as u64))
1167 {
1168 tracing::info!(
1169 height,
1170 parent,
1171 row_view,
1172 ?watermark,
1173 "waiting for gap-fill decide before advancing the decide cursor"
1174 );
1175 break;
1176 }
1177 }
1179 parent = Some(height);
1180 let cert = CertificatePair::new(qc, next_epoch_qc);
1181 final_qc = Some(cert.clone());
1182 leaves.push((leaf, cert));
1183 }
1184 drop(rows);
1185
1186 let Some(final_qc) = final_qc else {
1187 tracing::debug!(from_view, "no new leaves at decide");
1189 return Ok(None);
1190 };
1191
1192 let from_view = leaves[0].0.view_number();
1195 let to_view = leaves[leaves.len() - 1].0.view_number();
1196
1197 let vid_rows = tx
1199 .fetch_all(
1200 query("SELECT view, data FROM vid_share2 where view >= $1 AND view <= $2")
1201 .bind(from_view.u64() as i64)
1202 .bind(to_view.u64() as i64),
1203 )
1204 .await?;
1205 let vid_shares = vid_rows
1206 .into_iter()
1207 .map(|row| {
1208 let view: i64 = row.get("view");
1209 let data: Vec<u8> = row.get("data");
1210 let vid_proposal = bincode::deserialize::<
1211 Proposal<SeqTypes, VidDisperseShare<SeqTypes>>,
1212 >(&data)?;
1213 Ok((view as u64, vid_proposal))
1214 })
1215 .collect::<anyhow::Result<BTreeMap<_, _>>>()?;
1216
1217 let da_rows = tx
1219 .fetch_all(
1220 query(
1221 "SELECT view, data FROM da_proposal2 where view >= $1 AND view <= $2",
1222 )
1223 .bind(from_view.u64() as i64)
1224 .bind(to_view.u64() as i64),
1225 )
1226 .await?;
1227 let da_proposals = da_rows
1228 .into_iter()
1229 .map(|row| {
1230 let view: i64 = row.get("view");
1231 let data: Vec<u8> = row.get("data");
1232 let da_proposal = bincode::deserialize::<
1233 Proposal<SeqTypes, DaProposal2<SeqTypes>>,
1234 >(&data)?;
1235 Ok((view as u64, da_proposal.data))
1236 })
1237 .collect::<anyhow::Result<BTreeMap<_, _>>>()?;
1238
1239 let state_certs = Self::load_state_certs(&mut tx, from_view, to_view)
1241 .await
1242 .with_context(|| {
1243 format!("load_state_certs from_view={from_view:?} to_view={to_view:?}")
1244 })?;
1245
1246 let cert2_row = tx
1247 .fetch_optional(
1248 query("SELECT data FROM decided_cert2 WHERE view = $1")
1249 .bind(to_view.u64() as i64),
1250 )
1251 .await?;
1252 let cert2 = cert2_row
1253 .map(|row| {
1254 let bytes: Vec<u8> = row.get("data");
1255 bincode::deserialize::<Certificate2<SeqTypes>>(&bytes)
1256 .context("deserializing decided cert2")
1257 })
1258 .transpose()?;
1259 drop(tx);
1260 Ok(Some((
1261 from_view,
1262 to_view,
1263 leaves,
1264 final_qc,
1265 vid_shares,
1266 da_proposals,
1267 state_certs,
1268 cert2,
1269 )))
1270 })
1271 .await?
1272 else {
1273 return Ok(());
1274 };
1275
1276 let to_height = leaves
1277 .last()
1278 .map(|(leaf, _)| leaf.block_header().block_number());
1279
1280 let chain = leaves
1282 .into_iter()
1283 .rev()
1285 .map(|(mut leaf, cert)| {
1286 let view = leaf.view_number();
1287
1288 let vid_proposal = vid_shares.remove(&view);
1290 if vid_proposal.is_none() {
1291 tracing::debug!(?view, "VID share not available at decide");
1292 }
1293 let vid_share = vid_proposal.as_ref().map(|proposal| proposal.data.clone());
1294
1295 if let Some(proposal) = da_proposals.remove(&view) {
1297 let payload =
1298 Payload::from_bytes(&proposal.encoded_transactions, &proposal.metadata);
1299 leaf.fill_block_payload_unchecked(payload);
1300 } else if view == ViewNumber::genesis() {
1301 leaf.fill_block_payload_unchecked(Payload::empty().0);
1304 } else {
1305 tracing::debug!(?view, "DA proposal not available at decide");
1306 }
1307
1308 let state_cert = state_certs.get(&view).cloned();
1309
1310 let info = LeafInfo {
1311 leaf,
1312 vid_share,
1313 state_cert,
1314 state: Default::default(),
1317 delta: Default::default(),
1318 };
1319 DecidedLeaf { info, cert }
1320 })
1321 .collect();
1322
1323 tracing::debug!(
1324 ?from_view,
1325 ?to_view,
1326 ?final_qc,
1327 ?chain,
1328 "generating decide event"
1329 );
1330
1331 for event in decide_events_from_chain(chain, cert2, deciding_qc.clone()) {
1332 consumer.handle_event(&event).await?;
1333 }
1334
1335 let from_view_i64 = from_view.u64() as i64;
1336 let to_view_i64 = to_view.u64() as i64;
1337 let serialized_state_certs = state_certs
1338 .into_iter()
1339 .map(|(epoch, cert)| Ok((epoch as i64, bincode::serialize(&cert)?)))
1340 .collect::<anyhow::Result<Vec<(i64, Vec<u8>)>>>()?;
1341
1342 serializable_retry!(self, || async {
1348 let mut tx = self.db.write().await?;
1349 tx.upsert(
1350 "event_stream",
1351 ["id", "last_processed_view"],
1352 ["id"],
1353 [(1i32, to_view_i64)],
1354 )
1355 .await?;
1356
1357 for (epoch, state_cert_bytes) in &serialized_state_certs {
1359 tx.upsert(
1360 "finalized_state_cert",
1361 ["epoch", "state_cert"],
1362 ["epoch"],
1363 [(*epoch, state_cert_bytes.clone())],
1364 )
1365 .await?;
1366 }
1367
1368 tx.execute(
1370 query("DELETE FROM vid_share2 where view >= $1 AND view <= $2")
1371 .bind(from_view_i64)
1372 .bind(to_view_i64),
1373 )
1374 .await?;
1375 tx.execute(
1376 query("DELETE FROM da_proposal2 where view >= $1 AND view <= $2")
1377 .bind(from_view_i64)
1378 .bind(to_view_i64),
1379 )
1380 .await?;
1381 tx.execute(
1382 query("DELETE FROM quorum_proposals2 where view >= $1 AND view <= $2")
1383 .bind(from_view_i64)
1384 .bind(to_view_i64),
1385 )
1386 .await?;
1387 tx.execute(
1388 query("DELETE FROM quorum_certificate2 where view >= $1 AND view <= $2")
1389 .bind(from_view_i64)
1390 .bind(to_view_i64),
1391 )
1392 .await?;
1393 tx.execute(
1394 query("DELETE FROM state_cert where view >= $1 AND view <= $2")
1395 .bind(from_view_i64)
1396 .bind(to_view_i64),
1397 )
1398 .await?;
1399 tx.execute(
1400 query("DELETE FROM decided_cert2 where view >= $1 AND view <= $2")
1401 .bind(from_view_i64)
1402 .bind(to_view_i64),
1403 )
1404 .await?;
1405
1406 tx.execute(
1410 query("DELETE FROM anchor_leaf2 WHERE view >= $1 AND view < $2")
1411 .bind(from_view_i64)
1412 .bind(to_view_i64),
1413 )
1414 .await?;
1415
1416 tx.commit().await?;
1417 Ok(())
1418 })
1419 .await?;
1420 last_processed_view = Some(to_view_i64);
1421 last_processed_height = to_height;
1422 }
1423 }
1424
1425 async fn load_state_certs(
1426 tx: &mut Transaction<Read>,
1427 from_view: ViewNumber,
1428 to_view: ViewNumber,
1429 ) -> anyhow::Result<BTreeMap<u64, LightClientStateUpdateCertificateV2<SeqTypes>>> {
1430 let rows = tx
1431 .fetch_all(
1432 query("SELECT view, state_cert FROM state_cert WHERE view >= $1 AND view <= $2")
1433 .bind(from_view.u64() as i64)
1434 .bind(to_view.u64() as i64),
1435 )
1436 .await?;
1437
1438 let mut result = BTreeMap::new();
1439
1440 for row in rows {
1441 let data: Vec<u8> = row.get("state_cert");
1442
1443 let cert: LightClientStateUpdateCertificateV2<SeqTypes> = bincode::deserialize(&data)
1444 .or_else(|err_v2| {
1445 bincode::deserialize::<LightClientStateUpdateCertificateV1<SeqTypes>>(&data)
1446 .map(Into::into)
1447 .context(format!(
1448 "Failed to deserialize LightClientStateUpdateCertificate: with v1 and v2. \
1449 error: {err_v2}"
1450 ))
1451 })?;
1452
1453 result.insert(cert.epoch.u64(), cert);
1454 }
1455
1456 Ok(result)
1457 }
1458
1459 #[tracing::instrument(skip(self))]
1460 async fn prune(&self, cur_view: ViewNumber) -> anyhow::Result<()> {
1461 serializable_retry!(self, || async {
1462 let mut tx = self.db.write().await?;
1463
1464 prune_to_view(
1466 &mut tx,
1467 cur_view.u64().saturating_sub(self.gc_opt.target_retention),
1468 )
1469 .await?;
1470
1471 #[cfg(feature = "embedded-db")]
1474 let usage_query = format!(
1475 "SELECT sum(pgsize) FROM dbstat WHERE name IN ({})",
1476 PRUNE_TABLES
1477 .iter()
1478 .map(|table| format!("'{table}'"))
1479 .join(",")
1480 );
1481
1482 #[cfg(not(feature = "embedded-db"))]
1483 let usage_query = {
1484 let table_sizes = PRUNE_TABLES
1485 .iter()
1486 .map(|table| format!("pg_table_size('{table}')"))
1487 .join(" + ");
1488 format!("SELECT {table_sizes}")
1489 };
1490
1491 let (usage,): (i64,) = query_as(&usage_query).fetch_one(tx.as_mut()).await?;
1492 tracing::debug!(usage, "consensus storage usage after pruning");
1493
1494 if (usage as u64) > self.gc_opt.target_usage {
1495 tracing::warn!(
1496 usage,
1497 gc_opt = ?self.gc_opt,
1498 "consensus storage is running out of space, pruning to minimum retention"
1499 );
1500 prune_to_view(
1501 &mut tx,
1502 cur_view.u64().saturating_sub(self.gc_opt.minimum_retention),
1503 )
1504 .await?;
1505 }
1506
1507 tx.commit().await
1508 })
1509 .await
1510 }
1511}
1512
1513const PRUNE_TABLES: &[&str] = &[
1514 "anchor_leaf2",
1515 "vid_share2",
1516 "da_proposal2",
1517 "quorum_proposals2",
1518 "quorum_certificate2",
1519 "state_cert",
1520 "decided_cert2",
1521];
1522
1523async fn prune_to_view(tx: &mut Transaction<Write>, view: u64) -> anyhow::Result<()> {
1524 if view == 0 {
1525 return Ok(());
1527 }
1528 tracing::debug!(view, "pruning consensus storage");
1529
1530 for table in PRUNE_TABLES {
1531 let res = query(&format!("DELETE FROM {table} WHERE view < $1"))
1532 .bind(view as i64)
1533 .execute(tx.as_mut())
1534 .await
1535 .context(format!("pruning {table}"))?;
1536 if res.rows_affected() > 0 {
1537 tracing::info!(
1538 "garbage collected {} rows from {table}",
1539 res.rows_affected()
1540 );
1541 }
1542 }
1543
1544 Ok(())
1545}
1546
1547#[async_trait]
1548impl SequencerPersistence for Persistence {
1549 fn into_catchup_provider(
1550 self,
1551 backoff: BackoffParams,
1552 ) -> anyhow::Result<Arc<dyn StateCatchup>> {
1553 Ok(Arc::new(SqlStateCatchup::new(Arc::new(self.db), backoff)))
1554 }
1555
1556 async fn load_config(&self) -> anyhow::Result<Option<NetworkConfig>> {
1557 tracing::info!("loading config from Postgres");
1558
1559 serializable_retry!(self, || async {
1560 let Some(row) = self
1562 .db
1563 .read()
1564 .await?
1565 .fetch_optional("SELECT config FROM network_config ORDER BY id DESC LIMIT 1")
1566 .await?
1567 else {
1568 tracing::info!("config not found");
1569 return Ok(None);
1570 };
1571 let json = row.try_get("config")?;
1572
1573 let json =
1574 migrate_network_config(json).context("migration of network config failed")?;
1575 let config = serde_json::from_value(json).context("malformed config file")?;
1576
1577 Ok(Some(config))
1578 })
1579 .await
1580 }
1581
1582 async fn save_config(&self, cfg: &NetworkConfig) -> anyhow::Result<()> {
1583 tracing::info!("saving config to database");
1584 let json = serde_json::to_value(cfg)?;
1585
1586 serializable_retry!(self, || async {
1587 let mut tx = self.db.write().await?;
1588 tx.execute(query("INSERT INTO network_config (config) VALUES ($1)").bind(json.clone()))
1589 .await?;
1590 tx.commit().await
1591 })
1592 .await
1593 }
1594
1595 async fn persist_decided_leaves(
1596 &self,
1597 _view: ViewNumber,
1598 leaf_chain: impl IntoIterator<Item = (&LeafInfo<SeqTypes>, CertificatePair<SeqTypes>)> + Send,
1599 _deciding_qc: Option<Arc<CertificatePair<SeqTypes>>>,
1600 _consumer: &(impl EventConsumer + 'static),
1601 ) -> anyhow::Result<()> {
1602 let values = leaf_chain
1603 .into_iter()
1604 .map(|(info, cert)| {
1605 let mut leaf = info.leaf.clone();
1610 leaf.unfill_block_payload();
1611
1612 let view = cert.view_number().u64() as i64;
1613 let leaf_bytes = bincode::serialize(&leaf)?;
1614 let qc_bytes = bincode::serialize(cert.qc())?;
1615 let next_epoch_qc_bytes = match cert.next_epoch_qc() {
1616 Some(qc) => Some(bincode::serialize(qc)?),
1617 None => None,
1618 };
1619 Ok((view, leaf_bytes, qc_bytes, next_epoch_qc_bytes))
1620 })
1621 .collect::<anyhow::Result<Vec<_>>>()?;
1622
1623 serializable_retry!(self, || async {
1626 let mut tx = self.db.write().await?;
1627 tx.upsert(
1628 "anchor_leaf2",
1629 ["view", "leaf", "qc", "next_epoch_qc"],
1630 ["view"],
1631 values.clone(),
1632 )
1633 .await?;
1634 tx.commit().await
1635 })
1636 .await?;
1637
1638 Ok(())
1639 }
1640
1641 async fn process_decided_events(
1642 &self,
1643 view: ViewNumber,
1644 deciding_qc: Option<Arc<CertificatePair<SeqTypes>>>,
1645 consumer: &(impl EventConsumer + 'static),
1646 ) -> anyhow::Result<Option<ViewNumber>> {
1647 let now = Instant::now();
1648 self.generate_decide_events(deciding_qc, consumer).await?;
1651
1652 if let Err(err) = self.prune(view).await {
1654 tracing::warn!(?view, "pruning failed: {err:#}");
1655 }
1656 self.internal_metrics
1657 .internal_process_decided_events_duration
1658 .add_point(now.elapsed().as_secs_f64());
1659
1660 self.load_processed_view().await
1661 }
1662
1663 async fn load_latest_acted_view(&self) -> anyhow::Result<Option<ViewNumber>> {
1664 serializable_retry!(self, || async {
1665 Ok(self
1666 .db
1667 .read()
1668 .await?
1669 .fetch_optional(query("SELECT view FROM highest_voted_view WHERE id = 0"))
1670 .await?
1671 .map(|row| {
1672 let view: i64 = row.get("view");
1673 ViewNumber::new(view as u64)
1674 }))
1675 })
1676 .await
1677 }
1678
1679 async fn load_restart_view(&self) -> anyhow::Result<Option<ViewNumber>> {
1680 serializable_retry!(self, || async {
1681 Ok(self
1682 .db
1683 .read()
1684 .await?
1685 .fetch_optional(query("SELECT view FROM restart_view WHERE id = 0"))
1686 .await?
1687 .map(|row| {
1688 let view: i64 = row.get("view");
1689 ViewNumber::new(view as u64)
1690 }))
1691 })
1692 .await
1693 }
1694
1695 async fn load_anchor_leaf(&self) -> anyhow::Result<Option<(Leaf2, CertificatePair<SeqTypes>)>> {
1696 serializable_retry!(self, || async {
1697 let Some(row) = self
1698 .db
1699 .read()
1700 .await?
1701 .fetch_optional(
1702 "SELECT leaf, qc, next_epoch_qc FROM anchor_leaf2 ORDER BY view DESC LIMIT 1",
1703 )
1704 .await?
1705 else {
1706 return Ok(None);
1707 };
1708
1709 let leaf_bytes: Vec<u8> = row.get("leaf");
1710 let leaf2: Leaf2 = bincode::deserialize(&leaf_bytes)?;
1711
1712 let qc_bytes: Vec<u8> = row.get("qc");
1713 let qc2: QuorumCertificate2<SeqTypes> = bincode::deserialize(&qc_bytes)?;
1714
1715 let maybe_next_qc_bytes: Option<Vec<u8>> = row.try_get("next_epoch_qc").ok();
1716 let maybe_next_qc2 = maybe_next_qc_bytes
1717 .and_then(|next_qc_bytes| bincode::deserialize(&next_qc_bytes).ok());
1718
1719 let cert_pair = CertificatePair::new(qc2, maybe_next_qc2);
1720
1721 Ok(Some((leaf2, cert_pair)))
1722 })
1723 .await
1724 }
1725
1726 async fn load_anchor_view(&self) -> anyhow::Result<ViewNumber> {
1727 serializable_retry!(self, || async {
1728 let mut tx = self.db.read().await?;
1729 let (view,) = query_as::<(i64,)>("SELECT coalesce(max(view), 0) FROM anchor_leaf2")
1730 .fetch_one(tx.as_mut())
1731 .await?;
1732 Ok(ViewNumber::new(view as u64))
1733 })
1734 .await
1735 }
1736
1737 async fn load_da_proposal(
1738 &self,
1739 view: ViewNumber,
1740 ) -> anyhow::Result<Option<Proposal<SeqTypes, DaProposal2<SeqTypes>>>> {
1741 serializable_retry!(self, || async {
1742 let result = self
1743 .db
1744 .read()
1745 .await?
1746 .fetch_optional(
1747 query("SELECT data FROM da_proposal2 where view = $1").bind(view.u64() as i64),
1748 )
1749 .await?;
1750
1751 result
1752 .map(|row| {
1753 let bytes: Vec<u8> = row.get("data");
1754 anyhow::Result::<_>::Ok(bincode::deserialize(&bytes)?)
1755 })
1756 .transpose()
1757 })
1758 .await
1759 }
1760
1761 async fn load_vid_share(
1762 &self,
1763 view: ViewNumber,
1764 ) -> anyhow::Result<Option<Proposal<SeqTypes, VidDisperseShare<SeqTypes>>>> {
1765 serializable_retry!(self, || async {
1766 let result = self
1767 .db
1768 .read()
1769 .await?
1770 .fetch_optional(
1771 query("SELECT data FROM vid_share2 where view = $1").bind(view.u64() as i64),
1772 )
1773 .await?;
1774
1775 result
1776 .map(|row| {
1777 let bytes: Vec<u8> = row.get("data");
1778 anyhow::Result::<_>::Ok(bincode::deserialize(&bytes)?)
1779 })
1780 .transpose()
1781 })
1782 .await
1783 }
1784
1785 async fn load_quorum_proposals(
1786 &self,
1787 ) -> anyhow::Result<BTreeMap<ViewNumber, Proposal<SeqTypes, QuorumProposalWrapper<SeqTypes>>>>
1788 {
1789 serializable_retry!(self, || async {
1790 let rows = self
1791 .db
1792 .read()
1793 .await?
1794 .fetch_all("SELECT * FROM quorum_proposals2")
1795 .await?;
1796
1797 Ok(BTreeMap::from_iter(
1798 rows.into_iter()
1799 .map(|row| {
1800 let view: i64 = row.get("view");
1801 let view_number: ViewNumber = ViewNumber::new(view.try_into()?);
1802 let bytes: Vec<u8> = row.get("data");
1803 let proposal: Proposal<SeqTypes, QuorumProposalWrapper<SeqTypes>> =
1804 bincode::deserialize(&bytes).or_else(|error| {
1805 bincode::deserialize::<
1806 Proposal<SeqTypes, QuorumProposalWrapperLegacy<SeqTypes>>,
1807 >(&bytes)
1808 .map(convert_proposal)
1809 .inspect_err(|err_v3| {
1810 tracing::warn!(
1811 ?view_number,
1812 %error,
1813 %err_v3,
1814 "ignoring malformed quorum proposal DB row"
1815 );
1816 })
1817 })?;
1818 Ok((view_number, proposal))
1819 })
1820 .collect::<anyhow::Result<Vec<_>>>()?,
1821 ))
1822 })
1823 .await
1824 }
1825
1826 async fn load_quorum_proposal(
1827 &self,
1828 view: ViewNumber,
1829 ) -> anyhow::Result<Proposal<SeqTypes, QuorumProposalWrapper<SeqTypes>>> {
1830 serializable_retry!(self, || async {
1831 let mut tx = self.db.read().await?;
1832 let (data,) = query_as::<(Vec<u8>,)>(
1833 "SELECT data FROM quorum_proposals2 WHERE view = $1 LIMIT 1",
1834 )
1835 .bind(view.u64() as i64)
1836 .fetch_one(tx.as_mut())
1837 .await?;
1838 let proposal: Proposal<SeqTypes, QuorumProposalWrapper<SeqTypes>> =
1839 bincode::deserialize(&data).or_else(|error| {
1840 bincode::deserialize::<
1841 Proposal<SeqTypes, QuorumProposalWrapperLegacy<SeqTypes>>,
1842 >(&data)
1843 .map(convert_proposal)
1844 .context(format!(
1845 "Failed to deserialize quorum proposal for view {view}. \
1846 error={error}"
1847 ))
1848 })?;
1849 Ok(proposal)
1850 })
1851 .await
1852 }
1853
1854 async fn append_vid(
1855 &self,
1856 proposal: &Proposal<SeqTypes, VidDisperseShare<SeqTypes>>,
1857 ) -> anyhow::Result<()> {
1858 let view = proposal.data.view_number().u64();
1859 let payload_hash = proposal.data.payload_commitment();
1860 let data_bytes = bincode::serialize(proposal).unwrap();
1861
1862 let now = Instant::now();
1863 let res = serializable_retry!(self, || async {
1864 let mut tx = self.db.write().await?;
1865 tx.upsert(
1866 "vid_share2",
1867 ["view", "data", "payload_hash"],
1868 ["view"],
1869 [(view as i64, data_bytes.clone(), payload_hash.to_string())],
1870 )
1871 .await?;
1872 tx.commit().await
1873 })
1874 .await;
1875 self.internal_metrics
1876 .internal_append_vid_duration
1877 .add_point(now.elapsed().as_secs_f64());
1878 res
1879 }
1880
1881 async fn append_da(
1882 &self,
1883 proposal: &Proposal<SeqTypes, DaProposal<SeqTypes>>,
1884 vid_commit: VidCommitment,
1885 ) -> anyhow::Result<()> {
1886 let data = &proposal.data;
1887 let view = data.view_number().u64();
1888 let data_bytes = bincode::serialize(proposal).unwrap();
1889
1890 let now = Instant::now();
1891 let res = serializable_retry!(self, || async {
1892 let mut tx = self.db.write().await?;
1893 tx.upsert(
1894 "da_proposal",
1895 ["view", "data", "payload_hash"],
1896 ["view"],
1897 [(view as i64, data_bytes.clone(), vid_commit.to_string())],
1898 )
1899 .await?;
1900 tx.commit().await
1901 })
1902 .await;
1903 self.internal_metrics
1904 .internal_append_da_duration
1905 .add_point(now.elapsed().as_secs_f64());
1906 res
1907 }
1908
1909 async fn record_action(
1910 &self,
1911 view: ViewNumber,
1912 _epoch: Option<EpochNumber>,
1913 action: HotShotAction,
1914 ) -> anyhow::Result<()> {
1915 if !matches!(action, HotShotAction::Propose | HotShotAction::Vote) {
1917 return Ok(());
1918 }
1919
1920 serializable_retry!(self, || async {
1921 let stmt = format!(
1922 "INSERT INTO highest_voted_view (id, view) VALUES (0, $1)
1923 ON CONFLICT (id) DO UPDATE SET view = {MAX_FN}(highest_voted_view.view, \
1924 excluded.view)"
1925 );
1926
1927 let mut tx = self.db.write().await?;
1928 tx.execute(query(&stmt).bind(view.u64() as i64)).await?;
1929
1930 if matches!(action, HotShotAction::Vote) {
1931 let restart_view = view + 1;
1932 let stmt = format!(
1933 "INSERT INTO restart_view (id, view) VALUES (0, $1)
1934 ON CONFLICT (id) DO UPDATE SET view = {MAX_FN}(restart_view.view, \
1935 excluded.view)"
1936 );
1937 tx.execute(query(&stmt).bind(restart_view.u64() as i64))
1938 .await?;
1939 }
1940
1941 tx.commit().await
1942 })
1943 .await
1944 }
1945
1946 async fn append_quorum_proposal2(
1947 &self,
1948 proposal: &Proposal<SeqTypes, QuorumProposalWrapper<SeqTypes>>,
1949 ) -> anyhow::Result<()> {
1950 let view_number = proposal.data.view_number().u64();
1951
1952 let proposal_bytes = bincode::serialize(&proposal).context("serializing proposal")?;
1953 let leaf_hash = Committable::commit(&Leaf2::from_quorum_proposal(&proposal.data));
1954
1955 let justify_qc = proposal.data.justify_qc();
1957 let justify_qc_bytes = bincode::serialize(&justify_qc).context("serializing QC")?;
1958 let justify_qc_view = justify_qc.view_number.u64() as i64;
1959 let justify_qc_leaf_commit = justify_qc.data.leaf_commit.to_string();
1960
1961 let now = Instant::now();
1962 let res = serializable_retry!(self, || async {
1963 let mut tx = self.db.write().await?;
1964 tx.upsert(
1965 "quorum_proposals2",
1966 ["view", "leaf_hash", "data"],
1967 ["view"],
1968 [(
1969 view_number as i64,
1970 leaf_hash.to_string(),
1971 proposal_bytes.clone(),
1972 )],
1973 )
1974 .await?;
1975 tx.upsert(
1976 "quorum_certificate2",
1977 ["view", "leaf_hash", "data"],
1978 ["view"],
1979 [(
1980 justify_qc_view,
1981 justify_qc_leaf_commit.clone(),
1982 justify_qc_bytes.clone(),
1983 )],
1984 )
1985 .await?;
1986 tx.commit().await
1987 })
1988 .await;
1989 self.internal_metrics
1990 .internal_append_quorum2_duration
1991 .add_point(now.elapsed().as_secs_f64());
1992 res
1993 }
1994
1995 async fn append_cert2(
1996 &self,
1997 view: ViewNumber,
1998 cert2: Certificate2<SeqTypes>,
1999 ) -> anyhow::Result<()> {
2000 let data = bincode::serialize(&cert2).context("serializing cert2")?;
2001 let view_i64 = view.u64() as i64;
2002 serializable_retry!(self, || async {
2003 let mut tx = self.db.write().await?;
2004 tx.upsert(
2005 "decided_cert2",
2006 ["view", "data"],
2007 ["view"],
2008 [(view_i64, data.clone())],
2009 )
2010 .await?;
2011 tx.commit().await
2012 })
2013 .await
2014 }
2015
2016 async fn load_cert2(&self, view: ViewNumber) -> anyhow::Result<Option<Certificate2<SeqTypes>>> {
2017 let row = self
2018 .db
2019 .read()
2020 .await?
2021 .fetch_optional(
2022 query("SELECT data FROM decided_cert2 WHERE view = $1").bind(view.u64() as i64),
2023 )
2024 .await?;
2025 row.map(|row| {
2026 let bytes: Vec<u8> = row.get("data");
2027 bincode::deserialize::<Certificate2<SeqTypes>>(&bytes).context("deserializing cert2")
2028 })
2029 .transpose()
2030 }
2031
2032 async fn append_high_qc2(&self, high_qc: QuorumCertificate2<SeqTypes>) -> anyhow::Result<()> {
2033 let view = high_qc.view_number();
2034 let data = bincode::serialize(&high_qc).context("serializing high_qc2")?;
2035 serializable_retry!(self, || async {
2036 let mut tx = self.db.write().await?;
2037 let stored_view = query("SELECT data FROM high_qc2 WHERE id = true")
2042 .fetch_optional(tx.as_mut())
2043 .await?
2044 .map(|row| {
2045 let bytes: Vec<u8> = row.get("data");
2046 bincode::deserialize::<QuorumCertificate2<SeqTypes>>(&bytes)
2047 .context("deserializing existing high_qc2")
2048 .map(|qc| qc.view_number())
2049 })
2050 .transpose()?;
2051 if stored_view.is_some_and(|stored| stored >= view) {
2052 return Ok(());
2053 }
2054 tx.upsert("high_qc2", ["id", "data"], ["id"], [(true, data.clone())])
2055 .await?;
2056 tx.commit().await
2057 })
2058 .await
2059 }
2060
2061 async fn load_high_qc2(&self) -> anyhow::Result<Option<QuorumCertificate2<SeqTypes>>> {
2062 let row = self
2063 .db
2064 .read()
2065 .await?
2066 .fetch_optional("SELECT data FROM high_qc2 WHERE id = true")
2067 .await?;
2068 row.map(|row| {
2069 let bytes: Vec<u8> = row.get("data");
2070 bincode::deserialize::<QuorumCertificate2<SeqTypes>>(&bytes)
2071 .context("deserializing high_qc2")
2072 })
2073 .transpose()
2074 }
2075
2076 async fn load_upgrade_certificate(
2077 &self,
2078 ) -> anyhow::Result<Option<UpgradeCertificate<SeqTypes>>> {
2079 let result = self
2080 .db
2081 .read()
2082 .await?
2083 .fetch_optional("SELECT * FROM upgrade_certificate where id = true")
2084 .await?;
2085
2086 result
2087 .map(|row| {
2088 let bytes: Vec<u8> = row.get("data");
2089 anyhow::Result::<_>::Ok(bincode::deserialize(&bytes)?)
2090 })
2091 .transpose()
2092 }
2093
2094 async fn store_upgrade_certificate(
2095 &self,
2096 decided_upgrade_certificate: Option<UpgradeCertificate<SeqTypes>>,
2097 ) -> anyhow::Result<()> {
2098 let certificate = match decided_upgrade_certificate {
2099 Some(cert) => cert,
2100 None => return Ok(()),
2101 };
2102 let upgrade_certificate_bytes =
2103 bincode::serialize(&certificate).context("serializing upgrade certificate")?;
2104 serializable_retry!(self, || async {
2105 let mut tx = self.db.write().await?;
2106 tx.upsert(
2107 "upgrade_certificate",
2108 ["id", "data"],
2109 ["id"],
2110 [(true, upgrade_certificate_bytes.clone())],
2111 )
2112 .await?;
2113 tx.commit().await
2114 })
2115 .await
2116 }
2117
2118 async fn migrate_anchor_leaf(&self) -> anyhow::Result<()> {
2119 let batch_size: i64 = 10000;
2120 let mut tx = self.db.read().await?;
2121
2122 let (is_completed, mut offset) = query_as::<(bool, i64)>(
2127 "SELECT completed, migrated_rows from epoch_migration WHERE table_name = 'anchor_leaf'",
2128 )
2129 .fetch_one(tx.as_mut())
2130 .await?;
2131
2132 if is_completed {
2133 tracing::info!("decided leaves already migrated");
2134 return Ok(());
2135 }
2136
2137 tracing::warn!("migrating decided leaves..");
2138 loop {
2139 let mut tx = self.db.read().await?;
2140 let rows = query(
2141 "SELECT view, leaf, qc FROM anchor_leaf WHERE view >= $1 ORDER BY view LIMIT $2",
2142 )
2143 .bind(offset)
2144 .bind(batch_size)
2145 .fetch_all(tx.as_mut())
2146 .await?;
2147
2148 drop(tx);
2149 if rows.is_empty() {
2150 break;
2151 }
2152 let mut values = Vec::new();
2153
2154 for row in rows.iter() {
2155 let leaf: Vec<u8> = row.try_get("leaf")?;
2156 let qc: Vec<u8> = row.try_get("qc")?;
2157 let leaf1: Leaf = bincode::deserialize(&leaf)?;
2158 let qc1: QuorumCertificate<SeqTypes> = bincode::deserialize(&qc)?;
2159 let view: i64 = row.try_get("view")?;
2160
2161 let leaf2: Leaf2 = leaf1.into();
2162 let qc2: QuorumCertificate2<SeqTypes> = qc1.to_qc2();
2163
2164 let leaf2_bytes = bincode::serialize(&leaf2)?;
2165 let qc2_bytes = bincode::serialize(&qc2)?;
2166
2167 values.push((view, leaf2_bytes, qc2_bytes));
2168 }
2169
2170 let mut query_builder: sqlx::QueryBuilder<Db> =
2171 sqlx::QueryBuilder::new("INSERT INTO anchor_leaf2 (view, leaf, qc) ");
2172
2173 offset = values.last().context("last row")?.0;
2174
2175 query_builder.push_values(values, |mut b, (view, leaf, qc)| {
2176 b.push_bind(view).push_bind(leaf).push_bind(qc);
2177 });
2178
2179 query_builder.push(" ON CONFLICT DO NOTHING");
2182
2183 let query = query_builder.build();
2184
2185 let mut tx = self.db.write().await?;
2186 query.execute(tx.as_mut()).await?;
2187
2188 tx.upsert(
2189 "epoch_migration",
2190 ["table_name", "completed", "migrated_rows"],
2191 ["table_name"],
2192 [("anchor_leaf".to_string(), false, offset)],
2193 )
2194 .await?;
2195 tx.commit().await?;
2196
2197 tracing::info!(
2198 "anchor leaf migration progress: rows={} offset={}",
2199 rows.len(),
2200 offset
2201 );
2202
2203 if rows.len() < batch_size as usize {
2204 break;
2205 }
2206 }
2207
2208 tracing::warn!("migrated decided leaves");
2209
2210 let mut tx = self.db.write().await?;
2211 tx.upsert(
2212 "epoch_migration",
2213 ["table_name", "completed", "migrated_rows"],
2214 ["table_name"],
2215 [("anchor_leaf".to_string(), true, offset)],
2216 )
2217 .await?;
2218 tx.commit().await?;
2219
2220 tracing::info!("updated epoch_migration table for anchor_leaf");
2221
2222 Ok(())
2223 }
2224
2225 async fn migrate_da_proposals(&self) -> anyhow::Result<()> {
2226 let batch_size: i64 = 10000;
2227 let mut tx = self.db.read().await?;
2228
2229 let (is_completed, mut offset) = query_as::<(bool, i64)>(
2230 "SELECT completed, migrated_rows from epoch_migration WHERE table_name = 'da_proposal'",
2231 )
2232 .fetch_one(tx.as_mut())
2233 .await?;
2234
2235 if is_completed {
2236 tracing::info!("da proposals migration already done");
2237 return Ok(());
2238 }
2239
2240 tracing::warn!("migrating da proposals..");
2241
2242 loop {
2243 let mut tx = self.db.read().await?;
2244 let rows = query(
2245 "SELECT payload_hash, data FROM da_proposal WHERE view >= $1 ORDER BY view LIMIT \
2246 $2",
2247 )
2248 .bind(offset)
2249 .bind(batch_size)
2250 .fetch_all(tx.as_mut())
2251 .await?;
2252
2253 drop(tx);
2254 if rows.is_empty() {
2255 break;
2256 }
2257 let mut values = Vec::new();
2258
2259 for row in rows.iter() {
2260 let data: Vec<u8> = row.try_get("data")?;
2261 let payload_hash: String = row.try_get("payload_hash")?;
2262
2263 let da_proposal: Proposal<SeqTypes, DaProposal<SeqTypes>> =
2264 bincode::deserialize(&data)?;
2265 let da_proposal2: Proposal<SeqTypes, DaProposal2<SeqTypes>> =
2266 convert_proposal(da_proposal);
2267
2268 let view = da_proposal2.data.view_number.u64() as i64;
2269 let data = bincode::serialize(&da_proposal2)?;
2270
2271 values.push((view, payload_hash, data));
2272 }
2273
2274 let mut query_builder: sqlx::QueryBuilder<Db> =
2275 sqlx::QueryBuilder::new("INSERT INTO da_proposal2 (view, payload_hash, data) ");
2276
2277 offset = values.last().context("last row")?.0;
2278 query_builder.push_values(values, |mut b, (view, payload_hash, data)| {
2279 b.push_bind(view).push_bind(payload_hash).push_bind(data);
2280 });
2281 query_builder.push(" ON CONFLICT DO NOTHING");
2282 let query = query_builder.build();
2283
2284 let mut tx = self.db.write().await?;
2285 query.execute(tx.as_mut()).await?;
2286
2287 tx.upsert(
2288 "epoch_migration",
2289 ["table_name", "completed", "migrated_rows"],
2290 ["table_name"],
2291 [("da_proposal".to_string(), false, offset)],
2292 )
2293 .await?;
2294 tx.commit().await?;
2295
2296 tracing::info!(
2297 "DA proposals migration progress: rows={} offset={}",
2298 rows.len(),
2299 offset
2300 );
2301 if rows.len() < batch_size as usize {
2302 break;
2303 }
2304 }
2305
2306 tracing::warn!("migrated da proposals");
2307
2308 let mut tx = self.db.write().await?;
2309 tx.upsert(
2310 "epoch_migration",
2311 ["table_name", "completed", "migrated_rows"],
2312 ["table_name"],
2313 [("da_proposal".to_string(), true, offset)],
2314 )
2315 .await?;
2316 tx.commit().await?;
2317
2318 tracing::info!("updated epoch_migration table for da_proposal");
2319
2320 Ok(())
2321 }
2322
2323 async fn migrate_vid_shares(&self) -> anyhow::Result<()> {
2324 let batch_size: i64 = 10000;
2325
2326 let mut tx = self.db.read().await?;
2327
2328 let (is_completed, mut offset) = query_as::<(bool, i64)>(
2329 "SELECT completed, migrated_rows from epoch_migration WHERE table_name = 'vid_share'",
2330 )
2331 .fetch_one(tx.as_mut())
2332 .await?;
2333
2334 if is_completed {
2335 tracing::info!("vid_share migration already done");
2336 return Ok(());
2337 }
2338
2339 tracing::warn!("migrating vid shares..");
2340 loop {
2341 let mut tx = self.db.read().await?;
2342 let rows = query(
2343 "SELECT payload_hash, data FROM vid_share WHERE view >= $1 ORDER BY view LIMIT $2",
2344 )
2345 .bind(offset)
2346 .bind(batch_size)
2347 .fetch_all(tx.as_mut())
2348 .await?;
2349
2350 drop(tx);
2351 if rows.is_empty() {
2352 break;
2353 }
2354 let mut values = Vec::new();
2355
2356 for row in rows.iter() {
2357 let data: Vec<u8> = row.try_get("data")?;
2358 let payload_hash: String = row.try_get("payload_hash")?;
2359
2360 let vid_share: Proposal<SeqTypes, VidDisperseShare0<SeqTypes>> =
2361 bincode::deserialize(&data)?;
2362 let vid_share2: Proposal<SeqTypes, VidDisperseShare<SeqTypes>> =
2363 convert_proposal(vid_share);
2364
2365 let view = vid_share2.data.view_number().u64() as i64;
2366 let data = bincode::serialize(&vid_share2)?;
2367
2368 values.push((view, payload_hash, data));
2369 }
2370
2371 let mut query_builder: sqlx::QueryBuilder<Db> =
2372 sqlx::QueryBuilder::new("INSERT INTO vid_share2 (view, payload_hash, data) ");
2373
2374 offset = values.last().context("last row")?.0;
2375
2376 query_builder.push_values(values, |mut b, (view, payload_hash, data)| {
2377 b.push_bind(view).push_bind(payload_hash).push_bind(data);
2378 });
2379
2380 let query = query_builder.build();
2381
2382 let mut tx = self.db.write().await?;
2383 query.execute(tx.as_mut()).await?;
2384
2385 tx.upsert(
2386 "epoch_migration",
2387 ["table_name", "completed", "migrated_rows"],
2388 ["table_name"],
2389 [("vid_share".to_string(), false, offset)],
2390 )
2391 .await?;
2392 tx.commit().await?;
2393
2394 tracing::info!(
2395 "VID shares migration progress: rows={} offset={}",
2396 rows.len(),
2397 offset
2398 );
2399 if rows.len() < batch_size as usize {
2400 break;
2401 }
2402 }
2403
2404 tracing::warn!("migrated vid shares");
2405
2406 let mut tx = self.db.write().await?;
2407 tx.upsert(
2408 "epoch_migration",
2409 ["table_name", "completed", "migrated_rows"],
2410 ["table_name"],
2411 [("vid_share".to_string(), true, offset)],
2412 )
2413 .await?;
2414 tx.commit().await?;
2415
2416 tracing::info!("updated epoch_migration table for vid_share");
2417
2418 Ok(())
2419 }
2420
2421 async fn migrate_quorum_proposals(&self) -> anyhow::Result<()> {
2422 let batch_size: i64 = 10000;
2423 let mut tx = self.db.read().await?;
2424
2425 let (is_completed, mut offset) = query_as::<(bool, i64)>(
2426 "SELECT completed, migrated_rows from epoch_migration WHERE table_name = \
2427 'quorum_proposals'",
2428 )
2429 .fetch_one(tx.as_mut())
2430 .await?;
2431
2432 if is_completed {
2433 tracing::info!("quorum proposals migration already done");
2434 return Ok(());
2435 }
2436
2437 tracing::warn!("migrating quorum proposals..");
2438
2439 loop {
2440 let mut tx = self.db.read().await?;
2441 let rows = query(
2442 "SELECT view, leaf_hash, data FROM quorum_proposals WHERE view >= $1 ORDER BY \
2443 view LIMIT $2",
2444 )
2445 .bind(offset)
2446 .bind(batch_size)
2447 .fetch_all(tx.as_mut())
2448 .await?;
2449
2450 drop(tx);
2451
2452 if rows.is_empty() {
2453 break;
2454 }
2455
2456 let mut values = Vec::new();
2457
2458 for row in rows.iter() {
2459 let leaf_hash: String = row.try_get("leaf_hash")?;
2460 let data: Vec<u8> = row.try_get("data")?;
2461
2462 let quorum_proposal: Proposal<SeqTypes, QuorumProposal<SeqTypes>> =
2463 bincode::deserialize(&data)?;
2464 let quorum_proposal2: Proposal<SeqTypes, QuorumProposalWrapper<SeqTypes>> =
2465 convert_proposal(quorum_proposal);
2466
2467 let view = quorum_proposal2.data.view_number().u64() as i64;
2468 let data = bincode::serialize(&quorum_proposal2)?;
2469
2470 values.push((view, leaf_hash, data));
2471 }
2472
2473 let mut query_builder: sqlx::QueryBuilder<Db> =
2474 sqlx::QueryBuilder::new("INSERT INTO quorum_proposals2 (view, leaf_hash, data) ");
2475
2476 offset = values.last().context("last row")?.0;
2477 query_builder.push_values(values, |mut b, (view, leaf_hash, data)| {
2478 b.push_bind(view).push_bind(leaf_hash).push_bind(data);
2479 });
2480
2481 query_builder.push(" ON CONFLICT DO NOTHING");
2482
2483 let query = query_builder.build();
2484
2485 let mut tx = self.db.write().await?;
2486 query.execute(tx.as_mut()).await?;
2487
2488 tx.upsert(
2489 "epoch_migration",
2490 ["table_name", "completed", "migrated_rows"],
2491 ["table_name"],
2492 [("quorum_proposals".to_string(), false, offset)],
2493 )
2494 .await?;
2495 tx.commit().await?;
2496
2497 tracing::info!(
2498 "quorum proposals migration progress: rows={} offset={}",
2499 rows.len(),
2500 offset
2501 );
2502
2503 if rows.len() < batch_size as usize {
2504 break;
2505 }
2506 }
2507
2508 tracing::warn!("migrated quorum proposals");
2509
2510 let mut tx = self.db.write().await?;
2511 tx.upsert(
2512 "epoch_migration",
2513 ["table_name", "completed", "migrated_rows"],
2514 ["table_name"],
2515 [("quorum_proposals".to_string(), true, offset)],
2516 )
2517 .await?;
2518 tx.commit().await?;
2519
2520 tracing::info!("updated epoch_migration table for quorum_proposals");
2521
2522 Ok(())
2523 }
2524
2525 async fn migrate_quorum_certificates(&self) -> anyhow::Result<()> {
2526 let batch_size: i64 = 10000;
2527 let mut tx = self.db.read().await?;
2528
2529 let (is_completed, mut offset) = query_as::<(bool, i64)>(
2530 "SELECT completed, migrated_rows from epoch_migration WHERE table_name = \
2531 'quorum_certificate'",
2532 )
2533 .fetch_one(tx.as_mut())
2534 .await?;
2535
2536 if is_completed {
2537 tracing::info!("quorum certificates migration already done");
2538 return Ok(());
2539 }
2540
2541 tracing::warn!("migrating quorum certificates..");
2542 loop {
2543 let mut tx = self.db.read().await?;
2544 let rows = query(
2545 "SELECT view, leaf_hash, data FROM quorum_certificate WHERE view >= $1 ORDER BY \
2546 view LIMIT $2",
2547 )
2548 .bind(offset)
2549 .bind(batch_size)
2550 .fetch_all(tx.as_mut())
2551 .await?;
2552
2553 drop(tx);
2554 if rows.is_empty() {
2555 break;
2556 }
2557 let mut values = Vec::new();
2558
2559 for row in rows.iter() {
2560 let leaf_hash: String = row.try_get("leaf_hash")?;
2561 let data: Vec<u8> = row.try_get("data")?;
2562
2563 let qc: QuorumCertificate<SeqTypes> = bincode::deserialize(&data)?;
2564 let qc2: QuorumCertificate2<SeqTypes> = qc.to_qc2();
2565
2566 let view = qc2.view_number().u64() as i64;
2567 let data = bincode::serialize(&qc2)?;
2568
2569 values.push((view, leaf_hash, data));
2570 }
2571
2572 let mut query_builder: sqlx::QueryBuilder<Db> =
2573 sqlx::QueryBuilder::new("INSERT INTO quorum_certificate2 (view, leaf_hash, data) ");
2574
2575 offset = values.last().context("last row")?.0;
2576
2577 query_builder.push_values(values, |mut b, (view, leaf_hash, data)| {
2578 b.push_bind(view).push_bind(leaf_hash).push_bind(data);
2579 });
2580
2581 query_builder.push(" ON CONFLICT DO NOTHING");
2582 let query = query_builder.build();
2583
2584 let mut tx = self.db.write().await?;
2585 query.execute(tx.as_mut()).await?;
2586
2587 tx.upsert(
2588 "epoch_migration",
2589 ["table_name", "completed", "migrated_rows"],
2590 ["table_name"],
2591 [("quorum_certificate".to_string(), false, offset)],
2592 )
2593 .await?;
2594 tx.commit().await?;
2595
2596 tracing::info!(
2597 "Quorum certificates migration progress: rows={} offset={}",
2598 rows.len(),
2599 offset
2600 );
2601
2602 if rows.len() < batch_size as usize {
2603 break;
2604 }
2605 }
2606
2607 tracing::warn!("migrated quorum certificates");
2608
2609 let mut tx = self.db.write().await?;
2610 tx.upsert(
2611 "epoch_migration",
2612 ["table_name", "completed", "migrated_rows"],
2613 ["table_name"],
2614 [("quorum_certificate".to_string(), true, offset)],
2615 )
2616 .await?;
2617 tx.commit().await?;
2618 tracing::info!("updated epoch_migration table for quorum_certificate");
2619
2620 Ok(())
2621 }
2622
2623 async fn migrate_x25519_keys(&self) -> anyhow::Result<()> {
2628 use super::RegisteredValidatorNoX25519;
2629
2630 let name = DataMigration::X25519Keys.as_str();
2631
2632 if !self
2634 .is_migration_complete(name, "epoch_drb_and_root")
2635 .await?
2636 {
2637 let rows: Vec<(i64, Vec<u8>)> = {
2638 let mut tx = self.db.read().await?;
2639 query_as("SELECT epoch, stake FROM epoch_drb_and_root WHERE stake IS NOT NULL")
2640 .fetch_all(tx.as_mut())
2641 .await?
2642 };
2643
2644 let num_rows = rows.len();
2645 let mut tx = self.db.write().await?;
2646 for (epoch, stake_bytes) in rows {
2647 if bincode::deserialize::<AuthenticatedValidatorMap>(&stake_bytes).is_ok() {
2649 continue;
2650 }
2651
2652 let old_validators: IndexMap<Address, RegisteredValidatorNoX25519> =
2654 bincode::deserialize(&stake_bytes)
2655 .context("deserializing legacy stake table")?;
2656 let validators: AuthenticatedValidatorMap = old_validators
2657 .into_iter()
2658 .map(|(addr, v)| {
2659 let registered = v.migrate();
2660 (
2661 addr,
2662 AuthenticatedValidator::try_from(registered)
2663 .expect("stake tables only contain authenticated validators"),
2664 )
2665 })
2666 .collect();
2667
2668 let new_bytes =
2669 bincode::serialize(&validators).context("serializing stake table")?;
2670
2671 tracing::debug!(epoch, "migrating x25519 keys in stake table");
2672 tx.execute(
2673 query("UPDATE epoch_drb_and_root SET stake = $1 WHERE epoch = $2")
2674 .bind(&new_bytes)
2675 .bind(epoch),
2676 )
2677 .await?;
2678 }
2679 Self::mark_migration_complete(&mut tx, name, "epoch_drb_and_root", num_rows).await?;
2680 tx.commit().await?;
2681 tracing::info!(
2682 num_rows,
2683 "x25519_keys migration completed for epoch_drb_and_root"
2684 );
2685 }
2686
2687 if !self
2689 .is_migration_complete(name, "stake_table_validators")
2690 .await?
2691 {
2692 let rows: Vec<(i64, String, serde_json::Value)> = {
2693 let mut tx = self.db.read().await?;
2694 query_as("SELECT epoch, address, validator FROM stake_table_validators")
2695 .fetch_all(tx.as_mut())
2696 .await?
2697 };
2698
2699 let num_rows = rows.len();
2700 let mut tx = self.db.write().await?;
2701 for (epoch, address, validator_json) in rows {
2702 if validator_json
2705 .as_object()
2706 .is_some_and(|obj| obj.contains_key("x25519_key"))
2707 {
2708 continue;
2709 }
2710
2711 let validator: RegisteredValidator<PubKey> =
2714 serde_json::from_value(validator_json).context("deserializing validator")?;
2715
2716 let new_json = serde_json::to_value(&validator).context("serializing validator")?;
2717
2718 tracing::debug!(epoch, %address, "migrating x25519 keys for validator");
2719 tx.execute(
2720 query(
2721 "UPDATE stake_table_validators SET validator = $1 WHERE epoch = $2 AND \
2722 address = $3",
2723 )
2724 .bind(&new_json)
2725 .bind(epoch)
2726 .bind(&address),
2727 )
2728 .await?;
2729 }
2730 Self::mark_migration_complete(&mut tx, name, "stake_table_validators", num_rows)
2731 .await?;
2732 tx.commit().await?;
2733 tracing::info!(
2734 num_rows,
2735 "x25519_keys migration completed for stake_table_validators"
2736 );
2737 }
2738
2739 Ok(())
2740 }
2741
2742 async fn store_next_epoch_quorum_certificate(
2743 &self,
2744 high_qc: NextEpochQuorumCertificate2<SeqTypes>,
2745 ) -> anyhow::Result<()> {
2746 let qc2_bytes = bincode::serialize(&high_qc).context("serializing next epoch qc")?;
2747 serializable_retry!(self, || async {
2748 let mut tx = self.db.write().await?;
2749 tx.upsert(
2750 "next_epoch_quorum_certificate",
2751 ["id", "data"],
2752 ["id"],
2753 [(true, qc2_bytes.clone())],
2754 )
2755 .await?;
2756 tx.commit().await
2757 })
2758 .await
2759 }
2760
2761 async fn load_next_epoch_quorum_certificate(
2762 &self,
2763 ) -> anyhow::Result<Option<NextEpochQuorumCertificate2<SeqTypes>>> {
2764 let result = self
2765 .db
2766 .read()
2767 .await?
2768 .fetch_optional("SELECT * FROM next_epoch_quorum_certificate where id = true")
2769 .await?;
2770
2771 result
2772 .map(|row| {
2773 let bytes: Vec<u8> = row.get("data");
2774 anyhow::Result::<_>::Ok(bincode::deserialize(&bytes)?)
2775 })
2776 .transpose()
2777 }
2778
2779 async fn store_eqc(
2780 &self,
2781 high_qc: QuorumCertificate2<SeqTypes>,
2782 next_epoch_high_qc: NextEpochQuorumCertificate2<SeqTypes>,
2783 ) -> anyhow::Result<()> {
2784 let eqc_bytes =
2785 bincode::serialize(&(high_qc, next_epoch_high_qc)).context("serializing eqc")?;
2786 serializable_retry!(self, || async {
2787 let mut tx = self.db.write().await?;
2788 tx.upsert("eqc", ["id", "data"], ["id"], [(true, eqc_bytes.clone())])
2789 .await?;
2790 tx.commit().await
2791 })
2792 .await
2793 }
2794
2795 async fn load_eqc(
2796 &self,
2797 ) -> Option<(
2798 QuorumCertificate2<SeqTypes>,
2799 NextEpochQuorumCertificate2<SeqTypes>,
2800 )> {
2801 let result = self
2802 .db
2803 .read()
2804 .await
2805 .ok()?
2806 .fetch_optional("SELECT * FROM eqc where id = true")
2807 .await
2808 .ok()?;
2809
2810 result
2811 .map(|row| {
2812 let bytes: Vec<u8> = row.get("data");
2813 bincode::deserialize(&bytes)
2814 })
2815 .transpose()
2816 .ok()?
2817 }
2818
2819 async fn append_da2(
2820 &self,
2821 proposal: &Proposal<SeqTypes, DaProposal2<SeqTypes>>,
2822 vid_commit: VidCommitment,
2823 ) -> anyhow::Result<()> {
2824 let data = &proposal.data;
2825 let view = data.view_number().u64();
2826 let data_bytes = bincode::serialize(proposal).unwrap();
2827
2828 let now = Instant::now();
2829 let res = serializable_retry!(self, || async {
2830 let mut tx = self.db.write().await?;
2831 tx.upsert(
2832 "da_proposal2",
2833 ["view", "data", "payload_hash"],
2834 ["view"],
2835 [(view as i64, data_bytes.clone(), vid_commit.to_string())],
2836 )
2837 .await?;
2838 tx.commit().await
2839 })
2840 .await;
2841 self.internal_metrics
2842 .internal_append_da2_duration
2843 .add_point(now.elapsed().as_secs_f64());
2844 res
2845 }
2846
2847 async fn store_drb_result(
2848 &self,
2849 epoch: EpochNumber,
2850 drb_result: DrbResult,
2851 ) -> anyhow::Result<()> {
2852 let epoch_i64 = epoch.u64() as i64;
2853 let drb_result_vec = Vec::from(drb_result);
2854 serializable_retry!(self, || async {
2855 let mut tx = self.db.write().await?;
2856 tx.upsert(
2857 "epoch_drb_and_root",
2858 ["epoch", "drb_result"],
2859 ["epoch"],
2860 [(epoch_i64, drb_result_vec.clone())],
2861 )
2862 .await?;
2863 tx.commit().await
2864 })
2865 .await
2866 }
2867
2868 async fn store_epoch_root(
2869 &self,
2870 epoch: EpochNumber,
2871 block_header: <SeqTypes as NodeType>::BlockHeader,
2872 ) -> anyhow::Result<()> {
2873 let epoch_i64 = epoch.u64() as i64;
2874 let block_header_bytes =
2875 bincode::serialize(&block_header).context("serializing block header")?;
2876
2877 serializable_retry!(self, || async {
2878 let mut tx = self.db.write().await?;
2879 tx.upsert(
2880 "epoch_drb_and_root",
2881 ["epoch", "block_header"],
2882 ["epoch"],
2883 [(epoch_i64, block_header_bytes.clone())],
2884 )
2885 .await?;
2886 tx.commit().await
2887 })
2888 .await
2889 }
2890
2891 async fn store_drb_input(&self, drb_input: DrbInput) -> anyhow::Result<()> {
2892 if let Ok(loaded_drb_input) = self.load_drb_input(drb_input.epoch).await {
2893 if loaded_drb_input.difficulty_level != drb_input.difficulty_level {
2894 tracing::error!("Overwriting {loaded_drb_input:?} in storage with {drb_input:?}");
2895 } else if loaded_drb_input.iteration >= drb_input.iteration {
2896 anyhow::bail!(
2897 "DrbInput in storage {:?} is more recent than {:?}, refusing to update",
2898 loaded_drb_input,
2899 drb_input
2900 )
2901 }
2902 }
2903
2904 let drb_epoch_i64 = drb_input.epoch as i64;
2905 let drb_input_bytes = bincode::serialize(&drb_input)
2906 .context("Failed to serialize DrbInput. This is not fatal, but should never happen.")?;
2907
2908 serializable_retry!(self, || async {
2909 let mut tx = self.db.write().await?;
2910 tx.upsert(
2911 "drb",
2912 ["epoch", "drb_input"],
2913 ["epoch"],
2914 [(drb_epoch_i64, drb_input_bytes.clone())],
2915 )
2916 .await?;
2917 tx.commit().await
2918 })
2919 .await
2920 }
2921
2922 async fn load_drb_input(&self, epoch: u64) -> anyhow::Result<DrbInput> {
2923 let row = self
2924 .db
2925 .read()
2926 .await?
2927 .fetch_optional(query("SELECT drb_input FROM drb WHERE epoch = $1").bind(epoch as i64))
2928 .await?;
2929
2930 match row {
2931 None => anyhow::bail!("No DrbInput for epoch {} in storage", epoch),
2932 Some(row) => {
2933 let drb_input_bytes: Vec<u8> = row.try_get("drb_input")?;
2934 let drb_input = bincode::deserialize(&drb_input_bytes)
2935 .context("Failed to deserialize drb_input from storage")?;
2936
2937 Ok(drb_input)
2938 },
2939 }
2940 }
2941
2942 async fn add_state_cert(
2943 &self,
2944 state_cert: LightClientStateUpdateCertificateV2<SeqTypes>,
2945 ) -> anyhow::Result<()> {
2946 let view_number = state_cert.light_client_state.view_number as i64;
2947 let state_cert_bytes = bincode::serialize(&state_cert)
2948 .context("serializing light client state update certificate")?;
2949
2950 serializable_retry!(self, || async {
2951 let mut tx = self.db.write().await?;
2952 tx.upsert(
2953 "state_cert",
2954 ["view", "state_cert"],
2955 ["view"],
2956 [(view_number, state_cert_bytes.clone())],
2957 )
2958 .await?;
2959 tx.commit().await
2960 })
2961 .await
2962 }
2963
2964 async fn load_state_cert(
2965 &self,
2966 ) -> anyhow::Result<Option<LightClientStateUpdateCertificateV2<SeqTypes>>> {
2967 let Some(row) = self
2968 .db
2969 .read()
2970 .await?
2971 .fetch_optional(
2972 "SELECT state_cert FROM finalized_state_cert ORDER BY epoch DESC LIMIT 1",
2973 )
2974 .await?
2975 else {
2976 return Ok(None);
2977 };
2978 let bytes: Vec<u8> = row.get("state_cert");
2979
2980 let cert = match bincode::deserialize(&bytes) {
2981 Ok(cert) => cert,
2982 Err(err) => {
2983 tracing::info!(
2984 error = %err,
2985 "Failed to deserialize state certificate with v2. attempting with v1"
2986 );
2987
2988 let v1_cert =
2989 bincode::deserialize::<LightClientStateUpdateCertificateV1<SeqTypes>>(&bytes)
2990 .with_context(|| {
2991 format!("Failed to deserialize using both v1 and v2. error: {err}")
2992 })?;
2993
2994 v1_cert.into()
2995 },
2996 };
2997
2998 Ok(Some(cert))
2999 }
3000
3001 async fn get_state_cert_by_epoch(
3002 &self,
3003 epoch: u64,
3004 ) -> anyhow::Result<Option<LightClientStateUpdateCertificateV2<SeqTypes>>> {
3005 let Some(row) = self
3006 .db
3007 .read()
3008 .await?
3009 .fetch_optional(
3010 query("SELECT state_cert FROM finalized_state_cert WHERE epoch = $1")
3011 .bind(epoch as i64),
3012 )
3013 .await?
3014 else {
3015 return Ok(None);
3016 };
3017 let bytes: Vec<u8> = row.get("state_cert");
3018
3019 let cert = match bincode::deserialize(&bytes) {
3020 Ok(cert) => cert,
3021 Err(err) => {
3022 tracing::info!(
3023 error = %err,
3024 "Failed to deserialize state certificate with v2. attempting with v1"
3025 );
3026
3027 let v1_cert =
3028 bincode::deserialize::<LightClientStateUpdateCertificateV1<SeqTypes>>(&bytes)
3029 .with_context(|| {
3030 format!("Failed to deserialize using both v1 and v2. error: {err}")
3031 })?;
3032
3033 v1_cert.into()
3034 },
3035 };
3036
3037 Ok(Some(cert))
3038 }
3039
3040 async fn insert_state_cert(
3041 &self,
3042 epoch: u64,
3043 cert: LightClientStateUpdateCertificateV2<SeqTypes>,
3044 ) -> anyhow::Result<()> {
3045 let epoch_i64 = epoch as i64;
3046 let bytes = bincode::serialize(&cert)
3047 .with_context(|| format!("Failed to serialize state cert for epoch {epoch}"))?;
3048
3049 serializable_retry!(self, || async {
3050 let mut tx = self.db.write().await?;
3051 tx.upsert(
3052 "finalized_state_cert",
3053 ["epoch", "state_cert"],
3054 ["epoch"],
3055 [(epoch_i64, bytes.clone())],
3056 )
3057 .await?;
3058 tx.commit().await
3059 })
3060 .await
3061 }
3062
3063 async fn load_start_epoch_info(&self) -> anyhow::Result<Vec<InitializerEpochInfo<SeqTypes>>> {
3064 let rows = self
3065 .db
3066 .read()
3067 .await?
3068 .fetch_all(
3069 query("SELECT * from epoch_drb_and_root ORDER BY epoch DESC LIMIT $1")
3070 .bind(RECENT_STAKE_TABLES_LIMIT as i64),
3071 )
3072 .await?;
3073
3074 rows.into_iter()
3076 .rev()
3077 .map(|row| {
3078 let epoch: i64 = row.try_get("epoch")?;
3079 let drb_result: Option<Vec<u8>> = row.try_get("drb_result")?;
3080 let block_header: Option<Vec<u8>> = row.try_get("block_header")?;
3081 if let Some(drb_result) = drb_result {
3082 let drb_result_array = drb_result
3083 .try_into()
3084 .or_else(|_| bail!("invalid drb result"))?;
3085 let block_header: Option<<SeqTypes as NodeType>::BlockHeader> = block_header
3086 .map(|data| bincode::deserialize(&data))
3087 .transpose()?;
3088 Ok(Some(InitializerEpochInfo::<SeqTypes> {
3089 epoch: EpochNumber::new(epoch as u64),
3090 drb_result: drb_result_array,
3091 block_header,
3092 }))
3093 } else {
3094 Ok(None)
3097 }
3098 })
3099 .filter_map(|e| match e {
3100 Err(v) => Some(Err(v)),
3101 Ok(Some(v)) => Some(Ok(v)),
3102 Ok(None) => None,
3103 })
3104 .collect()
3105 }
3106
3107 fn enable_metrics(&mut self, metrics: &dyn Metrics) {
3108 self.internal_metrics = PersistenceMetricsValue::new(metrics);
3109 }
3110}
3111
3112fn deserialize_authenticated_validator_map(
3113 bytes: &[u8],
3114) -> anyhow::Result<AuthenticatedValidatorMap> {
3115 if let Ok(map) = bincode::deserialize::<AuthenticatedValidatorMap>(bytes) {
3116 return Ok(map);
3117 }
3118
3119 if let Ok(pre_schnorr) =
3121 bincode::deserialize::<IndexMap<Address, super::RegisteredValidatorPreSchnorrOption>>(bytes)
3122 {
3123 return pre_schnorr
3124 .into_iter()
3125 .map(|(addr, v)| {
3126 let registered = v.migrate();
3127 let authenticated = AuthenticatedValidator::try_from(registered)?;
3128 Ok((addr, authenticated))
3129 })
3130 .collect();
3131 }
3132
3133 let legacy: IndexMap<Address, super::RegisteredValidatorPreOption> =
3135 bincode::deserialize(bytes).context("deserializing stake table")?;
3136 legacy
3137 .into_iter()
3138 .map(|(addr, v)| {
3139 let registered = v.migrate();
3140 let authenticated = AuthenticatedValidator::try_from(registered)?;
3141 Ok((addr, authenticated))
3142 })
3143 .collect()
3144}
3145
3146#[async_trait]
3147impl MembershipPersistence for Persistence {
3148 async fn load_stake(&self, epoch: EpochNumber) -> anyhow::Result<Option<StakeTuple>> {
3149 let result = self
3150 .db
3151 .read()
3152 .await?
3153 .fetch_optional(
3154 query(
3155 "SELECT stake, block_reward, stake_table_hash FROM epoch_drb_and_root WHERE \
3156 epoch = $1",
3157 )
3158 .bind(epoch.u64() as i64),
3159 )
3160 .await?;
3161
3162 result
3163 .map(|row| {
3164 let stake_table_bytes: Vec<u8> = row.get("stake");
3165 let reward_bytes: Option<Vec<u8>> = row.get("block_reward");
3166 let stake_table_hash_bytes: Option<Vec<u8>> = row.get("stake_table_hash");
3167 let stake_table = deserialize_authenticated_validator_map(&stake_table_bytes)?;
3168 let reward: Option<RewardAmount> = reward_bytes
3169 .map(|b| bincode::deserialize(&b).context("deserializing block_reward"))
3170 .transpose()?;
3171 let stake_table_hash: Option<StakeTableHash> = stake_table_hash_bytes
3172 .map(|b| bincode::deserialize(&b).context("deserializing stake table hash"))
3173 .transpose()?;
3174
3175 Ok((stake_table, reward, stake_table_hash))
3176 })
3177 .transpose()
3178 }
3179
3180 async fn load_latest_stake(&self, limit: u64) -> anyhow::Result<Option<Vec<IndexedStake>>> {
3181 let mut tx = self.db.read().await?;
3182
3183 let rows = match query_as::<(i64, Vec<u8>, Option<Vec<u8>>, Option<Vec<u8>>)>(
3184 "SELECT epoch, stake, block_reward, stake_table_hash FROM epoch_drb_and_root WHERE \
3185 stake is NOT NULL ORDER BY epoch DESC LIMIT $1",
3186 )
3187 .bind(limit as i64)
3188 .fetch_all(tx.as_mut())
3189 .await
3190 {
3191 Ok(bytes) => bytes,
3192 Err(err) => {
3193 tracing::error!("error loading stake tables: {err:#}");
3194 bail!("{err:#}");
3195 },
3196 };
3197
3198 let stakes: anyhow::Result<Vec<IndexedStake>> = rows
3199 .into_iter()
3200 .map(
3201 |(id, stake_bytes, reward_bytes_opt, stake_table_hash_bytes_opt)| {
3202 let stake_table = deserialize_authenticated_validator_map(&stake_bytes)?;
3203
3204 let block_reward: Option<RewardAmount> = reward_bytes_opt
3205 .map(|b| bincode::deserialize(&b).context("deserializing block_reward"))
3206 .transpose()?;
3207
3208 let stake_table_hash: Option<StakeTableHash> = stake_table_hash_bytes_opt
3209 .map(|b| bincode::deserialize(&b).context("deserializing stake table hash"))
3210 .transpose()?;
3211
3212 Ok((
3213 EpochNumber::new(id as u64),
3214 (stake_table, block_reward),
3215 stake_table_hash,
3216 ))
3217 },
3218 )
3219 .collect();
3220
3221 Ok(Some(stakes?))
3222 }
3223
3224 async fn store_stake(
3225 &self,
3226 epoch: EpochNumber,
3227 stake: AuthenticatedValidatorMap,
3228 block_reward: Option<RewardAmount>,
3229 stake_table_hash: Option<StakeTableHash>,
3230 ) -> anyhow::Result<()> {
3231 let epoch_i64 = epoch.u64() as i64;
3232 let stake_table_bytes = bincode::serialize(&stake).context("serializing stake table")?;
3233 let reward_bytes = block_reward
3234 .map(|r| bincode::serialize(&r).context("serializing block reward"))
3235 .transpose()?;
3236 let stake_table_hash_bytes = stake_table_hash
3237 .map(|h| bincode::serialize(&h).context("serializing stake table hash"))
3238 .transpose()?;
3239 serializable_retry!(self, || async {
3240 let mut tx = self.db.write().await?;
3241 tx.upsert(
3242 "epoch_drb_and_root",
3243 ["epoch", "stake", "block_reward", "stake_table_hash"],
3244 ["epoch"],
3245 [(
3246 epoch_i64,
3247 stake_table_bytes.clone(),
3248 reward_bytes.clone(),
3249 stake_table_hash_bytes.clone(),
3250 )],
3251 )
3252 .await?;
3253 tx.commit().await
3254 })
3255 .await
3256 }
3257
3258 async fn store_events(
3259 &self,
3260 l1_finalized: u64,
3261 events: Vec<(EventKey, StakeTableEvent)>,
3262 ) -> anyhow::Result<()> {
3263 let l1_finalized_i64: i64 = l1_finalized.try_into()?;
3264 let serialized_events = events
3265 .into_iter()
3266 .map(|((block_number, index), event)| {
3267 Ok((
3268 i64::try_from(block_number)?,
3269 i64::try_from(index)?,
3270 serde_json::to_value(event).context("l1 event to value")?,
3271 ))
3272 })
3273 .collect::<anyhow::Result<Vec<_>>>()?;
3274
3275 serializable_retry!(self, || async {
3276 let mut tx = self.db.write().await?;
3277
3278 let last_processed_l1_block = query_as::<(i64,)>(
3280 "SELECT last_l1_block FROM stake_table_events_l1_block where id = 0",
3281 )
3282 .fetch_optional(tx.as_mut())
3283 .await?
3284 .map(|(l1,)| l1);
3285
3286 tracing::debug!("last l1 finalizes in database = {last_processed_l1_block:?}");
3287
3288 let serialized_events_len = serialized_events.len();
3290 if last_processed_l1_block > Some(l1_finalized_i64) {
3291 tracing::debug!(
3292 ?last_processed_l1_block,
3293 l1_finalized,
3294 serialized_events_len,
3295 "last l1 finalized stored is already higher"
3296 );
3297 return Ok(());
3298 }
3299
3300 if !serialized_events.is_empty() {
3301 let mut query_builder: sqlx::QueryBuilder<Db> = sqlx::QueryBuilder::new(
3302 "INSERT INTO stake_table_events (l1_block, log_index, event) ",
3303 );
3304
3305 query_builder.push_values(
3306 serialized_events.iter().cloned(),
3307 |mut b, (l1_block, log_index, event)| {
3308 b.push_bind(l1_block).push_bind(log_index).push_bind(event);
3309 },
3310 );
3311
3312 query_builder.push(" ON CONFLICT DO NOTHING");
3313 let query = query_builder.build();
3314
3315 query.execute(tx.as_mut()).await?;
3316 }
3317
3318 tx.upsert(
3320 "stake_table_events_l1_block",
3321 ["id", "last_l1_block"],
3322 ["id"],
3323 [(0_i32, l1_finalized_i64)],
3324 )
3325 .await?;
3326
3327 tx.commit().await?;
3328
3329 Ok(())
3330 })
3331 .await
3332 }
3333
3334 async fn load_events(
3345 &self,
3346 from_l1_block: u64,
3347 to_l1_block: u64,
3348 ) -> anyhow::Result<(
3349 Option<EventsPersistenceRead>,
3350 Vec<(EventKey, StakeTableEvent)>,
3351 )> {
3352 let mut tx = self.db.read().await?;
3353
3354 let res = query_as::<(i64,)>(
3356 "SELECT last_l1_block FROM stake_table_events_l1_block where id = 0",
3357 )
3358 .fetch_optional(tx.as_mut())
3359 .await?;
3360
3361 let Some((last_processed_l1_block,)) = res else {
3362 return Ok((None, Vec::new()));
3364 };
3365
3366 let to_l1_block = to_l1_block.try_into()?;
3370 let query_l1_block = if last_processed_l1_block > to_l1_block {
3371 to_l1_block
3372 } else {
3373 last_processed_l1_block
3374 };
3375
3376 let rows = query(
3377 "SELECT l1_block, log_index, event FROM stake_table_events WHERE $1 <= l1_block AND \
3378 l1_block <= $2 ORDER BY l1_block ASC, log_index ASC",
3379 )
3380 .bind(i64::try_from(from_l1_block)?)
3381 .bind(query_l1_block)
3382 .fetch_all(tx.as_mut())
3383 .await?;
3384
3385 let events = rows
3386 .into_iter()
3387 .map(|row| {
3388 let l1_block: i64 = row.try_get("l1_block")?;
3389 let log_index: i64 = row.try_get("log_index")?;
3390 let event = serde_json::from_value(row.try_get("event")?)?;
3391
3392 Ok(((l1_block.try_into()?, log_index.try_into()?), event))
3393 })
3394 .collect::<anyhow::Result<Vec<_>>>()?;
3395
3396 if query_l1_block == to_l1_block {
3400 Ok((Some(EventsPersistenceRead::Complete), events))
3401 } else {
3402 Ok((
3403 Some(EventsPersistenceRead::UntilL1Block(
3404 query_l1_block.try_into()?,
3405 )),
3406 events,
3407 ))
3408 }
3409 }
3410
3411 async fn delete_stake_tables(&self) -> anyhow::Result<()> {
3412 serializable_retry!(self, || async {
3413 let mut tx = self.db.write().await?;
3414 #[cfg(not(feature = "embedded-db"))]
3415 query(
3416 "TRUNCATE stake_table_events, stake_table_events_l1_block, epoch_drb_and_root, \
3417 stake_table_validators",
3418 )
3419 .execute(tx.as_mut())
3420 .await?;
3421 #[cfg(feature = "embedded-db")]
3422 {
3423 query("DELETE FROM stake_table_events")
3424 .execute(tx.as_mut())
3425 .await?;
3426 query("DELETE FROM stake_table_events_l1_block")
3427 .execute(tx.as_mut())
3428 .await?;
3429 query("DELETE FROM epoch_drb_and_root")
3430 .execute(tx.as_mut())
3431 .await?;
3432 query("DELETE FROM stake_table_validators")
3433 .execute(tx.as_mut())
3434 .await?;
3435 }
3436 tx.commit().await?;
3437 Ok(())
3438 })
3439 .await
3440 }
3441
3442 async fn store_all_validators(
3443 &self,
3444 epoch: EpochNumber,
3445 all_validators: RegisteredValidatorMap,
3446 ) -> anyhow::Result<()> {
3447 if all_validators.is_empty() {
3448 return Ok(());
3449 }
3450
3451 let epoch_i64 = epoch.u64() as i64;
3452 let serialized_validators = all_validators
3453 .into_iter()
3454 .map(|(address, validator)| {
3455 let validator_json =
3456 serde_json::to_value(&validator).context("serializing validator to json")?;
3457 Ok((address.to_string(), validator_json))
3458 })
3459 .collect::<anyhow::Result<Vec<_>>>()?;
3460
3461 serializable_retry!(self, || async {
3462 let mut tx = self.db.write().await?;
3463
3464 let mut query_builder = QueryBuilder::new(
3465 "INSERT INTO stake_table_validators (epoch, address, validator) ",
3466 );
3467
3468 query_builder.push_values(
3469 serialized_validators.iter().cloned(),
3470 |mut b, (address, validator)| {
3471 b.push_bind(epoch_i64)
3472 .push_bind(address)
3473 .push_bind(validator);
3474 },
3475 );
3476
3477 query_builder
3478 .push(" ON CONFLICT (epoch, address) DO UPDATE SET validator = EXCLUDED.validator");
3479
3480 let query = query_builder.build();
3481
3482 query.execute(tx.as_mut()).await?;
3483
3484 tx.commit().await?;
3485 Ok(())
3486 })
3487 .await
3488 }
3489
3490 async fn load_all_validators(
3491 &self,
3492 epoch: EpochNumber,
3493 offset: u64,
3494 limit: u64,
3495 ) -> anyhow::Result<Vec<RegisteredValidator<PubKey>>> {
3496 let mut tx = self.db.read().await?;
3497
3498 let rows = query(
3502 "SELECT address, validator
3503 FROM stake_table_validators
3504 WHERE epoch = $1
3505 ORDER BY LOWER(address) ASC
3506 LIMIT $2 OFFSET $3",
3507 )
3508 .bind(epoch.u64() as i64)
3509 .bind(limit as i64)
3510 .bind(offset as i64)
3511 .fetch_all(tx.as_mut())
3512 .await?;
3513 rows.into_iter()
3514 .map(|row| {
3515 let validator_json: serde_json::Value = row.try_get("validator")?;
3516 serde_json::from_value::<RegisteredValidator<PubKey>>(validator_json)
3517 .map_err(Into::into)
3518 })
3519 .collect()
3520 }
3521}
3522
3523#[async_trait]
3524impl DhtPersistentStorage for Persistence {
3525 async fn save(&self, records: Vec<SerializableRecord>) -> anyhow::Result<()> {
3531 let to_save =
3533 bincode::serialize(&records).with_context(|| "failed to serialize records")?;
3534
3535 let stmt = "INSERT INTO libp2p_dht (id, serialized_records) VALUES (0, $1) ON CONFLICT \
3537 (id) DO UPDATE SET serialized_records = $1";
3538
3539 serializable_retry!(self, || async {
3540 let mut tx = self
3542 .db
3543 .write()
3544 .await
3545 .with_context(|| "failed to start an atomic DB transaction")?;
3546 tx.execute(query(stmt).bind(to_save.clone()))
3547 .await
3548 .with_context(|| "failed to execute DB query")?;
3549
3550 tx.commit().await.with_context(|| "failed to commit to DB")
3552 })
3553 .await
3554 }
3555
3556 async fn load(&self) -> anyhow::Result<Vec<SerializableRecord>> {
3562 let result = self
3564 .db
3565 .read()
3566 .await
3567 .with_context(|| "failed to start a DB read transaction")?
3568 .fetch_one("SELECT * FROM libp2p_dht where id = 0")
3569 .await
3570 .with_context(|| "failed to fetch from DB")?;
3571
3572 let serialied_records: Vec<u8> = result.get("serialized_records");
3574
3575 let records: Vec<SerializableRecord> = bincode::deserialize(&serialied_records)
3577 .with_context(|| "Failed to deserialize records")?;
3578
3579 Ok(records)
3580 }
3581}
3582
3583#[async_trait]
3584impl Provider<SeqTypes, VidCommonRequest> for Persistence {
3585 #[tracing::instrument(skip(self))]
3586 async fn fetch(&self, req: VidCommonRequest) -> Option<VidCommon> {
3587 let mut tx = match self.db.read().await {
3588 Ok(tx) => tx,
3589 Err(err) => {
3590 tracing::warn!("could not open transaction: {err:#}");
3591 return None;
3592 },
3593 };
3594
3595 let bytes = match query_as::<(Vec<u8>,)>(
3596 "SELECT data FROM vid_share2 WHERE payload_hash = $1 LIMIT 1",
3597 )
3598 .bind(req.0.to_string())
3599 .fetch_optional(tx.as_mut())
3600 .await
3601 {
3602 Ok(Some((bytes,))) => bytes,
3603 Ok(None) => return None,
3604 Err(err) => {
3605 tracing::error!("error loading VID share: {err:#}");
3606 return None;
3607 },
3608 };
3609
3610 let share: Proposal<SeqTypes, VidDisperseShare<SeqTypes>> =
3611 match bincode::deserialize(&bytes) {
3612 Ok(share) => share,
3613 Err(err) => {
3614 tracing::warn!("error decoding VID share: {err:#}");
3615 return None;
3616 },
3617 };
3618
3619 match share.data {
3620 VidDisperseShare::V0(vid) => Some(VidCommon::V0(vid.common)),
3621 VidDisperseShare::V1(vid) => Some(VidCommon::V1(vid.common)),
3622 VidDisperseShare::V2(vid) => Some(VidCommon::V2(vid.common)),
3623 }
3624 }
3625}
3626
3627#[async_trait]
3628impl Provider<SeqTypes, PayloadRequest> for Persistence {
3629 #[tracing::instrument(skip(self))]
3630 async fn fetch(&self, req: PayloadRequest) -> Option<Payload> {
3631 let mut tx = match self.db.read().await {
3632 Ok(tx) => tx,
3633 Err(err) => {
3634 tracing::warn!("could not open transaction: {err:#}");
3635 return None;
3636 },
3637 };
3638
3639 let bytes = match query_as::<(Vec<u8>,)>(
3640 "SELECT data FROM da_proposal2 WHERE payload_hash = $1 LIMIT 1",
3641 )
3642 .bind(req.0.to_string())
3643 .fetch_optional(tx.as_mut())
3644 .await
3645 {
3646 Ok(Some((bytes,))) => bytes,
3647 Ok(None) => return None,
3648 Err(err) => {
3649 tracing::warn!("error loading DA proposal: {err:#}");
3650 return None;
3651 },
3652 };
3653
3654 let proposal: Proposal<SeqTypes, DaProposal2<SeqTypes>> = match bincode::deserialize(&bytes)
3655 {
3656 Ok(proposal) => proposal,
3657 Err(err) => {
3658 tracing::error!("error decoding DA proposal: {err:#}");
3659 return None;
3660 },
3661 };
3662
3663 Some(Payload::from_bytes(
3664 &proposal.data.encoded_transactions,
3665 &proposal.data.metadata,
3666 ))
3667 }
3668}
3669
3670#[cfg(test)]
3671mod testing {
3672 use hotshot_query_service::data_source::storage::sql::testing::TmpDb;
3673
3674 use super::*;
3675 use crate::persistence::tests::TestablePersistence;
3676
3677 #[async_trait]
3678 impl TestablePersistence for Persistence {
3679 type Storage = Arc<TmpDb>;
3680
3681 async fn tmp_storage() -> Self::Storage {
3682 Arc::new(TmpDb::init().await)
3683 }
3684
3685 #[allow(refining_impl_trait)]
3686 fn options(db: &Self::Storage) -> Options {
3687 #[cfg(not(feature = "embedded-db"))]
3688 {
3689 PostgresOptions {
3690 port: Some(db.port()),
3691 host: Some(db.host()),
3692 user: Some("postgres".into()),
3693 password: Some("password".into()),
3694 ..Default::default()
3695 }
3696 .into()
3697 }
3698
3699 #[cfg(feature = "embedded-db")]
3700 {
3701 SqliteOptions { path: db.path() }.into()
3702 }
3703 }
3704 }
3705}
3706
3707#[cfg(test)]
3708mod test {
3709 use committable::{Commitment, CommitmentBoundsArkless};
3710 use espresso_types::{Header, Leaf, NodeState, ValidatedState, traits::NullEventConsumer};
3711 use futures::stream::TryStreamExt;
3712 use hotshot_example_types::node_types::TEST_VERSIONS;
3713 use hotshot_types::{
3714 data::{
3715 EpochNumber, QuorumProposal2, ns_table::parse_ns_table,
3716 vid_disperse::AvidMDisperseShare,
3717 },
3718 message::convert_proposal,
3719 simple_certificate::QuorumCertificate,
3720 simple_vote::QuorumData,
3721 traits::{
3722 EncodeBytes,
3723 block_contents::{BlockHeader, GENESIS_VID_NUM_STORAGE_NODES},
3724 signature_key::SignatureKey,
3725 },
3726 utils::EpochTransitionIndicator,
3727 vid::{
3728 advz::advz_scheme,
3729 avidm::{AvidMScheme, init_avidm_param},
3730 },
3731 };
3732 use jf_advz::VidScheme;
3733
3734 use super::*;
3735 use crate::{BLSPubKey, PubKey, persistence::tests::TestablePersistence as _};
3736
3737 #[test_log::test(tokio::test(flavor = "multi_thread"))]
3738 async fn test_quorum_proposals_leaf_hash_migration() {
3739 let leaf: Leaf2 = Leaf::genesis(
3741 &ValidatedState::default(),
3742 &NodeState::mock(),
3743 TEST_VERSIONS.test.base,
3744 )
3745 .await
3746 .into();
3747 let privkey = BLSPubKey::generated_from_seed_indexed([0; 32], 1).1;
3748 let signature = PubKey::sign(&privkey, &[]).unwrap();
3749 let mut quorum_proposal = Proposal {
3750 data: QuorumProposal2::<SeqTypes> {
3751 epoch: None,
3752 block_header: leaf.block_header().clone(),
3753 view_number: ViewNumber::genesis(),
3754 justify_qc: QuorumCertificate::genesis(
3755 &ValidatedState::default(),
3756 &NodeState::mock(),
3757 TEST_VERSIONS.test,
3758 )
3759 .await
3760 .to_qc2(),
3761 upgrade_certificate: None,
3762 view_change_evidence: None,
3763 next_drb_result: None,
3764 next_epoch_justify_qc: None,
3765 state_cert: None,
3766 },
3767 signature,
3768 _pd: Default::default(),
3769 };
3770
3771 let qp1: Proposal<SeqTypes, QuorumProposal<SeqTypes>> =
3772 convert_proposal(quorum_proposal.clone());
3773
3774 quorum_proposal.data.view_number = ViewNumber::new(1);
3775
3776 let qp2: Proposal<SeqTypes, QuorumProposal<SeqTypes>> =
3777 convert_proposal(quorum_proposal.clone());
3778 let qps = [qp1, qp2];
3779
3780 let db = Persistence::tmp_storage().await;
3782 let persistence = Persistence::connect(&db).await;
3783 let mut tx = persistence.db.write().await.unwrap();
3784 let params = qps
3785 .iter()
3786 .map(|qp| {
3787 (
3788 qp.data.view_number.u64() as i64,
3789 bincode::serialize(&qp).unwrap(),
3790 )
3791 })
3792 .collect::<Vec<_>>();
3793 tx.upsert("quorum_proposals", ["view", "data"], ["view"], params)
3794 .await
3795 .unwrap();
3796 tx.commit().await.unwrap();
3797
3798 let persistence = Persistence::connect(&db).await;
3800 let mut tx = persistence.db.read().await.unwrap();
3801 let rows = tx
3802 .fetch("SELECT * FROM quorum_proposals ORDER BY view ASC")
3803 .try_collect::<Vec<_>>()
3804 .await
3805 .unwrap();
3806 assert_eq!(rows.len(), qps.len());
3807 for (row, qp) in rows.into_iter().zip(qps) {
3808 assert_eq!(row.get::<i64, _>("view"), qp.data.view_number.u64() as i64);
3809 assert_eq!(
3810 row.get::<Vec<u8>, _>("data"),
3811 bincode::serialize(&qp).unwrap()
3812 );
3813 assert_eq!(
3814 row.get::<String, _>("leaf_hash"),
3815 Committable::commit(&Leaf::from_quorum_proposal(&qp.data)).to_string()
3816 );
3817 }
3818 }
3819
3820 #[test_log::test(tokio::test(flavor = "multi_thread"))]
3821 async fn test_x25519_keys_migration() {
3822 use std::collections::HashMap;
3823
3824 use crate::persistence::RegisteredValidatorNoX25519;
3825
3826 let mut validator = RegisteredValidator::mock();
3827 validator.delegators.clear();
3828 validator.stake = alloy::primitives::U256::from(1000u64);
3829
3830 let epoch = 1i64;
3831 let address = validator.account;
3832 let legacy_bls_key = validator.stake_table_key.expect("mock has BLS key");
3833
3834 let state_ver_key_raw = validator
3835 .state_ver_key
3836 .clone()
3837 .expect("mock has valid Schnorr key");
3838
3839 let legacy = RegisteredValidatorNoX25519 {
3841 account: validator.account,
3842 stake_table_key: legacy_bls_key,
3843 state_ver_key: state_ver_key_raw.clone(),
3844 stake: validator.stake,
3845 commission: validator.commission,
3846 delegators: HashMap::new(),
3847 authenticated: true,
3848 };
3849
3850 let mut legacy_map: IndexMap<Address, RegisteredValidatorNoX25519> = IndexMap::new();
3852 legacy_map.insert(address, legacy);
3853 let stake_bytes = bincode::serialize(&legacy_map).unwrap();
3854
3855 let json_legacy = RegisteredValidatorNoX25519 {
3857 account: validator.account,
3858 stake_table_key: legacy_bls_key,
3859 state_ver_key: state_ver_key_raw,
3860 stake: validator.stake,
3861 commission: validator.commission,
3862 delegators: HashMap::new(),
3863 authenticated: true,
3864 };
3865 let validator_json = serde_json::to_value(&json_legacy).unwrap();
3866
3867 let db = Persistence::tmp_storage().await;
3868 let persistence = Persistence::connect(&db).await;
3869 let mut tx = persistence.db.write().await.unwrap();
3870
3871 tx.execute(
3872 query(
3873 "INSERT INTO stake_table_validators (epoch, address, validator) VALUES ($1, $2, \
3874 $3)",
3875 )
3876 .bind(epoch)
3877 .bind(format!("{:?}", address))
3878 .bind(&validator_json),
3879 )
3880 .await
3881 .unwrap();
3882
3883 tx.execute(
3884 query("INSERT INTO epoch_drb_and_root (epoch, stake) VALUES ($1, $2)")
3885 .bind(epoch)
3886 .bind(&stake_bytes),
3887 )
3888 .await
3889 .unwrap();
3890
3891 tx.execute(query(
3893 "UPDATE data_migrations SET completed = false, migrated_rows = 0 WHERE name = \
3894 'x25519_keys'",
3895 ))
3896 .await
3897 .unwrap();
3898
3899 tx.commit().await.unwrap();
3900
3901 {
3903 let mut tx = persistence.db.read().await.unwrap();
3904 let row: (serde_json::Value,) =
3905 query_as("SELECT validator FROM stake_table_validators WHERE epoch = $1")
3906 .bind(epoch)
3907 .fetch_one(tx.as_mut())
3908 .await
3909 .unwrap();
3910 let json_obj = row.0.as_object().unwrap();
3911 assert!(!json_obj.contains_key("x25519_key"));
3912 }
3913
3914 let persistence = Persistence::connect(&db).await;
3916 persistence.migrate_storage().await.unwrap();
3917
3918 {
3920 let mut tx = persistence.db.read().await.unwrap();
3921 let row: (serde_json::Value,) =
3922 query_as("SELECT validator FROM stake_table_validators WHERE epoch = $1")
3923 .bind(epoch)
3924 .fetch_one(tx.as_mut())
3925 .await
3926 .unwrap();
3927 let json_obj = row.0.as_object().unwrap();
3928 assert!(json_obj.contains_key("x25519_key"));
3929 assert!(json_obj.get("x25519_key").unwrap().is_null());
3930 }
3931
3932 {
3934 let mut tx = persistence.db.read().await.unwrap();
3935 let row: (Vec<u8>,) = query_as("SELECT stake FROM epoch_drb_and_root WHERE epoch = $1")
3936 .bind(epoch)
3937 .fetch_one(tx.as_mut())
3938 .await
3939 .unwrap();
3940 let migrated_map: AuthenticatedValidatorMap = bincode::deserialize(&row.0).unwrap();
3941 assert!(migrated_map.contains_key(&address));
3942 let v = migrated_map.get(&address).unwrap();
3943 assert!(v.x25519_key.is_none());
3944 }
3945
3946 {
3948 let mut tx = persistence.db.read().await.unwrap();
3949 let row: (bool, i64) = query_as(
3950 "SELECT completed, migrated_rows FROM data_migrations WHERE name = 'x25519_keys' \
3951 AND table_name = 'epoch_drb_and_root'",
3952 )
3953 .fetch_one(tx.as_mut())
3954 .await
3955 .unwrap();
3956 assert!(row.0);
3957 assert_eq!(row.1, 1);
3958 }
3959 }
3960
3961 fn pre_option_validator(seed: u8, stake: u64) -> super::super::RegisteredValidatorPreOption {
3962 use std::collections::HashMap;
3963
3964 use alloy::primitives::U256;
3965 use hotshot_types::light_client::StateVerKey;
3966
3967 super::super::RegisteredValidatorPreOption {
3968 account: Address::random(),
3969 stake_table_key: BLSPubKey::generated_from_seed_indexed([seed; 32], 0).0,
3970 state_ver_key: StateVerKey::default(),
3971 stake: U256::from(stake),
3972 commission: 0,
3973 delegators: HashMap::new(),
3974 authenticated: true,
3975 x25519_key: None,
3976 p2p_addr: None,
3977 }
3978 }
3979
3980 async fn insert_legacy_stake_row(
3981 persistence: &Persistence,
3982 epoch: i64,
3983 validator: super::super::RegisteredValidatorPreOption,
3984 ) {
3985 let mut map: IndexMap<Address, super::super::RegisteredValidatorPreOption> =
3986 IndexMap::new();
3987 map.insert(validator.account, validator);
3988 let stake_bytes = bincode::serialize(&map).unwrap();
3989 let mut tx = persistence.db.write().await.unwrap();
3990 tx.execute(
3991 query("INSERT INTO epoch_drb_and_root (epoch, stake) VALUES ($1, $2)")
3992 .bind(epoch)
3993 .bind(&stake_bytes),
3994 )
3995 .await
3996 .unwrap();
3997 tx.commit().await.unwrap();
3998 }
3999
4000 #[test_log::test(tokio::test(flavor = "multi_thread"))]
4001 async fn test_load_stake_legacy_storage() {
4002 let tmp = Persistence::tmp_storage().await;
4003 let persistence = Persistence::connect(&tmp).await;
4004
4005 let v1 = pre_option_validator(1, 100);
4006 let v2 = pre_option_validator(2, 200);
4007 let v1_addr = v1.account;
4008 let v2_addr = v2.account;
4009 insert_legacy_stake_row(&persistence, 1, v1).await;
4010 insert_legacy_stake_row(&persistence, 2, v2).await;
4011
4012 let (loaded1, ..) = persistence
4013 .load_stake(EpochNumber::new(1))
4014 .await
4015 .unwrap()
4016 .unwrap();
4017 assert_eq!(loaded1.len(), 1);
4018 assert!(loaded1.get(&v1_addr).unwrap().stake_table_key.is_some());
4019
4020 let (loaded2, ..) = persistence
4021 .load_stake(EpochNumber::new(2))
4022 .await
4023 .unwrap()
4024 .unwrap();
4025 assert_eq!(loaded2.len(), 1);
4026 assert!(loaded2.get(&v2_addr).unwrap().stake_table_key.is_some());
4027
4028 let latest = persistence.load_latest_stake(10).await.unwrap().unwrap();
4029 assert_eq!(latest.len(), 2);
4030 let epochs: Vec<_> = latest.iter().map(|(e, ..)| *e).collect();
4031 assert!(epochs.contains(&EpochNumber::new(1)));
4032 assert!(epochs.contains(&EpochNumber::new(2)));
4033 }
4034
4035 #[test_log::test(tokio::test(flavor = "multi_thread"))]
4036 async fn test_load_stake_mixed_storage() {
4037 let tmp = Persistence::tmp_storage().await;
4038 let persistence = Persistence::connect(&tmp).await;
4039
4040 let legacy_v = pre_option_validator(3, 300);
4041 let legacy_addr = legacy_v.account;
4042 insert_legacy_stake_row(&persistence, 5, legacy_v).await;
4043
4044 let current_v = espresso_types::v0_3::AuthenticatedValidator::mock();
4045 let current_addr = current_v.account;
4046 let mut current_map = IndexMap::new();
4047 current_map.insert(current_addr, current_v);
4048 persistence
4049 .store_stake(EpochNumber::new(6), current_map, None, None)
4050 .await
4051 .unwrap();
4052
4053 let latest = persistence.load_latest_stake(10).await.unwrap().unwrap();
4054 assert_eq!(latest.len(), 2);
4055 let by_epoch: std::collections::HashMap<_, _> = latest
4056 .into_iter()
4057 .map(|(e, (map, _), _)| (e, map))
4058 .collect();
4059 assert!(
4060 by_epoch
4061 .get(&EpochNumber::new(5))
4062 .unwrap()
4063 .contains_key(&legacy_addr)
4064 );
4065 assert!(
4066 by_epoch
4067 .get(&EpochNumber::new(6))
4068 .unwrap()
4069 .contains_key(¤t_addr)
4070 );
4071 }
4072
4073 #[test_log::test(tokio::test(flavor = "multi_thread"))]
4074 async fn test_store_all_validators_authenticated_and_unauthenticated() {
4075 use std::collections::HashMap;
4076
4077 use alloy::primitives::{Address, U256};
4078 use hotshot_types::light_client::StateVerKey;
4079 use indexmap::IndexMap;
4080
4081 let tmp = Persistence::tmp_storage().await;
4082 let storage = Persistence::connect(&tmp).await;
4083
4084 let authenticated_validator = RegisteredValidator {
4086 account: Address::random(),
4087 stake_table_key: Some(BLSPubKey::generated_from_seed_indexed([0u8; 32], 0).0),
4088 state_ver_key: Some(StateVerKey::default()),
4089 stake: U256::from(1000),
4090 commission: 100,
4091 delegators: HashMap::new(),
4092 authenticated: true,
4093 x25519_key: None,
4094 p2p_addr: None,
4095 };
4096
4097 let unauthenticated_validator = RegisteredValidator {
4099 account: Address::random(),
4100 stake_table_key: Some(BLSPubKey::generated_from_seed_indexed([0u8; 32], 1).0),
4101 state_ver_key: Some(StateVerKey::default()),
4102 stake: U256::from(2000),
4103 commission: 200,
4104 delegators: HashMap::new(),
4105 authenticated: false,
4106 x25519_key: None,
4107 p2p_addr: None,
4108 };
4109
4110 let mut validators: IndexMap<Address, RegisteredValidator<BLSPubKey>> = IndexMap::new();
4111 validators.insert(
4112 authenticated_validator.account,
4113 authenticated_validator.clone(),
4114 );
4115 validators.insert(
4116 unauthenticated_validator.account,
4117 unauthenticated_validator.clone(),
4118 );
4119
4120 storage
4122 .store_all_validators(EpochNumber::new(1), validators)
4123 .await
4124 .unwrap();
4125
4126 let loaded = storage
4128 .load_all_validators(EpochNumber::new(1), 0, 100)
4129 .await
4130 .unwrap();
4131 assert_eq!(loaded.len(), 2);
4132
4133 let loaded_auth = loaded
4135 .iter()
4136 .find(|v| v.account == authenticated_validator.account)
4137 .unwrap();
4138 assert!(
4139 loaded_auth.authenticated,
4140 "authenticated validator should remain authenticated"
4141 );
4142
4143 let loaded_unauth = loaded
4144 .iter()
4145 .find(|v| v.account == unauthenticated_validator.account)
4146 .unwrap();
4147 assert!(
4148 !loaded_unauth.authenticated,
4149 "unauthenticated validator should remain unauthenticated"
4150 );
4151 }
4152
4153 #[test_log::test(tokio::test(flavor = "multi_thread"))]
4154 async fn test_fetching_providers() {
4155 let tmp = Persistence::tmp_storage().await;
4156 let storage = Persistence::connect(&tmp).await;
4157
4158 let leaf = Leaf2::genesis(
4160 &ValidatedState::default(),
4161 &NodeState::mock(),
4162 TEST_VERSIONS.test.base,
4163 )
4164 .await;
4165 let leaf_payload = leaf.block_payload().unwrap();
4166 let leaf_payload_bytes_arc = leaf_payload.encode();
4167
4168 let avidm_param = init_avidm_param(2).unwrap();
4169 let weights = vec![1u32; 2];
4170
4171 let ns_table = parse_ns_table(
4172 leaf_payload.byte_len().as_usize(),
4173 &leaf_payload.ns_table().encode(),
4174 );
4175 let (payload_commitment, shares) =
4176 AvidMScheme::ns_disperse(&avidm_param, &weights, &leaf_payload_bytes_arc, ns_table)
4177 .unwrap();
4178 let (pubkey, privkey) = BLSPubKey::generated_from_seed_indexed([0; 32], 1);
4179 let vid_share = convert_proposal(
4180 AvidMDisperseShare::<SeqTypes> {
4181 view_number: ViewNumber::new(0),
4182 payload_commitment,
4183 share: shares[0].clone(),
4184 recipient_key: pubkey,
4185 epoch: None,
4186 target_epoch: None,
4187 common: avidm_param.clone(),
4188 }
4189 .to_proposal(&privkey)
4190 .unwrap()
4191 .clone(),
4192 );
4193
4194 let quorum_proposal = QuorumProposalWrapper::<SeqTypes> {
4195 proposal: QuorumProposal2::<SeqTypes> {
4196 block_header: leaf.block_header().clone(),
4197 view_number: leaf.view_number(),
4198 justify_qc: leaf.justify_qc(),
4199 upgrade_certificate: None,
4200 view_change_evidence: None,
4201 next_drb_result: None,
4202 next_epoch_justify_qc: None,
4203 epoch: None,
4204 state_cert: None,
4205 },
4206 };
4207 let quorum_proposal_signature =
4208 BLSPubKey::sign(&privkey, &bincode::serialize(&quorum_proposal).unwrap())
4209 .expect("Failed to sign quorum proposal");
4210 let quorum_proposal = Proposal {
4211 data: quorum_proposal,
4212 signature: quorum_proposal_signature,
4213 _pd: Default::default(),
4214 };
4215
4216 let block_payload_signature = BLSPubKey::sign(&privkey, &leaf_payload_bytes_arc)
4217 .expect("Failed to sign block payload");
4218 let da_proposal = Proposal {
4219 data: DaProposal2::<SeqTypes> {
4220 encoded_transactions: leaf_payload_bytes_arc,
4221 metadata: leaf_payload.ns_table().clone(),
4222 view_number: ViewNumber::new(0),
4223 epoch: None,
4224 epoch_transition_indicator: EpochTransitionIndicator::NotInTransition,
4225 },
4226 signature: block_payload_signature,
4227 _pd: Default::default(),
4228 };
4229
4230 let mut next_quorum_proposal = quorum_proposal.clone();
4231 next_quorum_proposal.data.proposal.view_number += 1;
4232 next_quorum_proposal.data.proposal.justify_qc.view_number += 1;
4233 next_quorum_proposal
4234 .data
4235 .proposal
4236 .justify_qc
4237 .data
4238 .leaf_commit = Committable::commit(&leaf.clone());
4239
4240 storage
4242 .append_da2(&da_proposal, VidCommitment::V1(payload_commitment))
4243 .await
4244 .unwrap();
4245 storage.append_vid(&vid_share).await.unwrap();
4246 storage
4247 .append_quorum_proposal2(&quorum_proposal)
4248 .await
4249 .unwrap();
4250
4251 storage
4253 .append_quorum_proposal2(&next_quorum_proposal)
4254 .await
4255 .unwrap();
4256
4257 assert_eq!(
4259 Some(VidCommon::V1(avidm_param)),
4260 storage
4261 .fetch(VidCommonRequest(vid_share.data.payload_commitment()))
4262 .await
4263 );
4264 assert_eq!(
4265 leaf_payload,
4266 storage
4267 .fetch(PayloadRequest(vid_share.data.payload_commitment()))
4268 .await
4269 .unwrap()
4270 );
4271 }
4272
4273 async fn test_pruning_helper(pruning_opt: ConsensusPruningOptions) {
4281 let tmp = Persistence::tmp_storage().await;
4282 let mut opt = Persistence::options(&tmp);
4283 opt.consensus_pruning = pruning_opt;
4284 let storage = opt.create().await.unwrap();
4285
4286 let data_view = ViewNumber::new(1);
4287
4288 let leaf = Leaf2::genesis(
4290 &ValidatedState::default(),
4291 &NodeState::mock(),
4292 TEST_VERSIONS.test.base,
4293 )
4294 .await;
4295 let leaf_payload = leaf.block_payload().unwrap();
4296 let leaf_payload_bytes_arc = leaf_payload.encode();
4297
4298 let avidm_param = init_avidm_param(2).unwrap();
4299 let weights = vec![1u32; 2];
4300
4301 let ns_table = parse_ns_table(
4302 leaf_payload.byte_len().as_usize(),
4303 &leaf_payload.ns_table().encode(),
4304 );
4305 let (payload_commitment, shares) =
4306 AvidMScheme::ns_disperse(&avidm_param, &weights, &leaf_payload_bytes_arc, ns_table)
4307 .unwrap();
4308
4309 let (pubkey, privkey) = BLSPubKey::generated_from_seed_indexed([0; 32], 1);
4310 let vid = convert_proposal(
4311 AvidMDisperseShare::<SeqTypes> {
4312 view_number: data_view,
4313 payload_commitment,
4314 share: shares[0].clone(),
4315 recipient_key: pubkey,
4316 epoch: None,
4317 target_epoch: None,
4318 common: avidm_param,
4319 }
4320 .to_proposal(&privkey)
4321 .unwrap()
4322 .clone(),
4323 );
4324 let quorum_proposal = QuorumProposalWrapper::<SeqTypes> {
4325 proposal: QuorumProposal2::<SeqTypes> {
4326 epoch: None,
4327 block_header: leaf.block_header().clone(),
4328 view_number: data_view,
4329 justify_qc: QuorumCertificate2::genesis(
4330 &ValidatedState::default(),
4331 &NodeState::mock(),
4332 TEST_VERSIONS.test,
4333 )
4334 .await,
4335 upgrade_certificate: None,
4336 view_change_evidence: None,
4337 next_drb_result: None,
4338 next_epoch_justify_qc: None,
4339 state_cert: None,
4340 },
4341 };
4342 let quorum_proposal_signature =
4343 BLSPubKey::sign(&privkey, &bincode::serialize(&quorum_proposal).unwrap())
4344 .expect("Failed to sign quorum proposal");
4345 let quorum_proposal = Proposal {
4346 data: quorum_proposal,
4347 signature: quorum_proposal_signature,
4348 _pd: Default::default(),
4349 };
4350
4351 let block_payload_signature = BLSPubKey::sign(&privkey, &leaf_payload_bytes_arc)
4352 .expect("Failed to sign block payload");
4353 let da_proposal = Proposal {
4354 data: DaProposal2::<SeqTypes> {
4355 encoded_transactions: leaf_payload_bytes_arc.clone(),
4356 metadata: leaf_payload.ns_table().clone(),
4357 view_number: data_view,
4358 epoch: Some(EpochNumber::new(0)),
4359 epoch_transition_indicator: EpochTransitionIndicator::NotInTransition,
4360 },
4361 signature: block_payload_signature,
4362 _pd: Default::default(),
4363 };
4364
4365 tracing::info!(?vid, ?da_proposal, ?quorum_proposal, "append data");
4366 storage.append_vid(&vid).await.unwrap();
4367 storage
4368 .append_da2(&da_proposal, VidCommitment::V1(payload_commitment))
4369 .await
4370 .unwrap();
4371 storage
4372 .append_quorum_proposal2(&quorum_proposal)
4373 .await
4374 .unwrap();
4375
4376 {
4379 let mut tx = storage.db.write().await.unwrap();
4380 tx.upsert(
4381 "state_cert",
4382 ["view", "state_cert"],
4383 ["view"],
4384 [(data_view.u64() as i64, b"state_cert".to_vec())],
4385 )
4386 .await
4387 .unwrap();
4388 tx.upsert(
4389 "decided_cert2",
4390 ["view", "data"],
4391 ["view"],
4392 [(data_view.u64() as i64, b"cert2".to_vec())],
4393 )
4394 .await
4395 .unwrap();
4396 tx.commit().await.unwrap();
4397 }
4398
4399 tracing::info!("decide view 1");
4402 storage
4403 .append_decided_leaves(data_view + 1, [], None, &NullEventConsumer)
4404 .await
4405 .unwrap();
4406 assert_eq!(
4407 storage.load_vid_share(data_view).await.unwrap().unwrap(),
4408 vid
4409 );
4410 assert_eq!(
4411 storage.load_da_proposal(data_view).await.unwrap().unwrap(),
4412 da_proposal
4413 );
4414 assert_eq!(
4415 storage.load_quorum_proposal(data_view).await.unwrap(),
4416 quorum_proposal
4417 );
4418 assert!(view_row_exists(&storage, "state_cert", data_view).await);
4419 assert!(view_row_exists(&storage, "decided_cert2", data_view).await);
4420
4421 tracing::info!("decide view 2");
4424 storage
4425 .append_decided_leaves(data_view + 2, [], None, &NullEventConsumer)
4426 .await
4427 .unwrap();
4428 assert!(storage.load_vid_share(data_view).await.unwrap().is_none(),);
4429 assert!(storage.load_da_proposal(data_view).await.unwrap().is_none());
4430 storage.load_quorum_proposal(data_view).await.unwrap_err();
4431 assert!(!view_row_exists(&storage, "state_cert", data_view).await);
4432 assert!(!view_row_exists(&storage, "decided_cert2", data_view).await);
4433 }
4434
4435 async fn view_row_exists(storage: &Persistence, table: &str, view: ViewNumber) -> bool {
4437 storage
4438 .db
4439 .read()
4440 .await
4441 .unwrap()
4442 .fetch_optional(
4443 query(&format!("SELECT view FROM {table} WHERE view = $1")).bind(view.u64() as i64),
4444 )
4445 .await
4446 .unwrap()
4447 .is_some()
4448 }
4449
4450 #[test_log::test(tokio::test(flavor = "multi_thread"))]
4451 async fn test_pruning_minimum_retention() {
4452 test_pruning_helper(ConsensusPruningOptions {
4453 target_usage: 0,
4456 minimum_retention: 1,
4457 target_retention: u64::MAX,
4460 })
4461 .await
4462 }
4463
4464 #[test_log::test(tokio::test(flavor = "multi_thread"))]
4465 async fn test_pruning_target_retention() {
4466 test_pruning_helper(ConsensusPruningOptions {
4467 target_retention: 1,
4468 minimum_retention: 0,
4471 target_usage: u64::MAX,
4474 })
4475 .await
4476 }
4477
4478 #[test_log::test(tokio::test(flavor = "multi_thread"))]
4479 async fn test_consensus_migration() {
4480 let tmp = Persistence::tmp_storage().await;
4481 let mut opt = Persistence::options(&tmp);
4482
4483 let storage = opt.create().await.unwrap();
4484
4485 let rows = 300;
4486
4487 assert!(storage.load_state_cert().await.unwrap().is_none());
4488
4489 for i in 0..rows {
4490 let view = ViewNumber::new(i);
4491 let validated_state = ValidatedState::default();
4492 let instance_state = NodeState::default();
4493
4494 let (pubkey, privkey) = BLSPubKey::generated_from_seed_indexed([0; 32], i);
4495 let (payload, metadata) =
4496 Payload::from_transactions([], &validated_state, &instance_state)
4497 .await
4498 .unwrap();
4499
4500 let payload_bytes = payload.encode();
4501
4502 let block_header = Header::genesis(
4503 &instance_state,
4504 payload.clone(),
4505 &metadata,
4506 TEST_VERSIONS.test.base,
4507 );
4508
4509 let null_quorum_data = QuorumData {
4510 leaf_commit: Commitment::<Leaf>::default_commitment_no_preimage(),
4511 };
4512
4513 let justify_qc = QuorumCertificate::new(
4514 null_quorum_data.clone(),
4515 null_quorum_data.commit(),
4516 view,
4517 None,
4518 std::marker::PhantomData,
4519 );
4520
4521 let quorum_proposal = QuorumProposal {
4522 block_header,
4523 view_number: view,
4524 justify_qc: justify_qc.clone(),
4525 upgrade_certificate: None,
4526 proposal_certificate: None,
4527 };
4528
4529 let quorum_proposal_signature =
4530 BLSPubKey::sign(&privkey, &bincode::serialize(&quorum_proposal).unwrap())
4531 .expect("Failed to sign quorum proposal");
4532
4533 let proposal = Proposal {
4534 data: quorum_proposal.clone(),
4535 signature: quorum_proposal_signature,
4536 _pd: std::marker::PhantomData::<SeqTypes>,
4537 };
4538
4539 let proposal_bytes = bincode::serialize(&proposal)
4540 .context("serializing proposal")
4541 .unwrap();
4542
4543 let mut leaf = Leaf::from_quorum_proposal(&quorum_proposal);
4544 leaf.fill_block_payload(
4545 payload,
4546 GENESIS_VID_NUM_STORAGE_NODES,
4547 TEST_VERSIONS.test.base,
4548 )
4549 .unwrap();
4550
4551 let mut tx = storage.db.write().await.unwrap();
4552
4553 let qc_bytes = bincode::serialize(&justify_qc).unwrap();
4554 let leaf_bytes = bincode::serialize(&leaf).unwrap();
4555
4556 tx.upsert(
4557 "anchor_leaf",
4558 ["view", "leaf", "qc"],
4559 ["view"],
4560 [(i as i64, leaf_bytes, qc_bytes)],
4561 )
4562 .await
4563 .unwrap();
4564
4565 let state_cert = LightClientStateUpdateCertificateV2::<SeqTypes> {
4566 epoch: EpochNumber::new(i),
4567 light_client_state: Default::default(), next_stake_table_state: Default::default(), signatures: vec![], auth_root: Default::default(),
4571 };
4572 let state_cert_bytes = bincode::serialize(&state_cert).unwrap();
4574 tx.upsert(
4575 "finalized_state_cert",
4576 ["epoch", "state_cert"],
4577 ["epoch"],
4578 [(i as i64, state_cert_bytes)],
4579 )
4580 .await
4581 .unwrap();
4582
4583 tx.commit().await.unwrap();
4584
4585 let disperse = advz_scheme(GENESIS_VID_NUM_STORAGE_NODES)
4586 .disperse(payload_bytes.clone())
4587 .unwrap();
4588
4589 let vid = VidDisperseShare0::<SeqTypes> {
4590 view_number: ViewNumber::new(i),
4591 payload_commitment: Default::default(),
4592 share: disperse.shares[0].clone(),
4593 common: disperse.common,
4594 recipient_key: pubkey,
4595 };
4596
4597 let (payload, metadata) =
4598 Payload::from_transactions([], &ValidatedState::default(), &NodeState::default())
4599 .await
4600 .unwrap();
4601
4602 let da = DaProposal::<SeqTypes> {
4603 encoded_transactions: payload.encode(),
4604 metadata,
4605 view_number: ViewNumber::new(i),
4606 };
4607
4608 let block_payload_signature =
4609 BLSPubKey::sign(&privkey, &payload_bytes).expect("Failed to sign block payload");
4610
4611 let da_proposal = Proposal {
4612 data: da,
4613 signature: block_payload_signature,
4614 _pd: Default::default(),
4615 };
4616
4617 storage
4618 .append_vid(&convert_proposal(vid.to_proposal(&privkey).unwrap()))
4619 .await
4620 .unwrap();
4621 storage
4622 .append_da(&da_proposal, VidCommitment::V0(disperse.commit))
4623 .await
4624 .unwrap();
4625
4626 let leaf_hash = Committable::commit(&leaf);
4627 let mut tx = storage.db.write().await.expect("failed to start write tx");
4628 tx.upsert(
4629 "quorum_proposals",
4630 ["view", "leaf_hash", "data"],
4631 ["view"],
4632 [(i as i64, leaf_hash.to_string(), proposal_bytes)],
4633 )
4634 .await
4635 .expect("failed to upsert quorum proposal");
4636
4637 let justify_qc = &proposal.data.justify_qc;
4638 let justify_qc_bytes = bincode::serialize(&justify_qc)
4639 .context("serializing QC")
4640 .unwrap();
4641 tx.upsert(
4642 "quorum_certificate",
4643 ["view", "leaf_hash", "data"],
4644 ["view"],
4645 [(
4646 justify_qc.view_number.u64() as i64,
4647 justify_qc.data.leaf_commit.to_string(),
4648 &justify_qc_bytes,
4649 )],
4650 )
4651 .await
4652 .expect("failed to upsert qc");
4653
4654 tx.commit().await.expect("failed to commit");
4655 }
4656
4657 storage.migrate_storage().await.unwrap();
4658
4659 let mut tx = storage.db.read().await.unwrap();
4660 let (anchor_leaf2_count,) = query_as::<(i64,)>("SELECT COUNT(*) from anchor_leaf2")
4661 .fetch_one(tx.as_mut())
4662 .await
4663 .unwrap();
4664 assert_eq!(
4665 anchor_leaf2_count, rows as i64,
4666 "anchor leaf count does not match rows",
4667 );
4668
4669 let (da_proposal_count,) = query_as::<(i64,)>("SELECT COUNT(*) from da_proposal2")
4670 .fetch_one(tx.as_mut())
4671 .await
4672 .unwrap();
4673 assert_eq!(
4674 da_proposal_count, rows as i64,
4675 "da proposal count does not match rows",
4676 );
4677
4678 let (vid_share_count,) = query_as::<(i64,)>("SELECT COUNT(*) from vid_share2")
4679 .fetch_one(tx.as_mut())
4680 .await
4681 .unwrap();
4682 assert_eq!(
4683 vid_share_count, rows as i64,
4684 "vid share count does not match rows"
4685 );
4686
4687 let (quorum_proposals_count,) =
4688 query_as::<(i64,)>("SELECT COUNT(*) from quorum_proposals2")
4689 .fetch_one(tx.as_mut())
4690 .await
4691 .unwrap();
4692 assert_eq!(
4693 quorum_proposals_count, rows as i64,
4694 "quorum proposals count does not match rows",
4695 );
4696
4697 let (quorum_certificates_count,) =
4698 query_as::<(i64,)>("SELECT COUNT(*) from quorum_certificate2")
4699 .fetch_one(tx.as_mut())
4700 .await
4701 .unwrap();
4702 assert_eq!(
4703 quorum_certificates_count, rows as i64,
4704 "quorum certificates count does not match rows",
4705 );
4706
4707 let (state_cert_count,) = query_as::<(i64,)>("SELECT COUNT(*) from finalized_state_cert")
4708 .fetch_one(tx.as_mut())
4709 .await
4710 .unwrap();
4711 assert_eq!(
4712 state_cert_count, rows as i64,
4713 "Light client state update certificates count does not match rows",
4714 );
4715 assert_eq!(
4716 storage.load_state_cert().await.unwrap().unwrap(),
4717 LightClientStateUpdateCertificateV2::<SeqTypes> {
4718 epoch: EpochNumber::new(rows - 1),
4719 light_client_state: Default::default(),
4720 next_stake_table_state: Default::default(),
4721 signatures: vec![],
4722 auth_root: Default::default(),
4723 },
4724 "Wrong light client state update certificate in the storage",
4725 );
4726
4727 storage.migrate_storage().await.unwrap();
4728 }
4729
4730 #[test_log::test(tokio::test(flavor = "multi_thread"))]
4753 async fn test_store_events_empty() {
4754 let tmp = Persistence::tmp_storage().await;
4755 let mut opt = Persistence::options(&tmp);
4756 let storage = opt.create().await.unwrap();
4757
4758 assert_eq!(storage.load_events(0, 100).await.unwrap(), (None, vec![]));
4759
4760 for i in 1..=2 {
4762 tracing::info!(i, "update l1 height");
4763 storage.store_events(i, vec![]).await.unwrap();
4764 assert_eq!(
4765 storage.load_events(0, 100).await.unwrap(),
4766 (Some(EventsPersistenceRead::UntilL1Block(i)), vec![])
4767 );
4768 }
4769 }
4770}
4771
4772#[cfg(test)]
4773#[cfg(not(feature = "embedded-db"))]
4774mod postgres_tests {
4775 use espresso_types::{FeeAccount, Header, Leaf, NodeState, Transaction as Tx};
4776 use hotshot_example_types::node_types::TEST_VERSIONS;
4777 use hotshot_query_service::{
4778 availability::{BlockQueryData, LeafQueryData},
4779 data_source::storage::UpdateAvailabilityStorage,
4780 };
4781 use hotshot_types::{
4782 data::vid_commitment,
4783 simple_certificate::QuorumCertificate,
4784 traits::{
4785 EncodeBytes,
4786 block_contents::{BlockHeader, BuilderFee, GENESIS_VID_NUM_STORAGE_NODES},
4787 election::Membership,
4788 signature_key::BuilderSignatureKey,
4789 },
4790 };
4791
4792 use super::*;
4793 use crate::persistence::tests::TestablePersistence as _;
4794
4795 async fn test_postgres_read_ns_table(instance_state: NodeState) {
4796 instance_state
4797 .coordinator
4798 .membership()
4799 .set_first_epoch(EpochNumber::genesis(), Default::default());
4800
4801 let tmp = Persistence::tmp_storage().await;
4802 let mut opt = Persistence::options(&tmp);
4803 let storage = opt.create().await.unwrap();
4804
4805 let txs = [
4806 Tx::new(10001u32.into(), vec![1, 2, 3]),
4807 Tx::new(10001u32.into(), vec![4, 5, 6]),
4808 Tx::new(10009u32.into(), vec![7, 8, 9]),
4809 ];
4810
4811 let validated_state = Default::default();
4812 let justify_qc =
4813 QuorumCertificate::genesis(&validated_state, &instance_state, TEST_VERSIONS.test).await;
4814 let view_number: ViewNumber = justify_qc.view_number + 1;
4815 let parent_leaf = Leaf::genesis(&validated_state, &instance_state, TEST_VERSIONS.test.base)
4816 .await
4817 .into();
4818
4819 let (payload, ns_table) =
4820 Payload::from_transactions(txs.clone(), &validated_state, &instance_state)
4821 .await
4822 .unwrap();
4823 let payload_bytes = payload.encode();
4824 let payload_commitment = vid_commitment(
4825 &payload_bytes,
4826 &ns_table.encode(),
4827 GENESIS_VID_NUM_STORAGE_NODES,
4828 instance_state.current_version,
4829 );
4830 let builder_commitment = payload.builder_commitment(&ns_table);
4831 let (fee_account, fee_key) = FeeAccount::generated_from_seed_indexed([0; 32], 0);
4832 let fee_amount = 0;
4833 let fee_signature = FeeAccount::sign_fee(&fee_key, fee_amount, &ns_table).unwrap();
4834 let block_header = Header::new(
4835 &validated_state,
4836 &instance_state,
4837 &parent_leaf,
4838 payload_commitment,
4839 builder_commitment,
4840 ns_table,
4841 BuilderFee {
4842 fee_amount,
4843 fee_account,
4844 fee_signature,
4845 },
4846 instance_state.current_version,
4847 view_number.u64(),
4848 )
4849 .await
4850 .unwrap();
4851 let proposal = QuorumProposal {
4852 block_header: block_header.clone(),
4853 view_number,
4854 justify_qc: justify_qc.clone(),
4855 upgrade_certificate: None,
4856 proposal_certificate: None,
4857 };
4858 let leaf: Leaf2 = Leaf::from_quorum_proposal(&proposal).into();
4859 let mut qc = justify_qc.to_qc2();
4860 qc.data.leaf_commit = leaf.commit();
4861 qc.view_number = view_number;
4862
4863 let mut tx = storage.db.write().await.unwrap();
4864 tx.insert_leaf(&LeafQueryData::new(leaf, qc).unwrap())
4865 .await
4866 .unwrap();
4867 tx.insert_block(&BlockQueryData::<SeqTypes>::new(block_header, payload))
4868 .await
4869 .unwrap();
4870 tx.commit().await.unwrap();
4871
4872 let mut tx = storage.db.read().await.unwrap();
4873 let rows = query(
4874 "
4875 SELECT ns_id, read_ns_id(get_ns_table(h.data), t.ns_index) AS read_ns_id
4876 FROM header AS h
4877 JOIN transactions AS t ON t.block_height = h.height
4878 ORDER BY t.ns_index, t.position
4879 ",
4880 )
4881 .fetch_all(tx.as_mut())
4882 .await
4883 .unwrap();
4884 assert_eq!(rows.len(), txs.len());
4885 for (i, row) in rows.into_iter().enumerate() {
4886 let ns = u64::from(txs[i].namespace()) as i64;
4887 assert_eq!(row.get::<i64, _>("ns_id"), ns);
4888 assert_eq!(row.get::<i64, _>("read_ns_id"), ns);
4889 }
4890 }
4891
4892 #[test_log::test(tokio::test(flavor = "multi_thread"))]
4893 async fn test_postgres_read_ns_table_v0_1() {
4894 test_postgres_read_ns_table(NodeState::mock()).await;
4895 }
4896
4897 #[test_log::test(tokio::test(flavor = "multi_thread"))]
4898 async fn test_postgres_read_ns_table_v0_2() {
4899 test_postgres_read_ns_table(NodeState::mock_v2()).await;
4900 }
4901
4902 #[test_log::test(tokio::test(flavor = "multi_thread"))]
4903 async fn test_postgres_read_ns_table_v0_3() {
4904 test_postgres_read_ns_table(NodeState::mock_v3().with_epoch_height(0)).await;
4905 }
4906
4907 #[test_log::test(tokio::test(flavor = "multi_thread"))]
4912 async fn test_record_action_concurrent() {
4913 let tmp = Persistence::tmp_storage().await;
4914 let storage = Arc::new(Persistence::connect(&tmp).await);
4915
4916 let handles: Vec<_> = (0u64..20)
4917 .map(|i| {
4918 let storage = Arc::clone(&storage);
4919 tokio::spawn(async move {
4920 storage
4921 .record_action(ViewNumber::new(i), None, HotShotAction::Vote)
4922 .await
4923 })
4924 })
4925 .collect();
4926
4927 for handle in handles {
4928 handle.await.unwrap().unwrap();
4929 }
4930
4931 let latest = storage.load_latest_acted_view().await.unwrap();
4932 assert_eq!(latest, Some(ViewNumber::new(19)));
4933 }
4934}