1use 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 #[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 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
103pub 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 }
149 _ => {
150 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
166async 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 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 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 .map_err(|why| Box::new((*block_cid, TipsetSyncerError::TipsetParentNotFound(why))))?;
222
223 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 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 let mut validations = JoinSet::new();
242
243 validations.spawn(check_block_messages(
245 state_manager.shallow_clone(),
246 block.shallow_clone(),
247 base_tipset.shallow_clone(),
248 ));
249
250 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 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 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 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 TipsetSyncerError::concat(
351 errs.into_iter_ne()
352 .map(TipsetSyncerError::ConsensusError)
353 .collect_vec(),
354 )
355 })
356 .await
357 }
358 });
359
360 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
370async 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 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 let mut check_msg = |msg: &Message,
421 account_sequences: &mut HashMap<Address, u64>,
422 tree: &StateTree<DbImpl>|
423 -> anyhow::Result<()> {
424 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 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 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 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 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 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 msg.signature
506 .authenticate_msg(eth_chain_id, msg, &key_addr)
507 .map_err(|e| TipsetSyncerError::MessageSignatureInvalid(e.to_string()))?;
508 }
509
510 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
524fn 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
537fn 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}