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