Skip to main content

espresso_node/persistence/
sql.rs

1use std::{collections::BTreeMap, future::Future, sync::Arc, time::Instant};
2
3use alloy::primitives::Address;
4use anyhow::{Context, bail};
5use async_trait::async_trait;
6use committable::Committable;
7use derive_more::{Deref, DerefMut};
8use espresso_types::{
9    AuthenticatedValidatorMap, BackoffParams, Header, Leaf, Leaf2, NetworkConfig, Payload, PubKey,
10    RegisteredValidatorMap, StakeTableHash,
11    traits::{EventsPersistenceRead, MembershipPersistence, StakeTuple},
12    v0::traits::{
13        EventConsumer, PersistenceOptions, SequencerPersistence, StateCatchup, StateCertPersistence,
14    },
15    v0_3::{
16        AuthenticatedValidator, EventKey, IndexedStake, RegisteredValidator, RewardAmount,
17        StakeTableEvent,
18    },
19};
20use futures::stream::StreamExt;
21use hotshot_new_protocol::message::Certificate2;
22use hotshot_query_service::{
23    data_source::{
24        Transaction as _, VersionedDataSource,
25        storage::{SerializableRetry, pruning::PrunerCfg, sql::StorageConnectionType},
26    },
27    fetching::{
28        Provider,
29        request::{PayloadRequest, VidCommonRequest},
30    },
31    serializable_retry,
32};
33use hotshot_types::{
34    data::{
35        DaProposal, DaProposal2, EpochNumber, QuorumProposal, QuorumProposalWrapper,
36        QuorumProposalWrapperLegacy, VidCommitment, VidCommon, VidDisperseShare,
37    },
38    drb::{DrbInput, DrbResult},
39    event::{HotShotAction, LeafInfo},
40    initializer::InitializerEpochInfo,
41    message::{Proposal, convert_proposal},
42    new_protocol::CoordinatorEvent,
43    simple_certificate::{
44        CertificatePair, LightClientStateUpdateCertificateV1, LightClientStateUpdateCertificateV2,
45        NextEpochQuorumCertificate2, QuorumCertificate2, UpgradeCertificate,
46    },
47    simple_vote::LockView,
48    traits::{
49        block_contents::{BlockHeader, BlockPayload},
50        metrics::Metrics,
51    },
52    vote::HasViewNumber,
53};
54use indexmap::IndexMap;
55use itertools::Itertools;
56use light_client::storage::LightClientDbOptions;
57use sqlx::{Executor, QueryBuilder, Row, query};
58
59use super::{
60    super::{ConsensusPruningOptions, Options as SqlOptions, within_gap_fill_horizon},
61    qs::{
62        Backend as _, Config, Db, DbKind, Read, SerializableRetryConfig, SqlStorage, Transaction,
63        Write, query_as,
64    },
65};
66use crate::{
67    NodeType, RECENT_STAKE_TABLES_LIMIT, SeqTypes, ViewNumber,
68    api::options::Query,
69    catchup::SqlStateCatchup,
70    persistence::{
71        migrate_network_config, persistence_metrics::PersistenceMetricsValue,
72        storage_probe::StorageProbe,
73    },
74};
75
76/// Node setup that differs between the SQL backends.
77pub(crate) trait BackendOptions {
78    fn apply(cfg: Config, opt: &SqlOptions) -> anyhow::Result<Config>;
79
80    fn light_client_db(cfg: &Config, query: &Query) -> LightClientDbOptions;
81
82    async fn reset_light_client(cfg: &Config) -> anyhow::Result<()>;
83
84    /// A probe of the database file and its settings, for a backend that keeps the database in one.
85    async fn storage_probe(
86        db: &SqlStorage,
87        opt: &SqlOptions,
88    ) -> anyhow::Result<Option<StorageProbe>>;
89}
90
91impl TryFrom<&SqlOptions> for Config {
92    type Error = anyhow::Error;
93
94    fn try_from(opt: &SqlOptions) -> Result<Self, Self::Error> {
95        let mut cfg = match &opt.uri {
96            Some(uri) => uri.parse()?,
97            None => Self::default(),
98        };
99
100        if let Some(pool) = opt.pool.clone() {
101            let pool = sqlx::Pool::<Db>::try_from(pool)
102                .ok()
103                .context("the connection pool is for the other SQL backend")?;
104            cfg = cfg.pool(pool);
105        }
106
107        cfg = cfg.max_connections(opt.max_connections);
108        cfg = cfg.idle_connection_timeout(opt.idle_connection_timeout);
109        cfg = cfg.min_connections(opt.min_connections);
110        cfg = cfg.connection_timeout(opt.connection_timeout);
111        cfg = cfg.slow_statement_threshold(opt.slow_statement_threshold);
112        cfg = cfg.statement_timeout(opt.statement_timeout);
113        cfg = Db::apply(cfg, opt)?;
114
115        if opt.prune {
116            cfg = cfg.pruner_cfg(PrunerCfg::from(opt.pruning))?;
117        }
118        if opt.archive {
119            cfg = cfg.archive();
120        }
121
122        let retry = &opt.serializable_retry;
123        cfg = cfg.serializable_retry(SerializableRetryConfig::new(
124            retry.backoff_base,
125            retry.backoff_max,
126            retry.backoff_factor,
127            retry.backoff_jitter.into(),
128            retry.retry_max,
129            retry.pg_stat_diag,
130        ));
131
132        Ok(cfg)
133    }
134}
135
136/// [`SqlOptions`] for persistence on this backend, built by
137/// [`SqlOptions::into_backend`](super::super::Options::into_backend) or checked with `try_from`.
138#[derive(Clone, Debug, Deref, DerefMut)]
139pub struct Options(pub(in crate::persistence::sql) SqlOptions);
140
141impl Options {
142    pub fn into_inner(self) -> SqlOptions {
143        self.0
144    }
145}
146
147impl TryFrom<SqlOptions> for Options {
148    type Error = anyhow::Error;
149
150    fn try_from(opt: SqlOptions) -> anyhow::Result<Options> {
151        anyhow::ensure!(
152            opt.embedded_db == (Db::KIND == DbKind::Sqlite),
153            "the SQL options select the other backend"
154        );
155        Ok(Options(opt))
156    }
157}
158
159#[async_trait]
160impl PersistenceOptions for Options {
161    type Persistence = Persistence;
162
163    fn set_view_retention(&mut self, view_retention: u64) {
164        self.0.consensus_pruning.target_retention = view_retention;
165        self.0.consensus_pruning.minimum_retention = view_retention;
166    }
167
168    fn set_consensus_only(&mut self) {
169        self.0.consensus_only = true;
170    }
171
172    async fn create(&mut self) -> anyhow::Result<Self::Persistence> {
173        let config = Config::try_from(&self.0)?;
174        let db = SqlStorage::connect(config, StorageConnectionType::Sequencer).await?;
175
176        let probe = Db::storage_probe(&db, &self.0).await?;
177
178        let persistence = Persistence {
179            db,
180            gc_opt: self.0.consensus_pruning,
181            last_usage_check: Arc::default(),
182            consensus_only: self.0.consensus_only,
183            internal_metrics: PersistenceMetricsValue::default(),
184            probe,
185        };
186        persistence.migrate_quorum_proposal_leaf_hashes().await?;
187        self.0.pool = Some(persistence.db.pool().into());
188
189        Ok(persistence)
190    }
191
192    async fn reset(self) -> anyhow::Result<()> {
193        let config = Config::try_from(&self.0)?;
194        Db::reset_light_client(&config).await?;
195        SqlStorage::connect(config.reset_schema(), StorageConnectionType::Sequencer).await?;
196        Ok(())
197    }
198}
199
200/// SQL-backed persistence.
201#[derive(Clone, Debug)]
202pub struct Persistence {
203    pub(super) db: SqlStorage,
204    gc_opt: ConsensusPruningOptions,
205    /// When consensus storage usage was last measured, shared by clones.
206    last_usage_check: Arc<parking_lot::Mutex<Option<Instant>>>,
207    consensus_only: bool,
208    /// A reference to the internal metrics
209    internal_metrics: PersistenceMetricsValue,
210    /// Startup findings about the filesystem and SQLite pragmas backing `db`, on SQLite only.
211    pub(super) probe: Option<StorageProbe>,
212}
213
214/// PostgreSQL error code for serialization failures under SERIALIZABLE isolation.
215/// Transactions that fail with this code are safe to retry from scratch.
216const PG_SERIALIZATION_FAILURE_CODE: &str = "40001";
217
218#[derive(Debug)]
219struct DecidedLeaf {
220    info: LeafInfo<SeqTypes>,
221    cert: CertificatePair<SeqTypes>,
222}
223
224/// The decide for `chain`, newest leaf first; `cert2`, if any, finalizes the newest leaf only.
225fn decide_event_from_chain(
226    chain: Vec<DecidedLeaf>,
227    cert2: Option<Certificate2<SeqTypes>>,
228) -> Option<CoordinatorEvent<SeqTypes>> {
229    let Some(newest) = chain.first() else {
230        if cert2.is_some() {
231            tracing::warn!(
232                "decide_event_from_chain called with cert2 but no leaves; cert2 will be dropped"
233            );
234        }
235        return None;
236    };
237    let cert1 = newest.cert.qc().clone();
238    let leaf_infos = chain.into_iter().map(|leaf| leaf.info).collect();
239    Some(CoordinatorEvent::NewDecide {
240        leaf_infos,
241        cert1,
242        cert2,
243    })
244}
245
246impl Persistence {
247    /// Run `f` under the database's serialization-conflict retry policy.
248    async fn serializable_retry<F, Fut, T>(&self, op: &'static str, f: F) -> anyhow::Result<T>
249    where
250        T: Send,
251        F: Fn() -> Fut + Send + Sync,
252        Fut: Future<Output = anyhow::Result<T>> + Send,
253    {
254        self.db.serializable_retry(op, f).await
255    }
256
257    /// Ensure the `leaf_hash` column is populated for all existing quorum proposals.
258    ///
259    /// This column was added in a migration, but because it requires computing a commitment of the
260    /// existing data, it is not easy to populate in the SQL migration itself. Thus, on startup, we
261    /// check if there are any just-migrated quorum proposals with a `NULL` value for this column,
262    /// and if so we populate the column manually.
263    async fn migrate_quorum_proposal_leaf_hashes(&self) -> anyhow::Result<()> {
264        serializable_retry!(self, || async {
265            let mut tx = self.db.write().await?;
266
267            let mut proposals = tx.fetch("SELECT * FROM quorum_proposals");
268
269            let mut updates = vec![];
270            while let Some(row) = proposals.next().await {
271                let row = row?;
272
273                let hash: Option<String> = row.try_get("leaf_hash")?;
274                if hash.is_none() {
275                    let view: i64 = row.try_get("view")?;
276                    let data: Vec<u8> = row.try_get("data")?;
277                    let proposal: Proposal<SeqTypes, QuorumProposal<SeqTypes>> =
278                        bincode::deserialize(&data)?;
279                    let leaf = Leaf::from_quorum_proposal(&proposal.data);
280                    let leaf_hash = Committable::commit(&leaf);
281                    tracing::info!(view, %leaf_hash, "populating quorum proposal leaf hash");
282                    updates.push((view, leaf_hash.to_string()));
283                }
284            }
285            drop(proposals);
286
287            tx.upsert("quorum_proposals", ["view", "leaf_hash"], ["view"], updates)
288                .await?;
289
290            tx.commit().await
291        })
292        .await
293    }
294
295    /// The `last_processed_view` cursor: highest view with a generated decide event, or `None`.
296    async fn load_processed_view(&self) -> anyhow::Result<Option<ViewNumber>> {
297        Ok(self
298            .db
299            .read()
300            .await?
301            .fetch_optional("SELECT last_processed_view FROM event_stream WHERE id = 1 LIMIT 1")
302            .await?
303            .map(|row| ViewNumber::new(row.get::<i64, _>("last_processed_view") as u64)))
304    }
305
306    async fn generate_decide_events(&self, consumer: &impl EventConsumer) -> anyhow::Result<()> {
307        let mut last_processed_view: Option<i64> = serializable_retry!(self, || async {
308            Ok(self
309                .db
310                .read()
311                .await?
312                .fetch_optional("SELECT last_processed_view FROM event_stream WHERE id = 1 LIMIT 1")
313                .await?
314                .map(|row| row.get("last_processed_view")))
315        })
316        .await?;
317        // Seed the height-continuity check below from the leaf at the cursor; if retention
318        // pruning has since removed it, fall back to accepting the first row as before.
319        let mut last_processed_height: Option<u64> = match last_processed_view {
320            Some(view) => {
321                serializable_retry!(self, || async {
322                    self.db
323                        .read()
324                        .await?
325                        .fetch_optional(
326                            query("SELECT leaf FROM anchor_leaf2 WHERE view = $1").bind(view),
327                        )
328                        .await?
329                        .map(|row| -> anyhow::Result<u64> {
330                            let leaf_data: Vec<u8> = row.get("leaf");
331                            let leaf = bincode::deserialize::<Leaf2>(&leaf_data)?;
332                            Ok(leaf.block_header().block_number())
333                        })
334                        .transpose()
335                })
336                .await?
337            },
338            None => None,
339        };
340        loop {
341            // In SQLite, overlapping read and write transactions can lead to database errors. To
342            // avoid this:
343            // - start a read transaction to query and collect all the necessary data.
344            // - Commit (or implicitly drop) the read transaction once the data is fetched.
345            // - use the collected data to generate a "decide" event for the consumer.
346            // - begin a write transaction to delete the data and update the event stream.
347
348            // Retry the entire read section on serialization failures (40001) via
349            // `serializable_retry!`. The closure returns `None` when there is no
350            // more work to do, which we propagate out of the outer function.
351            let Some((
352                from_view,
353                to_view,
354                leaves,
355                final_qc,
356                mut vid_shares,
357                mut da_proposals,
358                state_certs,
359                cert2,
360            )) = serializable_retry!(self, || async {
361                let mut tx = self.db.read().await?;
362
363                // Collect a chain of consecutive leaves, starting from the first view after the
364                // last decide. This will correspond to a decide event, and defines a range of
365                // views which can be garbage collected. This may even include views for which
366                // there was no leaf, for which we might still have artifacts like proposals that
367                // never finalized.
368                let from_view = match last_processed_view {
369                    Some(v) => v + 1,
370                    None => 0,
371                };
372                tracing::debug!(?from_view, "generate decide event");
373
374                // The newest persisted decide; bounds how far back a gap-fill can arrive.
375                let (watermark,): (Option<i64>,) = query_as("SELECT max(view) FROM anchor_leaf2")
376                    .fetch_one(tx.as_mut())
377                    .await?;
378
379                let mut parent = last_processed_height;
380                let mut rows = query(
381                    "SELECT leaf, qc, next_epoch_qc FROM anchor_leaf2 WHERE view >= $1 ORDER BY \
382                     view",
383                )
384                .bind(from_view)
385                .fetch(tx.as_mut());
386                let mut leaves: Vec<(Leaf2, CertificatePair<SeqTypes>)> = vec![];
387                let mut final_qc = None;
388                while let Some(row) = rows.next().await {
389                    let row = match row {
390                        Ok(row) => row,
391                        Err(err) => {
392                            if err.as_database_error().is_some_and(|e| {
393                                e.code().as_deref() == Some(PG_SERIALIZATION_FAILURE_CODE)
394                            }) {
395                                drop(rows);
396                                return Err(anyhow::Error::from(err));
397                            }
398                            // If there's an error getting a row, try generating an event with
399                            // the rows we do have.
400                            tracing::warn!("error loading row: {err:#}");
401                            break;
402                        },
403                    };
404
405                    let leaf_data: Vec<u8> = row.get("leaf");
406                    let leaf = bincode::deserialize::<Leaf2>(&leaf_data)?;
407                    let qc_data: Vec<u8> = row.get("qc");
408                    let qc = bincode::deserialize::<QuorumCertificate2<SeqTypes>>(&qc_data)?;
409                    let next_epoch_qc = match row.get::<Option<Vec<u8>>, _>("next_epoch_qc") {
410                        Some(bytes) => Some(bincode::deserialize::<
411                            NextEpochQuorumCertificate2<SeqTypes>,
412                        >(&bytes)?),
413                        None => None,
414                    };
415                    let height = leaf.block_header().block_number();
416
417                    // Ensure we are only dealing with a consecutive chain of leaves. We don't want to
418                    // garbage collect any views for which we missed a leaf or decide event; at least
419                    // not right away, in case we need to recover that data later.
420                    if let Some(parent) = parent
421                        && height != parent + 1
422                    {
423                        if !leaves.is_empty() {
424                            tracing::debug!(
425                                height,
426                                parent,
427                                "ending decide event at non-consecutive leaf"
428                            );
429                            break;
430                        }
431                        // A height jump within `DECIDE_GAP_FILL_HORIZON` of the watermark
432                        // can still be gap-filled: hold the cursor (emit nothing) until that
433                        // decide upserts the missing row. Legacy or beyond-horizon gaps are
434                        // permanent; skip as before, leaving the missing block to the
435                        // consumer's own fetching.
436                        let row_view = leaf.view_number().u64();
437                        if height > parent + 1
438                            && leaf.block_header().version() >= versions::NEW_PROTOCOL_VERSION
439                            && watermark
440                                .is_some_and(|w| within_gap_fill_horizon(row_view, w as u64))
441                        {
442                            tracing::info!(
443                                height,
444                                parent,
445                                row_view,
446                                ?watermark,
447                                "waiting for gap-fill decide before advancing the decide cursor"
448                            );
449                            break;
450                        }
451                        // A first row at or below the cursor (a replayed decide) is accepted.
452                    }
453                    parent = Some(height);
454                    let cert = CertificatePair::new(qc, next_epoch_qc);
455                    final_qc = Some(cert.clone());
456                    leaves.push((leaf, cert));
457                }
458                drop(rows);
459
460                let Some(final_qc) = final_qc else {
461                    // End event processing when there are no more decided views.
462                    tracing::debug!(from_view, "no new leaves at decide");
463                    return Ok(None);
464                };
465
466                // Find the range of views encompassed by this leaf chain. All data in this range can be
467                // processed by the consumer and then deleted.
468                let from_view = leaves[0].0.view_number();
469                let to_view = leaves[leaves.len() - 1].0.view_number();
470
471                // Collect VID shares for the decide event.
472                let vid_rows = tx
473                    .fetch_all(
474                        query("SELECT view, data FROM vid_share2 where view >= $1 AND view <= $2")
475                            .bind(from_view.u64() as i64)
476                            .bind(to_view.u64() as i64),
477                    )
478                    .await?;
479                let vid_shares = vid_rows
480                    .into_iter()
481                    .map(|row| {
482                        let view: i64 = row.get("view");
483                        let data: Vec<u8> = row.get("data");
484                        let vid_proposal = bincode::deserialize::<
485                            Proposal<SeqTypes, VidDisperseShare<SeqTypes>>,
486                        >(&data)?;
487                        Ok((view as u64, vid_proposal))
488                    })
489                    .collect::<anyhow::Result<BTreeMap<_, _>>>()?;
490
491                // Collect DA proposals for the decide event.
492                let da_rows = tx
493                    .fetch_all(
494                        query(
495                            "SELECT view, data FROM da_proposal2 where view >= $1 AND view <= $2",
496                        )
497                        .bind(from_view.u64() as i64)
498                        .bind(to_view.u64() as i64),
499                    )
500                    .await?;
501                let da_proposals = da_rows
502                    .into_iter()
503                    .map(|row| {
504                        let view: i64 = row.get("view");
505                        let data: Vec<u8> = row.get("data");
506                        let da_proposal = bincode::deserialize::<
507                            Proposal<SeqTypes, DaProposal2<SeqTypes>>,
508                        >(&data)?;
509                        Ok((view as u64, da_proposal.data))
510                    })
511                    .collect::<anyhow::Result<BTreeMap<_, _>>>()?;
512
513                // Collect state certs for the decide event.
514                let state_certs = Self::load_state_certs(&mut tx, from_view, to_view)
515                    .await
516                    .with_context(|| {
517                        format!("load_state_certs from_view={from_view:?} to_view={to_view:?}")
518                    })?;
519
520                let cert2_row = tx
521                    .fetch_optional(
522                        query("SELECT data FROM decided_cert2 WHERE view = $1")
523                            .bind(to_view.u64() as i64),
524                    )
525                    .await?;
526                let cert2 = cert2_row
527                    .map(|row| {
528                        let bytes: Vec<u8> = row.get("data");
529                        bincode::deserialize::<Certificate2<SeqTypes>>(&bytes)
530                            .context("deserializing decided cert2")
531                    })
532                    .transpose()?;
533                drop(tx);
534                Ok(Some((
535                    from_view,
536                    to_view,
537                    leaves,
538                    final_qc,
539                    vid_shares,
540                    da_proposals,
541                    state_certs,
542                    cert2,
543                )))
544            })
545            .await?
546            else {
547                return Ok(());
548            };
549
550            let to_height = leaves
551                .last()
552                .map(|(leaf, _)| leaf.block_header().block_number());
553
554            // Collate all the information by view number and construct a chain of leaves.
555            let chain = leaves
556                .into_iter()
557                // Go in reverse chronological order, as expected by Decide events.
558                .rev()
559                .map(|(mut leaf, cert)| {
560                    let view = leaf.view_number();
561
562                    // Include the VID share if available.
563                    let vid_proposal = vid_shares.remove(&view);
564                    if vid_proposal.is_none() {
565                        tracing::debug!(?view, "VID share not available at decide");
566                    }
567                    let vid_share = vid_proposal.as_ref().map(|proposal| proposal.data.clone());
568
569                    // Fill in the full block payload using the DA proposals we had persisted.
570                    if let Some(proposal) = da_proposals.remove(&view) {
571                        let payload =
572                            Payload::from_bytes(&proposal.encoded_transactions, &proposal.metadata);
573                        leaf.fill_block_payload_unchecked(payload);
574                    } else if view == ViewNumber::genesis() {
575                        // We don't get a DA proposal for the genesis view, but we know what the
576                        // payload always is.
577                        leaf.fill_block_payload_unchecked(Payload::empty().0);
578                    } else {
579                        tracing::debug!(?view, "DA proposal not available at decide");
580                    }
581
582                    let state_cert = state_certs.get(&view).cloned();
583
584                    let info = LeafInfo {
585                        leaf,
586                        vid_share,
587                        state_cert,
588                        // Note: the following fields are not used in Decide event processing,
589                        // and should be removed. For now, we just default them.
590                        state: Default::default(),
591                        delta: Default::default(),
592                    };
593                    DecidedLeaf { info, cert }
594                })
595                .collect();
596
597            tracing::debug!(
598                ?from_view,
599                ?to_view,
600                ?final_qc,
601                ?chain,
602                "generating decide event"
603            );
604
605            if let Some(event) = decide_event_from_chain(chain, cert2) {
606                consumer.handle_event(&event).await?;
607            }
608
609            let from_view_i64 = from_view.u64() as i64;
610            let to_view_i64 = to_view.u64() as i64;
611            let serialized_state_certs = state_certs
612                .into_iter()
613                .map(|(epoch, cert)| Ok((epoch as i64, bincode::serialize(&cert)?)))
614                .collect::<anyhow::Result<Vec<(i64, Vec<u8>)>>>()?;
615
616            // Now that we have definitely processed leaves up to `to_view`, we can update
617            // `last_processed_view` so we don't process these leaves again. We may still fail at
618            // this point, or shut down, and fail to complete this update. At worst this will lead
619            // to us sending a duplicate decide event the next time we are called; this is fine as
620            // the event consumer is required to be idempotent.
621            serializable_retry!(self, || async {
622                let mut tx = self.db.write().await?;
623                tx.upsert(
624                    "event_stream",
625                    ["id", "last_processed_view"],
626                    ["id"],
627                    [(1i32, to_view_i64)],
628                )
629                .await?;
630
631                // Store all the finalized state certs
632                for (epoch, state_cert_bytes) in &serialized_state_certs {
633                    tx.upsert(
634                        "finalized_state_cert",
635                        ["epoch", "state_cert"],
636                        ["epoch"],
637                        [(*epoch, state_cert_bytes.clone())],
638                    )
639                    .await?;
640                }
641
642                // Delete the data that has been fully processed.
643                tx.execute(
644                    query("DELETE FROM vid_share2 where view >= $1 AND view <= $2")
645                        .bind(from_view_i64)
646                        .bind(to_view_i64),
647                )
648                .await?;
649                tx.execute(
650                    query("DELETE FROM da_proposal2 where view >= $1 AND view <= $2")
651                        .bind(from_view_i64)
652                        .bind(to_view_i64),
653                )
654                .await?;
655                tx.execute(
656                    query("DELETE FROM quorum_proposals2 where view >= $1 AND view <= $2")
657                        .bind(from_view_i64)
658                        .bind(to_view_i64),
659                )
660                .await?;
661                tx.execute(
662                    query("DELETE FROM quorum_certificate2 where view >= $1 AND view <= $2")
663                        .bind(from_view_i64)
664                        .bind(to_view_i64),
665                )
666                .await?;
667                tx.execute(
668                    query("DELETE FROM state_cert where view >= $1 AND view <= $2")
669                        .bind(from_view_i64)
670                        .bind(to_view_i64),
671                )
672                .await?;
673                tx.execute(
674                    query("DELETE FROM decided_cert2 where view >= $1 AND view < $2")
675                        .bind(from_view_i64)
676                        .bind(to_view_i64),
677                )
678                .await?;
679
680                // Clean up leaves, but do not delete the most recent one (all leaves with a view
681                // number less than the given value). This is necessary to ensure that, in case of
682                // a restart, we can resume from the last decided leaf.
683                tx.execute(
684                    query("DELETE FROM anchor_leaf2 WHERE view >= $1 AND view < $2")
685                        .bind(from_view_i64)
686                        .bind(to_view_i64),
687                )
688                .await?;
689
690                tx.commit().await?;
691                Ok(())
692            })
693            .await?;
694            last_processed_view = Some(to_view_i64);
695            last_processed_height = to_height;
696        }
697    }
698
699    async fn skip_decide_events(&self, view: ViewNumber) -> anyhow::Result<()> {
700        let processed = self.load_processed_view().await?;
701        // A gap-fill decide reports the older view it filled. Writing that as the cursor would
702        // rewind it, and a later restart with the query module would resume from pruned views.
703        if processed.is_some_and(|processed| processed >= view) {
704            return Ok(());
705        }
706        let from_view = processed.map_or(ViewNumber::genesis(), |processed| processed + 1);
707        let state_certs = serializable_retry!(self, || async {
708            let mut tx = self.db.read().await?;
709            Self::load_state_certs(&mut tx, from_view, view).await
710        })
711        .await?;
712
713        serializable_retry!(self, || async {
714            let mut tx = self.db.write().await?;
715            tx.upsert(
716                "event_stream",
717                ["id", "last_processed_view"],
718                ["id"],
719                [(1i32, view.u64() as i64)],
720            )
721            .await?;
722            for (epoch, cert) in &state_certs {
723                tx.upsert(
724                    "finalized_state_cert",
725                    ["epoch", "state_cert"],
726                    ["epoch"],
727                    [(*epoch as i64, bincode::serialize(cert)?)],
728                )
729                .await?;
730            }
731            prune_to_view(&mut tx, view.u64()).await?;
732            tx.commit().await
733        })
734        .await
735    }
736
737    async fn load_state_certs(
738        tx: &mut Transaction<Read>,
739        from_view: ViewNumber,
740        to_view: ViewNumber,
741    ) -> anyhow::Result<BTreeMap<u64, LightClientStateUpdateCertificateV2<SeqTypes>>> {
742        let rows = tx
743            .fetch_all(
744                query("SELECT view, state_cert FROM state_cert WHERE view >= $1 AND view <= $2")
745                    .bind(from_view.u64() as i64)
746                    .bind(to_view.u64() as i64),
747            )
748            .await?;
749
750        let mut result = BTreeMap::new();
751
752        for row in rows {
753            let data: Vec<u8> = row.get("state_cert");
754
755            let cert: LightClientStateUpdateCertificateV2<SeqTypes> = bincode::deserialize(&data)
756                .or_else(|err_v2| {
757                bincode::deserialize::<LightClientStateUpdateCertificateV1<SeqTypes>>(&data)
758                    .map(Into::into)
759                    .context(format!(
760                        "Failed to deserialize LightClientStateUpdateCertificate: with v1 and v2. \
761                         error: {err_v2}"
762                    ))
763            })?;
764
765            result.insert(cert.epoch.u64(), cert);
766        }
767
768        Ok(result)
769    }
770
771    /// Prune everything older than the target retention, then, at most once per usage check
772    /// interval, prune toward the minimum retention if storage is over the target usage.
773    #[tracing::instrument(skip(self))]
774    async fn prune_to_retention(&self, cur_view: ViewNumber) -> anyhow::Result<()> {
775        self.prune_below(cur_view.u64().saturating_sub(self.gc_opt.target_retention))
776            .await?;
777
778        if !self.usage_check_due() {
779            return Ok(());
780        }
781        let usage = self.consensus_storage_usage().await?;
782        tracing::debug!(usage, "consensus storage usage after pruning");
783        if usage > self.gc_opt.target_usage {
784            tracing::warn!(
785                usage,
786                gc_opt = ?self.gc_opt,
787                "consensus storage is running out of space, pruning to minimum retention"
788            );
789            self.prune_below(cur_view.u64().saturating_sub(self.gc_opt.minimum_retention))
790                .await?;
791        }
792        Ok(())
793    }
794
795    async fn prune_below(&self, view: u64) -> anyhow::Result<()> {
796        // An empty write transaction still takes SQLite's write lock.
797        if view == 0 {
798            return Ok(());
799        }
800        serializable_retry!(self, || async {
801            let mut tx = self.db.write().await?;
802            prune_to_view(&mut tx, view).await?;
803            tx.commit().await
804        })
805        .await
806    }
807
808    /// Measured in a read transaction: on SQLite it scans every page of the pruned tables, and
809    /// holding the write lock that long stalls the vote and proposal writes consensus waits on.
810    async fn consensus_storage_usage(&self) -> anyhow::Result<u64> {
811        let usage_query = match Db::KIND {
812            DbKind::Sqlite => format!(
813                "SELECT sum(pgsize) FROM dbstat WHERE name IN ({})",
814                PRUNE_TABLES
815                    .iter()
816                    .map(|table| format!("'{table}'"))
817                    .join(",")
818            ),
819            DbKind::Postgres => {
820                let table_sizes = PRUNE_TABLES
821                    .iter()
822                    .map(|table| format!("pg_table_size('{table}')"))
823                    .join(" + ");
824                format!("SELECT {table_sizes}")
825            },
826        };
827
828        let mut tx = self.db.read().await?;
829        let (usage,): (i64,) = query_as(&usage_query).fetch_one(tx.as_mut()).await?;
830        Ok(usage as u64)
831    }
832
833    fn usage_check_due(&self) -> bool {
834        let now = Instant::now();
835        let mut last = self.last_usage_check.lock();
836        if last.is_some_and(|last| now.duration_since(last) < self.gc_opt.usage_check_interval) {
837            return false;
838        }
839        *last = Some(now);
840        true
841    }
842}
843
844const PRUNE_TABLES: &[&str] = &[
845    "anchor_leaf2",
846    "vid_share2",
847    "da_proposal2",
848    "quorum_proposals2",
849    "quorum_certificate2",
850    "state_cert",
851    "decided_cert2",
852];
853
854async fn prune_to_view(tx: &mut Transaction<Write>, view: u64) -> anyhow::Result<()> {
855    tracing::debug!(view, "pruning consensus storage");
856
857    for table in PRUNE_TABLES {
858        let res = query(&format!("DELETE FROM {table} WHERE view < $1"))
859            .bind(view as i64)
860            .execute(tx.as_mut())
861            .await
862            .context(format!("pruning {table}"))?;
863        if res.rows_affected() > 0 {
864            tracing::info!(
865                "garbage collected {} rows from {table}",
866                res.rows_affected()
867            );
868        }
869    }
870
871    Ok(())
872}
873
874#[async_trait]
875impl SequencerPersistence for Persistence {
876    fn into_catchup_provider(
877        self,
878        backoff: BackoffParams,
879    ) -> anyhow::Result<Arc<dyn StateCatchup>> {
880        Ok(Arc::new(SqlStateCatchup::new(Arc::new(self.db), backoff)))
881    }
882
883    async fn load_config(&self) -> anyhow::Result<Option<NetworkConfig>> {
884        tracing::info!("loading config from Postgres");
885
886        serializable_retry!(self, || async {
887            // Select the most recent config (although there should only be one).
888            let Some(row) = self
889                .db
890                .read()
891                .await?
892                .fetch_optional("SELECT config FROM network_config ORDER BY id DESC LIMIT 1")
893                .await?
894            else {
895                tracing::info!("config not found");
896                return Ok(None);
897            };
898            let json = row.try_get("config")?;
899
900            let json =
901                migrate_network_config(json).context("migration of network config failed")?;
902            let config = serde_json::from_value(json).context("malformed config file")?;
903
904            Ok(Some(config))
905        })
906        .await
907    }
908
909    async fn save_config(&self, cfg: &NetworkConfig) -> anyhow::Result<()> {
910        tracing::info!("saving config to database");
911        let json = serde_json::to_value(cfg)?;
912
913        serializable_retry!(self, || async {
914            let mut tx = self.db.write().await?;
915            tx.execute(query("INSERT INTO network_config (config) VALUES ($1)").bind(json.clone()))
916                .await?;
917            tx.commit().await
918        })
919        .await
920    }
921
922    async fn persist_decided_leaves(
923        &self,
924        leaf_chain: impl IntoIterator<Item = (&LeafInfo<SeqTypes>, CertificatePair<SeqTypes>)> + Send,
925        cert2: Option<&Certificate2<SeqTypes>>,
926        _consumer: &(impl EventConsumer + 'static),
927    ) -> anyhow::Result<()> {
928        let leaf_chain: Vec<_> = leaf_chain.into_iter().collect();
929        // Keyed by the view of the newest leaf, which a re-vote's cert2 is later than.
930        let newest = leaf_chain
931            .iter()
932            .map(|(info, _)| info.leaf.view_number())
933            .max();
934        if let Some(cert2) = cert2
935            && newest.is_none()
936        {
937            tracing::warn!(view = %cert2.view_number, "cert2 without a decided leaf; not stored");
938        }
939        let cert2_row = cert2
940            .zip(newest)
941            .map(|(cert2, newest)| {
942                let data = bincode::serialize(cert2).context("serializing cert2")?;
943                anyhow::Ok((newest.u64() as i64, data))
944            })
945            .transpose()?;
946        let values = leaf_chain
947            .into_iter()
948            .map(|(info, cert)| {
949                // The leaf may come with a large payload attached. We don't care about this payload
950                // because we already store it separately, as part of the DA proposal. Storing it
951                // here contributes to load on the DB for no reason, so we remove it before
952                // serializing the leaf.
953                let mut leaf = info.leaf.clone();
954                leaf.unfill_block_payload();
955
956                // Keyed by the leaf's view, as everything joined with it is: the certificate of
957                // an epoch's last block may be a re-vote's, at a later view.
958                let view = leaf.view_number().u64() as i64;
959                let leaf_bytes = bincode::serialize(&leaf)?;
960                let qc_bytes = bincode::serialize(cert.qc())?;
961                let next_epoch_qc_bytes = match cert.next_epoch_qc() {
962                    Some(qc) => Some(bincode::serialize(qc)?),
963                    None => None,
964                };
965                Ok((view, leaf_bytes, qc_bytes, next_epoch_qc_bytes))
966            })
967            .collect::<anyhow::Result<Vec<_>>>()?;
968
969        // Append the new leaves and their cert2. We do this in its own transaction because even if
970        // GC or the event consumer later fails, there is no need to abort the storage of the leaves.
971        serializable_retry!(self, || async {
972            let mut tx = self.db.write().await?;
973            tx.upsert(
974                "anchor_leaf2",
975                ["view", "leaf", "qc", "next_epoch_qc"],
976                ["view"],
977                values.clone(),
978            )
979            .await?;
980            if let Some(row) = &cert2_row {
981                tx.upsert("decided_cert2", ["view", "data"], ["view"], [row.clone()])
982                    .await?;
983            }
984            tx.commit().await
985        })
986        .await?;
987
988        Ok(())
989    }
990
991    async fn process_decided_events(
992        &self,
993        view: ViewNumber,
994        consumer: &(impl EventConsumer + 'static),
995    ) -> anyhow::Result<Option<ViewNumber>> {
996        let now = Instant::now();
997        if self.consensus_only {
998            self.skip_decide_events(view).await?;
999        } else {
1000            // Generate events for the new leaves, then GC. On error `last_processed_view` is not
1001            // advanced past the failure point, so no data is lost and the range is retried.
1002            self.generate_decide_events(consumer).await?;
1003
1004            // Best-effort GC of data not included in any decide event; runs again at the next
1005            // decide.
1006            if let Err(err) = self.prune_to_retention(view).await {
1007                tracing::warn!(?view, "pruning failed: {err:#}");
1008            }
1009        }
1010        self.internal_metrics
1011            .internal_process_decided_events_duration
1012            .add_point(now.elapsed().as_secs_f64());
1013
1014        self.load_processed_view().await
1015    }
1016
1017    async fn load_latest_acted_view(&self) -> anyhow::Result<Option<ViewNumber>> {
1018        serializable_retry!(self, || async {
1019            Ok(self
1020                .db
1021                .read()
1022                .await?
1023                .fetch_optional(query("SELECT view FROM highest_voted_view WHERE id = 0"))
1024                .await?
1025                .map(|row| {
1026                    let view: i64 = row.get("view");
1027                    ViewNumber::new(view as u64)
1028                }))
1029        })
1030        .await
1031    }
1032
1033    async fn load_restart_view(&self) -> anyhow::Result<Option<ViewNumber>> {
1034        serializable_retry!(self, || async {
1035            Ok(self
1036                .db
1037                .read()
1038                .await?
1039                .fetch_optional(query("SELECT view FROM restart_view WHERE id = 0"))
1040                .await?
1041                .map(|row| {
1042                    let view: i64 = row.get("view");
1043                    ViewNumber::new(view as u64)
1044                }))
1045        })
1046        .await
1047    }
1048
1049    async fn load_anchor_leaf(&self) -> anyhow::Result<Option<(Leaf2, CertificatePair<SeqTypes>)>> {
1050        serializable_retry!(self, || async {
1051            let Some(row) = self
1052                .db
1053                .read()
1054                .await?
1055                .fetch_optional(
1056                    "SELECT leaf, qc, next_epoch_qc FROM anchor_leaf2 ORDER BY view DESC LIMIT 1",
1057                )
1058                .await?
1059            else {
1060                return Ok(None);
1061            };
1062
1063            let leaf_bytes: Vec<u8> = row.get("leaf");
1064            let leaf2: Leaf2 = bincode::deserialize(&leaf_bytes)?;
1065
1066            let qc_bytes: Vec<u8> = row.get("qc");
1067            let qc2: QuorumCertificate2<SeqTypes> = bincode::deserialize(&qc_bytes)?;
1068
1069            let maybe_next_qc_bytes: Option<Vec<u8>> = row.try_get("next_epoch_qc").ok();
1070            let maybe_next_qc2 = maybe_next_qc_bytes
1071                .and_then(|next_qc_bytes| bincode::deserialize(&next_qc_bytes).ok());
1072
1073            let cert_pair = CertificatePair::new(qc2, maybe_next_qc2);
1074
1075            Ok(Some((leaf2, cert_pair)))
1076        })
1077        .await
1078    }
1079
1080    async fn load_anchor_view(&self) -> anyhow::Result<ViewNumber> {
1081        serializable_retry!(self, || async {
1082            let mut tx = self.db.read().await?;
1083            let (view,) = query_as::<(i64,)>("SELECT coalesce(max(view), 0) FROM anchor_leaf2")
1084                .fetch_one(tx.as_mut())
1085                .await?;
1086            Ok(ViewNumber::new(view as u64))
1087        })
1088        .await
1089    }
1090
1091    async fn load_da_proposal(
1092        &self,
1093        view: ViewNumber,
1094    ) -> anyhow::Result<Option<Proposal<SeqTypes, DaProposal2<SeqTypes>>>> {
1095        serializable_retry!(self, || async {
1096            let result = self
1097                .db
1098                .read()
1099                .await?
1100                .fetch_optional(
1101                    query("SELECT data FROM da_proposal2 where view = $1").bind(view.u64() as i64),
1102                )
1103                .await?;
1104
1105            result
1106                .map(|row| {
1107                    let bytes: Vec<u8> = row.get("data");
1108                    anyhow::Result::<_>::Ok(bincode::deserialize(&bytes)?)
1109                })
1110                .transpose()
1111        })
1112        .await
1113    }
1114
1115    async fn load_vid_share(
1116        &self,
1117        view: ViewNumber,
1118    ) -> anyhow::Result<Option<Proposal<SeqTypes, VidDisperseShare<SeqTypes>>>> {
1119        serializable_retry!(self, || async {
1120            let result = self
1121                .db
1122                .read()
1123                .await?
1124                .fetch_optional(
1125                    query("SELECT data FROM vid_share2 where view = $1").bind(view.u64() as i64),
1126                )
1127                .await?;
1128
1129            result
1130                .map(|row| {
1131                    let bytes: Vec<u8> = row.get("data");
1132                    anyhow::Result::<_>::Ok(bincode::deserialize(&bytes)?)
1133                })
1134                .transpose()
1135        })
1136        .await
1137    }
1138
1139    async fn load_quorum_proposals(
1140        &self,
1141    ) -> anyhow::Result<BTreeMap<ViewNumber, Proposal<SeqTypes, QuorumProposalWrapper<SeqTypes>>>>
1142    {
1143        serializable_retry!(self, || async {
1144            let rows = self
1145                .db
1146                .read()
1147                .await?
1148                .fetch_all("SELECT * FROM quorum_proposals2")
1149                .await?;
1150
1151            Ok(BTreeMap::from_iter(
1152                rows.into_iter()
1153                    .map(|row| {
1154                        let view: i64 = row.get("view");
1155                        let view_number: ViewNumber = ViewNumber::new(view.try_into()?);
1156                        let bytes: Vec<u8> = row.get("data");
1157                        let proposal: Proposal<SeqTypes, QuorumProposalWrapper<SeqTypes>> =
1158                            bincode::deserialize(&bytes).or_else(|error| {
1159                                bincode::deserialize::<
1160                                    Proposal<SeqTypes, QuorumProposalWrapperLegacy<SeqTypes>>,
1161                                >(&bytes)
1162                                .map(convert_proposal)
1163                                .inspect_err(|err_v3| {
1164                                    tracing::warn!(
1165                                        ?view_number,
1166                                        %error,
1167                                        %err_v3,
1168                                        "ignoring malformed quorum proposal DB row"
1169                                    );
1170                                })
1171                            })?;
1172                        Ok((view_number, proposal))
1173                    })
1174                    .collect::<anyhow::Result<Vec<_>>>()?,
1175            ))
1176        })
1177        .await
1178    }
1179
1180    async fn load_quorum_proposal(
1181        &self,
1182        view: ViewNumber,
1183    ) -> anyhow::Result<Proposal<SeqTypes, QuorumProposalWrapper<SeqTypes>>> {
1184        serializable_retry!(self, || async {
1185            let mut tx = self.db.read().await?;
1186            let (data,) = query_as::<(Vec<u8>,)>(
1187                "SELECT data FROM quorum_proposals2 WHERE view = $1 LIMIT 1",
1188            )
1189            .bind(view.u64() as i64)
1190            .fetch_one(tx.as_mut())
1191            .await?;
1192            let proposal: Proposal<SeqTypes, QuorumProposalWrapper<SeqTypes>> =
1193                bincode::deserialize(&data).or_else(|error| {
1194                    bincode::deserialize::<
1195                                Proposal<SeqTypes, QuorumProposalWrapperLegacy<SeqTypes>>,
1196                            >(&data)
1197                            .map(convert_proposal)
1198                            .context(format!(
1199                                "Failed to deserialize quorum proposal for view {view}. \
1200                                 error={error}"
1201                            ))
1202                })?;
1203            Ok(proposal)
1204        })
1205        .await
1206    }
1207
1208    async fn append_vid(
1209        &self,
1210        proposal: &Proposal<SeqTypes, VidDisperseShare<SeqTypes>>,
1211    ) -> anyhow::Result<()> {
1212        let view = proposal.data.view_number().u64();
1213        let payload_hash = proposal.data.payload_commitment();
1214        let data_bytes = bincode::serialize(proposal).unwrap();
1215
1216        let now = Instant::now();
1217        let res = serializable_retry!(self, || async {
1218            let mut tx = self.db.write().await?;
1219            tx.upsert(
1220                "vid_share2",
1221                ["view", "data", "payload_hash"],
1222                ["view"],
1223                [(view as i64, data_bytes.clone(), payload_hash.to_string())],
1224            )
1225            .await?;
1226            tx.commit().await
1227        })
1228        .await;
1229        self.internal_metrics
1230            .internal_append_vid_duration
1231            .add_point(now.elapsed().as_secs_f64());
1232        res
1233    }
1234
1235    async fn append_da(
1236        &self,
1237        proposal: &Proposal<SeqTypes, DaProposal<SeqTypes>>,
1238        vid_commit: VidCommitment,
1239    ) -> anyhow::Result<()> {
1240        let data = &proposal.data;
1241        let view = data.view_number().u64();
1242        let data_bytes = bincode::serialize(proposal).unwrap();
1243
1244        let now = Instant::now();
1245        let res = serializable_retry!(self, || async {
1246            let mut tx = self.db.write().await?;
1247            tx.upsert(
1248                "da_proposal",
1249                ["view", "data", "payload_hash"],
1250                ["view"],
1251                [(view as i64, data_bytes.clone(), vid_commit.to_string())],
1252            )
1253            .await?;
1254            tx.commit().await
1255        })
1256        .await;
1257        self.internal_metrics
1258            .internal_append_da_duration
1259            .add_point(now.elapsed().as_secs_f64());
1260        res
1261    }
1262
1263    async fn record_action(&self, view: ViewNumber, action: HotShotAction) -> anyhow::Result<()> {
1264        // Todo Remove this after https://github.com/EspressoSystems/espresso-network/issues/1931
1265        //
1266        // A timeout vote counts: a restarted node must not vote2 at a view it
1267        // sent a timeout vote for.
1268        if !matches!(
1269            action,
1270            HotShotAction::Propose | HotShotAction::Vote | HotShotAction::TimeoutVote
1271        ) {
1272            return Ok(());
1273        }
1274
1275        serializable_retry!(self, || async {
1276            let stmt = format!(
1277                "INSERT INTO highest_voted_view (id, view) VALUES (0, $1)
1278                ON CONFLICT (id) DO UPDATE SET view = {MAX_FN}(highest_voted_view.view, \
1279                 excluded.view)",
1280                MAX_FN = Db::MAX_FN,
1281            );
1282
1283            let mut tx = self.db.write().await?;
1284            tx.execute(query(&stmt).bind(view.u64() as i64)).await?;
1285
1286            if matches!(action, HotShotAction::Vote) {
1287                let restart_view = view + 1;
1288                let stmt = format!(
1289                    "INSERT INTO restart_view (id, view) VALUES (0, $1)
1290                    ON CONFLICT (id) DO UPDATE SET view = {MAX_FN}(restart_view.view, \
1291                     excluded.view)",
1292                    MAX_FN = Db::MAX_FN,
1293                );
1294                tx.execute(query(&stmt).bind(restart_view.u64() as i64))
1295                    .await?;
1296            }
1297
1298            tx.commit().await
1299        })
1300        .await
1301    }
1302
1303    async fn append_quorum_proposal2(
1304        &self,
1305        proposal: &Proposal<SeqTypes, QuorumProposalWrapper<SeqTypes>>,
1306    ) -> anyhow::Result<()> {
1307        let view_number = proposal.data.view_number().u64();
1308
1309        let proposal_bytes = bincode::serialize(&proposal).context("serializing proposal")?;
1310        let leaf_hash = Committable::commit(&Leaf2::from_quorum_proposal(&proposal.data));
1311
1312        // We also keep track of any QC we see in case we need it to recover our archival storage.
1313        let justify_qc = proposal.data.justify_qc();
1314        let justify_qc_bytes = bincode::serialize(&justify_qc).context("serializing QC")?;
1315        let justify_qc_view = justify_qc.view_number.u64() as i64;
1316        let justify_qc_leaf_commit = justify_qc.data.leaf_commit.to_string();
1317
1318        let now = Instant::now();
1319        let res = serializable_retry!(self, || async {
1320            let mut tx = self.db.write().await?;
1321            tx.upsert(
1322                "quorum_proposals2",
1323                ["view", "leaf_hash", "data"],
1324                ["view"],
1325                [(
1326                    view_number as i64,
1327                    leaf_hash.to_string(),
1328                    proposal_bytes.clone(),
1329                )],
1330            )
1331            .await?;
1332            tx.upsert(
1333                "quorum_certificate2",
1334                ["view", "leaf_hash", "data"],
1335                ["view"],
1336                [(
1337                    justify_qc_view,
1338                    justify_qc_leaf_commit.clone(),
1339                    justify_qc_bytes.clone(),
1340                )],
1341            )
1342            .await?;
1343            tx.commit().await
1344        })
1345        .await;
1346        self.internal_metrics
1347            .internal_append_quorum2_duration
1348            .add_point(now.elapsed().as_secs_f64());
1349        res
1350    }
1351
1352    async fn append_cert2(
1353        &self,
1354        view: ViewNumber,
1355        cert2: Certificate2<SeqTypes>,
1356    ) -> anyhow::Result<()> {
1357        let data = bincode::serialize(&cert2).context("serializing cert2")?;
1358        let view_i64 = view.u64() as i64;
1359        serializable_retry!(self, || async {
1360            let mut tx = self.db.write().await?;
1361            tx.upsert(
1362                "decided_cert2",
1363                ["view", "data"],
1364                ["view"],
1365                [(view_i64, data.clone())],
1366            )
1367            .await?;
1368            tx.commit().await
1369        })
1370        .await
1371    }
1372
1373    async fn load_cert2(&self, view: ViewNumber) -> anyhow::Result<Option<Certificate2<SeqTypes>>> {
1374        let row = self
1375            .db
1376            .read()
1377            .await?
1378            .fetch_optional(
1379                query("SELECT data FROM decided_cert2 WHERE view = $1").bind(view.u64() as i64),
1380            )
1381            .await?;
1382        row.map(|row| {
1383            let bytes: Vec<u8> = row.get("data");
1384            bincode::deserialize::<Certificate2<SeqTypes>>(&bytes).context("deserializing cert2")
1385        })
1386        .transpose()
1387    }
1388
1389    async fn append_high_qc2(&self, high_qc: QuorumCertificate2<SeqTypes>) -> anyhow::Result<()> {
1390        // Locks are ordered by epoch, then view.
1391        let lock = LockView::of(&high_qc);
1392        let data = bincode::serialize(&high_qc).context("serializing high_qc2")?;
1393        serializable_retry!(self, || async {
1394            let mut tx = self.db.write().await?;
1395            // Compare-and-set inside one write transaction so a stale
1396            // concurrent write can never regress the stored view: under
1397            // SERIALIZABLE a racing writer conflicts and retries; on SQLite
1398            // writes are serialized.
1399            let stored_view = query("SELECT data FROM high_qc2 WHERE id = true")
1400                .fetch_optional(tx.as_mut())
1401                .await?
1402                .map(|row| {
1403                    let bytes: Vec<u8> = row.get("data");
1404                    bincode::deserialize::<QuorumCertificate2<SeqTypes>>(&bytes)
1405                        .context("deserializing existing high_qc2")
1406                        .map(|qc| LockView::of(&qc))
1407                })
1408                .transpose()?;
1409            if stored_view.is_some_and(|stored| stored >= lock) {
1410                return Ok(());
1411            }
1412            tx.upsert("high_qc2", ["id", "data"], ["id"], [(true, data.clone())])
1413                .await?;
1414            tx.commit().await
1415        })
1416        .await
1417    }
1418
1419    async fn load_high_qc2(&self) -> anyhow::Result<Option<QuorumCertificate2<SeqTypes>>> {
1420        let row = self
1421            .db
1422            .read()
1423            .await?
1424            .fetch_optional("SELECT data FROM high_qc2 WHERE id = true")
1425            .await?;
1426        row.map(|row| {
1427            let bytes: Vec<u8> = row.get("data");
1428            bincode::deserialize::<QuorumCertificate2<SeqTypes>>(&bytes)
1429                .context("deserializing high_qc2")
1430        })
1431        .transpose()
1432    }
1433
1434    async fn append_boundary_qc2(&self, qc: QuorumCertificate2<SeqTypes>) -> anyhow::Result<()> {
1435        let lock = LockView::of(&qc);
1436        let data = bincode::serialize(&qc).context("serializing boundary_qc2")?;
1437        serializable_retry!(self, || async {
1438            let mut tx = self.db.write().await?;
1439            // Compare-and-set inside one write transaction, as for `high_qc2`.
1440            let stored = query("SELECT data FROM boundary_qc2 WHERE id = true")
1441                .fetch_optional(tx.as_mut())
1442                .await?
1443                .map(|row| {
1444                    let bytes: Vec<u8> = row.get("data");
1445                    bincode::deserialize::<QuorumCertificate2<SeqTypes>>(&bytes)
1446                        .context("deserializing existing boundary_qc2")
1447                        .map(|qc| LockView::of(&qc))
1448                })
1449                .transpose()?;
1450            if stored.is_some_and(|stored| stored >= lock) {
1451                return Ok(());
1452            }
1453            tx.upsert(
1454                "boundary_qc2",
1455                ["id", "data"],
1456                ["id"],
1457                [(true, data.clone())],
1458            )
1459            .await?;
1460            tx.commit().await
1461        })
1462        .await
1463    }
1464
1465    async fn load_boundary_qc2(&self) -> anyhow::Result<Option<QuorumCertificate2<SeqTypes>>> {
1466        let row = self
1467            .db
1468            .read()
1469            .await?
1470            .fetch_optional("SELECT data FROM boundary_qc2 WHERE id = true")
1471            .await?;
1472        row.map(|row| {
1473            let bytes: Vec<u8> = row.get("data");
1474            bincode::deserialize::<QuorumCertificate2<SeqTypes>>(&bytes)
1475                .context("deserializing boundary_qc2")
1476        })
1477        .transpose()
1478    }
1479
1480    async fn load_upgrade_certificate(
1481        &self,
1482    ) -> anyhow::Result<Option<UpgradeCertificate<SeqTypes>>> {
1483        let result = self
1484            .db
1485            .read()
1486            .await?
1487            .fetch_optional("SELECT * FROM upgrade_certificate where id = true")
1488            .await?;
1489
1490        result
1491            .map(|row| {
1492                let bytes: Vec<u8> = row.get("data");
1493                anyhow::Result::<_>::Ok(bincode::deserialize(&bytes)?)
1494            })
1495            .transpose()
1496    }
1497
1498    async fn store_upgrade_certificate(
1499        &self,
1500        decided_upgrade_certificate: Option<UpgradeCertificate<SeqTypes>>,
1501    ) -> anyhow::Result<()> {
1502        let certificate = match decided_upgrade_certificate {
1503            Some(cert) => cert,
1504            None => return Ok(()),
1505        };
1506        let upgrade_certificate_bytes =
1507            bincode::serialize(&certificate).context("serializing upgrade certificate")?;
1508        serializable_retry!(self, || async {
1509            let mut tx = self.db.write().await?;
1510            tx.upsert(
1511                "upgrade_certificate",
1512                ["id", "data"],
1513                ["id"],
1514                [(true, upgrade_certificate_bytes.clone())],
1515            )
1516            .await?;
1517            tx.commit().await
1518        })
1519        .await
1520    }
1521
1522    async fn append_next_epoch_high_qc2(
1523        &self,
1524        next_epoch_high_qc: NextEpochQuorumCertificate2<SeqTypes>,
1525    ) -> anyhow::Result<()> {
1526        let view = next_epoch_high_qc.view_number();
1527        let data =
1528            bincode::serialize(&next_epoch_high_qc).context("serializing next epoch high_qc2")?;
1529        serializable_retry!(self, || async {
1530            let mut tx = self.db.write().await?;
1531            let stored_view =
1532                query("SELECT data FROM next_epoch_quorum_certificate WHERE id = true")
1533                    .fetch_optional(tx.as_mut())
1534                    .await?
1535                    .map(|row| {
1536                        let bytes: Vec<u8> = row.get("data");
1537                        bincode::deserialize::<NextEpochQuorumCertificate2<SeqTypes>>(&bytes)
1538                            .context("deserializing existing next epoch high_qc2")
1539                            .map(|qc| qc.view_number())
1540                    })
1541                    .transpose()?;
1542            if stored_view.is_some_and(|stored| stored >= view) {
1543                return Ok(());
1544            }
1545            tx.upsert(
1546                "next_epoch_quorum_certificate",
1547                ["id", "data"],
1548                ["id"],
1549                [(true, data.clone())],
1550            )
1551            .await?;
1552            tx.commit().await
1553        })
1554        .await
1555    }
1556
1557    async fn store_eqc(
1558        &self,
1559        high_qc: QuorumCertificate2<SeqTypes>,
1560        next_epoch_high_qc: NextEpochQuorumCertificate2<SeqTypes>,
1561    ) -> anyhow::Result<()> {
1562        let eqc_bytes =
1563            bincode::serialize(&(high_qc, next_epoch_high_qc)).context("serializing eqc")?;
1564        serializable_retry!(self, || async {
1565            let mut tx = self.db.write().await?;
1566            tx.upsert("eqc", ["id", "data"], ["id"], [(true, eqc_bytes.clone())])
1567                .await?;
1568            tx.commit().await
1569        })
1570        .await
1571    }
1572
1573    async fn load_eqc(
1574        &self,
1575    ) -> Option<(
1576        QuorumCertificate2<SeqTypes>,
1577        NextEpochQuorumCertificate2<SeqTypes>,
1578    )> {
1579        let result = self
1580            .db
1581            .read()
1582            .await
1583            .ok()?
1584            .fetch_optional("SELECT * FROM eqc where id = true")
1585            .await
1586            .ok()?;
1587
1588        result
1589            .map(|row| {
1590                let bytes: Vec<u8> = row.get("data");
1591                bincode::deserialize(&bytes)
1592            })
1593            .transpose()
1594            .ok()?
1595    }
1596
1597    async fn append_da2(
1598        &self,
1599        proposal: &Proposal<SeqTypes, DaProposal2<SeqTypes>>,
1600        vid_commit: VidCommitment,
1601    ) -> anyhow::Result<()> {
1602        if self.consensus_only {
1603            return Ok(());
1604        }
1605        let data = &proposal.data;
1606        let view = data.view_number().u64();
1607        let data_bytes = bincode::serialize(proposal).unwrap();
1608
1609        let now = Instant::now();
1610        let res = serializable_retry!(self, || async {
1611            let mut tx = self.db.write().await?;
1612            tx.upsert(
1613                "da_proposal2",
1614                ["view", "data", "payload_hash"],
1615                ["view"],
1616                [(view as i64, data_bytes.clone(), vid_commit.to_string())],
1617            )
1618            .await?;
1619            tx.commit().await
1620        })
1621        .await;
1622        self.internal_metrics
1623            .internal_append_da2_duration
1624            .add_point(now.elapsed().as_secs_f64());
1625        res
1626    }
1627
1628    async fn store_drb_result(
1629        &self,
1630        epoch: EpochNumber,
1631        drb_result: DrbResult,
1632    ) -> anyhow::Result<()> {
1633        let epoch_i64 = epoch.u64() as i64;
1634        let drb_result_vec = Vec::from(drb_result);
1635        serializable_retry!(self, || async {
1636            let mut tx = self.db.write().await?;
1637            tx.upsert(
1638                "epoch_drb_and_root",
1639                ["epoch", "drb_result"],
1640                ["epoch"],
1641                [(epoch_i64, drb_result_vec.clone())],
1642            )
1643            .await?;
1644            tx.commit().await
1645        })
1646        .await
1647    }
1648
1649    async fn store_drb_input(&self, drb_input: DrbInput) -> anyhow::Result<()> {
1650        if let Ok(loaded_drb_input) = self.load_drb_input(drb_input.epoch).await {
1651            if loaded_drb_input.difficulty_level != drb_input.difficulty_level {
1652                tracing::error!("Overwriting {loaded_drb_input:?} in storage with {drb_input:?}");
1653            } else if loaded_drb_input.iteration >= drb_input.iteration {
1654                anyhow::bail!(
1655                    "DrbInput in storage {:?} is more recent than {:?}, refusing to update",
1656                    loaded_drb_input,
1657                    drb_input
1658                )
1659            }
1660        }
1661
1662        let drb_epoch_i64 = drb_input.epoch as i64;
1663        let drb_input_bytes = bincode::serialize(&drb_input)
1664            .context("Failed to serialize DrbInput. This is not fatal, but should never happen.")?;
1665
1666        serializable_retry!(self, || async {
1667            let mut tx = self.db.write().await?;
1668            tx.upsert(
1669                "drb",
1670                ["epoch", "drb_input"],
1671                ["epoch"],
1672                [(drb_epoch_i64, drb_input_bytes.clone())],
1673            )
1674            .await?;
1675            tx.commit().await
1676        })
1677        .await
1678    }
1679
1680    async fn load_drb_input(&self, epoch: u64) -> anyhow::Result<DrbInput> {
1681        let row = self
1682            .db
1683            .read()
1684            .await?
1685            .fetch_optional(query("SELECT drb_input FROM drb WHERE epoch = $1").bind(epoch as i64))
1686            .await?;
1687
1688        match row {
1689            None => anyhow::bail!("No DrbInput for epoch {} in storage", epoch),
1690            Some(row) => {
1691                let drb_input_bytes: Vec<u8> = row.try_get("drb_input")?;
1692                let drb_input = bincode::deserialize(&drb_input_bytes)
1693                    .context("Failed to deserialize drb_input from storage")?;
1694
1695                Ok(drb_input)
1696            },
1697        }
1698    }
1699
1700    async fn add_state_cert(
1701        &self,
1702        state_cert: LightClientStateUpdateCertificateV2<SeqTypes>,
1703    ) -> anyhow::Result<()> {
1704        let view_number = state_cert.light_client_state.view_number as i64;
1705        let state_cert_bytes = bincode::serialize(&state_cert)
1706            .context("serializing light client state update certificate")?;
1707
1708        serializable_retry!(self, || async {
1709            let mut tx = self.db.write().await?;
1710            tx.upsert(
1711                "state_cert",
1712                ["view", "state_cert"],
1713                ["view"],
1714                [(view_number, state_cert_bytes.clone())],
1715            )
1716            .await?;
1717            tx.commit().await
1718        })
1719        .await
1720    }
1721
1722    async fn load_state_cert(
1723        &self,
1724    ) -> anyhow::Result<Option<LightClientStateUpdateCertificateV2<SeqTypes>>> {
1725        let Some(row) = self
1726            .db
1727            .read()
1728            .await?
1729            .fetch_optional(
1730                "SELECT state_cert FROM finalized_state_cert ORDER BY epoch DESC LIMIT 1",
1731            )
1732            .await?
1733        else {
1734            return Ok(None);
1735        };
1736        let bytes: Vec<u8> = row.get("state_cert");
1737
1738        let cert = match bincode::deserialize(&bytes) {
1739            Ok(cert) => cert,
1740            Err(err) => {
1741                tracing::info!(
1742                    error = %err,
1743                    "Failed to deserialize state certificate with v2. attempting with v1"
1744                );
1745
1746                let v1_cert =
1747                    bincode::deserialize::<LightClientStateUpdateCertificateV1<SeqTypes>>(&bytes)
1748                        .with_context(|| {
1749                        format!("Failed to deserialize using both v1 and v2. error: {err}")
1750                    })?;
1751
1752                v1_cert.into()
1753            },
1754        };
1755
1756        Ok(Some(cert))
1757    }
1758
1759    async fn load_start_epoch_info(&self) -> anyhow::Result<Vec<InitializerEpochInfo<SeqTypes>>> {
1760        let rows = self
1761            .db
1762            .read()
1763            .await?
1764            .fetch_all(
1765                query("SELECT * from epoch_drb_and_root ORDER BY epoch DESC LIMIT $1")
1766                    .bind(RECENT_STAKE_TABLES_LIMIT as i64),
1767            )
1768            .await?;
1769
1770        // reverse the rows vector to return the most recent epochs, but in ascending order
1771        rows.into_iter()
1772            .rev()
1773            .map(|row| {
1774                let epoch: i64 = row.try_get("epoch")?;
1775                let drb_result: Option<Vec<u8>> = row.try_get("drb_result")?;
1776                let block_header: Option<Vec<u8>> = row.try_get("block_header")?;
1777                if let Some(drb_result) = drb_result {
1778                    let drb_result_array = drb_result
1779                        .try_into()
1780                        .or_else(|_| bail!("invalid drb result"))?;
1781                    let block_header: Option<<SeqTypes as NodeType>::BlockHeader> = block_header
1782                        .map(|data| bincode::deserialize(&data))
1783                        .transpose()?;
1784                    Ok(Some(InitializerEpochInfo::<SeqTypes> {
1785                        epoch: EpochNumber::new(epoch as u64),
1786                        drb_result: drb_result_array,
1787                        block_header,
1788                    }))
1789                } else {
1790                    // Right now we skip the epoch_drb_and_root row if there is no drb result.
1791                    // This seems reasonable based on the expected order of events, but please double check!
1792                    Ok(None)
1793                }
1794            })
1795            .filter_map(|e| match e {
1796                Err(v) => Some(Err(v)),
1797                Ok(Some(v)) => Some(Ok(v)),
1798                Ok(None) => None,
1799            })
1800            .collect()
1801    }
1802
1803    fn enable_metrics(&mut self, metrics: &dyn Metrics) {
1804        self.internal_metrics = PersistenceMetricsValue::new(metrics);
1805
1806        if let Some(probe) = &self.probe {
1807            probe.register(&*metrics.subgroup("disk".into()));
1808        }
1809    }
1810}
1811
1812#[async_trait]
1813impl StateCertPersistence for Persistence {
1814    async fn get_state_cert_by_epoch(
1815        &self,
1816        epoch: u64,
1817    ) -> anyhow::Result<Option<LightClientStateUpdateCertificateV2<SeqTypes>>> {
1818        let Some(row) = self
1819            .db
1820            .read()
1821            .await?
1822            .fetch_optional(
1823                query("SELECT state_cert FROM finalized_state_cert WHERE epoch = $1")
1824                    .bind(epoch as i64),
1825            )
1826            .await?
1827        else {
1828            return Ok(None);
1829        };
1830        let bytes: Vec<u8> = row.get("state_cert");
1831
1832        let cert = match bincode::deserialize(&bytes) {
1833            Ok(cert) => cert,
1834            Err(err) => {
1835                tracing::info!(
1836                    error = %err,
1837                    "Failed to deserialize state certificate with v2. attempting with v1"
1838                );
1839
1840                let v1_cert =
1841                    bincode::deserialize::<LightClientStateUpdateCertificateV1<SeqTypes>>(&bytes)
1842                        .with_context(|| {
1843                        format!("Failed to deserialize using both v1 and v2. error: {err}")
1844                    })?;
1845
1846                v1_cert.into()
1847            },
1848        };
1849
1850        Ok(Some(cert))
1851    }
1852
1853    async fn insert_state_cert(
1854        &self,
1855        epoch: u64,
1856        cert: LightClientStateUpdateCertificateV2<SeqTypes>,
1857    ) -> anyhow::Result<()> {
1858        let epoch_i64 = epoch as i64;
1859        let bytes = bincode::serialize(&cert)
1860            .with_context(|| format!("Failed to serialize state cert for epoch {epoch}"))?;
1861
1862        serializable_retry!(self, || async {
1863            let mut tx = self.db.write().await?;
1864            tx.upsert(
1865                "finalized_state_cert",
1866                ["epoch", "state_cert"],
1867                ["epoch"],
1868                [(epoch_i64, bytes.clone())],
1869            )
1870            .await?;
1871            tx.commit().await
1872        })
1873        .await
1874    }
1875}
1876
1877fn deserialize_authenticated_validator_map(
1878    bytes: &[u8],
1879) -> anyhow::Result<AuthenticatedValidatorMap> {
1880    if let Ok(map) = bincode::deserialize::<AuthenticatedValidatorMap>(bytes) {
1881        return Ok(map);
1882    }
1883
1884    // Pre-Schnorr-Option: stake_table_key as Option<KEY>, state_ver_key as raw KEY.
1885    if let Ok(pre_schnorr) = bincode::deserialize::<
1886        IndexMap<Address, crate::persistence::RegisteredValidatorPreSchnorrOption>,
1887    >(bytes)
1888    {
1889        return pre_schnorr
1890            .into_iter()
1891            .map(|(addr, v)| {
1892                let registered = v.migrate();
1893                let authenticated = AuthenticatedValidator::try_from(registered)?;
1894                Ok((addr, authenticated))
1895            })
1896            .collect();
1897    }
1898
1899    // Pre-Option: both keys raw, with x25519/p2p fields.
1900    let legacy: IndexMap<Address, crate::persistence::RegisteredValidatorPreOption> =
1901        bincode::deserialize(bytes).context("deserializing stake table")?;
1902    legacy
1903        .into_iter()
1904        .map(|(addr, v)| {
1905            let registered = v.migrate();
1906            let authenticated = AuthenticatedValidator::try_from(registered)?;
1907            Ok((addr, authenticated))
1908        })
1909        .collect()
1910}
1911
1912#[async_trait]
1913impl MembershipPersistence for Persistence {
1914    async fn load_stake(&self, epoch: EpochNumber) -> anyhow::Result<Option<StakeTuple>> {
1915        let result = self
1916            .db
1917            .read()
1918            .await?
1919            .fetch_optional(
1920                query(
1921                    "SELECT stake, block_reward, stake_table_hash FROM epoch_drb_and_root WHERE \
1922                     epoch = $1 AND stake IS NOT NULL",
1923                )
1924                .bind(epoch.u64() as i64),
1925            )
1926            .await?;
1927
1928        result
1929            .map(|row| {
1930                let stake_table_bytes: Vec<u8> = row.get("stake");
1931                let reward_bytes: Option<Vec<u8>> = row.get("block_reward");
1932                let stake_table_hash_bytes: Option<Vec<u8>> = row.get("stake_table_hash");
1933                let stake_table = deserialize_authenticated_validator_map(&stake_table_bytes)?;
1934                let reward: Option<RewardAmount> = reward_bytes
1935                    .map(|b| bincode::deserialize(&b).context("deserializing block_reward"))
1936                    .transpose()?;
1937                let stake_table_hash: Option<StakeTableHash> = stake_table_hash_bytes
1938                    .map(|b| bincode::deserialize(&b).context("deserializing stake table hash"))
1939                    .transpose()?;
1940
1941                Ok((stake_table, reward, stake_table_hash))
1942            })
1943            .transpose()
1944    }
1945
1946    async fn load_drb_result(&self, epoch: EpochNumber) -> anyhow::Result<Option<DrbResult>> {
1947        let result = self
1948            .db
1949            .read()
1950            .await?
1951            .fetch_optional(
1952                query(
1953                    "SELECT drb_result FROM epoch_drb_and_root WHERE epoch = $1 AND drb_result IS \
1954                     NOT NULL",
1955                )
1956                .bind(epoch.u64() as i64),
1957            )
1958            .await?;
1959
1960        result
1961            .map(|row| {
1962                let bytes: Vec<u8> = row.get("drb_result");
1963                bytes.try_into().or_else(|_| bail!("invalid drb result"))
1964            })
1965            .transpose()
1966    }
1967
1968    async fn load_epoch_root(&self, epoch: EpochNumber) -> anyhow::Result<Option<Header>> {
1969        let result = self
1970            .db
1971            .read()
1972            .await?
1973            .fetch_optional(
1974                query(
1975                    "SELECT block_header FROM epoch_drb_and_root WHERE epoch = $1 AND \
1976                     block_header IS NOT NULL",
1977                )
1978                .bind(epoch.u64() as i64),
1979            )
1980            .await?;
1981
1982        result
1983            .map(|row| {
1984                let bytes: Vec<u8> = row.get("block_header");
1985                bincode::deserialize(&bytes).context("deserializing block header")
1986            })
1987            .transpose()
1988    }
1989
1990    async fn store_epoch_root(
1991        &self,
1992        epoch: EpochNumber,
1993        block_header: Header,
1994    ) -> anyhow::Result<()> {
1995        let epoch_i64 = epoch.u64() as i64;
1996        let block_header_bytes =
1997            bincode::serialize(&block_header).context("serializing block header")?;
1998
1999        serializable_retry!(self, || async {
2000            let mut tx = self.db.write().await?;
2001            tx.upsert(
2002                "epoch_drb_and_root",
2003                ["epoch", "block_header"],
2004                ["epoch"],
2005                [(epoch_i64, block_header_bytes.clone())],
2006            )
2007            .await?;
2008            tx.commit().await
2009        })
2010        .await
2011    }
2012
2013    async fn load_latest_stake(&self, limit: u64) -> anyhow::Result<Option<Vec<IndexedStake>>> {
2014        let mut tx = self.db.read().await?;
2015
2016        let rows = match query_as::<(i64, Vec<u8>, Option<Vec<u8>>, Option<Vec<u8>>)>(
2017            "SELECT epoch, stake, block_reward, stake_table_hash FROM epoch_drb_and_root WHERE \
2018             stake is NOT NULL ORDER BY epoch DESC LIMIT $1",
2019        )
2020        .bind(limit as i64)
2021        .fetch_all(tx.as_mut())
2022        .await
2023        {
2024            Ok(bytes) => bytes,
2025            Err(err) => {
2026                tracing::error!("error loading stake tables: {err:#}");
2027                bail!("{err:#}");
2028            },
2029        };
2030
2031        let stakes: anyhow::Result<Vec<IndexedStake>> = rows
2032            .into_iter()
2033            .map(
2034                |(id, stake_bytes, reward_bytes_opt, stake_table_hash_bytes_opt)| {
2035                    let stake_table = deserialize_authenticated_validator_map(&stake_bytes)?;
2036
2037                    let block_reward: Option<RewardAmount> = reward_bytes_opt
2038                        .map(|b| bincode::deserialize(&b).context("deserializing block_reward"))
2039                        .transpose()?;
2040
2041                    let stake_table_hash: Option<StakeTableHash> = stake_table_hash_bytes_opt
2042                        .map(|b| bincode::deserialize(&b).context("deserializing stake table hash"))
2043                        .transpose()?;
2044
2045                    Ok((
2046                        EpochNumber::new(id as u64),
2047                        (stake_table, block_reward),
2048                        stake_table_hash,
2049                    ))
2050                },
2051            )
2052            .collect();
2053
2054        Ok(Some(stakes?))
2055    }
2056
2057    async fn store_stake(
2058        &self,
2059        epoch: EpochNumber,
2060        stake: AuthenticatedValidatorMap,
2061        block_reward: Option<RewardAmount>,
2062        stake_table_hash: Option<StakeTableHash>,
2063    ) -> anyhow::Result<()> {
2064        let epoch_i64 = epoch.u64() as i64;
2065        let stake_table_bytes = bincode::serialize(&stake).context("serializing stake table")?;
2066        let reward_bytes = block_reward
2067            .map(|r| bincode::serialize(&r).context("serializing block reward"))
2068            .transpose()?;
2069        let stake_table_hash_bytes = stake_table_hash
2070            .map(|h| bincode::serialize(&h).context("serializing stake table hash"))
2071            .transpose()?;
2072        serializable_retry!(self, || async {
2073            let mut tx = self.db.write().await?;
2074            tx.upsert(
2075                "epoch_drb_and_root",
2076                ["epoch", "stake", "block_reward", "stake_table_hash"],
2077                ["epoch"],
2078                [(
2079                    epoch_i64,
2080                    stake_table_bytes.clone(),
2081                    reward_bytes.clone(),
2082                    stake_table_hash_bytes.clone(),
2083                )],
2084            )
2085            .await?;
2086            tx.commit().await
2087        })
2088        .await
2089    }
2090
2091    async fn store_events(
2092        &self,
2093        l1_finalized: u64,
2094        events: Vec<(EventKey, StakeTableEvent)>,
2095    ) -> anyhow::Result<()> {
2096        let l1_finalized_i64: i64 = l1_finalized.try_into()?;
2097        let serialized_events = events
2098            .into_iter()
2099            .map(|((block_number, index), event)| {
2100                Ok((
2101                    i64::try_from(block_number)?,
2102                    i64::try_from(index)?,
2103                    serde_json::to_value(event).context("l1 event to value")?,
2104                ))
2105            })
2106            .collect::<anyhow::Result<Vec<_>>>()?;
2107
2108        serializable_retry!(self, || async {
2109            let mut tx = self.db.write().await?;
2110
2111            // check last l1 block if there is any
2112            let last_processed_l1_block = query_as::<(i64,)>(
2113                "SELECT last_l1_block FROM stake_table_events_l1_block where id = 0",
2114            )
2115            .fetch_optional(tx.as_mut())
2116            .await?
2117            .map(|(l1,)| l1);
2118
2119            tracing::debug!("last l1 finalizes in database = {last_processed_l1_block:?}");
2120
2121            // skip events storage if the database already has higher l1 block events
2122            let serialized_events_len = serialized_events.len();
2123            if last_processed_l1_block > Some(l1_finalized_i64) {
2124                tracing::debug!(
2125                    ?last_processed_l1_block,
2126                    l1_finalized,
2127                    serialized_events_len,
2128                    "last l1 finalized stored is already higher"
2129                );
2130                return Ok(());
2131            }
2132
2133            if !serialized_events.is_empty() {
2134                let mut query_builder: sqlx::QueryBuilder<Db> = sqlx::QueryBuilder::new(
2135                    "INSERT INTO stake_table_events (l1_block, log_index, event) ",
2136                );
2137
2138                query_builder.push_values(
2139                    serialized_events.iter().cloned(),
2140                    |mut b, (l1_block, log_index, event)| {
2141                        b.push_bind(l1_block).push_bind(log_index).push_bind(event);
2142                    },
2143                );
2144
2145                query_builder.push(" ON CONFLICT DO NOTHING");
2146                let query = query_builder.build();
2147
2148                query.execute(tx.as_mut()).await?;
2149            }
2150
2151            // update l1 block
2152            tx.upsert(
2153                "stake_table_events_l1_block",
2154                ["id", "last_l1_block"],
2155                ["id"],
2156                [(0_i32, l1_finalized_i64)],
2157            )
2158            .await?;
2159
2160            tx.commit().await?;
2161
2162            Ok(())
2163        })
2164        .await
2165    }
2166
2167    /// Loads all events from persistent storage up to the specified L1 block.
2168    ///
2169    /// # Returns
2170    ///
2171    /// Returns a tuple containing:
2172    /// - `Option<u64>` - The queried L1 block for which all events have been successfully fetched.
2173    /// - `Vec<(EventKey, StakeTableEvent)>` - A list of events, where each entry is a tuple of the event key
2174    /// event key is (l1 block number, log index)
2175    ///   and the corresponding StakeTable event.
2176    ///
2177    async fn load_events(
2178        &self,
2179        from_l1_block: u64,
2180        to_l1_block: u64,
2181    ) -> anyhow::Result<(
2182        Option<EventsPersistenceRead>,
2183        Vec<(EventKey, StakeTableEvent)>,
2184    )> {
2185        let mut tx = self.db.read().await?;
2186
2187        // check last l1 block if there is any
2188        let res = query_as::<(i64,)>(
2189            "SELECT last_l1_block FROM stake_table_events_l1_block where id = 0",
2190        )
2191        .fetch_optional(tx.as_mut())
2192        .await?;
2193
2194        let Some((last_processed_l1_block,)) = res else {
2195            // this just means we dont have any events stored
2196            return Ok((None, Vec::new()));
2197        };
2198
2199        // Determine the L1 block for querying events.
2200        // If the last stored L1 block is greater than the requested block, limit the query to the requested block.
2201        // Otherwise, query up to the last stored block.
2202        let to_l1_block = to_l1_block.try_into()?;
2203        let query_l1_block = if last_processed_l1_block > to_l1_block {
2204            to_l1_block
2205        } else {
2206            last_processed_l1_block
2207        };
2208
2209        let rows = query(
2210            "SELECT l1_block, log_index, event FROM stake_table_events WHERE $1 <= l1_block AND \
2211             l1_block <= $2 ORDER BY l1_block ASC, log_index ASC",
2212        )
2213        .bind(i64::try_from(from_l1_block)?)
2214        .bind(query_l1_block)
2215        .fetch_all(tx.as_mut())
2216        .await?;
2217
2218        let events = rows
2219            .into_iter()
2220            .map(|row| {
2221                let l1_block: i64 = row.try_get("l1_block")?;
2222                let log_index: i64 = row.try_get("log_index")?;
2223                let event = serde_json::from_value(row.try_get("event")?)?;
2224
2225                Ok(((l1_block.try_into()?, log_index.try_into()?), event))
2226            })
2227            .collect::<anyhow::Result<Vec<_>>>()?;
2228
2229        // Determine the read state based on the queried block range.
2230        // - If the persistence returned events up to the requested block, the read is complete.
2231        // - Otherwise, indicate that the read is up to the last processed block.
2232        if query_l1_block == to_l1_block {
2233            Ok((Some(EventsPersistenceRead::Complete), events))
2234        } else {
2235            Ok((
2236                Some(EventsPersistenceRead::UntilL1Block(
2237                    query_l1_block.try_into()?,
2238                )),
2239                events,
2240            ))
2241        }
2242    }
2243
2244    async fn delete_stake_tables(&self) -> anyhow::Result<()> {
2245        serializable_retry!(self, || async {
2246            let mut tx = self.db.write().await?;
2247            if Db::KIND == DbKind::Postgres {
2248                query(
2249                    "TRUNCATE stake_table_events, stake_table_events_l1_block, \
2250                     epoch_drb_and_root, stake_table_validators",
2251                )
2252                .execute(tx.as_mut())
2253                .await?;
2254            } else {
2255                query("DELETE FROM stake_table_events")
2256                    .execute(tx.as_mut())
2257                    .await?;
2258                query("DELETE FROM stake_table_events_l1_block")
2259                    .execute(tx.as_mut())
2260                    .await?;
2261                query("DELETE FROM epoch_drb_and_root")
2262                    .execute(tx.as_mut())
2263                    .await?;
2264                query("DELETE FROM stake_table_validators")
2265                    .execute(tx.as_mut())
2266                    .await?;
2267            }
2268            tx.commit().await?;
2269            Ok(())
2270        })
2271        .await
2272    }
2273
2274    async fn store_all_validators(
2275        &self,
2276        epoch: EpochNumber,
2277        all_validators: RegisteredValidatorMap,
2278    ) -> anyhow::Result<()> {
2279        if all_validators.is_empty() {
2280            return Ok(());
2281        }
2282
2283        let epoch_i64 = epoch.u64() as i64;
2284        let serialized_validators = all_validators
2285            .into_iter()
2286            .map(|(address, validator)| {
2287                let validator_json =
2288                    serde_json::to_value(&validator).context("serializing validator to json")?;
2289                Ok((address.to_string(), validator_json))
2290            })
2291            .collect::<anyhow::Result<Vec<_>>>()?;
2292
2293        serializable_retry!(self, || async {
2294            let mut tx = self.db.write().await?;
2295
2296            let mut query_builder = QueryBuilder::new(
2297                "INSERT INTO stake_table_validators (epoch, address, validator) ",
2298            );
2299
2300            query_builder.push_values(
2301                serialized_validators.iter().cloned(),
2302                |mut b, (address, validator)| {
2303                    b.push_bind(epoch_i64)
2304                        .push_bind(address)
2305                        .push_bind(validator);
2306                },
2307            );
2308
2309            query_builder
2310                .push(" ON CONFLICT (epoch, address) DO UPDATE SET validator = EXCLUDED.validator");
2311
2312            let query = query_builder.build();
2313
2314            query.execute(tx.as_mut()).await?;
2315
2316            tx.commit().await?;
2317            Ok(())
2318        })
2319        .await
2320    }
2321
2322    async fn load_all_validators(
2323        &self,
2324        epoch: EpochNumber,
2325        offset: u64,
2326        limit: u64,
2327    ) -> anyhow::Result<Vec<RegisteredValidator<PubKey>>> {
2328        let mut tx = self.db.read().await?;
2329
2330        // Use LOWER(address) in ORDER BY to ensure consistent ordering for SQlite and Postgres.
2331        // Postgres sorts text case sensitively by default, while SQLite sorts case insensitively.
2332        // Applying LOWER() makes the result consistent.
2333        let rows = query(
2334            "SELECT address, validator
2335         FROM stake_table_validators
2336         WHERE epoch = $1
2337         ORDER BY LOWER(address) ASC
2338         LIMIT $2 OFFSET $3",
2339        )
2340        .bind(epoch.u64() as i64)
2341        .bind(limit as i64)
2342        .bind(offset as i64)
2343        .fetch_all(tx.as_mut())
2344        .await?;
2345        rows.into_iter()
2346            .map(|row| {
2347                let validator_json: serde_json::Value = row.try_get("validator")?;
2348                serde_json::from_value::<RegisteredValidator<PubKey>>(validator_json)
2349                    .map_err(Into::into)
2350            })
2351            .collect()
2352    }
2353}
2354
2355#[async_trait]
2356impl Provider<SeqTypes, VidCommonRequest> for Persistence {
2357    #[tracing::instrument(skip(self))]
2358    async fn fetch(&self, req: VidCommonRequest) -> Option<VidCommon> {
2359        let mut tx = match self.db.read().await {
2360            Ok(tx) => tx,
2361            Err(err) => {
2362                tracing::warn!("could not open transaction: {err:#}");
2363                return None;
2364            },
2365        };
2366
2367        let bytes = match query_as::<(Vec<u8>,)>(
2368            "SELECT data FROM vid_share2 WHERE payload_hash = $1 LIMIT 1",
2369        )
2370        .bind(req.0.to_string())
2371        .fetch_optional(tx.as_mut())
2372        .await
2373        {
2374            Ok(Some((bytes,))) => bytes,
2375            Ok(None) => return None,
2376            Err(err) => {
2377                tracing::error!("error loading VID share: {err:#}");
2378                return None;
2379            },
2380        };
2381
2382        let share: Proposal<SeqTypes, VidDisperseShare<SeqTypes>> =
2383            match bincode::deserialize(&bytes) {
2384                Ok(share) => share,
2385                Err(err) => {
2386                    tracing::warn!("error decoding VID share: {err:#}");
2387                    return None;
2388                },
2389            };
2390
2391        match share.data {
2392            VidDisperseShare::V0(vid) => Some(VidCommon::V0(vid.common)),
2393            VidDisperseShare::V1(vid) => Some(VidCommon::V1(vid.common)),
2394            VidDisperseShare::V2(vid) => Some(VidCommon::V2(vid.common)),
2395        }
2396    }
2397}
2398
2399#[async_trait]
2400impl Provider<SeqTypes, PayloadRequest> for Persistence {
2401    #[tracing::instrument(skip(self))]
2402    async fn fetch(&self, req: PayloadRequest) -> Option<Payload> {
2403        let mut tx = match self.db.read().await {
2404            Ok(tx) => tx,
2405            Err(err) => {
2406                tracing::warn!("could not open transaction: {err:#}");
2407                return None;
2408            },
2409        };
2410
2411        let bytes = match query_as::<(Vec<u8>,)>(
2412            "SELECT data FROM da_proposal2 WHERE payload_hash = $1 LIMIT 1",
2413        )
2414        .bind(req.0.to_string())
2415        .fetch_optional(tx.as_mut())
2416        .await
2417        {
2418            Ok(Some((bytes,))) => bytes,
2419            Ok(None) => return None,
2420            Err(err) => {
2421                tracing::warn!("error loading DA proposal: {err:#}");
2422                return None;
2423            },
2424        };
2425
2426        let proposal: Proposal<SeqTypes, DaProposal2<SeqTypes>> = match bincode::deserialize(&bytes)
2427        {
2428            Ok(proposal) => proposal,
2429            Err(err) => {
2430                tracing::error!("error decoding DA proposal: {err:#}");
2431                return None;
2432            },
2433        };
2434
2435        Some(Payload::from_bytes(
2436            &proposal.data.encoded_transactions,
2437            &proposal.data.metadata,
2438        ))
2439    }
2440}
2441
2442#[cfg(test)]
2443mod testing {
2444    use super::{super::qs::testing::TmpDb, *};
2445    use crate::persistence::tests::TestablePersistence;
2446
2447    #[async_trait]
2448    impl TestablePersistence for Persistence {
2449        type Storage = Arc<TmpDb>;
2450
2451        async fn tmp_storage() -> Self::Storage {
2452            Arc::new(TmpDb::init().await)
2453        }
2454
2455        #[allow(refining_impl_trait)]
2456        fn options(db: &Self::Storage) -> Options {
2457            Options(super::super::tmp_options(db))
2458        }
2459    }
2460}
2461
2462#[cfg(test)]
2463mod test {
2464    use std::time::Duration;
2465
2466    use espresso_types::{Leaf, NodeState, PubKey, ValidatedState, traits::NullEventConsumer};
2467    use futures::stream::TryStreamExt;
2468    use hotshot_example_types::node_types::TEST_VERSIONS;
2469    use hotshot_types::{
2470        data::{
2471            EpochNumber, QuorumProposal2, ns_table::parse_ns_table,
2472            vid_disperse::AvidMDisperseShare,
2473        },
2474        message::convert_proposal,
2475        simple_certificate::QuorumCertificate,
2476        traits::{EncodeBytes, signature_key::SignatureKey},
2477        utils::EpochTransitionIndicator,
2478        vid::avidm::{AvidMScheme, init_avidm_param},
2479    };
2480
2481    use super::*;
2482    use crate::{BLSPubKey, persistence::tests::TestablePersistence as _};
2483
2484    #[test_log::test(tokio::test(flavor = "multi_thread"))]
2485    async fn test_quorum_proposals_leaf_hash_migration() {
2486        // Create some legacy quorum proposals to test with, as a pre-migration node wrote them.
2487        let leaf = Leaf::genesis(
2488            &ValidatedState::default(),
2489            &NodeState::mock(),
2490            TEST_VERSIONS.test.base,
2491        )
2492        .await;
2493        let privkey = BLSPubKey::generated_from_seed_indexed([0; 32], 1).1;
2494        let signature = PubKey::sign(&privkey, &[]).unwrap();
2495        let mut quorum_proposal: Proposal<SeqTypes, QuorumProposal<SeqTypes>> = Proposal {
2496            data: QuorumProposal {
2497                block_header: leaf.block_header().clone(),
2498                view_number: ViewNumber::genesis(),
2499                justify_qc: QuorumCertificate::genesis(
2500                    &ValidatedState::default(),
2501                    &NodeState::mock(),
2502                    TEST_VERSIONS.test,
2503                )
2504                .await,
2505                upgrade_certificate: None,
2506                proposal_certificate: None,
2507            },
2508            signature,
2509            _pd: Default::default(),
2510        };
2511
2512        let qp1 = quorum_proposal.clone();
2513        quorum_proposal.data.view_number = ViewNumber::new(1);
2514        let qp2 = quorum_proposal;
2515        let qps = [qp1, qp2];
2516
2517        // Create persistence and add the quorum proposals with NULL leaf hash.
2518        let db = Persistence::tmp_storage().await;
2519        let persistence = Persistence::connect(&db).await;
2520        let mut tx = persistence.db.write().await.unwrap();
2521        let params = qps
2522            .iter()
2523            .map(|qp| {
2524                (
2525                    qp.data.view_number.u64() as i64,
2526                    bincode::serialize(&qp).unwrap(),
2527                )
2528            })
2529            .collect::<Vec<_>>();
2530        tx.upsert("quorum_proposals", ["view", "data"], ["view"], params)
2531            .await
2532            .unwrap();
2533        tx.commit().await.unwrap();
2534
2535        // Create a new persistence and ensure the commitments get populated.
2536        let persistence = Persistence::connect(&db).await;
2537        let mut tx = persistence.db.read().await.unwrap();
2538        let rows = tx
2539            .fetch("SELECT * FROM quorum_proposals ORDER BY view ASC")
2540            .try_collect::<Vec<_>>()
2541            .await
2542            .unwrap();
2543        assert_eq!(rows.len(), qps.len());
2544        for (row, qp) in rows.into_iter().zip(qps) {
2545            assert_eq!(row.get::<i64, _>("view"), qp.data.view_number.u64() as i64);
2546            assert_eq!(
2547                row.get::<Vec<u8>, _>("data"),
2548                bincode::serialize(&qp).unwrap()
2549            );
2550            assert_eq!(
2551                row.get::<String, _>("leaf_hash"),
2552                Committable::commit(&Leaf::from_quorum_proposal(&qp.data)).to_string()
2553            );
2554        }
2555    }
2556
2557    fn pre_option_validator(
2558        seed: u8,
2559        stake: u64,
2560    ) -> crate::persistence::RegisteredValidatorPreOption {
2561        use std::collections::HashMap;
2562
2563        use alloy::primitives::U256;
2564        use hotshot_types::light_client::StateVerKey;
2565
2566        crate::persistence::RegisteredValidatorPreOption {
2567            account: Address::random(),
2568            stake_table_key: BLSPubKey::generated_from_seed_indexed([seed; 32], 0).0,
2569            state_ver_key: StateVerKey::default(),
2570            stake: U256::from(stake),
2571            commission: 0,
2572            delegators: HashMap::new(),
2573            authenticated: true,
2574            x25519_key: None,
2575            p2p_addr: None,
2576        }
2577    }
2578
2579    async fn insert_legacy_stake_row(
2580        persistence: &Persistence,
2581        epoch: i64,
2582        validator: crate::persistence::RegisteredValidatorPreOption,
2583    ) {
2584        let mut map: IndexMap<Address, crate::persistence::RegisteredValidatorPreOption> =
2585            IndexMap::new();
2586        map.insert(validator.account, validator);
2587        let stake_bytes = bincode::serialize(&map).unwrap();
2588        let mut tx = persistence.db.write().await.unwrap();
2589        tx.execute(
2590            query("INSERT INTO epoch_drb_and_root (epoch, stake) VALUES ($1, $2)")
2591                .bind(epoch)
2592                .bind(&stake_bytes),
2593        )
2594        .await
2595        .unwrap();
2596        tx.commit().await.unwrap();
2597    }
2598
2599    #[test_log::test(tokio::test(flavor = "multi_thread"))]
2600    async fn test_load_stake_legacy_storage() {
2601        let tmp = Persistence::tmp_storage().await;
2602        let persistence = Persistence::connect(&tmp).await;
2603
2604        let v1 = pre_option_validator(1, 100);
2605        let v2 = pre_option_validator(2, 200);
2606        let v1_addr = v1.account;
2607        let v2_addr = v2.account;
2608        insert_legacy_stake_row(&persistence, 1, v1).await;
2609        insert_legacy_stake_row(&persistence, 2, v2).await;
2610
2611        let (loaded1, ..) = persistence
2612            .load_stake(EpochNumber::new(1))
2613            .await
2614            .unwrap()
2615            .unwrap();
2616        assert_eq!(loaded1.len(), 1);
2617        assert!(loaded1.get(&v1_addr).unwrap().stake_table_key.is_some());
2618
2619        let (loaded2, ..) = persistence
2620            .load_stake(EpochNumber::new(2))
2621            .await
2622            .unwrap()
2623            .unwrap();
2624        assert_eq!(loaded2.len(), 1);
2625        assert!(loaded2.get(&v2_addr).unwrap().stake_table_key.is_some());
2626
2627        let latest = persistence.load_latest_stake(10).await.unwrap().unwrap();
2628        assert_eq!(latest.len(), 2);
2629        let epochs: Vec<_> = latest.iter().map(|(e, ..)| *e).collect();
2630        assert!(epochs.contains(&EpochNumber::new(1)));
2631        assert!(epochs.contains(&EpochNumber::new(2)));
2632    }
2633
2634    #[test_log::test(tokio::test(flavor = "multi_thread"))]
2635    async fn test_load_stake_mixed_storage() {
2636        let tmp = Persistence::tmp_storage().await;
2637        let persistence = Persistence::connect(&tmp).await;
2638
2639        let legacy_v = pre_option_validator(3, 300);
2640        let legacy_addr = legacy_v.account;
2641        insert_legacy_stake_row(&persistence, 5, legacy_v).await;
2642
2643        let current_v = espresso_types::v0_3::AuthenticatedValidator::mock();
2644        let current_addr = current_v.account;
2645        let mut current_map = IndexMap::new();
2646        current_map.insert(current_addr, current_v);
2647        persistence
2648            .store_stake(EpochNumber::new(6), current_map, None, None)
2649            .await
2650            .unwrap();
2651
2652        let latest = persistence.load_latest_stake(10).await.unwrap().unwrap();
2653        assert_eq!(latest.len(), 2);
2654        let by_epoch: std::collections::HashMap<_, _> = latest
2655            .into_iter()
2656            .map(|(e, (map, _), _)| (e, map))
2657            .collect();
2658        assert!(
2659            by_epoch
2660                .get(&EpochNumber::new(5))
2661                .unwrap()
2662                .contains_key(&legacy_addr)
2663        );
2664        assert!(
2665            by_epoch
2666                .get(&EpochNumber::new(6))
2667                .unwrap()
2668                .contains_key(&current_addr)
2669        );
2670    }
2671
2672    #[test_log::test(tokio::test(flavor = "multi_thread"))]
2673    async fn test_store_all_validators_authenticated_and_unauthenticated() {
2674        use std::collections::HashMap;
2675
2676        use alloy::primitives::{Address, U256};
2677        use hotshot_types::light_client::StateVerKey;
2678        use indexmap::IndexMap;
2679
2680        let tmp = Persistence::tmp_storage().await;
2681        let storage = Persistence::connect(&tmp).await;
2682
2683        // Create an authenticated validator
2684        let authenticated_validator = RegisteredValidator {
2685            account: Address::random(),
2686            stake_table_key: Some(BLSPubKey::generated_from_seed_indexed([0u8; 32], 0).0),
2687            state_ver_key: Some(StateVerKey::default()),
2688            stake: U256::from(1000),
2689            commission: 100,
2690            delegators: HashMap::new(),
2691            authenticated: true,
2692            x25519_key: None,
2693            p2p_addr: None,
2694        };
2695
2696        // Create an unauthenticated validator
2697        let unauthenticated_validator = RegisteredValidator {
2698            account: Address::random(),
2699            stake_table_key: Some(BLSPubKey::generated_from_seed_indexed([0u8; 32], 1).0),
2700            state_ver_key: Some(StateVerKey::default()),
2701            stake: U256::from(2000),
2702            commission: 200,
2703            delegators: HashMap::new(),
2704            authenticated: false,
2705            x25519_key: None,
2706            p2p_addr: None,
2707        };
2708
2709        let mut validators: IndexMap<Address, RegisteredValidator<BLSPubKey>> = IndexMap::new();
2710        validators.insert(
2711            authenticated_validator.account,
2712            authenticated_validator.clone(),
2713        );
2714        validators.insert(
2715            unauthenticated_validator.account,
2716            unauthenticated_validator.clone(),
2717        );
2718
2719        // Store both validators
2720        storage
2721            .store_all_validators(EpochNumber::new(1), validators)
2722            .await
2723            .unwrap();
2724
2725        // Load and verify
2726        let loaded = storage
2727            .load_all_validators(EpochNumber::new(1), 0, 100)
2728            .await
2729            .unwrap();
2730        assert_eq!(loaded.len(), 2);
2731
2732        // Find each validator and verify authenticated state is preserved
2733        let loaded_auth = loaded
2734            .iter()
2735            .find(|v| v.account == authenticated_validator.account)
2736            .unwrap();
2737        assert!(
2738            loaded_auth.authenticated,
2739            "authenticated validator should remain authenticated"
2740        );
2741
2742        let loaded_unauth = loaded
2743            .iter()
2744            .find(|v| v.account == unauthenticated_validator.account)
2745            .unwrap();
2746        assert!(
2747            !loaded_unauth.authenticated,
2748            "unauthenticated validator should remain unauthenticated"
2749        );
2750    }
2751
2752    #[test_log::test(tokio::test(flavor = "multi_thread"))]
2753    async fn test_fetching_providers() {
2754        let tmp = Persistence::tmp_storage().await;
2755        let storage = Persistence::connect(&tmp).await;
2756
2757        // Mock up some data.
2758        let leaf = Leaf2::genesis(
2759            &ValidatedState::default(),
2760            &NodeState::mock(),
2761            TEST_VERSIONS.test.base,
2762        )
2763        .await;
2764        let leaf_payload = leaf.block_payload().unwrap();
2765        let leaf_payload_bytes_arc = leaf_payload.encode();
2766
2767        let avidm_param = init_avidm_param(2).unwrap();
2768        let weights = vec![1u32; 2];
2769
2770        let ns_table = parse_ns_table(
2771            leaf_payload.byte_len().as_usize(),
2772            &leaf_payload.ns_table().encode(),
2773        );
2774        let (payload_commitment, shares) =
2775            AvidMScheme::ns_disperse(&avidm_param, &weights, &leaf_payload_bytes_arc, ns_table)
2776                .unwrap();
2777        let (pubkey, privkey) = BLSPubKey::generated_from_seed_indexed([0; 32], 1);
2778        let vid_share = convert_proposal(
2779            AvidMDisperseShare::<SeqTypes> {
2780                view_number: ViewNumber::new(0),
2781                payload_commitment,
2782                share: shares[0].clone(),
2783                recipient_key: pubkey,
2784                epoch: None,
2785                target_epoch: None,
2786                common: avidm_param.clone(),
2787            }
2788            .to_proposal(&privkey)
2789            .unwrap()
2790            .clone(),
2791        );
2792
2793        let quorum_proposal = QuorumProposalWrapper::<SeqTypes> {
2794            proposal: QuorumProposal2::<SeqTypes> {
2795                block_header: leaf.block_header().clone(),
2796                view_number: leaf.view_number(),
2797                justify_qc: leaf.justify_qc().clone(),
2798                upgrade_certificate: None,
2799                view_change_evidence: None,
2800                next_drb_result: None,
2801                next_epoch_justify_qc: None,
2802                epoch: None,
2803                state_cert: None,
2804            },
2805        };
2806        let quorum_proposal_signature =
2807            BLSPubKey::sign(&privkey, &bincode::serialize(&quorum_proposal).unwrap())
2808                .expect("Failed to sign quorum proposal");
2809        let quorum_proposal = Proposal {
2810            data: quorum_proposal,
2811            signature: quorum_proposal_signature,
2812            _pd: Default::default(),
2813        };
2814
2815        let block_payload_signature = BLSPubKey::sign(&privkey, &leaf_payload_bytes_arc)
2816            .expect("Failed to sign block payload");
2817        let da_proposal = Proposal {
2818            data: DaProposal2::<SeqTypes> {
2819                encoded_transactions: leaf_payload_bytes_arc,
2820                metadata: leaf_payload.ns_table().clone(),
2821                view_number: ViewNumber::new(0),
2822                epoch: None,
2823                epoch_transition_indicator: EpochTransitionIndicator::NotInTransition,
2824            },
2825            signature: block_payload_signature,
2826            _pd: Default::default(),
2827        };
2828
2829        let mut next_quorum_proposal = quorum_proposal.clone();
2830        next_quorum_proposal.data.proposal.view_number += 1;
2831        next_quorum_proposal.data.proposal.justify_qc.view_number += 1;
2832        next_quorum_proposal
2833            .data
2834            .proposal
2835            .justify_qc
2836            .data
2837            .leaf_commit = Committable::commit(&leaf.clone());
2838
2839        // Add to database.
2840        storage
2841            .append_da2(&da_proposal, VidCommitment::V1(payload_commitment))
2842            .await
2843            .unwrap();
2844        storage.append_vid(&vid_share).await.unwrap();
2845        storage
2846            .append_quorum_proposal2(&quorum_proposal)
2847            .await
2848            .unwrap();
2849
2850        // Add an extra quorum proposal so we have a QC pointing back at `leaf`.
2851        storage
2852            .append_quorum_proposal2(&next_quorum_proposal)
2853            .await
2854            .unwrap();
2855
2856        // Fetch it as if we were rebuilding an archive.
2857        assert_eq!(
2858            Some(VidCommon::V1(avidm_param)),
2859            storage
2860                .fetch(VidCommonRequest(vid_share.data.payload_commitment()))
2861                .await
2862        );
2863        assert_eq!(
2864            leaf_payload,
2865            storage
2866                .fetch(PayloadRequest(vid_share.data.payload_commitment()))
2867                .await
2868                .unwrap()
2869        );
2870    }
2871
2872    /// Test conditions that trigger pruning.
2873    ///
2874    /// This is a configurable test that can be used to test different configurations of GC,
2875    /// `pruning_opt`. The test populates the database with some data for view 1, asserts that it is
2876    /// retained for view 2, and then asserts that it is pruned by view 3. There are various
2877    /// different configurations that can achieve this behavior, such that the data is retained and
2878    /// then pruned due to different logic and code paths.
2879    async fn test_pruning_helper(pruning_opt: ConsensusPruningOptions) {
2880        let tmp = Persistence::tmp_storage().await;
2881        let mut opt = Persistence::options(&tmp);
2882        opt.consensus_pruning = pruning_opt;
2883        let storage = opt.create().await.unwrap();
2884
2885        let data_view = ViewNumber::new(1);
2886
2887        // Populate some data.
2888        let leaf = Leaf2::genesis(
2889            &ValidatedState::default(),
2890            &NodeState::mock(),
2891            TEST_VERSIONS.test.base,
2892        )
2893        .await;
2894        let leaf_payload = leaf.block_payload().unwrap();
2895        let leaf_payload_bytes_arc = leaf_payload.encode();
2896
2897        let avidm_param = init_avidm_param(2).unwrap();
2898        let weights = vec![1u32; 2];
2899
2900        let ns_table = parse_ns_table(
2901            leaf_payload.byte_len().as_usize(),
2902            &leaf_payload.ns_table().encode(),
2903        );
2904        let (payload_commitment, shares) =
2905            AvidMScheme::ns_disperse(&avidm_param, &weights, &leaf_payload_bytes_arc, ns_table)
2906                .unwrap();
2907
2908        let (pubkey, privkey) = BLSPubKey::generated_from_seed_indexed([0; 32], 1);
2909        let vid = convert_proposal(
2910            AvidMDisperseShare::<SeqTypes> {
2911                view_number: data_view,
2912                payload_commitment,
2913                share: shares[0].clone(),
2914                recipient_key: pubkey,
2915                epoch: None,
2916                target_epoch: None,
2917                common: avidm_param,
2918            }
2919            .to_proposal(&privkey)
2920            .unwrap()
2921            .clone(),
2922        );
2923        let quorum_proposal = QuorumProposalWrapper::<SeqTypes> {
2924            proposal: QuorumProposal2::<SeqTypes> {
2925                epoch: None,
2926                block_header: leaf.block_header().clone(),
2927                view_number: data_view,
2928                justify_qc: QuorumCertificate2::genesis(
2929                    &ValidatedState::default(),
2930                    &NodeState::mock(),
2931                    TEST_VERSIONS.test,
2932                )
2933                .await,
2934                upgrade_certificate: None,
2935                view_change_evidence: None,
2936                next_drb_result: None,
2937                next_epoch_justify_qc: None,
2938                state_cert: None,
2939            },
2940        };
2941        let quorum_proposal_signature =
2942            BLSPubKey::sign(&privkey, &bincode::serialize(&quorum_proposal).unwrap())
2943                .expect("Failed to sign quorum proposal");
2944        let quorum_proposal = Proposal {
2945            data: quorum_proposal,
2946            signature: quorum_proposal_signature,
2947            _pd: Default::default(),
2948        };
2949
2950        let block_payload_signature = BLSPubKey::sign(&privkey, &leaf_payload_bytes_arc)
2951            .expect("Failed to sign block payload");
2952        let da_proposal = Proposal {
2953            data: DaProposal2::<SeqTypes> {
2954                encoded_transactions: leaf_payload_bytes_arc.clone(),
2955                metadata: leaf_payload.ns_table().clone(),
2956                view_number: data_view,
2957                epoch: Some(EpochNumber::new(0)),
2958                epoch_transition_indicator: EpochTransitionIndicator::NotInTransition,
2959            },
2960            signature: block_payload_signature,
2961            _pd: Default::default(),
2962        };
2963
2964        tracing::info!(?vid, ?da_proposal, ?quorum_proposal, "append data");
2965        storage.append_vid(&vid).await.unwrap();
2966        storage
2967            .append_da2(&da_proposal, VidCommitment::V1(payload_commitment))
2968            .await
2969            .unwrap();
2970        storage
2971            .append_quorum_proposal2(&quorum_proposal)
2972            .await
2973            .unwrap();
2974
2975        // Populate the view-indexed cert tables. Contents are opaque to the pruner (it deletes by
2976        // `view`), so raw bytes suffice.
2977        {
2978            let mut tx = storage.db.write().await.unwrap();
2979            tx.upsert(
2980                "state_cert",
2981                ["view", "state_cert"],
2982                ["view"],
2983                [(data_view.u64() as i64, b"state_cert".to_vec())],
2984            )
2985            .await
2986            .unwrap();
2987            tx.upsert(
2988                "decided_cert2",
2989                ["view", "data"],
2990                ["view"],
2991                [(data_view.u64() as i64, b"cert2".to_vec())],
2992            )
2993            .await
2994            .unwrap();
2995            tx.commit().await.unwrap();
2996        }
2997
2998        // The first decide doesn't trigger any garbage collection, even though our usage exceeds
2999        // the target, because of the minimum retention.
3000        tracing::info!("decide view 1");
3001        storage
3002            .append_decided_leaves(data_view + 1, [], &NullEventConsumer)
3003            .await
3004            .unwrap();
3005        assert_eq!(
3006            storage.load_vid_share(data_view).await.unwrap().unwrap(),
3007            vid
3008        );
3009        assert_eq!(
3010            storage.load_da_proposal(data_view).await.unwrap().unwrap(),
3011            da_proposal
3012        );
3013        assert_eq!(
3014            storage.load_quorum_proposal(data_view).await.unwrap(),
3015            quorum_proposal
3016        );
3017        assert!(view_row_exists(&storage, "state_cert", data_view).await);
3018        assert!(view_row_exists(&storage, "decided_cert2", data_view).await);
3019
3020        // After another view, our data is beyond the minimum retention (though not the target
3021        // retention) so it gets pruned.
3022        tracing::info!("decide view 2");
3023        storage
3024            .append_decided_leaves(data_view + 2, [], &NullEventConsumer)
3025            .await
3026            .unwrap();
3027        assert!(storage.load_vid_share(data_view).await.unwrap().is_none(),);
3028        assert!(storage.load_da_proposal(data_view).await.unwrap().is_none());
3029        storage.load_quorum_proposal(data_view).await.unwrap_err();
3030        assert!(!view_row_exists(&storage, "state_cert", data_view).await);
3031        assert!(!view_row_exists(&storage, "decided_cert2", data_view).await);
3032    }
3033
3034    /// Whether a view-indexed consensus table has a row at `view`.
3035    async fn view_row_exists(storage: &Persistence, table: &str, view: ViewNumber) -> bool {
3036        storage
3037            .db
3038            .read()
3039            .await
3040            .unwrap()
3041            .fetch_optional(
3042                query(&format!("SELECT view FROM {table} WHERE view = $1")).bind(view.u64() as i64),
3043            )
3044            .await
3045            .unwrap()
3046            .is_some()
3047    }
3048
3049    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3050    async fn test_pruning_minimum_retention() {
3051        test_pruning_helper(ConsensusPruningOptions {
3052            // Use a very low target usage, to show that we still retain data up to the minimum
3053            // retention even when usage is above target.
3054            target_usage: 0,
3055            minimum_retention: 1,
3056            // Use a very high target retention, so that pruning is only triggered by the minimum
3057            // retention.
3058            target_retention: u64::MAX,
3059            usage_check_interval: Duration::ZERO,
3060        })
3061        .await
3062    }
3063
3064    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3065    async fn test_pruning_target_retention() {
3066        test_pruning_helper(ConsensusPruningOptions {
3067            target_retention: 1,
3068            // Use a very low minimum retention, so that data is only kept around due to the target
3069            // retention.
3070            minimum_retention: 0,
3071            // Use a very high target usage, so that pruning is only triggered by the target
3072            // retention.
3073            target_usage: u64::MAX,
3074            usage_check_interval: Duration::ZERO,
3075        })
3076        .await
3077    }
3078
3079    /// Regression test for an ambiguous behavior in `store_events`/`load_events`.
3080    ///
3081    /// Previously, `store_events` did nothing when given an empty events list (in fact,
3082    /// `fetch_and_store_stake_table_events` was not even calling it). But this means that the
3083    /// `stake_table_events_l1_block` column does not get updated when we enter a new epoch with no
3084    /// new stake table events. This makes it impossible to distinguish between two very different
3085    /// scenarios:
3086    ///
3087    /// 1. The node has successfully processed events through the latest L1 finalized block, but
3088    ///    there are no new events from the last epoch.
3089    /// 2. The node is lagging behind the latest L1 finalized block, and is possibly missing some
3090    ///    new events.
3091    ///
3092    /// In scenario 1, clients of this node should be able to treat the empty list of stake table
3093    /// events as authoritative, and derive the stake table for the next epoch (which will end up
3094    /// being the same as the previous one. However, in scenario 2, clients need to wait, because we
3095    /// don't yet know whether there could be any events that modify the stake table. Thus,
3096    /// distinguishing these two scenarios is important.
3097    ///
3098    /// This regression test ensures that even if there are no new events, at least the
3099    /// `stake_table_events_l1_block` column gets updated. We can then distinguish the two scenarios
3100    /// using the `EventsPersistenceRead`` return value from load_events.
3101    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3102    async fn test_store_events_empty() {
3103        let tmp = Persistence::tmp_storage().await;
3104        let mut opt = Persistence::options(&tmp);
3105        let storage = opt.create().await.unwrap();
3106
3107        assert_eq!(storage.load_events(0, 100).await.unwrap(), (None, vec![]));
3108
3109        // Storing an empty events list still updates the latest L1 block.
3110        for i in 1..=2 {
3111            tracing::info!(i, "update l1 height");
3112            storage.store_events(i, vec![]).await.unwrap();
3113            assert_eq!(
3114                storage.load_events(0, 100).await.unwrap(),
3115                (Some(EventsPersistenceRead::UntilL1Block(i)), vec![])
3116            );
3117        }
3118    }
3119
3120    /// A gap-fill decide reports the older view it filled, after a newer decide already moved
3121    /// the cursor past it.
3122    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3123    async fn test_consensus_only_decide_never_rewinds_cursor() {
3124        let tmp = Persistence::tmp_storage().await;
3125        let mut opt = Persistence::options(&tmp);
3126        opt.set_consensus_only();
3127        let storage = opt.create().await.unwrap();
3128
3129        for view in [10, 5] {
3130            storage
3131                .append_decided_leaves(ViewNumber::new(view), [], &NullEventConsumer)
3132                .await
3133                .unwrap();
3134        }
3135        assert_eq!(
3136            storage.load_processed_view().await.unwrap(),
3137            Some(ViewNumber::new(10))
3138        );
3139    }
3140}