Skip to main content

forest/chain_sync/
validation.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use std::time::{SystemTime, UNIX_EPOCH};
5
6use crate::blocks::{BLOCK_MESSAGE_LIMIT, Block, FullTipset, GossipBlock, Tipset, TxMeta};
7use crate::chain::ChainStore;
8use crate::message::SignedMessage;
9use crate::shim::clock::ChainEpoch;
10use crate::shim::message::Message;
11use crate::utils::{cid::CidCborExt, db::CborStoreExt};
12use cid::Cid;
13use fil_actors_shared::fvm_ipld_amt::{Amtv0 as Amt, Error as IpldAmtError};
14use fvm_ipld_blockstore::Blockstore;
15use fvm_ipld_encoding::Error as EncodingError;
16use thiserror::Error;
17
18use crate::chain_sync::bad_block_cache::{BadBlockCache, SeenBlockCache};
19
20const MAX_HEIGHT_DRIFT: ChainEpoch = 5;
21
22/// Compute the maximum allowed epoch given the current time (seconds since
23/// UNIX epoch). Returns `None` if inputs are nonsensical (clock before
24/// genesis, zero block delay).
25fn max_allowed_epoch(
26    now_secs: u64,
27    genesis_timestamp: u64,
28    block_delay: u32,
29) -> Option<ChainEpoch> {
30    let elapsed = now_secs.checked_sub(genesis_timestamp)?;
31    let delay = u64::from(block_delay);
32    if delay == 0 {
33        return None;
34    }
35    let epoch = ChainEpoch::try_from(elapsed / delay).unwrap_or(ChainEpoch::MAX);
36    Some(epoch.saturating_add(MAX_HEIGHT_DRIFT))
37}
38
39fn now_secs() -> u64 {
40    SystemTime::now()
41        .duration_since(UNIX_EPOCH)
42        .unwrap_or_default()
43        .as_secs()
44}
45
46#[derive(Debug, Error)]
47pub enum TipsetValidationError {
48    #[error("Tipset has no blocks")]
49    NoBlocks,
50    #[error("Tipset has an epoch that is too large")]
51    EpochTooLarge,
52    #[error("Tipset has an insufficient weight")]
53    InsufficientWeight,
54    #[error("Tipset block = [CID = {0}] is invalid")]
55    InvalidBlock(Cid),
56    #[error("Tipset headers are invalid")]
57    InvalidRoots,
58    #[error("Tipset IPLD error: {0}")]
59    IpldAmt(String),
60    #[error("Block store error while validating tipset: {0}")]
61    Blockstore(String),
62    #[error("Encoding error while validating tipset: {0}")]
63    Encoding(EncodingError),
64}
65
66impl From<EncodingError> for TipsetValidationError {
67    fn from(err: EncodingError) -> Self {
68        TipsetValidationError::Encoding(err)
69    }
70}
71
72impl From<IpldAmtError> for TipsetValidationError {
73    fn from(err: IpldAmtError) -> Self {
74        TipsetValidationError::IpldAmt(err.to_string())
75    }
76}
77
78pub struct TipsetValidator<'a>(pub &'a FullTipset);
79
80impl TipsetValidator<'_> {
81    pub fn validate(
82        &self,
83        chainstore: &ChainStore,
84        bad_block_cache: Option<&BadBlockCache>,
85        genesis_tipset: &Tipset,
86        block_delay: u32,
87    ) -> Result<(), TipsetValidationError> {
88        // No empty blocks
89        if self.0.blocks().is_empty() {
90            return Err(TipsetValidationError::NoBlocks);
91        }
92
93        // Tipset epoch must not be behind current max
94        self.validate_epoch(genesis_tipset, block_delay)?;
95
96        // Validate each block in the tipset by:
97        // 1. Calculating the message root using all of the messages to ensure it
98        // matches the mst root in the block header 2. Ensuring it has not
99        // previously been seen in the bad blocks cache
100        for block in self.0.blocks() {
101            Self::validate_msg_root(chainstore.db(), block)?;
102            if let Some(bad_block_cache) = bad_block_cache
103                && bad_block_cache.get(block.cid()).is_some()
104            {
105                return Err(TipsetValidationError::InvalidBlock(*block.cid()));
106            }
107        }
108
109        Ok(())
110    }
111
112    pub fn validate_epoch(
113        &self,
114        genesis_tipset: &Tipset,
115        block_delay: u32,
116    ) -> Result<(), TipsetValidationError> {
117        let max = max_allowed_epoch(now_secs(), genesis_tipset.min_timestamp(), block_delay)
118            .unwrap_or(ChainEpoch::MAX);
119        if self.0.epoch() > max {
120            Err(TipsetValidationError::EpochTooLarge)
121        } else {
122            Ok(())
123        }
124    }
125
126    pub fn validate_msg_root<DB: Blockstore>(
127        blockstore: &DB,
128        block: &Block,
129    ) -> Result<(), TipsetValidationError> {
130        let msg_root = Self::compute_msg_root(blockstore, block.bls_msgs(), block.secp_msgs())?;
131        if block.header().messages != msg_root {
132            Err(TipsetValidationError::InvalidRoots)
133        } else {
134            Ok(())
135        }
136    }
137
138    pub fn compute_msg_root<DB: Blockstore>(
139        blockstore: &DB,
140        bls_msgs: &[Message],
141        secp_msgs: &[SignedMessage],
142    ) -> Result<Cid, TipsetValidationError> {
143        // Generate message CIDs
144        let bls_cids = bls_msgs
145            .iter()
146            .map(Cid::from_cbor_blake2b256)
147            .collect::<Result<Vec<Cid>, fvm_ipld_encoding::Error>>()?;
148        let secp_cids = secp_msgs
149            .iter()
150            .map(Cid::from_cbor_blake2b256)
151            .collect::<Result<Vec<Cid>, fvm_ipld_encoding::Error>>()?;
152
153        // Generate Amt and batch set message values
154        let bls_message_root = Amt::new_from_iter(blockstore, bls_cids)?;
155        let secp_message_root = Amt::new_from_iter(blockstore, secp_cids)?;
156        let meta = TxMeta {
157            bls_message_root,
158            secp_message_root,
159        };
160
161        // Store message roots and receive meta_root CID
162        blockstore
163            .put_cbor_default(&meta)
164            .map_err(|e| TipsetValidationError::Blockstore(e.to_string()))
165    }
166}
167
168#[derive(Debug, Error)]
169pub enum GossipBlockRejectReason {
170    #[error("block epoch {0} is too far in the future")]
171    EpochTooFarAhead(ChainEpoch),
172    #[error("block epoch {epoch} is beyond finality (finalized: {finalized_epoch})")]
173    EpochBeyondFinality {
174        epoch: ChainEpoch,
175        finalized_epoch: ChainEpoch,
176    },
177    #[error("block epoch {0} is negative")]
178    NegativeEpoch(ChainEpoch),
179    #[error("block timestamp {timestamp} inconsistent with epoch {epoch} (expected {expected})")]
180    TimestampMismatch {
181        timestamp: u64,
182        epoch: ChainEpoch,
183        expected: u64,
184    },
185    #[error("block has no signature")]
186    MissingSignature,
187    #[error("block has no election proof")]
188    MissingElectionProof,
189    #[error("block election proof has win_count {0} < 1")]
190    InvalidWinCount(i64),
191    #[error("block has {0} messages, exceeding limit of {BLOCK_MESSAGE_LIMIT}")]
192    TooManyMessages(usize),
193    #[error("block CID {0} is in bad block cache")]
194    BadBlock(Cid),
195    #[error("duplicate block CID {0}")]
196    DuplicateBlock(Cid),
197}
198
199impl GossipBlockRejectReason {
200    pub fn label(&self) -> &'static str {
201        match self {
202            Self::EpochTooFarAhead(_) => "epoch_too_far_ahead",
203            Self::EpochBeyondFinality { .. } => "epoch_beyond_finality",
204            Self::NegativeEpoch(_) => "negative_epoch",
205            Self::TimestampMismatch { .. } => "timestamp_mismatch",
206            Self::MissingSignature => "missing_signature",
207            Self::MissingElectionProof => "missing_election_proof",
208            Self::InvalidWinCount(_) => "invalid_win_count",
209            Self::TooManyMessages(_) => "too_many_messages",
210            Self::BadBlock(_) => "bad_block",
211            Self::DuplicateBlock(_) => "duplicate_block",
212        }
213    }
214}
215
216/// Pre-validation of gossip blocks to avoid expensive `get_full_tipset`
217/// network round-trips and DB writes for obviously invalid blocks.
218/// Only uses data already present in the gossip message (header + CIDs).
219pub struct GossipBlockValidator<'a> {
220    block: &'a GossipBlock,
221}
222
223impl<'a> GossipBlockValidator<'a> {
224    pub fn new(block: &'a GossipBlock) -> Self {
225        Self { block }
226    }
227
228    /// Run all pre-fetch validation checks.
229    /// Checks are ordered cheapest/most-likely-to-reject first.
230    pub fn validate_pre_fetch(
231        &self,
232        genesis_tipset: &Tipset,
233        block_delay: u32,
234        finalized_epoch: ChainEpoch,
235        bad_block_cache: Option<&BadBlockCache>,
236        seen_block_cache: &SeenBlockCache,
237    ) -> Result<(), GossipBlockRejectReason> {
238        let cid = *self.block.header.cid();
239        Self::check_bad_block_cache(cid, bad_block_cache)?;
240        self.validate_epoch_range(genesis_tipset, block_delay, finalized_epoch)?;
241        self.validate_timestamp(genesis_tipset, block_delay)?;
242        self.validate_election_proof()?;
243        self.validate_signature_present()?;
244        self.validate_message_count()?;
245        // Insert into seen cache only after all checks pass, so transiently
246        // rejected blocks (e.g., slightly-future epoch) aren't suppressed later.
247        Self::check_duplicate(cid, seen_block_cache)?;
248        Ok(())
249    }
250
251    fn check_duplicate(
252        cid: Cid,
253        seen_block_cache: &SeenBlockCache,
254    ) -> Result<(), GossipBlockRejectReason> {
255        if seen_block_cache.test_and_insert(&cid) {
256            return Err(GossipBlockRejectReason::DuplicateBlock(cid));
257        }
258        Ok(())
259    }
260
261    fn check_bad_block_cache(
262        cid: Cid,
263        bad_block_cache: Option<&BadBlockCache>,
264    ) -> Result<(), GossipBlockRejectReason> {
265        if let Some(cache) = bad_block_cache
266            && cache.get(&cid).is_some()
267        {
268            return Err(GossipBlockRejectReason::BadBlock(cid));
269        }
270        Ok(())
271    }
272
273    fn validate_epoch_range(
274        &self,
275        genesis_tipset: &Tipset,
276        block_delay: u32,
277        finalized_epoch: ChainEpoch,
278    ) -> Result<(), GossipBlockRejectReason> {
279        let epoch = self.block.header.epoch;
280        if epoch < 0 {
281            return Err(GossipBlockRejectReason::NegativeEpoch(epoch));
282        }
283        let max = max_allowed_epoch(now_secs(), genesis_tipset.min_timestamp(), block_delay)
284            .unwrap_or(ChainEpoch::MAX);
285        if epoch > max {
286            return Err(GossipBlockRejectReason::EpochTooFarAhead(epoch));
287        }
288        if epoch < finalized_epoch {
289            return Err(GossipBlockRejectReason::EpochBeyondFinality {
290                epoch,
291                finalized_epoch,
292            });
293        }
294        Ok(())
295    }
296
297    /// Verify that block timestamp is consistent with its epoch:
298    /// `timestamp == genesis_timestamp + epoch * block_delay`
299    fn validate_timestamp(
300        &self,
301        genesis_tipset: &Tipset,
302        block_delay: u32,
303    ) -> Result<(), GossipBlockRejectReason> {
304        let epoch = self.block.header.epoch;
305        let timestamp = self.block.header.timestamp;
306        // epoch is validated non-negative by validate_epoch_range before this
307        let expected =
308            genesis_tipset.min_timestamp() + (epoch as u64).saturating_mul(u64::from(block_delay));
309        if timestamp != expected {
310            return Err(GossipBlockRejectReason::TimestampMismatch {
311                timestamp,
312                epoch,
313                expected,
314            });
315        }
316        Ok(())
317    }
318
319    fn validate_election_proof(&self) -> Result<(), GossipBlockRejectReason> {
320        match &self.block.header.election_proof {
321            None => Err(GossipBlockRejectReason::MissingElectionProof),
322            Some(proof) if proof.win_count < 1 => {
323                Err(GossipBlockRejectReason::InvalidWinCount(proof.win_count))
324            }
325            _ => Ok(()),
326        }
327    }
328
329    fn validate_signature_present(&self) -> Result<(), GossipBlockRejectReason> {
330        if self.block.header.signature.is_none() {
331            return Err(GossipBlockRejectReason::MissingSignature);
332        }
333        Ok(())
334    }
335
336    fn validate_message_count(&self) -> Result<(), GossipBlockRejectReason> {
337        let count = self.block.bls_messages.len() + self.block.secpk_messages.len();
338        if count > BLOCK_MESSAGE_LIMIT {
339            return Err(GossipBlockRejectReason::TooManyMessages(count));
340        }
341        Ok(())
342    }
343}
344
345#[cfg(test)]
346mod tests {
347    use std::convert::TryFrom;
348
349    use crate::blocks::{CachingBlockHeader, ElectionProof, GossipBlock, RawBlockHeader, Tipset};
350    use crate::chain_sync::bad_block_cache::{BadBlockCache, SeenBlockCache};
351    use crate::db::MemoryDB;
352    use crate::message::SignedMessage;
353    use crate::shim::crypto::{Signature, SignatureType};
354    use crate::shim::message::Message;
355    use crate::test_utils::construct_messages;
356    use crate::utils::encoding::from_slice_with_fallback;
357    use base64::{Engine, prelude::BASE64_STANDARD};
358    use cid::Cid;
359
360    use super::{GossipBlockRejectReason, GossipBlockValidator, TipsetValidator};
361
362    #[test]
363    fn compute_msg_meta_given_msgs_test() {
364        let blockstore = MemoryDB::default();
365
366        let (bls, secp) = construct_messages();
367
368        let expected_root =
369            Cid::try_from("bafy2bzaceasssikoiintnok7f3sgnekfifarzobyr3r4f25sgxmn23q4c35ic")
370                .unwrap();
371
372        let root = TipsetValidator::compute_msg_root(&blockstore, &[bls], &[secp])
373            .expect("Computing message root should succeed");
374        assert_eq!(root, expected_root);
375    }
376
377    #[test]
378    fn empty_msg_meta_vector() {
379        let blockstore = MemoryDB::default();
380        let usm: Vec<Message> =
381            from_slice_with_fallback(&BASE64_STANDARD.decode("gA==").unwrap()).unwrap();
382        let sm: Vec<SignedMessage> =
383            from_slice_with_fallback(&BASE64_STANDARD.decode("gA==").unwrap()).unwrap();
384
385        assert_eq!(
386            TipsetValidator::compute_msg_root(&blockstore, &usm, &sm)
387                .expect("Computing message root should succeed")
388                .to_string(),
389            "bafy2bzacecmda75ovposbdateg7eyhwij65zklgyijgcjwynlklmqazpwlhba"
390        );
391    }
392
393    #[test]
394    fn max_allowed_epoch_basic() {
395        // genesis at t=1000, now at t=1300, block_delay=30
396        // elapsed=300, 300/30=10, +5 drift = 15
397        assert_eq!(super::max_allowed_epoch(1300, 1000, 30), Some(15));
398    }
399
400    #[test]
401    fn max_allowed_epoch_at_genesis() {
402        // now == genesis → epoch 0 + drift
403        assert_eq!(super::max_allowed_epoch(1000, 1000, 30), Some(5));
404    }
405
406    #[test]
407    fn max_allowed_epoch_clock_before_genesis() {
408        // clock is behind genesis — should not panic, returns None
409        assert_eq!(super::max_allowed_epoch(500, 1000, 30), None);
410    }
411
412    #[test]
413    fn max_allowed_epoch_zero_block_delay() {
414        // zero block delay would divide by zero — returns None
415        assert_eq!(super::max_allowed_epoch(2000, 1000, 0), None);
416    }
417
418    fn make_gossip_block_with(f: impl FnOnce(&mut RawBlockHeader)) -> GossipBlock {
419        let mut raw = RawBlockHeader {
420            election_proof: Some(ElectionProof {
421                win_count: 1,
422                vrfproof: Default::default(),
423            }),
424            signature: Some(Signature {
425                sig_type: SignatureType::Bls,
426                bytes: vec![0u8; 96],
427            }),
428            ..Default::default()
429        };
430        f(&mut raw);
431        GossipBlock {
432            header: CachingBlockHeader::from(raw),
433            bls_messages: vec![],
434            secpk_messages: vec![],
435        }
436    }
437
438    fn make_valid_gossip_block() -> GossipBlock {
439        make_gossip_block_with(|_| {})
440    }
441
442    fn make_genesis() -> Tipset {
443        Tipset::from(CachingBlockHeader::default())
444    }
445
446    #[test]
447    fn gossip_block_validator_accepts_valid_block() {
448        let block = make_valid_gossip_block();
449        let genesis = make_genesis();
450        let seen = SeenBlockCache::default();
451
452        let result = GossipBlockValidator::new(&block).validate_pre_fetch(
453            &genesis, 30,   // block_delay
454            0,    // finalized_epoch
455            None, // no bad block cache
456            &seen,
457        );
458        assert!(result.is_ok());
459    }
460
461    #[test]
462    fn gossip_block_validator_rejects_duplicate() {
463        let block = make_valid_gossip_block();
464        let genesis = make_genesis();
465        let seen = SeenBlockCache::default();
466
467        assert!(
468            GossipBlockValidator::new(&block)
469                .validate_pre_fetch(&genesis, 30, 0, None, &seen)
470                .is_ok()
471        );
472
473        let err = GossipBlockValidator::new(&block)
474            .validate_pre_fetch(&genesis, 30, 0, None, &seen)
475            .unwrap_err();
476        assert!(matches!(err, GossipBlockRejectReason::DuplicateBlock(_)));
477    }
478
479    #[test]
480    fn gossip_block_validator_rejects_bad_block() {
481        let block = make_valid_gossip_block();
482        let genesis = make_genesis();
483        let seen = SeenBlockCache::default();
484        let bad_cache = BadBlockCache::default();
485        bad_cache.push(*block.header.cid());
486
487        let err = GossipBlockValidator::new(&block)
488            .validate_pre_fetch(&genesis, 30, 0, Some(&bad_cache), &seen)
489            .unwrap_err();
490        assert!(matches!(err, GossipBlockRejectReason::BadBlock(_)));
491    }
492
493    #[test]
494    fn gossip_block_validator_rejects_epoch_too_far_ahead() {
495        let block = make_gossip_block_with(|h| h.epoch = i64::MAX);
496        let genesis = make_genesis();
497        let seen = SeenBlockCache::default();
498
499        let err = GossipBlockValidator::new(&block)
500            .validate_pre_fetch(&genesis, 30, 0, None, &seen)
501            .unwrap_err();
502        assert!(matches!(err, GossipBlockRejectReason::EpochTooFarAhead(_)));
503    }
504
505    #[test]
506    fn gossip_block_validator_rejects_epoch_beyond_finality() {
507        let block = make_valid_gossip_block(); // epoch = 0
508        let genesis = make_genesis();
509        let seen = SeenBlockCache::default();
510
511        let err = GossipBlockValidator::new(&block)
512            .validate_pre_fetch(&genesis, 30, 100, None, &seen)
513            .unwrap_err();
514        assert!(matches!(
515            err,
516            GossipBlockRejectReason::EpochBeyondFinality { .. }
517        ));
518    }
519
520    #[test]
521    fn gossip_block_validator_rejects_missing_election_proof() {
522        let block = make_gossip_block_with(|h| h.election_proof = None);
523        let genesis = make_genesis();
524        let seen = SeenBlockCache::default();
525
526        let err = GossipBlockValidator::new(&block)
527            .validate_pre_fetch(&genesis, 30, 0, None, &seen)
528            .unwrap_err();
529        assert!(matches!(err, GossipBlockRejectReason::MissingElectionProof));
530    }
531
532    #[test]
533    fn gossip_block_validator_rejects_zero_win_count() {
534        let block = make_gossip_block_with(|h| {
535            h.election_proof = Some(ElectionProof {
536                win_count: 0,
537                vrfproof: Default::default(),
538            })
539        });
540        let genesis = make_genesis();
541        let seen = SeenBlockCache::default();
542
543        let err = GossipBlockValidator::new(&block)
544            .validate_pre_fetch(&genesis, 30, 0, None, &seen)
545            .unwrap_err();
546        assert!(matches!(err, GossipBlockRejectReason::InvalidWinCount(0)));
547    }
548
549    #[test]
550    fn gossip_block_validator_rejects_missing_signature() {
551        let block = make_gossip_block_with(|h| h.signature = None);
552        let genesis = make_genesis();
553        let seen = SeenBlockCache::default();
554
555        let err = GossipBlockValidator::new(&block)
556            .validate_pre_fetch(&genesis, 30, 0, None, &seen)
557            .unwrap_err();
558        assert!(matches!(err, GossipBlockRejectReason::MissingSignature));
559    }
560
561    #[test]
562    fn gossip_block_validator_rejects_too_many_messages() {
563        let mut block = make_valid_gossip_block();
564        block.bls_messages = vec![Cid::default(); 10_001];
565        let genesis = make_genesis();
566        let seen = SeenBlockCache::default();
567
568        let err = GossipBlockValidator::new(&block)
569            .validate_pre_fetch(&genesis, 30, 0, None, &seen)
570            .unwrap_err();
571        assert!(matches!(err, GossipBlockRejectReason::TooManyMessages(_)));
572    }
573
574    #[test]
575    fn gossip_block_validator_rejects_negative_epoch() {
576        let block = make_gossip_block_with(|h| h.epoch = -1);
577        let genesis = make_genesis();
578        let seen = SeenBlockCache::default();
579
580        let err = GossipBlockValidator::new(&block)
581            .validate_pre_fetch(&genesis, 30, 0, None, &seen)
582            .unwrap_err();
583        assert!(matches!(err, GossipBlockRejectReason::NegativeEpoch(-1)));
584    }
585
586    #[test]
587    fn gossip_block_validator_rejects_timestamp_mismatch() {
588        // epoch=0, genesis timestamp=0, so expected timestamp = 0 + 0*30 = 0
589        // but we set timestamp=999
590        let block = make_gossip_block_with(|h| h.timestamp = 999);
591        let genesis = make_genesis();
592        let seen = SeenBlockCache::default();
593
594        let err = GossipBlockValidator::new(&block)
595            .validate_pre_fetch(&genesis, 30, 0, None, &seen)
596            .unwrap_err();
597        assert!(matches!(
598            err,
599            GossipBlockRejectReason::TimestampMismatch { .. }
600        ));
601    }
602
603    #[test]
604    fn rejected_block_not_cached_as_seen() {
605        // A block rejected for a transient reason (e.g., epoch too far ahead)
606        // must NOT be inserted into the seen cache. Otherwise, if the same
607        // block is received later when it becomes valid, it would be
608        // incorrectly suppressed as a duplicate.
609        let block = make_gossip_block_with(|h| h.epoch = i64::MAX);
610        let genesis = make_genesis();
611        let seen = SeenBlockCache::default();
612
613        // First attempt: rejected as too far ahead
614        let err = GossipBlockValidator::new(&block)
615            .validate_pre_fetch(&genesis, 30, 0, None, &seen)
616            .unwrap_err();
617        assert!(matches!(err, GossipBlockRejectReason::EpochTooFarAhead(_)));
618
619        // Second attempt: must still be EpochTooFarAhead, NOT DuplicateBlock
620        let err = GossipBlockValidator::new(&block)
621            .validate_pre_fetch(&genesis, 30, 0, None, &seen)
622            .unwrap_err();
623        assert!(matches!(err, GossipBlockRejectReason::EpochTooFarAhead(_)));
624    }
625
626    #[test]
627    fn seen_block_cache_deduplicates() {
628        let cache = SeenBlockCache::default();
629        let cid = Cid::default();
630
631        assert!(!cache.test_and_insert(&cid));
632        assert!(cache.test_and_insert(&cid));
633    }
634}