Skip to main content

espresso_node/api/
sql.rs

1use std::{
2    collections::{HashMap, HashSet, VecDeque},
3    sync::Arc,
4};
5
6use anyhow::{Context, bail, ensure};
7use async_trait::async_trait;
8use committable::{Commitment, Committable};
9use espresso_types::{
10    BlockMerkleCommitment, BlockMerkleTree, ChainConfig, FeeAccount, FeeMerkleCommitment,
11    FeeMerkleTree, Leaf2, NodeState, ValidatedState, get_l1_deposits,
12    v0::traits::PersistenceOptions as _,
13    v0_1::IterableFeeInfo,
14    v0_3::{
15        REWARD_MERKLE_TREE_V1_HEIGHT, RewardAccountProofV1, RewardAccountQueryDataV1,
16        RewardAccountV1, RewardMerkleTreeV1,
17    },
18    v0_4::{PermittedRewardMerkleTreeV2, RewardAccountV2, RewardMerkleTreeV2},
19    v0_6::RewardAccountQueryDataV2,
20};
21use futures::future::Future;
22use hotshot_query_service::{
23    Resolvable,
24    availability::LeafId,
25    data_source::{
26        VersionedDataSource,
27        storage::{
28            AvailabilityStorage, MerklizedStateStorage, NodeStorage,
29            pruning::{PrunerCfg, PrunerConfig},
30        },
31    },
32    merklized_state::Snapshot,
33    status::HasMetrics as _,
34};
35use hotshot_types::{
36    data::{EpochNumber, QuorumProposalWrapper, ViewNumber},
37    message::Proposal,
38    traits::{ValidatedState as _, election::MembershipSnapshot, metrics::Metrics},
39    utils::{epoch_from_block_number, is_last_block},
40    vote::HasViewNumber,
41};
42use jf_merkle_tree_compat::{
43    ForgetableMerkleTreeScheme, ForgetableUniversalMerkleTreeScheme, LookupResult,
44    MerkleTreeScheme, prelude::MerkleNode,
45};
46use light_client::storage::LightClientDbOptions;
47use process_metrics::ProcessMetrics;
48use sqlx::{Encode, Row, Type};
49use tokio::time::sleep;
50use vbs::version::Version;
51use versions::{
52    DRB_AND_HEADER_UPGRADE_VERSION, EPOCH_REWARD_VERSION, EPOCH_VERSION, NEW_PROTOCOL_VERSION,
53};
54
55use super::{
56    persistence::{BackendOptions as _, Options},
57    qs::{
58        Backend as _, Config, Db, DbKind, SqlDataSource, SqlStorage, Transaction, TransactionMode,
59        Write, query_as,
60    },
61};
62use crate::{
63    SeqTypes,
64    api::{
65        ApiState, BlocksFrontier, LightClientProvider, RewardMerkleTreeDataSource,
66        RewardMerkleTreeV2Data,
67        data_source::{DataSourceOptions, NodeStateDataSource as _, Provider, SequencerDataSource},
68        options::{self, Query, get_public_env_vars, init_query_data_source},
69        state::NodeApiStateImpl,
70        update::{ApiEventConsumer, ApiSink},
71    },
72    catchup::{CatchupStorage, NullStateCatchup},
73    context::TaskList,
74    persistence::{ChainConfigPersistence, sql::Options as SqlOptions},
75    request_response::data_source::Storage as RequestResponseStorage,
76    state::{EmptyStateStart, compute_state_update, update_state_storage_loop},
77    util::BoundedJoinSet,
78};
79
80pub type DataSource = SqlDataSource<SeqTypes, Provider>;
81
82impl DataSourceOptions for Options {
83    type DataSource = DataSource;
84
85    fn enable_query_module(&self, opt: options::Options, query: Query) -> options::Options {
86        opt.query_sql(query, self.clone().into_inner())
87    }
88
89    fn light_client_db(&self, query: &Query) -> anyhow::Result<LightClientDbOptions> {
90        Ok(Db::light_client_db(&Config::try_from(&**self)?, query))
91    }
92}
93
94#[async_trait]
95impl SequencerDataSource for DataSource {
96    type Options = Options;
97
98    async fn create(opt: Self::Options, provider: Provider, reset: bool) -> anyhow::Result<Self> {
99        let fetch_limit = opt.fetch_rate_limit;
100        let active_fetch_delay = opt.active_fetch_delay;
101        let chunk_fetch_delay = opt.chunk_fetch_delay;
102        let mut cfg = Config::try_from(&*opt)?;
103
104        if reset {
105            cfg = cfg.reset_schema();
106        }
107
108        let mut builder = cfg.builder(provider).await?;
109
110        if let Some(limit) = fetch_limit {
111            builder = builder.with_rate_limit(limit);
112        }
113
114        if opt.lightweight {
115            tracing::warn!("enabling light weight mode..");
116            builder = builder.leaf_only();
117        }
118
119        // Transaction and payload totals count from genesis, and a pruned node never has those
120        // blocks.
121        if opt.prune {
122            builder = builder.disable_aggregator();
123        }
124
125        if let Some(delay) = active_fetch_delay {
126            builder = builder.with_active_fetch_delay(delay);
127        }
128        if let Some(delay) = chunk_fetch_delay {
129            builder = builder.with_chunk_fetch_delay(delay);
130        }
131        if let Some(chunk_size) = opt.sync_status_chunk_size {
132            builder = builder.with_sync_status_chunk_size(chunk_size);
133        }
134        if let Some(ttl) = opt.sync_status_ttl {
135            builder = builder.with_sync_status_ttl(ttl);
136        }
137        if let Some(chunk_size) = opt.proactive_scan_chunk_size {
138            builder = builder.with_proactive_range_chunk_size(chunk_size);
139        }
140        if let Some(timeout) = opt.proactive_fetch_timeout {
141            builder = builder.with_proactive_fetch_timeout(timeout);
142        }
143        if let Some(interval) = opt.proactive_scan_interval {
144            builder = builder.with_proactive_interval(interval);
145        }
146        if opt.disable_proactive_fetching {
147            builder = builder.disable_proactive_fetching();
148        }
149
150        builder.build().await
151    }
152}
153
154/// Start the query module of `api` over this backend, returning what consensus needs from it.
155pub(in crate::api) async fn init_query_module(
156    api: &options::Options,
157    query_opt: Query,
158    mod_opt: Options,
159    state: ApiState,
160    tasks: &mut TaskList,
161) -> anyhow::Result<(
162    Box<dyn Metrics>,
163    Box<dyn ApiSink>,
164    Option<RequestResponseStorage>,
165)> {
166    let mut provider = Provider::default();
167
168    // Use the database itself as a fetching provider: sometimes we can fetch data that is
169    // missing from the query service from ephemeral consensus storage.
170    let db_provider = mod_opt.clone().create().await?;
171    provider = provider
172        .with_block_provider(db_provider.clone())
173        .with_vid_common_provider(db_provider);
174    // If that fails, fetch missing data from peers.
175    let light_client_db = mod_opt.light_client_db(&query_opt)?;
176    let light_client = Arc::new(LightClientProvider::new(
177        query_opt.peers,
178        state.clone(),
179        query_opt.light_client,
180        light_client_db,
181    )?);
182    provider = provider.with_provider(Arc::clone(&light_client));
183
184    let ranges_concurrency = mod_opt.ranges_concurrency;
185    let ds = DataSource::create(mod_opt.clone(), provider, false).await?;
186    let inner_storage = ds.inner();
187    tasks.spawn("process_metrics", ProcessMetrics::new(ds.metrics()).run());
188    let (metrics, ds) = init_query_data_source(ds, state.clone());
189
190    let get_node_state = {
191        let state = state.clone();
192        async move { state.node_state().await }
193    };
194    let empty_state_start = if mod_opt.prune {
195        EmptyStateStart::RecentBoundary
196    } else {
197        EmptyStateStart::Genesis
198    };
199    tasks.spawn(
200        "merklized state storage update loop",
201        update_state_storage_loop(ds.clone(), get_node_state, light_client, empty_state_start),
202    );
203
204    // Archive mode disables the pruner, so nothing else bounds the merklized state tables.
205    if mod_opt.archive && !mod_opt.archive_full_state {
206        let get_node_state = {
207            let state = state.clone();
208            async move { state.node_state().await }
209        };
210        tasks.spawn(
211            "archive state garbage collector",
212            ArchiveStateGc::new(&mod_opt).run(inner_storage.clone(), get_node_state),
213        );
214    }
215
216    let port = api.http.port;
217    let env_vars = get_public_env_vars().unwrap_or_default();
218    let node_cfg = api.public_node_config.as_deref().cloned();
219    let modules = espresso_api::OptionalModules {
220        submit: api.submit.is_some(),
221        config: api.config.is_some(),
222        explorer: api.explorer.is_some(),
223        ..Default::default()
224    };
225    let max_connections = api.http.max_connections;
226    // Both transports serve the same state; cloning shares the env vars and node config
227    // rather than copying the genesis they embed.
228    let mut api_state = NodeApiStateImpl::new(ds.clone())
229        .with_env_vars(env_vars)
230        .with_public_node_config(node_cfg);
231    if let Some(ranges_concurrency) = ranges_concurrency {
232        api_state = api_state.with_ranges_concurrency(ranges_concurrency);
233    }
234    let tonic_state = api_state.clone();
235    tasks.spawn("API server", async move {
236        if let Err(e) = espresso_api::serve_axum(port, api_state, modules, max_connections).await {
237            tracing::error!("Axum server error: {}", e);
238        }
239        anyhow::Ok(())
240    });
241
242    if let Some(tonic_port) = api.http.tonic_port {
243        tasks.spawn("Tonic gRPC server", async move {
244            if let Err(e) = espresso_api::serve_tonic(tonic_port, tonic_state, modules).await {
245                tracing::error!("Tonic gRPC server error: {}", e);
246            }
247        });
248    }
249
250    Ok((
251        metrics,
252        Box::new(ApiEventConsumer::from(ds)),
253        Some(RequestResponseStorage::Sql(inner_storage)),
254    ))
255}
256
257/// Prune archive state older than the light client contract's retained history. Block merkle paths
258/// below the marker are rebuilt on request from newer snapshots; fee and reward state is gone.
259///
260/// TODO: the `hash` rows these deletes orphan are never collected.
261#[derive(Clone, Debug)]
262pub(crate) struct ArchiveStateGc {
263    /// Heights retained regardless of the light client.
264    min_retention: u64,
265    cfg: PrunerCfg,
266}
267
268impl ArchiveStateGc {
269    pub(crate) fn new(opt: &SqlOptions) -> Self {
270        Self {
271            min_retention: opt.archive_state_min_retention,
272            cfg: PrunerCfg::from(opt.pruning),
273        }
274    }
275
276    pub(crate) async fn run(
277        self,
278        storage: Arc<SqlStorage>,
279        instance: impl Future<Output = NodeState>,
280    ) {
281        let node_state = instance.await;
282        loop {
283            sleep(self.cfg.interval()).await;
284            if let Err(err) = self.collect(&storage, &node_state).await {
285                tracing::warn!(
286                    err = %format!("{err:#}"),
287                    "archive state garbage collection failed"
288                );
289            }
290        }
291    }
292
293    async fn collect(&self, storage: &SqlStorage, node_state: &NodeState) -> anyhow::Result<()> {
294        let Some(history_start) = node_state
295            .light_client_history_start()
296            .await
297            .context("reading the oldest light client root")?
298        else {
299            tracing::info!("light client reports no retained history; retaining all state");
300            return Ok(());
301        };
302        storage
303            .prune_state_below(history_start, self.min_retention, &self.cfg)
304            .await
305    }
306}
307
308impl RewardMerkleTreeDataSource for SqlStorage {
309    async fn load_v1_reward_account_proof(
310        &self,
311        height: u64,
312        account: RewardAccountV1,
313    ) -> anyhow::Result<RewardAccountQueryDataV1> {
314        let mut tx = self.read().await.context(format!(
315            "opening transaction to fetch v1 reward account {account:?}; height {height}"
316        ))?;
317
318        let block_height = NodeStorage::<SeqTypes>::block_height(&mut tx)
319            .await
320            .context("getting block height")? as u64;
321        ensure!(
322            block_height > 0,
323            "cannot get accounts for height {height}: no blocks available"
324        );
325
326        // Check if we have the desired state snapshot. If so, we can load the desired accounts
327        // directly.
328        if height < block_height {
329            let (tree, _) = load_v1_reward_accounts(self, height, &[account])
330                .await
331                .with_context(|| {
332                    format!("failed to load v1 reward account {account:?} at height {height}")
333                })?;
334
335            let (proof, balance) = RewardAccountProofV1::prove(&tree, account.into())
336                .with_context(|| {
337                    format!("reward account {account:?} not available at height {height}")
338                })?;
339
340            Ok(RewardAccountQueryDataV1 { balance, proof })
341        } else {
342            bail!(
343                "requested height {height} is not yet available (latest block height: \
344                 {block_height})"
345            );
346        }
347    }
348
349    fn persist_tree(
350        &self,
351        height: u64,
352        merkle_tree: Vec<u8>,
353    ) -> impl Send + Future<Output = anyhow::Result<()>> {
354        async move {
355            let mut tx = self
356                .write()
357                .await
358                .context("opening transaction for reward merkle tree v2")?;
359
360            tx.upsert(
361                "reward_merkle_tree_v2_data",
362                ["height", "balances"],
363                ["height"],
364                [(height as i64, merkle_tree)],
365            )
366            .await?;
367
368            hotshot_query_service::data_source::Transaction::commit(tx)
369                .await
370                .context("Transaction to store reward merkle tree v2 failed.")
371        }
372    }
373
374    fn load_tree(&self, height: u64) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>> {
375        async move {
376            let mut tx = self
377                .read()
378                .await
379                .context("opening transaction for state update")?;
380
381            let row = sqlx::query(
382                r#"
383                SELECT balances
384                FROM reward_merkle_tree_v2_data
385                WHERE height = $1
386                "#,
387            )
388            .bind(height as i64)
389            .fetch_optional(tx.as_mut())
390            .await?
391            .context(format!(
392                "No reward merkle tree for height {} in storage",
393                height
394            ))?;
395
396            row.try_get::<Vec<u8>, _>("balances")
397                .context("Missing field balances from row; this should never happen")
398        }
399    }
400
401    fn load_latest_tree(
402        &self,
403        height: u64,
404    ) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>> {
405        async move {
406            let mut tx = self
407                .read()
408                .await
409                .context("opening transaction for load_latest_tree")?;
410
411            let row = sqlx::query(
412                r#"
413                SELECT balances
414                FROM reward_merkle_tree_v2_data
415                WHERE height <= $1
416                ORDER BY height DESC
417                LIMIT 1
418                "#,
419            )
420            .bind(height as i64)
421            .fetch_optional(tx.as_mut())
422            .await?
423            .context(format!(
424                "No reward merkle tree at or below height {} in storage",
425                height
426            ))?;
427
428            row.try_get::<Vec<u8>, _>("balances")
429                .context("Missing field balances from row; this should never happen")
430        }
431    }
432
433    fn persist_proofs(
434        &self,
435        height: u64,
436        proofs: impl Iterator<Item = (Vec<u8>, Vec<u8>)> + Send,
437    ) -> impl Send + Future<Output = anyhow::Result<()>> {
438        async move {
439            let mut iter = proofs.map(|(account, proof)| (height as i64, account, proof));
440
441            loop {
442                let mut chunk = Vec::with_capacity(20);
443
444                for _ in 0..20 {
445                    let Some(row) = iter.next() else {
446                        continue;
447                    };
448                    chunk.push(row);
449                }
450
451                if chunk.is_empty() {
452                    break;
453                }
454
455                let mut tx = self
456                    .write()
457                    .await
458                    .context("opening transaction for state update")?;
459
460                tokio::spawn(async move {
461                    tx.upsert(
462                        "reward_merkle_tree_v2_proofs",
463                        ["height", "account", "proof"],
464                        ["height", "account"],
465                        chunk,
466                    )
467                    .await?;
468
469                    hotshot_query_service::data_source::Transaction::commit(tx)
470                        .await
471                        .context("Transaction to store reward merkle tree failed.")?;
472
473                    Ok::<_, anyhow::Error>(())
474                });
475            }
476            Ok(())
477        }
478    }
479
480    /// Load a reward proof for a given height and account.
481    ///
482    /// For V5+ (epoch rewards), if the requested height is not an epoch boundary,
483    /// resolves to the previous epoch's last block. Verifies the boundary block is
484    /// V5+ to handle V4→V5 upgrades.
485    /// For V4 (per-block rewards), proofs are stored at every block height.
486    fn load_proof(
487        &self,
488        height: u64,
489        account: Vec<u8>,
490        epoch_height: u64,
491    ) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>> {
492        async move {
493            let mut tx = self
494                .read()
495                .await
496                .context("opening transaction for load_proof")?;
497
498            let leaf = tx
499                .get_leaf(LeafId::<SeqTypes>::from(height as usize))
500                .await
501                .context(format!("leaf {height} not available"))?;
502
503            // For V5+, resolve to the last epoch boundary where proofs were stored.
504            // For V4, proofs exist at every height.
505            let proof_height = if leaf.header().version() < EPOCH_REWARD_VERSION
506                || is_last_block(height, epoch_height)
507            {
508                height
509            } else {
510                let prev_epoch_last_block =
511                    epoch_from_block_number(height, epoch_height).saturating_sub(1) * epoch_height;
512                let boundary_leaf = tx
513                    .get_leaf(LeafId::<SeqTypes>::from(prev_epoch_last_block as usize))
514                    .await
515                    .context(format!(
516                        "leaf at epoch boundary {prev_epoch_last_block} not available"
517                    ))?;
518                ensure!(
519                    boundary_leaf.header().version() >= EPOCH_REWARD_VERSION,
520                    "no epoch reward proofs available at boundary {prev_epoch_last_block}"
521                );
522                prev_epoch_last_block
523            };
524
525            sqlx::query_scalar(
526                "SELECT proof FROM reward_merkle_tree_v2_proofs WHERE height = $1 AND account = $2",
527            )
528            .bind(proof_height as i64)
529            .bind(account)
530            .fetch_optional(tx.as_mut())
531            .await?
532            .context(format!(
533                "Missing proofs at height {proof_height} (resolved from {height})"
534            ))
535        }
536    }
537
538    fn load_latest_proof(
539        &self,
540        account: Vec<u8>,
541    ) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>> {
542        async move {
543            let mut tx = self
544                .read()
545                .await
546                .context("opening transaction for state update")?;
547
548            let row = sqlx::query(
549                r#"
550                SELECT proof
551                FROM reward_merkle_tree_v2_proofs
552                WHERE account = $1
553                ORDER BY height DESC
554                LIMIT 1
555                "#,
556            )
557            .bind(account)
558            .fetch_optional(tx.as_mut())
559            .await?
560            .context("Missing proofs")?;
561
562            row.try_get::<Vec<u8>, _>("proof")
563                .context("Missing field proof from row; this should never happen")
564        }
565    }
566
567    fn garbage_collect(&self, height: u64) -> impl Send + Future<Output = anyhow::Result<()>> {
568        async move {
569            let batch_size = self.get_pruning_config().unwrap_or_default().batch_size();
570
571            // Postgres supports improved performance by using `FOR UPDATE` in a sub-select to
572            // acquire exclusive locks on visited rows, which are later deleted. SQLite does not
573            // support this syntax.
574            let for_update = match Db::KIND {
575                DbKind::Sqlite => "",
576                DbKind::Postgres => "FOR UPDATE",
577            };
578
579            // Delete batches from the merkle data table until there is nothing left to delete.
580            loop {
581                let mut tx = self
582                    .write()
583                    .await
584                    .context("opening transaction for state update")?;
585
586                let res = sqlx::query(&format!(
587                    "
588                    WITH delete_batch AS (
589                        SELECT d.height FROM reward_merkle_tree_v2_data AS d
590                            WHERE d.height < $1
591                            ORDER BY d.height DESC
592                            LIMIT $2
593                            {for_update}
594                    )
595                    DELETE FROM reward_merkle_tree_v2_data AS del
596                    WHERE del.height IN (SELECT * FROM delete_batch)
597                "
598                ))
599                .bind(height as i64)
600                .bind(batch_size as i64)
601                .execute(tx.as_mut())
602                .await?;
603
604                hotshot_query_service::data_source::Transaction::commit(tx)
605                    .await
606                    .context(
607                        "Transaction to garbage collect reward merkle trees from storage failed.",
608                    )?;
609
610                if res.rows_affected() == 0 {
611                    break;
612                } else {
613                    tracing::debug!(
614                        "deleted {} rows from reward_merkle_tree_v2_data",
615                        res.rows_affected()
616                    );
617                }
618            }
619
620            // Delete batches from the proofs table until there is nothing left to delete.
621            loop {
622                let mut tx = self
623                    .write()
624                    .await
625                    .context("opening transaction for state update")?;
626
627                let res = sqlx::query(&format!(
628                    "
629                    WITH delete_batch AS (
630                        SELECT d.height, d.account FROM reward_merkle_tree_v2_proofs AS d
631                            WHERE d.height < $1
632                            ORDER BY d.height, d.account DESC
633                            LIMIT $2
634                            {for_update}
635                    )
636                    DELETE FROM reward_merkle_tree_v2_proofs AS del
637                    WHERE (del.height, del.account) IN (SELECT * FROM delete_batch)
638                ",
639                ))
640                .bind(height as i64)
641                .bind(batch_size as i64)
642                .execute(tx.as_mut())
643                .await?;
644
645                hotshot_query_service::data_source::Transaction::commit(tx)
646                    .await
647                    .context("Transaction to garbage collect proofs from storage failed.")?;
648
649                if res.rows_affected() == 0 {
650                    break;
651                } else {
652                    tracing::debug!(
653                        "deleted {} rows from reward_merkle_tree_v2_proofs",
654                        res.rows_affected()
655                    );
656                }
657            }
658
659            Ok(())
660        }
661    }
662
663    fn proof_exists(&self, height: u64) -> impl Send + Future<Output = bool> {
664        async move {
665            let Ok(mut tx) = self.write().await else {
666                return false;
667            };
668
669            sqlx::query_as(
670                r#"
671                SELECT EXISTS(
672                SELECT 1 FROM reward_merkle_tree_v2_proofs
673                WHERE height = $1
674                )
675                "#,
676            )
677            .bind(height as i64)
678            .fetch_one(tx.as_mut())
679            .await
680            .ok()
681            .unwrap_or((false,))
682            .0
683        }
684    }
685    /// Generate and persist reward proofs for the current L1-finalized height.
686    ///
687    /// For V5+ (epoch rewards), the reward tree is only stored at epoch boundaries.
688    /// We resolve to the nearest epoch boundary to load the tree, verify it's V5+
689    /// (for V4→V5 upgrades), and store the generated proofs at the same boundary height.
690    /// For V4 (per-block rewards), the tree exists at every block height.
691    fn persist_reward_proofs(
692        &self,
693        node_state: &NodeState,
694        height: u64,
695        version: Version,
696    ) -> impl Send + Future<Output = anyhow::Result<()>> {
697        async move {
698            // Only attempt to persist proofs every 30th block.
699            //
700            // In tests, we persist proofs at every block height so
701            // reward claim tests can query proofs at arbitrary heights. This can be
702            // removed once all reward claim tests are updated
703            if !cfg!(any(test, feature = "testing"))
704                && !(height + node_state.node_id).is_multiple_of(30)
705            {
706                return Ok(());
707            }
708
709            // In tests, we use the current block height directly instead
710            // of querying the L1 finalized HotShot height, because test environments don't
711            // deploy a light client contract
712            let finalized_hotshot_height = if cfg!(any(test, feature = "testing")) {
713                height
714            } else {
715                match node_state.finalized_hotshot_height().await {
716                    Ok(h) => h,
717                    Err(err) => {
718                        tracing::warn!("failed to get finalized hotshot height: {err:#}");
719                        return Ok(());
720                    },
721                }
722            };
723
724            // Resolve which height to load the reward tree from.
725            // V4: tree exists at every height. V5+: only at epoch boundaries.
726            let mut tree_height = finalized_hotshot_height;
727            let mut proof_height = finalized_hotshot_height;
728            if version >= EPOCH_REWARD_VERSION {
729                let epoch_height = node_state
730                    .epoch_height
731                    .context("epoch_height not set in node state")?;
732                if !is_last_block(finalized_hotshot_height, epoch_height) {
733                    tree_height = epoch_from_block_number(finalized_hotshot_height, epoch_height)
734                        .saturating_sub(1)
735                        * epoch_height;
736                    if tree_height == 0 {
737                        return Ok(());
738                    }
739                    // A loop replaying history below the boundary may not have its leaf yet, and a
740                    // failed read would fail the whole state update.
741                    if height < tree_height {
742                        return Ok(());
743                    }
744                    // During V4→V5 upgrades the previous epoch boundary may be V4. The data pruner
745                    // may have deleted the boundary leaf, in which case the tree load below decides.
746                    let mut tx = self.read().await.context("opening read transaction")?;
747                    if let Ok(leaf) = tx
748                        .get_leaf(LeafId::<SeqTypes>::from(tree_height as usize))
749                        .await
750                        && leaf.header().version() < EPOCH_REWARD_VERSION
751                    {
752                        return Ok(());
753                    }
754                }
755                proof_height = tree_height;
756            }
757
758            // The state loop stores the tree when it processes `tree_height`, so a loop replaying
759            // history below the L1-finalized height has nothing to load yet.
760            if height < tree_height {
761                return Ok(());
762            }
763
764            if self.proof_exists(proof_height).await {
765                return Ok(());
766            }
767
768            let permitted_tree = match self.load_reward_merkle_tree_v2(tree_height).await {
769                Ok(tree) => tree,
770                Err(err) => {
771                    tracing::warn!(tree_height, "failed to load reward merkle tree: {err:#}");
772                    return Ok(());
773                },
774            };
775
776            let tree = permitted_tree.tree;
777            let iter = tree
778                .iter()
779                .filter_map(|(account, balance): (&RewardAccountV2, _)| {
780                    let proof = espresso_types::v0_6::RewardAccountProofV2::prove(
781                        &tree,
782                        (*account).into(),
783                    )?;
784                    let proof = RewardAccountQueryDataV2 {
785                        balance: (*balance).into(),
786                        proof: proof.0,
787                    };
788                    Some((
789                        bincode::serialize(&account).ok()?,
790                        bincode::serialize(&proof).ok()?,
791                    ))
792                });
793
794            if let Err(err) = self.persist_proofs(proof_height, iter).await {
795                tracing::warn!(proof_height, "failed to persist proofs: {err:#}");
796            }
797
798            Ok(())
799        }
800    }
801}
802
803impl CatchupStorage for SqlStorage {
804    async fn get_reward_accounts_v1(
805        &self,
806        instance: &NodeState,
807        height: u64,
808        view: ViewNumber,
809        accounts: &[RewardAccountV1],
810    ) -> anyhow::Result<(RewardMerkleTreeV1, Leaf2)> {
811        let mut tx = self.read().await.context(format!(
812            "opening transaction to fetch v1 reward account {accounts:?}; height {height}"
813        ))?;
814
815        let block_height = NodeStorage::<SeqTypes>::block_height(&mut tx)
816            .await
817            .context("getting block height")? as u64;
818        ensure!(
819            block_height > 0,
820            "cannot get accounts for height {height}: no blocks available"
821        );
822
823        // Check if we have the desired state snapshot. If so, we can load the desired accounts
824        // directly.
825        if height < block_height {
826            load_v1_reward_accounts(self, height, accounts).await
827        } else {
828            let accounts: Vec<_> = accounts
829                .iter()
830                .map(|acct| RewardAccountV2::from(*acct))
831                .collect();
832            // If we do not have the exact snapshot we need, we can try going back to the last
833            // snapshot we _do_ have and replaying subsequent blocks to compute the desired state.
834            let (state, leaf) = reconstruct_state(
835                instance,
836                self,
837                &mut tx,
838                block_height - 1,
839                view,
840                &[],
841                &accounts,
842            )
843            .await?;
844            Ok((state.reward_merkle_tree_v1, leaf))
845        }
846    }
847
848    async fn get_reward_accounts_v2(
849        &self,
850        instance: &NodeState,
851        height: u64,
852        view: ViewNumber,
853        accounts: &[RewardAccountV2],
854    ) -> anyhow::Result<(RewardMerkleTreeV2, Leaf2)> {
855        let mut tx = self.read().await.context(format!(
856            "opening transaction to fetch reward account {accounts:?}; height {height}"
857        ))?;
858
859        let block_height = NodeStorage::<SeqTypes>::block_height(&mut tx)
860            .await
861            .context("getting block height")? as u64;
862        ensure!(
863            block_height > 0,
864            "cannot get accounts for height {height}: no blocks available"
865        );
866
867        // Check if we have the desired state snapshot. If so, we can load the desired accounts
868        // directly.
869        if height < block_height {
870            load_reward_merkle_tree_v2(self, height)
871                .await
872                .map(|(permitted_tree, leaf)| (permitted_tree.tree, leaf))
873        } else {
874            // If we do not have the exact snapshot we need, we can try going back to the last
875            // snapshot we _do_ have and replaying subsequent blocks to compute the desired state.
876            let (state, leaf) = reconstruct_state(
877                instance,
878                self,
879                &mut tx,
880                block_height - 1,
881                view,
882                &[],
883                accounts,
884            )
885            .await?;
886            Ok((state.reward_merkle_tree_v2, leaf))
887        }
888    }
889
890    async fn get_accounts(
891        &self,
892        instance: &NodeState,
893        height: u64,
894        view: ViewNumber,
895        accounts: &[FeeAccount],
896    ) -> anyhow::Result<(FeeMerkleTree, Leaf2)> {
897        let mut tx = self.read().await.context(format!(
898            "opening transaction to fetch account {accounts:?}; height {height}"
899        ))?;
900
901        let block_height = NodeStorage::<SeqTypes>::block_height(&mut tx)
902            .await
903            .context("getting block height")? as u64;
904        ensure!(
905            block_height > 0,
906            "cannot get accounts for height {height}: no blocks available"
907        );
908
909        // Check if we have the desired state snapshot. If so, we can load the desired accounts
910        // directly.
911        if height < block_height {
912            load_accounts(&mut tx, height, accounts).await
913        } else {
914            // If we do not have the exact snapshot we need, we can try going back to the last
915            // snapshot we _do_ have and replaying subsequent blocks to compute the desired state.
916            let (state, leaf) = reconstruct_state(
917                instance,
918                self,
919                &mut tx,
920                block_height - 1,
921                view,
922                accounts,
923                &[],
924            )
925            .await?;
926            Ok((state.fee_merkle_tree, leaf))
927        }
928    }
929
930    async fn get_accounts_at(
931        &self,
932        height: u64,
933        root: FeeMerkleCommitment,
934        accounts: &[FeeAccount],
935    ) -> anyhow::Result<FeeMerkleTree> {
936        let mut tx = self.read().await.context(format!(
937            "opening transaction to fetch account {accounts:?}; height {height}"
938        ))?;
939        load_accounts_at(&mut tx, height, root, accounts).await
940    }
941
942    async fn get_frontier(
943        &self,
944        instance: &NodeState,
945        height: u64,
946        view: ViewNumber,
947    ) -> anyhow::Result<BlocksFrontier> {
948        let mut tx = self.read().await.context(format!(
949            "opening transaction to fetch frontier at height {height}"
950        ))?;
951
952        let block_height = NodeStorage::<SeqTypes>::block_height(&mut tx)
953            .await
954            .context("getting block height")? as u64;
955        ensure!(
956            block_height > 0,
957            "cannot get frontier for height {height}: no blocks available"
958        );
959
960        // Check if we have the desired state snapshot. If so, we can load the desired frontier
961        // directly.
962        if height < block_height {
963            load_frontier(&mut tx, height).await
964        } else {
965            // If we do not have the exact snapshot we need, we can try going back to the last
966            // snapshot we _do_ have and replaying subsequent blocks to compute the desired state.
967            let (state, _) =
968                reconstruct_state(instance, self, &mut tx, block_height - 1, view, &[], &[])
969                    .await?;
970            match state.block_merkle_tree.lookup(height - 1) {
971                LookupResult::Ok(_, proof) => Ok(proof),
972                _ => {
973                    bail!(
974                        "state snapshot {view:?},{height} was found but does not contain frontier \
975                         at height {}; this should not be possible",
976                        height - 1
977                    );
978                },
979            }
980        }
981    }
982
983    async fn get_frontier_at(
984        &self,
985        height: u64,
986        root: BlockMerkleCommitment,
987    ) -> anyhow::Result<BlocksFrontier> {
988        let mut tx = self.read().await.context(format!(
989            "opening transaction to fetch frontier at height {height}"
990        ))?;
991        load_frontier_at(&mut tx, height, root).await
992    }
993
994    async fn get_chain_config(
995        &self,
996        commitment: Commitment<ChainConfig>,
997    ) -> anyhow::Result<ChainConfig> {
998        let mut tx = self.read().await.context(format!(
999            "opening transaction to fetch chain config {commitment}"
1000        ))?;
1001        load_chain_config(&mut tx, commitment).await
1002    }
1003
1004    async fn get_leaf_chain(&self, height: u64) -> anyhow::Result<Vec<Leaf2>> {
1005        let mut tx = self
1006            .read()
1007            .await
1008            .context(format!("opening transaction to fetch leaf at {height}"))?;
1009        let leaf: Leaf2 = tx
1010            .get_leaf((height as usize).into())
1011            .await
1012            .context(format!("leaf {height} not available"))?
1013            .leaf()
1014            .clone();
1015
1016        // New protocol: cert2 alone proves finality. Return the leaf range up to the finalizing
1017        // cert2's height.
1018        if leaf.block_header().version() >= NEW_PROTOCOL_VERSION {
1019            let cert2: espresso_types::Certificate2<SeqTypes> =
1020                tx.load_earliest_cert2(height).await?.context(format!(
1021                    "no cert2 available for new-protocol leaf at {height}"
1022                ))?;
1023            let cert2_height = cert2.data.block_number;
1024            let mut leaves = vec![leaf];
1025            if height < cert2_height {
1026                let descendants = tx
1027                    .get_leaf_range((height as usize + 1)..=cert2_height as usize)
1028                    .await?;
1029                leaves.extend(descendants.into_iter().flatten().map(|l| l.leaf().clone()));
1030            }
1031            return Ok(leaves);
1032        }
1033
1034        // Legacy protocol: build a 3-chain that decides `height`.
1035        let mut last_leaf = leaf;
1036        let mut chain = vec![last_leaf.clone()];
1037        let mut h = height + 1;
1038
1039        loop {
1040            let lqd = tx.get_leaf((h as usize).into()).await?;
1041            let leaf = lqd.leaf();
1042
1043            if leaf.justify_qc().view_number() == last_leaf.view_number() {
1044                chain.push(leaf.clone());
1045            } else {
1046                h += 1;
1047                continue;
1048            }
1049
1050            // just one away from deciding
1051            if leaf.view_number() == last_leaf.view_number() + 1 {
1052                last_leaf = leaf.clone();
1053                h += 1;
1054                break;
1055            }
1056            h += 1;
1057            last_leaf = leaf.clone();
1058        }
1059
1060        loop {
1061            let lqd = tx.get_leaf((h as usize).into()).await?;
1062            let leaf = lqd.leaf();
1063            if leaf.justify_qc().view_number() == last_leaf.view_number() {
1064                chain.push(leaf.clone());
1065                break;
1066            }
1067            h += 1;
1068        }
1069
1070        Ok(chain)
1071    }
1072
1073    async fn load_cert2(
1074        &self,
1075        height: u64,
1076    ) -> anyhow::Result<Option<espresso_types::Certificate2<SeqTypes>>> {
1077        let mut tx = self
1078            .read()
1079            .await
1080            .context("opening transaction to fetch cert2")?;
1081        Ok(tx.load_cert2(height).await?)
1082    }
1083
1084    async fn get_leaf(&self, height: u64) -> anyhow::Result<Leaf2> {
1085        let mut tx = self
1086            .read()
1087            .await
1088            .context(format!("opening transaction to fetch leaf at {height}"))?;
1089        let lqd = tx
1090            .get_leaf((height as usize).into())
1091            .await
1092            .context(format!("leaf {height} not available"))?;
1093        Ok(lqd.leaf().clone())
1094    }
1095
1096    /// The tree is stored only at the heights it changed, so the latest row at or below `height`
1097    /// may be an older epoch's: it is served only if it matches the header at `height`.
1098    async fn load_serialized_reward_merkle_tree_v2(&self, height: u64) -> anyhow::Result<Vec<u8>> {
1099        let tree_bytes = self.load_latest_tree(height).await?;
1100        let tree_data = bincode::deserialize::<RewardMerkleTreeV2Data>(&tree_bytes)
1101            .context("Failed to deserialize RewardMerkleTreeV2 from storage")?;
1102        let tree = PermittedRewardMerkleTreeV2::try_from_kv_set(tree_data.balances)
1103            .await
1104            .context("Failed to reconstruct reward merkle tree from storage")?;
1105        let leaf = CatchupStorage::get_leaf(self, height).await?;
1106        let either::Either::Right(expected_root) = leaf.block_header().reward_merkle_tree_root()
1107        else {
1108            bail!("header {height} has no V2 reward merkle tree");
1109        };
1110        ensure!(
1111            tree.tree.commitment() == expected_root,
1112            "the reward merkle tree in storage at or below height {height} is from an older epoch"
1113        );
1114        Ok(tree_bytes)
1115    }
1116}
1117
1118impl RewardMerkleTreeDataSource for DataSource {
1119    async fn load_v1_reward_account_proof(
1120        &self,
1121        height: u64,
1122        account: RewardAccountV1,
1123    ) -> anyhow::Result<RewardAccountQueryDataV1> {
1124        self.as_ref()
1125            .load_v1_reward_account_proof(height, account)
1126            .await
1127    }
1128
1129    fn persist_tree(
1130        &self,
1131        height: u64,
1132        merkle_tree: Vec<u8>,
1133    ) -> impl Send + Future<Output = anyhow::Result<()>> {
1134        async move { self.as_ref().persist_tree(height, merkle_tree).await }
1135    }
1136
1137    fn load_tree(&self, height: u64) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>> {
1138        async move { self.as_ref().load_tree(height).await }
1139    }
1140
1141    fn load_latest_tree(
1142        &self,
1143        height: u64,
1144    ) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>> {
1145        async move { self.as_ref().load_latest_tree(height).await }
1146    }
1147
1148    fn garbage_collect(&self, height: u64) -> impl Send + Future<Output = anyhow::Result<()>> {
1149        async move { self.as_ref().garbage_collect(height).await }
1150    }
1151
1152    fn persist_proofs(
1153        &self,
1154        height: u64,
1155        proofs: impl Iterator<Item = (Vec<u8>, Vec<u8>)> + Send,
1156    ) -> impl Send + Future<Output = anyhow::Result<()>> {
1157        async move { self.as_ref().persist_proofs(height, proofs).await }
1158    }
1159
1160    fn load_proof(
1161        &self,
1162        height: u64,
1163        account: Vec<u8>,
1164        epoch_height: u64,
1165    ) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>> {
1166        async move {
1167            self.as_ref()
1168                .load_proof(height, account, epoch_height)
1169                .await
1170        }
1171    }
1172
1173    fn load_latest_proof(
1174        &self,
1175        account: Vec<u8>,
1176    ) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>> {
1177        async move { self.as_ref().load_latest_proof(account).await }
1178    }
1179
1180    fn proof_exists(&self, height: u64) -> impl Send + Future<Output = bool> {
1181        async move { self.as_ref().proof_exists(height).await }
1182    }
1183
1184    fn persist_reward_proofs(
1185        &self,
1186        node_state: &NodeState,
1187        height: u64,
1188        version: Version,
1189    ) -> impl Send + Future<Output = anyhow::Result<()>> {
1190        async move {
1191            self.as_ref()
1192                .persist_reward_proofs(node_state, height, version)
1193                .await
1194        }
1195    }
1196}
1197
1198// A module of its own so `RequestStorage` is not in scope where `CatchupStorage` methods are
1199// called by name on this type.
1200mod request_storage {
1201    use async_trait::async_trait;
1202    use espresso_types::{
1203        Certificate2, ChainConfig, FeeAccount, FeeMerkleTree, Leaf2, NodeState,
1204        v0_3::{RewardAccountV1, RewardMerkleTreeV1},
1205        v0_4::{RewardAccountV2, RewardMerkleTreeV2},
1206    };
1207    use hotshot_types::data::{VidShare, ViewNumber};
1208
1209    use super::SqlStorage;
1210    use crate::{
1211        SeqTypes,
1212        api::{BlocksFrontier, RewardMerkleTreeDataSource},
1213        catchup::CatchupStorage,
1214        request_response::data_source::RequestStorage,
1215    };
1216
1217    #[async_trait]
1218    impl RequestStorage for SqlStorage {
1219        async fn get_accounts(
1220            &self,
1221            instance: &NodeState,
1222            height: u64,
1223            view: ViewNumber,
1224            accounts: &[FeeAccount],
1225        ) -> anyhow::Result<(FeeMerkleTree, Leaf2)> {
1226            CatchupStorage::get_accounts(self, instance, height, view, accounts).await
1227        }
1228
1229        async fn get_reward_accounts_v1(
1230            &self,
1231            instance: &NodeState,
1232            height: u64,
1233            view: ViewNumber,
1234            accounts: &[RewardAccountV1],
1235        ) -> anyhow::Result<(RewardMerkleTreeV1, Leaf2)> {
1236            CatchupStorage::get_reward_accounts_v1(self, instance, height, view, accounts).await
1237        }
1238
1239        async fn get_reward_accounts_v2(
1240            &self,
1241            instance: &NodeState,
1242            height: u64,
1243            view: ViewNumber,
1244            accounts: &[RewardAccountV2],
1245        ) -> anyhow::Result<(RewardMerkleTreeV2, Leaf2)> {
1246            CatchupStorage::get_reward_accounts_v2(self, instance, height, view, accounts).await
1247        }
1248
1249        async fn get_frontier(
1250            &self,
1251            instance: &NodeState,
1252            height: u64,
1253            view: ViewNumber,
1254        ) -> anyhow::Result<BlocksFrontier> {
1255            CatchupStorage::get_frontier(self, instance, height, view).await
1256        }
1257
1258        async fn get_chain_config(
1259            &self,
1260            commitment: committable::Commitment<ChainConfig>,
1261        ) -> anyhow::Result<ChainConfig> {
1262            CatchupStorage::get_chain_config(self, commitment).await
1263        }
1264
1265        async fn get_leaf_chain(&self, height: u64) -> anyhow::Result<Vec<Leaf2>> {
1266            CatchupStorage::get_leaf_chain(self, height).await
1267        }
1268
1269        async fn load_cert2(&self, height: u64) -> anyhow::Result<Option<Certificate2<SeqTypes>>> {
1270            CatchupStorage::load_cert2(self, height).await
1271        }
1272
1273        async fn load_tree(&self, height: u64) -> anyhow::Result<Vec<u8>> {
1274            RewardMerkleTreeDataSource::load_tree(self, height).await
1275        }
1276
1277        async fn get_vid_share(&self, block_number: u64) -> anyhow::Result<VidShare> {
1278            let id = hotshot_query_service::node::BlockId::Number(block_number as usize);
1279            Ok(SqlStorage::get_vid_share::<SeqTypes>(self, id).await?)
1280        }
1281    }
1282}
1283
1284impl CatchupStorage for DataSource {
1285    async fn get_accounts(
1286        &self,
1287        instance: &NodeState,
1288        height: u64,
1289        view: ViewNumber,
1290        accounts: &[FeeAccount],
1291    ) -> anyhow::Result<(FeeMerkleTree, Leaf2)> {
1292        self.as_ref()
1293            .get_accounts(instance, height, view, accounts)
1294            .await
1295    }
1296
1297    async fn get_accounts_at(
1298        &self,
1299        height: u64,
1300        root: FeeMerkleCommitment,
1301        accounts: &[FeeAccount],
1302    ) -> anyhow::Result<FeeMerkleTree> {
1303        self.as_ref().get_accounts_at(height, root, accounts).await
1304    }
1305
1306    async fn get_reward_accounts_v2(
1307        &self,
1308        instance: &NodeState,
1309        height: u64,
1310        view: ViewNumber,
1311        accounts: &[RewardAccountV2],
1312    ) -> anyhow::Result<(RewardMerkleTreeV2, Leaf2)> {
1313        self.as_ref()
1314            .get_reward_accounts_v2(instance, height, view, accounts)
1315            .await
1316    }
1317
1318    async fn get_reward_accounts_v1(
1319        &self,
1320        instance: &NodeState,
1321        height: u64,
1322        view: ViewNumber,
1323        accounts: &[RewardAccountV1],
1324    ) -> anyhow::Result<(RewardMerkleTreeV1, Leaf2)> {
1325        self.as_ref()
1326            .get_reward_accounts_v1(instance, height, view, accounts)
1327            .await
1328    }
1329
1330    async fn get_frontier(
1331        &self,
1332        instance: &NodeState,
1333        height: u64,
1334        view: ViewNumber,
1335    ) -> anyhow::Result<BlocksFrontier> {
1336        self.as_ref().get_frontier(instance, height, view).await
1337    }
1338
1339    async fn get_frontier_at(
1340        &self,
1341        height: u64,
1342        root: BlockMerkleCommitment,
1343    ) -> anyhow::Result<BlocksFrontier> {
1344        self.as_ref().get_frontier_at(height, root).await
1345    }
1346
1347    async fn get_chain_config(
1348        &self,
1349        commitment: Commitment<ChainConfig>,
1350    ) -> anyhow::Result<ChainConfig> {
1351        self.as_ref().get_chain_config(commitment).await
1352    }
1353    async fn get_leaf_chain(&self, height: u64) -> anyhow::Result<Vec<Leaf2>> {
1354        self.as_ref().get_leaf_chain(height).await
1355    }
1356
1357    async fn load_cert2(
1358        &self,
1359        height: u64,
1360    ) -> anyhow::Result<Option<espresso_types::Certificate2<SeqTypes>>> {
1361        self.as_ref().load_cert2(height).await
1362    }
1363
1364    async fn get_leaf(&self, height: u64) -> anyhow::Result<Leaf2> {
1365        self.as_ref().get_leaf(height).await
1366    }
1367
1368    async fn load_serialized_reward_merkle_tree_v2(&self, height: u64) -> anyhow::Result<Vec<u8>> {
1369        self.as_ref()
1370            .load_serialized_reward_merkle_tree_v2(height)
1371            .await
1372    }
1373}
1374
1375#[async_trait]
1376impl ChainConfigPersistence for Transaction<Write> {
1377    async fn insert_chain_config(&mut self, chain_config: ChainConfig) -> anyhow::Result<()> {
1378        let commitment = chain_config.commitment();
1379        let data = bincode::serialize(&chain_config)?;
1380        self.upsert(
1381            "chain_config",
1382            ["commitment", "data"],
1383            ["commitment"],
1384            [(commitment.to_string(), data)],
1385        )
1386        .await
1387    }
1388}
1389
1390impl crate::api::data_source::PruningDataSource for SqlStorage {
1391    async fn get_oldest_block(
1392        &self,
1393    ) -> anyhow::Result<Option<hotshot_query_service::availability::BlockQueryData<SeqTypes>>> {
1394        let mut tx = self
1395            .read()
1396            .await
1397            .context("opening transaction to fetch oldest block")?;
1398        let row = sqlx::query("SELECT MIN(height) AS height FROM header")
1399            .fetch_one(tx.as_mut())
1400            .await
1401            .context("failed to query oldest block height")?;
1402        let height: Option<i64> = row.try_get("height")?;
1403        match height {
1404            None => Ok(None),
1405            Some(h) => {
1406                let h = usize::try_from(h).context("block height out of range")?;
1407                Ok(Some(
1408                    tx.get_block(hotshot_query_service::availability::BlockId::<SeqTypes>::from(h))
1409                        .await
1410                        .context(format!("block {h} not available"))?,
1411                ))
1412            },
1413        }
1414    }
1415
1416    async fn get_oldest_leaf(
1417        &self,
1418    ) -> anyhow::Result<Option<hotshot_query_service::availability::LeafQueryData<SeqTypes>>> {
1419        let mut tx = self
1420            .read()
1421            .await
1422            .context("opening transaction to fetch oldest leaf")?;
1423        let row = sqlx::query("SELECT MIN(height) AS height FROM leaf2")
1424            .fetch_one(tx.as_mut())
1425            .await
1426            .context("failed to query oldest leaf height")?;
1427        let height: Option<i64> = row.try_get("height")?;
1428        match height {
1429            None => Ok(None),
1430            Some(h) => {
1431                let h = usize::try_from(h).context("leaf height out of range")?;
1432                Ok(Some(
1433                    tx.get_leaf(LeafId::<SeqTypes>::from(h))
1434                        .await
1435                        .context(format!("leaf {h} not available"))?,
1436                ))
1437            },
1438        }
1439    }
1440}
1441
1442impl crate::api::data_source::DatabaseMetadataSource for SqlStorage {
1443    async fn get_table_sizes(&self) -> anyhow::Result<Vec<espresso_api::v1::database::TableSize>> {
1444        let mut tx = self
1445            .read()
1446            .await
1447            .context("opening transaction to fetch table sizes")?;
1448
1449        if Db::KIND == DbKind::Postgres {
1450            let query = r#"
1451                SELECT
1452                    schemaname || '.' || relname AS table_name,
1453                    n_live_tup AS row_count,
1454                    pg_total_relation_size(relid) AS total_size_bytes
1455                FROM pg_stat_user_tables
1456                ORDER BY pg_total_relation_size(relid) DESC
1457            "#;
1458
1459            let rows = sqlx::query(query)
1460                .fetch_all(tx.as_mut())
1461                .await
1462                .context("failed to query table sizes")?;
1463
1464            let mut table_sizes = Vec::new();
1465            for row in rows {
1466                let table_name: String = row.try_get("table_name")?;
1467                let row_count: i64 = row.try_get("row_count").unwrap_or(-1);
1468                let total_size_bytes: Option<i64> = row.try_get("total_size_bytes").ok();
1469
1470                table_sizes.push(espresso_api::v1::database::TableSize {
1471                    table_name,
1472                    row_count,
1473                    total_size_bytes,
1474                });
1475            }
1476
1477            Ok(table_sizes)
1478        } else {
1479            // First, get all table names from sqlite_master
1480            let table_names_query = r#"
1481                SELECT name
1482                FROM sqlite_master
1483                WHERE type = 'table'
1484                AND name NOT LIKE 'sqlite_%'
1485                ORDER BY name
1486            "#;
1487
1488            let table_rows = sqlx::query(table_names_query)
1489                .fetch_all(tx.as_mut())
1490                .await
1491                .context("failed to query table names")?;
1492
1493            let mut table_sizes = Vec::new();
1494
1495            // For each table, get the row count
1496            for row in table_rows {
1497                let table_name: String = row.try_get("name")?;
1498
1499                // Run SELECT COUNT(*) for this specific table
1500                // Use format! here since we need to dynamically insert the table name
1501                let count_query = format!("SELECT COUNT(*) as count FROM \"{}\"", table_name);
1502                let count_row = sqlx::query(&count_query)
1503                    .fetch_one(tx.as_mut())
1504                    .await
1505                    .context(format!(
1506                        "failed to query row count for table {}",
1507                        table_name
1508                    ))?;
1509
1510                let row_count: i64 = count_row.try_get("count").unwrap_or(0);
1511
1512                table_sizes.push(espresso_api::v1::database::TableSize {
1513                    table_name,
1514                    row_count,
1515                    total_size_bytes: None,
1516                });
1517            }
1518
1519            Ok(table_sizes)
1520        }
1521    }
1522
1523    async fn get_migration_status(
1524        &self,
1525    ) -> anyhow::Result<Vec<espresso_api::v1::database::MigrationStatus>> {
1526        let mut tx = self
1527            .read()
1528            .await
1529            .context("opening transaction to fetch migration status")?;
1530
1531        type MigrationStatusRow = (
1532            String,
1533            chrono::DateTime<chrono::Utc>,
1534            Option<chrono::DateTime<chrono::Utc>>,
1535            Option<i64>,
1536        );
1537
1538        let rows: Vec<MigrationStatusRow> = sqlx::query_as(
1539            "SELECT name, started_at, completed_at, last_offset FROM deferred_migrations ORDER BY \
1540             started_at",
1541        )
1542        .fetch_all(tx.as_mut())
1543        .await
1544        .context("failed to query deferred_migrations")?;
1545
1546        Ok(rows
1547            .into_iter()
1548            .map(|(name, started_at, completed_at, last_offset)| {
1549                espresso_api::v1::database::MigrationStatus {
1550                    name,
1551                    started_at,
1552                    completed_at,
1553                    last_offset,
1554                }
1555            })
1556            .collect())
1557    }
1558}
1559
1560impl crate::api::data_source::DatabaseMetadataSource for DataSource {
1561    async fn get_table_sizes(&self) -> anyhow::Result<Vec<espresso_api::v1::database::TableSize>> {
1562        self.as_ref().get_table_sizes().await
1563    }
1564
1565    async fn get_migration_status(
1566        &self,
1567    ) -> anyhow::Result<Vec<espresso_api::v1::database::MigrationStatus>> {
1568        self.as_ref().get_migration_status().await
1569    }
1570}
1571
1572impl crate::api::data_source::PruningDataSource for DataSource {
1573    async fn get_oldest_block(
1574        &self,
1575    ) -> anyhow::Result<Option<hotshot_query_service::availability::BlockQueryData<SeqTypes>>> {
1576        self.as_ref().get_oldest_block().await
1577    }
1578
1579    async fn get_oldest_leaf(
1580        &self,
1581    ) -> anyhow::Result<Option<hotshot_query_service::availability::LeafQueryData<SeqTypes>>> {
1582        self.as_ref().get_oldest_leaf().await
1583    }
1584}
1585
1586async fn load_frontier<Mode: TransactionMode>(
1587    tx: &mut Transaction<Mode>,
1588    height: u64,
1589) -> anyhow::Result<BlocksFrontier> {
1590    tx.get_path(
1591        Snapshot::<SeqTypes, BlockMerkleTree, { BlockMerkleTree::ARITY }>::Index(height),
1592        height
1593            .checked_sub(1)
1594            .ok_or(anyhow::anyhow!("Subtract with overflow ({height})!"))?,
1595    )
1596    .await
1597    .context(format!("fetching frontier at height {height}"))
1598}
1599
1600/// Load the frontier from the snapshot at `height` whose root is `root`, without reading the header
1601/// at `height`.
1602async fn load_frontier_at<Mode: TransactionMode>(
1603    tx: &mut Transaction<Mode>,
1604    height: u64,
1605    root: BlockMerkleCommitment,
1606) -> anyhow::Result<BlocksFrontier> {
1607    tx.get_path_at::<SeqTypes, BlockMerkleTree, { BlockMerkleTree::ARITY }>(
1608        height,
1609        root,
1610        height
1611            .checked_sub(1)
1612            .ok_or(anyhow::anyhow!("Subtract with overflow ({height})!"))?,
1613    )
1614    .await
1615    .context(format!("fetching frontier at height {height}"))
1616}
1617
1618async fn load_v1_reward_accounts(
1619    db: &SqlStorage,
1620    height: u64,
1621    accounts: &[RewardAccountV1],
1622) -> anyhow::Result<(RewardMerkleTreeV1, Leaf2)> {
1623    // Open a new read transaction to get the leaf
1624    let mut tx = db
1625        .read()
1626        .await
1627        .with_context(|| "failed to open read transaction")?;
1628
1629    // Get the leaf from the database
1630    let leaf = tx
1631        .get_leaf(LeafId::<SeqTypes>::from(height as usize))
1632        .await
1633        .context(format!("leaf {height} not available"))?;
1634    let header = leaf.header();
1635
1636    if header.version() < EPOCH_VERSION || header.version() >= DRB_AND_HEADER_UPGRADE_VERSION {
1637        return Ok((
1638            RewardMerkleTreeV1::new(REWARD_MERKLE_TREE_V1_HEIGHT),
1639            leaf.leaf().clone(),
1640        ));
1641    }
1642
1643    // Get the merkle root from the header and create a snapshot from it
1644    let merkle_root = header.reward_merkle_tree_root().unwrap_left();
1645    let mut snapshot = RewardMerkleTreeV1::from_commitment(merkle_root);
1646
1647    // Create a bounded join set with 10 concurrent tasks
1648    let mut join_set = BoundedJoinSet::new(10);
1649
1650    // Create a map from task ID to account
1651    let mut task_id_to_account = HashMap::new();
1652
1653    // Loop through each account, spawning a task to get the path for the account
1654    for account in accounts {
1655        // Clone things we will need in the closure
1656        let db_clone = db.clone();
1657        let account_clone = *account;
1658        let header_height = header.height();
1659
1660        // Create the closure that will get the path for the account
1661        let func = async move {
1662            // Open a new transaction
1663            let mut tx = db_clone
1664                .read()
1665                .await
1666                .with_context(|| "failed to open read transaction")?;
1667
1668            // Get the path for the account
1669            let proof = tx
1670                .get_path(
1671                    Snapshot::<SeqTypes, RewardMerkleTreeV1, { RewardMerkleTreeV1::ARITY }>::Index(
1672                        header_height,
1673                    ),
1674                    account_clone,
1675                )
1676                .await
1677                .with_context(|| {
1678                    format!(
1679                        "failed to get path for v1 reward account {account_clone:?}; height \
1680                         {height}"
1681                    )
1682                })?;
1683
1684            Ok::<_, anyhow::Error>(proof)
1685        };
1686
1687        // Spawn the task
1688        let id = join_set.spawn(func).id();
1689
1690        // Add the task ID to the account map
1691        task_id_to_account.insert(id, account);
1692    }
1693
1694    // Wait for each task to complete
1695    while let Some(result) = join_set.join_next_with_id().await {
1696        // Get the inner result (past the join error)
1697        let (id, result) = result.with_context(|| "failed to join task")?;
1698
1699        // Get the proof from the result
1700        let proof = result?;
1701
1702        // Get the account from the task ID to account map
1703        let account = task_id_to_account
1704            .remove(&id)
1705            .with_context(|| "task ID for spawned task not found")?;
1706
1707        match proof.proof.first().with_context(|| {
1708            format!("empty proof for v1 reward account {account:?}; height {height}")
1709        })? {
1710            MerkleNode::Leaf { pos, elem, .. } => {
1711                snapshot.remember(*pos, *elem, proof)?;
1712            },
1713            MerkleNode::Empty => {
1714                snapshot.non_membership_remember(*account, proof)?;
1715            },
1716            _ => {
1717                bail!("invalid proof for v1 reward account {account:?}; height {height}");
1718            },
1719        }
1720    }
1721
1722    Ok((snapshot, leaf.leaf().clone()))
1723}
1724
1725/// Loads reward accounts for new reward merkle tree (V4).
1726async fn load_reward_merkle_tree_v2(
1727    db: &SqlStorage,
1728    height: u64,
1729) -> anyhow::Result<(PermittedRewardMerkleTreeV2, Leaf2)> {
1730    // Open a new read transaction to get the leaf
1731    let mut tx = db
1732        .read()
1733        .await
1734        .with_context(|| "failed to open read transaction")?;
1735
1736    // Get the leaf from the database
1737    let leaf = tx
1738        .get_leaf(LeafId::<SeqTypes>::from(height as usize))
1739        .await
1740        .with_context(|| format!("leaf {height} not available"))?;
1741
1742    let snapshot = db.load_reward_merkle_tree_v2(height).await?;
1743
1744    Ok((snapshot, leaf.leaf().clone()))
1745}
1746
1747async fn load_accounts<Mode: TransactionMode>(
1748    tx: &mut Transaction<Mode>,
1749    height: u64,
1750    accounts: &[FeeAccount],
1751) -> anyhow::Result<(FeeMerkleTree, Leaf2)> {
1752    let leaf = tx
1753        .get_leaf(LeafId::<SeqTypes>::from(height as usize))
1754        .await
1755        .context(format!("leaf {height} not available"))?;
1756    let header = leaf.header();
1757    let snapshot =
1758        load_accounts_at(tx, header.height(), header.fee_merkle_tree_root(), accounts).await?;
1759    Ok((snapshot, leaf.leaf().clone()))
1760}
1761
1762/// Load `accounts` from the snapshot at `height` whose root is `root`, without reading the leaf or
1763/// header at `height`.
1764async fn load_accounts_at<Mode: TransactionMode>(
1765    tx: &mut Transaction<Mode>,
1766    height: u64,
1767    root: FeeMerkleCommitment,
1768    accounts: &[FeeAccount],
1769) -> anyhow::Result<FeeMerkleTree> {
1770    let mut snapshot = FeeMerkleTree::from_commitment(root);
1771    for account in accounts {
1772        let proof = tx
1773            .get_path_at::<SeqTypes, FeeMerkleTree, { FeeMerkleTree::ARITY }>(
1774                height, root, *account,
1775            )
1776            .await
1777            .context(format!("fetching account {account}; height {height}"))?;
1778        match proof.proof.first().context(format!(
1779            "empty proof for account {account}; height {height}"
1780        ))? {
1781            MerkleNode::Leaf { pos, elem, .. } => {
1782                snapshot.remember(*pos, *elem, proof)?;
1783            },
1784            MerkleNode::Empty => {
1785                snapshot.non_membership_remember(*account, proof)?;
1786            },
1787            _ => {
1788                bail!("Invalid proof");
1789            },
1790        }
1791    }
1792
1793    Ok(snapshot)
1794}
1795
1796async fn load_chain_config<Mode: TransactionMode>(
1797    tx: &mut Transaction<Mode>,
1798    commitment: Commitment<ChainConfig>,
1799) -> anyhow::Result<ChainConfig> {
1800    let (data,) = query_as::<(Vec<u8>,)>("SELECT data from chain_config where commitment = $1")
1801        .bind(commitment.to_string())
1802        .fetch_one(tx.as_mut())
1803        .await
1804        .unwrap();
1805
1806    bincode::deserialize(&data[..]).context("failed to deserialize")
1807}
1808
1809/// Reconstructs the `ValidatedState` from a specific block height up to a given view.
1810///
1811/// This loads all required fee and reward accounts into the Merkle tree before applying the
1812/// State Transition Function (STF), preventing recursive catchup during STF replay.
1813///
1814/// Note: Even if the primary goal is to catch up the block Merkle tree,
1815/// fee and reward header dependencies must still be present beforehand
1816/// This is because reconstructing the `ValidatedState` involves replaying the STF over a
1817/// range of leaves, and the STF requires all associated data to be present in the `ValidatedState`;
1818/// otherwise, it will attempt to trigger catchup itself.
1819#[tracing::instrument(skip(instance, db, tx))]
1820pub(crate) async fn reconstruct_state<Mode: TransactionMode>(
1821    instance: &NodeState,
1822    db: &SqlStorage,
1823    tx: &mut Transaction<Mode>,
1824    from_height: u64,
1825    to_view: ViewNumber,
1826    fee_accounts: &[FeeAccount],
1827    reward_accounts: &[RewardAccountV2],
1828) -> anyhow::Result<(ValidatedState, Leaf2)> {
1829    tracing::info!("attempting to reconstruct fee state");
1830    let from_leaf = tx
1831        .get_leaf((from_height as usize).into())
1832        .await
1833        .context(format!("leaf {from_height} not available"))?;
1834    let from_leaf: Leaf2 = from_leaf.leaf().clone();
1835    ensure!(
1836        from_leaf.view_number() < to_view,
1837        "state reconstruction: starting state {:?} must be before ending state {to_view:?}",
1838        from_leaf.view_number(),
1839    );
1840
1841    // Get the sequence of headers we will be applying to compute the latest state.
1842    let mut leaves = VecDeque::new();
1843    let to_leaf = get_leaf_from_proposal(tx, "view = $1", &(to_view.u64() as i64))
1844        .await
1845        .context(format!(
1846            "unable to reconstruct state because leaf {to_view:?} is not available"
1847        ))?;
1848    let mut parent = to_leaf.parent_commitment();
1849    tracing::debug!(?to_leaf, ?parent, view = ?to_view, "have required leaf");
1850    leaves.push_front(to_leaf.clone());
1851    while parent != Committable::commit(&from_leaf) {
1852        let leaf = get_leaf_from_proposal(tx, "leaf_hash = $1", &parent.to_string())
1853            .await
1854            .context(format!(
1855                "unable to reconstruct state because leaf {parent} is not available"
1856            ))?;
1857        parent = leaf.parent_commitment();
1858        tracing::debug!(?leaf, ?parent, "have required leaf");
1859        leaves.push_front(leaf);
1860    }
1861
1862    // Get the initial state.
1863    let mut parent = from_leaf;
1864    let mut state = ValidatedState::from_header(parent.block_header());
1865
1866    // Pre-load the state with the accounts we care about to ensure they will be present in the
1867    // final state.
1868    let mut catchup = NullStateCatchup::default();
1869
1870    let mut fee_accounts = fee_accounts.iter().copied().collect::<HashSet<_>>();
1871    // Add in all the accounts we will need to replay any of the headers, to ensure that we don't
1872    // need to do catchup recursively.
1873    tracing::info!(
1874        "reconstructing fee accounts state for from height {from_height} to view {to_view}"
1875    );
1876
1877    let dependencies =
1878        fee_header_dependencies(&mut catchup, tx, instance, &parent, &leaves).await?;
1879    fee_accounts.extend(dependencies);
1880    let fee_accounts = fee_accounts.into_iter().collect::<Vec<_>>();
1881    state.fee_merkle_tree = load_accounts(tx, from_height, &fee_accounts)
1882        .await
1883        .context("unable to reconstruct state because accounts are not available at origin")?
1884        .0;
1885    ensure!(
1886        state.fee_merkle_tree.commitment() == parent.block_header().fee_merkle_tree_root(),
1887        "loaded fee state does not match parent header"
1888    );
1889
1890    tracing::info!(
1891        "reconstructing reward accounts for from height {from_height} to view {to_view}"
1892    );
1893
1894    let mut reward_accounts = reward_accounts.iter().copied().collect::<HashSet<_>>();
1895
1896    // Collect all reward account dependencies needed for replaying the STF.
1897    // These accounts must be preloaded into the reward Merkle tree to prevent recursive catchups.
1898    let dependencies = reward_header_dependencies(instance, &leaves).await?;
1899    reward_accounts.extend(dependencies);
1900    let reward_accounts = reward_accounts.into_iter().collect::<Vec<_>>();
1901
1902    // Load all required reward accounts and update the reward Merkle tree.
1903    match parent.block_header().reward_merkle_tree_root() {
1904        either::Either::Left(expected_root) => {
1905            let accts = reward_accounts
1906                .into_iter()
1907                .map(RewardAccountV1::from)
1908                .collect::<Vec<_>>();
1909            state.reward_merkle_tree_v1 = load_v1_reward_accounts(db, from_height, &accts)
1910                .await
1911                .context(
1912                    "unable to reconstruct state because v1 reward accounts are not available at \
1913                     origin",
1914                )?
1915                .0;
1916            ensure!(
1917                state.reward_merkle_tree_v1.commitment() == expected_root,
1918                "loaded v1 reward state does not match parent header"
1919            );
1920        },
1921        either::Either::Right(expected_root) => {
1922            let version = parent.block_header().version();
1923            let epoch_height = instance
1924                .epoch_height
1925                .context("epoch_height not set but parent has V2 reward tree")?;
1926
1927            // V5+ only stores the reward merkle tree at epoch's last block, so if we're
1928            // reconstructing at a non boundary height there is no row at from_height.
1929            // Load the latest tree instead
1930            // the commitment check below will catch it if
1931            // we ended up with a tree from an older epoch.
1932            // But this should never happen as we don't garbage collect the latest tree
1933            if version >= EPOCH_REWARD_VERSION && !is_last_block(from_height, epoch_height) {
1934                let tree = db
1935                    .load_latest_reward_merkle_tree_v2(from_height)
1936                    .await
1937                    .context("RewardMerkleTreeV2 not available at or below origin")?;
1938                state.reward_merkle_tree_v2 = tree.tree;
1939            } else {
1940                state.reward_merkle_tree_v2 = load_reward_merkle_tree_v2(db, from_height)
1941                    .await
1942                    .context("RewardMerkleTreeV2 not available at origin")?
1943                    .0
1944                    .tree;
1945            }
1946            ensure!(
1947                state.reward_merkle_tree_v2.commitment() == expected_root,
1948                "loaded reward state does not match parent header"
1949            );
1950        },
1951    }
1952
1953    // We need the blocks frontier as well, to apply the STF.
1954    let frontier = load_frontier(tx, from_height)
1955        .await
1956        .context("unable to reconstruct state because frontier is not available at origin")?;
1957    match frontier
1958        .proof
1959        .first()
1960        .context("empty proof for frontier at origin")?
1961    {
1962        MerkleNode::Leaf { pos, elem, .. } => state
1963            .block_merkle_tree
1964            .remember(*pos, *elem, frontier)
1965            .context("failed to remember frontier")?,
1966        _ => bail!("invalid frontier proof"),
1967    }
1968
1969    // Apply subsequent headers to compute the later state.
1970    for proposal in leaves {
1971        state = compute_state_update(&state, instance, &catchup, &parent, &proposal)
1972            .await
1973            .context(format!(
1974                "unable to reconstruct state because state update {} failed",
1975                proposal.height(),
1976            ))?
1977            .0;
1978        parent = proposal;
1979    }
1980
1981    tracing::info!(from_height, ?to_view, "successfully reconstructed state");
1982    Ok((state, to_leaf))
1983}
1984
1985/// Get the dependencies needed to apply the STF to the given list of headers.
1986///
1987/// Returns
1988/// * A state catchup implementation seeded with all the chain configs required to apply the headers
1989///   in `leaves`
1990/// * The set of accounts that must be preloaded to apply these headers
1991async fn fee_header_dependencies<Mode: TransactionMode>(
1992    catchup: &mut NullStateCatchup,
1993    tx: &mut Transaction<Mode>,
1994    instance: &NodeState,
1995    mut parent: &Leaf2,
1996    leaves: impl IntoIterator<Item = &Leaf2>,
1997) -> anyhow::Result<HashSet<FeeAccount>> {
1998    let mut accounts = HashSet::default();
1999
2000    for proposal in leaves {
2001        let header = proposal.block_header();
2002        let height = header.height();
2003        let view = proposal.view_number();
2004        tracing::debug!(height, ?view, "fetching dependencies for proposal");
2005
2006        let header_cf = header.chain_config();
2007        let chain_config = if header_cf.commit() == instance.chain_config.commit() {
2008            instance.chain_config
2009        } else {
2010            match header_cf.resolve() {
2011                Some(cf) => cf,
2012                None => {
2013                    tracing::info!(
2014                        height,
2015                        ?view,
2016                        commit = %header_cf.commit(),
2017                        "chain config not available, attempting to load from storage",
2018                    );
2019                    let cf = load_chain_config(tx, header_cf.commit())
2020                        .await
2021                        .context(format!(
2022                            "loading chain config {} for header {},{:?}",
2023                            header_cf.commit(),
2024                            header.height(),
2025                            proposal.view_number()
2026                        ))?;
2027
2028                    // If we had to fetch a chain config now, store it in the catchup implementation
2029                    // so the STF will be able to look it up later.
2030                    catchup.add_chain_config(cf);
2031                    cf
2032                },
2033            }
2034        };
2035
2036        accounts.insert(chain_config.fee_recipient);
2037        accounts.extend(
2038            get_l1_deposits(instance, header, parent, chain_config.fee_contract)
2039                .await
2040                .into_iter()
2041                .map(|fee| fee.account()),
2042        );
2043        accounts.extend(header.fee_info().accounts());
2044        parent = proposal;
2045    }
2046    Ok(accounts)
2047}
2048
2049/// Identifies all reward accounts required to replay the State Transition Function
2050/// for the given leaf proposals. These accounts should be present in the Merkle tree
2051/// *before* applying the STF to avoid recursive catchup (i.e., STF triggering another catchup).
2052async fn reward_header_dependencies(
2053    instance: &NodeState,
2054    leaves: impl IntoIterator<Item = &Leaf2>,
2055) -> anyhow::Result<HashSet<RewardAccountV2>> {
2056    let mut reward_accounts = HashSet::default();
2057    let epoch_height = instance.epoch_height;
2058
2059    let Some(epoch_height) = epoch_height else {
2060        tracing::info!("epoch height is not set. returning empty reward_header_dependencies");
2061        return Ok(HashSet::new());
2062    };
2063
2064    let coordinator = instance.coordinator.clone();
2065    let first_epoch = coordinator.membership().first_epoch();
2066    // add all the chain configs needed to apply STF to headers to the catchup
2067    for proposal in leaves {
2068        let header = proposal.block_header();
2069
2070        let height = header.height();
2071        let view = proposal.view_number();
2072        tracing::debug!(height, ?view, "fetching dependencies for proposal");
2073
2074        let version = header.version();
2075        // Skip if version is less than epoch version
2076        if version < EPOCH_VERSION || version >= EPOCH_REWARD_VERSION {
2077            continue;
2078        }
2079
2080        let proposal_epoch = EpochNumber::new(epoch_from_block_number(height, epoch_height));
2081
2082        // reward distribution starts third epoch onwards
2083        if proposal_epoch <= first_epoch + 1 {
2084            continue;
2085        }
2086
2087        let epoch_membership = match coordinator.membership_for_epoch(Some(proposal_epoch)) {
2088            Ok(e) => e,
2089            Err(err) => {
2090                tracing::info!(
2091                    "failed to get membership for epoch={proposal_epoch:?}. err={err:#}"
2092                );
2093
2094                coordinator
2095                    .wait_for_catchup(proposal_epoch)
2096                    .await
2097                    .context(format!("failed to catchup for epoch={proposal_epoch}"))?
2098            },
2099        };
2100
2101        let snapshot = epoch_membership
2102            .snapshot()
2103            .with_context(|| format!("no committee for epoch={proposal_epoch}"))?;
2104        let leader = snapshot.lookup_leader(proposal.view_number())?;
2105        let validator = snapshot.validator_config(&leader)?;
2106
2107        reward_accounts.insert(RewardAccountV2(validator.account));
2108
2109        let delegators: Vec<RewardAccountV2> = validator
2110            .delegators
2111            .keys()
2112            .map(|d| RewardAccountV2(*d))
2113            .collect();
2114
2115        reward_accounts.extend(delegators);
2116    }
2117    Ok(reward_accounts)
2118}
2119
2120async fn get_leaf_from_proposal<Mode, P>(
2121    tx: &mut Transaction<Mode>,
2122    where_clause: &str,
2123    param: P,
2124) -> anyhow::Result<Leaf2>
2125where
2126    P: Type<Db> + for<'q> Encode<'q, Db>,
2127{
2128    let (data,) = query_as::<(Vec<u8>,)>(&format!(
2129        "SELECT data FROM quorum_proposals2 WHERE {where_clause} LIMIT 1",
2130    ))
2131    .bind(param)
2132    .fetch_one(tx.as_mut())
2133    .await?;
2134    let proposal: Proposal<SeqTypes, QuorumProposalWrapper<SeqTypes>> =
2135        bincode::deserialize(&data)?;
2136    Ok(Leaf2::from_quorum_proposal(&proposal.data))
2137}
2138
2139#[cfg(any(test, feature = "testing"))]
2140pub(crate) mod impl_testable_data_source {
2141
2142    use hotshot_query_service::{
2143        availability::LeafQueryData,
2144        data_source::{
2145            Transaction as _,
2146            storage::{
2147                MerklizedStateHeightStorage as _, SerializableRetry as _,
2148                UpdateAvailabilityStorage as _, pruning::PrunedHeightStorage as _,
2149                sql::StorageConnectionType,
2150            },
2151        },
2152        merklized_state::UpdateStateData,
2153        serializable_retry,
2154    };
2155
2156    pub(crate) use super::super::persistence::tmp_options;
2157    use super::{super::qs::testing::TmpDb, *};
2158    use crate::api::{
2159        self,
2160        data_source::testing::{
2161            DeferredMigration, TestableSequencerDataSource, TestableSqlDataSource,
2162        },
2163    };
2164
2165    #[async_trait]
2166    impl TestableSequencerDataSource for DataSource {
2167        type Storage = TmpDb;
2168
2169        async fn create_storage() -> Self::Storage {
2170            TmpDb::init().await
2171        }
2172
2173        fn persistence_options(storage: &Self::Storage) -> Self::Options {
2174            Options::try_from(tmp_options(storage)).unwrap()
2175        }
2176
2177        fn leaf_only_ds_options(
2178            storage: &Self::Storage,
2179            opt: api::Options,
2180        ) -> anyhow::Result<api::Options> {
2181            let mut ds_opts = tmp_options(storage);
2182            ds_opts.lightweight = true;
2183            Ok(opt.query_sql(Default::default(), ds_opts))
2184        }
2185
2186        fn options(storage: &Self::Storage, opt: api::Options) -> api::Options {
2187            opt.query_sql(Query::default(), tmp_options(storage))
2188        }
2189    }
2190
2191    #[async_trait]
2192    impl TestableSqlDataSource for DataSource {
2193        type SqlStorage = SqlStorage;
2194
2195        fn tmp_options(storage: &Self::Storage) -> SqlOptions {
2196            tmp_options(storage)
2197        }
2198
2199        fn sql_persistence_options(opt: SqlOptions) -> Self::Options {
2200            Options::try_from(opt).unwrap()
2201        }
2202
2203        async fn on_request_data_source(
2204            storage: &Self::Storage,
2205            provider: Provider,
2206        ) -> anyhow::Result<Self> {
2207            Config::try_from(&tmp_options(storage))?
2208                .builder(provider)
2209                .await?
2210                .disable_proactive_fetching()
2211                .disable_aggregator()
2212                .build()
2213                .await
2214        }
2215
2216        async fn prune_state_below(ds: &Self, height: u64, cfg: &PrunerCfg) -> anyhow::Result<()> {
2217            let sql: &SqlStorage = ds.as_ref();
2218            sql.prune_state_below(height, 0, cfg).await
2219        }
2220
2221        async fn connect(
2222            opt: &SqlOptions,
2223            connection_type: StorageConnectionType,
2224        ) -> anyhow::Result<SqlStorage> {
2225            Ok(SqlStorage::connect(Config::try_from(opt)?, connection_type).await?)
2226        }
2227
2228        async fn local_block_height(db: &SqlStorage) -> anyhow::Result<u64> {
2229            let mut tx = db.read().await?;
2230            Ok(NodeStorage::<SeqTypes>::block_height(&mut tx).await? as u64)
2231        }
2232
2233        // Read in this order because every move of the pruned height, up by a batch or down by
2234        // the startup repair, lands below the head, and the head only grows, so a head read
2235        // after the pruned height is always above it.
2236        async fn state_heights(db: &SqlStorage) -> anyhow::Result<(Option<u64>, u64)> {
2237            let mut tx = db.read().await?;
2238            let pruned = tx.load_state_pruned_height().await?;
2239            let head = tx.get_last_state_height().await? as u64;
2240            Ok((pruned, head))
2241        }
2242
2243        async fn state_start_height(db: &SqlStorage) -> anyhow::Result<Option<u64>> {
2244            Ok(db.read().await?.load_state_start_height().await?)
2245        }
2246
2247        async fn lowest_block_tree_node(db: &SqlStorage) -> anyhow::Result<u64> {
2248            let mut tx = db.read().await?;
2249            let lowest: Option<i64> =
2250                sqlx::query_scalar("SELECT min(created) FROM block_merkle_tree")
2251                    .fetch_one(tx.as_mut())
2252                    .await?;
2253            lowest
2254                .map(|height| height as u64)
2255                .context("no block tree nodes")
2256        }
2257
2258        async fn insert_leaf(
2259            db: &SqlStorage,
2260            leaf: &LeafQueryData<SeqTypes>,
2261        ) -> anyhow::Result<()> {
2262            let mut tx = db.write().await?;
2263            tx.insert_leaf(leaf).await?;
2264            tx.commit().await
2265        }
2266
2267        async fn set_last_state_height(db: &SqlStorage, height: u64) -> anyhow::Result<()> {
2268            let mut tx = db.write().await?;
2269            UpdateStateData::<SeqTypes, BlockMerkleTree, { BlockMerkleTree::ARITY }>::set_last_state_height(
2270                &mut tx,
2271                height as usize,
2272            )
2273            .await?;
2274            tx.commit().await
2275        }
2276
2277        async fn stamp_state_pruned_height(db: &SqlStorage, height: u64) -> anyhow::Result<()> {
2278            let mut tx = db.write().await?;
2279            tx.upsert(
2280                "pruned_height",
2281                ["id", "last_height"],
2282                ["id"],
2283                [(Transaction::<Write>::STATE_PRUNED_HEIGHT_ID, height as i64)],
2284            )
2285            .await?;
2286            tx.commit().await
2287        }
2288
2289        // The node may be writing new blocks meanwhile, so a serialization conflict is retried.
2290        async fn prune_data_to(db: &SqlStorage, pruned: u64) -> anyhow::Result<()> {
2291            serializable_retry!(db, || async {
2292                let mut tx = db.write().await?;
2293                tx.upsert(
2294                    "pruned_height",
2295                    ["id", "last_height"],
2296                    ["id"],
2297                    [(Transaction::<Write>::PRUNED_HEIGHT_ID, pruned as i64)],
2298                )
2299                .await?;
2300                for statement in [
2301                    "DELETE FROM transactions WHERE block_height <= $1",
2302                    "DELETE FROM leaf2 WHERE height <= $1",
2303                    "DELETE FROM header WHERE height <= $1",
2304                ] {
2305                    sqlx::query(statement)
2306                        .bind(pruned as i64)
2307                        .execute(tx.as_mut())
2308                        .await?;
2309                }
2310                tx.commit().await
2311            })
2312            .await
2313        }
2314
2315        async fn delete_data_between(db: &SqlStorage, from: u64, to: u64) -> anyhow::Result<()> {
2316            let mut tx = db.write().await?;
2317            for statement in [
2318                "DELETE FROM transactions WHERE block_height > $1 AND block_height <= $2",
2319                "DELETE FROM leaf2 WHERE height > $1 AND height <= $2",
2320                "DELETE FROM header WHERE height > $1 AND height <= $2",
2321            ] {
2322                sqlx::query(statement)
2323                    .bind(from as i64)
2324                    .bind(to as i64)
2325                    .execute(tx.as_mut())
2326                    .await?;
2327            }
2328            tx.commit().await
2329        }
2330
2331        async fn count_leaves_up_to(db: &SqlStorage, height: u64) -> anyhow::Result<i64> {
2332            let mut tx = db.read().await?;
2333            Ok(
2334                sqlx::query_scalar("SELECT count(*) FROM leaf2 WHERE height <= $1")
2335                    .bind(height as i64)
2336                    .fetch_one(tx.as_mut())
2337                    .await?,
2338            )
2339        }
2340
2341        async fn data_heights(db: &SqlStorage) -> anyhow::Result<(Option<u64>, Option<u64>)> {
2342            let mut tx = db.read().await?;
2343            let pruned = tx.load_pruned_height().await?;
2344            let lowest: Option<i64> =
2345                sqlx::query_scalar("SELECT min(height) FROM leaf2 WHERE height > 0")
2346                    .fetch_one(tx.as_mut())
2347                    .await?;
2348            Ok((pruned, lowest.map(|height| height as u64)))
2349        }
2350
2351        async fn insert_deferred_migration(
2352            db: &SqlStorage,
2353            migration: DeferredMigration,
2354        ) -> anyhow::Result<()> {
2355            let DeferredMigration {
2356                name,
2357                started_at,
2358                completed_at,
2359                last_offset,
2360            } = migration;
2361            let mut tx = db.write().await?;
2362            sqlx::query(
2363                "INSERT INTO deferred_migrations (name, started_at, completed_at, last_offset)
2364                 VALUES ($1, $2, $3, $4)",
2365            )
2366            .bind(name)
2367            .bind(started_at)
2368            .bind(completed_at)
2369            .bind(last_offset)
2370            .execute(tx.as_mut())
2371            .await?;
2372            tx.commit().await
2373        }
2374
2375        async fn reconstruct_state(
2376            db: &SqlStorage,
2377            instance: &NodeState,
2378            from_height: u64,
2379            to_view: ViewNumber,
2380            fee_accounts: &[FeeAccount],
2381            reward_accounts: &[RewardAccountV2],
2382        ) -> anyhow::Result<(ValidatedState, Leaf2)> {
2383            let mut tx = db.read().await?;
2384            reconstruct_state(
2385                instance,
2386                db,
2387                &mut tx,
2388                from_height,
2389                to_view,
2390                fee_accounts,
2391                reward_accounts,
2392            )
2393            .await
2394        }
2395    }
2396}
2397
2398#[cfg(test)]
2399mod tests {
2400    use std::sync::Arc;
2401
2402    use alloy::primitives::{Address, U256};
2403    use espresso_types::{
2404        BLOCK_MERKLE_TREE_HEIGHT, BackoffParams, BlockMerkleTree, FEE_MERKLE_TREE_HEIGHT,
2405        FeeAccount, FeeAmount, FeeMerkleTree, NodeState,
2406        v0::traits::StateCatchup,
2407        v0_4::{REWARD_MERKLE_TREE_V2_HEIGHT, RewardMerkleTreeV2},
2408    };
2409    use hotshot_query_service::{
2410        QueryError,
2411        data_source::{
2412            Transaction, VersionedDataSource,
2413            storage::{
2414                UpdateAvailabilityStorage, pruning::PrunedHeightStorage, sql::StorageConnectionType,
2415            },
2416        },
2417        merklized_state::{MerklizedState, UpdateStateData},
2418        node::NodeDataSource,
2419    };
2420    use hotshot_types::data::ViewNumber;
2421    use jf_merkle_tree_compat::{
2422        AppendableMerkleTreeScheme, ForgetableMerkleTreeScheme, LookupResult, MerkleTreeScheme,
2423        ToTraversalPath, UniversalMerkleTreeScheme,
2424    };
2425    use light_client::testing::{leaf_chain, leaf_chain_with_upgrade};
2426    use versions::{DRB_AND_HEADER_UPGRADE_VERSION, EPOCH_REWARD_VERSION, Upgrade};
2427
2428    use super::{
2429        super::qs::{Config, SqlStorage, Transaction as SqlTransaction, Write, testing::TmpDb},
2430        ArchiveStateGc, SeqTypes,
2431        impl_testable_data_source::tmp_options,
2432        query_as,
2433    };
2434    use crate::{
2435        api::{RewardMerkleTreeDataSource, data_source::SequencerDataSource},
2436        catchup::{CatchupStorage, SqlStateCatchup},
2437    };
2438
2439    async fn write_fee_state(storage: &SqlStorage, account: FeeAccount, balance: u64, height: u64) {
2440        let mut tree = FeeMerkleTree::new(FEE_MERKLE_TREE_HEIGHT);
2441        tree.update(account, FeeAmount::from(balance)).unwrap();
2442        let proof = match tree.universal_lookup(account) {
2443            LookupResult::Ok(_, proof) => proof,
2444            _ => panic!("account not in tree"),
2445        };
2446        let path = <FeeAccount as ToTraversalPath<{ FeeMerkleTree::ARITY }>>::to_traversal_path(
2447            &account,
2448            tree.height(),
2449        );
2450
2451        let mut tx = storage.write().await.unwrap();
2452        UpdateStateData::<SeqTypes, FeeMerkleTree, { FeeMerkleTree::ARITY }>::insert_merkle_nodes(
2453            &mut tx, proof, path, height,
2454        )
2455        .await
2456        .unwrap();
2457        UpdateStateData::<SeqTypes, FeeMerkleTree, { FeeMerkleTree::ARITY }>::set_last_state_height(
2458            &mut tx,
2459            height as usize,
2460        )
2461        .await
2462        .unwrap();
2463        Transaction::commit(tx).await.unwrap();
2464    }
2465
2466    async fn fee_state_heights(storage: &SqlStorage) -> Vec<u64> {
2467        let mut tx = storage.read().await.unwrap();
2468        query_as::<(i64,)>(&format!(
2469            "SELECT DISTINCT created FROM {} ORDER BY created",
2470            FeeMerkleTree::state_type()
2471        ))
2472        .fetch_all(tx.as_mut())
2473        .await
2474        .unwrap()
2475        .into_iter()
2476        .map(|(height,)| height as u64)
2477        .collect()
2478    }
2479
2480    /// The collector's options reach storage: the fee tree is a state table and the floor applies.
2481    #[tokio::test]
2482    #[test_log::test]
2483    async fn test_archive_state_gc_prunes_fee_state() {
2484        let db = TmpDb::init().await;
2485        let mut opt = tmp_options(&db);
2486        opt.archive_state_min_retention = 2;
2487        let storage = SqlStorage::connect(
2488            Config::try_from(&opt).unwrap(),
2489            StorageConnectionType::Query,
2490        )
2491        .await
2492        .unwrap();
2493
2494        let account = FeeAccount::from(Address::repeat_byte(0x42));
2495        for height in 1..=5 {
2496            write_fee_state(&storage, account, 100 * height, height).await;
2497        }
2498
2499        // Light client history at 4, floor 2 under head 5: target 3, marker 2.
2500        let gc = ArchiveStateGc::new(&opt);
2501        storage
2502            .prune_state_below(4, gc.min_retention, &gc.cfg)
2503            .await
2504            .unwrap();
2505        assert_eq!(fee_state_heights(&storage).await, [2, 3, 4, 5]);
2506        let mut tx = storage.read().await.unwrap();
2507        assert_eq!(tx.load_state_pruned_height().await.unwrap(), Some(2));
2508        assert_eq!(tx.load_pruned_height().await.unwrap(), None);
2509    }
2510
2511    /// The data pruner deletes the leaf and header at the state head and keeps the ones above it,
2512    /// while the state pruner keeps the head's snapshot. The local catchup still reads that
2513    /// snapshot, by the root it is given.
2514    #[tokio::test]
2515    #[test_log::test]
2516    async fn test_catchup_reads_snapshot_without_header() {
2517        let db = TmpDb::init().await;
2518        let storage = Arc::new(
2519            SqlStorage::connect(
2520                Config::try_from(&tmp_options(&db)).unwrap(),
2521                StorageConnectionType::Query,
2522            )
2523            .await
2524            .unwrap(),
2525        );
2526
2527        // A head snapshot whose leaf and header the data pruner deleted, below a stored leaf.
2528        let height = 3;
2529        let leaves = leaf_chain(0..=height + 1, DRB_AND_HEADER_UPGRADE_VERSION).await;
2530        let mut tx = storage.write().await.unwrap();
2531        tx.insert_leaf(&leaves[height as usize + 1]).await.unwrap();
2532        Transaction::commit(tx).await.unwrap();
2533
2534        let account = FeeAccount::from(Address::repeat_byte(0x42));
2535        let absent = FeeAccount::from(Address::repeat_byte(0x43));
2536        write_fee_state(&storage, account, 100, height).await;
2537        let mut fee_tree = FeeMerkleTree::new(FEE_MERKLE_TREE_HEIGHT);
2538        fee_tree.update(account, FeeAmount::from(100)).unwrap();
2539        let fee_root = fee_tree.commitment();
2540
2541        let mut block_tree = BlockMerkleTree::new(BLOCK_MERKLE_TREE_HEIGHT);
2542        for leaf in &leaves[..height as usize] {
2543            block_tree.push(leaf.block_hash()).unwrap();
2544        }
2545        let frontier = block_tree.lookup(height - 1).expect_ok().unwrap().1;
2546        let mut tx = storage.write().await.unwrap();
2547        UpdateStateData::<SeqTypes, BlockMerkleTree, { BlockMerkleTree::ARITY }>::insert_merkle_nodes(
2548            &mut tx,
2549            frontier,
2550            ToTraversalPath::<{ BlockMerkleTree::ARITY }>::to_traversal_path(
2551                &(height - 1),
2552                BLOCK_MERKLE_TREE_HEIGHT,
2553            ),
2554            height,
2555        )
2556        .await
2557        .unwrap();
2558        Transaction::commit(tx).await.unwrap();
2559
2560        let instance = NodeState::mock();
2561        let view = ViewNumber::new(height);
2562
2563        // The reads through the leaf and header fail, so the catchup has to read by root.
2564        let err = storage
2565            .get_accounts(&instance, height, view, &[account])
2566            .await
2567            .unwrap_err();
2568        assert!(
2569            format!("{err:#}").contains(&format!("leaf {height} not available")),
2570            "{err:#}"
2571        );
2572        storage
2573            .get_frontier(&instance, height, view)
2574            .await
2575            .unwrap_err();
2576
2577        let catchup = SqlStateCatchup::new(storage, BackoffParams::disabled());
2578
2579        let proofs = catchup
2580            .try_fetch_accounts(0, &instance, height, view, fee_root, &[account, absent])
2581            .await
2582            .unwrap();
2583        let balances = proofs
2584            .iter()
2585            .map(|proof| proof.verify(&fee_root).unwrap())
2586            .collect::<Vec<_>>();
2587        assert_eq!(balances, [U256::from(100), U256::ZERO]);
2588
2589        let mut block_tree_from_root = BlockMerkleTree::from_commitment(block_tree.commitment());
2590        catchup
2591            .try_remember_blocks_merkle_tree(0, &instance, height, view, &mut block_tree_from_root)
2592            .await
2593            .unwrap();
2594        assert!(block_tree_from_root.lookup(height - 1).expect_ok().is_ok());
2595
2596        // A root the snapshot does not have is rejected, not trusted.
2597        let wrong_root = FeeMerkleTree::new(FEE_MERKLE_TREE_HEIGHT).commitment();
2598        catchup
2599            .try_fetch_accounts(0, &instance, height, view, wrong_root, &[account])
2600            .await
2601            .unwrap_err();
2602    }
2603
2604    async fn insert_test_header(
2605        tx: &mut SqlTransaction<Write>,
2606        block_height: u64,
2607        reward_tree: &RewardMerkleTreeV2,
2608    ) {
2609        let reward_commitment = serde_json::to_value(reward_tree.commitment()).unwrap();
2610        let test_data = serde_json::json!({
2611            "block_merkle_tree_root": format!("block_root_{}", block_height),
2612            "fee_merkle_tree_root": format!("fee_root_{}", block_height),
2613            "fields": {
2614                RewardMerkleTreeV2::header_state_commitment_field(): reward_commitment
2615            }
2616        });
2617        tx.upsert(
2618            "header",
2619            [
2620                "height",
2621                "hash",
2622                "payload_hash",
2623                "timestamp",
2624                "data",
2625                "ns_table",
2626            ],
2627            ["height"],
2628            [(
2629                block_height as i64,
2630                format!("hash_{}", block_height),
2631                format!("payload_{}", block_height),
2632                block_height as i64,
2633                test_data,
2634                "ns_table".to_string(),
2635            )],
2636        )
2637        .await
2638        .unwrap();
2639    }
2640
2641    #[tokio::test]
2642    #[test_log::test]
2643    async fn test_merkle_proof_gc() {
2644        let db = TmpDb::init().await;
2645        let opt = tmp_options(&db);
2646        let cfg = Config::try_from(&opt).expect("failed to create config from options");
2647        let storage = SqlStorage::connect(cfg, StorageConnectionType::Query)
2648            .await
2649            .expect("failed to connect to storage");
2650
2651        let account = vec![0; 32];
2652
2653        // Insert mock leaves at heights 0-2 so load_proof can check leaf version.
2654        let leaves = leaf_chain(0..=2, DRB_AND_HEADER_UPGRADE_VERSION).await;
2655        let mut tx = storage.write().await.unwrap();
2656        for leaf in &leaves {
2657            tx.insert_leaf(leaf).await.unwrap();
2658        }
2659        Transaction::commit(tx).await.unwrap();
2660
2661        // Insert some mock proofs at heights 0-2.
2662        for h in 0..=2 {
2663            storage
2664                .persist_proofs(h, [(account.clone(), h.to_le_bytes().to_vec())].into_iter())
2665                .await
2666                .unwrap();
2667        }
2668
2669        // Test garbage collection.
2670        storage.garbage_collect(2).await.unwrap();
2671
2672        // Make sure the proof at height 2 is still available.
2673        assert_eq!(
2674            storage.load_proof(2, account.clone(), 0).await.unwrap(),
2675            2u64.to_le_bytes()
2676        );
2677
2678        // Meanwhile, the proofs at heights 0-1 have been garbage collected.
2679        for h in 0..2 {
2680            let err = storage.load_proof(h, account.clone(), 0).await.unwrap_err();
2681            assert!(err.to_string().contains("Missing proof"), "{err:#}");
2682        }
2683
2684        // Garbage collect the remaining proof.
2685        storage.garbage_collect(3).await.unwrap();
2686        let err = storage.load_proof(2, account, 0).await.unwrap_err();
2687        assert!(err.to_string().contains("Missing proof"), "{err:#}");
2688    }
2689
2690    #[tokio::test]
2691    #[test_log::test]
2692    async fn test_load_proof_v5_epoch_boundary() {
2693        let db = TmpDb::init().await;
2694        let opt = tmp_options(&db);
2695        let cfg = Config::try_from(&opt).expect("failed to create config from options");
2696        let storage = SqlStorage::connect(cfg, StorageConnectionType::Query)
2697            .await
2698            .expect("failed to connect to storage");
2699
2700        let epoch_height = 10u64;
2701        let account = vec![0; 32];
2702
2703        // Create V5 leaves at heights 0..=15.
2704        let leaves = leaf_chain(0..=15, EPOCH_REWARD_VERSION).await;
2705        let mut tx = storage.write().await.unwrap();
2706        for leaf in &leaves {
2707            tx.insert_leaf(leaf).await.unwrap();
2708        }
2709        Transaction::commit(tx).await.unwrap();
2710
2711        // Store proofs only at epoch boundaries (height 10).
2712        let boundary_proof = b"proof_at_10".to_vec();
2713        {
2714            let mut tx = storage.write().await.unwrap();
2715            tx.upsert(
2716                "reward_merkle_tree_v2_proofs",
2717                ["height", "account", "proof"],
2718                ["height", "account"],
2719                [(10i64, account.clone(), boundary_proof.clone())],
2720            )
2721            .await
2722            .unwrap();
2723            Transaction::commit(tx).await.unwrap();
2724        }
2725
2726        // Querying at the epoch boundary itself should return the proof directly.
2727        assert_eq!(
2728            storage
2729                .load_proof(10, account.clone(), epoch_height)
2730                .await
2731                .unwrap(),
2732            boundary_proof,
2733        );
2734
2735        // Querying at a non-boundary V5 height (e.g. 15) should resolve to the
2736        // previous epoch boundary (10) and return its proof.
2737        assert_eq!(
2738            storage
2739                .load_proof(15, account.clone(), epoch_height)
2740                .await
2741                .unwrap(),
2742            boundary_proof,
2743        );
2744    }
2745
2746    #[tokio::test]
2747    #[test_log::test]
2748    async fn test_load_proof_v4_to_v5_upgrade_boundary() {
2749        let db = TmpDb::init().await;
2750        let opt = tmp_options(&db);
2751        let cfg = Config::try_from(&opt).expect("failed to create config from options");
2752        let storage = SqlStorage::connect(cfg, StorageConnectionType::Query)
2753            .await
2754            .expect("failed to connect to storage");
2755
2756        let epoch_height = 10u64;
2757        let account = vec![0; 32];
2758
2759        // V4 leaves at heights 0..=10, V5 from height 11 onward.
2760        // The upgrade happens at height 11.
2761        let upgrade = Upgrade::new(DRB_AND_HEADER_UPGRADE_VERSION, EPOCH_REWARD_VERSION);
2762        let leaves = leaf_chain_with_upgrade(0..=15, 11, upgrade).await;
2763        {
2764            let mut tx = storage.write().await.unwrap();
2765            for leaf in &leaves {
2766                tx.insert_leaf(leaf).await.unwrap();
2767            }
2768            Transaction::commit(tx).await.unwrap();
2769        }
2770
2771        // Querying at V5 height 15 resolves to prev epoch boundary
2772        // But leaf at height 10 is V4, so this should fail.
2773        storage
2774            .load_proof(15, account.clone(), epoch_height)
2775            .await
2776            .unwrap_err();
2777
2778        let v4_proof = b"v4_proof_at_5".to_vec();
2779        {
2780            let mut tx = storage.write().await.unwrap();
2781            tx.upsert(
2782                "reward_merkle_tree_v2_proofs",
2783                ["height", "account", "proof"],
2784                ["height", "account"],
2785                [(5i64, account.clone(), v4_proof.clone())],
2786            )
2787            .await
2788            .unwrap();
2789            Transaction::commit(tx).await.unwrap();
2790        }
2791        assert_eq!(
2792            storage.load_proof(5, account.clone(), 0).await.unwrap(),
2793            v4_proof,
2794        );
2795    }
2796
2797    #[test_log::test(tokio::test(flavor = "multi_thread"))]
2798    async fn test_get_table_sizes() {
2799        use crate::api::data_source::DatabaseMetadataSource;
2800
2801        let db = TmpDb::init().await;
2802        let opt = tmp_options(&db);
2803        let cfg = Config::try_from(&opt).expect("failed to create config from options");
2804        let storage = SqlStorage::connect(cfg, StorageConnectionType::Query)
2805            .await
2806            .expect("failed to connect to storage");
2807
2808        // Insert some test data to ensure tables have rows
2809        let mut tx = storage.write().await.unwrap();
2810
2811        // Insert a test header
2812        let reward_tree = RewardMerkleTreeV2::new(REWARD_MERKLE_TREE_V2_HEIGHT);
2813        insert_test_header(&mut tx, 1, &reward_tree).await;
2814
2815        tx.commit().await.unwrap();
2816
2817        // Call get_table_sizes and verify it doesn't error
2818        let table_sizes = storage
2819            .get_table_sizes()
2820            .await
2821            .expect("get_table_sizes should succeed");
2822
2823        // Verify we got some tables back
2824        assert!(
2825            !table_sizes.is_empty(),
2826            "should have at least one table in the database"
2827        );
2828    }
2829
2830    /// A pruned node runs without an aggregator, so it has no transaction or payload totals.
2831    #[test_log::test(tokio::test(flavor = "multi_thread"))]
2832    async fn test_pruned_node_has_no_totals() {
2833        for prune in [false, true] {
2834            let storage = TmpDb::init().await;
2835            let mut opt = tmp_options(&storage);
2836            opt.prune = prune;
2837            let ds = super::DataSource::create(
2838                opt.try_into().unwrap(),
2839                super::Provider::default(),
2840                false,
2841            )
2842            .await
2843            .unwrap();
2844
2845            let res = NodeDataSource::count_transactions_in_range(&ds, .., None).await;
2846            if prune {
2847                assert!(matches!(res, Err(QueryError::NotFound)), "{res:?}");
2848            } else {
2849                assert_eq!(res.unwrap(), 0);
2850            }
2851        }
2852    }
2853}