Skip to content

Commit 287b116

Browse files
lexnvskunert
andauthored
chainHead: Ensure reasonable distance between leaf and finalized block (#3562)
This PR ensure that the distance between any leaf and the finalized block is within a reasonable distance. For a new subscription, the chainHead has to provide all blocks between the leaves of the chain and the finalized block. When the distance between a leaf and the finalized block is large: - The tree route is costly to compute - We could deliver an unbounded number of blocks (potentially millions) (For more details see #3445 (comment)) The configuration of the ChainHead is extended with: - suspend on lagging distance: When the distance between any leaf and the finalized block is greater than this number, the subscriptions are suspended for a given duration. - All active subscriptions are terminated with the `Stop` event, all blocks are unpinned and data discarded. - For incoming subscriptions, until the suspended period expires the subscriptions will immediately receive the `Stop` event. - Defaults to 128 blocks - suspended duration: The amount of time for which subscriptions are suspended - Defaults to 30 seconds cc @paritytech/subxt-team --------- Signed-off-by: Alexandru Vasile <[email protected]> Co-authored-by: Sebastian Kunert <[email protected]>
1 parent cdacfb9 commit 287b116

7 files changed

Lines changed: 313 additions & 164 deletions

File tree

Cargo.lock

Lines changed: 5 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

substrate/client/rpc-spec-v2/src/chain_head/chain_head.rs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ pub struct ChainHeadConfig {
6262
pub subscription_max_pinned_duration: Duration,
6363
/// The maximum number of ongoing operations per subscription.
6464
pub subscription_max_ongoing_operations: usize,
65+
/// Stop all subscriptions if the distance between the leaves and the current finalized
66+
/// block is larger than this value.
67+
pub max_lagging_distance: usize,
6568
/// The maximum number of items reported by the `chainHead_storage` before
6669
/// pagination is required.
6770
pub operation_max_storage_items: usize,
@@ -88,6 +91,10 @@ const MAX_ONGOING_OPERATIONS: usize = 16;
8891
/// before paginations is required.
8992
const MAX_STORAGE_ITER_ITEMS: usize = 5;
9093

94+
/// Stop all subscriptions if the distance between the leaves and the current finalized
95+
/// block is larger than this value.
96+
const MAX_LAGGING_DISTANCE: usize = 128;
97+
9198
/// The maximum number of `chainHead_follow` subscriptions per connection.
9299
const MAX_FOLLOW_SUBSCRIPTIONS_PER_CONNECTION: usize = 4;
93100

@@ -97,6 +104,7 @@ impl Default for ChainHeadConfig {
97104
global_max_pinned_blocks: MAX_PINNED_BLOCKS,
98105
subscription_max_pinned_duration: MAX_PINNED_DURATION,
99106
subscription_max_ongoing_operations: MAX_ONGOING_OPERATIONS,
107+
max_lagging_distance: MAX_LAGGING_DISTANCE,
100108
operation_max_storage_items: MAX_STORAGE_ITER_ITEMS,
101109
max_follow_subscriptions_per_connection: MAX_FOLLOW_SUBSCRIPTIONS_PER_CONNECTION,
102110
}
@@ -116,6 +124,9 @@ pub struct ChainHead<BE: Backend<Block>, Block: BlockT, Client> {
116124
/// The maximum number of items reported by the `chainHead_storage` before
117125
/// pagination is required.
118126
operation_max_storage_items: usize,
127+
/// Stop all subscriptions if the distance between the leaves and the current finalized
128+
/// block is larger than this value.
129+
max_lagging_distance: usize,
119130
/// Phantom member to pin the block type.
120131
_phantom: PhantomData<Block>,
121132
}
@@ -140,6 +151,7 @@ impl<BE: Backend<Block>, Block: BlockT, Client> ChainHead<BE, Block, Client> {
140151
backend,
141152
),
142153
operation_max_storage_items: config.operation_max_storage_items,
154+
max_lagging_distance: config.max_lagging_distance,
143155
_phantom: PhantomData,
144156
}
145157
}
@@ -187,6 +199,7 @@ where
187199
let subscriptions = self.subscriptions.clone();
188200
let backend = self.backend.clone();
189201
let client = self.client.clone();
202+
let max_lagging_distance = self.max_lagging_distance;
190203

191204
let fut = async move {
192205
// Ensure the current connection ID has enough space to accept a new subscription.
@@ -207,8 +220,8 @@ where
207220
let Some(sub_data) =
208221
reserved_subscription.insert_subscription(sub_id.clone(), with_runtime)
209222
else {
210-
// Inserting the subscription can only fail if the JsonRPSee
211-
// generated a duplicate subscription ID.
223+
// Inserting the subscription can only fail if the JsonRPSee generated a duplicate
224+
// subscription ID.
212225
debug!(target: LOG_TARGET, "[follow][id={:?}] Subscription already accepted", sub_id);
213226
let msg = to_sub_message(&sink, &FollowEvent::<String>::Stop);
214227
let _ = sink.send(msg).await;
@@ -222,9 +235,13 @@ where
222235
subscriptions,
223236
with_runtime,
224237
sub_id.clone(),
238+
max_lagging_distance,
225239
);
226-
227-
chain_head_follow.generate_events(sink, sub_data).await;
240+
let result = chain_head_follow.generate_events(sink, sub_data).await;
241+
if let Err(SubscriptionManagementError::BlockDistanceTooLarge) = result {
242+
debug!(target: LOG_TARGET, "[follow][id={:?}] All subscriptions are stopped", sub_id);
243+
reserved_subscription.stop_all_subscriptions();
244+
}
228245

229246
debug!(target: LOG_TARGET, "[follow][id={:?}] Subscription removed", sub_id);
230247
};

substrate/client/rpc-spec-v2/src/chain_head/chain_head_follow.rs

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,14 @@ use sp_api::CallApiAt;
4141
use sp_blockchain::{
4242
Backend as BlockChainBackend, Error as BlockChainError, HeaderBackend, HeaderMetadata, Info,
4343
};
44-
use sp_runtime::traits::{Block as BlockT, Header as HeaderT, NumberFor};
44+
use sp_runtime::{
45+
traits::{Block as BlockT, Header as HeaderT, NumberFor},
46+
SaturatedConversion, Saturating,
47+
};
4548
use std::{
4649
collections::{HashSet, VecDeque},
4750
sync::Arc,
4851
};
49-
5052
/// The maximum number of finalized blocks provided by the
5153
/// `Initialized` event.
5254
const MAX_FINALIZED_BLOCKS: usize = 16;
@@ -67,6 +69,9 @@ pub struct ChainHeadFollower<BE: Backend<Block>, Block: BlockT, Client> {
6769
sub_id: String,
6870
/// The best reported block by this subscription.
6971
best_block_cache: Option<Block::Hash>,
72+
/// Stop all subscriptions if the distance between the leaves and the current finalized
73+
/// block is larger than this value.
74+
max_lagging_distance: usize,
7075
}
7176

7277
impl<BE: Backend<Block>, Block: BlockT, Client> ChainHeadFollower<BE, Block, Client> {
@@ -77,8 +82,17 @@ impl<BE: Backend<Block>, Block: BlockT, Client> ChainHeadFollower<BE, Block, Cli
7782
sub_handle: SubscriptionManagement<Block, BE>,
7883
with_runtime: bool,
7984
sub_id: String,
85+
max_lagging_distance: usize,
8086
) -> Self {
81-
Self { client, backend, sub_handle, with_runtime, sub_id, best_block_cache: None }
87+
Self {
88+
client,
89+
backend,
90+
sub_handle,
91+
with_runtime,
92+
sub_id,
93+
best_block_cache: None,
94+
max_lagging_distance,
95+
}
8296
}
8397
}
8498

@@ -186,6 +200,35 @@ where
186200
}
187201
}
188202

203+
/// Check the distance between the provided blocks does not exceed a
204+
/// a reasonable range.
205+
///
206+
/// When the blocks are too far apart (potentially millions of blocks):
207+
/// - Tree route is expensive to calculate.
208+
/// - The RPC layer will not be able to generate the `NewBlock` events for all blocks.
209+
///
210+
/// This edge-case can happen for parachains where the relay chain syncs slower to
211+
/// the head of the chain than the parachain node that is synced already.
212+
fn distace_within_reason(
213+
&self,
214+
block: Block::Hash,
215+
finalized: Block::Hash,
216+
) -> Result<(), SubscriptionManagementError> {
217+
let Some(block_num) = self.client.number(block)? else {
218+
return Err(SubscriptionManagementError::BlockHashAbsent)
219+
};
220+
let Some(finalized_num) = self.client.number(finalized)? else {
221+
return Err(SubscriptionManagementError::BlockHashAbsent)
222+
};
223+
224+
let distance: usize = block_num.saturating_sub(finalized_num).saturated_into();
225+
if distance > self.max_lagging_distance {
226+
return Err(SubscriptionManagementError::BlockDistanceTooLarge);
227+
}
228+
229+
Ok(())
230+
}
231+
189232
/// Get the in-memory blocks of the client, starting from the provided finalized hash.
190233
///
191234
/// The reported blocks are pinned by this function.
@@ -198,6 +241,13 @@ where
198241
let mut pruned_forks = HashSet::new();
199242
let mut finalized_block_descendants = Vec::new();
200243
let mut unique_descendants = HashSet::new();
244+
245+
// Ensure all leaves are within a reasonable distance from the finalized block,
246+
// before traversing the tree.
247+
for leaf in &leaves {
248+
self.distace_within_reason(*leaf, finalized)?;
249+
}
250+
201251
for leaf in leaves {
202252
let tree_route = sp_blockchain::tree_route(blockchain, finalized, leaf)?;
203253

@@ -542,7 +592,8 @@ where
542592
mut to_ignore: HashSet<Block::Hash>,
543593
sink: SubscriptionSink,
544594
rx_stop: oneshot::Receiver<()>,
545-
) where
595+
) -> Result<(), SubscriptionManagementError>
596+
where
546597
EventStream: Stream<Item = NotificationType<Block>> + Unpin,
547598
{
548599
let mut stream_item = stream.next();
@@ -576,7 +627,7 @@ where
576627
);
577628
let msg = to_sub_message(&sink, &FollowEvent::<String>::Stop);
578629
let _ = sink.send(msg).await;
579-
return
630+
return Err(err)
580631
},
581632
};
582633

@@ -591,7 +642,8 @@ where
591642

592643
let msg = to_sub_message(&sink, &FollowEvent::<String>::Stop);
593644
let _ = sink.send(msg).await;
594-
return
645+
// No need to propagate this error further, the client disconnected.
646+
return Ok(())
595647
}
596648
}
597649

@@ -605,14 +657,15 @@ where
605657
// - the client disconnected.
606658
let msg = to_sub_message(&sink, &FollowEvent::<String>::Stop);
607659
let _ = sink.send(msg).await;
660+
Ok(())
608661
}
609662

610663
/// Generate the block events for the `chainHead_follow` method.
611664
pub async fn generate_events(
612665
&mut self,
613666
sink: SubscriptionSink,
614667
sub_data: InsertedSubscriptionData<Block>,
615-
) {
668+
) -> Result<(), SubscriptionManagementError> {
616669
// Register for the new block and finalized notifications.
617670
let stream_import = self
618671
.client
@@ -640,7 +693,7 @@ where
640693
);
641694
let msg = to_sub_message(&sink, &FollowEvent::<String>::Stop);
642695
let _ = sink.send(msg).await;
643-
return
696+
return Err(err)
644697
},
645698
};
646699

@@ -650,6 +703,6 @@ where
650703
let stream = stream::once(futures::future::ready(initial)).chain(merged);
651704

652705
self.submit_events(&startup_point, stream.boxed(), pruned_forks, sink, sub_data.rx_stop)
653-
.await;
706+
.await
654707
}
655708
}

substrate/client/rpc-spec-v2/src/chain_head/subscription/error.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ pub enum SubscriptionManagementError {
4141
/// The unpin method was called with duplicate hashes.
4242
#[error("Duplicate hashes")]
4343
DuplicateHashes,
44+
/// The distance between the leaves and the current finalized block is too large.
45+
#[error("Distance too large")]
46+
BlockDistanceTooLarge,
4447
/// Custom error.
4548
#[error("Subscription error {0}")]
4649
Custom(String),
@@ -57,6 +60,7 @@ impl PartialEq for SubscriptionManagementError {
5760
(Self::BlockHeaderAbsent, Self::BlockHeaderAbsent) |
5861
(Self::SubscriptionAbsent, Self::SubscriptionAbsent) |
5962
(Self::DuplicateHashes, Self::DuplicateHashes) => true,
63+
(Self::BlockDistanceTooLarge, Self::BlockDistanceTooLarge) => true,
6064
(Self::Custom(lhs), Self::Custom(rhs)) => lhs == rhs,
6165
_ => false,
6266
}

0 commit comments

Comments
 (0)