Skip to main content

espresso_types/v0/impls/
state.rs

1use std::ops::Add;
2
3use alloy::primitives::{Address, U256};
4use anyhow::{Context, bail};
5use committable::{Commitment, Committable};
6use either::Either;
7use hotshot_query_service_types::merklized_state::MerklizedState;
8use hotshot_types::{
9    data::{BlockError, EpochNumber, ViewNumber},
10    traits::{
11        ValidatedState as HotShotState, block_contents::BlockHeader,
12        signature_key::BuilderSignatureKey, states::StateDelta,
13    },
14    utils::{epoch_from_block_number, is_ge_epoch_root},
15};
16use itertools::Itertools;
17use jf_merkle_tree_compat::{
18    AppendableMerkleTreeScheme, ForgetableMerkleTreeScheme, ForgetableUniversalMerkleTreeScheme,
19    LookupResult, MerkleCommitment, MerkleTreeError, MerkleTreeScheme,
20    PersistentUniversalMerkleTreeScheme, UniversalMerkleTreeScheme,
21    prelude::{MerkleProof, Sha3Digest, Sha3Node},
22};
23use num_traits::CheckedSub;
24use serde::{Deserialize, Serialize};
25use thiserror::Error;
26use time::OffsetDateTime;
27use vbs::version::Version;
28use versions::{
29    DRB_AND_HEADER_UPGRADE_VERSION, EPOCH_REWARD_VERSION, EPOCH_VERSION, NEW_PROTOCOL_VERSION,
30};
31
32#[cfg(feature = "node")]
33use super::L1Client;
34use super::{
35    BlockMerkleCommitment, BlockSize, FeeMerkleCommitment, fee_info::FeeError,
36    instance_state::NodeState, v0_1::IterableFeeInfo,
37};
38use crate::{
39    BLOCK_MERKLE_TREE_HEIGHT, BlockMerkleTree, FEE_MERKLE_TREE_HEIGHT, FeeAccount, FeeAmount,
40    FeeInfo, FeeMerkleTree, Header, Leaf2, NsTableValidationError, PayloadByteLen, SeqTypes,
41    UpgradeType,
42    traits::StateCatchup,
43    v0::{
44        impls::{StakeTableHash, distribute_block_reward},
45        sparse_mt::{Keccak256Hasher, KeccakNode},
46    },
47    v0_3::{
48        ChainConfig, REWARD_MERKLE_TREE_V1_HEIGHT, ResolvableChainConfig, RewardAccountV1,
49        RewardAmount, RewardMerkleCommitmentV1, RewardMerkleTreeV1,
50    },
51    v0_4::{
52        Delta, REWARD_MERKLE_TREE_V2_HEIGHT, RewardAccountV2, RewardMerkleCommitmentV2,
53        RewardMerkleTreeV2,
54    },
55    v0_5::LeaderCounts,
56};
57
58/// This enum is not used in code but functions as an index of
59/// possible validation errors.
60#[allow(dead_code)]
61pub enum StateValidationError {
62    ProposalValidation(ProposalValidationError),
63    BuilderValidation(BuilderValidationError),
64    Fee(FeeError),
65}
66
67/// Possible builder validation failures
68#[derive(Error, Debug, Eq, PartialEq)]
69pub enum BuilderValidationError {
70    #[error("Builder signature not found")]
71    SignatureNotFound,
72    #[error("Fee amount out of range: {0}")]
73    FeeAmountOutOfRange(FeeAmount),
74    #[error("Invalid Builder Signature")]
75    InvalidBuilderSignature,
76    #[error(
77        "Fee info / builder signature count mismatch: fee_count={fee_count}, \
78         signature_count={signature_count}"
79    )]
80    FeeAndSignatureCountMismatch {
81        fee_count: usize,
82        signature_count: usize,
83    },
84}
85
86/// Possible proposal validation failures
87#[derive(Error, Debug, Eq, PartialEq)]
88pub enum ProposalValidationError {
89    #[error("Next stake table hash mismatch: expected={expected:?}, proposal={proposal:?}")]
90    NextStakeTableHashMismatch {
91        expected: StakeTableHash,
92        proposal: StakeTableHash,
93    },
94    #[error("Invalid ChainConfig: expected={expected:?}, proposal={proposal:?}")]
95    InvalidChainConfig {
96        expected: Box<ChainConfig>,
97        proposal: Box<ResolvableChainConfig>,
98    },
99    #[error(
100        "Invalid Payload Size: (max_block_size={max_block_size}, proposed_block_size={block_size})"
101    )]
102    MaxBlockSizeExceeded {
103        max_block_size: BlockSize,
104        block_size: BlockSize,
105    },
106    #[error(
107        "Insufficient Fee: block_size={max_block_size}, base_fee={base_fee}, \
108         proposed_fee={proposed_fee}"
109    )]
110    InsufficientFee {
111        max_block_size: BlockSize,
112        base_fee: FeeAmount,
113        proposed_fee: FeeAmount,
114    },
115    #[error("Invalid Height: parent_height={parent_height}, proposal_height={proposal_height}")]
116    InvalidHeight {
117        parent_height: u64,
118        proposal_height: u64,
119    },
120    #[error("Invalid Block Root Error: expected={expected_root:?}, proposal={proposal_root:?}")]
121    InvalidBlockRoot {
122        expected_root: BlockMerkleCommitment,
123        proposal_root: BlockMerkleCommitment,
124    },
125    #[error("Invalid Fee Root Error: expected={expected_root:?}, proposal={proposal_root:?}")]
126    InvalidFeeRoot {
127        expected_root: FeeMerkleCommitment,
128        proposal_root: FeeMerkleCommitment,
129    },
130    #[error("Invalid v1 Reward Root Error: expected={expected_root:?}, proposal={proposal_root:?}")]
131    InvalidV1RewardRoot {
132        expected_root: RewardMerkleCommitmentV1,
133        proposal_root: RewardMerkleCommitmentV1,
134    },
135    #[error("Invalid v2 Reward Root Error: expected={expected_root:?}, proposal={proposal_root:?}")]
136    InvalidV2RewardRoot {
137        expected_root: RewardMerkleCommitmentV2,
138        proposal_root: RewardMerkleCommitmentV2,
139    },
140    #[error("Invalid namespace table: {0}")]
141    InvalidNsTable(NsTableValidationError),
142    #[error("Some fee amount or their sum total out of range")]
143    SomeFeeAmountOutOfRange,
144    #[error("Invalid timestamp: proposal={proposal_timestamp}, parent={parent_timestamp}")]
145    DecrementingTimestamp {
146        proposal_timestamp: u64,
147        parent_timestamp: u64,
148    },
149    #[error("Timestamp drift too high: proposed:={proposal}, system={system}, diff={diff}")]
150    InvalidTimestampDrift {
151        proposal: u64,
152        system: u64,
153        diff: u64,
154    },
155    #[error(
156        "Inconsistent timestamps on header: timestamp:={timestamp}, \
157         timestamp_millis={timestamp_millis}"
158    )]
159    InconsistentTimestamps {
160        timestamp: u64,
161        timestamp_millis: u64,
162    },
163    #[error("l1_finalized has `None` value")]
164    L1FinalizedNotFound,
165    #[error("l1_finalized height is decreasing: parent={parent:?} proposed={proposed:?}")]
166    L1FinalizedDecrementing {
167        parent: Option<(u64, u64)>,
168        proposed: Option<(u64, u64)>,
169    },
170    #[error("Invalid proposal: l1_head height is decreasing")]
171    DecrementingL1Head,
172    #[error("Builder Validation Error: {0}")]
173    BuilderValidationError(BuilderValidationError),
174    #[error("Invalid proposal: l1 finalized does not match the proposal")]
175    InvalidL1Finalized,
176    #[error("reward root not found")]
177    RewardRootNotFound {},
178    #[error("Next stake table not found")]
179    NextStakeTableNotFound,
180    #[error("Next stake table hash missing")]
181    NextStakeTableHashNotFound,
182    #[error("Next stake table hash was not `None`")]
183    NextStakeTableHashNotNone,
184    #[error("No Epoch Height")]
185    NoEpochHeight,
186    #[error("Total rewards mismatch: proposed header has {proposed} but actual is {actual}")]
187    TotalRewardsMismatch {
188        proposed: RewardAmount,
189        actual: RewardAmount,
190    },
191    #[error("Leader counts missing in V6 header")]
192    LeaderCountsMissing,
193    #[error("Leader index missing for V6 validation")]
194    LeaderIndexMissing,
195    #[error("Leader counts should reset at epoch start but didn't")]
196    LeaderCountsNotReset,
197    #[error("Invalid leader counts: expected {expected:?}, proposed {proposed:?}")]
198    InvalidLeaderCounts {
199        expected: Box<LeaderCounts>,
200        proposed: Box<LeaderCounts>,
201    },
202}
203
204impl StateDelta for Delta {}
205
206#[derive(Hash, Clone, Deserialize, Serialize, PartialEq, Eq)]
207/// State to be validated by replicas.
208pub struct ValidatedState {
209    /// Frontier of [`BlockMerkleTree`]
210    pub block_merkle_tree: BlockMerkleTree,
211    /// Frontier of [`FeeMerkleTree`]
212    pub fee_merkle_tree: FeeMerkleTree,
213    pub reward_merkle_tree_v1: RewardMerkleTreeV1,
214    pub reward_merkle_tree_v2: RewardMerkleTreeV2,
215    /// Configuration [`Header`] proposals will be validated against.
216    pub chain_config: ResolvableChainConfig,
217}
218
219impl std::fmt::Debug for ValidatedState {
220    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
221        f.debug_struct("ValidatedState")
222            .field("block_merkle_tree", &self.block_merkle_tree.commitment())
223            .field("fee_merkle_tree", &self.fee_merkle_tree.commitment())
224            .field(
225                "reward_merkle_tree_v1",
226                &self.reward_merkle_tree_v1.commitment(),
227            )
228            .field(
229                "reward_merkle_tree_v2",
230                &self.reward_merkle_tree_v2.commitment(),
231            )
232            .field("chain_config", &self.chain_config)
233            .finish()
234    }
235}
236
237impl Default for ValidatedState {
238    fn default() -> Self {
239        let block_merkle_tree = BlockMerkleTree::from_elems(
240            Some(BLOCK_MERKLE_TREE_HEIGHT),
241            Vec::<Commitment<Header>>::new(),
242        )
243        .unwrap();
244
245        // Words of wisdom from @mrain: "capacity = arity^height"
246        // "For index space 2^160, arity 256 (2^8),
247        // you should set the height as 160/8=20"
248        let fee_merkle_tree = FeeMerkleTree::from_kv_set(
249            FEE_MERKLE_TREE_HEIGHT,
250            Vec::<(FeeAccount, FeeAmount)>::new(),
251        )
252        .unwrap();
253
254        let reward_merkle_tree_v1 = RewardMerkleTreeV1::from_kv_set(
255            REWARD_MERKLE_TREE_V1_HEIGHT,
256            Vec::<(RewardAccountV1, RewardAmount)>::new(),
257        )
258        .unwrap();
259
260        let reward_merkle_tree_v2 = RewardMerkleTreeV2::from_kv_set(
261            REWARD_MERKLE_TREE_V2_HEIGHT,
262            Vec::<(RewardAccountV2, RewardAmount)>::new(),
263        )
264        .unwrap();
265
266        let chain_config = ResolvableChainConfig::from(ChainConfig::default());
267
268        Self {
269            block_merkle_tree,
270            fee_merkle_tree,
271            reward_merkle_tree_v1,
272            reward_merkle_tree_v2,
273            chain_config,
274        }
275    }
276}
277
278impl ValidatedState {
279    /// Prefund an account with a given amount. Only for demo purposes.
280    pub fn prefund_account(&mut self, account: FeeAccount, amount: FeeAmount) {
281        self.fee_merkle_tree.update(account, amount).unwrap();
282    }
283
284    pub fn balance(&mut self, account: FeeAccount) -> Option<FeeAmount> {
285        match self.fee_merkle_tree.lookup(account) {
286            LookupResult::Ok(balance, _) => Some(balance),
287            LookupResult::NotFound(_) => Some(0.into()),
288            LookupResult::NotInMemory => None,
289        }
290    }
291
292    /// Find accounts that are not in memory.
293    ///
294    /// As an optimization we could try to apply updates and return the
295    /// forgotten accounts to be fetched from peers and update them later.
296    pub fn forgotten_accounts(
297        &self,
298        accounts: impl IntoIterator<Item = FeeAccount>,
299    ) -> Vec<FeeAccount> {
300        accounts
301            .into_iter()
302            .unique()
303            .filter(|account| {
304                self.fee_merkle_tree
305                    .lookup(*account)
306                    .expect_not_in_memory()
307                    .is_ok()
308            })
309            .collect()
310    }
311
312    pub fn forgotten_reward_accounts_v2(
313        &self,
314        accounts: impl IntoIterator<Item = RewardAccountV2>,
315    ) -> Vec<RewardAccountV2> {
316        accounts
317            .into_iter()
318            .filter(|account| {
319                self.reward_merkle_tree_v2
320                    .lookup(*account)
321                    .expect_not_in_memory()
322                    .is_ok()
323            })
324            .collect()
325    }
326
327    pub fn forgotten_reward_accounts_v1(
328        &self,
329        accounts: impl IntoIterator<Item = RewardAccountV1>,
330    ) -> Vec<RewardAccountV1> {
331        accounts
332            .into_iter()
333            .unique()
334            .filter(|account| {
335                self.reward_merkle_tree_v1
336                    .lookup(*account)
337                    .expect_not_in_memory()
338                    .is_ok()
339            })
340            .collect()
341    }
342
343    /// Check if the merkle tree is available
344    pub fn need_to_fetch_blocks_mt_frontier(&self) -> bool {
345        let num_leaves = self.block_merkle_tree.num_leaves();
346        if num_leaves == 0 {
347            false
348        } else {
349            self.block_merkle_tree
350                .lookup(num_leaves - 1)
351                .expect_ok()
352                .is_err()
353        }
354    }
355
356    /// Insert a fee deposit receipt
357    pub fn insert_fee_deposit(
358        &mut self,
359        fee_info: FeeInfo,
360    ) -> anyhow::Result<LookupResult<FeeAmount, (), ()>> {
361        Ok(self
362            .fee_merkle_tree
363            .update_with(fee_info.account, |balance| {
364                Some(balance.cloned().unwrap_or_default().add(fee_info.amount))
365            })?)
366    }
367
368    pub fn apply_proposal(
369        &mut self,
370        delta: &mut Delta,
371        parent_leaf: &Leaf2,
372        l1_deposits: Vec<FeeInfo>,
373    ) {
374        // pushing a block into merkle tree shouldn't fail
375        self.block_merkle_tree
376            .push(parent_leaf.block_header().commit())
377            .unwrap();
378
379        for FeeInfo { account, amount } in l1_deposits.iter() {
380            self.fee_merkle_tree
381                .update_with(account, |balance| {
382                    Some(balance.cloned().unwrap_or_default().add(*amount))
383                })
384                .expect("update_with succeeds");
385            delta.fees_delta.insert(*account);
386        }
387    }
388
389    pub fn charge_fees(
390        &mut self,
391        delta: &mut Delta,
392        fee_info: Vec<FeeInfo>,
393        recipient: FeeAccount,
394    ) -> Result<(), FeeError> {
395        for fee_info in fee_info {
396            self.charge_fee(fee_info, recipient)?;
397            delta.fees_delta.extend([fee_info.account, recipient]);
398        }
399        Ok(())
400    }
401
402    /// Charge a fee to an account, transferring the funds to the fee recipient account.
403    pub fn charge_fee(&mut self, fee_info: FeeInfo, recipient: FeeAccount) -> Result<(), FeeError> {
404        if fee_info.amount == 0.into() {
405            return Ok(());
406        }
407
408        let fee_state = self.fee_merkle_tree.clone();
409
410        // Deduct the fee from the paying account.
411        let FeeInfo { account, amount } = fee_info;
412        let mut err = None;
413        let fee_state = fee_state.persistent_update_with(account, |balance| {
414            let balance = balance.copied();
415            let Some(updated) = balance.unwrap_or_default().checked_sub(&amount) else {
416                // Return an error without updating the account.
417                err = Some(FeeError::InsufficientFunds { balance, amount });
418                return balance;
419            };
420            if updated == FeeAmount::default() {
421                // Delete the account from the tree if its balance ended up at 0; this saves some
422                // space since the account is no longer carrying any information.
423                None
424            } else {
425                // Otherwise store the updated balance.
426                Some(updated)
427            }
428        })?;
429
430        // Fail if there was an error during `persistent_update_with` (e.g. insufficient balance).
431        if let Some(err) = err {
432            return Err(err);
433        }
434
435        // If we successfully deducted the fee from the source account, increment the balance of the
436        // recipient account.
437        let fee_state = fee_state.persistent_update_with(recipient, |balance| {
438            Some(balance.copied().unwrap_or_default() + amount)
439        })?;
440
441        // If the whole update was successful, update the original state.
442        self.fee_merkle_tree = fee_state;
443        Ok(())
444    }
445}
446/// Block Proposal to be verified and applied.
447#[cfg_attr(not(feature = "node"), allow(dead_code))]
448#[derive(Debug)]
449pub(crate) struct Proposal<'a> {
450    header: &'a Header,
451    /// `None` when this node does not hold the payload; see [`Self::without_size`].
452    block_size: Option<u32>,
453}
454
455#[cfg_attr(not(feature = "node"), allow(dead_code))]
456impl<'a> Proposal<'a> {
457    pub(crate) fn new(header: &'a Header, block_size: u32) -> Self {
458        Self {
459            header,
460            block_size: Some(block_size),
461        }
462    }
463
464    /// A proposal whose payload size this node does not know, e.g. one fetched
465    /// from a peer without a VID share. Only valid for a proposal a quorum has
466    /// already certified, which vouches for the checks this skips: block size,
467    /// fee and namespace table.
468    pub(crate) fn without_size(header: &'a Header) -> Self {
469        Self {
470            header,
471            block_size: None,
472        }
473    }
474    /// The L1 head block number in the proposal must be non-decreasing relative
475    /// to the parent.
476    fn validate_l1_head(&self, parent_l1_head: u64) -> Result<(), ProposalValidationError> {
477        if self.header.l1_head() < parent_l1_head {
478            return Err(ProposalValidationError::DecrementingL1Head);
479        }
480        Ok(())
481    }
482    /// The [`ChainConfig`] of proposal must be equal to the one stored in state.
483    ///
484    /// Equality is checked by comparing commitments.
485    fn validate_chain_config(
486        &self,
487        expected_chain_config: &ChainConfig,
488    ) -> Result<(), ProposalValidationError> {
489        let proposed_chain_config = self.header.chain_config();
490        if proposed_chain_config.commit() != expected_chain_config.commit() {
491            return Err(ProposalValidationError::InvalidChainConfig {
492                expected: Box::new(*expected_chain_config),
493                proposal: Box::new(proposed_chain_config),
494            });
495        }
496        Ok(())
497    }
498
499    /// The timestamp must be non-decreasing relative to parent.
500    fn validate_timestamp_non_dec(
501        &self,
502        parent_timestamp: u64,
503    ) -> Result<(), ProposalValidationError> {
504        if self.header.timestamp() < parent_timestamp {
505            return Err(ProposalValidationError::DecrementingTimestamp {
506                proposal_timestamp: self.header.timestamp(),
507                parent_timestamp,
508            });
509        }
510
511        Ok(())
512    }
513
514    /// The timestamp must not drift too much from local system time.
515    ///
516    /// The tolerance is currently `12` seconds. This value may be moved to
517    /// configuration in the future.
518    fn validate_timestamp_drift(
519        &self,
520        system_time: OffsetDateTime,
521    ) -> Result<(), ProposalValidationError> {
522        // TODO 12 seconds of tolerance should be enough for reasonably
523        // configured nodes, but we should make this configurable.
524        let system_timestamp = system_time.unix_timestamp() as u64;
525        let diff = self.header.timestamp().abs_diff(system_timestamp);
526        if diff > 12 {
527            return Err(ProposalValidationError::InvalidTimestampDrift {
528                proposal: self.header.timestamp(),
529                system: system_timestamp,
530                diff,
531            });
532        }
533
534        Ok(())
535    }
536
537    /// The `timestamp` and `timestamp_millis` fields must be coherent
538    fn validate_timestamp_consistency(&self) -> Result<(), ProposalValidationError> {
539        if self.header.timestamp() != self.header.timestamp_millis() / 1_000 {
540            return Err(ProposalValidationError::InconsistentTimestamps {
541                timestamp: self.header.timestamp(),
542                timestamp_millis: self.header.timestamp_millis(),
543            });
544        }
545
546        Ok(())
547    }
548
549    /// The proposed ['BlockMerkleTree'] must match the one in ['ValidatedState'].
550    fn validate_block_merkle_tree(
551        &self,
552        block_merkle_tree_root: BlockMerkleCommitment,
553    ) -> Result<(), ProposalValidationError> {
554        if self.header.block_merkle_tree_root() != block_merkle_tree_root {
555            return Err(ProposalValidationError::InvalidBlockRoot {
556                expected_root: block_merkle_tree_root,
557                proposal_root: self.header.block_merkle_tree_root(),
558            });
559        }
560
561        Ok(())
562    }
563}
564/// Type to hold cloned validated state and provide validation methods.
565///
566/// The [Self::validate] method must be called to validate the proposal.
567#[cfg_attr(not(feature = "node"), allow(dead_code))]
568#[derive(Debug)]
569pub(crate) struct ValidatedTransition<'a> {
570    state: ValidatedState,
571    expected_chain_config: ChainConfig,
572    parent: &'a Header,
573    proposal: Proposal<'a>,
574    total_rewards_distributed: Option<RewardAmount>,
575    version: Version,
576    validation_start_time: OffsetDateTime,
577    epoch_height: Option<u64>,
578    leader_index: Option<usize>,
579}
580
581#[cfg_attr(not(feature = "node"), allow(dead_code))]
582impl<'a> ValidatedTransition<'a> {
583    #[allow(clippy::too_many_arguments)]
584    pub(crate) fn new(
585        state: ValidatedState,
586        parent: &'a Header,
587        proposal: Proposal<'a>,
588        total_rewards_distributed: Option<RewardAmount>,
589        version: Version,
590        validation_start_time: OffsetDateTime,
591        epoch_height: Option<u64>,
592        leader_index: Option<usize>,
593    ) -> Self {
594        let expected_chain_config = state
595            .chain_config
596            .resolve()
597            .expect("Chain Config not found in validated state");
598        Self {
599            state,
600            expected_chain_config,
601            parent,
602            proposal,
603            total_rewards_distributed,
604            version,
605            validation_start_time,
606            epoch_height,
607            leader_index,
608        }
609    }
610
611    /// Top level validation routine. Performs all validation units in
612    /// the given order.
613    /// ```ignore
614    /// self.validate_timestamp()?;
615    /// self.validate_builder_fee()?;
616    /// self.validate_height()?;
617    /// self.validate_chain_config()?;
618    /// self.validate_block_size()?;
619    /// self.validate_fee()?;
620    /// self.validate_fee_merkle_tree()?;
621    /// self.validate_block_merkle_tree()?;
622    /// self.validate_l1_finalized()?;
623    /// self.validate_l1_head()?;
624    /// self.validate_namespace_table()?;
625    /// self.validate_total_rewards_distributed()?;
626    /// ```
627    pub(crate) fn validate(self) -> Result<Self, ProposalValidationError> {
628        self.validate_timestamp()?;
629        self.validate_builder_fee()?;
630        self.validate_height()?;
631        self.validate_chain_config()?;
632        self.validate_block_size()?;
633        self.validate_fee()?;
634        self.validate_fee_merkle_tree()?;
635        self.validate_block_merkle_tree()?;
636        self.validate_reward_merkle_tree()?;
637        self.validate_l1_finalized()?;
638        self.validate_l1_head()?;
639        self.validate_namespace_table()?;
640        self.validate_total_rewards_distributed()?;
641        self.validate_leader_counts()?;
642
643        Ok(self)
644    }
645
646    /// The proposal [Header::l1_finalized] must be `Some` and non-decreasing relative to parent.
647    fn validate_l1_finalized(&self) -> Result<(), ProposalValidationError> {
648        let proposed_finalized = self.proposal.header.l1_finalized();
649        let parent_finalized = self.parent.l1_finalized();
650
651        if proposed_finalized < parent_finalized {
652            // We are keeping the `Option` in the error b/c its the
653            // cleanest way to represent all the different error
654            // cases. The hash seems less useful and explodes the size
655            // of the error, so we strip it out.
656            return Err(ProposalValidationError::L1FinalizedDecrementing {
657                parent: parent_finalized.map(|block| (block.number, block.timestamp.to::<u64>())),
658                proposed: proposed_finalized
659                    .map(|block| (block.number, block.timestamp.to::<u64>())),
660            });
661        }
662        Ok(())
663    }
664    /// Wait for our view of the L1 chain to catch up to the proposal.
665    ///
666    /// The finalized [L1BlockInfo](super::L1BlockInfo) in the proposal must match the one fetched
667    /// from L1.
668    #[cfg(feature = "node")]
669    async fn wait_for_l1(self, l1_client: &L1Client) -> Result<Self, ProposalValidationError> {
670        self.wait_for_l1_head(l1_client).await;
671        self.wait_for_finalized_block(l1_client).await?;
672        Ok(self)
673    }
674
675    /// Wait for our view of the latest L1 block number to catch up to the
676    /// proposal.
677    #[cfg(feature = "node")]
678    async fn wait_for_l1_head(&self, l1_client: &L1Client) {
679        let _ = l1_client
680            .wait_for_block(self.proposal.header.l1_head())
681            .await;
682    }
683    /// Wait for our view of the finalized L1 block number to catch up to the
684    /// proposal.
685    #[cfg(feature = "node")]
686    async fn wait_for_finalized_block(
687        &self,
688        l1_client: &L1Client,
689    ) -> Result<(), ProposalValidationError> {
690        let proposed_finalized = self.proposal.header.l1_finalized();
691
692        if let Some(proposed_finalized) = proposed_finalized {
693            let finalized = l1_client
694                .wait_for_finalized_block(proposed_finalized.number())
695                .await;
696
697            if finalized != proposed_finalized {
698                return Err(ProposalValidationError::InvalidL1Finalized);
699            }
700        }
701
702        Ok(())
703    }
704
705    /// Ensure that L1 Head on proposal is not decreasing.
706    fn validate_l1_head(&self) -> Result<(), ProposalValidationError> {
707        self.proposal.validate_l1_head(self.parent.l1_head())?;
708        Ok(())
709    }
710    /// Validate basic numerical soundness and builder accounts by
711    /// verifying signatures. Signatures are identified by index of fee `Vec`.
712    fn validate_builder_fee(&self) -> Result<(), ProposalValidationError> {
713        // TODO move logic from stand alone fn to here.
714        if let Err(err) = validate_builder_fee(self.proposal.header, self.version) {
715            return Err(ProposalValidationError::BuilderValidationError(err));
716        }
717        Ok(())
718    }
719    /// Validates proposals [`ChainConfig`] against expectation by comparing commitments.
720    fn validate_chain_config(&self) -> Result<(), ProposalValidationError> {
721        self.proposal
722            .validate_chain_config(&self.expected_chain_config)?;
723        Ok(())
724    }
725    /// Validate that proposal block size does not exceed configured
726    /// `ChainConfig.max_block_size`.
727    fn validate_block_size(&self) -> Result<(), ProposalValidationError> {
728        let Some(block_size) = self.proposal.block_size else {
729            return Ok(());
730        };
731        let block_size = block_size as u64;
732        if block_size > *self.expected_chain_config.max_block_size {
733            return Err(ProposalValidationError::MaxBlockSizeExceeded {
734                max_block_size: self.expected_chain_config.max_block_size,
735                block_size: block_size.into(),
736            });
737        }
738        Ok(())
739    }
740    /// Validate that [`FeeAmount`] that is
741    /// sufficient for block size.
742    fn validate_fee(&self) -> Result<(), ProposalValidationError> {
743        // TODO this should be updated to `base_fee * bundle_size` when we have
744        // VID per bundle or namespace.
745        let Some(amount) = self.proposal.header.fee_info().amount() else {
746            return Err(ProposalValidationError::SomeFeeAmountOutOfRange);
747        };
748        let Some(block_size) = self.proposal.block_size else {
749            return Ok(());
750        };
751
752        if amount < self.expected_chain_config.base_fee * U256::from(block_size) {
753            return Err(ProposalValidationError::InsufficientFee {
754                max_block_size: self.expected_chain_config.max_block_size,
755                base_fee: self.expected_chain_config.base_fee,
756                proposed_fee: amount,
757            });
758        }
759        Ok(())
760    }
761    /// Validate that proposal height is `parent_height + 1`.
762    fn validate_height(&self) -> Result<(), ProposalValidationError> {
763        let parent_header = self.parent;
764        if self.proposal.header.height() != parent_header.height() + 1 {
765            return Err(ProposalValidationError::InvalidHeight {
766                parent_height: parent_header.height(),
767                proposal_height: self.proposal.header.height(),
768            });
769        }
770        Ok(())
771    }
772    /// Validate timestamp is not decreasing relative to parent and is
773    /// within a given tolerance of system time. Tolerance is
774    /// currently 12 seconds. This value may be moved to configuration
775    /// in the future. Do this check first so we don't add unnecessary drift.
776    fn validate_timestamp(&self) -> Result<(), ProposalValidationError> {
777        self.proposal.validate_timestamp_consistency()?;
778
779        self.proposal
780            .validate_timestamp_non_dec(self.parent.timestamp())?;
781
782        self.proposal
783            .validate_timestamp_drift(self.validation_start_time)?;
784
785        Ok(())
786    }
787    /// Validate [`BlockMerkleTree`] by comparing proposed commitment
788    /// that stored in [`ValidatedState`].
789    fn validate_block_merkle_tree(&self) -> Result<(), ProposalValidationError> {
790        let block_merkle_tree_root = self.state.block_merkle_tree.commitment();
791        self.proposal
792            .validate_block_merkle_tree(block_merkle_tree_root)?;
793
794        Ok(())
795    }
796
797    /// Validate [`RewardMerkleTreeV2`] by comparing proposed commitment
798    /// against that stored in [`ValidatedState`].
799    fn validate_reward_merkle_tree(&self) -> Result<(), ProposalValidationError> {
800        match self.proposal.header.reward_merkle_tree_root() {
801            Either::Left(proposal_root) => {
802                let expected_root = self.state.reward_merkle_tree_v1.commitment();
803                if proposal_root != expected_root {
804                    return Err(ProposalValidationError::InvalidV1RewardRoot {
805                        expected_root,
806                        proposal_root,
807                    });
808                }
809            },
810            Either::Right(proposal_root) => {
811                let expected_root = self.state.reward_merkle_tree_v2.commitment();
812                if proposal_root != expected_root {
813                    return Err(ProposalValidationError::InvalidV2RewardRoot {
814                        expected_root,
815                        proposal_root,
816                    });
817                }
818            },
819        }
820
821        Ok(())
822    }
823
824    /// Validate [`FeeMerkleTree`] by comparing proposed commitment
825    /// against that stored in [`ValidatedState`].
826    fn validate_fee_merkle_tree(&self) -> Result<(), ProposalValidationError> {
827        let fee_merkle_tree_root = self.state.fee_merkle_tree.commitment();
828        if self.proposal.header.fee_merkle_tree_root() != fee_merkle_tree_root {
829            return Err(ProposalValidationError::InvalidFeeRoot {
830                expected_root: fee_merkle_tree_root,
831                proposal_root: self.proposal.header.fee_merkle_tree_root(),
832            });
833        }
834
835        Ok(())
836    }
837    /// Proxy to [`super::NsTable::validate()`].
838    fn validate_namespace_table(&self) -> Result<(), ProposalValidationError> {
839        let Some(block_size) = self.proposal.block_size else {
840            return Ok(());
841        };
842        self.proposal
843            .header
844            .ns_table()
845            // Should be safe since `u32` will always fit in a `usize`.
846            .validate(&PayloadByteLen(block_size as usize))
847            .map_err(ProposalValidationError::from)
848    }
849
850    /// Validate that the total rewards distributed in the proposed header matches the actual distributed amount.
851    /// This field is only present in >= V4 version.
852    fn validate_total_rewards_distributed(&self) -> Result<(), ProposalValidationError> {
853        if self.version >= DRB_AND_HEADER_UPGRADE_VERSION {
854            let Some(actual_total) = self.total_rewards_distributed else {
855                // This should never happen - if version >= V4, total_rewards_distributed must be Some
856                return Err(ProposalValidationError::TotalRewardsMismatch {
857                    proposed: self
858                        .proposal
859                        .header
860                        .total_reward_distributed()
861                        .unwrap_or_default(),
862                    actual: RewardAmount::from(0),
863                });
864            };
865
866            let proposed_total =
867                self.proposal
868                    .header
869                    .total_reward_distributed()
870                    .ok_or_else(|| ProposalValidationError::TotalRewardsMismatch {
871                        proposed: RewardAmount::from(0),
872                        actual: actual_total,
873                    })?;
874
875            if proposed_total != actual_total {
876                return Err(ProposalValidationError::TotalRewardsMismatch {
877                    proposed: proposed_total,
878                    actual: actual_total,
879                });
880            }
881        }
882        Ok(())
883    }
884
885    /// Validate leader_counts field in V6 headers.
886    ///
887    /// Uses the leader_index passed during construction to calculate expected counts
888    /// and validate
889    fn validate_leader_counts(&self) -> Result<(), ProposalValidationError> {
890        if self.version < EPOCH_REWARD_VERSION {
891            return Ok(());
892        }
893
894        let Some(epoch_height) = self.epoch_height else {
895            return Err(ProposalValidationError::NoEpochHeight);
896        };
897
898        let Some(leader_index) = self.leader_index else {
899            return Err(ProposalValidationError::LeaderIndexMissing);
900        };
901
902        let proposed_counts = self
903            .proposal
904            .header
905            .leader_counts()
906            .ok_or(ProposalValidationError::LeaderCountsMissing)?;
907
908        let proposed_height = self.proposal.header.height();
909
910        let expected_counts = Header::calculate_leader_counts(
911            self.parent,
912            proposed_height,
913            leader_index,
914            epoch_height,
915        );
916
917        if proposed_counts != &expected_counts {
918            return Err(ProposalValidationError::InvalidLeaderCounts {
919                expected: Box::new(expected_counts),
920                proposed: Box::new(*proposed_counts),
921            });
922        }
923
924        Ok(())
925    }
926}
927
928#[cfg(any(test, feature = "testing"))]
929impl ValidatedState {
930    pub fn forget(&self) -> Self {
931        Self {
932            fee_merkle_tree: FeeMerkleTree::from_commitment(self.fee_merkle_tree.commitment()),
933            block_merkle_tree: BlockMerkleTree::from_commitment(
934                self.block_merkle_tree.commitment(),
935            ),
936            reward_merkle_tree_v2: RewardMerkleTreeV2::from_commitment(
937                self.reward_merkle_tree_v2.commitment(),
938            ),
939            reward_merkle_tree_v1: RewardMerkleTreeV1::from_commitment(
940                self.reward_merkle_tree_v1.commitment(),
941            ),
942            chain_config: ResolvableChainConfig::from(self.chain_config.commit()),
943        }
944    }
945}
946
947impl From<NsTableValidationError> for ProposalValidationError {
948    fn from(err: NsTableValidationError) -> Self {
949        Self::InvalidNsTable(err)
950    }
951}
952
953impl From<ProposalValidationError> for BlockError {
954    fn from(err: ProposalValidationError) -> Self {
955        tracing::error!("Invalid Block Header: {err:#}");
956        BlockError::InvalidBlockHeader(err.to_string())
957    }
958}
959
960impl From<MerkleTreeError> for FeeError {
961    fn from(item: MerkleTreeError) -> Self {
962        Self::MerkleTreeError(item)
963    }
964}
965
966/// Validate builder accounts by verifying signatures. All fees are
967/// verified against signature by index.
968#[cfg_attr(not(feature = "node"), allow(dead_code))]
969fn validate_builder_fee(
970    proposed_header: &Header,
971    version: Version,
972) -> Result<(), BuilderValidationError> {
973    let fee_info = proposed_header.fee_info();
974    let builder_signature = proposed_header.builder_signature();
975
976    if version >= EPOCH_REWARD_VERSION && fee_info.len() != builder_signature.len() {
977        return Err(BuilderValidationError::FeeAndSignatureCountMismatch {
978            fee_count: fee_info.len(),
979            signature_count: builder_signature.len(),
980        });
981    }
982
983    // TODO since we are iterating, should we include account/amount in errors?
984    for (fee_info, signature) in fee_info.iter().zip(builder_signature) {
985        // check that `amount` fits in a u64
986        fee_info
987            .amount()
988            .as_u64()
989            .ok_or(BuilderValidationError::FeeAmountOutOfRange(fee_info.amount))?;
990
991        // Verify signatures.
992        if !fee_info.account().validate_fee_signature(
993            &signature,
994            fee_info.amount().as_u64().unwrap(),
995            proposed_header.metadata(),
996        ) && !fee_info
997            .account()
998            .validate_fee_signature_with_vid_commitment(
999                &signature,
1000                fee_info.amount().as_u64().unwrap(),
1001                proposed_header.metadata(),
1002                &proposed_header.payload_commitment(),
1003            )
1004        {
1005            return Err(BuilderValidationError::InvalidBuilderSignature);
1006        }
1007    }
1008
1009    Ok(())
1010}
1011
1012impl ValidatedState {
1013    /// Updates state with [`Header`] proposal.
1014    ///   * Clones and updates [`ValidatedState`] (avoiding mutation).
1015    ///   * Resolves [`ChainConfig`].
1016    ///   * Performs catchup.
1017    ///   * Charges fees.
1018    pub async fn apply_header(
1019        &self,
1020        instance: &NodeState,
1021        peers: &impl StateCatchup,
1022        parent_leaf: &Leaf2,
1023        proposed_header: &Header,
1024        version: Version,
1025        view_number: ViewNumber,
1026    ) -> anyhow::Result<(Self, Delta, Option<RewardAmount>)> {
1027        // Clone state to avoid mutation. Consumer can take update
1028        // through returned value.
1029        let mut validated_state = self.clone();
1030        validated_state.apply_upgrade(instance, version);
1031
1032        // TODO double check there is not some possibility we are
1033        // validating proposal values against ChainConfig of the proposal.
1034        let chain_config = validated_state
1035            .get_chain_config(instance, peers, &proposed_header.chain_config())
1036            .await?;
1037
1038        if Some(chain_config) != validated_state.chain_config.resolve() {
1039            validated_state.chain_config = chain_config.into();
1040        }
1041
1042        // The new protocol charges no fees and credits no deposits, so it never touches the fee tree.
1043        let (l1_deposits, missing_accounts) = if version < NEW_PROTOCOL_VERSION {
1044            let l1_deposits = get_l1_deposits(
1045                instance,
1046                proposed_header,
1047                parent_leaf,
1048                chain_config.fee_contract,
1049            )
1050            .await;
1051
1052            // Find missing fee state entries. We will need to use the builder account which is
1053            // paying a fee and the recipient account which is receiving it, plus any counts
1054            // receiving deposits in this block.
1055            let missing_accounts = self.forgotten_accounts(
1056                [chain_config.fee_recipient]
1057                    .into_iter()
1058                    .chain(proposed_header.fee_info().accounts())
1059                    .chain(l1_deposits.accounts()),
1060            );
1061            (l1_deposits, missing_accounts)
1062        } else {
1063            (Vec::new(), Vec::new())
1064        };
1065
1066        let parent_height = parent_leaf.height();
1067        let parent_view = parent_leaf.view_number();
1068
1069        // Ensure merkle tree has frontier
1070        if self.need_to_fetch_blocks_mt_frontier() {
1071            tracing::info!(
1072                parent_height,
1073                ?parent_view,
1074                "fetching block frontier from peers"
1075            );
1076            peers
1077                .remember_blocks_merkle_tree(
1078                    instance,
1079                    parent_height,
1080                    parent_view,
1081                    &mut validated_state.block_merkle_tree,
1082                )
1083                .await?;
1084        }
1085
1086        // Fetch missing fee state entries
1087        if !missing_accounts.is_empty() {
1088            tracing::info!(
1089                parent_height,
1090                ?parent_view,
1091                ?missing_accounts,
1092                "fetching missing accounts from peers"
1093            );
1094
1095            let missing_account_proofs = peers
1096                .fetch_accounts(
1097                    instance,
1098                    parent_height,
1099                    parent_view,
1100                    validated_state.fee_merkle_tree.commitment(),
1101                    missing_accounts,
1102                )
1103                .await?;
1104
1105            // Remember the fee state entries
1106            for proof in missing_account_proofs.iter() {
1107                proof
1108                    .remember(&mut validated_state.fee_merkle_tree)
1109                    .expect("proof previously verified");
1110            }
1111        }
1112
1113        let mut delta = Delta::default();
1114        validated_state.apply_proposal(&mut delta, parent_leaf, l1_deposits);
1115
1116        // TODO(abdul): builder is unfunded error
1117        if version < NEW_PROTOCOL_VERSION {
1118            validated_state.charge_fees(
1119                &mut delta,
1120                proposed_header.fee_info(),
1121                chain_config.fee_recipient,
1122            )?;
1123        }
1124
1125        // total_rewards_distributed is only present in >= V4
1126        let total_rewards_distributed = if version < EPOCH_VERSION {
1127            None
1128        } else if version >= EPOCH_REWARD_VERSION {
1129            let parent_header = parent_leaf.block_header();
1130            let epoch_height = instance
1131                .epoch_height
1132                .context("epoch height not in instance state for V6")?;
1133            let leader_index = Header::get_leader_index(
1134                version,
1135                proposed_header.height(),
1136                view_number.u64(),
1137                instance,
1138            )
1139            .await?
1140            .context("leader index not found for V6")?;
1141            let leader_counts = Header::calculate_leader_counts(
1142                parent_header,
1143                proposed_header.height(),
1144                leader_index,
1145                epoch_height,
1146            );
1147            let (epoch_rewards_applied, changed_accounts) = Header::handle_epoch_rewards(
1148                proposed_header.height(),
1149                &leader_counts,
1150                instance,
1151                &mut validated_state,
1152                proposed_header.reward_merkle_tree_root().right(),
1153            )
1154            .await?;
1155
1156            delta.rewards_delta.extend(changed_accounts);
1157
1158            // V6+: parent's total + epoch rewards applied at this boundary
1159            let parent_total = parent_leaf
1160                .block_header()
1161                .total_reward_distributed()
1162                .unwrap_or_default();
1163            Some(RewardAmount(parent_total.0 + epoch_rewards_applied.0))
1164        } else if let Some(reward_distributor) = distribute_block_reward(
1165            instance,
1166            &mut validated_state,
1167            parent_leaf,
1168            view_number,
1169            version,
1170        )
1171        .await?
1172        {
1173            reward_distributor
1174                .update_rewards_delta(&mut delta)
1175                .context("failed to update rewards delta")?;
1176
1177            Some(reward_distributor.total_distributed())
1178        } else {
1179            // Version >= V4 but no rewards were distributed because epoch <= first epoch + 1
1180            Some(Default::default())
1181        };
1182
1183        Ok((validated_state, delta, total_rewards_distributed))
1184    }
1185
1186    /// Updates the `ValidatedState` if a protocol upgrade has occurred.
1187    pub(crate) fn apply_upgrade(&mut self, instance: &NodeState, version: Version) {
1188        // Check for protocol upgrade based on sequencer version
1189        if version <= instance.current_version {
1190            return;
1191        }
1192
1193        let Some(upgrade) = instance.upgrades.get(&version) else {
1194            return;
1195        };
1196
1197        let cf = match upgrade.upgrade_type {
1198            UpgradeType::Fee { chain_config } => chain_config,
1199            UpgradeType::Epoch { chain_config } => chain_config,
1200            UpgradeType::DrbAndHeader { chain_config } => chain_config,
1201            UpgradeType::NewProtocol { chain_config } => chain_config,
1202            UpgradeType::EpochReward { chain_config } => chain_config,
1203            UpgradeType::LargeBlock { chain_config } => chain_config,
1204        };
1205
1206        self.chain_config = cf.into();
1207    }
1208
1209    /// Retrieves the `ChainConfig`.
1210    ///
1211    ///  Returns the `NodeState` `ChainConfig` if the `ValidatedState` `ChainConfig` commitment matches the `NodeState` `ChainConfig`` commitment.
1212    ///  If the commitments do not match, it returns the `ChainConfig` available in either `ValidatedState` or proposed header.
1213    ///  If neither has the `ChainConfig`, it fetches the config from the peers.
1214    ///
1215    /// Returns an error if it fails to fetch the `ChainConfig` from the peers.
1216    pub(crate) async fn get_chain_config(
1217        &self,
1218        instance: &NodeState,
1219        peers: &impl StateCatchup,
1220        header_cf: &ResolvableChainConfig,
1221    ) -> anyhow::Result<ChainConfig> {
1222        let state_cf = self.chain_config;
1223
1224        if state_cf.commit() == instance.chain_config.commit() {
1225            return Ok(instance.chain_config);
1226        }
1227
1228        let cf = match (state_cf.resolve(), header_cf.resolve()) {
1229            (Some(cf), _) => cf,
1230            (_, Some(cf)) if cf.commit() == state_cf.commit() => cf,
1231            (_, Some(_)) | (None, None) => peers.fetch_chain_config(state_cf.commit()).await?,
1232        };
1233
1234        Ok(cf)
1235    }
1236}
1237
1238#[cfg(feature = "node")]
1239pub async fn get_l1_deposits(
1240    instance: &NodeState,
1241    header: &Header,
1242    parent_leaf: &Leaf2,
1243    fee_contract_address: Option<Address>,
1244) -> Vec<FeeInfo> {
1245    if let (Some(addr), Some(block_info)) = (fee_contract_address, header.l1_finalized()) {
1246        instance
1247            .l1_client
1248            .get_finalized_deposits(
1249                addr,
1250                parent_leaf
1251                    .block_header()
1252                    .l1_finalized()
1253                    .map(|block_info| block_info.number),
1254                block_info.number,
1255            )
1256            .await
1257    } else {
1258        vec![]
1259    }
1260}
1261
1262/// Twin of the `node` variant so callers compile unchanged. Only the actual
1263/// fetch requires the node feature; chains without a fee contract or without a
1264/// finalized L1 block behave identically in both cfgs.
1265#[cfg(not(feature = "node"))]
1266pub async fn get_l1_deposits(
1267    _instance: &NodeState,
1268    header: &Header,
1269    _parent_leaf: &Leaf2,
1270    fee_contract_address: Option<Address>,
1271) -> Vec<FeeInfo> {
1272    if fee_contract_address.is_some() && header.l1_finalized().is_some() {
1273        unimplemented!("fetching L1 deposits requires the node feature")
1274    } else {
1275        vec![]
1276    }
1277}
1278
1279#[cfg_attr(not(feature = "node"), allow(dead_code))]
1280async fn validate_next_stake_table_hash(
1281    instance: &NodeState,
1282    proposed_header: &Header,
1283) -> Result<(), ProposalValidationError> {
1284    let Some(epoch_height) = instance.epoch_height else {
1285        return Err(ProposalValidationError::NoEpochHeight);
1286    };
1287    if !is_ge_epoch_root(proposed_header.height(), epoch_height) {
1288        return Ok(());
1289    }
1290    let epoch = EpochNumber::new(epoch_from_block_number(
1291        proposed_header.height(),
1292        epoch_height,
1293    ));
1294    let coordinator = instance.coordinator.clone();
1295    let first_epoch = coordinator.membership().first_epoch();
1296
1297    // We only require a `stake_table_hash` for epochs past the second
1298    let Some(proposed_next_stake_table_hash) = proposed_header.next_stake_table_hash() else {
1299        if epoch <= first_epoch {
1300            return Ok(());
1301        } else {
1302            return Err(ProposalValidationError::NextStakeTableHashNotNone);
1303        }
1304    };
1305
1306    let epoch_membership = instance
1307        .coordinator
1308        .stake_table_for_epoch(Some(epoch + 1))
1309        .map_err(|_| ProposalValidationError::NextStakeTableNotFound)?;
1310    let next_stake_table_hash = epoch_membership
1311        .stake_table_hash()
1312        .ok_or(ProposalValidationError::NextStakeTableHashNotFound)?;
1313    if next_stake_table_hash != proposed_next_stake_table_hash {
1314        return Err(ProposalValidationError::NextStakeTableHashMismatch {
1315            expected: next_stake_table_hash,
1316            proposal: proposed_next_stake_table_hash,
1317        });
1318    }
1319    Ok(())
1320}
1321
1322impl HotShotState<SeqTypes> for ValidatedState {
1323    type Error = BlockError;
1324    type Instance = NodeState;
1325
1326    type Delta = Delta;
1327    fn on_commit(&self) {}
1328    /// Validate parent against known values (from state) and validate
1329    /// proposal descends from parent. Returns updated `ValidatedState`.
1330    #[tracing::instrument(
1331        skip_all,
1332        fields(
1333            node_id = instance.node_id,
1334            view = ?parent_leaf.view_number(),
1335            height = parent_leaf.height(),
1336        ),
1337    )]
1338    #[cfg_attr(not(feature = "node"), allow(unused_variables))]
1339    async fn validate_and_apply_header(
1340        &self,
1341        instance: &Self::Instance,
1342        parent_leaf: &Leaf2,
1343        proposed_header: &Header,
1344        payload_byte_len: Option<u32>,
1345        version: Version,
1346        view_number: u64,
1347    ) -> Result<(Self, Self::Delta), Self::Error> {
1348        // The L1 deposit fetch and the wait for the proposal's L1 blocks require an L1 client.
1349        #[cfg(not(feature = "node"))]
1350        {
1351            unimplemented!("validate_and_apply_header requires the node feature");
1352        }
1353        #[cfg(feature = "node")]
1354        {
1355            // Preferably we would do all validation that does not require catchup first, but this
1356            // would require some refactoring of the header validation code that is out of scope for
1357            // now. Record the time when validation started to later use it to validate the
1358            // timestamp drift.
1359            let validation_start_time = OffsetDateTime::now_utc();
1360
1361            let (validated_state, delta, total_rewards_distributed) = self
1362                // TODO We can add this logic to `ValidatedTransition` or do something similar to that here.
1363                .apply_header(
1364                    instance,
1365                    &instance.state_catchup,
1366                    parent_leaf,
1367                    proposed_header,
1368                    version,
1369                    ViewNumber::new(view_number),
1370                )
1371                .await
1372                .map_err(|e| BlockError::FailedHeaderApply(e.to_string()))?;
1373
1374            if version >= DRB_AND_HEADER_UPGRADE_VERSION {
1375                validate_next_stake_table_hash(instance, proposed_header).await?;
1376            }
1377
1378            // Get leader index for V6+ validation
1379            let leader_index =
1380                Header::get_leader_index(version, proposed_header.height(), view_number, instance)
1381                    .await
1382                    .map_err(|e| BlockError::InvalidBlockHeader(e.to_string()))?;
1383
1384            // Validate the proposal.
1385            let validated_state = ValidatedTransition::new(
1386                validated_state,
1387                parent_leaf.block_header(),
1388                match payload_byte_len {
1389                    Some(len) => Proposal::new(proposed_header, len),
1390                    None => Proposal::without_size(proposed_header),
1391                },
1392                total_rewards_distributed,
1393                version,
1394                validation_start_time,
1395                instance.epoch_height,
1396                leader_index,
1397            )
1398            .validate()?
1399            .wait_for_l1(&instance.l1_client)
1400            .await?
1401            .state;
1402
1403            // log successful progress about once in 10 - 20 seconds,
1404            // TODO: we may want to make this configurable
1405            if parent_leaf.view_number().u64().is_multiple_of(10) {
1406                tracing::info!("validated and applied new header");
1407            }
1408            Ok((validated_state, delta))
1409        }
1410    }
1411    /// Construct the state with the given block header.
1412    ///
1413    /// This can also be used to rebuild the state for catchup.
1414    fn from_header(block_header: &Header) -> Self {
1415        let fee_merkle_tree = if block_header.fee_merkle_tree_root().size() == 0 {
1416            // If the commitment tells us that the tree is supposed to be empty, it is convenient to
1417            // just create an empty tree, rather than a commitment-only tree.
1418            FeeMerkleTree::new(FEE_MERKLE_TREE_HEIGHT)
1419        } else {
1420            FeeMerkleTree::from_commitment(block_header.fee_merkle_tree_root())
1421        };
1422        let block_merkle_tree = if block_header.block_merkle_tree_root().size() == 0 {
1423            // If the commitment tells us that the tree is supposed to be empty, it is convenient to
1424            // just create an empty tree, rather than a commitment-only tree.
1425            BlockMerkleTree::new(BLOCK_MERKLE_TREE_HEIGHT)
1426        } else {
1427            BlockMerkleTree::from_commitment(block_header.block_merkle_tree_root())
1428        };
1429
1430        let (reward_merkle_tree_v1, reward_merkle_tree_v2) = match block_header
1431            .reward_merkle_tree_root()
1432        {
1433            Either::Left(reward_tree_v1) => {
1434                let reward_merkle_tree_v2 = RewardMerkleTreeV2::new(REWARD_MERKLE_TREE_V2_HEIGHT);
1435                let reward_merkle_tree_v1 = if reward_tree_v1.size() == 0 {
1436                    RewardMerkleTreeV1::new(REWARD_MERKLE_TREE_V1_HEIGHT)
1437                } else {
1438                    RewardMerkleTreeV1::from_commitment(reward_tree_v1)
1439                };
1440                (reward_merkle_tree_v1, reward_merkle_tree_v2)
1441            },
1442            Either::Right(reward_tree_v2) => {
1443                let reward_merkle_tree_v1 = RewardMerkleTreeV1::new(REWARD_MERKLE_TREE_V1_HEIGHT);
1444                let reward_merkle_tree_v2 = if reward_tree_v2.size() == 0 {
1445                    RewardMerkleTreeV2::new(REWARD_MERKLE_TREE_V2_HEIGHT)
1446                } else {
1447                    RewardMerkleTreeV2::from_commitment(reward_tree_v2)
1448                };
1449                (reward_merkle_tree_v1, reward_merkle_tree_v2)
1450            },
1451        };
1452
1453        Self {
1454            fee_merkle_tree,
1455            block_merkle_tree,
1456            reward_merkle_tree_v2,
1457            reward_merkle_tree_v1,
1458            chain_config: block_header.chain_config(),
1459        }
1460    }
1461    /// Construct a genesis validated state.
1462    fn genesis(instance: &Self::Instance) -> (Self, Self::Delta) {
1463        (instance.genesis_state.clone(), Delta::default())
1464    }
1465}
1466
1467// Required for TestableState
1468#[cfg(any(test, feature = "testing"))]
1469impl std::fmt::Display for ValidatedState {
1470    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1471        write!(f, "{self:#?}")
1472    }
1473}
1474
1475#[cfg(any(test, feature = "testing"))]
1476impl hotshot_types::traits::states::TestableState<SeqTypes> for ValidatedState {
1477    fn create_random_transaction(
1478        _state: Option<&Self>,
1479        rng: &mut dyn rand::RngCore,
1480        _padding: u64,
1481    ) -> crate::Transaction {
1482        crate::Transaction::random(rng)
1483    }
1484}
1485
1486impl MerklizedState<SeqTypes, { Self::ARITY }> for BlockMerkleTree {
1487    type Key = Self::Index;
1488    type Entry = Commitment<Header>;
1489    type T = Sha3Node;
1490    type Commit = Self::Commitment;
1491    type Digest = Sha3Digest;
1492
1493    fn state_type() -> &'static str {
1494        "block_merkle_tree"
1495    }
1496
1497    fn header_state_commitment_field() -> &'static str {
1498        "block_merkle_tree_root"
1499    }
1500
1501    fn tree_height() -> usize {
1502        BLOCK_MERKLE_TREE_HEIGHT
1503    }
1504
1505    fn insert_path(
1506        &mut self,
1507        key: Self::Key,
1508        proof: &MerkleProof<Self::Entry, Self::Key, Self::T, { Self::ARITY }>,
1509    ) -> anyhow::Result<()> {
1510        let Some(elem) = proof.elem() else {
1511            bail!("BlockMerkleTree does not support non-membership proofs");
1512        };
1513        self.remember(key, elem, proof)?;
1514        Ok(())
1515    }
1516}
1517
1518impl MerklizedState<SeqTypes, { Self::ARITY }> for FeeMerkleTree {
1519    type Key = Self::Index;
1520    type Entry = Self::Element;
1521    type T = Sha3Node;
1522    type Commit = Self::Commitment;
1523    type Digest = Sha3Digest;
1524
1525    fn state_type() -> &'static str {
1526        "fee_merkle_tree"
1527    }
1528
1529    fn header_state_commitment_field() -> &'static str {
1530        "fee_merkle_tree_root"
1531    }
1532
1533    fn tree_height() -> usize {
1534        FEE_MERKLE_TREE_HEIGHT
1535    }
1536
1537    fn insert_path(
1538        &mut self,
1539        key: Self::Key,
1540        proof: &MerkleProof<Self::Entry, Self::Key, Self::T, { Self::ARITY }>,
1541    ) -> anyhow::Result<()> {
1542        match proof.elem() {
1543            Some(elem) => self.remember(key, elem, proof)?,
1544            None => self.non_membership_remember(key, proof)?,
1545        }
1546        Ok(())
1547    }
1548}
1549
1550impl MerklizedState<SeqTypes, { Self::ARITY }> for RewardMerkleTreeV2 {
1551    type Key = Self::Index;
1552    type Entry = Self::Element;
1553    type T = KeccakNode;
1554    type Commit = Self::Commitment;
1555    type Digest = Keccak256Hasher;
1556
1557    fn state_type() -> &'static str {
1558        "reward_merkle_tree_v2"
1559    }
1560
1561    fn header_state_commitment_field() -> &'static str {
1562        "reward_merkle_tree_root"
1563    }
1564
1565    fn tree_height() -> usize {
1566        REWARD_MERKLE_TREE_V2_HEIGHT
1567    }
1568
1569    fn insert_path(
1570        &mut self,
1571        key: Self::Key,
1572        proof: &MerkleProof<Self::Entry, Self::Key, Self::T, { Self::ARITY }>,
1573    ) -> anyhow::Result<()> {
1574        match proof.elem() {
1575            Some(elem) => self.remember(key, elem, proof)?,
1576            None => self.non_membership_remember(key, proof)?,
1577        }
1578        Ok(())
1579    }
1580}
1581
1582impl MerklizedState<SeqTypes, { Self::ARITY }> for RewardMerkleTreeV1 {
1583    type Key = Self::Index;
1584    type Entry = Self::Element;
1585    type T = Sha3Node;
1586    type Commit = Self::Commitment;
1587    type Digest = Sha3Digest;
1588
1589    fn state_type() -> &'static str {
1590        "reward_merkle_tree"
1591    }
1592
1593    fn header_state_commitment_field() -> &'static str {
1594        "reward_merkle_tree_root"
1595    }
1596
1597    fn tree_height() -> usize {
1598        REWARD_MERKLE_TREE_V1_HEIGHT
1599    }
1600
1601    fn insert_path(
1602        &mut self,
1603        key: Self::Key,
1604        proof: &MerkleProof<Self::Entry, Self::Key, Self::T, { Self::ARITY }>,
1605    ) -> anyhow::Result<()> {
1606        match proof.elem() {
1607            Some(elem) => self.remember(key, elem, proof)?,
1608            None => self.non_membership_remember(key, proof)?,
1609        }
1610        Ok(())
1611    }
1612}
1613
1614#[cfg(test)]
1615mod test {
1616    use std::{sync::Arc, time::Duration};
1617
1618    use espresso_utils::ser::FromStringOrInteger;
1619    use hotshot_example_types::node_types::TEST_VERSIONS;
1620    use hotshot_query_service::{Resolvable, testing::mocks::MOCK_UPGRADE};
1621    use hotshot_types::{
1622        data::ViewNumber,
1623        traits::{BlockPayload, signature_key::BuilderSignatureKey},
1624    };
1625    use tracing::debug;
1626    use versions::{
1627        FEE_VERSION, LARGE_BLOCK_VERSION, MAX_SUPPORTED_VERSION, NEW_PROTOCOL_VERSION, version,
1628    };
1629
1630    use super::*;
1631    use crate::{
1632        BlockSize, FeeAccountProof, FeeMerkleProof, Leaf, Payload, TimestampMillis, Transaction,
1633        UpgradeMode, UpgradeType, ViewBasedUpgrade,
1634        eth_signature_key::{BuilderSignature, EthKeyPair},
1635        mock::MockStateCatchup,
1636        v0_1, v0_2, v0_3, v0_4, v0_5, v0_6, v0_7,
1637    };
1638
1639    impl Transaction {
1640        async fn into_mock_header(self) -> (Header, u32) {
1641            let instance = NodeState::mock();
1642            let (payload, metadata) =
1643                Payload::from_transactions(&[self], &instance.genesis_state, &instance)
1644                    .await
1645                    .unwrap();
1646
1647            let header = Header::genesis(
1648                &instance,
1649                payload.clone(),
1650                &metadata,
1651                instance.genesis_version,
1652            );
1653
1654            let header = header.sign();
1655
1656            (header, payload.byte_len().0 as u32)
1657        }
1658    }
1659    impl Header {
1660        /// Build a new header from parent.
1661        fn next(self) -> Self {
1662            let time = OffsetDateTime::now_utc();
1663            let timestamp = time.unix_timestamp() as u64;
1664            let timestamp_millis = TimestampMillis::from_time(&time);
1665
1666            match self {
1667                Header::V1(_) => panic!("You called `Header.next()` on unimplemented version (v1)"),
1668                Header::V2(parent) => Header::V2(v0_2::Header {
1669                    height: parent.height + 1,
1670                    timestamp,
1671                    ..parent.clone()
1672                }),
1673                Header::V3(parent) => Header::V3(v0_3::Header {
1674                    height: parent.height + 1,
1675                    timestamp,
1676                    ..parent.clone()
1677                }),
1678                Header::V4(parent) => Header::V4(v0_4::Header {
1679                    height: parent.height + 1,
1680                    timestamp,
1681                    timestamp_millis,
1682                    ..parent.clone()
1683                }),
1684                Header::V5(parent) => Header::V5(v0_5::Header {
1685                    height: parent.height + 1,
1686                    timestamp,
1687                    timestamp_millis,
1688                    ..parent.clone()
1689                }),
1690                Header::V6(parent) => Header::V6(v0_6::Header {
1691                    height: parent.height + 1,
1692                    timestamp,
1693                    timestamp_millis,
1694                    ..parent.clone()
1695                }),
1696                Header::V7(parent) => Header::V7(v0_7::Header {
1697                    height: parent.height + 1,
1698                    timestamp,
1699                    timestamp_millis,
1700                    ..parent.clone()
1701                }),
1702            }
1703        }
1704        /// Replaces builder signature w/ invalid one.
1705        fn sign(&self) -> Self {
1706            let key_pair = EthKeyPair::random();
1707            let fee_info = FeeInfo::new(key_pair.fee_account(), 1);
1708
1709            let sig = FeeAccount::sign_fee(
1710                &key_pair,
1711                fee_info.amount().as_u64().unwrap(),
1712                self.metadata(),
1713            )
1714            .unwrap();
1715
1716            match self {
1717                Header::V1(_) => panic!("You called `Header.sign()` on unimplemented version (v1)"),
1718                Header::V2(header) => Header::V2(v0_2::Header {
1719                    fee_info,
1720                    builder_signature: Some(sig),
1721                    ..header.clone()
1722                }),
1723                Header::V3(header) => Header::V3(v0_3::Header {
1724                    fee_info,
1725                    builder_signature: Some(sig),
1726                    ..header.clone()
1727                }),
1728                Header::V4(header) => Header::V4(v0_4::Header {
1729                    fee_info,
1730                    builder_signature: Some(sig),
1731                    ..header.clone()
1732                }),
1733                Header::V5(header) => Header::V5(v0_5::Header {
1734                    fee_info,
1735                    builder_signature: Some(sig),
1736                    ..header.clone()
1737                }),
1738                Header::V6(header) => Header::V6(v0_6::Header {
1739                    fee_info,
1740                    builder_signature: Some(sig),
1741                    ..header.clone()
1742                }),
1743                Header::V7(header) => Header::V7(v0_7::Header {
1744                    fee_info,
1745                    builder_signature: Some(sig),
1746                    ..header.clone()
1747                }),
1748            }
1749        }
1750
1751        /// Replaces builder signature w/ invalid one.
1752        fn invalid_builder_signature(&self) -> Self {
1753            let key_pair = EthKeyPair::random();
1754            let key_pair2 = EthKeyPair::random();
1755            let fee_info = FeeInfo::new(key_pair.fee_account(), 1);
1756
1757            let sig = FeeAccount::sign_fee(
1758                &key_pair2,
1759                fee_info.amount().as_u64().unwrap(),
1760                self.metadata(),
1761            )
1762            .unwrap();
1763            self.with_fee(fee_info, Some(sig))
1764        }
1765
1766        /// Replaces the fee info and builder signature.
1767        fn with_fee(&self, fee_info: FeeInfo, builder_signature: Option<BuilderSignature>) -> Self {
1768            match self {
1769                Header::V1(_) => {
1770                    panic!("You called `Header.with_fee()` on unimplemented version (v1)")
1771                },
1772                Header::V2(parent) => Header::V2(v0_2::Header {
1773                    fee_info,
1774                    builder_signature,
1775                    ..parent.clone()
1776                }),
1777                Header::V3(parent) => Header::V3(v0_3::Header {
1778                    fee_info,
1779                    builder_signature,
1780                    ..parent.clone()
1781                }),
1782                Header::V4(parent) => Header::V4(v0_4::Header {
1783                    fee_info,
1784                    builder_signature,
1785                    ..parent.clone()
1786                }),
1787                Header::V5(parent) => Header::V5(v0_5::Header {
1788                    fee_info,
1789                    builder_signature,
1790                    ..parent.clone()
1791                }),
1792                Header::V6(parent) => Header::V6(v0_6::Header {
1793                    fee_info,
1794                    builder_signature,
1795                    ..parent.clone()
1796                }),
1797                Header::V7(parent) => Header::V7(v0_7::Header {
1798                    fee_info,
1799                    builder_signature,
1800                    ..parent.clone()
1801                }),
1802            }
1803        }
1804    }
1805
1806    impl<'a> ValidatedTransition<'a> {
1807        fn mock(instance: NodeState, parent: &'a Header, proposal: Proposal<'a>) -> Self {
1808            let expected_chain_config = instance.chain_config;
1809            let validation_start_time = OffsetDateTime::now_utc();
1810
1811            Self {
1812                state: instance.genesis_state,
1813                expected_chain_config,
1814                parent,
1815                proposal,
1816                total_rewards_distributed: None,
1817                epoch_height: instance.epoch_height,
1818                version: instance.current_version,
1819                validation_start_time,
1820                leader_index: None,
1821            }
1822        }
1823    }
1824
1825    #[test_log::test]
1826    fn test_fee_proofs() {
1827        let mut tree = ValidatedState::default().fee_merkle_tree;
1828        let account1 = Address::random();
1829        let account2 = Address::default();
1830        tracing::info!(%account1, %account2);
1831
1832        let balance1 = U256::from(100);
1833        tree.update(FeeAccount(account1), FeeAmount(balance1))
1834            .unwrap();
1835
1836        // Membership proof.
1837        let (proof1, balance) = FeeAccountProof::prove(&tree, account1).unwrap();
1838        tracing::info!(?proof1, %balance);
1839        assert_eq!(balance, balance1);
1840        assert!(matches!(proof1.proof, FeeMerkleProof::Presence(_)));
1841        assert_eq!(proof1.verify(&tree.commitment()).unwrap(), balance1);
1842
1843        // Non-membership proof.
1844        let (proof2, balance) = FeeAccountProof::prove(&tree, account2).unwrap();
1845        tracing::info!(?proof2, %balance);
1846        assert_eq!(balance, U256::ZERO);
1847        assert!(matches!(proof2.proof, FeeMerkleProof::Absence(_)));
1848        assert_eq!(proof2.verify(&tree.commitment()).unwrap(), U256::ZERO);
1849
1850        // Test forget/remember. We cannot generate proofs in a completely sparse tree:
1851        let mut tree = FeeMerkleTree::from_commitment(tree.commitment());
1852        assert!(FeeAccountProof::prove(&tree, account1).is_none());
1853        assert!(FeeAccountProof::prove(&tree, account2).is_none());
1854        // After remembering the proofs, we can generate proofs again:
1855        proof1.remember(&mut tree).unwrap();
1856        proof2.remember(&mut tree).unwrap();
1857        FeeAccountProof::prove(&tree, account1).unwrap();
1858        FeeAccountProof::prove(&tree, account2).unwrap();
1859    }
1860
1861    #[test_log::test(tokio::test(flavor = "multi_thread"))]
1862    async fn test_validation_l1_head() {
1863        // Setup.
1864        let tx = Transaction::of_size(10);
1865        let (header, block_size) = tx.into_mock_header().await;
1866
1867        // Success Case
1868        let proposal = Proposal::new(&header, block_size);
1869        // Note we are using the same header for parent and proposal,
1870        // this may be OK depending on what we are testing.
1871        ValidatedTransition::mock(NodeState::mock(), &header, proposal)
1872            .validate_l1_head()
1873            .unwrap();
1874
1875        // Error Case
1876        let proposal = Proposal::new(&header, block_size);
1877        let err = proposal.validate_l1_head(u64::MAX).unwrap_err();
1878        assert_eq!(ProposalValidationError::DecrementingL1Head, err);
1879    }
1880
1881    #[test_log::test(tokio::test(flavor = "multi_thread"))]
1882    async fn test_validation_builder_fee() {
1883        // Setup.
1884        let instance = NodeState::mock();
1885        let tx = Transaction::of_size(20);
1886        let (header, block_size) = tx.into_mock_header().await;
1887
1888        // Success Case
1889        let proposal = Proposal::new(&header, block_size);
1890        ValidatedTransition::mock(instance.clone(), &header, proposal)
1891            .validate_builder_fee()
1892            .unwrap();
1893
1894        // Error Case
1895        let header = header.invalid_builder_signature();
1896        let proposal = Proposal::new(&header, block_size);
1897        let err = ValidatedTransition::mock(instance, &header, proposal)
1898            .validate_builder_fee()
1899            .unwrap_err();
1900
1901        tracing::info!(%err, "task failed successfully");
1902        assert_eq!(
1903            ProposalValidationError::BuilderValidationError(
1904                BuilderValidationError::InvalidBuilderSignature
1905            ),
1906            err
1907        );
1908    }
1909
1910    /// A state holding only a chain config commitment takes the full config from the instance
1911    /// when the commitments match, without asking peers.
1912    #[test_log::test(tokio::test(flavor = "multi_thread"))]
1913    async fn test_chain_config_resolved_from_instance() {
1914        // The mock peers serve the default config, so the instance's must differ from it for
1915        // an answer fetched from them to fail the assertion.
1916        let instance = NodeState::mock().with_chain_config(ChainConfig {
1917            max_block_size: BlockSize(300),
1918            ..Default::default()
1919        });
1920        let commitment_only = ResolvableChainConfig::from(instance.chain_config.commit());
1921        let state = ValidatedState {
1922            chain_config: commitment_only,
1923            ..Default::default()
1924        };
1925
1926        let chain_config = state
1927            .get_chain_config(&instance, &MockStateCatchup::default(), &commitment_only)
1928            .await
1929            .unwrap();
1930
1931        assert_eq!(chain_config, instance.chain_config);
1932    }
1933
1934    #[test_log::test(tokio::test(flavor = "multi_thread"))]
1935    async fn test_validation_chain_config() {
1936        // Setup.
1937        let instance = NodeState::mock();
1938        let tx = Transaction::of_size(20);
1939        let (header, block_size) = tx.into_mock_header().await;
1940
1941        // Success Case
1942        let proposal = Proposal::new(&header, block_size);
1943        ValidatedTransition::mock(instance.clone(), &header, proposal)
1944            .validate_chain_config()
1945            .unwrap();
1946
1947        // Error Case
1948        let proposal = Proposal::new(&header, block_size);
1949        let expected_chain_config = ChainConfig {
1950            max_block_size: BlockSize(3333),
1951            ..instance.chain_config
1952        };
1953        let err = proposal
1954            .validate_chain_config(&expected_chain_config)
1955            .unwrap_err();
1956
1957        tracing::info!(%err, "task failed successfully");
1958
1959        assert_eq!(
1960            ProposalValidationError::InvalidChainConfig {
1961                expected: Box::new(expected_chain_config),
1962                proposal: Box::new(header.chain_config())
1963            },
1964            err
1965        );
1966    }
1967
1968    #[test_log::test(tokio::test(flavor = "multi_thread"))]
1969    async fn test_validation_max_block_size() {
1970        const MAX_BLOCK_SIZE: usize = 10;
1971
1972        // Setup.
1973        let state = ValidatedState::default();
1974        let expected_chain_config = ChainConfig {
1975            max_block_size: BlockSize::from_integer(MAX_BLOCK_SIZE as u64).unwrap(),
1976            ..state.chain_config.resolve().unwrap()
1977        };
1978        let instance = NodeState::mock().with_chain_config(expected_chain_config);
1979        let tx = Transaction::of_size(20);
1980        let (header, block_size) = tx.into_mock_header().await;
1981
1982        // Error Case
1983        let proposal = Proposal::new(&header, block_size);
1984        let err = ValidatedTransition::mock(instance.clone(), &header, proposal)
1985            .validate_block_size()
1986            .unwrap_err();
1987
1988        tracing::info!(%err, "task failed successfully");
1989        assert_eq!(
1990            ProposalValidationError::MaxBlockSizeExceeded {
1991                max_block_size: instance.chain_config.max_block_size,
1992                block_size: BlockSize::from_integer(block_size as u64).unwrap()
1993            },
1994            err
1995        );
1996
1997        // Success Case
1998        let proposal = Proposal::new(&header, 1);
1999        ValidatedTransition::mock(instance, &header, proposal)
2000            .validate_block_size()
2001            .unwrap()
2002    }
2003
2004    #[test_log::test(tokio::test(flavor = "multi_thread"))]
2005    async fn test_validation_base_fee() {
2006        // Setup
2007        let tx = Transaction::of_size(20);
2008        let (header, block_size) = tx.into_mock_header().await;
2009        let state = ValidatedState::default();
2010        let instance = NodeState::mock().with_chain_config(ChainConfig {
2011            base_fee: 1000.into(), // High expected base fee
2012            ..state.chain_config.resolve().unwrap()
2013        });
2014
2015        let proposal = Proposal::new(&header, block_size);
2016        let err = ValidatedTransition::mock(instance.clone(), &header, proposal)
2017            .validate_fee()
2018            .unwrap_err();
2019
2020        // Validation fails because the genesis fee (0) is too low.
2021        tracing::info!(%err, "task failed successfully");
2022        assert_eq!(
2023            ProposalValidationError::InsufficientFee {
2024                max_block_size: instance.chain_config.max_block_size,
2025                base_fee: instance.chain_config.base_fee,
2026                proposed_fee: header.fee_info().amount().unwrap()
2027            },
2028            err
2029        );
2030    }
2031
2032    #[test_log::test(tokio::test(flavor = "multi_thread"))]
2033    async fn test_validation_height() {
2034        // Setup
2035        let instance = NodeState::mock();
2036        let tx = Transaction::of_size(10);
2037        let (parent, block_size) = tx.into_mock_header().await;
2038
2039        let proposal = Proposal::new(&parent, block_size);
2040        let err = ValidatedTransition::mock(instance.clone(), &parent, proposal)
2041            .validate_height()
2042            .unwrap_err();
2043
2044        // Validation fails because the proposal is using same default.
2045        tracing::info!(%err, "task failed successfully");
2046        assert_eq!(
2047            ProposalValidationError::InvalidHeight {
2048                parent_height: parent.height(),
2049                proposal_height: parent.height()
2050            },
2051            err
2052        );
2053
2054        // Success case. Increment height on proposal.
2055        let mut header = parent.clone();
2056        *header.height_mut() += 1;
2057        let proposal = Proposal::new(&header, block_size);
2058
2059        ValidatedTransition::mock(instance, &parent, proposal)
2060            .validate_height()
2061            .unwrap();
2062    }
2063
2064    #[test_log::test(tokio::test(flavor = "multi_thread"))]
2065    async fn test_validation_timestamp_non_dec() {
2066        let tx = Transaction::of_size(10);
2067        let (parent, block_size) = tx.into_mock_header().await;
2068
2069        // Error case
2070        let proposal = Proposal::new(&parent, block_size);
2071        let proposal_timestamp = proposal.header.timestamp();
2072        let err = proposal.validate_timestamp_non_dec(u64::MAX).unwrap_err();
2073
2074        // Validation fails because the proposal is using same default.
2075        tracing::info!(%err, "task failed successfully");
2076        assert_eq!(
2077            ProposalValidationError::DecrementingTimestamp {
2078                proposal_timestamp,
2079                parent_timestamp: u64::MAX,
2080            },
2081            err
2082        );
2083
2084        // Success case (genesis timestamp is `0`).
2085        let proposal = Proposal::new(&parent, block_size);
2086        proposal.validate_timestamp_non_dec(0).unwrap();
2087    }
2088
2089    #[test_log::test(tokio::test(flavor = "multi_thread"))]
2090    async fn test_validation_timestamp_drift() {
2091        // Setup
2092        let instance = NodeState::mock();
2093        let (parent, block_size) = Transaction::of_size(10).into_mock_header().await;
2094
2095        let header = parent.clone();
2096        // Error case.
2097        let proposal = Proposal::new(&header, block_size);
2098        let proposal_timestamp = header.timestamp();
2099
2100        let mock_time = OffsetDateTime::now_utc().unix_timestamp() as u64;
2101        // TODO
2102        let err = ValidatedTransition::mock(instance.clone(), &parent, proposal)
2103            .validate_timestamp()
2104            .unwrap_err();
2105
2106        tracing::info!(%err, "task failed successfully");
2107        assert_eq!(
2108            ProposalValidationError::InvalidTimestampDrift {
2109                proposal: proposal_timestamp,
2110                system: mock_time,
2111                diff: mock_time
2112            },
2113            err
2114        );
2115
2116        let time = OffsetDateTime::now_utc();
2117        let timestamp: u64 = time.unix_timestamp() as u64;
2118        let timestamp_millis = TimestampMillis::from_time(&time).u64();
2119
2120        let mut header = parent.clone();
2121        header.set_timestamp(timestamp - 13, timestamp_millis - 13_000);
2122        let proposal = Proposal::new(&header, block_size);
2123
2124        let err = proposal.validate_timestamp_drift(time).unwrap_err();
2125        tracing::info!(%err, "task failed successfully");
2126        assert_eq!(
2127            ProposalValidationError::InvalidTimestampDrift {
2128                proposal: timestamp - 13,
2129                system: timestamp,
2130                diff: 13
2131            },
2132            err
2133        );
2134
2135        // Success cases.
2136        let mut header = parent.clone();
2137        header.set_timestamp(timestamp, timestamp_millis);
2138        let proposal = Proposal::new(&header, block_size);
2139        proposal.validate_timestamp_drift(time).unwrap();
2140
2141        header.set_timestamp(timestamp - 11, timestamp_millis - 11_000);
2142        let proposal = Proposal::new(&header, block_size);
2143        proposal.validate_timestamp_drift(time).unwrap();
2144
2145        header.set_timestamp(timestamp - 12, timestamp_millis - 12_000);
2146        let proposal = Proposal::new(&header, block_size);
2147        proposal.validate_timestamp_drift(time).unwrap();
2148    }
2149
2150    #[test_log::test(tokio::test(flavor = "multi_thread"))]
2151    async fn test_validation_fee_root() {
2152        // Setup
2153        let instance = NodeState::mock();
2154        let (header, block_size) = Transaction::of_size(10).into_mock_header().await;
2155
2156        // Success case.
2157        let proposal = Proposal::new(&header, block_size);
2158        ValidatedTransition::mock(instance.clone(), &header, proposal)
2159            .validate_fee_merkle_tree()
2160            .unwrap();
2161
2162        // Error case.
2163        let proposal = Proposal::new(&header, block_size);
2164
2165        let mut fee_merkle_tree = instance.genesis_state.fee_merkle_tree;
2166        fee_merkle_tree
2167            .update_with(FeeAccount::default(), |_| Some(100.into()))
2168            .unwrap();
2169
2170        let err = proposal
2171            .validate_block_merkle_tree(fee_merkle_tree.commitment())
2172            .unwrap_err();
2173
2174        tracing::info!(%err, "task failed successfully");
2175        assert_eq!(
2176            ProposalValidationError::InvalidBlockRoot {
2177                expected_root: fee_merkle_tree.commitment(),
2178                proposal_root: header.block_merkle_tree_root(),
2179            },
2180            err
2181        );
2182    }
2183
2184    #[test_log::test(tokio::test(flavor = "multi_thread"))]
2185    async fn test_validation_block_root() {
2186        // Setup.
2187        let instance = NodeState::mock();
2188        let (header, block_size) = Transaction::of_size(10).into_mock_header().await;
2189
2190        // Success case.
2191        let proposal = Proposal::new(&header, block_size);
2192        ValidatedTransition::mock(instance.clone(), &header, proposal)
2193            .validate_block_merkle_tree()
2194            .unwrap();
2195
2196        // Error case.
2197        let proposal = Proposal::new(&header, block_size);
2198        let mut block_merkle_tree = instance.genesis_state.block_merkle_tree;
2199        block_merkle_tree.push(header.commitment()).unwrap();
2200        block_merkle_tree
2201            .push(header.clone().next().commitment())
2202            .unwrap();
2203
2204        let err = proposal
2205            .validate_block_merkle_tree(block_merkle_tree.commitment())
2206            .unwrap_err();
2207
2208        tracing::info!(%err, "task failed successfully");
2209        assert_eq!(
2210            ProposalValidationError::InvalidBlockRoot {
2211                expected_root: block_merkle_tree.commitment(),
2212                proposal_root: proposal.header.block_merkle_tree_root(),
2213            },
2214            err
2215        );
2216    }
2217
2218    #[test_log::test(tokio::test(flavor = "multi_thread"))]
2219    async fn test_validation_ns_table() {
2220        use NsTableValidationError::InvalidFinalOffset;
2221        // Setup.
2222        let tx = Transaction::of_size(10);
2223        let (header, block_size) = tx.into_mock_header().await;
2224
2225        // Success case.
2226        let proposal = Proposal::new(&header, block_size);
2227        ValidatedTransition::mock(NodeState::mock(), &header, proposal)
2228            .validate_namespace_table()
2229            .unwrap();
2230
2231        // Error case
2232        let proposal = Proposal::new(&header, 40);
2233        let err = ValidatedTransition::mock(NodeState::mock(), &header, proposal)
2234            .validate_namespace_table()
2235            .unwrap_err();
2236        tracing::info!(%err, "task failed successfully");
2237        // TODO NsTable has other error variants, but these should be
2238        // tested in unit tests of `NsTable.validate()`.
2239        assert_eq!(
2240            ProposalValidationError::InvalidNsTable(InvalidFinalOffset),
2241            err
2242        );
2243    }
2244
2245    #[test_log::test(tokio::test(flavor = "multi_thread"))]
2246    async fn test_validation_without_payload_size_skips_size_checks() {
2247        use NsTableValidationError::InvalidFinalOffset;
2248        // A proposal adopted without its VID share carries no payload size. Under
2249        // this chain config every size-dependent check rejects the block once a
2250        // size is supplied.
2251        let tx = Transaction::of_size(20);
2252        let (header, block_size) = tx.into_mock_header().await;
2253        let instance = NodeState::mock().with_chain_config(ChainConfig {
2254            max_block_size: BlockSize::from_integer(10).unwrap(),
2255            base_fee: 1000.into(),
2256            ..ValidatedState::default().chain_config.resolve().unwrap()
2257        });
2258
2259        let sized = ValidatedTransition::mock(
2260            instance.clone(),
2261            &header,
2262            Proposal::new(&header, block_size),
2263        );
2264        assert!(matches!(
2265            sized.validate_block_size(),
2266            Err(ProposalValidationError::MaxBlockSizeExceeded { .. })
2267        ));
2268        assert!(matches!(
2269            sized.validate_fee(),
2270            Err(ProposalValidationError::InsufficientFee { .. })
2271        ));
2272        // Standing in a size of zero, as the fetch path once did, rejects every
2273        // non-empty block.
2274        let zero_sized =
2275            ValidatedTransition::mock(instance.clone(), &header, Proposal::new(&header, 0));
2276        assert_eq!(
2277            ProposalValidationError::InvalidNsTable(InvalidFinalOffset),
2278            zero_sized.validate_namespace_table().unwrap_err()
2279        );
2280
2281        let sizeless =
2282            ValidatedTransition::mock(instance.clone(), &header, Proposal::without_size(&header));
2283        sizeless.validate_block_size().unwrap();
2284        sizeless.validate_fee().unwrap();
2285        sizeless.validate_namespace_table().unwrap();
2286
2287        // Fee presence does not depend on the size and is still checked.
2288        let fee_account = EthKeyPair::random().fee_account();
2289        let header = header.with_fee(FeeInfo::new(fee_account, FeeAmount(U256::MAX)), None);
2290        let err = ValidatedTransition::mock(instance, &header, Proposal::without_size(&header))
2291            .validate_fee()
2292            .unwrap_err();
2293        assert_eq!(ProposalValidationError::SomeFeeAmountOutOfRange, err);
2294    }
2295
2296    #[test_log::test]
2297    fn test_charge_fee() {
2298        let src = FeeAccount::generated_from_seed_indexed([0; 32], 0).0;
2299        let dst = FeeAccount::generated_from_seed_indexed([0; 32], 1).0;
2300        let amt = FeeAmount::from(1);
2301
2302        let fee_info = FeeInfo::new(src, amt);
2303
2304        let new_state = || {
2305            let mut state = ValidatedState::default();
2306            state.prefund_account(src, amt);
2307            state
2308        };
2309
2310        tracing::info!("test successful fee");
2311        let mut state = new_state();
2312        state.charge_fee(fee_info, dst).unwrap();
2313        assert_eq!(state.balance(src), Some(0.into()));
2314        assert_eq!(state.balance(dst), Some(amt));
2315
2316        tracing::info!("test insufficient balance");
2317        let err = state.charge_fee(fee_info, dst).unwrap_err();
2318        assert_eq!(state.balance(src), Some(0.into()));
2319        assert_eq!(state.balance(dst), Some(amt));
2320        assert_eq!(
2321            FeeError::InsufficientFunds {
2322                balance: None,
2323                amount: amt
2324            },
2325            err
2326        );
2327
2328        tracing::info!("test src not in memory");
2329        let mut state = new_state();
2330        state.fee_merkle_tree.forget(src).expect_ok().unwrap();
2331        assert_eq!(
2332            FeeError::MerkleTreeError(MerkleTreeError::ForgottenLeaf),
2333            state.charge_fee(fee_info, dst).unwrap_err()
2334        );
2335
2336        tracing::info!("test dst not in memory");
2337        let mut state = new_state();
2338        state.prefund_account(dst, amt);
2339        state.fee_merkle_tree.forget(dst).expect_ok().unwrap();
2340        assert_eq!(
2341            FeeError::MerkleTreeError(MerkleTreeError::ForgottenLeaf),
2342            state.charge_fee(fee_info, dst).unwrap_err()
2343        );
2344    }
2345
2346    #[test]
2347    fn test_fee_amount_serde_json_as_decimal() {
2348        let amt = FeeAmount::from(123);
2349        let serialized = serde_json::to_string(&amt).unwrap();
2350
2351        // The value is serialized as a decimal string.
2352        assert_eq!(serialized, "\"123\"");
2353
2354        // Deserialization produces the original value
2355        let deserialized: FeeAmount = serde_json::from_str(&serialized).unwrap();
2356        assert_eq!(deserialized, amt);
2357    }
2358
2359    #[test]
2360    fn test_fee_amount_from_units() {
2361        for (unit, multiplier) in [
2362            ("wei", 1),
2363            ("gwei", 1_000_000_000),
2364            ("eth", 1_000_000_000_000_000_000),
2365        ] {
2366            let amt: FeeAmount = serde_json::from_str(&format!("\"1 {unit}\"")).unwrap();
2367            assert_eq!(amt, multiplier.into());
2368        }
2369    }
2370
2371    #[test]
2372    fn test_fee_amount_serde_json_from_hex() {
2373        // For backwards compatibility, fee amounts can also be deserialized from a 0x-prefixed hex
2374        // string.
2375        let amt: FeeAmount = serde_json::from_str("\"0x123\"").unwrap();
2376        assert_eq!(amt, FeeAmount::from(0x123));
2377    }
2378
2379    #[test]
2380    fn test_fee_amount_serde_json_from_number() {
2381        // For convenience, fee amounts can also be deserialized from a JSON number.
2382        let amt: FeeAmount = serde_json::from_str("123").unwrap();
2383        assert_eq!(amt, FeeAmount::from(123));
2384    }
2385
2386    #[test]
2387    fn test_fee_amount_serde_bincode_unchanged() {
2388        // For non-human-readable formats, FeeAmount just serializes as the underlying U256.
2389        // note: for backward compat, it has to be the same as ethers' U256 instead of alloy's
2390        let n = ethers_core::types::U256::from(123);
2391        let amt = FeeAmount(U256::from(123));
2392        assert_eq!(
2393            bincode::serialize(&n).unwrap(),
2394            bincode::serialize(&amt).unwrap(),
2395        );
2396    }
2397
2398    #[test_log::test(tokio::test(flavor = "multi_thread"))]
2399    async fn test_validate_builder_fee() {
2400        let max_block_size = 10;
2401
2402        let validated_state = ValidatedState::default();
2403        let instance_state = NodeState::mock().with_chain_config(ChainConfig {
2404            base_fee: 1000.into(), // High base fee
2405            max_block_size: max_block_size.into(),
2406            ..validated_state.chain_config.resolve().unwrap()
2407        });
2408
2409        let parent: Leaf2 = Leaf::genesis(
2410            &instance_state.genesis_state,
2411            &instance_state,
2412            MOCK_UPGRADE.base,
2413        )
2414        .await
2415        .into();
2416        let header = parent.block_header().clone();
2417        let metadata = parent.block_header().metadata();
2418
2419        debug!("{:?}", header.version());
2420
2421        let key_pair = EthKeyPair::random();
2422        let account = key_pair.fee_account();
2423
2424        let data = header.fee_info()[0].amount().as_u64().unwrap();
2425        let sig = FeeAccount::sign_builder_message(&key_pair, &data.to_be_bytes()).unwrap();
2426
2427        // ensure the signature is indeed valid
2428        account
2429            .validate_builder_signature(&sig, &data.to_be_bytes())
2430            .then_some(())
2431            .unwrap();
2432
2433        // test v1 sig
2434        let sig = FeeAccount::sign_fee(&key_pair, data, metadata).unwrap();
2435
2436        let header = match header {
2437            Header::V1(header) => Header::V1(v0_1::Header {
2438                builder_signature: Some(sig),
2439                fee_info: FeeInfo::new(account, data),
2440                ..header
2441            }),
2442            Header::V2(header) => Header::V2(v0_2::Header {
2443                builder_signature: Some(sig),
2444                fee_info: FeeInfo::new(account, data),
2445                ..header
2446            }),
2447            Header::V3(header) => Header::V3(v0_3::Header {
2448                builder_signature: Some(sig),
2449                fee_info: FeeInfo::new(account, data),
2450                ..header
2451            }),
2452            Header::V4(header) => Header::V4(v0_4::Header {
2453                builder_signature: Some(sig),
2454                fee_info: FeeInfo::new(account, data),
2455                ..header
2456            }),
2457            Header::V5(header) => Header::V5(v0_5::Header {
2458                builder_signature: Some(sig),
2459                fee_info: FeeInfo::new(account, data),
2460                ..header
2461            }),
2462            Header::V6(header) => Header::V6(v0_6::Header {
2463                builder_signature: Some(sig),
2464                fee_info: FeeInfo::new(account, data),
2465                ..header
2466            }),
2467            Header::V7(header) => Header::V7(v0_7::Header {
2468                builder_signature: Some(sig),
2469                fee_info: FeeInfo::new(account, data),
2470                ..header
2471            }),
2472        };
2473
2474        let version = header.version();
2475        validate_builder_fee(&header, version).unwrap();
2476    }
2477
2478    #[test_log::test(tokio::test(flavor = "multi_thread"))]
2479    async fn test_validate_total_rewards_distributed() {
2480        let instance = NodeState::mock().with_genesis_version(version(0, 4));
2481
2482        let (payload, metadata) =
2483            Payload::from_transactions(&[], &instance.genesis_state, &instance)
2484                .await
2485                .unwrap();
2486
2487        let header = Header::genesis(
2488            &instance,
2489            payload.clone(),
2490            &metadata,
2491            TEST_VERSIONS.da_committee.base,
2492        );
2493
2494        let validated_state = ValidatedState::default();
2495        let actual_total = RewardAmount::from(1000u64);
2496        let block_size = 100u32;
2497
2498        let proposed_header = match header.clone() {
2499            Header::V4(mut h) => {
2500                h.total_reward_distributed = actual_total;
2501                Header::V4(h)
2502            },
2503            _ => unreachable!("Expected V4 header"),
2504        };
2505
2506        let validation_start_time = OffsetDateTime::now_utc();
2507        let validated_transition = ValidatedTransition::new(
2508            validated_state.clone(),
2509            &header,
2510            Proposal::new(&proposed_header, block_size),
2511            Some(actual_total),
2512            version(0, 4),
2513            validation_start_time,
2514            None, // epoch_height - not needed for V4 tests
2515            None,
2516        );
2517
2518        validated_transition
2519            .validate_total_rewards_distributed()
2520            .unwrap();
2521
2522        let wrong_total = RewardAmount::from(2000u64);
2523        let proposed_header = match header.clone() {
2524            Header::V4(mut h) => {
2525                h.total_reward_distributed = wrong_total;
2526                Header::V4(h)
2527            },
2528            _ => unreachable!("Expected V4 header"),
2529        };
2530
2531        ValidatedTransition::new(
2532            validated_state.clone(),
2533            &header,
2534            Proposal::new(&proposed_header, block_size),
2535            Some(actual_total),
2536            version(0, 4),
2537            validation_start_time,
2538            None, // epoch_height - not needed for V4 tests
2539            None,
2540        )
2541        .validate_total_rewards_distributed()
2542        .unwrap_err();
2543    }
2544
2545    #[test_log::test(tokio::test(flavor = "multi_thread"))]
2546    async fn test_regression_slow_validation_timestamp_drift() {
2547        let instance = NodeState::mock();
2548        let (parent, block_size) = Transaction::of_size(10).into_mock_header().await;
2549
2550        let validation_start_time = OffsetDateTime::now_utc();
2551        let timestamp = validation_start_time.unix_timestamp() as u64;
2552        let timestamp_millis = TimestampMillis::from_time(&validation_start_time).u64();
2553
2554        let mut header = parent.clone();
2555        header.set_timestamp(timestamp, timestamp_millis);
2556
2557        std::thread::sleep(Duration::from_secs(13));
2558
2559        // Validation fails if we pass the current timestamp (emulates issue before fix)
2560        let proposal_without_fix = Proposal::new(&header, block_size);
2561        let err = ValidatedTransition::new(
2562            instance.genesis_state.clone(),
2563            &parent,
2564            proposal_without_fix,
2565            None,
2566            MAX_SUPPORTED_VERSION,
2567            OffsetDateTime::now_utc(),
2568            None, // epoch_height
2569            None,
2570        )
2571        .validate_timestamp()
2572        .unwrap_err();
2573
2574        assert!(matches!(
2575            err,
2576            ProposalValidationError::InvalidTimestampDrift { .. }
2577        ));
2578
2579        // Validation succeeds if we pass a validation start timestamp
2580        let proposal = Proposal::new(&header, block_size);
2581        ValidatedTransition::new(
2582            instance.genesis_state.clone(),
2583            &parent,
2584            proposal,
2585            None,
2586            MAX_SUPPORTED_VERSION,
2587            validation_start_time,
2588            None, // epoch_height
2589            None,
2590        )
2591        .validate_timestamp()
2592        .unwrap();
2593    }
2594
2595    // Checks that slow catchup does not cause timestamp drift validation to fail.
2596    #[test_log::test(tokio::test(flavor = "multi_thread"))]
2597    async fn test_validate_and_apply_header_slow_catchup_succeeds() {
2598        // Using v2 here because mock_v3 is lacking for epoch related validation to work.
2599        let mut instance = NodeState::mock_v2();
2600
2601        let mut genesis_state = instance.genesis_state.clone();
2602
2603        // We need an element in the tree to forget it and trigger catchup.
2604        genesis_state
2605            .fee_merkle_tree
2606            .update(FeeAccount::default(), FeeAmount::from(1u64))
2607            .unwrap();
2608        instance.genesis_state = genesis_state.clone();
2609
2610        let genesis = Leaf::genesis(&genesis_state, &instance, MOCK_UPGRADE.base).await;
2611        let parent_leaf: Leaf2 = genesis.into();
2612        let parent_header = parent_leaf.block_header().clone();
2613
2614        let mut expected_block_tree = genesis_state.block_merkle_tree.clone();
2615        expected_block_tree.push(parent_header.commit()).unwrap();
2616
2617        let proposed_header = match parent_header {
2618            Header::V2(header) => Header::V2(v0_2::Header {
2619                height: header.height + 1,
2620                timestamp: OffsetDateTime::now_utc().unix_timestamp() as u64,
2621                block_merkle_tree_root: expected_block_tree.commitment(),
2622                chain_config: header.chain_config.commit().into(),
2623                ..header
2624            }),
2625            _ => panic!("Expected V2 header"),
2626        };
2627
2628        let slow_catchup =
2629            MockStateCatchup::from_iter([(ViewNumber::new(0), Arc::new(genesis_state.clone()))])
2630                .with_delay(Duration::from_secs(13));
2631        instance.state_catchup = Arc::new(slow_catchup);
2632
2633        // Forget leaf to trigger catchup
2634        genesis_state
2635            .fee_merkle_tree
2636            .forget(FeeAccount::default())
2637            .expect_ok()
2638            .unwrap();
2639
2640        genesis_state
2641            .validate_and_apply_header(
2642                &instance,
2643                &parent_leaf,
2644                &proposed_header,
2645                Some(0), /* payload_byte_len */
2646                FEE_VERSION,
2647                0, /* view_number */
2648            )
2649            .await
2650            .unwrap();
2651    }
2652
2653    #[test]
2654    fn test_apply_large_block_upgrade() {
2655        let upgraded_chain_config = ChainConfig {
2656            max_block_size: (10 * 1024 * 1024).into(),
2657            ..Default::default()
2658        };
2659        let upgrade = crate::Upgrade {
2660            mode: UpgradeMode::View(ViewBasedUpgrade {
2661                start_voting_view: None,
2662                stop_voting_view: None,
2663                start_proposing_view: 1,
2664                stop_proposing_view: 10,
2665            }),
2666            upgrade_type: UpgradeType::LargeBlock {
2667                chain_config: upgraded_chain_config,
2668            },
2669        };
2670        let instance = NodeState::mock()
2671            .with_current_version(NEW_PROTOCOL_VERSION)
2672            .with_upgrades([(LARGE_BLOCK_VERSION, upgrade)].into());
2673
2674        let mut state = ValidatedState::default();
2675        let pre_upgrade_chain_config = state.chain_config;
2676        state.apply_upgrade(&instance, NEW_PROTOCOL_VERSION);
2677        assert_eq!(state.chain_config, pre_upgrade_chain_config);
2678
2679        state.apply_upgrade(&instance, LARGE_BLOCK_VERSION);
2680        assert_eq!(state.chain_config, upgraded_chain_config.into());
2681        assert_eq!(
2682            state.chain_config.resolve().unwrap().max_block_size,
2683            (10 * 1024 * 1024).into()
2684        );
2685    }
2686}