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