Skip to main content

espresso_node/api/
sql.rs

1use std::collections::{HashMap, HashSet, VecDeque};
2
3use anyhow::{Context, bail, ensure};
4use async_trait::async_trait;
5use committable::{Commitment, Committable};
6use espresso_types::{
7    BlockMerkleTree, ChainConfig, FeeAccount, FeeMerkleTree, Leaf2, NodeState, ValidatedState,
8    get_l1_deposits,
9    v0_1::IterableFeeInfo,
10    v0_3::{
11        REWARD_MERKLE_TREE_V1_HEIGHT, RewardAccountProofV1, RewardAccountQueryDataV1,
12        RewardAccountV1, RewardMerkleTreeV1,
13    },
14    v0_4::{PermittedRewardMerkleTreeV2, RewardAccountV2, RewardMerkleTreeV2},
15    v0_6::RewardAccountQueryDataV2,
16};
17use futures::future::Future;
18use hotshot::traits::ValidatedState as _;
19use hotshot_query_service::{
20    Resolvable,
21    availability::LeafId,
22    data_source::{
23        VersionedDataSource,
24        sql::{Config, SqlDataSource, Transaction},
25        storage::{
26            AvailabilityStorage, MerklizedStateStorage, NodeStorage, SqlStorage,
27            pruning::PrunerConfig,
28            sql::{Db, TransactionMode, Write, query_as},
29        },
30    },
31    merklized_state::Snapshot,
32};
33use hotshot_types::{
34    data::{EpochNumber, QuorumProposalWrapper, ViewNumber},
35    message::Proposal,
36    traits::election::MembershipSnapshot,
37    utils::{epoch_from_block_number, is_last_block},
38    vote::HasViewNumber,
39};
40use jf_merkle_tree_compat::{
41    ForgetableMerkleTreeScheme, ForgetableUniversalMerkleTreeScheme, LookupResult,
42    MerkleTreeScheme, prelude::MerkleNode,
43};
44use sqlx::{Encode, Row, Type};
45use vbs::version::Version;
46use versions::{
47    DRB_AND_HEADER_UPGRADE_VERSION, EPOCH_REWARD_VERSION, EPOCH_VERSION, NEW_PROTOCOL_VERSION,
48};
49
50use super::{
51    BlocksFrontier,
52    data_source::{Provider, SequencerDataSource},
53};
54use crate::{
55    SeqTypes,
56    api::RewardMerkleTreeDataSource,
57    catchup::{CatchupStorage, NullStateCatchup},
58    persistence::{ChainConfigPersistence, sql::Options},
59    state::compute_state_update,
60    util::BoundedJoinSet,
61};
62
63pub type DataSource = SqlDataSource<SeqTypes, Provider>;
64
65#[async_trait]
66impl SequencerDataSource for DataSource {
67    type Options = Options;
68
69    async fn create(opt: Self::Options, provider: Provider, reset: bool) -> anyhow::Result<Self> {
70        let fetch_limit = opt.fetch_rate_limit;
71        let active_fetch_delay = opt.active_fetch_delay;
72        let chunk_fetch_delay = opt.chunk_fetch_delay;
73        let mut cfg = Config::try_from(&opt)?;
74
75        if reset {
76            cfg = cfg.reset_schema();
77        }
78
79        let mut builder = cfg.builder(provider).await?;
80
81        if let Some(limit) = fetch_limit {
82            builder = builder.with_rate_limit(limit);
83        }
84
85        if opt.lightweight {
86            tracing::warn!("enabling light weight mode..");
87            builder = builder.leaf_only();
88        }
89
90        if let Some(delay) = active_fetch_delay {
91            builder = builder.with_active_fetch_delay(delay);
92        }
93        if let Some(delay) = chunk_fetch_delay {
94            builder = builder.with_chunk_fetch_delay(delay);
95        }
96        if let Some(chunk_size) = opt.sync_status_chunk_size {
97            builder = builder.with_sync_status_chunk_size(chunk_size);
98        }
99        if let Some(ttl) = opt.sync_status_ttl {
100            builder = builder.with_sync_status_ttl(ttl);
101        }
102        if let Some(chunk_size) = opt.proactive_scan_chunk_size {
103            builder = builder.with_proactive_range_chunk_size(chunk_size);
104        }
105        if let Some(interval) = opt.proactive_scan_interval {
106            builder = builder.with_proactive_interval(interval);
107        }
108        if opt.disable_proactive_fetching {
109            builder = builder.disable_proactive_fetching();
110        }
111
112        builder.build().await
113    }
114}
115
116impl RewardMerkleTreeDataSource for SqlStorage {
117    async fn load_v1_reward_account_proof(
118        &self,
119        height: u64,
120        account: RewardAccountV1,
121    ) -> anyhow::Result<RewardAccountQueryDataV1> {
122        let mut tx = self.read().await.context(format!(
123            "opening transaction to fetch v1 reward account {account:?}; height {height}"
124        ))?;
125
126        let block_height = NodeStorage::<SeqTypes>::block_height(&mut tx)
127            .await
128            .context("getting block height")? as u64;
129        ensure!(
130            block_height > 0,
131            "cannot get accounts for height {height}: no blocks available"
132        );
133
134        // Check if we have the desired state snapshot. If so, we can load the desired accounts
135        // directly.
136        if height < block_height {
137            let (tree, _) = load_v1_reward_accounts(self, height, &[account])
138                .await
139                .with_context(|| {
140                    format!("failed to load v1 reward account {account:?} at height {height}")
141                })?;
142
143            let (proof, balance) = RewardAccountProofV1::prove(&tree, account.into())
144                .with_context(|| {
145                    format!("reward account {account:?} not available at height {height}")
146                })?;
147
148            Ok(RewardAccountQueryDataV1 { balance, proof })
149        } else {
150            bail!(
151                "requested height {height} is not yet available (latest block height: \
152                 {block_height})"
153            );
154        }
155    }
156
157    fn persist_tree(
158        &self,
159        height: u64,
160        merkle_tree: Vec<u8>,
161    ) -> impl Send + Future<Output = anyhow::Result<()>> {
162        async move {
163            let mut tx = self
164                .write()
165                .await
166                .context("opening transaction for reward merkle tree v2")?;
167
168            tx.upsert(
169                "reward_merkle_tree_v2_data",
170                ["height", "balances"],
171                ["height"],
172                [(height as i64, merkle_tree)],
173            )
174            .await?;
175
176            hotshot_query_service::data_source::Transaction::commit(tx)
177                .await
178                .context("Transaction to store reward merkle tree v2 failed.")
179        }
180    }
181
182    fn load_tree(&self, height: u64) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>> {
183        async move {
184            let mut tx = self
185                .read()
186                .await
187                .context("opening transaction for state update")?;
188
189            let row = sqlx::query(
190                r#"
191                SELECT balances
192                FROM reward_merkle_tree_v2_data
193                WHERE height = $1
194                "#,
195            )
196            .bind(height as i64)
197            .fetch_optional(tx.as_mut())
198            .await?
199            .context(format!(
200                "No reward merkle tree for height {} in storage",
201                height
202            ))?;
203
204            row.try_get::<Vec<u8>, _>("balances")
205                .context("Missing field balances from row; this should never happen")
206        }
207    }
208
209    fn load_latest_tree(
210        &self,
211        height: u64,
212    ) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>> {
213        async move {
214            let mut tx = self
215                .read()
216                .await
217                .context("opening transaction for load_latest_tree")?;
218
219            let row = sqlx::query(
220                r#"
221                SELECT balances
222                FROM reward_merkle_tree_v2_data
223                WHERE height <= $1
224                ORDER BY height DESC
225                LIMIT 1
226                "#,
227            )
228            .bind(height as i64)
229            .fetch_optional(tx.as_mut())
230            .await?
231            .context(format!(
232                "No reward merkle tree at or below height {} in storage",
233                height
234            ))?;
235
236            row.try_get::<Vec<u8>, _>("balances")
237                .context("Missing field balances from row; this should never happen")
238        }
239    }
240
241    fn persist_proofs(
242        &self,
243        height: u64,
244        proofs: impl Iterator<Item = (Vec<u8>, Vec<u8>)> + Send,
245    ) -> impl Send + Future<Output = anyhow::Result<()>> {
246        async move {
247            let mut iter = proofs.map(|(account, proof)| (height as i64, account, proof));
248
249            loop {
250                let mut chunk = Vec::with_capacity(20);
251
252                for _ in 0..20 {
253                    let Some(row) = iter.next() else {
254                        continue;
255                    };
256                    chunk.push(row);
257                }
258
259                if chunk.is_empty() {
260                    break;
261                }
262
263                let mut tx = self
264                    .write()
265                    .await
266                    .context("opening transaction for state update")?;
267
268                tokio::spawn(async move {
269                    tx.upsert(
270                        "reward_merkle_tree_v2_proofs",
271                        ["height", "account", "proof"],
272                        ["height", "account"],
273                        chunk,
274                    )
275                    .await?;
276
277                    hotshot_query_service::data_source::Transaction::commit(tx)
278                        .await
279                        .context("Transaction to store reward merkle tree failed.")?;
280
281                    Ok::<_, anyhow::Error>(())
282                });
283            }
284            Ok(())
285        }
286    }
287
288    /// Load a reward proof for a given height and account.
289    ///
290    /// For V5+ (epoch rewards), if the requested height is not an epoch boundary,
291    /// resolves to the previous epoch's last block. Verifies the boundary block is
292    /// V5+ to handle V4→V5 upgrades.
293    /// For V4 (per-block rewards), proofs are stored at every block height.
294    fn load_proof(
295        &self,
296        height: u64,
297        account: Vec<u8>,
298        epoch_height: u64,
299    ) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>> {
300        async move {
301            let mut tx = self
302                .read()
303                .await
304                .context("opening transaction for load_proof")?;
305
306            let leaf = tx
307                .get_leaf(LeafId::<SeqTypes>::from(height as usize))
308                .await
309                .context(format!("leaf {height} not available"))?;
310
311            // For V5+, resolve to the last epoch boundary where proofs were stored.
312            // For V4, proofs exist at every height.
313            let proof_height = if leaf.header().version() < EPOCH_REWARD_VERSION
314                || is_last_block(height, epoch_height)
315            {
316                height
317            } else {
318                let prev_epoch_last_block =
319                    epoch_from_block_number(height, epoch_height).saturating_sub(1) * epoch_height;
320                let boundary_leaf = tx
321                    .get_leaf(LeafId::<SeqTypes>::from(prev_epoch_last_block as usize))
322                    .await
323                    .context(format!(
324                        "leaf at epoch boundary {prev_epoch_last_block} not available"
325                    ))?;
326                ensure!(
327                    boundary_leaf.header().version() >= EPOCH_REWARD_VERSION,
328                    "no epoch reward proofs available at boundary {prev_epoch_last_block}"
329                );
330                prev_epoch_last_block
331            };
332
333            sqlx::query_scalar(
334                "SELECT proof FROM reward_merkle_tree_v2_proofs WHERE height = $1 AND account = $2",
335            )
336            .bind(proof_height as i64)
337            .bind(account)
338            .fetch_optional(tx.as_mut())
339            .await?
340            .context(format!(
341                "Missing proofs at height {proof_height} (resolved from {height})"
342            ))
343        }
344    }
345
346    fn load_latest_proof(
347        &self,
348        account: Vec<u8>,
349    ) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>> {
350        async move {
351            let mut tx = self
352                .read()
353                .await
354                .context("opening transaction for state update")?;
355
356            let row = sqlx::query(
357                r#"
358                SELECT proof
359                FROM reward_merkle_tree_v2_proofs
360                WHERE account = $1
361                ORDER BY height DESC
362                LIMIT 1
363                "#,
364            )
365            .bind(account)
366            .fetch_optional(tx.as_mut())
367            .await?
368            .context("Missing proofs")?;
369
370            row.try_get::<Vec<u8>, _>("proof")
371                .context("Missing field proof from row; this should never happen")
372        }
373    }
374
375    fn garbage_collect(&self, height: u64) -> impl Send + Future<Output = anyhow::Result<()>> {
376        async move {
377            let batch_size = self.get_pruning_config().unwrap_or_default().batch_size();
378
379            // Postgres supports improved performance by using `FOR UPDATE` in a sub-select to
380            // acquire exclusive locks on visited rows, which are later deleted. SQLite does not
381            // support this syntax.
382            #[cfg(not(feature = "embedded-db"))]
383            let for_update = "FOR UPDATE";
384            #[cfg(feature = "embedded-db")]
385            let for_update = "";
386
387            // Delete batches from the merkle data table until there is nothing left to delete.
388            loop {
389                let mut tx = self
390                    .write()
391                    .await
392                    .context("opening transaction for state update")?;
393
394                let res = sqlx::query(&format!(
395                    "
396                    WITH delete_batch AS (
397                        SELECT d.height FROM reward_merkle_tree_v2_data AS d
398                            WHERE d.height < $1
399                            ORDER BY d.height DESC
400                            LIMIT $2
401                            {for_update}
402                    )
403                    DELETE FROM reward_merkle_tree_v2_data AS del
404                    WHERE del.height IN (SELECT * FROM delete_batch)
405                "
406                ))
407                .bind(height as i64)
408                .bind(batch_size as i64)
409                .execute(tx.as_mut())
410                .await?;
411
412                hotshot_query_service::data_source::Transaction::commit(tx)
413                    .await
414                    .context(
415                        "Transaction to garbage collect reward merkle trees from storage failed.",
416                    )?;
417
418                if res.rows_affected() == 0 {
419                    break;
420                } else {
421                    tracing::debug!(
422                        "deleted {} rows from reward_merkle_tree_v2_data",
423                        res.rows_affected()
424                    );
425                }
426            }
427
428            // Delete batches from the proofs table until there is nothing left to delete.
429            loop {
430                let mut tx = self
431                    .write()
432                    .await
433                    .context("opening transaction for state update")?;
434
435                let res = sqlx::query(&format!(
436                    "
437                    WITH delete_batch AS (
438                        SELECT d.height, d.account FROM reward_merkle_tree_v2_proofs AS d
439                            WHERE d.height < $1
440                            ORDER BY d.height, d.account DESC
441                            LIMIT $2
442                            {for_update}
443                    )
444                    DELETE FROM reward_merkle_tree_v2_proofs AS del
445                    WHERE (del.height, del.account) IN (SELECT * FROM delete_batch)
446                ",
447                ))
448                .bind(height as i64)
449                .bind(batch_size as i64)
450                .execute(tx.as_mut())
451                .await?;
452
453                hotshot_query_service::data_source::Transaction::commit(tx)
454                    .await
455                    .context("Transaction to garbage collect proofs from storage failed.")?;
456
457                if res.rows_affected() == 0 {
458                    break;
459                } else {
460                    tracing::debug!(
461                        "deleted {} rows from reward_merkle_tree_v2_proofs",
462                        res.rows_affected()
463                    );
464                }
465            }
466
467            Ok(())
468        }
469    }
470
471    fn proof_exists(&self, height: u64) -> impl Send + Future<Output = bool> {
472        async move {
473            let Ok(mut tx) = self.write().await else {
474                return false;
475            };
476
477            sqlx::query_as(
478                r#"
479                SELECT EXISTS(
480                SELECT 1 FROM reward_merkle_tree_v2_proofs
481                WHERE height = $1
482                )
483                "#,
484            )
485            .bind(height as i64)
486            .fetch_one(tx.as_mut())
487            .await
488            .ok()
489            .unwrap_or((false,))
490            .0
491        }
492    }
493    /// Generate and persist reward proofs for the current L1-finalized height.
494    ///
495    /// For V5+ (epoch rewards), the reward tree is only stored at epoch boundaries.
496    /// We resolve to the nearest epoch boundary to load the tree, verify it's V5+
497    /// (for V4→V5 upgrades), and store the generated proofs at the same boundary height.
498    /// For V4 (per-block rewards), the tree exists at every block height.
499    fn persist_reward_proofs(
500        &self,
501        node_state: &NodeState,
502        height: u64,
503        version: Version,
504    ) -> impl Send + Future<Output = anyhow::Result<()>> {
505        async move {
506            // Only attempt to persist proofs every 30th block.
507            //
508            // In tests, we persist proofs at every block height so
509            // reward claim tests can query proofs at arbitrary heights. This can be
510            // removed once all reward claim tests are updated
511            if !cfg!(any(test, feature = "testing"))
512                && !(height + node_state.node_id).is_multiple_of(30)
513            {
514                return Ok(());
515            }
516
517            // In tests, we use the current block height directly instead
518            // of querying the L1 finalized HotShot height, because test environments don't
519            // deploy a light client contract
520            let finalized_hotshot_height = if cfg!(any(test, feature = "testing")) {
521                height
522            } else {
523                match node_state.finalized_hotshot_height().await {
524                    Ok(h) => h,
525                    Err(err) => {
526                        tracing::warn!("failed to get finalized hotshot height: {err:#}");
527                        return Ok(());
528                    },
529                }
530            };
531
532            // Resolve which height to load the reward tree from.
533            // V4: tree exists at every height. V5+: only at epoch boundaries.
534            let mut tree_height = finalized_hotshot_height;
535            let mut proof_height = finalized_hotshot_height;
536            if version >= EPOCH_REWARD_VERSION {
537                let epoch_height = node_state
538                    .epoch_height
539                    .context("epoch_height not set in node state")?;
540                if !is_last_block(finalized_hotshot_height, epoch_height) {
541                    tree_height = epoch_from_block_number(finalized_hotshot_height, epoch_height)
542                        .saturating_sub(1)
543                        * epoch_height;
544                    if tree_height == 0 {
545                        return Ok(());
546                    }
547                    // During V4→V5 upgrades the previous epoch boundary may be V4.
548                    let mut tx = self.read().await.context("opening read transaction")?;
549                    let leaf = tx
550                        .get_leaf(LeafId::<SeqTypes>::from(tree_height as usize))
551                        .await
552                        .context(format!(
553                            "leaf at epoch boundary {tree_height} not available"
554                        ))?;
555                    if leaf.header().version() < EPOCH_REWARD_VERSION {
556                        return Ok(());
557                    }
558                }
559                proof_height = tree_height;
560            }
561
562            if self.proof_exists(proof_height).await {
563                return Ok(());
564            }
565
566            let permitted_tree = match self.load_reward_merkle_tree_v2(tree_height).await {
567                Ok(tree) => tree,
568                Err(err) => {
569                    tracing::warn!(tree_height, "failed to load reward merkle tree: {err:#}");
570                    return Ok(());
571                },
572            };
573
574            let tree = permitted_tree.tree;
575            let iter = tree
576                .iter()
577                .filter_map(|(account, balance): (&RewardAccountV2, _)| {
578                    let proof = espresso_types::v0_6::RewardAccountProofV2::prove(
579                        &tree,
580                        (*account).into(),
581                    )?;
582                    let proof = RewardAccountQueryDataV2 {
583                        balance: (*balance).into(),
584                        proof: proof.0,
585                    };
586                    Some((
587                        bincode::serialize(&account).ok()?,
588                        bincode::serialize(&proof).ok()?,
589                    ))
590                });
591
592            if let Err(err) = self.persist_proofs(proof_height, iter).await {
593                tracing::warn!(proof_height, "failed to persist proofs: {err:#}");
594            }
595
596            Ok(())
597        }
598    }
599}
600
601impl CatchupStorage for SqlStorage {
602    async fn get_reward_accounts_v1(
603        &self,
604        instance: &NodeState,
605        height: u64,
606        view: ViewNumber,
607        accounts: &[RewardAccountV1],
608    ) -> anyhow::Result<(RewardMerkleTreeV1, Leaf2)> {
609        let mut tx = self.read().await.context(format!(
610            "opening transaction to fetch v1 reward account {accounts:?}; height {height}"
611        ))?;
612
613        let block_height = NodeStorage::<SeqTypes>::block_height(&mut tx)
614            .await
615            .context("getting block height")? as u64;
616        ensure!(
617            block_height > 0,
618            "cannot get accounts for height {height}: no blocks available"
619        );
620
621        // Check if we have the desired state snapshot. If so, we can load the desired accounts
622        // directly.
623        if height < block_height {
624            load_v1_reward_accounts(self, height, accounts).await
625        } else {
626            let accounts: Vec<_> = accounts
627                .iter()
628                .map(|acct| RewardAccountV2::from(*acct))
629                .collect();
630            // If we do not have the exact snapshot we need, we can try going back to the last
631            // snapshot we _do_ have and replaying subsequent blocks to compute the desired state.
632            let (state, leaf) = reconstruct_state(
633                instance,
634                self,
635                &mut tx,
636                block_height - 1,
637                view,
638                &[],
639                &accounts,
640            )
641            .await?;
642            Ok((state.reward_merkle_tree_v1, leaf))
643        }
644    }
645
646    async fn get_reward_accounts_v2(
647        &self,
648        instance: &NodeState,
649        height: u64,
650        view: ViewNumber,
651        accounts: &[RewardAccountV2],
652    ) -> anyhow::Result<(RewardMerkleTreeV2, Leaf2)> {
653        let mut tx = self.read().await.context(format!(
654            "opening transaction to fetch reward account {accounts:?}; height {height}"
655        ))?;
656
657        let block_height = NodeStorage::<SeqTypes>::block_height(&mut tx)
658            .await
659            .context("getting block height")? as u64;
660        ensure!(
661            block_height > 0,
662            "cannot get accounts for height {height}: no blocks available"
663        );
664
665        // Check if we have the desired state snapshot. If so, we can load the desired accounts
666        // directly.
667        if height < block_height {
668            load_reward_merkle_tree_v2(self, height)
669                .await
670                .map(|(permitted_tree, leaf)| (permitted_tree.tree, leaf))
671        } else {
672            // If we do not have the exact snapshot we need, we can try going back to the last
673            // snapshot we _do_ have and replaying subsequent blocks to compute the desired state.
674            let (state, leaf) = reconstruct_state(
675                instance,
676                self,
677                &mut tx,
678                block_height - 1,
679                view,
680                &[],
681                accounts,
682            )
683            .await?;
684            Ok((state.reward_merkle_tree_v2, leaf))
685        }
686    }
687
688    async fn get_accounts(
689        &self,
690        instance: &NodeState,
691        height: u64,
692        view: ViewNumber,
693        accounts: &[FeeAccount],
694    ) -> anyhow::Result<(FeeMerkleTree, Leaf2)> {
695        let mut tx = self.read().await.context(format!(
696            "opening transaction to fetch account {accounts:?}; height {height}"
697        ))?;
698
699        let block_height = NodeStorage::<SeqTypes>::block_height(&mut tx)
700            .await
701            .context("getting block height")? as u64;
702        ensure!(
703            block_height > 0,
704            "cannot get accounts for height {height}: no blocks available"
705        );
706
707        // Check if we have the desired state snapshot. If so, we can load the desired accounts
708        // directly.
709        if height < block_height {
710            load_accounts(&mut tx, height, accounts).await
711        } else {
712            // If we do not have the exact snapshot we need, we can try going back to the last
713            // snapshot we _do_ have and replaying subsequent blocks to compute the desired state.
714            let (state, leaf) = reconstruct_state(
715                instance,
716                self,
717                &mut tx,
718                block_height - 1,
719                view,
720                accounts,
721                &[],
722            )
723            .await?;
724            Ok((state.fee_merkle_tree, leaf))
725        }
726    }
727
728    async fn get_frontier(
729        &self,
730        instance: &NodeState,
731        height: u64,
732        view: ViewNumber,
733    ) -> anyhow::Result<BlocksFrontier> {
734        let mut tx = self.read().await.context(format!(
735            "opening transaction to fetch frontier at height {height}"
736        ))?;
737
738        let block_height = NodeStorage::<SeqTypes>::block_height(&mut tx)
739            .await
740            .context("getting block height")? as u64;
741        ensure!(
742            block_height > 0,
743            "cannot get frontier for height {height}: no blocks available"
744        );
745
746        // Check if we have the desired state snapshot. If so, we can load the desired frontier
747        // directly.
748        if height < block_height {
749            load_frontier(&mut tx, height).await
750        } else {
751            // If we do not have the exact snapshot we need, we can try going back to the last
752            // snapshot we _do_ have and replaying subsequent blocks to compute the desired state.
753            let (state, _) =
754                reconstruct_state(instance, self, &mut tx, block_height - 1, view, &[], &[])
755                    .await?;
756            match state.block_merkle_tree.lookup(height - 1) {
757                LookupResult::Ok(_, proof) => Ok(proof),
758                _ => {
759                    bail!(
760                        "state snapshot {view:?},{height} was found but does not contain frontier \
761                         at height {}; this should not be possible",
762                        height - 1
763                    );
764                },
765            }
766        }
767    }
768
769    async fn get_chain_config(
770        &self,
771        commitment: Commitment<ChainConfig>,
772    ) -> anyhow::Result<ChainConfig> {
773        let mut tx = self.read().await.context(format!(
774            "opening transaction to fetch chain config {commitment}"
775        ))?;
776        load_chain_config(&mut tx, commitment).await
777    }
778
779    async fn get_leaf_chain(&self, height: u64) -> anyhow::Result<Vec<Leaf2>> {
780        let mut tx = self
781            .read()
782            .await
783            .context(format!("opening transaction to fetch leaf at {height}"))?;
784        let leaf: Leaf2 = tx
785            .get_leaf((height as usize).into())
786            .await
787            .context(format!("leaf {height} not available"))?
788            .leaf()
789            .clone();
790
791        // New protocol: cert2 alone proves finality. Return the leaf range up to the finalizing
792        // cert2's height.
793        if leaf.block_header().version() >= NEW_PROTOCOL_VERSION {
794            let cert2: espresso_types::Certificate2<SeqTypes> =
795                tx.load_earliest_cert2(height).await?.context(format!(
796                    "no cert2 available for new-protocol leaf at {height}"
797                ))?;
798            let cert2_height = cert2.data.block_number;
799            let mut leaves = vec![leaf];
800            if height < cert2_height {
801                let descendants = tx
802                    .get_leaf_range((height as usize + 1)..=cert2_height as usize)
803                    .await?;
804                leaves.extend(descendants.into_iter().flatten().map(|l| l.leaf().clone()));
805            }
806            return Ok(leaves);
807        }
808
809        // Legacy protocol: build a 3-chain that decides `height`.
810        let mut last_leaf = leaf;
811        let mut chain = vec![last_leaf.clone()];
812        let mut h = height + 1;
813
814        loop {
815            let lqd = tx.get_leaf((h as usize).into()).await?;
816            let leaf = lqd.leaf();
817
818            if leaf.justify_qc().view_number() == last_leaf.view_number() {
819                chain.push(leaf.clone());
820            } else {
821                h += 1;
822                continue;
823            }
824
825            // just one away from deciding
826            if leaf.view_number() == last_leaf.view_number() + 1 {
827                last_leaf = leaf.clone();
828                h += 1;
829                break;
830            }
831            h += 1;
832            last_leaf = leaf.clone();
833        }
834
835        loop {
836            let lqd = tx.get_leaf((h as usize).into()).await?;
837            let leaf = lqd.leaf();
838            if leaf.justify_qc().view_number() == last_leaf.view_number() {
839                chain.push(leaf.clone());
840                break;
841            }
842            h += 1;
843        }
844
845        Ok(chain)
846    }
847
848    async fn load_cert2(
849        &self,
850        height: u64,
851    ) -> anyhow::Result<Option<espresso_types::Certificate2<SeqTypes>>> {
852        let mut tx = self
853            .read()
854            .await
855            .context("opening transaction to fetch cert2")?;
856        Ok(tx.load_cert2(height).await?)
857    }
858
859    async fn get_leaf(&self, height: u64) -> anyhow::Result<Leaf2> {
860        let mut tx = self
861            .read()
862            .await
863            .context(format!("opening transaction to fetch leaf at {height}"))?;
864        let lqd = tx
865            .get_leaf((height as usize).into())
866            .await
867            .context(format!("leaf {height} not available"))?;
868        Ok(lqd.leaf().clone())
869    }
870}
871
872impl RewardMerkleTreeDataSource for DataSource {
873    async fn load_v1_reward_account_proof(
874        &self,
875        height: u64,
876        account: RewardAccountV1,
877    ) -> anyhow::Result<RewardAccountQueryDataV1> {
878        self.as_ref()
879            .load_v1_reward_account_proof(height, account)
880            .await
881    }
882
883    fn persist_tree(
884        &self,
885        height: u64,
886        merkle_tree: Vec<u8>,
887    ) -> impl Send + Future<Output = anyhow::Result<()>> {
888        async move { self.as_ref().persist_tree(height, merkle_tree).await }
889    }
890
891    fn load_tree(&self, height: u64) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>> {
892        async move { self.as_ref().load_tree(height).await }
893    }
894
895    fn load_latest_tree(
896        &self,
897        height: u64,
898    ) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>> {
899        async move { self.as_ref().load_latest_tree(height).await }
900    }
901
902    fn garbage_collect(&self, height: u64) -> impl Send + Future<Output = anyhow::Result<()>> {
903        async move { self.as_ref().garbage_collect(height).await }
904    }
905
906    fn persist_proofs(
907        &self,
908        height: u64,
909        proofs: impl Iterator<Item = (Vec<u8>, Vec<u8>)> + Send,
910    ) -> impl Send + Future<Output = anyhow::Result<()>> {
911        async move { self.as_ref().persist_proofs(height, proofs).await }
912    }
913
914    fn load_proof(
915        &self,
916        height: u64,
917        account: Vec<u8>,
918        epoch_height: u64,
919    ) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>> {
920        async move {
921            self.as_ref()
922                .load_proof(height, account, epoch_height)
923                .await
924        }
925    }
926
927    fn load_latest_proof(
928        &self,
929        account: Vec<u8>,
930    ) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>> {
931        async move { self.as_ref().load_latest_proof(account).await }
932    }
933
934    fn proof_exists(&self, height: u64) -> impl Send + Future<Output = bool> {
935        async move { self.as_ref().proof_exists(height).await }
936    }
937
938    fn persist_reward_proofs(
939        &self,
940        node_state: &NodeState,
941        height: u64,
942        version: Version,
943    ) -> impl Send + Future<Output = anyhow::Result<()>> {
944        async move {
945            self.as_ref()
946                .persist_reward_proofs(node_state, height, version)
947                .await
948        }
949    }
950}
951
952impl CatchupStorage for DataSource {
953    async fn get_accounts(
954        &self,
955        instance: &NodeState,
956        height: u64,
957        view: ViewNumber,
958        accounts: &[FeeAccount],
959    ) -> anyhow::Result<(FeeMerkleTree, Leaf2)> {
960        self.as_ref()
961            .get_accounts(instance, height, view, accounts)
962            .await
963    }
964
965    async fn get_reward_accounts_v2(
966        &self,
967        instance: &NodeState,
968        height: u64,
969        view: ViewNumber,
970        accounts: &[RewardAccountV2],
971    ) -> anyhow::Result<(RewardMerkleTreeV2, Leaf2)> {
972        self.as_ref()
973            .get_reward_accounts_v2(instance, height, view, accounts)
974            .await
975    }
976
977    async fn get_reward_accounts_v1(
978        &self,
979        instance: &NodeState,
980        height: u64,
981        view: ViewNumber,
982        accounts: &[RewardAccountV1],
983    ) -> anyhow::Result<(RewardMerkleTreeV1, Leaf2)> {
984        self.as_ref()
985            .get_reward_accounts_v1(instance, height, view, accounts)
986            .await
987    }
988
989    async fn get_frontier(
990        &self,
991        instance: &NodeState,
992        height: u64,
993        view: ViewNumber,
994    ) -> anyhow::Result<BlocksFrontier> {
995        self.as_ref().get_frontier(instance, height, view).await
996    }
997
998    async fn get_chain_config(
999        &self,
1000        commitment: Commitment<ChainConfig>,
1001    ) -> anyhow::Result<ChainConfig> {
1002        self.as_ref().get_chain_config(commitment).await
1003    }
1004    async fn get_leaf_chain(&self, height: u64) -> anyhow::Result<Vec<Leaf2>> {
1005        self.as_ref().get_leaf_chain(height).await
1006    }
1007
1008    async fn load_cert2(
1009        &self,
1010        height: u64,
1011    ) -> anyhow::Result<Option<espresso_types::Certificate2<SeqTypes>>> {
1012        self.as_ref().load_cert2(height).await
1013    }
1014
1015    async fn get_leaf(&self, height: u64) -> anyhow::Result<Leaf2> {
1016        self.as_ref().get_leaf(height).await
1017    }
1018}
1019
1020#[async_trait]
1021impl ChainConfigPersistence for Transaction<Write> {
1022    async fn insert_chain_config(&mut self, chain_config: ChainConfig) -> anyhow::Result<()> {
1023        let commitment = chain_config.commitment();
1024        let data = bincode::serialize(&chain_config)?;
1025        self.upsert(
1026            "chain_config",
1027            ["commitment", "data"],
1028            ["commitment"],
1029            [(commitment.to_string(), data)],
1030        )
1031        .await
1032    }
1033}
1034
1035impl super::data_source::PruningDataSource for SqlStorage {
1036    async fn get_oldest_block(
1037        &self,
1038    ) -> anyhow::Result<Option<hotshot_query_service::availability::BlockQueryData<SeqTypes>>> {
1039        let mut tx = self
1040            .read()
1041            .await
1042            .context("opening transaction to fetch oldest block")?;
1043        let row = sqlx::query("SELECT MIN(height) AS height FROM header")
1044            .fetch_one(tx.as_mut())
1045            .await
1046            .context("failed to query oldest block height")?;
1047        let height: Option<i64> = row.try_get("height")?;
1048        match height {
1049            None => Ok(None),
1050            Some(h) => {
1051                let h = usize::try_from(h).context("block height out of range")?;
1052                Ok(Some(
1053                    tx.get_block(hotshot_query_service::availability::BlockId::<SeqTypes>::from(h))
1054                        .await
1055                        .context(format!("block {h} not available"))?,
1056                ))
1057            },
1058        }
1059    }
1060
1061    async fn get_oldest_leaf(
1062        &self,
1063    ) -> anyhow::Result<Option<hotshot_query_service::availability::LeafQueryData<SeqTypes>>> {
1064        let mut tx = self
1065            .read()
1066            .await
1067            .context("opening transaction to fetch oldest leaf")?;
1068        let row = sqlx::query("SELECT MIN(height) AS height FROM leaf2")
1069            .fetch_one(tx.as_mut())
1070            .await
1071            .context("failed to query oldest leaf height")?;
1072        let height: Option<i64> = row.try_get("height")?;
1073        match height {
1074            None => Ok(None),
1075            Some(h) => {
1076                let h = usize::try_from(h).context("leaf height out of range")?;
1077                Ok(Some(
1078                    tx.get_leaf(LeafId::<SeqTypes>::from(h))
1079                        .await
1080                        .context(format!("leaf {h} not available"))?,
1081                ))
1082            },
1083        }
1084    }
1085}
1086
1087impl super::data_source::DatabaseMetadataSource for SqlStorage {
1088    async fn get_table_sizes(&self) -> anyhow::Result<Vec<super::data_source::TableSize>> {
1089        let mut tx = self
1090            .read()
1091            .await
1092            .context("opening transaction to fetch table sizes")?;
1093
1094        #[cfg(not(feature = "embedded-db"))]
1095        {
1096            let query = r#"
1097                SELECT
1098                    schemaname || '.' || relname AS table_name,
1099                    n_live_tup AS row_count,
1100                    pg_total_relation_size(relid) AS total_size_bytes
1101                FROM pg_stat_user_tables
1102                ORDER BY pg_total_relation_size(relid) DESC
1103            "#;
1104
1105            let rows = sqlx::query(query)
1106                .fetch_all(tx.as_mut())
1107                .await
1108                .context("failed to query table sizes")?;
1109
1110            let mut table_sizes = Vec::new();
1111            for row in rows {
1112                let table_name: String = row.try_get("table_name")?;
1113                let row_count: i64 = row.try_get("row_count").unwrap_or(-1);
1114                let total_size_bytes: Option<i64> = row.try_get("total_size_bytes").ok();
1115
1116                table_sizes.push(super::data_source::TableSize {
1117                    table_name,
1118                    row_count,
1119                    total_size_bytes,
1120                });
1121            }
1122
1123            Ok(table_sizes)
1124        }
1125
1126        #[cfg(feature = "embedded-db")]
1127        {
1128            // First, get all table names from sqlite_master
1129            let table_names_query = r#"
1130                SELECT name
1131                FROM sqlite_master
1132                WHERE type = 'table'
1133                AND name NOT LIKE 'sqlite_%'
1134                ORDER BY name
1135            "#;
1136
1137            let table_rows = sqlx::query(table_names_query)
1138                .fetch_all(tx.as_mut())
1139                .await
1140                .context("failed to query table names")?;
1141
1142            let mut table_sizes = Vec::new();
1143
1144            // For each table, get the row count
1145            for row in table_rows {
1146                let table_name: String = row.try_get("name")?;
1147
1148                // Run SELECT COUNT(*) for this specific table
1149                // Use format! here since we need to dynamically insert the table name
1150                let count_query = format!("SELECT COUNT(*) as count FROM \"{}\"", table_name);
1151                let count_row = sqlx::query(&count_query)
1152                    .fetch_one(tx.as_mut())
1153                    .await
1154                    .context(format!(
1155                        "failed to query row count for table {}",
1156                        table_name
1157                    ))?;
1158
1159                let row_count: i64 = count_row.try_get("count").unwrap_or(0);
1160
1161                table_sizes.push(super::data_source::TableSize {
1162                    table_name,
1163                    row_count,
1164                    total_size_bytes: None,
1165                });
1166            }
1167
1168            Ok(table_sizes)
1169        }
1170    }
1171
1172    async fn get_migration_status(
1173        &self,
1174    ) -> anyhow::Result<Vec<super::data_source::MigrationStatus>> {
1175        let mut tx = self
1176            .read()
1177            .await
1178            .context("opening transaction to fetch migration status")?;
1179
1180        type MigrationStatusRow = (
1181            String,
1182            chrono::DateTime<chrono::Utc>,
1183            Option<chrono::DateTime<chrono::Utc>>,
1184            Option<i64>,
1185        );
1186
1187        let rows: Vec<MigrationStatusRow> = sqlx::query_as(
1188            "SELECT name, started_at, completed_at, last_offset FROM deferred_migrations ORDER BY \
1189             started_at",
1190        )
1191        .fetch_all(tx.as_mut())
1192        .await
1193        .context("failed to query deferred_migrations")?;
1194
1195        Ok(rows
1196            .into_iter()
1197            .map(|(name, started_at, completed_at, last_offset)| {
1198                super::data_source::MigrationStatus {
1199                    name,
1200                    started_at,
1201                    completed_at,
1202                    last_offset,
1203                }
1204            })
1205            .collect())
1206    }
1207}
1208
1209impl super::data_source::DatabaseMetadataSource for DataSource {
1210    async fn get_table_sizes(&self) -> anyhow::Result<Vec<super::data_source::TableSize>> {
1211        self.as_ref().get_table_sizes().await
1212    }
1213
1214    async fn get_migration_status(
1215        &self,
1216    ) -> anyhow::Result<Vec<super::data_source::MigrationStatus>> {
1217        self.as_ref().get_migration_status().await
1218    }
1219}
1220
1221impl super::data_source::PruningDataSource for DataSource {
1222    async fn get_oldest_block(
1223        &self,
1224    ) -> anyhow::Result<Option<hotshot_query_service::availability::BlockQueryData<SeqTypes>>> {
1225        self.as_ref().get_oldest_block().await
1226    }
1227
1228    async fn get_oldest_leaf(
1229        &self,
1230    ) -> anyhow::Result<Option<hotshot_query_service::availability::LeafQueryData<SeqTypes>>> {
1231        self.as_ref().get_oldest_leaf().await
1232    }
1233}
1234
1235async fn load_frontier<Mode: TransactionMode>(
1236    tx: &mut Transaction<Mode>,
1237    height: u64,
1238) -> anyhow::Result<BlocksFrontier> {
1239    tx.get_path(
1240        Snapshot::<SeqTypes, BlockMerkleTree, { BlockMerkleTree::ARITY }>::Index(height),
1241        height
1242            .checked_sub(1)
1243            .ok_or(anyhow::anyhow!("Subtract with overflow ({height})!"))?,
1244    )
1245    .await
1246    .context(format!("fetching frontier at height {height}"))
1247}
1248
1249async fn load_v1_reward_accounts(
1250    db: &SqlStorage,
1251    height: u64,
1252    accounts: &[RewardAccountV1],
1253) -> anyhow::Result<(RewardMerkleTreeV1, Leaf2)> {
1254    // Open a new read transaction to get the leaf
1255    let mut tx = db
1256        .read()
1257        .await
1258        .with_context(|| "failed to open read transaction")?;
1259
1260    // Get the leaf from the database
1261    let leaf = tx
1262        .get_leaf(LeafId::<SeqTypes>::from(height as usize))
1263        .await
1264        .context(format!("leaf {height} not available"))?;
1265    let header = leaf.header();
1266
1267    if header.version() < EPOCH_VERSION || header.version() >= DRB_AND_HEADER_UPGRADE_VERSION {
1268        return Ok((
1269            RewardMerkleTreeV1::new(REWARD_MERKLE_TREE_V1_HEIGHT),
1270            leaf.leaf().clone(),
1271        ));
1272    }
1273
1274    // Get the merkle root from the header and create a snapshot from it
1275    let merkle_root = header.reward_merkle_tree_root().unwrap_left();
1276    let mut snapshot = RewardMerkleTreeV1::from_commitment(merkle_root);
1277
1278    // Create a bounded join set with 10 concurrent tasks
1279    let mut join_set = BoundedJoinSet::new(10);
1280
1281    // Create a map from task ID to account
1282    let mut task_id_to_account = HashMap::new();
1283
1284    // Loop through each account, spawning a task to get the path for the account
1285    for account in accounts {
1286        // Clone things we will need in the closure
1287        let db_clone = db.clone();
1288        let account_clone = *account;
1289        let header_height = header.height();
1290
1291        // Create the closure that will get the path for the account
1292        let func = async move {
1293            // Open a new transaction
1294            let mut tx = db_clone
1295                .read()
1296                .await
1297                .with_context(|| "failed to open read transaction")?;
1298
1299            // Get the path for the account
1300            let proof = tx
1301                .get_path(
1302                    Snapshot::<SeqTypes, RewardMerkleTreeV1, { RewardMerkleTreeV1::ARITY }>::Index(
1303                        header_height,
1304                    ),
1305                    account_clone,
1306                )
1307                .await
1308                .with_context(|| {
1309                    format!(
1310                        "failed to get path for v1 reward account {account_clone:?}; height \
1311                         {height}"
1312                    )
1313                })?;
1314
1315            Ok::<_, anyhow::Error>(proof)
1316        };
1317
1318        // Spawn the task
1319        let id = join_set.spawn(func).id();
1320
1321        // Add the task ID to the account map
1322        task_id_to_account.insert(id, account);
1323    }
1324
1325    // Wait for each task to complete
1326    while let Some(result) = join_set.join_next_with_id().await {
1327        // Get the inner result (past the join error)
1328        let (id, result) = result.with_context(|| "failed to join task")?;
1329
1330        // Get the proof from the result
1331        let proof = result?;
1332
1333        // Get the account from the task ID to account map
1334        let account = task_id_to_account
1335            .remove(&id)
1336            .with_context(|| "task ID for spawned task not found")?;
1337
1338        match proof.proof.first().with_context(|| {
1339            format!("empty proof for v1 reward account {account:?}; height {height}")
1340        })? {
1341            MerkleNode::Leaf { pos, elem, .. } => {
1342                snapshot.remember(*pos, *elem, proof)?;
1343            },
1344            MerkleNode::Empty => {
1345                snapshot.non_membership_remember(*account, proof)?;
1346            },
1347            _ => {
1348                bail!("invalid proof for v1 reward account {account:?}; height {height}");
1349            },
1350        }
1351    }
1352
1353    Ok((snapshot, leaf.leaf().clone()))
1354}
1355
1356/// Loads reward accounts for new reward merkle tree (V4).
1357async fn load_reward_merkle_tree_v2(
1358    db: &SqlStorage,
1359    height: u64,
1360) -> anyhow::Result<(PermittedRewardMerkleTreeV2, Leaf2)> {
1361    // Open a new read transaction to get the leaf
1362    let mut tx = db
1363        .read()
1364        .await
1365        .with_context(|| "failed to open read transaction")?;
1366
1367    // Get the leaf from the database
1368    let leaf = tx
1369        .get_leaf(LeafId::<SeqTypes>::from(height as usize))
1370        .await
1371        .with_context(|| format!("leaf {height} not available"))?;
1372
1373    let snapshot = db.load_reward_merkle_tree_v2(height).await?;
1374
1375    Ok((snapshot, leaf.leaf().clone()))
1376}
1377
1378async fn load_accounts<Mode: TransactionMode>(
1379    tx: &mut Transaction<Mode>,
1380    height: u64,
1381    accounts: &[FeeAccount],
1382) -> anyhow::Result<(FeeMerkleTree, Leaf2)> {
1383    let leaf = tx
1384        .get_leaf(LeafId::<SeqTypes>::from(height as usize))
1385        .await
1386        .context(format!("leaf {height} not available"))?;
1387    let header = leaf.header();
1388
1389    let mut snapshot = FeeMerkleTree::from_commitment(header.fee_merkle_tree_root());
1390    for account in accounts {
1391        let proof = tx
1392            .get_path(
1393                Snapshot::<SeqTypes, FeeMerkleTree, { FeeMerkleTree::ARITY }>::Index(
1394                    header.height(),
1395                ),
1396                *account,
1397            )
1398            .await
1399            .context(format!(
1400                "fetching account {account}; height {}",
1401                header.height()
1402            ))?;
1403        match proof.proof.first().context(format!(
1404            "empty proof for account {account}; height {}",
1405            header.height()
1406        ))? {
1407            MerkleNode::Leaf { pos, elem, .. } => {
1408                snapshot.remember(*pos, *elem, proof)?;
1409            },
1410            MerkleNode::Empty => {
1411                snapshot.non_membership_remember(*account, proof)?;
1412            },
1413            _ => {
1414                bail!("Invalid proof");
1415            },
1416        }
1417    }
1418
1419    Ok((snapshot, leaf.leaf().clone()))
1420}
1421
1422async fn load_chain_config<Mode: TransactionMode>(
1423    tx: &mut Transaction<Mode>,
1424    commitment: Commitment<ChainConfig>,
1425) -> anyhow::Result<ChainConfig> {
1426    let (data,) = query_as::<(Vec<u8>,)>("SELECT data from chain_config where commitment = $1")
1427        .bind(commitment.to_string())
1428        .fetch_one(tx.as_mut())
1429        .await
1430        .unwrap();
1431
1432    bincode::deserialize(&data[..]).context("failed to deserialize")
1433}
1434
1435/// Reconstructs the `ValidatedState` from a specific block height up to a given view.
1436///
1437/// This loads all required fee and reward accounts into the Merkle tree before applying the
1438/// State Transition Function (STF), preventing recursive catchup during STF replay.
1439///
1440/// Note: Even if the primary goal is to catch up the block Merkle tree,
1441/// fee and reward header dependencies must still be present beforehand
1442/// This is because reconstructing the `ValidatedState` involves replaying the STF over a
1443/// range of leaves, and the STF requires all associated data to be present in the `ValidatedState`;
1444/// otherwise, it will attempt to trigger catchup itself.
1445#[tracing::instrument(skip(instance, db, tx))]
1446pub(crate) async fn reconstruct_state<Mode: TransactionMode>(
1447    instance: &NodeState,
1448    db: &SqlStorage,
1449    tx: &mut Transaction<Mode>,
1450    from_height: u64,
1451    to_view: ViewNumber,
1452    fee_accounts: &[FeeAccount],
1453    reward_accounts: &[RewardAccountV2],
1454) -> anyhow::Result<(ValidatedState, Leaf2)> {
1455    tracing::info!("attempting to reconstruct fee state");
1456    let from_leaf = tx
1457        .get_leaf((from_height as usize).into())
1458        .await
1459        .context(format!("leaf {from_height} not available"))?;
1460    let from_leaf: Leaf2 = from_leaf.leaf().clone();
1461    ensure!(
1462        from_leaf.view_number() < to_view,
1463        "state reconstruction: starting state {:?} must be before ending state {to_view:?}",
1464        from_leaf.view_number(),
1465    );
1466
1467    // Get the sequence of headers we will be applying to compute the latest state.
1468    let mut leaves = VecDeque::new();
1469    let to_leaf = get_leaf_from_proposal(tx, "view = $1", &(to_view.u64() as i64))
1470        .await
1471        .context(format!(
1472            "unable to reconstruct state because leaf {to_view:?} is not available"
1473        ))?;
1474    let mut parent = to_leaf.parent_commitment();
1475    tracing::debug!(?to_leaf, ?parent, view = ?to_view, "have required leaf");
1476    leaves.push_front(to_leaf.clone());
1477    while parent != Committable::commit(&from_leaf) {
1478        let leaf = get_leaf_from_proposal(tx, "leaf_hash = $1", &parent.to_string())
1479            .await
1480            .context(format!(
1481                "unable to reconstruct state because leaf {parent} is not available"
1482            ))?;
1483        parent = leaf.parent_commitment();
1484        tracing::debug!(?leaf, ?parent, "have required leaf");
1485        leaves.push_front(leaf);
1486    }
1487
1488    // Get the initial state.
1489    let mut parent = from_leaf;
1490    let mut state = ValidatedState::from_header(parent.block_header());
1491
1492    // Pre-load the state with the accounts we care about to ensure they will be present in the
1493    // final state.
1494    let mut catchup = NullStateCatchup::default();
1495
1496    let mut fee_accounts = fee_accounts.iter().copied().collect::<HashSet<_>>();
1497    // Add in all the accounts we will need to replay any of the headers, to ensure that we don't
1498    // need to do catchup recursively.
1499    tracing::info!(
1500        "reconstructing fee accounts state for from height {from_height} to view {to_view}"
1501    );
1502
1503    let dependencies =
1504        fee_header_dependencies(&mut catchup, tx, instance, &parent, &leaves).await?;
1505    fee_accounts.extend(dependencies);
1506    let fee_accounts = fee_accounts.into_iter().collect::<Vec<_>>();
1507    state.fee_merkle_tree = load_accounts(tx, from_height, &fee_accounts)
1508        .await
1509        .context("unable to reconstruct state because accounts are not available at origin")?
1510        .0;
1511    ensure!(
1512        state.fee_merkle_tree.commitment() == parent.block_header().fee_merkle_tree_root(),
1513        "loaded fee state does not match parent header"
1514    );
1515
1516    tracing::info!(
1517        "reconstructing reward accounts for from height {from_height} to view {to_view}"
1518    );
1519
1520    let mut reward_accounts = reward_accounts.iter().copied().collect::<HashSet<_>>();
1521
1522    // Collect all reward account dependencies needed for replaying the STF.
1523    // These accounts must be preloaded into the reward Merkle tree to prevent recursive catchups.
1524    let dependencies = reward_header_dependencies(instance, &leaves).await?;
1525    reward_accounts.extend(dependencies);
1526    let reward_accounts = reward_accounts.into_iter().collect::<Vec<_>>();
1527
1528    // Load all required reward accounts and update the reward Merkle tree.
1529    match parent.block_header().reward_merkle_tree_root() {
1530        either::Either::Left(expected_root) => {
1531            let accts = reward_accounts
1532                .into_iter()
1533                .map(RewardAccountV1::from)
1534                .collect::<Vec<_>>();
1535            state.reward_merkle_tree_v1 = load_v1_reward_accounts(db, from_height, &accts)
1536                .await
1537                .context(
1538                    "unable to reconstruct state because v1 reward accounts are not available at \
1539                     origin",
1540                )?
1541                .0;
1542            ensure!(
1543                state.reward_merkle_tree_v1.commitment() == expected_root,
1544                "loaded v1 reward state does not match parent header"
1545            );
1546        },
1547        either::Either::Right(expected_root) => {
1548            let version = parent.block_header().version();
1549            let epoch_height = instance
1550                .epoch_height
1551                .context("epoch_height not set but parent has V2 reward tree")?;
1552
1553            // V5+ only stores the reward merkle tree at epoch's last block, so if we're
1554            // reconstructing at a non boundary height there is no row at from_height.
1555            // Load the latest tree instead
1556            // the commitment check below will catch it if
1557            // we ended up with a tree from an older epoch.
1558            // But this should never happen as we don't garbage collect the latest tree
1559            if version >= EPOCH_REWARD_VERSION && !is_last_block(from_height, epoch_height) {
1560                let tree = db
1561                    .load_latest_reward_merkle_tree_v2(from_height)
1562                    .await
1563                    .context("RewardMerkleTreeV2 not available at or below origin")?;
1564                state.reward_merkle_tree_v2 = tree.tree;
1565            } else {
1566                state.reward_merkle_tree_v2 = load_reward_merkle_tree_v2(db, from_height)
1567                    .await
1568                    .context("RewardMerkleTreeV2 not available at origin")?
1569                    .0
1570                    .tree;
1571            }
1572            ensure!(
1573                state.reward_merkle_tree_v2.commitment() == expected_root,
1574                "loaded reward state does not match parent header"
1575            );
1576        },
1577    }
1578
1579    // We need the blocks frontier as well, to apply the STF.
1580    let frontier = load_frontier(tx, from_height)
1581        .await
1582        .context("unable to reconstruct state because frontier is not available at origin")?;
1583    match frontier
1584        .proof
1585        .first()
1586        .context("empty proof for frontier at origin")?
1587    {
1588        MerkleNode::Leaf { pos, elem, .. } => state
1589            .block_merkle_tree
1590            .remember(*pos, *elem, frontier)
1591            .context("failed to remember frontier")?,
1592        _ => bail!("invalid frontier proof"),
1593    }
1594
1595    // Apply subsequent headers to compute the later state.
1596    for proposal in leaves {
1597        state = compute_state_update(&state, instance, &catchup, &parent, &proposal)
1598            .await
1599            .context(format!(
1600                "unable to reconstruct state because state update {} failed",
1601                proposal.height(),
1602            ))?
1603            .0;
1604        parent = proposal;
1605    }
1606
1607    tracing::info!(from_height, ?to_view, "successfully reconstructed state");
1608    Ok((state, to_leaf))
1609}
1610
1611/// Get the dependencies needed to apply the STF to the given list of headers.
1612///
1613/// Returns
1614/// * A state catchup implementation seeded with all the chain configs required to apply the headers
1615///   in `leaves`
1616/// * The set of accounts that must be preloaded to apply these headers
1617async fn fee_header_dependencies<Mode: TransactionMode>(
1618    catchup: &mut NullStateCatchup,
1619    tx: &mut Transaction<Mode>,
1620    instance: &NodeState,
1621    mut parent: &Leaf2,
1622    leaves: impl IntoIterator<Item = &Leaf2>,
1623) -> anyhow::Result<HashSet<FeeAccount>> {
1624    let mut accounts = HashSet::default();
1625
1626    for proposal in leaves {
1627        let header = proposal.block_header();
1628        let height = header.height();
1629        let view = proposal.view_number();
1630        tracing::debug!(height, ?view, "fetching dependencies for proposal");
1631
1632        let header_cf = header.chain_config();
1633        let chain_config = if header_cf.commit() == instance.chain_config.commit() {
1634            instance.chain_config
1635        } else {
1636            match header_cf.resolve() {
1637                Some(cf) => cf,
1638                None => {
1639                    tracing::info!(
1640                        height,
1641                        ?view,
1642                        commit = %header_cf.commit(),
1643                        "chain config not available, attempting to load from storage",
1644                    );
1645                    let cf = load_chain_config(tx, header_cf.commit())
1646                        .await
1647                        .context(format!(
1648                            "loading chain config {} for header {},{:?}",
1649                            header_cf.commit(),
1650                            header.height(),
1651                            proposal.view_number()
1652                        ))?;
1653
1654                    // If we had to fetch a chain config now, store it in the catchup implementation
1655                    // so the STF will be able to look it up later.
1656                    catchup.add_chain_config(cf);
1657                    cf
1658                },
1659            }
1660        };
1661
1662        accounts.insert(chain_config.fee_recipient);
1663        accounts.extend(
1664            get_l1_deposits(instance, header, parent, chain_config.fee_contract)
1665                .await
1666                .into_iter()
1667                .map(|fee| fee.account()),
1668        );
1669        accounts.extend(header.fee_info().accounts());
1670        parent = proposal;
1671    }
1672    Ok(accounts)
1673}
1674
1675/// Identifies all reward accounts required to replay the State Transition Function
1676/// for the given leaf proposals. These accounts should be present in the Merkle tree
1677/// *before* applying the STF to avoid recursive catchup (i.e., STF triggering another catchup).
1678async fn reward_header_dependencies(
1679    instance: &NodeState,
1680    leaves: impl IntoIterator<Item = &Leaf2>,
1681) -> anyhow::Result<HashSet<RewardAccountV2>> {
1682    let mut reward_accounts = HashSet::default();
1683    let epoch_height = instance.epoch_height;
1684
1685    let Some(epoch_height) = epoch_height else {
1686        tracing::info!("epoch height is not set. returning empty reward_header_dependencies");
1687        return Ok(HashSet::new());
1688    };
1689
1690    let coordinator = instance.coordinator.clone();
1691    let first_epoch = coordinator.membership().first_epoch();
1692    // add all the chain configs needed to apply STF to headers to the catchup
1693    for proposal in leaves {
1694        let header = proposal.block_header();
1695
1696        let height = header.height();
1697        let view = proposal.view_number();
1698        tracing::debug!(height, ?view, "fetching dependencies for proposal");
1699
1700        let version = header.version();
1701        // Skip if version is less than epoch version
1702        if version < EPOCH_VERSION || version >= EPOCH_REWARD_VERSION {
1703            continue;
1704        }
1705
1706        let first_epoch = first_epoch.context("first epoch not found")?;
1707
1708        let proposal_epoch = EpochNumber::new(epoch_from_block_number(height, epoch_height));
1709
1710        // reward distribution starts third epoch onwards
1711        if proposal_epoch <= first_epoch + 1 {
1712            continue;
1713        }
1714
1715        let epoch_membership = match coordinator.membership_for_epoch(Some(proposal_epoch)) {
1716            Ok(e) => e,
1717            Err(err) => {
1718                tracing::info!(
1719                    "failed to get membership for epoch={proposal_epoch:?}. err={err:#}"
1720                );
1721
1722                coordinator
1723                    .wait_for_catchup(proposal_epoch)
1724                    .await
1725                    .context(format!("failed to catchup for epoch={proposal_epoch}"))?
1726            },
1727        };
1728
1729        let snapshot = epoch_membership
1730            .snapshot()
1731            .with_context(|| format!("no committee for epoch={proposal_epoch}"))?;
1732        let leader = snapshot.lookup_leader(proposal.view_number())?;
1733        let validator = snapshot.validator_config(&leader)?;
1734
1735        reward_accounts.insert(RewardAccountV2(validator.account));
1736
1737        let delegators: Vec<RewardAccountV2> = validator
1738            .delegators
1739            .keys()
1740            .map(|d| RewardAccountV2(*d))
1741            .collect();
1742
1743        reward_accounts.extend(delegators);
1744    }
1745    Ok(reward_accounts)
1746}
1747
1748async fn get_leaf_from_proposal<Mode, P>(
1749    tx: &mut Transaction<Mode>,
1750    where_clause: &str,
1751    param: P,
1752) -> anyhow::Result<Leaf2>
1753where
1754    P: Type<Db> + for<'q> Encode<'q, Db>,
1755{
1756    let (data,) = query_as::<(Vec<u8>,)>(&format!(
1757        "SELECT data FROM quorum_proposals2 WHERE {where_clause} LIMIT 1",
1758    ))
1759    .bind(param)
1760    .fetch_one(tx.as_mut())
1761    .await?;
1762    let proposal: Proposal<SeqTypes, QuorumProposalWrapper<SeqTypes>> =
1763        bincode::deserialize(&data)?;
1764    Ok(Leaf2::from_quorum_proposal(&proposal.data))
1765}
1766
1767#[cfg(any(test, feature = "testing"))]
1768pub(crate) mod impl_testable_data_source {
1769
1770    use hotshot_query_service::data_source::storage::sql::testing::TmpDb;
1771
1772    use super::*;
1773    use crate::api::{self, data_source::testing::TestableSequencerDataSource, options::Query};
1774
1775    pub fn tmp_options(db: &TmpDb) -> Options {
1776        #[cfg(not(feature = "embedded-db"))]
1777        {
1778            let opt = crate::persistence::sql::PostgresOptions {
1779                port: Some(db.port()),
1780                host: Some(db.host()),
1781                user: Some("postgres".into()),
1782                password: Some("password".into()),
1783                ..Default::default()
1784            };
1785
1786            opt.into()
1787        }
1788
1789        #[cfg(feature = "embedded-db")]
1790        {
1791            let opt = crate::persistence::sql::SqliteOptions { path: db.path() };
1792            opt.into()
1793        }
1794    }
1795
1796    #[async_trait]
1797    impl TestableSequencerDataSource for DataSource {
1798        type Storage = TmpDb;
1799
1800        async fn create_storage() -> Self::Storage {
1801            TmpDb::init().await
1802        }
1803
1804        fn persistence_options(storage: &Self::Storage) -> Self::Options {
1805            tmp_options(storage)
1806        }
1807
1808        fn leaf_only_ds_options(
1809            storage: &Self::Storage,
1810            opt: api::Options,
1811        ) -> anyhow::Result<api::Options> {
1812            let mut ds_opts = tmp_options(storage);
1813            ds_opts.lightweight = true;
1814            Ok(opt.query_sql(Default::default(), ds_opts))
1815        }
1816
1817        fn options(storage: &Self::Storage, opt: api::Options) -> api::Options {
1818            opt.query_sql(Query::default(), tmp_options(storage))
1819        }
1820    }
1821}
1822
1823#[cfg(test)]
1824mod tests {
1825    use espresso_types::v0_4::{REWARD_MERKLE_TREE_V2_HEIGHT, RewardMerkleTreeV2};
1826    use hotshot_query_service::{
1827        data_source::{
1828            Transaction, VersionedDataSource,
1829            sql::Config,
1830            storage::{
1831                UpdateAvailabilityStorage,
1832                sql::{
1833                    SqlStorage, StorageConnectionType, Transaction as SqlTransaction, Write,
1834                    testing::TmpDb,
1835                },
1836            },
1837        },
1838        merklized_state::MerklizedState,
1839    };
1840    use jf_merkle_tree_compat::MerkleTreeScheme;
1841    use light_client::testing::{leaf_chain, leaf_chain_with_upgrade};
1842    use versions::{DRB_AND_HEADER_UPGRADE_VERSION, EPOCH_REWARD_VERSION, Upgrade};
1843
1844    use super::impl_testable_data_source::tmp_options;
1845    use crate::api::RewardMerkleTreeDataSource;
1846
1847    async fn insert_test_header(
1848        tx: &mut SqlTransaction<Write>,
1849        block_height: u64,
1850        reward_tree: &RewardMerkleTreeV2,
1851    ) {
1852        let reward_commitment = serde_json::to_value(reward_tree.commitment()).unwrap();
1853        let test_data = serde_json::json!({
1854            "block_merkle_tree_root": format!("block_root_{}", block_height),
1855            "fee_merkle_tree_root": format!("fee_root_{}", block_height),
1856            "fields": {
1857                RewardMerkleTreeV2::header_state_commitment_field(): reward_commitment
1858            }
1859        });
1860        tx.upsert(
1861            "header",
1862            [
1863                "height",
1864                "hash",
1865                "payload_hash",
1866                "timestamp",
1867                "data",
1868                "ns_table",
1869            ],
1870            ["height"],
1871            [(
1872                block_height as i64,
1873                format!("hash_{}", block_height),
1874                format!("payload_{}", block_height),
1875                block_height as i64,
1876                test_data,
1877                "ns_table".to_string(),
1878            )],
1879        )
1880        .await
1881        .unwrap();
1882    }
1883
1884    #[tokio::test]
1885    #[test_log::test]
1886    async fn test_merkle_proof_gc() {
1887        let db = TmpDb::init().await;
1888        let opt = tmp_options(&db);
1889        let cfg = Config::try_from(&opt).expect("failed to create config from options");
1890        let storage = SqlStorage::connect(cfg, StorageConnectionType::Query)
1891            .await
1892            .expect("failed to connect to storage");
1893
1894        let account = vec![0; 32];
1895
1896        // Insert mock leaves at heights 0-2 so load_proof can check leaf version.
1897        let leaves = leaf_chain(0..=2, DRB_AND_HEADER_UPGRADE_VERSION).await;
1898        let mut tx = storage.write().await.unwrap();
1899        for leaf in &leaves {
1900            tx.insert_leaf(leaf).await.unwrap();
1901        }
1902        Transaction::commit(tx).await.unwrap();
1903
1904        // Insert some mock proofs at heights 0-2.
1905        for h in 0..=2 {
1906            storage
1907                .persist_proofs(h, [(account.clone(), h.to_le_bytes().to_vec())].into_iter())
1908                .await
1909                .unwrap();
1910        }
1911
1912        // Test garbage collection.
1913        storage.garbage_collect(2).await.unwrap();
1914
1915        // Make sure the proof at height 2 is still available.
1916        assert_eq!(
1917            storage.load_proof(2, account.clone(), 0).await.unwrap(),
1918            2u64.to_le_bytes()
1919        );
1920
1921        // Meanwhile, the proofs at heights 0-1 have been garbage collected.
1922        for h in 0..2 {
1923            let err = storage.load_proof(h, account.clone(), 0).await.unwrap_err();
1924            assert!(err.to_string().contains("Missing proof"), "{err:#}");
1925        }
1926
1927        // Garbage collect the remaining proof.
1928        storage.garbage_collect(3).await.unwrap();
1929        let err = storage.load_proof(2, account, 0).await.unwrap_err();
1930        assert!(err.to_string().contains("Missing proof"), "{err:#}");
1931    }
1932
1933    #[tokio::test]
1934    #[test_log::test]
1935    async fn test_load_proof_v5_epoch_boundary() {
1936        let db = TmpDb::init().await;
1937        let opt = tmp_options(&db);
1938        let cfg = Config::try_from(&opt).expect("failed to create config from options");
1939        let storage = SqlStorage::connect(cfg, StorageConnectionType::Query)
1940            .await
1941            .expect("failed to connect to storage");
1942
1943        let epoch_height = 10u64;
1944        let account = vec![0; 32];
1945
1946        // Create V5 leaves at heights 0..=15.
1947        let leaves = leaf_chain(0..=15, EPOCH_REWARD_VERSION).await;
1948        let mut tx = storage.write().await.unwrap();
1949        for leaf in &leaves {
1950            tx.insert_leaf(leaf).await.unwrap();
1951        }
1952        Transaction::commit(tx).await.unwrap();
1953
1954        // Store proofs only at epoch boundaries (height 10).
1955        let boundary_proof = b"proof_at_10".to_vec();
1956        {
1957            let mut tx = storage.write().await.unwrap();
1958            tx.upsert(
1959                "reward_merkle_tree_v2_proofs",
1960                ["height", "account", "proof"],
1961                ["height", "account"],
1962                [(10i64, account.clone(), boundary_proof.clone())],
1963            )
1964            .await
1965            .unwrap();
1966            Transaction::commit(tx).await.unwrap();
1967        }
1968
1969        // Querying at the epoch boundary itself should return the proof directly.
1970        assert_eq!(
1971            storage
1972                .load_proof(10, account.clone(), epoch_height)
1973                .await
1974                .unwrap(),
1975            boundary_proof,
1976        );
1977
1978        // Querying at a non-boundary V5 height (e.g. 15) should resolve to the
1979        // previous epoch boundary (10) and return its proof.
1980        assert_eq!(
1981            storage
1982                .load_proof(15, account.clone(), epoch_height)
1983                .await
1984                .unwrap(),
1985            boundary_proof,
1986        );
1987    }
1988
1989    #[tokio::test]
1990    #[test_log::test]
1991    async fn test_load_proof_v4_to_v5_upgrade_boundary() {
1992        let db = TmpDb::init().await;
1993        let opt = tmp_options(&db);
1994        let cfg = Config::try_from(&opt).expect("failed to create config from options");
1995        let storage = SqlStorage::connect(cfg, StorageConnectionType::Query)
1996            .await
1997            .expect("failed to connect to storage");
1998
1999        let epoch_height = 10u64;
2000        let account = vec![0; 32];
2001
2002        // V4 leaves at heights 0..=10, V5 from height 11 onward.
2003        // The upgrade happens at height 11.
2004        let upgrade = Upgrade::new(DRB_AND_HEADER_UPGRADE_VERSION, EPOCH_REWARD_VERSION);
2005        let leaves = leaf_chain_with_upgrade(0..=15, 11, upgrade).await;
2006        {
2007            let mut tx = storage.write().await.unwrap();
2008            for leaf in &leaves {
2009                tx.insert_leaf(leaf).await.unwrap();
2010            }
2011            Transaction::commit(tx).await.unwrap();
2012        }
2013
2014        // Querying at V5 height 15 resolves to prev epoch boundary
2015        // But leaf at height 10 is V4, so this should fail.
2016        storage
2017            .load_proof(15, account.clone(), epoch_height)
2018            .await
2019            .unwrap_err();
2020
2021        let v4_proof = b"v4_proof_at_5".to_vec();
2022        {
2023            let mut tx = storage.write().await.unwrap();
2024            tx.upsert(
2025                "reward_merkle_tree_v2_proofs",
2026                ["height", "account", "proof"],
2027                ["height", "account"],
2028                [(5i64, account.clone(), v4_proof.clone())],
2029            )
2030            .await
2031            .unwrap();
2032            Transaction::commit(tx).await.unwrap();
2033        }
2034        assert_eq!(
2035            storage.load_proof(5, account.clone(), 0).await.unwrap(),
2036            v4_proof,
2037        );
2038    }
2039
2040    #[test_log::test(tokio::test(flavor = "multi_thread"))]
2041    async fn test_get_table_sizes() {
2042        use super::super::data_source::DatabaseMetadataSource;
2043
2044        let db = TmpDb::init().await;
2045        let opt = tmp_options(&db);
2046        let cfg = Config::try_from(&opt).expect("failed to create config from options");
2047        let storage = SqlStorage::connect(cfg, StorageConnectionType::Query)
2048            .await
2049            .expect("failed to connect to storage");
2050
2051        // Insert some test data to ensure tables have rows
2052        let mut tx = storage.write().await.unwrap();
2053
2054        // Insert a test header
2055        let reward_tree = RewardMerkleTreeV2::new(REWARD_MERKLE_TREE_V2_HEIGHT);
2056        insert_test_header(&mut tx, 1, &reward_tree).await;
2057
2058        tx.commit().await.unwrap();
2059
2060        // Call get_table_sizes and verify it doesn't error
2061        let table_sizes = storage
2062            .get_table_sizes()
2063            .await
2064            .expect("get_table_sizes should succeed");
2065
2066        // Verify we got some tables back
2067        assert!(
2068            !table_sizes.is_empty(),
2069            "should have at least one table in the database"
2070        );
2071    }
2072}