Skip to content

Commit 12f7aa7

Browse files
authored
[1.13.0 backport] finalization: Skip tree route calculation if no forks present (#4760)
Backport of #4721
1 parent 13a7ae7 commit 12f7aa7

File tree

4 files changed

+198
-8
lines changed

4 files changed

+198
-8
lines changed

prdoc/pr_4721.prdoc

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Schema: Polkadot SDK PRDoc Schema (prdoc) v1.0.0
2+
# See doc at https://raw.githubusercontent.com/paritytech/polkadot-sdk/master/prdoc/schema_user.json
3+
4+
title: Skip tree route calculation if no forks present
5+
6+
doc:
7+
- audience: Node Operator
8+
description: |
9+
Fixes an issue with synchronisation on parachains. Once they reached the tip of the chain,
10+
nodes would show `Preparing 0.0 bps`. This is shown because the node is blocked on calculating
11+
the tree route from genesis to the tip of the chain many times. This PR solves that by skipping
12+
tree route calculation if there is only one leave. In addition, further optimizations have been
13+
done to alleviate long finalization distances.
14+
15+
crates:
16+
- name: sp-blockchain
17+
bump: minor
18+
- name: sc-client-db
19+
bump: none

substrate/client/db/src/lib.rs

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2547,7 +2547,7 @@ pub(crate) mod tests {
25472547
backend::{Backend as BTrait, BlockImportOperation as Op},
25482548
blockchain::Backend as BLBTrait,
25492549
};
2550-
use sp_blockchain::{lowest_common_ancestor, tree_route};
2550+
use sp_blockchain::{lowest_common_ancestor, lowest_common_ancestor_multiblock, tree_route};
25512551
use sp_core::H256;
25522552
use sp_runtime::{
25532553
testing::{Block as RawBlock, ExtrinsicWrapper, Header},
@@ -3108,6 +3108,125 @@ pub(crate) mod tests {
31083108
}
31093109
}
31103110

3111+
#[test]
3112+
fn lowest_common_ancestors_multiblock_works() {
3113+
let backend = Backend::<Block>::new_test(1000, 100);
3114+
let blockchain = backend.blockchain();
3115+
let block0 = insert_header(&backend, 0, Default::default(), None, Default::default());
3116+
3117+
// fork from genesis: 3 prong.
3118+
// block 0 -> a1 -> a2 -> a3
3119+
// |
3120+
// -> b1 -> b2 -> c1 -> c2
3121+
// |
3122+
// -> d1 -> d2
3123+
let a1 = insert_header(&backend, 1, block0, None, Default::default());
3124+
let a2 = insert_header(&backend, 2, a1, None, Default::default());
3125+
let a3 = insert_header(&backend, 3, a2, None, Default::default());
3126+
3127+
// fork from genesis: 2 prong.
3128+
let b1 = insert_header(&backend, 1, block0, None, H256::from([1; 32]));
3129+
let b2 = insert_header(&backend, 2, b1, None, Default::default());
3130+
3131+
// fork from b2.
3132+
let c1 = insert_header(&backend, 3, b2, None, H256::from([2; 32]));
3133+
let c2 = insert_header(&backend, 4, c1, None, Default::default());
3134+
3135+
// fork from b1.
3136+
let d1 = insert_header(&backend, 2, b1, None, H256::from([3; 32]));
3137+
let d2 = insert_header(&backend, 3, d1, None, Default::default());
3138+
{
3139+
let lca = lowest_common_ancestor_multiblock(blockchain, vec![a3, b2]).unwrap().unwrap();
3140+
3141+
assert_eq!(lca.hash, block0);
3142+
assert_eq!(lca.number, 0);
3143+
}
3144+
3145+
{
3146+
let lca = lowest_common_ancestor_multiblock(blockchain, vec![a1, a3]).unwrap().unwrap();
3147+
3148+
assert_eq!(lca.hash, a1);
3149+
assert_eq!(lca.number, 1);
3150+
}
3151+
3152+
{
3153+
let lca = lowest_common_ancestor_multiblock(blockchain, vec![a3, a1]).unwrap().unwrap();
3154+
3155+
assert_eq!(lca.hash, a1);
3156+
assert_eq!(lca.number, 1);
3157+
}
3158+
3159+
{
3160+
let lca = lowest_common_ancestor_multiblock(blockchain, vec![a2, a3]).unwrap().unwrap();
3161+
3162+
assert_eq!(lca.hash, a2);
3163+
assert_eq!(lca.number, 2);
3164+
}
3165+
3166+
{
3167+
let lca = lowest_common_ancestor_multiblock(blockchain, vec![a2, a1]).unwrap().unwrap();
3168+
3169+
assert_eq!(lca.hash, a1);
3170+
assert_eq!(lca.number, 1);
3171+
}
3172+
3173+
{
3174+
let lca = lowest_common_ancestor_multiblock(blockchain, vec![a2, a2]).unwrap().unwrap();
3175+
3176+
assert_eq!(lca.hash, a2);
3177+
assert_eq!(lca.number, 2);
3178+
}
3179+
3180+
{
3181+
let lca = lowest_common_ancestor_multiblock(blockchain, vec![a3, d2, c2])
3182+
.unwrap()
3183+
.unwrap();
3184+
3185+
assert_eq!(lca.hash, block0);
3186+
assert_eq!(lca.number, 0);
3187+
}
3188+
3189+
{
3190+
let lca = lowest_common_ancestor_multiblock(blockchain, vec![c2, d2, b2])
3191+
.unwrap()
3192+
.unwrap();
3193+
3194+
assert_eq!(lca.hash, b1);
3195+
assert_eq!(lca.number, 1);
3196+
}
3197+
3198+
{
3199+
let lca = lowest_common_ancestor_multiblock(blockchain, vec![a1, a2, a3])
3200+
.unwrap()
3201+
.unwrap();
3202+
3203+
assert_eq!(lca.hash, a1);
3204+
assert_eq!(lca.number, 1);
3205+
}
3206+
3207+
{
3208+
let lca = lowest_common_ancestor_multiblock(blockchain, vec![b1, b2, d1])
3209+
.unwrap()
3210+
.unwrap();
3211+
3212+
assert_eq!(lca.hash, b1);
3213+
assert_eq!(lca.number, 1);
3214+
}
3215+
3216+
{
3217+
let lca = lowest_common_ancestor_multiblock(blockchain, vec![]);
3218+
3219+
assert_eq!(true, matches!(lca, Ok(None)));
3220+
}
3221+
3222+
{
3223+
let lca = lowest_common_ancestor_multiblock(blockchain, vec![a1]).unwrap().unwrap();
3224+
3225+
assert_eq!(lca.hash, a1);
3226+
assert_eq!(lca.number, 1);
3227+
}
3228+
}
3229+
31113230
#[test]
31123231
fn test_tree_route_regression() {
31133232
// NOTE: this is a test for a regression introduced in #3665, the result

substrate/primitives/blockchain/src/backend.rs

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,15 @@ use log::warn;
2121
use parking_lot::RwLock;
2222
use sp_runtime::{
2323
generic::BlockId,
24-
traits::{Block as BlockT, Header as HeaderT, NumberFor, Zero},
24+
traits::{Block as BlockT, CheckedSub, Header as HeaderT, NumberFor, Zero},
2525
Justifications,
2626
};
2727
use std::collections::{btree_map::BTreeMap, btree_set::BTreeSet};
2828

29-
use crate::header_metadata::HeaderMetadata;
30-
3129
use crate::{
3230
error::{Error, Result},
33-
tree_route, TreeRoute,
31+
header_metadata::{self, HeaderMetadata},
32+
lowest_common_ancestor_multiblock, tree_route, TreeRoute,
3433
};
3534

3635
/// Blockchain database header backend. Does not perform any validation.
@@ -229,12 +228,41 @@ pub trait Backend<Block: BlockT>:
229228
) -> std::result::Result<DisplacedLeavesAfterFinalization<Block>, Error> {
230229
let mut result = DisplacedLeavesAfterFinalization::default();
231230

232-
if finalized_block_number == Zero::zero() {
231+
let leaves = self.leaves()?;
232+
233+
// If we have only one leaf there are no forks, and we can return early.
234+
if finalized_block_number == Zero::zero() || leaves.len() == 1 {
233235
return Ok(result)
234236
}
235237

238+
let first_leaf = leaves.first().ok_or(Error::Backend(
239+
"Unable to find any leaves. This should not happen.".to_string(),
240+
))?;
241+
let leaf_block_header = self.expect_header(*first_leaf)?;
242+
243+
// If the distance between the leafs and the finalized block is large, calculating
244+
// tree routes can be very expensive. In that case, we will try to find the
245+
// lowest common ancestor between all the leaves. The assumption here is that the forks are
246+
// close to the tip and not long. So the LCA can be computed from the header cache. If the
247+
// LCA is above the finalized block, we know that there are no displaced leaves by the
248+
// finalization.
249+
if leaf_block_header
250+
.number()
251+
.checked_sub(&finalized_block_number)
252+
.unwrap_or(0u32.into()) >
253+
header_metadata::LRU_CACHE_SIZE.into()
254+
{
255+
if let Some(lca) = lowest_common_ancestor_multiblock(self, leaves.clone())? {
256+
if lca.number > finalized_block_number {
257+
return Ok(result)
258+
} else {
259+
log::warn!("The distance between leafs and finalized block is large. Finalization can take a long time.");
260+
}
261+
};
262+
}
263+
236264
// For each leaf determine whether it belongs to a non-canonical branch.
237-
for leaf_hash in self.leaves()? {
265+
for leaf_hash in leaves {
238266
let leaf_block_header = self.expect_header(leaf_hash)?;
239267
let leaf_number = *leaf_block_header.number();
240268

substrate/primitives/blockchain/src/header_metadata.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use schnellru::{ByLength, LruMap};
2323
use sp_runtime::traits::{Block as BlockT, Header, NumberFor, One};
2424

2525
/// Set to the expected max difference between `best` and `finalized` blocks at sync.
26-
const LRU_CACHE_SIZE: u32 = 5_000;
26+
pub(crate) const LRU_CACHE_SIZE: u32 = 5_000;
2727

2828
/// Get lowest common ancestor between two blocks in the tree.
2929
///
@@ -96,6 +96,30 @@ pub fn lowest_common_ancestor<Block: BlockT, T: HeaderMetadata<Block> + ?Sized>(
9696
Ok(HashAndNumber { hash: header_one.hash, number: header_one.number })
9797
}
9898

99+
/// Get lowest common ancestor between multiple blocks.
100+
pub fn lowest_common_ancestor_multiblock<Block: BlockT, T: HeaderMetadata<Block> + ?Sized>(
101+
backend: &T,
102+
hashes: Vec<Block::Hash>,
103+
) -> Result<Option<HashAndNumber<Block>>, T::Error> {
104+
// Ensure the list of hashes is not empty
105+
let mut hashes_iter = hashes.into_iter();
106+
107+
let first_hash = match hashes_iter.next() {
108+
Some(hash) => hash,
109+
None => return Ok(None),
110+
};
111+
112+
// Start with the first hash as the initial LCA
113+
let first_cached = backend.header_metadata(first_hash)?;
114+
let mut lca = HashAndNumber { number: first_cached.number, hash: first_cached.hash };
115+
for hash in hashes_iter {
116+
// Calculate the LCA of the current LCA and the next hash
117+
lca = lowest_common_ancestor(backend, lca.hash, hash)?;
118+
}
119+
120+
Ok(Some(lca))
121+
}
122+
99123
/// Compute a tree-route between two blocks. See tree-route docs for more details.
100124
pub fn tree_route<Block: BlockT, T: HeaderMetadata<Block> + ?Sized>(
101125
backend: &T,

0 commit comments

Comments
 (0)