mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-04-26 23:57:56 +00:00
a84dd0dba5
Initial implementation for the plan discussed here: https://github.com/paritytech/polkadot-sdk/issues/701 Built on top of https://github.com/paritytech/polkadot-sdk/pull/1178 v0: https://github.com/paritytech/polkadot/pull/7554, ## Overall idea When approval-voting checks a candidate and is ready to advertise the approval, defer it in a per-relay chain block until we either have MAX_APPROVAL_COALESCE_COUNT candidates to sign or a candidate has stayed MAX_APPROVALS_COALESCE_TICKS in the queue, in both cases we sign what candidates we have available. This should allow us to reduce the number of approvals messages we have to create/send/verify. The parameters are configurable, so we should find some values that balance: - Security of the network: Delaying broadcasting of an approval shouldn't but the finality at risk and to make sure that never happens we won't delay sending a vote if we are past 2/3 from the no-show time. - Scalability of the network: MAX_APPROVAL_COALESCE_COUNT = 1 & MAX_APPROVALS_COALESCE_TICKS =0, is what we have now and we know from the measurements we did on versi, it bottlenecks approval-distribution/approval-voting when increase significantly the number of validators and parachains - Block storage: In case of disputes we have to import this votes on chain and that increase the necessary storage with MAX_APPROVAL_COALESCE_COUNT * CandidateHash per vote. Given that disputes are not the normal way of the network functioning and we will limit MAX_APPROVAL_COALESCE_COUNT in the single digits numbers, this should be good enough. Alternatively, we could try to create a better way to store this on-chain through indirection, if that's needed. ## Other fixes: - Fixed the fact that we were sending random assignments to non-validators, that was wrong because those won't do anything with it and they won't gossip it either because they do not have a grid topology set, so we would waste the random assignments. - Added metrics to be able to debug potential no-shows and mis-processing of approvals/assignments. ## TODO: - [x] Get feedback, that this is moving in the right direction. @ordian @sandreim @eskimor @burdges, let me know what you think. - [x] More and more testing. - [x] Test in versi. - [x] Make MAX_APPROVAL_COALESCE_COUNT & MAX_APPROVAL_COALESCE_WAIT_MILLIS a parachain host configuration. - [x] Make sure the backwards compatibility works correctly - [x] Make sure this direction is compatible with other streams of work: https://github.com/paritytech/polkadot-sdk/issues/635 & https://github.com/paritytech/polkadot-sdk/issues/742 - [x] Final versi burn-in before merging --------- Signed-off-by: Alexandru Gheorghe <alexandru.gheorghe@parity.io>
250 lines
8.4 KiB
Rust
250 lines
8.4 KiB
Rust
// Copyright (C) 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_node_subsystem::SubsystemResult;
|
|
use polkadot_primitives::{BlockNumber, CandidateHash, CandidateIndex, Hash};
|
|
|
|
use std::collections::HashMap;
|
|
|
|
use super::{
|
|
approval_db::common::StoredBlockRange,
|
|
persisted_entries::{BlockEntry, CandidateEntry},
|
|
};
|
|
|
|
#[derive(Debug)]
|
|
pub enum BackendWriteOp {
|
|
WriteStoredBlockRange(StoredBlockRange),
|
|
WriteBlocksAtHeight(BlockNumber, Vec<Hash>),
|
|
WriteBlockEntry(BlockEntry),
|
|
WriteCandidateEntry(CandidateEntry),
|
|
DeleteStoredBlockRange,
|
|
DeleteBlocksAtHeight(BlockNumber),
|
|
DeleteBlockEntry(Hash),
|
|
DeleteCandidateEntry(CandidateHash),
|
|
}
|
|
|
|
/// An abstraction over backend storage for the logic of this subsystem.
|
|
/// Implementation must always target latest storage version.
|
|
pub trait Backend {
|
|
/// Load a block entry from the DB.
|
|
fn load_block_entry(&self, hash: &Hash) -> SubsystemResult<Option<BlockEntry>>;
|
|
/// Load a candidate entry from the DB.
|
|
fn load_candidate_entry(
|
|
&self,
|
|
candidate_hash: &CandidateHash,
|
|
) -> SubsystemResult<Option<CandidateEntry>>;
|
|
|
|
/// Load all blocks at a specific height.
|
|
fn load_blocks_at_height(&self, height: &BlockNumber) -> SubsystemResult<Vec<Hash>>;
|
|
/// Load all block from the DB.
|
|
fn load_all_blocks(&self) -> SubsystemResult<Vec<Hash>>;
|
|
/// Load stored block range form the DB.
|
|
fn load_stored_blocks(&self) -> SubsystemResult<Option<StoredBlockRange>>;
|
|
/// Atomically write the list of operations, with later operations taking precedence over prior.
|
|
fn write<I>(&mut self, ops: I) -> SubsystemResult<()>
|
|
where
|
|
I: IntoIterator<Item = BackendWriteOp>;
|
|
}
|
|
|
|
/// A read only backend to enable db migration from version 1 of DB.
|
|
pub trait V1ReadBackend: Backend {
|
|
/// Load a candidate entry from the DB with scheme version 1.
|
|
fn load_candidate_entry_v1(
|
|
&self,
|
|
candidate_hash: &CandidateHash,
|
|
candidate_index: CandidateIndex,
|
|
) -> SubsystemResult<Option<CandidateEntry>>;
|
|
|
|
/// Load a block entry from the DB with scheme version 1.
|
|
fn load_block_entry_v1(&self, block_hash: &Hash) -> SubsystemResult<Option<BlockEntry>>;
|
|
}
|
|
|
|
/// A read only backend to enable db migration from version 2 of DB.
|
|
pub trait V2ReadBackend: Backend {
|
|
/// Load a candidate entry from the DB with scheme version 1.
|
|
fn load_candidate_entry_v2(
|
|
&self,
|
|
candidate_hash: &CandidateHash,
|
|
candidate_index: CandidateIndex,
|
|
) -> SubsystemResult<Option<CandidateEntry>>;
|
|
|
|
/// Load a block entry from the DB with scheme version 1.
|
|
fn load_block_entry_v2(&self, block_hash: &Hash) -> SubsystemResult<Option<BlockEntry>>;
|
|
}
|
|
|
|
// Status of block range in the `OverlayedBackend`.
|
|
#[derive(PartialEq)]
|
|
enum BlockRangeStatus {
|
|
// Value has not been modified.
|
|
NotModified,
|
|
// Value has been deleted
|
|
Deleted,
|
|
// Value has been updated.
|
|
Inserted(StoredBlockRange),
|
|
}
|
|
|
|
/// 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 struct OverlayedBackend<'a, B: 'a> {
|
|
inner: &'a B,
|
|
// `Some(None)` means deleted. Missing (`None`) means query inner.
|
|
stored_block_range: BlockRangeStatus,
|
|
// `None` means 'deleted', missing means query inner.
|
|
blocks_at_height: HashMap<BlockNumber, Option<Vec<Hash>>>,
|
|
// `None` means 'deleted', missing means query inner.
|
|
block_entries: HashMap<Hash, Option<BlockEntry>>,
|
|
// `None` means 'deleted', missing means query inner.
|
|
candidate_entries: HashMap<CandidateHash, Option<CandidateEntry>>,
|
|
}
|
|
|
|
impl<'a, B: 'a + Backend> OverlayedBackend<'a, B> {
|
|
pub fn new(backend: &'a B) -> Self {
|
|
OverlayedBackend {
|
|
inner: backend,
|
|
stored_block_range: BlockRangeStatus::NotModified,
|
|
blocks_at_height: HashMap::new(),
|
|
block_entries: HashMap::new(),
|
|
candidate_entries: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
self.block_entries.is_empty() &&
|
|
self.candidate_entries.is_empty() &&
|
|
self.blocks_at_height.is_empty() &&
|
|
self.stored_block_range == BlockRangeStatus::NotModified
|
|
}
|
|
|
|
pub fn load_all_blocks(&self) -> SubsystemResult<Vec<Hash>> {
|
|
let mut hashes = Vec::new();
|
|
if let Some(stored_blocks) = self.load_stored_blocks()? {
|
|
for height in stored_blocks.0..stored_blocks.1 {
|
|
hashes.extend(self.load_blocks_at_height(&height)?);
|
|
}
|
|
}
|
|
|
|
Ok(hashes)
|
|
}
|
|
|
|
pub fn load_stored_blocks(&self) -> SubsystemResult<Option<StoredBlockRange>> {
|
|
match self.stored_block_range {
|
|
BlockRangeStatus::Inserted(ref value) => Ok(Some(value.clone())),
|
|
BlockRangeStatus::Deleted => Ok(None),
|
|
BlockRangeStatus::NotModified => self.inner.load_stored_blocks(),
|
|
}
|
|
}
|
|
|
|
pub fn load_blocks_at_height(&self, height: &BlockNumber) -> SubsystemResult<Vec<Hash>> {
|
|
if let Some(val) = self.blocks_at_height.get(&height) {
|
|
return Ok(val.clone().unwrap_or_default())
|
|
}
|
|
|
|
self.inner.load_blocks_at_height(height)
|
|
}
|
|
|
|
pub fn load_block_entry(&self, hash: &Hash) -> SubsystemResult<Option<BlockEntry>> {
|
|
if let Some(val) = self.block_entries.get(&hash) {
|
|
return Ok(val.clone())
|
|
}
|
|
|
|
self.inner.load_block_entry(hash)
|
|
}
|
|
|
|
pub fn load_candidate_entry(
|
|
&self,
|
|
candidate_hash: &CandidateHash,
|
|
) -> SubsystemResult<Option<CandidateEntry>> {
|
|
if let Some(val) = self.candidate_entries.get(&candidate_hash) {
|
|
return Ok(val.clone())
|
|
}
|
|
|
|
self.inner.load_candidate_entry(candidate_hash)
|
|
}
|
|
|
|
pub fn write_stored_block_range(&mut self, range: StoredBlockRange) {
|
|
self.stored_block_range = BlockRangeStatus::Inserted(range);
|
|
}
|
|
|
|
pub fn delete_stored_block_range(&mut self) {
|
|
self.stored_block_range = BlockRangeStatus::Deleted;
|
|
}
|
|
|
|
pub fn write_blocks_at_height(&mut self, height: BlockNumber, blocks: Vec<Hash>) {
|
|
self.blocks_at_height.insert(height, Some(blocks));
|
|
}
|
|
|
|
pub fn delete_blocks_at_height(&mut self, height: BlockNumber) {
|
|
self.blocks_at_height.insert(height, None);
|
|
}
|
|
|
|
pub fn write_block_entry(&mut self, entry: BlockEntry) {
|
|
self.block_entries.insert(entry.block_hash(), Some(entry));
|
|
}
|
|
|
|
pub fn delete_block_entry(&mut self, hash: &Hash) {
|
|
self.block_entries.insert(*hash, None);
|
|
}
|
|
|
|
pub fn write_candidate_entry(&mut self, entry: CandidateEntry) {
|
|
self.candidate_entries.insert(entry.candidate_receipt().hash(), Some(entry));
|
|
}
|
|
|
|
pub fn delete_candidate_entry(&mut self, hash: &CandidateHash) {
|
|
self.candidate_entries.insert(*hash, None);
|
|
}
|
|
|
|
/// Transform this backend into a set of write-ops to be written to the
|
|
/// inner backend.
|
|
pub fn into_write_ops(self) -> impl Iterator<Item = BackendWriteOp> {
|
|
let blocks_at_height_ops = self.blocks_at_height.into_iter().map(|(h, v)| match v {
|
|
Some(v) => BackendWriteOp::WriteBlocksAtHeight(h, v),
|
|
None => BackendWriteOp::DeleteBlocksAtHeight(h),
|
|
});
|
|
|
|
let block_entry_ops = self.block_entries.into_iter().map(|(h, v)| match v {
|
|
Some(v) => BackendWriteOp::WriteBlockEntry(v),
|
|
None => BackendWriteOp::DeleteBlockEntry(h),
|
|
});
|
|
|
|
let candidate_entry_ops = self.candidate_entries.into_iter().map(|(h, v)| match v {
|
|
Some(v) => BackendWriteOp::WriteCandidateEntry(v),
|
|
None => BackendWriteOp::DeleteCandidateEntry(h),
|
|
});
|
|
|
|
let stored_block_range_ops = match self.stored_block_range {
|
|
BlockRangeStatus::Inserted(val) => Some(BackendWriteOp::WriteStoredBlockRange(val)),
|
|
BlockRangeStatus::Deleted => Some(BackendWriteOp::DeleteStoredBlockRange),
|
|
BlockRangeStatus::NotModified => None,
|
|
};
|
|
|
|
stored_block_range_ops
|
|
.into_iter()
|
|
.chain(blocks_at_height_ops)
|
|
.chain(block_entry_ops)
|
|
.chain(candidate_entry_ops)
|
|
}
|
|
}
|