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, Header, 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,
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, 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        Ok(persistence)
807    }
808
809    async fn reset(self) -> anyhow::Result<()> {
810        SqlStorage::connect(
811            Config::try_from(&self)?.reset_schema(),
812            StorageConnectionType::Sequencer,
813        )
814        .await?;
815        Ok(())
816    }
817}
818
819/// Postgres-backed persistence.
820#[derive(Clone, Debug)]
821pub struct Persistence {
822    db: SqlStorage,
823    gc_opt: ConsensusPruningOptions,
824    /// A reference to the internal metrics
825    internal_metrics: PersistenceMetricsValue,
826}
827
828/// PostgreSQL error code for serialization failures under SERIALIZABLE isolation.
829/// Transactions that fail with this code are safe to retry from scratch.
830const PG_SERIALIZATION_FAILURE_CODE: &str = "40001";
831
832/// How far behind the newest persisted decide an out-of-order ("gap-fill") decide can still
833/// arrive. Mirrors `DECIDE_BUFFER` in `hotshot-new-protocol`; a leaf missing further behind the
834/// watermark than this will never be filled in by consensus.
835pub(crate) const DECIDE_GAP_FILL_HORIZON: u64 = 20;
836
837/// Whether the height gap directly below the leaf at `view` can still be filled by a late
838/// decide: the missing view (at most `view - 1`) must be within [`DECIDE_GAP_FILL_HORIZON`] of
839/// `watermark`, the newest persisted decide.
840pub(crate) fn within_gap_fill_horizon(view: u64, watermark: u64) -> bool {
841    view.saturating_sub(1) + DECIDE_GAP_FILL_HORIZON > watermark
842}
843
844#[derive(Debug)]
845struct DecidedLeaf {
846    info: LeafInfo<SeqTypes>,
847    cert: CertificatePair<SeqTypes>,
848}
849
850fn decide_events_from_chain(
851    mut chain: Vec<DecidedLeaf>,
852    cert2: Option<Certificate2<SeqTypes>>,
853    deciding_qc: Option<Arc<CertificatePair<SeqTypes>>>,
854) -> Vec<CoordinatorEvent<SeqTypes>> {
855    let split_idx = chain
856        .iter()
857        .position(|leaf| leaf.info.leaf.block_header().version() < versions::NEW_PROTOCOL_VERSION)
858        .unwrap_or(chain.len());
859    let legacy_leaves = chain.split_off(split_idx);
860    let new_leaves = chain;
861
862    let mut events = Vec::with_capacity(2);
863    if !legacy_leaves.is_empty() {
864        let committing_qc = legacy_leaves[0].cert.clone();
865        let deciding_qc = new_leaves
866            .is_empty()
867            .then_some(deciding_qc)
868            .flatten()
869            .filter(|qc| qc.view_number() == committing_qc.view_number() + 1);
870        let view_number = legacy_leaves[0].info.leaf.view_number();
871        let leaf_chain = legacy_leaves
872            .into_iter()
873            .map(|leaf| leaf.info)
874            .collect::<Vec<_>>();
875
876        events.push(CoordinatorEvent::LegacyEvent(Event {
877            view_number,
878            event: EventType::Decide {
879                leaf_chain: Arc::new(leaf_chain),
880                committing_qc: Arc::new(committing_qc),
881                deciding_qc,
882                block_size: None,
883            },
884        }));
885    }
886
887    if new_leaves.is_empty() && cert2.is_some() {
888        tracing::warn!(
889            "decide_events_from_chain called with cert2 but no new-protocol leaves; cert2 will be \
890             dropped"
891        );
892    }
893
894    if !new_leaves.is_empty() {
895        // cert1 is the QC for the newest leaf
896        // ancestors are certified by
897        // their successor's justify_qc. cert2 finalizes the newest leaf.
898        // update() uses cert1 to build LeafQueryData for
899        // the newest leaf and only attaches cert2 to it.
900        let cert1 = new_leaves[0].cert.qc().clone();
901        let leaf_infos = new_leaves.into_iter().map(|leaf| leaf.info).collect();
902
903        events.push(CoordinatorEvent::NewDecide {
904            leaf_infos,
905            cert1,
906            cert2,
907        });
908    }
909
910    events
911}
912
913impl Persistence {
914    /// Run `f` under the database's serialization-conflict retry policy.
915    async fn serializable_retry<F, Fut, T>(&self, op: &'static str, f: F) -> anyhow::Result<T>
916    where
917        T: Send,
918        F: Fn() -> Fut + Send + Sync,
919        Fut: Future<Output = anyhow::Result<T>> + Send,
920    {
921        self.db.serializable_retry(op, f).await
922    }
923
924    /// Ensure the `leaf_hash` column is populated for all existing quorum proposals.
925    ///
926    /// This column was added in a migration, but because it requires computing a commitment of the
927    /// existing data, it is not easy to populate in the SQL migration itself. Thus, on startup, we
928    /// check if there are any just-migrated quorum proposals with a `NULL` value for this column,
929    /// and if so we populate the column manually.
930    async fn migrate_quorum_proposal_leaf_hashes(&self) -> anyhow::Result<()> {
931        serializable_retry!(self, || async {
932            let mut tx = self.db.write().await?;
933
934            let mut proposals = tx.fetch("SELECT * FROM quorum_proposals");
935
936            let mut updates = vec![];
937            while let Some(row) = proposals.next().await {
938                let row = row?;
939
940                let hash: Option<String> = row.try_get("leaf_hash")?;
941                if hash.is_none() {
942                    let view: i64 = row.try_get("view")?;
943                    let data: Vec<u8> = row.try_get("data")?;
944                    let proposal: Proposal<SeqTypes, QuorumProposal<SeqTypes>> =
945                        bincode::deserialize(&data)?;
946                    let leaf = Leaf::from_quorum_proposal(&proposal.data);
947                    let leaf_hash = Committable::commit(&leaf);
948                    tracing::info!(view, %leaf_hash, "populating quorum proposal leaf hash");
949                    updates.push((view, leaf_hash.to_string()));
950                }
951            }
952            drop(proposals);
953
954            tx.upsert("quorum_proposals", ["view", "leaf_hash"], ["view"], updates)
955                .await?;
956
957            tx.commit().await
958        })
959        .await
960    }
961
962    /// The `last_processed_view` cursor: highest view with a generated decide event, or `None`.
963    async fn load_processed_view(&self) -> anyhow::Result<Option<ViewNumber>> {
964        Ok(self
965            .db
966            .read()
967            .await?
968            .fetch_optional("SELECT last_processed_view FROM event_stream WHERE id = 1 LIMIT 1")
969            .await?
970            .map(|row| ViewNumber::new(row.get::<i64, _>("last_processed_view") as u64)))
971    }
972
973    async fn generate_decide_events(
974        &self,
975        deciding_qc: Option<Arc<CertificatePair<SeqTypes>>>,
976        consumer: &impl EventConsumer,
977    ) -> anyhow::Result<()> {
978        let mut last_processed_view: Option<i64> = serializable_retry!(self, || async {
979            Ok(self
980                .db
981                .read()
982                .await?
983                .fetch_optional("SELECT last_processed_view FROM event_stream WHERE id = 1 LIMIT 1")
984                .await?
985                .map(|row| row.get("last_processed_view")))
986        })
987        .await?;
988        // Seed the height-continuity check below from the leaf at the cursor; if retention
989        // pruning has since removed it, fall back to accepting the first row as before.
990        let mut last_processed_height: Option<u64> = match last_processed_view {
991            Some(view) => {
992                serializable_retry!(self, || async {
993                    self.db
994                        .read()
995                        .await?
996                        .fetch_optional(
997                            query("SELECT leaf FROM anchor_leaf2 WHERE view = $1").bind(view),
998                        )
999                        .await?
1000                        .map(|row| -> anyhow::Result<u64> {
1001                            let leaf_data: Vec<u8> = row.get("leaf");
1002                            let leaf = bincode::deserialize::<Leaf2>(&leaf_data)?;
1003                            Ok(leaf.block_header().block_number())
1004                        })
1005                        .transpose()
1006                })
1007                .await?
1008            },
1009            None => None,
1010        };
1011        loop {
1012            // In SQLite, overlapping read and write transactions can lead to database errors. To
1013            // avoid this:
1014            // - start a read transaction to query and collect all the necessary data.
1015            // - Commit (or implicitly drop) the read transaction once the data is fetched.
1016            // - use the collected data to generate a "decide" event for the consumer.
1017            // - begin a write transaction to delete the data and update the event stream.
1018
1019            // Retry the entire read section on serialization failures (40001) via
1020            // `serializable_retry!`. The closure returns `None` when there is no
1021            // more work to do, which we propagate out of the outer function.
1022            let Some((
1023                from_view,
1024                to_view,
1025                leaves,
1026                final_qc,
1027                mut vid_shares,
1028                mut da_proposals,
1029                state_certs,
1030                cert2,
1031            )) = serializable_retry!(self, || async {
1032                let mut tx = self.db.read().await?;
1033
1034                // Collect a chain of consecutive leaves, starting from the first view after the
1035                // last decide. This will correspond to a decide event, and defines a range of
1036                // views which can be garbage collected. This may even include views for which
1037                // there was no leaf, for which we might still have artifacts like proposals that
1038                // never finalized.
1039                let from_view = match last_processed_view {
1040                    Some(v) => v + 1,
1041                    None => 0,
1042                };
1043                tracing::debug!(?from_view, "generate decide event");
1044
1045                // The newest persisted decide; bounds how far back a gap-fill can arrive.
1046                let (watermark,): (Option<i64>,) = query_as("SELECT max(view) FROM anchor_leaf2")
1047                    .fetch_one(tx.as_mut())
1048                    .await?;
1049
1050                let mut parent = last_processed_height;
1051                let mut rows = query(
1052                    "SELECT leaf, qc, next_epoch_qc FROM anchor_leaf2 WHERE view >= $1 ORDER BY \
1053                     view",
1054                )
1055                .bind(from_view)
1056                .fetch(tx.as_mut());
1057                let mut leaves: Vec<(Leaf2, CertificatePair<SeqTypes>)> = vec![];
1058                let mut final_qc = None;
1059                while let Some(row) = rows.next().await {
1060                    let row = match row {
1061                        Ok(row) => row,
1062                        Err(err) => {
1063                            if err.as_database_error().is_some_and(|e| {
1064                                e.code().as_deref() == Some(PG_SERIALIZATION_FAILURE_CODE)
1065                            }) {
1066                                drop(rows);
1067                                return Err(anyhow::Error::from(err));
1068                            }
1069                            // If there's an error getting a row, try generating an event with
1070                            // the rows we do have.
1071                            tracing::warn!("error loading row: {err:#}");
1072                            break;
1073                        },
1074                    };
1075
1076                    let leaf_data: Vec<u8> = row.get("leaf");
1077                    let leaf = bincode::deserialize::<Leaf2>(&leaf_data)?;
1078                    let qc_data: Vec<u8> = row.get("qc");
1079                    let qc = bincode::deserialize::<QuorumCertificate2<SeqTypes>>(&qc_data)?;
1080                    let next_epoch_qc = match row.get::<Option<Vec<u8>>, _>("next_epoch_qc") {
1081                        Some(bytes) => Some(bincode::deserialize::<
1082                            NextEpochQuorumCertificate2<SeqTypes>,
1083                        >(&bytes)?),
1084                        None => None,
1085                    };
1086                    let height = leaf.block_header().block_number();
1087
1088                    // Ensure we are only dealing with a consecutive chain of leaves. We don't want to
1089                    // garbage collect any views for which we missed a leaf or decide event; at least
1090                    // not right away, in case we need to recover that data later.
1091                    if let Some(parent) = parent
1092                        && height != parent + 1
1093                    {
1094                        if !leaves.is_empty() {
1095                            tracing::debug!(
1096                                height,
1097                                parent,
1098                                "ending decide event at non-consecutive leaf"
1099                            );
1100                            break;
1101                        }
1102                        // A height jump within `DECIDE_GAP_FILL_HORIZON` of the watermark
1103                        // can still be gap-filled: hold the cursor (emit nothing) until that
1104                        // decide upserts the missing row. Legacy or beyond-horizon gaps are
1105                        // permanent; skip as before, leaving the missing block to the
1106                        // consumer's own fetching.
1107                        let row_view = leaf.view_number().u64();
1108                        if height > parent + 1
1109                            && leaf.block_header().version() >= versions::NEW_PROTOCOL_VERSION
1110                            && watermark
1111                                .is_some_and(|w| within_gap_fill_horizon(row_view, w as u64))
1112                        {
1113                            tracing::info!(
1114                                height,
1115                                parent,
1116                                row_view,
1117                                ?watermark,
1118                                "waiting for gap-fill decide before advancing the decide cursor"
1119                            );
1120                            break;
1121                        }
1122                        // A first row at or below the cursor (a replayed decide) is accepted.
1123                    }
1124                    parent = Some(height);
1125                    let cert = CertificatePair::new(qc, next_epoch_qc);
1126                    final_qc = Some(cert.clone());
1127                    leaves.push((leaf, cert));
1128                }
1129                drop(rows);
1130
1131                let Some(final_qc) = final_qc else {
1132                    // End event processing when there are no more decided views.
1133                    tracing::debug!(from_view, "no new leaves at decide");
1134                    return Ok(None);
1135                };
1136
1137                // Find the range of views encompassed by this leaf chain. All data in this range can be
1138                // processed by the consumer and then deleted.
1139                let from_view = leaves[0].0.view_number();
1140                let to_view = leaves[leaves.len() - 1].0.view_number();
1141
1142                // Collect VID shares for the decide event.
1143                let vid_rows = tx
1144                    .fetch_all(
1145                        query("SELECT view, data FROM vid_share2 where view >= $1 AND view <= $2")
1146                            .bind(from_view.u64() as i64)
1147                            .bind(to_view.u64() as i64),
1148                    )
1149                    .await?;
1150                let vid_shares = vid_rows
1151                    .into_iter()
1152                    .map(|row| {
1153                        let view: i64 = row.get("view");
1154                        let data: Vec<u8> = row.get("data");
1155                        let vid_proposal = bincode::deserialize::<
1156                            Proposal<SeqTypes, VidDisperseShare<SeqTypes>>,
1157                        >(&data)?;
1158                        Ok((view as u64, vid_proposal))
1159                    })
1160                    .collect::<anyhow::Result<BTreeMap<_, _>>>()?;
1161
1162                // Collect DA proposals for the decide event.
1163                let da_rows = tx
1164                    .fetch_all(
1165                        query(
1166                            "SELECT view, data FROM da_proposal2 where view >= $1 AND view <= $2",
1167                        )
1168                        .bind(from_view.u64() as i64)
1169                        .bind(to_view.u64() as i64),
1170                    )
1171                    .await?;
1172                let da_proposals = da_rows
1173                    .into_iter()
1174                    .map(|row| {
1175                        let view: i64 = row.get("view");
1176                        let data: Vec<u8> = row.get("data");
1177                        let da_proposal = bincode::deserialize::<
1178                            Proposal<SeqTypes, DaProposal2<SeqTypes>>,
1179                        >(&data)?;
1180                        Ok((view as u64, da_proposal.data))
1181                    })
1182                    .collect::<anyhow::Result<BTreeMap<_, _>>>()?;
1183
1184                // Collect state certs for the decide event.
1185                let state_certs = Self::load_state_certs(&mut tx, from_view, to_view)
1186                    .await
1187                    .with_context(|| {
1188                        format!("load_state_certs from_view={from_view:?} to_view={to_view:?}")
1189                    })?;
1190
1191                let cert2_row = tx
1192                    .fetch_optional(
1193                        query("SELECT data FROM decided_cert2 WHERE view = $1")
1194                            .bind(to_view.u64() as i64),
1195                    )
1196                    .await?;
1197                let cert2 = cert2_row
1198                    .map(|row| {
1199                        let bytes: Vec<u8> = row.get("data");
1200                        bincode::deserialize::<Certificate2<SeqTypes>>(&bytes)
1201                            .context("deserializing decided cert2")
1202                    })
1203                    .transpose()?;
1204                drop(tx);
1205                Ok(Some((
1206                    from_view,
1207                    to_view,
1208                    leaves,
1209                    final_qc,
1210                    vid_shares,
1211                    da_proposals,
1212                    state_certs,
1213                    cert2,
1214                )))
1215            })
1216            .await?
1217            else {
1218                return Ok(());
1219            };
1220
1221            let to_height = leaves
1222                .last()
1223                .map(|(leaf, _)| leaf.block_header().block_number());
1224
1225            // Collate all the information by view number and construct a chain of leaves.
1226            let chain = leaves
1227                .into_iter()
1228                // Go in reverse chronological order, as expected by Decide events.
1229                .rev()
1230                .map(|(mut leaf, cert)| {
1231                    let view = leaf.view_number();
1232
1233                    // Include the VID share if available.
1234                    let vid_proposal = vid_shares.remove(&view);
1235                    if vid_proposal.is_none() {
1236                        tracing::debug!(?view, "VID share not available at decide");
1237                    }
1238                    let vid_share = vid_proposal.as_ref().map(|proposal| proposal.data.clone());
1239
1240                    // Fill in the full block payload using the DA proposals we had persisted.
1241                    if let Some(proposal) = da_proposals.remove(&view) {
1242                        let payload =
1243                            Payload::from_bytes(&proposal.encoded_transactions, &proposal.metadata);
1244                        leaf.fill_block_payload_unchecked(payload);
1245                    } else if view == ViewNumber::genesis() {
1246                        // We don't get a DA proposal for the genesis view, but we know what the
1247                        // payload always is.
1248                        leaf.fill_block_payload_unchecked(Payload::empty().0);
1249                    } else {
1250                        tracing::debug!(?view, "DA proposal not available at decide");
1251                    }
1252
1253                    let state_cert = state_certs.get(&view).cloned();
1254
1255                    let info = LeafInfo {
1256                        leaf,
1257                        vid_share,
1258                        state_cert,
1259                        // Note: the following fields are not used in Decide event processing,
1260                        // and should be removed. For now, we just default them.
1261                        state: Default::default(),
1262                        delta: Default::default(),
1263                    };
1264                    DecidedLeaf { info, cert }
1265                })
1266                .collect();
1267
1268            tracing::debug!(
1269                ?from_view,
1270                ?to_view,
1271                ?final_qc,
1272                ?chain,
1273                "generating decide event"
1274            );
1275
1276            for event in decide_events_from_chain(chain, cert2, deciding_qc.clone()) {
1277                consumer.handle_event(&event).await?;
1278            }
1279
1280            let from_view_i64 = from_view.u64() as i64;
1281            let to_view_i64 = to_view.u64() as i64;
1282            let serialized_state_certs = state_certs
1283                .into_iter()
1284                .map(|(epoch, cert)| Ok((epoch as i64, bincode::serialize(&cert)?)))
1285                .collect::<anyhow::Result<Vec<(i64, Vec<u8>)>>>()?;
1286
1287            // Now that we have definitely processed leaves up to `to_view`, we can update
1288            // `last_processed_view` so we don't process these leaves again. We may still fail at
1289            // this point, or shut down, and fail to complete this update. At worst this will lead
1290            // to us sending a duplicate decide event the next time we are called; this is fine as
1291            // the event consumer is required to be idempotent.
1292            serializable_retry!(self, || async {
1293                let mut tx = self.db.write().await?;
1294                tx.upsert(
1295                    "event_stream",
1296                    ["id", "last_processed_view"],
1297                    ["id"],
1298                    [(1i32, to_view_i64)],
1299                )
1300                .await?;
1301
1302                // Store all the finalized state certs
1303                for (epoch, state_cert_bytes) in &serialized_state_certs {
1304                    tx.upsert(
1305                        "finalized_state_cert",
1306                        ["epoch", "state_cert"],
1307                        ["epoch"],
1308                        [(*epoch, state_cert_bytes.clone())],
1309                    )
1310                    .await?;
1311                }
1312
1313                // Delete the data that has been fully processed.
1314                tx.execute(
1315                    query("DELETE FROM vid_share2 where view >= $1 AND view <= $2")
1316                        .bind(from_view_i64)
1317                        .bind(to_view_i64),
1318                )
1319                .await?;
1320                tx.execute(
1321                    query("DELETE FROM da_proposal2 where view >= $1 AND view <= $2")
1322                        .bind(from_view_i64)
1323                        .bind(to_view_i64),
1324                )
1325                .await?;
1326                tx.execute(
1327                    query("DELETE FROM quorum_proposals2 where view >= $1 AND view <= $2")
1328                        .bind(from_view_i64)
1329                        .bind(to_view_i64),
1330                )
1331                .await?;
1332                tx.execute(
1333                    query("DELETE FROM quorum_certificate2 where view >= $1 AND view <= $2")
1334                        .bind(from_view_i64)
1335                        .bind(to_view_i64),
1336                )
1337                .await?;
1338                tx.execute(
1339                    query("DELETE FROM state_cert where view >= $1 AND view <= $2")
1340                        .bind(from_view_i64)
1341                        .bind(to_view_i64),
1342                )
1343                .await?;
1344                tx.execute(
1345                    query("DELETE FROM decided_cert2 where view >= $1 AND view <= $2")
1346                        .bind(from_view_i64)
1347                        .bind(to_view_i64),
1348                )
1349                .await?;
1350
1351                // Clean up leaves, but do not delete the most recent one (all leaves with a view
1352                // number less than the given value). This is necessary to ensure that, in case of
1353                // a restart, we can resume from the last decided leaf.
1354                tx.execute(
1355                    query("DELETE FROM anchor_leaf2 WHERE view >= $1 AND view < $2")
1356                        .bind(from_view_i64)
1357                        .bind(to_view_i64),
1358                )
1359                .await?;
1360
1361                tx.commit().await?;
1362                Ok(())
1363            })
1364            .await?;
1365            last_processed_view = Some(to_view_i64);
1366            last_processed_height = to_height;
1367        }
1368    }
1369
1370    async fn load_state_certs(
1371        tx: &mut Transaction<Read>,
1372        from_view: ViewNumber,
1373        to_view: ViewNumber,
1374    ) -> anyhow::Result<BTreeMap<u64, LightClientStateUpdateCertificateV2<SeqTypes>>> {
1375        let rows = tx
1376            .fetch_all(
1377                query("SELECT view, state_cert FROM state_cert WHERE view >= $1 AND view <= $2")
1378                    .bind(from_view.u64() as i64)
1379                    .bind(to_view.u64() as i64),
1380            )
1381            .await?;
1382
1383        let mut result = BTreeMap::new();
1384
1385        for row in rows {
1386            let data: Vec<u8> = row.get("state_cert");
1387
1388            let cert: LightClientStateUpdateCertificateV2<SeqTypes> = bincode::deserialize(&data)
1389                .or_else(|err_v2| {
1390                bincode::deserialize::<LightClientStateUpdateCertificateV1<SeqTypes>>(&data)
1391                    .map(Into::into)
1392                    .context(format!(
1393                        "Failed to deserialize LightClientStateUpdateCertificate: with v1 and v2. \
1394                         error: {err_v2}"
1395                    ))
1396            })?;
1397
1398            result.insert(cert.epoch.u64(), cert);
1399        }
1400
1401        Ok(result)
1402    }
1403
1404    #[tracing::instrument(skip(self))]
1405    async fn prune(&self, cur_view: ViewNumber) -> anyhow::Result<()> {
1406        serializable_retry!(self, || async {
1407            let mut tx = self.db.write().await?;
1408
1409            // Prune everything older than the target retention period.
1410            prune_to_view(
1411                &mut tx,
1412                cur_view.u64().saturating_sub(self.gc_opt.target_retention),
1413            )
1414            .await?;
1415
1416            // Check our storage usage; if necessary we will prune more aggressively (up to the
1417            // minimum retention) to get below the target usage.
1418            #[cfg(feature = "embedded-db")]
1419            let usage_query = format!(
1420                "SELECT sum(pgsize) FROM dbstat WHERE name IN ({})",
1421                PRUNE_TABLES
1422                    .iter()
1423                    .map(|table| format!("'{table}'"))
1424                    .join(",")
1425            );
1426
1427            #[cfg(not(feature = "embedded-db"))]
1428            let usage_query = {
1429                let table_sizes = PRUNE_TABLES
1430                    .iter()
1431                    .map(|table| format!("pg_table_size('{table}')"))
1432                    .join(" + ");
1433                format!("SELECT {table_sizes}")
1434            };
1435
1436            let (usage,): (i64,) = query_as(&usage_query).fetch_one(tx.as_mut()).await?;
1437            tracing::debug!(usage, "consensus storage usage after pruning");
1438
1439            if (usage as u64) > self.gc_opt.target_usage {
1440                tracing::warn!(
1441                    usage,
1442                    gc_opt = ?self.gc_opt,
1443                    "consensus storage is running out of space, pruning to minimum retention"
1444                );
1445                prune_to_view(
1446                    &mut tx,
1447                    cur_view.u64().saturating_sub(self.gc_opt.minimum_retention),
1448                )
1449                .await?;
1450            }
1451
1452            tx.commit().await
1453        })
1454        .await
1455    }
1456}
1457
1458const PRUNE_TABLES: &[&str] = &[
1459    "anchor_leaf2",
1460    "vid_share2",
1461    "da_proposal2",
1462    "quorum_proposals2",
1463    "quorum_certificate2",
1464    "state_cert",
1465    "decided_cert2",
1466];
1467
1468async fn prune_to_view(tx: &mut Transaction<Write>, view: u64) -> anyhow::Result<()> {
1469    if view == 0 {
1470        // Nothing to prune, the entire chain is younger than the retention period.
1471        return Ok(());
1472    }
1473    tracing::debug!(view, "pruning consensus storage");
1474
1475    for table in PRUNE_TABLES {
1476        let res = query(&format!("DELETE FROM {table} WHERE view < $1"))
1477            .bind(view as i64)
1478            .execute(tx.as_mut())
1479            .await
1480            .context(format!("pruning {table}"))?;
1481        if res.rows_affected() > 0 {
1482            tracing::info!(
1483                "garbage collected {} rows from {table}",
1484                res.rows_affected()
1485            );
1486        }
1487    }
1488
1489    Ok(())
1490}
1491
1492#[async_trait]
1493impl SequencerPersistence for Persistence {
1494    fn into_catchup_provider(
1495        self,
1496        backoff: BackoffParams,
1497    ) -> anyhow::Result<Arc<dyn StateCatchup>> {
1498        Ok(Arc::new(SqlStateCatchup::new(Arc::new(self.db), backoff)))
1499    }
1500
1501    async fn load_config(&self) -> anyhow::Result<Option<NetworkConfig>> {
1502        tracing::info!("loading config from Postgres");
1503
1504        serializable_retry!(self, || async {
1505            // Select the most recent config (although there should only be one).
1506            let Some(row) = self
1507                .db
1508                .read()
1509                .await?
1510                .fetch_optional("SELECT config FROM network_config ORDER BY id DESC LIMIT 1")
1511                .await?
1512            else {
1513                tracing::info!("config not found");
1514                return Ok(None);
1515            };
1516            let json = row.try_get("config")?;
1517
1518            let json =
1519                migrate_network_config(json).context("migration of network config failed")?;
1520            let config = serde_json::from_value(json).context("malformed config file")?;
1521
1522            Ok(Some(config))
1523        })
1524        .await
1525    }
1526
1527    async fn save_config(&self, cfg: &NetworkConfig) -> anyhow::Result<()> {
1528        tracing::info!("saving config to database");
1529        let json = serde_json::to_value(cfg)?;
1530
1531        serializable_retry!(self, || async {
1532            let mut tx = self.db.write().await?;
1533            tx.execute(query("INSERT INTO network_config (config) VALUES ($1)").bind(json.clone()))
1534                .await?;
1535            tx.commit().await
1536        })
1537        .await
1538    }
1539
1540    async fn persist_decided_leaves(
1541        &self,
1542        _view: ViewNumber,
1543        leaf_chain: impl IntoIterator<Item = (&LeafInfo<SeqTypes>, CertificatePair<SeqTypes>)> + Send,
1544        _deciding_qc: Option<Arc<CertificatePair<SeqTypes>>>,
1545        _consumer: &(impl EventConsumer + 'static),
1546    ) -> anyhow::Result<()> {
1547        let values = leaf_chain
1548            .into_iter()
1549            .map(|(info, cert)| {
1550                // The leaf may come with a large payload attached. We don't care about this payload
1551                // because we already store it separately, as part of the DA proposal. Storing it
1552                // here contributes to load on the DB for no reason, so we remove it before
1553                // serializing the leaf.
1554                let mut leaf = info.leaf.clone();
1555                leaf.unfill_block_payload();
1556
1557                let view = cert.view_number().u64() as i64;
1558                let leaf_bytes = bincode::serialize(&leaf)?;
1559                let qc_bytes = bincode::serialize(cert.qc())?;
1560                let next_epoch_qc_bytes = match cert.next_epoch_qc() {
1561                    Some(qc) => Some(bincode::serialize(qc)?),
1562                    None => None,
1563                };
1564                Ok((view, leaf_bytes, qc_bytes, next_epoch_qc_bytes))
1565            })
1566            .collect::<anyhow::Result<Vec<_>>>()?;
1567
1568        // Append the new leaves. We do this in its own transaction because even if GC or the
1569        // event consumer later fails, there is no need to abort the storage of the leaves.
1570        serializable_retry!(self, || async {
1571            let mut tx = self.db.write().await?;
1572            tx.upsert(
1573                "anchor_leaf2",
1574                ["view", "leaf", "qc", "next_epoch_qc"],
1575                ["view"],
1576                values.clone(),
1577            )
1578            .await?;
1579            tx.commit().await
1580        })
1581        .await?;
1582
1583        Ok(())
1584    }
1585
1586    async fn process_decided_events(
1587        &self,
1588        view: ViewNumber,
1589        deciding_qc: Option<Arc<CertificatePair<SeqTypes>>>,
1590        consumer: &(impl EventConsumer + 'static),
1591    ) -> anyhow::Result<Option<ViewNumber>> {
1592        let now = Instant::now();
1593        // Generate events for the new leaves, then GC. On error `last_processed_view` is not
1594        // advanced past the failure point, so no data is lost and the range is retried.
1595        self.generate_decide_events(deciding_qc, consumer).await?;
1596
1597        // Best-effort GC of data not included in any decide event; runs again at the next decide.
1598        if let Err(err) = self.prune(view).await {
1599            tracing::warn!(?view, "pruning failed: {err:#}");
1600        }
1601        self.internal_metrics
1602            .internal_process_decided_events_duration
1603            .add_point(now.elapsed().as_secs_f64());
1604
1605        self.load_processed_view().await
1606    }
1607
1608    async fn load_latest_acted_view(&self) -> anyhow::Result<Option<ViewNumber>> {
1609        serializable_retry!(self, || async {
1610            Ok(self
1611                .db
1612                .read()
1613                .await?
1614                .fetch_optional(query("SELECT view FROM highest_voted_view WHERE id = 0"))
1615                .await?
1616                .map(|row| {
1617                    let view: i64 = row.get("view");
1618                    ViewNumber::new(view as u64)
1619                }))
1620        })
1621        .await
1622    }
1623
1624    async fn load_restart_view(&self) -> anyhow::Result<Option<ViewNumber>> {
1625        serializable_retry!(self, || async {
1626            Ok(self
1627                .db
1628                .read()
1629                .await?
1630                .fetch_optional(query("SELECT view FROM restart_view WHERE id = 0"))
1631                .await?
1632                .map(|row| {
1633                    let view: i64 = row.get("view");
1634                    ViewNumber::new(view as u64)
1635                }))
1636        })
1637        .await
1638    }
1639
1640    async fn load_anchor_leaf(&self) -> anyhow::Result<Option<(Leaf2, CertificatePair<SeqTypes>)>> {
1641        serializable_retry!(self, || async {
1642            let Some(row) = self
1643                .db
1644                .read()
1645                .await?
1646                .fetch_optional(
1647                    "SELECT leaf, qc, next_epoch_qc FROM anchor_leaf2 ORDER BY view DESC LIMIT 1",
1648                )
1649                .await?
1650            else {
1651                return Ok(None);
1652            };
1653
1654            let leaf_bytes: Vec<u8> = row.get("leaf");
1655            let leaf2: Leaf2 = bincode::deserialize(&leaf_bytes)?;
1656
1657            let qc_bytes: Vec<u8> = row.get("qc");
1658            let qc2: QuorumCertificate2<SeqTypes> = bincode::deserialize(&qc_bytes)?;
1659
1660            let maybe_next_qc_bytes: Option<Vec<u8>> = row.try_get("next_epoch_qc").ok();
1661            let maybe_next_qc2 = maybe_next_qc_bytes
1662                .and_then(|next_qc_bytes| bincode::deserialize(&next_qc_bytes).ok());
1663
1664            let cert_pair = CertificatePair::new(qc2, maybe_next_qc2);
1665
1666            Ok(Some((leaf2, cert_pair)))
1667        })
1668        .await
1669    }
1670
1671    async fn load_anchor_view(&self) -> anyhow::Result<ViewNumber> {
1672        serializable_retry!(self, || async {
1673            let mut tx = self.db.read().await?;
1674            let (view,) = query_as::<(i64,)>("SELECT coalesce(max(view), 0) FROM anchor_leaf2")
1675                .fetch_one(tx.as_mut())
1676                .await?;
1677            Ok(ViewNumber::new(view as u64))
1678        })
1679        .await
1680    }
1681
1682    async fn load_da_proposal(
1683        &self,
1684        view: ViewNumber,
1685    ) -> anyhow::Result<Option<Proposal<SeqTypes, DaProposal2<SeqTypes>>>> {
1686        serializable_retry!(self, || async {
1687            let result = self
1688                .db
1689                .read()
1690                .await?
1691                .fetch_optional(
1692                    query("SELECT data FROM da_proposal2 where view = $1").bind(view.u64() as i64),
1693                )
1694                .await?;
1695
1696            result
1697                .map(|row| {
1698                    let bytes: Vec<u8> = row.get("data");
1699                    anyhow::Result::<_>::Ok(bincode::deserialize(&bytes)?)
1700                })
1701                .transpose()
1702        })
1703        .await
1704    }
1705
1706    async fn load_vid_share(
1707        &self,
1708        view: ViewNumber,
1709    ) -> anyhow::Result<Option<Proposal<SeqTypes, VidDisperseShare<SeqTypes>>>> {
1710        serializable_retry!(self, || async {
1711            let result = self
1712                .db
1713                .read()
1714                .await?
1715                .fetch_optional(
1716                    query("SELECT data FROM vid_share2 where view = $1").bind(view.u64() as i64),
1717                )
1718                .await?;
1719
1720            result
1721                .map(|row| {
1722                    let bytes: Vec<u8> = row.get("data");
1723                    anyhow::Result::<_>::Ok(bincode::deserialize(&bytes)?)
1724                })
1725                .transpose()
1726        })
1727        .await
1728    }
1729
1730    async fn load_quorum_proposals(
1731        &self,
1732    ) -> anyhow::Result<BTreeMap<ViewNumber, Proposal<SeqTypes, QuorumProposalWrapper<SeqTypes>>>>
1733    {
1734        serializable_retry!(self, || async {
1735            let rows = self
1736                .db
1737                .read()
1738                .await?
1739                .fetch_all("SELECT * FROM quorum_proposals2")
1740                .await?;
1741
1742            Ok(BTreeMap::from_iter(
1743                rows.into_iter()
1744                    .map(|row| {
1745                        let view: i64 = row.get("view");
1746                        let view_number: ViewNumber = ViewNumber::new(view.try_into()?);
1747                        let bytes: Vec<u8> = row.get("data");
1748                        let proposal: Proposal<SeqTypes, QuorumProposalWrapper<SeqTypes>> =
1749                            bincode::deserialize(&bytes).or_else(|error| {
1750                                bincode::deserialize::<
1751                                    Proposal<SeqTypes, QuorumProposalWrapperLegacy<SeqTypes>>,
1752                                >(&bytes)
1753                                .map(convert_proposal)
1754                                .inspect_err(|err_v3| {
1755                                    tracing::warn!(
1756                                        ?view_number,
1757                                        %error,
1758                                        %err_v3,
1759                                        "ignoring malformed quorum proposal DB row"
1760                                    );
1761                                })
1762                            })?;
1763                        Ok((view_number, proposal))
1764                    })
1765                    .collect::<anyhow::Result<Vec<_>>>()?,
1766            ))
1767        })
1768        .await
1769    }
1770
1771    async fn load_quorum_proposal(
1772        &self,
1773        view: ViewNumber,
1774    ) -> anyhow::Result<Proposal<SeqTypes, QuorumProposalWrapper<SeqTypes>>> {
1775        serializable_retry!(self, || async {
1776            let mut tx = self.db.read().await?;
1777            let (data,) = query_as::<(Vec<u8>,)>(
1778                "SELECT data FROM quorum_proposals2 WHERE view = $1 LIMIT 1",
1779            )
1780            .bind(view.u64() as i64)
1781            .fetch_one(tx.as_mut())
1782            .await?;
1783            let proposal: Proposal<SeqTypes, QuorumProposalWrapper<SeqTypes>> =
1784                bincode::deserialize(&data).or_else(|error| {
1785                    bincode::deserialize::<
1786                                Proposal<SeqTypes, QuorumProposalWrapperLegacy<SeqTypes>>,
1787                            >(&data)
1788                            .map(convert_proposal)
1789                            .context(format!(
1790                                "Failed to deserialize quorum proposal for view {view}. \
1791                                 error={error}"
1792                            ))
1793                })?;
1794            Ok(proposal)
1795        })
1796        .await
1797    }
1798
1799    async fn append_vid(
1800        &self,
1801        proposal: &Proposal<SeqTypes, VidDisperseShare<SeqTypes>>,
1802    ) -> anyhow::Result<()> {
1803        let view = proposal.data.view_number().u64();
1804        let payload_hash = proposal.data.payload_commitment();
1805        let data_bytes = bincode::serialize(proposal).unwrap();
1806
1807        let now = Instant::now();
1808        let res = serializable_retry!(self, || async {
1809            let mut tx = self.db.write().await?;
1810            tx.upsert(
1811                "vid_share2",
1812                ["view", "data", "payload_hash"],
1813                ["view"],
1814                [(view as i64, data_bytes.clone(), payload_hash.to_string())],
1815            )
1816            .await?;
1817            tx.commit().await
1818        })
1819        .await;
1820        self.internal_metrics
1821            .internal_append_vid_duration
1822            .add_point(now.elapsed().as_secs_f64());
1823        res
1824    }
1825
1826    async fn append_da(
1827        &self,
1828        proposal: &Proposal<SeqTypes, DaProposal<SeqTypes>>,
1829        vid_commit: VidCommitment,
1830    ) -> anyhow::Result<()> {
1831        let data = &proposal.data;
1832        let view = data.view_number().u64();
1833        let data_bytes = bincode::serialize(proposal).unwrap();
1834
1835        let now = Instant::now();
1836        let res = serializable_retry!(self, || async {
1837            let mut tx = self.db.write().await?;
1838            tx.upsert(
1839                "da_proposal",
1840                ["view", "data", "payload_hash"],
1841                ["view"],
1842                [(view as i64, data_bytes.clone(), vid_commit.to_string())],
1843            )
1844            .await?;
1845            tx.commit().await
1846        })
1847        .await;
1848        self.internal_metrics
1849            .internal_append_da_duration
1850            .add_point(now.elapsed().as_secs_f64());
1851        res
1852    }
1853
1854    async fn record_action(
1855        &self,
1856        view: ViewNumber,
1857        _epoch: Option<EpochNumber>,
1858        action: HotShotAction,
1859    ) -> anyhow::Result<()> {
1860        // Todo Remove this after https://github.com/EspressoSystems/espresso-network/issues/1931
1861        if !matches!(action, HotShotAction::Propose | HotShotAction::Vote) {
1862            return Ok(());
1863        }
1864
1865        serializable_retry!(self, || async {
1866            let stmt = format!(
1867                "INSERT INTO highest_voted_view (id, view) VALUES (0, $1)
1868                ON CONFLICT (id) DO UPDATE SET view = {MAX_FN}(highest_voted_view.view, \
1869                 excluded.view)"
1870            );
1871
1872            let mut tx = self.db.write().await?;
1873            tx.execute(query(&stmt).bind(view.u64() as i64)).await?;
1874
1875            if matches!(action, HotShotAction::Vote) {
1876                let restart_view = view + 1;
1877                let stmt = format!(
1878                    "INSERT INTO restart_view (id, view) VALUES (0, $1)
1879                    ON CONFLICT (id) DO UPDATE SET view = {MAX_FN}(restart_view.view, \
1880                     excluded.view)"
1881                );
1882                tx.execute(query(&stmt).bind(restart_view.u64() as i64))
1883                    .await?;
1884            }
1885
1886            tx.commit().await
1887        })
1888        .await
1889    }
1890
1891    async fn append_quorum_proposal2(
1892        &self,
1893        proposal: &Proposal<SeqTypes, QuorumProposalWrapper<SeqTypes>>,
1894    ) -> anyhow::Result<()> {
1895        let view_number = proposal.data.view_number().u64();
1896
1897        let proposal_bytes = bincode::serialize(&proposal).context("serializing proposal")?;
1898        let leaf_hash = Committable::commit(&Leaf2::from_quorum_proposal(&proposal.data));
1899
1900        // We also keep track of any QC we see in case we need it to recover our archival storage.
1901        let justify_qc = proposal.data.justify_qc();
1902        let justify_qc_bytes = bincode::serialize(&justify_qc).context("serializing QC")?;
1903        let justify_qc_view = justify_qc.view_number.u64() as i64;
1904        let justify_qc_leaf_commit = justify_qc.data.leaf_commit.to_string();
1905
1906        let now = Instant::now();
1907        let res = serializable_retry!(self, || async {
1908            let mut tx = self.db.write().await?;
1909            tx.upsert(
1910                "quorum_proposals2",
1911                ["view", "leaf_hash", "data"],
1912                ["view"],
1913                [(
1914                    view_number as i64,
1915                    leaf_hash.to_string(),
1916                    proposal_bytes.clone(),
1917                )],
1918            )
1919            .await?;
1920            tx.upsert(
1921                "quorum_certificate2",
1922                ["view", "leaf_hash", "data"],
1923                ["view"],
1924                [(
1925                    justify_qc_view,
1926                    justify_qc_leaf_commit.clone(),
1927                    justify_qc_bytes.clone(),
1928                )],
1929            )
1930            .await?;
1931            tx.commit().await
1932        })
1933        .await;
1934        self.internal_metrics
1935            .internal_append_quorum2_duration
1936            .add_point(now.elapsed().as_secs_f64());
1937        res
1938    }
1939
1940    async fn append_cert2(
1941        &self,
1942        view: ViewNumber,
1943        cert2: Certificate2<SeqTypes>,
1944    ) -> anyhow::Result<()> {
1945        let data = bincode::serialize(&cert2).context("serializing cert2")?;
1946        let view_i64 = view.u64() as i64;
1947        serializable_retry!(self, || async {
1948            let mut tx = self.db.write().await?;
1949            tx.upsert(
1950                "decided_cert2",
1951                ["view", "data"],
1952                ["view"],
1953                [(view_i64, data.clone())],
1954            )
1955            .await?;
1956            tx.commit().await
1957        })
1958        .await
1959    }
1960
1961    async fn load_cert2(&self, view: ViewNumber) -> anyhow::Result<Option<Certificate2<SeqTypes>>> {
1962        let row = self
1963            .db
1964            .read()
1965            .await?
1966            .fetch_optional(
1967                query("SELECT data FROM decided_cert2 WHERE view = $1").bind(view.u64() as i64),
1968            )
1969            .await?;
1970        row.map(|row| {
1971            let bytes: Vec<u8> = row.get("data");
1972            bincode::deserialize::<Certificate2<SeqTypes>>(&bytes).context("deserializing cert2")
1973        })
1974        .transpose()
1975    }
1976
1977    async fn append_high_qc2(&self, high_qc: QuorumCertificate2<SeqTypes>) -> anyhow::Result<()> {
1978        let view = high_qc.view_number();
1979        let data = bincode::serialize(&high_qc).context("serializing high_qc2")?;
1980        serializable_retry!(self, || async {
1981            let mut tx = self.db.write().await?;
1982            // Compare-and-set inside one write transaction so a stale
1983            // concurrent write can never regress the stored view: under
1984            // SERIALIZABLE a racing writer conflicts and retries; on SQLite
1985            // writes are serialized.
1986            let stored_view = query("SELECT data FROM high_qc2 WHERE id = true")
1987                .fetch_optional(tx.as_mut())
1988                .await?
1989                .map(|row| {
1990                    let bytes: Vec<u8> = row.get("data");
1991                    bincode::deserialize::<QuorumCertificate2<SeqTypes>>(&bytes)
1992                        .context("deserializing existing high_qc2")
1993                        .map(|qc| qc.view_number())
1994                })
1995                .transpose()?;
1996            if stored_view.is_some_and(|stored| stored >= view) {
1997                return Ok(());
1998            }
1999            tx.upsert("high_qc2", ["id", "data"], ["id"], [(true, data.clone())])
2000                .await?;
2001            tx.commit().await
2002        })
2003        .await
2004    }
2005
2006    async fn load_high_qc2(&self) -> anyhow::Result<Option<QuorumCertificate2<SeqTypes>>> {
2007        let row = self
2008            .db
2009            .read()
2010            .await?
2011            .fetch_optional("SELECT data FROM high_qc2 WHERE id = true")
2012            .await?;
2013        row.map(|row| {
2014            let bytes: Vec<u8> = row.get("data");
2015            bincode::deserialize::<QuorumCertificate2<SeqTypes>>(&bytes)
2016                .context("deserializing high_qc2")
2017        })
2018        .transpose()
2019    }
2020
2021    async fn load_upgrade_certificate(
2022        &self,
2023    ) -> anyhow::Result<Option<UpgradeCertificate<SeqTypes>>> {
2024        let result = self
2025            .db
2026            .read()
2027            .await?
2028            .fetch_optional("SELECT * FROM upgrade_certificate where id = true")
2029            .await?;
2030
2031        result
2032            .map(|row| {
2033                let bytes: Vec<u8> = row.get("data");
2034                anyhow::Result::<_>::Ok(bincode::deserialize(&bytes)?)
2035            })
2036            .transpose()
2037    }
2038
2039    async fn store_upgrade_certificate(
2040        &self,
2041        decided_upgrade_certificate: Option<UpgradeCertificate<SeqTypes>>,
2042    ) -> anyhow::Result<()> {
2043        let certificate = match decided_upgrade_certificate {
2044            Some(cert) => cert,
2045            None => return Ok(()),
2046        };
2047        let upgrade_certificate_bytes =
2048            bincode::serialize(&certificate).context("serializing upgrade certificate")?;
2049        serializable_retry!(self, || async {
2050            let mut tx = self.db.write().await?;
2051            tx.upsert(
2052                "upgrade_certificate",
2053                ["id", "data"],
2054                ["id"],
2055                [(true, upgrade_certificate_bytes.clone())],
2056            )
2057            .await?;
2058            tx.commit().await
2059        })
2060        .await
2061    }
2062
2063    async fn load_next_epoch_quorum_certificate(
2064        &self,
2065    ) -> anyhow::Result<Option<NextEpochQuorumCertificate2<SeqTypes>>> {
2066        let result = self
2067            .db
2068            .read()
2069            .await?
2070            .fetch_optional("SELECT * FROM next_epoch_quorum_certificate where id = true")
2071            .await?;
2072
2073        result
2074            .map(|row| {
2075                let bytes: Vec<u8> = row.get("data");
2076                anyhow::Result::<_>::Ok(bincode::deserialize(&bytes)?)
2077            })
2078            .transpose()
2079    }
2080
2081    async fn append_next_epoch_high_qc2(
2082        &self,
2083        next_epoch_high_qc: NextEpochQuorumCertificate2<SeqTypes>,
2084    ) -> anyhow::Result<()> {
2085        let view = next_epoch_high_qc.view_number();
2086        let data =
2087            bincode::serialize(&next_epoch_high_qc).context("serializing next epoch high_qc2")?;
2088        serializable_retry!(self, || async {
2089            let mut tx = self.db.write().await?;
2090            let stored_view =
2091                query("SELECT data FROM next_epoch_quorum_certificate WHERE id = true")
2092                    .fetch_optional(tx.as_mut())
2093                    .await?
2094                    .map(|row| {
2095                        let bytes: Vec<u8> = row.get("data");
2096                        bincode::deserialize::<NextEpochQuorumCertificate2<SeqTypes>>(&bytes)
2097                            .context("deserializing existing next epoch high_qc2")
2098                            .map(|qc| qc.view_number())
2099                    })
2100                    .transpose()?;
2101            if stored_view.is_some_and(|stored| stored >= view) {
2102                return Ok(());
2103            }
2104            tx.upsert(
2105                "next_epoch_quorum_certificate",
2106                ["id", "data"],
2107                ["id"],
2108                [(true, data.clone())],
2109            )
2110            .await?;
2111            tx.commit().await
2112        })
2113        .await
2114    }
2115
2116    async fn store_eqc(
2117        &self,
2118        high_qc: QuorumCertificate2<SeqTypes>,
2119        next_epoch_high_qc: NextEpochQuorumCertificate2<SeqTypes>,
2120    ) -> anyhow::Result<()> {
2121        let eqc_bytes =
2122            bincode::serialize(&(high_qc, next_epoch_high_qc)).context("serializing eqc")?;
2123        serializable_retry!(self, || async {
2124            let mut tx = self.db.write().await?;
2125            tx.upsert("eqc", ["id", "data"], ["id"], [(true, eqc_bytes.clone())])
2126                .await?;
2127            tx.commit().await
2128        })
2129        .await
2130    }
2131
2132    async fn load_eqc(
2133        &self,
2134    ) -> Option<(
2135        QuorumCertificate2<SeqTypes>,
2136        NextEpochQuorumCertificate2<SeqTypes>,
2137    )> {
2138        let result = self
2139            .db
2140            .read()
2141            .await
2142            .ok()?
2143            .fetch_optional("SELECT * FROM eqc where id = true")
2144            .await
2145            .ok()?;
2146
2147        result
2148            .map(|row| {
2149                let bytes: Vec<u8> = row.get("data");
2150                bincode::deserialize(&bytes)
2151            })
2152            .transpose()
2153            .ok()?
2154    }
2155
2156    async fn append_da2(
2157        &self,
2158        proposal: &Proposal<SeqTypes, DaProposal2<SeqTypes>>,
2159        vid_commit: VidCommitment,
2160    ) -> anyhow::Result<()> {
2161        let data = &proposal.data;
2162        let view = data.view_number().u64();
2163        let data_bytes = bincode::serialize(proposal).unwrap();
2164
2165        let now = Instant::now();
2166        let res = serializable_retry!(self, || async {
2167            let mut tx = self.db.write().await?;
2168            tx.upsert(
2169                "da_proposal2",
2170                ["view", "data", "payload_hash"],
2171                ["view"],
2172                [(view as i64, data_bytes.clone(), vid_commit.to_string())],
2173            )
2174            .await?;
2175            tx.commit().await
2176        })
2177        .await;
2178        self.internal_metrics
2179            .internal_append_da2_duration
2180            .add_point(now.elapsed().as_secs_f64());
2181        res
2182    }
2183
2184    async fn store_drb_result(
2185        &self,
2186        epoch: EpochNumber,
2187        drb_result: DrbResult,
2188    ) -> anyhow::Result<()> {
2189        let epoch_i64 = epoch.u64() as i64;
2190        let drb_result_vec = Vec::from(drb_result);
2191        serializable_retry!(self, || async {
2192            let mut tx = self.db.write().await?;
2193            tx.upsert(
2194                "epoch_drb_and_root",
2195                ["epoch", "drb_result"],
2196                ["epoch"],
2197                [(epoch_i64, drb_result_vec.clone())],
2198            )
2199            .await?;
2200            tx.commit().await
2201        })
2202        .await
2203    }
2204
2205    async fn store_drb_input(&self, drb_input: DrbInput) -> anyhow::Result<()> {
2206        if let Ok(loaded_drb_input) = self.load_drb_input(drb_input.epoch).await {
2207            if loaded_drb_input.difficulty_level != drb_input.difficulty_level {
2208                tracing::error!("Overwriting {loaded_drb_input:?} in storage with {drb_input:?}");
2209            } else if loaded_drb_input.iteration >= drb_input.iteration {
2210                anyhow::bail!(
2211                    "DrbInput in storage {:?} is more recent than {:?}, refusing to update",
2212                    loaded_drb_input,
2213                    drb_input
2214                )
2215            }
2216        }
2217
2218        let drb_epoch_i64 = drb_input.epoch as i64;
2219        let drb_input_bytes = bincode::serialize(&drb_input)
2220            .context("Failed to serialize DrbInput. This is not fatal, but should never happen.")?;
2221
2222        serializable_retry!(self, || async {
2223            let mut tx = self.db.write().await?;
2224            tx.upsert(
2225                "drb",
2226                ["epoch", "drb_input"],
2227                ["epoch"],
2228                [(drb_epoch_i64, drb_input_bytes.clone())],
2229            )
2230            .await?;
2231            tx.commit().await
2232        })
2233        .await
2234    }
2235
2236    async fn load_drb_input(&self, epoch: u64) -> anyhow::Result<DrbInput> {
2237        let row = self
2238            .db
2239            .read()
2240            .await?
2241            .fetch_optional(query("SELECT drb_input FROM drb WHERE epoch = $1").bind(epoch as i64))
2242            .await?;
2243
2244        match row {
2245            None => anyhow::bail!("No DrbInput for epoch {} in storage", epoch),
2246            Some(row) => {
2247                let drb_input_bytes: Vec<u8> = row.try_get("drb_input")?;
2248                let drb_input = bincode::deserialize(&drb_input_bytes)
2249                    .context("Failed to deserialize drb_input from storage")?;
2250
2251                Ok(drb_input)
2252            },
2253        }
2254    }
2255
2256    async fn add_state_cert(
2257        &self,
2258        state_cert: LightClientStateUpdateCertificateV2<SeqTypes>,
2259    ) -> anyhow::Result<()> {
2260        let view_number = state_cert.light_client_state.view_number as i64;
2261        let state_cert_bytes = bincode::serialize(&state_cert)
2262            .context("serializing light client state update certificate")?;
2263
2264        serializable_retry!(self, || async {
2265            let mut tx = self.db.write().await?;
2266            tx.upsert(
2267                "state_cert",
2268                ["view", "state_cert"],
2269                ["view"],
2270                [(view_number, state_cert_bytes.clone())],
2271            )
2272            .await?;
2273            tx.commit().await
2274        })
2275        .await
2276    }
2277
2278    async fn load_state_cert(
2279        &self,
2280    ) -> anyhow::Result<Option<LightClientStateUpdateCertificateV2<SeqTypes>>> {
2281        let Some(row) = self
2282            .db
2283            .read()
2284            .await?
2285            .fetch_optional(
2286                "SELECT state_cert FROM finalized_state_cert ORDER BY epoch DESC LIMIT 1",
2287            )
2288            .await?
2289        else {
2290            return Ok(None);
2291        };
2292        let bytes: Vec<u8> = row.get("state_cert");
2293
2294        let cert = match bincode::deserialize(&bytes) {
2295            Ok(cert) => cert,
2296            Err(err) => {
2297                tracing::info!(
2298                    error = %err,
2299                    "Failed to deserialize state certificate with v2. attempting with v1"
2300                );
2301
2302                let v1_cert =
2303                    bincode::deserialize::<LightClientStateUpdateCertificateV1<SeqTypes>>(&bytes)
2304                        .with_context(|| {
2305                        format!("Failed to deserialize using both v1 and v2. error: {err}")
2306                    })?;
2307
2308                v1_cert.into()
2309            },
2310        };
2311
2312        Ok(Some(cert))
2313    }
2314
2315    async fn get_state_cert_by_epoch(
2316        &self,
2317        epoch: u64,
2318    ) -> anyhow::Result<Option<LightClientStateUpdateCertificateV2<SeqTypes>>> {
2319        let Some(row) = self
2320            .db
2321            .read()
2322            .await?
2323            .fetch_optional(
2324                query("SELECT state_cert FROM finalized_state_cert WHERE epoch = $1")
2325                    .bind(epoch as i64),
2326            )
2327            .await?
2328        else {
2329            return Ok(None);
2330        };
2331        let bytes: Vec<u8> = row.get("state_cert");
2332
2333        let cert = match bincode::deserialize(&bytes) {
2334            Ok(cert) => cert,
2335            Err(err) => {
2336                tracing::info!(
2337                    error = %err,
2338                    "Failed to deserialize state certificate with v2. attempting with v1"
2339                );
2340
2341                let v1_cert =
2342                    bincode::deserialize::<LightClientStateUpdateCertificateV1<SeqTypes>>(&bytes)
2343                        .with_context(|| {
2344                        format!("Failed to deserialize using both v1 and v2. error: {err}")
2345                    })?;
2346
2347                v1_cert.into()
2348            },
2349        };
2350
2351        Ok(Some(cert))
2352    }
2353
2354    async fn insert_state_cert(
2355        &self,
2356        epoch: u64,
2357        cert: LightClientStateUpdateCertificateV2<SeqTypes>,
2358    ) -> anyhow::Result<()> {
2359        let epoch_i64 = epoch as i64;
2360        let bytes = bincode::serialize(&cert)
2361            .with_context(|| format!("Failed to serialize state cert for epoch {epoch}"))?;
2362
2363        serializable_retry!(self, || async {
2364            let mut tx = self.db.write().await?;
2365            tx.upsert(
2366                "finalized_state_cert",
2367                ["epoch", "state_cert"],
2368                ["epoch"],
2369                [(epoch_i64, bytes.clone())],
2370            )
2371            .await?;
2372            tx.commit().await
2373        })
2374        .await
2375    }
2376
2377    async fn load_start_epoch_info(&self) -> anyhow::Result<Vec<InitializerEpochInfo<SeqTypes>>> {
2378        let rows = self
2379            .db
2380            .read()
2381            .await?
2382            .fetch_all(
2383                query("SELECT * from epoch_drb_and_root ORDER BY epoch DESC LIMIT $1")
2384                    .bind(RECENT_STAKE_TABLES_LIMIT as i64),
2385            )
2386            .await?;
2387
2388        // reverse the rows vector to return the most recent epochs, but in ascending order
2389        rows.into_iter()
2390            .rev()
2391            .map(|row| {
2392                let epoch: i64 = row.try_get("epoch")?;
2393                let drb_result: Option<Vec<u8>> = row.try_get("drb_result")?;
2394                let block_header: Option<Vec<u8>> = row.try_get("block_header")?;
2395                if let Some(drb_result) = drb_result {
2396                    let drb_result_array = drb_result
2397                        .try_into()
2398                        .or_else(|_| bail!("invalid drb result"))?;
2399                    let block_header: Option<<SeqTypes as NodeType>::BlockHeader> = block_header
2400                        .map(|data| bincode::deserialize(&data))
2401                        .transpose()?;
2402                    Ok(Some(InitializerEpochInfo::<SeqTypes> {
2403                        epoch: EpochNumber::new(epoch as u64),
2404                        drb_result: drb_result_array,
2405                        block_header,
2406                    }))
2407                } else {
2408                    // Right now we skip the epoch_drb_and_root row if there is no drb result.
2409                    // This seems reasonable based on the expected order of events, but please double check!
2410                    Ok(None)
2411                }
2412            })
2413            .filter_map(|e| match e {
2414                Err(v) => Some(Err(v)),
2415                Ok(Some(v)) => Some(Ok(v)),
2416                Ok(None) => None,
2417            })
2418            .collect()
2419    }
2420
2421    fn enable_metrics(&mut self, metrics: &dyn Metrics) {
2422        self.internal_metrics = PersistenceMetricsValue::new(metrics);
2423    }
2424}
2425
2426fn deserialize_authenticated_validator_map(
2427    bytes: &[u8],
2428) -> anyhow::Result<AuthenticatedValidatorMap> {
2429    if let Ok(map) = bincode::deserialize::<AuthenticatedValidatorMap>(bytes) {
2430        return Ok(map);
2431    }
2432
2433    // Pre-Schnorr-Option: stake_table_key as Option<KEY>, state_ver_key as raw KEY.
2434    if let Ok(pre_schnorr) =
2435        bincode::deserialize::<IndexMap<Address, super::RegisteredValidatorPreSchnorrOption>>(bytes)
2436    {
2437        return pre_schnorr
2438            .into_iter()
2439            .map(|(addr, v)| {
2440                let registered = v.migrate();
2441                let authenticated = AuthenticatedValidator::try_from(registered)?;
2442                Ok((addr, authenticated))
2443            })
2444            .collect();
2445    }
2446
2447    // Pre-Option: both keys raw, with x25519/p2p fields.
2448    let legacy: IndexMap<Address, super::RegisteredValidatorPreOption> =
2449        bincode::deserialize(bytes).context("deserializing stake table")?;
2450    legacy
2451        .into_iter()
2452        .map(|(addr, v)| {
2453            let registered = v.migrate();
2454            let authenticated = AuthenticatedValidator::try_from(registered)?;
2455            Ok((addr, authenticated))
2456        })
2457        .collect()
2458}
2459
2460#[async_trait]
2461impl MembershipPersistence for Persistence {
2462    async fn load_stake(&self, epoch: EpochNumber) -> anyhow::Result<Option<StakeTuple>> {
2463        let result = self
2464            .db
2465            .read()
2466            .await?
2467            .fetch_optional(
2468                query(
2469                    "SELECT stake, block_reward, stake_table_hash FROM epoch_drb_and_root WHERE \
2470                     epoch = $1 AND stake IS NOT NULL",
2471                )
2472                .bind(epoch.u64() as i64),
2473            )
2474            .await?;
2475
2476        result
2477            .map(|row| {
2478                let stake_table_bytes: Vec<u8> = row.get("stake");
2479                let reward_bytes: Option<Vec<u8>> = row.get("block_reward");
2480                let stake_table_hash_bytes: Option<Vec<u8>> = row.get("stake_table_hash");
2481                let stake_table = deserialize_authenticated_validator_map(&stake_table_bytes)?;
2482                let reward: Option<RewardAmount> = reward_bytes
2483                    .map(|b| bincode::deserialize(&b).context("deserializing block_reward"))
2484                    .transpose()?;
2485                let stake_table_hash: Option<StakeTableHash> = stake_table_hash_bytes
2486                    .map(|b| bincode::deserialize(&b).context("deserializing stake table hash"))
2487                    .transpose()?;
2488
2489                Ok((stake_table, reward, stake_table_hash))
2490            })
2491            .transpose()
2492    }
2493
2494    async fn load_drb_result(&self, epoch: EpochNumber) -> anyhow::Result<Option<DrbResult>> {
2495        let result = self
2496            .db
2497            .read()
2498            .await?
2499            .fetch_optional(
2500                query(
2501                    "SELECT drb_result FROM epoch_drb_and_root WHERE epoch = $1 AND drb_result IS \
2502                     NOT NULL",
2503                )
2504                .bind(epoch.u64() as i64),
2505            )
2506            .await?;
2507
2508        result
2509            .map(|row| {
2510                let bytes: Vec<u8> = row.get("drb_result");
2511                bytes.try_into().or_else(|_| bail!("invalid drb result"))
2512            })
2513            .transpose()
2514    }
2515
2516    async fn load_epoch_root(&self, epoch: EpochNumber) -> anyhow::Result<Option<Header>> {
2517        let result = self
2518            .db
2519            .read()
2520            .await?
2521            .fetch_optional(
2522                query(
2523                    "SELECT block_header FROM epoch_drb_and_root WHERE epoch = $1 AND \
2524                     block_header IS NOT NULL",
2525                )
2526                .bind(epoch.u64() as i64),
2527            )
2528            .await?;
2529
2530        result
2531            .map(|row| {
2532                let bytes: Vec<u8> = row.get("block_header");
2533                bincode::deserialize(&bytes).context("deserializing block header")
2534            })
2535            .transpose()
2536    }
2537
2538    async fn store_epoch_root(
2539        &self,
2540        epoch: EpochNumber,
2541        block_header: Header,
2542    ) -> anyhow::Result<()> {
2543        let epoch_i64 = epoch.u64() as i64;
2544        let block_header_bytes =
2545            bincode::serialize(&block_header).context("serializing block header")?;
2546
2547        serializable_retry!(self, || async {
2548            let mut tx = self.db.write().await?;
2549            tx.upsert(
2550                "epoch_drb_and_root",
2551                ["epoch", "block_header"],
2552                ["epoch"],
2553                [(epoch_i64, block_header_bytes.clone())],
2554            )
2555            .await?;
2556            tx.commit().await
2557        })
2558        .await
2559    }
2560
2561    async fn load_latest_stake(&self, limit: u64) -> anyhow::Result<Option<Vec<IndexedStake>>> {
2562        let mut tx = self.db.read().await?;
2563
2564        let rows = match query_as::<(i64, Vec<u8>, Option<Vec<u8>>, Option<Vec<u8>>)>(
2565            "SELECT epoch, stake, block_reward, stake_table_hash FROM epoch_drb_and_root WHERE \
2566             stake is NOT NULL ORDER BY epoch DESC LIMIT $1",
2567        )
2568        .bind(limit as i64)
2569        .fetch_all(tx.as_mut())
2570        .await
2571        {
2572            Ok(bytes) => bytes,
2573            Err(err) => {
2574                tracing::error!("error loading stake tables: {err:#}");
2575                bail!("{err:#}");
2576            },
2577        };
2578
2579        let stakes: anyhow::Result<Vec<IndexedStake>> = rows
2580            .into_iter()
2581            .map(
2582                |(id, stake_bytes, reward_bytes_opt, stake_table_hash_bytes_opt)| {
2583                    let stake_table = deserialize_authenticated_validator_map(&stake_bytes)?;
2584
2585                    let block_reward: Option<RewardAmount> = reward_bytes_opt
2586                        .map(|b| bincode::deserialize(&b).context("deserializing block_reward"))
2587                        .transpose()?;
2588
2589                    let stake_table_hash: Option<StakeTableHash> = stake_table_hash_bytes_opt
2590                        .map(|b| bincode::deserialize(&b).context("deserializing stake table hash"))
2591                        .transpose()?;
2592
2593                    Ok((
2594                        EpochNumber::new(id as u64),
2595                        (stake_table, block_reward),
2596                        stake_table_hash,
2597                    ))
2598                },
2599            )
2600            .collect();
2601
2602        Ok(Some(stakes?))
2603    }
2604
2605    async fn store_stake(
2606        &self,
2607        epoch: EpochNumber,
2608        stake: AuthenticatedValidatorMap,
2609        block_reward: Option<RewardAmount>,
2610        stake_table_hash: Option<StakeTableHash>,
2611    ) -> anyhow::Result<()> {
2612        let epoch_i64 = epoch.u64() as i64;
2613        let stake_table_bytes = bincode::serialize(&stake).context("serializing stake table")?;
2614        let reward_bytes = block_reward
2615            .map(|r| bincode::serialize(&r).context("serializing block reward"))
2616            .transpose()?;
2617        let stake_table_hash_bytes = stake_table_hash
2618            .map(|h| bincode::serialize(&h).context("serializing stake table hash"))
2619            .transpose()?;
2620        serializable_retry!(self, || async {
2621            let mut tx = self.db.write().await?;
2622            tx.upsert(
2623                "epoch_drb_and_root",
2624                ["epoch", "stake", "block_reward", "stake_table_hash"],
2625                ["epoch"],
2626                [(
2627                    epoch_i64,
2628                    stake_table_bytes.clone(),
2629                    reward_bytes.clone(),
2630                    stake_table_hash_bytes.clone(),
2631                )],
2632            )
2633            .await?;
2634            tx.commit().await
2635        })
2636        .await
2637    }
2638
2639    async fn store_events(
2640        &self,
2641        l1_finalized: u64,
2642        events: Vec<(EventKey, StakeTableEvent)>,
2643    ) -> anyhow::Result<()> {
2644        let l1_finalized_i64: i64 = l1_finalized.try_into()?;
2645        let serialized_events = events
2646            .into_iter()
2647            .map(|((block_number, index), event)| {
2648                Ok((
2649                    i64::try_from(block_number)?,
2650                    i64::try_from(index)?,
2651                    serde_json::to_value(event).context("l1 event to value")?,
2652                ))
2653            })
2654            .collect::<anyhow::Result<Vec<_>>>()?;
2655
2656        serializable_retry!(self, || async {
2657            let mut tx = self.db.write().await?;
2658
2659            // check last l1 block if there is any
2660            let last_processed_l1_block = query_as::<(i64,)>(
2661                "SELECT last_l1_block FROM stake_table_events_l1_block where id = 0",
2662            )
2663            .fetch_optional(tx.as_mut())
2664            .await?
2665            .map(|(l1,)| l1);
2666
2667            tracing::debug!("last l1 finalizes in database = {last_processed_l1_block:?}");
2668
2669            // skip events storage if the database already has higher l1 block events
2670            let serialized_events_len = serialized_events.len();
2671            if last_processed_l1_block > Some(l1_finalized_i64) {
2672                tracing::debug!(
2673                    ?last_processed_l1_block,
2674                    l1_finalized,
2675                    serialized_events_len,
2676                    "last l1 finalized stored is already higher"
2677                );
2678                return Ok(());
2679            }
2680
2681            if !serialized_events.is_empty() {
2682                let mut query_builder: sqlx::QueryBuilder<Db> = sqlx::QueryBuilder::new(
2683                    "INSERT INTO stake_table_events (l1_block, log_index, event) ",
2684                );
2685
2686                query_builder.push_values(
2687                    serialized_events.iter().cloned(),
2688                    |mut b, (l1_block, log_index, event)| {
2689                        b.push_bind(l1_block).push_bind(log_index).push_bind(event);
2690                    },
2691                );
2692
2693                query_builder.push(" ON CONFLICT DO NOTHING");
2694                let query = query_builder.build();
2695
2696                query.execute(tx.as_mut()).await?;
2697            }
2698
2699            // update l1 block
2700            tx.upsert(
2701                "stake_table_events_l1_block",
2702                ["id", "last_l1_block"],
2703                ["id"],
2704                [(0_i32, l1_finalized_i64)],
2705            )
2706            .await?;
2707
2708            tx.commit().await?;
2709
2710            Ok(())
2711        })
2712        .await
2713    }
2714
2715    /// Loads all events from persistent storage up to the specified L1 block.
2716    ///
2717    /// # Returns
2718    ///
2719    /// Returns a tuple containing:
2720    /// - `Option<u64>` - The queried L1 block for which all events have been successfully fetched.
2721    /// - `Vec<(EventKey, StakeTableEvent)>` - A list of events, where each entry is a tuple of the event key
2722    /// event key is (l1 block number, log index)
2723    ///   and the corresponding StakeTable event.
2724    ///
2725    async fn load_events(
2726        &self,
2727        from_l1_block: u64,
2728        to_l1_block: u64,
2729    ) -> anyhow::Result<(
2730        Option<EventsPersistenceRead>,
2731        Vec<(EventKey, StakeTableEvent)>,
2732    )> {
2733        let mut tx = self.db.read().await?;
2734
2735        // check last l1 block if there is any
2736        let res = query_as::<(i64,)>(
2737            "SELECT last_l1_block FROM stake_table_events_l1_block where id = 0",
2738        )
2739        .fetch_optional(tx.as_mut())
2740        .await?;
2741
2742        let Some((last_processed_l1_block,)) = res else {
2743            // this just means we dont have any events stored
2744            return Ok((None, Vec::new()));
2745        };
2746
2747        // Determine the L1 block for querying events.
2748        // If the last stored L1 block is greater than the requested block, limit the query to the requested block.
2749        // Otherwise, query up to the last stored block.
2750        let to_l1_block = to_l1_block.try_into()?;
2751        let query_l1_block = if last_processed_l1_block > to_l1_block {
2752            to_l1_block
2753        } else {
2754            last_processed_l1_block
2755        };
2756
2757        let rows = query(
2758            "SELECT l1_block, log_index, event FROM stake_table_events WHERE $1 <= l1_block AND \
2759             l1_block <= $2 ORDER BY l1_block ASC, log_index ASC",
2760        )
2761        .bind(i64::try_from(from_l1_block)?)
2762        .bind(query_l1_block)
2763        .fetch_all(tx.as_mut())
2764        .await?;
2765
2766        let events = rows
2767            .into_iter()
2768            .map(|row| {
2769                let l1_block: i64 = row.try_get("l1_block")?;
2770                let log_index: i64 = row.try_get("log_index")?;
2771                let event = serde_json::from_value(row.try_get("event")?)?;
2772
2773                Ok(((l1_block.try_into()?, log_index.try_into()?), event))
2774            })
2775            .collect::<anyhow::Result<Vec<_>>>()?;
2776
2777        // Determine the read state based on the queried block range.
2778        // - If the persistence returned events up to the requested block, the read is complete.
2779        // - Otherwise, indicate that the read is up to the last processed block.
2780        if query_l1_block == to_l1_block {
2781            Ok((Some(EventsPersistenceRead::Complete), events))
2782        } else {
2783            Ok((
2784                Some(EventsPersistenceRead::UntilL1Block(
2785                    query_l1_block.try_into()?,
2786                )),
2787                events,
2788            ))
2789        }
2790    }
2791
2792    async fn delete_stake_tables(&self) -> anyhow::Result<()> {
2793        serializable_retry!(self, || async {
2794            let mut tx = self.db.write().await?;
2795            #[cfg(not(feature = "embedded-db"))]
2796            query(
2797                "TRUNCATE stake_table_events, stake_table_events_l1_block, epoch_drb_and_root, \
2798                 stake_table_validators",
2799            )
2800            .execute(tx.as_mut())
2801            .await?;
2802            #[cfg(feature = "embedded-db")]
2803            {
2804                query("DELETE FROM stake_table_events")
2805                    .execute(tx.as_mut())
2806                    .await?;
2807                query("DELETE FROM stake_table_events_l1_block")
2808                    .execute(tx.as_mut())
2809                    .await?;
2810                query("DELETE FROM epoch_drb_and_root")
2811                    .execute(tx.as_mut())
2812                    .await?;
2813                query("DELETE FROM stake_table_validators")
2814                    .execute(tx.as_mut())
2815                    .await?;
2816            }
2817            tx.commit().await?;
2818            Ok(())
2819        })
2820        .await
2821    }
2822
2823    async fn store_all_validators(
2824        &self,
2825        epoch: EpochNumber,
2826        all_validators: RegisteredValidatorMap,
2827    ) -> anyhow::Result<()> {
2828        if all_validators.is_empty() {
2829            return Ok(());
2830        }
2831
2832        let epoch_i64 = epoch.u64() as i64;
2833        let serialized_validators = all_validators
2834            .into_iter()
2835            .map(|(address, validator)| {
2836                let validator_json =
2837                    serde_json::to_value(&validator).context("serializing validator to json")?;
2838                Ok((address.to_string(), validator_json))
2839            })
2840            .collect::<anyhow::Result<Vec<_>>>()?;
2841
2842        serializable_retry!(self, || async {
2843            let mut tx = self.db.write().await?;
2844
2845            let mut query_builder = QueryBuilder::new(
2846                "INSERT INTO stake_table_validators (epoch, address, validator) ",
2847            );
2848
2849            query_builder.push_values(
2850                serialized_validators.iter().cloned(),
2851                |mut b, (address, validator)| {
2852                    b.push_bind(epoch_i64)
2853                        .push_bind(address)
2854                        .push_bind(validator);
2855                },
2856            );
2857
2858            query_builder
2859                .push(" ON CONFLICT (epoch, address) DO UPDATE SET validator = EXCLUDED.validator");
2860
2861            let query = query_builder.build();
2862
2863            query.execute(tx.as_mut()).await?;
2864
2865            tx.commit().await?;
2866            Ok(())
2867        })
2868        .await
2869    }
2870
2871    async fn load_all_validators(
2872        &self,
2873        epoch: EpochNumber,
2874        offset: u64,
2875        limit: u64,
2876    ) -> anyhow::Result<Vec<RegisteredValidator<PubKey>>> {
2877        let mut tx = self.db.read().await?;
2878
2879        // Use LOWER(address) in ORDER BY to ensure consistent ordering for SQlite and Postgres.
2880        // Postgres sorts text case sensitively by default, while SQLite sorts case insensitively.
2881        // Applying LOWER() makes the result consistent.
2882        let rows = query(
2883            "SELECT address, validator
2884         FROM stake_table_validators
2885         WHERE epoch = $1
2886         ORDER BY LOWER(address) ASC
2887         LIMIT $2 OFFSET $3",
2888        )
2889        .bind(epoch.u64() as i64)
2890        .bind(limit as i64)
2891        .bind(offset as i64)
2892        .fetch_all(tx.as_mut())
2893        .await?;
2894        rows.into_iter()
2895            .map(|row| {
2896                let validator_json: serde_json::Value = row.try_get("validator")?;
2897                serde_json::from_value::<RegisteredValidator<PubKey>>(validator_json)
2898                    .map_err(Into::into)
2899            })
2900            .collect()
2901    }
2902}
2903
2904#[async_trait]
2905impl DhtPersistentStorage for Persistence {
2906    /// Save the DHT to the database
2907    ///
2908    /// # Errors
2909    /// - If we fail to serialize the records
2910    /// - If we fail to write the serialized records to the DB
2911    async fn save(&self, records: Vec<SerializableRecord>) -> anyhow::Result<()> {
2912        // Bincode-serialize the records
2913        let to_save =
2914            bincode::serialize(&records).with_context(|| "failed to serialize records")?;
2915
2916        // Prepare the statement
2917        let stmt = "INSERT INTO libp2p_dht (id, serialized_records) VALUES (0, $1) ON CONFLICT \
2918                    (id) DO UPDATE SET serialized_records = $1";
2919
2920        serializable_retry!(self, || async {
2921            // Execute the query
2922            let mut tx = self
2923                .db
2924                .write()
2925                .await
2926                .with_context(|| "failed to start an atomic DB transaction")?;
2927            tx.execute(query(stmt).bind(to_save.clone()))
2928                .await
2929                .with_context(|| "failed to execute DB query")?;
2930
2931            // Commit the state
2932            tx.commit().await.with_context(|| "failed to commit to DB")
2933        })
2934        .await
2935    }
2936
2937    /// Load the DHT from the database
2938    ///
2939    /// # Errors
2940    /// - If we fail to read from the DB
2941    /// - If we fail to deserialize the records
2942    async fn load(&self) -> anyhow::Result<Vec<SerializableRecord>> {
2943        // Fetch the results from the DB
2944        let result = self
2945            .db
2946            .read()
2947            .await
2948            .with_context(|| "failed to start a DB read transaction")?
2949            .fetch_one("SELECT * FROM libp2p_dht where id = 0")
2950            .await
2951            .with_context(|| "failed to fetch from DB")?;
2952
2953        // Get the `serialized_records` row
2954        let serialied_records: Vec<u8> = result.get("serialized_records");
2955
2956        // Deserialize it
2957        let records: Vec<SerializableRecord> = bincode::deserialize(&serialied_records)
2958            .with_context(|| "Failed to deserialize records")?;
2959
2960        Ok(records)
2961    }
2962}
2963
2964#[async_trait]
2965impl Provider<SeqTypes, VidCommonRequest> for Persistence {
2966    #[tracing::instrument(skip(self))]
2967    async fn fetch(&self, req: VidCommonRequest) -> Option<VidCommon> {
2968        let mut tx = match self.db.read().await {
2969            Ok(tx) => tx,
2970            Err(err) => {
2971                tracing::warn!("could not open transaction: {err:#}");
2972                return None;
2973            },
2974        };
2975
2976        let bytes = match query_as::<(Vec<u8>,)>(
2977            "SELECT data FROM vid_share2 WHERE payload_hash = $1 LIMIT 1",
2978        )
2979        .bind(req.0.to_string())
2980        .fetch_optional(tx.as_mut())
2981        .await
2982        {
2983            Ok(Some((bytes,))) => bytes,
2984            Ok(None) => return None,
2985            Err(err) => {
2986                tracing::error!("error loading VID share: {err:#}");
2987                return None;
2988            },
2989        };
2990
2991        let share: Proposal<SeqTypes, VidDisperseShare<SeqTypes>> =
2992            match bincode::deserialize(&bytes) {
2993                Ok(share) => share,
2994                Err(err) => {
2995                    tracing::warn!("error decoding VID share: {err:#}");
2996                    return None;
2997                },
2998            };
2999
3000        match share.data {
3001            VidDisperseShare::V0(vid) => Some(VidCommon::V0(vid.common)),
3002            VidDisperseShare::V1(vid) => Some(VidCommon::V1(vid.common)),
3003            VidDisperseShare::V2(vid) => Some(VidCommon::V2(vid.common)),
3004        }
3005    }
3006}
3007
3008#[async_trait]
3009impl Provider<SeqTypes, PayloadRequest> for Persistence {
3010    #[tracing::instrument(skip(self))]
3011    async fn fetch(&self, req: PayloadRequest) -> Option<Payload> {
3012        let mut tx = match self.db.read().await {
3013            Ok(tx) => tx,
3014            Err(err) => {
3015                tracing::warn!("could not open transaction: {err:#}");
3016                return None;
3017            },
3018        };
3019
3020        let bytes = match query_as::<(Vec<u8>,)>(
3021            "SELECT data FROM da_proposal2 WHERE payload_hash = $1 LIMIT 1",
3022        )
3023        .bind(req.0.to_string())
3024        .fetch_optional(tx.as_mut())
3025        .await
3026        {
3027            Ok(Some((bytes,))) => bytes,
3028            Ok(None) => return None,
3029            Err(err) => {
3030                tracing::warn!("error loading DA proposal: {err:#}");
3031                return None;
3032            },
3033        };
3034
3035        let proposal: Proposal<SeqTypes, DaProposal2<SeqTypes>> = match bincode::deserialize(&bytes)
3036        {
3037            Ok(proposal) => proposal,
3038            Err(err) => {
3039                tracing::error!("error decoding DA proposal: {err:#}");
3040                return None;
3041            },
3042        };
3043
3044        Some(Payload::from_bytes(
3045            &proposal.data.encoded_transactions,
3046            &proposal.data.metadata,
3047        ))
3048    }
3049}
3050
3051#[cfg(test)]
3052mod testing {
3053    use hotshot_query_service::data_source::storage::sql::testing::TmpDb;
3054
3055    use super::*;
3056    use crate::persistence::tests::TestablePersistence;
3057
3058    #[async_trait]
3059    impl TestablePersistence for Persistence {
3060        type Storage = Arc<TmpDb>;
3061
3062        async fn tmp_storage() -> Self::Storage {
3063            Arc::new(TmpDb::init().await)
3064        }
3065
3066        #[allow(refining_impl_trait)]
3067        fn options(db: &Self::Storage) -> Options {
3068            #[cfg(not(feature = "embedded-db"))]
3069            {
3070                PostgresOptions {
3071                    port: Some(db.port()),
3072                    host: Some(db.host()),
3073                    user: Some("postgres".into()),
3074                    password: Some("password".into()),
3075                    ..Default::default()
3076                }
3077                .into()
3078            }
3079
3080            #[cfg(feature = "embedded-db")]
3081            {
3082                SqliteOptions { path: db.path() }.into()
3083            }
3084        }
3085    }
3086}
3087
3088#[cfg(test)]
3089mod test {
3090    use espresso_types::{Leaf, NodeState, ValidatedState, traits::NullEventConsumer};
3091    use futures::stream::TryStreamExt;
3092    use hotshot_example_types::node_types::TEST_VERSIONS;
3093    use hotshot_types::{
3094        data::{
3095            EpochNumber, QuorumProposal2, ns_table::parse_ns_table,
3096            vid_disperse::AvidMDisperseShare,
3097        },
3098        message::convert_proposal,
3099        simple_certificate::QuorumCertificate,
3100        traits::{EncodeBytes, signature_key::SignatureKey},
3101        utils::EpochTransitionIndicator,
3102        vid::avidm::{AvidMScheme, init_avidm_param},
3103    };
3104
3105    use super::*;
3106    use crate::{BLSPubKey, PubKey, persistence::tests::TestablePersistence as _};
3107
3108    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3109    async fn test_quorum_proposals_leaf_hash_migration() {
3110        // Create some quorum proposals to test with.
3111        let leaf: Leaf2 = Leaf::genesis(
3112            &ValidatedState::default(),
3113            &NodeState::mock(),
3114            TEST_VERSIONS.test.base,
3115        )
3116        .await
3117        .into();
3118        let privkey = BLSPubKey::generated_from_seed_indexed([0; 32], 1).1;
3119        let signature = PubKey::sign(&privkey, &[]).unwrap();
3120        let mut quorum_proposal = Proposal {
3121            data: QuorumProposal2::<SeqTypes> {
3122                epoch: None,
3123                block_header: leaf.block_header().clone(),
3124                view_number: ViewNumber::genesis(),
3125                justify_qc: QuorumCertificate::genesis(
3126                    &ValidatedState::default(),
3127                    &NodeState::mock(),
3128                    TEST_VERSIONS.test,
3129                )
3130                .await
3131                .to_qc2(),
3132                upgrade_certificate: None,
3133                view_change_evidence: None,
3134                next_drb_result: None,
3135                next_epoch_justify_qc: None,
3136                state_cert: None,
3137            },
3138            signature,
3139            _pd: Default::default(),
3140        };
3141
3142        let qp1: Proposal<SeqTypes, QuorumProposal<SeqTypes>> =
3143            convert_proposal(quorum_proposal.clone());
3144
3145        quorum_proposal.data.view_number = ViewNumber::new(1);
3146
3147        let qp2: Proposal<SeqTypes, QuorumProposal<SeqTypes>> =
3148            convert_proposal(quorum_proposal.clone());
3149        let qps = [qp1, qp2];
3150
3151        // Create persistence and add the quorum proposals with NULL leaf hash.
3152        let db = Persistence::tmp_storage().await;
3153        let persistence = Persistence::connect(&db).await;
3154        let mut tx = persistence.db.write().await.unwrap();
3155        let params = qps
3156            .iter()
3157            .map(|qp| {
3158                (
3159                    qp.data.view_number.u64() as i64,
3160                    bincode::serialize(&qp).unwrap(),
3161                )
3162            })
3163            .collect::<Vec<_>>();
3164        tx.upsert("quorum_proposals", ["view", "data"], ["view"], params)
3165            .await
3166            .unwrap();
3167        tx.commit().await.unwrap();
3168
3169        // Create a new persistence and ensure the commitments get populated.
3170        let persistence = Persistence::connect(&db).await;
3171        let mut tx = persistence.db.read().await.unwrap();
3172        let rows = tx
3173            .fetch("SELECT * FROM quorum_proposals ORDER BY view ASC")
3174            .try_collect::<Vec<_>>()
3175            .await
3176            .unwrap();
3177        assert_eq!(rows.len(), qps.len());
3178        for (row, qp) in rows.into_iter().zip(qps) {
3179            assert_eq!(row.get::<i64, _>("view"), qp.data.view_number.u64() as i64);
3180            assert_eq!(
3181                row.get::<Vec<u8>, _>("data"),
3182                bincode::serialize(&qp).unwrap()
3183            );
3184            assert_eq!(
3185                row.get::<String, _>("leaf_hash"),
3186                Committable::commit(&Leaf::from_quorum_proposal(&qp.data)).to_string()
3187            );
3188        }
3189    }
3190
3191    fn pre_option_validator(seed: u8, stake: u64) -> super::super::RegisteredValidatorPreOption {
3192        use std::collections::HashMap;
3193
3194        use alloy::primitives::U256;
3195        use hotshot_types::light_client::StateVerKey;
3196
3197        super::super::RegisteredValidatorPreOption {
3198            account: Address::random(),
3199            stake_table_key: BLSPubKey::generated_from_seed_indexed([seed; 32], 0).0,
3200            state_ver_key: StateVerKey::default(),
3201            stake: U256::from(stake),
3202            commission: 0,
3203            delegators: HashMap::new(),
3204            authenticated: true,
3205            x25519_key: None,
3206            p2p_addr: None,
3207        }
3208    }
3209
3210    async fn insert_legacy_stake_row(
3211        persistence: &Persistence,
3212        epoch: i64,
3213        validator: super::super::RegisteredValidatorPreOption,
3214    ) {
3215        let mut map: IndexMap<Address, super::super::RegisteredValidatorPreOption> =
3216            IndexMap::new();
3217        map.insert(validator.account, validator);
3218        let stake_bytes = bincode::serialize(&map).unwrap();
3219        let mut tx = persistence.db.write().await.unwrap();
3220        tx.execute(
3221            query("INSERT INTO epoch_drb_and_root (epoch, stake) VALUES ($1, $2)")
3222                .bind(epoch)
3223                .bind(&stake_bytes),
3224        )
3225        .await
3226        .unwrap();
3227        tx.commit().await.unwrap();
3228    }
3229
3230    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3231    async fn test_load_stake_legacy_storage() {
3232        let tmp = Persistence::tmp_storage().await;
3233        let persistence = Persistence::connect(&tmp).await;
3234
3235        let v1 = pre_option_validator(1, 100);
3236        let v2 = pre_option_validator(2, 200);
3237        let v1_addr = v1.account;
3238        let v2_addr = v2.account;
3239        insert_legacy_stake_row(&persistence, 1, v1).await;
3240        insert_legacy_stake_row(&persistence, 2, v2).await;
3241
3242        let (loaded1, ..) = persistence
3243            .load_stake(EpochNumber::new(1))
3244            .await
3245            .unwrap()
3246            .unwrap();
3247        assert_eq!(loaded1.len(), 1);
3248        assert!(loaded1.get(&v1_addr).unwrap().stake_table_key.is_some());
3249
3250        let (loaded2, ..) = persistence
3251            .load_stake(EpochNumber::new(2))
3252            .await
3253            .unwrap()
3254            .unwrap();
3255        assert_eq!(loaded2.len(), 1);
3256        assert!(loaded2.get(&v2_addr).unwrap().stake_table_key.is_some());
3257
3258        let latest = persistence.load_latest_stake(10).await.unwrap().unwrap();
3259        assert_eq!(latest.len(), 2);
3260        let epochs: Vec<_> = latest.iter().map(|(e, ..)| *e).collect();
3261        assert!(epochs.contains(&EpochNumber::new(1)));
3262        assert!(epochs.contains(&EpochNumber::new(2)));
3263    }
3264
3265    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3266    async fn test_load_stake_mixed_storage() {
3267        let tmp = Persistence::tmp_storage().await;
3268        let persistence = Persistence::connect(&tmp).await;
3269
3270        let legacy_v = pre_option_validator(3, 300);
3271        let legacy_addr = legacy_v.account;
3272        insert_legacy_stake_row(&persistence, 5, legacy_v).await;
3273
3274        let current_v = espresso_types::v0_3::AuthenticatedValidator::mock();
3275        let current_addr = current_v.account;
3276        let mut current_map = IndexMap::new();
3277        current_map.insert(current_addr, current_v);
3278        persistence
3279            .store_stake(EpochNumber::new(6), current_map, None, None)
3280            .await
3281            .unwrap();
3282
3283        let latest = persistence.load_latest_stake(10).await.unwrap().unwrap();
3284        assert_eq!(latest.len(), 2);
3285        let by_epoch: std::collections::HashMap<_, _> = latest
3286            .into_iter()
3287            .map(|(e, (map, _), _)| (e, map))
3288            .collect();
3289        assert!(
3290            by_epoch
3291                .get(&EpochNumber::new(5))
3292                .unwrap()
3293                .contains_key(&legacy_addr)
3294        );
3295        assert!(
3296            by_epoch
3297                .get(&EpochNumber::new(6))
3298                .unwrap()
3299                .contains_key(&current_addr)
3300        );
3301    }
3302
3303    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3304    async fn test_store_all_validators_authenticated_and_unauthenticated() {
3305        use std::collections::HashMap;
3306
3307        use alloy::primitives::{Address, U256};
3308        use hotshot_types::light_client::StateVerKey;
3309        use indexmap::IndexMap;
3310
3311        let tmp = Persistence::tmp_storage().await;
3312        let storage = Persistence::connect(&tmp).await;
3313
3314        // Create an authenticated validator
3315        let authenticated_validator = RegisteredValidator {
3316            account: Address::random(),
3317            stake_table_key: Some(BLSPubKey::generated_from_seed_indexed([0u8; 32], 0).0),
3318            state_ver_key: Some(StateVerKey::default()),
3319            stake: U256::from(1000),
3320            commission: 100,
3321            delegators: HashMap::new(),
3322            authenticated: true,
3323            x25519_key: None,
3324            p2p_addr: None,
3325        };
3326
3327        // Create an unauthenticated validator
3328        let unauthenticated_validator = RegisteredValidator {
3329            account: Address::random(),
3330            stake_table_key: Some(BLSPubKey::generated_from_seed_indexed([0u8; 32], 1).0),
3331            state_ver_key: Some(StateVerKey::default()),
3332            stake: U256::from(2000),
3333            commission: 200,
3334            delegators: HashMap::new(),
3335            authenticated: false,
3336            x25519_key: None,
3337            p2p_addr: None,
3338        };
3339
3340        let mut validators: IndexMap<Address, RegisteredValidator<BLSPubKey>> = IndexMap::new();
3341        validators.insert(
3342            authenticated_validator.account,
3343            authenticated_validator.clone(),
3344        );
3345        validators.insert(
3346            unauthenticated_validator.account,
3347            unauthenticated_validator.clone(),
3348        );
3349
3350        // Store both validators
3351        storage
3352            .store_all_validators(EpochNumber::new(1), validators)
3353            .await
3354            .unwrap();
3355
3356        // Load and verify
3357        let loaded = storage
3358            .load_all_validators(EpochNumber::new(1), 0, 100)
3359            .await
3360            .unwrap();
3361        assert_eq!(loaded.len(), 2);
3362
3363        // Find each validator and verify authenticated state is preserved
3364        let loaded_auth = loaded
3365            .iter()
3366            .find(|v| v.account == authenticated_validator.account)
3367            .unwrap();
3368        assert!(
3369            loaded_auth.authenticated,
3370            "authenticated validator should remain authenticated"
3371        );
3372
3373        let loaded_unauth = loaded
3374            .iter()
3375            .find(|v| v.account == unauthenticated_validator.account)
3376            .unwrap();
3377        assert!(
3378            !loaded_unauth.authenticated,
3379            "unauthenticated validator should remain unauthenticated"
3380        );
3381    }
3382
3383    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3384    async fn test_fetching_providers() {
3385        let tmp = Persistence::tmp_storage().await;
3386        let storage = Persistence::connect(&tmp).await;
3387
3388        // Mock up some data.
3389        let leaf = Leaf2::genesis(
3390            &ValidatedState::default(),
3391            &NodeState::mock(),
3392            TEST_VERSIONS.test.base,
3393        )
3394        .await;
3395        let leaf_payload = leaf.block_payload().unwrap();
3396        let leaf_payload_bytes_arc = leaf_payload.encode();
3397
3398        let avidm_param = init_avidm_param(2).unwrap();
3399        let weights = vec![1u32; 2];
3400
3401        let ns_table = parse_ns_table(
3402            leaf_payload.byte_len().as_usize(),
3403            &leaf_payload.ns_table().encode(),
3404        );
3405        let (payload_commitment, shares) =
3406            AvidMScheme::ns_disperse(&avidm_param, &weights, &leaf_payload_bytes_arc, ns_table)
3407                .unwrap();
3408        let (pubkey, privkey) = BLSPubKey::generated_from_seed_indexed([0; 32], 1);
3409        let vid_share = convert_proposal(
3410            AvidMDisperseShare::<SeqTypes> {
3411                view_number: ViewNumber::new(0),
3412                payload_commitment,
3413                share: shares[0].clone(),
3414                recipient_key: pubkey,
3415                epoch: None,
3416                target_epoch: None,
3417                common: avidm_param.clone(),
3418            }
3419            .to_proposal(&privkey)
3420            .unwrap()
3421            .clone(),
3422        );
3423
3424        let quorum_proposal = QuorumProposalWrapper::<SeqTypes> {
3425            proposal: QuorumProposal2::<SeqTypes> {
3426                block_header: leaf.block_header().clone(),
3427                view_number: leaf.view_number(),
3428                justify_qc: leaf.justify_qc(),
3429                upgrade_certificate: None,
3430                view_change_evidence: None,
3431                next_drb_result: None,
3432                next_epoch_justify_qc: None,
3433                epoch: None,
3434                state_cert: None,
3435            },
3436        };
3437        let quorum_proposal_signature =
3438            BLSPubKey::sign(&privkey, &bincode::serialize(&quorum_proposal).unwrap())
3439                .expect("Failed to sign quorum proposal");
3440        let quorum_proposal = Proposal {
3441            data: quorum_proposal,
3442            signature: quorum_proposal_signature,
3443            _pd: Default::default(),
3444        };
3445
3446        let block_payload_signature = BLSPubKey::sign(&privkey, &leaf_payload_bytes_arc)
3447            .expect("Failed to sign block payload");
3448        let da_proposal = Proposal {
3449            data: DaProposal2::<SeqTypes> {
3450                encoded_transactions: leaf_payload_bytes_arc,
3451                metadata: leaf_payload.ns_table().clone(),
3452                view_number: ViewNumber::new(0),
3453                epoch: None,
3454                epoch_transition_indicator: EpochTransitionIndicator::NotInTransition,
3455            },
3456            signature: block_payload_signature,
3457            _pd: Default::default(),
3458        };
3459
3460        let mut next_quorum_proposal = quorum_proposal.clone();
3461        next_quorum_proposal.data.proposal.view_number += 1;
3462        next_quorum_proposal.data.proposal.justify_qc.view_number += 1;
3463        next_quorum_proposal
3464            .data
3465            .proposal
3466            .justify_qc
3467            .data
3468            .leaf_commit = Committable::commit(&leaf.clone());
3469
3470        // Add to database.
3471        storage
3472            .append_da2(&da_proposal, VidCommitment::V1(payload_commitment))
3473            .await
3474            .unwrap();
3475        storage.append_vid(&vid_share).await.unwrap();
3476        storage
3477            .append_quorum_proposal2(&quorum_proposal)
3478            .await
3479            .unwrap();
3480
3481        // Add an extra quorum proposal so we have a QC pointing back at `leaf`.
3482        storage
3483            .append_quorum_proposal2(&next_quorum_proposal)
3484            .await
3485            .unwrap();
3486
3487        // Fetch it as if we were rebuilding an archive.
3488        assert_eq!(
3489            Some(VidCommon::V1(avidm_param)),
3490            storage
3491                .fetch(VidCommonRequest(vid_share.data.payload_commitment()))
3492                .await
3493        );
3494        assert_eq!(
3495            leaf_payload,
3496            storage
3497                .fetch(PayloadRequest(vid_share.data.payload_commitment()))
3498                .await
3499                .unwrap()
3500        );
3501    }
3502
3503    /// Test conditions that trigger pruning.
3504    ///
3505    /// This is a configurable test that can be used to test different configurations of GC,
3506    /// `pruning_opt`. The test populates the database with some data for view 1, asserts that it is
3507    /// retained for view 2, and then asserts that it is pruned by view 3. There are various
3508    /// different configurations that can achieve this behavior, such that the data is retained and
3509    /// then pruned due to different logic and code paths.
3510    async fn test_pruning_helper(pruning_opt: ConsensusPruningOptions) {
3511        let tmp = Persistence::tmp_storage().await;
3512        let mut opt = Persistence::options(&tmp);
3513        opt.consensus_pruning = pruning_opt;
3514        let storage = opt.create().await.unwrap();
3515
3516        let data_view = ViewNumber::new(1);
3517
3518        // Populate some data.
3519        let leaf = Leaf2::genesis(
3520            &ValidatedState::default(),
3521            &NodeState::mock(),
3522            TEST_VERSIONS.test.base,
3523        )
3524        .await;
3525        let leaf_payload = leaf.block_payload().unwrap();
3526        let leaf_payload_bytes_arc = leaf_payload.encode();
3527
3528        let avidm_param = init_avidm_param(2).unwrap();
3529        let weights = vec![1u32; 2];
3530
3531        let ns_table = parse_ns_table(
3532            leaf_payload.byte_len().as_usize(),
3533            &leaf_payload.ns_table().encode(),
3534        );
3535        let (payload_commitment, shares) =
3536            AvidMScheme::ns_disperse(&avidm_param, &weights, &leaf_payload_bytes_arc, ns_table)
3537                .unwrap();
3538
3539        let (pubkey, privkey) = BLSPubKey::generated_from_seed_indexed([0; 32], 1);
3540        let vid = convert_proposal(
3541            AvidMDisperseShare::<SeqTypes> {
3542                view_number: data_view,
3543                payload_commitment,
3544                share: shares[0].clone(),
3545                recipient_key: pubkey,
3546                epoch: None,
3547                target_epoch: None,
3548                common: avidm_param,
3549            }
3550            .to_proposal(&privkey)
3551            .unwrap()
3552            .clone(),
3553        );
3554        let quorum_proposal = QuorumProposalWrapper::<SeqTypes> {
3555            proposal: QuorumProposal2::<SeqTypes> {
3556                epoch: None,
3557                block_header: leaf.block_header().clone(),
3558                view_number: data_view,
3559                justify_qc: QuorumCertificate2::genesis(
3560                    &ValidatedState::default(),
3561                    &NodeState::mock(),
3562                    TEST_VERSIONS.test,
3563                )
3564                .await,
3565                upgrade_certificate: None,
3566                view_change_evidence: None,
3567                next_drb_result: None,
3568                next_epoch_justify_qc: None,
3569                state_cert: None,
3570            },
3571        };
3572        let quorum_proposal_signature =
3573            BLSPubKey::sign(&privkey, &bincode::serialize(&quorum_proposal).unwrap())
3574                .expect("Failed to sign quorum proposal");
3575        let quorum_proposal = Proposal {
3576            data: quorum_proposal,
3577            signature: quorum_proposal_signature,
3578            _pd: Default::default(),
3579        };
3580
3581        let block_payload_signature = BLSPubKey::sign(&privkey, &leaf_payload_bytes_arc)
3582            .expect("Failed to sign block payload");
3583        let da_proposal = Proposal {
3584            data: DaProposal2::<SeqTypes> {
3585                encoded_transactions: leaf_payload_bytes_arc.clone(),
3586                metadata: leaf_payload.ns_table().clone(),
3587                view_number: data_view,
3588                epoch: Some(EpochNumber::new(0)),
3589                epoch_transition_indicator: EpochTransitionIndicator::NotInTransition,
3590            },
3591            signature: block_payload_signature,
3592            _pd: Default::default(),
3593        };
3594
3595        tracing::info!(?vid, ?da_proposal, ?quorum_proposal, "append data");
3596        storage.append_vid(&vid).await.unwrap();
3597        storage
3598            .append_da2(&da_proposal, VidCommitment::V1(payload_commitment))
3599            .await
3600            .unwrap();
3601        storage
3602            .append_quorum_proposal2(&quorum_proposal)
3603            .await
3604            .unwrap();
3605
3606        // Populate the view-indexed cert tables. Contents are opaque to the pruner (it deletes by
3607        // `view`), so raw bytes suffice.
3608        {
3609            let mut tx = storage.db.write().await.unwrap();
3610            tx.upsert(
3611                "state_cert",
3612                ["view", "state_cert"],
3613                ["view"],
3614                [(data_view.u64() as i64, b"state_cert".to_vec())],
3615            )
3616            .await
3617            .unwrap();
3618            tx.upsert(
3619                "decided_cert2",
3620                ["view", "data"],
3621                ["view"],
3622                [(data_view.u64() as i64, b"cert2".to_vec())],
3623            )
3624            .await
3625            .unwrap();
3626            tx.commit().await.unwrap();
3627        }
3628
3629        // The first decide doesn't trigger any garbage collection, even though our usage exceeds
3630        // the target, because of the minimum retention.
3631        tracing::info!("decide view 1");
3632        storage
3633            .append_decided_leaves(data_view + 1, [], None, &NullEventConsumer)
3634            .await
3635            .unwrap();
3636        assert_eq!(
3637            storage.load_vid_share(data_view).await.unwrap().unwrap(),
3638            vid
3639        );
3640        assert_eq!(
3641            storage.load_da_proposal(data_view).await.unwrap().unwrap(),
3642            da_proposal
3643        );
3644        assert_eq!(
3645            storage.load_quorum_proposal(data_view).await.unwrap(),
3646            quorum_proposal
3647        );
3648        assert!(view_row_exists(&storage, "state_cert", data_view).await);
3649        assert!(view_row_exists(&storage, "decided_cert2", data_view).await);
3650
3651        // After another view, our data is beyond the minimum retention (though not the target
3652        // retention) so it gets pruned.
3653        tracing::info!("decide view 2");
3654        storage
3655            .append_decided_leaves(data_view + 2, [], None, &NullEventConsumer)
3656            .await
3657            .unwrap();
3658        assert!(storage.load_vid_share(data_view).await.unwrap().is_none(),);
3659        assert!(storage.load_da_proposal(data_view).await.unwrap().is_none());
3660        storage.load_quorum_proposal(data_view).await.unwrap_err();
3661        assert!(!view_row_exists(&storage, "state_cert", data_view).await);
3662        assert!(!view_row_exists(&storage, "decided_cert2", data_view).await);
3663    }
3664
3665    /// Whether a view-indexed consensus table has a row at `view`.
3666    async fn view_row_exists(storage: &Persistence, table: &str, view: ViewNumber) -> bool {
3667        storage
3668            .db
3669            .read()
3670            .await
3671            .unwrap()
3672            .fetch_optional(
3673                query(&format!("SELECT view FROM {table} WHERE view = $1")).bind(view.u64() as i64),
3674            )
3675            .await
3676            .unwrap()
3677            .is_some()
3678    }
3679
3680    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3681    async fn test_pruning_minimum_retention() {
3682        test_pruning_helper(ConsensusPruningOptions {
3683            // Use a very low target usage, to show that we still retain data up to the minimum
3684            // retention even when usage is above target.
3685            target_usage: 0,
3686            minimum_retention: 1,
3687            // Use a very high target retention, so that pruning is only triggered by the minimum
3688            // retention.
3689            target_retention: u64::MAX,
3690        })
3691        .await
3692    }
3693
3694    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3695    async fn test_pruning_target_retention() {
3696        test_pruning_helper(ConsensusPruningOptions {
3697            target_retention: 1,
3698            // Use a very low minimum retention, so that data is only kept around due to the target
3699            // retention.
3700            minimum_retention: 0,
3701            // Use a very high target usage, so that pruning is only triggered by the target
3702            // retention.
3703            target_usage: u64::MAX,
3704        })
3705        .await
3706    }
3707
3708    /// Regression test for an ambiguous behavior in `store_events`/`load_events`.
3709    ///
3710    /// Previously, `store_events` did nothing when given an empty events list (in fact,
3711    /// `fetch_and_store_stake_table_events` was not even calling it). But this means that the
3712    /// `stake_table_events_l1_block` column does not get updated when we enter a new epoch with no
3713    /// new stake table events. This makes it impossible to distinguish between two very different
3714    /// scenarios:
3715    ///
3716    /// 1. The node has successfully processed events through the latest L1 finalized block, but
3717    ///    there are no new events from the last epoch.
3718    /// 2. The node is lagging behind the latest L1 finalized block, and is possibly missing some
3719    ///    new events.
3720    ///
3721    /// In scenario 1, clients of this node should be able to treat the empty list of stake table
3722    /// events as authoritative, and derive the stake table for the next epoch (which will end up
3723    /// being the same as the previous one. However, in scenario 2, clients need to wait, because we
3724    /// don't yet know whether there could be any events that modify the stake table. Thus,
3725    /// distinguishing these two scenarios is important.
3726    ///
3727    /// This regression test ensures that even if there are no new events, at least the
3728    /// `stake_table_events_l1_block` column gets updated. We can then distinguish the two scenarios
3729    /// using the `EventsPersistenceRead`` return value from load_events.
3730    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3731    async fn test_store_events_empty() {
3732        let tmp = Persistence::tmp_storage().await;
3733        let mut opt = Persistence::options(&tmp);
3734        let storage = opt.create().await.unwrap();
3735
3736        assert_eq!(storage.load_events(0, 100).await.unwrap(), (None, vec![]));
3737
3738        // Storing an empty events list still updates the latest L1 block.
3739        for i in 1..=2 {
3740            tracing::info!(i, "update l1 height");
3741            storage.store_events(i, vec![]).await.unwrap();
3742            assert_eq!(
3743                storage.load_events(0, 100).await.unwrap(),
3744                (Some(EventsPersistenceRead::UntilL1Block(i)), vec![])
3745            );
3746        }
3747    }
3748}
3749
3750#[cfg(test)]
3751#[cfg(not(feature = "embedded-db"))]
3752mod postgres_tests {
3753    use espresso_types::{FeeAccount, Header, Leaf, NodeState, Transaction as Tx};
3754    use hotshot_example_types::node_types::TEST_VERSIONS;
3755    use hotshot_query_service::{
3756        availability::{BlockQueryData, LeafQueryData},
3757        data_source::storage::UpdateAvailabilityStorage,
3758    };
3759    use hotshot_types::{
3760        data::vid_commitment,
3761        simple_certificate::QuorumCertificate,
3762        traits::{
3763            EncodeBytes,
3764            block_contents::{BlockHeader, BuilderFee, GENESIS_VID_NUM_STORAGE_NODES},
3765            election::Membership,
3766            signature_key::BuilderSignatureKey,
3767        },
3768    };
3769
3770    use super::*;
3771    use crate::persistence::tests::TestablePersistence as _;
3772
3773    async fn test_postgres_read_ns_table(instance_state: NodeState) {
3774        instance_state
3775            .coordinator
3776            .membership()
3777            .set_first_epoch(EpochNumber::genesis(), Default::default());
3778
3779        let tmp = Persistence::tmp_storage().await;
3780        let mut opt = Persistence::options(&tmp);
3781        let storage = opt.create().await.unwrap();
3782
3783        let txs = [
3784            Tx::new(10001u32.into(), vec![1, 2, 3]),
3785            Tx::new(10001u32.into(), vec![4, 5, 6]),
3786            Tx::new(10009u32.into(), vec![7, 8, 9]),
3787        ];
3788
3789        let validated_state = Default::default();
3790        let justify_qc =
3791            QuorumCertificate::genesis(&validated_state, &instance_state, TEST_VERSIONS.test).await;
3792        let view_number: ViewNumber = justify_qc.view_number + 1;
3793        let parent_leaf = Leaf::genesis(&validated_state, &instance_state, TEST_VERSIONS.test.base)
3794            .await
3795            .into();
3796
3797        let (payload, ns_table) =
3798            Payload::from_transactions(txs.clone(), &validated_state, &instance_state)
3799                .await
3800                .unwrap();
3801        let payload_bytes = payload.encode();
3802        let payload_commitment = vid_commitment(
3803            &payload_bytes,
3804            &ns_table.encode(),
3805            GENESIS_VID_NUM_STORAGE_NODES,
3806            instance_state.current_version,
3807        );
3808        let builder_commitment = payload.builder_commitment(&ns_table);
3809        let (fee_account, fee_key) = FeeAccount::generated_from_seed_indexed([0; 32], 0);
3810        let fee_amount = 0;
3811        let fee_signature = FeeAccount::sign_fee(&fee_key, fee_amount, &ns_table).unwrap();
3812        let block_header = Header::new(
3813            &validated_state,
3814            &instance_state,
3815            &parent_leaf,
3816            payload_commitment,
3817            builder_commitment,
3818            ns_table,
3819            BuilderFee {
3820                fee_amount,
3821                fee_account,
3822                fee_signature,
3823            },
3824            instance_state.current_version,
3825            view_number.u64(),
3826        )
3827        .await
3828        .unwrap();
3829        let proposal = QuorumProposal {
3830            block_header: block_header.clone(),
3831            view_number,
3832            justify_qc: justify_qc.clone(),
3833            upgrade_certificate: None,
3834            proposal_certificate: None,
3835        };
3836        let leaf: Leaf2 = Leaf::from_quorum_proposal(&proposal).into();
3837        let mut qc = justify_qc.to_qc2();
3838        qc.data.leaf_commit = leaf.commit();
3839        qc.view_number = view_number;
3840
3841        let mut tx = storage.db.write().await.unwrap();
3842        tx.insert_leaf(&LeafQueryData::new(leaf, qc).unwrap())
3843            .await
3844            .unwrap();
3845        tx.insert_block(&BlockQueryData::<SeqTypes>::new(block_header, payload))
3846            .await
3847            .unwrap();
3848        tx.commit().await.unwrap();
3849
3850        let mut tx = storage.db.read().await.unwrap();
3851        let rows = query(
3852            "
3853            SELECT ns_id, read_ns_id(get_ns_table(h.data), t.ns_index) AS read_ns_id
3854              FROM header AS h
3855              JOIN transactions AS t ON t.block_height = h.height
3856              ORDER BY t.ns_index, t.position
3857        ",
3858        )
3859        .fetch_all(tx.as_mut())
3860        .await
3861        .unwrap();
3862        assert_eq!(rows.len(), txs.len());
3863        for (i, row) in rows.into_iter().enumerate() {
3864            let ns = u64::from(txs[i].namespace()) as i64;
3865            assert_eq!(row.get::<i64, _>("ns_id"), ns);
3866            assert_eq!(row.get::<i64, _>("read_ns_id"), ns);
3867        }
3868    }
3869
3870    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3871    async fn test_postgres_read_ns_table_v0_1() {
3872        test_postgres_read_ns_table(NodeState::mock()).await;
3873    }
3874
3875    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3876    async fn test_postgres_read_ns_table_v0_2() {
3877        test_postgres_read_ns_table(NodeState::mock_v2()).await;
3878    }
3879
3880    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3881    async fn test_postgres_read_ns_table_v0_3() {
3882        test_postgres_read_ns_table(NodeState::mock_v3().with_epoch_height(0)).await;
3883    }
3884
3885    /// Verify that concurrent calls to `record_action` all succeed under
3886    /// PostgreSQL SERIALIZABLE isolation. `self.serializable_backoff.retry_if` handles any
3887    /// 40001 serialization failures that arise when many tasks race to update
3888    /// the same row.
3889    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3890    async fn test_record_action_concurrent() {
3891        let tmp = Persistence::tmp_storage().await;
3892        let storage = Arc::new(Persistence::connect(&tmp).await);
3893
3894        let handles: Vec<_> = (0u64..20)
3895            .map(|i| {
3896                let storage = Arc::clone(&storage);
3897                tokio::spawn(async move {
3898                    storage
3899                        .record_action(ViewNumber::new(i), None, HotShotAction::Vote)
3900                        .await
3901                })
3902            })
3903            .collect();
3904
3905        for handle in handles {
3906            handle.await.unwrap().unwrap();
3907        }
3908
3909        let latest = storage.load_latest_acted_view().await.unwrap();
3910        assert_eq!(latest, Some(ViewNumber::new(19)));
3911    }
3912}