diff --git a/zebrad/src/components/inbound.rs b/zebrad/src/components/inbound.rs index f99f58890..afc2fa954 100644 --- a/zebrad/src/components/inbound.rs +++ b/zebrad/src/components/inbound.rs @@ -237,6 +237,10 @@ impl Service for Inbound { Poll::Ready(result) } + /// Call the inbound service. + /// + /// Errors indicate that the peer has done something wrong or unexpected, + /// and will cause callers to disconnect from the remote peer. #[instrument(name = "inbound", skip(self, req))] fn call(&mut self, req: zn::Request) -> Self::Future { match req { diff --git a/zebrad/src/components/inbound/tests.rs b/zebrad/src/components/inbound/tests.rs index 91a84583b..41169df2c 100644 --- a/zebrad/src/components/inbound/tests.rs +++ b/zebrad/src/components/inbound/tests.rs @@ -6,7 +6,8 @@ use crate::components::sync::SyncStatus; use futures::FutureExt; use tokio::sync::oneshot; use tower::{ - buffer::Buffer, builder::ServiceBuilder, load_shed::LoadShed, util::BoxService, ServiceExt, + buffer::Buffer, builder::ServiceBuilder, load_shed::LoadShed, util::BoxService, Service, + ServiceExt, }; use tracing::Span; @@ -34,15 +35,16 @@ async fn mempool_requests_for_transactions() { .collect(); // Test `Request::MempoolTransactionIds` - let request = inbound_service + let response = inbound_service .clone() .oneshot(Request::MempoolTransactionIds) .await; - match request { + match response { Ok(Response::TransactionIds(response)) => assert_eq!(response, added_transaction_ids), - _ => unreachable!( - "`MempoolTransactionIds` requests should always respond `Ok(Vec)`" - ), + _ => unreachable!(format!( + "`MempoolTransactionIds` requests should always respond `Ok(Vec)`, got {:?}", + response + )), }; // Test `Request::TransactionsById` @@ -51,11 +53,11 @@ async fn mempool_requests_for_transactions() { .copied() .collect::>(); - let request = inbound_service + let response = inbound_service .oneshot(Request::TransactionsById(hash_set)) .await; - match request { + match response { Ok(Response::Transactions(response)) => assert_eq!(response, added_transactions.unwrap()), _ => unreachable!("`TransactionsById` requests should always respond `Ok(Vec)`"), }; @@ -184,12 +186,11 @@ async fn setup( let state_config = StateConfig::ephemeral(); let address_book = AddressBook::new(SocketAddr::from_str("0.0.0.0:0").unwrap(), Span::none()); let address_book = Arc::new(std::sync::Mutex::new(address_book)); - let (sync_status, _recent_syncs) = SyncStatus::new(); - let (_state_service, _latest_chain_tip, chain_tip_change) = + let (sync_status, mut recent_syncs) = SyncStatus::new(); + let (state, _latest_chain_tip, chain_tip_change) = zebra_state::init(state_config.clone(), network); - let (state, _, _) = zebra_state::init(state_config, network); - let state_service = ServiceBuilder::new().buffer(1).service(state); + let mut state_service = ServiceBuilder::new().buffer(1).service(state); let (block_verifier, _transaction_verifier) = zebra_consensus::chain::init(consensus_config.clone(), network, state_service.clone()) @@ -201,6 +202,22 @@ async fn setup( let mock_tx_verifier = MockService::build().for_unit_tests(); let buffered_tx_verifier = Buffer::new(BoxService::new(mock_tx_verifier.clone()), 10); + // Push the genesis block to the state. + // This must be done before creating the mempool to avoid `chain_tip_change` + // returning "reset" which would clear the mempool. + let genesis_block: Arc = zebra_test::vectors::BLOCK_MAINNET_GENESIS_BYTES + .zcash_deserialize_into() + .unwrap(); + state_service + .ready_and() + .await + .unwrap() + .call(zebra_state::Request::CommitFinalizedBlock( + genesis_block.clone().into(), + )) + .await + .unwrap(); + let mut mempool_service = Mempool::new( network, buffered_peer_set.clone(), @@ -210,6 +227,9 @@ async fn setup( chain_tip_change, ); + // Enable the mempool + let _ = mempool_service.enable(&mut recent_syncs).await; + let mut added_transactions = None; if add_transactions { added_transactions = Some(add_some_stuff_to_mempool(&mut mempool_service, network)); @@ -233,17 +253,6 @@ async fn setup( // We can't expect or unwrap because the returned Result does not implement Debug assert!(r.is_ok()); - // Push the genesis block to the state - let genesis_block: Arc = zebra_test::vectors::BLOCK_MAINNET_GENESIS_BYTES - .zcash_deserialize_into() - .unwrap(); - state_service - .oneshot(zebra_state::Request::CommitFinalizedBlock( - genesis_block.clone().into(), - )) - .await - .unwrap(); - ( inbound_service, added_transactions, diff --git a/zebrad/src/components/mempool.rs b/zebrad/src/components/mempool.rs index 884aea93b..0787e1d7c 100644 --- a/zebrad/src/components/mempool.rs +++ b/zebrad/src/components/mempool.rs @@ -3,6 +3,7 @@ use std::{ collections::HashSet, future::Future, + iter, pin::Pin, task::{Context, Poll}, }; @@ -38,6 +39,8 @@ use self::downloads::{ Downloads as TxDownloads, Gossip, TRANSACTION_DOWNLOAD_TIMEOUT, TRANSACTION_VERIFY_TIMEOUT, }; +#[cfg(test)] +use super::sync::RecentSyncLengths; use super::sync::SyncStatus; type Outbound = Buffer, zn::Request>; @@ -65,20 +68,33 @@ pub enum Response { Queued(Vec>), } +/// The state of the mempool. +/// +/// Indicates wether it is enabled or disabled and, if enabled, contains +/// the necessary data to run it. +enum ActiveState { + /// The Mempool is disabled. + Disabled, + /// The Mempool is enabled. + Enabled { + /// The Mempool storage itself. + /// + /// ##: Correctness: only components internal to the [`Mempool`] struct are allowed to + /// inject transactions into `storage`, as transactions must be verified beforehand. + storage: storage::Storage, + /// The transaction dowload and verify stream. + tx_downloads: Pin>, + }, +} + /// Mempool async management and query service. /// /// The mempool is the set of all verified transactions that this node is aware /// of that have yet to be confirmed by the Zcash network. A transaction is /// confirmed when it has been included in a block ('mined'). pub struct Mempool { - /// The Mempool storage itself. - /// - /// ##: Correctness: only components internal to the [`Mempool`] struct are allowed to - /// inject transactions into `storage`, as transactions must be verified beforehand. - storage: storage::Storage, - - /// The transaction dowload and verify stream. - tx_downloads: Pin>, + /// The state of the mempool. + active_state: ActiveState, /// Allows checking if we are near the tip to enable/disable the mempool. #[allow(dead_code)] @@ -87,6 +103,18 @@ pub struct Mempool { /// Allows the detection of chain tip resets. #[allow(dead_code)] chain_tip_change: ChainTipChange, + + /// Handle to the outbound service. + /// Used to construct the transaction downloader. + outbound: Outbound, + + /// Handle to the state service. + /// Used to construct the transaction downloader. + state: State, + + /// Handle to the transaction verifier service. + /// Used to construct the transaction downloader. + tx_verifier: TxVerifier, } impl Mempool { @@ -99,36 +127,100 @@ impl Mempool { sync_status: SyncStatus, chain_tip_change: ChainTipChange, ) -> Self { - let tx_downloads = Box::pin(TxDownloads::new( - Timeout::new(outbound, TRANSACTION_DOWNLOAD_TIMEOUT), - Timeout::new(tx_verifier, TRANSACTION_VERIFY_TIMEOUT), - state, - )); - Mempool { - storage: Default::default(), - tx_downloads, + active_state: ActiveState::Disabled, sync_status, chain_tip_change, + outbound, + state, + tx_verifier, + } + } + + /// Update the mempool state (enabled / disabled) depending on how close to + /// the tip is the synchronization, including side effects to state changes. + fn update_state(&mut self) { + let is_close_to_tip = self.sync_status.is_close_to_tip(); + if self.is_enabled() == is_close_to_tip { + // the active state is up to date + return; + } + + // Update enabled / disabled state + if is_close_to_tip { + let tx_downloads = Box::pin(TxDownloads::new( + Timeout::new(self.outbound.clone(), TRANSACTION_DOWNLOAD_TIMEOUT), + Timeout::new(self.tx_verifier.clone(), TRANSACTION_VERIFY_TIMEOUT), + self.state.clone(), + )); + self.active_state = ActiveState::Enabled { + storage: Default::default(), + tx_downloads, + }; + } else { + self.active_state = ActiveState::Disabled + } + } + + /// Return whether the mempool is enabled or not. + pub fn is_enabled(&self) -> bool { + match self.active_state { + ActiveState::Disabled => false, + ActiveState::Enabled { .. } => true, } } /// Get the storage field of the mempool for testing purposes. #[cfg(test)] pub fn storage(&mut self) -> &mut storage::Storage { - &mut self.storage + match &mut self.active_state { + ActiveState::Disabled => panic!("mempool must be enabled"), + ActiveState::Enabled { storage, .. } => storage, + } + } + + /// Get the transaction downloader of the mempool for testing purposes. + #[cfg(test)] + pub fn tx_downloads(&self) -> &Pin> { + match &self.active_state { + ActiveState::Disabled => panic!("mempool must be enabled"), + ActiveState::Enabled { tx_downloads, .. } => tx_downloads, + } + } + + /// Enable the mempool by pretending the synchronization is close to the tip. + #[cfg(test)] + pub async fn enable(&mut self, recent_syncs: &mut RecentSyncLengths) { + use tower::ServiceExt; + // Pretend we're close to tip + SyncStatus::sync_close_to_tip(recent_syncs); + // Make a dummy request to poll the mempool and make it enable itself + let _ = self.oneshot(Request::TransactionIds).await; + } + + /// Disable the mempool by pretending the synchronization is far from the tip. + #[cfg(test)] + pub async fn disable(&mut self, recent_syncs: &mut RecentSyncLengths) { + use tower::ServiceExt; + // Pretend we're far from the tip + SyncStatus::sync_far_from_tip(recent_syncs); + // Make a dummy request to poll the mempool and make it disable itself + let _ = self.oneshot(Request::TransactionIds).await; } /// Check if transaction should be downloaded and/or verified. /// /// If it is already in the mempool (or in its rejected list) /// then it shouldn't be downloaded/verified. - fn should_download_or_verify(&mut self, txid: UnminedTxId) -> Result<(), MempoolError> { + fn should_download_or_verify( + storage: &mut storage::Storage, + txid: UnminedTxId, + ) -> Result<(), MempoolError> { // Check if the transaction is already in the mempool. - if self.storage.contains(&txid) { + if storage.contains(&txid) { return Err(MempoolError::InMempool); } - if self.storage.contains_rejected(&txid) { + if storage.contains_rejected(&txid) { return Err(MempoolError::Rejected); } Ok(()) @@ -142,48 +234,90 @@ impl Service for Mempool { Pin> + Send + 'static>>; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - // Clear the mempool if there has been a chain tip reset. - if let Some(TipAction::Reset { .. }) = self.chain_tip_change.last_tip_change() { - self.storage.clear(); - } + self.update_state(); - // Clean up completed download tasks and add to mempool if successful - while let Poll::Ready(Some(r)) = self.tx_downloads.as_mut().poll_next(cx) { - if let Ok(tx) = r { - // TODO: should we do something with the result? - let _ = self.storage.insert(tx); + match &mut self.active_state { + ActiveState::Enabled { + storage, + tx_downloads, + } => { + // Clear the mempool if there has been a chain tip reset. + if let Some(TipAction::Reset { .. }) = self.chain_tip_change.last_tip_change() { + storage.clear(); + } + + // Clean up completed download tasks and add to mempool if successful + while let Poll::Ready(Some(r)) = tx_downloads.as_mut().poll_next(cx) { + if let Ok(tx) = r { + // Storage handles conflicting transactions or a full mempool internally, + // so just ignore the storage result here + let _ = storage.insert(tx); + } + } + } + ActiveState::Disabled => { + // When the mempool is disabled we still return that the service is ready. + // Otherwise, callers could block waiting for the mempool to be enabled, + // which may not be the desired behaviour. } } Poll::Ready(Ok(())) } + /// Call the mempool service. + /// + /// Errors indicate that the peer has done something wrong or unexpected, + /// and will cause callers to disconnect from the remote peer. #[instrument(name = "mempool", skip(self, req))] fn call(&mut self, req: Request) -> Self::Future { - match req { - Request::TransactionIds => { - let res = self.storage.tx_ids(); - async move { Ok(Response::TransactionIds(res)) }.boxed() - } - Request::TransactionsById(ids) => { - let rsp = Ok(self.storage.transactions(ids)).map(Response::Transactions); - async move { rsp }.boxed() - } - Request::RejectedTransactionIds(ids) => { - let rsp = Ok(self.storage.rejected_transactions(ids)) - .map(Response::RejectedTransactionIds); - async move { rsp }.boxed() - } - Request::Queue(gossiped_txs) => { - let rsp: Vec> = gossiped_txs - .into_iter() - .map(|gossiped_tx| { - self.should_download_or_verify(gossiped_tx.id())?; - self.tx_downloads - .download_if_needed_and_verify(gossiped_tx)?; - Ok(()) - }) - .collect(); - async move { Ok(Response::Queued(rsp)) }.boxed() + match &mut self.active_state { + ActiveState::Enabled { + storage, + tx_downloads, + } => match req { + Request::TransactionIds => { + let res = storage.tx_ids(); + async move { Ok(Response::TransactionIds(res)) }.boxed() + } + Request::TransactionsById(ids) => { + let rsp = Ok(storage.transactions(ids)).map(Response::Transactions); + async move { rsp }.boxed() + } + Request::RejectedTransactionIds(ids) => { + let rsp = Ok(storage.rejected_transactions(ids)) + .map(Response::RejectedTransactionIds); + async move { rsp }.boxed() + } + Request::Queue(gossiped_txs) => { + let rsp: Vec> = gossiped_txs + .into_iter() + .map(|gossiped_tx| { + Self::should_download_or_verify(storage, gossiped_tx.id())?; + tx_downloads.download_if_needed_and_verify(gossiped_tx)?; + Ok(()) + }) + .collect(); + async move { Ok(Response::Queued(rsp)) }.boxed() + } + }, + ActiveState::Disabled => { + // We can't return an error since that will cause a disconnection + // by the peer connection handler. Therefore, return successful + // empty responses. + let resp = match req { + Request::TransactionIds => Response::TransactionIds(Default::default()), + Request::TransactionsById(_) => Response::Transactions(Default::default()), + Request::RejectedTransactionIds(_) => { + Response::RejectedTransactionIds(Default::default()) + } + // Special case; we can signal the error inside the response. + Request::Queue(gossiped_txs) => Response::Queued( + iter::repeat(Err(MempoolError::Disabled)) + .take(gossiped_txs.len()) + .collect(), + ), + }; + async move { Ok(resp) }.boxed() } } } diff --git a/zebrad/src/components/mempool/downloads.rs b/zebrad/src/components/mempool/downloads.rs index 4c7efcdb5..446369885 100644 --- a/zebrad/src/components/mempool/downloads.rs +++ b/zebrad/src/components/mempool/downloads.rs @@ -315,6 +315,13 @@ where Ok(()) } + /// Get the number of currently in-flight download tasks. + // Note: copied from zebrad/src/components/sync/downloads.rs + #[allow(dead_code)] + pub fn in_flight(&self) -> usize { + self.pending.len() + } + /// Check if transaction is already in the state. async fn transaction_in_state(state: &mut ZS, txid: UnminedTxId) -> Result<(), BoxError> { // Check if the transaction is already in the state. diff --git a/zebrad/src/components/mempool/error.rs b/zebrad/src/components/mempool/error.rs index c700ced0f..9ac91ba46 100644 --- a/zebrad/src/components/mempool/error.rs +++ b/zebrad/src/components/mempool/error.rs @@ -50,4 +50,7 @@ pub enum MempoolError { its inputs" )] SpendConflict, + + #[error("mempool is disabled since synchronization is behind the chain tip")] + Disabled, } diff --git a/zebrad/src/components/mempool/tests.rs b/zebrad/src/components/mempool/tests.rs index 091557d07..b79659002 100644 --- a/zebrad/src/components/mempool/tests.rs +++ b/zebrad/src/components/mempool/tests.rs @@ -15,7 +15,7 @@ async fn mempool_service_basic() -> Result<(), Report> { let consensus_config = ConsensusConfig::default(); let state_config = StateConfig::ephemeral(); let peer_set = MockService::build().for_unit_tests(); - let (sync_status, _recent_syncs) = SyncStatus::new(); + let (sync_status, mut recent_syncs) = SyncStatus::new(); let (state, _latest_chain_tip, chain_tip_change) = zebra_state::init(state_config.clone(), network); @@ -29,8 +29,8 @@ async fn mempool_service_basic() -> Result<(), Report> { let genesis_transaction = unmined_transactions .next() .expect("Missing genesis transaction"); - let mut more_transactions = unmined_transactions; - let last_transaction = more_transactions.next_back().unwrap(); + let txid = unmined_transactions.next_back().unwrap().id; + let more_transactions = unmined_transactions; // Start the mempool service let mut service = Mempool::new( @@ -41,8 +41,12 @@ async fn mempool_service_basic() -> Result<(), Report> { sync_status, chain_tip_change, ); + + // Enable the mempool + let _ = service.enable(&mut recent_syncs).await; + // Insert the genesis block coinbase transaction into the mempool storage. - service.storage.insert(genesis_transaction.clone())?; + service.storage().insert(genesis_transaction.clone())?; // Test `Request::TransactionIds` let response = service @@ -84,7 +88,7 @@ async fn mempool_service_basic() -> Result<(), Report> { // This will cause the genesis transaction to be moved into rejected. // Skip the last (will be used later) for tx in more_transactions { - service.storage.insert(tx.clone())?; + service.storage().insert(tx.clone())?; } // Test `Request::RejectedTransactionIds` @@ -110,7 +114,7 @@ async fn mempool_service_basic() -> Result<(), Report> { .ready_and() .await .unwrap() - .call(Request::Queue(vec![last_transaction.id.into()])) + .call(Request::Queue(vec![txid.into()])) .await .unwrap(); let queued_responses = match response { @@ -119,6 +123,7 @@ async fn mempool_service_basic() -> Result<(), Report> { }; assert_eq!(queued_responses.len(), 1); assert!(queued_responses[0].is_ok()); + assert_eq!(service.tx_downloads().in_flight(), 1); Ok(()) } @@ -130,7 +135,7 @@ async fn mempool_queue() -> Result<(), Report> { let consensus_config = ConsensusConfig::default(); let state_config = StateConfig::ephemeral(); let peer_set = MockService::build().for_unit_tests(); - let (sync_status, _recent_syncs) = SyncStatus::new(); + let (sync_status, mut recent_syncs) = SyncStatus::new(); let (state, _latest_chain_tip, chain_tip_change) = zebra_state::init(state_config.clone(), network); @@ -162,15 +167,19 @@ async fn mempool_queue() -> Result<(), Report> { sync_status, chain_tip_change, ); + + // Enable the mempool + let _ = service.enable(&mut recent_syncs).await; + // Insert [rejected_tx, transactions..., stored_tx] into the mempool storage. // Insert the genesis block coinbase transaction into the mempool storage. - service.storage.insert(rejected_tx.clone())?; + service.storage().insert(rejected_tx.clone())?; // Insert more transactions into the mempool storage. // This will cause the `rejected_tx` to be moved into rejected. for tx in transactions { - service.storage.insert(tx.clone())?; + service.storage().insert(tx.clone())?; } - service.storage.insert(stored_tx.clone())?; + service.storage().insert(stored_tx.clone())?; // Test `Request::Queue` for a new transaction let response = service @@ -219,3 +228,120 @@ async fn mempool_queue() -> Result<(), Report> { Ok(()) } + +#[tokio::test] +async fn mempool_service_disabled() -> Result<(), Report> { + // Using the mainnet for now + let network = Network::Mainnet; + let consensus_config = ConsensusConfig::default(); + let state_config = StateConfig::ephemeral(); + let peer_set = MockService::build().for_unit_tests(); + let (sync_status, mut recent_syncs) = SyncStatus::new(); + + let (state, _latest_chain_tip, chain_tip_change) = zebra_state::init(state_config, network); + let state_service = ServiceBuilder::new().buffer(1).service(state); + let (_chain_verifier, tx_verifier) = + zebra_consensus::chain::init(consensus_config.clone(), network, state_service.clone()) + .await; + + // get the genesis block transactions from the Zcash blockchain. + let mut unmined_transactions = unmined_transactions_in_blocks(..=10, network); + let genesis_transaction = unmined_transactions + .next() + .expect("Missing genesis transaction"); + let more_transactions = unmined_transactions; + + // Start the mempool service + let mut service = Mempool::new( + network, + Buffer::new(BoxService::new(peer_set), 1), + state_service.clone(), + tx_verifier, + sync_status, + chain_tip_change, + ); + + // Test if mempool is disabled (it should start disabled) + assert!(!service.is_enabled()); + + // Enable the mempool + let _ = service.enable(&mut recent_syncs).await; + + assert!(service.is_enabled()); + + // Insert the genesis block coinbase transaction into the mempool storage. + service.storage().insert(genesis_transaction.clone())?; + + // Test if the mempool answers correctly (i.e. is enabled) + let response = service + .ready_and() + .await + .unwrap() + .call(Request::TransactionIds) + .await + .unwrap(); + let _genesis_transaction_ids = match response { + Response::TransactionIds(ids) => ids, + _ => unreachable!("will never happen in this test"), + }; + + // Queue a transaction for download + // Use the ID of the last transaction in the list + let txid = more_transactions.last().unwrap().id; + let response = service + .ready_and() + .await + .unwrap() + .call(Request::Queue(vec![txid.into()])) + .await + .unwrap(); + let queued_responses = match response { + Response::Queued(queue_responses) => queue_responses, + _ => unreachable!("will never happen in this test"), + }; + assert_eq!(queued_responses.len(), 1); + assert!(queued_responses[0].is_ok()); + assert_eq!(service.tx_downloads().in_flight(), 1); + + // Disable the mempool + let _ = service.disable(&mut recent_syncs).await; + + // Test if mempool is disabled again + assert!(!service.is_enabled()); + + // Test if the mempool returns no transactions when disabled + let response = service + .ready_and() + .await + .unwrap() + .call(Request::TransactionIds) + .await + .unwrap(); + match response { + Response::TransactionIds(ids) => { + assert_eq!( + ids.len(), + 0, + "mempool should return no transactions when disabled" + ) + } + _ => unreachable!("will never happen in this test"), + }; + + // Test if the mempool returns to Queue requests correctly when disabled + let response = service + .ready_and() + .await + .unwrap() + .call(Request::Queue(vec![txid.into()])) + .await + .unwrap(); + let queued_responses = match response { + Response::Queued(queue_responses) => queue_responses, + _ => unreachable!("will never happen in this test"), + }; + assert_eq!(queued_responses.len(), 1); + assert_eq!(queued_responses[0], Err(MempoolError::Disabled)); + + Ok(()) +} diff --git a/zebrad/src/components/sync.rs b/zebrad/src/components/sync.rs index 67f04757d..435d1ea0a 100644 --- a/zebrad/src/components/sync.rs +++ b/zebrad/src/components/sync.rs @@ -28,7 +28,7 @@ mod status; mod tests; use downloads::{AlwaysHedge, Downloads}; -use recent_sync_lengths::RecentSyncLengths; +pub use recent_sync_lengths::RecentSyncLengths; pub use status::SyncStatus; /// Controls the number of peers used for each ObtainTips and ExtendTips request. diff --git a/zebrad/src/components/sync/status.rs b/zebrad/src/components/sync/status.rs index eac751b1f..4da8a17d8 100644 --- a/zebrad/src/components/sync/status.rs +++ b/zebrad/src/components/sync/status.rs @@ -69,4 +69,22 @@ impl SyncStatus { // average sync length falls below the threshold. avg < Self::MIN_DIST_FROM_TIP } + + /// Feed the given [`RecentSyncLengths`] it order to make the matching + /// [`SyncStatus`] report that it's close to the tip. + #[cfg(test)] + pub(crate) fn sync_close_to_tip(recent_syncs: &mut RecentSyncLengths) { + for _ in 0..RecentSyncLengths::MAX_RECENT_LENGTHS { + recent_syncs.push_extend_tips_length(1); + } + } + + /// Feed the given [`RecentSyncLengths`] it order to make the matching + /// [`SyncStatus`] report that it's not close to the tip. + #[cfg(test)] + pub(crate) fn sync_far_from_tip(recent_syncs: &mut RecentSyncLengths) { + for _ in 0..RecentSyncLengths::MAX_RECENT_LENGTHS { + recent_syncs.push_extend_tips_length(Self::MIN_DIST_FROM_TIP * 10); + } + } }