Skip to content

Commit

Permalink
fix(f3): validate tipset before resetting the chain head (#4931)
Browse files Browse the repository at this point in the history
  • Loading branch information
hanabi1224 authored Oct 28, 2024
1 parent f94c2b1 commit 9d76a71
Show file tree
Hide file tree
Showing 8 changed files with 64 additions and 49 deletions.
2 changes: 2 additions & 0 deletions scripts/tests/api_compare/filter-list
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@
!Filecoin.EthGetTransactionReceipt
# TODO(elmattic): https://github.com/ChainSafe/forest/issues/4851
!Filecoin.EthGetLogs
# Enable after Lotus image is upgraded to a release in which F3IntitialPowerTable is hard coded.
!Filecoin.F3GetManifest
5 changes: 5 additions & 0 deletions src/chain/store/chain_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,11 @@ where
.expect("failed to load heaviest tipset")
}

/// Returns the genesis tipset.
pub fn genesis_tipset(&self) -> Tipset {
Tipset::from(self.genesis_block_header())
}

/// Returns a reference to the publisher of head changes.
pub fn publisher(&self) -> &Publisher<HeadChange> {
&self.publisher
Expand Down
8 changes: 4 additions & 4 deletions src/chain_sync/chain_muxer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ pub enum ChainMuxerError {
#[error("Tipset range syncer error: {0}")]
TipsetRangeSyncer(#[from] TipsetRangeSyncerError),
#[error("Tipset validation error: {0}")]
TipsetValidator(#[from] Box<TipsetValidationError>),
TipsetValidator(#[from] TipsetValidationError),
#[error("Sending tipset on channel failed: {0}")]
TipsetChannelSend(String),
#[error("Receiving p2p network event failed: {0}")]
Expand Down Expand Up @@ -512,9 +512,9 @@ where

// Validate tipset
if let Err(why) = TipsetValidator(&tipset).validate(
chain_store.clone(),
bad_block_cache.clone(),
genesis.clone(),
&chain_store,
Some(&bad_block_cache),
&genesis,
block_delay,
) {
metrics::INVALID_TIPSET_TOTAL.inc();
Expand Down
3 changes: 1 addition & 2 deletions src/chain_sync/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@ mod sync_state;
mod tipset_syncer;
mod validation;

pub use validation::TipsetValidator;

pub use self::{
bad_block_cache::BadBlockCache,
chain_muxer::{ChainMuxer, SyncConfig},
consensus::collect_errs,
sync_state::{SyncStage, SyncState},
validation::{TipsetValidationError, TipsetValidator},
};
46 changes: 21 additions & 25 deletions src/chain_sync/validation.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
// Copyright 2019-2024 ChainSafe Systems
// SPDX-License-Identifier: Apache-2.0, MIT

use std::{
sync::Arc,
time::{SystemTime, UNIX_EPOCH},
};
use std::time::{SystemTime, UNIX_EPOCH};

use crate::blocks::{Block, FullTipset, Tipset, TxMeta};
use crate::chain::ChainStore;
Expand Down Expand Up @@ -41,15 +38,15 @@ pub enum TipsetValidationError {
Encoding(EncodingError),
}

impl From<EncodingError> for Box<TipsetValidationError> {
impl From<EncodingError> for TipsetValidationError {
fn from(err: EncodingError) -> Self {
Box::new(TipsetValidationError::Encoding(err))
TipsetValidationError::Encoding(err)
}
}

impl From<IpldAmtError> for Box<TipsetValidationError> {
impl From<IpldAmtError> for TipsetValidationError {
fn from(err: IpldAmtError) -> Self {
Box::new(TipsetValidationError::IpldAmt(err.to_string()))
TipsetValidationError::IpldAmt(err.to_string())
}
}

Expand All @@ -58,14 +55,14 @@ pub struct TipsetValidator<'a>(pub &'a FullTipset);
impl<'a> TipsetValidator<'a> {
pub fn validate<DB: Blockstore>(
&self,
chainstore: Arc<ChainStore<DB>>,
bad_block_cache: Arc<BadBlockCache>,
genesis_tipset: Arc<Tipset>,
chainstore: &ChainStore<DB>,
bad_block_cache: Option<&BadBlockCache>,
genesis_tipset: &Tipset,
block_delay: u32,
) -> Result<(), Box<TipsetValidationError>> {
) -> Result<(), TipsetValidationError> {
// No empty blocks
if self.0.blocks().is_empty() {
return Err(Box::new(TipsetValidationError::NoBlocks));
return Err(TipsetValidationError::NoBlocks);
}

// Tipset epoch must not be behind current max
Expand All @@ -77,11 +74,10 @@ impl<'a> TipsetValidator<'a> {
// previously been seen in the bad blocks cache
for block in self.0.blocks() {
self.validate_msg_root(&chainstore.db, block)?;
if let Some(bad) = bad_block_cache.peek(block.cid()) {
return Err(Box::new(TipsetValidationError::InvalidBlock(
*block.cid(),
bad,
)));
if let Some(bad_block_cache) = bad_block_cache {
if let Some(bad) = bad_block_cache.peek(block.cid()) {
return Err(TipsetValidationError::InvalidBlock(*block.cid(), bad));
}
}
}

Expand All @@ -90,9 +86,9 @@ impl<'a> TipsetValidator<'a> {

pub fn validate_epoch(
&self,
genesis_tipset: Arc<Tipset>,
genesis_tipset: &Tipset,
block_delay: u32,
) -> Result<(), Box<TipsetValidationError>> {
) -> Result<(), TipsetValidationError> {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
Expand All @@ -101,7 +97,7 @@ impl<'a> TipsetValidator<'a> {
((now - genesis_tipset.min_timestamp()) / block_delay as u64) + MAX_HEIGHT_DRIFT;
let too_far_ahead_in_time = self.0.epoch() as u64 > max_epoch;
if too_far_ahead_in_time {
Err(Box::new(TipsetValidationError::EpochTooLarge))
Err(TipsetValidationError::EpochTooLarge)
} else {
Ok(())
}
Expand All @@ -111,10 +107,10 @@ impl<'a> TipsetValidator<'a> {
&self,
blockstore: &DB,
block: &Block,
) -> Result<(), Box<TipsetValidationError>> {
) -> Result<(), TipsetValidationError> {
let msg_root = Self::compute_msg_root(blockstore, block.bls_msgs(), block.secp_msgs())?;
if block.header().messages != msg_root {
Err(Box::new(TipsetValidationError::InvalidRoots))
Err(TipsetValidationError::InvalidRoots)
} else {
Ok(())
}
Expand All @@ -124,7 +120,7 @@ impl<'a> TipsetValidator<'a> {
blockstore: &DB,
bls_msgs: &[Message],
secp_msgs: &[SignedMessage],
) -> Result<Cid, Box<TipsetValidationError>> {
) -> Result<Cid, TipsetValidationError> {
// Generate message CIDs
let bls_cids = bls_msgs
.iter()
Expand All @@ -146,7 +142,7 @@ impl<'a> TipsetValidator<'a> {
// Store message roots and receive meta_root CID
blockstore
.put_cbor_default(&meta)
.map_err(|e| Box::new(TipsetValidationError::Blockstore(e.to_string())))
.map_err(|e| TipsetValidationError::Blockstore(e.to_string()))
}
}

Expand Down
1 change: 1 addition & 0 deletions src/rpc/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ from2internal! {
base64::DecodeError,
cid::multibase::Error,
crate::chain::store::Error,
crate::chain_sync::TipsetValidationError,
crate::key_management::Error,
crate::libp2p::ParseError,
crate::message_pool::Error,
Expand Down
38 changes: 25 additions & 13 deletions src/rpc/methods/f3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ pub use self::types::F3LeaseManager;
use self::{types::*, util::*};
use super::wallet::WalletSign;
use crate::{
blocks::Tipset,
chain::index::ResolveNullTipset,
chain_sync::TipsetValidator,
libp2p::{NetRPCMethods, NetworkMessage},
lotus_json::HasLotusJson as _,
rpc::{types::ApiTipsetKey, ApiPaths, Ctx, Permission, RpcMethod, ServerError},
Expand Down Expand Up @@ -481,19 +483,13 @@ impl RpcMethod<1> for Finalize {
let tsk = f3_tsk.try_into()?;
let finalized_ts = match ctx.chain_index().load_tipset(&tsk)? {
Some(ts) => ts,
None => {
let ts = ctx
.sync_network_context
.chain_exchange_headers(None, &tsk, NonZeroU64::new(1).expect("Infallible"))
.await?
.first()
.cloned()
.with_context(|| {
format!("failed to get tipset via chain exchange. tsk: {tsk}")
})?;
ctx.chain_store().put_tipset(&ts)?;
ts
}
None => ctx
.sync_network_context
.chain_exchange_headers(None, &tsk, NonZeroU64::new(1).expect("Infallible"))
.await?
.first()
.cloned()
.with_context(|| format!("failed to get tipset via chain exchange. tsk: {tsk}"))?,
};
tracing::info!(
"F3 finalized tsk {} at epoch {}",
Expand All @@ -516,6 +512,22 @@ impl RpcMethod<1> for Finalize {
finalized_ts.key(),
finalized_ts.epoch()
);
let fts = ctx
.sync_network_context
.chain_exchange_fts(None, &tsk)
.await?;
for block in fts.blocks() {
block.persist(ctx.store())?;
}
let validator = TipsetValidator(&fts);
validator.validate(
ctx.chain_store(),
None,
&ctx.chain_store().genesis_tipset(),
ctx.chain_config().block_delay_secs,
)?;
let ts = Arc::new(Tipset::from(fts));
ctx.chain_store().put_tipset(&ts)?;
ctx.chain_store().set_heaviest_tipset(finalized_ts)?;
}
Ok(())
Expand Down
10 changes: 5 additions & 5 deletions src/rpc/methods/sync.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright 2019-2024 ChainSafe Systems
// SPDX-License-Identifier: Apache-2.0, MIT

use crate::blocks::{Block, FullTipset, GossipBlock, Tipset};
use crate::blocks::{Block, FullTipset, GossipBlock};
use crate::libp2p::{IdentTopic, NetworkMessage, PUBSUB_BLOCK_STR};
use crate::lotus_json::{lotus_json_with_self, LotusJson};
use crate::rpc::{ApiPaths, Ctx, Permission, RpcMethod, ServerError};
Expand Down Expand Up @@ -100,13 +100,13 @@ impl RpcMethod<1> for SyncSubmitBlock {
secp_messages,
};
let ts = FullTipset::from(block);
let genesis_ts = Arc::new(Tipset::from(ctx.chain_store().genesis_block_header()));
let genesis_ts = ctx.chain_store().genesis_tipset();

TipsetValidator(&ts)
.validate(
ctx.chain_store().clone(),
ctx.bad_blocks.clone(),
genesis_ts,
ctx.chain_store(),
Some(&ctx.bad_blocks),
&genesis_ts,
ctx.chain_config().block_delay_secs,
)
.context("failed to validate the tipset")?;
Expand Down

0 comments on commit 9d76a71

Please sign in to comment.