Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

Chain Selection Subsystem Logic #3277

Merged
85 commits merged into from
Jun 21, 2021
Merged
Show file tree
Hide file tree
Changes from 82 commits
Commits
Show all changes
85 commits
Select commit Hold shift + click to select a range
66bf4b2
crate skeleton and type definitions
rphmeier Jun 16, 2021
dfb09e5
add ChainSelectionMessage
rphmeier Jun 16, 2021
5a474eb
add error type
rphmeier Jun 16, 2021
9c2340b
run loop
rphmeier Jun 16, 2021
3c49a32
fix overseer
rphmeier Jun 16, 2021
06e92a6
simplify determine_new_blocks API
rphmeier Jun 16, 2021
a2b0250
write an overlay struct and fetch new blocks
rphmeier Jun 16, 2021
8790e52
add new function to overlay
rphmeier Jun 16, 2021
d0f0f45
more flow
rphmeier Jun 16, 2021
60196bf
add leaves to overlay and add a strong type around leaves-set
rphmeier Jun 16, 2021
a023348
add is_parent_viable
rphmeier Jun 16, 2021
537e59b
implement block import, ignoring reversions
rphmeier Jun 16, 2021
22705aa
add stagnant-at to overlay
rphmeier Jun 16, 2021
c71d5b2
add stagnant
rphmeier Jun 17, 2021
422afc4
add revert consensus log
rphmeier Jun 17, 2021
70e5d8a
flow for reversions
rphmeier Jun 17, 2021
a792b1c
extract and import block reversions
rphmeier Jun 17, 2021
3487cf3
recursively update viability
rphmeier Jun 17, 2021
4a1f610
remove redundant parameter from WriteBlockEntry
rphmeier Jun 17, 2021
ae74536
do some removal of viable leaves
rphmeier Jun 17, 2021
b678828
address grumbles
rphmeier Jun 17, 2021
645327a
refactor
rphmeier Jun 17, 2021
2a40721
address grumbles
rphmeier Jun 17, 2021
f76d410
add comment about non-monotonicity
rphmeier Jun 17, 2021
2602e50
extract backend to submodule
rphmeier Jun 17, 2021
efc0963
begin the hunt for viable leaves
rphmeier Jun 17, 2021
5ec0064
viability pivots for updating the active leaves
rphmeier Jun 17, 2021
750b8d7
remove LeafSearchFrontier
rphmeier Jun 17, 2021
bb1de40
partially -> explicitly viable and untwist some booleans
rphmeier Jun 17, 2021
8f3c533
extract tree to submodule
rphmeier Jun 17, 2021
ec48118
implement block finality update
rphmeier Jun 17, 2021
82c1817
Implement block approval routine
rphmeier Jun 17, 2021
6f2edf3
implement stagnant detection
rphmeier Jun 17, 2021
a1d9169
ensure blocks pruned on finality are removed from the active leaves set
rphmeier Jun 17, 2021
a969a2b
write down some planned test cases
rphmeier Jun 17, 2021
6ed7ac4
floww
rphmeier Jun 17, 2021
0577ed6
leaf loading
rphmeier Jun 17, 2021
c10d52c
implement best_leaf_containing
rphmeier Jun 17, 2021
5407dae
write down a few more tests to do
rphmeier Jun 17, 2021
53e4539
Merge branch 'master' into rh-chain-selection-subsystem
rphmeier Jun 17, 2021
f975bf2
remove dependence of tree on header
rphmeier Jun 17, 2021
2518a17
guide: ChainApiMessage::BlockWeight
rphmeier Jun 18, 2021
32d73a1
node: BlockWeight ChainAPI
rphmeier Jun 18, 2021
ca45cc3
fix compile issue
rphmeier Jun 18, 2021
d08f418
Merge branch 'rh-block-weight-message' into rh-chain-selection-subsystem
rphmeier Jun 18, 2021
8a97b71
note a few TODOs for the future
rphmeier Jun 18, 2021
05dc72a
fetch block weight using new BlockWeight ChainAPI
rphmeier Jun 18, 2021
f95d23e
implement unimplemented
rphmeier Jun 18, 2021
5c2d2d9
sort leaves by block number after weight
rphmeier Jun 18, 2021
fc13846
remove warnings and add more TODOs
rphmeier Jun 18, 2021
fabc28d
create test module
rphmeier Jun 18, 2021
46d2416
storage for test backend
rphmeier Jun 18, 2021
14e8518
wrap inner in mutex
rphmeier Jun 18, 2021
c4457ae
add write waker query to test backend
rphmeier Jun 18, 2021
6308490
Add OverseerSignal -> FromOverseer conversion
rphmeier Jun 18, 2021
45a07c8
add test harnes
rphmeier Jun 18, 2021
f439ed7
add no-op test
rphmeier Jun 18, 2021
d163da5
add some more test helpers
rphmeier Jun 18, 2021
c838388
the first test
rphmeier Jun 18, 2021
b8c12ee
more progress on tests
rphmeier Jun 19, 2021
a05cb2b
test two subtrees
rphmeier Jun 19, 2021
1711f11
determine-new-blocks: cleaner genesis avoidance and tighter ancestry …
rphmeier Jun 19, 2021
0586dbc
don't make ancestry requests when asking for one block
rphmeier Jun 19, 2021
d73d621
add a couple more tests
rphmeier Jun 19, 2021
1185b75
Merge branch 'master' into rh-chain-selection-subsystem
rphmeier Jun 19, 2021
2dd5358
add to AllMessages in guide
rphmeier Jun 19, 2021
f42230c
remove bad spaces from bridge
rphmeier Jun 19, 2021
99b4392
compact iterator
rphmeier Jun 19, 2021
5afd66a
test import with gaps
rphmeier Jun 19, 2021
bf15a67
more reversion tests
rphmeier Jun 19, 2021
1aabcb3
test finalization pruning subtrees
rphmeier Jun 19, 2021
8dff999
fixups
rphmeier Jun 19, 2021
16c26e8
test clobbering and fix bug in overlay
rphmeier Jun 19, 2021
2b075b1
exhaustive backend state after finalizaiton tested
rphmeier Jun 19, 2021
5a7abf9
more finality tests
rphmeier Jun 20, 2021
a512985
leaf tests
rphmeier Jun 20, 2021
cb2d136
test approval
rphmeier Jun 20, 2021
4e01714
test ChainSelectionMessage::Leaves thoroughly
rphmeier Jun 20, 2021
9a16ffc
remove TODO
rphmeier Jun 20, 2021
31f47de
avoid Ordering::is_ne so CI can build
rphmeier Jun 20, 2021
eeb4c89
comment algorithmic complexity
rphmeier Jun 20, 2021
085b612
Merge branch 'master' into rh-chain-selection-subsystem
rphmeier Jun 20, 2021
4132129
Update node/core/chain-selection/src/lib.rs
rphmeier Jun 21, 2021
16ceda8
Merge branch 'master' into rh-chain-selection-subsystem
rphmeier Jun 21, 2021
0465903
Merge branch 'rh-chain-selection-subsystem' of https://github.com/par…
rphmeier Jun 21, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ members = [
"node/core/bitfield-signing",
"node/core/candidate-validation",
"node/core/chain-api",
"node/core/chain-selection",
"node/core/dispute-coordinator",
"node/core/dispute-participation",
"node/core/parachains-inherent",
Expand Down
23 changes: 23 additions & 0 deletions node/core/chain-selection/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[package]
name = "polkadot-node-core-chain-selection"
description = "Chain Selection Subsystem"
version = "0.1.0"
authors = ["Parity Technologies <admin@parity.io>"]
edition = "2018"

[dependencies]
futures = "0.3.15"
tracing = "0.1.26"
polkadot-primitives = { path = "../../../primitives" }
polkadot-node-primitives = { path = "../../primitives" }
polkadot-subsystem = { package = "polkadot-node-subsystem", path = "../../subsystem" }
polkadot-node-subsystem-util = { path = "../../subsystem-util" }
kvdb = "0.9.0"
thiserror = "1.0.23"
parity-scale-codec = "2"

[dev-dependencies]
polkadot-node-subsystem-test-helpers = { path = "../../subsystem-test-helpers" }
sp-core = { git = "https://github.com/paritytech/substrate", branch = "master" }
parking_lot = "0.11"
assert_matches = "1"
235 changes: 235 additions & 0 deletions node/core/chain-selection/src/backend.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
// Copyright 2021 Parity Technologies (UK) Ltd.
// This file is part of Polkadot.

// Polkadot is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// Polkadot is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.

//! An abstraction over storage used by the chain selection subsystem.
//!
//! This provides both a [`Backend`] trait and an [`OverlayedBackend`]
//! struct which allows in-memory changes to be applied on top of a
//! [`Backend`], maintaining consistency between queries and temporary writes,
//! before any commit to the underlying storage is made.

use polkadot_primitives::v1::{BlockNumber, Hash};

use std::collections::HashMap;

use crate::{Error, LeafEntrySet, BlockEntry, Timestamp};

pub(super) enum BackendWriteOp {
WriteBlockEntry(BlockEntry),
WriteBlocksByNumber(BlockNumber, Vec<Hash>),
WriteViableLeaves(LeafEntrySet),
WriteStagnantAt(Timestamp, Vec<Hash>),
DeleteBlocksByNumber(BlockNumber),
DeleteBlockEntry(Hash),
DeleteStagnantAt(Timestamp),
}

/// An abstraction over backend storage for the logic of this subsystem.
pub(super) trait Backend {
/// Load a block entry from the DB.
fn load_block_entry(&self, hash: &Hash) -> Result<Option<BlockEntry>, Error>;
/// Load the active-leaves set.
fn load_leaves(&self) -> Result<LeafEntrySet, Error>;
/// Load the stagnant list at the given timestamp.
fn load_stagnant_at(&self, timestamp: Timestamp) -> Result<Vec<Hash>, Error>;
/// Load all stagnant lists up to and including the given unix timestamp
/// in ascending order.
fn load_stagnant_at_up_to(&self, up_to: Timestamp)
-> Result<Vec<(Timestamp, Vec<Hash>)>, Error>;
/// Load the earliest kept block number.
fn load_first_block_number(&self) -> Result<Option<BlockNumber>, Error>;
/// Load blocks by number.
fn load_blocks_by_number(&self, number: BlockNumber) -> Result<Vec<Hash>, Error>;

/// Atomically write the list of operations, with later operations taking precedence over prior.
fn write<I>(&mut self, ops: I) -> Result<(), Error>
where I: IntoIterator<Item = BackendWriteOp>;
}

/// An in-memory overlay over the backend.
///
/// This maintains read-only access to the underlying backend, but can be
/// converted into a set of write operations which will, when written to
/// the underlying backend, give the same view as the state of the overlay.
pub(super) struct OverlayedBackend<'a, B: 'a> {
inner: &'a B,

// `None` means 'deleted', missing means query inner.
block_entries: HashMap<Hash, Option<BlockEntry>>,
// `None` means 'deleted', missing means query inner.
blocks_by_number: HashMap<BlockNumber, Option<Vec<Hash>>>,
// 'None' means 'deleted', missing means query inner.
stagnant_at: HashMap<Timestamp, Option<Vec<Hash>>>,
// 'None' means query inner.
leaves: Option<LeafEntrySet>,
}

impl<'a, B: 'a + Backend> OverlayedBackend<'a, B> {
pub(super) fn new(backend: &'a B) -> Self {
OverlayedBackend {
inner: backend,
block_entries: HashMap::new(),
blocks_by_number: HashMap::new(),
stagnant_at: HashMap::new(),
leaves: None,
}
}

pub(super) fn load_block_entry(&self, hash: &Hash) -> Result<Option<BlockEntry>, Error> {
if let Some(val) = self.block_entries.get(&hash) {
return Ok(val.clone())
}

self.inner.load_block_entry(hash)
ordian marked this conversation as resolved.
Show resolved Hide resolved
}

pub(super) fn load_blocks_by_number(&self, number: BlockNumber) -> Result<Vec<Hash>, Error> {
if let Some(val) = self.blocks_by_number.get(&number) {
return Ok(val.as_ref().map_or(Vec::new(), Clone::clone));
}

self.inner.load_blocks_by_number(number)
}

pub(super) fn load_leaves(&self) -> Result<LeafEntrySet, Error> {
if let Some(ref set) = self.leaves {
return Ok(set.clone())
}

self.inner.load_leaves()
}

pub(super) fn load_stagnant_at(&self, timestamp: Timestamp) -> Result<Vec<Hash>, Error> {
if let Some(val) = self.stagnant_at.get(&timestamp) {
return Ok(val.as_ref().map_or(Vec::new(), Clone::clone));
}

self.inner.load_stagnant_at(timestamp)
}

pub(super) fn write_block_entry(&mut self, entry: BlockEntry) {
self.block_entries.insert(entry.block_hash, Some(entry));
}

pub(super) fn delete_block_entry(&mut self, hash: &Hash) {
self.block_entries.insert(*hash, None);
}

pub(super) fn write_blocks_by_number(&mut self, number: BlockNumber, blocks: Vec<Hash>) {
if blocks.is_empty() {
self.blocks_by_number.insert(number, None);
} else {
self.blocks_by_number.insert(number, Some(blocks));
}
}

pub(super) fn delete_blocks_by_number(&mut self, number: BlockNumber) {
self.blocks_by_number.insert(number, None);
}

pub(super) fn write_leaves(&mut self, leaves: LeafEntrySet) {
self.leaves = Some(leaves);
}

pub(super) fn write_stagnant_at(&mut self, timestamp: Timestamp, hashes: Vec<Hash>) {
self.stagnant_at.insert(timestamp, Some(hashes));
}

pub(super) fn delete_stagnant_at(&mut self, timestamp: Timestamp) {
self.stagnant_at.insert(timestamp, None);
}

/// Transform this backend into a set of write-ops to be written to the
/// inner backend.
pub(super) fn into_write_ops(self) -> impl Iterator<Item = BackendWriteOp> {
let block_entry_ops = self.block_entries.into_iter().map(|(h, v)| match v {
Some(v) => BackendWriteOp::WriteBlockEntry(v),
None => BackendWriteOp::DeleteBlockEntry(h),
});

let blocks_by_number_ops = self.blocks_by_number.into_iter().map(|(n, v)| match v {
Some(v) => BackendWriteOp::WriteBlocksByNumber(n, v),
None => BackendWriteOp::DeleteBlocksByNumber(n),
});

let leaf_ops = self.leaves.into_iter().map(BackendWriteOp::WriteViableLeaves);

let stagnant_at_ops = self.stagnant_at.into_iter().map(|(n, v)| match v {
Some(v) => BackendWriteOp::WriteStagnantAt(n, v),
None => BackendWriteOp::DeleteStagnantAt(n),
});

block_entry_ops
.chain(blocks_by_number_ops)
.chain(leaf_ops)
.chain(stagnant_at_ops)
}
}

/// Attempt to find the given ancestor in the chain with given head.
///
/// If the ancestor is the most recently finalized block, and the `head` is
/// a known unfinalized block, this will return `true`.
///
/// If the ancestor is an unfinalized block and `head` is known, this will
/// return true if `ancestor` is in `head`'s chain.
///
/// If the ancestor is an older finalized block, this will return `false`.
fn contains_ancestor(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be part of the Backend.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I can see that, but I believe it's simpler right now to expose a minimal Backend API and build on top of that. We're going to be disk-bound by this anyway.

Copy link
Contributor

@Lldenaurois Lldenaurois Jun 21, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is \Omega(block_height) in the worst case, so if someone were to pass in the genesis hash we would basically read the entire chain, right?

I wonder whether it makes sense to put a limit on the depth of this loop...

Copy link
Contributor Author

@rphmeier rphmeier Jun 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Lldenaurois It's actually O(block_height - finalized_height) because we only store unfinalized subtrees here. Pretty much all the algorithms here have the same complexity but as long as we don't have thousands of unfinalized blocks they'll work fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now it's more inefficient than it needs to be but we could easily keep a HashSet<Hash> of block-entries we've visited already and bail early when we encounter a parent hash we've already seen. Most chains will have some common ancestor before the last finalized block.

backend: &impl Backend,
head: Hash,
ancestor: Hash,
) -> Result<bool, Error> {
let mut current_hash = head;
loop {
if current_hash == ancestor { return Ok(true) }
match backend.load_block_entry(&current_hash)? {
Some(e) => { current_hash = e.parent_hash }
None => break
}
}

Ok(false)
}
ordian marked this conversation as resolved.
Show resolved Hide resolved

/// This returns the best unfinalized leaf containing the required block.
///
/// If the required block is finalized but not the most recent finalized block,
/// this will return `None`.
///
/// If the required block is unfinalized but not an ancestor of any viable leaf,
/// this will return `None`.
//
// Note: this is O(N^2) in the depth of `required` and the number of leaves.
// We expect the number of unfinalized blocks to be small, as in, to not exceed
// single digits in practice, and exceedingly unlikely to surpass 1000.
//
// However, if we need to, we could implement some type of skip-list for
// fast ancestry checks.
pub(super) fn find_best_leaf_containing(
backend: &impl Backend,
required: Hash,
) -> Result<Option<Hash>, Error> {
let leaves = backend.load_leaves()?;
for leaf in leaves.into_hashes_descending() {
if contains_ancestor(backend, leaf, required)? {
return Ok(Some(leaf))
}
}
ordian marked this conversation as resolved.
Show resolved Hide resolved

// If there are no viable leaves containing the ancestor
Ok(None)
}
Loading