Skip to main content

forest/chain_sync/
tipset_syncer.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use crate::chain_sync::BadBlockCache;
5use crate::db::DbImpl;
6use crate::networks::Height;
7use crate::prelude::*;
8use crate::shim::clock::ALLOWABLE_CLOCK_DRIFT;
9use crate::shim::crypto::SignatureType;
10use crate::shim::message::Message;
11use crate::shim::{
12    address::Address, crypto::verify_bls_aggregate, econ::BLOCK_GAS_LIMIT,
13    gas::price_list_by_network_version, state_tree::StateTree,
14};
15use crate::state_manager::ExecutedTipset;
16use crate::state_manager::{Error as StateManagerError, StateManager, utils::is_valid_for_sending};
17use crate::{
18    blocks::{Block, CachingBlockHeader, Error as ForestBlockError, FullTipset, Tipset},
19    fil_cns::{self, FilecoinConsensus, FilecoinConsensusError},
20};
21use crate::{
22    chain::{ChainStore, Error as ChainStoreError},
23    metrics::HistogramTimerExt,
24};
25use crate::{
26    eth::is_valid_eth_tx_for_sending,
27    message::{MessageRead as _, valid_for_block_inclusion},
28};
29use ahash::HashMap;
30use futures::TryFutureExt;
31use fvm_ipld_encoding::to_vec;
32use nunny::Vec as NonEmpty;
33use thiserror::Error;
34use tokio::task::JoinSet;
35use tracing::{trace, warn};
36
37use crate::chain_sync::{consensus::collect_errs, metrics, validation::TipsetValidator};
38
39#[derive(Debug, Error)]
40pub enum TipsetSyncerError {
41    #[error("Block must have a signature")]
42    BlockWithoutSignature,
43    #[error("Block without BLS aggregate signature")]
44    BlockWithoutBlsAggregate,
45    #[error("Block received from the future: now = {0}, block = {1}")]
46    TimeTravellingBlock(u64, u64),
47    #[error("Validation error: {0}")]
48    Validation(String),
49    /// Locally computed parent state or receipt root does not match the block header.
50    /// Distinct from [`Self::Validation`] so the chain follower can repair locally
51    /// corrupted inputs (e.g. a stale tipset lookup entry) before treating the block as bad.
52    #[error("Parent chain state mismatch: {0}")]
53    ParentChainStateMismatch(String),
54    #[error("Processing error: {0}")]
55    Calculation(String),
56    #[error("Chain store error: {0}")]
57    ChainStore(#[from] ChainStoreError),
58    #[error("StateManager error: {0}")]
59    StateManager(#[from] StateManagerError),
60    #[error("Block error: {0}")]
61    BlockError(#[from] ForestBlockError),
62    #[error("Querying tipsets from the network failed: {0}")]
63    NetworkTipsetQueryFailed(String),
64    #[error("BLS aggregate signature {0} was invalid for msgs {1}")]
65    BlsAggregateSignatureInvalid(String, String),
66    #[error("Message signature invalid: {0}")]
67    MessageSignatureInvalid(String),
68    #[error("Block message root does not match: expected {0}, computed {1}")]
69    BlockMessageRootInvalid(String, String),
70    #[error("Computing message root failed: {0}")]
71    ComputingMessageRoot(String),
72    #[error("Resolving address from message failed: {0}")]
73    ResolvingAddressFromMessage(String),
74    #[error("Loading tipset parent from the store failed: {0}")]
75    TipsetParentNotFound(ChainStoreError),
76    #[error("Consensus error: {0}")]
77    ConsensusError(FilecoinConsensusError),
78}
79
80impl From<tokio::task::JoinError> for TipsetSyncerError {
81    fn from(err: tokio::task::JoinError) -> Self {
82        TipsetSyncerError::NetworkTipsetQueryFailed(format!("{err}"))
83    }
84}
85
86impl TipsetSyncerError {
87    /// Concatenate all validation error messages into one comma separated
88    /// version.
89    fn concat(errs: NonEmpty<TipsetSyncerError>) -> Self {
90        let msg = errs.iter().map(|e| e.to_string()).collect_vec().join(", ");
91
92        if errs
93            .iter()
94            .any(|e| matches!(e, TipsetSyncerError::ParentChainStateMismatch(_)))
95        {
96            TipsetSyncerError::ParentChainStateMismatch(msg)
97        } else {
98            TipsetSyncerError::Validation(msg)
99        }
100    }
101}
102
103/// Validates full blocks in the tipset in parallel (since the messages are not
104/// executed), adding the successful ones to the tipset tracker, and the failed
105/// ones to the bad block cache, depending on strategy. Any bad block fails
106/// validation.
107pub async fn validate_tipset(
108    state_manager: &StateManager,
109    full_tipset: FullTipset,
110    bad_block_cache: Option<BadBlockCache>,
111) -> Result<(), TipsetSyncerError> {
112    if full_tipset
113        .key()
114        .eq(state_manager.chain_store().genesis_tipset().key())
115    {
116        trace!("Skipping genesis tipset validation");
117        return Ok(());
118    }
119
120    let timer = metrics::TIPSET_PROCESSING_TIME.start_timer();
121
122    let epoch = full_tipset.epoch();
123    let parent_state = *full_tipset.parent_state();
124    let tipset_key = full_tipset.key();
125    trace!("Tipset keys: {tipset_key}");
126    let blocks = full_tipset.into_blocks();
127    let mut validations = JoinSet::new();
128    for b in blocks {
129        validations.spawn(validate_block(state_manager.shallow_clone(), Arc::new(b)));
130    }
131
132    while let Some(result) = validations.join_next().await {
133        match result? {
134            Ok(block) => {
135                state_manager
136                    .chain_store()
137                    .add_to_tipset_tracker(block.header());
138            }
139            Err(boxed) => {
140                let (cid, why) = *boxed;
141                warn!(
142                    "Validating block [CID = {cid}, PARENT_STATE = {parent_state}] in EPOCH = {epoch} failed: {why}",
143                );
144                match &why {
145                    TipsetSyncerError::TimeTravellingBlock(_, _) => {
146                        // Do not mark a block as bad for temporary errors.
147                        // See <https://github.com/filecoin-project/lotus/blob/v1.34.1/chain/sync.go#L602> in Lotus
148                    }
149                    _ => {
150                        // Do not mark block as bad if the parent state tree does not exist
151                        if StateTree::new_from_root(state_manager.db(), &parent_state).is_ok()
152                            && let Some(bad_block_cache) = bad_block_cache
153                        {
154                            bad_block_cache.push(cid);
155                        }
156                    }
157                };
158                return Err(why);
159            }
160        }
161    }
162    drop(timer);
163    Ok(())
164}
165
166/// Validate the block according to the rules specific to the consensus being
167/// used, and the common rules that pertain to the assumptions of the
168/// `ChainSync` protocol.
169///
170/// Returns the validated block if `Ok`.
171/// Returns the block CID (for marking bad) and `Error` if invalid (`Err`).
172///
173/// Common validation includes:
174/// * Sanity checks
175/// * Clock drifts
176/// * Signatures
177/// * Message inclusion (fees, sequences)
178/// * Parent related fields: base fee, weight, the state root
179/// * NB: This is where the messages in the *parent* tipset are executed.
180///
181/// Consensus specific validation should include:
182/// * Checking that the messages in the block correspond to the agreed upon
183///   total ordering
184/// * That the block is a deterministic derivative of the underlying consensus
185async fn validate_block(
186    state_manager: StateManager,
187    block: Arc<Block>,
188) -> Result<Arc<Block>, Box<(Cid, TipsetSyncerError)>> {
189    let consensus = FilecoinConsensus::new(state_manager.beacon_schedule().clone());
190    trace!(
191        "Validating block: epoch = {}, weight = {}, key = {}",
192        block.header().epoch,
193        block.header().weight,
194        block.header().cid(),
195    );
196    let chain_store = state_manager.chain_store().shallow_clone();
197    let block_cid = block.cid();
198
199    // Check block validation cache in store
200    let is_validated = chain_store.is_block_validated(block_cid);
201    if is_validated {
202        return Ok(block);
203    }
204
205    let _timer = metrics::BLOCK_VALIDATION_TIME.start_timer();
206
207    let header = block.header();
208
209    // Check to ensure all optional values exist
210    block_sanity_checks(header).map_err(|e| Box::new((*block_cid, e)))?;
211    block_timestamp_checks(header).map_err(|e| Box::new((*block_cid, e)))?;
212
213    let base_tipset = chain_store
214        .chain_index()
215        .load_required_tipset(&header.parents)
216        // The parent tipset will always be there when calling validate_block
217        // as part of the sync_tipset_range flow because all of the headers in the range
218        // have been committed to the store. When validate_block is called from sync_tipset
219        // this guarantee does not exist, so we create a specific error to inform the caller
220        // not to add this block to the bad blocks cache.
221        .map_err(|why| Box::new((*block_cid, TipsetSyncerError::TipsetParentNotFound(why))))?;
222
223    // Retrieve lookback tipset for validation
224    let lookback_state = ChainStore::get_lookback_tipset_for_round(
225        state_manager.chain_store().chain_index().shallow_clone(),
226        state_manager.chain_config().shallow_clone(),
227        base_tipset.shallow_clone(),
228        block.header().epoch,
229    )
230    .await
231    .map_err(|e| Box::new((*block_cid, e.into())))
232    .map(|(_, s)| Arc::new(s))?;
233
234    // Work address needed for async validations, so necessary
235    // to do sync to avoid duplication
236    let work_addr = state_manager
237        .get_miner_work_addr(*lookback_state, &header.miner_address)
238        .map_err(|e| Box::new((*block_cid, e.into())))?;
239
240    // Async validations
241    let mut validations = JoinSet::new();
242
243    // Check block messages
244    validations.spawn(check_block_messages(
245        state_manager.shallow_clone(),
246        block.shallow_clone(),
247        base_tipset.shallow_clone(),
248    ));
249
250    // Base fee check
251    validations.spawn_blocking({
252        let smoke_height = state_manager.chain_config().epoch(Height::Smoke);
253        let firehorse_height = state_manager.chain_config().epoch(Height::FireHorse);
254        let base_tipset = base_tipset.shallow_clone();
255        let block_store = state_manager.db_owned();
256        let block = block.shallow_clone();
257        move || {
258            let base_fee = crate::chain::compute_base_fee(
259                &block_store,
260                &base_tipset,
261                smoke_height,
262                firehorse_height,
263            )
264            .map_err(|e| {
265                TipsetSyncerError::Validation(format!("Could not compute base fee: {e}"))
266            })?;
267            let parent_base_fee = &block.header.parent_base_fee;
268            if &base_fee != parent_base_fee {
269                return Err(TipsetSyncerError::Validation(format!(
270                    "base fee doesn't match: {parent_base_fee} (header), {base_fee} (computed)"
271                )));
272            }
273            Ok(())
274        }
275    });
276
277    // Parent weight calculation check
278    validations.spawn_blocking({
279        let block_store = state_manager.db_owned();
280        let base_tipset = base_tipset.shallow_clone();
281        let weight = header.weight.clone();
282        move || {
283            let calc_weight = fil_cns::weight(&block_store, &base_tipset).map_err(|e| {
284                TipsetSyncerError::Calculation(format!("Error calculating weight: {e:#}"))
285            })?;
286            if weight != calc_weight {
287                return Err(TipsetSyncerError::Validation(format!(
288                    "Parent weight doesn't match: {weight} (header), {calc_weight} (computed)"
289                )));
290            }
291            Ok(())
292        }
293    });
294
295    // State root and receipt root validations
296    validations.spawn({
297        let state_manager = state_manager.shallow_clone();
298        let block = block.shallow_clone();
299        async move {
300            let header = block.header();
301            let ExecutedTipset {
302                state_root,
303                receipt_root,
304                ..
305            } = state_manager
306                .load_executed_tipset(&base_tipset)
307                .await
308                .map_err(|e| {
309                    TipsetSyncerError::Calculation(format!("Failed to calculate state: {e:#}"))
310                })?;
311
312            if state_root != header.state_root {
313                return Err(TipsetSyncerError::ParentChainStateMismatch(format!(
314                    "Parent state root did not match computed state: {} (header), {} (computed)",
315                    header.state_root, state_root,
316                )));
317            }
318
319            if receipt_root != header.message_receipts {
320                return Err(TipsetSyncerError::ParentChainStateMismatch(format!(
321                    "Parent receipt root did not match computed root: {} (header), {} (computed)",
322                    header.message_receipts, receipt_root
323                )));
324            }
325            Ok(())
326        }
327    });
328
329    // Block signature check
330    validations.spawn_blocking({
331        let block = block.shallow_clone();
332        move || {
333            block.header().verify_signature_against(&work_addr)?;
334            Ok(())
335        }
336    });
337
338    validations.spawn({
339        let block = block.shallow_clone();
340        async move {
341            consensus
342                .validate_block(state_manager, block)
343                .map_err(|errs| {
344                    // NOTE: Concatenating errors here means the wrapper type of error
345                    // never surfaces, yet we always pay the cost of the generic argument.
346                    // But there's no reason `validate_block` couldn't return a list of all
347                    // errors instead of a single one that has all the error messages,
348                    // removing the caller's ability to distinguish between them.
349
350                    TipsetSyncerError::concat(
351                        errs.into_iter_ne()
352                            .map(TipsetSyncerError::ConsensusError)
353                            .collect_vec(),
354                    )
355                })
356                .await
357        }
358    });
359
360    // Collect the errors from the async validations
361    if let Err(errs) = collect_errs(validations).await {
362        return Err(Box::new((*block_cid, TipsetSyncerError::concat(errs))));
363    }
364
365    chain_store.mark_block_as_validated(block_cid);
366
367    Ok(block)
368}
369
370/// Validate messages in a full block, relative to the parent tipset.
371///
372/// This includes:
373/// * signature checks
374/// * gas limits, and prices
375/// * account nonce values
376/// * the message root in the header
377///
378/// NB: This loads/computes the state resulting from the execution of the parent
379/// tipset.
380async fn check_block_messages(
381    state_manager: StateManager,
382    block: Arc<Block>,
383    base_tipset: Tipset,
384) -> Result<(), TipsetSyncerError> {
385    let network_version = state_manager
386        .chain_config()
387        .network_version(block.header.epoch);
388    let eth_chain_id = state_manager.chain_config().eth_chain_id;
389
390    if let Some(sig) = &block.header().bls_aggregate {
391        // Do the initial loop here
392        // check block message and signatures in them
393        let mut pub_keys = Vec::with_capacity(block.bls_msgs().len());
394        let mut cids = Vec::with_capacity(block.bls_msgs().len());
395        let db = state_manager.db();
396        for m in block.bls_msgs() {
397            let pk = StateManager::get_bls_public_key(db, m.from, *base_tipset.parent_state())?;
398            pub_keys.push(pk);
399            cids.push(m.cid().to_bytes());
400        }
401
402        if !verify_bls_aggregate(
403            &cids.iter().map(|x| x.as_slice()).collect_vec(),
404            &pub_keys,
405            sig,
406        ) {
407            return Err(TipsetSyncerError::BlsAggregateSignatureInvalid(
408                format!("{sig:?}"),
409                format!("{cids:?}"),
410            ));
411        }
412    } else {
413        return Err(TipsetSyncerError::BlockWithoutBlsAggregate);
414    }
415
416    let price_list = price_list_by_network_version(network_version);
417    let mut sum_gas_limit = 0;
418
419    // Check messages for validity
420    let mut check_msg = |msg: &Message,
421                         account_sequences: &mut HashMap<Address, u64>,
422                         tree: &StateTree<DbImpl>|
423     -> anyhow::Result<()> {
424        // Phase 1: Syntactic validation
425        let min_gas = price_list.on_chain_message(to_vec(msg)?.len());
426        valid_for_block_inclusion(msg, min_gas.total(), network_version)
427            .map_err(|e| anyhow::anyhow!("{e}"))?;
428        sum_gas_limit += msg.gas_limit;
429        if sum_gas_limit > BLOCK_GAS_LIMIT {
430            anyhow::bail!("block gas limit exceeded");
431        }
432
433        // Phase 2: (Partial) Semantic validation
434        // Send exists and is an account actor, and sequence is correct
435        let sequence: u64 = match account_sequences.get(&msg.from()) {
436            Some(sequence) => *sequence,
437            None => {
438                let actor = tree.get_actor(&msg.from)?.ok_or_else(|| {
439                    anyhow::anyhow!(
440                        "Failed to retrieve nonce for addr: Actor does not exist in state"
441                    )
442                })?;
443                let network_version = state_manager
444                    .chain_config()
445                    .network_version(block.header.epoch);
446                if !is_valid_for_sending(network_version, &actor) {
447                    anyhow::bail!("not valid for sending!");
448                }
449                actor.sequence
450            }
451        };
452
453        // Sequence equality check
454        if sequence != msg.sequence {
455            anyhow::bail!(
456                "Message has incorrect sequence (exp: {} got: {})",
457                sequence,
458                msg.sequence
459            );
460        }
461        account_sequences.insert(msg.from(), sequence + 1);
462        Ok(())
463    };
464
465    let mut account_sequences: HashMap<Address, u64> = HashMap::default();
466    let ExecutedTipset { state_root, .. } = state_manager
467        .load_executed_tipset(&base_tipset)
468        .await
469        .map_err(|e| TipsetSyncerError::Calculation(format!("Could not update state: {e:#}")))?;
470    let tree = StateTree::new_from_root(state_manager.db(), &state_root).map_err(|e| {
471        TipsetSyncerError::Calculation(format!(
472            "Could not load from new state root in state manager: {e:#}"
473        ))
474    })?;
475
476    // Check validity for BLS messages
477    for (i, msg) in block.bls_msgs().iter().enumerate() {
478        check_msg(msg, &mut account_sequences, &tree).map_err(|e| {
479            TipsetSyncerError::Validation(format!(
480                "Block had invalid BLS message at index {i}: {e:#}"
481            ))
482        })?;
483    }
484
485    // Check validity for SECP messages
486    for (i, msg) in block.secp_msgs().iter().enumerate() {
487        if msg.signature().signature_type() == SignatureType::Delegated
488            && !is_valid_eth_tx_for_sending(eth_chain_id, network_version, msg)
489        {
490            return Err(TipsetSyncerError::Validation(
491                "Network version must be at least NV23 for legacy Ethereum transactions".to_owned(),
492            ));
493        }
494        check_msg(msg.message(), &mut account_sequences, &tree).map_err(|e| {
495            TipsetSyncerError::Validation(format!(
496                "block had an invalid secp message at index {i}: {e:#}"
497            ))
498        })?;
499        // Resolve key address for signature verification
500        let key_addr = state_manager
501            .resolve_to_deterministic_address(msg.from(), &base_tipset)
502            .await
503            .map_err(|e| TipsetSyncerError::ResolvingAddressFromMessage(e.to_string()))?;
504        // SecP256K1 Signature validation
505        msg.signature
506            .authenticate_msg(eth_chain_id, msg, &key_addr)
507            .map_err(|e| TipsetSyncerError::MessageSignatureInvalid(e.to_string()))?;
508    }
509
510    // Validate message root from header matches message root
511    let msg_root =
512        TipsetValidator::compute_msg_root(state_manager.db(), block.bls_msgs(), block.secp_msgs())
513            .map_err(|err| TipsetSyncerError::ComputingMessageRoot(err.to_string()))?;
514    if block.header().messages != msg_root {
515        return Err(TipsetSyncerError::BlockMessageRootInvalid(
516            format!("{:?}", block.header().messages),
517            format!("{msg_root:?}"),
518        ));
519    }
520
521    Ok(())
522}
523
524/// Checks optional values in header.
525///
526/// It only looks for fields which are common to all consensus types.
527fn block_sanity_checks(header: &CachingBlockHeader) -> Result<(), TipsetSyncerError> {
528    if header.signature.is_none() {
529        return Err(TipsetSyncerError::BlockWithoutSignature);
530    }
531    if header.bls_aggregate.is_none() {
532        return Err(TipsetSyncerError::BlockWithoutBlsAggregate);
533    }
534    Ok(())
535}
536
537/// Check the clock drift.
538fn block_timestamp_checks(header: &CachingBlockHeader) -> Result<(), TipsetSyncerError> {
539    let time_now = chrono::Utc::now().timestamp() as u64;
540    if header.timestamp > time_now.saturating_add(ALLOWABLE_CLOCK_DRIFT) {
541        return Err(TipsetSyncerError::TimeTravellingBlock(
542            time_now,
543            header.timestamp,
544        ));
545    } else if header.timestamp > time_now {
546        warn!(
547            "Got block from the future, but within clock drift threshold, {} > {}",
548            header.timestamp, time_now
549        );
550    }
551    Ok(())
552}
553
554#[cfg(test)]
555mod tests {
556    use super::*;
557
558    #[test]
559    fn concat_preserves_parent_chain_state_mismatch() {
560        let concatenated = TipsetSyncerError::concat(nunny::vec![
561            TipsetSyncerError::Validation("a".into()),
562            TipsetSyncerError::ParentChainStateMismatch("b".into()),
563        ]);
564        assert!(matches!(
565            concatenated,
566            TipsetSyncerError::ParentChainStateMismatch(_)
567        ));
568
569        let concatenated =
570            TipsetSyncerError::concat(nunny::vec![TipsetSyncerError::Validation("a".into())]);
571        assert!(matches!(concatenated, TipsetSyncerError::Validation(_)));
572    }
573}