Skip to main content

espresso_node/
state.rs

1use core::fmt::Debug;
2use std::{ops::Range, sync::Arc, time::Duration};
3
4use anyhow::{Context, bail, ensure};
5use async_lock::Mutex;
6use committable::Committable;
7use either::Either;
8use espresso_types::{
9    BlockMerkleTree, EpochRewardsCalculator, FeeAccount, FeeMerkleTree, Leaf2, ValidatedState,
10    traits::StateCatchup,
11    v0_3::{ChainConfig, RewardMerkleTreeV1},
12    v0_4::{Delta, PermittedRewardMerkleTreeV2},
13};
14use futures::{StreamExt, future::Future};
15use hotshot_query_service::{
16    availability::{self, AvailabilityDataSource},
17    data_source::{Transaction, VersionedDataSource, storage::pruning::PrunedHeightDataSource},
18    merklized_state::{MerklizedStateHeightPersistence, UpdateStateData},
19    status::StatusDataSource,
20    types::HeightIndexed,
21};
22use hotshot_types::{traits::ValidatedState as HotShotState, utils::is_last_block};
23use jf_merkle_tree_compat::{
24    LookupResult, MerkleTreeScheme, ToTraversalPath, UniversalMerkleTreeScheme,
25};
26use tokio::time::{sleep, timeout};
27use vbs::version::Version;
28use versions::{DRB_AND_HEADER_UPGRADE_VERSION, EPOCH_REWARD_VERSION, EPOCH_VERSION};
29
30use crate::{
31    NodeState, SeqTypes,
32    api::{LightClientProvider, RewardMerkleTreeDataSource},
33    catchup::{CatchupStorage, SingleAttemptCatchup},
34    persistence::ChainConfigPersistence,
35};
36
37pub(crate) async fn compute_state_update(
38    parent_state: &ValidatedState,
39    instance: &NodeState,
40    peers: &impl StateCatchup,
41    parent_leaf: &Leaf2,
42    proposed_leaf: &Leaf2,
43) -> anyhow::Result<(ValidatedState, Delta)> {
44    let header = proposed_leaf.block_header();
45
46    let mut parent_state = parent_state.clone();
47
48    // if the protocol has been upgraded, the new chain_config should be used
49    // as the base chain config for the call to `apply_header`. This mirrors the
50    // `apply_upgrade` step at the start of `apply_header`
51    //
52    // We need to do this here because this loop may need to process historical upgrades
53    // that are no longer recorded in our genesis file. but it's safe, because this loop
54    // only handles decided leaves
55    if proposed_leaf.block_header().version() > parent_leaf.block_header().version() {
56        parent_state.chain_config = proposed_leaf.block_header().chain_config()
57    }
58
59    let (state, delta, total_rewards_distributed) = parent_state
60        .apply_header(
61            instance,
62            peers,
63            parent_leaf,
64            header,
65            header.version(),
66            proposed_leaf.view_number(),
67        )
68        .await?;
69
70    // Check internal consistency.
71    ensure!(
72        state.chain_config.commit() == header.chain_config().commit(),
73        "internal error! in-memory chain config {:?} does not match header {:?}",
74        state.chain_config,
75        header.chain_config(),
76    );
77    ensure!(
78        state.block_merkle_tree.commitment() == header.block_merkle_tree_root(),
79        "internal error! in-memory block tree {} does not match header {}",
80        state.block_merkle_tree.commitment(),
81        header.block_merkle_tree_root()
82    );
83    ensure!(
84        state.fee_merkle_tree.commitment() == header.fee_merkle_tree_root(),
85        "internal error! in-memory fee tree {} does not match header {}",
86        state.fee_merkle_tree.commitment(),
87        header.fee_merkle_tree_root()
88    );
89
90    match header.reward_merkle_tree_root() {
91        Either::Left(v1_root) => {
92            ensure!(
93                state.reward_merkle_tree_v1.commitment() == v1_root,
94                "internal error! in-memory v1 reward tree {} does not match header {}",
95                state.reward_merkle_tree_v1.commitment(),
96                v1_root
97            )
98        },
99        Either::Right(v2_root) => {
100            ensure!(
101                state.reward_merkle_tree_v2.commitment() == v2_root,
102                "internal error! in-memory v2 reward tree {} does not match header {}",
103                state.reward_merkle_tree_v2.commitment(),
104                v2_root
105            )
106        },
107    }
108
109    if header.version() >= DRB_AND_HEADER_UPGRADE_VERSION {
110        let Some(actual_total) = total_rewards_distributed else {
111            bail!(
112                "internal error! total_rewards_distributed is None for version {:?}",
113                header.version()
114            );
115        };
116
117        let Some(proposed_total) = header.total_reward_distributed() else {
118            bail!(
119                "internal error! proposed header.total_reward_distributed() is None for version \
120                 {:?}",
121                header.version()
122            );
123        };
124
125        ensure!(
126            proposed_total == actual_total,
127            "Total rewards mismatch: proposed header has {proposed_total} but actual total is \
128             {actual_total}",
129        );
130    }
131
132    Ok((state, delta))
133}
134
135async fn store_state_update(
136    tx: &mut impl SequencerStateUpdate,
137    block_number: u64,
138    _version: Version,
139    state: &ValidatedState,
140    delta: &Delta,
141) -> anyhow::Result<()> {
142    let ValidatedState {
143        fee_merkle_tree,
144        block_merkle_tree,
145        ..
146    } = state;
147    let Delta { fees_delta, .. } = delta;
148
149    // Collect fee merkle tree proofs for batch insertion
150    let fee_proofs: Vec<_> = fees_delta
151        .iter()
152        .map(|delta| {
153            let proof = match fee_merkle_tree.universal_lookup(*delta) {
154                LookupResult::Ok(_, proof) => proof,
155                LookupResult::NotFound(proof) => proof,
156                LookupResult::NotInMemory => bail!("missing merkle path for fee account {delta}"),
157            };
158            let path = FeeAccount::to_traversal_path(delta, fee_merkle_tree.height());
159            Ok((proof, path))
160        })
161        .collect::<anyhow::Result<Vec<_>>>()?;
162
163    tracing::debug!(count = fee_proofs.len(), "inserting fee accounts in batch");
164    UpdateStateData::<SeqTypes, FeeMerkleTree, { FeeMerkleTree::ARITY }>::insert_merkle_nodes_batch(
165        tx,
166        fee_proofs,
167        block_number,
168    )
169    .await
170    .context("failed to store fee merkle nodes")?;
171
172    store_block_frontier(tx, block_number, block_merkle_tree).await
173}
174
175/// Store the frontier of `block_merkle_tree`, the block tree at `block_number`, as that snapshot's
176/// block tree nodes.
177async fn store_block_frontier(
178    tx: &mut impl SequencerStateUpdate,
179    block_number: u64,
180    block_merkle_tree: &BlockMerkleTree,
181) -> anyhow::Result<()> {
182    let (_, proof) = block_merkle_tree
183        .lookup(block_number - 1)
184        .expect_ok()
185        .context("getting blocks frontier")?;
186    let path = <u64 as ToTraversalPath<{ BlockMerkleTree::ARITY }>>::to_traversal_path(
187        &(block_number - 1),
188        block_merkle_tree.height(),
189    );
190
191    tracing::debug!("inserting blocks frontier");
192    UpdateStateData::<SeqTypes, BlockMerkleTree, { BlockMerkleTree::ARITY }>::insert_merkle_nodes(
193        tx,
194        proof,
195        path,
196        block_number,
197    )
198    .await
199    .context("failed to store block merkle nodes")
200}
201
202#[tracing::instrument(
203    skip_all,
204    fields(
205        node_id = instance.node_id,
206        view = ?parent_leaf.view_number(),
207        height = parent_leaf.height(),
208    ),
209)]
210async fn update_state_storage<T>(
211    parent_state: &ValidatedState,
212    storage: &Arc<T>,
213    instance: &NodeState,
214    peers: &impl StateCatchup,
215    parent_leaf: &Leaf2,
216    proposed_leaf: &Leaf2,
217) -> anyhow::Result<ValidatedState>
218where
219    T: SequencerStateDataSource,
220    for<'a> T::Transaction<'a>: SequencerStateUpdate,
221{
222    let parent_chain_config = parent_state.chain_config;
223    let block_number = proposed_leaf.height();
224    let version = proposed_leaf.block_header().version();
225
226    let (state, delta) =
227        compute_state_update(parent_state, instance, peers, parent_leaf, proposed_leaf)
228            .await
229            .context("computing state update")?;
230
231    let has_changed_accounts = version > EPOCH_VERSION && !delta.rewards_delta.is_empty();
232    // For EPOCH_REWARD_VERSION+ we must persist the reward tree at every epoch
233    // boundary, even when no rewards were distributed. During a V4→V5 upgrade
234    // the first post upgrade epoch boundary skips rewards (the previous epoch's
235    // header is pre-V5), leaving rewards_delta empty. Without saving here, the
236    // tree would be missing from storage and catchup requests from peers or
237    // subsequent epoch reward calculations would fail.
238    //
239    // Example: V4→V5 upgrade at block 9756, epoch_height=3000.
240    // At block 12000 (first epoch boundary post upgrade), handle_epoch_rewards
241    // skips rewards because the previous epoch's boundary (block 9000) is pre-V5,
242    // so rewards_delta is empty. Without saving here, the tree is never persisted
243    // at height 12000. Later at block 15000, the epoch 4 reward calculation calls
244    // fetch_reward_merkle_tree_v2(height=12000) to catch up missing accounts and
245    // fails because no tree exists in storage at that height.
246    let is_epoch_boundary = version >= EPOCH_REWARD_VERSION
247        && is_last_block(
248            block_number,
249            instance
250                .epoch_height
251                .expect("epoch_height should be set for version > V3"),
252        );
253
254    if has_changed_accounts || is_epoch_boundary {
255        storage
256            .save_and_gc_reward_tree_v2(
257                instance,
258                block_number,
259                version,
260                &state.reward_merkle_tree_v2,
261            )
262            .await
263            .context("failed to save and gc reward merkle tree v2")?;
264    }
265
266    storage
267        .persist_reward_proofs(instance, block_number, version)
268        .await
269        .context("failed to persist reward proofs")?;
270
271    // The nodes and the new head commit together, so no node version ever exists above the head:
272    // the head snapshot is the newest version of everything it touches, which is what keeps it
273    // intact under pruning and lets the loop resume from it after a crash.
274    tracing::debug!("storing state update");
275    let mut tx = storage
276        .write()
277        .await
278        .context("opening transaction for state update")?;
279
280    store_state_update(&mut tx, block_number, version, &state, &delta).await?;
281
282    if parent_chain_config != state.chain_config {
283        let cf = state
284            .chain_config
285            .resolve()
286            .context("failed to resolve to chain config")?;
287
288        tx.insert_chain_config(cf).await?;
289    }
290
291    tracing::debug!(block_number, "updating state height");
292    UpdateStateData::<SeqTypes, _, { BlockMerkleTree::ARITY }>::set_last_state_height(
293        &mut tx,
294        block_number as usize,
295    )
296    .await
297    .context("setting state height")?;
298
299    tx.commit().await?;
300
301    Ok(state)
302}
303
304async fn store_genesis_state<S>(
305    storage: &S,
306    chain_config: ChainConfig,
307    state: &ValidatedState,
308) -> anyhow::Result<()>
309where
310    S: SequencerStateDataSource,
311    for<'a> S::Transaction<'a>: SequencerStateUpdate,
312{
313    ensure!(
314        state.block_merkle_tree.num_leaves() == 0,
315        "genesis state with non-empty block tree is unsupported"
316    );
317
318    let mut tx = storage
319        .write()
320        .await
321        .context("starting transaction for genesis state")?;
322
323    // Insert fee merkle tree nodes
324    for (account, _) in state.fee_merkle_tree.iter() {
325        let proof = match state.fee_merkle_tree.universal_lookup(account) {
326            LookupResult::Ok(_, proof) => proof,
327            LookupResult::NotFound(proof) => proof,
328            LookupResult::NotInMemory => bail!("missing merkle path for fee account {account}"),
329        };
330        let path: Vec<usize> =
331            <FeeAccount as ToTraversalPath<{ FeeMerkleTree::ARITY }>>::to_traversal_path(
332                account,
333                state.fee_merkle_tree.height(),
334            );
335
336        UpdateStateData::<SeqTypes, FeeMerkleTree, { FeeMerkleTree::ARITY }>::insert_merkle_nodes(
337            &mut tx, proof, path, 0,
338        )
339        .await
340        .context("failed to store fee merkle nodes")?;
341    }
342
343    tx.insert_chain_config(chain_config).await?;
344
345    tx.commit().await?;
346
347    // Store the genesis reward tree at height 0 so catchup can find it.
348    storage
349        .save_reward_tree_v2(0, &state.reward_merkle_tree_v2)
350        .await
351        .context("storing genesis reward merkle tree")?;
352
353    Ok(())
354}
355
356/// How long the loop waits on the local leaf stream before checking whether the data pruner has
357/// passed the height it is waiting for.
358const PRUNED_LEAF_CHECK_INTERVAL: Duration = Duration::from_secs(10);
359
360/// How long the loop waits before it retries a step of its start that failed, such as a try to
361/// start from a recent epoch boundary.
362const BOUNDARY_RETRY_INTERVAL: Duration = Duration::from_secs(10);
363
364/// How long the loop waits on a local catchup read. Consensus cuts local reads short, so that it
365/// can still vote in time. The loop has no such deadline, while peers may have pruned the heights
366/// it reads, so a slow read cut short on every try would stall it.
367const LOCAL_CATCHUP_TIMEOUT: Duration = Duration::from_secs(60);
368
369/// Where the state loop starts when it has no state of its own.
370#[derive(Clone, Copy, Debug, PartialEq, Eq)]
371pub(crate) enum EmptyStateStart {
372    /// From a snapshot at a recent epoch boundary, from peers, for a pruned node, which serves
373    /// recent state only. Fast, but the trees stay incomplete for good: they have no paths for the
374    /// blocks below the boundary, and no fee account the loop has not touched since.
375    RecentBoundary,
376    /// From genesis, on leaves from peers, for archive nodes and nodes without a pruner. Slow, but
377    /// the only way to complete trees.
378    Genesis,
379}
380
381#[tracing::instrument(skip_all)]
382pub(crate) async fn update_state_storage_loop<T>(
383    storage: Arc<T>,
384    instance: impl Future<Output = NodeState>,
385    provider: Arc<LightClientProvider>,
386    empty_state_start: EmptyStateStart,
387) -> anyhow::Result<()>
388where
389    T: SequencerStateDataSource,
390    for<'a> T::Transaction<'a>: SequencerStateUpdate,
391{
392    let mut instance = instance.await;
393    // Use a separate rewards calculator for the state loop so it doesn't
394    // interfere with consensus, which may be on a very different epoch.
395    instance.epoch_rewards_calculator = Arc::new(Mutex::new(EpochRewardsCalculator::new()));
396    // The loop can wait longer on its own database than consensus can.
397    if let Some(catchup) = instance
398        .state_catchup
399        .with_local_timeout(LOCAL_CATCHUP_TIMEOUT)
400    {
401        instance.state_catchup = catchup;
402    }
403
404    // Resume from the newest snapshot in storage. The loop cannot start anywhere else:
405    // `from_header` gives it a bare state, and its catchup fills in the block frontier and any fee
406    // account it has not seen from the parent's snapshot in this database, which exists only up to
407    // the head. The state pruner in turn never advances to the head, so that snapshot stays
408    // readable however far behind the chain the loop has fallen.
409    let mut last_height = retry_until_ok("loading the state head", || async {
410        Ok(storage.get_last_state_height().await? as u64)
411    })
412    .await;
413    // With no state of its own on a pruned database, as on a fresh pruned node, the loop would
414    // replay the whole chain from genesis, mostly on leaves from peers. A pruned node serves recent
415    // state only, so give it a recent snapshot to resume from instead.
416    if last_height == 0
417        && empty_state_start == EmptyStateStart::RecentBoundary
418        && let Some(boundary) = seed_boundary_snapshot(&storage, &instance, &provider).await
419    {
420        last_height = boundary;
421    }
422    if last_height > 0 && empty_state_start == EmptyStateStart::Genesis {
423        report_incomplete_trees(&*storage).await;
424    }
425    let current_height = storage.block_height().await.ok();
426    tracing::info!(
427        node_id = instance.node_id,
428        last_height,
429        ?current_height,
430        "updating state storage"
431    );
432
433    let parent_leaf = leaf_at(&storage, &provider, last_height).await;
434    let mut parent_state = ValidatedState::from_header(parent_leaf.block_header());
435
436    // Seed the parent's reward tree from storage.
437    //
438    // `from_header` starts the reward tree empty, and the epoch boundary only
439    // repopulates it when the previous epoch actually distributed rewards. If the
440    // previous epoch predates rewards (e.g. the first boundary after a V4→V5
441    // upgrade, where the previous epoch is pre-V5), `handle_epoch_rewards` returns
442    // zero and passes the empty tree through unchanged. So after a restart in such
443    // an epoch the tree stays empty until the next boundary, where the save in
444    // `update_state_storage` fails as it serializes the tree as full key-value pairs
445    // and bails because the accounts are missing.
446    //
447    // Load the parent's tree from storage to avoid this. Doing it once is enough
448    // every later iteration carries the tree forward in `parent_state`.
449    if parent_leaf.block_header().version() > EPOCH_VERSION && parent_leaf.height() > 0 {
450        parent_state.reward_merkle_tree_v2 =
451            retry_until_ok("loading the parent's reward tree", || {
452                load_parent_reward_tree(&*storage, &parent_leaf)
453            })
454            .await
455            .tree;
456    }
457
458    if last_height == 0 {
459        // If the last height is 0, we need to insert the genesis state, since this state is
460        // never the result of a state update and thus is not inserted in the loop below.
461        tracing::info!("storing genesis merklized state");
462        // Every write is an upsert, so a retry after a partial store is safe.
463        retry_until_ok("storing the genesis state", || {
464            store_genesis_state(&*storage, instance.chain_config, &instance.genesis_state)
465        })
466        .await;
467    }
468
469    follow_leaves(&storage, &instance, &provider, parent_leaf, parent_state).await;
470    Ok(())
471}
472
473/// Archive nodes and nodes without a pruner are meant to have complete trees, which a state that
474/// started from a boundary snapshot, as on a node that was pruned before, never gets: the loop only
475/// moves forward. So say so.
476async fn report_incomplete_trees<T>(storage: &T)
477where
478    T: SequencerStateDataSource,
479{
480    match storage.load_state_start_height().await {
481        Ok(Some(start)) => tracing::error!(
482            start,
483            "merklized state started from a boundary snapshot, not genesis, so its trees are \
484             incomplete"
485        ),
486        Ok(None) => {},
487        Err(err) => tracing::warn!(
488            err = %format_args!("{err:#}"),
489            "failed to load the state start height"
490        ),
491    }
492}
493
494/// Run `f` until it succeeds. An error would end the loop for good, possibly after a seed that
495/// waited minutes for peers, so the steps of its start retry, as the leaves it applies do.
496async fn retry_until_ok<R, Fut>(step: &str, mut f: impl FnMut() -> Fut) -> R
497where
498    Fut: Future<Output = anyhow::Result<R>>,
499{
500    loop {
501        match f().await {
502            Ok(res) => return res,
503            Err(err) => tracing::error!(
504                step,
505                err = %format_args!("{err:#}"),
506                "state loop start step failed, retrying"
507            ),
508        }
509        sleep(BOUNDARY_RETRY_INTERVAL).await;
510    }
511}
512
513/// The reward tree of `parent_leaf`, from storage.
514async fn load_parent_reward_tree<T>(
515    storage: &T,
516    parent_leaf: &Leaf2,
517) -> anyhow::Result<PermittedRewardMerkleTreeV2>
518where
519    T: SequencerStateDataSource,
520{
521    // The tree is only written at epoch boundaries, so the parent height usually has no row; fall
522    // back to the most recent tree at or below it.
523    let tree = match storage
524        .load_reward_merkle_tree_v2(parent_leaf.height())
525        .await
526    {
527        Ok(tree) => tree,
528        Err(_) => storage
529            .load_latest_reward_merkle_tree_v2(parent_leaf.height())
530            .await
531            .context("failed to load RewardMerkleTreeV2 for the previous height")?,
532    };
533
534    // The fallback returns the latest tree at or below the parent height, which is the parent's
535    // tree only if their roots match (they do within an epoch). Verify against the root the parent
536    // header commits to.
537    let parent_reward_root = parent_leaf
538        .block_header()
539        .reward_merkle_tree_root()
540        .right()
541        .context("V5+ parent header must have a v2 reward root")?;
542    ensure!(
543        tree.tree.commitment() == parent_reward_root,
544        "loaded reward tree root {} does not match parent header {parent_reward_root}",
545        tree.tree.commitment(),
546    );
547    Ok(tree)
548}
549
550/// Store a snapshot at a recent epoch boundary for a loop with no state of its own, and return the
551/// boundary, which the loop then resumes from as after a restart.
552///
553/// The boundary is the newest one at or below the local tip, once the node holds a block above the
554/// data pruned height, or above genesis before the data pruner has run; a fresh node holds none
555/// until consensus decides one. It has to be recent: the reward tree is stored only at boundaries,
556/// and every node garbage collects all but the last few, so peers have no tree for an older
557/// snapshot.
558///
559/// The snapshot holds the block frontier and the reward tree, fetched from peers and checked
560/// against the boundary header. Peers serve fee accounts only by address, and the loop cannot know
561/// them all, so the fee tree starts empty and the catchup in [`apply_leaf`] fetches each account
562/// from peers when the loop first needs it. From V0.6 on no block reads one, so it stays empty.
563///
564/// Returns `None`, which leaves the loop to replay from genesis, when there is no boundary yet, or
565/// when it predates epoch rewards and so has no reward tree stored at it.
566async fn seed_boundary_snapshot<T>(
567    storage: &Arc<T>,
568    instance: &NodeState,
569    provider: &LightClientProvider,
570) -> Option<u64>
571where
572    T: SequencerStateDataSource,
573    for<'a> T::Transaction<'a>: SequencerStateUpdate,
574{
575    let epoch_height = instance.epoch_height.filter(|height| *height > 0)?;
576    let pruned = retry_until_ok("loading the data pruned height", || {
577        storage.load_pruned_height()
578    })
579    .await
580    .unwrap_or(0);
581    // Each request is sent once, because this loop retries: peers never answer a request for a
582    // boundary they have pruned, and the next try then picks the boundary again from the tip. Each
583    // try passes its number on, so that peers get longer to answer.
584    let mut retry = 0;
585    let (leaf, state) = loop {
586        let tip = local_tip_above(&**storage, pruned).await;
587        let boundary = tip - tip % epoch_height;
588        if boundary == 0 {
589            return None;
590        }
591        let leaf = leaf_at(storage, provider, boundary).await;
592        if leaf.block_header().version() < EPOCH_REWARD_VERSION {
593            return None;
594        }
595        let catchup = SingleAttemptCatchup::new(instance.state_catchup.clone(), retry);
596        match fetch_boundary_state(&catchup, instance, &leaf).await {
597            Ok(state) => break (leaf, state),
598            // Peers may not have reached the boundary yet, or, if the local tip is stale, may
599            // have pruned its frontier or garbage collected its reward tree. Try again from
600            // wherever the tip is then.
601            Err(err) => {
602                tracing::warn!(
603                    boundary,
604                    retry,
605                    err = %format_args!("{err:#}"),
606                    "fetching the state at the epoch boundary failed, retrying"
607                );
608                retry += 1;
609                sleep(BOUNDARY_RETRY_INTERVAL).await;
610            },
611        }
612    };
613    let height = leaf.height();
614    // Every write is an upsert, so a retry after a partial store is safe.
615    retry_until_ok(
616        &format!("storing the boundary snapshot at height {height}"),
617        || store_boundary_snapshot(&**storage, height, &state),
618    )
619    .await;
620    tracing::info!(
621        height,
622        pruned,
623        "state loop starts from a snapshot at a recent epoch boundary"
624    );
625    Some(height)
626}
627
628/// The height of the newest block in local storage, once it is above `pruned`.
629async fn local_tip_above<T>(storage: &T, pruned: u64) -> u64
630where
631    T: SequencerStateDataSource,
632{
633    loop {
634        match storage.block_height().await {
635            Ok(height) if height as u64 > pruned + 1 => return height as u64 - 1,
636            Ok(height) => tracing::info!(
637                height,
638                pruned,
639                "no block above the data pruned height yet; waiting for one"
640            ),
641            Err(err) => tracing::warn!(
642                err = %format_args!("{err:#}"),
643                "failed to load the local block height"
644            ),
645        }
646        sleep(BOUNDARY_RETRY_INTERVAL).await;
647    }
648}
649
650/// The state at the epoch boundary `leaf`, from `catchup`: the block frontier and the whole reward
651/// tree, each checked against its root in the boundary header.
652async fn fetch_boundary_state(
653    catchup: &impl StateCatchup,
654    instance: &NodeState,
655    leaf: &Leaf2,
656) -> anyhow::Result<ValidatedState> {
657    let header = leaf.block_header();
658    let mut state = ValidatedState::from_header(header);
659    catchup
660        .remember_blocks_merkle_tree(
661            instance,
662            leaf.height(),
663            leaf.view_number(),
664            &mut state.block_merkle_tree,
665        )
666        .await
667        .context("fetching the block frontier")?;
668    let reward_root = header
669        .reward_merkle_tree_root()
670        .right()
671        .context("V5+ header must have a v2 reward root")?;
672    state.reward_merkle_tree_v2 = catchup
673        .fetch_reward_merkle_tree_v2(
674            leaf.height(),
675            leaf.view_number(),
676            reward_root,
677            Default::default(),
678        )
679        .await
680        .context("fetching the reward tree")?
681        .tree;
682    Ok(state)
683}
684
685/// Store `state`, the state at the epoch boundary `height`, as the loop's head snapshot. The loop
686/// keeps nothing of `state`: on every start it rebuilds the head's state from the header and fills
687/// it from this database, so the head needs what a state update stores:
688/// - The reward tree. The loop reads it from here only, so it goes first: a head without it would
689///   fail every start.
690/// - The block frontier. Without it, the loop would fetch it from peers again, and block proofs at
691///   the head would fail.
692/// - The chain config. The loop takes it from the header, but only the loop stores it, and chain
693///   config catchup reads it from here.
694///
695/// No state is stored below the boundary, so the state pruned height goes just below it: queries
696/// for lower heights then answer that the state is pruned, as on a node that pruned it. The
697/// boundary is also recorded as where the state starts, so that a node meant to have complete
698/// trees can tell that it does not.
699async fn store_boundary_snapshot<T>(
700    storage: &T,
701    height: u64,
702    state: &ValidatedState,
703) -> anyhow::Result<()>
704where
705    T: SequencerStateDataSource,
706    for<'a> T::Transaction<'a>: SequencerStateUpdate,
707{
708    storage
709        .save_reward_tree_v2(height, &state.reward_merkle_tree_v2)
710        .await
711        .context("storing the boundary reward tree")?;
712
713    let mut tx = storage
714        .write()
715        .await
716        .context("opening transaction for the boundary snapshot")?;
717    store_block_frontier(&mut tx, height, &state.block_merkle_tree).await?;
718    if let Some(chain_config) = state.chain_config.resolve() {
719        tx.insert_chain_config(chain_config).await?;
720    }
721    UpdateStateData::<SeqTypes, _, { BlockMerkleTree::ARITY }>::set_last_state_height(
722        &mut tx,
723        height as usize,
724    )
725    .await
726    .context("setting state height")?;
727    UpdateStateData::<SeqTypes, _, { BlockMerkleTree::ARITY }>::set_state_pruned_height(
728        &mut tx,
729        height - 1,
730    )
731    .await
732    .context("setting state pruned height")?;
733    UpdateStateData::<SeqTypes, _, { BlockMerkleTree::ARITY }>::set_state_start_height(
734        &mut tx, height,
735    )
736    .await
737    .context("setting state start height")?;
738    tx.commit().await
739}
740
741/// Apply every leaf above `parent_leaf` as it arrives. Leaves come from the local stream, except
742/// those the data pruner has already deleted: the availability layer never fetches a leaf at or
743/// below the data pruned height, so the stream would wait for one forever. Those come from peers,
744/// in runs. Returns when the stream ends, or when a leaf from peers does not extend the parent.
745async fn follow_leaves<T>(
746    storage: &Arc<T>,
747    instance: &NodeState,
748    provider: &LightClientProvider,
749    mut parent_leaf: Leaf2,
750    mut parent_state: ValidatedState,
751) where
752    T: SequencerStateDataSource,
753    for<'a> T::Transaction<'a>: SequencerStateUpdate,
754{
755    // Peers serve at most this many leaves per range request.
756    let limit = availability::Options::default().small_object_range_limit as u64;
757    let mut next_height = parent_leaf.height() + 1;
758    loop {
759        while let Some(pruned) = pruned_past(&**storage, next_height).await {
760            let end = (next_height + limit).min(pruned + 1);
761            tracing::info!(
762                next_height,
763                end,
764                pruned,
765                "fetching leaves below the data pruned height from peers"
766            );
767            for leaf in fetch_pruned_leaves(provider, next_height..end).await {
768                // The leaves are verified, so they are the decided leaves at these heights, and a
769                // refetch returns them again. A mismatch means the loop's parent is wrong, which
770                // no retry fixes.
771                if leaf.parent_commitment() != parent_leaf.commit() {
772                    tracing::error!(
773                        height = next_height,
774                        parent = %parent_leaf.commit(),
775                        leaf_parent = %leaf.parent_commitment(),
776                        "fetched leaf does not extend the state loop's parent; stopping the loop"
777                    );
778                    return;
779                }
780                apply_leaf(storage, instance, &mut parent_leaf, &mut parent_state, leaf).await;
781                next_height += 1;
782            }
783        }
784
785        let mut leaves = storage.subscribe_leaves(next_height as usize).await;
786        loop {
787            match timeout(PRUNED_LEAF_CHECK_INTERVAL, leaves.next()).await {
788                Ok(Some(leaf)) => {
789                    next_height = leaf.height() + 1;
790                    apply_leaf(
791                        storage,
792                        instance,
793                        &mut parent_leaf,
794                        &mut parent_state,
795                        leaf.leaf().clone(),
796                    )
797                    .await;
798                },
799                Ok(None) => return,
800                // Still waiting. If the data pruner has passed this height in the meantime, the
801                // stream will never deliver it.
802                Err(_) => {
803                    if pruned_past(&**storage, next_height).await.is_some() {
804                        break;
805                    }
806                },
807            }
808        }
809    }
810}
811
812/// The data pruned height when it is at or above `height`. The leaf and header there are out of
813/// local reach: the pruner deleted them, and the availability layer never fetches below its
814/// marker. A failed read counts as not pruned; every caller asks again before long, and the loop
815/// must not end over a transient database error.
816async fn pruned_past<T>(storage: &T, height: u64) -> Option<u64>
817where
818    T: SequencerStateDataSource,
819{
820    match storage.load_pruned_height().await {
821        Ok(pruned) => pruned.filter(|pruned| height <= *pruned),
822        Err(err) => {
823            tracing::warn!(height, "failed to load the data pruned height: {err:#}");
824            None
825        },
826    }
827}
828
829/// The leaf at `height`, from local storage, or from peers once the data pruner has deleted it.
830async fn leaf_at<T>(storage: &Arc<T>, provider: &LightClientProvider, height: u64) -> Leaf2
831where
832    T: SequencerStateDataSource,
833{
834    loop {
835        if let Some(pruned) = pruned_past(&**storage, height).await {
836            tracing::warn!(
837                height,
838                pruned,
839                "leaf is at or below the data pruned height; fetching it from peers"
840            );
841            return fetch_pruned_leaves(provider, height..height + 1)
842                .await
843                .pop()
844                .expect("a one-height range yields exactly one leaf");
845        }
846        let local = AvailabilityDataSource::get_leaf(&**storage, height as usize).await;
847        // On a timeout the data pruner may have passed this height in the meantime, so check
848        // again before waiting further.
849        if let Ok(leaf) = timeout(PRUNED_LEAF_CHECK_INTERVAL, local).await {
850            return leaf.leaf().clone();
851        }
852        tracing::info!(
853            height,
854            "leaf is not in local storage yet; still waiting for it"
855        );
856    }
857}
858
859/// The leaves at `heights`, which the data pruned height says are gone, from the query module's
860/// peers through its light client, retrying until they arrive. The light client verifies leaves
861/// of every protocol version, and one proof covers the run: it proves the last leaf final and
862/// chains the rest to it. The loop needs each leaf once, so the leaves are stored neither in this
863/// database, where the pruner would only delete them again, nor in the light client's cache.
864async fn fetch_pruned_leaves(provider: &LightClientProvider, heights: Range<u64>) -> Vec<Leaf2> {
865    let start = heights.start as usize;
866    let end = heights.end as usize;
867    let mut delay = Duration::from_secs(1);
868    loop {
869        if let Some(light_client) = provider.light_client().await {
870            match light_client
871                .fetch_leaves_in_range_uncached(start, end)
872                .await
873            {
874                Ok(leaves) => return leaves.into_iter().map(|leaf| leaf.leaf().clone()).collect(),
875                Err(err) => tracing::warn!(
876                    ?heights,
877                    ?delay,
878                    err = %format_args!("{err:#}"),
879                    "fetching leaves failed, retrying"
880                ),
881            }
882        }
883        sleep(delay).await;
884        delay = (delay * 2).min(Duration::from_secs(60));
885    }
886}
887
888/// Apply `leaf` to the merklized state, retrying until it is stored, then make it the parent.
889async fn apply_leaf<T>(
890    storage: &Arc<T>,
891    instance: &NodeState,
892    parent_leaf: &mut Leaf2,
893    parent_state: &mut ValidatedState,
894    leaf: Leaf2,
895) where
896    T: SequencerStateDataSource,
897    for<'a> T::Transaction<'a>: SequencerStateUpdate,
898{
899    // The node's catchup races this database, through the node's SQL persistence, against peers,
900    // which serve what the parent snapshot lacks: a loop that started from a boundary snapshot
901    // holds only the fee accounts it has touched since. The parent snapshot is the state head,
902    // which this database always keeps, while peers may have pruned it. When the data pruner has
903    // deleted the parent's leaf and header, the local read finds the snapshot by the roots in the
904    // parent state. Each request is sent once, because the loop below retries: peers never answer
905    // a request for a height they have pruned, and the next try then reads this database again.
906    // Each try passes its number on, so that peers get longer to answer, as they would on retries
907    // of the request itself.
908    for retry in 0.. {
909        tracing::debug!(
910            height = leaf.height(),
911            node_id = instance.node_id,
912            ?leaf,
913            "updating persistent merklized state"
914        );
915        match update_state_storage(
916            parent_state,
917            storage,
918            instance,
919            &SingleAttemptCatchup::new(instance.state_catchup.clone(), retry),
920            parent_leaf,
921            &leaf,
922        )
923        .await
924        {
925            Ok(state) => {
926                *parent_leaf = leaf;
927                *parent_state = state;
928                return;
929            },
930            Err(err) => {
931                tracing::error!(height = leaf.height(), "failed to update state: {err:#}");
932                // If we fail, delay for a second and retry.
933                sleep(Duration::from_secs(1)).await;
934            },
935        }
936    }
937}
938
939pub(crate) trait SequencerStateDataSource:
940    'static
941    + Debug
942    + AvailabilityDataSource<SeqTypes>
943    + StatusDataSource
944    + VersionedDataSource
945    + CatchupStorage
946    + RewardMerkleTreeDataSource
947    + PrunedHeightDataSource
948    + MerklizedStateHeightPersistence
949{
950}
951
952impl<T> SequencerStateDataSource for T where
953    T: 'static
954        + Debug
955        + AvailabilityDataSource<SeqTypes>
956        + StatusDataSource
957        + VersionedDataSource
958        + CatchupStorage
959        + RewardMerkleTreeDataSource
960        + PrunedHeightDataSource
961        + MerklizedStateHeightPersistence
962{
963}
964
965pub(crate) trait SequencerStateUpdate:
966    Transaction
967    + UpdateStateData<SeqTypes, FeeMerkleTree, { FeeMerkleTree::ARITY }>
968    + UpdateStateData<SeqTypes, BlockMerkleTree, { BlockMerkleTree::ARITY }>
969    + UpdateStateData<SeqTypes, RewardMerkleTreeV1, { RewardMerkleTreeV1::ARITY }>
970    + ChainConfigPersistence
971{
972}
973
974impl<T> SequencerStateUpdate for T where
975    T: Transaction
976        + UpdateStateData<SeqTypes, FeeMerkleTree, { FeeMerkleTree::ARITY }>
977        + UpdateStateData<SeqTypes, BlockMerkleTree, { BlockMerkleTree::ARITY }>
978        + UpdateStateData<SeqTypes, RewardMerkleTreeV1, { RewardMerkleTreeV1::ARITY }>
979        + ChainConfigPersistence
980{
981}