feat: Rebrand Polkadot/Substrate references to PezkuwiChain
This commit systematically rebrands various references from Parity Technologies' Polkadot/Substrate ecosystem to PezkuwiChain within the kurdistan-sdk. Key changes include: - Updated external repository URLs (zombienet-sdk, parity-db, parity-scale-codec, wasm-instrument) to point to pezkuwichain forks. - Modified internal documentation and code comments to reflect PezkuwiChain naming and structure. - Replaced direct references to with or specific paths within the for XCM, Pezkuwi, and other modules. - Cleaned up deprecated issue and PR references in various and files, particularly in and modules. - Adjusted image and logo URLs in documentation to point to PezkuwiChain assets. - Removed or rephrased comments related to external Polkadot/Substrate PRs and issues. This is a significant step towards fully customizing the SDK for the PezkuwiChain ecosystem.
This commit is contained in:
@@ -0,0 +1,410 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! [`BlockAnnounceValidator`] is responsible for async validation of block announcements.
|
||||
//! [`Stream`] implemented by [`BlockAnnounceValidator`] never terminates.
|
||||
|
||||
use crate::{futures_stream::FuturesStream, LOG_TARGET};
|
||||
use futures::{stream::FusedStream, Future, FutureExt, Stream, StreamExt};
|
||||
use log::{debug, error, trace, warn};
|
||||
use pezsc_network_common::sync::message::BlockAnnounce;
|
||||
use pezsc_network_types::PeerId;
|
||||
use pezsp_consensus::block_validation::Validation;
|
||||
use pezsp_runtime::traits::{Block as BlockT, Header, Zero};
|
||||
use std::{
|
||||
collections::{hash_map::Entry, HashMap},
|
||||
default::Default,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
/// Maximum number of concurrent block announce validations.
|
||||
///
|
||||
/// If the queue reaches the maximum, we drop any new block
|
||||
/// announcements.
|
||||
const MAX_CONCURRENT_BLOCK_ANNOUNCE_VALIDATIONS: usize = 256;
|
||||
|
||||
/// Maximum number of concurrent block announce validations per peer.
|
||||
///
|
||||
/// See [`MAX_CONCURRENT_BLOCK_ANNOUNCE_VALIDATIONS`] for more information.
|
||||
const MAX_CONCURRENT_BLOCK_ANNOUNCE_VALIDATIONS_PER_PEER: usize = 4;
|
||||
|
||||
/// Item that yields [`Stream`] implementation of [`BlockAnnounceValidator`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) enum BlockAnnounceValidationResult<H> {
|
||||
/// The announcement failed at validation.
|
||||
///
|
||||
/// The peer reputation should be decreased.
|
||||
Failure {
|
||||
/// The id of the peer that send us the announcement.
|
||||
peer_id: PeerId,
|
||||
/// Should the peer be disconnected?
|
||||
disconnect: bool,
|
||||
},
|
||||
/// The announcement was validated successfully and should be passed to [`crate::ChainSync`].
|
||||
Process {
|
||||
/// The id of the peer that send us the announcement.
|
||||
peer_id: PeerId,
|
||||
/// Was this their new best block?
|
||||
is_new_best: bool,
|
||||
/// The announcement.
|
||||
announce: BlockAnnounce<H>,
|
||||
},
|
||||
/// The block announcement should be skipped.
|
||||
Skip {
|
||||
/// The id of the peer that send us the announcement.
|
||||
peer_id: PeerId,
|
||||
},
|
||||
}
|
||||
|
||||
impl<H> BlockAnnounceValidationResult<H> {
|
||||
fn peer_id(&self) -> &PeerId {
|
||||
match self {
|
||||
BlockAnnounceValidationResult::Failure { peer_id, .. } |
|
||||
BlockAnnounceValidationResult::Process { peer_id, .. } |
|
||||
BlockAnnounceValidationResult::Skip { peer_id } => peer_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of [`BlockAnnounceValidator::allocate_slot_for_block_announce_validation`].
|
||||
enum AllocateSlotForBlockAnnounceValidation {
|
||||
/// Success, there is a slot for the block announce validation.
|
||||
Allocated,
|
||||
/// We reached the total maximum number of validation slots.
|
||||
TotalMaximumSlotsReached,
|
||||
/// We reached the maximum number of validation slots for the given peer.
|
||||
MaximumPeerSlotsReached,
|
||||
}
|
||||
|
||||
pub(crate) struct BlockAnnounceValidator<B: BlockT> {
|
||||
/// A type to check incoming block announcements.
|
||||
validator: Box<dyn pezsp_consensus::block_validation::BlockAnnounceValidator<B> + Send>,
|
||||
/// All block announcements that are currently being validated.
|
||||
validations: FuturesStream<
|
||||
Pin<Box<dyn Future<Output = BlockAnnounceValidationResult<B::Header>> + Send>>,
|
||||
>,
|
||||
/// Number of concurrent block announce validations per peer.
|
||||
validations_per_peer: HashMap<PeerId, usize>,
|
||||
}
|
||||
|
||||
impl<B: BlockT> BlockAnnounceValidator<B> {
|
||||
pub(crate) fn new(
|
||||
validator: Box<dyn pezsp_consensus::block_validation::BlockAnnounceValidator<B> + Send>,
|
||||
) -> Self {
|
||||
Self {
|
||||
validator,
|
||||
validations: Default::default(),
|
||||
validations_per_peer: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Push a block announce validation.
|
||||
pub(crate) fn push_block_announce_validation(
|
||||
&mut self,
|
||||
peer_id: PeerId,
|
||||
hash: B::Hash,
|
||||
announce: BlockAnnounce<B::Header>,
|
||||
is_best: bool,
|
||||
) {
|
||||
let header = &announce.header;
|
||||
let number = *header.number();
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"Pre-validating received block announcement {:?} with number {:?} from {}",
|
||||
hash,
|
||||
number,
|
||||
peer_id,
|
||||
);
|
||||
|
||||
if number.is_zero() {
|
||||
warn!(
|
||||
target: LOG_TARGET,
|
||||
"💔 Ignored genesis block (#0) announcement from {}: {}",
|
||||
peer_id,
|
||||
hash,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to allocate a slot for this block announce validation.
|
||||
match self.allocate_slot_for_block_announce_validation(&peer_id) {
|
||||
AllocateSlotForBlockAnnounceValidation::Allocated => {},
|
||||
AllocateSlotForBlockAnnounceValidation::TotalMaximumSlotsReached => {
|
||||
warn!(
|
||||
target: LOG_TARGET,
|
||||
"💔 Ignored block (#{} -- {}) announcement from {} because all validation slots are occupied.",
|
||||
number,
|
||||
hash,
|
||||
peer_id,
|
||||
);
|
||||
return;
|
||||
},
|
||||
AllocateSlotForBlockAnnounceValidation::MaximumPeerSlotsReached => {
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"💔 Ignored block (#{} -- {}) announcement from {} because all validation slots for this peer are occupied.",
|
||||
number,
|
||||
hash,
|
||||
peer_id,
|
||||
);
|
||||
return;
|
||||
},
|
||||
}
|
||||
|
||||
// Let external validator check the block announcement.
|
||||
let assoc_data = announce.data.as_ref().map_or(&[][..], |v| v.as_slice());
|
||||
let future = self.validator.validate(header, assoc_data);
|
||||
|
||||
self.validations.push(
|
||||
async move {
|
||||
match future.await {
|
||||
Ok(Validation::Success { is_new_best }) => {
|
||||
let is_new_best = is_new_best || is_best;
|
||||
|
||||
trace!(
|
||||
target: LOG_TARGET,
|
||||
"Block announcement validated successfully: from {}: {:?}. Local best: {}.",
|
||||
peer_id,
|
||||
announce.summary(),
|
||||
is_new_best,
|
||||
);
|
||||
|
||||
BlockAnnounceValidationResult::Process { is_new_best, announce, peer_id }
|
||||
},
|
||||
Ok(Validation::Failure { disconnect }) => {
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"Block announcement validation failed: from {}, block {:?}. Disconnect: {}.",
|
||||
peer_id,
|
||||
hash,
|
||||
disconnect,
|
||||
);
|
||||
|
||||
BlockAnnounceValidationResult::Failure { peer_id, disconnect }
|
||||
},
|
||||
Err(e) => {
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"💔 Ignoring block announcement validation from {} of block {:?} due to internal error: {}.",
|
||||
peer_id,
|
||||
hash,
|
||||
e,
|
||||
);
|
||||
|
||||
BlockAnnounceValidationResult::Skip { peer_id }
|
||||
},
|
||||
}
|
||||
}
|
||||
.boxed(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Checks if there is a slot for a block announce validation.
|
||||
///
|
||||
/// The total number and the number per peer of concurrent block announce validations
|
||||
/// is capped.
|
||||
///
|
||||
/// Returns [`AllocateSlotForBlockAnnounceValidation`] to inform about the result.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// It is *required* to call [`Self::deallocate_slot_for_block_announce_validation`] when the
|
||||
/// validation is finished to clear the slot.
|
||||
fn allocate_slot_for_block_announce_validation(
|
||||
&mut self,
|
||||
peer_id: &PeerId,
|
||||
) -> AllocateSlotForBlockAnnounceValidation {
|
||||
if self.validations.len() >= MAX_CONCURRENT_BLOCK_ANNOUNCE_VALIDATIONS {
|
||||
return AllocateSlotForBlockAnnounceValidation::TotalMaximumSlotsReached;
|
||||
}
|
||||
|
||||
match self.validations_per_peer.entry(*peer_id) {
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(1);
|
||||
AllocateSlotForBlockAnnounceValidation::Allocated
|
||||
},
|
||||
Entry::Occupied(mut entry) => {
|
||||
if *entry.get() < MAX_CONCURRENT_BLOCK_ANNOUNCE_VALIDATIONS_PER_PEER {
|
||||
*entry.get_mut() += 1;
|
||||
AllocateSlotForBlockAnnounceValidation::Allocated
|
||||
} else {
|
||||
AllocateSlotForBlockAnnounceValidation::MaximumPeerSlotsReached
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Should be called when a block announce validation is finished, to update the slots
|
||||
/// of the peer that send the block announce.
|
||||
fn deallocate_slot_for_block_announce_validation(&mut self, peer_id: &PeerId) {
|
||||
match self.validations_per_peer.entry(*peer_id) {
|
||||
Entry::Vacant(_) => {
|
||||
error!(
|
||||
target: LOG_TARGET,
|
||||
"💔 Block announcement validation from peer {} finished for a slot that was not allocated!",
|
||||
peer_id,
|
||||
);
|
||||
},
|
||||
Entry::Occupied(mut entry) => match entry.get().checked_sub(1) {
|
||||
Some(value) =>
|
||||
if value == 0 {
|
||||
entry.remove();
|
||||
} else {
|
||||
*entry.get_mut() = value;
|
||||
},
|
||||
None => {
|
||||
entry.remove();
|
||||
|
||||
error!(
|
||||
target: LOG_TARGET,
|
||||
"Invalid (zero) block announce validation slot counter for peer {peer_id}.",
|
||||
);
|
||||
debug_assert!(
|
||||
false,
|
||||
"Invalid (zero) block announce validation slot counter for peer {peer_id}.",
|
||||
);
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: BlockT> Stream for BlockAnnounceValidator<B> {
|
||||
type Item = BlockAnnounceValidationResult<B::Header>;
|
||||
|
||||
/// Poll for finished block announce validations. The stream never terminates.
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let validation = futures::ready!(self.validations.poll_next_unpin(cx))
|
||||
.expect("`FuturesStream` never terminates; qed");
|
||||
self.deallocate_slot_for_block_announce_validation(validation.peer_id());
|
||||
|
||||
Poll::Ready(Some(validation))
|
||||
}
|
||||
}
|
||||
|
||||
// As [`BlockAnnounceValidator`] never terminates, we can easily implement [`FusedStream`] for it.
|
||||
impl<B: BlockT> FusedStream for BlockAnnounceValidator<B> {
|
||||
fn is_terminated(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::block_announce_validator::AllocateSlotForBlockAnnounceValidation;
|
||||
use pezsc_network_types::PeerId;
|
||||
use pezsp_consensus::block_validation::DefaultBlockAnnounceValidator;
|
||||
use bizinikiwi_test_runtime_client::runtime::Block;
|
||||
|
||||
#[test]
|
||||
fn allocate_one_validation_slot() {
|
||||
let mut validator =
|
||||
BlockAnnounceValidator::<Block>::new(Box::new(DefaultBlockAnnounceValidator {}));
|
||||
let peer_id = PeerId::random();
|
||||
|
||||
assert!(matches!(
|
||||
validator.allocate_slot_for_block_announce_validation(&peer_id),
|
||||
AllocateSlotForBlockAnnounceValidation::Allocated,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allocate_validation_slots_for_two_peers() {
|
||||
let mut validator =
|
||||
BlockAnnounceValidator::<Block>::new(Box::new(DefaultBlockAnnounceValidator {}));
|
||||
let peer_id_1 = PeerId::random();
|
||||
let peer_id_2 = PeerId::random();
|
||||
|
||||
assert!(matches!(
|
||||
validator.allocate_slot_for_block_announce_validation(&peer_id_1),
|
||||
AllocateSlotForBlockAnnounceValidation::Allocated,
|
||||
));
|
||||
assert!(matches!(
|
||||
validator.allocate_slot_for_block_announce_validation(&peer_id_2),
|
||||
AllocateSlotForBlockAnnounceValidation::Allocated,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maximum_validation_slots_per_peer() {
|
||||
let mut validator =
|
||||
BlockAnnounceValidator::<Block>::new(Box::new(DefaultBlockAnnounceValidator {}));
|
||||
let peer_id = PeerId::random();
|
||||
|
||||
for _ in 0..MAX_CONCURRENT_BLOCK_ANNOUNCE_VALIDATIONS_PER_PEER {
|
||||
assert!(matches!(
|
||||
validator.allocate_slot_for_block_announce_validation(&peer_id),
|
||||
AllocateSlotForBlockAnnounceValidation::Allocated,
|
||||
));
|
||||
}
|
||||
|
||||
assert!(matches!(
|
||||
validator.allocate_slot_for_block_announce_validation(&peer_id),
|
||||
AllocateSlotForBlockAnnounceValidation::MaximumPeerSlotsReached,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validation_slots_per_peer_deallocated() {
|
||||
let mut validator =
|
||||
BlockAnnounceValidator::<Block>::new(Box::new(DefaultBlockAnnounceValidator {}));
|
||||
let peer_id = PeerId::random();
|
||||
|
||||
for _ in 0..MAX_CONCURRENT_BLOCK_ANNOUNCE_VALIDATIONS_PER_PEER {
|
||||
assert!(matches!(
|
||||
validator.allocate_slot_for_block_announce_validation(&peer_id),
|
||||
AllocateSlotForBlockAnnounceValidation::Allocated,
|
||||
));
|
||||
}
|
||||
|
||||
assert!(matches!(
|
||||
validator.allocate_slot_for_block_announce_validation(&peer_id),
|
||||
AllocateSlotForBlockAnnounceValidation::MaximumPeerSlotsReached,
|
||||
));
|
||||
|
||||
validator.deallocate_slot_for_block_announce_validation(&peer_id);
|
||||
|
||||
assert!(matches!(
|
||||
validator.allocate_slot_for_block_announce_validation(&peer_id),
|
||||
AllocateSlotForBlockAnnounceValidation::Allocated,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maximum_validation_slots_for_all_peers() {
|
||||
let mut validator =
|
||||
BlockAnnounceValidator::<Block>::new(Box::new(DefaultBlockAnnounceValidator {}));
|
||||
|
||||
for _ in 0..MAX_CONCURRENT_BLOCK_ANNOUNCE_VALIDATIONS {
|
||||
validator.validations.push(
|
||||
futures::future::ready(BlockAnnounceValidationResult::Skip {
|
||||
peer_id: PeerId::random(),
|
||||
})
|
||||
.boxed(),
|
||||
);
|
||||
}
|
||||
|
||||
let peer_id = PeerId::random();
|
||||
assert!(matches!(
|
||||
validator.allocate_slot_for_block_announce_validation(&peer_id),
|
||||
AllocateSlotForBlockAnnounceValidation::TotalMaximumSlotsReached,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Bizinikiwi.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// Bizinikiwi 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.
|
||||
|
||||
// Bizinikiwi 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 Bizinikiwi. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Block relay protocol related definitions.
|
||||
|
||||
use futures::channel::oneshot;
|
||||
use pezsc_network::{request_responses::RequestFailure, NetworkBackend, ProtocolName};
|
||||
use pezsc_network_common::sync::message::{BlockData, BlockRequest};
|
||||
use pezsc_network_types::PeerId;
|
||||
use pezsp_runtime::traits::Block as BlockT;
|
||||
use std::{fmt, sync::Arc};
|
||||
|
||||
/// The serving side of the block relay protocol. It runs a single instance
|
||||
/// of the server task that processes the incoming protocol messages.
|
||||
#[async_trait::async_trait]
|
||||
pub trait BlockServer<Block: BlockT>: Send {
|
||||
/// Starts the protocol processing.
|
||||
async fn run(&mut self);
|
||||
}
|
||||
|
||||
/// The client side stub to download blocks from peers. This is a handle
|
||||
/// that can be used to initiate concurrent downloads.
|
||||
#[async_trait::async_trait]
|
||||
pub trait BlockDownloader<Block: BlockT>: fmt::Debug + Send + Sync {
|
||||
/// Protocol name used by block downloader.
|
||||
fn protocol_name(&self) -> &ProtocolName;
|
||||
|
||||
/// Performs the protocol specific sequence to fetch the blocks from the peer.
|
||||
/// Output: if the download succeeds, the response is a `Vec<u8>` which is
|
||||
/// in a format specific to the protocol implementation. The block data
|
||||
/// can be extracted from this response using [`BlockDownloader::block_response_into_blocks`].
|
||||
async fn download_blocks(
|
||||
&self,
|
||||
who: PeerId,
|
||||
request: BlockRequest<Block>,
|
||||
) -> Result<Result<(Vec<u8>, ProtocolName), RequestFailure>, oneshot::Canceled>;
|
||||
|
||||
/// Parses the protocol specific response to retrieve the block data.
|
||||
fn block_response_into_blocks(
|
||||
&self,
|
||||
request: &BlockRequest<Block>,
|
||||
response: Vec<u8>,
|
||||
) -> Result<Vec<BlockData<Block>>, BlockResponseError>;
|
||||
}
|
||||
|
||||
/// Errors returned by [`BlockDownloader::block_response_into_blocks`].
|
||||
#[derive(Debug)]
|
||||
pub enum BlockResponseError {
|
||||
/// Failed to decode the response bytes.
|
||||
DecodeFailed(String),
|
||||
|
||||
/// Failed to extract the blocks from the decoded bytes.
|
||||
ExtractionFailed(String),
|
||||
}
|
||||
|
||||
/// Block relay specific params for network creation, specified in
|
||||
/// ['pezsc_service::BuildNetworkParams'].
|
||||
pub struct BlockRelayParams<Block: BlockT, N: NetworkBackend<Block, <Block as BlockT>::Hash>> {
|
||||
pub server: Box<dyn BlockServer<Block>>,
|
||||
pub downloader: Arc<dyn BlockDownloader<Block>>,
|
||||
pub request_response_config: N::RequestResponseProtocolConfig,
|
||||
}
|
||||
@@ -0,0 +1,627 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Bizinikiwi.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// Bizinikiwi 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.
|
||||
|
||||
// Bizinikiwi 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 Bizinikiwi. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Helper for handling (i.e. answering) block requests from a remote peer via the
|
||||
//! `crate::request_responses::RequestResponsesBehaviour`.
|
||||
|
||||
use crate::{
|
||||
block_relay_protocol::{BlockDownloader, BlockRelayParams, BlockResponseError, BlockServer},
|
||||
schema::v1::{
|
||||
block_request::FromBlock as FromBlockSchema, BlockRequest as BlockRequestSchema,
|
||||
BlockResponse as BlockResponseSchema, BlockResponse, Direction,
|
||||
},
|
||||
service::network::NetworkServiceHandle,
|
||||
LOG_TARGET,
|
||||
};
|
||||
|
||||
use codec::{Decode, DecodeAll, Encode};
|
||||
use futures::{channel::oneshot, stream::StreamExt};
|
||||
use log::debug;
|
||||
use prost::Message;
|
||||
use schnellru::{ByLength, LruMap};
|
||||
|
||||
use pezsc_client_api::BlockBackend;
|
||||
use pezsc_network::{
|
||||
config::ProtocolId,
|
||||
request_responses::{IfDisconnected, IncomingRequest, OutgoingResponse, RequestFailure},
|
||||
service::traits::RequestResponseConfig,
|
||||
types::ProtocolName,
|
||||
NetworkBackend, MAX_RESPONSE_SIZE,
|
||||
};
|
||||
use pezsc_network_common::sync::message::{BlockAttributes, BlockData, BlockRequest, FromBlock};
|
||||
use pezsc_network_types::PeerId;
|
||||
use pezsp_blockchain::HeaderBackend;
|
||||
use pezsp_runtime::{
|
||||
generic::BlockId,
|
||||
traits::{Block as BlockT, Header, One, Zero},
|
||||
};
|
||||
|
||||
use std::{
|
||||
cmp::min,
|
||||
hash::{Hash, Hasher},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
/// Maximum blocks per response.
|
||||
pub(crate) const MAX_BLOCKS_IN_RESPONSE: usize = 128;
|
||||
|
||||
const MAX_BODY_BYTES: usize = 8 * 1024 * 1024;
|
||||
const MAX_NUMBER_OF_SAME_REQUESTS_PER_PEER: usize = 2;
|
||||
|
||||
mod rep {
|
||||
use pezsc_network::ReputationChange as Rep;
|
||||
|
||||
/// Reputation change when a peer sent us the same request multiple times.
|
||||
pub const SAME_REQUEST: Rep = Rep::new_fatal("Same block request multiple times");
|
||||
|
||||
/// Reputation change when a peer sent us the same "small" request multiple times.
|
||||
pub const SAME_SMALL_REQUEST: Rep =
|
||||
Rep::new(-(1 << 10), "same small block request multiple times");
|
||||
}
|
||||
|
||||
/// Generates a `RequestResponseProtocolConfig` for the block request protocol,
|
||||
/// refusing incoming requests.
|
||||
pub fn generate_protocol_config<
|
||||
Hash: AsRef<[u8]>,
|
||||
B: BlockT,
|
||||
N: NetworkBackend<B, <B as BlockT>::Hash>,
|
||||
>(
|
||||
protocol_id: &ProtocolId,
|
||||
genesis_hash: Hash,
|
||||
fork_id: Option<&str>,
|
||||
inbound_queue: async_channel::Sender<IncomingRequest>,
|
||||
) -> N::RequestResponseProtocolConfig {
|
||||
N::request_response_config(
|
||||
generate_protocol_name(genesis_hash, fork_id).into(),
|
||||
std::iter::once(generate_legacy_protocol_name(protocol_id).into()).collect(),
|
||||
1024 * 1024,
|
||||
MAX_RESPONSE_SIZE,
|
||||
Duration::from_secs(20),
|
||||
Some(inbound_queue),
|
||||
)
|
||||
}
|
||||
|
||||
/// Generate the block protocol name from the genesis hash and fork id.
|
||||
fn generate_protocol_name<Hash: AsRef<[u8]>>(genesis_hash: Hash, fork_id: Option<&str>) -> String {
|
||||
let genesis_hash = genesis_hash.as_ref();
|
||||
if let Some(fork_id) = fork_id {
|
||||
format!("/{}/{}/sync/2", array_bytes::bytes2hex("", genesis_hash), fork_id)
|
||||
} else {
|
||||
format!("/{}/sync/2", array_bytes::bytes2hex("", genesis_hash))
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate the legacy block protocol name from chain specific protocol identifier.
|
||||
fn generate_legacy_protocol_name(protocol_id: &ProtocolId) -> String {
|
||||
format!("/{}/sync/2", protocol_id.as_ref())
|
||||
}
|
||||
|
||||
/// The key of [`BlockRequestHandler::seen_requests`].
|
||||
#[derive(Eq, PartialEq, Clone)]
|
||||
struct SeenRequestsKey<B: BlockT> {
|
||||
peer: PeerId,
|
||||
from: BlockId<B>,
|
||||
max_blocks: usize,
|
||||
direction: Direction,
|
||||
attributes: BlockAttributes,
|
||||
support_multiple_justifications: bool,
|
||||
}
|
||||
|
||||
#[allow(clippy::derived_hash_with_manual_eq)]
|
||||
impl<B: BlockT> Hash for SeenRequestsKey<B> {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.peer.hash(state);
|
||||
self.max_blocks.hash(state);
|
||||
self.direction.hash(state);
|
||||
self.attributes.hash(state);
|
||||
self.support_multiple_justifications.hash(state);
|
||||
match self.from {
|
||||
BlockId::Hash(h) => h.hash(state),
|
||||
BlockId::Number(n) => n.hash(state),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The value of [`BlockRequestHandler::seen_requests`].
|
||||
enum SeenRequestsValue {
|
||||
/// First time we have seen the request.
|
||||
First,
|
||||
/// We have fulfilled the request `n` times.
|
||||
Fulfilled(usize),
|
||||
}
|
||||
|
||||
/// The full block server implementation of [`BlockServer`]. It handles
|
||||
/// the incoming block requests from a remote peer.
|
||||
pub struct BlockRequestHandler<B: BlockT, Client> {
|
||||
client: Arc<Client>,
|
||||
request_receiver: async_channel::Receiver<IncomingRequest>,
|
||||
/// Maps from request to number of times we have seen this request.
|
||||
///
|
||||
/// This is used to check if a peer is spamming us with the same request.
|
||||
seen_requests: LruMap<SeenRequestsKey<B>, SeenRequestsValue>,
|
||||
}
|
||||
|
||||
impl<B, Client> BlockRequestHandler<B, Client>
|
||||
where
|
||||
B: BlockT,
|
||||
Client: HeaderBackend<B> + BlockBackend<B> + Send + Sync + 'static,
|
||||
{
|
||||
/// Create a new [`BlockRequestHandler`].
|
||||
pub fn new<N: NetworkBackend<B, <B as BlockT>::Hash>>(
|
||||
network: NetworkServiceHandle,
|
||||
protocol_id: &ProtocolId,
|
||||
fork_id: Option<&str>,
|
||||
client: Arc<Client>,
|
||||
num_peer_hint: usize,
|
||||
) -> BlockRelayParams<B, N> {
|
||||
// Reserve enough request slots for one request per peer when we are at the maximum
|
||||
// number of peers.
|
||||
let capacity = std::cmp::max(num_peer_hint, 1);
|
||||
let (tx, request_receiver) = async_channel::bounded(capacity);
|
||||
|
||||
let protocol_config = generate_protocol_config::<_, B, N>(
|
||||
protocol_id,
|
||||
client
|
||||
.block_hash(0u32.into())
|
||||
.ok()
|
||||
.flatten()
|
||||
.expect("Genesis block exists; qed"),
|
||||
fork_id,
|
||||
tx,
|
||||
);
|
||||
|
||||
let capacity = ByLength::new(num_peer_hint.max(1) as u32 * 2);
|
||||
let seen_requests = LruMap::new(capacity);
|
||||
|
||||
BlockRelayParams {
|
||||
server: Box::new(Self { client, request_receiver, seen_requests }),
|
||||
downloader: Arc::new(FullBlockDownloader::new(
|
||||
protocol_config.protocol_name().clone(),
|
||||
network,
|
||||
)),
|
||||
request_response_config: protocol_config,
|
||||
}
|
||||
}
|
||||
|
||||
/// Run [`BlockRequestHandler`].
|
||||
async fn process_requests(&mut self) {
|
||||
while let Some(request) = self.request_receiver.next().await {
|
||||
let IncomingRequest { peer, payload, pending_response } = request;
|
||||
|
||||
match self.handle_request(payload, pending_response, &peer) {
|
||||
Ok(()) => debug!(target: LOG_TARGET, "Handled block request from {}.", peer),
|
||||
Err(e) => debug!(
|
||||
target: LOG_TARGET,
|
||||
"Failed to handle block request from {}: {}", peer, e,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_request(
|
||||
&mut self,
|
||||
payload: Vec<u8>,
|
||||
pending_response: oneshot::Sender<OutgoingResponse>,
|
||||
peer: &PeerId,
|
||||
) -> Result<(), HandleRequestError> {
|
||||
let request = crate::schema::v1::BlockRequest::decode(&payload[..])?;
|
||||
|
||||
let from_block_id = match request.from_block.ok_or(HandleRequestError::MissingFromField)? {
|
||||
FromBlockSchema::Hash(ref h) => {
|
||||
let h = Decode::decode(&mut h.as_ref())?;
|
||||
BlockId::<B>::Hash(h)
|
||||
},
|
||||
FromBlockSchema::Number(ref n) => {
|
||||
let n = Decode::decode(&mut n.as_ref())?;
|
||||
BlockId::<B>::Number(n)
|
||||
},
|
||||
};
|
||||
|
||||
let max_blocks = if request.max_blocks == 0 {
|
||||
MAX_BLOCKS_IN_RESPONSE
|
||||
} else {
|
||||
min(request.max_blocks as usize, MAX_BLOCKS_IN_RESPONSE)
|
||||
};
|
||||
|
||||
let direction =
|
||||
i32::try_into(request.direction).map_err(|_| HandleRequestError::ParseDirection)?;
|
||||
|
||||
let attributes = BlockAttributes::from_be_u32(request.fields)?;
|
||||
|
||||
let support_multiple_justifications = request.support_multiple_justifications;
|
||||
|
||||
let key = SeenRequestsKey {
|
||||
peer: *peer,
|
||||
max_blocks,
|
||||
direction,
|
||||
from: from_block_id,
|
||||
attributes,
|
||||
support_multiple_justifications,
|
||||
};
|
||||
|
||||
let mut reputation_change = None;
|
||||
|
||||
let small_request = attributes
|
||||
.difference(BlockAttributes::HEADER | BlockAttributes::JUSTIFICATION)
|
||||
.is_empty();
|
||||
|
||||
match self.seen_requests.get(&key) {
|
||||
Some(SeenRequestsValue::First) => {},
|
||||
Some(SeenRequestsValue::Fulfilled(ref mut requests)) => {
|
||||
*requests = requests.saturating_add(1);
|
||||
|
||||
if *requests > MAX_NUMBER_OF_SAME_REQUESTS_PER_PEER {
|
||||
reputation_change = Some(if small_request {
|
||||
rep::SAME_SMALL_REQUEST
|
||||
} else {
|
||||
rep::SAME_REQUEST
|
||||
});
|
||||
}
|
||||
},
|
||||
None => {
|
||||
self.seen_requests.insert(key.clone(), SeenRequestsValue::First);
|
||||
},
|
||||
}
|
||||
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"Handling block request from {peer}: Starting at `{from_block_id:?}` with \
|
||||
maximum blocks of `{max_blocks}`, reputation_change: `{reputation_change:?}`, \
|
||||
small_request `{small_request:?}`, direction `{direction:?}` and \
|
||||
attributes `{attributes:?}`.",
|
||||
);
|
||||
|
||||
let maybe_block_response = if reputation_change.is_none() || small_request {
|
||||
let block_response = self.get_block_response(
|
||||
attributes,
|
||||
from_block_id,
|
||||
direction,
|
||||
max_blocks,
|
||||
support_multiple_justifications,
|
||||
)?;
|
||||
|
||||
// If any of the blocks contains any data, we can consider it as successful request.
|
||||
if block_response
|
||||
.blocks
|
||||
.iter()
|
||||
.any(|b| !b.header.is_empty() || !b.body.is_empty() || b.is_empty_justification)
|
||||
{
|
||||
if let Some(value) = self.seen_requests.get(&key) {
|
||||
// If this is the first time we have processed this request, we need to change
|
||||
// it to `Fulfilled`.
|
||||
if let SeenRequestsValue::First = value {
|
||||
*value = SeenRequestsValue::Fulfilled(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(block_response)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"Sending result of block request from {peer} starting at `{from_block_id:?}`: \
|
||||
blocks: {:?}, data: {:?}",
|
||||
maybe_block_response.as_ref().map(|res| res.blocks.len()),
|
||||
maybe_block_response.as_ref().map(|res| res.encoded_len()),
|
||||
);
|
||||
|
||||
let result = if let Some(block_response) = maybe_block_response {
|
||||
let mut data = Vec::with_capacity(block_response.encoded_len());
|
||||
block_response.encode(&mut data)?;
|
||||
Ok(data)
|
||||
} else {
|
||||
Err(())
|
||||
};
|
||||
|
||||
pending_response
|
||||
.send(OutgoingResponse {
|
||||
result,
|
||||
reputation_changes: reputation_change.into_iter().collect(),
|
||||
sent_feedback: None,
|
||||
})
|
||||
.map_err(|_| HandleRequestError::SendResponse)
|
||||
}
|
||||
|
||||
fn get_block_response(
|
||||
&self,
|
||||
attributes: BlockAttributes,
|
||||
mut block_id: BlockId<B>,
|
||||
direction: Direction,
|
||||
max_blocks: usize,
|
||||
support_multiple_justifications: bool,
|
||||
) -> Result<BlockResponse, HandleRequestError> {
|
||||
let get_header = attributes.contains(BlockAttributes::HEADER);
|
||||
let get_body = attributes.contains(BlockAttributes::BODY);
|
||||
let get_indexed_body = attributes.contains(BlockAttributes::INDEXED_BODY);
|
||||
let get_justification = attributes.contains(BlockAttributes::JUSTIFICATION);
|
||||
|
||||
let mut blocks = Vec::new();
|
||||
|
||||
let mut total_size: usize = 0;
|
||||
|
||||
let client_header_from_block_id =
|
||||
|block_id: BlockId<B>| -> Result<Option<B::Header>, HandleRequestError> {
|
||||
if let Some(hash) = self.client.block_hash_from_id(&block_id)? {
|
||||
return self.client.header(hash).map_err(Into::into);
|
||||
}
|
||||
Ok(None)
|
||||
};
|
||||
|
||||
while let Some(header) = client_header_from_block_id(block_id).unwrap_or_default() {
|
||||
let number = *header.number();
|
||||
let hash = header.hash();
|
||||
let parent_hash = *header.parent_hash();
|
||||
let justifications =
|
||||
if get_justification { self.client.justifications(hash)? } else { None };
|
||||
|
||||
let (justifications, justification, is_empty_justification) =
|
||||
if support_multiple_justifications {
|
||||
let justifications = match justifications {
|
||||
Some(v) => v.encode(),
|
||||
None => Vec::new(),
|
||||
};
|
||||
(justifications, Vec::new(), false)
|
||||
} else {
|
||||
// For now we keep compatibility by selecting precisely the GRANDPA one, and not
|
||||
// just the first one. When sending we could have just taken the first one,
|
||||
// since we don't expect there to be any other kind currently, but when
|
||||
// receiving we need to add the engine ID tag.
|
||||
// The ID tag is hardcoded here to avoid depending on the GRANDPA crate, and
|
||||
// will be removed once we remove the backwards compatibility.
|
||||
// See: https://github.com/pezkuwichain/kurdistan-sdk/issues/32
|
||||
let justification =
|
||||
justifications.and_then(|just| just.into_justification(*b"FRNK"));
|
||||
|
||||
let is_empty_justification =
|
||||
justification.as_ref().map(|j| j.is_empty()).unwrap_or(false);
|
||||
|
||||
let justification = justification.unwrap_or_default();
|
||||
|
||||
(Vec::new(), justification, is_empty_justification)
|
||||
};
|
||||
|
||||
let body = if get_body {
|
||||
match self.client.block_body(hash)? {
|
||||
Some(mut extrinsics) =>
|
||||
extrinsics.iter_mut().map(|extrinsic| extrinsic.encode()).collect(),
|
||||
None => {
|
||||
log::trace!(target: LOG_TARGET, "Missing data for block request.");
|
||||
break;
|
||||
},
|
||||
}
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let indexed_body = if get_indexed_body {
|
||||
match self.client.block_indexed_body(hash)? {
|
||||
Some(transactions) => transactions,
|
||||
None => {
|
||||
log::trace!(
|
||||
target: LOG_TARGET,
|
||||
"Missing indexed block data for block request."
|
||||
);
|
||||
// If the indexed body is missing we still continue returning headers.
|
||||
// Ideally `None` should distinguish a missing body from the empty body,
|
||||
// but the current protobuf based protocol does not allow it.
|
||||
Vec::new()
|
||||
},
|
||||
}
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let block_data = crate::schema::v1::BlockData {
|
||||
hash: hash.encode(),
|
||||
header: if get_header { header.encode() } else { Vec::new() },
|
||||
body,
|
||||
receipt: Vec::new(),
|
||||
message_queue: Vec::new(),
|
||||
justification,
|
||||
is_empty_justification,
|
||||
justifications,
|
||||
indexed_body,
|
||||
};
|
||||
|
||||
let new_total_size = total_size +
|
||||
block_data.body.iter().map(|ex| ex.len()).sum::<usize>() +
|
||||
block_data.indexed_body.iter().map(|ex| ex.len()).sum::<usize>();
|
||||
|
||||
// Send at least one block, but make sure to not exceed the limit.
|
||||
if !blocks.is_empty() && new_total_size > MAX_BODY_BYTES {
|
||||
break;
|
||||
}
|
||||
|
||||
total_size = new_total_size;
|
||||
|
||||
blocks.push(block_data);
|
||||
|
||||
if blocks.len() >= max_blocks as usize {
|
||||
break;
|
||||
}
|
||||
|
||||
match direction {
|
||||
Direction::Ascending => block_id = BlockId::Number(number + One::one()),
|
||||
Direction::Descending => {
|
||||
if number.is_zero() {
|
||||
break;
|
||||
}
|
||||
block_id = BlockId::Hash(parent_hash)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Ok(BlockResponse { blocks })
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<B, Client> BlockServer<B> for BlockRequestHandler<B, Client>
|
||||
where
|
||||
B: BlockT,
|
||||
Client: HeaderBackend<B> + BlockBackend<B> + Send + Sync + 'static,
|
||||
{
|
||||
async fn run(&mut self) {
|
||||
self.process_requests().await;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
enum HandleRequestError {
|
||||
#[error("Failed to decode request: {0}.")]
|
||||
DecodeProto(#[from] prost::DecodeError),
|
||||
#[error("Failed to encode response: {0}.")]
|
||||
EncodeProto(#[from] prost::EncodeError),
|
||||
#[error("Failed to decode block hash: {0}.")]
|
||||
DecodeScale(#[from] codec::Error),
|
||||
#[error("Missing `BlockRequest::from_block` field.")]
|
||||
MissingFromField,
|
||||
#[error("Failed to parse BlockRequest::direction.")]
|
||||
ParseDirection,
|
||||
#[error(transparent)]
|
||||
Client(#[from] pezsp_blockchain::Error),
|
||||
#[error("Failed to send response.")]
|
||||
SendResponse,
|
||||
}
|
||||
|
||||
/// The full block downloader implementation of [`BlockDownloader].
|
||||
#[derive(Debug)]
|
||||
pub struct FullBlockDownloader {
|
||||
protocol_name: ProtocolName,
|
||||
network: NetworkServiceHandle,
|
||||
}
|
||||
|
||||
impl FullBlockDownloader {
|
||||
fn new(protocol_name: ProtocolName, network: NetworkServiceHandle) -> Self {
|
||||
Self { protocol_name, network }
|
||||
}
|
||||
|
||||
/// Extracts the blocks from the response schema.
|
||||
fn blocks_from_schema<B: BlockT>(
|
||||
&self,
|
||||
request: &BlockRequest<B>,
|
||||
response: BlockResponseSchema,
|
||||
) -> Result<Vec<BlockData<B>>, String> {
|
||||
response
|
||||
.blocks
|
||||
.into_iter()
|
||||
.map(|block_data| {
|
||||
Ok(BlockData::<B> {
|
||||
hash: Decode::decode(&mut block_data.hash.as_ref())?,
|
||||
header: if !block_data.header.is_empty() {
|
||||
Some(Decode::decode(&mut block_data.header.as_ref())?)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
body: if request.fields.contains(BlockAttributes::BODY) {
|
||||
Some(
|
||||
block_data
|
||||
.body
|
||||
.iter()
|
||||
.map(|body| Decode::decode(&mut body.as_ref()))
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
indexed_body: if request.fields.contains(BlockAttributes::INDEXED_BODY) {
|
||||
Some(block_data.indexed_body)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
receipt: if !block_data.receipt.is_empty() {
|
||||
Some(block_data.receipt)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
message_queue: if !block_data.message_queue.is_empty() {
|
||||
Some(block_data.message_queue)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
justification: if !block_data.justification.is_empty() {
|
||||
Some(block_data.justification)
|
||||
} else if block_data.is_empty_justification {
|
||||
Some(Vec::new())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
justifications: if !block_data.justifications.is_empty() {
|
||||
Some(DecodeAll::decode_all(&mut block_data.justifications.as_ref())?)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
})
|
||||
})
|
||||
.collect::<Result<_, _>>()
|
||||
.map_err(|error: codec::Error| error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<B: BlockT> BlockDownloader<B> for FullBlockDownloader {
|
||||
fn protocol_name(&self) -> &ProtocolName {
|
||||
&self.protocol_name
|
||||
}
|
||||
|
||||
async fn download_blocks(
|
||||
&self,
|
||||
who: PeerId,
|
||||
request: BlockRequest<B>,
|
||||
) -> Result<Result<(Vec<u8>, ProtocolName), RequestFailure>, oneshot::Canceled> {
|
||||
// Build the request protobuf.
|
||||
let bytes = BlockRequestSchema {
|
||||
fields: request.fields.to_be_u32(),
|
||||
from_block: match request.from {
|
||||
FromBlock::Hash(h) => Some(FromBlockSchema::Hash(h.encode())),
|
||||
FromBlock::Number(n) => Some(FromBlockSchema::Number(n.encode())),
|
||||
},
|
||||
direction: request.direction as i32,
|
||||
max_blocks: request.max.unwrap_or(0),
|
||||
support_multiple_justifications: true,
|
||||
}
|
||||
.encode_to_vec();
|
||||
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.network.start_request(
|
||||
who,
|
||||
self.protocol_name.clone(),
|
||||
bytes,
|
||||
tx,
|
||||
IfDisconnected::ImmediateError,
|
||||
);
|
||||
rx.await
|
||||
}
|
||||
|
||||
fn block_response_into_blocks(
|
||||
&self,
|
||||
request: &BlockRequest<B>,
|
||||
response: Vec<u8>,
|
||||
) -> Result<Vec<BlockData<B>>, BlockResponseError> {
|
||||
// Decode the response protobuf
|
||||
let response_schema = BlockResponseSchema::decode(response.as_slice())
|
||||
.map_err(|error| BlockResponseError::DecodeFailed(error.to_string()))?;
|
||||
|
||||
// Extract the block data from the protobuf
|
||||
self.blocks_from_schema::<B>(request, response_schema)
|
||||
.map_err(|error| BlockResponseError::ExtractionFailed(error.to_string()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,649 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::LOG_TARGET;
|
||||
use log::trace;
|
||||
use pezsc_network_common::sync::message;
|
||||
use pezsc_network_types::PeerId;
|
||||
use pezsp_runtime::traits::{Block as BlockT, NumberFor, One};
|
||||
use std::{
|
||||
cmp,
|
||||
collections::{BTreeMap, HashMap},
|
||||
ops::Range,
|
||||
};
|
||||
|
||||
/// Block data with origin.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct BlockData<B: BlockT> {
|
||||
/// The Block Message from the wire
|
||||
pub block: message::BlockData<B>,
|
||||
/// The peer, we received this from
|
||||
pub origin: Option<PeerId>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum BlockRangeState<B: BlockT> {
|
||||
Downloading { len: NumberFor<B>, downloading: u32 },
|
||||
Complete(Vec<BlockData<B>>),
|
||||
Queued { len: NumberFor<B> },
|
||||
}
|
||||
|
||||
impl<B: BlockT> BlockRangeState<B> {
|
||||
pub fn len(&self) -> NumberFor<B> {
|
||||
match *self {
|
||||
Self::Downloading { len, .. } => len,
|
||||
Self::Complete(ref blocks) => (blocks.len() as u32).into(),
|
||||
Self::Queued { len } => len,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A collection of blocks being downloaded.
|
||||
#[derive(Default)]
|
||||
pub struct BlockCollection<B: BlockT> {
|
||||
/// Downloaded blocks.
|
||||
blocks: BTreeMap<NumberFor<B>, BlockRangeState<B>>,
|
||||
peer_requests: HashMap<PeerId, NumberFor<B>>,
|
||||
/// Block ranges downloaded and queued for import.
|
||||
/// Maps start_hash => (start_num, end_num).
|
||||
queued_blocks: HashMap<B::Hash, (NumberFor<B>, NumberFor<B>)>,
|
||||
}
|
||||
|
||||
impl<B: BlockT> BlockCollection<B> {
|
||||
/// Create a new instance.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
blocks: BTreeMap::new(),
|
||||
peer_requests: HashMap::new(),
|
||||
queued_blocks: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear everything.
|
||||
pub fn clear(&mut self) {
|
||||
self.blocks.clear();
|
||||
self.peer_requests.clear();
|
||||
}
|
||||
|
||||
/// Insert a set of blocks into collection.
|
||||
pub fn insert(&mut self, start: NumberFor<B>, blocks: Vec<message::BlockData<B>>, who: PeerId) {
|
||||
if blocks.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
match self.blocks.get(&start) {
|
||||
Some(&BlockRangeState::Downloading { .. }) => {
|
||||
trace!(target: LOG_TARGET, "Inserting block data still marked as being downloaded: {}", start);
|
||||
},
|
||||
Some(BlockRangeState::Complete(existing)) if existing.len() >= blocks.len() => {
|
||||
trace!(target: LOG_TARGET, "Ignored block data already downloaded: {}", start);
|
||||
return;
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
|
||||
self.blocks.insert(
|
||||
start,
|
||||
BlockRangeState::Complete(
|
||||
blocks.into_iter().map(|b| BlockData { origin: Some(who), block: b }).collect(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns a set of block hashes that require a header download. The returned set is marked as
|
||||
/// being downloaded.
|
||||
pub fn needed_blocks(
|
||||
&mut self,
|
||||
who: PeerId,
|
||||
count: u32,
|
||||
peer_best: NumberFor<B>,
|
||||
common: NumberFor<B>,
|
||||
max_parallel: u32,
|
||||
max_ahead: u32,
|
||||
) -> Option<Range<NumberFor<B>>> {
|
||||
if peer_best <= common {
|
||||
// Bail out early
|
||||
return None;
|
||||
}
|
||||
// First block number that we need to download
|
||||
let first_different = common + <NumberFor<B>>::one();
|
||||
let count = (count as u32).into();
|
||||
let (mut range, downloading) = {
|
||||
// Iterate through the ranges in `self.blocks` looking for a range to download
|
||||
let mut downloading_iter = self.blocks.iter().peekable();
|
||||
let mut prev: Option<(&NumberFor<B>, &BlockRangeState<B>)> = None;
|
||||
loop {
|
||||
let next = downloading_iter.next();
|
||||
break match (prev, next) {
|
||||
// If we are already downloading this range, request it from `max_parallel`
|
||||
// peers (`max_parallel = 5` by default).
|
||||
// Do not request already downloading range from peers with common number above
|
||||
// the range start.
|
||||
(Some((start, &BlockRangeState::Downloading { ref len, downloading })), _)
|
||||
if downloading < max_parallel && *start >= first_different =>
|
||||
(*start..*start + *len, downloading),
|
||||
// If there is a gap between ranges requested, download this gap unless the peer
|
||||
// has common number above the gap start
|
||||
(Some((start, r)), Some((next_start, _)))
|
||||
if *start + r.len() < *next_start &&
|
||||
*start + r.len() >= first_different =>
|
||||
(*start + r.len()..cmp::min(*next_start, *start + r.len() + count), 0),
|
||||
// Download `count` blocks after the last range requested unless the peer
|
||||
// has common number above this new range
|
||||
(Some((start, r)), None) if *start + r.len() >= first_different =>
|
||||
(*start + r.len()..*start + r.len() + count, 0),
|
||||
// If there are no ranges currently requested, download `count` blocks after
|
||||
// `common` number
|
||||
(None, None) => (first_different..first_different + count, 0),
|
||||
// If the first range starts above `common + 1`, download the gap at the start
|
||||
(None, Some((start, _))) if *start > first_different =>
|
||||
(first_different..cmp::min(first_different + count, *start), 0),
|
||||
// Move on to the next range pair
|
||||
_ => {
|
||||
prev = next;
|
||||
continue;
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
// crop to peers best
|
||||
if range.start > peer_best {
|
||||
trace!(target: LOG_TARGET, "Out of range for peer {} ({} vs {})", who, range.start, peer_best);
|
||||
return None;
|
||||
}
|
||||
range.end = cmp::min(peer_best + One::one(), range.end);
|
||||
|
||||
if self
|
||||
.blocks
|
||||
.iter()
|
||||
.next()
|
||||
.map_or(false, |(n, _)| range.start > *n + max_ahead.into())
|
||||
{
|
||||
trace!(target: LOG_TARGET, "Too far ahead for peer {} ({})", who, range.start);
|
||||
return None;
|
||||
}
|
||||
|
||||
self.peer_requests.insert(who, range.start);
|
||||
self.blocks.insert(
|
||||
range.start,
|
||||
BlockRangeState::Downloading {
|
||||
len: range.end - range.start,
|
||||
downloading: downloading + 1,
|
||||
},
|
||||
);
|
||||
if range.end <= range.start {
|
||||
panic!(
|
||||
"Empty range {:?}, count={}, peer_best={}, common={}, blocks={:?}",
|
||||
range, count, peer_best, common, self.blocks
|
||||
);
|
||||
}
|
||||
Some(range)
|
||||
}
|
||||
|
||||
/// Get a valid chain of blocks ordered in descending order and ready for importing into
|
||||
/// the blockchain.
|
||||
/// `from` is the maximum block number for the start of the range that we are interested in.
|
||||
/// The function will return empty Vec if the first block ready is higher than `from`.
|
||||
/// For each returned block hash `clear_queued` must be called at some later stage.
|
||||
pub fn ready_blocks(&mut self, from: NumberFor<B>) -> Vec<BlockData<B>> {
|
||||
let mut ready = Vec::new();
|
||||
|
||||
let mut prev = from;
|
||||
for (&start, range_data) in &mut self.blocks {
|
||||
if start > prev {
|
||||
break;
|
||||
}
|
||||
let len = match range_data {
|
||||
BlockRangeState::Complete(blocks) => {
|
||||
let len = (blocks.len() as u32).into();
|
||||
prev = start + len;
|
||||
if let Some(BlockData { block, .. }) = blocks.first() {
|
||||
self.queued_blocks
|
||||
.insert(block.hash, (start, start + (blocks.len() as u32).into()));
|
||||
}
|
||||
// Remove all elements from `blocks` and add them to `ready`
|
||||
ready.append(blocks);
|
||||
len
|
||||
},
|
||||
BlockRangeState::Queued { .. } => continue,
|
||||
_ => break,
|
||||
};
|
||||
*range_data = BlockRangeState::Queued { len };
|
||||
}
|
||||
trace!(target: LOG_TARGET, "{} blocks ready for import", ready.len());
|
||||
ready
|
||||
}
|
||||
|
||||
pub fn clear_queued(&mut self, hash: &B::Hash) {
|
||||
if let Some((from, to)) = self.queued_blocks.remove(hash) {
|
||||
let mut block_num = from;
|
||||
while block_num < to {
|
||||
self.blocks.remove(&block_num);
|
||||
block_num += One::one();
|
||||
}
|
||||
trace!(target: LOG_TARGET, "Cleared blocks from {:?} to {:?}", from, to);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_peer_download(&mut self, who: &PeerId) {
|
||||
if let Some(start) = self.peer_requests.remove(who) {
|
||||
let remove = match self.blocks.get_mut(&start) {
|
||||
Some(&mut BlockRangeState::Downloading { ref mut downloading, .. })
|
||||
if *downloading > 1 =>
|
||||
{
|
||||
*downloading -= 1;
|
||||
false
|
||||
},
|
||||
Some(&mut BlockRangeState::Downloading { .. }) => true,
|
||||
_ => false,
|
||||
};
|
||||
if remove {
|
||||
self.blocks.remove(&start);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{BlockCollection, BlockData, BlockRangeState};
|
||||
use pezsc_network_common::sync::message;
|
||||
use pezsc_network_types::PeerId;
|
||||
use pezsp_core::H256;
|
||||
use pezsp_runtime::testing::{Block as RawBlock, MockCallU64, TestXt};
|
||||
|
||||
type Block = RawBlock<TestXt<MockCallU64, ()>>;
|
||||
|
||||
fn is_empty(bc: &BlockCollection<Block>) -> bool {
|
||||
bc.blocks.is_empty() && bc.peer_requests.is_empty()
|
||||
}
|
||||
|
||||
fn generate_blocks(n: usize) -> Vec<message::BlockData<Block>> {
|
||||
(0..n)
|
||||
.map(|_| message::generic::BlockData {
|
||||
hash: H256::random(),
|
||||
header: None,
|
||||
body: None,
|
||||
indexed_body: None,
|
||||
message_queue: None,
|
||||
receipt: None,
|
||||
justification: None,
|
||||
justifications: None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_clear() {
|
||||
let mut bc = BlockCollection::new();
|
||||
assert!(is_empty(&bc));
|
||||
bc.insert(1, generate_blocks(100), PeerId::random());
|
||||
assert!(!is_empty(&bc));
|
||||
bc.clear();
|
||||
assert!(is_empty(&bc));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_blocks() {
|
||||
let mut bc = BlockCollection::new();
|
||||
assert!(is_empty(&bc));
|
||||
let peer0 = PeerId::random();
|
||||
let peer1 = PeerId::random();
|
||||
let peer2 = PeerId::random();
|
||||
|
||||
let blocks = generate_blocks(150);
|
||||
assert_eq!(bc.needed_blocks(peer0, 40, 150, 0, 1, 200), Some(1..41));
|
||||
assert_eq!(bc.needed_blocks(peer1, 40, 150, 0, 1, 200), Some(41..81));
|
||||
assert_eq!(bc.needed_blocks(peer2, 40, 150, 0, 1, 200), Some(81..121));
|
||||
|
||||
bc.clear_peer_download(&peer1);
|
||||
bc.insert(41, blocks[41..81].to_vec(), peer1);
|
||||
assert_eq!(bc.ready_blocks(1), vec![]);
|
||||
assert_eq!(bc.needed_blocks(peer1, 40, 150, 0, 1, 200), Some(121..151));
|
||||
bc.clear_peer_download(&peer0);
|
||||
bc.insert(1, blocks[1..11].to_vec(), peer0);
|
||||
|
||||
assert_eq!(bc.needed_blocks(peer0, 40, 150, 0, 1, 200), Some(11..41));
|
||||
assert_eq!(
|
||||
bc.ready_blocks(1),
|
||||
blocks[1..11]
|
||||
.iter()
|
||||
.map(|b| BlockData { block: b.clone(), origin: Some(peer0) })
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
bc.clear_peer_download(&peer0);
|
||||
bc.insert(11, blocks[11..41].to_vec(), peer0);
|
||||
|
||||
let ready = bc.ready_blocks(12);
|
||||
assert_eq!(
|
||||
ready[..30],
|
||||
blocks[11..41]
|
||||
.iter()
|
||||
.map(|b| BlockData { block: b.clone(), origin: Some(peer0) })
|
||||
.collect::<Vec<_>>()[..]
|
||||
);
|
||||
assert_eq!(
|
||||
ready[30..],
|
||||
blocks[41..81]
|
||||
.iter()
|
||||
.map(|b| BlockData { block: b.clone(), origin: Some(peer1) })
|
||||
.collect::<Vec<_>>()[..]
|
||||
);
|
||||
|
||||
bc.clear_peer_download(&peer2);
|
||||
assert_eq!(bc.needed_blocks(peer2, 40, 150, 80, 1, 200), Some(81..121));
|
||||
bc.clear_peer_download(&peer2);
|
||||
bc.insert(81, blocks[81..121].to_vec(), peer2);
|
||||
bc.clear_peer_download(&peer1);
|
||||
bc.insert(121, blocks[121..150].to_vec(), peer1);
|
||||
|
||||
assert_eq!(bc.ready_blocks(80), vec![]);
|
||||
let ready = bc.ready_blocks(81);
|
||||
assert_eq!(
|
||||
ready[..40],
|
||||
blocks[81..121]
|
||||
.iter()
|
||||
.map(|b| BlockData { block: b.clone(), origin: Some(peer2) })
|
||||
.collect::<Vec<_>>()[..]
|
||||
);
|
||||
assert_eq!(
|
||||
ready[40..],
|
||||
blocks[121..150]
|
||||
.iter()
|
||||
.map(|b| BlockData { block: b.clone(), origin: Some(peer1) })
|
||||
.collect::<Vec<_>>()[..]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_gap() {
|
||||
let mut bc: BlockCollection<Block> = BlockCollection::new();
|
||||
bc.blocks.insert(100, BlockRangeState::Downloading { len: 128, downloading: 1 });
|
||||
let blocks = generate_blocks(10)
|
||||
.into_iter()
|
||||
.map(|b| BlockData { block: b, origin: None })
|
||||
.collect();
|
||||
bc.blocks.insert(114305, BlockRangeState::Complete(blocks));
|
||||
|
||||
let peer0 = PeerId::random();
|
||||
assert_eq!(bc.needed_blocks(peer0, 128, 10000, 0, 1, 200), Some(1..100));
|
||||
assert_eq!(bc.needed_blocks(peer0, 128, 10000, 0, 1, 200), None); // too far ahead
|
||||
assert_eq!(
|
||||
bc.needed_blocks(peer0, 128, 10000, 0, 1, 200000),
|
||||
Some(100 + 128..100 + 128 + 128)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_duplicate_requests_on_fork() {
|
||||
let mut bc = BlockCollection::new();
|
||||
assert!(is_empty(&bc));
|
||||
let peer = PeerId::random();
|
||||
|
||||
let blocks = generate_blocks(10);
|
||||
|
||||
// count = 5, peer_best = 50, common = 39, max_parallel = 0, max_ahead = 200
|
||||
assert_eq!(bc.needed_blocks(peer, 5, 50, 39, 0, 200), Some(40..45));
|
||||
|
||||
// got a response on the request for `40..45`
|
||||
bc.clear_peer_download(&peer);
|
||||
bc.insert(40, blocks[..5].to_vec(), peer);
|
||||
|
||||
// our "node" started on a fork, with its current best = 47, which is > common
|
||||
let ready = bc.ready_blocks(48);
|
||||
assert_eq!(
|
||||
ready,
|
||||
blocks[..5]
|
||||
.iter()
|
||||
.map(|b| BlockData { block: b.clone(), origin: Some(peer) })
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
assert_eq!(bc.needed_blocks(peer, 5, 50, 39, 0, 200), Some(45..50));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_queued_subsequent_ranges() {
|
||||
let mut bc = BlockCollection::new();
|
||||
assert!(is_empty(&bc));
|
||||
let peer = PeerId::random();
|
||||
|
||||
let blocks = generate_blocks(10);
|
||||
|
||||
// Request 2 ranges
|
||||
assert_eq!(bc.needed_blocks(peer, 5, 50, 39, 0, 200), Some(40..45));
|
||||
assert_eq!(bc.needed_blocks(peer, 5, 50, 39, 0, 200), Some(45..50));
|
||||
|
||||
// got a response on the request for `40..50`
|
||||
bc.clear_peer_download(&peer);
|
||||
bc.insert(40, blocks.to_vec(), peer);
|
||||
|
||||
// request any blocks starting from 1000 or lower.
|
||||
let ready = bc.ready_blocks(1000);
|
||||
assert_eq!(
|
||||
ready,
|
||||
blocks
|
||||
.iter()
|
||||
.map(|b| BlockData { block: b.clone(), origin: Some(peer) })
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
bc.clear_queued(&blocks[0].hash);
|
||||
assert!(bc.blocks.is_empty());
|
||||
assert!(bc.queued_blocks.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn downloaded_range_is_requested_from_max_parallel_peers() {
|
||||
let mut bc = BlockCollection::new();
|
||||
assert!(is_empty(&bc));
|
||||
|
||||
let count = 5;
|
||||
// identical ranges requested from 2 peers
|
||||
let max_parallel = 2;
|
||||
let max_ahead = 200;
|
||||
|
||||
let peer1 = PeerId::random();
|
||||
let peer2 = PeerId::random();
|
||||
let peer3 = PeerId::random();
|
||||
|
||||
// common for all peers
|
||||
let best = 100;
|
||||
let common = 10;
|
||||
|
||||
assert_eq!(
|
||||
bc.needed_blocks(peer1, count, best, common, max_parallel, max_ahead),
|
||||
Some(11..16)
|
||||
);
|
||||
assert_eq!(
|
||||
bc.needed_blocks(peer2, count, best, common, max_parallel, max_ahead),
|
||||
Some(11..16)
|
||||
);
|
||||
assert_eq!(
|
||||
bc.needed_blocks(peer3, count, best, common, max_parallel, max_ahead),
|
||||
Some(16..21)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn downloaded_range_not_requested_from_peers_with_higher_common_number() {
|
||||
// A peer connects with a common number falling behind our best number
|
||||
// (either a fork or lagging behind).
|
||||
// We request a range from this peer starting at its common number + 1.
|
||||
// Even though we have less than `max_parallel` downloads, we do not request
|
||||
// this range from peers with a common number above the start of this range.
|
||||
|
||||
let mut bc = BlockCollection::new();
|
||||
assert!(is_empty(&bc));
|
||||
|
||||
let count = 5;
|
||||
let max_parallel = 2;
|
||||
let max_ahead = 200;
|
||||
|
||||
let peer1 = PeerId::random();
|
||||
let peer1_best = 20;
|
||||
let peer1_common = 10;
|
||||
|
||||
// `peer2` has first different above the start of the range downloaded from `peer1`
|
||||
let peer2 = PeerId::random();
|
||||
let peer2_best = 20;
|
||||
let peer2_common = 11; // first_different = 12
|
||||
|
||||
assert_eq!(
|
||||
bc.needed_blocks(peer1, count, peer1_best, peer1_common, max_parallel, max_ahead),
|
||||
Some(11..16),
|
||||
);
|
||||
assert_eq!(
|
||||
bc.needed_blocks(peer2, count, peer2_best, peer2_common, max_parallel, max_ahead),
|
||||
Some(16..21),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gap_above_common_number_requested() {
|
||||
let mut bc = BlockCollection::new();
|
||||
assert!(is_empty(&bc));
|
||||
|
||||
let count = 5;
|
||||
let best = 30;
|
||||
// We need at least 3 ranges requested to have a gap, so to minimize the number of peers
|
||||
// set `max_parallel = 1`
|
||||
let max_parallel = 1;
|
||||
let max_ahead = 200;
|
||||
|
||||
let peer1 = PeerId::random();
|
||||
let peer2 = PeerId::random();
|
||||
let peer3 = PeerId::random();
|
||||
|
||||
let common = 10;
|
||||
assert_eq!(
|
||||
bc.needed_blocks(peer1, count, best, common, max_parallel, max_ahead),
|
||||
Some(11..16),
|
||||
);
|
||||
assert_eq!(
|
||||
bc.needed_blocks(peer2, count, best, common, max_parallel, max_ahead),
|
||||
Some(16..21),
|
||||
);
|
||||
assert_eq!(
|
||||
bc.needed_blocks(peer3, count, best, common, max_parallel, max_ahead),
|
||||
Some(21..26),
|
||||
);
|
||||
|
||||
// For some reason there is now a gap at 16..21. We just disconnect `peer2`, but it might
|
||||
// also happen that 16..21 received first and got imported if our best is actually >= 15.
|
||||
bc.clear_peer_download(&peer2);
|
||||
|
||||
// Some peer connects with common number below the gap. The gap is requested from it.
|
||||
assert_eq!(
|
||||
bc.needed_blocks(peer2, count, best, common, max_parallel, max_ahead),
|
||||
Some(16..21),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gap_below_common_number_not_requested() {
|
||||
let mut bc = BlockCollection::new();
|
||||
assert!(is_empty(&bc));
|
||||
|
||||
let count = 5;
|
||||
let best = 30;
|
||||
// We need at least 3 ranges requested to have a gap, so to minimize the number of peers
|
||||
// set `max_parallel = 1`
|
||||
let max_parallel = 1;
|
||||
let max_ahead = 200;
|
||||
|
||||
let peer1 = PeerId::random();
|
||||
let peer2 = PeerId::random();
|
||||
let peer3 = PeerId::random();
|
||||
|
||||
let common = 10;
|
||||
assert_eq!(
|
||||
bc.needed_blocks(peer1, count, best, common, max_parallel, max_ahead),
|
||||
Some(11..16),
|
||||
);
|
||||
assert_eq!(
|
||||
bc.needed_blocks(peer2, count, best, common, max_parallel, max_ahead),
|
||||
Some(16..21),
|
||||
);
|
||||
assert_eq!(
|
||||
bc.needed_blocks(peer3, count, best, common, max_parallel, max_ahead),
|
||||
Some(21..26),
|
||||
);
|
||||
|
||||
// For some reason there is now a gap at 16..21. We just disconnect `peer2`, but it might
|
||||
// also happen that 16..21 received first and got imported if our best is actually >= 15.
|
||||
bc.clear_peer_download(&peer2);
|
||||
|
||||
// Some peer connects with common number above the gap. The gap is not requested from it.
|
||||
let common = 23;
|
||||
assert_eq!(
|
||||
bc.needed_blocks(peer2, count, best, common, max_parallel, max_ahead),
|
||||
Some(26..31), // not 16..21
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_at_the_end_above_common_number_requested() {
|
||||
let mut bc = BlockCollection::new();
|
||||
assert!(is_empty(&bc));
|
||||
|
||||
let count = 5;
|
||||
let best = 30;
|
||||
let max_parallel = 1;
|
||||
let max_ahead = 200;
|
||||
|
||||
let peer1 = PeerId::random();
|
||||
let peer2 = PeerId::random();
|
||||
|
||||
let common = 10;
|
||||
assert_eq!(
|
||||
bc.needed_blocks(peer1, count, best, common, max_parallel, max_ahead),
|
||||
Some(11..16),
|
||||
);
|
||||
assert_eq!(
|
||||
bc.needed_blocks(peer2, count, best, common, max_parallel, max_ahead),
|
||||
Some(16..21),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_at_the_end_below_common_number_not_requested() {
|
||||
let mut bc = BlockCollection::new();
|
||||
assert!(is_empty(&bc));
|
||||
|
||||
let count = 5;
|
||||
let best = 30;
|
||||
let max_parallel = 1;
|
||||
let max_ahead = 200;
|
||||
|
||||
let peer1 = PeerId::random();
|
||||
let peer2 = PeerId::random();
|
||||
|
||||
let common = 10;
|
||||
assert_eq!(
|
||||
bc.needed_blocks(peer1, count, best, common, max_parallel, max_ahead),
|
||||
Some(11..16),
|
||||
);
|
||||
|
||||
let common = 20;
|
||||
assert_eq!(
|
||||
bc.needed_blocks(peer2, count, best, common, max_parallel, max_ahead),
|
||||
Some(21..26), // not 16..21
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,134 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! A wrapper for [`FuturesUnordered`] that wakes the task up once a new future is pushed
|
||||
//! for it to be polled automatically. It's [`Stream`] never terminates.
|
||||
|
||||
use futures::{stream::FuturesUnordered, Future, Stream, StreamExt};
|
||||
use std::{
|
||||
pin::Pin,
|
||||
task::{Context, Poll, Waker},
|
||||
};
|
||||
|
||||
/// Wrapper around [`FuturesUnordered`] that wakes a task up automatically.
|
||||
pub struct FuturesStream<F> {
|
||||
futures: FuturesUnordered<F>,
|
||||
waker: Option<Waker>,
|
||||
}
|
||||
|
||||
/// Surprizingly, `#[derive(Default)]` doesn't work on [`FuturesStream`].
|
||||
impl<F> Default for FuturesStream<F> {
|
||||
fn default() -> FuturesStream<F> {
|
||||
FuturesStream { futures: Default::default(), waker: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> FuturesStream<F> {
|
||||
/// Push a future for processing.
|
||||
pub fn push(&mut self, future: F) {
|
||||
self.futures.push(future);
|
||||
|
||||
if let Some(waker) = self.waker.take() {
|
||||
waker.wake();
|
||||
}
|
||||
}
|
||||
|
||||
/// The number of futures in the stream.
|
||||
pub fn len(&self) -> usize {
|
||||
self.futures.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: Future> Stream for FuturesStream<F> {
|
||||
type Item = <F as Future>::Output;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let Poll::Ready(Some(result)) = self.futures.poll_next_unpin(cx) else {
|
||||
self.waker = Some(cx.waker().clone());
|
||||
|
||||
return Poll::Pending;
|
||||
};
|
||||
|
||||
Poll::Ready(Some(result))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use futures::future::{BoxFuture, FutureExt};
|
||||
|
||||
/// [`Stream`] implementation for [`FuturesStream`] relies on the undocumented
|
||||
/// feature that [`FuturesUnordered`] can be polled and repeatedly yield
|
||||
/// `Poll::Ready(None)` before any futures are added into it.
|
||||
#[tokio::test]
|
||||
async fn empty_futures_unordered_can_be_polled() {
|
||||
let mut unordered = FuturesUnordered::<BoxFuture<()>>::default();
|
||||
|
||||
futures::future::poll_fn(|cx| {
|
||||
assert_eq!(unordered.poll_next_unpin(cx), Poll::Ready(None));
|
||||
assert_eq!(unordered.poll_next_unpin(cx), Poll::Ready(None));
|
||||
|
||||
Poll::Ready(())
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
/// [`Stream`] implementation for [`FuturesStream`] relies on the undocumented
|
||||
/// feature that [`FuturesUnordered`] can be polled and repeatedly yield
|
||||
/// `Poll::Ready(None)` after all the futures in it have resolved.
|
||||
#[tokio::test]
|
||||
async fn deplenished_futures_unordered_can_be_polled() {
|
||||
let mut unordered = FuturesUnordered::<BoxFuture<()>>::default();
|
||||
|
||||
unordered.push(futures::future::ready(()).boxed());
|
||||
assert_eq!(unordered.next().await, Some(()));
|
||||
|
||||
futures::future::poll_fn(|cx| {
|
||||
assert_eq!(unordered.poll_next_unpin(cx), Poll::Ready(None));
|
||||
assert_eq!(unordered.poll_next_unpin(cx), Poll::Ready(None));
|
||||
|
||||
Poll::Ready(())
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn empty_futures_stream_yields_pending() {
|
||||
let mut stream = FuturesStream::<BoxFuture<()>>::default();
|
||||
|
||||
futures::future::poll_fn(|cx| {
|
||||
assert_eq!(stream.poll_next_unpin(cx), Poll::Pending);
|
||||
Poll::Ready(())
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn futures_stream_resolves_futures_and_yields_pending() {
|
||||
let mut stream = FuturesStream::default();
|
||||
stream.push(futures::future::ready(17));
|
||||
|
||||
futures::future::poll_fn(|cx| {
|
||||
assert_eq!(stream.poll_next_unpin(cx), Poll::Ready(Some(17)));
|
||||
assert_eq!(stream.poll_next_unpin(cx), Poll::Pending);
|
||||
Poll::Ready(())
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,679 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Justification requests scheduling. [`ExtraRequests`] manages requesting justifications
|
||||
//! from peers taking into account forks and their finalization (dropping pending requests
|
||||
//! that don't make sense after one of the forks is finalized).
|
||||
|
||||
use crate::{
|
||||
strategy::chain_sync::{PeerSync, PeerSyncState},
|
||||
LOG_TARGET,
|
||||
};
|
||||
use fork_tree::ForkTree;
|
||||
use log::{debug, trace, warn};
|
||||
use prometheus_endpoint::{
|
||||
prometheus::core::GenericGauge, register, GaugeVec, Opts, PrometheusError, Registry, U64,
|
||||
};
|
||||
use pezsc_network_types::PeerId;
|
||||
use pezsp_blockchain::Error as ClientError;
|
||||
use pezsp_runtime::traits::{Block as BlockT, NumberFor, Zero};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet, VecDeque},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
// Time to wait before trying to get the same extra data from the same peer.
|
||||
const EXTRA_RETRY_WAIT: Duration = Duration::from_secs(10);
|
||||
|
||||
/// Pending extra data request for the given block (hash and number).
|
||||
type ExtraRequest<B> = (<B as BlockT>::Hash, NumberFor<B>);
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Metrics {
|
||||
pending: GenericGauge<U64>,
|
||||
active: GenericGauge<U64>,
|
||||
failed: GenericGauge<U64>,
|
||||
importing: GenericGauge<U64>,
|
||||
}
|
||||
|
||||
impl Metrics {
|
||||
fn register(registry: &Registry) -> Result<Self, PrometheusError> {
|
||||
let justifications = GaugeVec::<U64>::new(
|
||||
Opts::new(
|
||||
"bizinikiwi_sync_extra_justifications",
|
||||
"Number of extra justifications requests",
|
||||
),
|
||||
&["status"],
|
||||
)?;
|
||||
let justifications = register(justifications, registry)?;
|
||||
|
||||
Ok(Self {
|
||||
pending: justifications.with_label_values(&["pending"]),
|
||||
active: justifications.with_label_values(&["active"]),
|
||||
failed: justifications.with_label_values(&["failed"]),
|
||||
importing: justifications.with_label_values(&["importing"]),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Manages pending block extra data (e.g. justification) requests.
|
||||
///
|
||||
/// Multiple extras may be requested for competing forks, or for the same branch
|
||||
/// at different (increasing) heights. This structure will guarantee that extras
|
||||
/// are fetched in-order, and that obsolete changes are pruned (when finalizing a
|
||||
/// competing fork).
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ExtraRequests<B: BlockT> {
|
||||
tree: ForkTree<B::Hash, NumberFor<B>, ()>,
|
||||
/// best finalized block number that we have seen since restart
|
||||
best_seen_finalized_number: NumberFor<B>,
|
||||
/// requests which have been queued for later processing
|
||||
pending_requests: VecDeque<ExtraRequest<B>>,
|
||||
/// requests which are currently underway to some peer
|
||||
active_requests: HashMap<PeerId, ExtraRequest<B>>,
|
||||
/// previous requests without response
|
||||
failed_requests: HashMap<ExtraRequest<B>, Vec<(PeerId, Instant)>>,
|
||||
/// successful requests
|
||||
importing_requests: HashSet<ExtraRequest<B>>,
|
||||
/// the name of this type of extra request (useful for logging.)
|
||||
request_type_name: &'static str,
|
||||
metrics: Option<Metrics>,
|
||||
}
|
||||
|
||||
impl<B: BlockT> ExtraRequests<B> {
|
||||
pub(crate) fn new(
|
||||
request_type_name: &'static str,
|
||||
metrics_registry: Option<&Registry>,
|
||||
) -> Self {
|
||||
Self {
|
||||
tree: ForkTree::new(),
|
||||
best_seen_finalized_number: Zero::zero(),
|
||||
pending_requests: VecDeque::new(),
|
||||
active_requests: HashMap::new(),
|
||||
failed_requests: HashMap::new(),
|
||||
importing_requests: HashSet::new(),
|
||||
request_type_name,
|
||||
metrics: metrics_registry.and_then(|registry| {
|
||||
Metrics::register(registry)
|
||||
.inspect_err(|error| {
|
||||
log::error!(
|
||||
target: LOG_TARGET,
|
||||
"Failed to register `ExtraRequests` metrics {error}",
|
||||
);
|
||||
})
|
||||
.ok()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset all state as if returned from `new`.
|
||||
pub(crate) fn reset(&mut self) {
|
||||
self.tree = ForkTree::new();
|
||||
self.pending_requests.clear();
|
||||
self.active_requests.clear();
|
||||
self.failed_requests.clear();
|
||||
|
||||
if let Some(metrics) = &self.metrics {
|
||||
metrics.pending.set(0);
|
||||
metrics.active.set(0);
|
||||
metrics.failed.set(0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an iterator-like struct that yields peers which extra
|
||||
/// requests can be sent to.
|
||||
pub(crate) fn matcher(&mut self) -> Matcher<'_, B> {
|
||||
Matcher::new(self)
|
||||
}
|
||||
|
||||
/// Queue an extra data request to be considered by the `Matcher`.
|
||||
pub(crate) fn schedule<F>(&mut self, request: ExtraRequest<B>, is_descendent_of: F)
|
||||
where
|
||||
F: Fn(&B::Hash, &B::Hash) -> Result<bool, ClientError>,
|
||||
{
|
||||
match self.tree.import(request.0, request.1, (), &is_descendent_of) {
|
||||
Ok(true) => {
|
||||
// this is a new root so we add it to the current `pending_requests`
|
||||
self.pending_requests.push_back((request.0, request.1));
|
||||
if let Some(metrics) = &self.metrics {
|
||||
metrics.pending.inc();
|
||||
}
|
||||
},
|
||||
Err(fork_tree::Error::Revert) => {
|
||||
// we have finalized further than the given request, presumably
|
||||
// by some other part of the system (not sync). we can safely
|
||||
// ignore the `Revert` error.
|
||||
},
|
||||
Err(err) => {
|
||||
debug!(target: LOG_TARGET, "Failed to insert request {:?} into tree: {}", request, err);
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
/// Retry any pending request if a peer disconnected.
|
||||
pub(crate) fn peer_disconnected(&mut self, who: &PeerId) {
|
||||
if let Some(request) = self.active_requests.remove(who) {
|
||||
self.pending_requests.push_front(request);
|
||||
if let Some(metrics) = &self.metrics {
|
||||
metrics.active.dec();
|
||||
metrics.pending.inc();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Processes the response for the request previously sent to the given peer.
|
||||
pub(crate) fn on_response<R>(
|
||||
&mut self,
|
||||
who: PeerId,
|
||||
resp: Option<R>,
|
||||
) -> Option<(PeerId, B::Hash, NumberFor<B>, R)> {
|
||||
// we assume that the request maps to the given response, this is
|
||||
// currently enforced by the outer network protocol before passing on
|
||||
// messages to chain sync.
|
||||
if let Some(request) = self.active_requests.remove(&who) {
|
||||
if let Some(metrics) = &self.metrics {
|
||||
metrics.active.dec();
|
||||
}
|
||||
|
||||
if let Some(r) = resp {
|
||||
trace!(target: LOG_TARGET,
|
||||
"Queuing import of {} from {:?} for {:?}",
|
||||
self.request_type_name, who, request,
|
||||
);
|
||||
|
||||
if self.importing_requests.insert(request) {
|
||||
if let Some(metrics) = &self.metrics {
|
||||
metrics.importing.inc();
|
||||
}
|
||||
}
|
||||
return Some((who, request.0, request.1, r));
|
||||
} else {
|
||||
trace!(target: LOG_TARGET,
|
||||
"Empty {} response from {:?} for {:?}",
|
||||
self.request_type_name, who, request,
|
||||
);
|
||||
}
|
||||
self.failed_requests.entry(request).or_default().push((who, Instant::now()));
|
||||
self.pending_requests.push_front(request);
|
||||
if let Some(metrics) = &self.metrics {
|
||||
metrics.failed.set(self.failed_requests.len().try_into().unwrap_or(u64::MAX));
|
||||
metrics.pending.inc();
|
||||
}
|
||||
} else {
|
||||
trace!(target: LOG_TARGET,
|
||||
"No active {} request to {:?}",
|
||||
self.request_type_name, who,
|
||||
);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Removes any pending extra requests for blocks lower than the given best finalized.
|
||||
pub(crate) fn on_block_finalized<F>(
|
||||
&mut self,
|
||||
best_finalized_hash: &B::Hash,
|
||||
best_finalized_number: NumberFor<B>,
|
||||
is_descendent_of: F,
|
||||
) -> Result<(), fork_tree::Error<ClientError>>
|
||||
where
|
||||
F: Fn(&B::Hash, &B::Hash) -> Result<bool, ClientError>,
|
||||
{
|
||||
let request = (*best_finalized_hash, best_finalized_number);
|
||||
|
||||
if self.try_finalize_root::<()>(request, Ok(request), false) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if best_finalized_number > self.best_seen_finalized_number {
|
||||
// we receive finality notification only for the finalized branch head.
|
||||
match self.tree.finalize_with_ancestors(
|
||||
best_finalized_hash,
|
||||
best_finalized_number,
|
||||
&is_descendent_of,
|
||||
) {
|
||||
Err(fork_tree::Error::Revert) => {
|
||||
// we might have finalized further already in which case we
|
||||
// will get a `Revert` error which we can safely ignore.
|
||||
},
|
||||
Err(err) => return Err(err),
|
||||
Ok(_) => {},
|
||||
}
|
||||
|
||||
self.best_seen_finalized_number = best_finalized_number;
|
||||
}
|
||||
|
||||
let roots = self.tree.roots().collect::<HashSet<_>>();
|
||||
|
||||
self.pending_requests.retain(|(h, n)| roots.contains(&(h, n, &())));
|
||||
self.active_requests.retain(|_, (h, n)| roots.contains(&(h, n, &())));
|
||||
self.failed_requests.retain(|(h, n), _| roots.contains(&(h, n, &())));
|
||||
if let Some(metrics) = &self.metrics {
|
||||
metrics.pending.set(self.pending_requests.len().try_into().unwrap_or(u64::MAX));
|
||||
metrics.active.set(self.active_requests.len().try_into().unwrap_or(u64::MAX));
|
||||
metrics.failed.set(self.failed_requests.len().try_into().unwrap_or(u64::MAX));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Try to finalize pending root.
|
||||
///
|
||||
/// Returns true if import of this request has been scheduled.
|
||||
pub(crate) fn try_finalize_root<E>(
|
||||
&mut self,
|
||||
request: ExtraRequest<B>,
|
||||
result: Result<ExtraRequest<B>, E>,
|
||||
reschedule_on_failure: bool,
|
||||
) -> bool {
|
||||
if !self.importing_requests.remove(&request) {
|
||||
return false;
|
||||
}
|
||||
if let Some(metrics) = &self.metrics {
|
||||
metrics.importing.dec();
|
||||
}
|
||||
|
||||
let (finalized_hash, finalized_number) = match result {
|
||||
Ok(req) => (req.0, req.1),
|
||||
Err(_) => {
|
||||
if reschedule_on_failure {
|
||||
self.pending_requests.push_front(request);
|
||||
if let Some(metrics) = &self.metrics {
|
||||
metrics.pending.inc();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
if self.tree.finalize_root(&finalized_hash).is_none() {
|
||||
warn!(target: LOG_TARGET,
|
||||
"‼️ Imported {:?} {:?} which isn't a root in the tree: {:?}",
|
||||
finalized_hash, finalized_number, self.tree.roots().collect::<Vec<_>>()
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
self.failed_requests.clear();
|
||||
self.active_requests.clear();
|
||||
self.pending_requests.clear();
|
||||
self.pending_requests.extend(self.tree.roots().map(|(&h, &n, _)| (h, n)));
|
||||
if let Some(metrics) = &self.metrics {
|
||||
metrics.failed.set(0);
|
||||
metrics.active.set(0);
|
||||
metrics.pending.set(self.pending_requests.len().try_into().unwrap_or(u64::MAX));
|
||||
}
|
||||
self.best_seen_finalized_number = finalized_number;
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Returns an iterator over all active (in-flight) requests and associated peer id.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn active_requests(&self) -> impl Iterator<Item = (&PeerId, &ExtraRequest<B>)> {
|
||||
self.active_requests.iter()
|
||||
}
|
||||
|
||||
/// Returns an iterator over all scheduled pending requests.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn pending_requests(&self) -> impl Iterator<Item = &ExtraRequest<B>> {
|
||||
self.pending_requests.iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// Matches peers with pending extra requests.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Matcher<'a, B: BlockT> {
|
||||
/// Length of pending requests collection.
|
||||
/// Used to ensure we do not loop more than once over all pending requests.
|
||||
remaining: usize,
|
||||
extras: &'a mut ExtraRequests<B>,
|
||||
}
|
||||
|
||||
impl<'a, B: BlockT> Matcher<'a, B> {
|
||||
fn new(extras: &'a mut ExtraRequests<B>) -> Self {
|
||||
Self { remaining: extras.pending_requests.len(), extras }
|
||||
}
|
||||
|
||||
/// Finds a peer to which a pending request can be sent.
|
||||
///
|
||||
/// Peers are filtered according to the current known best block (i.e. we won't
|
||||
/// send an extra request for block #10 to a peer at block #2), and we also
|
||||
/// throttle requests to the same peer if a previous request yielded no results.
|
||||
///
|
||||
/// This method returns as soon as it finds a peer that should be able to answer
|
||||
/// our request. If no request is pending or no peer can handle it, `None` is
|
||||
/// returned instead.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// The returned `PeerId` (if any) is guaranteed to come from the given `peers`
|
||||
/// argument.
|
||||
pub(crate) fn next(
|
||||
&mut self,
|
||||
peers: &HashMap<PeerId, PeerSync<B>>,
|
||||
) -> Option<(PeerId, ExtraRequest<B>)> {
|
||||
if self.remaining == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// clean up previously failed requests so we can retry again
|
||||
for requests in self.extras.failed_requests.values_mut() {
|
||||
requests.retain(|(_, instant)| instant.elapsed() < EXTRA_RETRY_WAIT);
|
||||
}
|
||||
if let Some(metrics) = &self.extras.metrics {
|
||||
metrics
|
||||
.failed
|
||||
.set(self.extras.failed_requests.len().try_into().unwrap_or(u64::MAX));
|
||||
}
|
||||
|
||||
while let Some(request) = self.extras.pending_requests.pop_front() {
|
||||
if let Some(metrics) = &self.extras.metrics {
|
||||
metrics.pending.dec();
|
||||
}
|
||||
|
||||
for (peer, sync) in
|
||||
peers.iter().filter(|(_, sync)| sync.state == PeerSyncState::Available)
|
||||
{
|
||||
// only ask peers that have synced at least up to the block number that we're asking
|
||||
// the extra for
|
||||
if sync.best_number < request.1 {
|
||||
continue;
|
||||
}
|
||||
// don't request to any peers that already have pending requests
|
||||
if self.extras.active_requests.contains_key(peer) {
|
||||
continue;
|
||||
}
|
||||
// only ask if the same request has not failed for this peer before
|
||||
if self
|
||||
.extras
|
||||
.failed_requests
|
||||
.get(&request)
|
||||
.map(|rr| rr.iter().any(|i| &i.0 == peer))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
self.extras.active_requests.insert(*peer, request);
|
||||
if let Some(metrics) = &self.extras.metrics {
|
||||
metrics.active.inc();
|
||||
}
|
||||
|
||||
trace!(target: LOG_TARGET,
|
||||
"Sending {} request to {:?} for {:?}",
|
||||
self.extras.request_type_name, peer, request,
|
||||
);
|
||||
|
||||
return Some((*peer, request));
|
||||
}
|
||||
|
||||
self.extras.pending_requests.push_back(request);
|
||||
if let Some(metrics) = &self.extras.metrics {
|
||||
metrics.pending.inc();
|
||||
}
|
||||
self.remaining -= 1;
|
||||
|
||||
if self.remaining == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::strategy::chain_sync::PeerSync;
|
||||
use quickcheck::{Arbitrary, Gen, QuickCheck};
|
||||
use pezsp_blockchain::Error as ClientError;
|
||||
use pezsp_test_primitives::{Block, BlockNumber, Hash};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
#[test]
|
||||
fn requests_are_processed_in_order() {
|
||||
fn property(mut peers: ArbitraryPeers) {
|
||||
let mut requests = ExtraRequests::<Block>::new("test", None);
|
||||
|
||||
let num_peers_available =
|
||||
peers.0.values().filter(|s| s.state == PeerSyncState::Available).count();
|
||||
|
||||
for i in 0..num_peers_available {
|
||||
requests.schedule((Hash::random(), i as u64), |a, b| Ok(a[0] >= b[0]))
|
||||
}
|
||||
|
||||
let pending = requests.pending_requests.clone();
|
||||
let mut m = requests.matcher();
|
||||
|
||||
for p in &pending {
|
||||
let (peer, r) = m.next(&peers.0).unwrap();
|
||||
assert_eq!(p, &r);
|
||||
peers.0.get_mut(&peer).unwrap().state =
|
||||
PeerSyncState::DownloadingJustification(r.0);
|
||||
}
|
||||
}
|
||||
|
||||
QuickCheck::new().quickcheck(property as fn(ArbitraryPeers))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_roots_schedule_new_request() {
|
||||
fn property(data: Vec<BlockNumber>) {
|
||||
let mut requests = ExtraRequests::<Block>::new("test", None);
|
||||
for (i, number) in data.into_iter().enumerate() {
|
||||
let hash = [i as u8; 32].into();
|
||||
let pending = requests.pending_requests.len();
|
||||
let is_root = requests.tree.roots().any(|(&h, &n, _)| hash == h && number == n);
|
||||
requests.schedule((hash, number), |a, b| Ok(a[0] >= b[0]));
|
||||
if !is_root {
|
||||
assert_eq!(1 + pending, requests.pending_requests.len())
|
||||
}
|
||||
}
|
||||
}
|
||||
QuickCheck::new().quickcheck(property as fn(Vec<BlockNumber>))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disconnecting_implies_rescheduling() {
|
||||
fn property(mut peers: ArbitraryPeers) -> bool {
|
||||
let mut requests = ExtraRequests::<Block>::new("test", None);
|
||||
|
||||
let num_peers_available =
|
||||
peers.0.values().filter(|s| s.state == PeerSyncState::Available).count();
|
||||
|
||||
for i in 0..num_peers_available {
|
||||
requests.schedule((Hash::random(), i as u64), |a, b| Ok(a[0] >= b[0]))
|
||||
}
|
||||
|
||||
let mut m = requests.matcher();
|
||||
while let Some((peer, r)) = m.next(&peers.0) {
|
||||
peers.0.get_mut(&peer).unwrap().state =
|
||||
PeerSyncState::DownloadingJustification(r.0);
|
||||
}
|
||||
|
||||
assert!(requests.pending_requests.is_empty());
|
||||
|
||||
let active_peers = requests.active_requests.keys().cloned().collect::<Vec<_>>();
|
||||
let previously_active =
|
||||
requests.active_requests.values().cloned().collect::<HashSet<_>>();
|
||||
|
||||
for peer in &active_peers {
|
||||
requests.peer_disconnected(peer)
|
||||
}
|
||||
|
||||
assert!(requests.active_requests.is_empty());
|
||||
|
||||
previously_active == requests.pending_requests.iter().cloned().collect::<HashSet<_>>()
|
||||
}
|
||||
|
||||
QuickCheck::new().quickcheck(property as fn(ArbitraryPeers) -> bool)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_response_reschedules() {
|
||||
fn property(mut peers: ArbitraryPeers) {
|
||||
let mut requests = ExtraRequests::<Block>::new("test", None);
|
||||
|
||||
let num_peers_available =
|
||||
peers.0.values().filter(|s| s.state == PeerSyncState::Available).count();
|
||||
|
||||
for i in 0..num_peers_available {
|
||||
requests.schedule((Hash::random(), i as u64), |a, b| Ok(a[0] >= b[0]))
|
||||
}
|
||||
|
||||
let mut m = requests.matcher();
|
||||
while let Some((peer, r)) = m.next(&peers.0) {
|
||||
peers.0.get_mut(&peer).unwrap().state =
|
||||
PeerSyncState::DownloadingJustification(r.0);
|
||||
}
|
||||
|
||||
let active = requests.active_requests.iter().map(|(&p, &r)| (p, r)).collect::<Vec<_>>();
|
||||
|
||||
for (peer, req) in &active {
|
||||
assert!(requests.failed_requests.get(req).is_none());
|
||||
assert!(!requests.pending_requests.contains(req));
|
||||
assert!(requests.on_response::<()>(*peer, None).is_none());
|
||||
assert!(requests.pending_requests.contains(req));
|
||||
assert_eq!(
|
||||
1,
|
||||
requests
|
||||
.failed_requests
|
||||
.get(req)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|(p, _)| p == peer)
|
||||
.count()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
QuickCheck::new().quickcheck(property as fn(ArbitraryPeers))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_is_rescheduled_when_earlier_block_is_finalized() {
|
||||
pezsp_tracing::try_init_simple();
|
||||
|
||||
let mut finality_proofs = ExtraRequests::<Block>::new("test", None);
|
||||
|
||||
let hash4 = [4; 32].into();
|
||||
let hash5 = [5; 32].into();
|
||||
let hash6 = [6; 32].into();
|
||||
let hash7 = [7; 32].into();
|
||||
|
||||
fn is_descendent_of(base: &Hash, target: &Hash) -> Result<bool, ClientError> {
|
||||
Ok(target[0] >= base[0])
|
||||
}
|
||||
|
||||
// make #4 last finalized block
|
||||
finality_proofs.tree.import(hash4, 4, (), &is_descendent_of).unwrap();
|
||||
finality_proofs.tree.finalize_root(&hash4);
|
||||
|
||||
// schedule request for #6
|
||||
finality_proofs.schedule((hash6, 6), is_descendent_of);
|
||||
|
||||
// receive finality proof for #5
|
||||
finality_proofs.importing_requests.insert((hash6, 6));
|
||||
finality_proofs.on_block_finalized(&hash5, 5, is_descendent_of).unwrap();
|
||||
finality_proofs.try_finalize_root::<()>((hash6, 6), Ok((hash5, 5)), true);
|
||||
|
||||
// ensure that request for #6 is still pending
|
||||
assert_eq!(finality_proofs.pending_requests.iter().collect::<Vec<_>>(), vec![&(hash6, 6)]);
|
||||
|
||||
// receive finality proof for #7
|
||||
finality_proofs.importing_requests.insert((hash6, 6));
|
||||
finality_proofs.on_block_finalized(&hash6, 6, is_descendent_of).unwrap();
|
||||
finality_proofs.on_block_finalized(&hash7, 7, is_descendent_of).unwrap();
|
||||
finality_proofs.try_finalize_root::<()>((hash6, 6), Ok((hash7, 7)), true);
|
||||
|
||||
// ensure that there's no request for #6
|
||||
assert_eq!(
|
||||
finality_proofs.pending_requests.iter().collect::<Vec<_>>(),
|
||||
Vec::<&(Hash, u64)>::new()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ancestor_roots_are_finalized_when_finality_notification_is_missed() {
|
||||
let mut finality_proofs = ExtraRequests::<Block>::new("test", None);
|
||||
|
||||
let hash4 = [4; 32].into();
|
||||
let hash5 = [5; 32].into();
|
||||
|
||||
fn is_descendent_of(base: &Hash, target: &Hash) -> Result<bool, ClientError> {
|
||||
Ok(target[0] >= base[0])
|
||||
}
|
||||
|
||||
// schedule request for #4
|
||||
finality_proofs.schedule((hash4, 4), is_descendent_of);
|
||||
|
||||
// receive finality notification for #5 (missing notification for #4!!!)
|
||||
finality_proofs.importing_requests.insert((hash4, 5));
|
||||
finality_proofs.on_block_finalized(&hash5, 5, is_descendent_of).unwrap();
|
||||
assert_eq!(finality_proofs.tree.roots().count(), 0);
|
||||
}
|
||||
|
||||
// Some Arbitrary instances to allow easy construction of random peer sets:
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ArbitraryPeerSyncState(PeerSyncState<Block>);
|
||||
|
||||
impl Arbitrary for ArbitraryPeerSyncState {
|
||||
fn arbitrary(g: &mut Gen) -> Self {
|
||||
let s = match u8::arbitrary(g) % 4 {
|
||||
0 => PeerSyncState::Available,
|
||||
// TODO: 1 => PeerSyncState::AncestorSearch(g.gen(), AncestorSearchState<B>),
|
||||
1 => PeerSyncState::DownloadingNew(BlockNumber::arbitrary(g)),
|
||||
2 => PeerSyncState::DownloadingStale(Hash::random()),
|
||||
_ => PeerSyncState::DownloadingJustification(Hash::random()),
|
||||
};
|
||||
ArbitraryPeerSyncState(s)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ArbitraryPeerSync(PeerSync<Block>);
|
||||
|
||||
impl Arbitrary for ArbitraryPeerSync {
|
||||
fn arbitrary(g: &mut Gen) -> Self {
|
||||
let ps = PeerSync {
|
||||
peer_id: PeerId::random(),
|
||||
common_number: u64::arbitrary(g),
|
||||
best_hash: Hash::random(),
|
||||
best_number: u64::arbitrary(g),
|
||||
state: ArbitraryPeerSyncState::arbitrary(g).0,
|
||||
};
|
||||
ArbitraryPeerSync(ps)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ArbitraryPeers(HashMap<PeerId, PeerSync<Block>>);
|
||||
|
||||
impl Arbitrary for ArbitraryPeers {
|
||||
fn arbitrary(g: &mut Gen) -> Self {
|
||||
let mut peers = HashMap::with_capacity(g.size());
|
||||
for _ in 0..g.size() {
|
||||
let ps = ArbitraryPeerSync::arbitrary(g).0;
|
||||
peers.insert(ps.peer_id, ps);
|
||||
}
|
||||
ArbitraryPeers(peers)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Blockchain syncing implementation in Bizinikiwi.
|
||||
|
||||
pub use schema::v1::*;
|
||||
pub use service::syncing_service::SyncingService;
|
||||
pub use strategy::warp::{WarpSyncConfig, WarpSyncPhase, WarpSyncProgress};
|
||||
pub use types::{SyncEvent, SyncEventStream, SyncState, SyncStatus, SyncStatusProvider};
|
||||
|
||||
mod block_announce_validator;
|
||||
mod futures_stream;
|
||||
mod justification_requests;
|
||||
mod pending_responses;
|
||||
mod schema;
|
||||
pub mod types;
|
||||
|
||||
pub mod block_relay_protocol;
|
||||
pub mod block_request_handler;
|
||||
pub mod blocks;
|
||||
pub mod engine;
|
||||
pub mod mock;
|
||||
pub mod service;
|
||||
pub mod state_request_handler;
|
||||
pub mod strategy;
|
||||
pub mod warp_request_handler;
|
||||
|
||||
/// Log target for this crate.
|
||||
const LOG_TARGET: &str = "sync";
|
||||
@@ -0,0 +1,48 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Contains mock implementations of `ChainSync` and 'BlockDownloader'.
|
||||
|
||||
use crate::block_relay_protocol::{BlockDownloader as BlockDownloaderT, BlockResponseError};
|
||||
|
||||
use futures::channel::oneshot;
|
||||
use pezsc_network::{ProtocolName, RequestFailure};
|
||||
use pezsc_network_common::sync::message::{BlockData, BlockRequest};
|
||||
use pezsc_network_types::PeerId;
|
||||
use pezsp_runtime::traits::Block as BlockT;
|
||||
|
||||
mockall::mock! {
|
||||
#[derive(Debug)]
|
||||
pub BlockDownloader<Block: BlockT> {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<Block: BlockT> BlockDownloaderT<Block> for BlockDownloader<Block> {
|
||||
fn protocol_name(&self) -> &ProtocolName;
|
||||
|
||||
async fn download_blocks(
|
||||
&self,
|
||||
who: PeerId,
|
||||
request: BlockRequest<Block>,
|
||||
) -> Result<Result<(Vec<u8>, ProtocolName), RequestFailure>, oneshot::Canceled>;
|
||||
fn block_response_into_blocks(
|
||||
&self,
|
||||
request: &BlockRequest<Block>,
|
||||
response: Vec<u8>,
|
||||
) -> Result<Vec<BlockData<Block>>, BlockResponseError>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! [`PendingResponses`] is responsible for keeping track of pending responses and
|
||||
//! polling them. [`Stream`] implemented by [`PendingResponses`] never terminates.
|
||||
|
||||
use crate::{strategy::StrategyKey, LOG_TARGET};
|
||||
use futures::{
|
||||
channel::oneshot,
|
||||
future::BoxFuture,
|
||||
stream::{BoxStream, FusedStream, Stream},
|
||||
FutureExt, StreamExt,
|
||||
};
|
||||
use log::error;
|
||||
use std::any::Any;
|
||||
|
||||
use pezsc_network::{request_responses::RequestFailure, types::ProtocolName};
|
||||
use pezsc_network_types::PeerId;
|
||||
use std::task::{Context, Poll, Waker};
|
||||
use tokio_stream::StreamMap;
|
||||
|
||||
/// Response result.
|
||||
type ResponseResult =
|
||||
Result<Result<(Box<dyn Any + Send>, ProtocolName), RequestFailure>, oneshot::Canceled>;
|
||||
|
||||
/// A future yielding [`ResponseResult`].
|
||||
pub(crate) type ResponseFuture = BoxFuture<'static, ResponseResult>;
|
||||
|
||||
/// An event we receive once a pending response future resolves.
|
||||
pub(crate) struct ResponseEvent {
|
||||
pub peer_id: PeerId,
|
||||
pub key: StrategyKey,
|
||||
pub response: ResponseResult,
|
||||
}
|
||||
|
||||
/// Stream taking care of polling pending responses.
|
||||
pub(crate) struct PendingResponses {
|
||||
/// Pending responses
|
||||
pending_responses: StreamMap<(PeerId, StrategyKey), BoxStream<'static, ResponseResult>>,
|
||||
/// Waker to implement never terminating stream
|
||||
waker: Option<Waker>,
|
||||
}
|
||||
|
||||
impl PendingResponses {
|
||||
pub fn new() -> Self {
|
||||
Self { pending_responses: StreamMap::new(), waker: None }
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, peer_id: PeerId, key: StrategyKey, response_future: ResponseFuture) {
|
||||
if self
|
||||
.pending_responses
|
||||
.insert((peer_id, key), Box::pin(response_future.into_stream()))
|
||||
.is_some()
|
||||
{
|
||||
error!(
|
||||
target: LOG_TARGET,
|
||||
"Discarded pending response from peer {peer_id}, strategy key: {key:?}.",
|
||||
);
|
||||
debug_assert!(false);
|
||||
}
|
||||
|
||||
if let Some(waker) = self.waker.take() {
|
||||
waker.wake();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, peer_id: PeerId, key: StrategyKey) -> bool {
|
||||
self.pending_responses.remove(&(peer_id, key)).is_some()
|
||||
}
|
||||
|
||||
pub fn remove_all(&mut self, peer_id: &PeerId) {
|
||||
let to_remove = self
|
||||
.pending_responses
|
||||
.keys()
|
||||
.filter(|(peer, _key)| peer == peer_id)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
to_remove.iter().for_each(|k| {
|
||||
self.pending_responses.remove(k);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.pending_responses.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for PendingResponses {
|
||||
type Item = ResponseEvent;
|
||||
|
||||
fn poll_next(
|
||||
mut self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Self::Item>> {
|
||||
match self.pending_responses.poll_next_unpin(cx) {
|
||||
Poll::Ready(Some(((peer_id, key), response))) => {
|
||||
// We need to manually remove the stream, because `StreamMap` doesn't know yet that
|
||||
// it's going to yield `None`, so may not remove it before the next request is made
|
||||
// to the same peer.
|
||||
self.pending_responses.remove(&(peer_id, key));
|
||||
|
||||
Poll::Ready(Some(ResponseEvent { peer_id, key, response }))
|
||||
},
|
||||
Poll::Ready(None) | Poll::Pending => {
|
||||
self.waker = Some(cx.waker().clone());
|
||||
|
||||
Poll::Pending
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// As [`PendingResponses`] never terminates, we can easily implement [`FusedStream`] for it.
|
||||
impl FusedStream for PendingResponses {
|
||||
fn is_terminated(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Include sources generated from protobuf definitions.
|
||||
|
||||
pub(crate) mod v1 {
|
||||
include!(concat!(env!("OUT_DIR"), "/api.v1.rs"));
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
// Schema definition for block request/response messages.
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package api.v1;
|
||||
|
||||
// Block enumeration direction.
|
||||
enum Direction {
|
||||
// Enumerate in ascending order (from child to parent).
|
||||
Ascending = 0;
|
||||
// Enumerate in descending order (from parent to canonical child).
|
||||
Descending = 1;
|
||||
}
|
||||
|
||||
// Request block data from a peer.
|
||||
message BlockRequest {
|
||||
// Bits of block data to request.
|
||||
uint32 fields = 1;
|
||||
// Start from this block.
|
||||
oneof from_block {
|
||||
// Start with given hash.
|
||||
bytes hash = 2;
|
||||
// Start with given block number.
|
||||
bytes number = 3;
|
||||
}
|
||||
// Sequence direction.
|
||||
// If missing, should be interpreted as "Ascending".
|
||||
Direction direction = 5;
|
||||
// Maximum number of blocks to return. An implementation defined maximum is used when unspecified.
|
||||
uint32 max_blocks = 6; // optional
|
||||
// Indicate to the receiver that we support multiple justifications. If the responder also
|
||||
// supports this it will populate the multiple justifications field in `BlockData` instead of
|
||||
// the single justification field.
|
||||
bool support_multiple_justifications = 7; // optional
|
||||
}
|
||||
|
||||
// Response to `BlockRequest`
|
||||
message BlockResponse {
|
||||
// Block data for the requested sequence.
|
||||
repeated BlockData blocks = 1;
|
||||
}
|
||||
|
||||
// Block data sent in the response.
|
||||
message BlockData {
|
||||
// Block header hash.
|
||||
bytes hash = 1;
|
||||
// Block header if requested.
|
||||
bytes header = 2; // optional
|
||||
// Block body if requested.
|
||||
repeated bytes body = 3; // optional
|
||||
// Block receipt if requested.
|
||||
bytes receipt = 4; // optional
|
||||
// Block message queue if requested.
|
||||
bytes message_queue = 5; // optional
|
||||
// Justification if requested.
|
||||
bytes justification = 6; // optional
|
||||
// True if justification should be treated as present but empty.
|
||||
// This hack is unfortunately necessary because shortcomings in the protobuf format otherwise
|
||||
// doesn't make in possible to differentiate between a lack of justification and an empty
|
||||
// justification.
|
||||
bool is_empty_justification = 7; // optional, false if absent
|
||||
// Justifications if requested.
|
||||
// Unlike the field for a single justification, this field does not required an associated
|
||||
// boolean to differentiate between the lack of justifications and empty justification(s). This
|
||||
// is because empty justifications, like all justifications, are paired with a non-empty
|
||||
// consensus engine ID.
|
||||
bytes justifications = 8; // optional
|
||||
// Indexed block body if requestd.
|
||||
repeated bytes indexed_body = 9; // optional
|
||||
}
|
||||
|
||||
// Request storage data from a peer.
|
||||
message StateRequest {
|
||||
// Block header hash.
|
||||
bytes block = 1;
|
||||
// Start from this key.
|
||||
// Multiple keys used for nested state start.
|
||||
repeated bytes start = 2; // optional
|
||||
// if 'true' indicates that response should contain raw key-values, rather than proof.
|
||||
bool no_proof = 3;
|
||||
}
|
||||
|
||||
message StateResponse {
|
||||
// A collection of keys-values states. Only populated if `no_proof` is `true`
|
||||
repeated KeyValueStateEntry entries = 1;
|
||||
// If `no_proof` is false in request, this contains proof nodes.
|
||||
bytes proof = 2;
|
||||
}
|
||||
|
||||
// A key value state.
|
||||
message KeyValueStateEntry {
|
||||
// Root of for this level, empty length bytes
|
||||
// if top level.
|
||||
bytes state_root = 1;
|
||||
// A collection of keys-values.
|
||||
repeated StateEntry entries = 2;
|
||||
// Set to true when there are no more keys to return.
|
||||
bool complete = 3;
|
||||
}
|
||||
|
||||
// A key-value pair.
|
||||
message StateEntry {
|
||||
bytes key = 1;
|
||||
bytes value = 2;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use futures::channel::oneshot;
|
||||
|
||||
use pezsc_consensus::{BlockImportError, BlockImportStatus};
|
||||
use pezsc_network::{
|
||||
config::MultiaddrWithPeerId,
|
||||
request_responses::{IfDisconnected, RequestFailure},
|
||||
types::ProtocolName,
|
||||
NetworkPeers, NetworkRequest, NetworkSyncForkRequest, ReputationChange,
|
||||
};
|
||||
use pezsc_network_common::role::ObservedRole;
|
||||
use pezsc_network_types::{multiaddr::Multiaddr, PeerId};
|
||||
use pezsp_runtime::traits::{Block as BlockT, NumberFor};
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
mockall::mock! {
|
||||
pub ChainSyncInterface<B: BlockT> {
|
||||
pub fn justification_sync_link_request_justification(&self, hash: &B::Hash, number: NumberFor<B>);
|
||||
pub fn justification_sync_link_clear_justification_requests(&self);
|
||||
}
|
||||
|
||||
impl<B: BlockT + 'static> NetworkSyncForkRequest<B::Hash, NumberFor<B>>
|
||||
for ChainSyncInterface<B>
|
||||
{
|
||||
fn set_sync_fork_request(&self, peers: Vec<PeerId>, hash: B::Hash, number: NumberFor<B>);
|
||||
}
|
||||
|
||||
impl<B: BlockT> pezsc_consensus::Link<B> for ChainSyncInterface<B> {
|
||||
fn blocks_processed(
|
||||
&self,
|
||||
imported: usize,
|
||||
count: usize,
|
||||
results: Vec<(Result<BlockImportStatus<NumberFor<B>>, BlockImportError>, B::Hash)>,
|
||||
);
|
||||
fn justification_imported(
|
||||
&self,
|
||||
who: PeerId,
|
||||
hash: &B::Hash,
|
||||
number: NumberFor<B>,
|
||||
import_result: pezsc_consensus::JustificationImportResult,
|
||||
);
|
||||
fn request_justification(&self, hash: &B::Hash, number: NumberFor<B>);
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: BlockT> pezsc_consensus::JustificationSyncLink<B> for MockChainSyncInterface<B> {
|
||||
fn request_justification(&self, hash: &B::Hash, number: NumberFor<B>) {
|
||||
self.justification_sync_link_request_justification(hash, number);
|
||||
}
|
||||
|
||||
fn clear_justification_requests(&self) {
|
||||
self.justification_sync_link_clear_justification_requests();
|
||||
}
|
||||
}
|
||||
|
||||
mockall::mock! {
|
||||
pub NetworkServiceHandle {}
|
||||
}
|
||||
|
||||
// Mocked `Network` for `ChainSync`-related tests
|
||||
mockall::mock! {
|
||||
pub Network {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl NetworkPeers for Network {
|
||||
fn set_authorized_peers(&self, peers: HashSet<PeerId>);
|
||||
fn set_authorized_only(&self, reserved_only: bool);
|
||||
fn add_known_address(&self, peer_id: PeerId, addr: Multiaddr);
|
||||
fn report_peer(&self, peer_id: PeerId, cost_benefit: ReputationChange);
|
||||
fn peer_reputation(&self, peer_id: &PeerId) -> i32;
|
||||
fn disconnect_peer(&self, peer_id: PeerId, protocol: ProtocolName);
|
||||
fn accept_unreserved_peers(&self);
|
||||
fn deny_unreserved_peers(&self);
|
||||
fn add_reserved_peer(&self, peer: MultiaddrWithPeerId) -> Result<(), String>;
|
||||
fn remove_reserved_peer(&self, peer_id: PeerId);
|
||||
fn set_reserved_peers(
|
||||
&self,
|
||||
protocol: ProtocolName,
|
||||
peers: HashSet<Multiaddr>,
|
||||
) -> Result<(), String>;
|
||||
fn add_peers_to_reserved_set(
|
||||
&self,
|
||||
protocol: ProtocolName,
|
||||
peers: HashSet<Multiaddr>,
|
||||
) -> Result<(), String>;
|
||||
fn remove_peers_from_reserved_set(
|
||||
&self,
|
||||
protocol: ProtocolName,
|
||||
peers: Vec<PeerId>
|
||||
) -> Result<(), String>;
|
||||
fn sync_num_connected(&self) -> usize;
|
||||
fn peer_role(&self, peer_id: PeerId, handshake: Vec<u8>) -> Option<ObservedRole>;
|
||||
async fn reserved_peers(&self) -> Result<Vec<pezsc_network_types::PeerId>, ()>;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl NetworkRequest for Network {
|
||||
async fn request(
|
||||
&self,
|
||||
target: PeerId,
|
||||
protocol: ProtocolName,
|
||||
request: Vec<u8>,
|
||||
fallback_request: Option<(Vec<u8>, ProtocolName)>,
|
||||
connect: IfDisconnected,
|
||||
) -> Result<(Vec<u8>, ProtocolName), RequestFailure>;
|
||||
fn start_request(
|
||||
&self,
|
||||
target: PeerId,
|
||||
protocol: ProtocolName,
|
||||
request: Vec<u8>,
|
||||
fallback_request: Option<(Vec<u8>, ProtocolName)>,
|
||||
tx: oneshot::Sender<Result<(Vec<u8>, ProtocolName), RequestFailure>>,
|
||||
connect: IfDisconnected,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! `SyncingEngine`-related service code
|
||||
|
||||
pub mod mock;
|
||||
pub mod network;
|
||||
pub mod syncing_service;
|
||||
@@ -0,0 +1,169 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use futures::{channel::oneshot, StreamExt};
|
||||
use pezsc_network_types::PeerId;
|
||||
|
||||
use pezsc_network::{
|
||||
request_responses::{IfDisconnected, RequestFailure},
|
||||
types::ProtocolName,
|
||||
NetworkPeers, NetworkRequest, ReputationChange,
|
||||
};
|
||||
use pezsc_utils::mpsc::{tracing_unbounded, TracingUnboundedReceiver, TracingUnboundedSender};
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Network-related services required by `sc-network-sync`
|
||||
pub trait Network: NetworkPeers + NetworkRequest {}
|
||||
|
||||
impl<T> Network for T where T: NetworkPeers + NetworkRequest {}
|
||||
|
||||
/// Network service provider for `ChainSync`
|
||||
///
|
||||
/// It runs as an asynchronous task and listens to commands coming from `ChainSync` and
|
||||
/// calls the `NetworkService` on its behalf.
|
||||
pub struct NetworkServiceProvider {
|
||||
rx: TracingUnboundedReceiver<ToServiceCommand>,
|
||||
handle: NetworkServiceHandle,
|
||||
}
|
||||
|
||||
/// Commands that `ChainSync` wishes to send to `NetworkService`
|
||||
#[derive(Debug)]
|
||||
pub enum ToServiceCommand {
|
||||
/// Call `NetworkPeers::disconnect_peer()`
|
||||
DisconnectPeer(PeerId, ProtocolName),
|
||||
|
||||
/// Call `NetworkPeers::report_peer()`
|
||||
ReportPeer(PeerId, ReputationChange),
|
||||
|
||||
/// Call `NetworkRequest::start_request()`
|
||||
StartRequest(
|
||||
PeerId,
|
||||
ProtocolName,
|
||||
Vec<u8>,
|
||||
oneshot::Sender<Result<(Vec<u8>, ProtocolName), RequestFailure>>,
|
||||
IfDisconnected,
|
||||
),
|
||||
}
|
||||
|
||||
/// Handle that is (temporarily) passed to `ChainSync` so it can
|
||||
/// communicate with `NetworkService` through `SyncingEngine`
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NetworkServiceHandle {
|
||||
tx: TracingUnboundedSender<ToServiceCommand>,
|
||||
}
|
||||
|
||||
impl NetworkServiceHandle {
|
||||
/// Create new service handle
|
||||
pub fn new(tx: TracingUnboundedSender<ToServiceCommand>) -> NetworkServiceHandle {
|
||||
Self { tx }
|
||||
}
|
||||
|
||||
/// Report peer
|
||||
pub fn report_peer(&self, who: PeerId, cost_benefit: ReputationChange) {
|
||||
let _ = self.tx.unbounded_send(ToServiceCommand::ReportPeer(who, cost_benefit));
|
||||
}
|
||||
|
||||
/// Disconnect peer
|
||||
pub fn disconnect_peer(&self, who: PeerId, protocol: ProtocolName) {
|
||||
let _ = self.tx.unbounded_send(ToServiceCommand::DisconnectPeer(who, protocol));
|
||||
}
|
||||
|
||||
/// Send request to peer
|
||||
pub fn start_request(
|
||||
&self,
|
||||
who: PeerId,
|
||||
protocol: ProtocolName,
|
||||
request: Vec<u8>,
|
||||
tx: oneshot::Sender<Result<(Vec<u8>, ProtocolName), RequestFailure>>,
|
||||
connect: IfDisconnected,
|
||||
) {
|
||||
let _ = self
|
||||
.tx
|
||||
.unbounded_send(ToServiceCommand::StartRequest(who, protocol, request, tx, connect));
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkServiceProvider {
|
||||
/// Create new `NetworkServiceProvider`
|
||||
pub fn new() -> Self {
|
||||
let (tx, rx) = tracing_unbounded("mpsc_network_service_provider", 100_000);
|
||||
|
||||
Self { rx, handle: NetworkServiceHandle::new(tx) }
|
||||
}
|
||||
|
||||
/// Get handle to talk to the provider
|
||||
pub fn handle(&self) -> NetworkServiceHandle {
|
||||
self.handle.clone()
|
||||
}
|
||||
|
||||
/// Run the `NetworkServiceProvider`
|
||||
pub async fn run(self, service: Arc<dyn Network + Send + Sync>) {
|
||||
let Self { mut rx, handle } = self;
|
||||
drop(handle);
|
||||
|
||||
while let Some(inner) = rx.next().await {
|
||||
match inner {
|
||||
ToServiceCommand::DisconnectPeer(peer, protocol_name) =>
|
||||
service.disconnect_peer(peer, protocol_name),
|
||||
ToServiceCommand::ReportPeer(peer, reputation_change) =>
|
||||
service.report_peer(peer, reputation_change),
|
||||
ToServiceCommand::StartRequest(peer, protocol, request, tx, connect) =>
|
||||
service.start_request(peer, protocol, request, None, tx, connect),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::service::mock::MockNetwork;
|
||||
|
||||
// typical pattern in `Protocol` code where peer is disconnected
|
||||
// and then reported
|
||||
#[tokio::test]
|
||||
async fn disconnect_and_report_peer() {
|
||||
let provider = NetworkServiceProvider::new();
|
||||
let handle = provider.handle();
|
||||
|
||||
let peer = PeerId::random();
|
||||
let proto = ProtocolName::from("test-protocol");
|
||||
let proto_clone = proto.clone();
|
||||
let change = pezsc_network::ReputationChange::new_fatal("test-change");
|
||||
|
||||
let mut mock_network = MockNetwork::new();
|
||||
mock_network
|
||||
.expect_disconnect_peer()
|
||||
.withf(move |in_peer, in_proto| &peer == in_peer && &proto == in_proto)
|
||||
.once()
|
||||
.returning(|_, _| ());
|
||||
mock_network
|
||||
.expect_report_peer()
|
||||
.withf(move |in_peer, in_change| &peer == in_peer && &change == in_change)
|
||||
.once()
|
||||
.returning(|_, _| ());
|
||||
|
||||
tokio::spawn(async move {
|
||||
provider.run(Arc::new(mock_network)).await;
|
||||
});
|
||||
|
||||
handle.disconnect_peer(peer, proto_clone);
|
||||
handle.report_peer(peer, change);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::types::{ExtendedPeerInfo, SyncEvent, SyncEventStream, SyncStatus, SyncStatusProvider};
|
||||
|
||||
use futures::{channel::oneshot, Stream};
|
||||
use pezsc_network_types::PeerId;
|
||||
|
||||
use pezsc_consensus::{
|
||||
BlockImportError, BlockImportStatus, JustificationImportResult, JustificationSyncLink, Link,
|
||||
};
|
||||
use pezsc_network::{NetworkBlock, NetworkSyncForkRequest};
|
||||
use pezsc_utils::mpsc::{tracing_unbounded, TracingUnboundedSender};
|
||||
use pezsp_runtime::traits::{Block as BlockT, NumberFor};
|
||||
|
||||
use std::{
|
||||
pin::Pin,
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
|
||||
/// Commands send to `SyncingEngine`
|
||||
pub enum ToServiceCommand<B: BlockT> {
|
||||
SetSyncForkRequest(Vec<PeerId>, B::Hash, NumberFor<B>),
|
||||
RequestJustification(B::Hash, NumberFor<B>),
|
||||
ClearJustificationRequests,
|
||||
BlocksProcessed(
|
||||
usize,
|
||||
usize,
|
||||
Vec<(Result<BlockImportStatus<NumberFor<B>>, BlockImportError>, B::Hash)>,
|
||||
),
|
||||
JustificationImported(PeerId, B::Hash, NumberFor<B>, JustificationImportResult),
|
||||
AnnounceBlock(B::Hash, Option<Vec<u8>>),
|
||||
NewBestBlockImported(B::Hash, NumberFor<B>),
|
||||
EventStream(TracingUnboundedSender<SyncEvent>),
|
||||
Status(oneshot::Sender<SyncStatus<B>>),
|
||||
NumActivePeers(oneshot::Sender<usize>),
|
||||
NumDownloadedBlocks(oneshot::Sender<usize>),
|
||||
NumSyncRequests(oneshot::Sender<usize>),
|
||||
PeersInfo(oneshot::Sender<Vec<(PeerId, ExtendedPeerInfo<B>)>>),
|
||||
OnBlockFinalized(B::Hash, B::Header),
|
||||
// Status {
|
||||
// pending_response: oneshot::Sender<SyncStatus<B>>,
|
||||
// },
|
||||
}
|
||||
|
||||
/// Handle for communicating with `SyncingEngine` asynchronously
|
||||
#[derive(Clone)]
|
||||
pub struct SyncingService<B: BlockT> {
|
||||
tx: TracingUnboundedSender<ToServiceCommand<B>>,
|
||||
/// Number of peers we're connected to.
|
||||
num_connected: Arc<AtomicUsize>,
|
||||
/// Are we actively catching up with the chain?
|
||||
is_major_syncing: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl<B: BlockT> SyncingService<B> {
|
||||
/// Create new handle
|
||||
pub fn new(
|
||||
tx: TracingUnboundedSender<ToServiceCommand<B>>,
|
||||
num_connected: Arc<AtomicUsize>,
|
||||
is_major_syncing: Arc<AtomicBool>,
|
||||
) -> Self {
|
||||
Self { tx, num_connected, is_major_syncing }
|
||||
}
|
||||
|
||||
/// Get the number of peers known to `SyncingEngine` (both full and light).
|
||||
pub fn num_connected_peers(&self) -> usize {
|
||||
self.num_connected.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Get the number of active peers.
|
||||
pub async fn num_active_peers(&self) -> Result<usize, oneshot::Canceled> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let _ = self.tx.unbounded_send(ToServiceCommand::NumActivePeers(tx));
|
||||
|
||||
rx.await
|
||||
}
|
||||
|
||||
/// Get the number of downloaded blocks.
|
||||
pub async fn num_downloaded_blocks(&self) -> Result<usize, oneshot::Canceled> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let _ = self.tx.unbounded_send(ToServiceCommand::NumDownloadedBlocks(tx));
|
||||
|
||||
rx.await
|
||||
}
|
||||
|
||||
/// Get the number of sync requests.
|
||||
pub async fn num_sync_requests(&self) -> Result<usize, oneshot::Canceled> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let _ = self.tx.unbounded_send(ToServiceCommand::NumSyncRequests(tx));
|
||||
|
||||
rx.await
|
||||
}
|
||||
|
||||
/// Get peer information.
|
||||
pub async fn peers_info(
|
||||
&self,
|
||||
) -> Result<Vec<(PeerId, ExtendedPeerInfo<B>)>, oneshot::Canceled> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let _ = self.tx.unbounded_send(ToServiceCommand::PeersInfo(tx));
|
||||
|
||||
rx.await
|
||||
}
|
||||
|
||||
/// Notify the `SyncingEngine` that a block has been finalized.
|
||||
pub fn on_block_finalized(&self, hash: B::Hash, header: B::Header) {
|
||||
let _ = self.tx.unbounded_send(ToServiceCommand::OnBlockFinalized(hash, header));
|
||||
}
|
||||
|
||||
/// Get sync status
|
||||
///
|
||||
/// Returns an error if `SyncingEngine` has terminated.
|
||||
pub async fn status(&self) -> Result<SyncStatus<B>, oneshot::Canceled> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let _ = self.tx.unbounded_send(ToServiceCommand::Status(tx));
|
||||
|
||||
rx.await
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: BlockT + 'static> NetworkSyncForkRequest<B::Hash, NumberFor<B>> for SyncingService<B> {
|
||||
/// Configure an explicit fork sync request.
|
||||
///
|
||||
/// Note that this function should not be used for recent blocks.
|
||||
/// Sync should be able to download all the recent forks normally.
|
||||
/// `set_sync_fork_request` should only be used if external code detects that there's
|
||||
/// a stale fork missing.
|
||||
///
|
||||
/// Passing empty `peers` set effectively removes the sync request.
|
||||
fn set_sync_fork_request(&self, peers: Vec<PeerId>, hash: B::Hash, number: NumberFor<B>) {
|
||||
let _ = self
|
||||
.tx
|
||||
.unbounded_send(ToServiceCommand::SetSyncForkRequest(peers, hash, number));
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: BlockT> JustificationSyncLink<B> for SyncingService<B> {
|
||||
/// Request a justification for the given block from the network.
|
||||
///
|
||||
/// On success, the justification will be passed to the import queue that was part at
|
||||
/// initialization as part of the configuration.
|
||||
fn request_justification(&self, hash: &B::Hash, number: NumberFor<B>) {
|
||||
let _ = self.tx.unbounded_send(ToServiceCommand::RequestJustification(*hash, number));
|
||||
}
|
||||
|
||||
fn clear_justification_requests(&self) {
|
||||
let _ = self.tx.unbounded_send(ToServiceCommand::ClearJustificationRequests);
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<B: BlockT> SyncStatusProvider<B> for SyncingService<B> {
|
||||
/// Get high-level view of the syncing status.
|
||||
async fn status(&self) -> Result<SyncStatus<B>, ()> {
|
||||
let (rtx, rrx) = oneshot::channel();
|
||||
|
||||
let _ = self.tx.unbounded_send(ToServiceCommand::Status(rtx));
|
||||
rrx.await.map_err(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: BlockT> Link<B> for SyncingService<B> {
|
||||
fn blocks_processed(
|
||||
&self,
|
||||
imported: usize,
|
||||
count: usize,
|
||||
results: Vec<(Result<BlockImportStatus<NumberFor<B>>, BlockImportError>, B::Hash)>,
|
||||
) {
|
||||
let _ = self
|
||||
.tx
|
||||
.unbounded_send(ToServiceCommand::BlocksProcessed(imported, count, results));
|
||||
}
|
||||
|
||||
fn justification_imported(
|
||||
&self,
|
||||
who: PeerId,
|
||||
hash: &B::Hash,
|
||||
number: NumberFor<B>,
|
||||
import_result: JustificationImportResult,
|
||||
) {
|
||||
let _ = self.tx.unbounded_send(ToServiceCommand::JustificationImported(
|
||||
who,
|
||||
*hash,
|
||||
number,
|
||||
import_result,
|
||||
));
|
||||
}
|
||||
|
||||
fn request_justification(&self, hash: &B::Hash, number: NumberFor<B>) {
|
||||
let _ = self.tx.unbounded_send(ToServiceCommand::RequestJustification(*hash, number));
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: BlockT> SyncEventStream for SyncingService<B> {
|
||||
/// Get syncing event stream.
|
||||
fn event_stream(&self, name: &'static str) -> Pin<Box<dyn Stream<Item = SyncEvent> + Send>> {
|
||||
let (tx, rx) = tracing_unbounded(name, 100_000);
|
||||
let _ = self.tx.unbounded_send(ToServiceCommand::EventStream(tx));
|
||||
Box::pin(rx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: BlockT> NetworkBlock<B::Hash, NumberFor<B>> for SyncingService<B> {
|
||||
fn announce_block(&self, hash: B::Hash, data: Option<Vec<u8>>) {
|
||||
let _ = self.tx.unbounded_send(ToServiceCommand::AnnounceBlock(hash, data));
|
||||
}
|
||||
|
||||
fn new_best_block_imported(&self, hash: B::Hash, number: NumberFor<B>) {
|
||||
let _ = self.tx.unbounded_send(ToServiceCommand::NewBestBlockImported(hash, number));
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: BlockT> pezsp_consensus::SyncOracle for SyncingService<B> {
|
||||
fn is_major_syncing(&self) -> bool {
|
||||
self.is_major_syncing.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
fn is_offline(&self) -> bool {
|
||||
self.num_connected.load(Ordering::Relaxed) == 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Bizinikiwi.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// Bizinikiwi 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.
|
||||
|
||||
// Bizinikiwi 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 Bizinikiwi. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Helper for handling (i.e. answering) state requests from a remote peer via the
|
||||
//! `crate::request_responses::RequestResponsesBehaviour`.
|
||||
|
||||
use crate::{
|
||||
schema::v1::{KeyValueStateEntry, StateEntry, StateRequest, StateResponse},
|
||||
LOG_TARGET,
|
||||
};
|
||||
|
||||
use codec::{Decode, Encode};
|
||||
use futures::{channel::oneshot, stream::StreamExt};
|
||||
use log::{debug, trace};
|
||||
use prost::Message;
|
||||
use pezsc_network_types::PeerId;
|
||||
use schnellru::{ByLength, LruMap};
|
||||
|
||||
use pezsc_client_api::{BlockBackend, ProofProvider};
|
||||
use pezsc_network::{
|
||||
config::ProtocolId,
|
||||
request_responses::{IncomingRequest, OutgoingResponse},
|
||||
NetworkBackend, MAX_RESPONSE_SIZE,
|
||||
};
|
||||
use pezsp_runtime::traits::Block as BlockT;
|
||||
|
||||
use std::{
|
||||
hash::{Hash, Hasher},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
const MAX_RESPONSE_BYTES: usize = 2 * 1024 * 1024; // Actual reponse may be bigger.
|
||||
const MAX_NUMBER_OF_SAME_REQUESTS_PER_PEER: usize = 2;
|
||||
|
||||
mod rep {
|
||||
use pezsc_network::ReputationChange as Rep;
|
||||
|
||||
/// Reputation change when a peer sent us the same request multiple times.
|
||||
pub const SAME_REQUEST: Rep = Rep::new(i32::MIN, "Same state request multiple times");
|
||||
}
|
||||
|
||||
/// Generates a `RequestResponseProtocolConfig` for the state request protocol, refusing incoming
|
||||
/// requests.
|
||||
pub fn generate_protocol_config<
|
||||
Hash: AsRef<[u8]>,
|
||||
B: BlockT,
|
||||
N: NetworkBackend<B, <B as BlockT>::Hash>,
|
||||
>(
|
||||
protocol_id: &ProtocolId,
|
||||
genesis_hash: Hash,
|
||||
fork_id: Option<&str>,
|
||||
inbound_queue: async_channel::Sender<IncomingRequest>,
|
||||
) -> N::RequestResponseProtocolConfig {
|
||||
N::request_response_config(
|
||||
generate_protocol_name(genesis_hash, fork_id).into(),
|
||||
std::iter::once(generate_legacy_protocol_name(protocol_id).into()).collect(),
|
||||
1024 * 1024,
|
||||
MAX_RESPONSE_SIZE,
|
||||
Duration::from_secs(40),
|
||||
Some(inbound_queue),
|
||||
)
|
||||
}
|
||||
|
||||
/// Generate the state protocol name from the genesis hash and fork id.
|
||||
fn generate_protocol_name<Hash: AsRef<[u8]>>(genesis_hash: Hash, fork_id: Option<&str>) -> String {
|
||||
let genesis_hash = genesis_hash.as_ref();
|
||||
if let Some(fork_id) = fork_id {
|
||||
format!("/{}/{}/state/2", array_bytes::bytes2hex("", genesis_hash), fork_id)
|
||||
} else {
|
||||
format!("/{}/state/2", array_bytes::bytes2hex("", genesis_hash))
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate the legacy state protocol name from chain specific protocol identifier.
|
||||
fn generate_legacy_protocol_name(protocol_id: &ProtocolId) -> String {
|
||||
format!("/{}/state/2", protocol_id.as_ref())
|
||||
}
|
||||
|
||||
/// The key of [`BlockRequestHandler::seen_requests`].
|
||||
#[derive(Eq, PartialEq, Clone)]
|
||||
struct SeenRequestsKey<B: BlockT> {
|
||||
peer: PeerId,
|
||||
block: B::Hash,
|
||||
start: Vec<Vec<u8>>,
|
||||
}
|
||||
|
||||
#[allow(clippy::derived_hash_with_manual_eq)]
|
||||
impl<B: BlockT> Hash for SeenRequestsKey<B> {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.peer.hash(state);
|
||||
self.block.hash(state);
|
||||
self.start.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
/// The value of [`StateRequestHandler::seen_requests`].
|
||||
enum SeenRequestsValue {
|
||||
/// First time we have seen the request.
|
||||
First,
|
||||
/// We have fulfilled the request `n` times.
|
||||
Fulfilled(usize),
|
||||
}
|
||||
|
||||
/// Handler for incoming block requests from a remote peer.
|
||||
pub struct StateRequestHandler<B: BlockT, Client> {
|
||||
client: Arc<Client>,
|
||||
request_receiver: async_channel::Receiver<IncomingRequest>,
|
||||
/// Maps from request to number of times we have seen this request.
|
||||
///
|
||||
/// This is used to check if a peer is spamming us with the same request.
|
||||
seen_requests: LruMap<SeenRequestsKey<B>, SeenRequestsValue>,
|
||||
}
|
||||
|
||||
impl<B, Client> StateRequestHandler<B, Client>
|
||||
where
|
||||
B: BlockT,
|
||||
Client: BlockBackend<B> + ProofProvider<B> + Send + Sync + 'static,
|
||||
{
|
||||
/// Create a new [`StateRequestHandler`].
|
||||
pub fn new<N: NetworkBackend<B, <B as BlockT>::Hash>>(
|
||||
protocol_id: &ProtocolId,
|
||||
fork_id: Option<&str>,
|
||||
client: Arc<Client>,
|
||||
num_peer_hint: usize,
|
||||
) -> (Self, N::RequestResponseProtocolConfig) {
|
||||
// Reserve enough request slots for one request per peer when we are at the maximum
|
||||
// number of peers.
|
||||
let capacity = std::cmp::max(num_peer_hint, 1);
|
||||
let (tx, request_receiver) = async_channel::bounded(capacity);
|
||||
|
||||
let protocol_config = generate_protocol_config::<_, B, N>(
|
||||
protocol_id,
|
||||
client
|
||||
.block_hash(0u32.into())
|
||||
.ok()
|
||||
.flatten()
|
||||
.expect("Genesis block exists; qed"),
|
||||
fork_id,
|
||||
tx,
|
||||
);
|
||||
|
||||
let capacity = ByLength::new(num_peer_hint.max(1) as u32 * 2);
|
||||
let seen_requests = LruMap::new(capacity);
|
||||
|
||||
(Self { client, request_receiver, seen_requests }, protocol_config)
|
||||
}
|
||||
|
||||
/// Run [`StateRequestHandler`].
|
||||
pub async fn run(mut self) {
|
||||
while let Some(request) = self.request_receiver.next().await {
|
||||
let IncomingRequest { peer, payload, pending_response } = request;
|
||||
|
||||
match self.handle_request(payload, pending_response, &peer) {
|
||||
Ok(()) => debug!(target: LOG_TARGET, "Handled block request from {}.", peer),
|
||||
Err(e) => debug!(
|
||||
target: LOG_TARGET,
|
||||
"Failed to handle state request from {}: {}", peer, e,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_request(
|
||||
&mut self,
|
||||
payload: Vec<u8>,
|
||||
pending_response: oneshot::Sender<OutgoingResponse>,
|
||||
peer: &PeerId,
|
||||
) -> Result<(), HandleRequestError> {
|
||||
let request = StateRequest::decode(&payload[..])?;
|
||||
let block: B::Hash = Decode::decode(&mut request.block.as_ref())?;
|
||||
|
||||
let key = SeenRequestsKey { peer: *peer, block, start: request.start.clone() };
|
||||
|
||||
let mut reputation_changes = Vec::new();
|
||||
|
||||
match self.seen_requests.get(&key) {
|
||||
Some(SeenRequestsValue::First) => {},
|
||||
Some(SeenRequestsValue::Fulfilled(ref mut requests)) => {
|
||||
*requests = requests.saturating_add(1);
|
||||
|
||||
if *requests > MAX_NUMBER_OF_SAME_REQUESTS_PER_PEER {
|
||||
reputation_changes.push(rep::SAME_REQUEST);
|
||||
}
|
||||
},
|
||||
None => {
|
||||
self.seen_requests.insert(key.clone(), SeenRequestsValue::First);
|
||||
},
|
||||
}
|
||||
|
||||
trace!(
|
||||
target: LOG_TARGET,
|
||||
"Handling state request from {}: Block {:?}, Starting at {:x?}, no_proof={}",
|
||||
peer,
|
||||
request.block,
|
||||
&request.start,
|
||||
request.no_proof,
|
||||
);
|
||||
|
||||
let result = if reputation_changes.is_empty() {
|
||||
let mut response = StateResponse::default();
|
||||
|
||||
if !request.no_proof {
|
||||
let (proof, _count) = self.client.read_proof_collection(
|
||||
block,
|
||||
request.start.as_slice(),
|
||||
MAX_RESPONSE_BYTES,
|
||||
)?;
|
||||
response.proof = proof.encode();
|
||||
} else {
|
||||
let entries = self.client.storage_collection(
|
||||
block,
|
||||
request.start.as_slice(),
|
||||
MAX_RESPONSE_BYTES,
|
||||
)?;
|
||||
response.entries = entries
|
||||
.into_iter()
|
||||
.map(|(state, complete)| KeyValueStateEntry {
|
||||
state_root: state.state_root,
|
||||
entries: state
|
||||
.key_values
|
||||
.into_iter()
|
||||
.map(|(key, value)| StateEntry { key, value })
|
||||
.collect(),
|
||||
complete,
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
trace!(
|
||||
target: LOG_TARGET,
|
||||
"StateResponse contains {} keys, {}, proof nodes, from {:?} to {:?}",
|
||||
response.entries.len(),
|
||||
response.proof.len(),
|
||||
response.entries.get(0).and_then(|top| top
|
||||
.entries
|
||||
.first()
|
||||
.map(|e| pezsp_core::hexdisplay::HexDisplay::from(&e.key))),
|
||||
response.entries.get(0).and_then(|top| top
|
||||
.entries
|
||||
.last()
|
||||
.map(|e| pezsp_core::hexdisplay::HexDisplay::from(&e.key))),
|
||||
);
|
||||
if let Some(value) = self.seen_requests.get(&key) {
|
||||
// If this is the first time we have processed this request, we need to change
|
||||
// it to `Fulfilled`.
|
||||
if let SeenRequestsValue::First = value {
|
||||
*value = SeenRequestsValue::Fulfilled(1);
|
||||
}
|
||||
}
|
||||
|
||||
let mut data = Vec::with_capacity(response.encoded_len());
|
||||
response.encode(&mut data)?;
|
||||
Ok(data)
|
||||
} else {
|
||||
Err(())
|
||||
};
|
||||
|
||||
pending_response
|
||||
.send(OutgoingResponse { result, reputation_changes, sent_feedback: None })
|
||||
.map_err(|_| HandleRequestError::SendResponse)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
enum HandleRequestError {
|
||||
#[error("Failed to decode request: {0}.")]
|
||||
DecodeProto(#[from] prost::DecodeError),
|
||||
|
||||
#[error("Failed to encode response: {0}.")]
|
||||
EncodeProto(#[from] prost::EncodeError),
|
||||
|
||||
#[error("Failed to decode block hash: {0}.")]
|
||||
InvalidHash(#[from] codec::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Client(#[from] pezsp_blockchain::Error),
|
||||
|
||||
#[error("Failed to send response.")]
|
||||
SendResponse,
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! [`SyncingStrategy`] defines an interface [`crate::engine::SyncingEngine`] uses as a specific
|
||||
//! syncing algorithm.
|
||||
//!
|
||||
//! A few different strategies are provided by Bizinikiwi out of the box with custom strategies
|
||||
//! possible too.
|
||||
|
||||
pub mod chain_sync;
|
||||
mod disconnected_peers;
|
||||
pub mod pezkuwi;
|
||||
pub mod state;
|
||||
pub mod state_sync;
|
||||
pub mod warp;
|
||||
|
||||
use crate::{
|
||||
pending_responses::ResponseFuture,
|
||||
service::network::NetworkServiceHandle,
|
||||
types::{BadPeer, SyncStatus},
|
||||
};
|
||||
use pezsc_consensus::{BlockImportError, BlockImportStatus, IncomingBlock};
|
||||
use pezsc_network::ProtocolName;
|
||||
use pezsc_network_common::sync::message::BlockAnnounce;
|
||||
use pezsc_network_types::PeerId;
|
||||
use pezsp_blockchain::Error as ClientError;
|
||||
use pezsp_consensus::BlockOrigin;
|
||||
use pezsp_runtime::{
|
||||
traits::{Block as BlockT, NumberFor},
|
||||
Justifications,
|
||||
};
|
||||
use std::any::Any;
|
||||
|
||||
/// Syncing strategy for syncing engine to use
|
||||
pub trait SyncingStrategy<B: BlockT>: Send
|
||||
where
|
||||
B: BlockT,
|
||||
{
|
||||
/// Notify syncing state machine that a new sync peer has connected.
|
||||
fn add_peer(&mut self, peer_id: PeerId, best_hash: B::Hash, best_number: NumberFor<B>);
|
||||
|
||||
/// Notify that a sync peer has disconnected.
|
||||
fn remove_peer(&mut self, peer_id: &PeerId);
|
||||
|
||||
/// Submit a validated block announcement.
|
||||
///
|
||||
/// Returns new best hash & best number of the peer if they are updated.
|
||||
#[must_use]
|
||||
fn on_validated_block_announce(
|
||||
&mut self,
|
||||
is_best: bool,
|
||||
peer_id: PeerId,
|
||||
announce: &BlockAnnounce<B::Header>,
|
||||
) -> Option<(B::Hash, NumberFor<B>)>;
|
||||
|
||||
/// Configure an explicit fork sync request in case external code has detected that there is a
|
||||
/// stale fork missing.
|
||||
///
|
||||
/// Note that this function should not be used for recent blocks.
|
||||
/// Sync should be able to download all the recent forks normally.
|
||||
///
|
||||
/// Passing empty `peers` set effectively removes the sync request.
|
||||
fn set_sync_fork_request(&mut self, peers: Vec<PeerId>, hash: &B::Hash, number: NumberFor<B>);
|
||||
|
||||
/// Request extra justification.
|
||||
fn request_justification(&mut self, hash: &B::Hash, number: NumberFor<B>);
|
||||
|
||||
/// Clear extra justification requests.
|
||||
fn clear_justification_requests(&mut self);
|
||||
|
||||
/// Report a justification import (successful or not).
|
||||
fn on_justification_import(&mut self, hash: B::Hash, number: NumberFor<B>, success: bool);
|
||||
|
||||
/// Process generic response.
|
||||
///
|
||||
/// Strategy has to create opaque response and should be to downcast it back into concrete type
|
||||
/// internally. Failure to downcast is an implementation bug.
|
||||
fn on_generic_response(
|
||||
&mut self,
|
||||
peer_id: &PeerId,
|
||||
key: StrategyKey,
|
||||
protocol_name: ProtocolName,
|
||||
response: Box<dyn Any + Send>,
|
||||
);
|
||||
|
||||
/// A batch of blocks that have been processed, with or without errors.
|
||||
///
|
||||
/// Call this when a batch of blocks that have been processed by the import queue, with or
|
||||
/// without errors.
|
||||
fn on_blocks_processed(
|
||||
&mut self,
|
||||
imported: usize,
|
||||
count: usize,
|
||||
results: Vec<(Result<BlockImportStatus<NumberFor<B>>, BlockImportError>, B::Hash)>,
|
||||
);
|
||||
|
||||
/// Notify a syncing strategy that a block has been finalized.
|
||||
fn on_block_finalized(&mut self, hash: &B::Hash, number: NumberFor<B>);
|
||||
|
||||
/// Inform sync about a new best imported block.
|
||||
fn update_chain_info(&mut self, best_hash: &B::Hash, best_number: NumberFor<B>);
|
||||
|
||||
// Are we in major sync mode?
|
||||
fn is_major_syncing(&self) -> bool;
|
||||
|
||||
/// Get the number of peers known to the syncing strategy.
|
||||
fn num_peers(&self) -> usize;
|
||||
|
||||
/// Returns the current sync status.
|
||||
fn status(&self) -> SyncStatus<B>;
|
||||
|
||||
/// Get the total number of downloaded blocks.
|
||||
fn num_downloaded_blocks(&self) -> usize;
|
||||
|
||||
/// Get an estimate of the number of parallel sync requests.
|
||||
fn num_sync_requests(&self) -> usize;
|
||||
|
||||
/// Get actions that should be performed by the owner on the strategy's behalf
|
||||
#[must_use]
|
||||
fn actions(
|
||||
&mut self,
|
||||
// TODO: Consider making this internal property of the strategy
|
||||
network_service: &NetworkServiceHandle,
|
||||
) -> Result<Vec<SyncingAction<B>>, ClientError>;
|
||||
}
|
||||
|
||||
/// The key identifying a specific strategy for responses routing.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct StrategyKey(&'static str);
|
||||
|
||||
impl StrategyKey {
|
||||
/// Instantiate opaque strategy key.
|
||||
pub const fn new(key: &'static str) -> Self {
|
||||
Self(key)
|
||||
}
|
||||
}
|
||||
|
||||
pub enum SyncingAction<B: BlockT> {
|
||||
/// Start request to peer.
|
||||
StartRequest {
|
||||
peer_id: PeerId,
|
||||
key: StrategyKey,
|
||||
request: ResponseFuture,
|
||||
// Whether to remove obsolete pending responses.
|
||||
remove_obsolete: bool,
|
||||
},
|
||||
/// Drop stale request.
|
||||
CancelRequest { peer_id: PeerId, key: StrategyKey },
|
||||
/// Peer misbehaved. Disconnect, report it and cancel any requests to it.
|
||||
DropPeer(BadPeer),
|
||||
/// Import blocks.
|
||||
ImportBlocks { origin: BlockOrigin, blocks: Vec<IncomingBlock<B>> },
|
||||
/// Import justifications.
|
||||
ImportJustifications {
|
||||
peer_id: PeerId,
|
||||
hash: B::Hash,
|
||||
number: NumberFor<B>,
|
||||
justifications: Justifications,
|
||||
},
|
||||
/// Strategy finished. Nothing to do, this is handled by `PezkuwiSyncingStrategy`.
|
||||
Finished,
|
||||
}
|
||||
|
||||
// Note: Ideally we can deduce this information with #[derive(derive_more::Debug)].
|
||||
// However, we'd need a bump to the latest version 2 of the crate.
|
||||
impl<B> std::fmt::Debug for SyncingAction<B>
|
||||
where
|
||||
B: BlockT,
|
||||
{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match &self {
|
||||
Self::StartRequest { peer_id, key, remove_obsolete, .. } => {
|
||||
write!(
|
||||
f,
|
||||
"StartRequest {{ peer_id: {:?}, key: {:?}, remove_obsolete: {:?} }}",
|
||||
peer_id, key, remove_obsolete
|
||||
)
|
||||
},
|
||||
Self::CancelRequest { peer_id, key } => {
|
||||
write!(f, "CancelRequest {{ peer_id: {:?}, key: {:?} }}", peer_id, key)
|
||||
},
|
||||
Self::DropPeer(peer) => write!(f, "DropPeer({:?})", peer),
|
||||
Self::ImportBlocks { blocks, .. } => write!(f, "ImportBlocks({:?})", blocks),
|
||||
Self::ImportJustifications { hash, number, .. } => {
|
||||
write!(f, "ImportJustifications({:?}, {:?})", hash, number)
|
||||
},
|
||||
Self::Finished => write!(f, "Finished"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: BlockT> SyncingAction<B> {
|
||||
/// Returns `true` if the syncing action has completed.
|
||||
pub fn is_finished(&self) -> bool {
|
||||
matches!(self, SyncingAction::Finished)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::StartRequest { .. } => "StartRequest",
|
||||
Self::CancelRequest { .. } => "CancelRequest",
|
||||
Self::DropPeer(_) => "DropPeer",
|
||||
Self::ImportBlocks { .. } => "ImportBlocks",
|
||||
Self::ImportJustifications { .. } => "ImportJustifications",
|
||||
Self::Finished => "Finished",
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,196 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::types::BadPeer;
|
||||
use pezsc_network::ReputationChange as Rep;
|
||||
use pezsc_network_types::PeerId;
|
||||
use schnellru::{ByLength, LruMap};
|
||||
|
||||
const LOG_TARGET: &str = "sync::disconnected_peers";
|
||||
|
||||
/// The maximum number of disconnected peers to keep track of.
|
||||
///
|
||||
/// When a peer disconnects, we must keep track if it was in the middle of a request.
|
||||
/// The peer may disconnect because it cannot keep up with the number of requests
|
||||
/// (ie not having enough resources available to handle the requests); or because it is malicious.
|
||||
const MAX_DISCONNECTED_PEERS_STATE: u32 = 512;
|
||||
|
||||
/// The time we are going to backoff a peer that has disconnected with an inflight request.
|
||||
///
|
||||
/// The backoff time is calculated as `num_disconnects * DISCONNECTED_PEER_BACKOFF_SECONDS`.
|
||||
/// This is to prevent submitting a request to a peer that has disconnected because it could not
|
||||
/// keep up with the number of requests.
|
||||
///
|
||||
/// The peer may disconnect due to the keep-alive timeout, however disconnections without
|
||||
/// an inflight request are not tracked.
|
||||
const DISCONNECTED_PEER_BACKOFF_SECONDS: u64 = 60;
|
||||
|
||||
/// Maximum number of disconnects with a request in flight before a peer is banned.
|
||||
const MAX_NUM_DISCONNECTS: u64 = 3;
|
||||
|
||||
/// Peer disconnected with a request in flight after backoffs.
|
||||
///
|
||||
/// The peer may be slow to respond to the request after backoffs, or it refuses to respond.
|
||||
/// Report the peer and let the reputation system handle disconnecting the peer.
|
||||
pub const REPUTATION_REPORT: Rep = Rep::new_fatal("Peer disconnected with inflight after backoffs");
|
||||
|
||||
/// The state of a disconnected peer with a request in flight.
|
||||
#[derive(Debug)]
|
||||
struct DisconnectedState {
|
||||
/// The total number of disconnects.
|
||||
num_disconnects: u64,
|
||||
/// The time at the last disconnect.
|
||||
last_disconnect: std::time::Instant,
|
||||
}
|
||||
|
||||
impl DisconnectedState {
|
||||
/// Create a new `DisconnectedState`.
|
||||
pub fn new() -> Self {
|
||||
Self { num_disconnects: 1, last_disconnect: std::time::Instant::now() }
|
||||
}
|
||||
|
||||
/// Increment the number of disconnects.
|
||||
pub fn increment(&mut self) {
|
||||
self.num_disconnects = self.num_disconnects.saturating_add(1);
|
||||
self.last_disconnect = std::time::Instant::now();
|
||||
}
|
||||
|
||||
/// Get the number of disconnects.
|
||||
pub fn num_disconnects(&self) -> u64 {
|
||||
self.num_disconnects
|
||||
}
|
||||
|
||||
/// Get the time of the last disconnect.
|
||||
pub fn last_disconnect(&self) -> std::time::Instant {
|
||||
self.last_disconnect
|
||||
}
|
||||
}
|
||||
|
||||
/// Tracks the state of disconnected peers with a request in flight.
|
||||
///
|
||||
/// This helps to prevent submitting requests to peers that have disconnected
|
||||
/// before responding to the request to offload the peer.
|
||||
pub struct DisconnectedPeers {
|
||||
/// The state of disconnected peers.
|
||||
disconnected_peers: LruMap<PeerId, DisconnectedState>,
|
||||
/// Backoff duration in seconds.
|
||||
backoff_seconds: u64,
|
||||
}
|
||||
|
||||
impl DisconnectedPeers {
|
||||
/// Create a new `DisconnectedPeers`.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
disconnected_peers: LruMap::new(ByLength::new(MAX_DISCONNECTED_PEERS_STATE)),
|
||||
backoff_seconds: DISCONNECTED_PEER_BACKOFF_SECONDS,
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert a new peer to the persistent state if not seen before, or update the state if seen.
|
||||
///
|
||||
/// Returns true if the peer should be disconnected.
|
||||
pub fn on_disconnect_during_request(&mut self, peer: PeerId) -> Option<BadPeer> {
|
||||
if let Some(state) = self.disconnected_peers.get(&peer) {
|
||||
state.increment();
|
||||
|
||||
let should_ban = state.num_disconnects() >= MAX_NUM_DISCONNECTS;
|
||||
log::debug!(
|
||||
target: LOG_TARGET,
|
||||
"Disconnected known peer {peer} state: {state:?}, should ban: {should_ban}",
|
||||
);
|
||||
|
||||
should_ban.then(|| {
|
||||
// We can lose track of the peer state and let the banning mechanism handle
|
||||
// the peer backoff.
|
||||
//
|
||||
// After the peer banning expires, if the peer continues to misbehave, it will be
|
||||
// backed off again.
|
||||
self.disconnected_peers.remove(&peer);
|
||||
BadPeer(peer, REPUTATION_REPORT)
|
||||
})
|
||||
} else {
|
||||
log::debug!(
|
||||
target: LOG_TARGET,
|
||||
"Added peer {peer} for the first time"
|
||||
);
|
||||
// First time we see this peer.
|
||||
self.disconnected_peers.insert(peer, DisconnectedState::new());
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a peer is available for queries.
|
||||
pub fn is_peer_available(&mut self, peer_id: &PeerId) -> bool {
|
||||
let Some(state) = self.disconnected_peers.get(peer_id) else {
|
||||
return true;
|
||||
};
|
||||
|
||||
let elapsed = state.last_disconnect().elapsed();
|
||||
if elapsed.as_secs() >= self.backoff_seconds * state.num_disconnects {
|
||||
log::debug!(target: LOG_TARGET, "Peer {peer_id} is available for queries");
|
||||
self.disconnected_peers.remove(peer_id);
|
||||
true
|
||||
} else {
|
||||
log::debug!(target: LOG_TARGET,"Peer {peer_id} is backedoff");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn test_disconnected_peer_state() {
|
||||
let mut state = DisconnectedPeers::new();
|
||||
let peer = PeerId::random();
|
||||
|
||||
// Is not part of the disconnected peers yet.
|
||||
assert_eq!(state.is_peer_available(&peer), true);
|
||||
|
||||
for _ in 0..MAX_NUM_DISCONNECTS - 1 {
|
||||
assert!(state.on_disconnect_during_request(peer).is_none());
|
||||
assert_eq!(state.is_peer_available(&peer), false);
|
||||
}
|
||||
|
||||
assert!(state.on_disconnect_during_request(peer).is_some());
|
||||
// Peer is supposed to get banned and disconnected.
|
||||
// The state ownership moves to the PeerStore.
|
||||
assert!(state.disconnected_peers.get(&peer).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_backoff_time() {
|
||||
const TEST_BACKOFF_SECONDS: u64 = 2;
|
||||
let mut state = DisconnectedPeers {
|
||||
disconnected_peers: LruMap::new(ByLength::new(1)),
|
||||
backoff_seconds: TEST_BACKOFF_SECONDS,
|
||||
};
|
||||
let peer = PeerId::random();
|
||||
|
||||
assert!(state.on_disconnect_during_request(peer).is_none());
|
||||
assert_eq!(state.is_peer_available(&peer), false);
|
||||
|
||||
// Wait until the backoff time has passed
|
||||
std::thread::sleep(Duration::from_secs(TEST_BACKOFF_SECONDS + 1));
|
||||
|
||||
assert_eq!(state.is_peer_available(&peer), true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,484 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! [`PezkuwiSyncingStrategy`] is a proxy between [`crate::engine::SyncingEngine`]
|
||||
//! and specific syncing algorithms.
|
||||
|
||||
use crate::{
|
||||
block_relay_protocol::BlockDownloader,
|
||||
block_request_handler::MAX_BLOCKS_IN_RESPONSE,
|
||||
service::network::NetworkServiceHandle,
|
||||
strategy::{
|
||||
chain_sync::{ChainSync, ChainSyncMode},
|
||||
state::StateStrategy,
|
||||
warp::{WarpSync, WarpSyncConfig},
|
||||
StrategyKey, SyncingAction, SyncingStrategy,
|
||||
},
|
||||
types::SyncStatus,
|
||||
LOG_TARGET,
|
||||
};
|
||||
use log::{debug, error, info, warn};
|
||||
use prometheus_endpoint::Registry;
|
||||
use pezsc_client_api::{BlockBackend, ProofProvider};
|
||||
use pezsc_consensus::{BlockImportError, BlockImportStatus};
|
||||
use pezsc_network::ProtocolName;
|
||||
use pezsc_network_common::sync::{message::BlockAnnounce, SyncMode};
|
||||
use pezsc_network_types::PeerId;
|
||||
use pezsp_blockchain::{Error as ClientError, HeaderBackend, HeaderMetadata};
|
||||
use pezsp_runtime::traits::{Block as BlockT, Header, NumberFor};
|
||||
use std::{any::Any, collections::HashMap, sync::Arc};
|
||||
|
||||
/// Corresponding `ChainSync` mode.
|
||||
fn chain_sync_mode(sync_mode: SyncMode) -> ChainSyncMode {
|
||||
match sync_mode {
|
||||
SyncMode::Full => ChainSyncMode::Full,
|
||||
SyncMode::LightState { skip_proofs, storage_chain_mode } =>
|
||||
ChainSyncMode::LightState { skip_proofs, storage_chain_mode },
|
||||
SyncMode::Warp => ChainSyncMode::Full,
|
||||
}
|
||||
}
|
||||
|
||||
/// Syncing configuration containing data for [`PezkuwiSyncingStrategy`].
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PezkuwiSyncingStrategyConfig<Block>
|
||||
where
|
||||
Block: BlockT,
|
||||
{
|
||||
/// Syncing mode.
|
||||
pub mode: SyncMode,
|
||||
/// The number of parallel downloads to guard against slow peers.
|
||||
pub max_parallel_downloads: u32,
|
||||
/// Maximum number of blocks to request.
|
||||
pub max_blocks_per_request: u32,
|
||||
/// Number of peers that need to be connected before warp sync is started.
|
||||
pub min_peers_to_start_warp_sync: Option<usize>,
|
||||
/// Prometheus metrics registry.
|
||||
pub metrics_registry: Option<Registry>,
|
||||
/// Protocol name used to send out state requests
|
||||
pub state_request_protocol_name: ProtocolName,
|
||||
/// Block downloader
|
||||
pub block_downloader: Arc<dyn BlockDownloader<Block>>,
|
||||
}
|
||||
|
||||
/// Proxy to specific syncing strategies used in Pezkuwi.
|
||||
pub struct PezkuwiSyncingStrategy<B: BlockT, Client> {
|
||||
/// Initial syncing configuration.
|
||||
config: PezkuwiSyncingStrategyConfig<B>,
|
||||
/// Client used by syncing strategies.
|
||||
client: Arc<Client>,
|
||||
/// Warp strategy.
|
||||
warp: Option<WarpSync<B, Client>>,
|
||||
/// State strategy.
|
||||
state: Option<StateStrategy<B>>,
|
||||
/// `ChainSync` strategy.`
|
||||
chain_sync: Option<ChainSync<B, Client>>,
|
||||
/// Connected peers and their best blocks used to seed a new strategy when switching to it in
|
||||
/// `PezkuwiSyncingStrategy::proceed_to_next`.
|
||||
peer_best_blocks: HashMap<PeerId, (B::Hash, NumberFor<B>)>,
|
||||
}
|
||||
|
||||
impl<B: BlockT, Client> SyncingStrategy<B> for PezkuwiSyncingStrategy<B, Client>
|
||||
where
|
||||
B: BlockT,
|
||||
Client: HeaderBackend<B>
|
||||
+ BlockBackend<B>
|
||||
+ HeaderMetadata<B, Error = pezsp_blockchain::Error>
|
||||
+ ProofProvider<B>
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
{
|
||||
fn add_peer(&mut self, peer_id: PeerId, best_hash: B::Hash, best_number: NumberFor<B>) {
|
||||
self.peer_best_blocks.insert(peer_id, (best_hash, best_number));
|
||||
|
||||
self.warp.as_mut().map(|s| s.add_peer(peer_id, best_hash, best_number));
|
||||
self.state.as_mut().map(|s| s.add_peer(peer_id, best_hash, best_number));
|
||||
self.chain_sync.as_mut().map(|s| s.add_peer(peer_id, best_hash, best_number));
|
||||
}
|
||||
|
||||
fn remove_peer(&mut self, peer_id: &PeerId) {
|
||||
self.warp.as_mut().map(|s| s.remove_peer(peer_id));
|
||||
self.state.as_mut().map(|s| s.remove_peer(peer_id));
|
||||
self.chain_sync.as_mut().map(|s| s.remove_peer(peer_id));
|
||||
|
||||
self.peer_best_blocks.remove(peer_id);
|
||||
}
|
||||
|
||||
fn on_validated_block_announce(
|
||||
&mut self,
|
||||
is_best: bool,
|
||||
peer_id: PeerId,
|
||||
announce: &BlockAnnounce<B::Header>,
|
||||
) -> Option<(B::Hash, NumberFor<B>)> {
|
||||
let new_best = if let Some(ref mut warp) = self.warp {
|
||||
warp.on_validated_block_announce(is_best, peer_id, announce)
|
||||
} else if let Some(ref mut state) = self.state {
|
||||
state.on_validated_block_announce(is_best, peer_id, announce)
|
||||
} else if let Some(ref mut chain_sync) = self.chain_sync {
|
||||
chain_sync.on_validated_block_announce(is_best, peer_id, announce)
|
||||
} else {
|
||||
error!(target: LOG_TARGET, "No syncing strategy is active.");
|
||||
debug_assert!(false);
|
||||
Some((announce.header.hash(), *announce.header.number()))
|
||||
};
|
||||
|
||||
if let Some(new_best) = new_best {
|
||||
if let Some(best) = self.peer_best_blocks.get_mut(&peer_id) {
|
||||
*best = new_best;
|
||||
} else {
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"Cannot update `peer_best_blocks` as peer {peer_id} is not known to `Strategy` \
|
||||
(already disconnected?)",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
new_best
|
||||
}
|
||||
|
||||
fn set_sync_fork_request(&mut self, peers: Vec<PeerId>, hash: &B::Hash, number: NumberFor<B>) {
|
||||
// Fork requests are only handled by `ChainSync`.
|
||||
if let Some(ref mut chain_sync) = self.chain_sync {
|
||||
chain_sync.set_sync_fork_request(peers.clone(), hash, number);
|
||||
}
|
||||
}
|
||||
|
||||
fn request_justification(&mut self, hash: &B::Hash, number: NumberFor<B>) {
|
||||
// Justifications can only be requested via `ChainSync`.
|
||||
if let Some(ref mut chain_sync) = self.chain_sync {
|
||||
chain_sync.request_justification(hash, number);
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_justification_requests(&mut self) {
|
||||
// Justification requests can only be cleared by `ChainSync`.
|
||||
if let Some(ref mut chain_sync) = self.chain_sync {
|
||||
chain_sync.clear_justification_requests();
|
||||
}
|
||||
}
|
||||
|
||||
fn on_justification_import(&mut self, hash: B::Hash, number: NumberFor<B>, success: bool) {
|
||||
// Only `ChainSync` is interested in justification import.
|
||||
if let Some(ref mut chain_sync) = self.chain_sync {
|
||||
chain_sync.on_justification_import(hash, number, success);
|
||||
}
|
||||
}
|
||||
|
||||
fn on_generic_response(
|
||||
&mut self,
|
||||
peer_id: &PeerId,
|
||||
key: StrategyKey,
|
||||
protocol_name: ProtocolName,
|
||||
response: Box<dyn Any + Send>,
|
||||
) {
|
||||
match key {
|
||||
StateStrategy::<B>::STRATEGY_KEY =>
|
||||
if let Some(state) = &mut self.state {
|
||||
let Ok(response) = response.downcast::<Vec<u8>>() else {
|
||||
warn!(target: LOG_TARGET, "Failed to downcast state response");
|
||||
debug_assert!(false);
|
||||
return;
|
||||
};
|
||||
|
||||
state.on_state_response(peer_id, *response);
|
||||
} else if let Some(chain_sync) = &mut self.chain_sync {
|
||||
chain_sync.on_generic_response(peer_id, key, protocol_name, response);
|
||||
} else {
|
||||
error!(
|
||||
target: LOG_TARGET,
|
||||
"`on_generic_response()` called with unexpected key {key:?} \
|
||||
or corresponding strategy is not active.",
|
||||
);
|
||||
debug_assert!(false);
|
||||
},
|
||||
WarpSync::<B, Client>::STRATEGY_KEY =>
|
||||
if let Some(warp) = &mut self.warp {
|
||||
warp.on_generic_response(peer_id, protocol_name, response);
|
||||
} else {
|
||||
error!(
|
||||
target: LOG_TARGET,
|
||||
"`on_generic_response()` called with unexpected key {key:?} \
|
||||
or warp strategy is not active",
|
||||
);
|
||||
debug_assert!(false);
|
||||
},
|
||||
ChainSync::<B, Client>::STRATEGY_KEY =>
|
||||
if let Some(chain_sync) = &mut self.chain_sync {
|
||||
chain_sync.on_generic_response(peer_id, key, protocol_name, response);
|
||||
} else {
|
||||
error!(
|
||||
target: LOG_TARGET,
|
||||
"`on_generic_response()` called with unexpected key {key:?} \
|
||||
or corresponding strategy is not active.",
|
||||
);
|
||||
debug_assert!(false);
|
||||
},
|
||||
key => {
|
||||
warn!(
|
||||
target: LOG_TARGET,
|
||||
"Unexpected generic response strategy key {key:?}, protocol {protocol_name}",
|
||||
);
|
||||
debug_assert!(false);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn on_blocks_processed(
|
||||
&mut self,
|
||||
imported: usize,
|
||||
count: usize,
|
||||
results: Vec<(Result<BlockImportStatus<NumberFor<B>>, BlockImportError>, B::Hash)>,
|
||||
) {
|
||||
// Only `StateStrategy` and `ChainSync` are interested in block processing notifications.
|
||||
if let Some(ref mut state) = self.state {
|
||||
state.on_blocks_processed(imported, count, results);
|
||||
} else if let Some(ref mut chain_sync) = self.chain_sync {
|
||||
chain_sync.on_blocks_processed(imported, count, results);
|
||||
}
|
||||
}
|
||||
|
||||
fn on_block_finalized(&mut self, hash: &B::Hash, number: NumberFor<B>) {
|
||||
// Only `ChainSync` is interested in block finalization notifications.
|
||||
if let Some(ref mut chain_sync) = self.chain_sync {
|
||||
chain_sync.on_block_finalized(hash, number);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_chain_info(&mut self, best_hash: &B::Hash, best_number: NumberFor<B>) {
|
||||
// This is relevant to `ChainSync` only.
|
||||
if let Some(ref mut chain_sync) = self.chain_sync {
|
||||
chain_sync.update_chain_info(best_hash, best_number);
|
||||
}
|
||||
}
|
||||
|
||||
fn is_major_syncing(&self) -> bool {
|
||||
self.warp.is_some() ||
|
||||
self.state.is_some() ||
|
||||
match self.chain_sync {
|
||||
Some(ref s) => s.status().state.is_major_syncing(),
|
||||
None => unreachable!("At least one syncing strategy is active; qed"),
|
||||
}
|
||||
}
|
||||
|
||||
fn num_peers(&self) -> usize {
|
||||
self.peer_best_blocks.len()
|
||||
}
|
||||
|
||||
fn status(&self) -> SyncStatus<B> {
|
||||
// This function presumes that strategies are executed serially and must be refactored
|
||||
// once we have parallel strategies.
|
||||
if let Some(ref warp) = self.warp {
|
||||
warp.status()
|
||||
} else if let Some(ref state) = self.state {
|
||||
state.status()
|
||||
} else if let Some(ref chain_sync) = self.chain_sync {
|
||||
chain_sync.status()
|
||||
} else {
|
||||
unreachable!("At least one syncing strategy is always active; qed")
|
||||
}
|
||||
}
|
||||
|
||||
fn num_downloaded_blocks(&self) -> usize {
|
||||
self.chain_sync
|
||||
.as_ref()
|
||||
.map_or(0, |chain_sync| chain_sync.num_downloaded_blocks())
|
||||
}
|
||||
|
||||
fn num_sync_requests(&self) -> usize {
|
||||
self.chain_sync.as_ref().map_or(0, |chain_sync| chain_sync.num_sync_requests())
|
||||
}
|
||||
|
||||
fn actions(
|
||||
&mut self,
|
||||
network_service: &NetworkServiceHandle,
|
||||
) -> Result<Vec<SyncingAction<B>>, ClientError> {
|
||||
// This function presumes that strategies are executed serially and must be refactored once
|
||||
// we have parallel strategies.
|
||||
let actions: Vec<_> = if let Some(ref mut warp) = self.warp {
|
||||
warp.actions(network_service).map(Into::into).collect()
|
||||
} else if let Some(ref mut state) = self.state {
|
||||
state.actions(network_service).map(Into::into).collect()
|
||||
} else if let Some(ref mut chain_sync) = self.chain_sync {
|
||||
chain_sync.actions(network_service)?
|
||||
} else {
|
||||
unreachable!("At least one syncing strategy is always active; qed")
|
||||
};
|
||||
|
||||
if actions.iter().any(SyncingAction::is_finished) {
|
||||
self.proceed_to_next()?;
|
||||
}
|
||||
|
||||
Ok(actions)
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: BlockT, Client> PezkuwiSyncingStrategy<B, Client>
|
||||
where
|
||||
B: BlockT,
|
||||
Client: HeaderBackend<B>
|
||||
+ BlockBackend<B>
|
||||
+ HeaderMetadata<B, Error = pezsp_blockchain::Error>
|
||||
+ ProofProvider<B>
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
{
|
||||
/// Initialize a new syncing strategy.
|
||||
pub fn new(
|
||||
mut config: PezkuwiSyncingStrategyConfig<B>,
|
||||
client: Arc<Client>,
|
||||
warp_sync_config: Option<WarpSyncConfig<B>>,
|
||||
warp_sync_protocol_name: Option<ProtocolName>,
|
||||
) -> Result<Self, ClientError> {
|
||||
if config.max_blocks_per_request > MAX_BLOCKS_IN_RESPONSE as u32 {
|
||||
info!(
|
||||
target: LOG_TARGET,
|
||||
"clamping maximum blocks per request to {MAX_BLOCKS_IN_RESPONSE}",
|
||||
);
|
||||
config.max_blocks_per_request = MAX_BLOCKS_IN_RESPONSE as u32;
|
||||
}
|
||||
|
||||
if let SyncMode::Warp = config.mode {
|
||||
let warp_sync_config = warp_sync_config
|
||||
.expect("Warp sync configuration must be supplied in warp sync mode.");
|
||||
let warp_sync = WarpSync::new(
|
||||
client.clone(),
|
||||
warp_sync_config,
|
||||
warp_sync_protocol_name,
|
||||
config.block_downloader.clone(),
|
||||
config.min_peers_to_start_warp_sync,
|
||||
);
|
||||
Ok(Self {
|
||||
config,
|
||||
client,
|
||||
warp: Some(warp_sync),
|
||||
state: None,
|
||||
chain_sync: None,
|
||||
peer_best_blocks: Default::default(),
|
||||
})
|
||||
} else {
|
||||
let chain_sync = ChainSync::new(
|
||||
chain_sync_mode(config.mode),
|
||||
client.clone(),
|
||||
config.max_parallel_downloads,
|
||||
config.max_blocks_per_request,
|
||||
config.state_request_protocol_name.clone(),
|
||||
config.block_downloader.clone(),
|
||||
config.metrics_registry.as_ref(),
|
||||
std::iter::empty(),
|
||||
)?;
|
||||
Ok(Self {
|
||||
config,
|
||||
client,
|
||||
warp: None,
|
||||
state: None,
|
||||
chain_sync: Some(chain_sync),
|
||||
peer_best_blocks: Default::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Proceed with the next strategy if the active one finished.
|
||||
pub fn proceed_to_next(&mut self) -> Result<(), ClientError> {
|
||||
// The strategies are switched as `WarpSync` -> `StateStrategy` -> `ChainSync`.
|
||||
if let Some(ref mut warp) = self.warp {
|
||||
match warp.take_result() {
|
||||
Some(res) => {
|
||||
info!(
|
||||
target: LOG_TARGET,
|
||||
"Warp sync is complete, continuing with state sync."
|
||||
);
|
||||
let state_sync = StateStrategy::new(
|
||||
self.client.clone(),
|
||||
res.target_header,
|
||||
res.target_body,
|
||||
res.target_justifications,
|
||||
false,
|
||||
self.peer_best_blocks
|
||||
.iter()
|
||||
.map(|(peer_id, (_, best_number))| (*peer_id, *best_number)),
|
||||
self.config.state_request_protocol_name.clone(),
|
||||
);
|
||||
|
||||
self.warp = None;
|
||||
self.state = Some(state_sync);
|
||||
Ok(())
|
||||
},
|
||||
None => {
|
||||
error!(
|
||||
target: LOG_TARGET,
|
||||
"Warp sync failed. Continuing with full sync."
|
||||
);
|
||||
let chain_sync = match ChainSync::new(
|
||||
chain_sync_mode(self.config.mode),
|
||||
self.client.clone(),
|
||||
self.config.max_parallel_downloads,
|
||||
self.config.max_blocks_per_request,
|
||||
self.config.state_request_protocol_name.clone(),
|
||||
self.config.block_downloader.clone(),
|
||||
self.config.metrics_registry.as_ref(),
|
||||
self.peer_best_blocks.iter().map(|(peer_id, (best_hash, best_number))| {
|
||||
(*peer_id, *best_hash, *best_number)
|
||||
}),
|
||||
) {
|
||||
Ok(chain_sync) => chain_sync,
|
||||
Err(e) => {
|
||||
error!(target: LOG_TARGET, "Failed to start `ChainSync`.");
|
||||
return Err(e);
|
||||
},
|
||||
};
|
||||
|
||||
self.warp = None;
|
||||
self.chain_sync = Some(chain_sync);
|
||||
Ok(())
|
||||
},
|
||||
}
|
||||
} else if let Some(state) = &self.state {
|
||||
if state.is_succeeded() {
|
||||
info!(target: LOG_TARGET, "State sync is complete, continuing with block sync.");
|
||||
} else {
|
||||
error!(target: LOG_TARGET, "State sync failed. Falling back to full sync.");
|
||||
}
|
||||
let chain_sync = match ChainSync::new(
|
||||
chain_sync_mode(self.config.mode),
|
||||
self.client.clone(),
|
||||
self.config.max_parallel_downloads,
|
||||
self.config.max_blocks_per_request,
|
||||
self.config.state_request_protocol_name.clone(),
|
||||
self.config.block_downloader.clone(),
|
||||
self.config.metrics_registry.as_ref(),
|
||||
self.peer_best_blocks.iter().map(|(peer_id, (best_hash, best_number))| {
|
||||
(*peer_id, *best_hash, *best_number)
|
||||
}),
|
||||
) {
|
||||
Ok(chain_sync) => chain_sync,
|
||||
Err(e) => {
|
||||
error!(target: LOG_TARGET, "Failed to start `ChainSync`.");
|
||||
return Err(e);
|
||||
},
|
||||
};
|
||||
|
||||
self.state = None;
|
||||
self.chain_sync = Some(chain_sync);
|
||||
Ok(())
|
||||
} else {
|
||||
unreachable!("Only warp & state strategies can finish; qed")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,884 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! State sync strategy.
|
||||
|
||||
use crate::{
|
||||
schema::v1::{StateRequest, StateResponse},
|
||||
service::network::NetworkServiceHandle,
|
||||
strategy::{
|
||||
disconnected_peers::DisconnectedPeers,
|
||||
state_sync::{ImportResult, StateSync, StateSyncProvider},
|
||||
StrategyKey, SyncingAction,
|
||||
},
|
||||
types::{BadPeer, SyncState, SyncStatus},
|
||||
LOG_TARGET,
|
||||
};
|
||||
use futures::{channel::oneshot, FutureExt};
|
||||
use log::{debug, error, trace};
|
||||
use prost::Message;
|
||||
use pezsc_client_api::ProofProvider;
|
||||
use pezsc_consensus::{BlockImportError, BlockImportStatus, IncomingBlock};
|
||||
use pezsc_network::{IfDisconnected, ProtocolName};
|
||||
use pezsc_network_common::sync::message::BlockAnnounce;
|
||||
use pezsc_network_types::PeerId;
|
||||
use pezsp_consensus::BlockOrigin;
|
||||
use pezsp_runtime::{
|
||||
traits::{Block as BlockT, Header, NumberFor},
|
||||
Justifications, SaturatedConversion,
|
||||
};
|
||||
use std::{any::Any, collections::HashMap, sync::Arc};
|
||||
|
||||
mod rep {
|
||||
use pezsc_network::ReputationChange as Rep;
|
||||
|
||||
/// Peer response data does not have requested bits.
|
||||
pub const BAD_RESPONSE: Rep = Rep::new(-(1 << 12), "Incomplete response");
|
||||
|
||||
/// Reputation change for peers which send us a known bad state.
|
||||
pub const BAD_STATE: Rep = Rep::new(-(1 << 29), "Bad state");
|
||||
}
|
||||
|
||||
enum PeerState {
|
||||
Available,
|
||||
DownloadingState,
|
||||
}
|
||||
|
||||
impl PeerState {
|
||||
fn is_available(&self) -> bool {
|
||||
matches!(self, PeerState::Available)
|
||||
}
|
||||
}
|
||||
|
||||
struct Peer<B: BlockT> {
|
||||
best_number: NumberFor<B>,
|
||||
state: PeerState,
|
||||
}
|
||||
|
||||
/// Syncing strategy that downloads and imports a recent state directly.
|
||||
pub struct StateStrategy<B: BlockT> {
|
||||
state_sync: Box<dyn StateSyncProvider<B>>,
|
||||
peers: HashMap<PeerId, Peer<B>>,
|
||||
disconnected_peers: DisconnectedPeers,
|
||||
actions: Vec<SyncingAction<B>>,
|
||||
protocol_name: ProtocolName,
|
||||
succeeded: bool,
|
||||
}
|
||||
|
||||
impl<B: BlockT> StateStrategy<B> {
|
||||
/// Strategy key used by state sync.
|
||||
pub const STRATEGY_KEY: StrategyKey = StrategyKey::new("State");
|
||||
|
||||
/// Create a new instance.
|
||||
pub fn new<Client>(
|
||||
client: Arc<Client>,
|
||||
target_header: B::Header,
|
||||
target_body: Option<Vec<B::Extrinsic>>,
|
||||
target_justifications: Option<Justifications>,
|
||||
skip_proof: bool,
|
||||
initial_peers: impl Iterator<Item = (PeerId, NumberFor<B>)>,
|
||||
protocol_name: ProtocolName,
|
||||
) -> Self
|
||||
where
|
||||
Client: ProofProvider<B> + Send + Sync + 'static,
|
||||
{
|
||||
let peers = initial_peers
|
||||
.map(|(peer_id, best_number)| {
|
||||
(peer_id, Peer { best_number, state: PeerState::Available })
|
||||
})
|
||||
.collect();
|
||||
Self {
|
||||
state_sync: Box::new(StateSync::new(
|
||||
client,
|
||||
target_header,
|
||||
target_body,
|
||||
target_justifications,
|
||||
skip_proof,
|
||||
)),
|
||||
peers,
|
||||
disconnected_peers: DisconnectedPeers::new(),
|
||||
actions: Vec::new(),
|
||||
protocol_name,
|
||||
succeeded: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new instance with a custom state sync provider.
|
||||
///
|
||||
/// Note: In most cases, users should use [`StateStrategy::new`].
|
||||
/// This method is intended for custom sync strategies and advanced use cases.
|
||||
pub fn new_with_provider(
|
||||
state_sync_provider: Box<dyn StateSyncProvider<B>>,
|
||||
initial_peers: impl Iterator<Item = (PeerId, NumberFor<B>)>,
|
||||
protocol_name: ProtocolName,
|
||||
) -> Self {
|
||||
Self {
|
||||
state_sync: state_sync_provider,
|
||||
peers: initial_peers
|
||||
.map(|(peer_id, best_number)| {
|
||||
(peer_id, Peer { best_number, state: PeerState::Available })
|
||||
})
|
||||
.collect(),
|
||||
disconnected_peers: DisconnectedPeers::new(),
|
||||
actions: Vec::new(),
|
||||
protocol_name,
|
||||
succeeded: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Notify that a new peer has connected.
|
||||
pub fn add_peer(&mut self, peer_id: PeerId, _best_hash: B::Hash, best_number: NumberFor<B>) {
|
||||
self.peers.insert(peer_id, Peer { best_number, state: PeerState::Available });
|
||||
}
|
||||
|
||||
/// Notify that a peer has disconnected.
|
||||
pub fn remove_peer(&mut self, peer_id: &PeerId) {
|
||||
if let Some(state) = self.peers.remove(peer_id) {
|
||||
if !state.state.is_available() {
|
||||
if let Some(bad_peer) =
|
||||
self.disconnected_peers.on_disconnect_during_request(*peer_id)
|
||||
{
|
||||
self.actions.push(SyncingAction::DropPeer(bad_peer));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Submit a validated block announcement.
|
||||
///
|
||||
/// Returns new best hash & best number of the peer if they are updated.
|
||||
#[must_use]
|
||||
pub fn on_validated_block_announce(
|
||||
&mut self,
|
||||
is_best: bool,
|
||||
peer_id: PeerId,
|
||||
announce: &BlockAnnounce<B::Header>,
|
||||
) -> Option<(B::Hash, NumberFor<B>)> {
|
||||
is_best.then(|| {
|
||||
let best_number = *announce.header.number();
|
||||
let best_hash = announce.header.hash();
|
||||
if let Some(ref mut peer) = self.peers.get_mut(&peer_id) {
|
||||
peer.best_number = best_number;
|
||||
}
|
||||
// Let `SyncingEngine` know that we should update the peer info.
|
||||
(best_hash, best_number)
|
||||
})
|
||||
}
|
||||
|
||||
/// Process state response.
|
||||
pub fn on_state_response(&mut self, peer_id: &PeerId, response: Vec<u8>) {
|
||||
if let Err(bad_peer) = self.on_state_response_inner(peer_id, &response) {
|
||||
self.actions.push(SyncingAction::DropPeer(bad_peer));
|
||||
}
|
||||
}
|
||||
|
||||
fn on_state_response_inner(
|
||||
&mut self,
|
||||
peer_id: &PeerId,
|
||||
response: &[u8],
|
||||
) -> Result<(), BadPeer> {
|
||||
if let Some(peer) = self.peers.get_mut(&peer_id) {
|
||||
peer.state = PeerState::Available;
|
||||
}
|
||||
|
||||
let response = match StateResponse::decode(response) {
|
||||
Ok(response) => response,
|
||||
Err(error) => {
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"Failed to decode state response from peer {peer_id:?}: {error:?}.",
|
||||
);
|
||||
|
||||
return Err(BadPeer(*peer_id, rep::BAD_RESPONSE));
|
||||
},
|
||||
};
|
||||
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"Importing state data from {} with {} keys, {} proof nodes.",
|
||||
peer_id,
|
||||
response.entries.len(),
|
||||
response.proof.len(),
|
||||
);
|
||||
|
||||
match self.state_sync.import(response) {
|
||||
ImportResult::Import(hash, header, state, body, justifications) => {
|
||||
let origin = BlockOrigin::NetworkInitialSync;
|
||||
let block = IncomingBlock {
|
||||
hash,
|
||||
header: Some(header),
|
||||
body,
|
||||
indexed_body: None,
|
||||
justifications,
|
||||
origin: None,
|
||||
allow_missing_state: true,
|
||||
import_existing: true,
|
||||
skip_execution: true,
|
||||
state: Some(state),
|
||||
};
|
||||
debug!(target: LOG_TARGET, "State download is complete. Import is queued");
|
||||
self.actions.push(SyncingAction::ImportBlocks { origin, blocks: vec![block] });
|
||||
Ok(())
|
||||
},
|
||||
ImportResult::Continue => Ok(()),
|
||||
ImportResult::BadResponse => {
|
||||
debug!(target: LOG_TARGET, "Bad state data received from {peer_id}");
|
||||
Err(BadPeer(*peer_id, rep::BAD_STATE))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// A batch of blocks have been processed, with or without errors.
|
||||
///
|
||||
/// Normally this should be called when target block with state is imported.
|
||||
pub fn on_blocks_processed(
|
||||
&mut self,
|
||||
imported: usize,
|
||||
count: usize,
|
||||
results: Vec<(Result<BlockImportStatus<NumberFor<B>>, BlockImportError>, B::Hash)>,
|
||||
) {
|
||||
trace!(target: LOG_TARGET, "State sync: imported {imported} of {count}.");
|
||||
|
||||
let results = results
|
||||
.into_iter()
|
||||
.filter_map(|(result, hash)| {
|
||||
if hash == self.state_sync.target_hash() {
|
||||
Some(result)
|
||||
} else {
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"Unexpected block processed: {hash} with result {result:?}.",
|
||||
);
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if !results.is_empty() {
|
||||
// We processed the target block
|
||||
results.iter().filter_map(|result| result.as_ref().err()).for_each(|e| {
|
||||
error!(
|
||||
target: LOG_TARGET,
|
||||
"Failed to import target block with state: {e:?}."
|
||||
);
|
||||
});
|
||||
self.succeeded |= results.into_iter().any(|result| result.is_ok());
|
||||
self.actions.push(SyncingAction::Finished);
|
||||
}
|
||||
}
|
||||
|
||||
/// Produce state request.
|
||||
fn state_request(&mut self) -> Option<(PeerId, StateRequest)> {
|
||||
if self.state_sync.is_complete() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if self
|
||||
.peers
|
||||
.values()
|
||||
.any(|peer| matches!(peer.state, PeerState::DownloadingState))
|
||||
{
|
||||
// Only one state request at a time is possible.
|
||||
return None;
|
||||
}
|
||||
|
||||
let peer_id =
|
||||
self.schedule_next_peer(PeerState::DownloadingState, self.state_sync.target_number())?;
|
||||
let request = self.state_sync.next_request();
|
||||
trace!(
|
||||
target: LOG_TARGET,
|
||||
"New state request to {peer_id}: {request:?}.",
|
||||
);
|
||||
Some((peer_id, request))
|
||||
}
|
||||
|
||||
fn schedule_next_peer(
|
||||
&mut self,
|
||||
new_state: PeerState,
|
||||
min_best_number: NumberFor<B>,
|
||||
) -> Option<PeerId> {
|
||||
let mut targets: Vec<_> = self.peers.values().map(|p| p.best_number).collect();
|
||||
if targets.is_empty() {
|
||||
return None;
|
||||
}
|
||||
targets.sort();
|
||||
let median = targets[targets.len() / 2];
|
||||
let threshold = std::cmp::max(median, min_best_number);
|
||||
// Find a random peer that is synced as much as peer majority and is above
|
||||
// `min_best_number`.
|
||||
for (peer_id, peer) in self.peers.iter_mut() {
|
||||
if peer.state.is_available() &&
|
||||
peer.best_number >= threshold &&
|
||||
self.disconnected_peers.is_peer_available(peer_id)
|
||||
{
|
||||
peer.state = new_state;
|
||||
return Some(*peer_id);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns the current sync status.
|
||||
pub fn status(&self) -> SyncStatus<B> {
|
||||
SyncStatus {
|
||||
state: if self.state_sync.is_complete() {
|
||||
SyncState::Idle
|
||||
} else {
|
||||
SyncState::Downloading { target: self.state_sync.target_number() }
|
||||
},
|
||||
best_seen_block: Some(self.state_sync.target_number()),
|
||||
num_peers: self.peers.len().saturated_into(),
|
||||
queued_blocks: 0,
|
||||
state_sync: Some(self.state_sync.progress()),
|
||||
warp_sync: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get actions that should be performed.
|
||||
#[must_use]
|
||||
pub fn actions(
|
||||
&mut self,
|
||||
network_service: &NetworkServiceHandle,
|
||||
) -> impl Iterator<Item = SyncingAction<B>> {
|
||||
let state_request = self.state_request().into_iter().map(|(peer_id, request)| {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
network_service.start_request(
|
||||
peer_id,
|
||||
self.protocol_name.clone(),
|
||||
request.encode_to_vec(),
|
||||
tx,
|
||||
IfDisconnected::ImmediateError,
|
||||
);
|
||||
|
||||
SyncingAction::StartRequest {
|
||||
peer_id,
|
||||
key: Self::STRATEGY_KEY,
|
||||
request: async move {
|
||||
Ok(rx.await?.and_then(|(response, protocol_name)| {
|
||||
Ok((Box::new(response) as Box<dyn Any + Send>, protocol_name))
|
||||
}))
|
||||
}
|
||||
.boxed(),
|
||||
remove_obsolete: false,
|
||||
}
|
||||
});
|
||||
self.actions.extend(state_request);
|
||||
|
||||
std::mem::take(&mut self.actions).into_iter()
|
||||
}
|
||||
|
||||
/// Check if state sync has succeeded.
|
||||
#[must_use]
|
||||
pub fn is_succeeded(&self) -> bool {
|
||||
self.succeeded
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::{
|
||||
schema::v1::{StateRequest, StateResponse},
|
||||
service::network::NetworkServiceProvider,
|
||||
strategy::state_sync::{ImportResult, StateSyncProgress, StateSyncProvider},
|
||||
};
|
||||
use codec::Decode;
|
||||
use pezsc_block_builder::BlockBuilderBuilder;
|
||||
use pezsc_client_api::KeyValueStates;
|
||||
use pezsc_consensus::{ImportedAux, ImportedState};
|
||||
use pezsp_core::H256;
|
||||
use pezsp_runtime::traits::Zero;
|
||||
use bizinikiwi_test_runtime_client::{
|
||||
runtime::{Block, Hash},
|
||||
BlockBuilderExt, DefaultTestClientBuilderExt, TestClientBuilder, TestClientBuilderExt,
|
||||
};
|
||||
|
||||
mockall::mock! {
|
||||
pub StateSync<B: BlockT> {}
|
||||
|
||||
impl<B: BlockT> StateSyncProvider<B> for StateSync<B> {
|
||||
fn import(&mut self, response: StateResponse) -> ImportResult<B>;
|
||||
fn next_request(&self) -> StateRequest;
|
||||
fn is_complete(&self) -> bool;
|
||||
fn target_number(&self) -> NumberFor<B>;
|
||||
fn target_hash(&self) -> B::Hash;
|
||||
fn progress(&self) -> StateSyncProgress;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_peer_is_scheduled_if_no_peers_connected() {
|
||||
let client = Arc::new(TestClientBuilder::new().set_no_genesis().build());
|
||||
let target_block = BlockBuilderBuilder::new(&*client)
|
||||
.on_parent_block(client.chain_info().best_hash)
|
||||
.with_parent_block_number(client.chain_info().best_number)
|
||||
.build()
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block;
|
||||
let target_header = target_block.header().clone();
|
||||
|
||||
let mut state_strategy = StateStrategy::new(
|
||||
client,
|
||||
target_header,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
std::iter::empty(),
|
||||
ProtocolName::Static(""),
|
||||
);
|
||||
|
||||
assert!(state_strategy
|
||||
.schedule_next_peer(PeerState::DownloadingState, Zero::zero())
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn at_least_median_synced_peer_is_scheduled() {
|
||||
let client = Arc::new(TestClientBuilder::new().set_no_genesis().build());
|
||||
let target_block = BlockBuilderBuilder::new(&*client)
|
||||
.on_parent_block(client.chain_info().best_hash)
|
||||
.with_parent_block_number(client.chain_info().best_number)
|
||||
.build()
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block;
|
||||
|
||||
for _ in 0..100 {
|
||||
let peers = (1..=10)
|
||||
.map(|best_number| (PeerId::random(), best_number))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let initial_peers = peers.iter().map(|(p, n)| (*p, *n));
|
||||
|
||||
let mut state_strategy = StateStrategy::new(
|
||||
client.clone(),
|
||||
target_block.header().clone(),
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
initial_peers,
|
||||
ProtocolName::Static(""),
|
||||
);
|
||||
|
||||
let peer_id =
|
||||
state_strategy.schedule_next_peer(PeerState::DownloadingState, Zero::zero());
|
||||
assert!(*peers.get(&peer_id.unwrap()).unwrap() >= 6);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn min_best_number_peer_is_scheduled() {
|
||||
let client = Arc::new(TestClientBuilder::new().set_no_genesis().build());
|
||||
let target_block = BlockBuilderBuilder::new(&*client)
|
||||
.on_parent_block(client.chain_info().best_hash)
|
||||
.with_parent_block_number(client.chain_info().best_number)
|
||||
.build()
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block;
|
||||
|
||||
for _ in 0..10 {
|
||||
let peers = (1..=10)
|
||||
.map(|best_number| (PeerId::random(), best_number))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let initial_peers = peers.iter().map(|(p, n)| (*p, *n));
|
||||
|
||||
let mut state_strategy = StateStrategy::new(
|
||||
client.clone(),
|
||||
target_block.header().clone(),
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
initial_peers,
|
||||
ProtocolName::Static(""),
|
||||
);
|
||||
|
||||
let peer_id = state_strategy.schedule_next_peer(PeerState::DownloadingState, 10);
|
||||
assert!(*peers.get(&peer_id.unwrap()).unwrap() == 10);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backedoff_number_peer_is_not_scheduled() {
|
||||
let client = Arc::new(TestClientBuilder::new().set_no_genesis().build());
|
||||
let target_block = BlockBuilderBuilder::new(&*client)
|
||||
.on_parent_block(client.chain_info().best_hash)
|
||||
.with_parent_block_number(client.chain_info().best_number)
|
||||
.build()
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block;
|
||||
|
||||
let peers = (1..=10)
|
||||
.map(|best_number| (PeerId::random(), best_number))
|
||||
.collect::<Vec<(_, _)>>();
|
||||
let ninth_peer = peers[8].0;
|
||||
let tenth_peer = peers[9].0;
|
||||
let initial_peers = peers.iter().map(|(p, n)| (*p, *n));
|
||||
|
||||
let mut state_strategy = StateStrategy::new(
|
||||
client.clone(),
|
||||
target_block.header().clone(),
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
initial_peers,
|
||||
ProtocolName::Static(""),
|
||||
);
|
||||
|
||||
// Disconnecting a peer without an inflight request has no effect on persistent states.
|
||||
state_strategy.remove_peer(&tenth_peer);
|
||||
assert!(state_strategy.disconnected_peers.is_peer_available(&tenth_peer));
|
||||
|
||||
// Disconnect the peer with an inflight request.
|
||||
state_strategy.add_peer(tenth_peer, H256::random(), 10);
|
||||
let peer_id: Option<PeerId> =
|
||||
state_strategy.schedule_next_peer(PeerState::DownloadingState, 10);
|
||||
assert_eq!(tenth_peer, peer_id.unwrap());
|
||||
state_strategy.remove_peer(&tenth_peer);
|
||||
|
||||
// Peer is backed off.
|
||||
assert!(!state_strategy.disconnected_peers.is_peer_available(&tenth_peer));
|
||||
|
||||
// No peer available for 10'th best block because of the backoff.
|
||||
state_strategy.add_peer(tenth_peer, H256::random(), 10);
|
||||
let peer_id: Option<PeerId> =
|
||||
state_strategy.schedule_next_peer(PeerState::DownloadingState, 10);
|
||||
assert!(peer_id.is_none());
|
||||
|
||||
// Other requests can still happen.
|
||||
let peer_id: Option<PeerId> =
|
||||
state_strategy.schedule_next_peer(PeerState::DownloadingState, 9);
|
||||
assert_eq!(ninth_peer, peer_id.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_request_contains_correct_hash() {
|
||||
let client = Arc::new(TestClientBuilder::new().set_no_genesis().build());
|
||||
let target_block = BlockBuilderBuilder::new(&*client)
|
||||
.on_parent_block(client.chain_info().best_hash)
|
||||
.with_parent_block_number(client.chain_info().best_number)
|
||||
.build()
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block;
|
||||
|
||||
let initial_peers = (1..=10).map(|best_number| (PeerId::random(), best_number));
|
||||
|
||||
let mut state_strategy = StateStrategy::new(
|
||||
client.clone(),
|
||||
target_block.header().clone(),
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
initial_peers,
|
||||
ProtocolName::Static(""),
|
||||
);
|
||||
|
||||
let (_peer_id, request) = state_strategy.state_request().unwrap();
|
||||
let hash = Hash::decode(&mut &*request.block).unwrap();
|
||||
|
||||
assert_eq!(hash, target_block.header().hash());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_parallel_state_requests() {
|
||||
let client = Arc::new(TestClientBuilder::new().set_no_genesis().build());
|
||||
let target_block = BlockBuilderBuilder::new(&*client)
|
||||
.on_parent_block(client.chain_info().best_hash)
|
||||
.with_parent_block_number(client.chain_info().best_number)
|
||||
.build()
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block;
|
||||
|
||||
let initial_peers = (1..=10).map(|best_number| (PeerId::random(), best_number));
|
||||
|
||||
let mut state_strategy = StateStrategy::new(
|
||||
client.clone(),
|
||||
target_block.header().clone(),
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
initial_peers,
|
||||
ProtocolName::Static(""),
|
||||
);
|
||||
|
||||
// First request is sent.
|
||||
assert!(state_strategy.state_request().is_some());
|
||||
|
||||
// No parallel request is sent.
|
||||
assert!(state_strategy.state_request().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn received_state_response_makes_peer_available_again() {
|
||||
let mut state_sync_provider = MockStateSync::<Block>::new();
|
||||
state_sync_provider.expect_import().return_once(|_| ImportResult::Continue);
|
||||
let peer_id = PeerId::random();
|
||||
let initial_peers = std::iter::once((peer_id, 10));
|
||||
let mut state_strategy = StateStrategy::new_with_provider(
|
||||
Box::new(state_sync_provider),
|
||||
initial_peers,
|
||||
ProtocolName::Static(""),
|
||||
);
|
||||
// Manually set the peer's state.
|
||||
state_strategy.peers.get_mut(&peer_id).unwrap().state = PeerState::DownloadingState;
|
||||
|
||||
let dummy_response = StateResponse::default().encode_to_vec();
|
||||
state_strategy.on_state_response(&peer_id, dummy_response);
|
||||
|
||||
assert!(state_strategy.peers.get(&peer_id).unwrap().state.is_available());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_state_response_drops_peer() {
|
||||
let mut state_sync_provider = MockStateSync::<Block>::new();
|
||||
// Provider says that state response is bad.
|
||||
state_sync_provider.expect_import().return_once(|_| ImportResult::BadResponse);
|
||||
let peer_id = PeerId::random();
|
||||
let initial_peers = std::iter::once((peer_id, 10));
|
||||
let mut state_strategy = StateStrategy::new_with_provider(
|
||||
Box::new(state_sync_provider),
|
||||
initial_peers,
|
||||
ProtocolName::Static(""),
|
||||
);
|
||||
// Manually set the peer's state.
|
||||
state_strategy.peers.get_mut(&peer_id).unwrap().state = PeerState::DownloadingState;
|
||||
let dummy_response = StateResponse::default().encode_to_vec();
|
||||
// Receiving response drops the peer.
|
||||
assert!(matches!(
|
||||
state_strategy.on_state_response_inner(&peer_id, &dummy_response),
|
||||
Err(BadPeer(id, _rep)) if id == peer_id,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_state_response_doesnt_generate_actions() {
|
||||
let mut state_sync_provider = MockStateSync::<Block>::new();
|
||||
// Sync provider says that the response is partial.
|
||||
state_sync_provider.expect_import().return_once(|_| ImportResult::Continue);
|
||||
let peer_id = PeerId::random();
|
||||
let initial_peers = std::iter::once((peer_id, 10));
|
||||
let mut state_strategy = StateStrategy::new_with_provider(
|
||||
Box::new(state_sync_provider),
|
||||
initial_peers,
|
||||
ProtocolName::Static(""),
|
||||
);
|
||||
// Manually set the peer's state .
|
||||
state_strategy.peers.get_mut(&peer_id).unwrap().state = PeerState::DownloadingState;
|
||||
|
||||
let dummy_response = StateResponse::default().encode_to_vec();
|
||||
state_strategy.on_state_response(&peer_id, dummy_response);
|
||||
|
||||
// No actions generated.
|
||||
assert_eq!(state_strategy.actions.len(), 0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_state_response_leads_to_block_import() {
|
||||
// Build block to use for checks.
|
||||
let client = Arc::new(TestClientBuilder::new().set_no_genesis().build());
|
||||
let mut block_builder = BlockBuilderBuilder::new(&*client)
|
||||
.on_parent_block(client.chain_info().best_hash)
|
||||
.with_parent_block_number(client.chain_info().best_number)
|
||||
.build()
|
||||
.unwrap();
|
||||
block_builder.push_storage_change(vec![1, 2, 3], Some(vec![4, 5, 6])).unwrap();
|
||||
let block = block_builder.build().unwrap().block;
|
||||
let header = block.header().clone();
|
||||
let hash = header.hash();
|
||||
let body = Some(block.extrinsics().iter().cloned().collect::<Vec<_>>());
|
||||
let state = ImportedState { block: hash, state: KeyValueStates(Vec::new()) };
|
||||
let justifications = Some(Justifications::from((*b"FRNK", Vec::new())));
|
||||
|
||||
// Prepare `StateSync`
|
||||
let mut state_sync_provider = MockStateSync::<Block>::new();
|
||||
let import = ImportResult::Import(
|
||||
hash,
|
||||
header.clone(),
|
||||
state.clone(),
|
||||
body.clone(),
|
||||
justifications.clone(),
|
||||
);
|
||||
state_sync_provider.expect_import().return_once(move |_| import);
|
||||
|
||||
// Reference values to check against.
|
||||
let expected_origin = BlockOrigin::NetworkInitialSync;
|
||||
let expected_block = IncomingBlock {
|
||||
hash,
|
||||
header: Some(header),
|
||||
body,
|
||||
indexed_body: None,
|
||||
justifications,
|
||||
origin: None,
|
||||
allow_missing_state: true,
|
||||
import_existing: true,
|
||||
skip_execution: true,
|
||||
state: Some(state),
|
||||
};
|
||||
let expected_blocks = vec![expected_block];
|
||||
|
||||
// Prepare `StateStrategy`.
|
||||
let peer_id = PeerId::random();
|
||||
let initial_peers = std::iter::once((peer_id, 10));
|
||||
let mut state_strategy = StateStrategy::new_with_provider(
|
||||
Box::new(state_sync_provider),
|
||||
initial_peers,
|
||||
ProtocolName::Static(""),
|
||||
);
|
||||
// Manually set the peer's state .
|
||||
state_strategy.peers.get_mut(&peer_id).unwrap().state = PeerState::DownloadingState;
|
||||
|
||||
// Receive response.
|
||||
let dummy_response = StateResponse::default().encode_to_vec();
|
||||
state_strategy.on_state_response(&peer_id, dummy_response);
|
||||
|
||||
assert_eq!(state_strategy.actions.len(), 1);
|
||||
assert!(matches!(
|
||||
&state_strategy.actions[0],
|
||||
SyncingAction::ImportBlocks { origin, blocks }
|
||||
if *origin == expected_origin && *blocks == expected_blocks,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn importing_unknown_block_doesnt_finish_strategy() {
|
||||
let target_hash = Hash::random();
|
||||
let unknown_hash = Hash::random();
|
||||
let mut state_sync_provider = MockStateSync::<Block>::new();
|
||||
state_sync_provider.expect_target_hash().return_const(target_hash);
|
||||
|
||||
let mut state_strategy = StateStrategy::new_with_provider(
|
||||
Box::new(state_sync_provider),
|
||||
std::iter::empty(),
|
||||
ProtocolName::Static(""),
|
||||
);
|
||||
|
||||
// Unknown block imported.
|
||||
state_strategy.on_blocks_processed(
|
||||
1,
|
||||
1,
|
||||
vec![(
|
||||
Ok(BlockImportStatus::ImportedUnknown(1, ImportedAux::default(), None)),
|
||||
unknown_hash,
|
||||
)],
|
||||
);
|
||||
|
||||
// No actions generated.
|
||||
assert_eq!(state_strategy.actions.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn successfully_importing_target_block_finishes_strategy() {
|
||||
let target_hash = Hash::random();
|
||||
let mut state_sync_provider = MockStateSync::<Block>::new();
|
||||
state_sync_provider.expect_target_hash().return_const(target_hash);
|
||||
|
||||
let mut state_strategy = StateStrategy::new_with_provider(
|
||||
Box::new(state_sync_provider),
|
||||
std::iter::empty(),
|
||||
ProtocolName::Static(""),
|
||||
);
|
||||
|
||||
// Target block imported.
|
||||
state_strategy.on_blocks_processed(
|
||||
1,
|
||||
1,
|
||||
vec![(
|
||||
Ok(BlockImportStatus::ImportedUnknown(1, ImportedAux::default(), None)),
|
||||
target_hash,
|
||||
)],
|
||||
);
|
||||
|
||||
// Strategy finishes.
|
||||
assert_eq!(state_strategy.actions.len(), 1);
|
||||
assert!(matches!(&state_strategy.actions[0], SyncingAction::Finished));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failure_to_import_target_block_finishes_strategy() {
|
||||
let target_hash = Hash::random();
|
||||
let mut state_sync_provider = MockStateSync::<Block>::new();
|
||||
state_sync_provider.expect_target_hash().return_const(target_hash);
|
||||
|
||||
let mut state_strategy = StateStrategy::new_with_provider(
|
||||
Box::new(state_sync_provider),
|
||||
std::iter::empty(),
|
||||
ProtocolName::Static(""),
|
||||
);
|
||||
|
||||
// Target block import failed.
|
||||
state_strategy.on_blocks_processed(
|
||||
1,
|
||||
1,
|
||||
vec![(
|
||||
Err(BlockImportError::VerificationFailed(None, String::from("test-error"))),
|
||||
target_hash,
|
||||
)],
|
||||
);
|
||||
|
||||
// Strategy finishes.
|
||||
assert_eq!(state_strategy.actions.len(), 1);
|
||||
assert!(matches!(&state_strategy.actions[0], SyncingAction::Finished));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finished_strategy_doesnt_generate_more_actions() {
|
||||
let target_hash = Hash::random();
|
||||
let mut state_sync_provider = MockStateSync::<Block>::new();
|
||||
state_sync_provider.expect_target_hash().return_const(target_hash);
|
||||
state_sync_provider.expect_is_complete().return_const(true);
|
||||
|
||||
// Get enough peers for possible spurious requests.
|
||||
let initial_peers = (1..=10).map(|best_number| (PeerId::random(), best_number));
|
||||
|
||||
let mut state_strategy = StateStrategy::new_with_provider(
|
||||
Box::new(state_sync_provider),
|
||||
initial_peers,
|
||||
ProtocolName::Static(""),
|
||||
);
|
||||
|
||||
state_strategy.on_blocks_processed(
|
||||
1,
|
||||
1,
|
||||
vec![(
|
||||
Ok(BlockImportStatus::ImportedUnknown(1, ImportedAux::default(), None)),
|
||||
target_hash,
|
||||
)],
|
||||
);
|
||||
|
||||
let network_provider = NetworkServiceProvider::new();
|
||||
let network_handle = network_provider.handle();
|
||||
|
||||
// Strategy finishes.
|
||||
let actions = state_strategy.actions(&network_handle).collect::<Vec<_>>();
|
||||
assert_eq!(actions.len(), 1);
|
||||
assert!(matches!(&actions[0], SyncingAction::Finished));
|
||||
|
||||
// No more actions generated.
|
||||
assert_eq!(state_strategy.actions(&network_handle).count(), 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! State sync support.
|
||||
|
||||
use crate::{
|
||||
schema::v1::{KeyValueStateEntry, StateEntry, StateRequest, StateResponse},
|
||||
LOG_TARGET,
|
||||
};
|
||||
use codec::{Decode, Encode};
|
||||
use log::debug;
|
||||
use pezsc_client_api::{CompactProof, KeyValueStates, ProofProvider};
|
||||
use pezsc_consensus::ImportedState;
|
||||
use smallvec::SmallVec;
|
||||
use pezsp_core::storage::well_known_keys;
|
||||
use pezsp_runtime::{
|
||||
traits::{Block as BlockT, Header, NumberFor},
|
||||
Justifications,
|
||||
};
|
||||
use std::{collections::HashMap, fmt, sync::Arc};
|
||||
|
||||
/// Generic state sync provider. Used for mocking in tests.
|
||||
pub trait StateSyncProvider<B: BlockT>: Send + Sync {
|
||||
/// Validate and import a state response.
|
||||
fn import(&mut self, response: StateResponse) -> ImportResult<B>;
|
||||
/// Produce next state request.
|
||||
fn next_request(&self) -> StateRequest;
|
||||
/// Check if the state is complete.
|
||||
fn is_complete(&self) -> bool;
|
||||
/// Returns target block number.
|
||||
fn target_number(&self) -> NumberFor<B>;
|
||||
/// Returns target block hash.
|
||||
fn target_hash(&self) -> B::Hash;
|
||||
/// Returns state sync estimated progress.
|
||||
fn progress(&self) -> StateSyncProgress;
|
||||
}
|
||||
|
||||
// Reported state sync phase.
|
||||
#[derive(Clone, Eq, PartialEq, Debug)]
|
||||
pub enum StateSyncPhase {
|
||||
// State download in progress.
|
||||
DownloadingState,
|
||||
// Download is complete, state is being imported.
|
||||
ImportingState,
|
||||
}
|
||||
|
||||
impl fmt::Display for StateSyncPhase {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Self::DownloadingState => write!(f, "Downloading state"),
|
||||
Self::ImportingState => write!(f, "Importing state"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reported state download progress.
|
||||
#[derive(Clone, Eq, PartialEq, Debug)]
|
||||
pub struct StateSyncProgress {
|
||||
/// Estimated download percentage.
|
||||
pub percentage: u32,
|
||||
/// Total state size in bytes downloaded so far.
|
||||
pub size: u64,
|
||||
/// Current state sync phase.
|
||||
pub phase: StateSyncPhase,
|
||||
}
|
||||
|
||||
/// Import state chunk result.
|
||||
pub enum ImportResult<B: BlockT> {
|
||||
/// State is complete and ready for import.
|
||||
Import(B::Hash, B::Header, ImportedState<B>, Option<Vec<B::Extrinsic>>, Option<Justifications>),
|
||||
/// Continue downloading.
|
||||
Continue,
|
||||
/// Bad state chunk.
|
||||
BadResponse,
|
||||
}
|
||||
|
||||
struct StateSyncMetadata<B: BlockT> {
|
||||
last_key: SmallVec<[Vec<u8>; 2]>,
|
||||
target_header: B::Header,
|
||||
target_body: Option<Vec<B::Extrinsic>>,
|
||||
target_justifications: Option<Justifications>,
|
||||
complete: bool,
|
||||
imported_bytes: u64,
|
||||
skip_proof: bool,
|
||||
}
|
||||
|
||||
impl<B: BlockT> StateSyncMetadata<B> {
|
||||
fn target_hash(&self) -> B::Hash {
|
||||
self.target_header.hash()
|
||||
}
|
||||
|
||||
/// Returns target block number.
|
||||
fn target_number(&self) -> NumberFor<B> {
|
||||
*self.target_header.number()
|
||||
}
|
||||
|
||||
fn target_root(&self) -> B::Hash {
|
||||
*self.target_header.state_root()
|
||||
}
|
||||
|
||||
fn next_request(&self) -> StateRequest {
|
||||
StateRequest {
|
||||
block: self.target_hash().encode(),
|
||||
start: self.last_key.clone().into_vec(),
|
||||
no_proof: self.skip_proof,
|
||||
}
|
||||
}
|
||||
|
||||
fn progress(&self) -> StateSyncProgress {
|
||||
let cursor = *self.last_key.get(0).and_then(|last| last.get(0)).unwrap_or(&0u8);
|
||||
let percent_done = cursor as u32 * 100 / 256;
|
||||
StateSyncProgress {
|
||||
percentage: percent_done,
|
||||
size: self.imported_bytes,
|
||||
phase: if self.complete {
|
||||
StateSyncPhase::ImportingState
|
||||
} else {
|
||||
StateSyncPhase::DownloadingState
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// State sync state machine.
|
||||
///
|
||||
/// Accumulates partial state data until it is ready to be imported.
|
||||
pub struct StateSync<B: BlockT, Client> {
|
||||
metadata: StateSyncMetadata<B>,
|
||||
state: HashMap<Vec<u8>, (Vec<(Vec<u8>, Vec<u8>)>, Vec<Vec<u8>>)>,
|
||||
client: Arc<Client>,
|
||||
}
|
||||
|
||||
impl<B, Client> StateSync<B, Client>
|
||||
where
|
||||
B: BlockT,
|
||||
Client: ProofProvider<B> + Send + Sync + 'static,
|
||||
{
|
||||
/// Create a new instance.
|
||||
pub fn new(
|
||||
client: Arc<Client>,
|
||||
target_header: B::Header,
|
||||
target_body: Option<Vec<B::Extrinsic>>,
|
||||
target_justifications: Option<Justifications>,
|
||||
skip_proof: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
client,
|
||||
metadata: StateSyncMetadata {
|
||||
last_key: SmallVec::default(),
|
||||
target_header,
|
||||
target_body,
|
||||
target_justifications,
|
||||
complete: false,
|
||||
imported_bytes: 0,
|
||||
skip_proof,
|
||||
},
|
||||
state: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn process_state_key_values(
|
||||
&mut self,
|
||||
state_root: Vec<u8>,
|
||||
key_values: impl IntoIterator<Item = (Vec<u8>, Vec<u8>)>,
|
||||
) {
|
||||
let is_top = state_root.is_empty();
|
||||
|
||||
let entry = self.state.entry(state_root).or_default();
|
||||
|
||||
if entry.0.len() > 0 && entry.1.len() > 1 {
|
||||
// Already imported child_trie with same root.
|
||||
// Warning this will not work with parallel download.
|
||||
return;
|
||||
}
|
||||
|
||||
let mut child_storage_roots = Vec::new();
|
||||
|
||||
for (key, value) in key_values {
|
||||
// Skip all child key root (will be recalculated on import)
|
||||
if is_top && well_known_keys::is_child_storage_key(key.as_slice()) {
|
||||
child_storage_roots.push((value, key));
|
||||
} else {
|
||||
self.metadata.imported_bytes += key.len() as u64;
|
||||
entry.0.push((key, value));
|
||||
}
|
||||
}
|
||||
|
||||
for (root, storage_key) in child_storage_roots {
|
||||
self.state.entry(root).or_default().1.push(storage_key);
|
||||
}
|
||||
}
|
||||
|
||||
fn process_state_verified(&mut self, values: KeyValueStates) {
|
||||
for values in values.0 {
|
||||
self.process_state_key_values(values.state_root, values.key_values);
|
||||
}
|
||||
}
|
||||
|
||||
fn process_state_unverified(&mut self, response: StateResponse) -> bool {
|
||||
let mut complete = true;
|
||||
// if the trie is a child trie and one of its parent trie is empty,
|
||||
// the parent cursor stays valid.
|
||||
// Empty parent trie content only happens when all the response content
|
||||
// is part of a single child trie.
|
||||
if self.metadata.last_key.len() == 2 && response.entries[0].entries.is_empty() {
|
||||
// Do not remove the parent trie position.
|
||||
self.metadata.last_key.pop();
|
||||
} else {
|
||||
self.metadata.last_key.clear();
|
||||
}
|
||||
for state in response.entries {
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"Importing state from {:?} to {:?}",
|
||||
state.entries.last().map(|e| pezsp_core::hexdisplay::HexDisplay::from(&e.key)),
|
||||
state.entries.first().map(|e| pezsp_core::hexdisplay::HexDisplay::from(&e.key)),
|
||||
);
|
||||
|
||||
if !state.complete {
|
||||
if let Some(e) = state.entries.last() {
|
||||
self.metadata.last_key.push(e.key.clone());
|
||||
}
|
||||
complete = false;
|
||||
}
|
||||
|
||||
let KeyValueStateEntry { state_root, entries, complete: _ } = state;
|
||||
self.process_state_key_values(
|
||||
state_root,
|
||||
entries.into_iter().map(|StateEntry { key, value }| (key, value)),
|
||||
);
|
||||
}
|
||||
complete
|
||||
}
|
||||
}
|
||||
|
||||
impl<B, Client> StateSyncProvider<B> for StateSync<B, Client>
|
||||
where
|
||||
B: BlockT,
|
||||
Client: ProofProvider<B> + Send + Sync + 'static,
|
||||
{
|
||||
/// Validate and import a state response.
|
||||
fn import(&mut self, response: StateResponse) -> ImportResult<B> {
|
||||
if response.entries.is_empty() && response.proof.is_empty() {
|
||||
debug!(target: LOG_TARGET, "Bad state response");
|
||||
return ImportResult::BadResponse;
|
||||
}
|
||||
if !self.metadata.skip_proof && response.proof.is_empty() {
|
||||
debug!(target: LOG_TARGET, "Missing proof");
|
||||
return ImportResult::BadResponse;
|
||||
}
|
||||
let complete = if !self.metadata.skip_proof {
|
||||
debug!(target: LOG_TARGET, "Importing state from {} trie nodes", response.proof.len());
|
||||
let proof_size = response.proof.len() as u64;
|
||||
let proof = match CompactProof::decode(&mut response.proof.as_ref()) {
|
||||
Ok(proof) => proof,
|
||||
Err(e) => {
|
||||
debug!(target: LOG_TARGET, "Error decoding proof: {:?}", e);
|
||||
return ImportResult::BadResponse;
|
||||
},
|
||||
};
|
||||
let (values, completed) = match self.client.verify_range_proof(
|
||||
self.metadata.target_root(),
|
||||
proof,
|
||||
self.metadata.last_key.as_slice(),
|
||||
) {
|
||||
Err(e) => {
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"StateResponse failed proof verification: {}",
|
||||
e,
|
||||
);
|
||||
return ImportResult::BadResponse;
|
||||
},
|
||||
Ok(values) => values,
|
||||
};
|
||||
debug!(target: LOG_TARGET, "Imported with {} keys", values.len());
|
||||
|
||||
let complete = completed == 0;
|
||||
if !complete && !values.update_last_key(completed, &mut self.metadata.last_key) {
|
||||
debug!(target: LOG_TARGET, "Error updating key cursor, depth: {}", completed);
|
||||
};
|
||||
|
||||
self.process_state_verified(values);
|
||||
self.metadata.imported_bytes += proof_size;
|
||||
complete
|
||||
} else {
|
||||
self.process_state_unverified(response)
|
||||
};
|
||||
if complete {
|
||||
self.metadata.complete = true;
|
||||
let target_hash = self.metadata.target_hash();
|
||||
ImportResult::Import(
|
||||
target_hash,
|
||||
self.metadata.target_header.clone(),
|
||||
ImportedState { block: target_hash, state: std::mem::take(&mut self.state).into() },
|
||||
self.metadata.target_body.clone(),
|
||||
self.metadata.target_justifications.clone(),
|
||||
)
|
||||
} else {
|
||||
ImportResult::Continue
|
||||
}
|
||||
}
|
||||
|
||||
/// Produce next state request.
|
||||
fn next_request(&self) -> StateRequest {
|
||||
self.metadata.next_request()
|
||||
}
|
||||
|
||||
/// Check if the state is complete.
|
||||
fn is_complete(&self) -> bool {
|
||||
self.metadata.complete
|
||||
}
|
||||
|
||||
/// Returns target block number.
|
||||
fn target_number(&self) -> NumberFor<B> {
|
||||
self.metadata.target_number()
|
||||
}
|
||||
|
||||
/// Returns target block hash.
|
||||
fn target_hash(&self) -> B::Hash {
|
||||
self.metadata.target_hash()
|
||||
}
|
||||
|
||||
/// Returns state sync estimated progress.
|
||||
fn progress(&self) -> StateSyncProgress {
|
||||
self.metadata.progress()
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,150 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Common syncing types.
|
||||
|
||||
use futures::Stream;
|
||||
use pezsc_network_common::{role::Roles, types::ReputationChange};
|
||||
|
||||
use crate::strategy::{state_sync::StateSyncProgress, warp::WarpSyncProgress};
|
||||
|
||||
use pezsc_network_types::PeerId;
|
||||
use pezsp_runtime::traits::{Block as BlockT, NumberFor};
|
||||
|
||||
use std::{fmt, pin::Pin, sync::Arc};
|
||||
|
||||
/// The sync status of a peer we are trying to sync with
|
||||
#[derive(Debug)]
|
||||
pub struct PeerInfo<Block: BlockT> {
|
||||
/// Their best block hash.
|
||||
pub best_hash: Block::Hash,
|
||||
/// Their best block number.
|
||||
pub best_number: NumberFor<Block>,
|
||||
}
|
||||
|
||||
/// Info about a peer's known state (both full and light).
|
||||
#[derive(Debug)]
|
||||
pub struct ExtendedPeerInfo<B: BlockT> {
|
||||
/// Roles
|
||||
pub roles: Roles,
|
||||
/// Peer best block hash
|
||||
pub best_hash: B::Hash,
|
||||
/// Peer best block number
|
||||
pub best_number: NumberFor<B>,
|
||||
}
|
||||
|
||||
impl<B> Clone for ExtendedPeerInfo<B>
|
||||
where
|
||||
B: BlockT,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self { roles: self.roles, best_hash: self.best_hash, best_number: self.best_number }
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> Copy for ExtendedPeerInfo<B> where B: BlockT {}
|
||||
|
||||
/// Reported sync state.
|
||||
#[derive(Clone, Eq, PartialEq, Debug)]
|
||||
pub enum SyncState<BlockNumber> {
|
||||
/// Initial sync is complete, keep-up sync is active.
|
||||
Idle,
|
||||
/// Actively catching up with the chain.
|
||||
Downloading { target: BlockNumber },
|
||||
/// All blocks are downloaded and are being imported.
|
||||
Importing { target: BlockNumber },
|
||||
}
|
||||
|
||||
impl<BlockNumber> SyncState<BlockNumber> {
|
||||
/// Are we actively catching up with the chain?
|
||||
pub fn is_major_syncing(&self) -> bool {
|
||||
!matches!(self, SyncState::Idle)
|
||||
}
|
||||
}
|
||||
|
||||
/// Syncing status and statistics.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SyncStatus<Block: BlockT> {
|
||||
/// Current global sync state.
|
||||
pub state: SyncState<NumberFor<Block>>,
|
||||
/// Target sync block number.
|
||||
pub best_seen_block: Option<NumberFor<Block>>,
|
||||
/// Number of peers participating in syncing.
|
||||
pub num_peers: u32,
|
||||
/// Number of blocks queued for import
|
||||
pub queued_blocks: u32,
|
||||
/// State sync status in progress, if any.
|
||||
pub state_sync: Option<StateSyncProgress>,
|
||||
/// Warp sync in progress, if any.
|
||||
pub warp_sync: Option<WarpSyncProgress<Block>>,
|
||||
}
|
||||
|
||||
/// A peer did not behave as expected and should be reported.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct BadPeer(pub PeerId, pub ReputationChange);
|
||||
|
||||
impl fmt::Display for BadPeer {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "Bad peer {}; Reputation change: {:?}", self.0, self.1)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for BadPeer {}
|
||||
|
||||
/// Provides high-level status of syncing.
|
||||
#[async_trait::async_trait]
|
||||
pub trait SyncStatusProvider<Block: BlockT>: Send + Sync {
|
||||
/// Get high-level view of the syncing status.
|
||||
async fn status(&self) -> Result<SyncStatus<Block>, ()>;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<T, Block> SyncStatusProvider<Block> for Arc<T>
|
||||
where
|
||||
T: ?Sized,
|
||||
T: SyncStatusProvider<Block>,
|
||||
Block: BlockT,
|
||||
{
|
||||
async fn status(&self) -> Result<SyncStatus<Block>, ()> {
|
||||
T::status(self).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Syncing-related events that other protocols can subscribe to.
|
||||
pub enum SyncEvent {
|
||||
/// Peer that the syncing implementation is tracking connected.
|
||||
PeerConnected(PeerId),
|
||||
|
||||
/// Peer that the syncing implementation was tracking disconnected.
|
||||
PeerDisconnected(PeerId),
|
||||
}
|
||||
|
||||
pub trait SyncEventStream: Send + Sync {
|
||||
/// Subscribe to syncing-related events.
|
||||
fn event_stream(&self, name: &'static str) -> Pin<Box<dyn Stream<Item = SyncEvent> + Send>>;
|
||||
}
|
||||
|
||||
impl<T> SyncEventStream for Arc<T>
|
||||
where
|
||||
T: ?Sized,
|
||||
T: SyncEventStream,
|
||||
{
|
||||
fn event_stream(&self, name: &'static str) -> Pin<Box<dyn Stream<Item = SyncEvent> + Send>> {
|
||||
T::event_stream(self, name)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Bizinikiwi.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// Bizinikiwi 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.
|
||||
|
||||
// Bizinikiwi 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 Bizinikiwi. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Helper for handling (i.e. answering) grandpa warp sync requests from a remote peer.
|
||||
|
||||
use codec::Decode;
|
||||
use futures::{channel::oneshot, stream::StreamExt};
|
||||
use log::debug;
|
||||
|
||||
use crate::{
|
||||
strategy::warp::{EncodedProof, WarpProofRequest, WarpSyncProvider},
|
||||
LOG_TARGET,
|
||||
};
|
||||
use pezsc_network::{
|
||||
config::ProtocolId,
|
||||
request_responses::{IncomingRequest, OutgoingResponse},
|
||||
NetworkBackend, MAX_RESPONSE_SIZE,
|
||||
};
|
||||
use pezsp_runtime::traits::Block as BlockT;
|
||||
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
/// Incoming warp requests bounded queue size.
|
||||
const MAX_WARP_REQUEST_QUEUE: usize = 20;
|
||||
|
||||
/// Generates a `RequestResponseProtocolConfig` for the grandpa warp sync request protocol, refusing
|
||||
/// incoming requests.
|
||||
pub fn generate_request_response_config<
|
||||
Hash: AsRef<[u8]>,
|
||||
B: BlockT,
|
||||
N: NetworkBackend<B, <B as BlockT>::Hash>,
|
||||
>(
|
||||
protocol_id: ProtocolId,
|
||||
genesis_hash: Hash,
|
||||
fork_id: Option<&str>,
|
||||
inbound_queue: async_channel::Sender<IncomingRequest>,
|
||||
) -> N::RequestResponseProtocolConfig {
|
||||
N::request_response_config(
|
||||
generate_protocol_name(genesis_hash, fork_id).into(),
|
||||
std::iter::once(generate_legacy_protocol_name(protocol_id).into()).collect(),
|
||||
32,
|
||||
MAX_RESPONSE_SIZE,
|
||||
Duration::from_secs(10),
|
||||
Some(inbound_queue),
|
||||
)
|
||||
}
|
||||
|
||||
/// Generate the grandpa warp sync protocol name from the genesis hash and fork id.
|
||||
fn generate_protocol_name<Hash: AsRef<[u8]>>(genesis_hash: Hash, fork_id: Option<&str>) -> String {
|
||||
let genesis_hash = genesis_hash.as_ref();
|
||||
if let Some(fork_id) = fork_id {
|
||||
format!("/{}/{}/sync/warp", array_bytes::bytes2hex("", genesis_hash), fork_id)
|
||||
} else {
|
||||
format!("/{}/sync/warp", array_bytes::bytes2hex("", genesis_hash))
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate the legacy grandpa warp sync protocol name from chain specific protocol identifier.
|
||||
fn generate_legacy_protocol_name(protocol_id: ProtocolId) -> String {
|
||||
format!("/{}/sync/warp", protocol_id.as_ref())
|
||||
}
|
||||
|
||||
/// Handler for incoming grandpa warp sync requests from a remote peer.
|
||||
pub struct RequestHandler<TBlock: BlockT> {
|
||||
backend: Arc<dyn WarpSyncProvider<TBlock>>,
|
||||
request_receiver: async_channel::Receiver<IncomingRequest>,
|
||||
}
|
||||
|
||||
impl<TBlock: BlockT> RequestHandler<TBlock> {
|
||||
/// Create a new [`RequestHandler`].
|
||||
pub fn new<Hash: AsRef<[u8]>, N: NetworkBackend<TBlock, <TBlock as BlockT>::Hash>>(
|
||||
protocol_id: ProtocolId,
|
||||
genesis_hash: Hash,
|
||||
fork_id: Option<&str>,
|
||||
backend: Arc<dyn WarpSyncProvider<TBlock>>,
|
||||
) -> (Self, N::RequestResponseProtocolConfig) {
|
||||
let (tx, request_receiver) = async_channel::bounded(MAX_WARP_REQUEST_QUEUE);
|
||||
|
||||
let request_response_config = generate_request_response_config::<_, TBlock, N>(
|
||||
protocol_id,
|
||||
genesis_hash,
|
||||
fork_id,
|
||||
tx,
|
||||
);
|
||||
|
||||
(Self { backend, request_receiver }, request_response_config)
|
||||
}
|
||||
|
||||
fn handle_request(
|
||||
&self,
|
||||
payload: Vec<u8>,
|
||||
pending_response: oneshot::Sender<OutgoingResponse>,
|
||||
) -> Result<(), HandleRequestError> {
|
||||
let request = WarpProofRequest::<TBlock>::decode(&mut &payload[..])?;
|
||||
|
||||
let EncodedProof(proof) = self
|
||||
.backend
|
||||
.generate(request.begin)
|
||||
.map_err(HandleRequestError::InvalidRequest)?;
|
||||
|
||||
pending_response
|
||||
.send(OutgoingResponse {
|
||||
result: Ok(proof),
|
||||
reputation_changes: Vec::new(),
|
||||
sent_feedback: None,
|
||||
})
|
||||
.map_err(|_| HandleRequestError::SendResponse)
|
||||
}
|
||||
|
||||
/// Run [`RequestHandler`].
|
||||
pub async fn run(mut self) {
|
||||
while let Some(request) = self.request_receiver.next().await {
|
||||
let IncomingRequest { peer, payload, pending_response } = request;
|
||||
|
||||
match self.handle_request(payload, pending_response) {
|
||||
Ok(()) => {
|
||||
debug!(target: LOG_TARGET, "Handled grandpa warp sync request from {}.", peer)
|
||||
},
|
||||
Err(e) => debug!(
|
||||
target: LOG_TARGET,
|
||||
"Failed to handle grandpa warp sync request from {}: {}",
|
||||
peer, e,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
enum HandleRequestError {
|
||||
#[error("Failed to decode request: {0}.")]
|
||||
DecodeProto(#[from] prost::DecodeError),
|
||||
|
||||
#[error("Failed to encode response: {0}.")]
|
||||
EncodeProto(#[from] prost::EncodeError),
|
||||
|
||||
#[error("Failed to decode block hash: {0}.")]
|
||||
DecodeScale(#[from] codec::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Client(#[from] pezsp_blockchain::Error),
|
||||
|
||||
#[error("Invalid request {0}.")]
|
||||
InvalidRequest(#[from] Box<dyn std::error::Error + Send + Sync>),
|
||||
|
||||
#[error("Failed to send response.")]
|
||||
SendResponse,
|
||||
}
|
||||
Reference in New Issue
Block a user