use std::{marker::PhantomData, sync::Arc, collections::HashMap}; use async_trait::async_trait; use sp_api::BlockId; use sp_runtime::traits::{Header, Block}; use sp_blockchain::{BlockStatus, HeaderBackend, Backend as BlockchainBackend}; use sp_consensus::{Error, CacheKeyId, BlockOrigin, SelectChain}; use sc_consensus::{BlockCheckParams, BlockImportParams, ImportResult, BlockImport, Verifier}; use sc_client_api::{Backend, BlockBackend}; use crate::{TendermintValidator, tendermint::TendermintImport}; impl TendermintImport { fn check_already_in_chain(&self, hash: ::Hash) -> bool { let id = BlockId::Hash(hash); // If it's in chain, with justifications, return it's already on chain // If it's in chain, without justifications, continue the block import process to import its // justifications // This can be triggered if the validators add a block, without justifications, yet the p2p // process then broadcasts it with its justifications (self.client.status(id).unwrap() == BlockStatus::InChain) && self.client.justifications(hash).unwrap().is_some() } } #[async_trait] impl BlockImport for TendermintImport where Arc: BlockImport, as BlockImport>::Error: Into, { type Error = Error; type Transaction = T::BackendTransaction; // TODO: Is there a DoS where you send a block without justifications, causing it to error, // yet adding it to the blacklist in the process preventing further syncing? async fn check_block( &mut self, mut block: BlockCheckParams, ) -> Result { if self.check_already_in_chain(block.hash) { return Ok(ImportResult::AlreadyInChain); } self.verify_order(block.parent_hash, block.number)?; // Does not verify origin here as origin only applies to unfinalized blocks // We don't have context on if this block has justifications or not block.allow_missing_state = false; block.allow_missing_parent = false; self.client.check_block(block).await.map_err(Into::into) } async fn import_block( &mut self, mut block: BlockImportParams, new_cache: HashMap>, ) -> Result { // Don't allow multiple blocks to be imported at once let _lock = self.sync_lock.lock().await; if self.check_already_in_chain(block.header.hash()) { return Ok(ImportResult::AlreadyInChain); } self.check(&mut block).await?; self.client.import_block(block, new_cache).await.map_err(Into::into) } } #[async_trait] impl Verifier for TendermintImport where Arc: BlockImport, as BlockImport>::Error: Into, { async fn verify( &mut self, mut block: BlockImportParams, ) -> Result<(BlockImportParams, Option)>>), String> { block.origin = match block.origin { BlockOrigin::Genesis => BlockOrigin::Genesis, BlockOrigin::NetworkBroadcast => BlockOrigin::NetworkBroadcast, // Re-map NetworkInitialSync to NetworkBroadcast so it still triggers notifications // Tendermint will listen to the finality stream. If we sync a block we're running a machine // for, it'll force the machine to move ahead. We can only do that if there actually are // notifications // // Then Serai also runs data indexing code based on block addition, so ensuring it always // emits events ensures we always perform our necessary indexing (albeit with a race // condition since Substrate will eventually prune the block's state, potentially before // indexing finishes when syncing) // // The alternative to this would be editing Substrate directly, which would be a lot less // fragile, manually triggering the notifications (which may be possible with code intended // for testing), writing our own notification system, or implementing lock_import_and_run // on our end, letting us directly set the notifications, so we're not beholden to when // Substrate decides to call notify_finalized // // lock_import_and_run unfortunately doesn't allow async code and generally isn't feasible to // work with though. We also couldn't use it to prevent Substrate from creating // notifications, so it only solves half the problem. We'd *still* have to keep this patch, // with all its fragility, unless we edit Substrate or move the entire block import flow here BlockOrigin::NetworkInitialSync => BlockOrigin::NetworkBroadcast, // Also re-map File so bootstraps also trigger notifications, enabling using bootstraps BlockOrigin::File => BlockOrigin::NetworkBroadcast, // We do not want this block, which hasn't been confirmed, to be broadcast over the net // Substrate will generate notifications unless it's Genesis, which this isn't, InitialSync, // which changes telemetry behavior, or File, which is... close enough BlockOrigin::ConsensusBroadcast => BlockOrigin::File, BlockOrigin::Own => BlockOrigin::File, }; if self.check_already_in_chain(block.header.hash()) { return Ok((block, None)); } self.check(&mut block).await.map_err(|e| format!("{}", e))?; Ok((block, None)) } } /// Tendermint's Select Chain, where the best chain is defined as the most recently finalized /// block. /// /// leaves panics on call due to not being applicable under Tendermint. Any provided answer would /// have conflicts best left unraised. // // SelectChain, while provided by Substrate and part of PartialComponents, isn't used by Substrate // It's common between various block-production/finality crates, yet Substrate as a system doesn't // rely on it, which is good, because its definition is explicitly incompatible with Tendermint // // leaves is supposed to return all leaves of the blockchain. While Tendermint maintains that view, // an honest node will only build on the most recently finalized block, so it is a 'leaf' despite // having descendants // // best_chain will always be this finalized block, yet Substrate explicitly defines it as one of // the above leaves, which this finalized block is explicitly not included in. Accordingly, we // can never provide a compatible decision // // Since PartialComponents expects it, an implementation which does its best is provided. It panics // if leaves is called, yet returns the finalized chain tip for best_chain, as that's intended to // be the header to build upon pub struct TendermintSelectChain>(Arc, PhantomData); impl> Clone for TendermintSelectChain { fn clone(&self) -> Self { TendermintSelectChain(self.0.clone(), PhantomData) } } impl> TendermintSelectChain { pub fn new(backend: Arc) -> TendermintSelectChain { TendermintSelectChain(backend, PhantomData) } } #[async_trait] impl> SelectChain for TendermintSelectChain { async fn leaves(&self) -> Result, Error> { panic!("Substrate definition of leaves is incompatible with Tendermint") } async fn best_chain(&self) -> Result { Ok( self .0 .blockchain() // There should always be a finalized block .header(BlockId::Hash(self.0.blockchain().last_finalized().unwrap())) // There should not be an error in retrieving it and since it's finalized, it should exist .unwrap() .unwrap(), ) } }