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:
2025-12-14 00:04:10 +03:00
parent 286de54384
commit 1c0e57d984
9084 changed files with 997839 additions and 997557 deletions
+124
View File
@@ -0,0 +1,124 @@
[package]
description = "Bizinikiwi network protocol"
name = "pezsc-network"
version = "0.34.0"
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
authors.workspace = true
edition.workspace = true
homepage.workspace = true
repository.workspace = true
documentation = "https://docs.rs/pezsc-network"
readme = "README.md"
[lints]
workspace = true
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[[bench]]
name = "notifications_protocol"
harness = false
[[bench]]
name = "request_response_protocol"
harness = false
[dependencies]
array-bytes = { workspace = true, default-features = true }
async-channel = { workspace = true }
async-trait = { workspace = true }
asynchronous-codec = { workspace = true }
bytes = { workspace = true, default-features = true }
cid = { workspace = true }
codec = { features = ["derive"], workspace = true, default-features = true }
either = { workspace = true, default-features = true }
fnv = { workspace = true }
futures = { workspace = true }
futures-timer = { workspace = true }
ip_network = { workspace = true }
libp2p = { features = [
"dns",
"identify",
"kad",
"macros",
"mdns",
"noise",
"ping",
"request-response",
"tcp",
"tokio",
"websocket",
"yamux",
], workspace = true }
linked_hash_set = { workspace = true }
litep2p = { workspace = true }
log = { workspace = true, default-features = true }
mockall = { workspace = true }
parking_lot = { workspace = true, default-features = true }
partial_sort = { workspace = true }
pin-project = { workspace = true }
prometheus-endpoint = { workspace = true, default-features = true }
prost = { workspace = true }
rand = { workspace = true, default-features = true }
pezsc-client-api = { workspace = true, default-features = true }
pezsc-network-common = { workspace = true, default-features = true }
pezsc-network-types = { workspace = true, default-features = true }
pezsc-utils = { workspace = true, default-features = true }
schnellru = { workspace = true }
serde = { features = ["derive"], workspace = true, default-features = true }
serde_json = { workspace = true, default-features = true }
smallvec = { workspace = true, default-features = true }
pezsp-arithmetic = { workspace = true, default-features = true }
pezsp-blockchain = { workspace = true, default-features = true }
pezsp-core = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
thiserror = { workspace = true }
tokio = { features = [
"macros",
"sync",
], workspace = true, default-features = true }
tokio-stream = { workspace = true }
unsigned-varint = { features = [
"asynchronous_codec",
"futures",
], workspace = true }
void = { workspace = true }
wasm-timer = { workspace = true }
zeroize = { workspace = true, default-features = true }
[dev-dependencies]
assert_matches = { workspace = true }
multistream-select = { workspace = true }
pezsc-block-builder = { workspace = true, default-features = true }
pezsp-consensus = { workspace = true, default-features = true }
pezsp-crypto-hashing = { workspace = true, default-features = true }
pezsp-tracing = { workspace = true, default-features = true }
bizinikiwi-test-runtime = { workspace = true }
bizinikiwi-test-runtime-client = { workspace = true }
tempfile = { workspace = true }
tokio = { features = [
"macros",
"rt-multi-thread",
], workspace = true, default-features = true }
tokio-util = { features = ["compat"], workspace = true }
criterion = { workspace = true, default-features = true, features = [
"async_tokio",
] }
[build-dependencies]
prost-build = { workspace = true }
[features]
default = []
runtime-benchmarks = [
"pezsc-block-builder/runtime-benchmarks",
"pezsc-client-api/runtime-benchmarks",
"pezsc-network-common/runtime-benchmarks",
"pezsp-blockchain/runtime-benchmarks",
"pezsp-consensus/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
"bizinikiwi-test-runtime-client/runtime-benchmarks",
"bizinikiwi-test-runtime/runtime-benchmarks",
]
+287
View File
@@ -0,0 +1,287 @@
Bizinikiwi-specific P2P networking.
**Important**: This crate is unstable and the API and usage may change.
# Node identities and addresses
In a decentralized network, each node possesses a network private key and a network public key.
In Bizinikiwi, the keys are based on the ed25519 curve.
From a node's public key, we can derive its *identity*. In Bizinikiwi and libp2p, a node's
identity is represented with the [`PeerId`] struct. All network communications between nodes on
the network use encryption derived from both sides's keys, which means that **identities cannot
be faked**.
A node's identity uniquely identifies a machine on the network. If you start two or more
clients using the same network key, large interferences will happen.
# Bizinikiwi's network protocol
Bizinikiwi's networking protocol is based upon libp2p. It is at the moment not possible and not
planned to permit using something else than the libp2p network stack and the rust-libp2p
library. However the libp2p framework is very flexible and the rust-libp2p library could be
extended to support a wider range of protocols than what is offered by libp2p.
## Discovery mechanisms
In order for our node to join a peer-to-peer network, it has to know a list of nodes that are
part of said network. This includes nodes identities and their address (how to reach them).
Building such a list is called the **discovery** mechanism. There are three mechanisms that
Bizinikiwi uses:
- Bootstrap nodes. These are hard-coded node identities and addresses passed alongside with
the network configuration.
- mDNS. We perform a UDP broadcast on the local network. Nodes that listen may respond with
their identity. More info [here](https://github.com/libp2p/specs/blob/master/discovery/mdns.md).
mDNS can be disabled in the network configuration.
- Kademlia random walk. Once connected, we perform random Kademlia `FIND_NODE` requests on the
configured Kademlia DHTs (one per configured chain protocol) in order for nodes to propagate to
us their view of the network. More information about Kademlia can be found [on
Wikipedia](https://en.wikipedia.org/wiki/Kademlia).
## Connection establishment
When node Alice knows node Bob's identity and address, it can establish a connection with Bob.
All connections must always use encryption and multiplexing. While some node addresses (eg.
addresses using `/quic`) already imply which encryption and/or multiplexing to use, for others
the **multistream-select** protocol is used in order to negotiate an encryption layer and/or a
multiplexing layer.
The connection establishment mechanism is called the **transport**.
As of the writing of this documentation, the following base-layer protocols are supported by
Bizinikiwi:
- TCP/IP for addresses of the form `/ip4/1.2.3.4/tcp/5`. Once the TCP connection is open, an
encryption and a multiplexing layer are negotiated on top.
- WebSockets for addresses of the form `/ip4/1.2.3.4/tcp/5/ws`. A TCP/IP connection is open and
the WebSockets protocol is negotiated on top. Communications then happen inside WebSockets data
frames. Encryption and multiplexing are additionally negotiated again inside this channel.
- DNS for addresses of the form `/dns/example.com/tcp/5` or `/dns/example.com/tcp/5/ws`. A
node's address can contain a domain name.
- (All of the above using IPv6 instead of IPv4.)
On top of the base-layer protocol, the [Noise](https://noiseprotocol.org/) protocol is
negotiated and applied. The exact handshake protocol is experimental and is subject to change.
The following multiplexing protocols are supported:
- [Yamux](https://github.com/hashicorp/yamux/blob/master/spec.md).
## Substreams
Once a connection has been established and uses multiplexing, substreams can be opened. When
a substream is open, the **multistream-select** protocol is used to negotiate which protocol
to use on that given substream.
Protocols that are specific to a certain chain have a `<protocol-id>` in their name. This
"protocol ID" is defined in the chain specifications. For example, the protocol ID of PezkuwiChain
is "hez". In the protocol names below, `<protocol-id>` must be replaced with the corresponding
protocol ID.
> **Note**: It is possible for the same connection to be used for multiple chains. For example,
> one can use both the `/hez/sync/2` and `/sub/sync/2` protocols on the same
> connection, provided that the remote supports them.
Bizinikiwi uses the following standard libp2p protocols:
- **`/ipfs/ping/1.0.0`**. We periodically open an ephemeral substream in order to ping the
remote and check whether the connection is still alive. Failure for the remote to reply leads
to a disconnection.
- **[`/ipfs/id/1.0.0`](https://github.com/libp2p/specs/tree/master/identify)**. We
periodically open an ephemeral substream in order to ask information from the remote.
- **[`/<protocol_id>/kad`](https://github.com/libp2p/specs/pull/108)**. We periodically open
ephemeral substreams for Kademlia random walk queries. Each Kademlia query is done in a
separate substream.
Additionally, Bizinikiwi uses the following non-libp2p-standard protocols:
- **`/bizinikiwi/<protocol-id>/<version>`** (where `<protocol-id>` must be replaced with the
protocol ID of the targeted chain, and `<version>` is a number between 2 and 6). For each
connection we optionally keep an additional substream for all Bizinikiwi-based communications alive.
This protocol is considered legacy, and is progressively being replaced with alternatives.
This is designated as "The legacy Bizinikiwi substream" in this documentation. See below for
more details.
- **`/<protocol-id>/sync/2`** is a request-response protocol (see below) that lets one perform
requests for information about blocks. Each request is the encoding of a `BlockRequest` and
each response is the encoding of a `BlockResponse`, as defined in the `api.v1.proto` file in
this source tree.
- **`/<protocol-id>/light/2`** is a request-response protocol (see below) that lets one perform
light-client-related requests for information about the state. Each request is the encoding of
a `light::Request` and each response is the encoding of a `light::Response`, as defined in the
`light.v1.proto` file in this source tree.
- **`/<protocol-id>/transactions/1`** is a notifications protocol (see below) where
transactions are pushed to other nodes. The handshake is empty on both sides. The message
format is a SCALE-encoded list of transactions, where each transaction is an opaque list of
bytes.
- **`/<protocol-id>/block-announces/1`** is a notifications protocol (see below) where
block announces are pushed to other nodes. The handshake is empty on both sides. The message
format is a SCALE-encoded tuple containing a block header followed with an opaque list of
bytes containing some data associated with this block announcement, e.g. a candidate message.
- Notifications protocols that are registered using `NetworkConfiguration::notifications_protocols`.
For example: `/paritytech/grandpa/1`. See below for more information.
## The legacy Bizinikiwi substream
Bizinikiwi uses a component named the **peerset manager (PSM)**. Through the discovery
mechanism, the PSM is aware of the nodes that are part of the network and decides which nodes
we should perform Bizinikiwi-based communications with. For these nodes, we open a connection
if necessary and open a unique substream for Bizinikiwi-based communications. If the PSM decides
that we should disconnect a node, then that substream is closed.
For more information about the PSM, see the *pezsc-peerset* crate.
Note that at the moment there is no mechanism in place to solve the issues that arise where the
two sides of a connection open the unique substream simultaneously. In order to not run into
issues, only the dialer of a connection is allowed to open the unique substream. When the
substream is closed, the entire connection is closed as well. This is a bug that will be
resolved by deprecating the protocol entirely.
Within the unique Bizinikiwi substream, messages encoded using
[`parity-scale-codec``](https://github.com/paritytech/parity-scale-codec) are exchanged.
The detail of theses messages is not totally in place, but they can be found in the
`message.rs` file.
Once the substream is open, the first step is an exchange of a *status* message from both
sides, containing information such as the chain root hash, head of chain, and so on.
Communications within this substream include:
- Syncing. Blocks are announced and requested from other nodes.
- Light-client requests. When a light client requires information, a random node we have a
substream open with is chosen, and the information is requested from it.
- Gossiping. Used for example by grandpa.
## Request-response protocols
A so-called request-response protocol is defined as follow:
- When a substream is opened, the opening side sends a message whose content is
protocol-specific. The message must be prefixed with an
[LEB128-encoded number](https://en.wikipedia.org/wiki/LEB128) indicating its length. After the
message has been sent, the writing side is closed.
- The remote sends back the response prefixed with a LEB128-encoded length, and closes its
side as well.
Each request is performed in a new separate substream.
## Notifications protocols
A so-called notifications protocol is defined as follow:
- When a substream is opened, the opening side sends a handshake message whose content is
protocol-specific. The handshake message must be prefixed with an
[LEB128-encoded number](https://en.wikipedia.org/wiki/LEB128) indicating its length. The
handshake message can be of length 0, in which case the sender has to send a single `0`.
- The receiver then either immediately closes the substream, or answers with its own
LEB128-prefixed protocol-specific handshake response. The message can be of length 0, in which
case a single `0` has to be sent back.
- Once the handshake has completed, the notifications protocol is unidirectional. Only the
node which initiated the substream can push notifications. If the remote wants to send
notifications as well, it has to open its own undirectional substream.
- Each notification must be prefixed with an LEB128-encoded length. The encoding of the
messages is specific to each protocol.
- Either party can signal that it doesn't want a notifications substream anymore by closing
its writing side. The other party should respond by closing its own writing side soon after.
The API of `pezsc-network` allows one to register user-defined notification protocols.
`pezsc-network` automatically tries to open a substream towards each node for which the legacy
Substream substream is open. The handshake is then performed automatically.
For example, the `pezsc-consensus-grandpa` crate registers the `/paritytech/grandpa/1`
notifications protocol.
At the moment, for backwards-compatibility, notification protocols are tied to the legacy
Bizinikiwi substream. Additionally, the handshake message is hardcoded to be a single 8-bits
integer representing the role of the node:
- 1 for a full node.
- 2 for a light node.
- 4 for an authority.
In the future, though, these restrictions will be removed.
# Sync
The crate implements a number of syncing algorithms. The main purpose of the syncing algorithm is
get the chain to the latest state and keep it synced with the rest of the network by downloading and
importing new data as soon as it becomes available. Once the node starts it catches up with the network
with one of the initial sync methods listed below, and once it is completed uses a keep-up sync to
download new blocks.
## Full and light sync
This is the default syncing method for the initial and keep-up sync. The algorithm starts with the
current best block and downloads block data progressively from multiple peers if available. Once
there's a sequence of blocks ready to be imported they are fed to the import queue. Full nodes download
and execute full blocks, while light nodes only download and import headers. This continues until each peers
has no more new blocks to give.
For each peer the sync maintains the number of our common best block with that peer. This number is updates
whenever peer announce new blocks or our best block advances. This allows to keep track of peers that have new
block data and request new information as soon as it is announced. In keep-up mode, we also track peers that
announce blocks on all branches and not just the best branch. The sync algorithm tries to be greedy and download
all data that's announced.
## Fast sync
In this mode the initial downloads and verifies full header history. This allows to validate
authority set transitions and arrive at a recent header. After header chain is verified and imported
the node starts downloading a state snapshot using the state request protocol. Each `StateRequest`
contains a starting storage key, which is empty for the first request.
`StateResponse` contains a storage proof for a sequence of keys and values in the storage
starting (but not including) from the key that is in the request. After iterating the proof trie against
the storage root that is in the target header, the node issues The next `StateRequest` with set starting
key set to the last key from the previous response. This continues until trie iteration reaches the end.
The state is then imported into the database and the keep-up sync starts in normal full/light sync mode.
## Warp sync
This is similar to fast sync, but instead of downloading and verifying full header chain, the algorithm
only downloads finalized authority set changes.
### GRANDPA warp sync
GRANDPA keeps justifications for each finalized authority set change. Each change is signed by the
authorities from the previous set. By downloading and verifying these signed hand-offs starting from genesis,
we arrive at a recent header faster than downloading full header chain. Each `WarpSyncRequest` contains a block
hash to start collecting proofs from. `WarpSyncResponse` contains a sequence of block headers and
justifications. The proof downloader checks the justifications and continues requesting proofs from the last
header hash, until it arrives at some recent header.
Once the finality chain is proved for a header, the state matching the header is downloaded much like during
the fast sync. The state is verified to match the header storage root. After the state is imported into the
database it is queried for the information that allows GRANDPA and BABE to continue operating from that state.
This includes BABE epoch information and GRANDPA authority set id.
### Background block download
After the latest state has been imported the node is fully operational, but is still missing historic block
data. I.e. it is unable to serve bock bodies and headers other than the most recent one. To make sure all
nodes have block history available, a background sync process is started that downloads all the missing blocks.
It is run in parallel with the keep-up sync and does not interfere with downloading of the recent blocks.
During this download we also import GRANDPA justifications for blocks with authority set changes, so that
the warp-synced node has all the data to serve for other nodes that might want to sync from it with
any method.
# Usage
Using the `pezsc-network` crate is done through the [`NetworkWorker`] struct. Create this
struct by passing a [`config::Params`], then poll it as if it was a `Future`. You can extract an
`Arc<NetworkService>` from the `NetworkWorker`, which can be shared amongst multiple places
in order to give orders to the networking.
See the [`config`] module for more information about how to configure the networking.
After the `NetworkWorker` has been created, the important things to do are:
- Calling `NetworkWorker::poll` in order to advance the network. This can be done by
dispatching a background task with the [`NetworkWorker`].
- Calling `on_block_import` whenever a block is added to the client.
- Calling `on_block_finalized` whenever a block is finalized.
- Calling `trigger_repropagate` when a transaction is added to the pool.
More precise usage details are still being worked on and will likely change in the future.
License: GPL-3.0-or-later WITH Classpath-exception-2.0
@@ -0,0 +1,311 @@
// 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 criterion::{
criterion_group, criterion_main, AxisScale, BenchmarkId, Criterion, PlotConfiguration,
Throughput,
};
use pezsc_network::{
config::{
FullNetworkConfiguration, MultiaddrWithPeerId, NetworkConfiguration, NonReservedPeerMode,
NotificationHandshake, Params, ProtocolId, Role, SetConfig,
},
service::traits::{NetworkService, NotificationEvent},
Litep2pNetworkBackend, NetworkBackend, NetworkWorker, NotificationMetrics, NotificationService,
PeerId, Roles,
};
use pezsc_network_common::{sync::message::BlockAnnouncesHandshake, ExHashT};
use pezsp_core::H256;
use pezsp_runtime::traits::{Block as BlockT, Zero};
use std::{sync::Arc, time::Duration};
use bizinikiwi_test_runtime_client::runtime;
use tokio::{sync::Mutex, task::JoinHandle};
const NUMBER_OF_NOTIFICATIONS: usize = 100;
const PAYLOAD: &[(u32, &'static str)] = &[
// (Exponent of size, label)
(6, "64B"),
(9, "512B"),
(12, "4KB"),
(15, "64KB"),
(18, "256KB"),
(21, "2MB"),
(24, "16MB"),
];
const MAX_SIZE: u64 = 2u64.pow(30);
fn create_network_worker<B, H, N>(
) -> (N, Arc<dyn NetworkService>, Arc<Mutex<Box<dyn NotificationService>>>)
where
B: BlockT<Hash = H256> + 'static,
H: ExHashT,
N: NetworkBackend<B, H>,
{
let role = Role::Full;
let net_conf = NetworkConfiguration::new_local();
let network_config = FullNetworkConfiguration::<B, H, N>::new(&net_conf, None);
let genesis_hash = runtime::Hash::zero();
let (block_announce_config, notification_service) = N::notification_config(
"/block-announces/1".into(),
vec!["/bench-notifications-protocol/block-announces/1".into()],
MAX_SIZE,
Some(NotificationHandshake::new(BlockAnnouncesHandshake::<runtime::Block>::build(
Roles::from(&role),
Zero::zero(),
genesis_hash,
genesis_hash,
))),
SetConfig {
in_peers: 1,
out_peers: 1,
reserved_nodes: vec![],
non_reserved_mode: NonReservedPeerMode::Accept,
},
NotificationMetrics::new(None),
network_config.peer_store_handle(),
);
let worker = N::new(Params::<B, H, N> {
block_announce_config,
role,
executor: Box::new(|f| {
tokio::spawn(f);
}),
genesis_hash,
network_config,
protocol_id: ProtocolId::from("bench-protocol-name"),
fork_id: None,
metrics_registry: None,
bitswap_config: None,
notification_metrics: NotificationMetrics::new(None),
})
.unwrap();
let network_service = worker.network_service();
let notification_service = Arc::new(Mutex::new(notification_service));
(worker, network_service, notification_service)
}
struct BenchSetup {
notification_service1: Arc<Mutex<Box<dyn NotificationService>>>,
notification_service2: Arc<Mutex<Box<dyn NotificationService>>>,
peer_id2: PeerId,
handle1: JoinHandle<()>,
handle2: JoinHandle<()>,
}
impl Drop for BenchSetup {
fn drop(&mut self) {
self.handle1.abort();
self.handle2.abort();
}
}
fn setup_workers<B, H, N>(rt: &tokio::runtime::Runtime) -> Arc<BenchSetup>
where
B: BlockT<Hash = H256> + 'static,
H: ExHashT,
N: NetworkBackend<B, H>,
{
let _guard = rt.enter();
let (worker1, network_service1, notification_service1) = create_network_worker::<B, H, N>();
let (worker2, network_service2, notification_service2) = create_network_worker::<B, H, N>();
let peer_id2: pezsc_network::PeerId = network_service2.local_peer_id().into();
let handle1 = tokio::spawn(worker1.run());
let handle2 = tokio::spawn(worker2.run());
let ready = tokio::spawn({
let notification_service1 = Arc::clone(&notification_service1);
let notification_service2 = Arc::clone(&notification_service2);
async move {
let listen_address2 = {
while network_service2.listen_addresses().is_empty() {
tokio::time::sleep(Duration::from_millis(10)).await;
}
network_service2.listen_addresses()[0].clone()
};
network_service1
.add_reserved_peer(MultiaddrWithPeerId {
multiaddr: listen_address2,
peer_id: peer_id2,
})
.unwrap();
let mut notification_service1 = notification_service1.lock().await;
let mut notification_service2 = notification_service2.lock().await;
loop {
tokio::select! {
Some(event) = notification_service1.next_event() => {
if let NotificationEvent::NotificationStreamOpened { .. } = event {
// Send a 32MB notification to preheat the network
notification_service1.send_async_notification(&peer_id2, vec![0; 2usize.pow(25)]).await.unwrap();
}
},
Some(event) = notification_service2.next_event() => {
match event {
NotificationEvent::ValidateInboundSubstream { result_tx, .. } => {
result_tx.send(pezsc_network::service::traits::ValidationResult::Accept).unwrap();
},
NotificationEvent::NotificationReceived { .. } => {
break;
}
_ => {}
}
},
}
}
}
});
tokio::task::block_in_place(|| {
let _ = tokio::runtime::Handle::current().block_on(ready);
});
Arc::new(BenchSetup {
notification_service1,
notification_service2,
peer_id2,
handle1,
handle2,
})
}
async fn run_serially(setup: Arc<BenchSetup>, size: usize, limit: usize) {
let (tx, rx) = async_channel::bounded(1);
let _ = tx.send(Some(())).await;
let network1 = tokio::spawn({
let notification_service1 = Arc::clone(&setup.notification_service1);
let peer_id2 = setup.peer_id2;
async move {
let mut notification_service1 = notification_service1.lock().await;
while let Ok(message) = rx.recv().await {
let Some(_) = message else { break };
notification_service1
.send_async_notification(&peer_id2, vec![0; size])
.await
.unwrap();
}
}
});
let network2 = tokio::spawn({
let notification_service2 = Arc::clone(&setup.notification_service2);
async move {
let mut notification_service2 = notification_service2.lock().await;
let mut received_counter = 0;
while let Some(event) = notification_service2.next_event().await {
if let NotificationEvent::NotificationReceived { .. } = event {
received_counter += 1;
if received_counter >= limit {
let _ = tx.send(None).await;
break;
}
let _ = tx.send(Some(())).await;
}
}
}
});
let _ = tokio::join!(network1, network2);
}
async fn run_with_backpressure(setup: Arc<BenchSetup>, size: usize, limit: usize) {
let (tx, rx) = async_channel::bounded(1);
let network1 = tokio::spawn({
let setup = Arc::clone(&setup);
async move {
let mut notification_service1 = setup.notification_service1.lock().await;
for _ in 0..limit {
notification_service1
.send_async_notification(&setup.peer_id2, vec![0; size])
.await
.unwrap();
}
let _ = rx.recv().await;
}
});
let network2 = tokio::spawn({
let setup = Arc::clone(&setup);
async move {
let mut notification_service2 = setup.notification_service2.lock().await;
let mut received_counter = 0;
while let Some(event) = notification_service2.next_event().await {
if let NotificationEvent::NotificationReceived { .. } = event {
received_counter += 1;
if received_counter >= limit {
let _ = tx.send(()).await;
break;
}
}
}
}
});
let _ = tokio::join!(network1, network2);
}
fn run_benchmark(c: &mut Criterion) {
let rt = tokio::runtime::Runtime::new().unwrap();
let plot_config = PlotConfiguration::default().summary_scale(AxisScale::Logarithmic);
let mut group = c.benchmark_group("notifications_protocol");
group.plot_config(plot_config);
group.sample_size(10);
let libp2p_setup = setup_workers::<runtime::Block, runtime::Hash, NetworkWorker<_, _>>(&rt);
for &(exponent, label) in PAYLOAD.iter() {
let size = 2usize.pow(exponent);
group.throughput(Throughput::Bytes(NUMBER_OF_NOTIFICATIONS as u64 * size as u64));
group.bench_with_input(BenchmarkId::new("libp2p/serially", label), &size, |b, &size| {
b.to_async(&rt)
.iter(|| run_serially(Arc::clone(&libp2p_setup), size, NUMBER_OF_NOTIFICATIONS));
});
group.bench_with_input(
BenchmarkId::new("libp2p/with_backpressure", label),
&size,
|b, &size| {
b.to_async(&rt).iter(|| {
run_with_backpressure(Arc::clone(&libp2p_setup), size, NUMBER_OF_NOTIFICATIONS)
});
},
);
}
drop(libp2p_setup);
let litep2p_setup = setup_workers::<runtime::Block, runtime::Hash, Litep2pNetworkBackend>(&rt);
for &(exponent, label) in PAYLOAD.iter() {
let size = 2usize.pow(exponent);
group.throughput(Throughput::Bytes(NUMBER_OF_NOTIFICATIONS as u64 * size as u64));
group.bench_with_input(BenchmarkId::new("litep2p/serially", label), &size, |b, &size| {
b.to_async(&rt)
.iter(|| run_serially(Arc::clone(&litep2p_setup), size, NUMBER_OF_NOTIFICATIONS));
});
group.bench_with_input(
BenchmarkId::new("litep2p/with_backpressure", label),
&size,
|b, &size| {
b.to_async(&rt).iter(|| {
run_with_backpressure(Arc::clone(&litep2p_setup), size, NUMBER_OF_NOTIFICATIONS)
});
},
);
}
drop(litep2p_setup);
}
criterion_group!(benches, run_benchmark);
criterion_main!(benches);
@@ -0,0 +1,325 @@
// 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 criterion::{
criterion_group, criterion_main, AxisScale, BenchmarkId, Criterion, PlotConfiguration,
Throughput,
};
use pezsc_network::{
config::{
FullNetworkConfiguration, IncomingRequest, NetworkConfiguration, NonReservedPeerMode,
NotificationHandshake, OutgoingResponse, Params, ProtocolId, Role, SetConfig,
},
service::traits::NetworkService,
IfDisconnected, Litep2pNetworkBackend, NetworkBackend, NetworkRequest, NetworkWorker,
NotificationMetrics, NotificationService, PeerId, Roles,
};
use pezsc_network_common::{sync::message::BlockAnnouncesHandshake, ExHashT};
use pezsp_core::H256;
use pezsp_runtime::traits::{Block as BlockT, Zero};
use std::{sync::Arc, time::Duration};
use bizinikiwi_test_runtime_client::runtime;
use tokio::{sync::Mutex, task::JoinHandle};
const MAX_SIZE: u64 = 2u64.pow(30);
const NUMBER_OF_REQUESTS: usize = 100;
const PAYLOAD: &[(u32, &'static str)] = &[
// (Exponent of size, label)
(6, "64B"),
(9, "512B"),
(12, "4KB"),
(15, "64KB"),
(18, "256KB"),
(21, "2MB"),
(24, "16MB"),
];
pub fn create_network_worker<B, H, N>() -> (
N,
Arc<dyn NetworkService>,
async_channel::Receiver<IncomingRequest>,
Arc<Mutex<Box<dyn NotificationService>>>,
)
where
B: BlockT<Hash = H256> + 'static,
H: ExHashT,
N: NetworkBackend<B, H>,
{
let (tx, rx) = async_channel::bounded(10);
let request_response_config = N::request_response_config(
"/request-response/1".into(),
vec![],
MAX_SIZE,
MAX_SIZE,
Duration::from_secs(2),
Some(tx),
);
let role = Role::Full;
let net_conf = NetworkConfiguration::new_local();
let mut network_config = FullNetworkConfiguration::new(&net_conf, None);
network_config.add_request_response_protocol(request_response_config);
let genesis_hash = runtime::Hash::zero();
let (block_announce_config, notification_service) = N::notification_config(
"/block-announces/1".into(),
vec![],
1024,
Some(NotificationHandshake::new(BlockAnnouncesHandshake::<runtime::Block>::build(
Roles::from(&Role::Full),
Zero::zero(),
genesis_hash,
genesis_hash,
))),
SetConfig {
in_peers: 1,
out_peers: 1,
reserved_nodes: vec![],
non_reserved_mode: NonReservedPeerMode::Accept,
},
NotificationMetrics::new(None),
network_config.peer_store_handle(),
);
let worker = N::new(Params::<B, H, N> {
block_announce_config,
role,
executor: Box::new(|f| {
tokio::spawn(f);
}),
genesis_hash: runtime::Hash::zero(),
network_config,
protocol_id: ProtocolId::from("bench-request-response-protocol"),
fork_id: None,
metrics_registry: None,
bitswap_config: None,
notification_metrics: NotificationMetrics::new(None),
})
.unwrap();
let notification_service = Arc::new(Mutex::new(notification_service));
let network_service = worker.network_service();
(worker, network_service, rx, notification_service)
}
struct BenchSetup {
#[allow(dead_code)]
notification_service1: Arc<Mutex<Box<dyn NotificationService>>>,
#[allow(dead_code)]
notification_service2: Arc<Mutex<Box<dyn NotificationService>>>,
network_service1: Arc<dyn NetworkService>,
peer_id2: PeerId,
handle1: JoinHandle<()>,
handle2: JoinHandle<()>,
#[allow(dead_code)]
rx1: async_channel::Receiver<IncomingRequest>,
rx2: async_channel::Receiver<IncomingRequest>,
}
impl Drop for BenchSetup {
fn drop(&mut self) {
self.handle1.abort();
self.handle2.abort();
}
}
fn setup_workers<B, H, N>(rt: &tokio::runtime::Runtime) -> Arc<BenchSetup>
where
B: BlockT<Hash = H256> + 'static,
H: ExHashT,
N: NetworkBackend<B, H>,
{
let _guard = rt.enter();
let (worker1, network_service1, rx1, notification_service1) =
create_network_worker::<B, H, N>();
let (worker2, network_service2, rx2, notification_service2) =
create_network_worker::<B, H, N>();
let peer_id2 = worker2.network_service().local_peer_id();
let handle1 = tokio::spawn(worker1.run());
let handle2 = tokio::spawn(worker2.run());
let _ = tokio::spawn({
let rx2 = rx2.clone();
async move {
let req = rx2.recv().await.unwrap();
req.pending_response
.send(OutgoingResponse {
result: Ok(vec![0; 2usize.pow(25)]),
reputation_changes: vec![],
sent_feedback: None,
})
.unwrap();
}
});
let ready = tokio::spawn({
let network_service1 = Arc::clone(&network_service1);
async move {
let listen_address2 = {
while network_service2.listen_addresses().is_empty() {
tokio::time::sleep(Duration::from_millis(10)).await;
}
network_service2.listen_addresses()[0].clone()
};
network_service1.add_known_address(peer_id2, listen_address2.into());
let _ = network_service1
.request(
peer_id2.into(),
"/request-response/1".into(),
vec![0; 2],
None,
IfDisconnected::TryConnect,
)
.await
.unwrap();
}
});
tokio::task::block_in_place(|| {
let _ = tokio::runtime::Handle::current().block_on(ready);
});
Arc::new(BenchSetup {
notification_service1,
notification_service2,
network_service1,
peer_id2,
handle1,
handle2,
rx1,
rx2,
})
}
async fn run_serially(setup: Arc<BenchSetup>, size: usize, limit: usize) {
let (break_tx, break_rx) = async_channel::bounded(1);
let network1 = tokio::spawn({
let network_service1 = Arc::clone(&setup.network_service1);
let peer_id2 = setup.peer_id2;
async move {
for _ in 0..limit {
let _ = network_service1
.request(
peer_id2.into(),
"/request-response/1".into(),
vec![0; 2],
None,
IfDisconnected::TryConnect,
)
.await
.unwrap();
}
let _ = break_tx.send(()).await;
}
});
let network2 = tokio::spawn({
let rx2 = setup.rx2.clone();
async move {
loop {
tokio::select! {
req = rx2.recv() => {
let IncomingRequest { pending_response, .. } = req.unwrap();
pending_response.send(OutgoingResponse {
result: Ok(vec![0; size]),
reputation_changes: vec![],
sent_feedback: None,
}).unwrap();
},
_ = break_rx.recv() => break,
}
}
}
});
let _ = tokio::join!(network1, network2);
}
// The libp2p request-response implementation does not provide any backpressure feedback.
// So this benchmark is useless until we implement it for litep2p.
#[allow(dead_code)]
async fn run_with_backpressure(setup: Arc<BenchSetup>, size: usize, limit: usize) {
let (break_tx, break_rx) = async_channel::bounded(1);
let requests = futures::future::join_all((0..limit).into_iter().map(|_| {
let (tx, rx) = futures::channel::oneshot::channel();
setup.network_service1.start_request(
setup.peer_id2.into(),
"/request-response/1".into(),
vec![0; 8],
None,
tx,
IfDisconnected::TryConnect,
);
rx
}));
let network1 = tokio::spawn(async move {
let responses = requests.await;
for res in responses {
res.unwrap().unwrap();
}
let _ = break_tx.send(()).await;
});
let network2 = tokio::spawn(async move {
for _ in 0..limit {
let IncomingRequest { pending_response, .. } = setup.rx2.recv().await.unwrap();
pending_response
.send(OutgoingResponse {
result: Ok(vec![0; size]),
reputation_changes: vec![],
sent_feedback: None,
})
.unwrap();
}
break_rx.recv().await
});
let _ = tokio::join!(network1, network2);
}
fn run_benchmark(c: &mut Criterion) {
let rt = tokio::runtime::Runtime::new().unwrap();
let plot_config = PlotConfiguration::default().summary_scale(AxisScale::Logarithmic);
let mut group = c.benchmark_group("request_response_protocol");
group.plot_config(plot_config);
group.sample_size(10);
let libp2p_setup = setup_workers::<runtime::Block, runtime::Hash, NetworkWorker<_, _>>(&rt);
for &(exponent, label) in PAYLOAD.iter() {
let size = 2usize.pow(exponent);
group.throughput(Throughput::Bytes(NUMBER_OF_REQUESTS as u64 * size as u64));
group.bench_with_input(BenchmarkId::new("libp2p/serially", label), &size, |b, &size| {
b.to_async(&rt)
.iter(|| run_serially(Arc::clone(&libp2p_setup), size, NUMBER_OF_REQUESTS));
});
}
drop(libp2p_setup);
let litep2p_setup = setup_workers::<runtime::Block, runtime::Hash, Litep2pNetworkBackend>(&rt);
for &(exponent, label) in PAYLOAD.iter() {
let size = 2usize.pow(exponent);
group.throughput(Throughput::Bytes(NUMBER_OF_REQUESTS as u64 * size as u64));
group.bench_with_input(BenchmarkId::new("litep2p/serially", label), &size, |b, &size| {
b.to_async(&rt)
.iter(|| run_serially(Arc::clone(&litep2p_setup), size, NUMBER_OF_REQUESTS));
});
}
drop(litep2p_setup);
}
criterion_group!(benches, run_benchmark);
criterion_main!(benches);
+23
View File
@@ -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/>.
const PROTOS: &[&str] = &["src/schema/bitswap.v1.2.0.proto"];
fn main() {
prost_build::compile_protos(PROTOS, &["src/schema"]).unwrap();
}
@@ -0,0 +1,24 @@
[package]
description = "Bizinikiwi network common"
name = "pezsc-network-common"
version = "0.33.0"
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
authors.workspace = true
edition.workspace = true
homepage.workspace = true
repository.workspace = true
documentation = "https://docs.rs/pezsc-network-sync"
[lints]
workspace = true
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
bitflags = { workspace = true }
codec = { features = ["derive"], workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
[features]
runtime-benchmarks = ["pezsp-runtime/runtime-benchmarks"]
@@ -0,0 +1,30 @@
// 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 data structures of the networking layer.
pub mod message;
pub mod role;
pub mod sync;
pub mod types;
/// Minimum Requirements for a Hash within Networking
pub trait ExHashT: std::hash::Hash + Eq + std::fmt::Debug + Clone + Send + Sync + 'static {}
impl<T> ExHashT for T where T: std::hash::Hash + Eq + std::fmt::Debug + Clone + Send + Sync + 'static
{}
@@ -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/>.
//! Network packet message types. These get serialized and put into the lower level protocol
//! payload.
/// A unique ID of a request.
pub type RequestId = u64;
@@ -0,0 +1,137 @@
// 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/>.
// file-level lint whitelist to avoid problem with bitflags macro below
// TODO: can be dropped after an update to bitflags 2.4
#![allow(clippy::bad_bit_mask)]
use codec::{self, Encode, EncodeLike, Input, Output};
/// Role that the peer sent to us during the handshake, with the addition of what our local node
/// knows about that peer.
///
/// > **Note**: This enum is different from the `Role` enum. The `Role` enum indicates what a
/// > node says about itself, while `ObservedRole` is a `Role` merged with the
/// > information known locally about that node.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum ObservedRole {
/// Full node.
Full,
/// Light node.
Light,
/// Third-party authority.
Authority,
}
impl ObservedRole {
/// Returns `true` for `ObservedRole::Light`.
pub fn is_light(&self) -> bool {
matches!(self, Self::Light)
}
}
impl From<Roles> for ObservedRole {
fn from(roles: Roles) -> Self {
if roles.is_authority() {
ObservedRole::Authority
} else if roles.is_full() {
ObservedRole::Full
} else {
ObservedRole::Light
}
}
}
/// Role of the local node.
#[derive(Debug, Clone, Copy)]
pub enum Role {
/// Regular full node.
Full,
/// Actual authority.
Authority,
}
impl Role {
/// True for [`Role::Authority`].
pub fn is_authority(&self) -> bool {
matches!(self, Self::Authority)
}
}
impl std::fmt::Display for Role {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Full => write!(f, "FULL"),
Self::Authority => write!(f, "AUTHORITY"),
}
}
}
bitflags::bitflags! {
/// Bitmask of the roles that a node fulfills.
pub struct Roles: u8 {
/// No network.
const NONE = 0b00000000;
/// Full node, does not participate in consensus.
const FULL = 0b00000001;
/// Light client node.
const LIGHT = 0b00000010;
/// Act as an authority
const AUTHORITY = 0b00000100;
}
}
impl Roles {
/// Does this role represents a client that holds full chain data locally?
pub fn is_full(&self) -> bool {
self.intersects(Self::FULL | Self::AUTHORITY)
}
/// Does this role represents a client that does not participates in the consensus?
pub fn is_authority(&self) -> bool {
*self == Self::AUTHORITY
}
/// Does this role represents a client that does not hold full chain data locally?
pub fn is_light(&self) -> bool {
!self.is_full()
}
}
impl<'a> From<&'a Role> for Roles {
fn from(roles: &'a Role) -> Self {
match roles {
Role::Full => Self::FULL,
Role::Authority => Self::AUTHORITY,
}
}
}
impl Encode for Roles {
fn encode_to<T: Output + ?Sized>(&self, dest: &mut T) {
dest.push_byte(self.bits())
}
}
impl EncodeLike for Roles {}
impl codec::Decode for Roles {
fn decode<I: Input>(input: &mut I) -> Result<Self, codec::Error> {
Self::from_bits(input.read_byte()?).ok_or_else(|| codec::Error::from("Invalid bytes"))
}
}
@@ -0,0 +1,55 @@
// 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/>.
//! Abstract interfaces and data structures related to network sync.
pub mod message;
/// Sync operation mode.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum SyncMode {
/// Full block download and verification.
Full,
/// Download blocks and the latest state.
LightState {
/// Skip state proof download and verification.
skip_proofs: bool,
/// Download indexed transactions for recent blocks.
storage_chain_mode: bool,
},
/// Warp sync - verify authority set transitions and the latest state.
Warp,
}
impl SyncMode {
/// Returns `true` if `self` is [`Self::Warp`].
pub fn is_warp(&self) -> bool {
matches!(self, Self::Warp)
}
/// Returns `true` if `self` is [`Self::LightState`].
pub fn light_state(&self) -> bool {
matches!(self, Self::LightState { .. })
}
}
impl Default for SyncMode {
fn default() -> Self {
Self::Full
}
}
@@ -0,0 +1,246 @@
// 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/>.
//! Network packet message types. These get serialized and put into the lower level protocol
//! payload.
use crate::role::Roles;
use bitflags::bitflags;
use codec::{Decode, Encode, Error, Input, Output};
pub use generic::{BlockAnnounce, FromBlock};
use pezsp_runtime::traits::{Block as BlockT, Header as HeaderT, NumberFor};
/// Type alias for using the block request type using block type parameters.
pub type BlockRequest<B> =
generic::BlockRequest<<B as BlockT>::Hash, <<B as BlockT>::Header as HeaderT>::Number>;
/// Type alias for using the BlockData type using block type parameters.
pub type BlockData<B> =
generic::BlockData<<B as BlockT>::Header, <B as BlockT>::Hash, <B as BlockT>::Extrinsic>;
/// Type alias for using the BlockResponse type using block type parameters.
pub type BlockResponse<B> =
generic::BlockResponse<<B as BlockT>::Header, <B as BlockT>::Hash, <B as BlockT>::Extrinsic>;
// Bits of block data and associated artifacts to request.
bitflags! {
/// Node roles bitmask.
pub struct BlockAttributes: u8 {
/// Include block header.
const HEADER = 0b00000001;
/// Include block body.
const BODY = 0b00000010;
/// Include block receipt.
const RECEIPT = 0b00000100;
/// Include block message queue.
const MESSAGE_QUEUE = 0b00001000;
/// Include a justification for the block.
const JUSTIFICATION = 0b00010000;
/// Include indexed transactions for a block.
const INDEXED_BODY = 0b00100000;
}
}
impl BlockAttributes {
/// Encodes attributes as big endian u32, compatible with SCALE-encoding (i.e the
/// significant byte has zero index).
pub fn to_be_u32(&self) -> u32 {
u32::from_be_bytes([self.bits(), 0, 0, 0])
}
/// Decodes attributes, encoded with the `encode_to_be_u32()` call.
pub fn from_be_u32(encoded: u32) -> Result<Self, Error> {
Self::from_bits(encoded.to_be_bytes()[0])
.ok_or_else(|| Error::from("Invalid BlockAttributes"))
}
}
impl Encode for BlockAttributes {
fn encode_to<T: Output + ?Sized>(&self, dest: &mut T) {
dest.push_byte(self.bits())
}
}
impl codec::EncodeLike for BlockAttributes {}
impl Decode for BlockAttributes {
fn decode<I: Input>(input: &mut I) -> Result<Self, Error> {
Self::from_bits(input.read_byte()?).ok_or_else(|| Error::from("Invalid bytes"))
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Encode, Decode)]
/// Block enumeration direction.
pub enum Direction {
/// Enumerate in ascending order (from child to parent).
Ascending = 0,
/// Enumerate in descending order (from parent to canonical child).
Descending = 1,
}
/// Block state in the chain.
#[derive(Debug, PartialEq, Eq, Clone, Copy, Encode, Decode)]
pub enum BlockState {
/// Block is not part of the best chain.
Normal,
/// Latest best block.
Best,
}
/// Announcement summary used for debug logging.
#[derive(Debug)]
pub struct AnnouncementSummary<H: HeaderT> {
pub block_hash: H::Hash,
pub number: H::Number,
pub parent_hash: H::Hash,
pub state: Option<BlockState>,
}
impl<H: HeaderT> BlockAnnounce<H> {
pub fn summary(&self) -> AnnouncementSummary<H> {
AnnouncementSummary {
block_hash: self.header.hash(),
number: *self.header.number(),
parent_hash: *self.header.parent_hash(),
state: self.state,
}
}
}
/// Generic types.
pub mod generic {
use super::{BlockAttributes, BlockState, Direction};
use crate::message::RequestId;
use codec::{Decode, Encode, Input, Output};
use pezsp_runtime::{EncodedJustification, Justifications};
/// Block data sent in the response.
#[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
pub struct BlockData<Header, Hash, Extrinsic> {
/// Block header hash.
pub hash: Hash,
/// Block header if requested.
pub header: Option<Header>,
/// Block body if requested.
pub body: Option<Vec<Extrinsic>>,
/// Block body indexed transactions if requested.
pub indexed_body: Option<Vec<Vec<u8>>>,
/// Block receipt if requested.
pub receipt: Option<Vec<u8>>,
/// Block message queue if requested.
pub message_queue: Option<Vec<u8>>,
/// Justification if requested.
pub justification: Option<EncodedJustification>,
/// Justifications if requested.
pub justifications: Option<Justifications>,
}
/// Request block data from a peer.
#[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
pub struct BlockRequest<Hash, Number> {
/// Unique request id.
pub id: RequestId,
/// Bits of block data to request.
pub fields: BlockAttributes,
/// Start from this block.
pub from: FromBlock<Hash, Number>,
/// Sequence direction.
pub direction: Direction,
/// Maximum number of blocks to return. An implementation defined maximum is used when
/// unspecified.
pub max: Option<u32>,
}
/// Identifies starting point of a block sequence.
#[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
pub enum FromBlock<Hash, Number> {
/// Start with given hash.
Hash(Hash),
/// Start with given block number.
Number(Number),
}
/// Response to `BlockRequest`
#[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
pub struct BlockResponse<Header, Hash, Extrinsic> {
/// Id of a request this response was made for.
pub id: RequestId,
/// Block data for the requested sequence.
pub blocks: Vec<BlockData<Header, Hash, Extrinsic>>,
}
/// Announce a new complete block on the network.
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct BlockAnnounce<H> {
/// New block header.
pub header: H,
/// Block state. TODO: Remove `Option` and custom encoding when v4 becomes common.
pub state: Option<BlockState>,
/// Data associated with this block announcement, e.g. a candidate message.
pub data: Option<Vec<u8>>,
}
// Custom Encode/Decode impl to maintain backwards compatibility with v3.
// This assumes that the packet contains nothing but the announcement message.
// TODO: Get rid of it once protocol v4 is common.
impl<H: Encode> Encode for BlockAnnounce<H> {
fn encode_to<T: Output + ?Sized>(&self, dest: &mut T) {
self.header.encode_to(dest);
if let Some(state) = &self.state {
state.encode_to(dest);
}
if let Some(data) = &self.data {
data.encode_to(dest)
}
}
}
impl<H: Decode> Decode for BlockAnnounce<H> {
fn decode<I: Input>(input: &mut I) -> Result<Self, codec::Error> {
let header = H::decode(input)?;
let state = BlockState::decode(input).ok();
let data = Vec::decode(input).ok();
Ok(Self { header, state, data })
}
}
}
/// Handshake sent when we open a block announces substream.
#[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
pub struct BlockAnnouncesHandshake<B: BlockT> {
/// Roles of the node.
pub roles: Roles,
/// Best block number.
pub best_number: NumberFor<B>,
/// Best block hash.
pub best_hash: B::Hash,
/// Genesis block hash.
pub genesis_hash: B::Hash,
}
impl<B: BlockT> BlockAnnouncesHandshake<B> {
pub fn build(
roles: Roles,
best_number: NumberFor<B>,
best_hash: B::Hash,
genesis_hash: B::Hash,
) -> Self {
Self { genesis_hash, roles, best_number, best_hash }
}
}
@@ -0,0 +1,38 @@
// 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/>.
/// Description of a reputation adjustment for a node.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ReputationChange {
/// Reputation delta.
pub value: i32,
/// Reason for reputation change.
pub reason: &'static str,
}
impl ReputationChange {
/// New reputation change with given delta and reason.
pub const fn new(value: i32, reason: &'static str) -> ReputationChange {
Self { value, reason }
}
/// New reputation change that forces minimum possible reputation.
pub const fn new_fatal(reason: &'static str) -> ReputationChange {
Self { value: i32::MIN, reason }
}
}
@@ -0,0 +1,42 @@
[package]
description = "Bizinikiwi light network protocol"
name = "pezsc-network-light"
version = "0.33.0"
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
authors.workspace = true
edition.workspace = true
homepage.workspace = true
repository.workspace = true
documentation = "https://docs.rs/pezsc-network-light"
[lints]
workspace = true
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
array-bytes = { workspace = true, default-features = true }
async-channel = { workspace = true }
codec = { features = ["derive"], workspace = true, default-features = true }
futures = { workspace = true }
log = { workspace = true, default-features = true }
prost = { workspace = true }
pezsc-client-api = { workspace = true, default-features = true }
pezsc-network = { workspace = true, default-features = true }
pezsc-network-types = { workspace = true, default-features = true }
pezsp-blockchain = { workspace = true, default-features = true }
pezsp-core = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
thiserror = { workspace = true }
[build-dependencies]
prost-build = { workspace = true }
[features]
runtime-benchmarks = [
"pezsc-client-api/runtime-benchmarks",
"pezsc-network/runtime-benchmarks",
"pezsp-blockchain/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
]
+23
View File
@@ -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/>.
const PROTOS: &[&str] = &["src/schema/light.v1.proto"];
fn main() {
prost_build::compile_protos(PROTOS, &["src/schema"]).unwrap();
}
@@ -0,0 +1,22 @@
// 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/>.
//! Light client data structures of the networking layer.
pub mod light_client_requests;
mod schema;
@@ -0,0 +1,66 @@
// 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/>.
//! Helpers for outgoing and incoming light client requests.
use pezsc_network::{
config::ProtocolId, request_responses::IncomingRequest, NetworkBackend, MAX_RESPONSE_SIZE,
};
use pezsp_runtime::traits::Block;
use std::time::Duration;
/// For incoming light client requests.
pub mod handler;
/// Generate the light client 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!("/{}/{}/light/2", array_bytes::bytes2hex("", genesis_hash), fork_id)
} else {
format!("/{}/light/2", array_bytes::bytes2hex("", genesis_hash))
}
}
/// Generate the legacy light client protocol name from chain specific protocol identifier.
fn generate_legacy_protocol_name(protocol_id: &ProtocolId) -> String {
format!("/{}/light/2", protocol_id.as_ref())
}
/// Generates a `RequestResponseProtocolConfig` for the light client request protocol, refusing
/// incoming requests.
pub fn generate_protocol_config<
Hash: AsRef<[u8]>,
B: Block,
N: NetworkBackend<B, <B as Block>::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(),
1 * 1024 * 1024,
MAX_RESPONSE_SIZE,
Duration::from_secs(15),
Some(inbound_queue),
)
}
@@ -0,0 +1,313 @@
// 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/>.
//! Helper for incoming light client requests.
//!
//! Handle (i.e. answer) incoming light client requests from a remote peer received via
//! `crate::request_responses::RequestResponsesBehaviour` with
//! [`LightClientRequestHandler`](handler::LightClientRequestHandler).
use crate::schema;
use codec::{self, Decode, Encode};
use futures::prelude::*;
use log::{debug, trace};
use prost::Message;
use pezsc_client_api::{BlockBackend, ProofProvider};
use pezsc_network::{
config::ProtocolId,
request_responses::{IncomingRequest, OutgoingResponse},
NetworkBackend, ReputationChange,
};
use pezsc_network_types::PeerId;
use pezsp_core::{
hexdisplay::HexDisplay,
storage::{ChildInfo, ChildType, PrefixedStorageKey},
};
use pezsp_runtime::traits::Block;
use std::{marker::PhantomData, sync::Arc};
const LOG_TARGET: &str = "light-client-request-handler";
/// Incoming requests bounded queue size. For now due to lack of data on light client request
/// handling in production systems, this value is chosen to match the block request limit.
const MAX_LIGHT_REQUEST_QUEUE: usize = 20;
/// Handler for incoming light client requests from a remote peer.
pub struct LightClientRequestHandler<B, Client> {
request_receiver: async_channel::Receiver<IncomingRequest>,
/// Blockchain client.
client: Arc<Client>,
_block: PhantomData<B>,
}
impl<B, Client> LightClientRequestHandler<B, Client>
where
B: Block,
Client: BlockBackend<B> + ProofProvider<B> + Send + Sync + 'static,
{
/// Create a new [`LightClientRequestHandler`].
pub fn new<N: NetworkBackend<B, <B as Block>::Hash>>(
protocol_id: &ProtocolId,
fork_id: Option<&str>,
client: Arc<Client>,
) -> (Self, N::RequestResponseProtocolConfig) {
let (tx, request_receiver) = async_channel::bounded(MAX_LIGHT_REQUEST_QUEUE);
let protocol_config = super::generate_protocol_config::<_, B, N>(
protocol_id,
client
.block_hash(0u32.into())
.ok()
.flatten()
.expect("Genesis block exists; qed"),
fork_id,
tx,
);
(Self { client, request_receiver, _block: PhantomData::default() }, protocol_config)
}
/// Run [`LightClientRequestHandler`].
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(peer, payload) {
Ok(response_data) => {
let response = OutgoingResponse {
result: Ok(response_data),
reputation_changes: Vec::new(),
sent_feedback: None,
};
match pending_response.send(response) {
Ok(()) => trace!(
target: LOG_TARGET,
"Handled light client request from {}.",
peer,
),
Err(_) => debug!(
target: LOG_TARGET,
"Failed to handle light client request from {}: {}",
peer,
HandleRequestError::SendResponse,
),
};
},
Err(e) => {
debug!(
target: LOG_TARGET,
"Failed to handle light client request from {}: {}", peer, e,
);
let reputation_changes = match e {
HandleRequestError::BadRequest(_) => {
vec![ReputationChange::new(-(1 << 12), "bad request")]
},
_ => Vec::new(),
};
let response = OutgoingResponse {
result: Err(()),
reputation_changes,
sent_feedback: None,
};
if pending_response.send(response).is_err() {
debug!(
target: LOG_TARGET,
"Failed to handle light client request from {}: {}",
peer,
HandleRequestError::SendResponse,
);
};
},
}
}
}
fn handle_request(
&mut self,
peer: PeerId,
payload: Vec<u8>,
) -> Result<Vec<u8>, HandleRequestError> {
let request = schema::v1::light::Request::decode(&payload[..])?;
let response = match &request.request {
Some(schema::v1::light::request::Request::RemoteCallRequest(r)) =>
self.on_remote_call_request(&peer, r)?,
Some(schema::v1::light::request::Request::RemoteReadRequest(r)) =>
self.on_remote_read_request(&peer, r)?,
Some(schema::v1::light::request::Request::RemoteReadChildRequest(r)) =>
self.on_remote_read_child_request(&peer, r)?,
None =>
return Err(HandleRequestError::BadRequest("Remote request without request data.")),
};
let mut data = Vec::new();
response.encode(&mut data)?;
Ok(data)
}
fn on_remote_call_request(
&mut self,
peer: &PeerId,
request: &schema::v1::light::RemoteCallRequest,
) -> Result<schema::v1::light::Response, HandleRequestError> {
trace!("Remote call request from {} ({} at {:?}).", peer, request.method, request.block,);
let block = Decode::decode(&mut request.block.as_ref())?;
let response = match self.client.execution_proof(block, &request.method, &request.data) {
Ok((_, proof)) => schema::v1::light::RemoteCallResponse { proof: Some(proof.encode()) },
Err(e) => {
trace!(
"remote call request from {} ({} at {:?}) failed with: {}",
peer,
request.method,
request.block,
e,
);
schema::v1::light::RemoteCallResponse { proof: None }
},
};
Ok(schema::v1::light::Response {
response: Some(schema::v1::light::response::Response::RemoteCallResponse(response)),
})
}
fn on_remote_read_request(
&mut self,
peer: &PeerId,
request: &schema::v1::light::RemoteReadRequest,
) -> Result<schema::v1::light::Response, HandleRequestError> {
if request.keys.is_empty() {
debug!("Invalid remote read request sent by {}.", peer);
return Err(HandleRequestError::BadRequest("Remote read request without keys."));
}
trace!(
"Remote read request from {} ({} at {:?}).",
peer,
fmt_keys(request.keys.first(), request.keys.last()),
request.block,
);
let block = Decode::decode(&mut request.block.as_ref())?;
let response =
match self.client.read_proof(block, &mut request.keys.iter().map(AsRef::as_ref)) {
Ok(proof) => schema::v1::light::RemoteReadResponse { proof: Some(proof.encode()) },
Err(error) => {
trace!(
"remote read request from {} ({} at {:?}) failed with: {}",
peer,
fmt_keys(request.keys.first(), request.keys.last()),
request.block,
error,
);
schema::v1::light::RemoteReadResponse { proof: None }
},
};
Ok(schema::v1::light::Response {
response: Some(schema::v1::light::response::Response::RemoteReadResponse(response)),
})
}
fn on_remote_read_child_request(
&mut self,
peer: &PeerId,
request: &schema::v1::light::RemoteReadChildRequest,
) -> Result<schema::v1::light::Response, HandleRequestError> {
if request.keys.is_empty() {
debug!("Invalid remote child read request sent by {}.", peer);
return Err(HandleRequestError::BadRequest("Remove read child request without keys."));
}
trace!(
"Remote read child request from {} ({} {} at {:?}).",
peer,
HexDisplay::from(&request.storage_key),
fmt_keys(request.keys.first(), request.keys.last()),
request.block,
);
let block = Decode::decode(&mut request.block.as_ref())?;
let prefixed_key = PrefixedStorageKey::new_ref(&request.storage_key);
let child_info = match ChildType::from_prefixed_key(prefixed_key) {
Some((ChildType::ParentKeyId, storage_key)) => Ok(ChildInfo::new_default(storage_key)),
None => Err(pezsp_blockchain::Error::InvalidChildStorageKey),
};
let response = match child_info.and_then(|child_info| {
self.client.read_child_proof(
block,
&child_info,
&mut request.keys.iter().map(AsRef::as_ref),
)
}) {
Ok(proof) => schema::v1::light::RemoteReadResponse { proof: Some(proof.encode()) },
Err(error) => {
trace!(
"remote read child request from {} ({} {} at {:?}) failed with: {}",
peer,
HexDisplay::from(&request.storage_key),
fmt_keys(request.keys.first(), request.keys.last()),
request.block,
error,
);
schema::v1::light::RemoteReadResponse { proof: None }
},
};
Ok(schema::v1::light::Response {
response: Some(schema::v1::light::response::Response::RemoteReadResponse(response)),
})
}
}
#[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 send response.")]
SendResponse,
/// A bad request has been received.
#[error("bad request: {0}")]
BadRequest(&'static str),
/// Encoding or decoding of some data failed.
#[error("codec error: {0}")]
Codec(#[from] codec::Error),
}
fn fmt_keys(first: Option<&Vec<u8>>, last: Option<&Vec<u8>>) -> String {
if let (Some(first), Some(last)) = (first, last) {
if first == last {
HexDisplay::from(first).to_string()
} else {
format!("{}..{}", HexDisplay::from(first), HexDisplay::from(last))
}
} else {
String::from("n/a")
}
}
@@ -0,0 +1,69 @@
// 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 {
pub(crate) mod light {
include!(concat!(env!("OUT_DIR"), "/api.v1.light.rs"));
}
}
#[cfg(test)]
mod tests {
use prost::Message as _;
#[test]
fn empty_proof_encodes_correctly() {
let encoded = super::v1::light::Response {
response: Some(super::v1::light::response::Response::RemoteReadResponse(
super::v1::light::RemoteReadResponse { proof: Some(Vec::new()) },
)),
}
.encode_to_vec();
// Make sure that the response contains one field of number 2 and wire type 2 (message),
// then another field of number 2 and wire type 2 (bytes), then a length of 0.
assert_eq!(encoded, vec![(2 << 3) | 2, 2, (2 << 3) | 2, 0]);
}
#[test]
fn no_proof_encodes_correctly() {
let encoded = super::v1::light::Response {
response: Some(super::v1::light::response::Response::RemoteReadResponse(
super::v1::light::RemoteReadResponse { proof: None },
)),
}
.encode_to_vec();
// Make sure that the response contains one field of number 2 and wire type 2 (message).
assert_eq!(encoded, vec![(2 << 3) | 2, 0]);
}
#[test]
fn proof_encodes_correctly() {
let encoded = super::v1::light::Response {
response: Some(super::v1::light::response::Response::RemoteReadResponse(
super::v1::light::RemoteReadResponse { proof: Some(vec![1, 2, 3, 4]) },
)),
}
.encode_to_vec();
assert_eq!(encoded, vec![(2 << 3) | 2, 6, (2 << 3) | 2, 4, 1, 2, 3, 4]);
}
}
@@ -0,0 +1,67 @@
// Schema definition for light client messages.
syntax = "proto2";
package api.v1.light;
// Enumerate all possible light client request messages.
message Request {
oneof request {
RemoteCallRequest remote_call_request = 1;
RemoteReadRequest remote_read_request = 2;
RemoteReadChildRequest remote_read_child_request = 4;
// Note: ids 3 and 5 were used in the past. It would be preferable to not re-use them.
}
}
// Enumerate all possible light client response messages.
message Response {
oneof response {
RemoteCallResponse remote_call_response = 1;
RemoteReadResponse remote_read_response = 2;
// Note: ids 3 and 4 were used in the past. It would be preferable to not re-use them.
}
}
// Remote call request.
message RemoteCallRequest {
// Block at which to perform call.
required bytes block = 2;
// Method name.
required string method = 3;
// Call data.
required bytes data = 4;
}
// Remote call response.
message RemoteCallResponse {
// Execution proof. If missing, indicates that the remote couldn't answer, for example because
// the block is pruned.
optional bytes proof = 2;
}
// Remote storage read request.
message RemoteReadRequest {
// Block at which to perform call.
required bytes block = 2;
// Storage keys.
repeated bytes keys = 3;
}
// Remote read response.
message RemoteReadResponse {
// Read proof. If missing, indicates that the remote couldn't answer, for example because
// the block is pruned.
optional bytes proof = 2;
}
// Remote storage read child request.
message RemoteReadChildRequest {
// Block at which to perform call.
required bytes block = 2;
// Child Storage key, this is relative
// to the child type storage location.
required bytes storage_key = 3;
// Storage keys.
repeated bytes keys = 6;
}
+451
View File
@@ -0,0 +1,451 @@
// 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::{
discovery::{DiscoveryBehaviour, DiscoveryConfig, DiscoveryOut},
event::DhtEvent,
peer_info,
peer_store::PeerStoreProvider,
protocol::{CustomMessageOutcome, NotificationsSink, Protocol},
protocol_controller::SetId,
request_responses::{self, IfDisconnected, ProtocolConfig, RequestFailure},
service::traits::Direction,
types::ProtocolName,
ReputationChange,
};
use futures::channel::oneshot;
use libp2p::{
connection_limits::ConnectionLimits,
core::Multiaddr,
identify::Info as IdentifyInfo,
identity::PublicKey,
kad::{Record, RecordKey},
swarm::NetworkBehaviour,
PeerId, StreamProtocol,
};
use parking_lot::Mutex;
use pezsp_runtime::traits::Block as BlockT;
use std::{
collections::HashSet,
sync::Arc,
time::{Duration, Instant},
};
pub use crate::request_responses::{InboundFailure, OutboundFailure, ResponseFailure};
/// General behaviour of the network. Combines all protocols together.
#[derive(NetworkBehaviour)]
#[behaviour(to_swarm = "BehaviourOut")]
pub struct Behaviour<B: BlockT> {
/// Connection limits.
connection_limits: libp2p::connection_limits::Behaviour,
/// All the bizinikiwi-specific protocols.
bizinikiwi: Protocol<B>,
/// Periodically pings and identifies the nodes we are connected to, and store information in a
/// cache.
peer_info: peer_info::PeerInfoBehaviour,
/// Discovers nodes of the network.
discovery: DiscoveryBehaviour,
/// Generic request-response protocols.
request_responses: request_responses::RequestResponsesBehaviour,
}
/// Event generated by `Behaviour`.
#[derive(Debug)]
pub enum BehaviourOut {
/// Started a random iterative Kademlia discovery query.
RandomKademliaStarted,
/// We have received a request from a peer and answered it.
///
/// This event is generated for statistics purposes.
InboundRequest {
/// Protocol name of the request.
protocol: ProtocolName,
/// If `Ok`, contains the time elapsed between when we received the request and when we
/// sent back the response. If `Err`, the error that happened.
result: Result<Duration, ResponseFailure>,
},
/// A request has succeeded or failed.
///
/// This event is generated for statistics purposes.
RequestFinished {
/// Name of the protocol in question.
protocol: ProtocolName,
/// Duration the request took.
duration: Duration,
/// Result of the request.
result: Result<(), RequestFailure>,
},
/// A request protocol handler issued reputation changes for the given peer.
ReputationChanges { peer: PeerId, changes: Vec<ReputationChange> },
/// Opened a substream with the given node with the given notifications protocol.
///
/// The protocol is always one of the notification protocols that have been registered.
NotificationStreamOpened {
/// Node we opened the substream with.
remote: PeerId,
/// Set ID.
set_id: SetId,
/// Direction of the stream.
direction: Direction,
/// If the negotiation didn't use the main name of the protocol (the one in
/// `notifications_protocol`), then this field contains which name has actually been
/// used.
/// See also [`crate::Event::NotificationStreamOpened`].
negotiated_fallback: Option<ProtocolName>,
/// Object that permits sending notifications to the peer.
notifications_sink: NotificationsSink,
/// Received handshake.
received_handshake: Vec<u8>,
},
/// The [`NotificationsSink`] object used to send notifications with the given peer must be
/// replaced with a new one.
///
/// This event is typically emitted when a transport-level connection is closed and we fall
/// back to a secondary connection.
NotificationStreamReplaced {
/// Id of the peer we are connected to.
remote: PeerId,
/// Set ID.
set_id: SetId,
/// Replacement for the previous [`NotificationsSink`].
notifications_sink: NotificationsSink,
},
/// Closed a substream with the given node. Always matches a corresponding previous
/// `NotificationStreamOpened` message.
NotificationStreamClosed {
/// Node we closed the substream with.
remote: PeerId,
/// Set ID.
set_id: SetId,
},
/// Received one or more messages from the given node using the given protocol.
NotificationsReceived {
/// Node we received the message from.
remote: PeerId,
/// Set ID.
set_id: SetId,
/// Concerned protocol and associated message.
notification: Vec<u8>,
},
/// We have obtained identity information from a peer, including the addresses it is listening
/// on.
PeerIdentify {
/// Id of the peer that has been identified.
peer_id: PeerId,
/// Information about the peer.
info: IdentifyInfo,
},
/// We have learned about the existence of a node on the default set.
Discovered(PeerId),
/// Events generated by a DHT as a response to get_value or put_value requests with the
/// request duration. Or events generated by the DHT as a consequnce of receiving a record
/// to store from peers.
Dht(DhtEvent, Option<Duration>),
/// Ignored event generated by lower layers.
None,
}
impl<B: BlockT> Behaviour<B> {
/// Builds a new `Behaviour`.
pub fn new(
bizinikiwi: Protocol<B>,
user_agent: String,
local_public_key: PublicKey,
disco_config: DiscoveryConfig,
request_response_protocols: Vec<ProtocolConfig>,
peer_store_handle: Arc<dyn PeerStoreProvider>,
external_addresses: Arc<Mutex<HashSet<Multiaddr>>>,
public_addresses: Vec<Multiaddr>,
connection_limits: ConnectionLimits,
) -> Result<Self, request_responses::RegisterError> {
Ok(Self {
bizinikiwi,
peer_info: peer_info::PeerInfoBehaviour::new(
user_agent,
local_public_key,
external_addresses,
public_addresses,
),
discovery: disco_config.finish(),
request_responses: request_responses::RequestResponsesBehaviour::new(
request_response_protocols.into_iter(),
peer_store_handle,
)?,
connection_limits: libp2p::connection_limits::Behaviour::new(connection_limits),
})
}
/// Returns the list of nodes that we know exist in the network.
pub fn known_peers(&mut self) -> HashSet<PeerId> {
self.discovery.known_peers()
}
/// Adds a hard-coded address for the given peer, that never expires.
pub fn add_known_address(&mut self, peer_id: PeerId, addr: Multiaddr) {
self.discovery.add_known_address(peer_id, addr)
}
/// Returns the number of nodes in each Kademlia kbucket.
///
/// Identifies kbuckets by the base 2 logarithm of their lower bound.
pub fn num_entries_per_kbucket(&mut self) -> Option<Vec<(u32, usize)>> {
self.discovery.num_entries_per_kbucket()
}
/// Returns the number of records in the Kademlia record stores.
pub fn num_kademlia_records(&mut self) -> Option<usize> {
self.discovery.num_kademlia_records()
}
/// Returns the total size in bytes of all the records in the Kademlia record stores.
pub fn kademlia_records_total_size(&mut self) -> Option<usize> {
self.discovery.kademlia_records_total_size()
}
/// Borrows `self` and returns a struct giving access to the information about a node.
///
/// Returns `None` if we don't know anything about this node. Always returns `Some` for nodes
/// we're connected to, meaning that if `None` is returned then we're not connected to that
/// node.
pub fn node(&self, peer_id: &PeerId) -> Option<peer_info::Node<'_>> {
self.peer_info.node(peer_id)
}
/// Initiates sending a request.
pub fn send_request(
&mut self,
target: &PeerId,
protocol: ProtocolName,
request: Vec<u8>,
fallback_request: Option<(Vec<u8>, ProtocolName)>,
pending_response: oneshot::Sender<Result<(Vec<u8>, ProtocolName), RequestFailure>>,
connect: IfDisconnected,
) {
self.request_responses.send_request(
target,
protocol,
request,
fallback_request,
pending_response,
connect,
)
}
/// Returns a shared reference to the user protocol.
pub fn user_protocol(&self) -> &Protocol<B> {
&self.bizinikiwi
}
/// Returns a mutable reference to the user protocol.
pub fn user_protocol_mut(&mut self) -> &mut Protocol<B> {
&mut self.bizinikiwi
}
/// Add a self-reported address of a remote peer to the k-buckets of the supported
/// DHTs (`supported_protocols`).
pub fn add_self_reported_address_to_dht(
&mut self,
peer_id: &PeerId,
supported_protocols: &[StreamProtocol],
addr: Multiaddr,
) {
self.discovery.add_self_reported_address(peer_id, supported_protocols, addr);
}
/// Start finding closest peerst to the target `PeerId`. Will later produce either a
/// `ClosestPeersFound` or `ClosestPeersNotFound` event.
pub fn find_closest_peers(&mut self, target: PeerId) {
self.discovery.find_closest_peers(target);
}
/// Start querying a record from the DHT. Will later produce either a `ValueFound` or a
/// `ValueNotFound` event.
pub fn get_value(&mut self, key: RecordKey) {
self.discovery.get_value(key);
}
/// Starts putting a record into DHT. Will later produce either a `ValuePut` or a
/// `ValuePutFailed` event.
pub fn put_value(&mut self, key: RecordKey, value: Vec<u8>) {
self.discovery.put_value(key, value);
}
/// Puts a record into DHT, on the provided Peers
pub fn put_record_to(
&mut self,
record: Record,
peers: HashSet<pezsc_network_types::PeerId>,
update_local_storage: bool,
) {
self.discovery.put_record_to(record, peers, update_local_storage);
}
/// Stores value in DHT
pub fn store_record(
&mut self,
record_key: RecordKey,
record_value: Vec<u8>,
publisher: Option<PeerId>,
expires: Option<Instant>,
) {
self.discovery.store_record(record_key, record_value, publisher, expires);
}
/// Start providing `key` on the DHT.
pub fn start_providing(&mut self, key: RecordKey) {
self.discovery.start_providing(key)
}
/// Stop providing `key` on the DHT.
pub fn stop_providing(&mut self, key: &RecordKey) {
self.discovery.stop_providing(key)
}
/// Start searching for providers on the DHT. Will later produce either a `ProvidersFound`
/// or `ProvidersNotFound` event.
pub fn get_providers(&mut self, key: RecordKey) {
self.discovery.get_providers(key)
}
}
impl From<CustomMessageOutcome> for BehaviourOut {
fn from(event: CustomMessageOutcome) -> Self {
match event {
CustomMessageOutcome::NotificationStreamOpened {
remote,
set_id,
direction,
negotiated_fallback,
received_handshake,
notifications_sink,
} => BehaviourOut::NotificationStreamOpened {
remote,
set_id,
direction,
negotiated_fallback,
received_handshake,
notifications_sink,
},
CustomMessageOutcome::NotificationStreamReplaced {
remote,
set_id,
notifications_sink,
} => BehaviourOut::NotificationStreamReplaced { remote, set_id, notifications_sink },
CustomMessageOutcome::NotificationStreamClosed { remote, set_id } =>
BehaviourOut::NotificationStreamClosed { remote, set_id },
CustomMessageOutcome::NotificationsReceived { remote, set_id, notification } =>
BehaviourOut::NotificationsReceived { remote, set_id, notification },
}
}
}
impl From<request_responses::Event> for BehaviourOut {
fn from(event: request_responses::Event) -> Self {
match event {
request_responses::Event::InboundRequest { protocol, result, .. } =>
BehaviourOut::InboundRequest { protocol, result },
request_responses::Event::RequestFinished { protocol, duration, result, .. } =>
BehaviourOut::RequestFinished { protocol, duration, result },
request_responses::Event::ReputationChanges { peer, changes } =>
BehaviourOut::ReputationChanges { peer, changes },
}
}
}
impl From<peer_info::PeerInfoEvent> for BehaviourOut {
fn from(event: peer_info::PeerInfoEvent) -> Self {
let peer_info::PeerInfoEvent::Identified { peer_id, info } = event;
BehaviourOut::PeerIdentify { peer_id, info }
}
}
impl From<DiscoveryOut> for BehaviourOut {
fn from(event: DiscoveryOut) -> Self {
match event {
DiscoveryOut::UnroutablePeer(_peer_id) => {
// Obtaining and reporting listen addresses for unroutable peers back
// to Kademlia is handled by the `Identify` protocol, part of the
// `PeerInfoBehaviour`. See the `From<peer_info::PeerInfoEvent>`
// implementation.
BehaviourOut::None
},
DiscoveryOut::Discovered(peer_id) => BehaviourOut::Discovered(peer_id),
DiscoveryOut::ClosestPeersFound(target, peers, duration) => BehaviourOut::Dht(
DhtEvent::ClosestPeersFound(
target.into(),
peers
.into_iter()
.map(|(p, addrs)| (p.into(), addrs.into_iter().map(Into::into).collect()))
.collect(),
),
Some(duration),
),
DiscoveryOut::ClosestPeersNotFound(target, duration) =>
BehaviourOut::Dht(DhtEvent::ClosestPeersNotFound(target.into()), Some(duration)),
DiscoveryOut::ValueFound(results, duration) =>
BehaviourOut::Dht(DhtEvent::ValueFound(results.into()), Some(duration)),
DiscoveryOut::ValueNotFound(key, duration) =>
BehaviourOut::Dht(DhtEvent::ValueNotFound(key.into()), Some(duration)),
DiscoveryOut::ValuePut(key, duration) =>
BehaviourOut::Dht(DhtEvent::ValuePut(key.into()), Some(duration)),
DiscoveryOut::PutRecordRequest(record_key, record_value, publisher, expires) =>
BehaviourOut::Dht(
DhtEvent::PutRecordRequest(record_key.into(), record_value, publisher, expires),
None,
),
DiscoveryOut::ValuePutFailed(key, duration) =>
BehaviourOut::Dht(DhtEvent::ValuePutFailed(key.into()), Some(duration)),
DiscoveryOut::StartedProviding(key, duration) =>
BehaviourOut::Dht(DhtEvent::StartedProviding(key.into()), Some(duration)),
DiscoveryOut::StartProvidingFailed(key, duration) =>
BehaviourOut::Dht(DhtEvent::StartProvidingFailed(key.into()), Some(duration)),
DiscoveryOut::ProvidersFound(key, providers, duration) => BehaviourOut::Dht(
DhtEvent::ProvidersFound(
key.into(),
providers.into_iter().map(Into::into).collect(),
),
Some(duration),
),
DiscoveryOut::NoMoreProviders(key, duration) =>
BehaviourOut::Dht(DhtEvent::NoMoreProviders(key.into()), Some(duration)),
DiscoveryOut::ProvidersNotFound(key, duration) =>
BehaviourOut::Dht(DhtEvent::ProvidersNotFound(key.into()), Some(duration)),
DiscoveryOut::RandomKademliaStarted => BehaviourOut::RandomKademliaStarted,
}
}
}
impl From<void::Void> for BehaviourOut {
fn from(e: void::Void) -> Self {
void::unreachable(e)
}
}
@@ -0,0 +1,534 @@
// 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/>.
//! Bitswap server for Bizinikiwi.
//!
//! Allows querying transactions by hash over standard bitswap protocol
//! Only supports bitswap 1.2.0.
//! CID is expected to reference 256-bit Blake2b transaction hash.
use crate::{
request_responses::{IncomingRequest, OutgoingResponse, ProtocolConfig},
types::ProtocolName,
MAX_RESPONSE_SIZE,
};
use cid::{self, Version};
use futures::StreamExt;
use log::{debug, error, trace};
use prost::Message;
use pezsc_client_api::BlockBackend;
use pezsc_network_types::PeerId;
use schema::bitswap::{
message::{wantlist::WantType, Block as MessageBlock, BlockPresence, BlockPresenceType},
Message as BitswapMessage,
};
use pezsp_runtime::traits::Block as BlockT;
use std::{io, sync::Arc, time::Duration};
use unsigned_varint::encode as varint_encode;
mod schema;
const LOG_TARGET: &str = "bitswap";
// Undocumented, but according to JS the bitswap messages have a max size of 512*1024 bytes
// https://github.com/ipfs/js-ipfs-bitswap/blob/
// d8f80408aadab94c962f6b88f343eb9f39fa0fcc/src/decision-engine/index.js#L16
// We set it to the same value as max bizinikiwi protocol message
const MAX_PACKET_SIZE: u64 = MAX_RESPONSE_SIZE;
/// Max number of queued responses before denying requests.
const MAX_REQUEST_QUEUE: usize = 20;
/// Max number of blocks per wantlist
const MAX_WANTED_BLOCKS: usize = 16;
/// Bitswap protocol name
const PROTOCOL_NAME: &'static str = "/ipfs/bitswap/1.2.0";
/// Prefix represents all metadata of a CID, without the actual content.
#[derive(PartialEq, Eq, Clone, Debug)]
struct Prefix {
/// The version of CID.
pub version: Version,
/// The codec of CID.
pub codec: u64,
/// The multihash type of CID.
pub mh_type: u64,
/// The multihash length of CID.
pub mh_len: u8,
}
impl Prefix {
/// Convert the prefix to encoded bytes.
pub fn to_bytes(&self) -> Vec<u8> {
let mut res = Vec::with_capacity(4);
let mut buf = varint_encode::u64_buffer();
let version = varint_encode::u64(self.version.into(), &mut buf);
res.extend_from_slice(version);
let mut buf = varint_encode::u64_buffer();
let codec = varint_encode::u64(self.codec, &mut buf);
res.extend_from_slice(codec);
let mut buf = varint_encode::u64_buffer();
let mh_type = varint_encode::u64(self.mh_type, &mut buf);
res.extend_from_slice(mh_type);
let mut buf = varint_encode::u64_buffer();
let mh_len = varint_encode::u64(self.mh_len as u64, &mut buf);
res.extend_from_slice(mh_len);
res
}
}
/// Bitswap request handler
pub struct BitswapRequestHandler<B> {
client: Arc<dyn BlockBackend<B> + Send + Sync>,
request_receiver: async_channel::Receiver<IncomingRequest>,
}
impl<B: BlockT> BitswapRequestHandler<B> {
/// Create a new [`BitswapRequestHandler`].
pub fn new(client: Arc<dyn BlockBackend<B> + Send + Sync>) -> (Self, ProtocolConfig) {
let (tx, request_receiver) = async_channel::bounded(MAX_REQUEST_QUEUE);
let config = ProtocolConfig {
name: ProtocolName::from(PROTOCOL_NAME),
fallback_names: vec![],
max_request_size: MAX_PACKET_SIZE,
max_response_size: MAX_PACKET_SIZE,
request_timeout: Duration::from_secs(15),
inbound_queue: Some(tx),
};
(Self { client, request_receiver }, config)
}
/// Run [`BitswapRequestHandler`].
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_message(&peer, &payload) {
Ok(response) => {
let response = OutgoingResponse {
result: Ok(response),
reputation_changes: Vec::new(),
sent_feedback: None,
};
match pending_response.send(response) {
Ok(()) => {
trace!(target: LOG_TARGET, "Handled bitswap request from {peer}.",)
},
Err(_) => debug!(
target: LOG_TARGET,
"Failed to handle light client request from {peer}: {}",
BitswapError::SendResponse,
),
}
},
Err(err) => {
error!(target: LOG_TARGET, "Failed to process request from {peer}: {err}");
// TODO: adjust reputation?
let response = OutgoingResponse {
result: Err(()),
reputation_changes: vec![],
sent_feedback: None,
};
if pending_response.send(response).is_err() {
debug!(
target: LOG_TARGET,
"Failed to handle bitswap request from {peer}: {}",
BitswapError::SendResponse,
);
}
},
}
}
}
/// Handle received Bitswap request
fn handle_message(
&mut self,
peer: &PeerId,
payload: &Vec<u8>,
) -> Result<Vec<u8>, BitswapError> {
let request = schema::bitswap::Message::decode(&payload[..])?;
trace!(target: LOG_TARGET, "Received request: {:?} from {}", request, peer);
let mut response = BitswapMessage::default();
let wantlist = match request.wantlist {
Some(wantlist) => wantlist,
None => {
debug!(target: LOG_TARGET, "Unexpected bitswap message from {}", peer);
return Err(BitswapError::InvalidWantList);
},
};
if wantlist.entries.len() > MAX_WANTED_BLOCKS {
trace!(target: LOG_TARGET, "Ignored request: too many entries");
return Err(BitswapError::TooManyEntries);
}
for entry in wantlist.entries {
let cid = match cid::Cid::read_bytes(entry.block.as_slice()) {
Ok(cid) => cid,
Err(e) => {
trace!(target: LOG_TARGET, "Bad CID {:?}: {:?}", entry.block, e);
continue;
},
};
if cid.version() != cid::Version::V1 ||
cid.hash().code() != u64::from(cid::multihash::Code::Blake2b256) ||
cid.hash().size() != 32
{
debug!(target: LOG_TARGET, "Ignoring unsupported CID {}: {}", peer, cid);
continue;
}
let mut hash = B::Hash::default();
hash.as_mut().copy_from_slice(&cid.hash().digest()[0..32]);
let transaction = match self.client.indexed_transaction(hash) {
Ok(ex) => ex,
Err(e) => {
error!(target: LOG_TARGET, "Error retrieving transaction {}: {}", hash, e);
None
},
};
match transaction {
Some(transaction) => {
trace!(target: LOG_TARGET, "Found CID {:?}, hash {:?}", cid, hash);
if entry.want_type == WantType::Block as i32 {
let prefix = Prefix {
version: cid.version(),
codec: cid.codec(),
mh_type: cid.hash().code(),
mh_len: cid.hash().size(),
};
response
.payload
.push(MessageBlock { prefix: prefix.to_bytes(), data: transaction });
} else {
response.block_presences.push(BlockPresence {
r#type: BlockPresenceType::Have as i32,
cid: cid.to_bytes(),
});
}
},
None => {
trace!(target: LOG_TARGET, "Missing CID {:?}, hash {:?}", cid, hash);
if entry.send_dont_have {
response.block_presences.push(BlockPresence {
r#type: BlockPresenceType::DontHave as i32,
cid: cid.to_bytes(),
});
}
},
}
}
Ok(response.encode_to_vec())
}
}
/// Bitswap protocol error.
#[derive(Debug, thiserror::Error)]
pub enum BitswapError {
/// Protobuf decoding error.
#[error("Failed to decode request: {0}.")]
DecodeProto(#[from] prost::DecodeError),
/// Protobuf encoding error.
#[error("Failed to encode response: {0}.")]
EncodeProto(#[from] prost::EncodeError),
/// Client backend error.
#[error(transparent)]
Client(#[from] pezsp_blockchain::Error),
/// Error parsing CID
#[error(transparent)]
BadCid(#[from] cid::Error),
/// Packet read error.
#[error(transparent)]
Read(#[from] io::Error),
/// Error sending response.
#[error("Failed to send response.")]
SendResponse,
/// Message doesn't have a WANT list.
#[error("Invalid WANT list.")]
InvalidWantList,
/// Too many blocks requested.
#[error("Too many block entries in the request.")]
TooManyEntries,
}
#[cfg(test)]
mod tests {
use super::*;
use futures::channel::oneshot;
use pezsc_block_builder::BlockBuilderBuilder;
use schema::bitswap::{
message::{wantlist::Entry, Wantlist},
Message as BitswapMessage,
};
use pezsp_consensus::BlockOrigin;
use pezsp_runtime::codec::Encode;
use bizinikiwi_test_runtime::ExtrinsicBuilder;
use bizinikiwi_test_runtime_client::{self, prelude::*, TestClientBuilder};
#[tokio::test]
async fn undecodable_message() {
let client = bizinikiwi_test_runtime_client::new();
let (bitswap, config) = BitswapRequestHandler::new(Arc::new(client));
tokio::spawn(async move { bitswap.run().await });
let (tx, rx) = oneshot::channel();
config
.inbound_queue
.unwrap()
.send(IncomingRequest {
peer: PeerId::random(),
payload: vec![0x13, 0x37, 0x13, 0x38],
pending_response: tx,
})
.await
.unwrap();
if let Ok(OutgoingResponse { result, reputation_changes, sent_feedback }) = rx.await {
assert_eq!(result, Err(()));
assert_eq!(reputation_changes, Vec::new());
assert!(sent_feedback.is_none());
} else {
panic!("invalid event received");
}
}
#[tokio::test]
async fn empty_want_list() {
let client = bizinikiwi_test_runtime_client::new();
let (bitswap, mut config) = BitswapRequestHandler::new(Arc::new(client));
tokio::spawn(async move { bitswap.run().await });
let (tx, rx) = oneshot::channel();
config
.inbound_queue
.as_mut()
.unwrap()
.send(IncomingRequest {
peer: PeerId::random(),
payload: BitswapMessage { wantlist: None, ..Default::default() }.encode_to_vec(),
pending_response: tx,
})
.await
.unwrap();
if let Ok(OutgoingResponse { result, reputation_changes, sent_feedback }) = rx.await {
assert_eq!(result, Err(()));
assert_eq!(reputation_changes, Vec::new());
assert!(sent_feedback.is_none());
} else {
panic!("invalid event received");
}
// Empty WANT list should not cause an error
let (tx, rx) = oneshot::channel();
config
.inbound_queue
.unwrap()
.send(IncomingRequest {
peer: PeerId::random(),
payload: BitswapMessage {
wantlist: Some(Default::default()),
..Default::default()
}
.encode_to_vec(),
pending_response: tx,
})
.await
.unwrap();
if let Ok(OutgoingResponse { result, reputation_changes, sent_feedback }) = rx.await {
assert_eq!(result, Ok(BitswapMessage::default().encode_to_vec()));
assert_eq!(reputation_changes, Vec::new());
assert!(sent_feedback.is_none());
} else {
panic!("invalid event received");
}
}
#[tokio::test]
async fn too_long_want_list() {
let client = bizinikiwi_test_runtime_client::new();
let (bitswap, config) = BitswapRequestHandler::new(Arc::new(client));
tokio::spawn(async move { bitswap.run().await });
let (tx, rx) = oneshot::channel();
config
.inbound_queue
.unwrap()
.send(IncomingRequest {
peer: PeerId::random(),
payload: BitswapMessage {
wantlist: Some(Wantlist {
entries: (0..MAX_WANTED_BLOCKS + 1)
.map(|_| Entry::default())
.collect::<Vec<_>>(),
full: false,
}),
..Default::default()
}
.encode_to_vec(),
pending_response: tx,
})
.await
.unwrap();
if let Ok(OutgoingResponse { result, reputation_changes, sent_feedback }) = rx.await {
assert_eq!(result, Err(()));
assert_eq!(reputation_changes, Vec::new());
assert!(sent_feedback.is_none());
} else {
panic!("invalid event received");
}
}
#[tokio::test]
async fn transaction_not_found() {
let client = TestClientBuilder::with_tx_storage(u32::MAX).build();
let (bitswap, config) = BitswapRequestHandler::new(Arc::new(client));
tokio::spawn(async move { bitswap.run().await });
let (tx, rx) = oneshot::channel();
config
.inbound_queue
.unwrap()
.send(IncomingRequest {
peer: PeerId::random(),
payload: BitswapMessage {
wantlist: Some(Wantlist {
entries: vec![Entry {
block: cid::Cid::new_v1(
0x70,
cid::multihash::Multihash::wrap(
u64::from(cid::multihash::Code::Blake2b256),
&[0u8; 32],
)
.unwrap(),
)
.to_bytes(),
..Default::default()
}],
full: false,
}),
..Default::default()
}
.encode_to_vec(),
pending_response: tx,
})
.await
.unwrap();
if let Ok(OutgoingResponse { result, reputation_changes, sent_feedback }) = rx.await {
assert_eq!(result, Ok(vec![]));
assert_eq!(reputation_changes, Vec::new());
assert!(sent_feedback.is_none());
} else {
panic!("invalid event received");
}
}
#[tokio::test]
async fn transaction_found() {
let client = TestClientBuilder::with_tx_storage(u32::MAX).build();
let mut block_builder = BlockBuilderBuilder::new(&client)
.on_parent_block(client.chain_info().genesis_hash)
.with_parent_block_number(0)
.build()
.unwrap();
// encoded extrinsic: [161, .. , 2, 6, 16, 19, 55, 19, 56]
let ext = ExtrinsicBuilder::new_indexed_call(vec![0x13, 0x37, 0x13, 0x38]).build();
let pattern_index = ext.encoded_size() - 4;
block_builder.push(ext.clone()).unwrap();
let block = block_builder.build().unwrap().block;
client.import(BlockOrigin::File, block).await.unwrap();
let (bitswap, config) = BitswapRequestHandler::new(Arc::new(client));
tokio::spawn(async move { bitswap.run().await });
let (tx, rx) = oneshot::channel();
config
.inbound_queue
.unwrap()
.send(IncomingRequest {
peer: PeerId::random(),
payload: BitswapMessage {
wantlist: Some(Wantlist {
entries: vec![Entry {
block: cid::Cid::new_v1(
0x70,
cid::multihash::Multihash::wrap(
u64::from(cid::multihash::Code::Blake2b256),
&pezsp_crypto_hashing::blake2_256(&ext.encode()[pattern_index..]),
)
.unwrap(),
)
.to_bytes(),
..Default::default()
}],
full: false,
}),
..Default::default()
}
.encode_to_vec(),
pending_response: tx,
})
.await
.unwrap();
if let Ok(OutgoingResponse { result, reputation_changes, sent_feedback }) = rx.await {
assert_eq!(reputation_changes, Vec::new());
assert!(sent_feedback.is_none());
let response =
schema::bitswap::Message::decode(&result.expect("fetch to succeed")[..]).unwrap();
assert_eq!(response.payload[0].data, vec![0x13, 0x37, 0x13, 0x38]);
} else {
panic!("invalid event received");
}
}
}
@@ -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 bitswap {
include!(concat!(env!("OUT_DIR"), "/bitswap.message.rs"));
}
+997
View File
@@ -0,0 +1,997 @@
// 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/>.
//! Configuration of the networking layer.
//!
//! The [`Params`] struct is the struct that must be passed in order to initialize the networking.
//! See the documentation of [`Params`].
pub use crate::{
discovery::DEFAULT_KADEMLIA_REPLICATION_FACTOR,
peer_store::PeerStoreProvider,
protocol::{notification_service, NotificationsSink, ProtocolHandlePair},
request_responses::{
IncomingRequest, OutgoingResponse, ProtocolConfig as RequestResponseConfig,
},
service::{
metrics::NotificationMetrics,
traits::{NotificationConfig, NotificationService, PeerStore},
},
types::ProtocolName,
};
pub use pezsc_network_types::{build_multiaddr, ed25519};
use pezsc_network_types::{
multiaddr::{self, Multiaddr},
PeerId,
};
use crate::service::{ensure_addresses_consistent_with_transport, traits::NetworkBackend};
use codec::Encode;
use prometheus_endpoint::Registry;
use zeroize::Zeroize;
pub use pezsc_network_common::{
role::{Role, Roles},
sync::SyncMode,
ExHashT,
};
use pezsp_runtime::traits::Block as BlockT;
use std::{
error::Error,
fmt, fs,
future::Future,
io::{self, Write},
iter,
net::Ipv4Addr,
num::NonZeroUsize,
path::{Path, PathBuf},
pin::Pin,
str::{self, FromStr},
sync::Arc,
time::Duration,
};
/// Default timeout for idle connections of 10 seconds is good enough for most networks.
/// It doesn't make sense to expose it as a CLI parameter on individual nodes, but customizations
/// are possible in custom nodes through [`NetworkConfiguration`].
pub const DEFAULT_IDLE_CONNECTION_TIMEOUT: Duration = Duration::from_secs(10);
/// Maximum number of locally kept Kademlia provider keys.
///
/// 10000 keys is enough for a testnet with fast runtime (1-minute epoch) and 13 teyrchains.
pub const KADEMLIA_MAX_PROVIDER_KEYS: usize = 10000;
/// Time to keep Kademlia content provider records.
///
/// 10 h is enough time to keep the teyrchain bootnode record for two 4-hour epochs.
pub const KADEMLIA_PROVIDER_RECORD_TTL: Duration = Duration::from_secs(10 * 3600);
/// Interval of republishing Kademlia provider records.
///
/// 3.5 h means we refresh next epoch provider record 30 minutes before next 4-hour epoch comes.
pub const KADEMLIA_PROVIDER_REPUBLISH_INTERVAL: Duration = Duration::from_secs(12600);
/// Protocol name prefix, transmitted on the wire for legacy protocol names.
/// I.e., `dot` in `/hez/sync/2`. Should be unique for each chain. Always UTF-8.
/// Deprecated in favour of genesis hash & fork ID based protocol names.
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct ProtocolId(smallvec::SmallVec<[u8; 6]>);
impl<'a> From<&'a str> for ProtocolId {
fn from(bytes: &'a str) -> ProtocolId {
Self(bytes.as_bytes().into())
}
}
impl AsRef<str> for ProtocolId {
fn as_ref(&self) -> &str {
str::from_utf8(&self.0[..])
.expect("the only way to build a ProtocolId is through a UTF-8 String; qed")
}
}
impl fmt::Debug for ProtocolId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Debug::fmt(self.as_ref(), f)
}
}
/// Parses a string address and splits it into Multiaddress and PeerId, if
/// valid.
///
/// # Example
///
/// ```
/// # use pezsc_network_types::{multiaddr::Multiaddr, PeerId};
/// use pezsc_network::config::parse_str_addr;
/// let (peer_id, addr) = parse_str_addr(
/// "/ip4/198.51.100.19/tcp/30333/p2p/QmSk5HQbn6LhUwDiNMseVUjuRYhEtYj4aUZ6WfWoGURpdV"
/// ).unwrap();
/// assert_eq!(peer_id, "QmSk5HQbn6LhUwDiNMseVUjuRYhEtYj4aUZ6WfWoGURpdV".parse::<PeerId>().unwrap().into());
/// assert_eq!(addr, "/ip4/198.51.100.19/tcp/30333".parse::<Multiaddr>().unwrap());
/// ```
pub fn parse_str_addr(addr_str: &str) -> Result<(PeerId, Multiaddr), ParseErr> {
let addr: Multiaddr = addr_str.parse()?;
parse_addr(addr)
}
/// Splits a Multiaddress into a Multiaddress and PeerId.
pub fn parse_addr(mut addr: Multiaddr) -> Result<(PeerId, Multiaddr), ParseErr> {
let multihash = match addr.pop() {
Some(multiaddr::Protocol::P2p(multihash)) => multihash,
_ => return Err(ParseErr::PeerIdMissing),
};
let peer_id = PeerId::from_multihash(multihash).map_err(|_| ParseErr::InvalidPeerId)?;
Ok((peer_id, addr))
}
/// Address of a node, including its identity.
///
/// This struct represents a decoded version of a multiaddress that ends with `/p2p/<peerid>`.
///
/// # Example
///
/// ```
/// # use pezsc_network_types::{multiaddr::Multiaddr, PeerId};
/// use pezsc_network::config::MultiaddrWithPeerId;
/// let addr: MultiaddrWithPeerId =
/// "/ip4/198.51.100.19/tcp/30333/p2p/QmSk5HQbn6LhUwDiNMseVUjuRYhEtYj4aUZ6WfWoGURpdV".parse().unwrap();
/// assert_eq!(addr.peer_id.to_base58(), "QmSk5HQbn6LhUwDiNMseVUjuRYhEtYj4aUZ6WfWoGURpdV");
/// assert_eq!(addr.multiaddr.to_string(), "/ip4/198.51.100.19/tcp/30333");
/// ```
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
#[serde(try_from = "String", into = "String")]
pub struct MultiaddrWithPeerId {
/// Address of the node.
pub multiaddr: Multiaddr,
/// Its identity.
pub peer_id: PeerId,
}
impl MultiaddrWithPeerId {
/// Concatenates the multiaddress and peer ID into one multiaddress containing both.
pub fn concat(&self) -> Multiaddr {
let proto = multiaddr::Protocol::P2p(From::from(self.peer_id));
self.multiaddr.clone().with(proto)
}
}
impl fmt::Display for MultiaddrWithPeerId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Display::fmt(&self.concat(), f)
}
}
impl FromStr for MultiaddrWithPeerId {
type Err = ParseErr;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (peer_id, multiaddr) = parse_str_addr(s)?;
Ok(Self { peer_id, multiaddr })
}
}
impl From<MultiaddrWithPeerId> for String {
fn from(ma: MultiaddrWithPeerId) -> String {
format!("{}", ma)
}
}
impl TryFrom<String> for MultiaddrWithPeerId {
type Error = ParseErr;
fn try_from(string: String) -> Result<Self, Self::Error> {
string.parse()
}
}
/// Error that can be generated by `parse_str_addr`.
#[derive(Debug)]
pub enum ParseErr {
/// Error while parsing the multiaddress.
MultiaddrParse(multiaddr::ParseError),
/// Multihash of the peer ID is invalid.
InvalidPeerId,
/// The peer ID is missing from the address.
PeerIdMissing,
}
impl fmt::Display for ParseErr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MultiaddrParse(err) => write!(f, "{}", err),
Self::InvalidPeerId => write!(f, "Peer id at the end of the address is invalid"),
Self::PeerIdMissing => write!(f, "Peer id is missing from the address"),
}
}
}
impl std::error::Error for ParseErr {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::MultiaddrParse(err) => Some(err),
Self::InvalidPeerId => None,
Self::PeerIdMissing => None,
}
}
}
impl From<multiaddr::ParseError> for ParseErr {
fn from(err: multiaddr::ParseError) -> ParseErr {
Self::MultiaddrParse(err)
}
}
/// Custom handshake for the notification protocol
#[derive(Debug, Clone)]
pub struct NotificationHandshake(Vec<u8>);
impl NotificationHandshake {
/// Create new `NotificationHandshake` from an object that implements `Encode`
pub fn new<H: Encode>(handshake: H) -> Self {
Self(handshake.encode())
}
/// Create new `NotificationHandshake` from raw bytes
pub fn from_bytes(bytes: Vec<u8>) -> Self {
Self(bytes)
}
}
impl std::ops::Deref for NotificationHandshake {
type Target = Vec<u8>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
/// Configuration for the transport layer.
#[derive(Clone, Debug)]
pub enum TransportConfig {
/// Normal transport mode.
Normal {
/// If true, the network will use mDNS to discover other libp2p nodes on the local network
/// and connect to them if they support the same chain.
enable_mdns: bool,
/// If true, allow connecting to private IPv4/IPv6 addresses (as defined in
/// [RFC1918](https://tools.ietf.org/html/rfc1918)). Irrelevant for addresses that have
/// been passed in `::pezsc_network::config::NetworkConfiguration::boot_nodes`.
allow_private_ip: bool,
},
/// Only allow connections within the same process.
/// Only addresses of the form `/memory/...` will be supported.
MemoryOnly,
}
/// The policy for connections to non-reserved peers.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum NonReservedPeerMode {
/// Accept them. This is the default.
Accept,
/// Deny them.
Deny,
}
impl NonReservedPeerMode {
/// Attempt to parse the peer mode from a string.
pub fn parse(s: &str) -> Option<Self> {
match s {
"accept" => Some(Self::Accept),
"deny" => Some(Self::Deny),
_ => None,
}
}
/// If we are in "reserved-only" peer mode.
pub fn is_reserved_only(&self) -> bool {
matches!(self, NonReservedPeerMode::Deny)
}
}
/// The configuration of a node's secret key, describing the type of key
/// and how it is obtained. A node's identity keypair is the result of
/// the evaluation of the node key configuration.
#[derive(Clone, Debug)]
pub enum NodeKeyConfig {
/// A Ed25519 secret key configuration.
Ed25519(Secret<ed25519::SecretKey>),
}
impl Default for NodeKeyConfig {
fn default() -> NodeKeyConfig {
Self::Ed25519(Secret::New)
}
}
/// The options for obtaining a Ed25519 secret key.
pub type Ed25519Secret = Secret<ed25519::SecretKey>;
/// The configuration options for obtaining a secret key `K`.
#[derive(Clone)]
pub enum Secret<K> {
/// Use the given secret key `K`.
Input(K),
/// Read the secret key from a file. If the file does not exist,
/// it is created with a newly generated secret key `K`. The format
/// of the file is determined by `K`:
///
/// * `ed25519::SecretKey`: An unencoded 32 bytes Ed25519 secret key.
File(PathBuf),
/// Always generate a new secret key `K`.
New,
}
impl<K> fmt::Debug for Secret<K> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Input(_) => f.debug_tuple("Secret::Input").finish(),
Self::File(path) => f.debug_tuple("Secret::File").field(path).finish(),
Self::New => f.debug_tuple("Secret::New").finish(),
}
}
}
impl NodeKeyConfig {
/// Evaluate a `NodeKeyConfig` to obtain an identity `Keypair`:
///
/// * If the secret is configured as input, the corresponding keypair is returned.
///
/// * If the secret is configured as a file, it is read from that file, if it exists. Otherwise
/// a new secret is generated and stored. In either case, the keypair obtained from the
/// secret is returned.
///
/// * If the secret is configured to be new, it is generated and the corresponding keypair is
/// returned.
pub fn into_keypair(self) -> io::Result<ed25519::Keypair> {
use NodeKeyConfig::*;
match self {
Ed25519(Secret::New) => Ok(ed25519::Keypair::generate()),
Ed25519(Secret::Input(k)) => Ok(ed25519::Keypair::from(k).into()),
Ed25519(Secret::File(f)) => get_secret(
f,
|mut b| match String::from_utf8(b.to_vec()).ok().and_then(|s| {
if s.len() == 64 {
array_bytes::hex2bytes(&s).ok()
} else {
None
}
}) {
Some(s) => ed25519::SecretKey::try_from_bytes(s),
_ => ed25519::SecretKey::try_from_bytes(&mut b),
},
ed25519::SecretKey::generate,
|b| b.as_ref().to_vec(),
)
.map(ed25519::Keypair::from),
}
}
}
/// Load a secret key from a file, if it exists, or generate a
/// new secret key and write it to that file. In either case,
/// the secret key is returned.
fn get_secret<P, F, G, E, W, K>(file: P, parse: F, generate: G, serialize: W) -> io::Result<K>
where
P: AsRef<Path>,
F: for<'r> FnOnce(&'r mut [u8]) -> Result<K, E>,
G: FnOnce() -> K,
E: Error + Send + Sync + 'static,
W: Fn(&K) -> Vec<u8>,
{
std::fs::read(&file)
.and_then(|mut sk_bytes| {
parse(&mut sk_bytes).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
})
.or_else(|e| {
if e.kind() == io::ErrorKind::NotFound {
file.as_ref().parent().map_or(Ok(()), fs::create_dir_all)?;
let sk = generate();
let mut sk_vec = serialize(&sk);
write_secret_file(file, &sk_vec)?;
sk_vec.zeroize();
Ok(sk)
} else {
Err(e)
}
})
}
/// Write secret bytes to a file.
fn write_secret_file<P>(path: P, sk_bytes: &[u8]) -> io::Result<()>
where
P: AsRef<Path>,
{
let mut file = open_secret_file(&path)?;
file.write_all(sk_bytes)
}
/// Opens a file containing a secret key in write mode.
#[cfg(unix)]
fn open_secret_file<P>(path: P) -> io::Result<fs::File>
where
P: AsRef<Path>,
{
use std::os::unix::fs::OpenOptionsExt;
fs::OpenOptions::new().write(true).create_new(true).mode(0o600).open(path)
}
/// Opens a file containing a secret key in write mode.
#[cfg(not(unix))]
fn open_secret_file<P>(path: P) -> Result<fs::File, io::Error>
where
P: AsRef<Path>,
{
fs::OpenOptions::new().write(true).create_new(true).open(path)
}
/// Configuration for a set of nodes.
#[derive(Clone, Debug)]
pub struct SetConfig {
/// Maximum allowed number of incoming substreams related to this set.
pub in_peers: u32,
/// Number of outgoing substreams related to this set that we're trying to maintain.
pub out_peers: u32,
/// List of reserved node addresses.
pub reserved_nodes: Vec<MultiaddrWithPeerId>,
/// Whether nodes that aren't in [`SetConfig::reserved_nodes`] are accepted or automatically
/// refused.
pub non_reserved_mode: NonReservedPeerMode,
}
impl Default for SetConfig {
fn default() -> Self {
Self {
in_peers: 25,
out_peers: 75,
reserved_nodes: Vec::new(),
non_reserved_mode: NonReservedPeerMode::Accept,
}
}
}
/// Extension to [`SetConfig`] for sets that aren't the default set.
///
/// > **Note**: As new fields might be added in the future, please consider using the `new` method
/// > and modifiers instead of creating this struct manually.
#[derive(Debug)]
pub struct NonDefaultSetConfig {
/// Name of the notifications protocols of this set. A substream on this set will be
/// considered established once this protocol is open.
///
/// > **Note**: This field isn't present for the default set, as this is handled internally
/// > by the networking code.
protocol_name: ProtocolName,
/// If the remote reports that it doesn't support the protocol indicated in the
/// `notifications_protocol` field, then each of these fallback names will be tried one by
/// one.
///
/// If a fallback is used, it will be reported in
/// `pezsc_network::protocol::event::Event::NotificationStreamOpened::negotiated_fallback`
fallback_names: Vec<ProtocolName>,
/// Handshake of the protocol
///
/// NOTE: Currently custom handshakes are not fully supported. See issue #5685 for more
/// details. This field is temporarily used to allow moving the hardcoded block announcement
/// protocol out of `protocol.rs`.
handshake: Option<NotificationHandshake>,
/// Maximum allowed size of single notifications.
max_notification_size: u64,
/// Base configuration.
set_config: SetConfig,
/// Notification handle.
///
/// Notification handle is created during `NonDefaultSetConfig` creation and its other half,
/// `Box<dyn NotificationService>` is given to the protocol created the config and
/// `ProtocolHandle` is given to `Notifications` when it initializes itself. This handle allows
/// `Notifications ` to communicate with the protocol directly without relaying events through
/// `sc-network.`
protocol_handle_pair: ProtocolHandlePair,
}
impl NonDefaultSetConfig {
/// Creates a new [`NonDefaultSetConfig`]. Zero slots and accepts only reserved nodes.
/// Also returns an object which allows the protocol to communicate with `Notifications`.
pub fn new(
protocol_name: ProtocolName,
fallback_names: Vec<ProtocolName>,
max_notification_size: u64,
handshake: Option<NotificationHandshake>,
set_config: SetConfig,
) -> (Self, Box<dyn NotificationService>) {
let (protocol_handle_pair, notification_service) =
notification_service(protocol_name.clone());
(
Self {
protocol_name,
max_notification_size,
fallback_names,
handshake,
set_config,
protocol_handle_pair,
},
notification_service,
)
}
/// Get reference to protocol name.
pub fn protocol_name(&self) -> &ProtocolName {
&self.protocol_name
}
/// Get reference to fallback protocol names.
pub fn fallback_names(&self) -> impl Iterator<Item = &ProtocolName> {
self.fallback_names.iter()
}
/// Get reference to handshake.
pub fn handshake(&self) -> &Option<NotificationHandshake> {
&self.handshake
}
/// Get maximum notification size.
pub fn max_notification_size(&self) -> u64 {
self.max_notification_size
}
/// Get reference to `SetConfig`.
pub fn set_config(&self) -> &SetConfig {
&self.set_config
}
/// Take `ProtocolHandlePair` from `NonDefaultSetConfig`
pub fn take_protocol_handle(self) -> ProtocolHandlePair {
self.protocol_handle_pair
}
/// Modifies the configuration to allow non-reserved nodes.
pub fn allow_non_reserved(&mut self, in_peers: u32, out_peers: u32) {
self.set_config.in_peers = in_peers;
self.set_config.out_peers = out_peers;
self.set_config.non_reserved_mode = NonReservedPeerMode::Accept;
}
/// Add a node to the list of reserved nodes.
pub fn add_reserved(&mut self, peer: MultiaddrWithPeerId) {
self.set_config.reserved_nodes.push(peer);
}
/// Add a list of protocol names used for backward compatibility.
///
/// See the explanations in [`NonDefaultSetConfig::fallback_names`].
pub fn add_fallback_names(&mut self, fallback_names: Vec<ProtocolName>) {
self.fallback_names.extend(fallback_names);
}
}
impl NotificationConfig for NonDefaultSetConfig {
fn set_config(&self) -> &SetConfig {
&self.set_config
}
/// Get reference to protocol name.
fn protocol_name(&self) -> &ProtocolName {
&self.protocol_name
}
}
/// Network service configuration.
#[derive(Clone, Debug)]
pub struct NetworkConfiguration {
/// Directory path to store network-specific configuration. None means nothing will be saved.
pub net_config_path: Option<PathBuf>,
/// Multiaddresses to listen for incoming connections.
pub listen_addresses: Vec<Multiaddr>,
/// Multiaddresses to advertise. Detected automatically if empty.
pub public_addresses: Vec<Multiaddr>,
/// List of initial node addresses
pub boot_nodes: Vec<MultiaddrWithPeerId>,
/// The node key configuration, which determines the node's network identity keypair.
pub node_key: NodeKeyConfig,
/// Configuration for the default set of nodes used for block syncing and transactions.
pub default_peers_set: SetConfig,
/// Number of substreams to reserve for full nodes for block syncing and transactions.
/// Any other slot will be dedicated to light nodes.
///
/// This value is implicitly capped to `default_set.out_peers + default_set.in_peers`.
pub default_peers_set_num_full: u32,
/// Client identifier. Sent over the wire for debugging purposes.
pub client_version: String,
/// Name of the node. Sent over the wire for debugging purposes.
pub node_name: String,
/// Configuration for the transport layer.
pub transport: TransportConfig,
/// Idle connection timeout.
///
/// Set by default to [`DEFAULT_IDLE_CONNECTION_TIMEOUT`].
pub idle_connection_timeout: Duration,
/// Maximum number of peers to ask the same blocks in parallel.
pub max_parallel_downloads: u32,
/// Maximum number of blocks per 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>,
/// Initial syncing mode.
pub sync_mode: SyncMode,
/// True if Kademlia random discovery should be enabled.
///
/// If true, the node will automatically randomly walk the DHT in order to find new peers.
pub enable_dht_random_walk: bool,
/// Should we insert non-global addresses into the DHT?
pub allow_non_globals_in_dht: bool,
/// Require iterative Kademlia DHT queries to use disjoint paths for increased resiliency in
/// the presence of potentially adversarial nodes.
pub kademlia_disjoint_query_paths: bool,
/// Kademlia replication factor determines to how many closest peers a record is replicated to.
///
/// Discovery mechanism requires successful replication to all
/// `kademlia_replication_factor` peers to consider record successfully put.
pub kademlia_replication_factor: NonZeroUsize,
/// Enable serving block data over IPFS bitswap.
pub ipfs_server: bool,
/// Networking backend used for P2P communication.
pub network_backend: NetworkBackendType,
}
impl NetworkConfiguration {
/// Create new default configuration
pub fn new<SN: Into<String>, SV: Into<String>>(
node_name: SN,
client_version: SV,
node_key: NodeKeyConfig,
net_config_path: Option<PathBuf>,
) -> Self {
let default_peers_set = SetConfig::default();
Self {
net_config_path,
listen_addresses: Vec::new(),
public_addresses: Vec::new(),
boot_nodes: Vec::new(),
node_key,
default_peers_set_num_full: default_peers_set.in_peers + default_peers_set.out_peers,
default_peers_set,
client_version: client_version.into(),
node_name: node_name.into(),
transport: TransportConfig::Normal { enable_mdns: false, allow_private_ip: true },
idle_connection_timeout: DEFAULT_IDLE_CONNECTION_TIMEOUT,
max_parallel_downloads: 5,
max_blocks_per_request: 64,
min_peers_to_start_warp_sync: None,
sync_mode: SyncMode::Full,
enable_dht_random_walk: true,
allow_non_globals_in_dht: false,
kademlia_disjoint_query_paths: false,
kademlia_replication_factor: NonZeroUsize::new(DEFAULT_KADEMLIA_REPLICATION_FACTOR)
.expect("value is a constant; constant is non-zero; qed."),
ipfs_server: false,
network_backend: NetworkBackendType::Litep2p,
}
}
/// Create new default configuration for localhost-only connection with random port (useful for
/// testing)
pub fn new_local() -> NetworkConfiguration {
let mut config =
NetworkConfiguration::new("test-node", "test-client", Default::default(), None);
config.listen_addresses =
vec![iter::once(multiaddr::Protocol::Ip4(Ipv4Addr::new(127, 0, 0, 1)))
.chain(iter::once(multiaddr::Protocol::Tcp(0)))
.collect()];
config.allow_non_globals_in_dht = true;
config
}
/// Create new default configuration for localhost-only connection with random port (useful for
/// testing)
pub fn new_memory() -> NetworkConfiguration {
let mut config =
NetworkConfiguration::new("test-node", "test-client", Default::default(), None);
config.listen_addresses =
vec![iter::once(multiaddr::Protocol::Ip4(Ipv4Addr::new(127, 0, 0, 1)))
.chain(iter::once(multiaddr::Protocol::Tcp(0)))
.collect()];
config.allow_non_globals_in_dht = true;
config
}
}
/// Network initialization parameters.
pub struct Params<Block: BlockT, H: ExHashT, N: NetworkBackend<Block, H>> {
/// Assigned role for our node (full, light, ...).
pub role: Role,
/// How to spawn background tasks.
pub executor: Box<dyn Fn(Pin<Box<dyn Future<Output = ()> + Send>>) + Send + Sync>,
/// Network layer configuration.
pub network_config: FullNetworkConfiguration<Block, H, N>,
/// Legacy name of the protocol to use on the wire. Should be different for each chain.
pub protocol_id: ProtocolId,
/// Genesis hash of the chain
pub genesis_hash: Block::Hash,
/// Fork ID to distinguish protocols of different hard forks. Part of the standard protocol
/// name on the wire.
pub fork_id: Option<String>,
/// Registry for recording prometheus metrics to.
pub metrics_registry: Option<Registry>,
/// Block announce protocol configuration
pub block_announce_config: N::NotificationProtocolConfig,
/// Bitswap configuration, if the server has been enabled.
pub bitswap_config: Option<N::BitswapConfig>,
/// Notification metrics.
pub notification_metrics: NotificationMetrics,
}
/// Full network configuration.
pub struct FullNetworkConfiguration<B: BlockT + 'static, H: ExHashT, N: NetworkBackend<B, H>> {
/// Installed notification protocols.
pub(crate) notification_protocols: Vec<N::NotificationProtocolConfig>,
/// List of request-response protocols that the node supports.
pub(crate) request_response_protocols: Vec<N::RequestResponseProtocolConfig>,
/// Network configuration.
pub network_config: NetworkConfiguration,
/// [`PeerStore`](crate::peer_store::PeerStore),
peer_store: Option<N::PeerStore>,
/// Handle to [`PeerStore`](crate::peer_store::PeerStore).
peer_store_handle: Arc<dyn PeerStoreProvider>,
/// Registry for recording prometheus metrics to.
pub metrics_registry: Option<Registry>,
}
impl<B: BlockT + 'static, H: ExHashT, N: NetworkBackend<B, H>> FullNetworkConfiguration<B, H, N> {
/// Create new [`FullNetworkConfiguration`].
pub fn new(network_config: &NetworkConfiguration, metrics_registry: Option<Registry>) -> Self {
let bootnodes = network_config.boot_nodes.iter().map(|bootnode| bootnode.peer_id).collect();
let peer_store = N::peer_store(bootnodes, metrics_registry.clone());
let peer_store_handle = peer_store.handle();
Self {
peer_store: Some(peer_store),
peer_store_handle,
notification_protocols: Vec::new(),
request_response_protocols: Vec::new(),
network_config: network_config.clone(),
metrics_registry,
}
}
/// Add a notification protocol.
pub fn add_notification_protocol(&mut self, config: N::NotificationProtocolConfig) {
self.notification_protocols.push(config);
}
/// Get reference to installed notification protocols.
pub fn notification_protocols(&self) -> &Vec<N::NotificationProtocolConfig> {
&self.notification_protocols
}
/// Add a request-response protocol.
pub fn add_request_response_protocol(&mut self, config: N::RequestResponseProtocolConfig) {
self.request_response_protocols.push(config);
}
/// Get handle to [`PeerStore`].
pub fn peer_store_handle(&self) -> Arc<dyn PeerStoreProvider> {
Arc::clone(&self.peer_store_handle)
}
/// Take [`PeerStore`].
///
/// `PeerStore` is created when `FullNetworkConfig` is initialized so that `PeerStoreHandle`s
/// can be passed onto notification protocols. `PeerStore` itself should be started only once
/// and since technically it's not a libp2p task, it should be started with `SpawnHandle` in
/// `builder.rs` instead of using the libp2p/litep2p executor in the networking backend. This
/// function consumes `PeerStore` and starts its event loop in the appropriate place.
pub fn take_peer_store(&mut self) -> N::PeerStore {
self.peer_store
.take()
.expect("`PeerStore` can only be taken once when it's started; qed")
}
/// Verify addresses are consistent with enabled transports.
pub fn sanity_check_addresses(&self) -> Result<(), crate::error::Error> {
ensure_addresses_consistent_with_transport(
self.network_config.listen_addresses.iter(),
&self.network_config.transport,
)?;
ensure_addresses_consistent_with_transport(
self.network_config.boot_nodes.iter().map(|x| &x.multiaddr),
&self.network_config.transport,
)?;
ensure_addresses_consistent_with_transport(
self.network_config
.default_peers_set
.reserved_nodes
.iter()
.map(|x| &x.multiaddr),
&self.network_config.transport,
)?;
for notification_protocol in &self.notification_protocols {
ensure_addresses_consistent_with_transport(
notification_protocol.set_config().reserved_nodes.iter().map(|x| &x.multiaddr),
&self.network_config.transport,
)?;
}
ensure_addresses_consistent_with_transport(
self.network_config.public_addresses.iter(),
&self.network_config.transport,
)?;
Ok(())
}
/// Check for duplicate bootnodes.
pub fn sanity_check_bootnodes(&self) -> Result<(), crate::error::Error> {
self.network_config.boot_nodes.iter().try_for_each(|bootnode| {
if let Some(other) = self
.network_config
.boot_nodes
.iter()
.filter(|o| o.multiaddr == bootnode.multiaddr)
.find(|o| o.peer_id != bootnode.peer_id)
{
Err(crate::error::Error::DuplicateBootnode {
address: bootnode.multiaddr.clone().into(),
first_id: bootnode.peer_id.into(),
second_id: other.peer_id.into(),
})
} else {
Ok(())
}
})
}
/// Collect all reserved nodes and bootnodes addresses.
pub fn known_addresses(&self) -> Vec<(PeerId, Multiaddr)> {
let mut addresses: Vec<_> = self
.network_config
.default_peers_set
.reserved_nodes
.iter()
.map(|reserved| (reserved.peer_id, reserved.multiaddr.clone()))
.chain(self.notification_protocols.iter().flat_map(|protocol| {
protocol
.set_config()
.reserved_nodes
.iter()
.map(|reserved| (reserved.peer_id, reserved.multiaddr.clone()))
}))
.chain(
self.network_config
.boot_nodes
.iter()
.map(|bootnode| (bootnode.peer_id, bootnode.multiaddr.clone())),
)
.collect();
// Remove possible duplicates.
addresses.sort();
addresses.dedup();
addresses
}
}
/// Network backend type.
#[derive(Debug, Clone, Default, Copy)]
pub enum NetworkBackendType {
/// Use litep2p for P2P networking.
///
/// This is the preferred option for Bizinikiwi-based chains.
#[default]
Litep2p,
/// Use libp2p for P2P networking.
///
/// The libp2p is still used for compatibility reasons until the
/// ecosystem switches entirely to litep2p. The backend will enter
/// a "best-effort" maintenance mode, where only critical issues will
/// get fixed. If you are unsure, please use `NetworkBackendType::Litep2p`.
Libp2p,
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn tempdir_with_prefix(prefix: &str) -> TempDir {
tempfile::Builder::new().prefix(prefix).tempdir().unwrap()
}
fn secret_bytes(kp: ed25519::Keypair) -> Vec<u8> {
kp.secret().to_bytes().into()
}
#[test]
fn test_secret_file() {
let tmp = tempdir_with_prefix("x");
std::fs::remove_dir(tmp.path()).unwrap(); // should be recreated
let file = tmp.path().join("x").to_path_buf();
let kp1 = NodeKeyConfig::Ed25519(Secret::File(file.clone())).into_keypair().unwrap();
let kp2 = NodeKeyConfig::Ed25519(Secret::File(file.clone())).into_keypair().unwrap();
assert!(file.is_file() && secret_bytes(kp1) == secret_bytes(kp2))
}
#[test]
fn test_secret_input() {
let sk = ed25519::SecretKey::generate();
let kp1 = NodeKeyConfig::Ed25519(Secret::Input(sk.clone())).into_keypair().unwrap();
let kp2 = NodeKeyConfig::Ed25519(Secret::Input(sk)).into_keypair().unwrap();
assert!(secret_bytes(kp1) == secret_bytes(kp2));
}
#[test]
fn test_secret_new() {
let kp1 = NodeKeyConfig::Ed25519(Secret::New).into_keypair().unwrap();
let kp2 = NodeKeyConfig::Ed25519(Secret::New).into_keypair().unwrap();
assert!(secret_bytes(kp1) != secret_bytes(kp2));
}
}
File diff suppressed because it is too large Load Diff
+90
View File
@@ -0,0 +1,90 @@
// 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/>.
//! Bizinikiwi network possible errors.
use crate::{config::TransportConfig, types::ProtocolName};
use pezsc_network_types::{multiaddr::Multiaddr, PeerId};
use std::fmt;
/// Result type alias for the network.
pub type Result<T> = std::result::Result<T, Error>;
/// Error type for the network.
#[derive(thiserror::Error)]
pub enum Error {
/// Io error
#[error(transparent)]
Io(#[from] std::io::Error),
/// Client error
#[error(transparent)]
Client(#[from] Box<pezsp_blockchain::Error>),
/// The same bootnode (based on address) is registered with two different peer ids.
#[error(
"The same bootnode (`{address}`) is registered with two different peer ids: `{first_id}` and `{second_id}`"
)]
DuplicateBootnode {
/// The address of the bootnode.
address: Multiaddr,
/// The first peer id that was found for the bootnode.
first_id: PeerId,
/// The second peer id that was found for the bootnode.
second_id: PeerId,
},
/// Prometheus metrics error.
#[error(transparent)]
Prometheus(#[from] prometheus_endpoint::PrometheusError),
/// The network addresses are invalid because they don't match the transport.
#[error(
"The following addresses are invalid because they don't match the transport: {addresses:?}"
)]
AddressesForAnotherTransport {
/// Transport used.
transport: TransportConfig,
/// The invalid addresses.
addresses: Vec<Multiaddr>,
},
/// The same request-response protocol has been registered multiple times.
#[error("Request-response protocol registered multiple times: {protocol}")]
DuplicateRequestResponseProtocol {
/// Name of the protocol registered multiple times.
protocol: ProtocolName,
},
/// Peer does not exist.
#[error("Peer `{0}` does not exist.")]
PeerDoesntExist(PeerId),
/// Channel closed.
#[error("Channel closed")]
ChannelClosed,
/// Connection closed.
#[error("Connection closed")]
ConnectionClosed,
/// Litep2p error.
#[error("Litep2p error: `{0}`")]
Litep2p(litep2p::Error),
}
// Make `Debug` use the `Display` implementation.
impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}
+123
View File
@@ -0,0 +1,123 @@
// 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/>.
//! Network event types. These are not the part of the protocol, but rather
//! events that happen on the network like DHT get/put results received.
use crate::types::ProtocolName;
use bytes::Bytes;
use pezsc_network_common::role::ObservedRole;
use pezsc_network_types::{
kad::{Key, PeerRecord},
multiaddr::Multiaddr,
PeerId,
};
/// Events generated by DHT as a response to get_value and put_value requests.
#[derive(Debug, Clone)]
#[must_use]
pub enum DhtEvent {
/// Found closest peers to the target `PeerId`. With libp2p also delivers a partial result
/// in case the query timed out, because it can contain the target peer's address.
ClosestPeersFound(PeerId, Vec<(PeerId, Vec<Multiaddr>)>),
/// Closest peers to the target `PeerId` has not been found.
ClosestPeersNotFound(PeerId),
/// The value was found.
ValueFound(PeerRecord),
/// The requested record has not been found in the DHT.
ValueNotFound(Key),
/// The record has been successfully inserted into the DHT.
ValuePut(Key),
/// An error has occurred while putting a record into the DHT.
ValuePutFailed(Key),
/// Successfully started providing the given key.
StartedProviding(Key),
/// An error occured while registering as a content provider on the DHT.
StartProvidingFailed(Key),
/// The DHT received a put record request.
PutRecordRequest(Key, Vec<u8>, Option<PeerId>, Option<std::time::Instant>),
/// The providers for [`Key`] were found. Multiple such events can be generated per provider
/// discovery request.
ProvidersFound(Key, Vec<PeerId>),
/// `GET_PROVIDERS` query finished and won't yield any more providers.
NoMoreProviders(Key),
/// `GET_PROVIDERS` query failed and no providers for [`Key`] were found. libp2p also emits
/// this event after already yielding some results via [`DhtEvent::ProvidersFound`].
ProvidersNotFound(Key),
}
/// Type for events generated by networking layer.
#[derive(Debug, Clone)]
#[must_use]
pub enum Event {
/// Event generated by a DHT.
Dht(DhtEvent),
/// Opened a substream with the given node with the given notifications protocol.
///
/// The protocol is always one of the notification protocols that have been registered.
NotificationStreamOpened {
/// Node we opened the substream with.
remote: PeerId,
/// The concerned protocol. Each protocol uses a different substream.
/// This is always equal to the value of
/// `pezsc_network::config::NonDefaultSetConfig::notifications_protocol` of one of the
/// configured sets.
protocol: ProtocolName,
/// If the negotiation didn't use the main name of the protocol (the one in
/// `notifications_protocol`), then this field contains which name has actually been
/// used.
/// Always contains a value equal to the value in
/// `pezsc_network::config::NonDefaultSetConfig::fallback_names`.
negotiated_fallback: Option<ProtocolName>,
/// Role of the remote.
role: ObservedRole,
/// Received handshake.
received_handshake: Vec<u8>,
},
/// Closed a substream with the given node. Always matches a corresponding previous
/// `NotificationStreamOpened` message.
NotificationStreamClosed {
/// Node we closed the substream with.
remote: PeerId,
/// The concerned protocol. Each protocol uses a different substream.
protocol: ProtocolName,
},
/// Received one or more messages from the given node using the given protocol.
NotificationsReceived {
/// Node we received the message from.
remote: PeerId,
/// Concerned protocol and associated message.
messages: Vec<(ProtocolName, Bytes)>,
},
}
+310
View File
@@ -0,0 +1,310 @@
// 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/>.
#![warn(unused_extern_crates)]
#![warn(missing_docs)]
//! Bizinikiwi-specific P2P networking.
//!
//! **Important**: This crate is unstable and the API and usage may change.
//!
//! # Node identities and addresses
//!
//! In a decentralized network, each node possesses a network private key and a network public key.
//! In Bizinikiwi, the keys are based on the ed25519 curve.
//!
//! From a node's public key, we can derive its *identity*. In Bizinikiwi and libp2p, a node's
//! identity is represented with the [`PeerId`] struct. All network communications between nodes on
//! the network use encryption derived from both sides's keys, which means that **identities cannot
//! be faked**.
//!
//! A node's identity uniquely identifies a machine on the network. If you start two or more
//! clients using the same network key, large interferences will happen.
//!
//! # Bizinikiwi's network protocol
//!
//! Bizinikiwi's networking protocol is based upon libp2p. It is at the moment not possible and not
//! planned to permit using something else than the libp2p network stack and the rust-libp2p
//! library. However the libp2p framework is very flexible and the rust-libp2p library could be
//! extended to support a wider range of protocols than what is offered by libp2p.
//!
//! ## Discovery mechanisms
//!
//! In order for our node to join a peer-to-peer network, it has to know a list of nodes that are
//! part of said network. This includes nodes identities and their address (how to reach them).
//! Building such a list is called the **discovery** mechanism. There are three mechanisms that
//! Bizinikiwi uses:
//!
//! - Bootstrap nodes. These are hard-coded node identities and addresses passed alongside with
//! the network configuration.
//! - mDNS. We perform a UDP broadcast on the local network. Nodes that listen may respond with
//! their identity. More info [here](https://github.com/libp2p/specs/blob/master/discovery/mdns.md).
//! mDNS can be disabled in the network configuration.
//! - Kademlia random walk. Once connected, we perform random Kademlia `FIND_NODE` requests on the
//! configured Kademlia DHTs (one per configured chain protocol) in order for nodes to propagate to
//! us their view of the network. More information about Kademlia can be found [on
//! Wikipedia](https://en.wikipedia.org/wiki/Kademlia).
//!
//! ## Connection establishment
//!
//! When node Alice knows node Bob's identity and address, it can establish a connection with Bob.
//! All connections must always use encryption and multiplexing. While some node addresses (eg.
//! addresses using `/quic`) already imply which encryption and/or multiplexing to use, for others
//! the **multistream-select** protocol is used in order to negotiate an encryption layer and/or a
//! multiplexing layer.
//!
//! The connection establishment mechanism is called the **transport**.
//!
//! As of the writing of this documentation, the following base-layer protocols are supported by
//! Bizinikiwi:
//!
//! - TCP/IP for addresses of the form `/ip4/1.2.3.4/tcp/5`. Once the TCP connection is open, an
//! encryption and a multiplexing layer are negotiated on top.
//! - WebSockets for addresses of the form `/ip4/1.2.3.4/tcp/5/ws`. A TCP/IP connection is open and
//! the WebSockets protocol is negotiated on top. Communications then happen inside WebSockets data
//! frames. Encryption and multiplexing are additionally negotiated again inside this channel.
//! - DNS for addresses of the form `/dns/example.com/tcp/5` or `/dns/example.com/tcp/5/ws`. A
//! node's address can contain a domain name.
//! - (All of the above using IPv6 instead of IPv4.)
//!
//! On top of the base-layer protocol, the [Noise](https://noiseprotocol.org/) protocol is
//! negotiated and applied. The exact handshake protocol is experimental and is subject to change.
//!
//! The following multiplexing protocols are supported:
//!
//! - [Yamux](https://github.com/hashicorp/yamux/blob/master/spec.md).
//!
//! ## Substreams
//!
//! Once a connection has been established and uses multiplexing, substreams can be opened. When
//! a substream is open, the **multistream-select** protocol is used to negotiate which protocol
//! to use on that given substream.
//!
//! Protocols that are specific to a certain chain have a `<protocol-id>` in their name. This
//! "protocol ID" is defined in the chain specifications. For example, the protocol ID of Pezkuwi
//! is "hez". In the protocol names below, `<protocol-id>` must be replaced with the corresponding
//! protocol ID.
//!
//! > **Note**: It is possible for the same connection to be used for multiple chains. For example,
//! > one can use both the `/hez/sync/2` and `/sub/sync/2` protocols on the same
//! > connection, provided that the remote supports them.
//!
//! Bizinikiwi uses the following standard libp2p protocols:
//!
//! - **`/ipfs/ping/1.0.0`**. We periodically open an ephemeral substream in order to ping the
//! remote and check whether the connection is still alive. Failure for the remote to reply leads
//! to a disconnection.
//! - **[`/ipfs/id/1.0.0`](https://github.com/libp2p/specs/tree/master/identify)**. We
//! periodically open an ephemeral substream in order to ask information from the remote.
//! - **[`/<protocol_id>/kad`](https://github.com/libp2p/specs/pull/108)**. We periodically open
//! ephemeral substreams for Kademlia random walk queries. Each Kademlia query is done in a
//! separate substream.
//!
//! Additionally, Bizinikiwi uses the following non-libp2p-standard protocols:
//!
//! - **`/bizinikiwi/<protocol-id>/<version>`** (where `<protocol-id>` must be replaced with the
//! protocol ID of the targeted chain, and `<version>` is a number between 2 and 6). For each
//! connection we optionally keep an additional substream for all Bizinikiwi-based communications
//! alive. This protocol is considered legacy, and is progressively being replaced with
//! alternatives. This is designated as "The legacy Bizinikiwi substream" in this documentation. See
//! below for more details.
//! - **`/<protocol-id>/sync/2`** is a request-response protocol (see below) that lets one perform
//! requests for information about blocks. Each request is the encoding of a `BlockRequest` and
//! each response is the encoding of a `BlockResponse`, as defined in the `api.v1.proto` file in
//! this source tree.
//! - **`/<protocol-id>/light/2`** is a request-response protocol (see below) that lets one perform
//! light-client-related requests for information about the state. Each request is the encoding of
//! a `light::Request` and each response is the encoding of a `light::Response`, as defined in the
//! `light.v1.proto` file in this source tree.
//! - **`/<protocol-id>/transactions/1`** is a notifications protocol (see below) where
//! transactions are pushed to other nodes. The handshake is empty on both sides. The message
//! format is a SCALE-encoded list of transactions, where each transaction is an opaque list of
//! bytes.
//! - **`/<protocol-id>/block-announces/1`** is a notifications protocol (see below) where
//! block announces are pushed to other nodes. The handshake is empty on both sides. The message
//! format is a SCALE-encoded tuple containing a block header followed with an opaque list of
//! bytes containing some data associated with this block announcement, e.g. a candidate message.
//! - Notifications protocols that are registered using
//! `NetworkConfiguration::notifications_protocols`. For example: `/paritytech/grandpa/1`. See
//! below for more information.
//!
//! ## The legacy Bizinikiwi substream
//!
//! Bizinikiwi uses a component named the **peerset manager (PSM)**. Through the discovery
//! mechanism, the PSM is aware of the nodes that are part of the network and decides which nodes
//! we should perform Bizinikiwi-based communications with. For these nodes, we open a connection
//! if necessary and open a unique substream for Bizinikiwi-based communications. If the PSM decides
//! that we should disconnect a node, then that substream is closed.
//!
//! For more information about the PSM, see the *sc-peerset* crate.
//!
//! Note that at the moment there is no mechanism in place to solve the issues that arise where the
//! two sides of a connection open the unique substream simultaneously. In order to not run into
//! issues, only the dialer of a connection is allowed to open the unique substream. When the
//! substream is closed, the entire connection is closed as well. This is a bug that will be
//! resolved by deprecating the protocol entirely.
//!
//! Within the unique Bizinikiwi substream, messages encoded using
//! [*parity-scale-codec*](https://github.com/paritytech/parity-scale-codec) are exchanged.
//! The detail of theses messages is not totally in place, but they can be found in the
//! `message.rs` file.
//!
//! Once the substream is open, the first step is an exchange of a *status* message from both
//! sides, containing information such as the chain root hash, head of chain, and so on.
//!
//! Communications within this substream include:
//!
//! - Syncing. Blocks are announced and requested from other nodes.
//! - Light-client requests. When a light client requires information, a random node we have a
//! substream open with is chosen, and the information is requested from it.
//! - Gossiping. Used for example by grandpa.
//!
//! ## Request-response protocols
//!
//! A so-called request-response protocol is defined as follow:
//!
//! - When a substream is opened, the opening side sends a message whose content is
//! protocol-specific. The message must be prefixed with an
//! [LEB128-encoded number](https://en.wikipedia.org/wiki/LEB128) indicating its length. After the
//! message has been sent, the writing side is closed.
//! - The remote sends back the response prefixed with a LEB128-encoded length, and closes its
//! side as well.
//!
//! Each request is performed in a new separate substream.
//!
//! ## Notifications protocols
//!
//! A so-called notifications protocol is defined as follow:
//!
//! - When a substream is opened, the opening side sends a handshake message whose content is
//! protocol-specific. The handshake message must be prefixed with an
//! [LEB128-encoded number](https://en.wikipedia.org/wiki/LEB128) indicating its length. The
//! handshake message can be of length 0, in which case the sender has to send a single `0`.
//! - The receiver then either immediately closes the substream, or answers with its own
//! LEB128-prefixed protocol-specific handshake response. The message can be of length 0, in which
//! case a single `0` has to be sent back.
//! - Once the handshake has completed, the notifications protocol is unidirectional. Only the
//! node which initiated the substream can push notifications. If the remote wants to send
//! notifications as well, it has to open its own undirectional substream.
//! - Each notification must be prefixed with an LEB128-encoded length. The encoding of the
//! messages is specific to each protocol.
//! - Either party can signal that it doesn't want a notifications substream anymore by closing
//! its writing side. The other party should respond by closing its own writing side soon after.
//!
//! The API of `sc-network` allows one to register user-defined notification protocols.
//! `sc-network` automatically tries to open a substream towards each node for which the legacy
//! Substream substream is open. The handshake is then performed automatically.
//!
//! For example, the `sc-consensus-grandpa` crate registers the `/paritytech/grandpa/1`
//! notifications protocol.
//!
//! At the moment, for backwards-compatibility, notification protocols are tied to the legacy
//! Bizinikiwi substream. Additionally, the handshake message is hardcoded to be a single 8-bits
//! integer representing the role of the node:
//!
//! - 1 for a full node.
//! - 2 for a light node.
//! - 4 for an authority.
//!
//! In the future, though, these restrictions will be removed.
//!
//! # Usage
//!
//! Using the `sc-network` crate is done through the [`NetworkWorker`] struct. Create this
//! struct by passing a [`config::Params`], then poll it as if it was a `Future`. You can extract an
//! `Arc<NetworkService>` from the `NetworkWorker`, which can be shared amongst multiple places
//! in order to give orders to the networking.
//!
//! See the [`config`] module for more information about how to configure the networking.
//!
//! After the `NetworkWorker` has been created, the important things to do are:
//!
//! - Calling `NetworkWorker::poll` in order to advance the network. This can be done by
//! dispatching a background task with the [`NetworkWorker`].
//! - Calling `on_block_import` whenever a block is added to the client.
//! - Calling `on_block_finalized` whenever a block is finalized.
//! - Calling `trigger_repropagate` when a transaction is added to the pool.
//!
//! More precise usage details are still being worked on and will likely change in the future.
mod behaviour;
mod bitswap;
mod litep2p;
mod protocol;
#[cfg(test)]
mod mock;
pub mod config;
pub mod discovery;
pub mod error;
pub mod event;
pub mod network_state;
pub mod peer_info;
pub mod peer_store;
pub mod protocol_controller;
pub mod request_responses;
pub mod service;
pub mod transport;
pub mod types;
pub mod utils;
pub use crate::litep2p::Litep2pNetworkBackend;
pub use event::{DhtEvent, Event};
#[doc(inline)]
pub use request_responses::{Config, IfDisconnected, RequestFailure};
pub use pezsc_network_common::{
role::{ObservedRole, Roles},
types::ReputationChange,
};
pub use pezsc_network_types::{
multiaddr::{self, Multiaddr},
PeerId,
};
pub use service::{
metrics::NotificationMetrics,
signature::Signature,
traits::{
KademliaKey, MessageSink, NetworkBackend, NetworkBlock, NetworkDHTProvider,
NetworkEventStream, NetworkPeers, NetworkRequest, NetworkSigner, NetworkStateInfo,
NetworkStatus, NetworkStatusProvider, NetworkSyncForkRequest, NotificationConfig,
NotificationSender as NotificationSenderT, NotificationSenderError,
NotificationSenderReady, NotificationService,
},
DecodingError, Keypair, NetworkService, NetworkWorker, NotificationSender, OutboundFailure,
PublicKey,
};
pub use types::ProtocolName;
/// Log target for `sc-network`.
const LOG_TARGET: &str = "sub-libp2p";
/// The maximum allowed number of established connections per peer.
///
/// Typically, and by design of the network behaviours in this crate,
/// there is a single established connection per peer. However, to
/// avoid unnecessary and nondeterministic connection closure in
/// case of (possibly repeated) simultaneous dialing attempts between
/// two peers, the per-peer connection limit is not set to 1 but 2.
const MAX_CONNECTIONS_PER_PEER: usize = 2;
/// The maximum number of concurrent established connections that were incoming.
const MAX_CONNECTIONS_ESTABLISHED_INCOMING: u32 = 10_000;
/// Maximum response size limit.
pub const MAX_RESPONSE_SIZE: u64 = 16 * 1024 * 1024;
@@ -0,0 +1,962 @@
// 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/>.
//! libp2p-related discovery code for litep2p backend.
use crate::{
config::{
NetworkConfiguration, ProtocolId, KADEMLIA_MAX_PROVIDER_KEYS, KADEMLIA_PROVIDER_RECORD_TTL,
KADEMLIA_PROVIDER_REPUBLISH_INTERVAL,
},
peer_store::PeerStoreProvider,
};
use array_bytes::bytes2hex;
use futures::{FutureExt, Stream};
use futures_timer::Delay;
use ip_network::IpNetwork;
use litep2p::{
protocol::{
libp2p::{
identify::{Config as IdentifyConfig, IdentifyEvent},
kademlia::{
Config as KademliaConfig, ConfigBuilder as KademliaConfigBuilder, ContentProvider,
IncomingRecordValidationMode, KademliaEvent, KademliaHandle, PeerRecord, QueryId,
Quorum, Record, RecordKey,
},
ping::{Config as PingConfig, PingEvent},
},
mdns::{Config as MdnsConfig, MdnsEvent},
},
types::multiaddr::{Multiaddr, Protocol},
PeerId, ProtocolName,
};
use parking_lot::RwLock;
use pezsc_network_types::kad::Key as KademliaKey;
use schnellru::{ByLength, LruMap};
use std::{
cmp,
collections::{HashMap, HashSet, VecDeque},
iter,
num::NonZeroUsize,
pin::Pin,
sync::Arc,
task::{Context, Poll},
time::{Duration, Instant},
};
/// Logging target for the file.
const LOG_TARGET: &str = "sub-libp2p::discovery";
/// Kademlia query interval.
const KADEMLIA_QUERY_INTERVAL: Duration = Duration::from_secs(5);
/// mDNS query interval.
const MDNS_QUERY_INTERVAL: Duration = Duration::from_secs(30);
/// The minimum number of peers we expect an answer before we terminate the request.
const GET_RECORD_REDUNDANCY_FACTOR: usize = 4;
/// The maximum number of tracked external addresses we allow.
const MAX_EXTERNAL_ADDRESSES: u32 = 32;
/// Number of times observed address is received from different peers before it is confirmed as
/// external.
const MIN_ADDRESS_CONFIRMATIONS: usize = 3;
/// Quorum threshold to interpret `PUT_VALUE` & `ADD_PROVIDER` as successful.
///
/// As opposed to libp2p, litep2p does not finish the query as soon as the required number of
/// peers have reached. Instead, it tries to put the record to all target peers (typically 20) and
/// uses the quorum setting only to determine the success of the query.
///
/// We set the threshold to 50% of the target peers to account for unreachable peers. The actual
/// number of stored records may be higher.
const QUORUM_THRESHOLD: NonZeroUsize = NonZeroUsize::new(10).expect("10 > 0; qed");
/// Discovery events.
#[derive(Debug)]
pub enum DiscoveryEvent {
/// Ping RTT measured for peer.
Ping {
/// Remote peer ID.
peer: PeerId,
/// Ping round-trip time.
rtt: Duration,
},
/// Peer identified over `/ipfs/identify/1.0.0` protocol.
Identified {
/// Peer ID.
peer: PeerId,
/// Listen addresses.
listen_addresses: Vec<Multiaddr>,
/// Supported protocols.
supported_protocols: HashSet<ProtocolName>,
},
/// One or more addresses discovered.
///
/// This event is emitted when a new peer is discovered over mDNS.
Discovered {
/// Discovered addresses.
addresses: Vec<Multiaddr>,
},
/// Routing table has been updated.
RoutingTableUpdate {
/// Peers that were added to routing table.
peers: HashSet<PeerId>,
},
/// New external address discovered.
ExternalAddressDiscovered {
/// Discovered address.
address: Multiaddr,
},
/// The external address has expired.
///
/// This happens when the internal buffers exceed the maximum number of external addresses,
/// and this address is the oldest one.
ExternalAddressExpired {
/// Expired address.
address: Multiaddr,
},
/// `FIND_NODE` query succeeded.
FindNodeSuccess {
/// Query ID.
query_id: QueryId,
/// Target.
target: PeerId,
/// Found peers.
peers: Vec<(PeerId, Vec<Multiaddr>)>,
},
/// `GetRecord` query succeeded.
GetRecordSuccess {
/// Query ID.
query_id: QueryId,
},
/// Record was found from the DHT.
GetRecordPartialResult {
/// Query ID.
query_id: QueryId,
/// Record.
record: PeerRecord,
},
/// Record was successfully stored on the DHT.
PutRecordSuccess {
/// Query ID.
query_id: QueryId,
},
/// Providers were successfully retrieved.
GetProvidersSuccess {
/// Query ID.
query_id: QueryId,
/// Found providers sorted by distance to provided key.
providers: Vec<ContentProvider>,
},
/// Provider was successfully published.
AddProviderSuccess {
/// Query ID.
query_id: QueryId,
/// Provided key.
provided_key: RecordKey,
},
/// Query failed.
QueryFailed {
/// Query ID.
query_id: QueryId,
},
/// Incoming record to store.
IncomingRecord {
/// Record.
record: Record,
},
/// Started a random Kademlia query.
RandomKademliaStarted,
}
/// Discovery.
pub struct Discovery {
/// Local peer ID.
local_peer_id: litep2p::PeerId,
/// Ping event stream.
ping_event_stream: Box<dyn Stream<Item = PingEvent> + Send + Unpin>,
/// Identify event stream.
identify_event_stream: Box<dyn Stream<Item = IdentifyEvent> + Send + Unpin>,
/// mDNS event stream, if enabled.
mdns_event_stream: Option<Box<dyn Stream<Item = MdnsEvent> + Send + Unpin>>,
/// Kademlia handle.
kademlia_handle: KademliaHandle,
/// `Peerstore` handle.
_peerstore_handle: Arc<dyn PeerStoreProvider>,
/// Next Kademlia query for a random peer ID.
///
/// If `None`, there is currently a query pending.
next_kad_query: Option<Delay>,
/// Active `FIND_NODE` query if it exists.
random_walk_query_id: Option<QueryId>,
/// Pending events.
pending_events: VecDeque<DiscoveryEvent>,
/// Allow non-global addresses in the DHT.
allow_non_global_addresses: bool,
/// Protocols supported by the local node.
local_protocols: HashSet<ProtocolName>,
/// Public addresses.
public_addresses: HashSet<Multiaddr>,
/// Listen addresses.
listen_addresses: Arc<RwLock<HashSet<Multiaddr>>>,
/// External address confirmations.
address_confirmations: LruMap<Multiaddr, HashSet<PeerId>>,
/// Delay to next `FIND_NODE` query.
duration_to_next_find_query: Duration,
}
/// Legacy (fallback) Kademlia protocol name based on `protocol_id`.
fn legacy_kademlia_protocol_name(id: &ProtocolId) -> ProtocolName {
ProtocolName::from(format!("/{}/kad", id.as_ref()))
}
/// Kademlia protocol name based on `genesis_hash` and `fork_id`.
fn kademlia_protocol_name<Hash: AsRef<[u8]>>(
genesis_hash: Hash,
fork_id: Option<&str>,
) -> ProtocolName {
let genesis_hash_hex = bytes2hex("", genesis_hash.as_ref());
let protocol = if let Some(fork_id) = fork_id {
format!("/{}/{}/kad", genesis_hash_hex, fork_id)
} else {
format!("/{}/kad", genesis_hash_hex)
};
ProtocolName::from(protocol)
}
impl Discovery {
/// Create new [`Discovery`].
///
/// Enables `/ipfs/ping/1.0.0` and `/ipfs/identify/1.0.0` by default and starts
/// the mDNS peer discovery if it was enabled.
pub fn new<Hash: AsRef<[u8]> + Clone>(
local_peer_id: litep2p::PeerId,
config: &NetworkConfiguration,
genesis_hash: Hash,
fork_id: Option<&str>,
protocol_id: &ProtocolId,
known_peers: HashMap<PeerId, Vec<Multiaddr>>,
listen_addresses: Arc<RwLock<HashSet<Multiaddr>>>,
_peerstore_handle: Arc<dyn PeerStoreProvider>,
) -> (Self, PingConfig, IdentifyConfig, KademliaConfig, Option<MdnsConfig>) {
let (ping_config, ping_event_stream) = PingConfig::default();
let user_agent = format!("{} ({}) (litep2p)", config.client_version, config.node_name);
let (identify_config, identify_event_stream) =
IdentifyConfig::new("/bizinikiwi/1.0".to_string(), Some(user_agent));
let (mdns_config, mdns_event_stream) = match config.transport {
crate::config::TransportConfig::Normal { enable_mdns, .. } => match enable_mdns {
true => {
let (mdns_config, mdns_event_stream) = MdnsConfig::new(MDNS_QUERY_INTERVAL);
(Some(mdns_config), Some(mdns_event_stream))
},
false => (None, None),
},
_ => panic!("memory transport not supported"),
};
let (kademlia_config, kademlia_handle) = {
let protocol_names = vec![
kademlia_protocol_name(genesis_hash.clone(), fork_id),
legacy_kademlia_protocol_name(protocol_id),
];
KademliaConfigBuilder::new()
.with_known_peers(known_peers)
.with_protocol_names(protocol_names)
.with_incoming_records_validation_mode(IncomingRecordValidationMode::Manual)
.with_provider_record_ttl(KADEMLIA_PROVIDER_RECORD_TTL)
.with_provider_refresh_interval(KADEMLIA_PROVIDER_REPUBLISH_INTERVAL)
.with_max_provider_keys(KADEMLIA_MAX_PROVIDER_KEYS)
.build()
};
(
Self {
local_peer_id,
ping_event_stream,
identify_event_stream,
mdns_event_stream,
kademlia_handle,
_peerstore_handle,
listen_addresses,
random_walk_query_id: None,
pending_events: VecDeque::new(),
duration_to_next_find_query: Duration::from_secs(1),
address_confirmations: LruMap::new(ByLength::new(MAX_EXTERNAL_ADDRESSES)),
allow_non_global_addresses: config.allow_non_globals_in_dht,
public_addresses: config.public_addresses.iter().cloned().map(Into::into).collect(),
next_kad_query: Some(Delay::new(KADEMLIA_QUERY_INTERVAL)),
local_protocols: HashSet::from_iter([kademlia_protocol_name(
genesis_hash,
fork_id,
)]),
},
ping_config,
identify_config,
kademlia_config,
mdns_config,
)
}
/// Add known peer to `Kademlia`.
#[allow(unused)]
pub async fn add_known_peer(&mut self, peer: PeerId, addresses: Vec<Multiaddr>) {
self.kademlia_handle.add_known_peer(peer, addresses).await;
}
/// Add self-reported addresses to routing table if `peer` supports
/// at least one of the locally supported DHT protocol.
pub async fn add_self_reported_address(
&mut self,
peer: PeerId,
supported_protocols: HashSet<ProtocolName>,
addresses: Vec<Multiaddr>,
) {
if self.local_protocols.is_disjoint(&supported_protocols) {
log::trace!(
target: LOG_TARGET,
"Ignoring self-reported address of peer {peer} as remote node is not part of the \
Kademlia DHT supported by the local node.",
);
return;
}
let addresses = addresses
.into_iter()
.filter_map(|address| {
if !self.allow_non_global_addresses && !Discovery::can_add_to_dht(&address) {
log::trace!(
target: LOG_TARGET,
"ignoring self-reported non-global address {address} from {peer}."
);
return None;
}
Some(address)
})
.collect();
log::trace!(
target: LOG_TARGET,
"add self-reported addresses for {peer:?}: {addresses:?}",
);
self.kademlia_handle.add_known_peer(peer, addresses).await;
}
/// Start Kademlia `FIND_NODE` query for `target`.
pub async fn find_node(&mut self, target: PeerId) -> QueryId {
self.kademlia_handle.find_node(target).await
}
/// Start Kademlia `GET_VALUE` query for `key`.
pub async fn get_value(&mut self, key: KademliaKey) -> QueryId {
self.kademlia_handle
.get_record(
RecordKey::new(&key.to_vec()),
Quorum::N(NonZeroUsize::new(GET_RECORD_REDUNDANCY_FACTOR).unwrap()),
)
.await
}
/// Publish value on the DHT using Kademlia `PUT_VALUE`.
pub async fn put_value(&mut self, key: KademliaKey, value: Vec<u8>) -> QueryId {
self.kademlia_handle
.put_record(
Record::new(RecordKey::new(&key.to_vec()), value),
Quorum::N(QUORUM_THRESHOLD),
)
.await
}
/// Put record to given peers.
pub async fn put_value_to_peers(
&mut self,
record: Record,
peers: Vec<pezsc_network_types::PeerId>,
update_local_storage: bool,
) -> QueryId {
self.kademlia_handle
.put_record_to_peers(
record,
peers.into_iter().map(|peer| peer.into()).collect(),
update_local_storage,
// These are the peers that just returned the record to us in authority-discovery,
// so we assume they are all reachable.
Quorum::All,
)
.await
}
/// Store record in the local DHT store.
pub async fn store_record(
&mut self,
key: KademliaKey,
value: Vec<u8>,
publisher: Option<pezsc_network_types::PeerId>,
expires: Option<Instant>,
) {
log::debug!(
target: LOG_TARGET,
"Storing DHT record with key {key:?}, originally published by {publisher:?}, \
expires {expires:?}.",
);
self.kademlia_handle
.store_record(Record {
key: RecordKey::new(&key.to_vec()),
value,
publisher: publisher.map(Into::into),
expires,
})
.await;
}
/// Start providing `key`.
pub async fn start_providing(&mut self, key: KademliaKey) -> QueryId {
self.kademlia_handle
.start_providing(key.into(), Quorum::N(QUORUM_THRESHOLD))
.await
}
/// Stop providing `key`.
pub async fn stop_providing(&mut self, key: KademliaKey) {
self.kademlia_handle.stop_providing(key.into()).await;
}
/// Get providers for `key`.
pub async fn get_providers(&mut self, key: KademliaKey) -> QueryId {
self.kademlia_handle.get_providers(key.into()).await
}
/// Check if the observed address is a known address.
fn is_known_address(known: &Multiaddr, observed: &Multiaddr) -> bool {
let mut known = known.iter();
let mut observed = observed.iter();
loop {
match (known.next(), observed.next()) {
(None, None) => return true,
(None, Some(Protocol::P2p(_))) => return true,
(Some(Protocol::P2p(_)), None) => return true,
(known, observed) if known != observed => return false,
_ => {},
}
}
}
/// Can `address` be added to DHT.
fn can_add_to_dht(address: &Multiaddr) -> bool {
let ip = match address.iter().next() {
Some(Protocol::Ip4(ip)) => IpNetwork::from(ip),
Some(Protocol::Ip6(ip)) => IpNetwork::from(ip),
Some(Protocol::Dns(_)) | Some(Protocol::Dns4(_)) | Some(Protocol::Dns6(_)) =>
return true,
_ => return false,
};
ip.is_global()
}
/// Check if `address` can be considered a new external address.
///
/// If this address replaces an older address, the expired address is returned.
fn is_new_external_address(
&mut self,
address: &Multiaddr,
peer: PeerId,
) -> (bool, Option<Multiaddr>) {
log::trace!(target: LOG_TARGET, "verify new external address: {address}");
if !self.allow_non_global_addresses && !Discovery::can_add_to_dht(&address) {
log::trace!(
target: LOG_TARGET,
"ignoring externally reported non-global address {address} from {peer}."
);
return (false, None);
}
// is the address one of our known addresses
if self
.listen_addresses
.read()
.iter()
.chain(self.public_addresses.iter())
.any(|known_address| Discovery::is_known_address(&known_address, &address))
{
return (true, None);
}
match self.address_confirmations.get(address) {
Some(confirmations) => {
confirmations.insert(peer);
if confirmations.len() >= MIN_ADDRESS_CONFIRMATIONS {
return (true, None);
}
},
None => {
let oldest = (self.address_confirmations.len() >=
self.address_confirmations.limiter().max_length() as usize)
.then(|| {
self.address_confirmations.pop_oldest().map(|(address, peers)| {
if peers.len() >= MIN_ADDRESS_CONFIRMATIONS {
return Some(address);
} else {
None
}
})
})
.flatten()
.flatten();
self.address_confirmations.insert(address.clone(), iter::once(peer).collect());
return (false, oldest);
},
}
(false, None)
}
}
impl Stream for Discovery {
type Item = DiscoveryEvent;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = Pin::into_inner(self);
if let Some(event) = this.pending_events.pop_front() {
return Poll::Ready(Some(event));
}
if let Some(mut delay) = this.next_kad_query.take() {
match delay.poll_unpin(cx) {
Poll::Pending => {
this.next_kad_query = Some(delay);
},
Poll::Ready(()) => {
let peer = PeerId::random();
log::trace!(target: LOG_TARGET, "start next kademlia query for {peer:?}");
match this.kademlia_handle.try_find_node(peer) {
Ok(query_id) => {
this.random_walk_query_id = Some(query_id);
return Poll::Ready(Some(DiscoveryEvent::RandomKademliaStarted));
},
Err(()) => {
this.duration_to_next_find_query = cmp::min(
this.duration_to_next_find_query * 2,
Duration::from_secs(60),
);
this.next_kad_query =
Some(Delay::new(this.duration_to_next_find_query));
},
}
},
}
}
match Pin::new(&mut this.kademlia_handle).poll_next(cx) {
Poll::Pending => {},
Poll::Ready(None) => return Poll::Ready(None),
Poll::Ready(Some(KademliaEvent::FindNodeSuccess { query_id, peers, .. }))
if Some(query_id) == this.random_walk_query_id =>
{
// the addresses are already inserted into the DHT and in `TransportManager` so
// there is no need to add them again. The found peers must be registered to
// `Peerstore` so other protocols are aware of them through `Peerset`.
log::trace!(target: LOG_TARGET, "dht random walk yielded {} peers", peers.len());
this.next_kad_query = Some(Delay::new(KADEMLIA_QUERY_INTERVAL));
return Poll::Ready(Some(DiscoveryEvent::RoutingTableUpdate {
peers: peers.into_iter().map(|(peer, _)| peer).collect(),
}));
},
Poll::Ready(Some(KademliaEvent::FindNodeSuccess { query_id, target, peers })) => {
log::trace!(target: LOG_TARGET, "find node query yielded {} peers", peers.len());
return Poll::Ready(Some(DiscoveryEvent::FindNodeSuccess {
query_id,
target,
peers,
}));
},
Poll::Ready(Some(KademliaEvent::RoutingTableUpdate { peers })) => {
log::trace!(target: LOG_TARGET, "routing table update, discovered {} peers", peers.len());
return Poll::Ready(Some(DiscoveryEvent::RoutingTableUpdate {
peers: peers.into_iter().collect(),
}));
},
Poll::Ready(Some(KademliaEvent::GetRecordSuccess { query_id })) => {
log::trace!(
target: LOG_TARGET,
"`GET_RECORD` succeeded for {query_id:?}",
);
return Poll::Ready(Some(DiscoveryEvent::GetRecordSuccess { query_id }));
},
Poll::Ready(Some(KademliaEvent::GetRecordPartialResult { query_id, record })) => {
log::trace!(
target: LOG_TARGET,
"`GET_RECORD` intermediary succeeded for {query_id:?}: {record:?}",
);
return Poll::Ready(Some(DiscoveryEvent::GetRecordPartialResult {
query_id,
record,
}));
},
Poll::Ready(Some(KademliaEvent::PutRecordSuccess { query_id, key: _ })) =>
return Poll::Ready(Some(DiscoveryEvent::PutRecordSuccess { query_id })),
Poll::Ready(Some(KademliaEvent::QueryFailed { query_id })) => {
match this.random_walk_query_id == Some(query_id) {
true => {
this.random_walk_query_id = None;
this.duration_to_next_find_query =
cmp::min(this.duration_to_next_find_query * 2, Duration::from_secs(60));
this.next_kad_query = Some(Delay::new(this.duration_to_next_find_query));
},
false => return Poll::Ready(Some(DiscoveryEvent::QueryFailed { query_id })),
}
},
Poll::Ready(Some(KademliaEvent::IncomingRecord { record })) => {
log::trace!(
target: LOG_TARGET,
"incoming `PUT_RECORD` request with key {:?} from publisher {:?}",
record.key,
record.publisher,
);
return Poll::Ready(Some(DiscoveryEvent::IncomingRecord { record }));
},
Poll::Ready(Some(KademliaEvent::GetProvidersSuccess {
provided_key,
providers,
query_id,
})) => {
log::trace!(
target: LOG_TARGET,
"`GET_PROVIDERS` for {query_id:?} with {provided_key:?} yielded {providers:?}",
);
return Poll::Ready(Some(DiscoveryEvent::GetProvidersSuccess {
query_id,
providers,
}));
},
Poll::Ready(Some(KademliaEvent::AddProviderSuccess { query_id, provided_key })) => {
log::trace!(
target: LOG_TARGET,
"`ADD_PROVIDER` for {query_id:?} with {provided_key:?} succeeded",
);
return Poll::Ready(Some(DiscoveryEvent::AddProviderSuccess {
query_id,
provided_key,
}));
},
// We do not validate incoming providers.
Poll::Ready(Some(KademliaEvent::IncomingProvider { .. })) => {},
}
match Pin::new(&mut this.identify_event_stream).poll_next(cx) {
Poll::Pending => {},
Poll::Ready(None) => return Poll::Ready(None),
Poll::Ready(Some(IdentifyEvent::PeerIdentified {
peer,
listen_addresses,
supported_protocols,
observed_address,
..
})) => {
let observed_address =
if let Some(Protocol::P2p(peer_id)) = observed_address.iter().last() {
if peer_id != *this.local_peer_id.as_ref() {
log::warn!(
target: LOG_TARGET,
"Discovered external address for a peer that is not us: {observed_address}",
);
None
} else {
Some(observed_address)
}
} else {
Some(observed_address.with(Protocol::P2p(this.local_peer_id.into())))
};
// Ensure that an external address with a different peer ID does not have
// side effects of evicting other external addresses via `ExternalAddressExpired`.
if let Some(observed_address) = observed_address {
let (is_new, expired_address) =
this.is_new_external_address(&observed_address, peer);
if let Some(expired_address) = expired_address {
log::trace!(
target: LOG_TARGET,
"Removing expired external address expired={expired_address} is_new={is_new} observed={observed_address}",
);
this.pending_events.push_back(DiscoveryEvent::ExternalAddressExpired {
address: expired_address,
});
}
if is_new {
this.pending_events.push_back(DiscoveryEvent::ExternalAddressDiscovered {
address: observed_address.clone(),
});
}
}
return Poll::Ready(Some(DiscoveryEvent::Identified {
peer,
listen_addresses,
supported_protocols,
}));
},
}
match Pin::new(&mut this.ping_event_stream).poll_next(cx) {
Poll::Pending => {},
Poll::Ready(None) => return Poll::Ready(None),
Poll::Ready(Some(PingEvent::Ping { peer, ping })) =>
return Poll::Ready(Some(DiscoveryEvent::Ping { peer, rtt: ping })),
}
if let Some(ref mut mdns_event_stream) = &mut this.mdns_event_stream {
match Pin::new(mdns_event_stream).poll_next(cx) {
Poll::Pending => {},
Poll::Ready(None) => return Poll::Ready(None),
Poll::Ready(Some(MdnsEvent::Discovered(addresses))) =>
return Poll::Ready(Some(DiscoveryEvent::Discovered { addresses })),
}
}
Poll::Pending
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::AtomicU32;
use crate::{
config::ProtocolId,
peer_store::{PeerStore, PeerStoreProvider},
};
use futures::{stream::FuturesUnordered, StreamExt};
use pezsp_core::H256;
use pezsp_tracing::tracing_subscriber;
use litep2p::{
config::ConfigBuilder as Litep2pConfigBuilder, transport::tcp::config::Config as TcpConfig,
Litep2p,
};
#[tokio::test]
async fn litep2p_discovery_works() {
let _ = tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.try_init();
let mut known_peers = HashMap::new();
let genesis_hash = H256::from_low_u64_be(1);
let fork_id = Some("test-fork-id");
let protocol_id = ProtocolId::from("hez");
// Build backends such that the first peer is known to all other peers.
let backends = (0..10)
.map(|i| {
let keypair = litep2p::crypto::ed25519::Keypair::generate();
let peer_id: PeerId = keypair.public().to_peer_id().into();
let listen_addresses = Arc::new(RwLock::new(HashSet::new()));
let peer_store = PeerStore::new(vec![], None);
let peer_store_handle: Arc<dyn PeerStoreProvider> = Arc::new(peer_store.handle());
let (discovery, ping_config, identify_config, kademlia_config, _mdns) =
Discovery::new(
peer_id,
&NetworkConfiguration::new_local(),
genesis_hash,
fork_id,
&protocol_id,
known_peers.clone(),
listen_addresses.clone(),
peer_store_handle,
);
let config = Litep2pConfigBuilder::new()
.with_keypair(keypair)
.with_tcp(TcpConfig {
listen_addresses: vec!["/ip6/::1/tcp/0".parse().unwrap()],
..Default::default()
})
.with_libp2p_ping(ping_config)
.with_libp2p_identify(identify_config)
.with_libp2p_kademlia(kademlia_config)
.build();
let mut litep2p = Litep2p::new(config).unwrap();
let addresses = litep2p.listen_addresses().cloned().collect::<Vec<_>>();
// Propagate addresses to discovery.
addresses.iter().for_each(|address| {
listen_addresses.write().insert(address.clone());
});
// Except the first peer, all other peers know the first peer addresses.
if i == 0 {
log::info!(target: LOG_TARGET, "First peer is {peer_id:?} with addresses {addresses:?}");
known_peers.insert(peer_id, addresses.clone());
} else {
let (peer, addresses) = known_peers.iter().next().unwrap();
let result = litep2p.add_known_address(*peer, addresses.into_iter().cloned());
log::info!(target: LOG_TARGET, "{peer_id:?}: Adding known peer {peer:?} with addresses {addresses:?} result={result:?}");
}
(peer_id, litep2p, discovery)
})
.collect::<Vec<_>>();
let total_peers = backends.len() as u32;
let remaining_peers =
backends.iter().map(|(peer_id, _, _)| *peer_id).collect::<HashSet<_>>();
let first_peer = *known_peers.iter().next().unwrap().0;
// Each backend must discover the whole network.
let mut futures = FuturesUnordered::new();
let num_finished = Arc::new(AtomicU32::new(0));
for (peer_id, mut litep2p, mut discovery) in backends {
// Remove the local peer id from the set.
let mut remaining_peers = remaining_peers.clone();
remaining_peers.remove(&peer_id);
let num_finished = num_finished.clone();
let future = async move {
log::info!(target: LOG_TARGET, "{peer_id:?} starting loop");
if peer_id != first_peer {
log::info!(target: LOG_TARGET, "{peer_id:?} dialing {first_peer:?}");
litep2p.dial(&first_peer).await.unwrap();
}
loop {
// We need to keep the network alive until all peers are discovered.
if num_finished.load(std::sync::atomic::Ordering::Relaxed) == total_peers {
log::info!(target: LOG_TARGET, "{peer_id:?} all peers discovered");
break;
}
tokio::select! {
// Drive litep2p backend forward.
event = litep2p.next_event() => {
log::info!(target: LOG_TARGET, "{peer_id:?} Litep2p event: {event:?}");
},
// Detect discovery events.
event = discovery.next() => {
match event.unwrap() {
// We have discovered the peer via kademlia and established
// a connection on the identify protocol.
DiscoveryEvent::Identified { peer, .. } => {
log::info!(target: LOG_TARGET, "{peer_id:?} Peer {peer} identified");
remaining_peers.remove(&peer);
if remaining_peers.is_empty() {
log::info!(target: LOG_TARGET, "{peer_id:?} All peers discovered");
num_finished.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
}
},
event => {
log::info!(target: LOG_TARGET, "{peer_id:?} Discovery event: {event:?}");
}
}
}
}
}
};
futures.push(future);
}
// Futures will exit when all peers are discovered.
tokio::time::timeout(Duration::from_secs(60), futures.next())
.await
.expect("All peers should finish within 60 seconds");
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,481 @@
// 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/>.
//! `Peerstore` implementation for `litep2p`.
//!
//! `Peerstore` is responsible for storing information about remote peers
//! such as their addresses, reputations, supported protocols etc.
use crate::{
peer_store::{PeerStoreProvider, ProtocolHandle},
service::{metrics::PeerStoreMetrics, traits::PeerStore},
ObservedRole, ReputationChange,
};
use parking_lot::Mutex;
use prometheus_endpoint::Registry;
use wasm_timer::Delay;
use pezsc_network_types::PeerId;
use std::{
collections::{HashMap, HashSet},
sync::Arc,
time::{Duration, Instant},
};
/// Logging target for the file.
const LOG_TARGET: &str = "sub-libp2p::peerstore";
/// We don't accept nodes whose reputation is under this value.
pub const BANNED_THRESHOLD: i32 = 71 * (i32::MIN / 100);
/// Relative decrement of a reputation value that is applied every second. I.e., for inverse
/// decrement of 200 we decrease absolute value of the reputation by 1/200.
///
/// This corresponds to a factor of `k = 0.995`, where k = 1 - 1 / INVERSE_DECREMENT.
///
/// It takes ~ `ln(0.5) / ln(k)` seconds to reduce the reputation by half, or 138.63 seconds for the
/// values above.
///
/// In this setup:
/// - `i32::MAX` becomes 0 in exactly 3544 seconds, or approximately 59 minutes
/// - `i32::MIN` escapes the banned threshold in 69 seconds
const INVERSE_DECREMENT: i32 = 200;
/// Amount of time between the moment we last updated the [`PeerStore`] entry and the moment we
/// remove it, once the reputation value reaches 0.
const FORGET_AFTER: Duration = Duration::from_secs(3600);
/// Peer information.
#[derive(Debug, Clone, Copy)]
struct PeerInfo {
/// Reputation of the peer.
reputation: i32,
/// Instant when the peer was last updated.
last_updated: Instant,
/// Role of the peer, if known.
role: Option<ObservedRole>,
}
impl Default for PeerInfo {
fn default() -> Self {
Self { reputation: 0i32, last_updated: Instant::now(), role: None }
}
}
impl PeerInfo {
fn is_banned(&self) -> bool {
self.reputation < BANNED_THRESHOLD
}
fn add_reputation(&mut self, increment: i32) {
self.reputation = self.reputation.saturating_add(increment);
self.bump_last_updated();
}
fn decay_reputation(&mut self, seconds_passed: u64) {
// Note that decaying the reputation value happens "on its own",
// so we don't do `bump_last_updated()`.
for _ in 0..seconds_passed {
let mut diff = self.reputation / INVERSE_DECREMENT;
if diff == 0 && self.reputation < 0 {
diff = -1;
} else if diff == 0 && self.reputation > 0 {
diff = 1;
}
self.reputation = self.reputation.saturating_sub(diff);
if self.reputation == 0 {
break;
}
}
}
fn bump_last_updated(&mut self) {
self.last_updated = Instant::now();
}
}
#[derive(Debug, Default)]
pub struct PeerstoreHandleInner {
peers: HashMap<PeerId, PeerInfo>,
protocols: Vec<Arc<dyn ProtocolHandle>>,
metrics: Option<PeerStoreMetrics>,
}
#[derive(Debug, Clone, Default)]
pub struct PeerstoreHandle(Arc<Mutex<PeerstoreHandleInner>>);
impl PeerstoreHandle {
/// Constructs a new [`PeerstoreHandle`].
fn new(
peers: HashMap<PeerId, PeerInfo>,
protocols: Vec<Arc<dyn ProtocolHandle>>,
metrics: Option<PeerStoreMetrics>,
) -> Self {
Self(Arc::new(Mutex::new(PeerstoreHandleInner { peers, protocols, metrics })))
}
/// Add known peer to [`Peerstore`].
pub fn add_known_peer(&self, peer: PeerId) {
self.0
.lock()
.peers
.insert(peer, PeerInfo { reputation: 0i32, last_updated: Instant::now(), role: None });
}
pub fn peer_count(&self) -> usize {
self.0.lock().peers.len()
}
fn progress_time(&self, seconds_passed: u64) {
if seconds_passed == 0 {
return;
}
let mut lock = self.0.lock();
// Drive reputation values towards 0.
lock.peers
.iter_mut()
.for_each(|(_, info)| info.decay_reputation(seconds_passed));
// Retain only entries with non-zero reputation values or not expired ones.
let now = Instant::now();
let mut num_banned_peers = 0;
lock.peers.retain(|_, info| {
if info.is_banned() {
num_banned_peers += 1;
}
info.reputation != 0 || info.last_updated + FORGET_AFTER > now
});
if let Some(metrics) = &lock.metrics {
metrics.num_discovered.set(lock.peers.len() as u64);
metrics.num_banned_peers.set(num_banned_peers);
}
}
}
impl PeerStoreProvider for PeerstoreHandle {
fn is_banned(&self, peer: &PeerId) -> bool {
self.0.lock().peers.get(peer).map_or(false, |info| info.is_banned())
}
/// Register a protocol handle to disconnect peers whose reputation drops below the threshold.
fn register_protocol(&self, protocol_handle: Arc<dyn ProtocolHandle>) {
self.0.lock().protocols.push(protocol_handle);
}
/// Report peer disconnection for reputation adjustment.
fn report_disconnect(&self, _peer: PeerId) {
unimplemented!();
}
/// Adjust peer reputation.
fn report_peer(&self, peer_id: PeerId, change: ReputationChange) {
let mut lock = self.0.lock();
let peer_info = lock.peers.entry(peer_id).or_default();
let was_banned = peer_info.is_banned();
peer_info.add_reputation(change.value);
let peer_reputation = peer_info.reputation;
log::trace!(
target: LOG_TARGET,
"Report {}: {:+} to {}. Reason: {}.",
peer_id,
change.value,
peer_reputation,
change.reason,
);
if !peer_info.is_banned() {
if was_banned {
log::info!(
target: LOG_TARGET,
"Peer {} is now unbanned: {:+} to {}. Reason: {}.",
peer_id,
change.value,
peer_reputation,
change.reason,
);
}
return;
}
// Peer is currently banned, disconnect it from all protocols.
lock.protocols.iter().for_each(|handle| handle.disconnect_peer(peer_id.into()));
// The peer is banned for the first time.
if !was_banned {
log::warn!(
target: LOG_TARGET,
"Report {}: {:+} to {}. Reason: {}. Banned, disconnecting.",
peer_id,
change.value,
peer_reputation,
change.reason,
);
return;
}
// The peer was already banned and it got another negative report.
// This may happen during a batch report.
if change.value < 0 {
log::debug!(
target: LOG_TARGET,
"Report {}: {:+} to {}. Reason: {}. Misbehaved during the ban threshold.",
peer_id,
change.value,
peer_reputation,
change.reason,
);
}
}
/// Set peer role.
fn set_peer_role(&self, peer: &PeerId, role: ObservedRole) {
self.0.lock().peers.entry(*peer).or_default().role = Some(role);
}
/// Get peer reputation.
fn peer_reputation(&self, peer: &PeerId) -> i32 {
self.0.lock().peers.get(peer).map_or(0i32, |info| info.reputation)
}
/// Get peer role, if available.
fn peer_role(&self, peer: &PeerId) -> Option<ObservedRole> {
self.0.lock().peers.get(peer).and_then(|info| info.role)
}
/// Get candidates with highest reputations for initiating outgoing connections.
fn outgoing_candidates(&self, count: usize, ignored: HashSet<PeerId>) -> Vec<PeerId> {
let handle = self.0.lock();
let mut candidates = handle
.peers
.iter()
.filter_map(|(peer, info)| {
(!ignored.contains(&peer) && !info.is_banned()).then_some((*peer, info.reputation))
})
.collect::<Vec<(PeerId, _)>>();
candidates.sort_by(|(_, a), (_, b)| b.cmp(a));
candidates
.into_iter()
.take(count)
.map(|(peer, _score)| peer)
.collect::<Vec<_>>()
}
/// Add known peer.
fn add_known_peer(&self, peer: PeerId) {
self.0.lock().peers.entry(peer).or_default().last_updated = Instant::now();
}
}
/// `Peerstore` handle for testing.
///
/// This instance of `Peerstore` is not shared between protocols.
#[cfg(test)]
pub fn peerstore_handle_test() -> PeerstoreHandle {
PeerstoreHandle(Arc::new(Mutex::new(Default::default())))
}
/// Peerstore implementation.
pub struct Peerstore {
/// Handle to `Peerstore`.
peerstore_handle: PeerstoreHandle,
}
impl Peerstore {
/// Create new [`Peerstore`].
pub fn new(bootnodes: Vec<PeerId>, metrics_registry: Option<Registry>) -> Self {
let metrics = if let Some(registry) = &metrics_registry {
PeerStoreMetrics::register(registry)
.map_err(|err| {
log::error!(target: LOG_TARGET, "Failed to register peer store metrics: {}", err);
err
})
.ok()
} else {
None
};
let peerstore_handle = PeerstoreHandle::new(
bootnodes.iter().map(|peer_id| (*peer_id, PeerInfo::default())).collect(),
Vec::new(),
metrics,
);
Self { peerstore_handle }
}
/// Get mutable reference to the underlying [`PeerstoreHandle`].
pub fn handle(&mut self) -> &mut PeerstoreHandle {
&mut self.peerstore_handle
}
/// Add known peer to [`Peerstore`].
pub fn add_known_peer(&mut self, peer: PeerId) {
self.peerstore_handle.add_known_peer(peer);
}
/// Start [`Peerstore`] event loop.
async fn run(self) {
let started = Instant::now();
let mut latest_time_update = started;
loop {
let now = Instant::now();
// We basically do `(now - self.latest_update).as_secs()`, except that by the way we do
// it we know that we're not going to miss seconds because of rounding to integers.
let seconds_passed = {
let elapsed_latest = latest_time_update - started;
let elapsed_now = now - started;
latest_time_update = now;
elapsed_now.as_secs() - elapsed_latest.as_secs()
};
self.peerstore_handle.progress_time(seconds_passed);
let _ = Delay::new(Duration::from_secs(1)).await;
}
}
}
#[async_trait::async_trait]
impl PeerStore for Peerstore {
/// Get handle to `PeerStore`.
fn handle(&self) -> Arc<dyn PeerStoreProvider> {
Arc::new(self.peerstore_handle.clone())
}
/// Start running `PeerStore` event loop.
async fn run(self) {
self.run().await;
}
}
#[cfg(test)]
mod tests {
use super::{PeerInfo, PeerStoreProvider, Peerstore};
#[test]
fn decaying_zero_reputation_yields_zero() {
let mut peer_info = PeerInfo::default();
assert_eq!(peer_info.reputation, 0);
peer_info.decay_reputation(1);
assert_eq!(peer_info.reputation, 0);
peer_info.decay_reputation(100_000);
assert_eq!(peer_info.reputation, 0);
}
#[test]
fn decaying_positive_reputation_decreases_it() {
const INITIAL_REPUTATION: i32 = 100;
let mut peer_info = PeerInfo::default();
peer_info.reputation = INITIAL_REPUTATION;
peer_info.decay_reputation(1);
assert!(peer_info.reputation >= 0);
assert!(peer_info.reputation < INITIAL_REPUTATION);
}
#[test]
fn decaying_negative_reputation_increases_it() {
const INITIAL_REPUTATION: i32 = -100;
let mut peer_info = PeerInfo::default();
peer_info.reputation = INITIAL_REPUTATION;
peer_info.decay_reputation(1);
assert!(peer_info.reputation <= 0);
assert!(peer_info.reputation > INITIAL_REPUTATION);
}
#[test]
fn decaying_max_reputation_finally_yields_zero() {
const INITIAL_REPUTATION: i32 = i32::MAX;
const SECONDS: u64 = 3544;
let mut peer_info = PeerInfo::default();
peer_info.reputation = INITIAL_REPUTATION;
peer_info.decay_reputation(SECONDS / 2);
assert!(peer_info.reputation > 0);
peer_info.decay_reputation(SECONDS / 2);
assert_eq!(peer_info.reputation, 0);
}
#[test]
fn decaying_min_reputation_finally_yields_zero() {
const INITIAL_REPUTATION: i32 = i32::MIN;
const SECONDS: u64 = 3544;
let mut peer_info = PeerInfo::default();
peer_info.reputation = INITIAL_REPUTATION;
peer_info.decay_reputation(SECONDS / 2);
assert!(peer_info.reputation < 0);
peer_info.decay_reputation(SECONDS / 2);
assert_eq!(peer_info.reputation, 0);
}
#[test]
fn report_banned_peers() {
let peer_a = pezsc_network_types::PeerId::random();
let peer_b = pezsc_network_types::PeerId::random();
let peer_c = pezsc_network_types::PeerId::random();
let metrics_registry = prometheus_endpoint::Registry::new();
let mut peerstore = Peerstore::new(
vec![peer_a, peer_b, peer_c].into_iter().map(Into::into).collect(),
Some(metrics_registry),
);
let metrics = peerstore.peerstore_handle.0.lock().metrics.as_ref().unwrap().clone();
let handle = peerstore.handle();
// Check initial state. Advance time to propagate peers.
handle.progress_time(1);
assert_eq!(metrics.num_discovered.get(), 3);
assert_eq!(metrics.num_banned_peers.get(), 0);
// Report 2 peers with a negative reputation.
handle.report_peer(
peer_a,
pezsc_network_common::types::ReputationChange { value: i32::MIN, reason: "test".into() },
);
handle.report_peer(
peer_b,
pezsc_network_common::types::ReputationChange { value: i32::MIN, reason: "test".into() },
);
// Advance time to propagate peers.
handle.progress_time(1);
assert_eq!(metrics.num_discovered.get(), 3);
assert_eq!(metrics.num_banned_peers.get(), 2);
}
}
@@ -0,0 +1,584 @@
// 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/>.
//! `NetworkService` implementation for `litep2p`.
use crate::{
config::MultiaddrWithPeerId,
litep2p::shim::{
notification::{config::ProtocolControlHandle, peerset::PeersetCommand},
request_response::OutboundRequest,
},
network_state::NetworkState,
peer_store::PeerStoreProvider,
service::out_events,
Event, IfDisconnected, NetworkDHTProvider, NetworkEventStream, NetworkPeers, NetworkRequest,
NetworkSigner, NetworkStateInfo, NetworkStatus, NetworkStatusProvider, OutboundFailure,
ProtocolName, RequestFailure, Signature,
};
use codec::DecodeAll;
use futures::{channel::oneshot, stream::BoxStream};
use libp2p::identity::SigningError;
use litep2p::{
addresses::PublicAddresses, crypto::ed25519::Keypair,
types::multiaddr::Multiaddr as LiteP2pMultiaddr,
};
use parking_lot::RwLock;
use pezsc_network_types::kad::{Key as KademliaKey, Record};
use pezsc_network_common::{
role::{ObservedRole, Roles},
types::ReputationChange,
};
use pezsc_network_types::{
multiaddr::{Multiaddr, Protocol},
PeerId,
};
use pezsc_utils::mpsc::TracingUnboundedSender;
use std::{
collections::{HashMap, HashSet},
sync::{atomic::Ordering, Arc},
time::Instant,
};
/// Logging target for the file.
const LOG_TARGET: &str = "sub-libp2p";
/// Commands sent by [`Litep2pNetworkService`] to
/// [`Litep2pNetworkBackend`](super::Litep2pNetworkBackend).
#[derive(Debug)]
pub enum NetworkServiceCommand {
/// Find peers closest to `target` in the DHT.
FindClosestPeers {
/// Target peer ID.
target: PeerId,
},
/// Get value from DHT.
GetValue {
/// Record key.
key: KademliaKey,
},
/// Put value to DHT.
PutValue {
/// Record key.
key: KademliaKey,
/// Record value.
value: Vec<u8>,
},
/// Put value to DHT.
PutValueTo {
/// Record.
record: Record,
/// Peers we want to put the record.
peers: Vec<pezsc_network_types::PeerId>,
/// If we should update the local storage or not.
update_local_storage: bool,
},
/// Store record in the local DHT store.
StoreRecord {
/// Record key.
key: KademliaKey,
/// Record value.
value: Vec<u8>,
/// Original publisher of the record.
publisher: Option<PeerId>,
/// Record expiration time as measured by a local, monothonic clock.
expires: Option<Instant>,
},
/// Start providing `key`.
StartProviding { key: KademliaKey },
/// Stop providing `key`.
StopProviding { key: KademliaKey },
/// Get providers for `key`.
GetProviders { key: KademliaKey },
/// Query network status.
Status {
/// `oneshot::Sender` for sending the status.
tx: oneshot::Sender<NetworkStatus>,
},
/// Add `peers` to `protocol`'s reserved set.
AddPeersToReservedSet {
/// Protocol.
protocol: ProtocolName,
/// Reserved peers.
peers: HashSet<Multiaddr>,
},
/// Add known address for peer.
AddKnownAddress {
/// Peer ID.
peer: PeerId,
/// Address.
address: Multiaddr,
},
/// Set reserved peers for `protocol`.
SetReservedPeers {
/// Protocol.
protocol: ProtocolName,
/// Reserved peers.
peers: HashSet<Multiaddr>,
},
/// Disconnect peer from protocol.
DisconnectPeer {
/// Protocol.
protocol: ProtocolName,
/// Peer ID.
peer: PeerId,
},
/// Set protocol to reserved only (true/false) mode.
SetReservedOnly {
/// Protocol.
protocol: ProtocolName,
/// Reserved only?
reserved_only: bool,
},
/// Remove reserved peers from protocol.
RemoveReservedPeers {
/// Protocol.
protocol: ProtocolName,
/// Peers to remove from the reserved set.
peers: HashSet<PeerId>,
},
/// Create event stream for DHT events.
EventStream {
/// Sender for the events.
tx: out_events::Sender,
},
}
/// `NetworkService` implementation for `litep2p`.
#[derive(Debug, Clone)]
pub struct Litep2pNetworkService {
/// Local peer ID.
local_peer_id: litep2p::PeerId,
/// The `KeyPair` that defines the `PeerId` of the local node.
keypair: Keypair,
/// TX channel for sending commands to [`Litep2pNetworkBackend`](super::Litep2pNetworkBackend).
cmd_tx: TracingUnboundedSender<NetworkServiceCommand>,
/// Handle to `PeerStore`.
peer_store_handle: Arc<dyn PeerStoreProvider>,
/// Peerset handles.
peerset_handles: HashMap<ProtocolName, ProtocolControlHandle>,
/// Name for the block announce protocol.
block_announce_protocol: ProtocolName,
/// Installed request-response protocols.
request_response_protocols: HashMap<ProtocolName, TracingUnboundedSender<OutboundRequest>>,
/// Listen addresses.
listen_addresses: Arc<RwLock<HashSet<LiteP2pMultiaddr>>>,
/// External addresses.
external_addresses: PublicAddresses,
}
impl Litep2pNetworkService {
/// Create new [`Litep2pNetworkService`].
pub fn new(
local_peer_id: litep2p::PeerId,
keypair: Keypair,
cmd_tx: TracingUnboundedSender<NetworkServiceCommand>,
peer_store_handle: Arc<dyn PeerStoreProvider>,
peerset_handles: HashMap<ProtocolName, ProtocolControlHandle>,
block_announce_protocol: ProtocolName,
request_response_protocols: HashMap<ProtocolName, TracingUnboundedSender<OutboundRequest>>,
listen_addresses: Arc<RwLock<HashSet<LiteP2pMultiaddr>>>,
external_addresses: PublicAddresses,
) -> Self {
Self {
local_peer_id,
keypair,
cmd_tx,
peer_store_handle,
peerset_handles,
block_announce_protocol,
request_response_protocols,
listen_addresses,
external_addresses,
}
}
}
impl NetworkSigner for Litep2pNetworkService {
fn sign_with_local_identity(&self, msg: Vec<u8>) -> Result<Signature, SigningError> {
let public_key = self.keypair.public();
let bytes = self.keypair.sign(msg.as_ref());
Ok(Signature {
public_key: crate::service::signature::PublicKey::Litep2p(
litep2p::crypto::PublicKey::Ed25519(public_key),
),
bytes,
})
}
fn verify(
&self,
peer: PeerId,
public_key: &Vec<u8>,
signature: &Vec<u8>,
message: &Vec<u8>,
) -> Result<bool, String> {
let identity = litep2p::PeerId::from_public_key_protobuf(&public_key);
let public_key = litep2p::crypto::RemotePublicKey::from_protobuf_encoding(&public_key)
.map_err(|error| error.to_string())?;
let peer: litep2p::PeerId = peer.into();
Ok(peer == identity && public_key.verify(message, signature))
}
}
impl NetworkDHTProvider for Litep2pNetworkService {
fn find_closest_peers(&self, target: PeerId) {
let _ = self.cmd_tx.unbounded_send(NetworkServiceCommand::FindClosestPeers { target });
}
fn get_value(&self, key: &KademliaKey) {
let _ = self.cmd_tx.unbounded_send(NetworkServiceCommand::GetValue { key: key.clone() });
}
fn put_value(&self, key: KademliaKey, value: Vec<u8>) {
let _ = self.cmd_tx.unbounded_send(NetworkServiceCommand::PutValue { key, value });
}
fn put_record_to(&self, record: Record, peers: HashSet<PeerId>, update_local_storage: bool) {
let _ = self.cmd_tx.unbounded_send(NetworkServiceCommand::PutValueTo {
record: Record {
key: record.key.to_vec().into(),
value: record.value,
publisher: record.publisher.map(|peer_id| {
let peer_id: pezsc_network_types::PeerId = peer_id.into();
peer_id.into()
}),
expires: record.expires,
},
peers: peers.into_iter().collect(),
update_local_storage,
});
}
fn store_record(
&self,
key: KademliaKey,
value: Vec<u8>,
publisher: Option<PeerId>,
expires: Option<Instant>,
) {
let _ = self.cmd_tx.unbounded_send(NetworkServiceCommand::StoreRecord {
key,
value,
publisher,
expires,
});
}
fn start_providing(&self, key: KademliaKey) {
let _ = self.cmd_tx.unbounded_send(NetworkServiceCommand::StartProviding { key });
}
fn stop_providing(&self, key: KademliaKey) {
let _ = self.cmd_tx.unbounded_send(NetworkServiceCommand::StopProviding { key });
}
fn get_providers(&self, key: KademliaKey) {
let _ = self.cmd_tx.unbounded_send(NetworkServiceCommand::GetProviders { key });
}
}
#[async_trait::async_trait]
impl NetworkStatusProvider for Litep2pNetworkService {
async fn status(&self) -> Result<NetworkStatus, ()> {
let (tx, rx) = oneshot::channel();
self.cmd_tx
.unbounded_send(NetworkServiceCommand::Status { tx })
.map_err(|_| ())?;
rx.await.map_err(|_| ())
}
async fn network_state(&self) -> Result<NetworkState, ()> {
Ok(NetworkState {
peer_id: self.local_peer_id.to_base58(),
listened_addresses: self
.listen_addresses
.read()
.iter()
.cloned()
.map(|a| Multiaddr::from(a).into())
.collect(),
external_addresses: self
.external_addresses
.get_addresses()
.into_iter()
.map(|a| Multiaddr::from(a).into())
.collect(),
connected_peers: HashMap::new(),
not_connected_peers: HashMap::new(),
// TODO: Check what info we can include here.
// Issue reference: https://github.com/pezkuwichain/kurdistan-sdk/issues/15.
peerset: serde_json::json!(
"Unimplemented. See https://github.com/pezkuwichain/kurdistan-sdk/issues/15."
),
})
}
}
// Manual implementation to avoid extra boxing here
// TODO: functions modifying peerset state could be modified to call peerset directly if the
// `Multiaddr` only contains a `PeerId`
#[async_trait::async_trait]
impl NetworkPeers for Litep2pNetworkService {
fn set_authorized_peers(&self, peers: HashSet<PeerId>) {
let _ = self.cmd_tx.unbounded_send(NetworkServiceCommand::SetReservedPeers {
protocol: self.block_announce_protocol.clone(),
peers: peers
.into_iter()
.map(|peer| Multiaddr::empty().with(Protocol::P2p(peer.into())))
.collect(),
});
}
fn set_authorized_only(&self, reserved_only: bool) {
let _ = self.cmd_tx.unbounded_send(NetworkServiceCommand::SetReservedOnly {
protocol: self.block_announce_protocol.clone(),
reserved_only,
});
}
fn add_known_address(&self, peer: PeerId, address: Multiaddr) {
let _ = self
.cmd_tx
.unbounded_send(NetworkServiceCommand::AddKnownAddress { peer, address });
}
fn peer_reputation(&self, peer_id: &PeerId) -> i32 {
self.peer_store_handle.peer_reputation(peer_id)
}
fn report_peer(&self, peer: PeerId, cost_benefit: ReputationChange) {
self.peer_store_handle.report_peer(peer, cost_benefit);
}
fn disconnect_peer(&self, peer: PeerId, protocol: ProtocolName) {
let _ = self
.cmd_tx
.unbounded_send(NetworkServiceCommand::DisconnectPeer { protocol, peer });
}
fn accept_unreserved_peers(&self) {
let _ = self.cmd_tx.unbounded_send(NetworkServiceCommand::SetReservedOnly {
protocol: self.block_announce_protocol.clone(),
reserved_only: false,
});
}
fn deny_unreserved_peers(&self) {
let _ = self.cmd_tx.unbounded_send(NetworkServiceCommand::SetReservedOnly {
protocol: self.block_announce_protocol.clone(),
reserved_only: true,
});
}
fn add_reserved_peer(&self, peer: MultiaddrWithPeerId) -> Result<(), String> {
let _ = self.cmd_tx.unbounded_send(NetworkServiceCommand::AddPeersToReservedSet {
protocol: self.block_announce_protocol.clone(),
peers: HashSet::from_iter([peer.concat().into()]),
});
Ok(())
}
fn remove_reserved_peer(&self, peer: PeerId) {
let _ = self.cmd_tx.unbounded_send(NetworkServiceCommand::RemoveReservedPeers {
protocol: self.block_announce_protocol.clone(),
peers: HashSet::from_iter([peer]),
});
}
fn set_reserved_peers(
&self,
protocol: ProtocolName,
peers: HashSet<Multiaddr>,
) -> Result<(), String> {
let _ = self
.cmd_tx
.unbounded_send(NetworkServiceCommand::SetReservedPeers { protocol, peers });
Ok(())
}
fn add_peers_to_reserved_set(
&self,
protocol: ProtocolName,
peers: HashSet<Multiaddr>,
) -> Result<(), String> {
let _ = self
.cmd_tx
.unbounded_send(NetworkServiceCommand::AddPeersToReservedSet { protocol, peers });
Ok(())
}
fn remove_peers_from_reserved_set(
&self,
protocol: ProtocolName,
peers: Vec<PeerId>,
) -> Result<(), String> {
let _ = self.cmd_tx.unbounded_send(NetworkServiceCommand::RemoveReservedPeers {
protocol,
peers: peers.into_iter().map(From::from).collect(),
});
Ok(())
}
fn sync_num_connected(&self) -> usize {
self.peerset_handles
.get(&self.block_announce_protocol)
.map_or(0usize, |handle| handle.connected_peers.load(Ordering::Relaxed))
}
fn peer_role(&self, peer: PeerId, handshake: Vec<u8>) -> Option<ObservedRole> {
match Roles::decode_all(&mut &handshake[..]) {
Ok(role) => Some(role.into()),
Err(_) => {
log::debug!(target: LOG_TARGET, "handshake doesn't contain peer role: {handshake:?}");
self.peer_store_handle.peer_role(&(peer.into()))
},
}
}
/// Get the list of reserved peers.
///
/// Returns an error if the `NetworkWorker` is no longer running.
async fn reserved_peers(&self) -> Result<Vec<PeerId>, ()> {
let Some(handle) = self.peerset_handles.get(&self.block_announce_protocol) else {
return Err(());
};
let (tx, rx) = oneshot::channel();
handle
.tx
.unbounded_send(PeersetCommand::GetReservedPeers { tx })
.map_err(|_| ())?;
// the channel can only be closed if `Peerset` no longer exists
rx.await.map_err(|_| ())
}
}
impl NetworkEventStream for Litep2pNetworkService {
fn event_stream(&self, stream_name: &'static str) -> BoxStream<'static, Event> {
let (tx, rx) = out_events::channel(stream_name, 100_000);
let _ = self.cmd_tx.unbounded_send(NetworkServiceCommand::EventStream { tx });
Box::pin(rx)
}
}
impl NetworkStateInfo for Litep2pNetworkService {
fn external_addresses(&self) -> Vec<Multiaddr> {
self.external_addresses.get_addresses().into_iter().map(Into::into).collect()
}
fn listen_addresses(&self) -> Vec<Multiaddr> {
self.listen_addresses.read().iter().cloned().map(Into::into).collect()
}
fn local_peer_id(&self) -> PeerId {
self.local_peer_id.into()
}
}
// Manual implementation to avoid extra boxing here
#[async_trait::async_trait]
impl NetworkRequest for Litep2pNetworkService {
async fn request(
&self,
target: PeerId,
protocol: ProtocolName,
request: Vec<u8>,
fallback_request: Option<(Vec<u8>, ProtocolName)>,
connect: IfDisconnected,
) -> Result<(Vec<u8>, ProtocolName), RequestFailure> {
let (tx, rx) = oneshot::channel();
self.start_request(target, protocol, request, fallback_request, tx, connect);
match rx.await {
Ok(v) => v,
// The channel can only be closed if the network worker no longer exists. If the
// network worker no longer exists, then all connections to `target` are necessarily
// closed, and we legitimately report this situation as a "ConnectionClosed".
Err(_) => Err(RequestFailure::Network(OutboundFailure::ConnectionClosed)),
}
}
fn start_request(
&self,
peer: PeerId,
protocol: ProtocolName,
request: Vec<u8>,
fallback_request: Option<(Vec<u8>, ProtocolName)>,
sender: oneshot::Sender<Result<(Vec<u8>, ProtocolName), RequestFailure>>,
connect: IfDisconnected,
) {
match self.request_response_protocols.get(&protocol) {
Some(tx) => {
let _ = tx.unbounded_send(OutboundRequest::new(
peer,
request,
sender,
fallback_request,
connect,
));
},
None => log::warn!(
target: LOG_TARGET,
"{protocol} doesn't exist, cannot send request to {peer:?}"
),
}
}
}
@@ -0,0 +1,104 @@
// 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/>.
//! Shim for litep2p's Bitswap implementation to make it work with `sc-network`.
use futures::StreamExt;
use litep2p::protocol::libp2p::bitswap::{
BitswapEvent, BitswapHandle, BlockPresenceType, Config, ResponseType, WantType,
};
use pezsc_client_api::BlockBackend;
use pezsp_runtime::traits::Block as BlockT;
use std::{future::Future, pin::Pin, sync::Arc};
/// Logging target for the file.
const LOG_TARGET: &str = "sub-libp2p::bitswap";
pub struct BitswapServer<Block: BlockT> {
/// Bitswap handle.
handle: BitswapHandle,
/// Blockchain client.
client: Arc<dyn BlockBackend<Block> + Send + Sync>,
}
impl<Block: BlockT> BitswapServer<Block> {
/// Create new [`BitswapServer`].
pub fn new(
client: Arc<dyn BlockBackend<Block> + Send + Sync>,
) -> (Pin<Box<dyn Future<Output = ()> + Send>>, Config) {
let (config, handle) = Config::new();
let bitswap = Self { client, handle };
(Box::pin(async move { bitswap.run().await }), config)
}
async fn run(mut self) {
log::debug!(target: LOG_TARGET, "starting bitswap server");
while let Some(event) = self.handle.next().await {
match event {
BitswapEvent::Request { peer, cids } => {
log::debug!(target: LOG_TARGET, "handle bitswap request from {peer:?} for {cids:?}");
let response: Vec<ResponseType> = cids
.into_iter()
.map(|(cid, want_type)| {
let mut hash = Block::Hash::default();
hash.as_mut().copy_from_slice(&cid.hash().digest()[0..32]);
let transaction = match self.client.indexed_transaction(hash) {
Ok(ex) => ex,
Err(error) => {
log::error!(target: LOG_TARGET, "error retrieving transaction {hash}: {error}");
None
},
};
match transaction {
Some(transaction) => {
log::trace!(target: LOG_TARGET, "found cid {cid:?}, hash {hash:?}");
match want_type {
WantType::Block =>
ResponseType::Block { cid, block: transaction },
_ => ResponseType::Presence {
cid,
presence: BlockPresenceType::Have,
},
}
},
None => {
log::trace!(target: LOG_TARGET, "missing cid {cid:?}, hash {hash:?}");
ResponseType::Presence {
cid,
presence: BlockPresenceType::DontHave,
}
},
}
})
.collect();
self.handle.send_response(peer, response).await;
},
}
}
}
}
@@ -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/>.
//! Shims for fitting `litep2p` APIs to `sc-network` APIs.
pub(crate) mod bitswap;
pub(crate) mod notification;
pub(crate) mod request_response;
@@ -0,0 +1,168 @@
// 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/>.
//! `litep2p` notification protocol configuration.
use crate::{
config::{MultiaddrWithPeerId, NonReservedPeerMode, NotificationHandshake, SetConfig},
litep2p::shim::notification::{
peerset::{Peerset, PeersetCommand},
NotificationProtocol,
},
peer_store::PeerStoreProvider,
service::{metrics::NotificationMetrics, traits::NotificationConfig},
NotificationService, ProtocolName,
};
use litep2p::protocol::notification::{Config, ConfigBuilder};
use pezsc_utils::mpsc::TracingUnboundedSender;
use std::sync::{atomic::AtomicUsize, Arc};
/// Handle for controlling the notification protocol.
#[derive(Debug, Clone)]
pub struct ProtocolControlHandle {
/// TX channel for sending commands to `Peerset` of the notification protocol.
pub tx: TracingUnboundedSender<PeersetCommand>,
/// Peers currently connected to this protocol.
pub connected_peers: Arc<AtomicUsize>,
}
impl ProtocolControlHandle {
/// Create new [`ProtocolControlHandle`].
pub fn new(
tx: TracingUnboundedSender<PeersetCommand>,
connected_peers: Arc<AtomicUsize>,
) -> Self {
Self { tx, connected_peers }
}
}
/// Configuration for the notification protocol.
#[derive(Debug)]
pub struct NotificationProtocolConfig {
/// Name of the notifications protocols of this set. A substream on this set will be
/// considered established once this protocol is open.
pub protocol_name: ProtocolName,
/// Maximum allowed size of single notifications.
max_notification_size: usize,
/// Base configuration.
set_config: SetConfig,
/// `litep2p` notification config.
pub config: Config,
/// Handle for controlling the notification protocol.
pub handle: ProtocolControlHandle,
}
impl NotificationProtocolConfig {
// Create new [`NotificationProtocolConfig`].
pub fn new(
protocol_name: ProtocolName,
fallback_names: Vec<ProtocolName>,
max_notification_size: usize,
handshake: Option<NotificationHandshake>,
set_config: SetConfig,
metrics: NotificationMetrics,
peerstore_handle: Arc<dyn PeerStoreProvider>,
) -> (Self, Box<dyn NotificationService>) {
// create `Peerset`/`Peerstore` handle for the protocol
let connected_peers = Arc::new(Default::default());
let (peerset, peerset_tx) = Peerset::new(
protocol_name.clone(),
set_config.out_peers as usize,
set_config.in_peers as usize,
set_config.non_reserved_mode == NonReservedPeerMode::Deny,
set_config.reserved_nodes.iter().map(|address| address.peer_id).collect(),
Arc::clone(&connected_peers),
peerstore_handle,
);
// create `litep2p` notification protocol configuration for the protocol
//
// NOTE: currently only dummy value is given as the handshake as protocols (apart from
// syncing) are not configuring their own handshake and instead default to role being the
// handshake. As the time of writing this, most protocols are not aware of the role and
// that should be refactored in the future.
let (config, handle) = ConfigBuilder::new(protocol_name.clone().into())
.with_handshake(handshake.map_or(vec![1], |handshake| (*handshake).to_vec()))
.with_max_size(max_notification_size as usize)
.with_auto_accept_inbound(true)
.with_fallback_names(fallback_names.into_iter().map(From::from).collect())
.build();
// initialize the actual object implementing `NotificationService` and combine the
// `litep2p::NotificationHandle` with `Peerset` to implement a full and independent
// notification protocol runner
let protocol = NotificationProtocol::new(protocol_name.clone(), handle, peerset, metrics);
(
Self {
protocol_name,
max_notification_size,
set_config,
config,
handle: ProtocolControlHandle::new(peerset_tx, connected_peers),
},
Box::new(protocol),
)
}
/// Get reference to protocol name.
pub fn protocol_name(&self) -> &ProtocolName {
&self.protocol_name
}
/// Get reference to `SetConfig`.
pub fn set_config(&self) -> &SetConfig {
&self.set_config
}
/// Modifies the configuration to allow non-reserved nodes.
pub fn allow_non_reserved(&mut self, in_peers: u32, out_peers: u32) {
self.set_config.in_peers = in_peers;
self.set_config.out_peers = out_peers;
self.set_config.non_reserved_mode = NonReservedPeerMode::Accept;
}
/// Add a node to the list of reserved nodes.
pub fn add_reserved(&mut self, peer: MultiaddrWithPeerId) {
self.set_config.reserved_nodes.push(peer);
}
/// Get maximum notification size.
pub fn max_notification_size(&self) -> usize {
self.max_notification_size
}
}
impl NotificationConfig for NotificationProtocolConfig {
fn set_config(&self) -> &SetConfig {
&self.set_config
}
/// Get reference to protocol name.
fn protocol_name(&self) -> &ProtocolName {
&self.protocol_name
}
}
@@ -0,0 +1,376 @@
// 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/>.
//! Shim for `litep2p::NotificationHandle` to combine `Peerset`-like behavior
//! with `NotificationService`.
use crate::{
error::Error,
litep2p::shim::notification::peerset::{OpenResult, Peerset, PeersetNotificationCommand},
service::{
metrics::NotificationMetrics,
traits::{NotificationEvent as BizinikiwiNotificationEvent, ValidationResult},
},
MessageSink, NotificationService, ProtocolName,
};
use futures::{future::BoxFuture, stream::FuturesUnordered, StreamExt};
use litep2p::protocol::notification::{
NotificationEvent, NotificationHandle, NotificationSink,
ValidationResult as Litep2pValidationResult,
};
use tokio::sync::oneshot;
use pezsc_network_types::PeerId;
use std::{collections::HashSet, fmt};
pub mod config;
pub mod peerset;
#[cfg(test)]
mod tests;
/// Logging target for the file.
const LOG_TARGET: &str = "sub-libp2p::notification";
/// Wrapper over `litep2p`'s notification sink.
pub struct Litep2pMessageSink {
/// Protocol.
protocol: ProtocolName,
/// Remote peer ID.
peer: PeerId,
/// Notification sink.
sink: NotificationSink,
/// Notification metrics.
metrics: NotificationMetrics,
}
impl Litep2pMessageSink {
/// Create new [`Litep2pMessageSink`].
fn new(
peer: PeerId,
protocol: ProtocolName,
sink: NotificationSink,
metrics: NotificationMetrics,
) -> Self {
Self { protocol, peer, sink, metrics }
}
}
#[async_trait::async_trait]
impl MessageSink for Litep2pMessageSink {
/// Send synchronous `notification` to the peer associated with this [`MessageSink`].
fn send_sync_notification(&self, notification: Vec<u8>) {
let size = notification.len();
match self.sink.send_sync_notification(notification) {
Ok(_) => self.metrics.register_notification_sent(&self.protocol, size),
Err(error) => log::trace!(
target: LOG_TARGET,
"{}: failed to send sync notification to {:?}: {error:?}",
self.protocol,
self.peer,
),
}
}
/// Send an asynchronous `notification` to to the peer associated with this [`MessageSink`],
/// allowing sender to exercise backpressure.
///
/// Returns an error if the peer does not exist.
async fn send_async_notification(&self, notification: Vec<u8>) -> Result<(), Error> {
let size = notification.len();
match self.sink.send_async_notification(notification).await {
Ok(_) => {
self.metrics.register_notification_sent(&self.protocol, size);
Ok(())
},
Err(error) => {
log::trace!(
target: LOG_TARGET,
"{}: failed to send async notification to {:?}: {error:?}",
self.protocol,
self.peer,
);
Err(Error::Litep2p(error))
},
}
}
}
/// Notification protocol implementation.
pub struct NotificationProtocol {
/// Protocol name.
protocol: ProtocolName,
/// `litep2p` notification handle.
handle: NotificationHandle,
/// Peerset for the notification protocol.
///
/// Listens to peering-related events and either opens or closes substreams to remote peers.
peerset: Peerset,
/// Pending validations for inbound substreams.
pending_validations: FuturesUnordered<
BoxFuture<'static, (PeerId, Result<ValidationResult, oneshot::error::RecvError>)>,
>,
/// Pending cancels.
pending_cancels: HashSet<litep2p::PeerId>,
/// Notification metrics.
metrics: NotificationMetrics,
}
impl fmt::Debug for NotificationProtocol {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("NotificationProtocol")
.field("protocol", &self.protocol)
.field("handle", &self.handle)
.finish()
}
}
impl NotificationProtocol {
/// Create new [`NotificationProtocol`].
pub fn new(
protocol: ProtocolName,
handle: NotificationHandle,
peerset: Peerset,
metrics: NotificationMetrics,
) -> Self {
Self {
protocol,
handle,
peerset,
metrics,
pending_cancels: HashSet::new(),
pending_validations: FuturesUnordered::new(),
}
}
/// Handle `Peerset` command.
async fn on_peerset_command(&mut self, command: PeersetNotificationCommand) {
if !command.open_peers.is_empty() {
log::trace!(target: LOG_TARGET, "{}: open substreams to {:?}", self.protocol, command.open_peers);
let _ = self
.handle
.open_substream_batch(command.open_peers.into_iter().map(From::from))
.await;
}
if !command.close_peers.is_empty() {
log::trace!(target: LOG_TARGET, "{}: close substreams to {:?}", self.protocol, command.close_peers);
self.handle
.close_substream_batch(command.close_peers.into_iter().map(From::from))
.await;
}
}
}
#[async_trait::async_trait]
impl NotificationService for NotificationProtocol {
async fn open_substream(&mut self, _peer: PeerId) -> Result<(), ()> {
unimplemented!();
}
async fn close_substream(&mut self, _peer: PeerId) -> Result<(), ()> {
unimplemented!();
}
fn send_sync_notification(&mut self, peer: &PeerId, notification: Vec<u8>) {
let size = notification.len();
if let Ok(_) = self.handle.send_sync_notification(peer.into(), notification) {
self.metrics.register_notification_sent(&self.protocol, size);
}
}
async fn send_async_notification(
&mut self,
peer: &PeerId,
notification: Vec<u8>,
) -> Result<(), Error> {
let size = notification.len();
match self.handle.send_async_notification(peer.into(), notification).await {
Ok(_) => {
self.metrics.register_notification_sent(&self.protocol, size);
Ok(())
},
Err(_) => Err(Error::ChannelClosed),
}
}
/// Set handshake for the notification protocol replacing the old handshake.
async fn set_handshake(&mut self, handshake: Vec<u8>) -> Result<(), ()> {
self.handle.set_handshake(handshake);
Ok(())
}
/// Set handshake for the notification protocol replacing the old handshake.
///
/// For `litep2p` this is identical to `NotificationService::set_handshake()` since `litep2p`
/// allows updating the handshake synchronously.
fn try_set_handshake(&mut self, handshake: Vec<u8>) -> Result<(), ()> {
self.handle.set_handshake(handshake);
Ok(())
}
/// Make a copy of the object so it can be shared between protocol components
/// who wish to have access to the same underlying notification protocol.
fn clone(&mut self) -> Result<Box<dyn NotificationService>, ()> {
unimplemented!("clonable `NotificationService` not supported by `litep2p`");
}
/// Get protocol name of the `NotificationService`.
fn protocol(&self) -> &ProtocolName {
&self.protocol
}
/// Get message sink of the peer.
fn message_sink(&self, peer: &PeerId) -> Option<Box<dyn MessageSink>> {
self.handle.notification_sink(peer.into()).map(|sink| {
let sink: Box<dyn MessageSink> = Box::new(Litep2pMessageSink::new(
*peer,
self.protocol.clone(),
sink,
self.metrics.clone(),
));
sink
})
}
/// Get next event from the `Notifications` event stream.
async fn next_event(&mut self) -> Option<BizinikiwiNotificationEvent> {
loop {
tokio::select! {
biased;
event = self.handle.next() => match event? {
NotificationEvent::ValidateSubstream { peer, handshake, .. } => {
if let ValidationResult::Reject = self.peerset.report_inbound_substream(peer.into()) {
self.handle.send_validation_result(peer, Litep2pValidationResult::Reject);
continue;
}
let (tx, rx) = oneshot::channel();
self.pending_validations.push(Box::pin(async move { (peer.into(), rx.await) }));
log::trace!(target: LOG_TARGET, "{}: validate substream for {peer:?}", self.protocol);
return Some(BizinikiwiNotificationEvent::ValidateInboundSubstream {
peer: peer.into(),
handshake,
result_tx: tx,
});
}
NotificationEvent::NotificationStreamOpened {
peer,
fallback,
handshake,
direction,
..
} => {
self.metrics.register_substream_opened(&self.protocol);
match self.peerset.report_substream_opened(peer.into(), direction.into()) {
OpenResult::Reject => {
let _ = self.handle.close_substream_batch(vec![peer].into_iter().map(From::from)).await;
self.pending_cancels.insert(peer);
continue
}
OpenResult::Accept { direction } => {
log::trace!(target: LOG_TARGET, "{}: substream opened for {peer:?}", self.protocol);
return Some(BizinikiwiNotificationEvent::NotificationStreamOpened {
peer: peer.into(),
handshake,
direction,
negotiated_fallback: fallback.map(From::from),
});
}
}
}
NotificationEvent::NotificationStreamClosed {
peer,
} => {
log::trace!(target: LOG_TARGET, "{}: substream closed for {peer:?}", self.protocol);
self.metrics.register_substream_closed(&self.protocol);
self.peerset.report_substream_closed(peer.into());
if self.pending_cancels.remove(&peer) {
log::debug!(
target: LOG_TARGET,
"{}: substream closed to canceled peer ({peer:?})",
self.protocol
);
continue
}
return Some(BizinikiwiNotificationEvent::NotificationStreamClosed { peer: peer.into() })
}
NotificationEvent::NotificationStreamOpenFailure {
peer,
error,
} => {
log::trace!(target: LOG_TARGET, "{}: open failure for {peer:?}", self.protocol);
self.peerset.report_substream_open_failure(peer.into(), error);
}
NotificationEvent::NotificationReceived {
peer,
notification,
} => {
self.metrics.register_notification_received(&self.protocol, notification.len());
if !self.pending_cancels.contains(&peer) {
return Some(BizinikiwiNotificationEvent::NotificationReceived {
peer: peer.into(),
notification: notification.to_vec(),
});
}
}
},
result = self.pending_validations.next(), if !self.pending_validations.is_empty() => {
let (peer, result) = result?;
let validation_result = match result {
Ok(ValidationResult::Accept) => Litep2pValidationResult::Accept,
_ => {
self.peerset.report_substream_rejected(peer);
Litep2pValidationResult::Reject
}
};
self.handle.send_validation_result(peer.into(), validation_result);
}
command = self.peerset.next() => self.on_peerset_command(command?).await,
}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,383 @@
// 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/>.
//! Fuzz test emulates network events and peer connection handling by `Peerset`
//! and `PeerStore` to discover possible inconsistencies in peer management.
use crate::{
litep2p::{
peerstore::Peerstore,
shim::notification::peerset::{OpenResult, Peerset, PeersetCommand},
},
service::traits::{Direction, PeerStore, ValidationResult},
ProtocolName,
};
use futures::{FutureExt, StreamExt};
use litep2p::protocol::notification::NotificationError;
use rand::{
distributions::{Distribution, Uniform, WeightedIndex},
seq::IteratorRandom,
};
use pezsc_network_common::types::ReputationChange;
use pezsc_network_types::PeerId;
use std::{
collections::{HashMap, HashSet},
sync::Arc,
};
#[tokio::test]
#[cfg(debug_assertions)]
async fn run() {
pezsp_tracing::try_init_simple();
for _ in 0..50 {
test_once().await;
}
}
#[cfg(debug_assertions)]
async fn test_once() {
// PRNG to use.
let mut rng = rand::thread_rng();
// peers that the peerset knows about.
let mut known_peers = HashSet::<PeerId>::new();
// peers that we have reserved. Always a subset of `known_peers`.
let mut reserved_peers = HashSet::<PeerId>::new();
// reserved only mode
let mut reserved_only = Uniform::new_inclusive(0, 10).sample(&mut rng) == 0;
// Bootnodes for `PeerStore` initialization.
let bootnodes = (0..Uniform::new_inclusive(0, 4).sample(&mut rng))
.map(|_| {
let id = PeerId::random();
known_peers.insert(id);
id
})
.collect();
let peerstore = Peerstore::new(bootnodes, None);
let peer_store_handle = peerstore.handle();
let (mut peerset, to_peerset) = Peerset::new(
ProtocolName::from("/notif/1"),
Uniform::new_inclusive(0, 25).sample(&mut rng),
Uniform::new_inclusive(0, 25).sample(&mut rng),
reserved_only,
(0..Uniform::new_inclusive(0, 2).sample(&mut rng))
.map(|_| {
let id = PeerId::random();
known_peers.insert(id);
reserved_peers.insert(id);
id
})
.collect(),
Default::default(),
Arc::clone(&peer_store_handle),
);
tokio::spawn(peerstore.run());
// opening substreams
let mut opening = HashMap::<PeerId, Direction>::new();
// open substreams
let mut open = HashMap::<PeerId, Direction>::new();
// closing substreams
let mut closing = HashSet::<PeerId>::new();
// closed substreams
let mut closed = HashSet::<PeerId>::new();
// perform a certain number of actions while checking that the state is consistent.
//
// if we reach the end of the loop, the run has succeeded
let _ = tokio::task::spawn_blocking(move || {
// PRNG to use in `spawn_blocking` context.
let mut rng = rand::thread_rng();
for _ in 0..2500 {
// each of these weights corresponds to an action that we may perform
let action_weights =
[300, 110, 110, 110, 110, 90, 70, 30, 110, 110, 110, 110, 20, 110, 50, 110];
match WeightedIndex::new(&action_weights).unwrap().sample(&mut rng) {
0 => match peerset.next().now_or_never() {
Some(Some(command)) => {
// open substreams to `peers`
for peer in command.open_peers {
opening.insert(peer, Direction::Outbound);
closed.remove(&peer);
assert!(!closing.contains(&peer));
assert!(!open.contains_key(&peer));
}
// close substreams to `peers`
for peer in command.close_peers {
assert!(closing.insert(peer));
assert!(open.remove(&peer).is_some());
assert!(!opening.contains_key(&peer));
}
},
Some(None) => panic!("peerset exited"),
None => {},
},
// get inbound connection from an unknown peer
1 => {
let new_peer = PeerId::random();
peer_store_handle.add_known_peer(new_peer);
match peerset.report_inbound_substream(new_peer) {
ValidationResult::Accept => {
opening.insert(new_peer, Direction::Inbound);
},
ValidationResult::Reject => {},
}
},
// substream opened successfully
//
// remove peer from `opening` (which contains its direction), report the open
// substream to `Peerset` and move peer state to `open`.
//
// if the substream was canceled while it was opening, move peer to `closing`
2 =>
if let Some(peer) = opening.keys().choose(&mut rng).copied() {
let direction = opening.remove(&peer).unwrap();
match peerset.report_substream_opened(peer, direction) {
OpenResult::Accept { .. } => {
assert!(open.insert(peer, direction).is_none());
},
OpenResult::Reject => {
assert!(closing.insert(peer));
},
}
},
// substream failed to open
3 =>
if let Some(peer) = opening.keys().choose(&mut rng).copied() {
let _ = opening.remove(&peer).unwrap();
peerset.report_substream_open_failure(peer, NotificationError::Rejected);
},
// substream was closed by remote peer
4 =>
if let Some(peer) = open.keys().choose(&mut rng).copied() {
let _ = open.remove(&peer).unwrap();
peerset.report_substream_closed(peer);
assert!(closed.insert(peer));
},
// substream was closed by local node
5 =>
if let Some(peer) = closing.iter().choose(&mut rng).copied() {
assert!(closing.remove(&peer));
assert!(closed.insert(peer));
peerset.report_substream_closed(peer);
},
// random connected peer was disconnected by the protocol
6 =>
if let Some(peer) = open.keys().choose(&mut rng).copied() {
to_peerset.unbounded_send(PeersetCommand::DisconnectPeer { peer }).unwrap();
},
// ban random peer
7 =>
if let Some(peer) = known_peers.iter().choose(&mut rng).copied() {
peer_store_handle.report_peer(peer, ReputationChange::new_fatal(""));
},
// inbound substream is received for a peer that was considered
// outbound
8 => {
let outbound_peers = opening
.iter()
.filter_map(|(peer, direction)| {
std::matches!(direction, Direction::Outbound).then_some(*peer)
})
.collect::<HashSet<_>>();
if let Some(peer) = outbound_peers.iter().choose(&mut rng).copied() {
match peerset.report_inbound_substream(peer) {
ValidationResult::Accept => {
opening.insert(peer, Direction::Inbound);
},
ValidationResult::Reject => {},
}
}
},
// set reserved peers
//
// choose peers from all available sets (open, opening, closing, closed) + some new
// peers
9 => {
let num_open = Uniform::new_inclusive(0, open.len()).sample(&mut rng);
let num_opening = Uniform::new_inclusive(0, opening.len()).sample(&mut rng);
let num_closing = Uniform::new_inclusive(0, closing.len()).sample(&mut rng);
let num_closed = Uniform::new_inclusive(0, closed.len()).sample(&mut rng);
let peers = open
.keys()
.copied()
.choose_multiple(&mut rng, num_open)
.into_iter()
.chain(
opening
.keys()
.copied()
.choose_multiple(&mut rng, num_opening)
.into_iter(),
)
.chain(
closing
.iter()
.copied()
.choose_multiple(&mut rng, num_closing)
.into_iter(),
)
.chain(
closed
.iter()
.copied()
.choose_multiple(&mut rng, num_closed)
.into_iter(),
)
.chain((0..5).map(|_| {
let peer = PeerId::random();
known_peers.insert(peer);
peer_store_handle.add_known_peer(peer);
peer
}))
.filter(|peer| !reserved_peers.contains(peer))
.collect::<HashSet<_>>();
reserved_peers.extend(peers.clone().into_iter());
to_peerset.unbounded_send(PeersetCommand::SetReservedPeers { peers }).unwrap();
},
// add reserved peers
10 => {
let num_open = Uniform::new_inclusive(0, open.len()).sample(&mut rng);
let num_opening = Uniform::new_inclusive(0, opening.len()).sample(&mut rng);
let num_closing = Uniform::new_inclusive(0, closing.len()).sample(&mut rng);
let num_closed = Uniform::new_inclusive(0, closed.len()).sample(&mut rng);
let peers = open
.keys()
.copied()
.choose_multiple(&mut rng, num_open)
.into_iter()
.chain(
opening
.keys()
.copied()
.choose_multiple(&mut rng, num_opening)
.into_iter(),
)
.chain(
closing
.iter()
.copied()
.choose_multiple(&mut rng, num_closing)
.into_iter(),
)
.chain(
closed
.iter()
.copied()
.choose_multiple(&mut rng, num_closed)
.into_iter(),
)
.chain((0..5).map(|_| {
let peer = PeerId::random();
known_peers.insert(peer);
peer_store_handle.add_known_peer(peer);
peer
}))
.filter(|peer| !reserved_peers.contains(peer))
.collect::<HashSet<_>>();
reserved_peers.extend(peers.clone().into_iter());
to_peerset.unbounded_send(PeersetCommand::AddReservedPeers { peers }).unwrap();
},
// remove reserved peers
11 => {
let num_to_remove =
Uniform::new_inclusive(0, reserved_peers.len()).sample(&mut rng);
let peers = reserved_peers
.iter()
.copied()
.choose_multiple(&mut rng, num_to_remove)
.into_iter()
.collect::<HashSet<_>>();
peers.iter().for_each(|peer| {
assert!(reserved_peers.remove(peer));
});
to_peerset
.unbounded_send(PeersetCommand::RemoveReservedPeers { peers })
.unwrap();
},
// set reserved only
12 => {
reserved_only = !reserved_only;
let _ = to_peerset
.unbounded_send(PeersetCommand::SetReservedOnly { reserved_only });
},
//
// discover a new node.
13 => {
let new_peer = PeerId::random();
known_peers.insert(new_peer);
peer_store_handle.add_known_peer(new_peer);
},
// protocol rejected a substream that was accepted by `Peerset`
14 => {
let inbound_peers = opening
.iter()
.filter_map(|(peer, direction)| {
std::matches!(direction, Direction::Inbound).then_some(*peer)
})
.collect::<HashSet<_>>();
if let Some(peer) = inbound_peers.iter().choose(&mut rng).copied() {
peerset.report_substream_rejected(peer);
opening.remove(&peer);
}
},
// inbound substream received for a peer in `closed`
15 =>
if let Some(peer) = closed.iter().choose(&mut rng).copied() {
match peerset.report_inbound_substream(peer) {
ValidationResult::Accept => {
assert!(closed.remove(&peer));
opening.insert(peer, Direction::Inbound);
},
ValidationResult::Reject => {},
}
},
_ => unreachable!(),
}
}
})
.await
.unwrap();
}
@@ -0,0 +1,22 @@
// 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/>.
#[cfg(test)]
mod fuzz;
#[cfg(test)]
mod peerset;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,78 @@
// 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/>.
//! Metrics for [`RequestResponseProtocol`](super::RequestResponseProtocol).
use crate::{service::metrics::Metrics, types::ProtocolName};
use std::time::Duration;
/// Request-response metrics.
pub struct RequestResponseMetrics {
/// Metrics.
metrics: Option<Metrics>,
/// Protocol name.
protocol: ProtocolName,
}
impl RequestResponseMetrics {
pub fn new(metrics: Option<Metrics>, protocol: ProtocolName) -> Self {
Self { metrics, protocol }
}
/// Register inbound request failure to Prometheus
pub fn register_inbound_request_failure(&self, reason: &str) {
if let Some(metrics) = &self.metrics {
metrics
.requests_in_failure_total
.with_label_values(&[&self.protocol, reason])
.inc();
}
}
/// Register inbound request success to Prometheus
pub fn register_inbound_request_success(&self, serve_time: Duration) {
if let Some(metrics) = &self.metrics {
metrics
.requests_in_success_total
.with_label_values(&[&self.protocol])
.observe(serve_time.as_secs_f64());
}
}
/// Register inbound request failure to Prometheus
pub fn register_outbound_request_failure(&self, reason: &str) {
if let Some(metrics) = &self.metrics {
metrics
.requests_out_failure_total
.with_label_values(&[&self.protocol, reason])
.inc();
}
}
/// Register inbound request success to Prometheus
pub fn register_outbound_request_success(&self, duration: Duration) {
if let Some(metrics) = &self.metrics {
metrics
.requests_out_success_total
.with_label_values(&[&self.protocol])
.observe(duration.as_secs_f64());
}
}
}
@@ -0,0 +1,568 @@
// 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/>.
//! Shim for litep2p's request-response implementation to make it work with `pezsc_network`'s
//! request-response API.
use crate::{
litep2p::shim::request_response::metrics::RequestResponseMetrics,
peer_store::PeerStoreProvider,
request_responses::{IncomingRequest, OutgoingResponse},
service::{metrics::Metrics, traits::RequestResponseConfig as RequestResponseConfigT},
IfDisconnected, OutboundFailure, ProtocolName, RequestFailure,
};
use futures::{channel::oneshot, future::BoxFuture, stream::FuturesUnordered, StreamExt};
use litep2p::{
error::{ImmediateDialError, NegotiationError, SubstreamError},
protocol::request_response::{
DialOptions, RejectReason, RequestResponseError, RequestResponseEvent,
RequestResponseHandle,
},
types::RequestId,
};
use pezsc_network_types::PeerId;
use pezsc_utils::mpsc::{TracingUnboundedReceiver, TracingUnboundedSender};
use std::{
collections::HashMap,
sync::Arc,
time::{Duration, Instant},
};
mod metrics;
#[cfg(test)]
mod tests;
/// Logging target for the file.
const LOG_TARGET: &str = "sub-libp2p::request-response";
/// Type containing information related to an outbound request.
#[derive(Debug)]
pub struct OutboundRequest {
/// Peer ID.
peer: PeerId,
/// Request.
request: Vec<u8>,
/// Fallback request, if provided.
fallback_request: Option<(Vec<u8>, ProtocolName)>,
/// `oneshot::Sender` for sending the received response, or failure.
sender: oneshot::Sender<Result<(Vec<u8>, ProtocolName), RequestFailure>>,
/// What should the node do if `peer` is disconnected.
dial_behavior: IfDisconnected,
}
impl OutboundRequest {
/// Create new [`OutboundRequest`].
pub fn new(
peer: PeerId,
request: Vec<u8>,
sender: oneshot::Sender<Result<(Vec<u8>, ProtocolName), RequestFailure>>,
fallback_request: Option<(Vec<u8>, ProtocolName)>,
dial_behavior: IfDisconnected,
) -> Self {
OutboundRequest { peer, request, sender, fallback_request, dial_behavior }
}
}
/// Pending request.
struct PendingRequest {
tx: oneshot::Sender<Result<(Vec<u8>, ProtocolName), RequestFailure>>,
started: Instant,
fallback_request: Option<(Vec<u8>, ProtocolName)>,
}
impl PendingRequest {
/// Create new [`PendingRequest`].
fn new(
tx: oneshot::Sender<Result<(Vec<u8>, ProtocolName), RequestFailure>>,
started: Instant,
fallback_request: Option<(Vec<u8>, ProtocolName)>,
) -> Self {
Self { tx, started, fallback_request }
}
}
/// Request-response protocol configuration.
///
/// See [`RequestResponseConfiguration`](crate::request_response::ProtocolConfig) for more details.
#[derive(Debug)]
pub struct RequestResponseConfig {
/// Name of the protocol on the wire. Should be something like `/foo/bar`.
pub protocol_name: ProtocolName,
/// Fallback on the wire protocol names to support.
pub fallback_names: Vec<ProtocolName>,
/// Maximum allowed size, in bytes, of a request.
pub max_request_size: u64,
/// Maximum allowed size, in bytes, of a response.
pub max_response_size: u64,
/// Duration after which emitted requests are considered timed out.
pub request_timeout: Duration,
/// Channel on which the networking service will send incoming requests.
pub inbound_queue: Option<async_channel::Sender<IncomingRequest>>,
}
impl RequestResponseConfig {
/// Create new [`RequestResponseConfig`].
pub(crate) fn new(
protocol_name: ProtocolName,
fallback_names: Vec<ProtocolName>,
max_request_size: u64,
max_response_size: u64,
request_timeout: Duration,
inbound_queue: Option<async_channel::Sender<IncomingRequest>>,
) -> Self {
Self {
protocol_name,
fallback_names,
max_request_size,
max_response_size,
request_timeout,
inbound_queue,
}
}
}
impl RequestResponseConfigT for RequestResponseConfig {
fn protocol_name(&self) -> &ProtocolName {
&self.protocol_name
}
}
/// Request-response protocol.
///
/// This is slightly different from the `RequestResponsesBehaviour` in that it is protocol-specific,
/// meaning there is an instance of `RequestResponseProtocol` for each installed request-response
/// protocol and that instance deals only with the requests and responses of that protocol, nothing
/// else. It also differs from the other implementation by combining both inbound and outbound
/// requests under one instance so all request-response-related behavior of any given protocol is
/// handled through one instance of `RequestResponseProtocol`.
pub struct RequestResponseProtocol {
/// Protocol name.
protocol: ProtocolName,
/// Handle to request-response protocol.
handle: RequestResponseHandle,
/// Inbound queue for sending received requests to protocol implementation in Pezkuwi SDK.
inbound_queue: Option<async_channel::Sender<IncomingRequest>>,
/// Handle to `Peerstore`.
peerstore_handle: Arc<dyn PeerStoreProvider>,
/// Pending responses.
pending_inbound_responses: HashMap<RequestId, PendingRequest>,
/// Pending outbound responses.
pending_outbound_responses: FuturesUnordered<
BoxFuture<'static, (litep2p::PeerId, RequestId, Result<OutgoingResponse, ()>, Instant)>,
>,
/// RX channel for receiving info for outbound requests.
request_rx: TracingUnboundedReceiver<OutboundRequest>,
/// Map of supported request-response protocols which are used to support fallback requests.
///
/// If negotiation for the main protocol fails and the request was sent with a fallback,
/// [`RequestResponseProtocol`] queries this map and sends the request that protocol for
/// processing.
request_tx: HashMap<ProtocolName, TracingUnboundedSender<OutboundRequest>>,
/// Metrics, if enabled.
metrics: RequestResponseMetrics,
}
impl RequestResponseProtocol {
/// Create new [`RequestResponseProtocol`].
pub fn new(
protocol: ProtocolName,
handle: RequestResponseHandle,
peerstore_handle: Arc<dyn PeerStoreProvider>,
inbound_queue: Option<async_channel::Sender<IncomingRequest>>,
request_rx: TracingUnboundedReceiver<OutboundRequest>,
request_tx: HashMap<ProtocolName, TracingUnboundedSender<OutboundRequest>>,
metrics: Option<Metrics>,
) -> Self {
Self {
handle,
request_rx,
request_tx,
inbound_queue,
peerstore_handle,
protocol: protocol.clone(),
pending_inbound_responses: HashMap::new(),
pending_outbound_responses: FuturesUnordered::new(),
metrics: RequestResponseMetrics::new(metrics, protocol),
}
}
/// Send `request` to `peer`.
async fn on_send_request(
&mut self,
peer: PeerId,
request: Vec<u8>,
fallback_request: Option<(Vec<u8>, ProtocolName)>,
tx: oneshot::Sender<Result<(Vec<u8>, ProtocolName), RequestFailure>>,
connect: IfDisconnected,
) {
let dial_options = match connect {
IfDisconnected::TryConnect => DialOptions::Dial,
IfDisconnected::ImmediateError => DialOptions::Reject,
};
log::trace!(
target: LOG_TARGET,
"{}: send request to {:?} (fallback {:?}) (dial options: {:?})",
self.protocol,
peer,
fallback_request,
dial_options,
);
match self.handle.try_send_request(peer.into(), request, dial_options) {
Ok(request_id) => {
self.pending_inbound_responses
.insert(request_id, PendingRequest::new(tx, Instant::now(), fallback_request));
},
Err(error) => {
log::warn!(
target: LOG_TARGET,
"{}: failed to send request to {peer:?}: {error:?}",
self.protocol,
);
let _ = tx.send(Err(RequestFailure::Refused));
self.metrics.register_inbound_request_failure(error.to_string().as_ref());
},
}
}
/// Handle inbound request from `peer`
///
/// If the protocol is configured outbound only, reject the request immediately.
fn on_inbound_request(
&mut self,
peer: litep2p::PeerId,
fallback: Option<litep2p::ProtocolName>,
request_id: RequestId,
request: Vec<u8>,
) {
log::trace!(
target: LOG_TARGET,
"{}: request received from {peer:?} ({fallback:?} {request_id:?}), request size {:?}",
self.protocol,
request.len(),
);
let Some(inbound_queue) = &self.inbound_queue else {
log::trace!(
target: LOG_TARGET,
"{}: rejecting inbound request from {peer:?}, protocol configured as outbound only",
self.protocol,
);
self.handle.reject_request(request_id);
return;
};
if self.peerstore_handle.is_banned(&peer.into()) {
log::trace!(
target: LOG_TARGET,
"{}: rejecting inbound request from banned {peer:?} ({request_id:?})",
self.protocol,
);
self.handle.reject_request(request_id);
self.metrics.register_inbound_request_failure("banned-peer");
return;
}
let (tx, rx) = oneshot::channel();
match inbound_queue.try_send(IncomingRequest {
peer: peer.into(),
payload: request,
pending_response: tx,
}) {
Ok(_) => {
self.pending_outbound_responses.push(Box::pin(async move {
(peer, request_id, rx.await.map_err(|_| ()), Instant::now())
}));
},
Err(error) => {
log::trace!(
target: LOG_TARGET,
"{:?}: dropping request from {peer:?} ({request_id:?}), inbound queue full",
self.protocol,
);
self.handle.reject_request(request_id);
self.metrics.register_inbound_request_failure(error.to_string().as_ref());
},
}
}
/// Handle received inbound response.
fn on_inbound_response(
&mut self,
peer: litep2p::PeerId,
request_id: RequestId,
_fallback: Option<litep2p::ProtocolName>,
response: Vec<u8>,
) {
match self.pending_inbound_responses.remove(&request_id) {
None => log::warn!(
target: LOG_TARGET,
"{:?}: response received for {peer:?} but {request_id:?} doesn't exist",
self.protocol,
),
Some(PendingRequest { tx, started, .. }) => {
log::trace!(
target: LOG_TARGET,
"{:?}: response received for {peer:?} ({request_id:?}), response size {:?}",
self.protocol,
response.len(),
);
let _ = tx.send(Ok((response, self.protocol.clone())));
self.metrics.register_outbound_request_success(started.elapsed());
},
}
}
/// Handle failed outbound request.
fn on_request_failed(
&mut self,
peer: litep2p::PeerId,
request_id: RequestId,
error: RequestResponseError,
) {
log::debug!(
target: LOG_TARGET,
"{:?}: request failed for {peer:?} ({request_id:?}): {error:?}",
self.protocol
);
let Some(PendingRequest { tx, fallback_request, .. }) =
self.pending_inbound_responses.remove(&request_id)
else {
log::warn!(
target: LOG_TARGET,
"{:?}: request failed for peer {peer:?} but {request_id:?} doesn't exist",
self.protocol,
);
return;
};
let status = match error {
RequestResponseError::NotConnected =>
Some((RequestFailure::NotConnected, "not-connected")),
RequestResponseError::Rejected(reason) => {
let reason = match reason {
RejectReason::ConnectionClosed => "connection-closed",
RejectReason::SubstreamClosed => "substream-closed",
RejectReason::SubstreamOpenError(substream_error) => match substream_error {
SubstreamError::NegotiationError(NegotiationError::Timeout) =>
"substream-timeout",
_ => "substream-open-error",
},
RejectReason::DialFailed(None) => "dial-failed",
RejectReason::DialFailed(Some(ImmediateDialError::AlreadyConnected)) =>
"dial-already-connected",
RejectReason::DialFailed(Some(ImmediateDialError::PeerIdMissing)) =>
"dial-peerid-missing",
RejectReason::DialFailed(Some(ImmediateDialError::TriedToDialSelf)) =>
"dial-tried-to-dial-self",
RejectReason::DialFailed(Some(ImmediateDialError::NoAddressAvailable)) =>
"dial-no-address-available",
RejectReason::DialFailed(Some(ImmediateDialError::TaskClosed)) =>
"dial-task-closed",
RejectReason::DialFailed(Some(ImmediateDialError::ChannelClogged)) =>
"dial-channel-clogged",
};
Some((RequestFailure::Refused, reason))
},
RequestResponseError::Timeout =>
Some((RequestFailure::Network(OutboundFailure::Timeout), "timeout")),
RequestResponseError::Canceled => {
log::debug!(
target: LOG_TARGET,
"{}: request canceled by local node to {peer:?} ({request_id:?})",
self.protocol,
);
None
},
RequestResponseError::TooLargePayload => {
log::warn!(
target: LOG_TARGET,
"{}: tried to send too large request to {peer:?} ({request_id:?})",
self.protocol,
);
Some((RequestFailure::Refused, "payload-too-large"))
},
RequestResponseError::UnsupportedProtocol => match fallback_request {
Some((request, protocol)) => match self.request_tx.get(&protocol) {
Some(sender) => {
log::debug!(
target: LOG_TARGET,
"{}: failed to negotiate protocol with {:?}. Trying the fallback protocol ({})",
self.protocol,
peer,
protocol,
);
let outbound_request = OutboundRequest::new(
peer.into(),
request,
tx,
None,
IfDisconnected::ImmediateError,
);
// since remote peer doesn't support the main protocol (`self.protocol`),
// try to send the request over a fallback protocol by creating a new
// `OutboundRequest` from the original data, now with the fallback request
// payload, and send it over to the (fallback) request handler like it was
// a normal request.
let _ = sender.unbounded_send(outbound_request);
return;
},
None => {
log::warn!(
target: LOG_TARGET,
"{}: fallback request provided but protocol ({}) doesn't exist (peer {:?})",
self.protocol,
protocol,
peer,
);
Some((RequestFailure::Refused, "invalid-fallback-protocol"))
},
},
None => Some((RequestFailure::Refused, "unsupported-protocol")),
},
};
if let Some((error, reason)) = status {
self.metrics.register_outbound_request_failure(reason);
let _ = tx.send(Err(error));
}
}
/// Handle outbound response.
fn on_outbound_response(
&mut self,
peer: litep2p::PeerId,
request_id: RequestId,
response: OutgoingResponse,
started: Instant,
) {
let OutgoingResponse { result, reputation_changes, sent_feedback } = response;
for change in reputation_changes {
log::trace!(target: LOG_TARGET, "{}: report {peer:?}: {change:?}", self.protocol);
self.peerstore_handle.report_peer(peer.into(), change);
}
match result {
Err(()) => {
log::debug!(
target: LOG_TARGET,
"{}: response rejected ({request_id:?}) for {peer:?}",
self.protocol,
);
self.handle.reject_request(request_id);
self.metrics.register_inbound_request_failure("rejected");
},
Ok(response) => {
log::trace!(
target: LOG_TARGET,
"{}: send response ({request_id:?}) to {peer:?}, response size {}",
self.protocol,
response.len(),
);
match sent_feedback {
None => self.handle.send_response(request_id, response),
Some(feedback) =>
self.handle.send_response_with_feedback(request_id, response, feedback),
}
self.metrics.register_inbound_request_success(started.elapsed());
},
}
}
/// Start running event loop of the request-response protocol.
pub async fn run(mut self) {
loop {
tokio::select! {
event = self.handle.next() => match event {
None => return,
Some(RequestResponseEvent::RequestReceived {
peer,
fallback,
request_id,
request,
}) => self.on_inbound_request(peer, fallback, request_id, request),
Some(RequestResponseEvent::ResponseReceived { peer, request_id, fallback, response }) => {
self.on_inbound_response(peer, request_id, fallback, response);
},
Some(RequestResponseEvent::RequestFailed { peer, request_id, error }) => {
self.on_request_failed(peer, request_id, error);
},
},
event = self.pending_outbound_responses.next(), if !self.pending_outbound_responses.is_empty() => match event {
None => return,
Some((peer, request_id, Err(()), _)) => {
log::debug!(target: LOG_TARGET, "{}: reject request ({request_id:?}) from {peer:?}", self.protocol);
self.handle.reject_request(request_id);
self.metrics.register_inbound_request_failure("rejected");
}
Some((peer, request_id, Ok(response), started)) => {
self.on_outbound_response(peer, request_id, response, started);
}
},
event = self.request_rx.next() => match event {
None => return,
Some(outbound_request) => {
let OutboundRequest { peer, request, sender, dial_behavior, fallback_request } = outbound_request;
self.on_send_request(peer, request, fallback_request, sender, dial_behavior).await;
}
}
}
}
}
}
@@ -0,0 +1,906 @@
// 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::{
litep2p::{
peerstore::peerstore_handle_test,
shim::request_response::{OutboundRequest, RequestResponseProtocol},
},
request_responses::{IfDisconnected, IncomingRequest, OutgoingResponse},
ProtocolName, RequestFailure,
};
use futures::{channel::oneshot, StreamExt};
use litep2p::{
config::ConfigBuilder as Litep2pConfigBuilder,
protocol::request_response::{
ConfigBuilder, DialOptions, RequestResponseError, RequestResponseEvent,
RequestResponseHandle,
},
transport::tcp::config::Config as TcpConfig,
Litep2p, Litep2pEvent,
};
use pezsc_network_types::PeerId;
use pezsc_utils::mpsc::tracing_unbounded;
use std::{collections::HashMap, sync::Arc, task::Poll};
/// Create `litep2p` for testing.
async fn make_litep2p() -> (Litep2p, RequestResponseHandle) {
let (config, handle) = ConfigBuilder::new(litep2p::ProtocolName::from("/protocol/1"))
.with_max_size(1024)
.build();
(
Litep2p::new(
Litep2pConfigBuilder::new()
.with_request_response_protocol(config)
.with_tcp(TcpConfig {
listen_addresses: vec![
"/ip4/0.0.0.0/tcp/0".parse().unwrap(),
"/ip6/::/tcp/0".parse().unwrap(),
],
..Default::default()
})
.build(),
)
.unwrap(),
handle,
)
}
// connect two `litep2p` instances together
async fn connect_peers(litep2p1: &mut Litep2p, litep2p2: &mut Litep2p) {
let address = litep2p2.listen_addresses().next().unwrap().clone();
litep2p1.dial_address(address).await.unwrap();
let mut litep2p1_connected = false;
let mut litep2p2_connected = false;
loop {
tokio::select! {
event = litep2p1.next_event() => match event.unwrap() {
Litep2pEvent::ConnectionEstablished { .. } => {
litep2p1_connected = true;
}
_ => {},
},
event = litep2p2.next_event() => match event.unwrap() {
Litep2pEvent::ConnectionEstablished { .. } => {
litep2p2_connected = true;
}
_ => {},
}
}
if litep2p1_connected && litep2p2_connected {
break;
}
}
}
#[tokio::test]
async fn dial_failure() {
let (mut litep2p, handle) = make_litep2p().await;
let (tx, _rx) = async_channel::bounded(64);
let (outbound_tx, outbound_rx) = tracing_unbounded("outbound-request", 1000);
let senders = HashMap::from_iter([(ProtocolName::from("/protocol/1"), outbound_tx.clone())]);
let protocol = RequestResponseProtocol::new(
ProtocolName::from("/protocol/1"),
handle,
Arc::new(peerstore_handle_test()),
Some(tx),
outbound_rx,
senders,
None,
);
tokio::spawn(protocol.run());
tokio::spawn(async move { while let Some(_) = litep2p.next_event().await {} });
let peer = PeerId::random();
let (result_tx, result_rx) = oneshot::channel();
outbound_tx
.unbounded_send(OutboundRequest {
peer,
request: vec![1, 2, 3, 4],
sender: result_tx,
fallback_request: None,
dial_behavior: IfDisconnected::TryConnect,
})
.unwrap();
assert!(std::matches!(result_rx.await, Ok(Err(RequestFailure::Refused))));
}
#[tokio::test]
async fn send_request_to_disconnected_peer() {
let (mut litep2p, handle) = make_litep2p().await;
let (tx, _rx) = async_channel::bounded(64);
let (outbound_tx, outbound_rx) = tracing_unbounded("outbound-request", 1000);
let senders = HashMap::from_iter([(ProtocolName::from("/protocol/1"), outbound_tx.clone())]);
let protocol = RequestResponseProtocol::new(
ProtocolName::from("/protocol/1"),
handle,
Arc::new(peerstore_handle_test()),
Some(tx),
outbound_rx,
senders,
None,
);
tokio::spawn(protocol.run());
tokio::spawn(async move { while let Some(_) = litep2p.next_event().await {} });
let peer = PeerId::random();
let (result_tx, result_rx) = oneshot::channel();
outbound_tx
.unbounded_send(OutboundRequest {
peer,
request: vec![1, 2, 3, 4],
sender: result_tx,
fallback_request: None,
dial_behavior: IfDisconnected::ImmediateError,
})
.unwrap();
assert!(std::matches!(result_rx.await, Ok(Err(RequestFailure::NotConnected))));
}
#[tokio::test]
async fn send_request_to_disconnected_peer_and_dial() {
let (mut litep2p1, handle1) = make_litep2p().await;
let (mut litep2p2, handle2) = make_litep2p().await;
let peer1 = *litep2p1.local_peer_id();
let peer2 = *litep2p2.local_peer_id();
litep2p1.add_known_address(
peer2,
std::iter::once(litep2p2.listen_addresses().next().expect("listen address").clone()),
);
let (outbound_tx1, outbound_rx1) = tracing_unbounded("outbound-request", 1000);
let senders = HashMap::from_iter([(ProtocolName::from("/protocol/1"), outbound_tx1.clone())]);
let (tx1, _rx1) = async_channel::bounded(64);
let protocol1 = RequestResponseProtocol::new(
ProtocolName::from("/protocol/1"),
handle1,
Arc::new(peerstore_handle_test()),
Some(tx1),
outbound_rx1,
senders,
None,
);
let (outbound_tx2, outbound_rx2) = tracing_unbounded("outbound-request", 1000);
let senders = HashMap::from_iter([(ProtocolName::from("/protocol/1"), outbound_tx2)]);
let (tx2, rx2) = async_channel::bounded(64);
let protocol2 = RequestResponseProtocol::new(
ProtocolName::from("/protocol/1"),
handle2,
Arc::new(peerstore_handle_test()),
Some(tx2),
outbound_rx2,
senders,
None,
);
tokio::spawn(protocol1.run());
tokio::spawn(protocol2.run());
tokio::spawn(async move { while let Some(_) = litep2p1.next_event().await {} });
tokio::spawn(async move { while let Some(_) = litep2p2.next_event().await {} });
let (result_tx, _result_rx) = oneshot::channel();
outbound_tx1
.unbounded_send(OutboundRequest {
peer: peer2.into(),
request: vec![1, 2, 3, 4],
sender: result_tx,
fallback_request: None,
dial_behavior: IfDisconnected::TryConnect,
})
.unwrap();
match rx2.recv().await {
Ok(IncomingRequest { peer, payload, .. }) => {
assert_eq!(peer, Into::<PeerId>::into(peer1));
assert_eq!(payload, vec![1, 2, 3, 4]);
},
Err(error) => panic!("unexpected error: {error:?}"),
}
}
#[tokio::test]
async fn too_many_inbound_requests() {
let (mut litep2p1, handle1) = make_litep2p().await;
let (mut litep2p2, mut handle2) = make_litep2p().await;
let peer1 = *litep2p1.local_peer_id();
connect_peers(&mut litep2p1, &mut litep2p2).await;
let (outbound_tx, outbound_rx) = tracing_unbounded("outbound-request", 1000);
let senders = HashMap::from_iter([(ProtocolName::from("/protocol/1"), outbound_tx)]);
let (tx, _rx) = async_channel::bounded(4);
let protocol = RequestResponseProtocol::new(
ProtocolName::from("/protocol/1"),
handle1,
Arc::new(peerstore_handle_test()),
Some(tx),
outbound_rx,
senders,
None,
);
tokio::spawn(protocol.run());
tokio::spawn(async move { while let Some(_) = litep2p1.next_event().await {} });
tokio::spawn(async move { while let Some(_) = litep2p2.next_event().await {} });
// send 5 request and verify that one of the requests will fail
for _ in 0..5 {
handle2
.send_request(peer1, vec![1, 2, 3, 4], DialOptions::Reject)
.await
.unwrap();
}
// verify that one of the requests is rejected
match handle2.next().await {
Some(RequestResponseEvent::RequestFailed { peer, error, .. }) => {
assert_eq!(peer, peer1);
assert_eq!(
error,
RequestResponseError::Rejected(
litep2p::protocol::request_response::RejectReason::SubstreamClosed
)
);
},
event => panic!("inavlid event: {event:?}"),
}
// verify that no other events are read from the handle
futures::future::poll_fn(|cx| match handle2.poll_next_unpin(cx) {
Poll::Pending => Poll::Ready(()),
event => panic!("invalid event: {event:?}"),
})
.await;
}
#[tokio::test]
async fn feedback_works() {
let (mut litep2p1, handle1) = make_litep2p().await;
let (mut litep2p2, mut handle2) = make_litep2p().await;
let peer1 = *litep2p1.local_peer_id();
let peer2 = *litep2p2.local_peer_id();
connect_peers(&mut litep2p1, &mut litep2p2).await;
let (outbound_tx, outbound_rx) = tracing_unbounded("outbound-request", 1000);
let senders = HashMap::from_iter([(ProtocolName::from("/protocol/1"), outbound_tx)]);
let (tx, rx) = async_channel::bounded(4);
let protocol = RequestResponseProtocol::new(
ProtocolName::from("/protocol/1"),
handle1,
Arc::new(peerstore_handle_test()),
Some(tx),
outbound_rx,
senders,
None,
);
tokio::spawn(protocol.run());
tokio::spawn(async move { while let Some(_) = litep2p1.next_event().await {} });
tokio::spawn(async move { while let Some(_) = litep2p2.next_event().await {} });
let request_id = handle2
.send_request(peer1, vec![1, 2, 3, 4], DialOptions::Reject)
.await
.unwrap();
let rx = match rx.recv().await {
Ok(IncomingRequest { peer, payload, pending_response }) => {
assert_eq!(peer, peer2.into());
assert_eq!(payload, vec![1, 2, 3, 4]);
let (tx, rx) = oneshot::channel();
pending_response
.send(OutgoingResponse {
result: Ok(vec![5, 6, 7, 8]),
reputation_changes: Vec::new(),
sent_feedback: Some(tx),
})
.unwrap();
rx
},
event => panic!("invalid event: {event:?}"),
};
match handle2.next().await {
Some(RequestResponseEvent::ResponseReceived {
peer,
request_id: received_id,
response,
..
}) => {
assert_eq!(peer, peer1);
assert_eq!(request_id, received_id);
assert_eq!(response, vec![5, 6, 7, 8]);
assert!(rx.await.is_ok());
},
event => panic!("invalid event: {event:?}"),
}
}
#[tokio::test]
async fn fallback_request_compatible_peers() {
// `litep2p1` supports both the new and the old protocol
let (mut litep2p1, handle1_1, handle1_2) = {
let (config1, handle1) = ConfigBuilder::new(litep2p::ProtocolName::from("/protocol/2"))
.with_max_size(1024)
.build();
let (config2, handle2) = ConfigBuilder::new(litep2p::ProtocolName::from("/protocol/1"))
.with_max_size(1024)
.build();
(
Litep2p::new(
Litep2pConfigBuilder::new()
.with_request_response_protocol(config1)
.with_request_response_protocol(config2)
.with_tcp(TcpConfig {
listen_addresses: vec![
"/ip4/0.0.0.0/tcp/0".parse().unwrap(),
"/ip6/::/tcp/0".parse().unwrap(),
],
..Default::default()
})
.build(),
)
.unwrap(),
handle1,
handle2,
)
};
// `litep2p2` supports only the new protocol
let (config2, handle2) = ConfigBuilder::new(litep2p::ProtocolName::from("/protocol/2"))
.with_max_size(1024)
.build();
let mut litep2p2 = Litep2p::new(
Litep2pConfigBuilder::new()
.with_request_response_protocol(config2)
.with_tcp(TcpConfig {
listen_addresses: vec![
"/ip4/0.0.0.0/tcp/0".parse().unwrap(),
"/ip6/::/tcp/0".parse().unwrap(),
],
..Default::default()
})
.build(),
)
.unwrap();
let peer1 = *litep2p1.local_peer_id();
let peer2 = *litep2p2.local_peer_id();
connect_peers(&mut litep2p1, &mut litep2p2).await;
let (outbound_tx1, outbound_rx1) = tracing_unbounded("outbound-request", 1000);
let (outbound_tx_fallback, outbound_rx_fallback) = tracing_unbounded("outbound-request", 1000);
let senders1 = HashMap::from_iter([
(ProtocolName::from("/protocol/2"), outbound_tx1.clone()),
(ProtocolName::from("/protocol/1"), outbound_tx_fallback),
]);
let (tx1, _rx1) = async_channel::bounded(4);
let protocol1 = RequestResponseProtocol::new(
ProtocolName::from("/protocol/2"),
handle1_1,
Arc::new(peerstore_handle_test()),
Some(tx1),
outbound_rx1,
senders1.clone(),
None,
);
let (tx_fallback, _rx_fallback) = async_channel::bounded(4);
let protocol_fallback = RequestResponseProtocol::new(
ProtocolName::from("/protocol/1"),
handle1_2,
Arc::new(peerstore_handle_test()),
Some(tx_fallback),
outbound_rx_fallback,
senders1,
None,
);
let (outbound_tx2, outbound_rx2) = tracing_unbounded("outbound-request", 1000);
let senders2 = HashMap::from_iter([(ProtocolName::from("/protocol/2"), outbound_tx2)]);
let (tx2, rx2) = async_channel::bounded(4);
let protocol2 = RequestResponseProtocol::new(
ProtocolName::from("/protocol/2"),
handle2,
Arc::new(peerstore_handle_test()),
Some(tx2),
outbound_rx2,
senders2,
None,
);
tokio::spawn(protocol1.run());
tokio::spawn(protocol2.run());
tokio::spawn(protocol_fallback.run());
tokio::spawn(async move { while let Some(_) = litep2p1.next_event().await {} });
tokio::spawn(async move { while let Some(_) = litep2p2.next_event().await {} });
let (result_tx, result_rx) = oneshot::channel();
outbound_tx1
.unbounded_send(OutboundRequest {
peer: peer2.into(),
request: vec![1, 2, 3, 4],
sender: result_tx,
fallback_request: Some((vec![1, 3, 3, 7], ProtocolName::from("/protocol/1"))),
dial_behavior: IfDisconnected::ImmediateError,
})
.unwrap();
match rx2.recv().await {
Ok(IncomingRequest { peer, payload, pending_response }) => {
assert_eq!(peer, peer1.into());
assert_eq!(payload, vec![1, 2, 3, 4]);
pending_response
.send(OutgoingResponse {
result: Ok(vec![5, 6, 7, 8]),
reputation_changes: Vec::new(),
sent_feedback: None,
})
.unwrap();
},
event => panic!("invalid event: {event:?}"),
}
match result_rx.await {
Ok(Ok((response, protocol))) => {
assert_eq!(response, vec![5, 6, 7, 8]);
assert_eq!(protocol, ProtocolName::from("/protocol/2"));
},
event => panic!("invalid event: {event:?}"),
}
}
#[tokio::test]
async fn fallback_request_old_peer_receives() {
pezsp_tracing::try_init_simple();
// `litep2p1` supports both the new and the old protocol
let (mut litep2p1, handle1_1, handle1_2) = {
let (config1, handle1) = ConfigBuilder::new(litep2p::ProtocolName::from("/protocol/2"))
.with_max_size(1024)
.build();
let (config2, handle2) = ConfigBuilder::new(litep2p::ProtocolName::from("/protocol/1"))
.with_max_size(1024)
.build();
(
Litep2p::new(
Litep2pConfigBuilder::new()
.with_request_response_protocol(config1)
.with_request_response_protocol(config2)
.with_tcp(TcpConfig {
listen_addresses: vec![
"/ip4/0.0.0.0/tcp/0".parse().unwrap(),
"/ip6/::/tcp/0".parse().unwrap(),
],
..Default::default()
})
.build(),
)
.unwrap(),
handle1,
handle2,
)
};
// `litep2p2` supports only the new protocol
let (config2, handle2) = ConfigBuilder::new(litep2p::ProtocolName::from("/protocol/1"))
.with_max_size(1024)
.build();
let mut litep2p2 = Litep2p::new(
Litep2pConfigBuilder::new()
.with_request_response_protocol(config2)
.with_tcp(TcpConfig {
listen_addresses: vec![
"/ip4/0.0.0.0/tcp/0".parse().unwrap(),
"/ip6/::/tcp/0".parse().unwrap(),
],
..Default::default()
})
.build(),
)
.unwrap();
let peer1 = *litep2p1.local_peer_id();
let peer2 = *litep2p2.local_peer_id();
connect_peers(&mut litep2p1, &mut litep2p2).await;
let (outbound_tx1, outbound_rx1) = tracing_unbounded("outbound-request", 1000);
let (outbound_tx_fallback, outbound_rx_fallback) = tracing_unbounded("outbound-request", 1000);
let senders1 = HashMap::from_iter([
(ProtocolName::from("/protocol/2"), outbound_tx1.clone()),
(ProtocolName::from("/protocol/1"), outbound_tx_fallback),
]);
let (tx1, _rx1) = async_channel::bounded(4);
let protocol1 = RequestResponseProtocol::new(
ProtocolName::from("/protocol/2"),
handle1_1,
Arc::new(peerstore_handle_test()),
Some(tx1),
outbound_rx1,
senders1.clone(),
None,
);
let (tx_fallback, _rx_fallback) = async_channel::bounded(4);
let protocol_fallback = RequestResponseProtocol::new(
ProtocolName::from("/protocol/1"),
handle1_2,
Arc::new(peerstore_handle_test()),
Some(tx_fallback),
outbound_rx_fallback,
senders1,
None,
);
let (outbound_tx2, outbound_rx2) = tracing_unbounded("outbound-request", 1000);
let senders2 = HashMap::from_iter([(ProtocolName::from("/protocol/1"), outbound_tx2)]);
let (tx2, rx2) = async_channel::bounded(4);
let protocol2 = RequestResponseProtocol::new(
ProtocolName::from("/protocol/1"),
handle2,
Arc::new(peerstore_handle_test()),
Some(tx2),
outbound_rx2,
senders2,
None,
);
tokio::spawn(protocol1.run());
tokio::spawn(protocol2.run());
tokio::spawn(protocol_fallback.run());
tokio::spawn(async move { while let Some(_) = litep2p1.next_event().await {} });
tokio::spawn(async move { while let Some(_) = litep2p2.next_event().await {} });
let (result_tx, result_rx) = oneshot::channel();
outbound_tx1
.unbounded_send(OutboundRequest {
peer: peer2.into(),
request: vec![1, 2, 3, 4],
sender: result_tx,
fallback_request: Some((vec![1, 3, 3, 7], ProtocolName::from("/protocol/1"))),
dial_behavior: IfDisconnected::ImmediateError,
})
.unwrap();
match rx2.recv().await {
Ok(IncomingRequest { peer, payload, pending_response }) => {
assert_eq!(peer, peer1.into());
assert_eq!(payload, vec![1, 3, 3, 7]);
pending_response
.send(OutgoingResponse {
result: Ok(vec![1, 3, 3, 8]),
reputation_changes: Vec::new(),
sent_feedback: None,
})
.unwrap();
},
event => panic!("invalid event: {event:?}"),
}
match result_rx.await {
Ok(Ok((response, protocol))) => {
assert_eq!(response, vec![1, 3, 3, 8]);
assert_eq!(protocol, ProtocolName::from("/protocol/1"));
},
event => panic!("invalid event: {event:?}"),
}
}
#[tokio::test]
async fn fallback_request_old_peer_sends() {
pezsp_tracing::try_init_simple();
// `litep2p1` supports both the new and the old protocol
let (mut litep2p1, handle1_1, handle1_2) = {
let (config1, handle1) = ConfigBuilder::new(litep2p::ProtocolName::from("/protocol/2"))
.with_max_size(1024)
.build();
let (config2, handle2) = ConfigBuilder::new(litep2p::ProtocolName::from("/protocol/1"))
.with_max_size(1024)
.build();
(
Litep2p::new(
Litep2pConfigBuilder::new()
.with_request_response_protocol(config1)
.with_request_response_protocol(config2)
.with_tcp(TcpConfig {
listen_addresses: vec![
"/ip4/0.0.0.0/tcp/0".parse().unwrap(),
"/ip6/::/tcp/0".parse().unwrap(),
],
..Default::default()
})
.build(),
)
.unwrap(),
handle1,
handle2,
)
};
// `litep2p2` supports only the new protocol
let (config2, handle2) = ConfigBuilder::new(litep2p::ProtocolName::from("/protocol/1"))
.with_max_size(1024)
.build();
let mut litep2p2 = Litep2p::new(
Litep2pConfigBuilder::new()
.with_request_response_protocol(config2)
.with_tcp(TcpConfig {
listen_addresses: vec![
"/ip4/0.0.0.0/tcp/0".parse().unwrap(),
"/ip6/::/tcp/0".parse().unwrap(),
],
..Default::default()
})
.build(),
)
.unwrap();
let peer1 = *litep2p1.local_peer_id();
let peer2 = *litep2p2.local_peer_id();
connect_peers(&mut litep2p1, &mut litep2p2).await;
let (outbound_tx1, outbound_rx1) = tracing_unbounded("outbound-request", 1000);
let (outbound_tx_fallback, outbound_rx_fallback) = tracing_unbounded("outbound-request", 1000);
let senders1 = HashMap::from_iter([
(ProtocolName::from("/protocol/2"), outbound_tx1.clone()),
(ProtocolName::from("/protocol/1"), outbound_tx_fallback),
]);
let (tx1, _rx1) = async_channel::bounded(4);
let protocol1 = RequestResponseProtocol::new(
ProtocolName::from("/protocol/2"),
handle1_1,
Arc::new(peerstore_handle_test()),
Some(tx1),
outbound_rx1,
senders1.clone(),
None,
);
let (tx_fallback, rx_fallback) = async_channel::bounded(4);
let protocol_fallback = RequestResponseProtocol::new(
ProtocolName::from("/protocol/1"),
handle1_2,
Arc::new(peerstore_handle_test()),
Some(tx_fallback),
outbound_rx_fallback,
senders1,
None,
);
let (outbound_tx2, outbound_rx2) = tracing_unbounded("outbound-request", 1000);
let senders2 = HashMap::from_iter([(ProtocolName::from("/protocol/1"), outbound_tx2.clone())]);
let (tx2, _rx2) = async_channel::bounded(4);
let protocol2 = RequestResponseProtocol::new(
ProtocolName::from("/protocol/1"),
handle2,
Arc::new(peerstore_handle_test()),
Some(tx2),
outbound_rx2,
senders2,
None,
);
tokio::spawn(protocol1.run());
tokio::spawn(protocol2.run());
tokio::spawn(protocol_fallback.run());
tokio::spawn(async move { while let Some(_) = litep2p1.next_event().await {} });
tokio::spawn(async move { while let Some(_) = litep2p2.next_event().await {} });
let (result_tx, result_rx) = oneshot::channel();
outbound_tx2
.unbounded_send(OutboundRequest {
peer: peer1.into(),
request: vec![1, 2, 3, 4],
sender: result_tx,
fallback_request: None,
dial_behavior: IfDisconnected::ImmediateError,
})
.unwrap();
match rx_fallback.recv().await {
Ok(IncomingRequest { peer, payload, pending_response }) => {
assert_eq!(peer, peer2.into());
assert_eq!(payload, vec![1, 2, 3, 4]);
pending_response
.send(OutgoingResponse {
result: Ok(vec![1, 3, 3, 8]),
reputation_changes: Vec::new(),
sent_feedback: None,
})
.unwrap();
},
event => panic!("invalid event: {event:?}"),
}
match result_rx.await {
Ok(Ok((response, protocol))) => {
assert_eq!(response, vec![1, 3, 3, 8]);
assert_eq!(protocol, ProtocolName::from("/protocol/1"));
},
event => panic!("invalid event: {event:?}"),
}
}
#[tokio::test]
async fn old_protocol_supported_but_no_fallback_provided() {
pezsp_tracing::try_init_simple();
// `litep2p1` supports both the new and the old protocol
let (mut litep2p1, handle1_1, handle1_2) = {
let (config1, handle1) = ConfigBuilder::new(litep2p::ProtocolName::from("/protocol/2"))
.with_max_size(1024)
.build();
let (config2, handle2) = ConfigBuilder::new(litep2p::ProtocolName::from("/protocol/1"))
.with_max_size(1024)
.build();
(
Litep2p::new(
Litep2pConfigBuilder::new()
.with_request_response_protocol(config1)
.with_request_response_protocol(config2)
.with_tcp(TcpConfig {
listen_addresses: vec![
"/ip4/0.0.0.0/tcp/0".parse().unwrap(),
"/ip6/::/tcp/0".parse().unwrap(),
],
..Default::default()
})
.build(),
)
.unwrap(),
handle1,
handle2,
)
};
// `litep2p2` supports only the old protocol
let (config2, handle2) = ConfigBuilder::new(litep2p::ProtocolName::from("/protocol/1"))
.with_max_size(1024)
.build();
let mut litep2p2 = Litep2p::new(
Litep2pConfigBuilder::new()
.with_request_response_protocol(config2)
.with_tcp(TcpConfig {
listen_addresses: vec![
"/ip4/0.0.0.0/tcp/0".parse().unwrap(),
"/ip6/::/tcp/0".parse().unwrap(),
],
..Default::default()
})
.build(),
)
.unwrap();
let peer2 = *litep2p2.local_peer_id();
connect_peers(&mut litep2p1, &mut litep2p2).await;
let (outbound_tx1, outbound_rx1) = tracing_unbounded("outbound-request", 1000);
let (outbound_tx_fallback, outbound_rx_fallback) = tracing_unbounded("outbound-request", 1000);
let senders1 = HashMap::from_iter([
(ProtocolName::from("/protocol/2"), outbound_tx1.clone()),
(ProtocolName::from("/protocol/1"), outbound_tx_fallback),
]);
let (tx1, _rx1) = async_channel::bounded(4);
let protocol1 = RequestResponseProtocol::new(
ProtocolName::from("/protocol/2"),
handle1_1,
Arc::new(peerstore_handle_test()),
Some(tx1),
outbound_rx1,
senders1.clone(),
None,
);
let (tx_fallback, _rx_fallback) = async_channel::bounded(4);
let protocol_fallback = RequestResponseProtocol::new(
ProtocolName::from("/protocol/1"),
handle1_2,
Arc::new(peerstore_handle_test()),
Some(tx_fallback),
outbound_rx_fallback,
senders1,
None,
);
let (outbound_tx2, outbound_rx2) = tracing_unbounded("outbound-request", 1000);
let senders2 = HashMap::from_iter([(ProtocolName::from("/protocol/1"), outbound_tx2)]);
let (tx2, _rx2) = async_channel::bounded(4);
let protocol2 = RequestResponseProtocol::new(
ProtocolName::from("/protocol/1"),
handle2,
Arc::new(peerstore_handle_test()),
Some(tx2),
outbound_rx2,
senders2,
None,
);
tokio::spawn(protocol1.run());
tokio::spawn(protocol2.run());
tokio::spawn(protocol_fallback.run());
tokio::spawn(async move { while let Some(_) = litep2p1.next_event().await {} });
tokio::spawn(async move { while let Some(_) = litep2p2.next_event().await {} });
let (result_tx, result_rx) = oneshot::channel();
outbound_tx1
.unbounded_send(OutboundRequest {
peer: peer2.into(),
request: vec![1, 2, 3, 4],
sender: result_tx,
fallback_request: None,
dial_behavior: IfDisconnected::ImmediateError,
})
.unwrap();
match result_rx.await {
Ok(Err(error)) => {
assert!(std::matches!(error, RequestFailure::Refused));
},
event => panic!("invalid event: {event:?}"),
}
}
+73
View File
@@ -0,0 +1,73 @@
// 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/>.
//! Mocked components for tests.
use crate::{
peer_store::{PeerStoreProvider, ProtocolHandle},
ReputationChange,
};
use pezsc_network_common::role::ObservedRole;
use pezsc_network_types::PeerId;
use std::{collections::HashSet, sync::Arc};
/// No-op `PeerStore`.
#[derive(Debug)]
pub struct MockPeerStore {}
impl PeerStoreProvider for MockPeerStore {
fn is_banned(&self, _peer_id: &PeerId) -> bool {
// Make sure that the peer is not banned.
false
}
fn register_protocol(&self, _protocol_handle: Arc<dyn ProtocolHandle>) {
// Make sure not to fail.
}
fn report_disconnect(&self, _peer_id: PeerId) {
// Make sure not to fail.
}
fn report_peer(&self, _peer_id: PeerId, _change: ReputationChange) {
// Make sure not to fail.
}
fn peer_reputation(&self, _peer_id: &PeerId) -> i32 {
// Make sure that the peer is not banned.
0
}
fn peer_role(&self, _peer_id: &PeerId) -> Option<ObservedRole> {
None
}
fn set_peer_role(&self, _peer_id: &PeerId, _role: ObservedRole) {
unimplemented!();
}
fn outgoing_candidates(&self, _count: usize, _ignored: HashSet<PeerId>) -> Vec<PeerId> {
unimplemented!()
}
fn add_known_peer(&self, _peer_id: PeerId) {
unimplemented!()
}
}
@@ -0,0 +1,124 @@
// 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/>.
//! Information about the networking, for diagnostic purposes.
//!
//! **Warning**: These APIs are not stable.
use libp2p::{
core::{ConnectedPoint, Endpoint as CoreEndpoint},
Multiaddr,
};
use serde::{Deserialize, Serialize};
use std::{
collections::{HashMap, HashSet},
time::Duration,
};
/// Returns general information about the networking.
///
/// Meant for general diagnostic purposes.
///
/// **Warning**: This API is not stable.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NetworkState {
/// PeerId of the local node.
pub peer_id: String,
/// List of addresses the node is currently listening on.
pub listened_addresses: HashSet<Multiaddr>,
/// List of addresses the node knows it can be reached as.
pub external_addresses: HashSet<Multiaddr>,
/// List of node we're connected to.
pub connected_peers: HashMap<String, Peer>,
/// List of node that we know of but that we're not connected to.
pub not_connected_peers: HashMap<String, NotConnectedPeer>,
/// State of the peerset manager.
pub peerset: serde_json::Value,
}
/// Part of the `NetworkState` struct. Unstable.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Peer {
/// How we are connected to the node.
pub endpoint: PeerEndpoint,
/// Node information, as provided by the node itself. Can be empty if not known yet.
pub version_string: Option<String>,
/// Latest ping duration with this node.
pub latest_ping_time: Option<Duration>,
/// List of addresses known for this node.
pub known_addresses: HashSet<Multiaddr>,
}
/// Part of the `NetworkState` struct. Unstable.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NotConnectedPeer {
/// List of addresses known for this node.
pub known_addresses: HashSet<Multiaddr>,
/// Node information, as provided by the node itself, if we were ever connected to this node.
pub version_string: Option<String>,
/// Latest ping duration with this node, if we were ever connected to this node.
pub latest_ping_time: Option<Duration>,
}
/// Part of the `NetworkState` struct. Unstable.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum PeerEndpoint {
/// We are dialing the given address.
Dialing(Multiaddr, Endpoint),
/// We are listening.
Listening {
/// Local address of the connection.
local_addr: Multiaddr,
/// Address data is sent back to.
send_back_addr: Multiaddr,
},
}
/// Part of the `NetworkState` struct. Unstable.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum Endpoint {
/// The socket comes from a dialer.
Dialer,
/// The socket comes from a listener.
Listener,
}
impl From<ConnectedPoint> for PeerEndpoint {
fn from(endpoint: ConnectedPoint) -> Self {
match endpoint {
ConnectedPoint::Dialer { address, role_override, port_use: _ } =>
Self::Dialing(address, role_override.into()),
ConnectedPoint::Listener { local_addr, send_back_addr } =>
Self::Listening { local_addr, send_back_addr },
}
}
}
impl From<CoreEndpoint> for Endpoint {
fn from(endpoint: CoreEndpoint) -> Self {
match endpoint {
CoreEndpoint::Dialer => Self::Dialer,
CoreEndpoint::Listener => Self::Listener,
}
}
}
+664
View File
@@ -0,0 +1,664 @@
// 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/>.
//! [`PeerInfoBehaviour`] is implementation of `NetworkBehaviour` that holds information about peers
//! in cache.
use crate::{utils::interval, LOG_TARGET};
use either::Either;
use fnv::FnvHashMap;
use futures::prelude::*;
use libp2p::{
core::{transport::PortUse, ConnectedPoint, Endpoint},
identify::{
Behaviour as Identify, Config as IdentifyConfig, Event as IdentifyEvent,
Info as IdentifyInfo,
},
identity::PublicKey,
multiaddr::Protocol,
ping::{Behaviour as Ping, Config as PingConfig, Event as PingEvent},
swarm::{
behaviour::{
AddressChange, ConnectionClosed, ConnectionEstablished, DialFailure, FromSwarm,
ListenFailure,
},
ConnectionDenied, ConnectionHandler, ConnectionHandlerSelect, ConnectionId,
NetworkBehaviour, THandler, THandlerInEvent, THandlerOutEvent, ToSwarm,
},
Multiaddr, PeerId,
};
use log::{debug, error, trace, warn};
use parking_lot::Mutex;
use schnellru::{ByLength, LruMap};
use smallvec::SmallVec;
use std::{
collections::{hash_map::Entry, HashSet, VecDeque},
iter,
pin::Pin,
sync::Arc,
task::{Context, Poll},
time::{Duration, Instant},
};
/// Time after we disconnect from a node before we purge its information from the cache.
const CACHE_EXPIRE: Duration = Duration::from_secs(10 * 60);
/// Interval at which we perform garbage collection on the node info.
const GARBAGE_COLLECT_INTERVAL: Duration = Duration::from_secs(2 * 60);
/// The maximum number of tracked external addresses we allow.
const MAX_EXTERNAL_ADDRESSES: u32 = 32;
/// Number of times observed address is received from different peers before it is confirmed as
/// external.
const MIN_ADDRESS_CONFIRMATIONS: usize = 3;
/// Implementation of `NetworkBehaviour` that holds information about peers in cache.
pub struct PeerInfoBehaviour {
/// Periodically ping nodes, and close the connection if it's unresponsive.
ping: Ping,
/// Periodically identifies the remote and responds to incoming requests.
identify: Identify,
/// Information that we know about all nodes.
nodes_info: FnvHashMap<PeerId, NodeInfo>,
/// Interval at which we perform garbage collection in `nodes_info`.
garbage_collect: Pin<Box<dyn Stream<Item = ()> + Send>>,
/// PeerId of the local node.
local_peer_id: PeerId,
/// Public addresses supplied by the operator. Never expire.
public_addresses: Vec<Multiaddr>,
/// Listen addresses. External addresses matching listen addresses never expire.
listen_addresses: HashSet<Multiaddr>,
/// External address confirmations.
address_confirmations: LruMap<Multiaddr, HashSet<PeerId>>,
/// Record keeping of external addresses. Data is queried by the `NetworkService`.
/// The addresses contain the `/p2p/...` part with local peer ID.
external_addresses: ExternalAddresses,
/// Pending events to emit to [`Swarm`](libp2p::swarm::Swarm).
pending_actions: VecDeque<ToSwarm<PeerInfoEvent, THandlerInEvent<PeerInfoBehaviour>>>,
}
/// Information about a node we're connected to.
#[derive(Debug)]
struct NodeInfo {
/// When we will remove the entry about this node from the list, or `None` if we're connected
/// to the node.
info_expire: Option<Instant>,
/// Non-empty list of connected endpoints, one per connection.
endpoints: SmallVec<[ConnectedPoint; crate::MAX_CONNECTIONS_PER_PEER]>,
/// Version reported by the remote, or `None` if unknown.
client_version: Option<String>,
/// Latest ping time with this node.
latest_ping: Option<Duration>,
}
impl NodeInfo {
fn new(endpoint: ConnectedPoint) -> Self {
let mut endpoints = SmallVec::new();
endpoints.push(endpoint);
Self { info_expire: None, endpoints, client_version: None, latest_ping: None }
}
}
/// Utility struct for tracking external addresses. The data is shared with the `NetworkService`.
#[derive(Debug, Clone, Default)]
pub struct ExternalAddresses {
addresses: Arc<Mutex<HashSet<Multiaddr>>>,
}
impl ExternalAddresses {
/// Add an external address.
pub fn add(&mut self, addr: Multiaddr) -> bool {
self.addresses.lock().insert(addr)
}
/// Remove an external address.
pub fn remove(&mut self, addr: &Multiaddr) -> bool {
self.addresses.lock().remove(addr)
}
}
impl PeerInfoBehaviour {
/// Builds a new `PeerInfoBehaviour`.
pub fn new(
user_agent: String,
local_public_key: PublicKey,
external_addresses: Arc<Mutex<HashSet<Multiaddr>>>,
public_addresses: Vec<Multiaddr>,
) -> Self {
let identify = {
let cfg = IdentifyConfig::new("/bizinikiwi/1.0".to_string(), local_public_key.clone())
.with_agent_version(user_agent)
// We don't need any peer information cached.
.with_cache_size(0);
Identify::new(cfg)
};
Self {
ping: Ping::new(PingConfig::new()),
identify,
nodes_info: FnvHashMap::default(),
garbage_collect: Box::pin(interval(GARBAGE_COLLECT_INTERVAL)),
local_peer_id: local_public_key.to_peer_id(),
public_addresses,
listen_addresses: HashSet::new(),
address_confirmations: LruMap::new(ByLength::new(MAX_EXTERNAL_ADDRESSES)),
external_addresses: ExternalAddresses { addresses: external_addresses },
pending_actions: Default::default(),
}
}
/// Borrows `self` and returns a struct giving access to the information about a node.
///
/// Returns `None` if we don't know anything about this node. Always returns `Some` for nodes
/// we're connected to, meaning that if `None` is returned then we're not connected to that
/// node.
pub fn node(&self, peer_id: &PeerId) -> Option<Node<'_>> {
self.nodes_info.get(peer_id).map(Node)
}
/// Inserts a ping time in the cache. Has no effect if we don't have any entry for that node,
/// which shouldn't happen.
fn handle_ping_report(
&mut self,
peer_id: &PeerId,
ping_time: Duration,
connection: ConnectionId,
) {
trace!(target: LOG_TARGET, "Ping time with {:?} via {:?}: {:?}", peer_id, connection, ping_time);
if let Some(entry) = self.nodes_info.get_mut(peer_id) {
entry.latest_ping = Some(ping_time);
} else {
error!(target: LOG_TARGET,
"Received ping from node we're not connected to {:?} via {:?}", peer_id, connection);
}
}
/// Ensure address has the `/p2p/...` part with local peer id. Returns `Err` if the address
/// already contains a different peer id.
fn with_local_peer_id(&self, address: Multiaddr) -> Result<Multiaddr, Multiaddr> {
if let Some(Protocol::P2p(peer_id)) = address.iter().last() {
if peer_id == self.local_peer_id {
Ok(address)
} else {
Err(address)
}
} else {
Ok(address.with(Protocol::P2p(self.local_peer_id)))
}
}
/// Inserts an identify record in the cache & discovers external addresses when multiple
/// peers report the same address as observed.
fn handle_identify_report(&mut self, peer_id: &PeerId, info: &IdentifyInfo) {
trace!(target: LOG_TARGET, "Identified {:?} => {:?}", peer_id, info);
if let Some(entry) = self.nodes_info.get_mut(peer_id) {
entry.client_version = Some(info.agent_version.clone());
} else {
error!(target: LOG_TARGET,
"Received identify message from node we're not connected to {peer_id:?}");
}
// Discover external addresses.
match self.with_local_peer_id(info.observed_addr.clone()) {
Ok(observed_addr) => {
let (is_new, expired) = self.is_new_external_address(&observed_addr, *peer_id);
if is_new && self.external_addresses.add(observed_addr.clone()) {
trace!(
target: LOG_TARGET,
"Observed address reported by Identify confirmed as external {}",
observed_addr,
);
self.pending_actions.push_back(ToSwarm::ExternalAddrConfirmed(observed_addr));
}
if let Some(expired) = expired {
trace!(target: LOG_TARGET, "Removing replaced external address: {expired}");
self.external_addresses.remove(&expired);
self.pending_actions.push_back(ToSwarm::ExternalAddrExpired(expired));
}
},
Err(addr) => {
warn!(
target: LOG_TARGET,
"Identify reported observed address for a peer that is not us: {addr}",
);
},
}
}
/// Check if addresses are equal taking into account they can contain or not contain
/// the `/p2p/...` part.
fn is_same_address(left: &Multiaddr, right: &Multiaddr) -> bool {
let mut left = left.iter();
let mut right = right.iter();
loop {
match (left.next(), right.next()) {
(None, None) => return true,
(None, Some(Protocol::P2p(_))) => return true,
(Some(Protocol::P2p(_)), None) => return true,
(left, right) if left != right => return false,
_ => {},
}
}
}
/// Check if `address` can be considered a new external address.
///
/// If this address replaces an older address, the expired address is returned.
fn is_new_external_address(
&mut self,
address: &Multiaddr,
peer_id: PeerId,
) -> (bool, Option<Multiaddr>) {
trace!(target: LOG_TARGET, "Verify new external address: {address}");
// Public and listen addresses don't count towards discovered external addresses
// and are always confirmed.
// Because they are not kept in the LRU, they are never replaced by discovered
// external addresses.
if self
.listen_addresses
.iter()
.chain(self.public_addresses.iter())
.any(|known_address| PeerInfoBehaviour::is_same_address(&known_address, &address))
{
return (true, None);
}
match self.address_confirmations.get(address) {
Some(confirmations) => {
confirmations.insert(peer_id);
if confirmations.len() >= MIN_ADDRESS_CONFIRMATIONS {
return (true, None);
}
},
None => {
let oldest = (self.address_confirmations.len() >=
self.address_confirmations.limiter().max_length() as usize)
.then(|| {
self.address_confirmations.pop_oldest().map(|(address, peers)| {
if peers.len() >= MIN_ADDRESS_CONFIRMATIONS {
return Some(address);
} else {
None
}
})
})
.flatten()
.flatten();
self.address_confirmations
.insert(address.clone(), iter::once(peer_id).collect());
return (false, oldest);
},
}
(false, None)
}
}
/// Gives access to the information about a node.
pub struct Node<'a>(&'a NodeInfo);
impl<'a> Node<'a> {
/// Returns the endpoint of an established connection to the peer.
///
/// Returns `None` if we are disconnected from the node.
pub fn endpoint(&self) -> Option<&'a ConnectedPoint> {
self.0.endpoints.get(0)
}
/// Returns the latest version information we know of.
pub fn client_version(&self) -> Option<&'a str> {
self.0.client_version.as_deref()
}
/// Returns the latest ping time we know of for this node. `None` if we never successfully
/// pinged this node.
pub fn latest_ping(&self) -> Option<Duration> {
self.0.latest_ping
}
}
/// Event that can be emitted by the behaviour.
#[derive(Debug)]
pub enum PeerInfoEvent {
/// We have obtained identity information from a peer, including the addresses it is listening
/// on.
Identified {
/// Id of the peer that has been identified.
peer_id: PeerId,
/// Information about the peer.
info: IdentifyInfo,
},
}
impl NetworkBehaviour for PeerInfoBehaviour {
type ConnectionHandler = ConnectionHandlerSelect<
<Ping as NetworkBehaviour>::ConnectionHandler,
<Identify as NetworkBehaviour>::ConnectionHandler,
>;
type ToSwarm = PeerInfoEvent;
fn handle_pending_inbound_connection(
&mut self,
connection_id: ConnectionId,
local_addr: &Multiaddr,
remote_addr: &Multiaddr,
) -> Result<(), ConnectionDenied> {
self.ping
.handle_pending_inbound_connection(connection_id, local_addr, remote_addr)?;
self.identify
.handle_pending_inbound_connection(connection_id, local_addr, remote_addr)
}
fn handle_pending_outbound_connection(
&mut self,
_connection_id: ConnectionId,
_maybe_peer: Option<PeerId>,
_addresses: &[Multiaddr],
_effective_role: Endpoint,
) -> Result<Vec<Multiaddr>, ConnectionDenied> {
// Only `Discovery::handle_pending_outbound_connection` must be returning addresses to
// ensure that we don't return unwanted addresses.
Ok(Vec::new())
}
fn handle_established_inbound_connection(
&mut self,
connection_id: ConnectionId,
peer: PeerId,
local_addr: &Multiaddr,
remote_addr: &Multiaddr,
) -> Result<THandler<Self>, ConnectionDenied> {
let ping_handler = self.ping.handle_established_inbound_connection(
connection_id,
peer,
local_addr,
remote_addr,
)?;
let identify_handler = self.identify.handle_established_inbound_connection(
connection_id,
peer,
local_addr,
remote_addr,
)?;
Ok(ping_handler.select(identify_handler))
}
fn handle_established_outbound_connection(
&mut self,
connection_id: ConnectionId,
peer: PeerId,
addr: &Multiaddr,
role_override: Endpoint,
port_use: PortUse,
) -> Result<THandler<Self>, ConnectionDenied> {
let ping_handler = self.ping.handle_established_outbound_connection(
connection_id,
peer,
addr,
role_override,
port_use,
)?;
let identify_handler = self.identify.handle_established_outbound_connection(
connection_id,
peer,
addr,
role_override,
port_use,
)?;
Ok(ping_handler.select(identify_handler))
}
fn on_swarm_event(&mut self, event: FromSwarm) {
match event {
FromSwarm::ConnectionEstablished(
e @ ConnectionEstablished { peer_id, endpoint, .. },
) => {
self.ping.on_swarm_event(FromSwarm::ConnectionEstablished(e));
self.identify.on_swarm_event(FromSwarm::ConnectionEstablished(e));
match self.nodes_info.entry(peer_id) {
Entry::Vacant(e) => {
e.insert(NodeInfo::new(endpoint.clone()));
},
Entry::Occupied(e) => {
let e = e.into_mut();
if e.info_expire.as_ref().map(|exp| *exp < Instant::now()).unwrap_or(false)
{
e.client_version = None;
e.latest_ping = None;
}
e.info_expire = None;
e.endpoints.push(endpoint.clone());
},
}
},
FromSwarm::ConnectionClosed(ConnectionClosed {
peer_id,
connection_id,
endpoint,
cause,
remaining_established,
}) => {
self.ping.on_swarm_event(FromSwarm::ConnectionClosed(ConnectionClosed {
peer_id,
connection_id,
endpoint,
cause,
remaining_established,
}));
self.identify.on_swarm_event(FromSwarm::ConnectionClosed(ConnectionClosed {
peer_id,
connection_id,
endpoint,
cause,
remaining_established,
}));
if let Some(entry) = self.nodes_info.get_mut(&peer_id) {
if remaining_established == 0 {
entry.info_expire = Some(Instant::now() + CACHE_EXPIRE);
}
entry.endpoints.retain(|ep| ep != endpoint)
} else {
error!(target: LOG_TARGET,
"Unknown connection to {:?} closed: {:?}", peer_id, endpoint);
}
},
FromSwarm::DialFailure(DialFailure { peer_id, error, connection_id }) => {
self.ping.on_swarm_event(FromSwarm::DialFailure(DialFailure {
peer_id,
error,
connection_id,
}));
self.identify.on_swarm_event(FromSwarm::DialFailure(DialFailure {
peer_id,
error,
connection_id,
}));
},
FromSwarm::ListenerClosed(e) => {
self.ping.on_swarm_event(FromSwarm::ListenerClosed(e));
self.identify.on_swarm_event(FromSwarm::ListenerClosed(e));
},
FromSwarm::ListenFailure(ListenFailure {
local_addr,
send_back_addr,
error,
connection_id,
peer_id,
}) => {
self.ping.on_swarm_event(FromSwarm::ListenFailure(ListenFailure {
local_addr,
send_back_addr,
error,
connection_id,
peer_id,
}));
self.identify.on_swarm_event(FromSwarm::ListenFailure(ListenFailure {
local_addr,
send_back_addr,
error,
connection_id,
peer_id,
}));
},
FromSwarm::ListenerError(e) => {
self.ping.on_swarm_event(FromSwarm::ListenerError(e));
self.identify.on_swarm_event(FromSwarm::ListenerError(e));
},
FromSwarm::ExternalAddrExpired(e) => {
self.ping.on_swarm_event(FromSwarm::ExternalAddrExpired(e));
self.identify.on_swarm_event(FromSwarm::ExternalAddrExpired(e));
},
FromSwarm::NewListener(e) => {
self.ping.on_swarm_event(FromSwarm::NewListener(e));
self.identify.on_swarm_event(FromSwarm::NewListener(e));
},
FromSwarm::NewListenAddr(e) => {
self.ping.on_swarm_event(FromSwarm::NewListenAddr(e));
self.identify.on_swarm_event(FromSwarm::NewListenAddr(e));
self.listen_addresses.insert(e.addr.clone());
},
FromSwarm::ExpiredListenAddr(e) => {
self.ping.on_swarm_event(FromSwarm::ExpiredListenAddr(e));
self.identify.on_swarm_event(FromSwarm::ExpiredListenAddr(e));
self.listen_addresses.remove(e.addr);
// Remove matching external address.
match self.with_local_peer_id(e.addr.clone()) {
Ok(addr) => {
self.external_addresses.remove(&addr);
self.pending_actions.push_back(ToSwarm::ExternalAddrExpired(addr));
},
Err(addr) => {
warn!(
target: LOG_TARGET,
"Listen address expired with peer ID that is not us: {addr}",
);
},
}
},
FromSwarm::NewExternalAddrCandidate(e) => {
self.ping.on_swarm_event(FromSwarm::NewExternalAddrCandidate(e));
self.identify.on_swarm_event(FromSwarm::NewExternalAddrCandidate(e));
},
FromSwarm::ExternalAddrConfirmed(e) => {
self.ping.on_swarm_event(FromSwarm::ExternalAddrConfirmed(e));
self.identify.on_swarm_event(FromSwarm::ExternalAddrConfirmed(e));
},
FromSwarm::AddressChange(e @ AddressChange { peer_id, old, new, .. }) => {
self.ping.on_swarm_event(FromSwarm::AddressChange(e));
self.identify.on_swarm_event(FromSwarm::AddressChange(e));
if let Some(entry) = self.nodes_info.get_mut(&peer_id) {
if let Some(endpoint) = entry.endpoints.iter_mut().find(|e| e == &old) {
*endpoint = new.clone();
} else {
error!(target: LOG_TARGET,
"Unknown address change for peer {:?} from {:?} to {:?}", peer_id, old, new);
}
} else {
error!(target: LOG_TARGET,
"Unknown peer {:?} to change address from {:?} to {:?}", peer_id, old, new);
}
},
FromSwarm::NewExternalAddrOfPeer(e) => {
self.ping.on_swarm_event(FromSwarm::NewExternalAddrOfPeer(e));
self.identify.on_swarm_event(FromSwarm::NewExternalAddrOfPeer(e));
},
event => {
debug!(target: LOG_TARGET, "New unknown `FromSwarm` libp2p event: {event:?}");
self.ping.on_swarm_event(event);
self.identify.on_swarm_event(event);
},
}
}
fn on_connection_handler_event(
&mut self,
peer_id: PeerId,
connection_id: ConnectionId,
event: THandlerOutEvent<Self>,
) {
match event {
Either::Left(event) =>
self.ping.on_connection_handler_event(peer_id, connection_id, event),
Either::Right(event) =>
self.identify.on_connection_handler_event(peer_id, connection_id, event),
}
}
fn poll(&mut self, cx: &mut Context) -> Poll<ToSwarm<Self::ToSwarm, THandlerInEvent<Self>>> {
if let Some(event) = self.pending_actions.pop_front() {
return Poll::Ready(event);
}
loop {
match self.ping.poll(cx) {
Poll::Pending => break,
Poll::Ready(ToSwarm::GenerateEvent(ev)) => {
if let PingEvent { peer, result: Ok(rtt), connection } = ev {
self.handle_ping_report(&peer, rtt, connection)
}
},
Poll::Ready(event) => {
return Poll::Ready(event.map_in(Either::Left).map_out(|_| {
unreachable!("`GenerateEvent` is handled in a branch above; qed")
}));
},
}
}
loop {
match self.identify.poll(cx) {
Poll::Pending => break,
Poll::Ready(ToSwarm::GenerateEvent(event)) => match event {
IdentifyEvent::Received { peer_id, info, .. } => {
self.handle_identify_report(&peer_id, &info);
let event = PeerInfoEvent::Identified { peer_id, info };
return Poll::Ready(ToSwarm::GenerateEvent(event));
},
IdentifyEvent::Error { connection_id, peer_id, error } => {
debug!(
target: LOG_TARGET,
"Identification with peer {peer_id:?}({connection_id}) failed => {error}"
);
},
IdentifyEvent::Pushed { .. } => {},
IdentifyEvent::Sent { .. } => {},
},
Poll::Ready(event) => {
return Poll::Ready(event.map_in(Either::Right).map_out(|_| {
unreachable!("`GenerateEvent` is handled in a branch above; qed")
}));
},
}
}
while let Poll::Ready(Some(())) = self.garbage_collect.poll_next_unpin(cx) {
self.nodes_info.retain(|_, node| {
node.info_expire.as_ref().map(|exp| *exp >= Instant::now()).unwrap_or(true)
});
}
Poll::Pending
}
}
+573
View File
@@ -0,0 +1,573 @@
// 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/>.
//! [`PeerStore`] manages peer reputations and provides connection candidates to
//! [`crate::protocol_controller::ProtocolController`].
use crate::service::{metrics::PeerStoreMetrics, traits::PeerStore as PeerStoreT};
use libp2p::PeerId;
use log::trace;
use parking_lot::Mutex;
use partial_sort::PartialSort;
use prometheus_endpoint::Registry;
use pezsc_network_common::{role::ObservedRole, types::ReputationChange};
use std::{
cmp::{Ord, Ordering, PartialOrd},
collections::{hash_map::Entry, HashMap, HashSet},
fmt::Debug,
sync::Arc,
time::{Duration, Instant},
};
use wasm_timer::Delay;
/// Log target for this file.
pub const LOG_TARGET: &str = "peerset";
/// We don't accept nodes whose reputation is under this value.
pub const BANNED_THRESHOLD: i32 = 71 * (i32::MIN / 100);
/// Reputation change for a node when we get disconnected from it.
const DISCONNECT_REPUTATION_CHANGE: i32 = -256;
/// Relative decrement of a reputation value that is applied every second. I.e., for inverse
/// decrement of 200 we decrease absolute value of the reputation by 1/200.
///
/// This corresponds to a factor of `k = 0.955`, where k = 1 - 1 / INVERSE_DECREMENT.
///
/// It takes ~ `ln(0.5) / ln(k)` seconds to reduce the reputation by half, or 138.63 seconds for the
/// values above.
///
/// In this setup:
/// - `i32::MAX` becomes 0 in exactly 3544 seconds, or approximately 59 minutes
/// - `i32::MIN` becomes 0 in exactly 3544 seconds, or approximately 59 minutes
/// - `i32::MIN` escapes the banned threshold in 69 seconds
const INVERSE_DECREMENT: i32 = 200;
/// Amount of time between the moment we last updated the [`PeerStore`] entry and the moment we
/// remove it, once the reputation value reaches 0.
const FORGET_AFTER: Duration = Duration::from_secs(3600);
/// Trait describing the required functionality from a `Peerset` handle.
pub trait ProtocolHandle: Debug + Send + Sync {
/// Disconnect peer.
fn disconnect_peer(&self, peer_id: pezsc_network_types::PeerId);
}
/// Trait providing peer reputation management and connection candidates.
pub trait PeerStoreProvider: Debug + Send + Sync {
/// Check whether the peer is banned.
fn is_banned(&self, peer_id: &pezsc_network_types::PeerId) -> bool;
/// Register a protocol handle to disconnect peers whose reputation drops below the threshold.
fn register_protocol(&self, protocol_handle: Arc<dyn ProtocolHandle>);
/// Report peer disconnection for reputation adjustment.
fn report_disconnect(&self, peer_id: pezsc_network_types::PeerId);
/// Adjust peer reputation.
fn report_peer(&self, peer_id: pezsc_network_types::PeerId, change: ReputationChange);
/// Set peer role.
fn set_peer_role(&self, peer_id: &pezsc_network_types::PeerId, role: ObservedRole);
/// Get peer reputation.
fn peer_reputation(&self, peer_id: &pezsc_network_types::PeerId) -> i32;
/// Get peer role, if available.
fn peer_role(&self, peer_id: &pezsc_network_types::PeerId) -> Option<ObservedRole>;
/// Get candidates with highest reputations for initiating outgoing connections.
fn outgoing_candidates(
&self,
count: usize,
ignored: HashSet<pezsc_network_types::PeerId>,
) -> Vec<pezsc_network_types::PeerId>;
/// Add known peer.
fn add_known_peer(&self, peer_id: pezsc_network_types::PeerId);
}
/// Actual implementation of peer reputations and connection candidates provider.
#[derive(Debug, Clone)]
pub struct PeerStoreHandle {
inner: Arc<Mutex<PeerStoreInner>>,
}
impl PeerStoreProvider for PeerStoreHandle {
fn is_banned(&self, peer_id: &pezsc_network_types::PeerId) -> bool {
self.inner.lock().is_banned(&peer_id.into())
}
fn register_protocol(&self, protocol_handle: Arc<dyn ProtocolHandle>) {
self.inner.lock().register_protocol(protocol_handle);
}
fn report_disconnect(&self, peer_id: pezsc_network_types::PeerId) {
let mut inner = self.inner.lock();
inner.report_disconnect(peer_id.into())
}
fn report_peer(&self, peer_id: pezsc_network_types::PeerId, change: ReputationChange) {
let mut inner = self.inner.lock();
inner.report_peer(peer_id.into(), change)
}
fn set_peer_role(&self, peer_id: &pezsc_network_types::PeerId, role: ObservedRole) {
let mut inner = self.inner.lock();
inner.set_peer_role(&peer_id.into(), role)
}
fn peer_reputation(&self, peer_id: &pezsc_network_types::PeerId) -> i32 {
self.inner.lock().peer_reputation(&peer_id.into())
}
fn peer_role(&self, peer_id: &pezsc_network_types::PeerId) -> Option<ObservedRole> {
self.inner.lock().peer_role(&peer_id.into())
}
fn outgoing_candidates(
&self,
count: usize,
ignored: HashSet<pezsc_network_types::PeerId>,
) -> Vec<pezsc_network_types::PeerId> {
self.inner
.lock()
.outgoing_candidates(count, ignored.iter().map(|peer_id| (*peer_id).into()).collect())
.iter()
.map(|peer_id| peer_id.into())
.collect()
}
fn add_known_peer(&self, peer_id: pezsc_network_types::PeerId) {
self.inner.lock().add_known_peer(peer_id.into());
}
}
#[derive(Debug, Clone, Copy)]
struct PeerInfo {
/// Reputation of the peer.
reputation: i32,
/// Instant when the peer was last updated.
last_updated: Instant,
/// Role of the peer, if known.
role: Option<ObservedRole>,
}
impl Default for PeerInfo {
fn default() -> Self {
Self { reputation: 0, last_updated: Instant::now(), role: None }
}
}
impl PartialEq for PeerInfo {
fn eq(&self, other: &Self) -> bool {
self.reputation == other.reputation
}
}
impl Eq for PeerInfo {}
impl Ord for PeerInfo {
// We define reverse order by reputation values.
fn cmp(&self, other: &Self) -> Ordering {
self.reputation.cmp(&other.reputation).reverse()
}
}
impl PartialOrd for PeerInfo {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PeerInfo {
fn is_banned(&self) -> bool {
self.reputation < BANNED_THRESHOLD
}
fn add_reputation(&mut self, increment: i32) {
self.reputation = self.reputation.saturating_add(increment);
self.bump_last_updated();
}
fn decay_reputation(&mut self, seconds_passed: u64) {
// Note that decaying the reputation value happens "on its own",
// so we don't do `bump_last_updated()`.
for _ in 0..seconds_passed {
let mut diff = self.reputation / INVERSE_DECREMENT;
if diff == 0 && self.reputation < 0 {
diff = -1;
} else if diff == 0 && self.reputation > 0 {
diff = 1;
}
self.reputation = self.reputation.saturating_sub(diff);
if self.reputation == 0 {
break;
}
}
}
fn bump_last_updated(&mut self) {
self.last_updated = Instant::now();
}
}
#[derive(Debug)]
struct PeerStoreInner {
peers: HashMap<PeerId, PeerInfo>,
protocols: Vec<Arc<dyn ProtocolHandle>>,
metrics: Option<PeerStoreMetrics>,
}
impl PeerStoreInner {
fn is_banned(&self, peer_id: &PeerId) -> bool {
self.peers.get(peer_id).map_or(false, |info| info.is_banned())
}
fn register_protocol(&mut self, protocol_handle: Arc<dyn ProtocolHandle>) {
self.protocols.push(protocol_handle);
}
fn report_disconnect(&mut self, peer_id: PeerId) {
let peer_info = self.peers.entry(peer_id).or_default();
peer_info.add_reputation(DISCONNECT_REPUTATION_CHANGE);
log::trace!(
target: LOG_TARGET,
"Peer {} disconnected, reputation: {:+} to {}",
peer_id,
DISCONNECT_REPUTATION_CHANGE,
peer_info.reputation,
);
}
fn report_peer(&mut self, peer_id: PeerId, change: ReputationChange) {
let peer_info = self.peers.entry(peer_id).or_default();
let was_banned = peer_info.is_banned();
peer_info.add_reputation(change.value);
log::trace!(
target: LOG_TARGET,
"Report {}: {:+} to {}. Reason: {}.",
peer_id,
change.value,
peer_info.reputation,
change.reason,
);
if !peer_info.is_banned() {
if was_banned {
log::info!(
target: LOG_TARGET,
"Peer {} is now unbanned: {:+} to {}. Reason: {}.",
peer_id,
change.value,
peer_info.reputation,
change.reason,
);
}
return;
}
// Peer is currently banned, disconnect it from all protocols.
self.protocols.iter().for_each(|handle| handle.disconnect_peer(peer_id.into()));
// The peer is banned for the first time.
if !was_banned {
log::warn!(
target: LOG_TARGET,
"Report {}: {:+} to {}. Reason: {}. Banned, disconnecting.",
peer_id,
change.value,
peer_info.reputation,
change.reason,
);
return;
}
// The peer was already banned and it got another negative report.
// This may happen during a batch report.
if change.value < 0 {
log::debug!(
target: LOG_TARGET,
"Report {}: {:+} to {}. Reason: {}. Misbehaved during the ban threshold.",
peer_id,
change.value,
peer_info.reputation,
change.reason,
);
}
}
fn set_peer_role(&mut self, peer_id: &PeerId, role: ObservedRole) {
log::trace!(target: LOG_TARGET, "Set {peer_id} role to {role:?}");
match self.peers.entry(*peer_id) {
Entry::Occupied(mut entry) => {
entry.get_mut().role = Some(role);
},
Entry::Vacant(entry) => {
entry.insert(PeerInfo { role: Some(role), ..Default::default() });
},
}
}
fn peer_reputation(&self, peer_id: &PeerId) -> i32 {
self.peers.get(peer_id).map_or(0, |info| info.reputation)
}
fn peer_role(&self, peer_id: &PeerId) -> Option<ObservedRole> {
self.peers.get(peer_id).map_or(None, |info| info.role)
}
fn outgoing_candidates(&self, count: usize, ignored: HashSet<PeerId>) -> Vec<PeerId> {
let mut candidates = self
.peers
.iter()
.filter_map(|(peer_id, info)| {
(!info.is_banned() && !ignored.contains(peer_id)).then_some((*peer_id, *info))
})
.collect::<Vec<_>>();
let count = std::cmp::min(count, candidates.len());
candidates.partial_sort(count, |(_, info1), (_, info2)| info1.cmp(info2));
candidates.iter().take(count).map(|(peer_id, _)| *peer_id).collect()
// TODO: keep the peers sorted (in a "bi-multi-map"?) to not repeat sorting every time.
}
fn progress_time(&mut self, seconds_passed: u64) {
if seconds_passed == 0 {
return;
}
// Drive reputation values towards 0.
self.peers
.iter_mut()
.for_each(|(_, info)| info.decay_reputation(seconds_passed));
// Retain only entries with non-zero reputation values or not expired ones.
let now = Instant::now();
let mut num_banned_peers: u64 = 0;
self.peers.retain(|_, info| {
if info.is_banned() {
num_banned_peers += 1;
}
info.reputation != 0 || info.last_updated + FORGET_AFTER > now
});
if let Some(metrics) = &self.metrics {
metrics.num_discovered.set(self.peers.len() as u64);
metrics.num_banned_peers.set(num_banned_peers);
}
}
fn add_known_peer(&mut self, peer_id: PeerId) {
match self.peers.entry(peer_id) {
Entry::Occupied(mut e) => {
trace!(
target: LOG_TARGET,
"Trying to add an already known peer {peer_id}, bumping `last_updated`.",
);
e.get_mut().bump_last_updated();
},
Entry::Vacant(e) => {
trace!(target: LOG_TARGET, "Adding a new known peer {peer_id}.");
e.insert(PeerInfo::default());
},
}
}
}
/// Worker part of [`PeerStoreHandle`]
#[derive(Debug)]
pub struct PeerStore {
inner: Arc<Mutex<PeerStoreInner>>,
}
impl PeerStore {
/// Create a new peer store from the list of bootnodes.
pub fn new(bootnodes: Vec<PeerId>, metrics_registry: Option<Registry>) -> Self {
let metrics = if let Some(registry) = &metrics_registry {
PeerStoreMetrics::register(registry)
.map_err(|err| {
log::error!(target: LOG_TARGET, "Failed to register peer set metrics: {}", err);
err
})
.ok()
} else {
None
};
PeerStore {
inner: Arc::new(Mutex::new(PeerStoreInner {
peers: bootnodes
.into_iter()
.map(|peer_id| (peer_id, PeerInfo::default()))
.collect(),
protocols: Vec::new(),
metrics,
})),
}
}
/// Get `PeerStoreHandle`.
pub fn handle(&self) -> PeerStoreHandle {
PeerStoreHandle { inner: self.inner.clone() }
}
/// Drive the `PeerStore`, decaying reputation values over time and removing expired entries.
pub async fn run(self) {
let started = Instant::now();
let mut latest_time_update = started;
loop {
let now = Instant::now();
// We basically do `(now - self.latest_update).as_secs()`, except that by the way we do
// it we know that we're not going to miss seconds because of rounding to integers.
let seconds_passed = {
let elapsed_latest = latest_time_update - started;
let elapsed_now = now - started;
latest_time_update = now;
elapsed_now.as_secs() - elapsed_latest.as_secs()
};
self.inner.lock().progress_time(seconds_passed);
let _ = Delay::new(Duration::from_secs(1)).await;
}
}
}
#[async_trait::async_trait]
impl PeerStoreT for PeerStore {
fn handle(&self) -> Arc<dyn PeerStoreProvider> {
Arc::new(self.handle())
}
async fn run(self) {
self.run().await;
}
}
#[cfg(test)]
mod tests {
use super::{PeerInfo, PeerStore, PeerStoreProvider};
#[test]
fn decaying_zero_reputation_yields_zero() {
let mut peer_info = PeerInfo::default();
assert_eq!(peer_info.reputation, 0);
peer_info.decay_reputation(1);
assert_eq!(peer_info.reputation, 0);
peer_info.decay_reputation(100_000);
assert_eq!(peer_info.reputation, 0);
}
#[test]
fn decaying_positive_reputation_decreases_it() {
const INITIAL_REPUTATION: i32 = 100;
let mut peer_info = PeerInfo::default();
peer_info.reputation = INITIAL_REPUTATION;
peer_info.decay_reputation(1);
assert!(peer_info.reputation >= 0);
assert!(peer_info.reputation < INITIAL_REPUTATION);
}
#[test]
fn decaying_negative_reputation_increases_it() {
const INITIAL_REPUTATION: i32 = -100;
let mut peer_info = PeerInfo::default();
peer_info.reputation = INITIAL_REPUTATION;
peer_info.decay_reputation(1);
assert!(peer_info.reputation <= 0);
assert!(peer_info.reputation > INITIAL_REPUTATION);
}
#[test]
fn decaying_max_reputation_finally_yields_zero() {
const INITIAL_REPUTATION: i32 = i32::MAX;
const SECONDS: u64 = 3544;
let mut peer_info = PeerInfo::default();
peer_info.reputation = INITIAL_REPUTATION;
peer_info.decay_reputation(SECONDS / 2);
assert!(peer_info.reputation > 0);
peer_info.decay_reputation(SECONDS / 2);
assert_eq!(peer_info.reputation, 0);
}
#[test]
fn decaying_min_reputation_finally_yields_zero() {
const INITIAL_REPUTATION: i32 = i32::MIN;
const SECONDS: u64 = 3544;
let mut peer_info = PeerInfo::default();
peer_info.reputation = INITIAL_REPUTATION;
peer_info.decay_reputation(SECONDS / 2);
assert!(peer_info.reputation < 0);
peer_info.decay_reputation(SECONDS / 2);
assert_eq!(peer_info.reputation, 0);
}
#[test]
fn report_banned_peers() {
let peer_a = pezsc_network_types::PeerId::random();
let peer_b = pezsc_network_types::PeerId::random();
let peer_c = pezsc_network_types::PeerId::random();
let metrics_registry = prometheus_endpoint::Registry::new();
let peerstore = PeerStore::new(
vec![peer_a, peer_b, peer_c].into_iter().map(Into::into).collect(),
Some(metrics_registry),
);
let metrics = peerstore.inner.lock().metrics.as_ref().unwrap().clone();
let handle = peerstore.handle();
// Check initial state. Advance time to propagate peers.
handle.inner.lock().progress_time(1);
assert_eq!(metrics.num_discovered.get(), 3);
assert_eq!(metrics.num_banned_peers.get(), 0);
// Report 2 peers with a negative reputation.
handle.report_peer(
peer_a,
pezsc_network_common::types::ReputationChange { value: i32::MIN, reason: "test".into() },
);
handle.report_peer(
peer_b,
pezsc_network_common::types::ReputationChange { value: i32::MIN, reason: "test".into() },
);
// Advance time to propagate banned peers.
handle.inner.lock().progress_time(1);
assert_eq!(metrics.num_discovered.get(), 3);
assert_eq!(metrics.num_banned_peers.get(), 2);
}
}
+410
View File
@@ -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/>.
use crate::{
config, error,
peer_store::PeerStoreProvider,
protocol_controller::{self, SetId},
service::{metrics::NotificationMetrics, traits::Direction},
types::ProtocolName,
};
use codec::Encode;
use libp2p::{
core::{transport::PortUse, Endpoint},
swarm::{
behaviour::FromSwarm, ConnectionDenied, ConnectionId, NetworkBehaviour, THandler,
THandlerInEvent, THandlerOutEvent, ToSwarm,
},
Multiaddr, PeerId,
};
use log::{debug, warn};
use codec::DecodeAll;
use pezsc_network_common::{role::Roles, types::ReputationChange};
use pezsc_utils::mpsc::TracingUnboundedReceiver;
use pezsp_runtime::traits::Block as BlockT;
use std::{collections::HashSet, iter, sync::Arc, task::Poll};
use notifications::{Notifications, NotificationsOut};
pub(crate) use notifications::ProtocolHandle;
pub use notifications::{notification_service, NotificationsSink, ProtocolHandlePair, Ready};
mod notifications;
pub mod message;
// Log target for this file.
const LOG_TARGET: &str = "sub-libp2p";
/// Identifier of the peerset for the block announces protocol.
const HARDCODED_PEERSETS_SYNC: SetId = SetId::from(0);
// Lock must always be taken in order declared here.
pub struct Protocol<B: BlockT> {
/// Handles opening the unique substream and sending and receiving raw messages.
behaviour: Notifications,
/// List of notifications protocols that have been registered.
notification_protocols: Vec<ProtocolName>,
/// Handle to `PeerStore`.
peer_store_handle: Arc<dyn PeerStoreProvider>,
/// Streams for peers whose handshake couldn't be determined.
bad_handshake_streams: HashSet<PeerId>,
sync_handle: ProtocolHandle,
_marker: std::marker::PhantomData<B>,
}
impl<B: BlockT> Protocol<B> {
/// Create a new instance.
pub(crate) fn new(
roles: Roles,
notification_metrics: NotificationMetrics,
notification_protocols: Vec<config::NonDefaultSetConfig>,
block_announces_protocol: config::NonDefaultSetConfig,
peer_store_handle: Arc<dyn PeerStoreProvider>,
protocol_controller_handles: Vec<protocol_controller::ProtocolHandle>,
from_protocol_controllers: TracingUnboundedReceiver<protocol_controller::Message>,
) -> error::Result<(Self, Vec<ProtocolHandle>)> {
let (behaviour, notification_protocols, handles) = {
let installed_protocols = iter::once(block_announces_protocol.protocol_name().clone())
.chain(notification_protocols.iter().map(|p| p.protocol_name().clone()))
.collect::<Vec<_>>();
// NOTE: Block announcement protocol is still very much hardcoded into
// `Protocol`. This protocol must be the first notification protocol given to
// `Notifications`
let (protocol_configs, mut handles): (Vec<_>, Vec<_>) = iter::once({
let config = notifications::ProtocolConfig {
name: block_announces_protocol.protocol_name().clone(),
fallback_names: block_announces_protocol.fallback_names().cloned().collect(),
handshake: block_announces_protocol.handshake().as_ref().unwrap().to_vec(),
max_notification_size: block_announces_protocol.max_notification_size(),
};
let (handle, command_stream) =
block_announces_protocol.take_protocol_handle().split();
((config, handle.clone(), command_stream), handle)
})
.chain(notification_protocols.into_iter().map(|s| {
let config = notifications::ProtocolConfig {
name: s.protocol_name().clone(),
fallback_names: s.fallback_names().cloned().collect(),
handshake: s.handshake().as_ref().map_or(roles.encode(), |h| (*h).to_vec()),
max_notification_size: s.max_notification_size(),
};
let (handle, command_stream) = s.take_protocol_handle().split();
((config, handle.clone(), command_stream), handle)
}))
.unzip();
handles.iter_mut().for_each(|handle| {
handle.set_metrics(notification_metrics.clone());
});
protocol_configs.iter().enumerate().for_each(|(i, (p, _, _))| {
debug!(target: LOG_TARGET, "Notifications protocol {:?}: {}", SetId::from(i), p.name);
});
(
Notifications::new(
protocol_controller_handles,
from_protocol_controllers,
notification_metrics,
protocol_configs.into_iter(),
),
installed_protocols,
handles,
)
};
let protocol = Self {
behaviour,
sync_handle: handles[0].clone(),
peer_store_handle,
notification_protocols,
bad_handshake_streams: HashSet::new(),
// TODO: remove when `BlockAnnouncesHandshake` is moved away from `Protocol`
_marker: Default::default(),
};
Ok((protocol, handles))
}
pub fn num_sync_peers(&self) -> usize {
self.sync_handle.num_peers()
}
/// Returns the list of all the peers we have an open channel to.
pub fn open_peers(&self) -> impl Iterator<Item = &PeerId> {
self.behaviour.open_peers()
}
/// Disconnects the given peer if we are connected to it.
pub fn disconnect_peer(&mut self, peer_id: &PeerId, protocol_name: ProtocolName) {
if let Some(position) = self.notification_protocols.iter().position(|p| *p == protocol_name)
{
self.behaviour.disconnect_peer(peer_id, SetId::from(position));
} else {
warn!(target: LOG_TARGET, "disconnect_peer() with invalid protocol name")
}
}
/// Check if role is available for `peer_id` by attempt to decode the handshake to roles and if
/// that fails, check if the role has been registered to `PeerStore`.
fn role_available(&self, peer_id: &PeerId, handshake: &Vec<u8>) -> bool {
match Roles::decode_all(&mut &handshake[..]) {
Ok(_) => true,
Err(_) => self.peer_store_handle.peer_role(&((*peer_id).into())).is_some(),
}
}
}
/// Outcome of an incoming custom message.
#[derive(Debug)]
#[must_use]
pub enum CustomMessageOutcome {
/// Notification protocols have been opened with a remote.
NotificationStreamOpened {
remote: PeerId,
// protocol: ProtocolName,
set_id: SetId,
/// Direction of the stream.
direction: Direction,
/// See [`crate::Event::NotificationStreamOpened::negotiated_fallback`].
negotiated_fallback: Option<ProtocolName>,
/// Received handshake.
received_handshake: Vec<u8>,
/// Notification sink.
notifications_sink: NotificationsSink,
},
/// The [`NotificationsSink`] of some notification protocols need an update.
NotificationStreamReplaced {
// Peer ID.
remote: PeerId,
/// Set ID.
set_id: SetId,
/// New notification sink.
notifications_sink: NotificationsSink,
},
/// Notification protocols have been closed with a remote.
NotificationStreamClosed {
// Peer ID.
remote: PeerId,
/// Set ID.
set_id: SetId,
},
/// Messages have been received on one or more notifications protocols.
NotificationsReceived {
// Peer ID.
remote: PeerId,
/// Set ID.
set_id: SetId,
/// Received notification.
notification: Vec<u8>,
},
}
impl<B: BlockT> NetworkBehaviour for Protocol<B> {
type ConnectionHandler = <Notifications as NetworkBehaviour>::ConnectionHandler;
type ToSwarm = CustomMessageOutcome;
fn handle_established_inbound_connection(
&mut self,
connection_id: ConnectionId,
peer: PeerId,
local_addr: &Multiaddr,
remote_addr: &Multiaddr,
) -> Result<THandler<Self>, ConnectionDenied> {
self.behaviour.handle_established_inbound_connection(
connection_id,
peer,
local_addr,
remote_addr,
)
}
fn handle_established_outbound_connection(
&mut self,
connection_id: ConnectionId,
peer: PeerId,
addr: &Multiaddr,
role_override: Endpoint,
port_use: PortUse,
) -> Result<THandler<Self>, ConnectionDenied> {
self.behaviour.handle_established_outbound_connection(
connection_id,
peer,
addr,
role_override,
port_use,
)
}
fn handle_pending_outbound_connection(
&mut self,
_connection_id: ConnectionId,
_maybe_peer: Option<PeerId>,
_addresses: &[Multiaddr],
_effective_role: Endpoint,
) -> Result<Vec<Multiaddr>, ConnectionDenied> {
// Only `Discovery::handle_pending_outbound_connection` must be returning addresses to
// ensure that we don't return unwanted addresses.
Ok(Vec::new())
}
fn on_swarm_event(&mut self, event: FromSwarm) {
self.behaviour.on_swarm_event(event);
}
fn on_connection_handler_event(
&mut self,
peer_id: PeerId,
connection_id: ConnectionId,
event: THandlerOutEvent<Self>,
) {
self.behaviour.on_connection_handler_event(peer_id, connection_id, event);
}
fn poll(
&mut self,
cx: &mut std::task::Context,
) -> Poll<ToSwarm<Self::ToSwarm, THandlerInEvent<Self>>> {
let event = match self.behaviour.poll(cx) {
Poll::Pending => return Poll::Pending,
Poll::Ready(ToSwarm::GenerateEvent(ev)) => ev,
Poll::Ready(event) => {
return Poll::Ready(event.map_out(|_| {
unreachable!("`GenerateEvent` is handled in a branch above; qed")
}));
},
};
let outcome = match event {
NotificationsOut::CustomProtocolOpen {
peer_id,
set_id,
direction,
received_handshake,
notifications_sink,
negotiated_fallback,
..
} =>
if set_id == HARDCODED_PEERSETS_SYNC {
let _ = self.sync_handle.report_substream_opened(
peer_id,
direction,
received_handshake,
negotiated_fallback,
notifications_sink,
);
None
} else {
match self.role_available(&peer_id, &received_handshake) {
true => Some(CustomMessageOutcome::NotificationStreamOpened {
remote: peer_id,
set_id,
direction,
negotiated_fallback,
received_handshake,
notifications_sink,
}),
false => {
self.bad_handshake_streams.insert(peer_id);
None
},
}
},
NotificationsOut::CustomProtocolReplaced { peer_id, notifications_sink, set_id } =>
if set_id == HARDCODED_PEERSETS_SYNC {
let _ = self
.sync_handle
.report_notification_sink_replaced(peer_id, notifications_sink);
None
} else {
(!self.bad_handshake_streams.contains(&peer_id)).then_some(
CustomMessageOutcome::NotificationStreamReplaced {
remote: peer_id,
set_id,
notifications_sink,
},
)
},
NotificationsOut::CustomProtocolClosed { peer_id, set_id } => {
if set_id == HARDCODED_PEERSETS_SYNC {
let _ = self.sync_handle.report_substream_closed(peer_id);
None
} else {
(!self.bad_handshake_streams.remove(&peer_id)).then_some(
CustomMessageOutcome::NotificationStreamClosed { remote: peer_id, set_id },
)
}
},
NotificationsOut::Notification { peer_id, set_id, message } => {
if set_id == HARDCODED_PEERSETS_SYNC {
let _ = self
.sync_handle
.report_notification_received(peer_id, message.freeze().into());
None
} else {
(!self.bad_handshake_streams.contains(&peer_id)).then_some(
CustomMessageOutcome::NotificationsReceived {
remote: peer_id,
set_id,
notification: message.freeze().into(),
},
)
}
},
NotificationsOut::ProtocolMisbehavior { peer_id, set_id } => {
let index: usize = set_id.into();
let protocol_name = self.notification_protocols.get(index);
debug!(
target: LOG_TARGET,
"Received unexpected data on outbound notification stream from peer {:?} on protocol {:?}",
peer_id,
protocol_name
);
self.peer_store_handle.report_peer(
peer_id.into(),
ReputationChange::new_fatal(
"Received unexpected data on outbound notification stream",
),
);
None
},
};
match outcome {
Some(event) => Poll::Ready(ToSwarm::GenerateEvent(event)),
None => {
cx.waker().wake_by_ref();
Poll::Pending
},
}
}
}
@@ -0,0 +1,240 @@
// 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/>.
//! Network packet message types. These get serialized and put into the lower level protocol
//! payload.
use codec::{Decode, Encode};
use pezsc_client_api::StorageProof;
use pezsc_network_common::message::RequestId;
/// Remote call response.
#[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
#[allow(dead_code)]
pub struct RemoteCallResponse {
/// Id of a request this response was made for.
pub id: RequestId,
/// Execution proof.
pub proof: StorageProof,
}
#[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
#[allow(dead_code)]
/// Remote read response.
pub struct RemoteReadResponse {
/// Id of a request this response was made for.
pub id: RequestId,
/// Read proof.
pub proof: StorageProof,
}
/// Generic types.
pub mod generic {
use codec::{Decode, Encode, Input};
use pezsc_client_api::StorageProof;
use pezsc_network_common::{message::RequestId, role::Roles};
use pezsp_runtime::ConsensusEngineId;
/// Consensus is mostly opaque to us
#[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
#[allow(dead_code)]
pub struct ConsensusMessage {
/// Identifies consensus engine.
pub protocol: ConsensusEngineId,
/// Message payload.
pub data: Vec<u8>,
}
/// Status sent on connection.
// TODO https://github.com/pezkuwichain/kurdistan-sdk/issues/24: replace the `Status`
// struct with this one, after waiting a few releases beyond `NetworkSpecialization`'s
// removal (https://github.com/pezkuwichain/kurdistan-sdk/issues/55)
//
// and set MIN_VERSION to 6.
#[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
#[allow(dead_code)]
pub struct CompactStatus<Hash, Number> {
/// Protocol version.
pub version: u32,
/// Minimum supported version.
pub min_supported_version: u32,
/// Supported roles.
pub roles: Roles,
/// Best block number.
pub best_number: Number,
/// Best block hash.
pub best_hash: Hash,
/// Genesis block hash.
pub genesis_hash: Hash,
}
/// Status sent on connection.
#[derive(Debug, PartialEq, Eq, Clone, Encode)]
#[allow(dead_code)]
pub struct Status<Hash, Number> {
/// Protocol version.
pub version: u32,
/// Minimum supported version.
pub min_supported_version: u32,
/// Supported roles.
pub roles: Roles,
/// Best block number.
pub best_number: Number,
/// Best block hash.
pub best_hash: Hash,
/// Genesis block hash.
pub genesis_hash: Hash,
/// DEPRECATED. Chain-specific status.
pub chain_status: Vec<u8>,
}
impl<Hash: Decode, Number: Decode> Decode for Status<Hash, Number> {
fn decode<I: Input>(value: &mut I) -> Result<Self, codec::Error> {
const LAST_CHAIN_STATUS_VERSION: u32 = 5;
let compact = CompactStatus::decode(value)?;
let chain_status = match <Vec<u8>>::decode(value) {
Ok(v) => v,
Err(e) =>
if compact.version <= LAST_CHAIN_STATUS_VERSION {
return Err(e);
} else {
Vec::new()
},
};
let CompactStatus {
version,
min_supported_version,
roles,
best_number,
best_hash,
genesis_hash,
} = compact;
Ok(Self {
version,
min_supported_version,
roles,
best_number,
best_hash,
genesis_hash,
chain_status,
})
}
}
#[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
#[allow(dead_code)]
/// Remote call request.
pub struct RemoteCallRequest<H> {
/// Unique request id.
pub id: RequestId,
/// Block at which to perform call.
pub block: H,
/// Method name.
pub method: String,
/// Call data.
pub data: Vec<u8>,
}
#[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
#[allow(dead_code)]
/// Remote storage read request.
pub struct RemoteReadRequest<H> {
/// Unique request id.
pub id: RequestId,
/// Block at which to perform call.
pub block: H,
/// Storage key.
pub keys: Vec<Vec<u8>>,
}
#[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
#[allow(dead_code)]
/// Remote storage read child request.
pub struct RemoteReadChildRequest<H> {
/// Unique request id.
pub id: RequestId,
/// Block at which to perform call.
pub block: H,
/// Child Storage key.
pub storage_key: Vec<u8>,
/// Storage key.
pub keys: Vec<Vec<u8>>,
}
#[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
#[allow(dead_code)]
/// Remote header request.
pub struct RemoteHeaderRequest<N> {
/// Unique request id.
pub id: RequestId,
/// Block number to request header for.
pub block: N,
}
#[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
#[allow(dead_code)]
/// Remote header response.
pub struct RemoteHeaderResponse<Header> {
/// Id of a request this response was made for.
pub id: RequestId,
/// Header. None if proof generation has failed (e.g. header is unknown).
pub header: Option<Header>,
/// Header proof.
pub proof: StorageProof,
}
#[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
#[allow(dead_code)]
/// Remote changes request.
pub struct RemoteChangesRequest<H> {
/// Unique request id.
pub id: RequestId,
/// Hash of the first block of the range (including first) where changes are requested.
pub first: H,
/// Hash of the last block of the range (including last) where changes are requested.
pub last: H,
/// Hash of the first block for which the requester has the changes trie root. All other
/// affected roots must be proved.
pub min: H,
/// Hash of the last block that we can use when querying changes.
pub max: H,
/// Storage child node key which changes are requested.
pub storage_key: Option<Vec<u8>>,
/// Storage key which changes are requested.
pub key: Vec<u8>,
}
#[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
#[allow(dead_code)]
/// Remote changes response.
pub struct RemoteChangesResponse<N, H> {
/// Id of a request this response was made for.
pub id: RequestId,
/// Proof has been generated using block with this number as a max block. Should be
/// less than or equal to the RemoteChangesRequest::max block number.
pub max: N,
/// Changes proof.
pub proof: Vec<Vec<u8>>,
/// Changes tries roots missing on the requester' node.
pub roots: Vec<(N, H)>,
/// Missing changes tries roots proof.
pub roots_proof: StorageProof,
}
}
@@ -0,0 +1,34 @@
// 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/>.
//! Implementation of libp2p's `NetworkBehaviour` trait that establishes communications and opens
//! notifications substreams.
pub use self::{
behaviour::{Notifications, NotificationsOut, ProtocolConfig},
handler::{NotificationsSink, Ready},
service::{notification_service, ProtocolHandlePair},
};
pub(crate) use self::service::ProtocolHandle;
mod behaviour;
mod handler;
mod service;
mod tests;
mod upgrade;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,55 @@
// 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::{service::metrics::NotificationMetrics, types::ProtocolName};
/// Register opened substream to Prometheus.
pub fn register_substream_opened(metrics: &Option<NotificationMetrics>, protocol: &ProtocolName) {
if let Some(metrics) = metrics {
metrics.register_substream_opened(&protocol);
}
}
/// Register closed substream to Prometheus.
pub fn register_substream_closed(metrics: &Option<NotificationMetrics>, protocol: &ProtocolName) {
if let Some(metrics) = metrics {
metrics.register_substream_closed(&protocol);
}
}
/// Register sent notification to Prometheus.
pub fn register_notification_sent(
metrics: &Option<std::sync::Arc<NotificationMetrics>>,
protocol: &ProtocolName,
size: usize,
) {
if let Some(metrics) = metrics {
metrics.register_notification_sent(protocol, size);
}
}
/// Register received notification to Prometheus.
pub fn register_notification_received(
metrics: &Option<NotificationMetrics>,
protocol: &ProtocolName,
size: usize,
) {
if let Some(metrics) = metrics {
metrics.register_notification_received(protocol, size);
}
}
@@ -0,0 +1,656 @@
// 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/>.
//! Notification service implementation.
use crate::{
error,
protocol::notifications::handler::NotificationsSink,
service::{
metrics::NotificationMetrics,
traits::{
Direction, MessageSink, NotificationEvent, NotificationService, ValidationResult,
},
},
types::ProtocolName,
};
use futures::{
stream::{FuturesUnordered, Stream},
StreamExt,
};
use libp2p::PeerId;
use parking_lot::Mutex;
use tokio::sync::{mpsc, oneshot};
use tokio_stream::wrappers::ReceiverStream;
use pezsc_utils::mpsc::{tracing_unbounded, TracingUnboundedReceiver, TracingUnboundedSender};
use std::{collections::HashMap, fmt::Debug, sync::Arc};
pub(crate) mod metrics;
#[cfg(test)]
mod tests;
/// Logging target for the file.
const LOG_TARGET: &str = "sub-libp2p::notification::service";
/// Default command queue size.
const COMMAND_QUEUE_SIZE: usize = 64;
/// Type representing subscribers of a notification protocol.
type Subscribers = Arc<Mutex<Vec<TracingUnboundedSender<InnerNotificationEvent>>>>;
/// Type representing a distributable message sink.
/// Detached message sink must carry the protocol name for registering metrics.
///
/// See documentation for [`PeerContext`] for more details.
type NotificationSink = Arc<Mutex<(NotificationsSink, ProtocolName)>>;
#[async_trait::async_trait]
impl MessageSink for NotificationSink {
/// Send synchronous `notification` to the peer associated with this [`MessageSink`].
fn send_sync_notification(&self, notification: Vec<u8>) {
let sink = self.lock();
metrics::register_notification_sent(sink.0.metrics(), &sink.1, notification.len());
sink.0.send_sync_notification(notification);
}
/// Send an asynchronous `notification` to the peer associated with this [`MessageSink`],
/// allowing sender to exercise backpressure.
///
/// Returns an error if the peer does not exist.
async fn send_async_notification(&self, notification: Vec<u8>) -> Result<(), error::Error> {
// notification sink must be cloned because the lock cannot be held across `.await`
// this makes the implementation less efficient but not prohibitively so as the same
// method is also used by `NetworkService` when sending notifications.
let notification_len = notification.len();
let sink = self.lock().clone();
let permit = sink
.0
.reserve_notification()
.await
.map_err(|_| error::Error::ConnectionClosed)?;
permit.send(notification).map_err(|_| error::Error::ChannelClosed).inspect(|_| {
metrics::register_notification_sent(sink.0.metrics(), &sink.1, notification_len);
})
}
}
/// Inner notification event to deal with `NotificationsSinks` without exposing that
/// implementation detail to [`NotificationService`] consumers.
#[derive(Debug)]
enum InnerNotificationEvent {
/// Validate inbound substream.
ValidateInboundSubstream {
/// Peer ID.
peer: PeerId,
/// Received handshake.
handshake: Vec<u8>,
/// `oneshot::Sender` for sending validation result back to `Notifications`
result_tx: oneshot::Sender<ValidationResult>,
},
/// Notification substream open to `peer`.
NotificationStreamOpened {
/// Peer ID.
peer: PeerId,
/// Direction of the substream.
direction: Direction,
/// Received handshake.
handshake: Vec<u8>,
/// Negotiated fallback.
negotiated_fallback: Option<ProtocolName>,
/// Notification sink.
sink: NotificationsSink,
},
/// Substream was closed.
NotificationStreamClosed {
/// Peer ID.
peer: PeerId,
},
/// Notification was received from the substream.
NotificationReceived {
/// Peer ID.
peer: PeerId,
/// Received notification.
notification: Vec<u8>,
},
/// Notification sink has been replaced.
NotificationSinkReplaced {
/// Peer ID.
peer: PeerId,
/// Notification sink.
sink: NotificationsSink,
},
}
/// Notification commands.
///
/// Sent by the installed protocols to `Notifications` to open/close/modify substreams.
#[derive(Debug)]
pub enum NotificationCommand {
/// Instruct `Notifications` to open a substream to peer.
#[allow(unused)]
OpenSubstream(PeerId),
/// Instruct `Notifications` to close the substream to peer.
#[allow(unused)]
CloseSubstream(PeerId),
/// Set handshake for the notifications protocol.
SetHandshake(Vec<u8>),
}
/// Context assigned to each peer.
///
/// Contains `NotificationsSink` used by [`NotificationService`] to send notifications
/// and an additional, distributable `NotificationsSink` which the protocol may acquire
/// if it wishes to send notifications through `NotificationsSink` directly.
///
/// The distributable `NotificationsSink` is wrapped in an `Arc<Mutex<>>` to allow
/// `NotificationsService` to swap the underlying sink in case it's replaced.
#[derive(Debug, Clone)]
struct PeerContext {
/// Sink for sending notifications.
sink: NotificationsSink,
/// Distributable notification sink.
shared_sink: NotificationSink,
}
/// Handle that is passed on to the notifications protocol.
#[derive(Debug)]
pub struct NotificationHandle {
/// Protocol name.
protocol: ProtocolName,
/// TX channel for sending commands to `Notifications`.
tx: mpsc::Sender<NotificationCommand>,
/// RX channel for receiving events from `Notifications`.
rx: TracingUnboundedReceiver<InnerNotificationEvent>,
/// All subscribers of `NotificationEvent`s.
subscribers: Subscribers,
/// Connected peers.
peers: HashMap<PeerId, PeerContext>,
}
impl NotificationHandle {
/// Create new [`NotificationHandle`].
fn new(
protocol: ProtocolName,
tx: mpsc::Sender<NotificationCommand>,
rx: TracingUnboundedReceiver<InnerNotificationEvent>,
subscribers: Arc<Mutex<Vec<TracingUnboundedSender<InnerNotificationEvent>>>>,
) -> Self {
Self { protocol, tx, rx, subscribers, peers: HashMap::new() }
}
}
#[async_trait::async_trait]
impl NotificationService for NotificationHandle {
/// Instruct `Notifications` to open a new substream for `peer`.
async fn open_substream(&mut self, _peer: pezsc_network_types::PeerId) -> Result<(), ()> {
todo!("support for opening substreams not implemented yet");
}
/// Instruct `Notifications` to close substream for `peer`.
async fn close_substream(&mut self, _peer: pezsc_network_types::PeerId) -> Result<(), ()> {
todo!("support for closing substreams not implemented yet, call `NetworkService::disconnect_peer()` instead");
}
/// Send synchronous `notification` to `peer`.
fn send_sync_notification(&mut self, peer: &pezsc_network_types::PeerId, notification: Vec<u8>) {
if let Some(info) = self.peers.get(&((*peer).into())) {
metrics::register_notification_sent(
info.sink.metrics(),
&self.protocol,
notification.len(),
);
let _ = info.sink.send_sync_notification(notification);
}
}
/// Send asynchronous `notification` to `peer`, allowing sender to exercise backpressure.
async fn send_async_notification(
&mut self,
peer: &pezsc_network_types::PeerId,
notification: Vec<u8>,
) -> Result<(), error::Error> {
let notification_len = notification.len();
let sink = &self
.peers
.get(&peer.into())
.ok_or_else(|| error::Error::PeerDoesntExist((*peer).into()))?
.sink;
sink.reserve_notification()
.await
.map_err(|_| error::Error::ConnectionClosed)?
.send(notification)
.map_err(|_| error::Error::ChannelClosed)
.inspect(|_| {
metrics::register_notification_sent(
sink.metrics(),
&self.protocol,
notification_len,
);
})
}
/// Set handshake for the notification protocol replacing the old handshake.
async fn set_handshake(&mut self, handshake: Vec<u8>) -> Result<(), ()> {
log::trace!(target: LOG_TARGET, "{}: set handshake to {handshake:?}", self.protocol);
self.tx.send(NotificationCommand::SetHandshake(handshake)).await.map_err(|_| ())
}
/// Non-blocking variant of `set_handshake()` that attempts to update the handshake
/// and returns an error if the channel is blocked.
///
/// Technically the function can return an error if the channel to `Notifications` is closed
/// but that doesn't happen under normal operation.
fn try_set_handshake(&mut self, handshake: Vec<u8>) -> Result<(), ()> {
self.tx.try_send(NotificationCommand::SetHandshake(handshake)).map_err(|_| ())
}
/// Get next event from the `Notifications` event stream.
async fn next_event(&mut self) -> Option<NotificationEvent> {
loop {
match self.rx.next().await? {
InnerNotificationEvent::ValidateInboundSubstream { peer, handshake, result_tx } =>
return Some(NotificationEvent::ValidateInboundSubstream {
peer: peer.into(),
handshake,
result_tx,
}),
InnerNotificationEvent::NotificationStreamOpened {
peer,
handshake,
negotiated_fallback,
direction,
sink,
} => {
self.peers.insert(
peer,
PeerContext {
sink: sink.clone(),
shared_sink: Arc::new(Mutex::new((sink, self.protocol.clone()))),
},
);
return Some(NotificationEvent::NotificationStreamOpened {
peer: peer.into(),
handshake,
direction,
negotiated_fallback,
});
},
InnerNotificationEvent::NotificationStreamClosed { peer } => {
self.peers.remove(&peer);
return Some(NotificationEvent::NotificationStreamClosed { peer: peer.into() });
},
InnerNotificationEvent::NotificationReceived { peer, notification } =>
return Some(NotificationEvent::NotificationReceived {
peer: peer.into(),
notification,
}),
InnerNotificationEvent::NotificationSinkReplaced { peer, sink } => {
match self.peers.get_mut(&peer) {
None => log::error!(
"{}: notification sink replaced for {peer} but peer does not exist",
self.protocol
),
Some(context) => {
context.sink = sink.clone();
*context.shared_sink.lock() = (sink.clone(), self.protocol.clone());
},
}
},
}
}
}
// Clone [`NotificationService`]
fn clone(&mut self) -> Result<Box<dyn NotificationService>, ()> {
let mut subscribers = self.subscribers.lock();
let (event_tx, event_rx) = tracing_unbounded(self.rx.name(), 100_000);
subscribers.push(event_tx);
Ok(Box::new(NotificationHandle {
protocol: self.protocol.clone(),
tx: self.tx.clone(),
rx: event_rx,
peers: self.peers.clone(),
subscribers: self.subscribers.clone(),
}))
}
/// Get protocol name.
fn protocol(&self) -> &ProtocolName {
&self.protocol
}
/// Get message sink of the peer.
fn message_sink(&self, peer: &pezsc_network_types::PeerId) -> Option<Box<dyn MessageSink>> {
match self.peers.get(&peer.into()) {
Some(context) => Some(Box::new(context.shared_sink.clone())),
None => None,
}
}
}
/// Channel pair which allows `Notifications` to interact with a protocol.
#[derive(Debug)]
pub struct ProtocolHandlePair {
/// Protocol name.
protocol: ProtocolName,
/// Subscribers of the notification protocol events.
subscribers: Subscribers,
// Receiver for notification commands received from the protocol implementation.
rx: mpsc::Receiver<NotificationCommand>,
}
impl ProtocolHandlePair {
/// Create new [`ProtocolHandlePair`].
fn new(
protocol: ProtocolName,
subscribers: Subscribers,
rx: mpsc::Receiver<NotificationCommand>,
) -> Self {
Self { protocol, subscribers, rx }
}
/// Consume `self` and split [`ProtocolHandlePair`] into a handle which allows it to send events
/// to the protocol and a stream of commands received from the protocol.
pub(crate) fn split(
self,
) -> (ProtocolHandle, Box<dyn Stream<Item = NotificationCommand> + Send + Unpin>) {
(
ProtocolHandle::new(self.protocol, self.subscribers),
Box::new(ReceiverStream::new(self.rx)),
)
}
}
/// Handle that is passed on to `Notifications` and allows it to directly communicate
/// with the protocol.
#[derive(Debug, Clone)]
pub(crate) struct ProtocolHandle {
/// Protocol name.
protocol: ProtocolName,
/// Subscribers of the notification protocol.
subscribers: Subscribers,
/// Number of connected peers.
num_peers: usize,
/// Delegate validation to `Peerset`.
delegate_to_peerset: bool,
/// Prometheus metrics.
metrics: Option<NotificationMetrics>,
}
pub(crate) enum ValidationCallResult {
WaitForValidation(oneshot::Receiver<ValidationResult>),
Delegated,
}
impl ProtocolHandle {
/// Create new [`ProtocolHandle`].
fn new(protocol: ProtocolName, subscribers: Subscribers) -> Self {
Self { protocol, subscribers, num_peers: 0usize, metrics: None, delegate_to_peerset: false }
}
/// Set metrics.
pub fn set_metrics(&mut self, metrics: NotificationMetrics) {
self.metrics = Some(metrics);
}
/// Delegate validation to `Peerset`.
///
/// Protocols that do not do any validation themselves and only rely on `Peerset` handling
/// validation can disable protocol-side validation entirely by delegating all validation to
/// `Peerset`.
pub fn delegate_to_peerset(&mut self, delegate: bool) {
self.delegate_to_peerset = delegate;
}
/// Report to the protocol that a substream has been opened and it must be validated by the
/// protocol.
///
/// Return `oneshot::Receiver` which allows `Notifications` to poll for the validation result
/// from protocol.
pub fn report_incoming_substream(
&self,
peer: PeerId,
handshake: Vec<u8>,
) -> Result<ValidationCallResult, ()> {
let subscribers = self.subscribers.lock();
log::trace!(
target: LOG_TARGET,
"{}: report incoming substream for {peer}, handshake {handshake:?}",
self.protocol
);
if self.delegate_to_peerset {
return Ok(ValidationCallResult::Delegated);
}
// if there is only one subscriber, `Notifications` can wait directly on the
// `oneshot::channel()`'s RX half without indirection
if subscribers.len() == 1 {
let (result_tx, rx) = oneshot::channel();
return subscribers[0]
.unbounded_send(InnerNotificationEvent::ValidateInboundSubstream {
peer,
handshake,
result_tx,
})
.map(|_| ValidationCallResult::WaitForValidation(rx))
.map_err(|_| ());
}
// if there are multiple subscribers, create a task which waits for all of the
// validations to finish and returns the combined result to `Notifications`
let mut results: FuturesUnordered<_> = subscribers
.iter()
.filter_map(|subscriber| {
let (result_tx, rx) = oneshot::channel();
subscriber
.unbounded_send(InnerNotificationEvent::ValidateInboundSubstream {
peer,
handshake: handshake.clone(),
result_tx,
})
.is_ok()
.then_some(rx)
})
.collect();
let (tx, rx) = oneshot::channel();
tokio::spawn(async move {
while let Some(event) = results.next().await {
match event {
Err(_) | Ok(ValidationResult::Reject) =>
return tx.send(ValidationResult::Reject),
Ok(ValidationResult::Accept) => {},
}
}
return tx.send(ValidationResult::Accept);
});
Ok(ValidationCallResult::WaitForValidation(rx))
}
/// Report to the protocol that a substream has been opened and that it can now use the handle
/// to send notifications to the remote peer.
pub fn report_substream_opened(
&mut self,
peer: PeerId,
direction: Direction,
handshake: Vec<u8>,
negotiated_fallback: Option<ProtocolName>,
sink: NotificationsSink,
) -> Result<(), ()> {
metrics::register_substream_opened(&self.metrics, &self.protocol);
let mut subscribers = self.subscribers.lock();
log::trace!(target: LOG_TARGET, "{}: substream opened for {peer:?}", self.protocol);
subscribers.retain(|subscriber| {
subscriber
.unbounded_send(InnerNotificationEvent::NotificationStreamOpened {
peer,
direction,
handshake: handshake.clone(),
negotiated_fallback: negotiated_fallback.clone(),
sink: sink.clone(),
})
.is_ok()
});
self.num_peers += 1;
Ok(())
}
/// Substream was closed.
pub fn report_substream_closed(&mut self, peer: PeerId) -> Result<(), ()> {
metrics::register_substream_closed(&self.metrics, &self.protocol);
let mut subscribers = self.subscribers.lock();
log::trace!(target: LOG_TARGET, "{}: substream closed for {peer:?}", self.protocol);
subscribers.retain(|subscriber| {
subscriber
.unbounded_send(InnerNotificationEvent::NotificationStreamClosed { peer })
.is_ok()
});
self.num_peers -= 1;
Ok(())
}
/// Notification was received from the substream.
pub fn report_notification_received(
&mut self,
peer: PeerId,
notification: Vec<u8>,
) -> Result<(), ()> {
metrics::register_notification_received(&self.metrics, &self.protocol, notification.len());
let mut subscribers = self.subscribers.lock();
log::trace!(target: LOG_TARGET, "{}: notification received from {peer:?}", self.protocol);
subscribers.retain(|subscriber| {
subscriber
.unbounded_send(InnerNotificationEvent::NotificationReceived {
peer,
notification: notification.clone(),
})
.is_ok()
});
Ok(())
}
/// Notification sink was replaced.
pub fn report_notification_sink_replaced(
&mut self,
peer: PeerId,
sink: NotificationsSink,
) -> Result<(), ()> {
let mut subscribers = self.subscribers.lock();
log::trace!(
target: LOG_TARGET,
"{}: notification sink replaced for {peer:?}",
self.protocol
);
subscribers.retain(|subscriber| {
subscriber
.unbounded_send(InnerNotificationEvent::NotificationSinkReplaced {
peer,
sink: sink.clone(),
})
.is_ok()
});
Ok(())
}
/// Get the number of connected peers.
pub fn num_peers(&self) -> usize {
self.num_peers
}
}
/// Create new (protocol, notification) handle pair.
///
/// Handle pair allows `Notifications` and the protocol to communicate with each other directly.
pub fn notification_service(
protocol: ProtocolName,
) -> (ProtocolHandlePair, Box<dyn NotificationService>) {
let (cmd_tx, cmd_rx) = mpsc::channel(COMMAND_QUEUE_SIZE);
let (event_tx, event_rx) =
tracing_unbounded(metric_label_for_protocol(&protocol).leak(), 100_000);
let subscribers = Arc::new(Mutex::new(vec![event_tx]));
(
ProtocolHandlePair::new(protocol.clone(), subscribers.clone(), cmd_rx),
Box::new(NotificationHandle::new(protocol.clone(), cmd_tx, event_rx, subscribers)),
)
}
// Decorates the mpsc-notification-to-protocol metric with the name of the protocol,
// to be able to distiguish between different protocols in dashboards.
fn metric_label_for_protocol(protocol: &ProtocolName) -> String {
let protocol_name = protocol.to_string();
let keys = protocol_name.split("/").collect::<Vec<_>>();
keys.iter()
.rev()
.take(2) // Last two tokens give the protocol name and version
.fold("mpsc-notification-to-protocol".into(), |acc, val| format!("{}-{}", acc, val))
}
@@ -0,0 +1,844 @@
// 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 super::*;
use crate::protocol::notifications::handler::{
NotificationsSinkMessage, ASYNC_NOTIFICATIONS_BUFFER_SIZE,
};
use std::future::Future;
#[tokio::test]
async fn validate_and_accept_substream() {
let (proto, mut notif) = notification_service("/proto/1".into());
let (handle, _stream) = proto.split();
let peer_id = PeerId::random();
let ValidationCallResult::WaitForValidation(result_rx) =
handle.report_incoming_substream(peer_id, vec![1, 3, 3, 7]).unwrap()
else {
panic!("peerset not enabled");
};
if let Some(NotificationEvent::ValidateInboundSubstream { peer, handshake, result_tx }) =
notif.next_event().await
{
assert_eq!(peer_id, peer.into());
assert_eq!(handshake, vec![1, 3, 3, 7]);
let _ = result_tx.send(ValidationResult::Accept).unwrap();
} else {
panic!("invalid event received");
}
assert_eq!(result_rx.await.unwrap(), ValidationResult::Accept);
}
#[tokio::test]
async fn substream_opened() {
let (proto, mut notif) = notification_service("/proto/1".into());
let (sink, _, _) = NotificationsSink::new(PeerId::random());
let (mut handle, _stream) = proto.split();
let peer_id = PeerId::random();
handle
.report_substream_opened(peer_id, Direction::Inbound, vec![1, 3, 3, 7], None, sink)
.unwrap();
if let Some(NotificationEvent::NotificationStreamOpened {
peer,
negotiated_fallback,
handshake,
direction,
}) = notif.next_event().await
{
assert_eq!(peer_id, peer.into());
assert_eq!(negotiated_fallback, None);
assert_eq!(handshake, vec![1, 3, 3, 7]);
assert_eq!(direction, Direction::Inbound);
} else {
panic!("invalid event received");
}
}
#[tokio::test]
async fn send_sync_notification() {
let (proto, mut notif) = notification_service("/proto/1".into());
let (sink, _, mut sync_rx) = NotificationsSink::new(PeerId::random());
let (mut handle, _stream) = proto.split();
let peer_id = PeerId::random();
// validate inbound substream
let ValidationCallResult::WaitForValidation(result_rx) =
handle.report_incoming_substream(peer_id, vec![1, 3, 3, 7]).unwrap()
else {
panic!("peerset not enabled");
};
if let Some(NotificationEvent::ValidateInboundSubstream { peer, handshake, result_tx }) =
notif.next_event().await
{
assert_eq!(peer_id, peer.into());
assert_eq!(handshake, vec![1, 3, 3, 7]);
let _ = result_tx.send(ValidationResult::Accept).unwrap();
} else {
panic!("invalid event received");
}
assert_eq!(result_rx.await.unwrap(), ValidationResult::Accept);
// report that a substream has been opened
handle
.report_substream_opened(peer_id, Direction::Inbound, vec![1, 3, 3, 7], None, sink)
.unwrap();
if let Some(NotificationEvent::NotificationStreamOpened {
peer,
negotiated_fallback,
handshake,
direction,
}) = notif.next_event().await
{
assert_eq!(peer_id, peer.into());
assert_eq!(negotiated_fallback, None);
assert_eq!(handshake, vec![1, 3, 3, 7]);
assert_eq!(direction, Direction::Inbound);
} else {
panic!("invalid event received");
}
notif.send_sync_notification(&peer_id.into(), vec![1, 3, 3, 8]);
assert_eq!(
sync_rx.next().await,
Some(NotificationsSinkMessage::Notification { message: vec![1, 3, 3, 8] })
);
}
#[tokio::test]
async fn send_async_notification() {
let (proto, mut notif) = notification_service("/proto/1".into());
let (sink, mut async_rx, _) = NotificationsSink::new(PeerId::random());
let (mut handle, _stream) = proto.split();
let peer_id = PeerId::random();
// validate inbound substream
let ValidationCallResult::WaitForValidation(result_rx) =
handle.report_incoming_substream(peer_id, vec![1, 3, 3, 7]).unwrap()
else {
panic!("peerset not enabled");
};
if let Some(NotificationEvent::ValidateInboundSubstream { peer, handshake, result_tx }) =
notif.next_event().await
{
assert_eq!(peer_id, peer.into());
assert_eq!(handshake, vec![1, 3, 3, 7]);
let _ = result_tx.send(ValidationResult::Accept).unwrap();
} else {
panic!("invalid event received");
}
assert_eq!(result_rx.await.unwrap(), ValidationResult::Accept);
// report that a substream has been opened
handle
.report_substream_opened(peer_id, Direction::Inbound, vec![1, 3, 3, 7], None, sink)
.unwrap();
if let Some(NotificationEvent::NotificationStreamOpened {
peer,
negotiated_fallback,
handshake,
direction,
}) = notif.next_event().await
{
assert_eq!(peer_id, peer.into());
assert_eq!(negotiated_fallback, None);
assert_eq!(handshake, vec![1, 3, 3, 7]);
assert_eq!(direction, Direction::Inbound);
} else {
panic!("invalid event received");
}
notif.send_async_notification(&peer_id.into(), vec![1, 3, 3, 9]).await.unwrap();
assert_eq!(
async_rx.next().await,
Some(NotificationsSinkMessage::Notification { message: vec![1, 3, 3, 9] })
);
}
#[tokio::test]
async fn send_sync_notification_to_non_existent_peer() {
let (proto, mut notif) = notification_service("/proto/1".into());
let (_sink, _, _sync_rx) = NotificationsSink::new(PeerId::random());
let (_handle, _stream) = proto.split();
let peer = PeerId::random();
// as per the original implementation, the call doesn't fail
notif.send_sync_notification(&peer.into(), vec![1, 3, 3, 7])
}
#[tokio::test]
async fn send_async_notification_to_non_existent_peer() {
let (proto, mut notif) = notification_service("/proto/1".into());
let (_sink, _, _sync_rx) = NotificationsSink::new(PeerId::random());
let (_handle, _stream) = proto.split();
let peer = PeerId::random();
if let Err(error::Error::PeerDoesntExist(peer_id)) =
notif.send_async_notification(&peer.into(), vec![1, 3, 3, 7]).await
{
assert_eq!(peer, peer_id.into());
} else {
panic!("invalid error received from `send_async_notification()`");
}
}
#[tokio::test]
async fn receive_notification() {
let (proto, mut notif) = notification_service("/proto/1".into());
let (sink, _, _sync_rx) = NotificationsSink::new(PeerId::random());
let (mut handle, _stream) = proto.split();
let peer_id = PeerId::random();
// validate inbound substream
let ValidationCallResult::WaitForValidation(result_rx) =
handle.report_incoming_substream(peer_id, vec![1, 3, 3, 7]).unwrap()
else {
panic!("peerset not enabled");
};
if let Some(NotificationEvent::ValidateInboundSubstream { peer, handshake, result_tx }) =
notif.next_event().await
{
assert_eq!(peer_id, peer.into());
assert_eq!(handshake, vec![1, 3, 3, 7]);
let _ = result_tx.send(ValidationResult::Accept).unwrap();
} else {
panic!("invalid event received");
}
assert_eq!(result_rx.await.unwrap(), ValidationResult::Accept);
// report that a substream has been opened
handle
.report_substream_opened(peer_id, Direction::Inbound, vec![1, 3, 3, 7], None, sink)
.unwrap();
if let Some(NotificationEvent::NotificationStreamOpened {
peer,
negotiated_fallback,
handshake,
direction,
}) = notif.next_event().await
{
assert_eq!(peer_id, peer.into());
assert_eq!(negotiated_fallback, None);
assert_eq!(handshake, vec![1, 3, 3, 7]);
assert_eq!(direction, Direction::Inbound);
} else {
panic!("invalid event received");
}
// notification is received
handle.report_notification_received(peer_id, vec![1, 3, 3, 8]).unwrap();
if let Some(NotificationEvent::NotificationReceived { peer, notification }) =
notif.next_event().await
{
assert_eq!(peer_id, peer.into());
assert_eq!(notification, vec![1, 3, 3, 8]);
} else {
panic!("invalid event received");
}
}
#[tokio::test]
async fn backpressure_works() {
let (proto, mut notif) = notification_service("/proto/1".into());
let (sink, mut async_rx, _) = NotificationsSink::new(PeerId::random());
let (mut handle, _stream) = proto.split();
let peer_id = PeerId::random();
// validate inbound substream
let ValidationCallResult::WaitForValidation(result_rx) =
handle.report_incoming_substream(peer_id, vec![1, 3, 3, 7]).unwrap()
else {
panic!("peerset not enabled");
};
if let Some(NotificationEvent::ValidateInboundSubstream { peer, handshake, result_tx }) =
notif.next_event().await
{
assert_eq!(peer_id, peer.into());
assert_eq!(handshake, vec![1, 3, 3, 7]);
let _ = result_tx.send(ValidationResult::Accept).unwrap();
} else {
panic!("invalid event received");
}
assert_eq!(result_rx.await.unwrap(), ValidationResult::Accept);
// report that a substream has been opened
handle
.report_substream_opened(peer_id, Direction::Inbound, vec![1, 3, 3, 7], None, sink)
.unwrap();
if let Some(NotificationEvent::NotificationStreamOpened {
peer,
negotiated_fallback,
handshake,
direction,
}) = notif.next_event().await
{
assert_eq!(peer_id, peer.into());
assert_eq!(negotiated_fallback, None);
assert_eq!(handshake, vec![1, 3, 3, 7]);
assert_eq!(direction, Direction::Inbound);
} else {
panic!("invalid event received");
}
// fill the message buffer with messages
for i in 0..=ASYNC_NOTIFICATIONS_BUFFER_SIZE {
assert!(futures::poll!(
notif.send_async_notification(&peer_id.into(), vec![1, 3, 3, i as u8])
)
.is_ready());
}
// try to send one more message and verify that the call blocks
assert!(futures::poll!(notif.send_async_notification(&peer_id.into(), vec![1, 3, 3, 9]))
.is_pending());
// release one slot from the buffer for new message
assert_eq!(
async_rx.next().await,
Some(NotificationsSinkMessage::Notification { message: vec![1, 3, 3, 0] })
);
// verify that a message can be sent
assert!(
futures::poll!(notif.send_async_notification(&peer_id.into(), vec![1, 3, 3, 9])).is_ready()
);
}
#[tokio::test]
async fn peer_disconnects_then_sync_notification_is_sent() {
let (proto, mut notif) = notification_service("/proto/1".into());
let (sink, _, sync_rx) = NotificationsSink::new(PeerId::random());
let (mut handle, _stream) = proto.split();
let peer_id = PeerId::random();
// validate inbound substream
let ValidationCallResult::WaitForValidation(result_rx) =
handle.report_incoming_substream(peer_id, vec![1, 3, 3, 7]).unwrap()
else {
panic!("peerset not enabled");
};
if let Some(NotificationEvent::ValidateInboundSubstream { peer, handshake, result_tx }) =
notif.next_event().await
{
assert_eq!(peer_id, peer.into());
assert_eq!(handshake, vec![1, 3, 3, 7]);
let _ = result_tx.send(ValidationResult::Accept).unwrap();
} else {
panic!("invalid event received");
}
assert_eq!(result_rx.await.unwrap(), ValidationResult::Accept);
// report that a substream has been opened
handle
.report_substream_opened(peer_id, Direction::Inbound, vec![1, 3, 3, 7], None, sink)
.unwrap();
if let Some(NotificationEvent::NotificationStreamOpened {
peer,
negotiated_fallback,
handshake,
direction,
}) = notif.next_event().await
{
assert_eq!(peer_id, peer.into());
assert_eq!(negotiated_fallback, None);
assert_eq!(handshake, vec![1, 3, 3, 7]);
assert_eq!(direction, Direction::Inbound);
} else {
panic!("invalid event received");
}
// report that a substream has been closed but don't poll `notif` to receive this
// information
handle.report_substream_closed(peer_id).unwrap();
drop(sync_rx);
// as per documentation, error is not reported but the notification is silently dropped
notif.send_sync_notification(&peer_id.into(), vec![1, 3, 3, 7]);
}
#[tokio::test]
async fn peer_disconnects_then_async_notification_is_sent() {
let (proto, mut notif) = notification_service("/proto/1".into());
let (sink, async_rx, _) = NotificationsSink::new(PeerId::random());
let (mut handle, _stream) = proto.split();
let peer_id = PeerId::random();
// validate inbound substream
let ValidationCallResult::WaitForValidation(result_rx) =
handle.report_incoming_substream(peer_id, vec![1, 3, 3, 7]).unwrap()
else {
panic!("peerset not enabled");
};
if let Some(NotificationEvent::ValidateInboundSubstream { peer, handshake, result_tx }) =
notif.next_event().await
{
assert_eq!(peer_id, peer.into());
assert_eq!(handshake, vec![1, 3, 3, 7]);
let _ = result_tx.send(ValidationResult::Accept).unwrap();
} else {
panic!("invalid event received");
}
assert_eq!(result_rx.await.unwrap(), ValidationResult::Accept);
// report that a substream has been opened
handle
.report_substream_opened(peer_id, Direction::Inbound, vec![1, 3, 3, 7], None, sink)
.unwrap();
if let Some(NotificationEvent::NotificationStreamOpened {
peer,
negotiated_fallback,
handshake,
direction,
}) = notif.next_event().await
{
assert_eq!(peer_id, peer.into());
assert_eq!(negotiated_fallback, None);
assert_eq!(handshake, vec![1, 3, 3, 7]);
assert_eq!(direction, Direction::Inbound);
} else {
panic!("invalid event received");
}
// report that a substream has been closed but don't poll `notif` to receive this
// information
handle.report_substream_closed(peer_id).unwrap();
drop(async_rx);
// as per documentation, error is not reported but the notification is silently dropped
if let Err(error::Error::ConnectionClosed) =
notif.send_async_notification(&peer_id.into(), vec![1, 3, 3, 7]).await
{
} else {
panic!("invalid state after calling `send_async_notification()` on closed connection")
}
}
#[tokio::test]
async fn cloned_service_opening_substream_works() {
let (proto, mut notif1) = notification_service("/proto/1".into());
let (_sink, _async_rx, _) = NotificationsSink::new(PeerId::random());
let (handle, _stream) = proto.split();
let mut notif2 = notif1.clone().unwrap();
let peer_id = PeerId::random();
// validate inbound substream
let ValidationCallResult::WaitForValidation(mut result_rx) =
handle.report_incoming_substream(peer_id, vec![1, 3, 3, 7]).unwrap()
else {
panic!("peerset not enabled");
};
// verify that `notif1` gets the event
if let Some(NotificationEvent::ValidateInboundSubstream { peer, handshake, result_tx }) =
notif1.next_event().await
{
assert_eq!(peer_id, peer.into());
assert_eq!(handshake, vec![1, 3, 3, 7]);
let _ = result_tx.send(ValidationResult::Accept).unwrap();
} else {
panic!("invalid event received");
}
// verify that because only one listener has thus far send their result, the result is
// pending
assert!(result_rx.try_recv().is_err());
// verify that `notif2` also gets the event
if let Some(NotificationEvent::ValidateInboundSubstream { peer, handshake, result_tx }) =
notif2.next_event().await
{
assert_eq!(peer_id, peer.into());
assert_eq!(handshake, vec![1, 3, 3, 7]);
result_tx.send(ValidationResult::Accept).unwrap();
} else {
panic!("invalid event received");
}
assert_eq!(result_rx.await.unwrap(), ValidationResult::Accept);
}
#[tokio::test]
async fn cloned_service_one_service_rejects_substream() {
let (proto, mut notif1) = notification_service("/proto/1".into());
let (_sink, _async_rx, _) = NotificationsSink::new(PeerId::random());
let (handle, _stream) = proto.split();
let mut notif2 = notif1.clone().unwrap();
let mut notif3 = notif2.clone().unwrap();
let peer_id = PeerId::random();
// validate inbound substream
let ValidationCallResult::WaitForValidation(mut result_rx) =
handle.report_incoming_substream(peer_id, vec![1, 3, 3, 7]).unwrap()
else {
panic!("peerset not enabled");
};
for notif in vec![&mut notif1, &mut notif2] {
if let Some(NotificationEvent::ValidateInboundSubstream { peer, handshake, result_tx }) =
notif.next_event().await
{
assert_eq!(peer_id, peer.into());
assert_eq!(handshake, vec![1, 3, 3, 7]);
let _ = result_tx.send(ValidationResult::Accept).unwrap();
} else {
panic!("invalid event received");
}
}
// `notif3` has not yet sent their validation result
assert!(result_rx.try_recv().is_err());
if let Some(NotificationEvent::ValidateInboundSubstream { peer, handshake, result_tx }) =
notif3.next_event().await
{
assert_eq!(peer_id, peer.into());
assert_eq!(handshake, vec![1, 3, 3, 7]);
let _ = result_tx.send(ValidationResult::Reject).unwrap();
} else {
panic!("invalid event received");
}
assert_eq!(result_rx.await.unwrap(), ValidationResult::Reject);
}
#[tokio::test]
async fn cloned_service_opening_substream_sending_and_receiving_notifications_work() {
let (proto, mut notif1) = notification_service("/proto/1".into());
let (sink, _, mut sync_rx) = NotificationsSink::new(PeerId::random());
let (mut handle, _stream) = proto.split();
let mut notif2 = notif1.clone().unwrap();
let mut notif3 = notif1.clone().unwrap();
let peer_id = PeerId::random();
// validate inbound substream
let ValidationCallResult::WaitForValidation(result_rx) =
handle.report_incoming_substream(peer_id, vec![1, 3, 3, 7]).unwrap()
else {
panic!("peerset not enabled");
};
for notif in vec![&mut notif1, &mut notif2, &mut notif3] {
// accept the inbound substream for all services
if let Some(NotificationEvent::ValidateInboundSubstream { peer, handshake, result_tx }) =
notif.next_event().await
{
assert_eq!(peer_id, peer.into());
assert_eq!(handshake, vec![1, 3, 3, 7]);
let _ = result_tx.send(ValidationResult::Accept).unwrap();
} else {
panic!("invalid event received");
}
}
assert_eq!(result_rx.await.unwrap(), ValidationResult::Accept);
// report that then notification stream has been opened
handle
.report_substream_opened(peer_id, Direction::Inbound, vec![1, 3, 3, 7], None, sink)
.unwrap();
for notif in vec![&mut notif1, &mut notif2, &mut notif3] {
if let Some(NotificationEvent::NotificationStreamOpened {
peer,
negotiated_fallback,
handshake,
direction,
}) = notif.next_event().await
{
assert_eq!(peer_id, peer.into());
assert_eq!(negotiated_fallback, None);
assert_eq!(handshake, vec![1, 3, 3, 7]);
assert_eq!(direction, Direction::Inbound);
} else {
panic!("invalid event received");
}
}
// receive a notification from peer and verify all services receive it
handle.report_notification_received(peer_id, vec![1, 3, 3, 8]).unwrap();
for notif in vec![&mut notif1, &mut notif2, &mut notif3] {
if let Some(NotificationEvent::NotificationReceived { peer, notification }) =
notif.next_event().await
{
assert_eq!(peer_id, peer.into());
assert_eq!(notification, vec![1, 3, 3, 8]);
} else {
panic!("invalid event received");
}
}
for (i, notif) in vec![&mut notif1, &mut notif2, &mut notif3].iter_mut().enumerate() {
// send notification from each service and verify peer receives it
notif.send_sync_notification(&peer_id.into(), vec![1, 3, 3, i as u8]);
assert_eq!(
sync_rx.next().await,
Some(NotificationsSinkMessage::Notification { message: vec![1, 3, 3, i as u8] })
);
}
// close the substream for peer and verify all services receive the event
handle.report_substream_closed(peer_id).unwrap();
for notif in vec![&mut notif1, &mut notif2, &mut notif3] {
if let Some(NotificationEvent::NotificationStreamClosed { peer }) = notif.next_event().await
{
assert_eq!(peer_id, peer.into());
} else {
panic!("invalid event received");
}
}
}
#[tokio::test]
async fn sending_notifications_using_notifications_sink_works() {
let (proto, mut notif) = notification_service("/proto/1".into());
let (sink, mut async_rx, mut sync_rx) = NotificationsSink::new(PeerId::random());
let (mut handle, _stream) = proto.split();
let peer_id = PeerId::random();
// validate inbound substream
let ValidationCallResult::WaitForValidation(result_rx) =
handle.report_incoming_substream(peer_id, vec![1, 3, 3, 7]).unwrap()
else {
panic!("peerset not enabled");
};
if let Some(NotificationEvent::ValidateInboundSubstream { peer, handshake, result_tx }) =
notif.next_event().await
{
assert_eq!(peer_id, peer.into());
assert_eq!(handshake, vec![1, 3, 3, 7]);
let _ = result_tx.send(ValidationResult::Accept).unwrap();
} else {
panic!("invalid event received");
}
assert_eq!(result_rx.await.unwrap(), ValidationResult::Accept);
// report that a substream has been opened
handle
.report_substream_opened(peer_id, Direction::Inbound, vec![1, 3, 3, 7], None, sink)
.unwrap();
if let Some(NotificationEvent::NotificationStreamOpened {
peer,
negotiated_fallback,
handshake,
direction,
}) = notif.next_event().await
{
assert_eq!(peer_id, peer.into());
assert_eq!(negotiated_fallback, None);
assert_eq!(handshake, vec![1, 3, 3, 7]);
assert_eq!(direction, Direction::Inbound);
} else {
panic!("invalid event received");
}
// get a copy of the notification sink and send a synchronous notification using.
let sink = notif.message_sink(&peer_id.into()).unwrap();
sink.send_sync_notification(vec![1, 3, 3, 6]);
// send an asynchronous notification using the acquired notifications sink.
let _ = sink.send_async_notification(vec![1, 3, 3, 7]).await.unwrap();
assert_eq!(
sync_rx.next().await,
Some(NotificationsSinkMessage::Notification { message: vec![1, 3, 3, 6] }),
);
assert_eq!(
async_rx.next().await,
Some(NotificationsSinkMessage::Notification { message: vec![1, 3, 3, 7] }),
);
// send notifications using the stored notification sink as well.
notif.send_sync_notification(&peer_id.into(), vec![1, 3, 3, 8]);
notif.send_async_notification(&peer_id.into(), vec![1, 3, 3, 9]).await.unwrap();
assert_eq!(
sync_rx.next().await,
Some(NotificationsSinkMessage::Notification { message: vec![1, 3, 3, 8] }),
);
assert_eq!(
async_rx.next().await,
Some(NotificationsSinkMessage::Notification { message: vec![1, 3, 3, 9] }),
);
}
#[test]
fn try_to_get_notifications_sink_for_non_existent_peer() {
let (_proto, notif) = notification_service("/proto/1".into());
assert!(notif.message_sink(&pezsc_network_types::PeerId::random()).is_none());
}
#[tokio::test]
async fn notification_sink_replaced() {
let (proto, mut notif) = notification_service("/proto/1".into());
let (sink, mut async_rx, mut sync_rx) = NotificationsSink::new(PeerId::random());
let (mut handle, _stream) = proto.split();
let peer_id = PeerId::random();
// validate inbound substream
let ValidationCallResult::WaitForValidation(result_rx) =
handle.report_incoming_substream(peer_id, vec![1, 3, 3, 7]).unwrap()
else {
panic!("peerset not enabled");
};
if let Some(NotificationEvent::ValidateInboundSubstream { peer, handshake, result_tx }) =
notif.next_event().await
{
assert_eq!(peer_id, peer.into());
assert_eq!(handshake, vec![1, 3, 3, 7]);
let _ = result_tx.send(ValidationResult::Accept).unwrap();
} else {
panic!("invalid event received");
}
assert_eq!(result_rx.await.unwrap(), ValidationResult::Accept);
// report that a substream has been opened
handle
.report_substream_opened(peer_id, Direction::Inbound, vec![1, 3, 3, 7], None, sink)
.unwrap();
if let Some(NotificationEvent::NotificationStreamOpened {
peer,
negotiated_fallback,
handshake,
direction,
}) = notif.next_event().await
{
assert_eq!(peer_id, peer.into());
assert_eq!(negotiated_fallback, None);
assert_eq!(handshake, vec![1, 3, 3, 7]);
assert_eq!(direction, Direction::Inbound);
} else {
panic!("invalid event received");
}
// get a copy of the notification sink and send a synchronous notification using.
let sink = notif.message_sink(&peer_id.into()).unwrap();
sink.send_sync_notification(vec![1, 3, 3, 6]);
// send an asynchronous notification using the acquired notifications sink.
let _ = sink.send_async_notification(vec![1, 3, 3, 7]).await.unwrap();
assert_eq!(
sync_rx.next().await,
Some(NotificationsSinkMessage::Notification { message: vec![1, 3, 3, 6] }),
);
assert_eq!(
async_rx.next().await,
Some(NotificationsSinkMessage::Notification { message: vec![1, 3, 3, 7] }),
);
// send notifications using the stored notification sink as well.
notif.send_sync_notification(&peer_id.into(), vec![1, 3, 3, 8]);
notif.send_async_notification(&peer_id.into(), vec![1, 3, 3, 9]).await.unwrap();
assert_eq!(
sync_rx.next().await,
Some(NotificationsSinkMessage::Notification { message: vec![1, 3, 3, 8] }),
);
assert_eq!(
async_rx.next().await,
Some(NotificationsSinkMessage::Notification { message: vec![1, 3, 3, 9] }),
);
// the initial connection was closed and `Notifications` switched to secondary connection
// and emitted `CustomProtocolReplaced` which informs the local `NotificationService` that
// the notification sink was replaced.
let (new_sink, mut new_async_rx, mut new_sync_rx) = NotificationsSink::new(PeerId::random());
handle.report_notification_sink_replaced(peer_id, new_sink).unwrap();
// drop the old sinks and poll `notif` once to register the sink replacement
drop(sync_rx);
drop(async_rx);
futures::future::poll_fn(|cx| {
let _ = std::pin::Pin::new(&mut notif.next_event()).poll(cx);
std::task::Poll::Ready(())
})
.await;
// verify that using the `NotificationService` API automatically results in using the correct
// sink
notif.send_sync_notification(&peer_id.into(), vec![1, 3, 3, 8]);
notif.send_async_notification(&peer_id.into(), vec![1, 3, 3, 9]).await.unwrap();
assert_eq!(
new_sync_rx.next().await,
Some(NotificationsSinkMessage::Notification { message: vec![1, 3, 3, 8] }),
);
assert_eq!(
new_async_rx.next().await,
Some(NotificationsSinkMessage::Notification { message: vec![1, 3, 3, 9] }),
);
// now send two notifications using the acquired message sink and verify that
// it's also updated
sink.send_sync_notification(vec![1, 3, 3, 6]);
// send an asynchronous notification using the acquired notifications sink.
let _ = sink.send_async_notification(vec![1, 3, 3, 7]).await.unwrap();
assert_eq!(
new_sync_rx.next().await,
Some(NotificationsSinkMessage::Notification { message: vec![1, 3, 3, 6] }),
);
assert_eq!(
new_async_rx.next().await,
Some(NotificationsSinkMessage::Notification { message: vec![1, 3, 3, 7] }),
);
}
#[tokio::test]
async fn set_handshake() {
let (proto, mut notif) = notification_service("/proto/1".into());
let (_handle, mut stream) = proto.split();
assert!(notif.try_set_handshake(vec![1, 3, 3, 7]).is_ok());
match stream.next().await {
Some(NotificationCommand::SetHandshake(handshake)) => {
assert_eq!(handshake, vec![1, 3, 3, 7]);
},
_ => panic!("invalid event received"),
}
for _ in 0..COMMAND_QUEUE_SIZE {
assert!(notif.try_set_handshake(vec![1, 3, 3, 7]).is_ok());
}
assert!(notif.try_set_handshake(vec![1, 3, 3, 7]).is_err());
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,392 @@
// 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/>.
#![cfg(test)]
use crate::{
peer_store::PeerStore,
protocol::notifications::{Notifications, NotificationsOut, ProtocolConfig},
protocol_controller::{ProtoSetConfig, ProtocolController, SetId},
service::{
metrics::NotificationMetrics,
traits::{NotificationEvent, ValidationResult},
},
};
use futures::{future::BoxFuture, prelude::*};
use libp2p::{
core::{
transport::{MemoryTransport, PortUse},
upgrade, Endpoint,
},
identity, noise,
swarm::{
behaviour::FromSwarm, ConnectionDenied, ConnectionId, NetworkBehaviour, Swarm, SwarmEvent,
THandler, THandlerInEvent, THandlerOutEvent, ToSwarm,
},
yamux, Multiaddr, PeerId, SwarmBuilder, Transport,
};
use pezsc_utils::mpsc::tracing_unbounded;
use std::{
iter,
sync::Arc,
task::{Context, Poll},
time::Duration,
};
#[cfg(test)]
mod conformance;
/// Builds two nodes that have each other as bootstrap nodes.
/// This is to be used only for testing, and a panic will happen if something goes wrong.
fn build_nodes() -> (Swarm<CustomProtoWithAddr>, Swarm<CustomProtoWithAddr>) {
let mut out = Vec::with_capacity(2);
let keypairs: Vec<_> = (0..2).map(|_| identity::Keypair::generate_ed25519()).collect();
let addrs: Vec<Multiaddr> = (0..2)
.map(|_| format!("/memory/{}", rand::random::<u64>()).parse().unwrap())
.collect();
for index in 0..2 {
let keypair = keypairs[index].clone();
let (protocol_handle_pair, mut notif_service) =
crate::protocol::notifications::service::notification_service("/foo".into());
// The first swarm has the second peer ID present in the peerstore.
let peer_store = PeerStore::new(
if index == 0 {
keypairs.iter().skip(1).map(|keypair| keypair.public().to_peer_id()).collect()
} else {
vec![]
},
None,
);
let (to_notifications, from_controller) =
tracing_unbounded("test_protocol_controller_to_notifications", 10_000);
let (controller_handle, controller) = ProtocolController::new(
SetId::from(0),
ProtoSetConfig {
in_peers: 25,
out_peers: 25,
reserved_nodes: Default::default(),
reserved_only: false,
},
to_notifications,
Arc::new(peer_store.handle()),
);
let (notif_handle, command_stream) = protocol_handle_pair.split();
tokio::spawn(async move {
loop {
if let NotificationEvent::ValidateInboundSubstream { result_tx, .. } =
notif_service.next_event().await.unwrap()
{
result_tx.send(ValidationResult::Accept).unwrap();
}
}
});
let mut swarm = SwarmBuilder::with_existing_identity(keypair)
.with_tokio()
.with_other_transport(|keypair| {
MemoryTransport::new()
.upgrade(upgrade::Version::V1)
.authenticate(noise::Config::new(&keypair).unwrap())
.multiplex(yamux::Config::default())
.timeout(Duration::from_secs(20))
.boxed()
})
.unwrap()
.with_behaviour(|_keypair| CustomProtoWithAddr {
inner: Notifications::new(
vec![controller_handle],
from_controller,
NotificationMetrics::new(None),
iter::once((
ProtocolConfig {
name: "/foo".into(),
fallback_names: Vec::new(),
handshake: Vec::new(),
max_notification_size: 1024 * 1024,
},
notif_handle,
command_stream,
)),
),
peer_store_future: peer_store.run().boxed(),
protocol_controller_future: controller.run().boxed(),
addrs: addrs
.iter()
.enumerate()
.filter_map(|(n, a)| {
if n != index {
Some((keypairs[n].public().to_peer_id(), a.clone()))
} else {
None
}
})
.collect(),
})
.unwrap()
.build();
swarm.listen_on(addrs[index].clone()).unwrap();
out.push(swarm);
}
// Final output
let mut out_iter = out.into_iter();
let first = out_iter.next().unwrap();
let second = out_iter.next().unwrap();
(first, second)
}
/// Wraps around the `CustomBehaviour` network behaviour, and adds hardcoded node addresses to it.
struct CustomProtoWithAddr {
inner: Notifications,
peer_store_future: BoxFuture<'static, ()>,
protocol_controller_future: BoxFuture<'static, ()>,
addrs: Vec<(PeerId, Multiaddr)>,
}
impl std::ops::Deref for CustomProtoWithAddr {
type Target = Notifications;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl std::ops::DerefMut for CustomProtoWithAddr {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
impl NetworkBehaviour for CustomProtoWithAddr {
type ConnectionHandler = <Notifications as NetworkBehaviour>::ConnectionHandler;
type ToSwarm = <Notifications as NetworkBehaviour>::ToSwarm;
fn handle_pending_inbound_connection(
&mut self,
connection_id: ConnectionId,
local_addr: &Multiaddr,
remote_addr: &Multiaddr,
) -> Result<(), ConnectionDenied> {
self.inner
.handle_pending_inbound_connection(connection_id, local_addr, remote_addr)
}
fn handle_pending_outbound_connection(
&mut self,
connection_id: ConnectionId,
maybe_peer: Option<PeerId>,
addresses: &[Multiaddr],
effective_role: Endpoint,
) -> Result<Vec<Multiaddr>, ConnectionDenied> {
let mut list = self.inner.handle_pending_outbound_connection(
connection_id,
maybe_peer,
addresses,
effective_role,
)?;
if let Some(peer_id) = maybe_peer {
for (p, a) in self.addrs.iter() {
if *p == peer_id {
list.push(a.clone());
}
}
}
Ok(list)
}
fn handle_established_inbound_connection(
&mut self,
connection_id: ConnectionId,
peer: PeerId,
local_addr: &Multiaddr,
remote_addr: &Multiaddr,
) -> Result<THandler<Self>, ConnectionDenied> {
self.inner.handle_established_inbound_connection(
connection_id,
peer,
local_addr,
remote_addr,
)
}
fn handle_established_outbound_connection(
&mut self,
connection_id: ConnectionId,
peer: PeerId,
addr: &Multiaddr,
role_override: Endpoint,
port_use: PortUse,
) -> Result<THandler<Self>, ConnectionDenied> {
self.inner.handle_established_outbound_connection(
connection_id,
peer,
addr,
role_override,
port_use,
)
}
fn on_swarm_event(&mut self, event: FromSwarm) {
self.inner.on_swarm_event(event);
}
fn on_connection_handler_event(
&mut self,
peer_id: PeerId,
connection_id: ConnectionId,
event: THandlerOutEvent<Self>,
) {
self.inner.on_connection_handler_event(peer_id, connection_id, event);
}
fn poll(&mut self, cx: &mut Context) -> Poll<ToSwarm<Self::ToSwarm, THandlerInEvent<Self>>> {
let _ = self.peer_store_future.poll_unpin(cx);
let _ = self.protocol_controller_future.poll_unpin(cx);
self.inner.poll(cx)
}
}
#[tokio::test]
async fn reconnect_after_disconnect() {
// We connect two nodes together, then force a disconnect (through the API of the `Service`),
// check that the disconnect worked, and finally check whether they successfully reconnect.
let (mut service1, mut service2) = build_nodes();
// For this test, the services can be in the following states.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum ServiceState {
NotConnected,
FirstConnec,
Disconnected,
ConnectedAgain,
}
let mut service1_state = ServiceState::NotConnected;
let mut service2_state = ServiceState::NotConnected;
loop {
// Grab next event from services.
let event = {
let s1 = service1.select_next_some();
let s2 = service2.select_next_some();
futures::pin_mut!(s1, s2);
match future::select(s1, s2).await {
future::Either::Left((ev, _)) => future::Either::Left(ev),
future::Either::Right((ev, _)) => future::Either::Right(ev),
}
};
match event {
future::Either::Left(SwarmEvent::Behaviour(NotificationsOut::CustomProtocolOpen {
..
})) => match service1_state {
ServiceState::NotConnected => {
service1_state = ServiceState::FirstConnec;
if service2_state == ServiceState::FirstConnec {
service1
.behaviour_mut()
.disconnect_peer(Swarm::local_peer_id(&service2), SetId::from(0));
}
},
ServiceState::Disconnected => service1_state = ServiceState::ConnectedAgain,
ServiceState::FirstConnec | ServiceState::ConnectedAgain => panic!(),
},
future::Either::Left(SwarmEvent::Behaviour(
NotificationsOut::CustomProtocolClosed { .. },
)) => match service1_state {
ServiceState::FirstConnec => service1_state = ServiceState::Disconnected,
ServiceState::ConnectedAgain |
ServiceState::NotConnected |
ServiceState::Disconnected => panic!(),
},
future::Either::Right(SwarmEvent::Behaviour(
NotificationsOut::CustomProtocolOpen { .. },
)) => match service2_state {
ServiceState::NotConnected => {
service2_state = ServiceState::FirstConnec;
if service1_state == ServiceState::FirstConnec {
service1
.behaviour_mut()
.disconnect_peer(Swarm::local_peer_id(&service2), SetId::from(0));
}
},
ServiceState::Disconnected => service2_state = ServiceState::ConnectedAgain,
ServiceState::FirstConnec | ServiceState::ConnectedAgain => panic!(),
},
future::Either::Right(SwarmEvent::Behaviour(
NotificationsOut::CustomProtocolClosed { .. },
)) => match service2_state {
ServiceState::FirstConnec => service2_state = ServiceState::Disconnected,
ServiceState::ConnectedAgain |
ServiceState::NotConnected |
ServiceState::Disconnected => panic!(),
},
_ => {},
}
// Due to the bug in `Notifications`, the disconnected node does not always detect that
// it was disconnected. The closed inbound substream is tolerated by design, and the
// closed outbound substream is not detected until something is sent into it.
// See [PR #13396](https://github.com/pezkuwichain/kurdistan-sdk/issues/45).
// This happens if the disconnecting node reconnects to it fast enough.
// In this case the disconnected node does not transit via `ServiceState::NotConnected`
// and stays in `ServiceState::FirstConnec`.
// TODO: update this once the fix is finally merged.
if service1_state == ServiceState::ConnectedAgain &&
service2_state == ServiceState::ConnectedAgain ||
service1_state == ServiceState::ConnectedAgain &&
service2_state == ServiceState::FirstConnec ||
service1_state == ServiceState::FirstConnec &&
service2_state == ServiceState::ConnectedAgain
{
break;
}
}
// Now that the two services have disconnected and reconnected, wait for 3 seconds and
// check whether they're still connected.
let mut delay = futures_timer::Delay::new(Duration::from_secs(3));
loop {
// Grab next event from services.
let event = {
let s1 = service1.select_next_some();
let s2 = service2.select_next_some();
futures::pin_mut!(s1, s2);
match future::select(future::select(s1, s2), &mut delay).await {
future::Either::Right(_) => break, // success
future::Either::Left((future::Either::Left((ev, _)), _)) => ev,
future::Either::Left((future::Either::Right((ev, _)), _)) => ev,
}
};
match event {
SwarmEvent::Behaviour(NotificationsOut::CustomProtocolOpen { .. }) |
SwarmEvent::Behaviour(NotificationsOut::CustomProtocolClosed { .. }) => panic!(),
_ => {},
}
}
}
@@ -0,0 +1,34 @@
// 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/>.
#[cfg(test)]
pub(crate) use self::notifications::{
NotificationsInOpen, NotificationsInSubstreamHandshake, NotificationsOutOpen,
};
pub(crate) use notifications::NotificationsOutError;
pub use self::{
collec::UpgradeCollec,
notifications::{
NotificationsIn, NotificationsInSubstream, NotificationsOut, NotificationsOutSubstream,
},
};
mod collec;
mod notifications;
@@ -0,0 +1,178 @@
// 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::prelude::*;
use libp2p::core::upgrade::{InboundUpgrade, UpgradeInfo};
use std::{
pin::Pin,
task::{Context, Poll},
vec,
};
// TODO: move this to libp2p => https://github.com/libp2p/rust-libp2p/issues/1445
/// Upgrade that combines multiple upgrades of the same type into one. Supports all the protocols
/// supported by either sub-upgrade.
#[derive(Debug, Clone)]
pub struct UpgradeCollec<T>(pub Vec<T>);
impl<T> From<Vec<T>> for UpgradeCollec<T> {
fn from(list: Vec<T>) -> Self {
Self(list)
}
}
impl<T> FromIterator<T> for UpgradeCollec<T> {
fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
Self(iter.into_iter().collect())
}
}
impl<T: UpgradeInfo> UpgradeInfo for UpgradeCollec<T> {
type Info = ProtoNameWithUsize<T::Info>;
type InfoIter = vec::IntoIter<Self::Info>;
fn protocol_info(&self) -> Self::InfoIter {
self.0
.iter()
.enumerate()
.flat_map(|(n, p)| p.protocol_info().into_iter().map(move |i| ProtoNameWithUsize(i, n)))
.collect::<Vec<_>>()
.into_iter()
}
}
impl<T, C> InboundUpgrade<C> for UpgradeCollec<T>
where
T: InboundUpgrade<C>,
{
type Output = (T::Output, usize);
type Error = (T::Error, usize);
type Future = FutWithUsize<T::Future>;
fn upgrade_inbound(mut self, sock: C, info: Self::Info) -> Self::Future {
let fut = self.0.remove(info.1).upgrade_inbound(sock, info.0);
FutWithUsize(fut, info.1)
}
}
/// Groups a `ProtocolName` with a `usize`.
#[derive(Debug, Clone, PartialEq)]
pub struct ProtoNameWithUsize<T>(T, usize);
impl<T: AsRef<str>> AsRef<str> for ProtoNameWithUsize<T> {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
/// Equivalent to `fut.map_ok(|v| (v, num)).map_err(|e| (e, num))`, where `fut` and `num` are
/// the two fields of this struct.
#[pin_project::pin_project]
pub struct FutWithUsize<T>(#[pin] T, usize);
impl<T: Future<Output = Result<O, E>>, O, E> Future for FutWithUsize<T> {
type Output = Result<(O, usize), (E, usize)>;
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
let this = self.project();
match Future::poll(this.0, cx) {
Poll::Ready(Ok(v)) => Poll::Ready(Ok((v, *this.1))),
Poll::Ready(Err(e)) => Poll::Ready(Err((e, *this.1))),
Poll::Pending => Poll::Pending,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::ProtocolName as ProtoName;
use libp2p::core::upgrade::UpgradeInfo;
// TODO: move to mocks
mockall::mock! {
pub ProtocolUpgrade<T> {}
impl<T: Clone + AsRef<str>> UpgradeInfo for ProtocolUpgrade<T> {
type Info = T;
type InfoIter = vec::IntoIter<T>;
fn protocol_info(&self) -> vec::IntoIter<T>;
}
}
#[test]
fn protocol_info() {
let upgrades = (1..=3)
.map(|i| {
let mut upgrade = MockProtocolUpgrade::<ProtoNameWithUsize<ProtoName>>::new();
upgrade.expect_protocol_info().return_once(move || {
vec![ProtoNameWithUsize(ProtoName::from(format!("protocol{i}")), i)].into_iter()
});
upgrade
})
.collect::<Vec<_>>();
let upgrade: UpgradeCollec<_> = upgrades.into_iter().collect::<UpgradeCollec<_>>();
let protos = vec![
ProtoNameWithUsize(ProtoNameWithUsize(ProtoName::from("protocol1".to_string()), 1), 0),
ProtoNameWithUsize(ProtoNameWithUsize(ProtoName::from("protocol2".to_string()), 2), 1),
ProtoNameWithUsize(ProtoNameWithUsize(ProtoName::from("protocol3".to_string()), 3), 2),
];
let upgrades = upgrade.protocol_info().collect::<Vec<_>>();
assert_eq!(upgrades, protos,);
}
#[test]
fn nested_protocol_info() {
let mut upgrades = (1..=2)
.map(|i| {
let mut upgrade = MockProtocolUpgrade::<ProtoNameWithUsize<ProtoName>>::new();
upgrade.expect_protocol_info().return_once(move || {
vec![ProtoNameWithUsize(ProtoName::from(format!("protocol{i}")), i)].into_iter()
});
upgrade
})
.collect::<Vec<_>>();
upgrades.push({
let mut upgrade = MockProtocolUpgrade::<ProtoNameWithUsize<ProtoName>>::new();
upgrade.expect_protocol_info().return_once(move || {
vec![
ProtoNameWithUsize(ProtoName::from("protocol22".to_string()), 1),
ProtoNameWithUsize(ProtoName::from("protocol33".to_string()), 2),
ProtoNameWithUsize(ProtoName::from("protocol44".to_string()), 3),
]
.into_iter()
});
upgrade
});
let upgrade: UpgradeCollec<_> = upgrades.into_iter().collect::<UpgradeCollec<_>>();
let protos = vec![
ProtoNameWithUsize(ProtoNameWithUsize(ProtoName::from("protocol1".to_string()), 1), 0),
ProtoNameWithUsize(ProtoNameWithUsize(ProtoName::from("protocol2".to_string()), 2), 1),
ProtoNameWithUsize(ProtoNameWithUsize(ProtoName::from("protocol22".to_string()), 1), 2),
ProtoNameWithUsize(ProtoNameWithUsize(ProtoName::from("protocol33".to_string()), 2), 2),
ProtoNameWithUsize(ProtoNameWithUsize(ProtoName::from("protocol44".to_string()), 3), 2),
];
let upgrades = upgrade.protocol_info().collect::<Vec<_>>();
assert_eq!(upgrades, protos,);
}
}
@@ -0,0 +1,865 @@
// 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/>.
/// Notifications protocol.
///
/// The Bizinikiwi notifications protocol consists in the following:
///
/// - Node A opens a substream to node B and sends a message which contains some
/// protocol-specific higher-level logic. This message is prefixed with a variable-length
/// integer message length. This message can be empty, in which case `0` is sent.
/// - If node B accepts the substream, it sends back a message with the same properties.
/// - If instead B refuses the connection (which typically happens because no empty slot is
/// available), then it immediately closes the substream without sending back anything.
/// - Node A can then send notifications to B, prefixed with a variable-length integer
/// indicating the length of the message.
/// - Either node A or node B can signal that it doesn't want this notifications substream
/// anymore by closing its writing side. The other party should respond by also closing their
/// own writing side soon after.
///
/// Notification substreams are unidirectional. If A opens a substream with B, then B is
/// encouraged but not required to open a substream to A as well.
use crate::types::ProtocolName;
use asynchronous_codec::Framed;
use bytes::BytesMut;
use futures::prelude::*;
use libp2p::{
core::{InboundUpgrade, OutboundUpgrade, UpgradeInfo},
PeerId,
};
use log::{debug, error, warn};
use unsigned_varint::codec::UviBytes;
use std::{
fmt, io, mem,
pin::Pin,
task::{Context, Poll},
vec,
};
/// Logging target for the file.
const LOG_TARGET: &str = "sub-libp2p::notification::upgrade";
/// Maximum allowed size of the two handshake messages, in bytes.
const MAX_HANDSHAKE_SIZE: usize = 1024;
/// Upgrade that accepts a substream, sends back a status message, then becomes a unidirectional
/// stream of messages.
#[derive(Debug, Clone)]
pub struct NotificationsIn {
/// Protocol name to use when negotiating the substream.
/// The first one is the main name, while the other ones are fall backs.
protocol_names: Vec<ProtocolName>,
/// Maximum allowed size for a single notification.
max_notification_size: u64,
}
/// Upgrade that opens a substream, waits for the remote to accept by sending back a status
/// message, then becomes a unidirectional sink of data.
#[derive(Debug, Clone)]
pub struct NotificationsOut {
/// Protocol name to use when negotiating the substream.
/// The first one is the main name, while the other ones are fall backs.
protocol_names: Vec<ProtocolName>,
/// Message to send when we start the handshake.
initial_message: Vec<u8>,
/// Maximum allowed size for a single notification.
max_notification_size: u64,
/// The peerID of the remote.
peer_id: PeerId,
}
/// A substream for incoming notification messages.
///
/// When creating, this struct starts in a state in which we must first send back a handshake
/// message to the remote. No message will come before this has been done.
#[pin_project::pin_project]
pub struct NotificationsInSubstream<TSubstream> {
#[pin]
socket: Framed<TSubstream, UviBytes<io::Cursor<Vec<u8>>>>,
handshake: NotificationsInSubstreamHandshake,
}
/// State of the handshake sending back process.
#[derive(Debug)]
pub enum NotificationsInSubstreamHandshake {
/// Waiting for the user to give us the handshake message.
NotSent,
/// User gave us the handshake message. Trying to push it in the socket.
PendingSend(Vec<u8>),
/// Handshake message was pushed in the socket. Still need to flush.
Flush,
/// Handshake message successfully sent and flushed.
Sent,
/// Remote has closed their writing side. We close our own writing side in return.
ClosingInResponseToRemote,
/// Both our side and the remote have closed their writing side.
BothSidesClosed,
}
/// A substream for outgoing notification messages.
#[pin_project::pin_project]
pub struct NotificationsOutSubstream<TSubstream> {
/// Substream where to send messages.
#[pin]
socket: Framed<TSubstream, UviBytes<io::Cursor<Vec<u8>>>>,
/// The remote peer.
peer_id: PeerId,
}
#[cfg(test)]
impl<TSubstream> NotificationsOutSubstream<TSubstream> {
pub fn new(socket: Framed<TSubstream, UviBytes<io::Cursor<Vec<u8>>>>) -> Self {
Self { socket, peer_id: PeerId::random() }
}
}
impl NotificationsIn {
/// Builds a new potential upgrade.
pub fn new(
main_protocol_name: impl Into<ProtocolName>,
fallback_names: Vec<ProtocolName>,
max_notification_size: u64,
) -> Self {
let mut protocol_names = fallback_names;
protocol_names.insert(0, main_protocol_name.into());
Self { protocol_names, max_notification_size }
}
}
impl UpgradeInfo for NotificationsIn {
type Info = ProtocolName;
type InfoIter = vec::IntoIter<Self::Info>;
fn protocol_info(&self) -> Self::InfoIter {
self.protocol_names.clone().into_iter()
}
}
impl<TSubstream> InboundUpgrade<TSubstream> for NotificationsIn
where
TSubstream: AsyncRead + AsyncWrite + Unpin + Send + 'static,
{
type Output = NotificationsInOpen<TSubstream>;
type Future = Pin<Box<dyn Future<Output = Result<Self::Output, Self::Error>> + Send>>;
type Error = NotificationsHandshakeError;
fn upgrade_inbound(self, mut socket: TSubstream, _negotiated_name: Self::Info) -> Self::Future {
Box::pin(async move {
let handshake_len = unsigned_varint::aio::read_usize(&mut socket).await?;
if handshake_len > MAX_HANDSHAKE_SIZE {
return Err(NotificationsHandshakeError::TooLarge {
requested: handshake_len,
max: MAX_HANDSHAKE_SIZE,
});
}
let mut handshake = vec![0u8; handshake_len];
if !handshake.is_empty() {
socket.read_exact(&mut handshake).await?;
}
let mut codec = UviBytes::default();
codec.set_max_len(usize::try_from(self.max_notification_size).unwrap_or(usize::MAX));
let substream = NotificationsInSubstream {
socket: Framed::new(socket, codec),
handshake: NotificationsInSubstreamHandshake::NotSent,
};
Ok(NotificationsInOpen { handshake, substream })
})
}
}
/// Yielded by the [`NotificationsIn`] after a successfully upgrade.
pub struct NotificationsInOpen<TSubstream> {
/// Handshake sent by the remote.
pub handshake: Vec<u8>,
/// Implementation of `Stream` that allows receives messages from the substream.
pub substream: NotificationsInSubstream<TSubstream>,
}
impl<TSubstream> fmt::Debug for NotificationsInOpen<TSubstream> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("NotificationsInOpen")
.field("handshake", &self.handshake)
.finish_non_exhaustive()
}
}
impl<TSubstream> NotificationsInSubstream<TSubstream>
where
TSubstream: AsyncRead + AsyncWrite + Unpin,
{
#[cfg(test)]
pub fn new(
socket: Framed<TSubstream, UviBytes<io::Cursor<Vec<u8>>>>,
handshake: NotificationsInSubstreamHandshake,
) -> Self {
Self { socket, handshake }
}
/// Sends the handshake in order to inform the remote that we accept the substream.
pub fn send_handshake(&mut self, message: impl Into<Vec<u8>>) {
if !matches!(self.handshake, NotificationsInSubstreamHandshake::NotSent) {
error!(target: LOG_TARGET, "Tried to send handshake twice");
return;
}
self.handshake = NotificationsInSubstreamHandshake::PendingSend(message.into());
}
/// Equivalent to `Stream::poll_next`, except that it only drives the handshake and is
/// guaranteed to not generate any notification.
pub fn poll_process(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), io::Error>> {
let mut this = self.project();
loop {
match mem::replace(this.handshake, NotificationsInSubstreamHandshake::Sent) {
NotificationsInSubstreamHandshake::PendingSend(msg) => {
match Sink::poll_ready(this.socket.as_mut(), cx) {
Poll::Ready(_) => {
*this.handshake = NotificationsInSubstreamHandshake::Flush;
match Sink::start_send(this.socket.as_mut(), io::Cursor::new(msg)) {
Ok(()) => {},
Err(err) => return Poll::Ready(Err(err)),
}
},
Poll::Pending => {
*this.handshake = NotificationsInSubstreamHandshake::PendingSend(msg);
return Poll::Pending;
},
}
},
NotificationsInSubstreamHandshake::Flush => {
match Sink::poll_flush(this.socket.as_mut(), cx)? {
Poll::Ready(()) => {
*this.handshake = NotificationsInSubstreamHandshake::Sent;
return Poll::Ready(Ok(()));
},
Poll::Pending => {
*this.handshake = NotificationsInSubstreamHandshake::Flush;
return Poll::Pending;
},
}
},
st @ NotificationsInSubstreamHandshake::NotSent |
st @ NotificationsInSubstreamHandshake::Sent |
st @ NotificationsInSubstreamHandshake::ClosingInResponseToRemote |
st @ NotificationsInSubstreamHandshake::BothSidesClosed => {
*this.handshake = st;
return Poll::Ready(Ok(()));
},
}
}
}
}
impl<TSubstream> Stream for NotificationsInSubstream<TSubstream>
where
TSubstream: AsyncRead + AsyncWrite + Unpin,
{
type Item = Result<BytesMut, io::Error>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
let mut this = self.project();
// This `Stream` implementation first tries to send back the handshake if necessary.
loop {
match mem::replace(this.handshake, NotificationsInSubstreamHandshake::Sent) {
NotificationsInSubstreamHandshake::NotSent => {
*this.handshake = NotificationsInSubstreamHandshake::NotSent;
return Poll::Pending;
},
NotificationsInSubstreamHandshake::PendingSend(msg) => {
match Sink::poll_ready(this.socket.as_mut(), cx) {
Poll::Ready(_) => {
*this.handshake = NotificationsInSubstreamHandshake::Flush;
match Sink::start_send(this.socket.as_mut(), io::Cursor::new(msg)) {
Ok(()) => {},
Err(err) => return Poll::Ready(Some(Err(err))),
}
},
Poll::Pending => {
*this.handshake = NotificationsInSubstreamHandshake::PendingSend(msg);
return Poll::Pending;
},
}
},
NotificationsInSubstreamHandshake::Flush => {
match Sink::poll_flush(this.socket.as_mut(), cx)? {
Poll::Ready(()) =>
*this.handshake = NotificationsInSubstreamHandshake::Sent,
Poll::Pending => {
*this.handshake = NotificationsInSubstreamHandshake::Flush;
return Poll::Pending;
},
}
},
NotificationsInSubstreamHandshake::Sent => {
match Stream::poll_next(this.socket.as_mut(), cx) {
Poll::Ready(None) =>
*this.handshake =
NotificationsInSubstreamHandshake::ClosingInResponseToRemote,
Poll::Ready(Some(msg)) => {
*this.handshake = NotificationsInSubstreamHandshake::Sent;
return Poll::Ready(Some(msg));
},
Poll::Pending => {
*this.handshake = NotificationsInSubstreamHandshake::Sent;
return Poll::Pending;
},
}
},
NotificationsInSubstreamHandshake::ClosingInResponseToRemote =>
match Sink::poll_close(this.socket.as_mut(), cx)? {
Poll::Ready(()) =>
*this.handshake = NotificationsInSubstreamHandshake::BothSidesClosed,
Poll::Pending => {
*this.handshake =
NotificationsInSubstreamHandshake::ClosingInResponseToRemote;
return Poll::Pending;
},
},
NotificationsInSubstreamHandshake::BothSidesClosed => return Poll::Ready(None),
}
}
}
}
impl NotificationsOut {
/// Builds a new potential upgrade.
pub fn new(
main_protocol_name: impl Into<ProtocolName>,
fallback_names: Vec<ProtocolName>,
initial_message: impl Into<Vec<u8>>,
max_notification_size: u64,
peer_id: PeerId,
) -> Self {
let initial_message = initial_message.into();
if initial_message.len() > MAX_HANDSHAKE_SIZE {
error!(target: LOG_TARGET, "Outbound networking handshake is above allowed protocol limit");
}
let mut protocol_names = fallback_names;
protocol_names.insert(0, main_protocol_name.into());
Self { protocol_names, initial_message, max_notification_size, peer_id }
}
}
impl UpgradeInfo for NotificationsOut {
type Info = ProtocolName;
type InfoIter = vec::IntoIter<Self::Info>;
fn protocol_info(&self) -> Self::InfoIter {
self.protocol_names.clone().into_iter()
}
}
impl<TSubstream> OutboundUpgrade<TSubstream> for NotificationsOut
where
TSubstream: AsyncRead + AsyncWrite + Unpin + Send + 'static,
{
type Output = NotificationsOutOpen<TSubstream>;
type Future = Pin<Box<dyn Future<Output = Result<Self::Output, Self::Error>> + Send>>;
type Error = NotificationsHandshakeError;
fn upgrade_outbound(self, mut socket: TSubstream, negotiated_name: Self::Info) -> Self::Future {
Box::pin(async move {
{
let mut len_data = unsigned_varint::encode::usize_buffer();
let encoded_len =
unsigned_varint::encode::usize(self.initial_message.len(), &mut len_data).len();
socket.write_all(&len_data[..encoded_len]).await?;
}
socket.write_all(&self.initial_message).await?;
socket.flush().await?;
// Reading handshake.
let handshake_len = unsigned_varint::aio::read_usize(&mut socket).await?;
if handshake_len > MAX_HANDSHAKE_SIZE {
return Err(NotificationsHandshakeError::TooLarge {
requested: handshake_len,
max: MAX_HANDSHAKE_SIZE,
});
}
let mut handshake = vec![0u8; handshake_len];
if !handshake.is_empty() {
socket.read_exact(&mut handshake).await?;
}
let mut codec = UviBytes::default();
codec.set_max_len(usize::try_from(self.max_notification_size).unwrap_or(usize::MAX));
Ok(NotificationsOutOpen {
handshake,
negotiated_fallback: if negotiated_name == self.protocol_names[0] {
None
} else {
Some(negotiated_name)
},
substream: NotificationsOutSubstream {
socket: Framed::new(socket, codec),
peer_id: self.peer_id,
},
})
})
}
}
/// Yielded by the [`NotificationsOut`] after a successfully upgrade.
pub struct NotificationsOutOpen<TSubstream> {
/// Handshake returned by the remote.
pub handshake: Vec<u8>,
/// If the negotiated name is not the "main" protocol name but a fallback, contains the
/// name of the negotiated fallback.
pub negotiated_fallback: Option<ProtocolName>,
/// Implementation of `Sink` that allows sending messages on the substream.
pub substream: NotificationsOutSubstream<TSubstream>,
}
impl<TSubstream> fmt::Debug for NotificationsOutOpen<TSubstream> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("NotificationsOutOpen")
.field("handshake", &self.handshake)
.field("negotiated_fallback", &self.negotiated_fallback)
.finish_non_exhaustive()
}
}
impl<TSubstream> Sink<Vec<u8>> for NotificationsOutSubstream<TSubstream>
where
TSubstream: AsyncRead + AsyncWrite + Unpin,
{
type Error = NotificationsOutError;
fn poll_ready(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
let mut this = self.project();
Sink::poll_ready(this.socket.as_mut(), cx).map_err(NotificationsOutError::Io)
}
fn start_send(self: Pin<&mut Self>, item: Vec<u8>) -> Result<(), Self::Error> {
let mut this = self.project();
Sink::start_send(this.socket.as_mut(), io::Cursor::new(item))
.map_err(NotificationsOutError::Io)
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
let mut this = self.project();
// `Sink::poll_flush` does not expose stream closed error until we write something into
// the stream, so the code below makes sure we detect that the substream was closed
// even if we don't write anything into it.
match Stream::poll_next(this.socket.as_mut(), cx) {
Poll::Pending => {},
Poll::Ready(Some(result)) => match result {
Ok(_) => {
debug!(
target: "sub-libp2p",
"Unexpected incoming data in `NotificationsOutSubstream` peer={:?}",
this.peer_id
);
return Poll::Ready(Err(NotificationsOutError::UnexpectedData));
},
Err(error) => {
debug!(
target: "sub-libp2p",
"Error while reading from `NotificationsOutSubstream` peer={:?} error={error:?}",
this.peer_id
);
// The expectation is that the remote has closed the substream.
return Poll::Ready(Err(NotificationsOutError::Closed));
},
},
Poll::Ready(None) => return Poll::Ready(Err(NotificationsOutError::Closed)),
}
Sink::poll_flush(this.socket.as_mut(), cx).map_err(NotificationsOutError::Io)
}
fn poll_close(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
let mut this = self.project();
Sink::poll_close(this.socket.as_mut(), cx).map_err(NotificationsOutError::Io)
}
}
/// Error generated by sending on a notifications out substream.
#[derive(Debug, thiserror::Error)]
pub enum NotificationsHandshakeError {
/// I/O error on the substream.
#[error(transparent)]
Io(#[from] io::Error),
/// Initial message or handshake was too large.
#[error("Initial message or handshake was too large: {requested}")]
TooLarge {
/// Size requested by the remote.
requested: usize,
/// Maximum allowed,
max: usize,
},
/// Error while decoding the variable-length integer.
#[error(transparent)]
VarintDecode(#[from] unsigned_varint::decode::Error),
}
impl From<unsigned_varint::io::ReadError> for NotificationsHandshakeError {
fn from(err: unsigned_varint::io::ReadError) -> Self {
match err {
unsigned_varint::io::ReadError::Io(err) => Self::Io(err),
unsigned_varint::io::ReadError::Decode(err) => Self::VarintDecode(err),
_ => {
warn!("Unrecognized varint decoding error");
Self::Io(From::from(io::ErrorKind::InvalidData))
},
}
}
}
/// Error generated by sending on a notifications out substream.
#[derive(Debug, thiserror::Error)]
pub enum NotificationsOutError {
/// I/O error on the substream.
#[error(transparent)]
Io(#[from] io::Error),
/// The substream was closed.
#[error("substream was closed/reset")]
Closed,
/// The remote peer did not comply with the notification spec.
///
/// This is a terminal error and the peer should be banned immediately.
#[error("unexpected data received from the remote peer")]
UnexpectedData,
}
#[cfg(test)]
mod tests {
use crate::ProtocolName;
use super::{
NotificationsHandshakeError, NotificationsIn, NotificationsInOpen,
NotificationsInSubstream, NotificationsOut, NotificationsOutError, NotificationsOutOpen,
NotificationsOutSubstream,
};
use futures::{channel::oneshot, future, prelude::*, SinkExt, StreamExt};
use libp2p::{
core::{upgrade, InboundUpgrade, OutboundUpgrade, UpgradeInfo},
PeerId,
};
use std::{pin::Pin, task::Poll};
use tokio::net::{TcpListener, TcpStream};
use tokio_util::compat::TokioAsyncReadCompatExt;
/// Opens a substream to the given address, negotiates the protocol, and returns the substream
/// along with the handshake message.
async fn dial(
addr: std::net::SocketAddr,
handshake: impl Into<Vec<u8>>,
) -> Result<
(
Vec<u8>,
NotificationsOutSubstream<
multistream_select::Negotiated<tokio_util::compat::Compat<TcpStream>>,
>,
),
NotificationsHandshakeError,
> {
let socket = TcpStream::connect(addr).await.unwrap();
let notifs_out = NotificationsOut::new(
"/test/proto/1",
Vec::new(),
handshake,
1024 * 1024,
PeerId::random(),
);
let (_, substream) = multistream_select::dialer_select_proto(
socket.compat(),
notifs_out.protocol_info(),
upgrade::Version::V1,
)
.await
.unwrap();
let NotificationsOutOpen { handshake, substream, .. } =
<NotificationsOut as OutboundUpgrade<_>>::upgrade_outbound(
notifs_out,
substream,
"/test/proto/1".into(),
)
.await?;
Ok((handshake, substream))
}
/// Listens on a localhost, negotiates the protocol, and returns the substream along with the
/// handshake message.
///
/// Also sends the listener address through the given channel.
async fn listen_on_localhost(
listener_addr_tx: oneshot::Sender<std::net::SocketAddr>,
) -> Result<
(
Vec<u8>,
NotificationsInSubstream<
multistream_select::Negotiated<tokio_util::compat::Compat<TcpStream>>,
>,
),
NotificationsHandshakeError,
> {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
listener_addr_tx.send(listener.local_addr().unwrap()).unwrap();
let (socket, _) = listener.accept().await.unwrap();
let notifs_in = NotificationsIn::new("/test/proto/1", Vec::new(), 1024 * 1024);
let (_, substream) =
multistream_select::listener_select_proto(socket.compat(), notifs_in.protocol_info())
.await
.unwrap();
let NotificationsInOpen { handshake, substream, .. } =
<NotificationsIn as InboundUpgrade<_>>::upgrade_inbound(
notifs_in,
substream,
"/test/proto/1".into(),
)
.await?;
Ok((handshake, substream))
}
#[tokio::test]
async fn basic_works() {
let (listener_addr_tx, listener_addr_rx) = oneshot::channel();
let client = tokio::spawn(async move {
let (handshake, mut substream) =
dial(listener_addr_rx.await.unwrap(), &b"initial message"[..]).await.unwrap();
assert_eq!(handshake, b"hello world");
substream.send(b"test message".to_vec()).await.unwrap();
});
let (handshake, mut substream) = listen_on_localhost(listener_addr_tx).await.unwrap();
assert_eq!(handshake, b"initial message");
substream.send_handshake(&b"hello world"[..]);
let msg = substream.next().await.unwrap().unwrap();
assert_eq!(msg.as_ref(), b"test message");
client.await.unwrap();
}
#[tokio::test]
async fn empty_handshake() {
// Check that everything still works when the handshake messages are empty.
let (listener_addr_tx, listener_addr_rx) = oneshot::channel();
let client = tokio::spawn(async move {
let (handshake, mut substream) =
dial(listener_addr_rx.await.unwrap(), vec![]).await.unwrap();
assert!(handshake.is_empty());
substream.send(Default::default()).await.unwrap();
});
let (handshake, mut substream) = listen_on_localhost(listener_addr_tx).await.unwrap();
assert!(handshake.is_empty());
substream.send_handshake(vec![]);
let msg = substream.next().await.unwrap().unwrap();
assert!(msg.as_ref().is_empty());
client.await.unwrap();
}
#[tokio::test]
async fn refused() {
let (listener_addr_tx, listener_addr_rx) = oneshot::channel();
let client = tokio::spawn(async move {
let outcome = dial(listener_addr_rx.await.unwrap(), &b"hello"[..]).await;
// Despite the protocol negotiation being successfully conducted on the listener
// side, we have to receive an error here because the listener didn't send the
// handshake.
assert!(outcome.is_err());
});
let (handshake, substream) = listen_on_localhost(listener_addr_tx).await.unwrap();
assert_eq!(handshake, b"hello");
// We successfully upgrade to the protocol, but then close the substream.
drop(substream);
client.await.unwrap();
}
#[tokio::test]
async fn large_initial_message_refused() {
let (listener_addr_tx, listener_addr_rx) = oneshot::channel();
let client = tokio::spawn(async move {
let ret =
dial(listener_addr_rx.await.unwrap(), (0..32768).map(|_| 0).collect::<Vec<_>>())
.await;
assert!(ret.is_err());
});
let _ret = listen_on_localhost(listener_addr_tx).await;
client.await.unwrap();
}
#[tokio::test]
async fn large_handshake_refused() {
let (listener_addr_tx, listener_addr_rx) = oneshot::channel();
let client = tokio::spawn(async move {
let ret = dial(listener_addr_rx.await.unwrap(), &b"initial message"[..]).await;
assert!(ret.is_err());
});
let (handshake, mut substream) = listen_on_localhost(listener_addr_tx).await.unwrap();
assert_eq!(handshake, b"initial message");
// We check that a handshake that is too large gets refused.
substream.send_handshake((0..32768).map(|_| 0).collect::<Vec<_>>());
let _ = substream.next().await;
client.await.unwrap();
}
#[tokio::test]
async fn send_handshake_without_polling_for_incoming_data() {
const PROTO_NAME: &str = "/test/proto/1";
let (listener_addr_tx, listener_addr_rx) = oneshot::channel();
let client = tokio::spawn(async move {
let socket = TcpStream::connect(listener_addr_rx.await.unwrap()).await.unwrap();
let NotificationsOutOpen { handshake, .. } = OutboundUpgrade::upgrade_outbound(
NotificationsOut::new(
PROTO_NAME,
Vec::new(),
&b"initial message"[..],
1024 * 1024,
PeerId::random(),
),
socket.compat(),
ProtocolName::Static(PROTO_NAME),
)
.await
.unwrap();
assert_eq!(handshake, b"hello world");
});
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
listener_addr_tx.send(listener.local_addr().unwrap()).unwrap();
let (socket, _) = listener.accept().await.unwrap();
let NotificationsInOpen { handshake, mut substream, .. } = InboundUpgrade::upgrade_inbound(
NotificationsIn::new(PROTO_NAME, Vec::new(), 1024 * 1024),
socket.compat(),
ProtocolName::Static(PROTO_NAME),
)
.await
.unwrap();
assert_eq!(handshake, b"initial message");
substream.send_handshake(&b"hello world"[..]);
// Actually send the handshake.
future::poll_fn(|cx| Pin::new(&mut substream).poll_process(cx)).await.unwrap();
client.await.unwrap();
}
#[tokio::test]
async fn can_detect_dropped_out_substream_without_writing_data() {
const PROTO_NAME: &str = "/test/proto/1";
let (listener_addr_tx, listener_addr_rx) = oneshot::channel();
let client = tokio::spawn(async move {
let socket = TcpStream::connect(listener_addr_rx.await.unwrap()).await.unwrap();
let NotificationsOutOpen { handshake, mut substream, .. } =
OutboundUpgrade::upgrade_outbound(
NotificationsOut::new(
PROTO_NAME,
Vec::new(),
&b"initial message"[..],
1024 * 1024,
PeerId::random(),
),
socket.compat(),
ProtocolName::Static(PROTO_NAME),
)
.await
.unwrap();
assert_eq!(handshake, b"hello world");
future::poll_fn(|cx| match Pin::new(&mut substream).poll_flush(cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(Ok(())) => {
cx.waker().wake_by_ref();
Poll::Pending
},
Poll::Ready(Err(e)) => {
assert!(matches!(e, NotificationsOutError::Closed));
Poll::Ready(())
},
})
.await;
});
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
listener_addr_tx.send(listener.local_addr().unwrap()).unwrap();
let (socket, _) = listener.accept().await.unwrap();
let NotificationsInOpen { handshake, mut substream, .. } = InboundUpgrade::upgrade_inbound(
NotificationsIn::new(PROTO_NAME, Vec::new(), 1024 * 1024),
socket.compat(),
ProtocolName::Static(PROTO_NAME),
)
.await
.unwrap();
assert_eq!(handshake, b"initial message");
// Send the handhsake.
substream.send_handshake(&b"hello world"[..]);
future::poll_fn(|cx| Pin::new(&mut substream).poll_process(cx)).await.unwrap();
drop(substream);
client.await.unwrap();
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,43 @@
syntax = "proto3";
package bitswap.message;
message Message {
message Wantlist {
enum WantType {
Block = 0;
Have = 1;
}
message Entry {
bytes block = 1; // the block cid (cidV0 in bitswap 1.0.0, cidV1 in bitswap 1.1.0)
int32 priority = 2; // the priority (normalized). default to 1
bool cancel = 3; // whether this revokes an entry
WantType wantType = 4; // Note: defaults to enum 0, ie Block
bool sendDontHave = 5; // Note: defaults to false
}
repeated Entry entries = 1; // a list of wantlist entries
bool full = 2; // whether this is the full wantlist. default to false
}
message Block {
bytes prefix = 1; // CID prefix (cid version, multicodec and multihash prefix (type + length)
bytes data = 2;
}
enum BlockPresenceType {
Have = 0;
DontHave = 1;
}
message BlockPresence {
bytes cid = 1;
BlockPresenceType type = 2;
}
Wantlist wantlist = 1;
repeated bytes blocks = 2; // used to send Blocks in bitswap 1.0.0
repeated Block payload = 3; // used to send Blocks in bitswap 1.1.0
repeated BlockPresence blockPresences = 4;
int32 pendingBytes = 5;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,414 @@
// 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::{service::traits::BandwidthSink, ProtocolName};
use prometheus_endpoint::{
self as prometheus, Counter, CounterVec, Gauge, GaugeVec, HistogramOpts, MetricSource, Opts,
PrometheusError, Registry, SourcedCounter, SourcedGauge, U64,
};
use std::{
str,
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
};
pub use prometheus_endpoint::{Histogram, HistogramVec};
/// Registers all networking metrics with the given registry.
pub fn register(registry: &Registry, sources: MetricSources) -> Result<Metrics, PrometheusError> {
BandwidthCounters::register(registry, sources.bandwidth)?;
NumConnectedGauge::register(registry, sources.connected_peers)?;
Metrics::register(registry)
}
// Register `sc-network` metrics without bandwidth/connected peer sources.
pub fn register_without_sources(registry: &Registry) -> Result<Metrics, PrometheusError> {
Metrics::register(registry)
}
/// Predefined metric sources that are fed directly into prometheus.
pub struct MetricSources {
pub bandwidth: Arc<dyn BandwidthSink>,
pub connected_peers: Arc<AtomicUsize>,
}
impl MetricSources {
pub fn register(
registry: &Registry,
bandwidth: Arc<dyn BandwidthSink>,
connected_peers: Arc<AtomicUsize>,
) -> Result<(), PrometheusError> {
BandwidthCounters::register(registry, bandwidth)?;
NumConnectedGauge::register(registry, connected_peers)
}
}
/// Dedicated metrics.
#[derive(Clone)]
pub struct Metrics {
// This list is ordered alphabetically
pub connections_closed_total: CounterVec<U64>,
pub connections_opened_total: CounterVec<U64>,
pub distinct_peers_connections_closed_total: Counter<U64>,
pub distinct_peers_connections_opened_total: Counter<U64>,
pub incoming_connections_errors_total: CounterVec<U64>,
pub incoming_connections_total: Counter<U64>,
pub kademlia_query_duration: HistogramVec,
pub kademlia_random_queries_total: Counter<U64>,
pub kademlia_records_count: Gauge<U64>,
pub kademlia_records_sizes_total: Gauge<U64>,
pub kbuckets_num_nodes: GaugeVec<U64>,
pub listeners_local_addresses: Gauge<U64>,
pub listeners_errors_total: Counter<U64>,
pub pending_connections: Gauge<U64>,
pub pending_connections_errors_total: CounterVec<U64>,
pub requests_in_failure_total: CounterVec<U64>,
pub requests_in_success_total: HistogramVec,
pub requests_out_failure_total: CounterVec<U64>,
pub requests_out_success_total: HistogramVec,
}
impl Metrics {
fn register(registry: &Registry) -> Result<Self, PrometheusError> {
Ok(Self {
// This list is ordered alphabetically
connections_closed_total: prometheus::register(CounterVec::new(
Opts::new(
"bizinikiwi_sub_libp2p_connections_closed_total",
"Total number of connections closed, by direction and reason"
),
&["direction", "reason"]
)?, registry)?,
connections_opened_total: prometheus::register(CounterVec::new(
Opts::new(
"bizinikiwi_sub_libp2p_connections_opened_total",
"Total number of connections opened by direction"
),
&["direction"]
)?, registry)?,
distinct_peers_connections_closed_total: prometheus::register(Counter::new(
"bizinikiwi_sub_libp2p_distinct_peers_connections_closed_total",
"Total number of connections closed with distinct peers"
)?, registry)?,
distinct_peers_connections_opened_total: prometheus::register(Counter::new(
"bizinikiwi_sub_libp2p_distinct_peers_connections_opened_total",
"Total number of connections opened with distinct peers"
)?, registry)?,
incoming_connections_errors_total: prometheus::register(CounterVec::new(
Opts::new(
"bizinikiwi_sub_libp2p_incoming_connections_handshake_errors_total",
"Total number of incoming connections that have failed during the \
initial handshake"
),
&["reason"]
)?, registry)?,
incoming_connections_total: prometheus::register(Counter::new(
"bizinikiwi_sub_libp2p_incoming_connections_total",
"Total number of incoming connections on the listening sockets"
)?, registry)?,
kademlia_query_duration: prometheus::register(HistogramVec::new(
HistogramOpts {
common_opts: Opts::new(
"bizinikiwi_sub_libp2p_kademlia_query_duration",
"Duration of Kademlia queries per query type"
),
buckets: prometheus::exponential_buckets(0.5, 2.0, 10)
.expect("parameters are always valid values; qed"),
},
&["type"]
)?, registry)?,
kademlia_random_queries_total: prometheus::register(Counter::new(
"bizinikiwi_sub_libp2p_kademlia_random_queries_total",
"Number of random Kademlia queries started",
)?, registry)?,
kademlia_records_count: prometheus::register(Gauge::new(
"bizinikiwi_sub_libp2p_kademlia_records_count",
"Number of records in the Kademlia records store",
)?, registry)?,
kademlia_records_sizes_total: prometheus::register(Gauge::new(
"bizinikiwi_sub_libp2p_kademlia_records_sizes_total",
"Total size of all the records in the Kademlia records store",
)?, registry)?,
kbuckets_num_nodes: prometheus::register(GaugeVec::new(
Opts::new(
"bizinikiwi_sub_libp2p_kbuckets_num_nodes",
"Number of nodes per kbucket per Kademlia instance"
),
&["lower_ilog2_bucket_bound"]
)?, registry)?,
listeners_local_addresses: prometheus::register(Gauge::new(
"bizinikiwi_sub_libp2p_listeners_local_addresses",
"Number of local addresses we're listening on"
)?, registry)?,
listeners_errors_total: prometheus::register(Counter::new(
"bizinikiwi_sub_libp2p_listeners_errors_total",
"Total number of non-fatal errors reported by a listener"
)?, registry)?,
pending_connections: prometheus::register(Gauge::new(
"bizinikiwi_sub_libp2p_pending_connections",
"Number of connections in the process of being established",
)?, registry)?,
pending_connections_errors_total: prometheus::register(CounterVec::new(
Opts::new(
"bizinikiwi_sub_libp2p_pending_connections_errors_total",
"Total number of pending connection errors"
),
&["reason"]
)?, registry)?,
requests_in_failure_total: prometheus::register(CounterVec::new(
Opts::new(
"bizinikiwi_sub_libp2p_requests_in_failure_total",
"Total number of incoming requests that the node has failed to answer"
),
&["protocol", "reason"]
)?, registry)?,
requests_in_success_total: prometheus::register(HistogramVec::new(
HistogramOpts {
common_opts: Opts::new(
"bizinikiwi_sub_libp2p_requests_in_success_total",
"For successful incoming requests, time between receiving the request and \
starting to send the response"
),
buckets: prometheus::exponential_buckets(0.001, 2.0, 16)
.expect("parameters are always valid values; qed"),
},
&["protocol"]
)?, registry)?,
requests_out_failure_total: prometheus::register(CounterVec::new(
Opts::new(
"bizinikiwi_sub_libp2p_requests_out_failure_total",
"Total number of requests that have failed"
),
&["protocol", "reason"]
)?, registry)?,
requests_out_success_total: prometheus::register(HistogramVec::new(
HistogramOpts {
common_opts: Opts::new(
"bizinikiwi_sub_libp2p_requests_out_success_total",
"For successful outgoing requests, time between a request's start and finish"
),
buckets: prometheus::exponential_buckets(0.001, 2.0, 16)
.expect("parameters are always valid values; qed"),
},
&["protocol"]
)?, registry)?,
})
}
}
/// Peer store metrics.
#[derive(Clone, Debug)]
pub struct PeerStoreMetrics {
pub num_banned_peers: Gauge<U64>,
pub num_discovered: Gauge<U64>,
}
impl PeerStoreMetrics {
pub fn register(registry: &Registry) -> Result<Self, PrometheusError> {
Ok(Self {
num_banned_peers: prometheus::register(
Gauge::new(
"bizinikiwi_sub_libp2p_peerset_num_banned_peers",
"Number of banned peers stored in the peerset manager",
)?,
registry,
)?,
num_discovered: prometheus::register(
Gauge::new(
"bizinikiwi_sub_libp2p_peerset_num_discovered",
"Number of nodes stored in the peerset manager",
)?,
registry,
)?,
})
}
}
/// The bandwidth counter metric.
#[derive(Clone)]
pub struct BandwidthCounters(Arc<dyn BandwidthSink>);
impl BandwidthCounters {
/// Registers the `BandwidthCounters` metric whose values are
/// obtained from the given sinks.
fn register(registry: &Registry, sinks: Arc<dyn BandwidthSink>) -> Result<(), PrometheusError> {
prometheus::register(
SourcedCounter::new(
&Opts::new("bizinikiwi_sub_libp2p_network_bytes_total", "Total bandwidth usage")
.variable_label("direction"),
BandwidthCounters(sinks),
)?,
registry,
)?;
Ok(())
}
}
impl MetricSource for BandwidthCounters {
type N = u64;
fn collect(&self, mut set: impl FnMut(&[&str], Self::N)) {
set(&["in"], self.0.total_inbound());
set(&["out"], self.0.total_outbound());
}
}
/// The connected peers metric.
#[derive(Clone)]
pub struct NumConnectedGauge(Arc<AtomicUsize>);
impl NumConnectedGauge {
/// Registers the `MajorSyncingGauge` metric whose value is
/// obtained from the given `AtomicUsize`.
fn register(registry: &Registry, value: Arc<AtomicUsize>) -> Result<(), PrometheusError> {
prometheus::register(
SourcedGauge::new(
&Opts::new("bizinikiwi_sub_libp2p_peers_count", "Number of connected peers"),
NumConnectedGauge(value),
)?,
registry,
)?;
Ok(())
}
}
impl MetricSource for NumConnectedGauge {
type N = u64;
fn collect(&self, mut set: impl FnMut(&[&str], Self::N)) {
set(&[], self.0.load(Ordering::Relaxed) as u64);
}
}
/// Notification metrics.
///
/// Wrapper over `Option<InnerNotificationMetrics>` to make metrics reporting code cleaner.
#[derive(Debug, Clone)]
pub struct NotificationMetrics {
/// Metrics, if enabled.
metrics: Option<InnerNotificationMetrics>,
}
impl NotificationMetrics {
/// Create new [`NotificationMetrics`].
pub fn new(registry: Option<&Registry>) -> NotificationMetrics {
let metrics = match registry {
Some(registry) => InnerNotificationMetrics::register(registry).ok(),
None => None,
};
Self { metrics }
}
/// Register opened substream to Prometheus.
pub fn register_substream_opened(&self, protocol: &ProtocolName) {
if let Some(metrics) = &self.metrics {
metrics.notifications_streams_opened_total.with_label_values(&[&protocol]).inc();
}
}
/// Register closed substream to Prometheus.
pub fn register_substream_closed(&self, protocol: &ProtocolName) {
if let Some(metrics) = &self.metrics {
metrics
.notifications_streams_closed_total
.with_label_values(&[&protocol[..]])
.inc();
}
}
/// Register sent notification to Prometheus.
pub fn register_notification_sent(&self, protocol: &ProtocolName, size: usize) {
if let Some(metrics) = &self.metrics {
metrics
.notifications_sizes
.with_label_values(&["out", protocol])
.observe(size as f64);
}
}
/// Register received notification to Prometheus.
pub fn register_notification_received(&self, protocol: &ProtocolName, size: usize) {
if let Some(metrics) = &self.metrics {
metrics
.notifications_sizes
.with_label_values(&["in", protocol])
.observe(size as f64);
}
}
}
/// Notification metrics.
#[derive(Debug, Clone)]
struct InnerNotificationMetrics {
// Total number of opened substreams.
pub notifications_streams_opened_total: CounterVec<U64>,
/// Total number of closed substreams.
pub notifications_streams_closed_total: CounterVec<U64>,
/// In/outbound notification sizes.
pub notifications_sizes: HistogramVec,
}
impl InnerNotificationMetrics {
fn register(registry: &Registry) -> Result<Self, PrometheusError> {
Ok(Self {
notifications_sizes: prometheus::register(
HistogramVec::new(
HistogramOpts {
common_opts: Opts::new(
"bizinikiwi_sub_libp2p_notifications_sizes",
"Sizes of the notifications send to and received from all nodes",
),
buckets: prometheus::exponential_buckets(64.0, 4.0, 8)
.expect("parameters are always valid values; qed"),
},
&["direction", "protocol"],
)?,
registry,
)?,
notifications_streams_closed_total: prometheus::register(
CounterVec::new(
Opts::new(
"bizinikiwi_sub_libp2p_notifications_streams_closed_total",
"Total number of notification substreams that have been closed",
),
&["protocol"],
)?,
registry,
)?,
notifications_streams_opened_total: prometheus::register(
CounterVec::new(
Opts::new(
"bizinikiwi_sub_libp2p_notifications_streams_opened_total",
"Total number of notification substreams that have been opened",
),
&["protocol"],
)?,
registry,
)?,
})
}
}
@@ -0,0 +1,347 @@
// 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/>.
//! Registering events streams.
//!
//! This code holds the logic that is used for the network service to inform other parts of
//! Bizinikiwi about what is happening.
//!
//! # Usage
//!
//! - Create an instance of [`OutChannels`].
//! - Create channels using the [`channel`] function. The receiving side implements the `Stream`
//! trait.
//! - You cannot directly send an event on a sender. Instead, you have to call
//! [`OutChannels::push`] to put the sender within a [`OutChannels`].
//! - Send events by calling [`OutChannels::send`]. Events are cloned for each sender in the
//! collection.
use crate::event::Event;
use futures::{prelude::*, ready, stream::FusedStream};
use log::{debug, error};
use prometheus_endpoint::{register, CounterVec, GaugeVec, Opts, PrometheusError, Registry, U64};
use std::{
backtrace::Backtrace,
cell::RefCell,
fmt,
pin::Pin,
task::{Context, Poll},
};
/// Log target for this file.
pub const LOG_TARGET: &str = "sub-libp2p::out_events";
/// Creates a new channel that can be associated to a [`OutChannels`].
///
/// The name is used in Prometheus reports, the queue size threshold is used
/// to warn if there are too many unprocessed events in the channel.
pub fn channel(name: &'static str, queue_size_warning: usize) -> (Sender, Receiver) {
let (tx, rx) = async_channel::unbounded();
let tx = Sender {
inner: tx,
name,
queue_size_warning,
warning_fired: SenderWarningState::NotFired,
creation_backtrace: Backtrace::force_capture(),
metrics: None,
};
let rx = Receiver { inner: rx, name, metrics: None };
(tx, rx)
}
/// A state of a sender warning that is used to avoid spamming the logs.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SenderWarningState {
/// The warning has not been fired yet.
NotFired,
/// The warning has been fired, and the channel is full
FiredFull,
/// The warning has been fired and the channel is not full anymore.
FiredFree,
}
/// Sending side of a channel.
///
/// Must be associated with an [`OutChannels`] before anything can be sent on it
///
/// > **Note**: Contrary to regular channels, this `Sender` is purposefully designed to not
/// implement the `Clone` trait e.g. in Order to not complicate the logic keeping the metrics in
/// sync on drop. If someone adds a `#[derive(Clone)]` below, it is **wrong**.
pub struct Sender {
inner: async_channel::Sender<Event>,
/// Name to identify the channel (e.g., in Prometheus and logs).
name: &'static str,
/// Threshold queue size to generate an error message in the logs.
queue_size_warning: usize,
/// We generate the error message only once to not spam the logs after the first error.
/// Subsequently we indicate channel fullness on debug level.
warning_fired: SenderWarningState,
/// Backtrace of a place where the channel was created.
creation_backtrace: Backtrace,
/// Clone of [`Receiver::metrics`]. Will be initialized when [`Sender`] is added to
/// [`OutChannels`] with `OutChannels::push()`.
metrics: Option<Metrics>,
}
impl fmt::Debug for Sender {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_tuple("Sender").finish()
}
}
impl Drop for Sender {
fn drop(&mut self) {
if let Some(metrics) = self.metrics.as_ref() {
metrics.num_channels.with_label_values(&[self.name]).dec();
}
}
}
/// Receiving side of a channel.
pub struct Receiver {
inner: async_channel::Receiver<Event>,
name: &'static str,
/// Initially contains `None`, and will be set to a value once the corresponding [`Sender`]
/// is assigned to an instance of [`OutChannels`].
metrics: Option<Metrics>,
}
impl Stream for Receiver {
type Item = Event;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Event>> {
if let Some(ev) = ready!(Pin::new(&mut self.inner).poll_next(cx)) {
if let Some(metrics) = &self.metrics {
metrics.event_out(&ev, self.name);
}
Poll::Ready(Some(ev))
} else {
Poll::Ready(None)
}
}
}
impl fmt::Debug for Receiver {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_tuple("Receiver").finish()
}
}
impl Drop for Receiver {
fn drop(&mut self) {
if !self.inner.is_terminated() {
// Empty the list to properly decrease the metrics.
while let Some(Some(_)) = self.next().now_or_never() {}
}
}
}
/// Collection of senders.
pub struct OutChannels {
event_streams: Vec<Sender>,
/// The metrics we collect. A clone of this is sent to each [`Receiver`] associated with this
/// object.
metrics: Option<Metrics>,
}
impl OutChannels {
/// Creates a new empty collection of senders.
pub fn new(registry: Option<&Registry>) -> Result<Self, PrometheusError> {
let metrics =
if let Some(registry) = registry { Some(Metrics::register(registry)?) } else { None };
Ok(Self { event_streams: Vec::new(), metrics })
}
/// Adds a new [`Sender`] to the collection.
pub fn push(&mut self, mut sender: Sender) {
debug_assert!(sender.metrics.is_none());
sender.metrics = self.metrics.clone();
if let Some(metrics) = &self.metrics {
metrics.num_channels.with_label_values(&[sender.name]).inc();
}
self.event_streams.push(sender);
}
/// Sends an event.
pub fn send(&mut self, event: Event) {
self.event_streams.retain_mut(|sender| {
let current_pending = sender.inner.len();
if current_pending >= sender.queue_size_warning {
if sender.warning_fired == SenderWarningState::NotFired {
error!(
"The number of unprocessed events in channel `{}` exceeded {}.\n\
The channel was created at:\n{:}\n
The last event was sent from:\n{:}",
sender.name,
sender.queue_size_warning,
sender.creation_backtrace,
Backtrace::force_capture(),
);
} else if sender.warning_fired == SenderWarningState::FiredFree {
// We don't want to spam the logs, so we only log on debug level
debug!(
target: LOG_TARGET,
"Channel `{}` is overflowed again. Number of events: {}",
sender.name, current_pending
);
}
sender.warning_fired = SenderWarningState::FiredFull;
} else if sender.warning_fired == SenderWarningState::FiredFull &&
current_pending < sender.queue_size_warning.wrapping_div(2)
{
sender.warning_fired = SenderWarningState::FiredFree;
debug!(
target: LOG_TARGET,
"Channel `{}` is no longer overflowed. Number of events: {}",
sender.name, current_pending
);
}
sender.inner.try_send(event.clone()).is_ok()
});
if let Some(metrics) = &self.metrics {
for ev in &self.event_streams {
metrics.event_in(&event, ev.name);
}
}
}
}
impl fmt::Debug for OutChannels {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("OutChannels")
.field("num_channels", &self.event_streams.len())
.finish()
}
}
#[derive(Clone)]
struct Metrics {
// This list is ordered alphabetically
events_total: CounterVec<U64>,
notifications_sizes: CounterVec<U64>,
num_channels: GaugeVec<U64>,
}
thread_local! {
static LABEL_BUFFER: RefCell<String> = RefCell::new(String::new());
}
fn format_label(prefix: &str, protocol: &str, callback: impl FnOnce(&str)) {
LABEL_BUFFER.with(|label_buffer| {
let mut label_buffer = label_buffer.borrow_mut();
label_buffer.clear();
label_buffer.reserve(prefix.len() + protocol.len() + 2);
label_buffer.push_str(prefix);
label_buffer.push('"');
label_buffer.push_str(protocol);
label_buffer.push('"');
callback(&label_buffer);
});
}
impl Metrics {
fn register(registry: &Registry) -> Result<Self, PrometheusError> {
Ok(Self {
events_total: register(CounterVec::new(
Opts::new(
"bizinikiwi_sub_libp2p_out_events_events_total",
"Number of broadcast network events that have been sent or received across all \
channels"
),
&["event_name", "action", "name"]
)?, registry)?,
notifications_sizes: register(CounterVec::new(
Opts::new(
"bizinikiwi_sub_libp2p_out_events_notifications_sizes",
"Size of notification events that have been sent or received across all \
channels"
),
&["protocol", "action", "name"]
)?, registry)?,
num_channels: register(GaugeVec::new(
Opts::new(
"bizinikiwi_sub_libp2p_out_events_num_channels",
"Number of internal active channels that broadcast network events",
),
&["name"]
)?, registry)?,
})
}
fn event_in(&self, event: &Event, name: &str) {
match event {
Event::Dht(_) => {
self.events_total.with_label_values(&["dht", "sent", name]).inc();
},
Event::NotificationStreamOpened { protocol, .. } => {
format_label("notif-open-", protocol, |protocol_label| {
self.events_total.with_label_values(&[protocol_label, "sent", name]).inc();
});
},
Event::NotificationStreamClosed { protocol, .. } => {
format_label("notif-closed-", protocol, |protocol_label| {
self.events_total.with_label_values(&[protocol_label, "sent", name]).inc();
});
},
Event::NotificationsReceived { messages, .. } =>
for (protocol, message) in messages {
format_label("notif-", protocol, |protocol_label| {
self.events_total.with_label_values(&[protocol_label, "sent", name]).inc();
});
self.notifications_sizes
.with_label_values(&[protocol, "sent", name])
.inc_by(u64::try_from(message.len()).unwrap_or(u64::MAX));
},
}
}
fn event_out(&self, event: &Event, name: &str) {
match event {
Event::Dht(_) => {
self.events_total.with_label_values(&["dht", "received", name]).inc();
},
Event::NotificationStreamOpened { protocol, .. } => {
format_label("notif-open-", protocol, |protocol_label| {
self.events_total.with_label_values(&[protocol_label, "received", name]).inc();
});
},
Event::NotificationStreamClosed { protocol, .. } => {
format_label("notif-closed-", protocol, |protocol_label| {
self.events_total.with_label_values(&[protocol_label, "received", name]).inc();
});
},
Event::NotificationsReceived { messages, .. } =>
for (protocol, message) in messages {
format_label("notif-", protocol, |protocol_label| {
self.events_total
.with_label_values(&[protocol_label, "received", name])
.inc();
});
self.notifications_sizes
.with_label_values(&[protocol, "received", name])
.inc_by(u64::try_from(message.len()).unwrap_or(u64::MAX));
},
}
}
}
@@ -0,0 +1,113 @@
// 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/>.
//
// If you read this, you are very thorough, congratulations.
//! Signature-related code
pub use libp2p::identity::SigningError;
/// Public key.
pub enum PublicKey {
/// Litep2p public key.
Libp2p(libp2p::identity::PublicKey),
/// Libp2p public key.
Litep2p(litep2p::crypto::PublicKey),
}
impl PublicKey {
/// Protobuf-encode [`PublicKey`].
pub fn encode_protobuf(&self) -> Vec<u8> {
match self {
Self::Libp2p(public) => public.encode_protobuf(),
Self::Litep2p(public) => public.to_protobuf_encoding(),
}
}
/// Get `PeerId` of the [`PublicKey`].
pub fn to_peer_id(&self) -> pezsc_network_types::PeerId {
match self {
Self::Libp2p(public) => public.to_peer_id().into(),
Self::Litep2p(public) => public.to_peer_id().into(),
}
}
}
/// Keypair.
pub enum Keypair {
/// Litep2p keypair.
Libp2p(libp2p::identity::Keypair),
/// Libp2p keypair.
Litep2p(litep2p::crypto::ed25519::Keypair),
}
impl Keypair {
/// Generate ed25519 keypair.
pub fn generate_ed25519() -> Self {
Keypair::Litep2p(litep2p::crypto::ed25519::Keypair::generate())
}
/// Get [`Keypair`]'s public key.
pub fn public(&self) -> PublicKey {
match self {
Keypair::Libp2p(keypair) => PublicKey::Libp2p(keypair.public()),
Keypair::Litep2p(keypair) => PublicKey::Litep2p(keypair.public().into()),
}
}
}
/// A result of signing a message with a network identity. Since `PeerId` is potentially a hash of a
/// `PublicKey`, you need to reveal the `PublicKey` next to the signature, so the verifier can check
/// if the signature was made by the entity that controls a given `PeerId`.
pub struct Signature {
/// The public key derived from the network identity that signed the message.
pub public_key: PublicKey,
/// The actual signature made for the message signed.
pub bytes: Vec<u8>,
}
impl Signature {
/// Create new [`Signature`].
pub fn new(public_key: PublicKey, bytes: Vec<u8>) -> Self {
Self { public_key, bytes }
}
/// Create a signature for a message with a given network identity.
pub fn sign_message(
message: impl AsRef<[u8]>,
keypair: &Keypair,
) -> Result<Self, SigningError> {
match keypair {
Keypair::Libp2p(keypair) => {
let public_key = keypair.public();
let bytes = keypair.sign(message.as_ref())?;
Ok(Signature { public_key: PublicKey::Libp2p(public_key), bytes })
},
Keypair::Litep2p(keypair) => {
let public_key = keypair.public();
let bytes = keypair.sign(message.as_ref());
Ok(Signature { public_key: PublicKey::Litep2p(public_key.into()), bytes })
},
}
}
}
@@ -0,0 +1,972 @@
// 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/>.
//
// If you read this, you are very thorough, congratulations.
//! Traits defined by `sc-network`.
use crate::{
config::{IncomingRequest, MultiaddrWithPeerId, NotificationHandshake, Params, SetConfig},
error::{self, Error},
event::Event,
network_state::NetworkState,
request_responses::{IfDisconnected, RequestFailure},
service::{metrics::NotificationMetrics, signature::Signature, PeerStoreProvider},
types::ProtocolName,
ReputationChange,
};
use futures::{channel::oneshot, Stream};
use prometheus_endpoint::Registry;
use pezsc_client_api::BlockBackend;
use pezsc_network_common::{role::ObservedRole, ExHashT};
pub use pezsc_network_types::{
kad::{Key as KademliaKey, Record},
multiaddr::Multiaddr,
PeerId,
};
use pezsp_runtime::traits::Block as BlockT;
use std::{
collections::HashSet,
fmt::Debug,
future::Future,
pin::Pin,
sync::Arc,
time::{Duration, Instant},
};
pub use libp2p::identity::SigningError;
/// Supertrait defining the services provided by [`NetworkBackend`] service handle.
pub trait NetworkService:
NetworkSigner
+ NetworkDHTProvider
+ NetworkStatusProvider
+ NetworkPeers
+ NetworkEventStream
+ NetworkStateInfo
+ NetworkRequest
+ Send
+ Sync
+ 'static
{
}
impl<T> NetworkService for T where
T: NetworkSigner
+ NetworkDHTProvider
+ NetworkStatusProvider
+ NetworkPeers
+ NetworkEventStream
+ NetworkStateInfo
+ NetworkRequest
+ Send
+ Sync
+ 'static
{
}
/// Trait defining the required functionality from a notification protocol configuration.
pub trait NotificationConfig: Debug {
/// Get access to the `SetConfig` of the notification protocol.
fn set_config(&self) -> &SetConfig;
/// Get protocol name.
fn protocol_name(&self) -> &ProtocolName;
}
/// Trait defining the required functionality from a request-response protocol configuration.
pub trait RequestResponseConfig: Debug {
/// Get protocol name.
fn protocol_name(&self) -> &ProtocolName;
}
/// Trait defining required functionality from `PeerStore`.
#[async_trait::async_trait]
pub trait PeerStore {
/// Get handle to `PeerStore`.
fn handle(&self) -> Arc<dyn PeerStoreProvider>;
/// Start running `PeerStore` event loop.
async fn run(self);
}
/// Networking backend.
#[async_trait::async_trait]
pub trait NetworkBackend<B: BlockT + 'static, H: ExHashT>: Send + 'static {
/// Type representing notification protocol-related configuration.
type NotificationProtocolConfig: NotificationConfig;
/// Type representing request-response protocol-related configuration.
type RequestResponseProtocolConfig: RequestResponseConfig;
/// Type implementing `NetworkService` for the networking backend.
///
/// `NetworkService` allows other subsystems of the blockchain to interact with `sc-network`
/// using `NetworkService`.
type NetworkService<Block, Hash>: NetworkService + Clone;
/// Type implementing [`PeerStore`].
type PeerStore: PeerStore;
/// Bitswap config.
type BitswapConfig;
/// Create new `NetworkBackend`.
fn new(params: Params<B, H, Self>) -> Result<Self, Error>
where
Self: Sized;
/// Get handle to `NetworkService` of the `NetworkBackend`.
fn network_service(&self) -> Arc<dyn NetworkService>;
/// Create [`PeerStore`].
fn peer_store(bootnodes: Vec<PeerId>, metrics_registry: Option<Registry>) -> Self::PeerStore;
/// Register metrics that are used by the notification protocols.
fn register_notification_metrics(registry: Option<&Registry>) -> NotificationMetrics;
/// Create Bitswap server.
fn bitswap_server(
client: Arc<dyn BlockBackend<B> + Send + Sync>,
) -> (Pin<Box<dyn Future<Output = ()> + Send>>, Self::BitswapConfig);
/// Create notification protocol configuration and an associated `NotificationService`
/// for the protocol.
fn notification_config(
protocol_name: ProtocolName,
fallback_names: Vec<ProtocolName>,
max_notification_size: u64,
handshake: Option<NotificationHandshake>,
set_config: SetConfig,
metrics: NotificationMetrics,
peerstore_handle: Arc<dyn PeerStoreProvider>,
) -> (Self::NotificationProtocolConfig, Box<dyn NotificationService>);
/// Create request-response protocol configuration.
fn request_response_config(
protocol_name: ProtocolName,
fallback_names: Vec<ProtocolName>,
max_request_size: u64,
max_response_size: u64,
request_timeout: Duration,
inbound_queue: Option<async_channel::Sender<IncomingRequest>>,
) -> Self::RequestResponseProtocolConfig;
/// Start [`NetworkBackend`] event loop.
async fn run(mut self);
}
/// Signer with network identity
pub trait NetworkSigner {
/// Signs the message with the `KeyPair` that defines the local [`PeerId`].
fn sign_with_local_identity(&self, msg: Vec<u8>) -> Result<Signature, SigningError>;
/// Verify signature using peer's public key.
///
/// `public_key` must be Protobuf-encoded ed25519 public key.
///
/// Returns `Err(())` if public cannot be parsed into a valid ed25519 public key.
fn verify(
&self,
peer_id: pezsc_network_types::PeerId,
public_key: &Vec<u8>,
signature: &Vec<u8>,
message: &Vec<u8>,
) -> Result<bool, String>;
}
impl<T> NetworkSigner for Arc<T>
where
T: ?Sized,
T: NetworkSigner,
{
fn sign_with_local_identity(&self, msg: Vec<u8>) -> Result<Signature, SigningError> {
T::sign_with_local_identity(self, msg)
}
fn verify(
&self,
peer_id: pezsc_network_types::PeerId,
public_key: &Vec<u8>,
signature: &Vec<u8>,
message: &Vec<u8>,
) -> Result<bool, String> {
T::verify(self, peer_id, public_key, signature, message)
}
}
/// Provides access to the networking DHT.
pub trait NetworkDHTProvider {
/// Start finding closest peers to the target.
fn find_closest_peers(&self, target: PeerId);
/// Start getting a value from the DHT.
fn get_value(&self, key: &KademliaKey);
/// Start putting a value in the DHT.
fn put_value(&self, key: KademliaKey, value: Vec<u8>);
/// Start putting the record to `peers`.
///
/// If `update_local_storage` is true the local storage is udpated as well.
fn put_record_to(&self, record: Record, peers: HashSet<PeerId>, update_local_storage: bool);
/// Store a record in the DHT memory store.
fn store_record(
&self,
key: KademliaKey,
value: Vec<u8>,
publisher: Option<PeerId>,
expires: Option<Instant>,
);
/// Register this node as a provider for `key` on the DHT.
fn start_providing(&self, key: KademliaKey);
/// Deregister this node as a provider for `key` on the DHT.
fn stop_providing(&self, key: KademliaKey);
/// Start getting the list of providers for `key` on the DHT.
fn get_providers(&self, key: KademliaKey);
}
impl<T> NetworkDHTProvider for Arc<T>
where
T: ?Sized,
T: NetworkDHTProvider,
{
fn find_closest_peers(&self, target: PeerId) {
T::find_closest_peers(self, target)
}
fn get_value(&self, key: &KademliaKey) {
T::get_value(self, key)
}
fn put_value(&self, key: KademliaKey, value: Vec<u8>) {
T::put_value(self, key, value)
}
fn put_record_to(&self, record: Record, peers: HashSet<PeerId>, update_local_storage: bool) {
T::put_record_to(self, record, peers, update_local_storage)
}
fn store_record(
&self,
key: KademliaKey,
value: Vec<u8>,
publisher: Option<PeerId>,
expires: Option<Instant>,
) {
T::store_record(self, key, value, publisher, expires)
}
fn start_providing(&self, key: KademliaKey) {
T::start_providing(self, key)
}
fn stop_providing(&self, key: KademliaKey) {
T::stop_providing(self, key)
}
fn get_providers(&self, key: KademliaKey) {
T::get_providers(self, key)
}
}
/// Provides an ability to set a fork sync request for a particular block.
pub trait NetworkSyncForkRequest<BlockHash, BlockNumber> {
/// Notifies the sync service to try and sync the given block from the given
/// peers.
///
/// If the given vector of peers is empty then the underlying implementation
/// should make a best effort to fetch the block from any peers it is
/// connected to (NOTE: this assumption will change in the future #3629).
fn set_sync_fork_request(&self, peers: Vec<PeerId>, hash: BlockHash, number: BlockNumber);
}
impl<T, BlockHash, BlockNumber> NetworkSyncForkRequest<BlockHash, BlockNumber> for Arc<T>
where
T: ?Sized,
T: NetworkSyncForkRequest<BlockHash, BlockNumber>,
{
fn set_sync_fork_request(&self, peers: Vec<PeerId>, hash: BlockHash, number: BlockNumber) {
T::set_sync_fork_request(self, peers, hash, number)
}
}
/// Overview status of the network.
#[derive(Clone)]
pub struct NetworkStatus {
/// Total number of connected peers.
pub num_connected_peers: usize,
/// The total number of bytes received.
pub total_bytes_inbound: u64,
/// The total number of bytes sent.
pub total_bytes_outbound: u64,
}
/// Provides high-level status information about network.
#[async_trait::async_trait]
pub trait NetworkStatusProvider {
/// High-level network status information.
///
/// Returns an error if the `NetworkWorker` is no longer running.
async fn status(&self) -> Result<NetworkStatus, ()>;
/// Get the network state.
///
/// Returns an error if the `NetworkWorker` is no longer running.
async fn network_state(&self) -> Result<NetworkState, ()>;
}
// Manual implementation to avoid extra boxing here
impl<T> NetworkStatusProvider for Arc<T>
where
T: ?Sized,
T: NetworkStatusProvider,
{
fn status<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = Result<NetworkStatus, ()>> + Send + 'async_trait>>
where
'life0: 'async_trait,
Self: 'async_trait,
{
T::status(self)
}
fn network_state<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = Result<NetworkState, ()>> + Send + 'async_trait>>
where
'life0: 'async_trait,
Self: 'async_trait,
{
T::network_state(self)
}
}
/// Provides low-level API for manipulating network peers.
#[async_trait::async_trait]
pub trait NetworkPeers {
/// Set authorized peers.
///
/// Need a better solution to manage authorized peers, but now just use reserved peers for
/// prototyping.
fn set_authorized_peers(&self, peers: HashSet<PeerId>);
/// Set authorized_only flag.
///
/// Need a better solution to decide authorized_only, but now just use reserved_only flag for
/// prototyping.
fn set_authorized_only(&self, reserved_only: bool);
/// Adds an address known to a node.
fn add_known_address(&self, peer_id: PeerId, addr: Multiaddr);
/// Report a given peer as either beneficial (+) or costly (-) according to the
/// given scalar.
fn report_peer(&self, peer_id: PeerId, cost_benefit: ReputationChange);
/// Get peer reputation.
fn peer_reputation(&self, peer_id: &PeerId) -> i32;
/// Disconnect from a node as soon as possible.
///
/// This triggers the same effects as if the connection had closed itself spontaneously.
fn disconnect_peer(&self, peer_id: PeerId, protocol: ProtocolName);
/// Connect to unreserved peers and allow unreserved peers to connect for syncing purposes.
fn accept_unreserved_peers(&self);
/// Disconnect from unreserved peers and deny new unreserved peers to connect for syncing
/// purposes.
fn deny_unreserved_peers(&self);
/// Adds a `PeerId` and its `Multiaddr` as reserved for a sync protocol (default peer set).
///
/// Returns an `Err` if the given string is not a valid multiaddress
/// or contains an invalid peer ID (which includes the local peer ID).
fn add_reserved_peer(&self, peer: MultiaddrWithPeerId) -> Result<(), String>;
/// Removes a `PeerId` from the list of reserved peers for a sync protocol (default peer set).
fn remove_reserved_peer(&self, peer_id: PeerId);
/// Sets the reserved set of a protocol to the given set of peers.
///
/// Each `Multiaddr` must end with a `/p2p/` component containing the `PeerId`. It can also
/// consist of only `/p2p/<peerid>`.
///
/// The node will start establishing/accepting connections and substreams to/from peers in this
/// set, if it doesn't have any substream open with them yet.
///
/// Note however, if a call to this function results in less peers on the reserved set, they
/// will not necessarily get disconnected (depending on available free slots in the peer set).
/// If you want to also disconnect those removed peers, you will have to call
/// `remove_from_peers_set` on those in addition to updating the reserved set. You can omit
/// this step if the peer set is in reserved only mode.
///
/// Returns an `Err` if one of the given addresses is invalid or contains an
/// invalid peer ID (which includes the local peer ID), or if `protocol` does not
/// refer to a known protocol.
fn set_reserved_peers(
&self,
protocol: ProtocolName,
peers: HashSet<Multiaddr>,
) -> Result<(), String>;
/// Add peers to a peer set.
///
/// Each `Multiaddr` must end with a `/p2p/` component containing the `PeerId`. It can also
/// consist of only `/p2p/<peerid>`.
///
/// Returns an `Err` if one of the given addresses is invalid or contains an
/// invalid peer ID (which includes the local peer ID), or if `protocol` does not
/// refer to a know protocol.
fn add_peers_to_reserved_set(
&self,
protocol: ProtocolName,
peers: HashSet<Multiaddr>,
) -> Result<(), String>;
/// Remove peers from a peer set.
///
/// Returns `Err` if `protocol` does not refer to a known protocol.
fn remove_peers_from_reserved_set(
&self,
protocol: ProtocolName,
peers: Vec<PeerId>,
) -> Result<(), String>;
/// Returns the number of peers in the sync peer set we're connected to.
fn sync_num_connected(&self) -> usize;
/// Attempt to get peer role.
///
/// Right now the peer role is decoded from the received handshake for all protocols
/// (`/block-announces/1` has other information as well). If the handshake cannot be
/// decoded into a role, the role queried from `PeerStore` and if the role is not stored
/// there either, `None` is returned and the peer should be discarded.
fn peer_role(&self, peer_id: PeerId, handshake: Vec<u8>) -> Option<ObservedRole>;
/// Get the list of reserved peers.
///
/// Returns an error if the `NetworkWorker` is no longer running.
async fn reserved_peers(&self) -> Result<Vec<PeerId>, ()>;
}
// Manual implementation to avoid extra boxing here
#[async_trait::async_trait]
impl<T> NetworkPeers for Arc<T>
where
T: ?Sized,
T: NetworkPeers,
{
fn set_authorized_peers(&self, peers: HashSet<PeerId>) {
T::set_authorized_peers(self, peers)
}
fn set_authorized_only(&self, reserved_only: bool) {
T::set_authorized_only(self, reserved_only)
}
fn add_known_address(&self, peer_id: PeerId, addr: Multiaddr) {
T::add_known_address(self, peer_id, addr)
}
fn report_peer(&self, peer_id: PeerId, cost_benefit: ReputationChange) {
T::report_peer(self, peer_id, cost_benefit)
}
fn peer_reputation(&self, peer_id: &PeerId) -> i32 {
T::peer_reputation(self, peer_id)
}
fn disconnect_peer(&self, peer_id: PeerId, protocol: ProtocolName) {
T::disconnect_peer(self, peer_id, protocol)
}
fn accept_unreserved_peers(&self) {
T::accept_unreserved_peers(self)
}
fn deny_unreserved_peers(&self) {
T::deny_unreserved_peers(self)
}
fn add_reserved_peer(&self, peer: MultiaddrWithPeerId) -> Result<(), String> {
T::add_reserved_peer(self, peer)
}
fn remove_reserved_peer(&self, peer_id: PeerId) {
T::remove_reserved_peer(self, peer_id)
}
fn set_reserved_peers(
&self,
protocol: ProtocolName,
peers: HashSet<Multiaddr>,
) -> Result<(), String> {
T::set_reserved_peers(self, protocol, peers)
}
fn add_peers_to_reserved_set(
&self,
protocol: ProtocolName,
peers: HashSet<Multiaddr>,
) -> Result<(), String> {
T::add_peers_to_reserved_set(self, protocol, peers)
}
fn remove_peers_from_reserved_set(
&self,
protocol: ProtocolName,
peers: Vec<PeerId>,
) -> Result<(), String> {
T::remove_peers_from_reserved_set(self, protocol, peers)
}
fn sync_num_connected(&self) -> usize {
T::sync_num_connected(self)
}
fn peer_role(&self, peer_id: PeerId, handshake: Vec<u8>) -> Option<ObservedRole> {
T::peer_role(self, peer_id, handshake)
}
fn reserved_peers<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = Result<Vec<PeerId>, ()>> + Send + 'async_trait>>
where
'life0: 'async_trait,
Self: 'async_trait,
{
T::reserved_peers(self)
}
}
/// Provides access to network-level event stream.
pub trait NetworkEventStream {
/// Returns a stream containing the events that happen on the network.
///
/// If this method is called multiple times, the events are duplicated.
///
/// The stream never ends (unless the `NetworkWorker` gets shut down).
///
/// The name passed is used to identify the channel in the Prometheus metrics. Note that the
/// parameter is a `&'static str`, and not a `String`, in order to avoid accidentally having
/// an unbounded set of Prometheus metrics, which would be quite bad in terms of memory
fn event_stream(&self, name: &'static str) -> Pin<Box<dyn Stream<Item = Event> + Send>>;
}
impl<T> NetworkEventStream for Arc<T>
where
T: ?Sized,
T: NetworkEventStream,
{
fn event_stream(&self, name: &'static str) -> Pin<Box<dyn Stream<Item = Event> + Send>> {
T::event_stream(self, name)
}
}
/// Trait for providing information about the local network state
pub trait NetworkStateInfo {
/// Returns the local external addresses.
fn external_addresses(&self) -> Vec<Multiaddr>;
/// Returns the listening addresses (without trailing `/p2p/` with our `PeerId`).
fn listen_addresses(&self) -> Vec<Multiaddr>;
/// Returns the local Peer ID.
fn local_peer_id(&self) -> PeerId;
}
impl<T> NetworkStateInfo for Arc<T>
where
T: ?Sized,
T: NetworkStateInfo,
{
fn external_addresses(&self) -> Vec<Multiaddr> {
T::external_addresses(self)
}
fn listen_addresses(&self) -> Vec<Multiaddr> {
T::listen_addresses(self)
}
fn local_peer_id(&self) -> PeerId {
T::local_peer_id(self)
}
}
/// Reserved slot in the notifications buffer, ready to accept data.
pub trait NotificationSenderReady {
/// Consumes this slots reservation and actually queues the notification.
///
/// NOTE: Traits can't consume itself, but calling this method second time will return an error.
fn send(&mut self, notification: Vec<u8>) -> Result<(), NotificationSenderError>;
}
/// A `NotificationSender` allows for sending notifications to a peer with a chosen protocol.
#[async_trait::async_trait]
pub trait NotificationSender: Send + Sync + 'static {
/// Returns a future that resolves when the `NotificationSender` is ready to send a
/// notification.
async fn ready(&self)
-> Result<Box<dyn NotificationSenderReady + '_>, NotificationSenderError>;
}
/// Error returned by the notification sink.
#[derive(Debug, thiserror::Error)]
pub enum NotificationSenderError {
/// The notification receiver has been closed, usually because the underlying connection
/// closed.
///
/// Some of the notifications most recently sent may not have been received. However,
/// the peer may still be connected and a new notification sink for the same
/// protocol obtained from [`NotificationService::message_sink()`].
#[error("The notification receiver has been closed")]
Closed,
/// Protocol name hasn't been registered.
#[error("Protocol name hasn't been registered")]
BadProtocol,
}
/// Provides ability to send network requests.
#[async_trait::async_trait]
pub trait NetworkRequest {
/// Sends a single targeted request to a specific peer. On success, returns the response of
/// the peer.
///
/// Request-response protocols are a way to complement notifications protocols, but
/// notifications should remain the default ways of communicating information. For example, a
/// peer can announce something through a notification, after which the recipient can obtain
/// more information by performing a request.
/// As such, call this function with `IfDisconnected::ImmediateError` for `connect`. This way
/// you will get an error immediately for disconnected peers, instead of waiting for a
/// potentially very long connection attempt, which would suggest that something is wrong
/// anyway, as you are supposed to be connected because of the notification protocol.
///
/// No limit or throttling of concurrent outbound requests per peer and protocol are enforced.
/// Such restrictions, if desired, need to be enforced at the call site(s).
///
/// The protocol must have been registered through
/// `NetworkConfiguration::request_response_protocols`.
async fn request(
&self,
target: PeerId,
protocol: ProtocolName,
request: Vec<u8>,
fallback_request: Option<(Vec<u8>, ProtocolName)>,
connect: IfDisconnected,
) -> Result<(Vec<u8>, ProtocolName), RequestFailure>;
/// Variation of `request` which starts a request whose response is delivered on a provided
/// channel.
///
/// Instead of blocking and waiting for a reply, this function returns immediately, sending
/// responses via the passed in sender. This alternative API exists to make it easier to
/// integrate with message passing APIs.
///
/// Keep in mind that the connected receiver might receive a `Canceled` event in case of a
/// closing connection. This is expected behaviour. With `request` you would get a
/// `RequestFailure::Network(OutboundFailure::ConnectionClosed)` in that case.
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,
);
}
// Manual implementation to avoid extra boxing here
impl<T> NetworkRequest for Arc<T>
where
T: ?Sized,
T: NetworkRequest,
{
fn request<'life0, 'async_trait>(
&'life0 self,
target: PeerId,
protocol: ProtocolName,
request: Vec<u8>,
fallback_request: Option<(Vec<u8>, ProtocolName)>,
connect: IfDisconnected,
) -> Pin<
Box<
dyn Future<Output = Result<(Vec<u8>, ProtocolName), RequestFailure>>
+ Send
+ 'async_trait,
>,
>
where
'life0: 'async_trait,
Self: 'async_trait,
{
T::request(self, target, protocol, request, fallback_request, connect)
}
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,
) {
T::start_request(self, target, protocol, request, fallback_request, tx, connect)
}
}
/// Provides ability to announce blocks to the network.
pub trait NetworkBlock<BlockHash, BlockNumber> {
/// Make sure an important block is propagated to peers.
///
/// In chain-based consensus, we often need to make sure non-best forks are
/// at least temporarily synced. This function forces such an announcement.
fn announce_block(&self, hash: BlockHash, data: Option<Vec<u8>>);
/// Inform the network service about new best imported block.
fn new_best_block_imported(&self, hash: BlockHash, number: BlockNumber);
}
impl<T, BlockHash, BlockNumber> NetworkBlock<BlockHash, BlockNumber> for Arc<T>
where
T: ?Sized,
T: NetworkBlock<BlockHash, BlockNumber>,
{
fn announce_block(&self, hash: BlockHash, data: Option<Vec<u8>>) {
T::announce_block(self, hash, data)
}
fn new_best_block_imported(&self, hash: BlockHash, number: BlockNumber) {
T::new_best_block_imported(self, hash, number)
}
}
/// Substream acceptance result.
#[derive(Debug, PartialEq, Eq)]
pub enum ValidationResult {
/// Accept inbound substream.
Accept,
/// Reject inbound substream.
Reject,
}
/// Substream direction.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Direction {
/// Substream opened by the remote node.
Inbound,
/// Substream opened by the local node.
Outbound,
}
impl From<litep2p::protocol::notification::Direction> for Direction {
fn from(direction: litep2p::protocol::notification::Direction) -> Self {
match direction {
litep2p::protocol::notification::Direction::Inbound => Direction::Inbound,
litep2p::protocol::notification::Direction::Outbound => Direction::Outbound,
}
}
}
impl Direction {
/// Is the direction inbound.
pub fn is_inbound(&self) -> bool {
std::matches!(self, Direction::Inbound)
}
}
/// Events received by the protocol from `Notifications`.
#[derive(Debug)]
pub enum NotificationEvent {
/// Validate inbound substream.
ValidateInboundSubstream {
/// Peer ID.
peer: PeerId,
/// Received handshake.
handshake: Vec<u8>,
/// `oneshot::Sender` for sending validation result back to `Notifications`
result_tx: tokio::sync::oneshot::Sender<ValidationResult>,
},
/// Remote identified by `PeerId` opened a substream and sent `Handshake`.
/// Validate `Handshake` and report status (accept/reject) to `Notifications`.
NotificationStreamOpened {
/// Peer ID.
peer: PeerId,
/// Is the substream inbound or outbound.
direction: Direction,
/// Received handshake.
handshake: Vec<u8>,
/// Negotiated fallback.
negotiated_fallback: Option<ProtocolName>,
},
/// Substream was closed.
NotificationStreamClosed {
/// Peer Id.
peer: PeerId,
},
/// Notification was received from the substream.
NotificationReceived {
/// Peer ID.
peer: PeerId,
/// Received notification.
notification: Vec<u8>,
},
}
/// Notification service
///
/// Defines behaviors that both the protocol implementations and `Notifications` can expect from
/// each other.
///
/// `Notifications` can send two different kinds of information to protocol:
/// * substream-related information
/// * notification-related information
///
/// When an unvalidated, inbound substream is received by `Notifications`, it sends the inbound
/// stream information (peer ID, handshake) to protocol for validation. Protocol must then verify
/// that the handshake is valid (and in the future that it has a slot it can allocate for the peer)
/// and then report back the `ValidationResult` which is either `Accept` or `Reject`.
///
/// After the validation result has been received by `Notifications`, it prepares the
/// substream for communication by initializing the necessary sinks and emits
/// `NotificationStreamOpened` which informs the protocol that the remote peer is ready to receive
/// notifications.
///
/// Two different flavors of sending options are provided:
/// * synchronous sending ([`NotificationService::send_sync_notification()`])
/// * asynchronous sending ([`NotificationService::send_async_notification()`])
///
/// The former is used by the protocols not ready to exercise backpressure and the latter by the
/// protocols that can do it.
///
/// Both local and remote peer can close the substream at any time. Local peer can do so by calling
/// [`NotificationService::close_substream()`] which instructs `Notifications` to close the
/// substream. Remote closing the substream is indicated to the local peer by receiving
/// [`NotificationEvent::NotificationStreamClosed`] event.
///
/// In case the protocol must update its handshake while it's operating (such as updating the best
/// block information), it can do so by calling [`NotificationService::set_handshake()`]
/// which instructs `Notifications` to update the handshake it stored during protocol
/// initialization.
///
/// All peer events are multiplexed on the same incoming event stream from `Notifications` and thus
/// each event carries a `PeerId` so the protocol knows whose information to update when receiving
/// an event.
#[async_trait::async_trait]
pub trait NotificationService: Debug + Send {
/// Instruct `Notifications` to open a new substream for `peer`.
///
/// `dial_if_disconnected` informs `Notifications` whether to dial
// the peer if there is currently no active connection to it.
//
// NOTE: not offered by the current implementation
async fn open_substream(&mut self, peer: PeerId) -> Result<(), ()>;
/// Instruct `Notifications` to close substream for `peer`.
//
// NOTE: not offered by the current implementation
async fn close_substream(&mut self, peer: PeerId) -> Result<(), ()>;
/// Send synchronous `notification` to `peer`.
fn send_sync_notification(&mut self, peer: &PeerId, notification: Vec<u8>);
/// Send asynchronous `notification` to `peer`, allowing sender to exercise backpressure.
///
/// Returns an error if the peer doesn't exist.
async fn send_async_notification(
&mut self,
peer: &PeerId,
notification: Vec<u8>,
) -> Result<(), error::Error>;
/// Set handshake for the notification protocol replacing the old handshake.
async fn set_handshake(&mut self, handshake: Vec<u8>) -> Result<(), ()>;
/// Non-blocking variant of `set_handshake()` that attempts to update the handshake
/// and returns an error if the channel is blocked.
///
/// Technically the function can return an error if the channel to `Notifications` is closed
/// but that doesn't happen under normal operation.
fn try_set_handshake(&mut self, handshake: Vec<u8>) -> Result<(), ()>;
/// Get next event from the `Notifications` event stream.
async fn next_event(&mut self) -> Option<NotificationEvent>;
/// Make a copy of the object so it can be shared between protocol components
/// who wish to have access to the same underlying notification protocol.
fn clone(&mut self) -> Result<Box<dyn NotificationService>, ()>;
/// Get protocol name of the `NotificationService`.
fn protocol(&self) -> &ProtocolName;
/// Get message sink of the peer.
fn message_sink(&self, peer: &PeerId) -> Option<Box<dyn MessageSink>>;
}
/// Message sink for peers.
///
/// If protocol cannot use [`NotificationService`] to send notifications to peers and requires,
/// e.g., notifications to be sent in another task, the protocol may acquire a [`MessageSink`]
/// object for each peer by calling [`NotificationService::message_sink()`]. Calling this
/// function returns an object which allows the protocol to send notifications to the remote peer.
///
/// Use of this API is discouraged as it's not as performant as sending notifications through
/// [`NotificationService`] due to synchronization required to keep the underlying notification
/// sink up to date with possible sink replacement events.
#[async_trait::async_trait]
pub trait MessageSink: Send + Sync {
/// Send synchronous `notification` to the peer associated with this [`MessageSink`].
fn send_sync_notification(&self, notification: Vec<u8>);
/// Send an asynchronous `notification` to to the peer associated with this [`MessageSink`],
/// allowing sender to exercise backpressure.
///
/// Returns an error if the peer does not exist.
async fn send_async_notification(&self, notification: Vec<u8>) -> Result<(), error::Error>;
}
/// Trait defining the behavior of a bandwidth sink.
pub trait BandwidthSink: Send + Sync {
/// Get the number of bytes received.
fn total_inbound(&self) -> u64;
/// Get the number of bytes sent.
fn total_outbound(&self) -> u64;
}
@@ -0,0 +1,87 @@
// 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/>.
//! Transport that serves as a common ground for all connections.
use either::Either;
use libp2p::{
core::{
muxing::StreamMuxerBox,
transport::{Boxed, OptionalTransport},
upgrade,
},
dns, identity, noise, tcp, websocket, PeerId, Transport, TransportExt,
};
use std::{sync::Arc, time::Duration};
// TODO: Create a wrapper similar to upstream `BandwidthTransport` that tracks sent/received bytes
#[allow(deprecated)]
pub use libp2p::bandwidth::BandwidthSinks;
/// Builds the transport that serves as a common ground for all connections.
///
/// If `memory_only` is true, then only communication within the same process are allowed. Only
/// addresses with the format `/memory/...` are allowed.
///
/// Returns a `BandwidthSinks` object that allows querying the average bandwidth produced by all
/// the connections spawned with this transport.
#[allow(deprecated)]
pub fn build_transport(
keypair: identity::Keypair,
memory_only: bool,
) -> (Boxed<(PeerId, StreamMuxerBox)>, Arc<BandwidthSinks>) {
// Build the base layer of the transport.
let transport = if !memory_only {
// Main transport: DNS(TCP)
let tcp_config = tcp::Config::new().nodelay(true);
let tcp_trans = tcp::tokio::Transport::new(tcp_config.clone());
let dns_init = dns::tokio::Transport::system(tcp_trans);
Either::Left(if let Ok(dns) = dns_init {
// WS + WSS transport
//
// Main transport can't be used for `/wss` addresses because WSS transport needs
// unresolved addresses (BUT WSS transport itself needs an instance of DNS transport to
// resolve and dial addresses).
let tcp_trans = tcp::tokio::Transport::new(tcp_config);
let dns_for_wss = dns::tokio::Transport::system(tcp_trans)
.expect("same system_conf & resolver to work");
Either::Left(websocket::WsConfig::new(dns_for_wss).or_transport(dns))
} else {
// In case DNS can't be constructed, fallback to TCP + WS (WSS won't work)
let tcp_trans = tcp::tokio::Transport::new(tcp_config.clone());
let desktop_trans = websocket::WsConfig::new(tcp_trans)
.or_transport(tcp::tokio::Transport::new(tcp_config));
Either::Right(desktop_trans)
})
} else {
Either::Right(OptionalTransport::some(libp2p::core::transport::MemoryTransport::default()))
};
let authentication_config = noise::Config::new(&keypair).expect("Can create noise config. qed");
let multiplexing_config = libp2p::yamux::Config::default();
let transport = transport
.upgrade(upgrade::Version::V1Lazy)
.authenticate(authentication_config)
.multiplex(multiplexing_config)
.timeout(Duration::from_secs(20))
.boxed();
transport.with_bandwidth_logging()
}
+162
View File
@@ -0,0 +1,162 @@
// 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/>.
//! `sc-network` type definitions
use std::{
borrow::Borrow,
fmt,
hash::{Hash, Hasher},
ops::Deref,
sync::Arc,
};
/// The protocol name transmitted on the wire.
#[derive(Debug, Clone)]
pub enum ProtocolName {
/// The protocol name as a static string.
Static(&'static str),
/// The protocol name as a dynamically allocated string.
OnHeap(Arc<str>),
}
impl From<&'static str> for ProtocolName {
fn from(name: &'static str) -> Self {
Self::Static(name)
}
}
impl From<Arc<str>> for ProtocolName {
fn from(name: Arc<str>) -> Self {
Self::OnHeap(name)
}
}
impl From<String> for ProtocolName {
fn from(name: String) -> Self {
Self::OnHeap(Arc::from(name))
}
}
impl Deref for ProtocolName {
type Target = str;
fn deref(&self) -> &str {
match self {
Self::Static(name) => name,
Self::OnHeap(name) => &name,
}
}
}
impl Borrow<str> for ProtocolName {
fn borrow(&self) -> &str {
self
}
}
impl PartialEq for ProtocolName {
fn eq(&self, other: &Self) -> bool {
(self as &str) == (other as &str)
}
}
impl Eq for ProtocolName {}
impl Hash for ProtocolName {
fn hash<H: Hasher>(&self, state: &mut H) {
(self as &str).hash(state)
}
}
impl fmt::Display for ProtocolName {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(self)
}
}
impl AsRef<str> for ProtocolName {
fn as_ref(&self) -> &str {
self as &str
}
}
impl From<ProtocolName> for litep2p::ProtocolName {
fn from(protocol: ProtocolName) -> Self {
match protocol {
ProtocolName::Static(inner) => litep2p::ProtocolName::from(inner),
ProtocolName::OnHeap(inner) => litep2p::ProtocolName::from(inner),
}
}
}
impl From<litep2p::ProtocolName> for ProtocolName {
fn from(protocol: litep2p::ProtocolName) -> Self {
match protocol {
litep2p::ProtocolName::Static(protocol) => ProtocolName::from(protocol),
litep2p::ProtocolName::Allocated(protocol) => ProtocolName::from(protocol),
}
}
}
#[cfg(test)]
mod tests {
use super::ProtocolName;
use std::{
borrow::Borrow,
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
};
#[test]
fn protocol_name_keys_are_equivalent_to_str_keys() {
const PROTOCOL: &'static str = "/some/protocol/1";
let static_protocol_name = ProtocolName::from(PROTOCOL);
let on_heap_protocol_name = ProtocolName::from(String::from(PROTOCOL));
assert_eq!(<ProtocolName as Borrow<str>>::borrow(&static_protocol_name), PROTOCOL);
assert_eq!(<ProtocolName as Borrow<str>>::borrow(&on_heap_protocol_name), PROTOCOL);
assert_eq!(static_protocol_name, on_heap_protocol_name);
assert_eq!(hash(static_protocol_name), hash(PROTOCOL));
assert_eq!(hash(on_heap_protocol_name), hash(PROTOCOL));
}
#[test]
fn different_protocol_names_do_not_compare_equal() {
const PROTOCOL1: &'static str = "/some/protocol/1";
let static_protocol_name1 = ProtocolName::from(PROTOCOL1);
let on_heap_protocol_name1 = ProtocolName::from(String::from(PROTOCOL1));
const PROTOCOL2: &'static str = "/some/protocol/2";
let static_protocol_name2 = ProtocolName::from(PROTOCOL2);
let on_heap_protocol_name2 = ProtocolName::from(String::from(PROTOCOL2));
assert_ne!(<ProtocolName as Borrow<str>>::borrow(&static_protocol_name1), PROTOCOL2);
assert_ne!(<ProtocolName as Borrow<str>>::borrow(&on_heap_protocol_name1), PROTOCOL2);
assert_ne!(static_protocol_name1, static_protocol_name2);
assert_ne!(static_protocol_name1, on_heap_protocol_name2);
assert_ne!(on_heap_protocol_name1, on_heap_protocol_name2);
}
fn hash<T: Hash>(x: T) -> u64 {
let mut hasher = DefaultHasher::new();
x.hash(&mut hasher);
hasher.finish()
}
}
+88
View File
@@ -0,0 +1,88 @@
// 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/>.
//! `sc-network` utilities
use futures::{stream::unfold, FutureExt, Stream, StreamExt};
use futures_timer::Delay;
use linked_hash_set::LinkedHashSet;
use std::{hash::Hash, num::NonZeroUsize, time::Duration};
/// Creates a stream that returns a new value every `duration`.
pub fn interval(duration: Duration) -> impl Stream<Item = ()> + Unpin {
unfold((), move |_| Delay::new(duration).map(|_| Some(((), ())))).map(drop)
}
/// Wrapper around `LinkedHashSet` with bounded growth.
///
/// In the limit, for each element inserted the oldest existing element will be removed.
#[derive(Debug, Clone)]
pub struct LruHashSet<T: Hash + Eq> {
set: LinkedHashSet<T>,
limit: NonZeroUsize,
}
impl<T: Hash + Eq> LruHashSet<T> {
/// Create a new `LruHashSet` with the given (exclusive) limit.
pub fn new(limit: NonZeroUsize) -> Self {
Self { set: LinkedHashSet::new(), limit }
}
/// Insert element into the set.
///
/// Returns `true` if this is a new element to the set, `false` otherwise.
/// Maintains the limit of the set by removing the oldest entry if necessary.
/// Inserting the same element will update its LRU position.
pub fn insert(&mut self, e: T) -> bool {
if self.set.insert(e) {
if self.set.len() == usize::from(self.limit) {
self.set.pop_front(); // remove oldest entry
}
return true;
}
false
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn maintains_limit() {
let three = NonZeroUsize::new(3).unwrap();
let mut set = LruHashSet::<u8>::new(three);
// First element.
assert!(set.insert(1));
assert_eq!(vec![&1], set.set.iter().collect::<Vec<_>>());
// Second element.
assert!(set.insert(2));
assert_eq!(vec![&1, &2], set.set.iter().collect::<Vec<_>>());
// Inserting the same element updates its LRU position.
assert!(!set.insert(1));
assert_eq!(vec![&2, &1], set.set.iter().collect::<Vec<_>>());
// We reached the limit. The next element forces the oldest one out.
assert!(set.insert(3));
assert_eq!(vec![&1, &3], set.set.iter().collect::<Vec<_>>());
}
}
@@ -0,0 +1,45 @@
[package]
description = "Bizinikiwi statement protocol"
name = "pezsc-network-statement"
version = "0.16.0"
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
authors.workspace = true
edition.workspace = true
homepage.workspace = true
repository.workspace = true
documentation = "https://docs.rs/pezsc-network-statement"
[lints]
workspace = true
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
array-bytes = { workspace = true, default-features = true }
async-channel = { workspace = true }
codec = { features = ["derive"], workspace = true, default-features = true }
futures = { workspace = true }
log = { workspace = true, default-features = true }
prometheus-endpoint = { workspace = true, default-features = true }
pezsc-network = { workspace = true, default-features = true }
pezsc-network-common = { workspace = true, default-features = true }
pezsc-network-sync = { workspace = true, default-features = true }
pezsc-network-types = { workspace = true, default-features = true }
pezsp-consensus = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
pezsp-statement-store = { workspace = true, default-features = true }
tokio = { workspace = true }
[dev-dependencies]
async-trait = { workspace = true }
[features]
runtime-benchmarks = [
"pezsc-network-common/runtime-benchmarks",
"pezsc-network-sync/runtime-benchmarks",
"pezsc-network/runtime-benchmarks",
"pezsp-consensus/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
"pezsp-statement-store/runtime-benchmarks",
]
@@ -0,0 +1,33 @@
// 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/>.
//! Configuration of the statement protocol
use std::time;
/// Interval at which we propagate statements;
pub(crate) const PROPAGATE_TIMEOUT: time::Duration = time::Duration::from_millis(1000);
/// Maximum number of known statement hashes to keep for a peer.
pub(crate) const MAX_KNOWN_STATEMENTS: usize = 4 * 1024 * 1024; // * 32 bytes for hash = 128 MB per peer
/// Maximum allowed size for a statement notification.
pub const MAX_STATEMENT_NOTIFICATION_SIZE: u64 = 1024 * 1024;
/// Maximum number of statement validation request we keep at any moment.
pub(crate) const MAX_PENDING_STATEMENTS: usize = 2 * 1024 * 1024;
File diff suppressed because it is too large Load Diff
+73
View File
@@ -0,0 +1,73 @@
[package]
description = "Bizinikiwi sync network protocol"
name = "pezsc-network-sync"
version = "0.33.0"
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
authors.workspace = true
edition.workspace = true
homepage.workspace = true
repository.workspace = true
documentation = "https://docs.rs/pezsc-network-sync"
[lints]
workspace = true
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
array-bytes = { workspace = true, default-features = true }
async-channel = { workspace = true }
async-trait = { workspace = true }
codec = { features = ["derive"], workspace = true, default-features = true }
fork-tree = { workspace = true, default-features = true }
futures = { workspace = true }
log = { workspace = true, default-features = true }
mockall = { workspace = true }
prometheus-endpoint = { workspace = true, default-features = true }
prost = { workspace = true }
pezsc-client-api = { workspace = true, default-features = true }
pezsc-consensus = { workspace = true, default-features = true }
pezsc-network = { workspace = true, default-features = true }
pezsc-network-common = { workspace = true, default-features = true }
pezsc-network-types = { workspace = true, default-features = true }
pezsc-utils = { workspace = true, default-features = true }
schnellru = { workspace = true }
smallvec = { workspace = true, default-features = true }
pezsp-arithmetic = { workspace = true, default-features = true }
pezsp-blockchain = { workspace = true, default-features = true }
pezsp-consensus = { workspace = true, default-features = true }
pezsp-consensus-grandpa = { workspace = true, default-features = true }
pezsp-core = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
thiserror = { workspace = true }
tokio = { features = [
"macros",
"time",
], workspace = true, default-features = true }
tokio-stream = { workspace = true }
[dev-dependencies]
quickcheck = { workspace = true }
pezsc-block-builder = { workspace = true, default-features = true }
pezsp-test-primitives = { workspace = true }
pezsp-tracing = { workspace = true, default-features = true }
bizinikiwi-test-runtime-client = { workspace = true }
[build-dependencies]
prost-build = { workspace = true }
[features]
runtime-benchmarks = [
"pezsc-block-builder/runtime-benchmarks",
"pezsc-client-api/runtime-benchmarks",
"pezsc-consensus/runtime-benchmarks",
"pezsc-network-common/runtime-benchmarks",
"pezsc-network/runtime-benchmarks",
"pezsp-blockchain/runtime-benchmarks",
"pezsp-consensus-grandpa/runtime-benchmarks",
"pezsp-consensus/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
"pezsp-test-primitives/runtime-benchmarks",
"bizinikiwi-test-runtime-client/runtime-benchmarks",
]
+23
View File
@@ -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/>.
const PROTOS: &[&str] = &["src/schema/api.v1.proto"];
fn main() {
prost_build::compile_protos(PROTOS, &["src/schema"]).unwrap();
}
@@ -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)
}
}
}
+44
View File
@@ -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

Some files were not shown because too many files have changed in this diff Show More