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
+53
View File
@@ -0,0 +1,53 @@
[package]
description = "Bizinikiwi mixnet service"
name = "pezsc-mixnet"
version = "0.4.0"
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
authors = [
"Kurdistan Tech Institute <info@pezkuwichain.io>",
"Parity Technologies <admin@parity.io>",
]
edition.workspace = true
homepage.workspace = true
repository.workspace = true
readme = "README.md"
[lints]
workspace = true
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
array-bytes = { workspace = true, default-features = true }
arrayvec = { workspace = true }
blake2 = { workspace = true, default-features = true }
bytes = { workspace = true, default-features = true }
codec = { features = ["derive"], workspace = true }
futures = { workspace = true }
futures-timer = { workspace = true }
log = { workspace = true, default-features = true }
mixnet = { workspace = true }
parking_lot = { workspace = true, default-features = true }
pezsc-client-api = { workspace = true, default-features = true }
pezsc-network = { workspace = true, default-features = true }
pezsc-network-types = { workspace = true, default-features = true }
pezsc-transaction-pool-api = { workspace = true, default-features = true }
pezsp-api = { workspace = true, default-features = true }
pezsp-consensus = { workspace = true, default-features = true }
pezsp-core = { workspace = true, default-features = true }
pezsp-keystore = { workspace = true, default-features = true }
pezsp-mixnet = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
thiserror = { workspace = true }
[features]
runtime-benchmarks = [
"pezsc-client-api/runtime-benchmarks",
"pezsc-network/runtime-benchmarks",
"pezsc-transaction-pool-api/runtime-benchmarks",
"pezsp-api/runtime-benchmarks",
"pezsp-consensus/runtime-benchmarks",
"pezsp-mixnet/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
]
+3
View File
@@ -0,0 +1,3 @@
Bizinikiwi mixnet service.
License: GPL-3.0-or-later WITH Classpath-exception-2.0
+69
View File
@@ -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)?
}
}
}
+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/>.
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(),
}
}
}
+56
View File
@@ -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))
}
}
+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/>.
//! 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)
}
}
+36
View File
@@ -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)
}
+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/>.
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,
)
}
+119
View File
@@ -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);
}
}
+391
View File
@@ -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(&notification_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(&notification_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(&notification_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)),
]
);
}
}