Skip to main content

espresso_node/
state.rs

1use core::fmt::Debug;
2use std::{cmp::max, sync::Arc, time::Duration};
3
4use anyhow::{Context, bail, ensure};
5use async_lock::Mutex;
6use either::Either;
7use espresso_types::{
8    BlockMerkleTree, EpochRewardsCalculator, FeeAccount, FeeMerkleTree, Leaf2, ValidatedState,
9    traits::StateCatchup,
10    v0_3::{ChainConfig, RewardMerkleTreeV1},
11    v0_4::Delta,
12};
13use futures::{StreamExt, future::Future};
14use hotshot::traits::ValidatedState as HotShotState;
15use hotshot_query_service::{
16    availability::{AvailabilityDataSource, LeafQueryData},
17    data_source::{Transaction, VersionedDataSource, storage::pruning::PrunedHeightDataSource},
18    merklized_state::{MerklizedStateHeightPersistence, UpdateStateData},
19    status::StatusDataSource,
20    types::HeightIndexed,
21};
22use hotshot_types::utils::is_last_block;
23use jf_merkle_tree_compat::{
24    LookupResult, MerkleTreeScheme, ToTraversalPath, UniversalMerkleTreeScheme,
25};
26use tokio::time::sleep;
27use vbs::version::Version;
28use versions::{DRB_AND_HEADER_UPGRADE_VERSION, EPOCH_REWARD_VERSION, EPOCH_VERSION};
29
30use crate::{
31    NodeState, SeqTypes,
32    api::{RewardMerkleTreeDataSource, RewardMerkleTreeV2Data},
33    catchup::{CatchupStorage, SqlStateCatchup},
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    // Insert block merkle tree nodes
173    let (_, proof) = block_merkle_tree
174        .lookup(block_number - 1)
175        .expect_ok()
176        .context("getting blocks frontier")?;
177    let path = <u64 as ToTraversalPath<{ BlockMerkleTree::ARITY }>>::to_traversal_path(
178        &(block_number - 1),
179        block_merkle_tree.height(),
180    );
181
182    {
183        tracing::debug!("inserting blocks frontier");
184        UpdateStateData::<SeqTypes, BlockMerkleTree, { BlockMerkleTree::ARITY }>::insert_merkle_nodes(
185            tx,
186            proof,
187            path,
188            block_number,
189        )
190        .await
191        .context("failed to store block merkle nodes")?;
192    }
193
194    Ok(())
195}
196
197#[tracing::instrument(
198    skip_all,
199    fields(
200        node_id = instance.node_id,
201        view = ?parent_leaf.leaf().view_number(),
202        height = parent_leaf.height(),
203    ),
204)]
205async fn update_state_storage<T>(
206    parent_state: &ValidatedState,
207    storage: &Arc<T>,
208    instance: &NodeState,
209    peers: &impl StateCatchup,
210    parent_leaf: &LeafQueryData<SeqTypes>,
211    proposed_leaf: &LeafQueryData<SeqTypes>,
212) -> anyhow::Result<ValidatedState>
213where
214    T: SequencerStateDataSource,
215    for<'a> T::Transaction<'a>: SequencerStateUpdate,
216{
217    let parent_chain_config = parent_state.chain_config;
218    let block_number = proposed_leaf.height();
219    let version = proposed_leaf.header().version();
220
221    let (state, delta) = compute_state_update(
222        parent_state,
223        instance,
224        peers,
225        &parent_leaf.leaf().clone(),
226        &proposed_leaf.leaf().clone(),
227    )
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    tracing::debug!("storing state update");
272    let mut tx = storage
273        .write()
274        .await
275        .context("opening transaction for state update")?;
276
277    store_state_update(&mut tx, block_number, version, &state, &delta).await?;
278
279    tx.commit().await?;
280
281    let mut tx = storage
282        .write()
283        .await
284        .context("opening transaction for state update")?;
285
286    if parent_chain_config != state.chain_config {
287        let cf = state
288            .chain_config
289            .resolve()
290            .context("failed to resolve to chain config")?;
291
292        tx.insert_chain_config(cf).await?;
293    }
294
295    tracing::debug!(block_number, "updating state height");
296    UpdateStateData::<SeqTypes, _, { BlockMerkleTree::ARITY }>::set_last_state_height(
297        &mut tx,
298        block_number as usize,
299    )
300    .await
301    .context("setting state height")?;
302
303    tx.commit().await?;
304
305    Ok(state)
306}
307
308async fn store_genesis_state<S>(
309    storage: &S,
310    chain_config: ChainConfig,
311    state: &ValidatedState,
312) -> anyhow::Result<()>
313where
314    S: SequencerStateDataSource,
315    for<'a> S::Transaction<'a>: SequencerStateUpdate,
316{
317    ensure!(
318        state.block_merkle_tree.num_leaves() == 0,
319        "genesis state with non-empty block tree is unsupported"
320    );
321
322    let mut tx = storage
323        .write()
324        .await
325        .context("starting transaction for genesis state")?;
326
327    // Insert fee merkle tree nodes
328    for (account, _) in state.fee_merkle_tree.iter() {
329        let proof = match state.fee_merkle_tree.universal_lookup(account) {
330            LookupResult::Ok(_, proof) => proof,
331            LookupResult::NotFound(proof) => proof,
332            LookupResult::NotInMemory => bail!("missing merkle path for fee account {account}"),
333        };
334        let path: Vec<usize> =
335            <FeeAccount as ToTraversalPath<{ FeeMerkleTree::ARITY }>>::to_traversal_path(
336                account,
337                state.fee_merkle_tree.height(),
338            );
339
340        UpdateStateData::<SeqTypes, FeeMerkleTree, { FeeMerkleTree::ARITY }>::insert_merkle_nodes(
341            &mut tx, proof, path, 0,
342        )
343        .await
344        .context("failed to store fee merkle nodes")?;
345    }
346
347    tx.insert_chain_config(chain_config).await?;
348
349    tx.commit().await?;
350
351    // Store the genesis reward tree at height 0 so catchup can find it.
352    let tree_data: RewardMerkleTreeV2Data = (&state.reward_merkle_tree_v2)
353        .try_into()
354        .context("serializing genesis reward tree")?;
355    let tree_bytes = bincode::serialize(&tree_data).context("serializing genesis reward tree")?;
356    storage
357        .persist_tree(0, tree_bytes)
358        .await
359        .context("storing genesis reward merkle tree")?;
360
361    Ok(())
362}
363
364#[tracing::instrument(skip_all)]
365pub(crate) async fn update_state_storage_loop<T>(
366    storage: Arc<T>,
367    instance: impl Future<Output = NodeState>,
368) -> anyhow::Result<()>
369where
370    T: SequencerStateDataSource,
371    for<'a> T::Transaction<'a>: SequencerStateUpdate,
372{
373    let mut instance = instance.await;
374    // Use a separate rewards calculator for the state loop so it doesn't
375    // interfere with consensus, which may be on a very different epoch.
376    instance.epoch_rewards_calculator = Arc::new(Mutex::new(EpochRewardsCalculator::new()));
377    let peers = SqlStateCatchup::new(storage.clone(), Default::default());
378
379    // get last saved merklized state
380    let (last_height, parent_leaf, mut leaves) = {
381        let last_height = storage.get_last_state_height().await?;
382        let pruned_height = storage.load_state_pruned_height().await?;
383
384        let height = match pruned_height {
385            // If `last_height > pruned_height`, start from `last_height`
386            // as it represents the latest state in storage.
387            // If `pruned_height > last_height`, start from `pruned_height`
388            // as data below this height is no longer needed and will be pruned again during the next pruner run.
389            Some(pruned_height) => max(last_height, pruned_height as usize + 1),
390            // if we have not pruned any data then just start from last_height
391            None => last_height,
392        };
393
394        // Check for environment variable override
395        let height =
396            if let Ok(env_height) = std::env::var("ESPRESSO_NODE_STATE_STORAGE_INITIAL_HEIGHT") {
397                match env_height.parse::<usize>() {
398                    Ok(override_height) => {
399                        tracing::error!(
400                            node_id = instance.node_id,
401                            calculated_height = height,
402                            override_height,
403                            "overriding initial state storage height from environment variable"
404                        );
405                        override_height
406                    },
407                    Err(e) => {
408                        tracing::error!(
409                            "failed to parse ESPRESSO_NODE_STATE_STORAGE_INITIAL_HEIGHT: {e}, \
410                             using calculated height {height}"
411                        );
412                        height
413                    },
414                }
415            } else {
416                height
417            };
418
419        let current_height = storage.block_height().await?;
420        tracing::info!(
421            node_id = instance.node_id,
422            last_height,
423            height,
424            current_height,
425            "updating state storage"
426        );
427
428        let parent_leaf = AvailabilityDataSource::get_leaf(&*storage, height).await;
429        let leaves = storage.subscribe_leaves(height + 1).await;
430        (last_height, parent_leaf, leaves)
431    };
432    // resolve the parent leaf future _after_ dropping our lock on the state, in case it is not
433    // ready yet and another task needs a mutable lock on the state to produce the parent leaf.
434    let mut parent_leaf = parent_leaf.await;
435    let mut parent_state = ValidatedState::from_header(parent_leaf.header());
436
437    // Seed the parent's reward tree from storage.
438    //
439    // `from_header` starts the reward tree empty, and the epoch boundary only
440    // repopulates it when the previous epoch actually distributed rewards. If the
441    // previous epoch predates rewards (e.g. the first boundary after a V4→V5
442    // upgrade, where the previous epoch is pre-V5), `handle_epoch_rewards` returns
443    // zero and passes the empty tree through unchanged. So after a restart in such
444    // an epoch the tree stays empty until the next boundary, where the save in
445    // `update_state_storage` fails as it serializes the tree as full key-value pairs
446    // and bails because the accounts are missing.
447    //
448    // Load the parent's tree from storage to avoid this. Doing it once is enough
449    // every later iteration carries the tree forward in `parent_state`.
450    if parent_leaf.header().version() > EPOCH_VERSION && parent_leaf.height() > 0 {
451        // The tree is only written at epoch boundaries, so the parent height
452        // usually has no row; fall back to the most recent tree at or below it.
453        let reward_merkle_tree_v2 = match storage
454            .load_reward_merkle_tree_v2(parent_leaf.height())
455            .await
456        {
457            Ok(tree) => tree,
458            Err(_) => storage
459                .load_latest_reward_merkle_tree_v2(parent_leaf.height())
460                .await
461                .context(
462                    "Error starting the state storage update loop: failed to load \
463                     RewardMerkleTreeV2 for the previous height",
464                )?,
465        };
466
467        // The fallback returns the latest tree at or below the parent height,
468        // which is the parent's tree only if their roots match (they do within an
469        // epoch). Verify against the root the parent header commits to.
470        let parent_reward_root = parent_leaf
471            .header()
472            .reward_merkle_tree_root()
473            .right()
474            .context("V5+ parent header must have a v2 reward root")?;
475        ensure!(
476            reward_merkle_tree_v2.tree.commitment() == parent_reward_root,
477            "loaded reward tree root {} does not match parent header {parent_reward_root}",
478            reward_merkle_tree_v2.tree.commitment(),
479        );
480
481        parent_state.reward_merkle_tree_v2 = reward_merkle_tree_v2.tree;
482    }
483
484    if last_height == 0 {
485        // If the last height is 0, we need to insert the genesis state, since this state is
486        // never the result of a state update and thus is not inserted in the loop below.
487        tracing::info!("storing genesis merklized state");
488        store_genesis_state(&*storage, instance.chain_config, &instance.genesis_state)
489            .await
490            .context("storing genesis state")?;
491    }
492
493    while let Some(leaf) = leaves.next().await {
494        loop {
495            tracing::debug!(
496                height = leaf.height(),
497                node_id = instance.node_id,
498                ?leaf,
499                "updating persistent merklized state"
500            );
501            match update_state_storage(
502                &parent_state,
503                &storage,
504                &instance,
505                &peers,
506                &parent_leaf,
507                &leaf,
508            )
509            .await
510            {
511                Ok(state) => {
512                    parent_leaf = leaf;
513                    parent_state = state;
514                    break;
515                },
516                Err(err) => {
517                    tracing::error!(height = leaf.height(), "failed to update state: {err:#}");
518                    // If we fail, delay for a second and retry.
519                    sleep(Duration::from_secs(1)).await;
520                },
521            }
522        }
523    }
524
525    Ok(())
526}
527
528pub(crate) trait SequencerStateDataSource:
529    'static
530    + Debug
531    + AvailabilityDataSource<SeqTypes>
532    + StatusDataSource
533    + VersionedDataSource
534    + CatchupStorage
535    + RewardMerkleTreeDataSource
536    + PrunedHeightDataSource
537    + MerklizedStateHeightPersistence
538{
539}
540
541impl<T> SequencerStateDataSource for T where
542    T: 'static
543        + Debug
544        + AvailabilityDataSource<SeqTypes>
545        + StatusDataSource
546        + VersionedDataSource
547        + CatchupStorage
548        + RewardMerkleTreeDataSource
549        + PrunedHeightDataSource
550        + MerklizedStateHeightPersistence
551{
552}
553
554pub(crate) trait SequencerStateUpdate:
555    Transaction
556    + UpdateStateData<SeqTypes, FeeMerkleTree, { FeeMerkleTree::ARITY }>
557    + UpdateStateData<SeqTypes, BlockMerkleTree, { BlockMerkleTree::ARITY }>
558    + UpdateStateData<SeqTypes, RewardMerkleTreeV1, { RewardMerkleTreeV1::ARITY }>
559    + ChainConfigPersistence
560{
561}
562
563impl<T> SequencerStateUpdate for T where
564    T: Transaction
565        + UpdateStateData<SeqTypes, FeeMerkleTree, { FeeMerkleTree::ARITY }>
566        + UpdateStateData<SeqTypes, BlockMerkleTree, { BlockMerkleTree::ARITY }>
567        + UpdateStateData<SeqTypes, RewardMerkleTreeV1, { RewardMerkleTreeV1::ARITY }>
568        + ChainConfigPersistence
569{
570}