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