Skip to main content

espresso_node/persistence/
sql.rs

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/// Options for Postgres-backed persistence.
83#[derive(Parser, Clone, Derivative)]
84#[derivative(Debug)]
85pub struct PostgresOptions {
86    /// Hostname for the remote Postgres database server.
87    #[clap(long, env = "ESPRESSO_NODE_POSTGRES_HOST")]
88    pub(crate) host: Option<String>,
89
90    /// Port for the remote Postgres database server.
91    #[clap(long, env = "ESPRESSO_NODE_POSTGRES_PORT")]
92    pub(crate) port: Option<u16>,
93
94    /// Name of database to connect to.
95    #[clap(long, env = "ESPRESSO_NODE_POSTGRES_DATABASE")]
96    pub(crate) database: Option<String>,
97
98    /// Postgres user to connect as.
99    #[clap(long, env = "ESPRESSO_NODE_POSTGRES_USER")]
100    pub(crate) user: Option<String>,
101
102    /// Password for Postgres user.
103    #[clap(long, env = "ESPRESSO_NODE_POSTGRES_PASSWORD")]
104    // Hide from debug output since may contain sensitive data.
105    #[derivative(Debug = "ignore")]
106    pub(crate) password: Option<String>,
107
108    /// Use TLS for an encrypted connection to the database.
109    #[clap(long, env = "ESPRESSO_NODE_POSTGRES_USE_TLS")]
110    pub(crate) use_tls: bool,
111
112    /// Disable `DEFERRABLE` on read transactions for the query service.
113    ///
114    /// When true (the default), read transactions on Postgres start with `SERIALIZABLE READ ONLY`
115    /// (no `DEFERRABLE`), so they begin immediately rather than waiting for a safe serializable
116    /// snapshot. This trades start-up latency for the chance of a serialization-error retry.
117    /// Set to false to restore `DEFERRABLE`.
118    #[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    /// Base directory for the SQLite database.
136    /// The SQLite file will be created in the `sqlite` subdirectory with filename as `database`.
137    #[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 `sqlite` sub dir does not exist then create it
149    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/// Options for database-backed persistence, supporting both Postgres and SQLite.
158#[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    /// Database URI for Postgres or SQLite.
170    ///
171    /// This is a shorthand for setting a number of other options all at once. The URI has the
172    /// following format ([brackets] indicate optional segments):
173    ///
174    /// - **Postgres:** `postgres[ql]://[username[:password]@][host[:port],]/database[?parameter_list]`
175    /// - **SQLite:** `sqlite://path/to/db.sqlite`
176    ///
177    /// Options set explicitly via other env vars or flags will take precedence, so you can use this
178    /// URI to set a baseline and then use other parameters to override or add configuration. In
179    /// addition, there are some parameters which cannot be set via the URI, such as TLS.
180    // Hide from debug output since may contain sensitive data.
181    #[derivative(Debug = "ignore")]
182    pub(crate) uri: Option<String>,
183
184    /// This will enable the pruner and set the default pruning parameters unless provided.
185    /// Default parameters:
186    /// - pruning_threshold: 3 TB
187    /// - minimum_retention: 1 day
188    /// - target_retention: 7 days
189    /// - batch_size: 1000
190    /// - max_usage: 80%
191    /// - interval: 1 hour
192    #[clap(long, env = "ESPRESSO_NODE_DATABASE_PRUNE")]
193    pub(crate) prune: bool,
194
195    /// Pruning parameters.
196    #[clap(flatten)]
197    pub(crate) pruning: PruningOptions,
198
199    /// Pruning parameters for ephemeral consensus storage.
200    #[clap(flatten)]
201    pub(crate) consensus_pruning: ConsensusPruningOptions,
202
203    /// Retry/backoff parameters for PostgreSQL serialization conflicts.
204    #[clap(flatten)]
205    pub(crate) serializable_retry: SerializableRetryOptions,
206
207    /// Specifies the maximum number of concurrent fetch requests allowed from peers.
208    #[clap(long, env = "ESPRESSO_NODE_FETCH_RATE_LIMIT")]
209    pub(crate) fetch_rate_limit: Option<usize>,
210
211    /// The minimum delay between active fetches in a stream.
212    #[clap(long, env = "ESPRESSO_NODE_ACTIVE_FETCH_DELAY", value_parser = parse_duration)]
213    pub(crate) active_fetch_delay: Option<Duration>,
214
215    /// The minimum delay between loading chunks in a stream.
216    #[clap(long, env = "ESPRESSO_NODE_CHUNK_FETCH_DELAY", value_parser = parse_duration)]
217    pub(crate) chunk_fetch_delay: Option<Duration>,
218
219    /// The number of items to process in a single transaction when scanning the database for
220    /// missing objects.
221    #[clap(long, env = "ESPRESSO_NODE_SYNC_STATUS_CHUNK_SIZE")]
222    pub(crate) sync_status_chunk_size: Option<usize>,
223
224    /// Duration to cache sync status results for.
225    #[clap(long, env = "ESPRESSO_NODE_SYNC_STATUS_TTL", value_parser = parse_duration)]
226    pub(crate) sync_status_ttl: Option<Duration>,
227
228    /// The number of items to process at a time when scanning for proactive fetching.
229    #[clap(long, env = "ESPRESSO_NODE_PROACTIVE_SCAN_CHUNK_SIZE")]
230    pub(crate) proactive_scan_chunk_size: Option<usize>,
231
232    /// The time interval between proactive fetching scans.
233    #[clap(long, env = "ESPRESSO_NODE_PROACTIVE_SCAN_INTERVAL", value_parser = parse_duration)]
234    pub(crate) proactive_scan_interval: Option<Duration>,
235
236    /// Disable the proactive scanner task.
237    #[clap(long, env = "ESPRESSO_NODE_DISABLE_PROACTIVE_FETCHING")]
238    pub(crate) disable_proactive_fetching: bool,
239
240    /// Disable pruning and reconstruct previously pruned data.
241    ///
242    /// While running without pruning is the default behavior, the default will not try to
243    /// reconstruct data that was pruned in a previous run where pruning was enabled. This option
244    /// instructs the service to run without pruning _and_ reconstruct all previously pruned data by
245    /// fetching from peers.
246    #[clap(long, env = "ESPRESSO_NODE_ARCHIVE", conflicts_with = "prune")]
247    pub(crate) archive: bool,
248
249    /// Turns on leaf only data storage
250    #[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    /// The maximum idle time of a database connection.
259    ///
260    /// Any connection which has been open and unused longer than this duration will be
261    /// automatically closed to reduce load on the server.
262    #[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    /// The maximum lifetime of a database connection.
266    ///
267    /// Any connection which has been open longer than this duration will be automatically closed
268    /// (and, if needed, replaced), even if it is otherwise healthy. It is good practice to refresh
269    /// even healthy connections once in a while (e.g. daily) in case of resource leaks in the
270    /// server implementation.
271    #[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    /// The maximum time a single SQL statement is allowed to run before being canceled.
278    ///
279    /// This helps prevent queries from running indefinitely and consuming resources.
280    /// Set to 10 minutes by default
281    #[clap(long, env = "ESPRESSO_NODE_DATABASE_STATEMENT_TIMEOUT", value_parser = parse_duration, default_value = "10m")]
282    pub(crate) statement_timeout: Duration,
283
284    /// The minimum number of database connections to maintain at any time.
285    ///
286    /// The database client will, to the best of its ability, maintain at least `min` open
287    /// connections at all times. This can be used to reduce the latency hit of opening new
288    /// connections when at least this many simultaneous connections are frequently needed.
289    #[clap(
290        long,
291        env = "ESPRESSO_NODE_DATABASE_MIN_CONNECTIONS",
292        default_value = "0"
293    )]
294    pub(crate) min_connections: u32,
295
296    /// Allows setting a different maximum number of connections for query operations.
297    /// Default value of None implies using the min_connections value.
298    #[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    /// The maximum number of database connections to maintain at any time.
303    ///
304    /// Once `max` connections are in use simultaneously, further attempts to acquire a connection
305    /// (or begin a transaction) will block until one of the existing connections is released.
306    #[clap(
307        long,
308        env = "ESPRESSO_NODE_DATABASE_MAX_CONNECTIONS",
309        default_value = "25"
310    )]
311    pub(crate) max_connections: u32,
312
313    /// Allows setting a different maximum number of connections for query operations.
314    /// Default value of None implies using the max_connections value.
315    #[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    // Keep the database connection pool when persistence is created,
320    // allowing it to be reused across multiple instances instead of creating
321    // a new pool each time such as for API, consensus storage etc
322    // This also ensures all storage instances adhere to the MAX_CONNECTIONS limit if set
323    //
324    // Note: Cloning the `Pool` is lightweight and efficient because it simply
325    // creates a new reference-counted handle to the underlying pool state.
326    #[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)); // 10 minutes default
370
371        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/// Pruning parameters.
529#[derive(Parser, Clone, Copy, Debug)]
530pub struct PruningOptions {
531    /// Threshold for pruning, specified in bytes.
532    /// If the disk usage surpasses this threshold, pruning is initiated for data older than the specified minimum retention period.
533    /// Pruning continues until the disk usage drops below the MAX USAGE.
534    #[clap(long, env = "ESPRESSO_NODE_PRUNER_PRUNING_THRESHOLD", value_parser = parse_size)]
535    pub(crate) pruning_threshold: Option<u64>,
536
537    /// Minimum retention period.
538    /// Data is retained for at least this duration, even if there's no free disk space.
539    #[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    /// Minimum retention period for Merklized state.
547    /// State is retained for at least this duration, even if there's no free disk space.
548    #[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    /// Target retention period.
556    /// Data older than this is pruned to free up space.
557    #[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    /// Target retention period for Merklized state.
565    /// State older than this is pruned to free up space.
566    #[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    /// Batch size for pruning.
574    /// This is the number of blocks data to delete in a single transaction.
575    #[clap(long, env = "ESPRESSO_NODE_PRUNER_BATCH_SIZE")]
576    pub(crate) batch_size: Option<u64>,
577
578    /// Maximum disk usage (in basis points).
579    ///
580    /// Pruning stops once the disk usage falls below this value, even if
581    /// some data older than the `MINIMUM_RETENTION` remains. Values range
582    /// from 0 (0%) to 10000 (100%).
583    #[clap(long, env = "ESPRESSO_NODE_PRUNER_MAX_USAGE")]
584    pub(crate) max_usage: Option<u16>,
585
586    /// Interval for running the pruner.
587    #[clap(
588        long,
589        env = "ESPRESSO_NODE_PRUNER_INTERVAL",
590        value_parser = parse_duration,
591    )]
592    pub(crate) interval: Option<Duration>,
593
594    /// Number of SQLite pages to vacuum from the freelist
595    /// during each pruner cycle.
596    /// This value corresponds to `N` in the SQLite PRAGMA `incremental_vacuum(N)`,
597    #[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/// Pruning parameters for ephemeral consensus storage.
649#[derive(Parser, Clone, Copy, Debug)]
650pub struct ConsensusPruningOptions {
651    /// Number of views to try to retain in consensus storage before data that hasn't been archived
652    /// is garbage collected.
653    ///
654    /// The longer this is, the more certain that all data will eventually be archived, even if
655    /// there are temporary problems with archive storage or partially missing data. This can be set
656    /// very large, as most data is garbage collected as soon as it is finalized by consensus. This
657    /// setting only applies to views which never get decided (ie forks in consensus) and views for
658    /// which this node is partially offline. These should be exceptionally rare.
659    ///
660    /// Note that in extreme scenarios, data may be garbage collected even before TARGET_RETENTION
661    /// views, if consensus storage exceeds TARGET_USAGE. For a hard lower bound on how long
662    /// consensus data will be retained, see MINIMUM_RETENTION.
663    ///
664    /// The default of 302000 views equates to approximately 1 week (604800 seconds) at an average
665    /// view time of 2s.
666    #[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    /// Minimum number of views to try to retain in consensus storage before data that hasn't been
675    /// archived is garbage collected.
676    ///
677    /// This bound allows data to be retained even if consensus storage occupies more than
678    /// TARGET_USAGE. This can be used to ensure sufficient time to move consensus data to archival
679    /// storage as necessary, even under extreme circumstances where otherwise garbage collection
680    /// would kick in based on TARGET_RETENTION.
681    ///
682    /// The default of 130000 views equates to approximately 3 days (259200 seconds) at an average
683    /// view time of 2s.
684    #[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    /// Amount (in bytes) of data to retain in consensus storage before garbage collecting more
693    /// aggressively.
694    ///
695    /// See also TARGET_RETENTION and MINIMUM_RETENTION.
696    #[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/// Configuration for retrying transactions aborted by PostgreSQL serialization conflicts (error
712/// 40001).  Under SERIALIZABLE isolation these aborts are expected and safe to retry from scratch.
713#[derive(Parser, Clone, Copy, Debug)]
714pub struct SerializableRetryOptions {
715    /// Maximum number of retries on PostgreSQL serialization conflicts.
716    #[clap(
717        long = "serializable-retry-max",
718        env = "ESPRESSO_NODE_SERIALIZABLE_RETRY_MAX",
719        default_value = "100"
720    )]
721    retry_max: u32,
722
723    /// Initial delay before retrying after a PostgreSQL serialization conflict.
724    #[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    /// Maximum delay between retries after a PostgreSQL serialization conflict.
733    #[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    /// Multiplier applied to the backoff delay between successive retries.
742    #[clap(
743        long = "serializable-backoff-factor",
744        env = "ESPRESSO_NODE_SERIALIZABLE_BACKOFF_FACTOR",
745        default_value = "2"
746    )]
747    backoff_factor: u32,
748
749    /// Backoff jitter as a ratio of the backoff delay, numerator:denominator.
750    #[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    /// When set, enable verbose diagnostic logging on PostgreSQL serialization conflicts.  Each
758    /// retried conflict spawns a background query against `pg_stat_activity` and `pg_locks` to
759    /// log concurrent sessions and predicate locks. Off by default.
760    #[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    /// Convert these CLI/env options into the storage-layer retry policy.
775    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/// Postgres-backed persistence.
840#[derive(Clone, Debug)]
841pub struct Persistence {
842    db: SqlStorage,
843    gc_opt: ConsensusPruningOptions,
844    /// A reference to the internal metrics
845    internal_metrics: PersistenceMetricsValue,
846}
847
848/// PostgreSQL error code for serialization failures under SERIALIZABLE isolation.
849/// Transactions that fail with this code are safe to retry from scratch.
850const PG_SERIALIZATION_FAILURE_CODE: &str = "40001";
851
852/// How far behind the newest persisted decide an out-of-order ("gap-fill") decide can still
853/// arrive. Mirrors `DECIDE_BUFFER` in `hotshot-new-protocol`; a leaf missing further behind the
854/// watermark than this will never be filled in by consensus.
855pub(crate) const DECIDE_GAP_FILL_HORIZON: u64 = 20;
856
857/// Whether the height gap directly below the leaf at `view` can still be filled by a late
858/// decide: the missing view (at most `view - 1`) must be within [`DECIDE_GAP_FILL_HORIZON`] of
859/// `watermark`, the newest persisted decide.
860pub(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        // cert1 is the QC for the newest leaf
916        // ancestors are certified by
917        // their successor's justify_qc. cert2 finalizes the newest leaf.
918        // update() uses cert1 to build LeafQueryData for
919        // the newest leaf and only attaches cert2 to it.
920        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    /// Run `f` under the database's serialization-conflict retry policy.
935    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    /// Ensure the `leaf_hash` column is populated for all existing quorum proposals.
945    ///
946    /// This column was added in a migration, but because it requires computing a commitment of the
947    /// existing data, it is not easy to populate in the SQL migration itself. Thus, on startup, we
948    /// check if there are any just-migrated quorum proposals with a `NULL` value for this column,
949    /// and if so we populate the column manually.
950    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    /// The `last_processed_view` cursor: highest view with a generated decide event, or `None`.
1018    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        // Seed the height-continuity check below from the leaf at the cursor; if retention
1044        // pruning has since removed it, fall back to accepting the first row as before.
1045        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            // In SQLite, overlapping read and write transactions can lead to database errors. To
1068            // avoid this:
1069            // - start a read transaction to query and collect all the necessary data.
1070            // - Commit (or implicitly drop) the read transaction once the data is fetched.
1071            // - use the collected data to generate a "decide" event for the consumer.
1072            // - begin a write transaction to delete the data and update the event stream.
1073
1074            // Retry the entire read section on serialization failures (40001) via
1075            // `serializable_retry!`. The closure returns `None` when there is no
1076            // more work to do, which we propagate out of the outer function.
1077            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                // Collect a chain of consecutive leaves, starting from the first view after the
1090                // last decide. This will correspond to a decide event, and defines a range of
1091                // views which can be garbage collected. This may even include views for which
1092                // there was no leaf, for which we might still have artifacts like proposals that
1093                // never finalized.
1094                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                // The newest persisted decide; bounds how far back a gap-fill can arrive.
1101                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                            // If there's an error getting a row, try generating an event with
1125                            // the rows we do have.
1126                            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                    // Ensure we are only dealing with a consecutive chain of leaves. We don't want to
1144                    // garbage collect any views for which we missed a leaf or decide event; at least
1145                    // not right away, in case we need to recover that data later.
1146                    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                        // A height jump within `DECIDE_GAP_FILL_HORIZON` of the watermark
1158                        // can still be gap-filled: hold the cursor (emit nothing) until that
1159                        // decide upserts the missing row. Legacy or beyond-horizon gaps are
1160                        // permanent; skip as before, leaving the missing block to the
1161                        // consumer's own fetching.
1162                        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                        // A first row at or below the cursor (a replayed decide) is accepted.
1178                    }
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                    // End event processing when there are no more decided views.
1188                    tracing::debug!(from_view, "no new leaves at decide");
1189                    return Ok(None);
1190                };
1191
1192                // Find the range of views encompassed by this leaf chain. All data in this range can be
1193                // processed by the consumer and then deleted.
1194                let from_view = leaves[0].0.view_number();
1195                let to_view = leaves[leaves.len() - 1].0.view_number();
1196
1197                // Collect VID shares for the decide event.
1198                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                // Collect DA proposals for the decide event.
1218                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                // Collect state certs for the decide event.
1240                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            // Collate all the information by view number and construct a chain of leaves.
1281            let chain = leaves
1282                .into_iter()
1283                // Go in reverse chronological order, as expected by Decide events.
1284                .rev()
1285                .map(|(mut leaf, cert)| {
1286                    let view = leaf.view_number();
1287
1288                    // Include the VID share if available.
1289                    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                    // Fill in the full block payload using the DA proposals we had persisted.
1296                    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                        // We don't get a DA proposal for the genesis view, but we know what the
1302                        // payload always is.
1303                        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                        // Note: the following fields are not used in Decide event processing,
1315                        // and should be removed. For now, we just default them.
1316                        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            // Now that we have definitely processed leaves up to `to_view`, we can update
1343            // `last_processed_view` so we don't process these leaves again. We may still fail at
1344            // this point, or shut down, and fail to complete this update. At worst this will lead
1345            // to us sending a duplicate decide event the next time we are called; this is fine as
1346            // the event consumer is required to be idempotent.
1347            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                // Store all the finalized state certs
1358                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                // Delete the data that has been fully processed.
1369                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                // Clean up leaves, but do not delete the most recent one (all leaves with a view
1407                // number less than the given value). This is necessary to ensure that, in case of
1408                // a restart, we can resume from the last decided leaf.
1409                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 everything older than the target retention period.
1465            prune_to_view(
1466                &mut tx,
1467                cur_view.u64().saturating_sub(self.gc_opt.target_retention),
1468            )
1469            .await?;
1470
1471            // Check our storage usage; if necessary we will prune more aggressively (up to the
1472            // minimum retention) to get below the target usage.
1473            #[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        // Nothing to prune, the entire chain is younger than the retention period.
1526        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            // Select the most recent config (although there should only be one).
1561            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                // The leaf may come with a large payload attached. We don't care about this payload
1606                // because we already store it separately, as part of the DA proposal. Storing it
1607                // here contributes to load on the DB for no reason, so we remove it before
1608                // serializing the leaf.
1609                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        // Append the new leaves. We do this in its own transaction because even if GC or the
1624        // event consumer later fails, there is no need to abort the storage of the leaves.
1625        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        // Generate events for the new leaves, then GC. On error `last_processed_view` is not
1649        // advanced past the failure point, so no data is lost and the range is retried.
1650        self.generate_decide_events(deciding_qc, consumer).await?;
1651
1652        // Best-effort GC of data not included in any decide event; runs again at the next decide.
1653        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        // Todo Remove this after https://github.com/EspressoSystems/espresso-network/issues/1931
1916        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        // We also keep track of any QC we see in case we need it to recover our archival storage.
1956        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            // Compare-and-set inside one write transaction so a stale
2038            // concurrent write can never regress the stored view: under
2039            // SERIALIZABLE a racing writer conflicts and retries; on SQLite
2040            // writes are serialized.
2041            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        // The SQL migration populates the table name and sets a default value of 0 for migrated rows.
2123        // so, fetch_one() would always return a row
2124        // The number of migrated rows is updated after each batch insert.
2125        // This allows the types migration to resume from where it left off.
2126        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            // Offset tracking prevents duplicate inserts
2180            // Added as a safeguard.
2181            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    /// Migrate stake table data to include x25519_key and p2p_addr fields.
2624    ///
2625    /// Data written before x25519 support lacks these fields. This migration
2626    /// deserializes legacy records and re-serializes them with the new fields set to None.
2627    async fn migrate_x25519_keys(&self) -> anyhow::Result<()> {
2628        use super::RegisteredValidatorNoX25519;
2629
2630        let name = DataMigration::X25519Keys.as_str();
2631
2632        // Migrate bincode storage (epoch_drb_and_root.stake).
2633        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                // Try current format first
2648                if bincode::deserialize::<AuthenticatedValidatorMap>(&stake_bytes).is_ok() {
2649                    continue;
2650                }
2651
2652                // Legacy format without x25519 fields
2653                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        // Migrate JSONB storage (stake_table_validators).
2688        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                // Check if JSON already has x25519 fields (can't rely on deserialization
2703                // since serde_json fills missing Option<T> fields with None).
2704                if validator_json
2705                    .as_object()
2706                    .is_some_and(|obj| obj.contains_key("x25519_key"))
2707                {
2708                    continue;
2709                }
2710
2711                // Deserialize (serde_json fills missing Option fields with None),
2712                // then re-serialize to ensure x25519_key and p2p_addr are present.
2713                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        // reverse the rows vector to return the most recent epochs, but in ascending order
3075        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                    // Right now we skip the epoch_drb_and_root row if there is no drb result.
3095                    // This seems reasonable based on the expected order of events, but please double check!
3096                    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    // Pre-Schnorr-Option: stake_table_key as Option<KEY>, state_ver_key as raw KEY.
3120    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    // Pre-Option: both keys raw, with x25519/p2p fields.
3134    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            // check last l1 block if there is any
3279            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            // skip events storage if the database already has higher l1 block events
3289            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            // update l1 block
3319            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    /// Loads all events from persistent storage up to the specified L1 block.
3335    ///
3336    /// # Returns
3337    ///
3338    /// Returns a tuple containing:
3339    /// - `Option<u64>` - The queried L1 block for which all events have been successfully fetched.
3340    /// - `Vec<(EventKey, StakeTableEvent)>` - A list of events, where each entry is a tuple of the event key
3341    /// event key is (l1 block number, log index)
3342    ///   and the corresponding StakeTable event.
3343    ///
3344    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        // check last l1 block if there is any
3355        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            // this just means we dont have any events stored
3363            return Ok((None, Vec::new()));
3364        };
3365
3366        // Determine the L1 block for querying events.
3367        // If the last stored L1 block is greater than the requested block, limit the query to the requested block.
3368        // Otherwise, query up to the last stored block.
3369        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        // Determine the read state based on the queried block range.
3397        // - If the persistence returned events up to the requested block, the read is complete.
3398        // - Otherwise, indicate that the read is up to the last processed block.
3399        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        // Use LOWER(address) in ORDER BY to ensure consistent ordering for SQlite and Postgres.
3499        // Postgres sorts text case sensitively by default, while SQLite sorts case insensitively.
3500        // Applying LOWER() makes the result consistent.
3501        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    /// Save the DHT to the database
3526    ///
3527    /// # Errors
3528    /// - If we fail to serialize the records
3529    /// - If we fail to write the serialized records to the DB
3530    async fn save(&self, records: Vec<SerializableRecord>) -> anyhow::Result<()> {
3531        // Bincode-serialize the records
3532        let to_save =
3533            bincode::serialize(&records).with_context(|| "failed to serialize records")?;
3534
3535        // Prepare the statement
3536        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            // Execute the query
3541            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            // Commit the state
3551            tx.commit().await.with_context(|| "failed to commit to DB")
3552        })
3553        .await
3554    }
3555
3556    /// Load the DHT from the database
3557    ///
3558    /// # Errors
3559    /// - If we fail to read from the DB
3560    /// - If we fail to deserialize the records
3561    async fn load(&self) -> anyhow::Result<Vec<SerializableRecord>> {
3562        // Fetch the results from the DB
3563        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        // Get the `serialized_records` row
3573        let serialied_records: Vec<u8> = result.get("serialized_records");
3574
3575        // Deserialize it
3576        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        // Create some quorum proposals to test with.
3740        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        // Create persistence and add the quorum proposals with NULL leaf hash.
3781        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        // Create a new persistence and ensure the commitments get populated.
3799        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        // Create legacy data without x25519 fields
3840        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        // Bincode: serialize as legacy map
3851        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        // JSON: serialize without x25519 fields
3856        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        // Reset migration state so it runs on the newly inserted data
3892        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        // Verify JSON was inserted without x25519_key
3902        {
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        // Run migrations
3915        let persistence = Persistence::connect(&db).await;
3916        persistence.migrate_storage().await.unwrap();
3917
3918        // Verify stake_table_validators now has x25519_key field
3919        {
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        // Verify epoch_drb_and_root stake was migrated
3933        {
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        // Verify migration tracking
3947        {
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(&current_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        // Create an authenticated validator
4085        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        // Create an unauthenticated validator
4098        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        // Store both validators
4121        storage
4122            .store_all_validators(EpochNumber::new(1), validators)
4123            .await
4124            .unwrap();
4125
4126        // Load and verify
4127        let loaded = storage
4128            .load_all_validators(EpochNumber::new(1), 0, 100)
4129            .await
4130            .unwrap();
4131        assert_eq!(loaded.len(), 2);
4132
4133        // Find each validator and verify authenticated state is preserved
4134        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        // Mock up some data.
4159        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        // Add to database.
4241        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        // Add an extra quorum proposal so we have a QC pointing back at `leaf`.
4252        storage
4253            .append_quorum_proposal2(&next_quorum_proposal)
4254            .await
4255            .unwrap();
4256
4257        // Fetch it as if we were rebuilding an archive.
4258        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    /// Test conditions that trigger pruning.
4274    ///
4275    /// This is a configurable test that can be used to test different configurations of GC,
4276    /// `pruning_opt`. The test populates the database with some data for view 1, asserts that it is
4277    /// retained for view 2, and then asserts that it is pruned by view 3. There are various
4278    /// different configurations that can achieve this behavior, such that the data is retained and
4279    /// then pruned due to different logic and code paths.
4280    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        // Populate some data.
4289        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        // Populate the view-indexed cert tables. Contents are opaque to the pruner (it deletes by
4377        // `view`), so raw bytes suffice.
4378        {
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        // The first decide doesn't trigger any garbage collection, even though our usage exceeds
4400        // the target, because of the minimum retention.
4401        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        // After another view, our data is beyond the minimum retention (though not the target
4422        // retention) so it gets pruned.
4423        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    /// Whether a view-indexed consensus table has a row at `view`.
4436    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            // Use a very low target usage, to show that we still retain data up to the minimum
4454            // retention even when usage is above target.
4455            target_usage: 0,
4456            minimum_retention: 1,
4457            // Use a very high target retention, so that pruning is only triggered by the minimum
4458            // retention.
4459            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            // Use a very low minimum retention, so that data is only kept around due to the target
4469            // retention.
4470            minimum_retention: 0,
4471            // Use a very high target usage, so that pruning is only triggered by the target
4472            // retention.
4473            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(), // filling arbitrary value
4568                next_stake_table_state: Default::default(), // filling arbitrary value
4569                signatures: vec![],                     // filling arbitrary value
4570                auth_root: Default::default(),
4571            };
4572            // manually upsert the state cert to the finalized database
4573            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    /// Regression test for an ambiguous behavior in `store_events`/`load_events`.
4731    ///
4732    /// Previously, `store_events` did nothing when given an empty events list (in fact,
4733    /// `fetch_and_store_stake_table_events` was not even calling it). But this means that the
4734    /// `stake_table_events_l1_block` column does not get updated when we enter a new epoch with no
4735    /// new stake table events. This makes it impossible to distinguish between two very different
4736    /// scenarios:
4737    ///
4738    /// 1. The node has successfully processed events through the latest L1 finalized block, but
4739    ///    there are no new events from the last epoch.
4740    /// 2. The node is lagging behind the latest L1 finalized block, and is possibly missing some
4741    ///    new events.
4742    ///
4743    /// In scenario 1, clients of this node should be able to treat the empty list of stake table
4744    /// events as authoritative, and derive the stake table for the next epoch (which will end up
4745    /// being the same as the previous one. However, in scenario 2, clients need to wait, because we
4746    /// don't yet know whether there could be any events that modify the stake table. Thus,
4747    /// distinguishing these two scenarios is important.
4748    ///
4749    /// This regression test ensures that even if there are no new events, at least the
4750    /// `stake_table_events_l1_block` column gets updated. We can then distinguish the two scenarios
4751    /// using the `EventsPersistenceRead`` return value from load_events.
4752    #[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        // Storing an empty events list still updates the latest L1 block.
4761        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    /// Verify that concurrent calls to `record_action` all succeed under
4908    /// PostgreSQL SERIALIZABLE isolation. `self.serializable_backoff.retry_if` handles any
4909    /// 40001 serialization failures that arise when many tasks race to update
4910    /// the same row.
4911    #[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}