-
Notifications
You must be signed in to change notification settings - Fork 696
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
chainHead: Track reported blocks to capture notification gaps #5856
Changes from 9 commits
c42a4d8
d277b87
00e8eab
6e55454
678e9bf
35b6573
5e03600
5c80006
7e74e4a
fbb8b16
2b44eaf
e97b590
45f2bce
28911be
1d5af1a
308d388
a05ae72
d8e9a35
5135e89
a5bf18e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
# Schema: Polkadot SDK PRDoc Schema (prdoc) v1.0.0 | ||
# See doc at https://raw.githubusercontent.com/paritytech/polkadot-sdk/master/prdoc/schema_user.json | ||
|
||
title: Extend state tracking of chainHead to capture notification gaps | ||
|
||
doc: | ||
- audience: Node Dev | ||
description: | | ||
This PR extends the state tracking of the RPC-v2 chainHead methods. | ||
ChainHead tracks the reported blocks to detect notification gaps. | ||
This state tracking ensures we can detect `NewBlock` events for | ||
which we did not report previously the parent hash. | ||
|
||
crates: | ||
- name: sc-rpc-spec-v2 | ||
bump: minor | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,7 +24,7 @@ use crate::chain_head::{ | |
BestBlockChanged, Finalized, FollowEvent, Initialized, NewBlock, RuntimeEvent, | ||
RuntimeVersionEvent, | ||
}, | ||
subscription::{SubscriptionManagement, SubscriptionManagementError}, | ||
subscription::{InsertedSubscriptionData, SubscriptionManagement, SubscriptionManagementError}, | ||
}; | ||
use futures::{ | ||
channel::oneshot, | ||
|
@@ -53,8 +53,6 @@ use std::{ | |
/// `Initialized` event. | ||
const MAX_FINALIZED_BLOCKS: usize = 16; | ||
|
||
use super::subscription::InsertedSubscriptionData; | ||
|
||
/// Generates the events of the `chainHead_follow` method. | ||
pub struct ChainHeadFollower<BE: Backend<Block>, Block: BlockT, Client> { | ||
/// Substrate client. | ||
|
@@ -71,6 +69,8 @@ pub struct ChainHeadFollower<BE: Backend<Block>, Block: BlockT, Client> { | |
current_best_block: Option<Block::Hash>, | ||
/// LRU cache of pruned blocks. | ||
pruned_blocks: LruMap<Block::Hash, ()>, | ||
/// LRU cache of pruned blocks. | ||
lexnv marked this conversation as resolved.
Show resolved
Hide resolved
|
||
announced_blocks: LruMap<Block::Hash, ()>, | ||
/// Stop all subscriptions if the distance between the leaves and the current finalized | ||
/// block is larger than this value. | ||
max_lagging_distance: usize, | ||
|
@@ -96,6 +96,9 @@ impl<BE: Backend<Block>, Block: BlockT, Client> ChainHeadFollower<BE, Block, Cli | |
pruned_blocks: LruMap::new(ByLength::new( | ||
MAX_PINNED_BLOCKS.try_into().unwrap_or(u32::MAX), | ||
)), | ||
announced_blocks: LruMap::new(ByLength::new( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One downside of this approach is that we never remove anything manually and just rely on the LRU to remove items. However, that means that over time we will have the maximum amount of items in every LRU for every subscription. But in reality probably does not matter, for these 512 items thats approx. 16kb of memory usage then 🤷 |
||
MAX_PINNED_BLOCKS.try_into().unwrap_or(u32::MAX), | ||
)), | ||
max_lagging_distance, | ||
} | ||
} | ||
|
@@ -214,7 +217,7 @@ where | |
/// | ||
/// This edge-case can happen for parachains where the relay chain syncs slower to | ||
/// the head of the chain than the parachain node that is synced already. | ||
fn distace_within_reason( | ||
fn distance_within_reason( | ||
&self, | ||
block: Block::Hash, | ||
finalized: Block::Hash, | ||
|
@@ -250,7 +253,7 @@ where | |
// Ensure all leaves are within a reasonable distance from the finalized block, | ||
// before traversing the tree. | ||
for leaf in &leaves { | ||
self.distace_within_reason(*leaf, finalized)?; | ||
self.distance_within_reason(*leaf, finalized)?; | ||
} | ||
|
||
for leaf in leaves { | ||
|
@@ -326,6 +329,10 @@ where | |
let finalized_block_hash = startup_point.finalized_hash; | ||
let finalized_block_runtime = self.generate_runtime_event(finalized_block_hash, None); | ||
|
||
for finalized in &finalized_block_hashes { | ||
self.announced_blocks.insert(*finalized, ()); | ||
} | ||
|
||
let initialized_event = FollowEvent::Initialized(Initialized { | ||
finalized_block_hashes: finalized_block_hashes.into(), | ||
finalized_block_runtime, | ||
|
@@ -336,6 +343,13 @@ where | |
|
||
finalized_block_descendants.push(initialized_event); | ||
for (child, parent) in initial_blocks.into_iter() { | ||
// If the parent was not announced we have a gap currently. | ||
// This can happen during a WarpSync. | ||
if self.announced_blocks.get(&parent).is_none() { | ||
return Err(SubscriptionManagementError::BlockHeaderAbsent); | ||
} | ||
self.announced_blocks.insert(child, ()); | ||
|
||
let new_runtime = self.generate_runtime_event(child, Some(parent)); | ||
|
||
let event = FollowEvent::NewBlock(NewBlock { | ||
|
@@ -351,6 +365,11 @@ where | |
// Generate a new best block event. | ||
let best_block_hash = startup_point.best_hash; | ||
if best_block_hash != finalized_block_hash { | ||
if self.announced_blocks.get(&best_block_hash).is_none() { | ||
return Err(SubscriptionManagementError::BlockHeaderAbsent); | ||
} | ||
self.announced_blocks.insert(best_block_hash, ()); | ||
|
||
let best_block = FollowEvent::BestBlockChanged(BestBlockChanged { best_block_hash }); | ||
self.current_best_block = Some(best_block_hash); | ||
finalized_block_descendants.push(best_block); | ||
|
@@ -408,21 +427,30 @@ where | |
notification: BlockImportNotification<Block>, | ||
startup_point: &StartupPoint<Block>, | ||
) -> Result<Vec<FollowEvent<Block::Hash>>, SubscriptionManagementError> { | ||
// The block was already pinned by the initial block events or by the finalized event. | ||
if !self.sub_handle.pin_block(&self.sub_id, notification.hash)? { | ||
return Ok(Default::default()) | ||
} | ||
let block_hash = notification.hash; | ||
|
||
// Ensure the block is pinned before generating the events. | ||
self.sub_handle.pin_block(&self.sub_id, block_hash)?; | ||
|
||
// Ensure we are only reporting blocks after the starting point. | ||
if *notification.header.number() < startup_point.finalized_number { | ||
return Ok(Default::default()) | ||
} | ||
|
||
Ok(self.generate_import_events( | ||
notification.hash, | ||
*notification.header.parent_hash(), | ||
notification.is_new_best, | ||
)) | ||
if self.announced_blocks.get(&block_hash).is_some() { | ||
// Block was already reported by the finalized branch. | ||
return Ok(Default::default()) | ||
} | ||
|
||
// Double check the parent hash. If the parent hash is not reported, we have a gap. | ||
let parent_block_hash = *notification.header.parent_hash(); | ||
if self.announced_blocks.get(&parent_block_hash).is_none() { | ||
// The parent block was not reported, we have a gap. | ||
return Err(SubscriptionManagementError::Custom("Parent block was not reported".into())) | ||
} | ||
|
||
self.announced_blocks.insert(block_hash, ()); | ||
Ok(self.generate_import_events(block_hash, parent_block_hash, notification.is_new_best)) | ||
} | ||
|
||
/// Generates new block events from the given finalized hashes. | ||
|
@@ -448,19 +476,29 @@ where | |
return Err(SubscriptionManagementError::BlockHeaderAbsent) | ||
}; | ||
|
||
if self.announced_blocks.get(first_header.parent_hash()).is_none() { | ||
return Err(SubscriptionManagementError::Custom( | ||
"Parent block was not reported for a finalized block".into(), | ||
)); | ||
} | ||
|
||
let parents = | ||
std::iter::once(first_header.parent_hash()).chain(finalized_block_hashes.iter()); | ||
for (i, (hash, parent)) in finalized_block_hashes.iter().zip(parents).enumerate() { | ||
// Check if the block was already reported and thus, is already pinned. | ||
if !self.sub_handle.pin_block(&self.sub_id, *hash)? { | ||
continue | ||
// Ensure the block is pinned before generating the events. | ||
self.sub_handle.pin_block(&self.sub_id, *hash)?; | ||
|
||
// Check if the block was already reported. | ||
if self.announced_blocks.get(hash).is_some() { | ||
continue; | ||
} | ||
|
||
// Generate `NewBlock` events for all blocks beside the last block in the list | ||
let is_last = i + 1 == finalized_block_hashes.len(); | ||
if !is_last { | ||
// Generate only the `NewBlock` event for this block. | ||
events.extend(self.generate_import_events(*hash, *parent, false)); | ||
self.announced_blocks.insert(*hash, ()); | ||
continue; | ||
} | ||
|
||
|
@@ -483,7 +521,8 @@ where | |
} | ||
|
||
// Let's generate the `NewBlock` and `NewBestBlock` events for the block. | ||
events.extend(self.generate_import_events(*hash, *parent, true)) | ||
events.extend(self.generate_import_events(*hash, *parent, true)); | ||
self.announced_blocks.insert(*hash, ()); | ||
} | ||
|
||
Ok(events) | ||
|
@@ -545,6 +584,10 @@ where | |
let pruned_block_hashes = | ||
self.get_pruned_hashes(¬ification.stale_heads, last_finalized)?; | ||
|
||
for finalized in &finalized_block_hashes { | ||
self.announced_blocks.insert(*finalized, ()); | ||
} | ||
|
||
let finalized_event = FollowEvent::Finalized(Finalized { | ||
finalized_block_hashes, | ||
pruned_block_hashes: pruned_block_hashes.clone(), | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Believe that
pruned_blocks
are no longer needed (#5605), will tackle this in another PR