feat: Rebrand Polkadot/Substrate references to PezkuwiChain
This commit systematically rebrands various references from Parity Technologies' Polkadot/Substrate ecosystem to PezkuwiChain within the kurdistan-sdk. Key changes include: - Updated external repository URLs (zombienet-sdk, parity-db, parity-scale-codec, wasm-instrument) to point to pezkuwichain forks. - Modified internal documentation and code comments to reflect PezkuwiChain naming and structure. - Replaced direct references to with or specific paths within the for XCM, Pezkuwi, and other modules. - Cleaned up deprecated issue and PR references in various and files, particularly in and modules. - Adjusted image and logo URLs in documentation to point to PezkuwiChain assets. - Removed or rephrased comments related to external Polkadot/Substrate PRs and issues. This is a significant step towards fully customizing the SDK for the PezkuwiChain ecosystem.
This commit is contained in:
@@ -0,0 +1,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/>.
|
||||
|
||||
use super::{config::Config, error::Error, request::Request};
|
||||
use futures::{
|
||||
channel::{mpsc, oneshot},
|
||||
SinkExt,
|
||||
};
|
||||
use pezsp_core::Bytes;
|
||||
use std::future::Future;
|
||||
|
||||
/// The other end of an [`Api`]. This should be passed to [`run`](super::run::run).
|
||||
pub struct ApiBackend {
|
||||
pub(super) request_receiver: mpsc::Receiver<Request>,
|
||||
}
|
||||
|
||||
/// Interface to the mixnet service.
|
||||
#[derive(Clone)]
|
||||
pub struct Api {
|
||||
request_sender: mpsc::Sender<Request>,
|
||||
}
|
||||
|
||||
impl Api {
|
||||
/// Create a new `Api`. The [`ApiBackend`] should be passed to [`run`](super::run::run).
|
||||
pub fn new(config: &Config) -> (Self, ApiBackend) {
|
||||
let (request_sender, request_receiver) = mpsc::channel(config.bizinikiwi.request_buffer);
|
||||
(Self { request_sender }, ApiBackend { request_receiver })
|
||||
}
|
||||
|
||||
/// Submit an extrinsic via the mixnet.
|
||||
///
|
||||
/// Returns a [`Future`] which returns another `Future`.
|
||||
///
|
||||
/// The first `Future` resolves as soon as there is space in the mixnet service queue. The
|
||||
/// second `Future` resolves once a reply is received over the mixnet (or sooner if there is an
|
||||
/// error).
|
||||
///
|
||||
/// The first `Future` references `self`, but the second does not. This makes it possible to
|
||||
/// submit concurrent mixnet requests using a single `Api` instance.
|
||||
pub async fn submit_extrinsic(
|
||||
&mut self,
|
||||
extrinsic: Bytes,
|
||||
) -> impl Future<Output = Result<(), Error>> {
|
||||
let (reply_sender, reply_receiver) = oneshot::channel();
|
||||
let res = self
|
||||
.request_sender
|
||||
.feed(Request::SubmitExtrinsic { extrinsic, reply_sender })
|
||||
.await;
|
||||
async move {
|
||||
res.map_err(|_| Error::ServiceUnavailable)?;
|
||||
reply_receiver.await.map_err(|_| Error::ServiceUnavailable)?
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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/>.
|
||||
|
||||
pub use mixnet::core::Config as CoreConfig;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Bizinikiwi-specific mixnet configuration.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BizinikiwiConfig {
|
||||
/// Attempt to register the local node as a mixnode?
|
||||
pub register: bool,
|
||||
/// Maximum number of incoming mixnet connections to accept from non-mixnodes. If the local
|
||||
/// node will never be a mixnode, this can be set to 0.
|
||||
pub num_gateway_slots: u32,
|
||||
|
||||
/// Number of requests to the mixnet service that can be buffered, in addition to the one per
|
||||
/// [`Api`](super::api::Api) instance. Note that this does not include requests that are being
|
||||
/// actively handled.
|
||||
pub request_buffer: usize,
|
||||
/// Used to determine the number of SURBs to include in request messages: the maximum number of
|
||||
/// SURBs needed for a single reply is multiplied by this. This should not be set to 0.
|
||||
pub surb_factor: usize,
|
||||
|
||||
/// Maximum number of submit extrinsic requests waiting for their delay to elapse. When at the
|
||||
/// limit, any submit extrinsic requests that arrive will simply be dropped.
|
||||
pub extrinsic_queue_capacity: usize,
|
||||
/// Mean delay between receiving a submit extrinsic request and actually submitting the
|
||||
/// extrinsic. This should really be the same for all nodes!
|
||||
pub mean_extrinsic_delay: Duration,
|
||||
/// Maximum number of extrinsics being actively submitted. If a submit extrinsic request's
|
||||
/// delay elapses and we are already at this limit, the request will simply be dropped.
|
||||
pub max_pending_extrinsics: usize,
|
||||
}
|
||||
|
||||
impl Default for BizinikiwiConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
register: true,
|
||||
num_gateway_slots: 150,
|
||||
|
||||
request_buffer: 4,
|
||||
surb_factor: 2,
|
||||
|
||||
extrinsic_queue_capacity: 50,
|
||||
mean_extrinsic_delay: Duration::from_secs(1),
|
||||
max_pending_extrinsics: 20,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Mixnet configuration.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Config {
|
||||
/// Core configuration.
|
||||
pub core: CoreConfig,
|
||||
/// Request manager configuration.
|
||||
pub request_manager: mixnet::request_manager::Config,
|
||||
/// Reply manager configuration.
|
||||
pub reply_manager: mixnet::reply_manager::Config,
|
||||
/// Bizinikiwi-specific configuration.
|
||||
pub bizinikiwi: BizinikiwiConfig,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
core: Default::default(),
|
||||
request_manager: Default::default(),
|
||||
reply_manager: Default::default(),
|
||||
bizinikiwi: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// 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 codec::{Decode, Encode};
|
||||
use mixnet::core::PostErr;
|
||||
|
||||
/// Error handling a request. Sent in replies over the mixnet.
|
||||
#[derive(Debug, thiserror::Error, Decode, Encode)]
|
||||
pub enum RemoteErr {
|
||||
/// An error that doesn't map to any of the other variants.
|
||||
#[error("{0}")]
|
||||
Other(String),
|
||||
/// Failed to decode the request.
|
||||
#[error("Failed to decode the request: {0}")]
|
||||
Decode(String),
|
||||
}
|
||||
|
||||
/// Mixnet error.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// Failed to communicate with the mixnet service. Possibly it panicked. The node probably
|
||||
/// needs to be restarted.
|
||||
#[error(
|
||||
"Failed to communicate with the mixnet service; the node probably needs to be restarted"
|
||||
)]
|
||||
ServiceUnavailable,
|
||||
/// Did not receive a reply after the configured number of attempts.
|
||||
#[error("Did not receive a reply from the mixnet after the configured number of attempts")]
|
||||
NoReply,
|
||||
/// Received a malformed reply.
|
||||
#[error("Received a malformed reply from the mixnet")]
|
||||
BadReply,
|
||||
/// Failed to post the request to the mixnet. Note that some [`PostErr`] variants, eg
|
||||
/// [`PostErr::NotEnoughSpaceInQueue`], are handled internally and will never be returned from
|
||||
/// the top-level API.
|
||||
#[error("Failed to post the request to the mixnet: {0}")]
|
||||
Post(#[from] PostErr),
|
||||
/// Error reported by destination mixnode.
|
||||
#[error("Error reported by the destination mixnode: {0}")]
|
||||
Remote(#[from] RemoteErr),
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
// 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/>.
|
||||
|
||||
//! [`ExtrinsicQueue`] is a queue for extrinsics received from the mixnet. These extrinsics are
|
||||
//! explicitly delayed by a random amount, to decorrelate the times at which they are received from
|
||||
//! the times at which they are broadcast to peers.
|
||||
|
||||
use mixnet::reply_manager::ReplyContext;
|
||||
use std::{cmp::Ordering, collections::BinaryHeap, time::Instant};
|
||||
|
||||
/// An extrinsic that should be submitted to the transaction pool after `deadline`. `Eq` and `Ord`
|
||||
/// are implemented for this to support use in `BinaryHeap`s. Only `deadline` is compared.
|
||||
struct DelayedExtrinsic<E> {
|
||||
/// When the extrinsic should actually be submitted to the pool.
|
||||
deadline: Instant,
|
||||
extrinsic: E,
|
||||
reply_context: ReplyContext,
|
||||
}
|
||||
|
||||
impl<E> PartialEq for DelayedExtrinsic<E> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.deadline == other.deadline
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> Eq for DelayedExtrinsic<E> {}
|
||||
|
||||
impl<E> PartialOrd for DelayedExtrinsic<E> {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> Ord for DelayedExtrinsic<E> {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
// Extrinsics with the earliest deadline considered greatest
|
||||
self.deadline.cmp(&other.deadline).reverse()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ExtrinsicQueue<E> {
|
||||
capacity: usize,
|
||||
queue: BinaryHeap<DelayedExtrinsic<E>>,
|
||||
next_deadline_changed: bool,
|
||||
}
|
||||
|
||||
impl<E> ExtrinsicQueue<E> {
|
||||
pub fn new(capacity: usize) -> Self {
|
||||
Self { capacity, queue: BinaryHeap::with_capacity(capacity), next_deadline_changed: false }
|
||||
}
|
||||
|
||||
pub fn next_deadline(&self) -> Option<Instant> {
|
||||
self.queue.peek().map(|extrinsic| extrinsic.deadline)
|
||||
}
|
||||
|
||||
pub fn next_deadline_changed(&mut self) -> bool {
|
||||
let changed = self.next_deadline_changed;
|
||||
self.next_deadline_changed = false;
|
||||
changed
|
||||
}
|
||||
|
||||
pub fn has_space(&self) -> bool {
|
||||
self.queue.len() < self.capacity
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, deadline: Instant, extrinsic: E, reply_context: ReplyContext) {
|
||||
debug_assert!(self.has_space());
|
||||
let prev_deadline = self.next_deadline();
|
||||
self.queue.push(DelayedExtrinsic { deadline, extrinsic, reply_context });
|
||||
if self.next_deadline() != prev_deadline {
|
||||
self.next_deadline_changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pop(&mut self) -> Option<(E, ReplyContext)> {
|
||||
self.next_deadline_changed = true;
|
||||
self.queue.pop().map(|extrinsic| (extrinsic.extrinsic, extrinsic.reply_context))
|
||||
}
|
||||
}
|
||||
@@ -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/>.
|
||||
|
||||
//! Bizinikiwi mixnet service. This implements the [Bizinikiwi Mix Network
|
||||
//! Specification](https://paritytech.github.io/mixnet-spec/).
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
mod api;
|
||||
mod config;
|
||||
mod error;
|
||||
mod extrinsic_queue;
|
||||
mod maybe_inf_delay;
|
||||
mod packet_dispatcher;
|
||||
mod peer_id;
|
||||
mod protocol;
|
||||
mod request;
|
||||
mod run;
|
||||
mod sync_with_runtime;
|
||||
|
||||
pub use self::{
|
||||
api::{Api, ApiBackend},
|
||||
config::{Config, CoreConfig, BizinikiwiConfig},
|
||||
error::{Error, RemoteErr},
|
||||
protocol::{peers_set_config, protocol_name},
|
||||
run::run,
|
||||
};
|
||||
pub use mixnet::core::{KxSecret, PostErr, TopologyErr};
|
||||
@@ -0,0 +1,111 @@
|
||||
// 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::{future::FusedFuture, FutureExt};
|
||||
use futures_timer::Delay;
|
||||
use std::{
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
task::{Context, Poll, Waker},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
enum Inner {
|
||||
Infinite {
|
||||
/// Waker from the most recent `poll` call. If `None`, either `poll` has not been called
|
||||
/// yet, we returned `Poll::Ready` from the last call, or the waker is attached to `delay`.
|
||||
waker: Option<Waker>,
|
||||
delay: Option<Delay>,
|
||||
},
|
||||
Finite(Delay),
|
||||
}
|
||||
|
||||
/// Like [`Delay`] but the duration can be infinite (in which case the future will never fire).
|
||||
/// Unlike [`Delay`], implements [`FusedFuture`], with [`is_terminated`](Self::is_terminated)
|
||||
/// returning `true` when the delay is infinite. As with [`Delay`], once [`poll`](Self::poll)
|
||||
/// returns [`Poll::Ready`], it will continue to do so until [`reset`](Self::reset) is called.
|
||||
pub struct MaybeInfDelay(Inner);
|
||||
|
||||
impl MaybeInfDelay {
|
||||
/// Create a new `MaybeInfDelay` future. If `duration` is [`Some`], the future will fire after
|
||||
/// the given duration has elapsed. If `duration` is [`None`], the future will "never" fire
|
||||
/// (although see [`reset`](Self::reset)).
|
||||
pub fn new(duration: Option<Duration>) -> Self {
|
||||
match duration {
|
||||
Some(duration) => Self(Inner::Finite(Delay::new(duration))),
|
||||
None => Self(Inner::Infinite { waker: None, delay: None }),
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset the timer. `duration` is handled just like in [`new`](Self::new). Note that while
|
||||
/// this is similar to `std::mem::replace(&mut self, MaybeInfDelay::new(duration))`, with
|
||||
/// `replace` you would have to manually ensure [`poll`](Self::poll) was called again; with
|
||||
/// `reset` this is not necessary.
|
||||
pub fn reset(&mut self, duration: Option<Duration>) {
|
||||
match duration {
|
||||
Some(duration) => match &mut self.0 {
|
||||
Inner::Infinite { waker, delay } => {
|
||||
let mut delay = match delay.take() {
|
||||
Some(mut delay) => {
|
||||
delay.reset(duration);
|
||||
delay
|
||||
},
|
||||
None => Delay::new(duration),
|
||||
};
|
||||
if let Some(waker) = waker.take() {
|
||||
let mut cx = Context::from_waker(&waker);
|
||||
match delay.poll_unpin(&mut cx) {
|
||||
Poll::Pending => (), // Waker attached to delay
|
||||
Poll::Ready(_) => waker.wake(),
|
||||
}
|
||||
}
|
||||
self.0 = Inner::Finite(delay);
|
||||
},
|
||||
Inner::Finite(delay) => delay.reset(duration),
|
||||
},
|
||||
None =>
|
||||
self.0 = match std::mem::replace(
|
||||
&mut self.0,
|
||||
Inner::Infinite { waker: None, delay: None },
|
||||
) {
|
||||
Inner::Finite(delay) => Inner::Infinite { waker: None, delay: Some(delay) },
|
||||
infinite => infinite,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Future for MaybeInfDelay {
|
||||
type Output = ();
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
match &mut self.0 {
|
||||
Inner::Infinite { waker, .. } => {
|
||||
*waker = Some(cx.waker().clone());
|
||||
Poll::Pending
|
||||
},
|
||||
Inner::Finite(delay) => delay.poll_unpin(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FusedFuture for MaybeInfDelay {
|
||||
fn is_terminated(&self) -> bool {
|
||||
matches!(self.0, Inner::Infinite { .. })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
// 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/>.
|
||||
|
||||
//! [`AddressedPacket`] dispatching.
|
||||
|
||||
use super::peer_id::{from_core_peer_id, to_core_peer_id};
|
||||
use arrayvec::ArrayVec;
|
||||
use log::{debug, warn};
|
||||
use mixnet::core::{AddressedPacket, NetworkStatus, Packet, PeerId as CorePeerId};
|
||||
use parking_lot::Mutex;
|
||||
use pezsc_network::NotificationService;
|
||||
use pezsc_network_types::PeerId;
|
||||
use std::{collections::HashMap, future::Future, sync::Arc};
|
||||
|
||||
const LOG_TARGET: &str = "mixnet";
|
||||
|
||||
/// Packet queue for a peer.
|
||||
///
|
||||
/// Ideally we would use `Rc<RefCell<_>>`, but that would prevent the top-level future from being
|
||||
/// automatically marked `Send`. I believe it would be safe to manually mark it `Send`, but using
|
||||
/// `Arc<Mutex<_>>` here is not really a big deal.
|
||||
struct PeerQueue(Mutex<ArrayVec<Box<Packet>, 2>>);
|
||||
|
||||
impl PeerQueue {
|
||||
fn new() -> Self {
|
||||
Self(Mutex::new(ArrayVec::new()))
|
||||
}
|
||||
|
||||
/// Push `packet` onto the queue. Returns `true` if the queue was previously empty. Fails if
|
||||
/// the queue is full.
|
||||
fn push(&self, packet: Box<Packet>) -> Result<bool, ()> {
|
||||
let mut queue = self.0.lock();
|
||||
if queue.is_full() {
|
||||
Err(())
|
||||
} else {
|
||||
let was_empty = queue.is_empty();
|
||||
queue.push(packet);
|
||||
Ok(was_empty)
|
||||
}
|
||||
}
|
||||
|
||||
/// Drop all packets from the queue.
|
||||
fn clear(&self) {
|
||||
let mut queue = self.0.lock();
|
||||
queue.clear();
|
||||
}
|
||||
|
||||
/// Pop the packet at the head of the queue and return it, or, if the queue is empty, return
|
||||
/// `None`. Also returns `true` if there are more packets in the queue.
|
||||
fn pop(&self) -> (Option<Box<Packet>>, bool) {
|
||||
let mut queue = self.0.lock();
|
||||
let packet = queue.pop();
|
||||
(packet, !queue.is_empty())
|
||||
}
|
||||
}
|
||||
|
||||
/// A peer which has packets ready to send but is not currently being serviced.
|
||||
pub struct ReadyPeer {
|
||||
id: PeerId,
|
||||
/// The peer's packet queue. Not empty.
|
||||
queue: Arc<PeerQueue>,
|
||||
}
|
||||
|
||||
impl ReadyPeer {
|
||||
/// If a future is returned, and if that future returns `Some`, this function should be
|
||||
/// called again to send the next packet queued for the peer; `self` is placed in the `Some`
|
||||
/// to make this straightforward. Otherwise, we have either sent or dropped all packets
|
||||
/// queued for the peer, and it can be forgotten about for the time being.
|
||||
pub fn send_packet(
|
||||
self,
|
||||
notification_service: &Box<dyn NotificationService>,
|
||||
) -> Option<impl Future<Output = Option<Self>>> {
|
||||
match notification_service.message_sink(&self.id) {
|
||||
None => {
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"Failed to get message sink for peer ID {}", self.id,
|
||||
);
|
||||
self.queue.clear();
|
||||
None
|
||||
},
|
||||
Some(sink) => Some(async move {
|
||||
let (packet, more_packets) = self.queue.pop();
|
||||
let packet = packet.expect("Should only be called if there is a packet to send");
|
||||
|
||||
match sink.send_async_notification((packet as Box<[_]>).into()).await {
|
||||
Ok(_) => more_packets.then_some(self),
|
||||
Err(err) => {
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"Failed to send packet to peer ID {}: {err}", self.id,
|
||||
);
|
||||
self.queue.clear();
|
||||
None
|
||||
},
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PacketDispatcher {
|
||||
/// Peer ID of the local node. Only used to implement [`NetworkStatus`].
|
||||
local_peer_id: CorePeerId,
|
||||
/// Packet queue for each connected peer. These queues are very short and only exist to give
|
||||
/// packets somewhere to sit while waiting for notification senders to be ready.
|
||||
peer_queues: HashMap<CorePeerId, Arc<PeerQueue>>,
|
||||
}
|
||||
|
||||
impl PacketDispatcher {
|
||||
pub fn new(local_peer_id: &CorePeerId) -> Self {
|
||||
Self { local_peer_id: *local_peer_id, peer_queues: HashMap::new() }
|
||||
}
|
||||
|
||||
pub fn add_peer(&mut self, id: &PeerId) {
|
||||
let Some(core_id) = to_core_peer_id(id) else {
|
||||
debug!(target: LOG_TARGET,
|
||||
"Cannot add peer; failed to convert libp2p peer ID {id} to mixnet peer ID");
|
||||
return;
|
||||
};
|
||||
if self.peer_queues.insert(core_id, Arc::new(PeerQueue::new())).is_some() {
|
||||
warn!(target: LOG_TARGET, "Two stream opened notifications for peer ID {id}");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_peer(&mut self, id: &PeerId) {
|
||||
let Some(core_id) = to_core_peer_id(id) else {
|
||||
debug!(target: LOG_TARGET,
|
||||
"Cannot remove peer; failed to convert libp2p peer ID {id} to mixnet peer ID");
|
||||
return;
|
||||
};
|
||||
if self.peer_queues.remove(&core_id).is_none() {
|
||||
warn!(target: LOG_TARGET, "Stream closed notification for unknown peer ID {id}");
|
||||
}
|
||||
}
|
||||
|
||||
/// If the peer is not connected or the peer's packet queue is full, the packet is dropped.
|
||||
/// Otherwise the packet is pushed onto the peer's queue, and if the queue was previously empty
|
||||
/// a [`ReadyPeer`] is returned.
|
||||
pub fn dispatch(&mut self, packet: AddressedPacket) -> Option<ReadyPeer> {
|
||||
let Some(queue) = self.peer_queues.get_mut(&packet.peer_id) else {
|
||||
debug!(target: LOG_TARGET, "Dropped packet to mixnet peer ID {:x?}; not connected",
|
||||
packet.peer_id);
|
||||
return None;
|
||||
};
|
||||
|
||||
match queue.push(packet.packet) {
|
||||
Err(_) => {
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"Dropped packet to mixnet peer ID {:x?}; peer queue full", packet.peer_id
|
||||
);
|
||||
None
|
||||
},
|
||||
Ok(true) => {
|
||||
// Queue was empty. Construct and return a ReadyPeer.
|
||||
let Some(id) = from_core_peer_id(&packet.peer_id) else {
|
||||
debug!(target: LOG_TARGET, "Cannot send packet; \
|
||||
failed to convert mixnet peer ID {:x?} to libp2p peer ID",
|
||||
packet.peer_id);
|
||||
queue.clear();
|
||||
return None;
|
||||
};
|
||||
Some(ReadyPeer { id, queue: queue.clone() })
|
||||
},
|
||||
Ok(false) => None, // Queue was not empty
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkStatus for PacketDispatcher {
|
||||
fn local_peer_id(&self) -> CorePeerId {
|
||||
self.local_peer_id
|
||||
}
|
||||
|
||||
fn is_connected(&self, peer_id: &CorePeerId) -> bool {
|
||||
self.peer_queues.contains_key(peer_id)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// 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 mixnet::core::PeerId as CorePeerId;
|
||||
use pezsc_network_types::PeerId;
|
||||
|
||||
/// Convert a libp2p [`PeerId`] into a mixnet core [`PeerId`](CorePeerId).
|
||||
///
|
||||
/// This will succeed only if `peer_id` is an Ed25519 public key ("hashed" using the identity
|
||||
/// hasher). Returns `None` on failure.
|
||||
pub fn to_core_peer_id(peer_id: &PeerId) -> Option<CorePeerId> {
|
||||
peer_id.into_ed25519()
|
||||
}
|
||||
|
||||
/// Convert a mixnet core [`PeerId`](CorePeerId) into a libp2p [`PeerId`].
|
||||
///
|
||||
/// This will succeed only if `peer_id` represents a point on the Ed25519 curve. Returns `None` on
|
||||
/// failure.
|
||||
pub fn from_core_peer_id(core_peer_id: &CorePeerId) -> Option<PeerId> {
|
||||
PeerId::from_ed25519(core_peer_id)
|
||||
}
|
||||
@@ -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/>.
|
||||
|
||||
use super::config::Config;
|
||||
use mixnet::core::PACKET_SIZE;
|
||||
use pezsc_network::{
|
||||
config::{NonReservedPeerMode, SetConfig},
|
||||
peer_store::PeerStoreProvider,
|
||||
service::NotificationMetrics,
|
||||
NetworkBackend, NotificationService, ProtocolName,
|
||||
};
|
||||
use pezsp_runtime::traits::Block as BlockT;
|
||||
|
||||
/// Returns the protocol name to use for the mixnet controlled by the given chain.
|
||||
pub fn protocol_name(genesis_hash: &[u8], fork_id: Option<&str>) -> ProtocolName {
|
||||
let name = if let Some(fork_id) = fork_id {
|
||||
format!("/{}/{}/mixnet/1", array_bytes::bytes2hex("", genesis_hash), fork_id)
|
||||
} else {
|
||||
format!("/{}/mixnet/1", array_bytes::bytes2hex("", genesis_hash))
|
||||
};
|
||||
name.into()
|
||||
}
|
||||
|
||||
/// Returns the peers set configuration for the mixnet protocol.
|
||||
pub fn peers_set_config<Block: BlockT, Network: NetworkBackend<Block, <Block as BlockT>::Hash>>(
|
||||
name: ProtocolName,
|
||||
config: &Config,
|
||||
metrics: NotificationMetrics,
|
||||
peerstore_handle: std::sync::Arc<dyn PeerStoreProvider>,
|
||||
) -> (Network::NotificationProtocolConfig, Box<dyn NotificationService>) {
|
||||
let set_config = if config.bizinikiwi.num_gateway_slots != 0 {
|
||||
// out_peers is always 0; we are only interested in connecting to mixnodes, which we do by
|
||||
// setting them as reserved nodes
|
||||
SetConfig {
|
||||
in_peers: config.bizinikiwi.num_gateway_slots,
|
||||
out_peers: 0,
|
||||
reserved_nodes: Vec::new(),
|
||||
non_reserved_mode: NonReservedPeerMode::Accept,
|
||||
}
|
||||
} else {
|
||||
SetConfig {
|
||||
in_peers: 0,
|
||||
out_peers: 0,
|
||||
reserved_nodes: Vec::new(),
|
||||
non_reserved_mode: NonReservedPeerMode::Deny,
|
||||
}
|
||||
};
|
||||
|
||||
Network::notification_config(
|
||||
name,
|
||||
Vec::new(),
|
||||
PACKET_SIZE as u64,
|
||||
None,
|
||||
set_config,
|
||||
metrics,
|
||||
peerstore_handle,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
// 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/>.
|
||||
|
||||
//! Sender-side request logic. Some things from this module are also used on the receiver side, eg
|
||||
//! [`extrinsic_delay`], but most of the receiver-side request logic lives elsewhere.
|
||||
|
||||
use super::{config::BizinikiwiConfig, error::Error};
|
||||
use blake2::{
|
||||
digest::{consts::U16, Mac},
|
||||
Blake2bMac,
|
||||
};
|
||||
use codec::{Decode, DecodeAll};
|
||||
use futures::channel::oneshot;
|
||||
use log::debug;
|
||||
use mixnet::core::{Delay, MessageId, PostErr, Scattered};
|
||||
use pezsp_core::Bytes;
|
||||
use std::time::Duration;
|
||||
|
||||
const LOG_TARGET: &str = "mixnet";
|
||||
|
||||
fn send_err<T>(reply_sender: oneshot::Sender<Result<T, Error>>, err: Error) {
|
||||
if let Err(Err(err)) = reply_sender.send(Err(err)) {
|
||||
debug!(target: LOG_TARGET, "Failed to inform requester of error: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
fn send_reply<T: Decode>(reply_sender: oneshot::Sender<Result<T, Error>>, mut data: &[u8]) {
|
||||
let res = match Result::decode_all(&mut data) {
|
||||
Ok(res) => res.map_err(Error::Remote),
|
||||
Err(_) => Err(Error::BadReply),
|
||||
};
|
||||
match reply_sender.send(res) {
|
||||
Ok(_) => (),
|
||||
Err(Ok(_)) => debug!(target: LOG_TARGET, "Failed to send reply to requester"),
|
||||
Err(Err(err)) => debug!(target: LOG_TARGET, "Failed to inform requester of error: {err}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// First byte of a submit extrinsic request, identifying it as such.
|
||||
pub const SUBMIT_EXTRINSIC: u8 = 1;
|
||||
|
||||
const EXTRINSIC_DELAY_PERSONA: &[u8; 16] = b"submit-extrn-dly";
|
||||
|
||||
/// Returns the artificial delay that should be inserted between receipt of a submit extrinsic
|
||||
/// request with the given message ID and import of the extrinsic into the local transaction pool.
|
||||
pub fn extrinsic_delay(message_id: &MessageId, config: &BizinikiwiConfig) -> Duration {
|
||||
let h = Blake2bMac::<U16>::new_with_salt_and_personal(message_id, b"", EXTRINSIC_DELAY_PERSONA)
|
||||
.expect("Key, salt, and persona sizes are fixed and small enough");
|
||||
let delay = Delay::exp(h.finalize().into_bytes().as_ref());
|
||||
delay.to_duration(config.mean_extrinsic_delay)
|
||||
}
|
||||
|
||||
/// Request parameters and local reply channel. Stored by the
|
||||
/// [`RequestManager`](mixnet::request_manager::RequestManager).
|
||||
pub enum Request {
|
||||
SubmitExtrinsic { extrinsic: Bytes, reply_sender: oneshot::Sender<Result<(), Error>> },
|
||||
}
|
||||
|
||||
impl Request {
|
||||
/// Forward an error to the user of the mixnet service.
|
||||
fn send_err(self, err: Error) {
|
||||
match self {
|
||||
Request::SubmitExtrinsic { reply_sender, .. } => send_err(reply_sender, err),
|
||||
}
|
||||
}
|
||||
|
||||
/// Forward a reply to the user of the mixnet service.
|
||||
pub fn send_reply(self, data: &[u8]) {
|
||||
match self {
|
||||
Request::SubmitExtrinsic { reply_sender, .. } => send_reply(reply_sender, data),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl mixnet::request_manager::Request for Request {
|
||||
type Context = BizinikiwiConfig;
|
||||
|
||||
fn with_data<T>(&self, f: impl FnOnce(Scattered<u8>) -> T, _context: &Self::Context) -> T {
|
||||
match self {
|
||||
Request::SubmitExtrinsic { extrinsic, .. } =>
|
||||
f([&[SUBMIT_EXTRINSIC][..], extrinsic.0.as_slice()].as_slice().into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn num_surbs(&self, context: &Self::Context) -> usize {
|
||||
match self {
|
||||
Request::SubmitExtrinsic { .. } => context.surb_factor,
|
||||
}
|
||||
}
|
||||
|
||||
fn handling_delay(&self, message_id: &MessageId, context: &Self::Context) -> Duration {
|
||||
match self {
|
||||
Request::SubmitExtrinsic { .. } => extrinsic_delay(message_id, context),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_post_err(self, err: PostErr, _context: &Self::Context) {
|
||||
self.send_err(err.into());
|
||||
}
|
||||
|
||||
fn handle_retry_limit_reached(self, _context: &Self::Context) {
|
||||
self.send_err(Error::NoReply);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
// 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/>.
|
||||
|
||||
//! Top-level mixnet service function.
|
||||
|
||||
use super::{
|
||||
api::ApiBackend,
|
||||
config::{Config, BizinikiwiConfig},
|
||||
error::RemoteErr,
|
||||
extrinsic_queue::ExtrinsicQueue,
|
||||
maybe_inf_delay::MaybeInfDelay,
|
||||
packet_dispatcher::PacketDispatcher,
|
||||
peer_id::to_core_peer_id,
|
||||
request::{extrinsic_delay, Request, SUBMIT_EXTRINSIC},
|
||||
sync_with_runtime::sync_with_runtime,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use codec::{Decode, DecodeAll, Encode};
|
||||
use futures::{
|
||||
future::{pending, Either},
|
||||
stream::FuturesUnordered,
|
||||
FutureExt, StreamExt,
|
||||
};
|
||||
use log::{debug, error, trace, warn};
|
||||
use mixnet::{
|
||||
core::{Events, Message, Mixnet, Packet},
|
||||
reply_manager::{ReplyContext, ReplyManager},
|
||||
request_manager::RequestManager,
|
||||
};
|
||||
use pezsc_client_api::{BlockchainEvents, HeaderBackend};
|
||||
use pezsc_network::{
|
||||
service::traits::{NetworkService, NotificationEvent, ValidationResult},
|
||||
NetworkPeers, NetworkStateInfo, NotificationService, ProtocolName,
|
||||
};
|
||||
use pezsc_transaction_pool_api::{
|
||||
LocalTransactionPool, OffchainTransactionPoolFactory, TransactionPool,
|
||||
};
|
||||
use pezsp_api::{ApiExt, ProvideRuntimeApi};
|
||||
use pezsp_consensus::SyncOracle;
|
||||
use pezsp_keystore::{KeystoreExt, KeystorePtr};
|
||||
use pezsp_mixnet::{runtime_api::MixnetApi, types::Mixnode};
|
||||
use pezsp_runtime::{
|
||||
traits::{Block, Header},
|
||||
transaction_validity::TransactionSource,
|
||||
Saturating,
|
||||
};
|
||||
use std::{
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
const LOG_TARGET: &str = "mixnet";
|
||||
|
||||
const MIN_BLOCKS_BETWEEN_REGISTRATION_ATTEMPTS: u32 = 3;
|
||||
|
||||
fn complete_submit_extrinsic<X>(
|
||||
reply_manager: &mut ReplyManager,
|
||||
reply_context: ReplyContext,
|
||||
data: Result<(), RemoteErr>,
|
||||
mixnet: &mut Mixnet<X>,
|
||||
) {
|
||||
reply_manager.complete(reply_context, data.encode(), mixnet);
|
||||
}
|
||||
|
||||
fn handle_packet<X, E: Decode>(
|
||||
packet: &Packet,
|
||||
mixnet: &mut Mixnet<X>,
|
||||
request_manager: &mut RequestManager<Request>,
|
||||
reply_manager: &mut ReplyManager,
|
||||
extrinsic_queue: &mut ExtrinsicQueue<E>,
|
||||
config: &BizinikiwiConfig,
|
||||
) {
|
||||
match mixnet.handle_packet(packet) {
|
||||
Some(Message::Request(message)) => {
|
||||
let Some((reply_context, data)) = reply_manager.insert(message, mixnet) else { return };
|
||||
|
||||
match data.as_slice() {
|
||||
[SUBMIT_EXTRINSIC, encoded_extrinsic @ ..] => {
|
||||
if !extrinsic_queue.has_space() {
|
||||
debug!(target: LOG_TARGET, "No space in extrinsic queue; dropping request");
|
||||
// We don't send a reply in this case; we want the requester to retry
|
||||
reply_manager.abandon(reply_context);
|
||||
return;
|
||||
}
|
||||
|
||||
// Decode the extrinsic
|
||||
let mut encoded_extrinsic = encoded_extrinsic;
|
||||
let extrinsic = match E::decode_all(&mut encoded_extrinsic) {
|
||||
Ok(extrinsic) => extrinsic,
|
||||
Err(err) => {
|
||||
complete_submit_extrinsic(
|
||||
reply_manager,
|
||||
reply_context,
|
||||
Err(RemoteErr::Decode(format!("Bad extrinsic: {}", err))),
|
||||
mixnet,
|
||||
);
|
||||
return;
|
||||
},
|
||||
};
|
||||
|
||||
let deadline =
|
||||
Instant::now() + extrinsic_delay(reply_context.message_id(), config);
|
||||
extrinsic_queue.insert(deadline, extrinsic, reply_context);
|
||||
},
|
||||
_ => {
|
||||
debug!(target: LOG_TARGET, "Unrecognised request; discarding");
|
||||
// To keep things simple we don't bother sending a reply in this case. The
|
||||
// requester will give up and try another mixnode eventually.
|
||||
reply_manager.abandon(reply_context);
|
||||
},
|
||||
}
|
||||
},
|
||||
Some(Message::Reply(message)) => {
|
||||
let Some(request) = request_manager.remove(&message.request_id) else {
|
||||
trace!(
|
||||
target: LOG_TARGET,
|
||||
"Received reply to already-completed request with message ID {:x?}",
|
||||
message.request_id
|
||||
);
|
||||
return;
|
||||
};
|
||||
request.send_reply(&message.data);
|
||||
},
|
||||
None => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn time_until(instant: Instant) -> Duration {
|
||||
instant.saturating_duration_since(Instant::now())
|
||||
}
|
||||
|
||||
/// Run the mixnet service. If `keystore` is `None`, the service will not attempt to register the
|
||||
/// local node as a mixnode, even if `config.register` is `true`.
|
||||
pub async fn run<B, C, S, P>(
|
||||
config: Config,
|
||||
mut api_backend: ApiBackend,
|
||||
client: Arc<C>,
|
||||
sync: Arc<S>,
|
||||
network: Arc<dyn NetworkService>,
|
||||
protocol_name: ProtocolName,
|
||||
transaction_pool: Arc<P>,
|
||||
keystore: Option<KeystorePtr>,
|
||||
mut notification_service: Box<dyn NotificationService>,
|
||||
) where
|
||||
B: Block,
|
||||
C: BlockchainEvents<B> + ProvideRuntimeApi<B> + HeaderBackend<B>,
|
||||
C::Api: MixnetApi<B>,
|
||||
S: SyncOracle,
|
||||
P: TransactionPool<Block = B> + LocalTransactionPool<Block = B> + 'static,
|
||||
{
|
||||
let local_peer_id = network.local_peer_id();
|
||||
let Some(local_peer_id) = to_core_peer_id(&local_peer_id) else {
|
||||
error!(target: LOG_TARGET,
|
||||
"Failed to convert libp2p local peer ID {local_peer_id} to mixnet peer ID; \
|
||||
mixnet not running");
|
||||
return;
|
||||
};
|
||||
|
||||
let offchain_transaction_pool_factory =
|
||||
OffchainTransactionPoolFactory::new(transaction_pool.clone());
|
||||
|
||||
let mut mixnet = Mixnet::new(config.core);
|
||||
// It would make sense to reset this to 0 when the session changes, but registrations aren't
|
||||
// allowed at the start of a session anyway, so it doesn't really matter
|
||||
let mut min_register_block = 0u32.into();
|
||||
let mut packet_dispatcher = PacketDispatcher::new(&local_peer_id);
|
||||
let mut request_manager = RequestManager::new(config.request_manager);
|
||||
let mut reply_manager = ReplyManager::new(config.reply_manager);
|
||||
let mut extrinsic_queue = ExtrinsicQueue::new(config.bizinikiwi.extrinsic_queue_capacity);
|
||||
|
||||
let mut finality_notifications = client.finality_notification_stream();
|
||||
// Import notifications only used for triggering registration attempts
|
||||
let mut import_notifications = if config.bizinikiwi.register && keystore.is_some() {
|
||||
Some(client.import_notification_stream())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut next_forward_packet_delay = MaybeInfDelay::new(None);
|
||||
let mut next_authored_packet_delay = MaybeInfDelay::new(None);
|
||||
let mut ready_peers = FuturesUnordered::new();
|
||||
let mut next_retry_delay = MaybeInfDelay::new(None);
|
||||
let mut next_extrinsic_delay = MaybeInfDelay::new(None);
|
||||
let mut submit_extrinsic_results = FuturesUnordered::new();
|
||||
|
||||
loop {
|
||||
let mut next_request = if request_manager.has_space() {
|
||||
Either::Left(api_backend.request_receiver.select_next_some())
|
||||
} else {
|
||||
Either::Right(pending())
|
||||
};
|
||||
|
||||
let mut next_import_notification = import_notifications.as_mut().map_or_else(
|
||||
|| Either::Right(pending()),
|
||||
|notifications| Either::Left(notifications.select_next_some()),
|
||||
);
|
||||
|
||||
futures::select! {
|
||||
request = next_request =>
|
||||
request_manager.insert(request, &mut mixnet, &packet_dispatcher, &config.bizinikiwi),
|
||||
|
||||
notification = finality_notifications.select_next_some() => {
|
||||
// To avoid trying to connect to old mixnodes, ignore finality notifications while
|
||||
// offline or major syncing. This is a bit racy but should be good enough.
|
||||
if !sync.is_offline() && !sync.is_major_syncing() {
|
||||
let api = client.runtime_api();
|
||||
sync_with_runtime(&mut mixnet, api, notification.hash);
|
||||
request_manager.update_session_status(
|
||||
&mut mixnet, &packet_dispatcher, &config.bizinikiwi);
|
||||
}
|
||||
}
|
||||
|
||||
notification = next_import_notification => {
|
||||
if notification.is_new_best && (*notification.header.number() >= min_register_block) {
|
||||
let mut api = client.runtime_api();
|
||||
api.register_extension(KeystoreExt(keystore.clone().expect(
|
||||
"Import notification stream only setup if we have a keystore")));
|
||||
api.register_extension(offchain_transaction_pool_factory
|
||||
.offchain_transaction_pool(notification.hash));
|
||||
let session_index = mixnet.session_status().current_index;
|
||||
let mixnode = Mixnode {
|
||||
kx_public: *mixnet.next_kx_public(),
|
||||
peer_id: local_peer_id,
|
||||
external_addresses: network.external_addresses().into_iter()
|
||||
.map(|addr| addr.to_string().into_bytes()).collect(),
|
||||
};
|
||||
match api.maybe_register(notification.hash, session_index, mixnode) {
|
||||
Ok(true) => min_register_block = notification.header.number().saturating_add(
|
||||
MIN_BLOCKS_BETWEEN_REGISTRATION_ATTEMPTS.into()),
|
||||
Ok(false) => (),
|
||||
Err(err) => debug!(target: LOG_TARGET,
|
||||
"Error trying to register for the next session: {err}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
event = notification_service.next_event().fuse() => match event {
|
||||
None => todo!(),
|
||||
Some(NotificationEvent::ValidateInboundSubstream { result_tx, .. }) => {
|
||||
let _ = result_tx.send(ValidationResult::Accept);
|
||||
},
|
||||
Some(NotificationEvent::NotificationStreamOpened { peer, .. }) => {
|
||||
packet_dispatcher.add_peer(&peer);
|
||||
},
|
||||
Some(NotificationEvent::NotificationStreamClosed { peer }) => {
|
||||
packet_dispatcher.remove_peer(&peer);
|
||||
},
|
||||
Some(NotificationEvent::NotificationReceived { peer, notification }) => {
|
||||
let notification: Bytes = notification.into();
|
||||
|
||||
match notification.as_ref().try_into() {
|
||||
Ok(packet) => handle_packet(packet,
|
||||
&mut mixnet, &mut request_manager, &mut reply_manager,
|
||||
&mut extrinsic_queue, &config.bizinikiwi),
|
||||
Err(_) => debug!(target: LOG_TARGET,
|
||||
"Dropped incorrectly sized packet ({} bytes) from {peer}",
|
||||
notification.len(),
|
||||
),
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
_ = next_forward_packet_delay => {
|
||||
if let Some(packet) = mixnet.pop_next_forward_packet() {
|
||||
if let Some(ready_peer) = packet_dispatcher.dispatch(packet) {
|
||||
if let Some(fut) = ready_peer.send_packet(¬ification_service) {
|
||||
ready_peers.push(fut);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!(target: LOG_TARGET,
|
||||
"Next forward packet deadline reached, but no packet in queue; \
|
||||
this is a bug");
|
||||
}
|
||||
}
|
||||
|
||||
_ = next_authored_packet_delay => {
|
||||
if let Some(packet) = mixnet.pop_next_authored_packet(&packet_dispatcher) {
|
||||
if let Some(ready_peer) = packet_dispatcher.dispatch(packet) {
|
||||
if let Some(fut) = ready_peer.send_packet(¬ification_service) {
|
||||
ready_peers.push(fut);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ready_peer = ready_peers.select_next_some() => {
|
||||
if let Some(ready_peer) = ready_peer {
|
||||
if let Some(fut) = ready_peer.send_packet(¬ification_service) {
|
||||
ready_peers.push(fut);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ = next_retry_delay => {
|
||||
if !request_manager.pop_next_retry(&mut mixnet, &packet_dispatcher, &config.bizinikiwi) {
|
||||
warn!(target: LOG_TARGET,
|
||||
"Next retry deadline reached, but no request in retry queue; \
|
||||
this is a bug");
|
||||
}
|
||||
}
|
||||
|
||||
_ = next_extrinsic_delay => {
|
||||
if let Some((extrinsic, reply_context)) = extrinsic_queue.pop() {
|
||||
if submit_extrinsic_results.len() < config.bizinikiwi.max_pending_extrinsics {
|
||||
let fut = transaction_pool.submit_one(
|
||||
client.info().best_hash,
|
||||
TransactionSource::External,
|
||||
extrinsic);
|
||||
submit_extrinsic_results.push(async move {
|
||||
(fut.await, reply_context)
|
||||
});
|
||||
} else {
|
||||
// There are already too many pending extrinsics, just drop this one. We
|
||||
// don't send a reply; we want the requester to retry.
|
||||
debug!(target: LOG_TARGET,
|
||||
"Too many pending extrinsics; dropped submit extrinsic request");
|
||||
reply_manager.abandon(reply_context);
|
||||
}
|
||||
} else {
|
||||
warn!(target: LOG_TARGET,
|
||||
"Next extrinsic deadline reached, but no extrinsic in queue; \
|
||||
this is a bug");
|
||||
}
|
||||
}
|
||||
|
||||
res_reply_context = submit_extrinsic_results.select_next_some() => {
|
||||
let (res, reply_context) = res_reply_context;
|
||||
let res = match res {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(RemoteErr::Other(err.to_string())),
|
||||
};
|
||||
complete_submit_extrinsic(&mut reply_manager, reply_context, res, &mut mixnet);
|
||||
}
|
||||
}
|
||||
|
||||
let events = mixnet.take_events();
|
||||
if !events.is_empty() {
|
||||
if events.contains(Events::RESERVED_PEERS_CHANGED) {
|
||||
let reserved_peer_addrs = mixnet
|
||||
.reserved_peers()
|
||||
.flat_map(|mixnode| mixnode.extra.iter()) // External addresses
|
||||
.cloned()
|
||||
.collect();
|
||||
if let Err(err) =
|
||||
network.set_reserved_peers(protocol_name.clone(), reserved_peer_addrs)
|
||||
{
|
||||
debug!(target: LOG_TARGET, "Setting reserved peers failed: {err}");
|
||||
}
|
||||
}
|
||||
if events.contains(Events::NEXT_FORWARD_PACKET_DEADLINE_CHANGED) {
|
||||
next_forward_packet_delay
|
||||
.reset(mixnet.next_forward_packet_deadline().map(time_until));
|
||||
}
|
||||
if events.contains(Events::NEXT_AUTHORED_PACKET_DEADLINE_CHANGED) {
|
||||
next_authored_packet_delay.reset(mixnet.next_authored_packet_delay());
|
||||
}
|
||||
if events.contains(Events::SPACE_IN_AUTHORED_PACKET_QUEUE) {
|
||||
// Note this may cause the next retry deadline to change, but should not trigger
|
||||
// any mixnet events
|
||||
request_manager.process_post_queues(
|
||||
&mut mixnet,
|
||||
&packet_dispatcher,
|
||||
&config.bizinikiwi,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if request_manager.next_retry_deadline_changed() {
|
||||
next_retry_delay.reset(request_manager.next_retry_deadline().map(time_until));
|
||||
}
|
||||
|
||||
if extrinsic_queue.next_deadline_changed() {
|
||||
next_extrinsic_delay.reset(extrinsic_queue.next_deadline().map(time_until));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
// 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/>.
|
||||
|
||||
//! [`sync_with_runtime`] synchronises the session status and mixnode sets from the blockchain
|
||||
//! runtime to the core mixnet state. It is called every time a block is finalised.
|
||||
|
||||
use super::peer_id::from_core_peer_id;
|
||||
use log::{debug, info};
|
||||
use mixnet::core::{
|
||||
Mixnet, Mixnode as CoreMixnode, MixnodesErr as CoreMixnodesErr, RelSessionIndex,
|
||||
SessionPhase as CoreSessionPhase, SessionStatus as CoreSessionStatus,
|
||||
};
|
||||
use pezsc_network_types::{
|
||||
multiaddr::{multiaddr, Multiaddr, Protocol},
|
||||
PeerId,
|
||||
};
|
||||
use pezsp_api::{ApiError, ApiRef};
|
||||
use pezsp_mixnet::{
|
||||
runtime_api::MixnetApi,
|
||||
types::{
|
||||
Mixnode as RuntimeMixnode, MixnodesErr as RuntimeMixnodesErr,
|
||||
SessionPhase as RuntimeSessionPhase, SessionStatus as RuntimeSessionStatus,
|
||||
},
|
||||
};
|
||||
use pezsp_runtime::traits::Block;
|
||||
|
||||
const LOG_TARGET: &str = "mixnet";
|
||||
|
||||
/// Convert a [`RuntimeSessionStatus`] to a [`CoreSessionStatus`].
|
||||
///
|
||||
/// The [`RuntimeSessionStatus`] and [`CoreSessionStatus`] types are effectively the same.
|
||||
/// [`RuntimeSessionStatus`] is used in the runtime to avoid depending on the [`mixnet`] crate
|
||||
/// there.
|
||||
fn to_core_session_status(status: RuntimeSessionStatus) -> CoreSessionStatus {
|
||||
CoreSessionStatus {
|
||||
current_index: status.current_index,
|
||||
phase: match status.phase {
|
||||
RuntimeSessionPhase::CoverToCurrent => CoreSessionPhase::CoverToCurrent,
|
||||
RuntimeSessionPhase::RequestsToCurrent => CoreSessionPhase::RequestsToCurrent,
|
||||
RuntimeSessionPhase::CoverToPrev => CoreSessionPhase::CoverToPrev,
|
||||
RuntimeSessionPhase::DisconnectFromPrev => CoreSessionPhase::DisconnectFromPrev,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_external_addresses(external_addresses: Vec<Vec<u8>>) -> Vec<Multiaddr> {
|
||||
external_addresses
|
||||
.into_iter()
|
||||
.flat_map(|addr| {
|
||||
let addr = match String::from_utf8(addr) {
|
||||
Ok(addr) => addr,
|
||||
Err(addr) => {
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"Mixnode external address {:x?} is not valid UTF-8",
|
||||
addr.into_bytes(),
|
||||
);
|
||||
return None;
|
||||
},
|
||||
};
|
||||
match addr.parse() {
|
||||
Ok(addr) => Some(addr),
|
||||
Err(err) => {
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"Could not parse mixnode address {addr}: {err}",
|
||||
);
|
||||
None
|
||||
},
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Modify `external_addresses` such that there is at least one address and the final component of
|
||||
/// each address matches `peer_id`.
|
||||
fn fixup_external_addresses(external_addresses: &mut Vec<Multiaddr>, peer_id: &PeerId) {
|
||||
// Ensure the final component of each address matches peer_id
|
||||
external_addresses.retain_mut(|addr| match PeerId::try_from_multiaddr(addr) {
|
||||
Some(addr_peer_id) if addr_peer_id == *peer_id => true,
|
||||
Some(_) => {
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"Mixnode address {} does not match mixnode peer ID {}, ignoring",
|
||||
addr,
|
||||
peer_id
|
||||
);
|
||||
false
|
||||
},
|
||||
None if matches!(addr.iter().last(), Some(Protocol::P2p(_))) => {
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"Mixnode address {} has unrecognised P2P protocol, ignoring",
|
||||
addr
|
||||
);
|
||||
false
|
||||
},
|
||||
None => {
|
||||
addr.push(Protocol::P2p(*peer_id.as_ref()));
|
||||
true
|
||||
},
|
||||
});
|
||||
|
||||
// If there are no addresses, insert one consisting of just the peer ID
|
||||
if external_addresses.is_empty() {
|
||||
external_addresses.push(multiaddr!(P2p(*peer_id.as_ref())));
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a [`RuntimeMixnode`] to a [`CoreMixnode`]. If the conversion fails, an error message is
|
||||
/// logged, but a [`CoreMixnode`] is still returned.
|
||||
///
|
||||
/// It would be possible to handle conversion failure in a better way, but this would complicate
|
||||
/// things for what should be a rare case. Note that even if the conversion here succeeds, there is
|
||||
/// no guarantee that we will be able to connect to the mixnode or send packets to it. The most
|
||||
/// common failure case is expected to be that a mixnode is simply unreachable over the network.
|
||||
fn into_core_mixnode(mixnode: RuntimeMixnode) -> CoreMixnode<Vec<Multiaddr>> {
|
||||
let external_addresses = if let Some(peer_id) = from_core_peer_id(&mixnode.peer_id) {
|
||||
let mut external_addresses = parse_external_addresses(mixnode.external_addresses);
|
||||
fixup_external_addresses(&mut external_addresses, &peer_id);
|
||||
external_addresses
|
||||
} else {
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"Failed to convert mixnet peer ID {:x?} to libp2p peer ID",
|
||||
mixnode.peer_id,
|
||||
);
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
CoreMixnode {
|
||||
kx_public: mixnode.kx_public,
|
||||
peer_id: mixnode.peer_id,
|
||||
extra: external_addresses,
|
||||
}
|
||||
}
|
||||
|
||||
fn maybe_set_mixnodes(
|
||||
mixnet: &mut Mixnet<Vec<Multiaddr>>,
|
||||
rel_session_index: RelSessionIndex,
|
||||
mixnodes: &dyn Fn() -> Result<Result<Vec<RuntimeMixnode>, RuntimeMixnodesErr>, ApiError>,
|
||||
) {
|
||||
let current_session_index = mixnet.session_status().current_index;
|
||||
mixnet.maybe_set_mixnodes(rel_session_index, &mut || {
|
||||
// Note that RelSessionIndex::Prev + 0 would panic, but this closure will not get called in
|
||||
// that case so we are fine. Do not move this out of the closure!
|
||||
let session_index = rel_session_index + current_session_index;
|
||||
match mixnodes() {
|
||||
Ok(Ok(mixnodes)) => Ok(mixnodes.into_iter().map(into_core_mixnode).collect()),
|
||||
Ok(Err(err)) => {
|
||||
info!(target: LOG_TARGET, "Session {session_index}: Mixnet disabled: {err}");
|
||||
Err(CoreMixnodesErr::Permanent) // Disable the session slot
|
||||
},
|
||||
Err(err) => {
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"Session {session_index}: Error getting mixnodes from runtime: {err}"
|
||||
);
|
||||
Err(CoreMixnodesErr::Transient) // Just leave the session slot empty; try again next block
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn sync_with_runtime<B, A>(mixnet: &mut Mixnet<Vec<Multiaddr>>, api: ApiRef<A>, hash: B::Hash)
|
||||
where
|
||||
B: Block,
|
||||
A: MixnetApi<B>,
|
||||
{
|
||||
let session_status = match api.session_status(hash) {
|
||||
Ok(session_status) => session_status,
|
||||
Err(err) => {
|
||||
debug!(target: LOG_TARGET, "Error getting session status from runtime: {err}");
|
||||
return;
|
||||
},
|
||||
};
|
||||
mixnet.set_session_status(to_core_session_status(session_status));
|
||||
|
||||
maybe_set_mixnodes(mixnet, RelSessionIndex::Prev, &|| api.prev_mixnodes(hash));
|
||||
maybe_set_mixnodes(mixnet, RelSessionIndex::Current, &|| api.current_mixnodes(hash));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn fixup_empty_external_addresses() {
|
||||
let peer_id = PeerId::random();
|
||||
let mut external_addresses = Vec::new();
|
||||
fixup_external_addresses(&mut external_addresses, &peer_id);
|
||||
assert_eq!(external_addresses, vec![multiaddr!(P2p(peer_id))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixup_misc_external_addresses() {
|
||||
let peer_id = PeerId::random();
|
||||
let other_peer_id = PeerId::random();
|
||||
let mut external_addresses = vec![
|
||||
multiaddr!(Tcp(0u16), P2p(peer_id)),
|
||||
multiaddr!(Tcp(1u16), P2p(other_peer_id)),
|
||||
multiaddr!(Tcp(2u16)),
|
||||
Multiaddr::empty(),
|
||||
];
|
||||
fixup_external_addresses(&mut external_addresses, &peer_id);
|
||||
assert_eq!(
|
||||
external_addresses,
|
||||
vec![
|
||||
multiaddr!(Tcp(0u16), P2p(peer_id)),
|
||||
multiaddr!(Tcp(2u16), P2p(peer_id)),
|
||||
multiaddr!(P2p(peer_id)),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user