From 90beed15905ac1d45e237bf87bcdbe40eb871ab7 Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Fri, 13 Sep 2024 03:17:30 +0100 Subject: [PATCH] add docs to handler functions --- binaries/cuprated/src/blockchain/manager.rs | 4 +- .../src/blockchain/manager/handler.rs | 218 +++++++++++++----- binaries/cuprated/src/blockchain/syncer.rs | 23 +- p2p/p2p/src/constants.rs | 6 +- p2p_state.bin | Bin 172852 -> 172906 bytes 5 files changed, 186 insertions(+), 65 deletions(-) diff --git a/binaries/cuprated/src/blockchain/manager.rs b/binaries/cuprated/src/blockchain/manager.rs index 69de3399..ae5a1d3d 100644 --- a/binaries/cuprated/src/blockchain/manager.rs +++ b/binaries/cuprated/src/blockchain/manager.rs @@ -22,7 +22,7 @@ use tracing::error; pub struct IncomingBlock { pub block: Block, pub prepped_txs: HashMap<[u8; 32], TransactionVerificationData>, - pub response_tx: oneshot::Sender>, + pub response_tx: oneshot::Sender>, } pub struct BlockchainManager { @@ -35,7 +35,7 @@ pub struct BlockchainManager { TxVerifierService, ConsensusBlockchainReadHandle, >, - // TODO: stop_current_block_downloader: Notify, + stop_current_block_downloader: Notify, } impl BlockchainManager { diff --git a/binaries/cuprated/src/blockchain/manager/handler.rs b/binaries/cuprated/src/blockchain/manager/handler.rs index f9f6ce80..1bdae16c 100644 --- a/binaries/cuprated/src/blockchain/manager/handler.rs +++ b/binaries/cuprated/src/blockchain/manager/handler.rs @@ -1,40 +1,45 @@ -use crate::blockchain::types::ConsensusBlockchainReadHandle; -use crate::signals::REORG_LOCK; +use std::{collections::HashMap, sync::Arc}; + +use futures::{TryFutureExt, TryStreamExt}; +use monero_serai::{block::Block, transaction::Transaction}; +use rayon::prelude::*; +use tower::{Service, ServiceExt}; +use tracing::info; + use cuprate_blockchain::service::{BlockchainReadHandle, BlockchainWriteHandle}; -use cuprate_consensus::block::PreparedBlock; -use cuprate_consensus::context::NewBlockData; -use cuprate_consensus::transactions::new_tx_verification_data; use cuprate_consensus::{ + block::PreparedBlock, context::NewBlockData, transactions::new_tx_verification_data, BlockChainContextRequest, BlockChainContextResponse, BlockVerifierService, ExtendedConsensusError, VerifyBlockRequest, VerifyBlockResponse, VerifyTxRequest, VerifyTxResponse, }; -use cuprate_p2p::block_downloader::BlockBatch; -use cuprate_types::blockchain::{ - BlockchainReadRequest, BlockchainResponse, BlockchainWriteRequest, -}; +use cuprate_p2p::{block_downloader::BlockBatch, constants::LONG_BAN}; use cuprate_types::{ + blockchain::{BlockchainReadRequest, BlockchainResponse, BlockchainWriteRequest}, AltBlockInformation, HardFork, TransactionVerificationData, VerifiedBlockInformation, }; -use futures::{TryFutureExt, TryStreamExt}; -use monero_serai::block::Block; -use monero_serai::transaction::Transaction; -use rayon::prelude::*; -use std::collections::HashMap; -use std::sync::Arc; -use tower::{Service, ServiceExt}; -use tracing::info; + +use crate::{blockchain::types::ConsensusBlockchainReadHandle, signals::REORG_LOCK}; impl super::BlockchainManager { + /// Handle an incoming [`Block`]. + /// + /// This function will route to [`Self::handle_incoming_alt_block`] if the block does not follow + /// the top of the main chain. + /// + /// Otherwise, this function will validate and add the block to the main chain. + /// + /// On success returns a [`bool`] indicating if the block was added to the main chain ([`true`]) + /// of an alt-chain ([`false`]). pub async fn handle_incoming_block( &mut self, block: Block, prepared_txs: HashMap<[u8; 32], TransactionVerificationData>, - ) -> Result<(), anyhow::Error> { + ) -> Result { if block.header.previous != self.cached_blockchain_context.top_hash { self.handle_incoming_alt_block(block, prepared_txs).await?; - return Ok(()); + return Ok(false); } let VerifyBlockResponse::MainChain(verified_block) = self @@ -53,9 +58,18 @@ impl super::BlockchainManager { self.add_valid_block_to_main_chain(verified_block).await; - Ok(()) + Ok(true) } + /// Handle an incoming [`BlockBatch`]. + /// + /// This function will route to [`Self::handle_incoming_block_batch_main_chain`] or [`Self::handle_incoming_block_batch_alt_chain`] + /// depending on if the first block in the batch follows from the top of our chain. + /// + /// # Panics + /// + /// This function will panic if the batch is empty or if any internal service returns an unexpected + /// error that we cannot recover from. pub async fn handle_incoming_block_batch(&mut self, batch: BlockBatch) { let (first_block, _) = batch .blocks @@ -63,26 +77,36 @@ impl super::BlockchainManager { .expect("Block batch should not be empty"); if first_block.header.previous == self.cached_blockchain_context.top_hash { - self.handle_incoming_block_batch_main_chain(batch) - .await - .expect("TODO"); + self.handle_incoming_block_batch_main_chain(batch).await; } else { - self.handle_incoming_block_batch_alt_chain(batch) - .await - .expect("TODO"); + self.handle_incoming_block_batch_alt_chain(batch).await; } } - async fn handle_incoming_block_batch_main_chain( - &mut self, - batch: BlockBatch, - ) -> Result<(), anyhow::Error> { + /// Handles an incoming [`BlockBatch`] that follows the main chain. + /// + /// This function will handle validating the blocks in the batch and adding them to the blockchain + /// database and context cache. + /// + /// This function will also handle banning the peer and canceling the block downloader if the + /// block is invalid. + /// + /// # Panics + /// + /// This function will panic if any internal service returns an unexpected error that we cannot + /// recover from. + async fn handle_incoming_block_batch_main_chain(&mut self, batch: BlockBatch) { info!( "Handling batch to main chain height: {}", batch.blocks.first().unwrap().0.number().unwrap() ); - let VerifyBlockResponse::MainChainBatchPrepped(prepped) = self + let ban_cancel_download = || { + batch.peer_handle.ban_peer(LONG_BAN); + self.stop_current_block_downloader.notify_one(); + }; + + let batch_prep_res = self .block_verifier_service .ready() .await @@ -90,21 +114,33 @@ impl super::BlockchainManager { .call(VerifyBlockRequest::MainChainBatchPrepareBlocks { blocks: batch.blocks, }) - .await? - else { - panic!("Incorrect response!"); + .await; + + let prepped_blocks = match batch_prep_res { + Ok(VerifyBlockResponse::MainChainBatchPrepped(prepped_blocks)) => prepped_blocks, + Err(_) => { + ban_cancel_download(); + return; + } + _ => panic!("Incorrect response!"), }; - for (block, txs) in prepped { - let VerifyBlockResponse::MainChain(verified_block) = self + for (block, txs) in prepped_blocks { + let verify_res = self .block_verifier_service .ready() .await .expect("TODO") .call(VerifyBlockRequest::MainChainPrepped { block, txs }) - .await? - else { - panic!("Incorrect response!"); + .await; + + let VerifyBlockResponse::MainChain(verified_block) = match verify_res { + Ok(VerifyBlockResponse::MainChain(verified_block)) => verified_block, + Err(_) => { + ban_cancel_download(); + return; + } + _ => panic!("Incorrect response!"), }; self.add_valid_block_to_main_chain(verified_block).await; @@ -113,25 +149,60 @@ impl super::BlockchainManager { Ok(()) } - async fn handle_incoming_block_batch_alt_chain( - &mut self, - batch: BlockBatch, - ) -> Result<(), anyhow::Error> { + /// Handles an incoming [`BlockBatch`] that does not follow the main-chain. + /// + /// This function will handle validating the alt-blocks to add them to our cache and reorging the + /// chain if the alt-chain has a higher cumulative difficulty. + /// + /// This function will also handle banning the peer and canceling the block downloader if the + /// alt block is invalid or if a reorg fails. + /// + /// # Panics + /// + /// This function will panic if any internal service returns an unexpected error that we cannot + /// recover from. + async fn handle_incoming_block_batch_alt_chain(&mut self, batch: BlockBatch) { for (block, txs) in batch.blocks { - let txs = txs - .into_par_iter() - .map(|tx| { - let tx = new_tx_verification_data(tx)?; - Ok((tx.tx_hash, tx)) - }) - .collect::>()?; + // async blocks work as try blocks. + let res = async { + let txs = txs + .into_par_iter() + .map(|tx| { + let tx = new_tx_verification_data(tx)?; + Ok((tx.tx_hash, tx)) + }) + .collect::>()?; - self.handle_incoming_alt_block(block, txs).await?; + self.handle_incoming_alt_block(block, txs).await?; + + Ok(()) + } + .await; + + if let Err(e) = res { + batch.peer_handle.ban_peer(LONG_BAN); + self.stop_current_block_downloader.notify_one(); + return; + } } - - Ok(()) } + /// Handles an incoming alt [`Block`]. + /// + /// This function will do some pre-validation of the alt block, then if the cumulative difficulty + /// of the alt chain is higher than the main chain it will attempt a reorg otherwise it will add + /// the alt block to the alt block cache. + /// + /// # Errors + /// + /// This will return an [`Err`] if: + /// - The alt block was invalid. + /// - An attempt to reorg the chain failed. + /// + /// # Panics + /// + /// This function will panic if any internal service returns an unexpected error that we cannot + /// recover from. pub async fn handle_incoming_alt_block( &mut self, block: Block, @@ -157,8 +228,6 @@ impl super::BlockchainManager { > self.cached_blockchain_context.cumulative_difficulty { self.try_do_reorg(alt_block_info).await?; - // TODO: ban the peer if the reorg failed. - return Ok(()); } @@ -172,6 +241,21 @@ impl super::BlockchainManager { Ok(()) } + /// Attempt a re-org with the given top block of the alt-chain. + /// + /// This function will take a write lock on [`REORG_LOCK`] and then set up the blockchain database + /// and context cache to verify the alt-chain. It will then attempt to verify and add each block + /// in the alt-chain to tha main-chain. Releasing the lock on [`REORG_LOCK`] when finished. + /// + /// # Errors + /// + /// This function will return an [`Err`] if the re-org was unsuccessful, if this happens the chain + /// will be returned back into its state it was at when then function was called. + /// + /// # Panics + /// + /// This function will panic if any internal service returns an unexpected error that we cannot + /// recover from. async fn try_do_reorg( &mut self, top_alt_block: AltBlockInformation, @@ -230,6 +314,21 @@ impl super::BlockchainManager { } } + /// Verify and add a list of [`AltBlockInformation`]s to the main-chain. + /// + /// This function assumes the first [`AltBlockInformation`] is the next block in the blockchain + /// for the blockchain database and the context cache, or in other words that the blockchain database + /// and context cache has had the top blocks popped to where the alt-chain meets the main-chain. + /// + /// # Errors + /// + /// This function will return an [`Err`] if the alt-blocks were invalid, in this case the re-org should + /// be aborted and the chain should be returned to its previous state. + /// + /// # Panics + /// + /// This function will panic if any internal service returns an unexpected error that we cannot + /// recover from. async fn verify_add_alt_blocks_to_main_chain( &mut self, alt_blocks: Vec, @@ -263,6 +362,15 @@ impl super::BlockchainManager { Ok(()) } + /// Adds a [`VerifiedBlockInformation`] to the main-chain. + /// + /// This function will update the blockchain database and the context cache, it will also + /// update [`Self::cached_blockchain_context`]. + /// + /// # Panics + /// + /// This function will panic if any internal service returns an unexpected error that we cannot + /// recover from. pub async fn add_valid_block_to_main_chain( &mut self, verified_block: VerifiedBlockInformation, diff --git a/binaries/cuprated/src/blockchain/syncer.rs b/binaries/cuprated/src/blockchain/syncer.rs index dc738123..fbf4a88f 100644 --- a/binaries/cuprated/src/blockchain/syncer.rs +++ b/binaries/cuprated/src/blockchain/syncer.rs @@ -1,7 +1,11 @@ +use std::pin::pin; use std::time::Duration; use futures::StreamExt; -use tokio::{sync::mpsc, time::sleep}; +use tokio::{ + sync::{mpsc, Notify}, + time::sleep, +}; use tower::{Service, ServiceExt}; use tracing::instrument; @@ -27,6 +31,7 @@ pub async fn syncer( our_chain: CN, clearnet_interface: NetworkInterface, incoming_block_batch_tx: mpsc::Sender, + stop_current_block_downloader: Notify, block_downloader_config: BlockDownloaderConfig, ) -> Result<(), SyncerError> where @@ -82,10 +87,18 @@ where let mut block_batch_stream = clearnet_interface.block_downloader(our_chain.clone(), block_downloader_config); - while let Some(batch) = block_batch_stream.next().await { - tracing::debug!("Got batch, len: {}", batch.blocks.len()); - if incoming_block_batch_tx.send(batch).await.is_err() { - return Err(SyncerError::IncomingBlockChannelClosed); + loop { + tokio::select! { + _ = stop_current_block_downloader.notified() => { + tracing::info!("Stopping block downloader"); + break; + } + Some(batch) = block_batch_stream.next() => { + tracing::debug!("Got batch, len: {}", batch.blocks.len()); + if incoming_block_batch_tx.send(batch).await.is_err() { + return Err(SyncerError::IncomingBlockChannelClosed); + } + } } } } diff --git a/p2p/p2p/src/constants.rs b/p2p/p2p/src/constants.rs index 4e6daa73..0dbd188e 100644 --- a/p2p/p2p/src/constants.rs +++ b/p2p/p2p/src/constants.rs @@ -10,13 +10,13 @@ pub(crate) const MAX_SEED_CONNECTIONS: usize = 3; pub(crate) const OUTBOUND_CONNECTION_ATTEMPT_TIMEOUT: Duration = Duration::from_secs(5); /// The durations of a short ban. -pub(crate) const SHORT_BAN: Duration = Duration::from_secs(60 * 10); +pub const SHORT_BAN: Duration = Duration::from_secs(60 * 10); /// The durations of a medium ban. -pub(crate) const MEDIUM_BAN: Duration = Duration::from_secs(60 * 60 * 24); +pub const MEDIUM_BAN: Duration = Duration::from_secs(60 * 60 * 24); /// The durations of a long ban. -pub(crate) const LONG_BAN: Duration = Duration::from_secs(60 * 60 * 24 * 7); +pub const LONG_BAN: Duration = Duration::from_secs(60 * 60 * 24 * 7); /// The default amount of time between inbound diffusion flushes. pub(crate) const DIFFUSION_FLUSH_AVERAGE_SECONDS_INBOUND: Duration = Duration::from_secs(5); diff --git a/p2p_state.bin b/p2p_state.bin index fc17e050f72079bd7b70ecf81a388975afba2996..9faaaff02d13da96127ed10c9c77a8ab0a7b45ff 100644 GIT binary patch delta 4976 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 H2aEm>=pFwT delta 4415 zcmZ`+c|4T)ANM>n;|LEqXGR_wnQ|q^+IF=?^c^W;*$U;TR&WUXv^G}1@66iCYxebL+1|C$K9pNSQ2Y zUx&kkR(w$qV-*H#IHc$~(-()y)}-*~9E>lDR+xjqd@B;RE1rg1`Fwa(i4Wfy@I~!P zGckBkmxRX3bD)AU38R%C!6<7|v_mC_1uLxep|i$b7^}sHFE#E#0}VdBuUQ7mHA#3; z>k$mFCSkVrc_>oiGmYv>KMU3V!nmSX9XlxB-MYT;>Qqt`#75@5 zwnFA{2zh9)Ktk=Q=LwI;{@+lb`^l9RPAM)gL=98zFeoxZHt|y7XG0x^TrPl5cxz8w zMFNbF>>3^^%AR%}gLN7tCu&TmjRJ{FzFQF1=y_c3%MYI98^H#o>pnk`0n7QMDB9E( zgF&XG=&o5A3)V16W}LO|kFZ(DSrVs-7>7nX?luXUFY^t}Viy1SMbbai6Z>s$6u8s+ zUfFtRjh8HXg_&5D1-J!+_D4SGJcK3Rv6(Ge=20;D5)V4gBVEG|2wW*}ru2iy;Bp%u zS#BHWmaiyKp;_>-y8f>=Sroa5+3|#WR0vI;uy3k}l`>CeC)H z(r4o)-Fn4or@&ZA>hi3pno40&63%8$zqP?p7_bEA@@8A6vqA(j7UyiXpBBQG@`ef| z;8k({{EFMHOwqgnL(GY@R^RbWJ8OInYM@$!<%P>KiB84n3}bZY0lj z3VPNg2#MYn>1=MGCHw57L3%R<{52DO)TVWWO3`tLJCP+!L6ZH(JufeHw8Q5g+2$eZ z*RG_1VLH>-?%uZ!QigGYIV}ODwD5dcqMt_fGFYX=Hg}eoxopkdeWL?=4#_g)-FlAP zFMPx{HrgDfEq0%I@#x$*p|Uc@C3dOit)Rfx>B493>!y@Sq&=A(P)Gsxi-Dz~d6sHY zY2)@TJQBu6C1~};$vfQFA3lGI?A?!NBN;3P-8JJux-5gKAhL!6$J7t43~YY$6y8L45u zVLaw>dmT4eHJ$5FD(TXAqd6m*b8P<;P*5=U3I#lseLT)DU<}D>SmxY8!L$QB^Jldq zB4p+a#lp_sfo9ofi%`y(0*AU}(YhH1p5nX{KS;FBd`1or-uu&9JR67`!PUMLI5GOz zZm;FGT1n}CrTG=A(|d~cjmtmm;)*?Rbx&1aK!K>-IIwNxbh%X1c*ZRDmS<7G`Jm4? zeXaLTNYV6vW}XJn2%^BA+Rl0B z>ITZhStnLS>kLuA^{Lj;!=|%2D6eeZ12^%Um>xd0Uh#W+ z31E6U7iP{Sg_Z#rNfdNje4;?_nL3r-`K}S-Dr7!X+UE}qF>Z9Icf@ZLpk9?02APJ9 z$_h<&e3TI)DCK3}oW038N*1-PTm2K-7;XEzl5ah7lA_~*VN2$?-}EEz=HD9%mZIat zhd=ffqo68OZSdBdIj~UZsfuwHq6{?&Ce1$Wqq2SO23d~0V5OG-U(#wQsT*gy zro2rN-mOOSWk{WKkpeGN!!19U5o=`St$g-v1l)LyYaWOUWeQ&3%rU#VX#OVIXU2M> z9ts@)b&>jP{o+z#kQ=ILjhspY=o!w9?#Le=p@5e|R`h$fGqYq_(~&>3Tr$4i_VtCP z#NTAm)qTgVQNX)zxb%nTRsY#@X`OEa|9=;HAjz2^&$y`aSS zeB*B0#qch{X8xu0*jSjZi*a2yO8S7&yU6~|n!W$Aid`{EQb3;JRT)08>Z+{Z3n9h`JMu2PX?UalhJiwR>sUv{eBb} zQk-(I%`v@LijFtTveo)%L;bvYSKs=N8+xVaIN{K+;SXW68mikquU8=yu!z5XR^A|a zhwQs!^*621vK*7o`RIEmQ;LpPruMqV;}l?mV?EsUDqj*lZozVuefboaBWlu!S$dWs z&m@Ix|~g4<~YC7Vq;gCw)@ zmrU<^3Iy!v+PbMKT_jluiFq~|@NFws)ZnmzAk?#ZFHm3;XYq(p;3rb#v}Bzk`*HXd zbW9z7(D-P0%Ekbx;#|?KWxkU*?R+1!MiO7FXm#Qqoc09;B9~Qvs}UJ!vSfjGdB?(% z403EmUSalZ_BV%hDNvSO+t_R_jGf3)7{8WDl#XejVeR*zSZVgL^}EEQRAh6kUYUps zulJkf;vTotZIw=6rAQ(94FOr`B-YN0A(Z#WLJB-DADotbsnAZ8nYWM0{P=ZQESyYo zMT?4hj5#y)+X5&c*Ar!^ncD6sRVpVZa&Ju_h@hr19}2WZ8tFO5#!St8{gTDm?s&}_ zxvjymI_}Pko5-Qqy$s{9(k*8ncBRh0xlv!EdPxe$xr$s?*Mm-|9a4(_%wqaV9tS}dk* zZDWMc;5Nn;s(NE_g#WCeBNV`s>Duphsl|(zAuaO8925vQNjNx8cX(m`Z@h%q3ao`A zzAi!pCw`A@wrx2xF{5<<3d{&6$|UnIzPa<)$-UnmlI-6QAMwetK0oyoM}y8!!@^WKO8xdY zIy={=reEv|(x1p^cM)5tL7YR3nLz95%w5_$`gYJqE3q&mPOIT%R8Bo!&b<+{%qGq= zn*9=c%_5c#uU<|8_fIO5OhH7sODk)?eAomFh5m|oC+Cf3GTJlu z-_}~2$6u_O0Mq_TxRVCQP~!PWb%WuW)`Y@|_!T{TvOF=oR0Zvjix#gRIw@Mt5g&(i zw;?`5iJ12$LqGwB264hRW9R^FV1`#K5al<9ucJmOF%8)2cE3XMWkoC4;7{1ZVl?*}#8U;o7l;V6;pS5aoy0V#E;?woGvPN3El|Hx6E-m#oCkGKjs?EzmcF zgEJ7OymK26Z)92_2#-`H^zyEvTA6gv)^B%ww@|!K=#)+PBNlW-b^3Dz9!wI~kO`+; znCl&)27jA$6ncJ-x)$Q8iku}W)o9=L?kw1D96tC#8ZA_3ZOJ$bBSsJ-1*~3;*4fS3 p_?NKv9Nq+-*yM%Q6r3fbQ*aV42qT3IM~oD{T!fRtk(0Rb{{W@ru>=4B