forest/chain_sync/
validation.rs

1// Copyright 2019-2025 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use std::time::{SystemTime, UNIX_EPOCH};
5
6use crate::blocks::{Block, FullTipset, Tipset, TxMeta};
7use crate::chain::ChainStore;
8use crate::message::SignedMessage;
9use crate::shim::message::Message;
10use crate::utils::{cid::CidCborExt, db::CborStoreExt};
11use cid::Cid;
12use fil_actors_shared::fvm_ipld_amt::{Amtv0 as Amt, Error as IpldAmtError};
13use fvm_ipld_blockstore::Blockstore;
14use fvm_ipld_encoding::Error as EncodingError;
15use thiserror::Error;
16
17use crate::chain_sync::bad_block_cache::BadBlockCache;
18
19const MAX_HEIGHT_DRIFT: u64 = 5;
20
21#[derive(Debug, Error)]
22pub enum TipsetValidationError {
23    #[error("Tipset has no blocks")]
24    NoBlocks,
25    #[error("Tipset has an epoch that is too large")]
26    EpochTooLarge,
27    #[error("Tipset has an insufficient weight")]
28    InsufficientWeight,
29    #[error("Tipset block = [CID = {0}] is invalid")]
30    InvalidBlock(Cid),
31    #[error("Tipset headers are invalid")]
32    InvalidRoots,
33    #[error("Tipset IPLD error: {0}")]
34    IpldAmt(String),
35    #[error("Block store error while validating tipset: {0}")]
36    Blockstore(String),
37    #[error("Encoding error while validating tipset: {0}")]
38    Encoding(EncodingError),
39}
40
41impl From<EncodingError> for TipsetValidationError {
42    fn from(err: EncodingError) -> Self {
43        TipsetValidationError::Encoding(err)
44    }
45}
46
47impl From<IpldAmtError> for TipsetValidationError {
48    fn from(err: IpldAmtError) -> Self {
49        TipsetValidationError::IpldAmt(err.to_string())
50    }
51}
52
53pub struct TipsetValidator<'a>(pub &'a FullTipset);
54
55impl TipsetValidator<'_> {
56    pub fn validate<DB: Blockstore>(
57        &self,
58        chainstore: &ChainStore<DB>,
59        bad_block_cache: Option<&BadBlockCache>,
60        genesis_tipset: &Tipset,
61        block_delay: u32,
62    ) -> Result<(), TipsetValidationError> {
63        // No empty blocks
64        if self.0.blocks().is_empty() {
65            return Err(TipsetValidationError::NoBlocks);
66        }
67
68        // Tipset epoch must not be behind current max
69        self.validate_epoch(genesis_tipset, block_delay)?;
70
71        // Validate each block in the tipset by:
72        // 1. Calculating the message root using all of the messages to ensure it
73        // matches the mst root in the block header 2. Ensuring it has not
74        // previously been seen in the bad blocks cache
75        for block in self.0.blocks() {
76            Self::validate_msg_root(chainstore.blockstore(), block)?;
77            if let Some(bad_block_cache) = bad_block_cache
78                && bad_block_cache.peek(block.cid()).is_some()
79            {
80                return Err(TipsetValidationError::InvalidBlock(*block.cid()));
81            }
82        }
83
84        Ok(())
85    }
86
87    pub fn validate_epoch(
88        &self,
89        genesis_tipset: &Tipset,
90        block_delay: u32,
91    ) -> Result<(), TipsetValidationError> {
92        let now = SystemTime::now()
93            .duration_since(UNIX_EPOCH)
94            .unwrap()
95            .as_secs();
96        let max_epoch =
97            ((now - genesis_tipset.min_timestamp()) / block_delay as u64) + MAX_HEIGHT_DRIFT;
98        let too_far_ahead_in_time = self.0.epoch() as u64 > max_epoch;
99        if too_far_ahead_in_time {
100            Err(TipsetValidationError::EpochTooLarge)
101        } else {
102            Ok(())
103        }
104    }
105
106    pub fn validate_msg_root<DB: Blockstore>(
107        blockstore: &DB,
108        block: &Block,
109    ) -> Result<(), TipsetValidationError> {
110        let msg_root = Self::compute_msg_root(blockstore, block.bls_msgs(), block.secp_msgs())?;
111        if block.header().messages != msg_root {
112            Err(TipsetValidationError::InvalidRoots)
113        } else {
114            Ok(())
115        }
116    }
117
118    pub fn compute_msg_root<DB: Blockstore>(
119        blockstore: &DB,
120        bls_msgs: &[Message],
121        secp_msgs: &[SignedMessage],
122    ) -> Result<Cid, TipsetValidationError> {
123        // Generate message CIDs
124        let bls_cids = bls_msgs
125            .iter()
126            .map(Cid::from_cbor_blake2b256)
127            .collect::<Result<Vec<Cid>, fvm_ipld_encoding::Error>>()?;
128        let secp_cids = secp_msgs
129            .iter()
130            .map(Cid::from_cbor_blake2b256)
131            .collect::<Result<Vec<Cid>, fvm_ipld_encoding::Error>>()?;
132
133        // Generate Amt and batch set message values
134        let bls_message_root = Amt::new_from_iter(blockstore, bls_cids)?;
135        let secp_message_root = Amt::new_from_iter(blockstore, secp_cids)?;
136        let meta = TxMeta {
137            bls_message_root,
138            secp_message_root,
139        };
140
141        // Store message roots and receive meta_root CID
142        blockstore
143            .put_cbor_default(&meta)
144            .map_err(|e| TipsetValidationError::Blockstore(e.to_string()))
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use std::convert::TryFrom;
151
152    use crate::db::MemoryDB;
153    use crate::message::SignedMessage;
154    use crate::shim::message::Message;
155    use crate::test_utils::construct_messages;
156    use crate::utils::encoding::from_slice_with_fallback;
157    use base64::{Engine, prelude::BASE64_STANDARD};
158    use cid::Cid;
159
160    use super::TipsetValidator;
161
162    #[test]
163    fn compute_msg_meta_given_msgs_test() {
164        let blockstore = MemoryDB::default();
165
166        let (bls, secp) = construct_messages();
167
168        let expected_root =
169            Cid::try_from("bafy2bzaceasssikoiintnok7f3sgnekfifarzobyr3r4f25sgxmn23q4c35ic")
170                .unwrap();
171
172        let root = TipsetValidator::compute_msg_root(&blockstore, &[bls], &[secp])
173            .expect("Computing message root should succeed");
174        assert_eq!(root, expected_root);
175    }
176
177    #[test]
178    fn empty_msg_meta_vector() {
179        let blockstore = MemoryDB::default();
180        let usm: Vec<Message> =
181            from_slice_with_fallback(&BASE64_STANDARD.decode("gA==").unwrap()).unwrap();
182        let sm: Vec<SignedMessage> =
183            from_slice_with_fallback(&BASE64_STANDARD.decode("gA==").unwrap()).unwrap();
184
185        assert_eq!(
186            TipsetValidator::compute_msg_root(&blockstore, &usm, &sm)
187                .expect("Computing message root should succeed")
188                .to_string(),
189            "bafy2bzacecmda75ovposbdateg7eyhwij65zklgyijgcjwynlklmqazpwlhba"
190        );
191    }
192}