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<DB: Blockstore>(
82        &self,
83        chainstore: &ChainStore<DB>,
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.blockstore(), block)?;
102            if let Some(bad_block_cache) = bad_block_cache
103                && bad_block_cache.peek(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 {0} is beyond finality (heaviest: {1})")]
173    EpochBeyondFinality(ChainEpoch, ChainEpoch),
174    #[error("block epoch {0} is negative")]
175    NegativeEpoch(ChainEpoch),
176    #[error("block timestamp {timestamp} inconsistent with epoch {epoch} (expected {expected})")]
177    TimestampMismatch {
178        timestamp: u64,
179        epoch: ChainEpoch,
180        expected: u64,
181    },
182    #[error("block has no signature")]
183    MissingSignature,
184    #[error("block has no election proof")]
185    MissingElectionProof,
186    #[error("block election proof has win_count {0} < 1")]
187    InvalidWinCount(i64),
188    #[error("block has {0} messages, exceeding limit of {BLOCK_MESSAGE_LIMIT}")]
189    TooManyMessages(usize),
190    #[error("block CID {0} is in bad block cache")]
191    BadBlock(Cid),
192    #[error("duplicate block CID {0}")]
193    DuplicateBlock(Cid),
194}
195
196impl GossipBlockRejectReason {
197    pub fn label(&self) -> &'static str {
198        match self {
199            Self::EpochTooFarAhead(_) => "epoch_too_far_ahead",
200            Self::EpochBeyondFinality(_, _) => "epoch_beyond_finality",
201            Self::NegativeEpoch(_) => "negative_epoch",
202            Self::TimestampMismatch { .. } => "timestamp_mismatch",
203            Self::MissingSignature => "missing_signature",
204            Self::MissingElectionProof => "missing_election_proof",
205            Self::InvalidWinCount(_) => "invalid_win_count",
206            Self::TooManyMessages(_) => "too_many_messages",
207            Self::BadBlock(_) => "bad_block",
208            Self::DuplicateBlock(_) => "duplicate_block",
209        }
210    }
211}
212
213/// Pre-validation of gossip blocks to avoid expensive `get_full_tipset`
214/// network round-trips and DB writes for obviously invalid blocks.
215/// Only uses data already present in the gossip message (header + CIDs).
216pub struct GossipBlockValidator<'a> {
217    block: &'a GossipBlock,
218}
219
220impl<'a> GossipBlockValidator<'a> {
221    pub fn new(block: &'a GossipBlock) -> Self {
222        Self { block }
223    }
224
225    /// Run all pre-fetch validation checks.
226    /// Checks are ordered cheapest/most-likely-to-reject first.
227    pub fn validate_pre_fetch(
228        &self,
229        genesis_tipset: &Tipset,
230        block_delay: u32,
231        chain_finality: ChainEpoch,
232        heaviest_epoch: ChainEpoch,
233        bad_block_cache: Option<&BadBlockCache>,
234        seen_block_cache: &SeenBlockCache,
235    ) -> Result<(), GossipBlockRejectReason> {
236        let cid = *self.block.header.cid();
237        Self::check_bad_block_cache(cid, bad_block_cache)?;
238        self.validate_epoch_range(genesis_tipset, block_delay, chain_finality, heaviest_epoch)?;
239        self.validate_timestamp(genesis_tipset, block_delay)?;
240        self.validate_election_proof()?;
241        self.validate_signature_present()?;
242        self.validate_message_count()?;
243        // Insert into seen cache only after all checks pass, so transiently
244        // rejected blocks (e.g., slightly-future epoch) aren't suppressed later.
245        Self::check_duplicate(cid, seen_block_cache)?;
246        Ok(())
247    }
248
249    fn check_duplicate(
250        cid: Cid,
251        seen_block_cache: &SeenBlockCache,
252    ) -> Result<(), GossipBlockRejectReason> {
253        if seen_block_cache.test_and_insert(&cid) {
254            return Err(GossipBlockRejectReason::DuplicateBlock(cid));
255        }
256        Ok(())
257    }
258
259    fn check_bad_block_cache(
260        cid: Cid,
261        bad_block_cache: Option<&BadBlockCache>,
262    ) -> Result<(), GossipBlockRejectReason> {
263        if let Some(cache) = bad_block_cache
264            && cache.peek(&cid).is_some()
265        {
266            return Err(GossipBlockRejectReason::BadBlock(cid));
267        }
268        Ok(())
269    }
270
271    fn validate_epoch_range(
272        &self,
273        genesis_tipset: &Tipset,
274        block_delay: u32,
275        chain_finality: ChainEpoch,
276        heaviest_epoch: ChainEpoch,
277    ) -> Result<(), GossipBlockRejectReason> {
278        let epoch = self.block.header.epoch;
279        if epoch < 0 {
280            return Err(GossipBlockRejectReason::NegativeEpoch(epoch));
281        }
282        let max = max_allowed_epoch(now_secs(), genesis_tipset.min_timestamp(), block_delay)
283            .unwrap_or(ChainEpoch::MAX);
284        if epoch > max {
285            return Err(GossipBlockRejectReason::EpochTooFarAhead(epoch));
286        }
287        if heaviest_epoch.saturating_sub(epoch) > chain_finality {
288            return Err(GossipBlockRejectReason::EpochBeyondFinality(
289                epoch,
290                heaviest_epoch,
291            ));
292        }
293        Ok(())
294    }
295
296    /// Verify that block timestamp is consistent with its epoch:
297    /// `timestamp == genesis_timestamp + epoch * block_delay`
298    fn validate_timestamp(
299        &self,
300        genesis_tipset: &Tipset,
301        block_delay: u32,
302    ) -> Result<(), GossipBlockRejectReason> {
303        let epoch = self.block.header.epoch;
304        let timestamp = self.block.header.timestamp;
305        // epoch is validated non-negative by validate_epoch_range before this
306        let expected =
307            genesis_tipset.min_timestamp() + (epoch as u64).saturating_mul(u64::from(block_delay));
308        if timestamp != expected {
309            return Err(GossipBlockRejectReason::TimestampMismatch {
310                timestamp,
311                epoch,
312                expected,
313            });
314        }
315        Ok(())
316    }
317
318    fn validate_election_proof(&self) -> Result<(), GossipBlockRejectReason> {
319        match &self.block.header.election_proof {
320            None => Err(GossipBlockRejectReason::MissingElectionProof),
321            Some(proof) if proof.win_count < 1 => {
322                Err(GossipBlockRejectReason::InvalidWinCount(proof.win_count))
323            }
324            _ => Ok(()),
325        }
326    }
327
328    fn validate_signature_present(&self) -> Result<(), GossipBlockRejectReason> {
329        if self.block.header.signature.is_none() {
330            return Err(GossipBlockRejectReason::MissingSignature);
331        }
332        Ok(())
333    }
334
335    fn validate_message_count(&self) -> Result<(), GossipBlockRejectReason> {
336        let count = self.block.bls_messages.len() + self.block.secpk_messages.len();
337        if count > BLOCK_MESSAGE_LIMIT {
338            return Err(GossipBlockRejectReason::TooManyMessages(count));
339        }
340        Ok(())
341    }
342}
343
344#[cfg(test)]
345mod tests {
346    use std::convert::TryFrom;
347
348    use crate::blocks::{CachingBlockHeader, ElectionProof, GossipBlock, RawBlockHeader, Tipset};
349    use crate::chain_sync::bad_block_cache::{BadBlockCache, SeenBlockCache};
350    use crate::db::MemoryDB;
351    use crate::message::SignedMessage;
352    use crate::shim::crypto::{Signature, SignatureType};
353    use crate::shim::message::Message;
354    use crate::test_utils::construct_messages;
355    use crate::utils::encoding::from_slice_with_fallback;
356    use base64::{Engine, prelude::BASE64_STANDARD};
357    use cid::Cid;
358
359    use super::{GossipBlockRejectReason, GossipBlockValidator, TipsetValidator};
360
361    #[test]
362    fn compute_msg_meta_given_msgs_test() {
363        let blockstore = MemoryDB::default();
364
365        let (bls, secp) = construct_messages();
366
367        let expected_root =
368            Cid::try_from("bafy2bzaceasssikoiintnok7f3sgnekfifarzobyr3r4f25sgxmn23q4c35ic")
369                .unwrap();
370
371        let root = TipsetValidator::compute_msg_root(&blockstore, &[bls], &[secp])
372            .expect("Computing message root should succeed");
373        assert_eq!(root, expected_root);
374    }
375
376    #[test]
377    fn empty_msg_meta_vector() {
378        let blockstore = MemoryDB::default();
379        let usm: Vec<Message> =
380            from_slice_with_fallback(&BASE64_STANDARD.decode("gA==").unwrap()).unwrap();
381        let sm: Vec<SignedMessage> =
382            from_slice_with_fallback(&BASE64_STANDARD.decode("gA==").unwrap()).unwrap();
383
384        assert_eq!(
385            TipsetValidator::compute_msg_root(&blockstore, &usm, &sm)
386                .expect("Computing message root should succeed")
387                .to_string(),
388            "bafy2bzacecmda75ovposbdateg7eyhwij65zklgyijgcjwynlklmqazpwlhba"
389        );
390    }
391
392    #[test]
393    fn max_allowed_epoch_basic() {
394        // genesis at t=1000, now at t=1300, block_delay=30
395        // elapsed=300, 300/30=10, +5 drift = 15
396        assert_eq!(super::max_allowed_epoch(1300, 1000, 30), Some(15));
397    }
398
399    #[test]
400    fn max_allowed_epoch_at_genesis() {
401        // now == genesis → epoch 0 + drift
402        assert_eq!(super::max_allowed_epoch(1000, 1000, 30), Some(5));
403    }
404
405    #[test]
406    fn max_allowed_epoch_clock_before_genesis() {
407        // clock is behind genesis — should not panic, returns None
408        assert_eq!(super::max_allowed_epoch(500, 1000, 30), None);
409    }
410
411    #[test]
412    fn max_allowed_epoch_zero_block_delay() {
413        // zero block delay would divide by zero — returns None
414        assert_eq!(super::max_allowed_epoch(2000, 1000, 0), None);
415    }
416
417    fn make_gossip_block_with(f: impl FnOnce(&mut RawBlockHeader)) -> GossipBlock {
418        let mut raw = RawBlockHeader {
419            election_proof: Some(ElectionProof {
420                win_count: 1,
421                vrfproof: Default::default(),
422            }),
423            signature: Some(Signature {
424                sig_type: SignatureType::Bls,
425                bytes: vec![0u8; 96],
426            }),
427            ..Default::default()
428        };
429        f(&mut raw);
430        GossipBlock {
431            header: CachingBlockHeader::from(raw),
432            bls_messages: vec![],
433            secpk_messages: vec![],
434        }
435    }
436
437    fn make_valid_gossip_block() -> GossipBlock {
438        make_gossip_block_with(|_| {})
439    }
440
441    fn make_genesis() -> Tipset {
442        Tipset::from(CachingBlockHeader::default())
443    }
444
445    #[test]
446    fn gossip_block_validator_accepts_valid_block() {
447        let block = make_valid_gossip_block();
448        let genesis = make_genesis();
449        let seen = SeenBlockCache::default();
450
451        let result = GossipBlockValidator::new(&block).validate_pre_fetch(
452            &genesis, 30,   // block_delay
453            900,  // chain_finality
454            0,    // heaviest_epoch (same as block 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, 900, 0, None, &seen)
470                .is_ok()
471        );
472
473        let err = GossipBlockValidator::new(&block)
474            .validate_pre_fetch(&genesis, 30, 900, 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, 900, 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, 900, 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, 900, 1000, 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, 900, 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, 900, 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, 900, 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, 900, 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, 900, 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, 900, 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, 900, 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, 900, 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}