espresso_types/v0/impls/
state.rs

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