diff --git a/cumulus/zombienet/zombienet-sdk/tests/zombie_ci/full_node_warp_sync/common.rs b/cumulus/zombienet/zombienet-sdk/tests/zombie_ci/full_node_warp_sync/common.rs index 0f76fe920b9db..62a68bf669745 100644 --- a/cumulus/zombienet/zombienet-sdk/tests/zombie_ci/full_node_warp_sync/common.rs +++ b/cumulus/zombienet/zombienet-sdk/tests/zombie_ci/full_node_warp_sync/common.rs @@ -109,6 +109,7 @@ pub(crate) async fn build_network_config( ) .into(), ("--sync", "warp").into(), + ("--blocks-pruning", "256").into(), ]) }) .with_node(|node| { diff --git a/cumulus/zombienet/zombienet-sdk/tests/zombie_ci/full_node_warp_sync/full_node_warp_sync.rs b/cumulus/zombienet/zombienet-sdk/tests/zombie_ci/full_node_warp_sync/full_node_warp_sync.rs index 51e4480c5bec6..1e6475bb98412 100644 --- a/cumulus/zombienet/zombienet-sdk/tests/zombie_ci/full_node_warp_sync/full_node_warp_sync.rs +++ b/cumulus/zombienet/zombienet-sdk/tests/zombie_ci/full_node_warp_sync/full_node_warp_sync.rs @@ -86,9 +86,9 @@ async fn assert_warp_sync(node: &NetworkNode) -> Result<(), anyhow::Error> { Ok(()) } -// Asserting Gap sync requires at least sync=debug level -async fn assert_gap_sync(node: &NetworkNode) -> Result<(), anyhow::Error> { - let option_1_line = LogLineCountOptions::new(|n| n == 1, Duration::from_secs(20), false); +// Asserting Gap sync requires at least sync=trace level +async fn assert_gap_sync(node: &NetworkNode, is_archive: bool) -> Result<(), anyhow::Error> { + let option_1_line = LogLineCountOptions::new(|n| n == 1, Duration::from_secs(5), false); let option_at_least_5_lines = LogLineCountOptions::new(|n| n >= 5, Duration::from_secs(20), false); @@ -117,6 +117,27 @@ async fn assert_gap_sync(node: &NetworkNode) -> Result<(), anyhow::Error> { return Err(anyhow!("Gap sync block imports are not started")); } + // Verify body download behavior based on archive mode: + // - Archive nodes should download bodies (body: non-zero) + // - Non-archive nodes should skip bodies (body: 0 B) + let (body_pattern, body_error) = if is_archive { + ( + r"(? Result<(), anyhow::Error> { // Assert warp and gap syncs only for relaychain. // "five" is not warp syncing the relaychain - for name in ["dave", "eve", "four"] { + // dave is non-archive (--blocks-pruning 256), eve and four use default (archive-canonical) + for (name, is_archive) in [("dave", false), ("eve", true), ("four", true)] { assert_warp_sync(network.get_node(name)?).await?; - assert_gap_sync(network.get_node(name)?).await?; + assert_gap_sync(network.get_node(name)?, is_archive).await?; } // Check relaychain progress @@ -204,9 +226,9 @@ async fn full_node_warp_sync() -> Result<(), anyhow::Error> { log::info!("Waiting for ferdie to be up"); network.get_node("ferdie")?.wait_until_is_up(60u64).await?; - // Assert warp and gap sync for ferdie + // Assert warp and gap sync for ferdie (uses default archive-canonical) assert_warp_sync(network.get_node("ferdie")?).await?; - assert_gap_sync(network.get_node("ferdie")?).await?; + assert_gap_sync(network.get_node("ferdie")?, true).await?; // Check progress for ferdie log::info!("Checking full node ferdie is syncing"); diff --git a/prdoc/pr_10752.prdoc b/prdoc/pr_10752.prdoc new file mode 100644 index 0000000000000..836010933bbdd --- /dev/null +++ b/prdoc/pr_10752.prdoc @@ -0,0 +1,22 @@ +title: ' Gap Sync: Skip Body Requests for Non-Archive Nodes' +doc: +- audience: Node Operator + description: |- + ### Summary + This PR optimizes gap sync bandwidth usage by skipping body requests for non-archive nodes. Bodies are unnecessary during gap sync when the node doesn't maintain full block history, while archive nodes continue to request bodies to preserve complete history. + It reduces bandwidth consumption and database size significantly for typical validator/full nodes. + + Additionally added some gap sync statistics for observability: + - Introduced `GapSyncStats` to track bandwidth usage: header bytes, body bytes, justification bytes + - Logged on gap sync completion to provide visibility into bandwidth savings +crates: +- name: sc-network-sync + bump: major + validate: false +- name: sc-service + bump: major + validate: false +- name: sc-client-db + bump: patch +- name: sc-cli + bump: patch diff --git a/substrate/client/cli/src/arg_enums.rs b/substrate/client/cli/src/arg_enums.rs index 289b8694fc836..2a6dc79f1680a 100644 --- a/substrate/client/cli/src/arg_enums.rs +++ b/substrate/client/cli/src/arg_enums.rs @@ -294,6 +294,9 @@ pub enum SyncMode { /// Download blocks without executing them. Download latest state without proofs. FastUnsafe, /// Prove finality and download the latest state. + /// After warp sync completes, the node will have block headers but not bodies for historical + /// blocks (unless `blocks-pruning` is set to archive mode). This saves bandwidth while still + /// allowing the node to serve as a warp sync source for other nodes. Warp, } diff --git a/substrate/client/db/src/lib.rs b/substrate/client/db/src/lib.rs index ea349289ced73..15ba31d3fb1f9 100644 --- a/substrate/client/db/src/lib.rs +++ b/substrate/client/db/src/lib.rs @@ -1726,6 +1726,10 @@ impl Backend { } let should_check_block_gap = !header_exists_in_db || !body_exists_in_db; + debug!( + target: "db", + "should_check_block_gap = {should_check_block_gap}", + ); if should_check_block_gap { let update_gap = diff --git a/substrate/client/network/sync/src/strategy/chain_sync.rs b/substrate/client/network/sync/src/strategy/chain_sync.rs index a3ddccafd2d46..96aeb3786f61a 100644 --- a/substrate/client/network/sync/src/strategy/chain_sync.rs +++ b/substrate/client/network/sync/src/strategy/chain_sync.rs @@ -44,6 +44,7 @@ use crate::{ LOG_TARGET, }; +use codec::Encode; use futures::{channel::oneshot, FutureExt}; use log::{debug, error, info, trace, warn}; use prometheus_endpoint::{register, Gauge, PrometheusError, Registry, U64}; @@ -68,7 +69,8 @@ use sp_runtime::{ use std::{ any::Any, collections::{HashMap, HashSet}, - ops::Range, + fmt, + ops::{AddAssign, Range}, sync::Arc, }; @@ -200,10 +202,62 @@ impl Default for AllowedRequests { } } +/// Statistics for gap sync operations. +#[derive(Debug, Default, Clone)] +struct GapSyncStats { + /// Size of headers downloaded during gap sync + header_bytes: usize, + /// Size of bodies downloaded during gap sync + body_bytes: usize, + /// Size of justifications downloaded during gap sync + justification_bytes: usize, +} + +impl GapSyncStats { + fn new() -> Self { + Self::default() + } + + fn total_bytes(&self) -> usize { + self.header_bytes + self.body_bytes + self.justification_bytes + } + + fn bytes_to_mib(bytes: usize) -> f64 { + bytes as f64 / (1024.0 * 1024.0) + } +} + +impl fmt::Display for GapSyncStats { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let total = self.total_bytes(); + write!( + f, + "hdr: {} B ({:.2} MiB), body: {} B ({:.2} MiB), just: {} B ({:.2} MiB) | total: {} B ({:.2} MiB)", + self.header_bytes, + Self::bytes_to_mib(self.header_bytes), + self.body_bytes, + Self::bytes_to_mib(self.body_bytes), + self.justification_bytes, + Self::bytes_to_mib(self.justification_bytes), + total, + Self::bytes_to_mib(total), + ) + } +} + +impl AddAssign for GapSyncStats { + fn add_assign(&mut self, other: Self) { + self.header_bytes += other.header_bytes; + self.body_bytes += other.body_bytes; + self.justification_bytes += other.justification_bytes; + } +} + struct GapSync { blocks: BlockCollection, best_queued_number: NumberFor, target: NumberFor, + stats: GapSyncStats, } /// Sync operation mode. @@ -220,6 +274,32 @@ pub enum ChainSyncMode { }, } +impl ChainSyncMode { + /// Returns the base block attributes required for this sync mode. + pub fn required_block_attributes(&self, is_gap: bool, is_archive: bool) -> BlockAttributes { + let attrs = match self { + ChainSyncMode::Full => { + BlockAttributes::HEADER | BlockAttributes::JUSTIFICATION | BlockAttributes::BODY + }, + ChainSyncMode::LightState { storage_chain_mode: false, .. } => { + BlockAttributes::HEADER | BlockAttributes::JUSTIFICATION | BlockAttributes::BODY + }, + ChainSyncMode::LightState { storage_chain_mode: true, .. } => { + BlockAttributes::HEADER | + BlockAttributes::JUSTIFICATION | + BlockAttributes::INDEXED_BODY + }, + }; + // Skip body requests for gap sync only if not in archive mode. + // Archive nodes need bodies to maintain complete block history. + if is_gap && !is_archive { + attrs & !BlockAttributes::BODY + } else { + attrs + } + } +} + /// All the data we have about a Peer that we are trying to sync with #[derive(Debug, Clone)] pub(crate) struct PeerSync { @@ -337,6 +417,9 @@ pub struct ChainSync { import_existing: bool, /// Block downloader block_downloader: Arc>, + /// Whether to archive blocks. When `true`, gap sync requests bodies to maintain complete + /// block history. + archive_blocks: bool, /// Gap download process. gap_sync: Option>, /// Pending actions. @@ -945,6 +1028,7 @@ where max_blocks_per_request: u32, state_request_protocol_name: ProtocolName, block_downloader: Arc>, + archive_blocks: bool, metrics_registry: Option<&Registry>, initial_peers: impl Iterator)>, ) -> Result { @@ -968,6 +1052,7 @@ where state_sync: None, import_existing: false, block_downloader, + archive_blocks, gap_sync: None, actions: Vec::new(), metrics: metrics_registry.and_then(|r| match Metrics::register(r) { @@ -992,14 +1077,17 @@ where /// Complete the gap sync if the target number is reached and there is a gap. fn complete_gap_if_target(&mut self, number: NumberFor) { - let gap_sync_complete = self.gap_sync.as_ref().map_or(false, |s| s.target == number); - if gap_sync_complete { - info!( - target: LOG_TARGET, - "Block history download is complete." - ); - self.gap_sync = None; + let Some(gap_sync) = &self.gap_sync else { return }; + + if gap_sync.target != number { + return; } + + info!( + target: LOG_TARGET, + "Block history download is complete.", + ); + self.gap_sync = None; } #[must_use] @@ -1186,6 +1274,7 @@ where gap_sync.blocks.insert(start_block, blocks, *peer_id); } gap = true; + let mut batch_gap_sync_stats = GapSyncStats::new(); let blocks: Vec<_> = gap_sync .blocks .ready_blocks(gap_sync.best_queued_number + One::one()) @@ -1197,6 +1286,26 @@ where block_data.block.justification, ) }); + let gap_sync_stats = GapSyncStats { + header_bytes: block_data + .block + .header + .as_ref() + .map(|h| h.encoded_size()) + .unwrap_or(0), + body_bytes: block_data + .block + .body + .as_ref() + .map(|b| b.encoded_size()) + .unwrap_or(0), + justification_bytes: justifications + .as_ref() + .map(|j| j.encoded_size()) + .unwrap_or(0), + }; + batch_gap_sync_stats += gap_sync_stats; + IncomingBlock { hash: block_data.block.hash, header: block_data.block.header, @@ -1213,12 +1322,23 @@ where } }) .collect(); + debug!( target: LOG_TARGET, "Drained {} gap blocks from {}", blocks.len(), gap_sync.best_queued_number, ); + + gap_sync.stats += batch_gap_sync_stats; + + if blocks.len() > 0 { + trace!( + target: LOG_TARGET, + "Gap sync cumulative stats: {}", + gap_sync.stats + ); + } blocks } else { debug!(target: LOG_TARGET, "Unexpected gap block response from {peer_id}"); @@ -1432,6 +1552,7 @@ where (Some(first), Some(_)) => format!(" ({})", first), _ => Default::default(), }; + trace!( target: LOG_TARGET, "BlockResponse {} from {} with {} blocks {}", @@ -1526,22 +1647,6 @@ where } } - fn required_block_attributes(&self) -> BlockAttributes { - match self.mode { - ChainSyncMode::Full => { - BlockAttributes::HEADER | BlockAttributes::JUSTIFICATION | BlockAttributes::BODY - }, - ChainSyncMode::LightState { storage_chain_mode: false, .. } => { - BlockAttributes::HEADER | BlockAttributes::JUSTIFICATION | BlockAttributes::BODY - }, - ChainSyncMode::LightState { storage_chain_mode: true, .. } => { - BlockAttributes::HEADER | - BlockAttributes::JUSTIFICATION | - BlockAttributes::INDEXED_BODY - }, - } - } - fn skip_execution(&self) -> bool { match self.mode { ChainSyncMode::Full => false, @@ -1733,6 +1838,7 @@ where best_queued_number: start - One::one(), target: end, blocks: BlockCollection::new(), + stats: GapSyncStats::new(), }); } trace!( @@ -1828,7 +1934,8 @@ where return Vec::new(); } let is_major_syncing = self.status().state.is_major_syncing(); - let attrs = self.required_block_attributes(); + let mode = self.mode; + let is_archive = self.archive_blocks; let blocks = &mut self.blocks; let fork_targets = &mut self.fork_targets; let last_finalized = @@ -1882,7 +1989,7 @@ where &id, peer, blocks, - attrs, + mode.required_block_attributes(false, is_archive), max_parallel, max_blocks_per_request, last_finalized, @@ -1903,7 +2010,7 @@ where fork_targets, best_queued, last_finalized, - attrs, + mode.required_block_attributes(false, is_archive), |hash| { if queue_blocks.contains(hash) { BlockStatus::Queued @@ -1922,7 +2029,7 @@ where &id, peer, &mut sync.blocks, - attrs, + mode.required_block_attributes(true, is_archive), sync.target, sync.best_queued_number, max_blocks_per_request, diff --git a/substrate/client/network/sync/src/strategy/chain_sync/test.rs b/substrate/client/network/sync/src/strategy/chain_sync/test.rs index 64079c2bcc471..34a529f6de3be 100644 --- a/substrate/client/network/sync/src/strategy/chain_sync/test.rs +++ b/substrate/client/network/sync/src/strategy/chain_sync/test.rs @@ -93,6 +93,7 @@ fn processes_empty_response_on_justification_request_for_unknown_block() { 64, ProtocolName::Static(""), Arc::new(MockBlockDownloader::new()), + false, None, std::iter::empty(), ) @@ -158,6 +159,7 @@ fn restart_doesnt_affect_peers_downloading_finality_data() { 8, ProtocolName::Static(""), Arc::new(MockBlockDownloader::new()), + false, None, std::iter::empty(), ) @@ -364,6 +366,7 @@ fn do_ancestor_search_when_common_block_to_best_queued_gap_is_to_big() { 64, ProtocolName::Static(""), Arc::new(MockBlockDownloader::new()), + false, None, std::iter::empty(), ) @@ -522,6 +525,7 @@ fn can_sync_huge_fork() { 64, protocol_name, proxy_block_downloader.clone(), + false, None, std::iter::empty(), ) @@ -670,6 +674,7 @@ fn syncs_fork_without_duplicate_requests() { 64, protocol_name, proxy_block_downloader.clone(), + false, None, std::iter::empty(), ) @@ -818,6 +823,7 @@ fn removes_target_fork_on_disconnect() { 64, ProtocolName::Static(""), Arc::new(MockBlockDownloader::new()), + false, None, std::iter::empty(), ) @@ -853,6 +859,7 @@ fn can_import_response_with_missing_blocks() { 64, ProtocolName::Static(""), Arc::new(MockBlockDownloader::new()), + false, None, std::iter::empty(), ) @@ -894,6 +901,7 @@ fn sync_restart_removes_block_but_not_justification_requests() { 64, ProtocolName::Static(""), Arc::new(MockBlockDownloader::new()), + false, None, std::iter::empty(), ) @@ -1046,6 +1054,7 @@ fn request_across_forks() { 64, ProtocolName::Static(""), Arc::new(MockBlockDownloader::new()), + false, None, std::iter::empty(), ) @@ -1154,6 +1163,7 @@ fn sync_verification_failed_with_gap_filled() { 64, ProtocolName::Static(""), Arc::new(MockBlockDownloader::new()), + false, None, std::iter::empty(), ) @@ -1263,6 +1273,7 @@ fn sync_verification_failed_with_gap_filled() { best_queued_number: 64 as u64, target: 84 as u64, blocks: BlockCollection::new(), + stats: GapSyncStats::new(), }); } else if loop_index == 1 { if sync.gap_sync.is_none() { @@ -1291,6 +1302,7 @@ fn sync_gap_filled_regardless_of_blocks_origin() { 64, ProtocolName::Static(""), Arc::new(MockBlockDownloader::new()), + false, None, std::iter::empty(), ) @@ -1306,6 +1318,7 @@ fn sync_gap_filled_regardless_of_blocks_origin() { best_queued_number: *blocks[0].header().number(), target: *blocks[0].header().number(), blocks: BlockCollection::new(), + stats: GapSyncStats::new(), }); // Announce the block as unknown. @@ -1328,6 +1341,7 @@ fn sync_gap_filled_regardless_of_blocks_origin() { best_queued_number: *blocks[0].header().number(), target: *blocks[0].header().number(), blocks: BlockCollection::new(), + stats: GapSyncStats::new(), }); // Announce the block as known. @@ -1341,3 +1355,119 @@ fn sync_gap_filled_regardless_of_blocks_origin() { assert!(sync.gap_sync.is_none()); } } + +#[test] +fn gap_sync_body_request_depends_on_pruning_mode() { + sp_tracing::try_init_simple(); + + for archive_blocks in [true, false] { + // Bodies only needed for archive mode + let should_request_bodies = archive_blocks; + log::info!("Testing gap sync with archive_blocks: {}", archive_blocks); + + let client = Arc::new(TestClientBuilder::new().build()); + let blocks = (0..10).map(|_| build_block(&client, None, false)).collect::>(); + + let mut sync = ChainSync::new( + ChainSyncMode::Full, + client.clone(), + 5, + 64, + ProtocolName::Static(""), + Arc::new(MockBlockDownloader::new()), + archive_blocks, + None, + std::iter::empty(), + ) + .unwrap(); + + let peer_id = PeerId::random(); + + // Simulate gap: blocks 5-10 missing + sync.gap_sync = Some(GapSync { + best_queued_number: 5, + target: 10, + blocks: BlockCollection::new(), + stats: GapSyncStats::new(), + }); + + sync.add_peer(peer_id, blocks[9].hash(), 10); + + let requests = sync.block_requests(); + assert!( + !requests.is_empty(), + "[archive_blocks={archive_blocks}] Should generate gap sync request" + ); + + let (_peer, request) = &requests[0]; + + // Verify the exact expected field combination + let expected_fields = if should_request_bodies { + BlockAttributes::HEADER | BlockAttributes::BODY | BlockAttributes::JUSTIFICATION + } else { + BlockAttributes::HEADER | BlockAttributes::JUSTIFICATION + }; + + assert_eq!( + request.fields, expected_fields, + "[archive_blocks={archive_blocks}] Gap sync fields mismatch: expected {expected_fields:?}, got {:?}", + request.fields + ); + } +} + +#[test] +fn regular_sync_always_requests_bodies_regardless_of_pruning() { + sp_tracing::try_init_simple(); + + // Verify that regular (non-gap) sync always requests bodies, + // regardless of pruning mode - our optimization only applies to gap sync + for archive_blocks in [true, false] { + log::info!("Testing regular sync with archive_blocks: {}", archive_blocks); + + let client = Arc::new(TestClientBuilder::new().build()); + let blocks = (0..5).map(|_| build_block(&client, None, false)).collect::>(); + + let mut sync = ChainSync::new( + ChainSyncMode::Full, + client.clone(), + 5, + 64, + ProtocolName::Static(""), + Arc::new(MockBlockDownloader::new()), + archive_blocks, + None, + std::iter::empty(), + ) + .unwrap(); + + let peer_id = PeerId::random(); + + // Ensure we're NOT in gap sync mode + assert!( + sync.gap_sync.is_none(), + "[archive_blocks={archive_blocks}] Should not have gap sync active" + ); + + // Add peer ahead of us to trigger regular sync + sync.add_peer(peer_id, blocks[4].hash(), 5); + + let requests = sync.block_requests(); + + // Regular sync may not always generate requests immediately depending on state, + // but when it does, it should request bodies + if !requests.is_empty() { + let (_peer, request) = &requests[0]; + + // Verify exact expected fields for Full mode + let expected_fields = + BlockAttributes::HEADER | BlockAttributes::BODY | BlockAttributes::JUSTIFICATION; + + assert_eq!( + request.fields, expected_fields, + "[archive_blocks={archive_blocks}] Regular sync fields mismatch: expected {expected_fields:?}, got {:?}", + request.fields + ); + } + } +} diff --git a/substrate/client/network/sync/src/strategy/polkadot.rs b/substrate/client/network/sync/src/strategy/polkadot.rs index f645f5c349cf3..b32c98cf80a76 100644 --- a/substrate/client/network/sync/src/strategy/polkadot.rs +++ b/substrate/client/network/sync/src/strategy/polkadot.rs @@ -74,6 +74,9 @@ where pub state_request_protocol_name: ProtocolName, /// Block downloader pub block_downloader: Arc>, + /// Whether to archive blocks. When `true`, gap sync requests bodies to maintain complete + /// block history. + pub archive_blocks: bool, } /// Proxy to specific syncing strategies used in Polkadot. @@ -384,6 +387,7 @@ where config.max_blocks_per_request, config.state_request_protocol_name.clone(), config.block_downloader.clone(), + config.archive_blocks, config.metrics_registry.as_ref(), std::iter::empty(), )?; @@ -436,6 +440,7 @@ where self.config.max_blocks_per_request, self.config.state_request_protocol_name.clone(), self.config.block_downloader.clone(), + self.config.archive_blocks, self.config.metrics_registry.as_ref(), self.peer_best_blocks.iter().map(|(peer_id, (best_hash, best_number))| { (*peer_id, *best_hash, *best_number) @@ -466,6 +471,7 @@ where self.config.max_blocks_per_request, self.config.state_request_protocol_name.clone(), self.config.block_downloader.clone(), + self.config.archive_blocks, self.config.metrics_registry.as_ref(), self.peer_best_blocks.iter().map(|(peer_id, (best_hash, best_number))| { (*peer_id, *best_hash, *best_number) diff --git a/substrate/client/network/test/src/lib.rs b/substrate/client/network/test/src/lib.rs index f246f1656d54b..3694803f95e7d 100644 --- a/substrate/client/network/test/src/lib.rs +++ b/substrate/client/network/test/src/lib.rs @@ -963,6 +963,7 @@ pub trait TestNetFactory: Default + Sized + Send { state_request_protocol_name: state_request_protocol_config.name.clone(), block_downloader: block_relay_params.downloader, min_peers_to_start_warp_sync: None, + archive_blocks: config.blocks_pruning.is_none(), }; // Initialize syncing strategy. let syncing_strategy = Box::new( diff --git a/substrate/client/network/test/src/service.rs b/substrate/client/network/test/src/service.rs index 68c65eece12ab..c30a3bdb3d10f 100644 --- a/substrate/client/network/test/src/service.rs +++ b/substrate/client/network/test/src/service.rs @@ -212,6 +212,7 @@ impl TestNetworkBuilder { state_request_protocol_name: state_request_protocol_config.name.clone(), block_downloader: block_relay_params.downloader, min_peers_to_start_warp_sync: None, + archive_blocks: false, }; // Initialize syncing strategy. let syncing_strategy = Box::new( diff --git a/substrate/client/network/test/src/sync.rs b/substrate/client/network/test/src/sync.rs index f91601d93a6ca..80d95c66c9571 100644 --- a/substrate/client/network/test/src/sync.rs +++ b/substrate/client/network/test/src/sync.rs @@ -1200,7 +1200,7 @@ async fn syncs_indexed_blocks() { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn warp_sync() { +async fn warp_sync_gap_sync_skips_bodies_if_blocks_pruning() { sp_tracing::try_init_simple(); let mut net = TestNet::new(0); // Create 3 synced peers and 1 peer trying to warp sync. @@ -1209,12 +1209,18 @@ async fn warp_sync() { net.add_full_peer_with_config(Default::default()); net.add_full_peer_with_config(FullPeerConfig { sync_mode: SyncMode::Warp, + blocks_pruning: Some(256), // Pruning enabled, gap sync expected to not request bodies ..Default::default() }); - let gap_end = net.peer(0).push_blocks(63, false).pop().unwrap(); + + // Splitting blocks into chunks to demonstrate how gap sync works + let gap_start = net.peer(0).push_blocks(1, false); + let blocks = net.peer(0).push_blocks(61, false); + let gap_end = net.peer(0).push_blocks(1, false); let target = net.peer(0).push_blocks(1, false).pop().unwrap(); net.peer(1).push_blocks(64, false); net.peer(2).push_blocks(64, false); + // Wait for peer 3 to sync state. net.run_until_sync().await; // Make sure it was not a full sync. @@ -1225,7 +1231,67 @@ async fn warp_sync() { // Wait for peer 3 to download block history (gap sync). futures::future::poll_fn::<(), _>(|cx| { net.poll(cx); - if net.peer(3).has_body(gap_end) && net.peer(3).has_body(target) { + let peer = net.peer(3); + + // Gap blocks should only have headers (not bodies) due to pruning + let gap_blocks_dont_have_bodies = gap_start + .iter() + .chain(blocks.iter()) + .chain(gap_end.iter()) + .all(|b| peer.has_block(*b) && !peer.has_body(*b)); + + // Target block should have body (downloaded during warp sync) + let target_has_body = peer.has_body(target); + + if gap_blocks_dont_have_bodies && target_has_body { + Poll::Ready(()) + } else { + Poll::Pending + } + }) + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn warp_sync_gap_sync_requests_bodies_if_archive_node() { + sp_tracing::try_init_simple(); + let mut net = TestNet::new(0); + // Create 3 synced peers and 1 peer trying to warp sync. + net.add_full_peer_with_config(Default::default()); + net.add_full_peer_with_config(Default::default()); + net.add_full_peer_with_config(Default::default()); + net.add_full_peer_with_config(FullPeerConfig { + sync_mode: SyncMode::Warp, + blocks_pruning: None, // Archive mode, gap sync expected to request bodies too + ..Default::default() + }); + + // Splitting blocks into chunks to demonstrate how gap sync works + let gap_start = net.peer(0).push_blocks(1, false); + let blocks = net.peer(0).push_blocks(61, false); + let gap_end = net.peer(0).push_blocks(1, false); + let target = net.peer(0).push_blocks(1, false); + net.peer(1).push_blocks(64, false); + net.peer(2).push_blocks(64, false); + + // Wait for peer 3 to sync state. + net.run_until_sync().await; + // Make sure it was not a full sync. + assert!(!net.peer(3).client().has_state_at(&BlockId::Number(1))); + // Make sure warp sync was successful. + assert!(net.peer(3).client().has_state_at(&BlockId::Number(64))); + + // Wait for peer 3 to download block history (gap sync). + futures::future::poll_fn::<(), _>(|cx| { + net.poll(cx); + let peer = net.peer(3); + if gap_start + .iter() + .chain(blocks.iter()) + .chain(gap_end.iter()) + .chain(target.iter()) + .all(|b| peer.has_body(*b)) + { Poll::Ready(()) } else { Poll::Pending @@ -1281,6 +1347,7 @@ async fn warp_sync_to_target_block() { net.add_full_peer_with_config(FullPeerConfig { sync_mode: SyncMode::Warp, + blocks_pruning: Some(256), target_header: Some(target_block), ..Default::default() }); @@ -1292,7 +1359,7 @@ async fn warp_sync_to_target_block() { futures::future::poll_fn::<(), _>(|cx| { net.poll(cx); let peer = net.peer(3); - if blocks.iter().all(|b| peer.has_body(*b)) { + if blocks.iter().all(|b| peer.has_block(*b)) { Poll::Ready(()) } else { Poll::Pending diff --git a/substrate/client/service/src/builder.rs b/substrate/client/service/src/builder.rs index 3c7b1a8012f1d..7d04388b0873b 100644 --- a/substrate/client/service/src/builder.rs +++ b/substrate/client/service/src/builder.rs @@ -1058,6 +1058,7 @@ where client.clone(), &spawn_handle, metrics_registry, + config.blocks_pruning.is_archive(), )?; let (syncing_engine, sync_service, block_announce_config) = SyncingEngine::new( @@ -1334,6 +1335,9 @@ where pub metrics_registry: Option<&'a Registry>, /// Metrics. pub metrics: NotificationMetrics, + /// Whether to archive blocks. When `true`, gap sync requests bodies to maintain complete + /// block history. + pub archive_blocks: bool, } /// Build default syncing engine using [`build_default_block_downloader`] and @@ -1366,6 +1370,7 @@ where spawn_handle, metrics_registry, metrics, + archive_blocks, } = config; let block_downloader = build_default_block_downloader( @@ -1386,6 +1391,7 @@ where client.clone(), spawn_handle, metrics_registry, + archive_blocks, )?; let (syncing_engine, sync_service, block_announce_config) = SyncingEngine::new( @@ -1453,6 +1459,7 @@ pub fn build_polkadot_syncing_strategy( client: Arc, spawn_handle: &SpawnTaskHandle, metrics_registry: Option<&Registry>, + archive_blocks: bool, ) -> Result>, Error> where Block: BlockT, @@ -1522,6 +1529,7 @@ where metrics_registry: metrics_registry.cloned(), state_request_protocol_name, block_downloader, + archive_blocks, }; Ok(Box::new(PolkadotSyncingStrategy::new( syncing_config,