feat: initialize Kurdistan SDK - independent fork of Polkadot SDK
This commit is contained in:
@@ -0,0 +1,333 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi 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.
|
||||
|
||||
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! A small set of wrapping types to cover most of our adversary test cases.
|
||||
//!
|
||||
//! This allows types with internal mutability to synchronize across
|
||||
//! multiple subsystems and intercept or replace incoming and outgoing
|
||||
//! messages on the overseer level.
|
||||
|
||||
use pezkuwi_node_subsystem::*;
|
||||
pub use pezkuwi_node_subsystem::{messages::*, overseer, FromOrchestra};
|
||||
use std::{collections::VecDeque, future::Future, pin::Pin};
|
||||
|
||||
/// Filter incoming and outgoing messages.
|
||||
pub trait MessageInterceptor<Sender>: Send + Sync + Clone + 'static
|
||||
where
|
||||
Sender: overseer::SubsystemSender<<Self::Message as overseer::AssociateOutgoing>::OutgoingMessages>
|
||||
+ Clone
|
||||
+ 'static,
|
||||
{
|
||||
/// The message type the original subsystem handles incoming.
|
||||
type Message: overseer::AssociateOutgoing + Send + 'static;
|
||||
|
||||
/// Filter messages that are to be received by
|
||||
/// the subsystem.
|
||||
///
|
||||
/// For non-trivial cases, the `sender` can be used to send
|
||||
/// multiple messages after doing some additional processing.
|
||||
fn intercept_incoming(
|
||||
&self,
|
||||
_sender: &mut Sender,
|
||||
msg: FromOrchestra<Self::Message>,
|
||||
) -> Option<FromOrchestra<Self::Message>> {
|
||||
Some(msg)
|
||||
}
|
||||
|
||||
/// Specifies if we need to replace some outgoing message with another (potentially empty)
|
||||
/// message
|
||||
fn need_intercept_outgoing(
|
||||
&self,
|
||||
_msg: &<Self::Message as overseer::AssociateOutgoing>::OutgoingMessages,
|
||||
) -> bool {
|
||||
false
|
||||
}
|
||||
/// Send modified message instead of the original one
|
||||
fn intercept_outgoing(
|
||||
&self,
|
||||
_msg: &<Self::Message as overseer::AssociateOutgoing>::OutgoingMessages,
|
||||
) -> Option<<Self::Message as overseer::AssociateOutgoing>::OutgoingMessages> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// A sender with the outgoing messages filtered.
|
||||
#[derive(Clone)]
|
||||
pub struct InterceptedSender<Sender, Fil> {
|
||||
inner: Sender,
|
||||
message_filter: Fil,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<OutgoingMessage, Sender, Fil> overseer::SubsystemSender<OutgoingMessage> for InterceptedSender<Sender, Fil>
|
||||
where
|
||||
OutgoingMessage: overseer::AssociateOutgoing + Send + 'static + TryFrom<overseer::AllMessages>,
|
||||
Sender: overseer::SubsystemSender<OutgoingMessage>
|
||||
+ overseer::SubsystemSender<
|
||||
<
|
||||
<Fil as MessageInterceptor<Sender>>::Message as overseer::AssociateOutgoing
|
||||
>::OutgoingMessages
|
||||
>,
|
||||
Fil: MessageInterceptor<Sender>,
|
||||
<Fil as MessageInterceptor<Sender>>::Message: overseer::AssociateOutgoing,
|
||||
<
|
||||
<Fil as MessageInterceptor<Sender>>::Message as overseer::AssociateOutgoing
|
||||
>::OutgoingMessages:
|
||||
From<OutgoingMessage> + Send + Sync,
|
||||
<OutgoingMessage as TryFrom<overseer::AllMessages>>::Error: std::fmt::Debug,
|
||||
{
|
||||
async fn send_message(&mut self, msg: OutgoingMessage) {
|
||||
self.send_message_with_priority::<overseer::NormalPriority>(msg).await;
|
||||
}
|
||||
|
||||
async fn send_message_with_priority<P: Priority>(&mut self, msg: OutgoingMessage) {
|
||||
let msg = <
|
||||
<<Fil as MessageInterceptor<Sender>>::Message as overseer::AssociateOutgoing
|
||||
>::OutgoingMessages as From<OutgoingMessage>>::from(msg);
|
||||
if self.message_filter.need_intercept_outgoing(&msg) {
|
||||
if let Some(msg) = self.message_filter.intercept_outgoing(&msg) {
|
||||
self.inner.send_message(msg).await;
|
||||
}
|
||||
}
|
||||
else {
|
||||
self.inner.send_message(msg).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn try_send_message(
|
||||
&mut self,
|
||||
msg: OutgoingMessage,
|
||||
) -> Result<(), pezkuwi_node_subsystem_util::metered::TrySendError<OutgoingMessage>> {
|
||||
self.try_send_message_with_priority::<overseer::NormalPriority>(msg)
|
||||
}
|
||||
|
||||
fn try_send_message_with_priority<P: Priority>(&mut self, msg: OutgoingMessage) -> Result<(), TrySendError<OutgoingMessage>> {
|
||||
let msg = <
|
||||
<<Fil as MessageInterceptor<Sender>>::Message as overseer::AssociateOutgoing
|
||||
>::OutgoingMessages as From<OutgoingMessage>>::from(msg);
|
||||
if self.message_filter.need_intercept_outgoing(&msg) {
|
||||
if let Some(real_msg) = self.message_filter.intercept_outgoing(&msg) {
|
||||
let orig_msg : OutgoingMessage = msg.into().try_into().expect("must be able to recover the original message");
|
||||
self.inner.try_send_message(real_msg).map_err(|e| {
|
||||
match e {
|
||||
TrySendError::Full(_) => TrySendError::Full(orig_msg),
|
||||
TrySendError::Closed(_) => TrySendError::Closed(orig_msg),
|
||||
}
|
||||
})
|
||||
}
|
||||
else {
|
||||
// No message to send after intercepting
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
else {
|
||||
let orig_msg : OutgoingMessage = msg.into().try_into().expect("must be able to recover the original message");
|
||||
self.inner.try_send_message(orig_msg)
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_messages<T>(&mut self, msgs: T)
|
||||
where
|
||||
T: IntoIterator<Item = OutgoingMessage> + Send,
|
||||
T::IntoIter: Send,
|
||||
{
|
||||
for msg in msgs {
|
||||
self.send_message(msg).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn send_unbounded_message(&mut self, msg: OutgoingMessage) {
|
||||
let msg = <
|
||||
<<Fil as MessageInterceptor<Sender>>::Message as overseer::AssociateOutgoing
|
||||
>::OutgoingMessages as From<OutgoingMessage>>::from(msg);
|
||||
if self.message_filter.need_intercept_outgoing(&msg) {
|
||||
if let Some(msg) = self.message_filter.intercept_outgoing(&msg) {
|
||||
self.inner.send_unbounded_message(msg);
|
||||
}
|
||||
}
|
||||
else {
|
||||
self.inner.send_unbounded_message(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A subsystem context, that filters the outgoing messages.
|
||||
pub struct InterceptedContext<Context, Fil>
|
||||
where
|
||||
Context: overseer::SubsystemContext<Error=SubsystemError, Signal=OverseerSignal>,
|
||||
Fil: MessageInterceptor<<Context as overseer::SubsystemContext>::Sender>,
|
||||
<Context as overseer::SubsystemContext>::Sender:
|
||||
overseer::SubsystemSender<
|
||||
<
|
||||
<
|
||||
Fil as MessageInterceptor<<Context as overseer::SubsystemContext>::Sender>
|
||||
>::Message as overseer::AssociateOutgoing
|
||||
>::OutgoingMessages,
|
||||
>,
|
||||
{
|
||||
inner: Context,
|
||||
message_filter: Fil,
|
||||
sender: InterceptedSender<<Context as overseer::SubsystemContext>::Sender, Fil>,
|
||||
message_buffer: VecDeque<FromOrchestra<<Context as overseer::SubsystemContext>::Message>>,
|
||||
}
|
||||
|
||||
impl<Context, Fil> InterceptedContext<Context, Fil>
|
||||
where
|
||||
Context: overseer::SubsystemContext<Error=SubsystemError,Signal=OverseerSignal>,
|
||||
Fil: MessageInterceptor<
|
||||
<Context as overseer::SubsystemContext>::Sender,
|
||||
Message = <Context as overseer::SubsystemContext>::Message,
|
||||
>,
|
||||
<Context as overseer::SubsystemContext>::Message: overseer::AssociateOutgoing,
|
||||
<Context as overseer::SubsystemContext>::Sender: overseer::SubsystemSender<
|
||||
<<Context as overseer::SubsystemContext>::Message as overseer::AssociateOutgoing>::OutgoingMessages
|
||||
>
|
||||
{
|
||||
pub fn new(mut inner: Context, message_filter: Fil) -> Self {
|
||||
let sender = InterceptedSender::<<Context as overseer::SubsystemContext>::Sender, Fil> {
|
||||
inner: inner.sender().clone(),
|
||||
message_filter: message_filter.clone(),
|
||||
};
|
||||
Self { inner, message_filter, sender, message_buffer: VecDeque::new() }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<Context, Fil> overseer::SubsystemContext for InterceptedContext<Context, Fil>
|
||||
where
|
||||
Context: overseer::SubsystemContext<Error=SubsystemError,Signal=OverseerSignal>,
|
||||
<Context as overseer::SubsystemContext>::Message:
|
||||
overseer::AssociateOutgoing,
|
||||
<Context as overseer::SubsystemContext>::Sender:
|
||||
overseer::SubsystemSender<
|
||||
<<Context as overseer::SubsystemContext>::Message as overseer::AssociateOutgoing>::OutgoingMessages
|
||||
>,
|
||||
InterceptedSender<<Context as overseer::SubsystemContext>::Sender, Fil>:
|
||||
overseer::SubsystemSender<
|
||||
<<Context as overseer::SubsystemContext>::Message as overseer::AssociateOutgoing>::OutgoingMessages
|
||||
>,
|
||||
Fil: MessageInterceptor<
|
||||
<Context as overseer::SubsystemContext>::Sender,
|
||||
Message = <Context as overseer::SubsystemContext>::Message,
|
||||
>,
|
||||
{
|
||||
type Message = <Context as overseer::SubsystemContext>::Message;
|
||||
type Sender = InterceptedSender<<Context as overseer::SubsystemContext>::Sender, Fil>;
|
||||
type Error = SubsystemError;
|
||||
type OutgoingMessages = <<Context as overseer::SubsystemContext>::Message as overseer::AssociateOutgoing>::OutgoingMessages;
|
||||
type Signal = OverseerSignal;
|
||||
|
||||
async fn try_recv(&mut self) -> Result<Option<FromOrchestra<Self::Message>>, ()> {
|
||||
loop {
|
||||
match self.inner.try_recv().await? {
|
||||
None => return Ok(None),
|
||||
Some(msg) =>
|
||||
if let Some(msg) =
|
||||
self.message_filter.intercept_incoming(self.inner.sender(), msg)
|
||||
{
|
||||
return Ok(Some(msg))
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn recv(&mut self) -> SubsystemResult<FromOrchestra<Self::Message>> {
|
||||
if let Some(msg) = self.message_buffer.pop_front() {
|
||||
return Ok(msg)
|
||||
}
|
||||
loop {
|
||||
let msg = self.inner.recv().await?;
|
||||
if let Some(msg) = self.message_filter.intercept_incoming(self.inner.sender(), msg) {
|
||||
return Ok(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn recv_signal(&mut self) -> SubsystemResult<Self::Signal> {
|
||||
loop {
|
||||
let msg = self.inner.recv().await?;
|
||||
if let Some(msg) = self.message_filter.intercept_incoming(self.inner.sender(), msg) {
|
||||
if let FromOrchestra::Signal(sig) = msg {
|
||||
return Ok(sig)
|
||||
} else {
|
||||
self.message_buffer.push_back(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn(
|
||||
&mut self,
|
||||
name: &'static str,
|
||||
s: Pin<Box<dyn Future<Output = ()> + Send>>,
|
||||
) -> SubsystemResult<()> {
|
||||
self.inner.spawn(name, s)
|
||||
}
|
||||
|
||||
fn spawn_blocking(
|
||||
&mut self,
|
||||
name: &'static str,
|
||||
s: Pin<Box<dyn Future<Output = ()> + Send>>,
|
||||
) -> SubsystemResult<()> {
|
||||
self.inner.spawn_blocking(name, s)
|
||||
}
|
||||
|
||||
fn sender(&mut self) -> &mut Self::Sender {
|
||||
&mut self.sender
|
||||
}
|
||||
}
|
||||
|
||||
/// A subsystem to which incoming and outgoing filters are applied.
|
||||
pub struct InterceptedSubsystem<Sub, Interceptor> {
|
||||
pub subsystem: Sub,
|
||||
pub message_interceptor: Interceptor,
|
||||
}
|
||||
|
||||
impl<Sub, Interceptor> InterceptedSubsystem<Sub, Interceptor> {
|
||||
pub fn new(subsystem: Sub, message_interceptor: Interceptor) -> Self {
|
||||
Self { subsystem, message_interceptor }
|
||||
}
|
||||
}
|
||||
|
||||
impl<Context, Sub, Interceptor> overseer::Subsystem<Context, SubsystemError> for InterceptedSubsystem<Sub, Interceptor>
|
||||
where
|
||||
Context:
|
||||
overseer::SubsystemContext<Error=SubsystemError,Signal=OverseerSignal> + Sync + Send,
|
||||
InterceptedContext<Context, Interceptor>:
|
||||
overseer::SubsystemContext<Error=SubsystemError,Signal=OverseerSignal>,
|
||||
Sub:
|
||||
overseer::Subsystem<InterceptedContext<Context, Interceptor>, SubsystemError>,
|
||||
Interceptor:
|
||||
MessageInterceptor<
|
||||
<Context as overseer::SubsystemContext>::Sender,
|
||||
Message = <Context as overseer::SubsystemContext>::Message,
|
||||
>,
|
||||
<Context as overseer::SubsystemContext>::Message:
|
||||
overseer::AssociateOutgoing,
|
||||
<Context as overseer::SubsystemContext>::Sender:
|
||||
overseer::SubsystemSender<
|
||||
<<Context as overseer::SubsystemContext>::Message as overseer::AssociateOutgoing
|
||||
>::OutgoingMessages
|
||||
>,
|
||||
{
|
||||
fn start(self, ctx: Context) -> SpawnedSubsystem {
|
||||
let ctx = InterceptedContext::new(ctx, self.message_interceptor);
|
||||
overseer::Subsystem::<InterceptedContext<Context, Interceptor>, SubsystemError>::start(
|
||||
self.subsystem,
|
||||
ctx,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi 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.
|
||||
|
||||
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! A malus or nemesis node launch code.
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::eyre;
|
||||
|
||||
pub(crate) mod interceptor;
|
||||
pub(crate) mod shared;
|
||||
|
||||
mod variants;
|
||||
|
||||
use variants::*;
|
||||
|
||||
/// Define the different variants of behavior.
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(about = "Malus - the nemesis of pezkuwi.", version, rename_all = "kebab-case")]
|
||||
enum NemesisVariant {
|
||||
/// Suggest a candidate with an invalid proof of validity.
|
||||
SuggestGarbageCandidate(SuggestGarbageCandidateOptions),
|
||||
/// Support disabled validators in backing and statement distribution.
|
||||
SupportDisabled(SupportDisabledOptions),
|
||||
/// Back a candidate with a specifically crafted proof of validity.
|
||||
BackGarbageCandidate(BackGarbageCandidateOptions),
|
||||
/// Delayed disputing of ancestors that are perfectly fine.
|
||||
DisputeAncestor(DisputeAncestorOptions),
|
||||
/// Delayed disputing of finalized candidates.
|
||||
DisputeFinalizedCandidates(DisputeFinalizedCandidatesOptions),
|
||||
/// Spam many request statements instead of sending a single one.
|
||||
SpamStatementRequests(SpamStatementRequestsOptions),
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[allow(missing_docs)]
|
||||
struct MalusCli {
|
||||
#[command(subcommand)]
|
||||
pub variant: NemesisVariant,
|
||||
/// Sets the minimum delay between the best and finalized block.
|
||||
pub finality_delay: Option<u32>,
|
||||
}
|
||||
|
||||
impl MalusCli {
|
||||
/// Launch a malus node.
|
||||
fn launch(self) -> eyre::Result<()> {
|
||||
let finality_delay = self.finality_delay;
|
||||
match self.variant {
|
||||
NemesisVariant::BackGarbageCandidate(opts) => {
|
||||
let BackGarbageCandidateOptions { percentage, cli } = opts;
|
||||
|
||||
pezkuwi_cli::run_node(cli, BackGarbageCandidates { percentage }, finality_delay)?
|
||||
},
|
||||
NemesisVariant::SuggestGarbageCandidate(opts) => {
|
||||
let SuggestGarbageCandidateOptions { percentage, cli } = opts;
|
||||
|
||||
pezkuwi_cli::run_node(cli, SuggestGarbageCandidates { percentage }, finality_delay)?
|
||||
},
|
||||
NemesisVariant::SupportDisabled(opts) => {
|
||||
let SupportDisabledOptions { cli } = opts;
|
||||
|
||||
pezkuwi_cli::run_node(cli, SupportDisabled, finality_delay)?
|
||||
},
|
||||
NemesisVariant::DisputeAncestor(opts) => {
|
||||
let DisputeAncestorOptions {
|
||||
fake_validation,
|
||||
fake_validation_error,
|
||||
percentage,
|
||||
cli,
|
||||
} = opts;
|
||||
|
||||
pezkuwi_cli::run_node(
|
||||
cli,
|
||||
DisputeValidCandidates { fake_validation, fake_validation_error, percentage },
|
||||
finality_delay,
|
||||
)?
|
||||
},
|
||||
NemesisVariant::DisputeFinalizedCandidates(opts) => {
|
||||
let DisputeFinalizedCandidatesOptions { dispute_offset, cli } = opts;
|
||||
|
||||
pezkuwi_cli::run_node(
|
||||
cli,
|
||||
DisputeFinalizedCandidates { dispute_offset },
|
||||
finality_delay,
|
||||
)?
|
||||
},
|
||||
NemesisVariant::SpamStatementRequests(opts) => {
|
||||
let SpamStatementRequestsOptions { spam_factor, cli } = opts;
|
||||
|
||||
pezkuwi_cli::run_node(cli, SpamStatementRequests { spam_factor }, finality_delay)?
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> eyre::Result<()> {
|
||||
color_eyre::install()?;
|
||||
let cli = MalusCli::parse();
|
||||
cli.launch()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn subcommand_works() {
|
||||
let cli = MalusCli::try_parse_from(IntoIterator::into_iter([
|
||||
"malus",
|
||||
"dispute-ancestor",
|
||||
"--bob",
|
||||
]))
|
||||
.unwrap();
|
||||
assert_matches::assert_matches!(cli, MalusCli {
|
||||
variant: NemesisVariant::DisputeAncestor(run),
|
||||
..
|
||||
} => {
|
||||
assert!(run.cli.run.base.bob);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn percentage_works_suggest_garbage() {
|
||||
let cli = MalusCli::try_parse_from(IntoIterator::into_iter([
|
||||
"malus",
|
||||
"suggest-garbage-candidate",
|
||||
"--percentage",
|
||||
"100",
|
||||
"--bob",
|
||||
]))
|
||||
.unwrap();
|
||||
assert_matches::assert_matches!(cli, MalusCli {
|
||||
variant: NemesisVariant::SuggestGarbageCandidate(run),
|
||||
..
|
||||
} => {
|
||||
assert!(run.cli.run.base.bob);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn percentage_works_dispute_ancestor() {
|
||||
let cli = MalusCli::try_parse_from(IntoIterator::into_iter([
|
||||
"malus",
|
||||
"dispute-ancestor",
|
||||
"--percentage",
|
||||
"100",
|
||||
"--bob",
|
||||
]))
|
||||
.unwrap();
|
||||
assert_matches::assert_matches!(cli, MalusCli {
|
||||
variant: NemesisVariant::DisputeAncestor(run),
|
||||
..
|
||||
} => {
|
||||
assert!(run.cli.run.base.bob);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn percentage_works_back_garbage() {
|
||||
let cli = MalusCli::try_parse_from(IntoIterator::into_iter([
|
||||
"malus",
|
||||
"back-garbage-candidate",
|
||||
"--percentage",
|
||||
"100",
|
||||
"--bob",
|
||||
]))
|
||||
.unwrap();
|
||||
assert_matches::assert_matches!(cli, MalusCli {
|
||||
variant: NemesisVariant::BackGarbageCandidate(run),
|
||||
..
|
||||
} => {
|
||||
assert!(run.cli.run.base.bob);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn validate_range_for_percentage() {
|
||||
let cli = MalusCli::try_parse_from(IntoIterator::into_iter([
|
||||
"malus",
|
||||
"suggest-garbage-candidate",
|
||||
"--percentage",
|
||||
"101",
|
||||
"--bob",
|
||||
]))
|
||||
.unwrap();
|
||||
assert_matches::assert_matches!(cli, MalusCli {
|
||||
variant: NemesisVariant::DisputeAncestor(run),
|
||||
..
|
||||
} => {
|
||||
assert!(run.cli.run.base.bob);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dispute_finalized_candidates_works() {
|
||||
let cli = MalusCli::try_parse_from(IntoIterator::into_iter([
|
||||
"malus",
|
||||
"dispute-finalized-candidates",
|
||||
"--bob",
|
||||
]))
|
||||
.unwrap();
|
||||
assert_matches::assert_matches!(cli, MalusCli {
|
||||
variant: NemesisVariant::DisputeFinalizedCandidates(run),
|
||||
..
|
||||
} => {
|
||||
assert!(run.cli.run.base.bob);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dispute_finalized_offset_value_works() {
|
||||
let cli = MalusCli::try_parse_from(IntoIterator::into_iter([
|
||||
"malus",
|
||||
"dispute-finalized-candidates",
|
||||
"--dispute-offset",
|
||||
"13",
|
||||
"--bob",
|
||||
]))
|
||||
.unwrap();
|
||||
assert_matches::assert_matches!(cli, MalusCli {
|
||||
variant: NemesisVariant::DisputeFinalizedCandidates(opts),
|
||||
..
|
||||
} => {
|
||||
assert_eq!(opts.dispute_offset, 13); // This line checks that dispute_offset is correctly set to 13
|
||||
assert!(opts.cli.run.base.bob);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi 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.
|
||||
|
||||
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use futures::prelude::*;
|
||||
use sp_core::traits::SpawnNamed;
|
||||
|
||||
pub const MALUS: &str = "MALUS";
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) const MALICIOUS_POV: &[u8] = "😈😈pov_looks_valid_to_me😈😈".as_bytes();
|
||||
|
||||
/// Launch a service task for each item in the provided queue.
|
||||
#[allow(unused)]
|
||||
pub(crate) fn launch_processing_task<X, F, U, Q, S>(spawner: &S, queue: Q, action: F)
|
||||
where
|
||||
F: Fn(X) -> U + Send + 'static,
|
||||
U: Future<Output = ()> + Send + 'static,
|
||||
Q: Stream<Item = X> + Send + 'static,
|
||||
X: Send,
|
||||
S: 'static + SpawnNamed + Clone + Unpin,
|
||||
{
|
||||
let spawner2: S = spawner.clone();
|
||||
spawner.spawn(
|
||||
"nemesis-queue-processor",
|
||||
Some("malus"),
|
||||
Box::pin(async move {
|
||||
let spawner3 = spawner2.clone();
|
||||
queue
|
||||
.for_each(move |input| {
|
||||
spawner3.spawn("nemesis-task", Some("malus"), Box::pin(action(input)));
|
||||
async move { () }
|
||||
})
|
||||
.await;
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi 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.
|
||||
|
||||
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use super::*;
|
||||
|
||||
use pezkuwi_node_subsystem_test_helpers::*;
|
||||
|
||||
use pezkuwi_node_subsystem::{
|
||||
messages::AvailabilityStoreMessage,
|
||||
overseer::{dummy::DummySubsystem, gen::TimeoutExt, Subsystem, AssociateOutgoing},
|
||||
SubsystemError,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct BlackHoleInterceptor;
|
||||
|
||||
impl<Sender> MessageInterceptor<Sender> for BlackHoleInterceptor
|
||||
where
|
||||
Sender: overseer::AvailabilityStoreSenderTrait
|
||||
+ Clone
|
||||
+ 'static,
|
||||
{
|
||||
type Message = AvailabilityStoreMessage;
|
||||
fn intercept_incoming(
|
||||
&self,
|
||||
_sender: &mut Sender,
|
||||
msg: FromOrchestra<Self::Message>,
|
||||
) -> Option<FromOrchestra<Self::Message>> {
|
||||
match msg {
|
||||
FromOrchestra::Communication { msg: _msg } => None,
|
||||
// to conclude the test cleanly
|
||||
sig => Some(sig),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct PassInterceptor;
|
||||
|
||||
impl<Sender> MessageInterceptor<Sender> for PassInterceptor
|
||||
where
|
||||
Sender: overseer::AvailabilityStoreSenderTrait
|
||||
+ Clone
|
||||
+ 'static,
|
||||
{
|
||||
type Message = AvailabilityStoreMessage;
|
||||
}
|
||||
|
||||
async fn overseer_send<T: Into<AllMessages>>(overseer: &mut TestSubsystemContextHandle<T>, msg: T) {
|
||||
overseer.send(FromOrchestra::Communication { msg }).await;
|
||||
}
|
||||
|
||||
use sp_core::testing::TaskExecutor;
|
||||
|
||||
fn launch_harness<F, M, Sub, G>(test_gen: G)
|
||||
where
|
||||
F: Future<Output = TestSubsystemContextHandle<M>> + Send,
|
||||
M: AssociateOutgoing + std::fmt::Debug + Send + 'static,
|
||||
// <M as AssociateOutgoing>::OutgoingMessages: From<M>,
|
||||
Sub: Subsystem<TestSubsystemContext<M, SpawnGlue<TaskExecutor>>, SubsystemError>,
|
||||
G: Fn(TestSubsystemContextHandle<M>) -> (F, Sub),
|
||||
{
|
||||
let pool = TaskExecutor::new();
|
||||
let (context, overseer) = make_subsystem_context(pool);
|
||||
|
||||
let (test_fut, subsystem) = test_gen(overseer);
|
||||
let subsystem = async move {
|
||||
subsystem.start(context).future.await.unwrap();
|
||||
};
|
||||
futures::pin_mut!(test_fut);
|
||||
futures::pin_mut!(subsystem);
|
||||
|
||||
futures::executor::block_on(futures::future::join(
|
||||
async move {
|
||||
let mut overseer = test_fut.await;
|
||||
overseer.send(FromOrchestra::Signal(OverseerSignal::Conclude)).await;
|
||||
},
|
||||
subsystem,
|
||||
))
|
||||
.1;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integrity_test_intercept() {
|
||||
launch_harness(|mut overseer| {
|
||||
let sub = DummySubsystem;
|
||||
|
||||
let sub_intercepted = InterceptedSubsystem::new(sub, BlackHoleInterceptor);
|
||||
|
||||
(
|
||||
async move {
|
||||
let (tx, rx) = futures::channel::oneshot::channel();
|
||||
overseer_send(
|
||||
&mut overseer,
|
||||
AvailabilityStoreMessage::QueryChunk(Default::default(), 0.into(), tx),
|
||||
)
|
||||
.await;
|
||||
let _ = rx.timeout(std::time::Duration::from_millis(100)).await.unwrap();
|
||||
overseer
|
||||
},
|
||||
sub_intercepted,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integrity_test_pass() {
|
||||
launch_harness(|mut overseer| {
|
||||
let sub = DummySubsystem;
|
||||
|
||||
let sub_intercepted = InterceptedSubsystem::new(sub, PassInterceptor);
|
||||
|
||||
(
|
||||
async move {
|
||||
let (tx, rx) = futures::channel::oneshot::channel();
|
||||
overseer_send(
|
||||
&mut overseer,
|
||||
AvailabilityStoreMessage::QueryChunk(Default::default(), 0.into(), tx),
|
||||
)
|
||||
.await;
|
||||
let _ = rx.timeout(std::time::Duration::from_millis(100)).await.unwrap();
|
||||
overseer
|
||||
},
|
||||
sub_intercepted,
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi 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.
|
||||
|
||||
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! This variant of Malus backs/approves all malicious candidates crafted by
|
||||
//! `suggest-garbage-candidate` variant and behaves honestly with other
|
||||
//! candidates.
|
||||
|
||||
use pezkuwi_cli::{
|
||||
service::{
|
||||
AuxStore, Error, ExtendedOverseerGenArgs, Overseer, OverseerConnector, OverseerGen,
|
||||
OverseerGenArgs, OverseerHandle,
|
||||
},
|
||||
validator_overseer_builder, Cli,
|
||||
};
|
||||
use pezkuwi_node_subsystem::SpawnGlue;
|
||||
use pezkuwi_node_subsystem_types::{ChainApiBackend, RuntimeApiSubsystemClient};
|
||||
use sp_core::traits::SpawnNamed;
|
||||
|
||||
use crate::{
|
||||
interceptor::*,
|
||||
variants::{FakeCandidateValidation, FakeCandidateValidationError, ReplaceValidationResult},
|
||||
};
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, clap::Parser)]
|
||||
#[clap(rename_all = "kebab-case")]
|
||||
#[allow(missing_docs)]
|
||||
pub struct BackGarbageCandidateOptions {
|
||||
/// Determines the percentage of garbage candidates that should be backed.
|
||||
/// Defaults to 100% of garbage candidates being backed.
|
||||
#[clap(short, long, ignore_case = true, default_value_t = 100, value_parser = clap::value_parser!(u8).range(0..=100))]
|
||||
pub percentage: u8,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub cli: Cli,
|
||||
}
|
||||
|
||||
/// Generates an overseer that replaces the candidate validation subsystem with our malicious
|
||||
/// variant.
|
||||
pub(crate) struct BackGarbageCandidates {
|
||||
/// The probability of behaving maliciously.
|
||||
pub percentage: u8,
|
||||
}
|
||||
|
||||
impl OverseerGen for BackGarbageCandidates {
|
||||
fn generate<Spawner, RuntimeClient>(
|
||||
&self,
|
||||
connector: OverseerConnector,
|
||||
args: OverseerGenArgs<'_, Spawner, RuntimeClient>,
|
||||
ext_args: Option<ExtendedOverseerGenArgs>,
|
||||
) -> Result<(Overseer<SpawnGlue<Spawner>, Arc<RuntimeClient>>, OverseerHandle), Error>
|
||||
where
|
||||
RuntimeClient: RuntimeApiSubsystemClient + ChainApiBackend + AuxStore + 'static,
|
||||
Spawner: 'static + SpawnNamed + Clone + Unpin,
|
||||
{
|
||||
let validation_filter = ReplaceValidationResult::new(
|
||||
FakeCandidateValidation::BackingAndApprovalValid,
|
||||
FakeCandidateValidationError::InvalidOutputs,
|
||||
f64::from(self.percentage),
|
||||
);
|
||||
|
||||
validator_overseer_builder(
|
||||
args,
|
||||
ext_args.expect("Extended arguments required to build validator overseer are provided"),
|
||||
)?
|
||||
.replace_candidate_validation(move |cv_subsystem| {
|
||||
InterceptedSubsystem::new(cv_subsystem, validation_filter)
|
||||
})
|
||||
.build_with_connector(connector)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi 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.
|
||||
|
||||
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Implements common code for nemesis. Currently, only `ReplaceValidationResult`
|
||||
//! interceptor is implemented.
|
||||
use crate::{
|
||||
interceptor::*,
|
||||
shared::{MALICIOUS_POV, MALUS},
|
||||
};
|
||||
|
||||
use pezkuwi_node_primitives::{InvalidCandidate, ValidationResult};
|
||||
|
||||
use pezkuwi_primitives::{
|
||||
CandidateCommitments, CandidateDescriptorV2 as CandidateDescriptor,
|
||||
CandidateReceiptV2 as CandidateReceipt, PersistedValidationData, PvfExecKind,
|
||||
};
|
||||
|
||||
use futures::channel::oneshot;
|
||||
use rand::distributions::{Bernoulli, Distribution};
|
||||
|
||||
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq)]
|
||||
#[value(rename_all = "kebab-case")]
|
||||
#[non_exhaustive]
|
||||
pub enum FakeCandidateValidation {
|
||||
Disabled,
|
||||
BackingInvalid,
|
||||
ApprovalInvalid,
|
||||
BackingAndApprovalInvalid,
|
||||
BackingValid,
|
||||
ApprovalValid,
|
||||
BackingAndApprovalValid,
|
||||
}
|
||||
|
||||
impl FakeCandidateValidation {
|
||||
fn misbehaves_valid(&self) -> bool {
|
||||
use FakeCandidateValidation::*;
|
||||
|
||||
match *self {
|
||||
BackingValid | ApprovalValid | BackingAndApprovalValid => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn misbehaves_invalid(&self) -> bool {
|
||||
use FakeCandidateValidation::*;
|
||||
|
||||
match *self {
|
||||
BackingInvalid | ApprovalInvalid | BackingAndApprovalInvalid => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn includes_backing(&self) -> bool {
|
||||
use FakeCandidateValidation::*;
|
||||
|
||||
match *self {
|
||||
BackingInvalid | BackingAndApprovalInvalid | BackingValid | BackingAndApprovalValid =>
|
||||
true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn includes_approval(&self) -> bool {
|
||||
use FakeCandidateValidation::*;
|
||||
|
||||
match *self {
|
||||
ApprovalInvalid |
|
||||
BackingAndApprovalInvalid |
|
||||
ApprovalValid |
|
||||
BackingAndApprovalValid => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn should_misbehave(&self, timeout: PvfExecKind) -> bool {
|
||||
match timeout {
|
||||
PvfExecKind::Backing => self.includes_backing(),
|
||||
PvfExecKind::Approval => self.includes_approval(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Candidate invalidity details
|
||||
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq)]
|
||||
#[value(rename_all = "kebab-case")]
|
||||
pub enum FakeCandidateValidationError {
|
||||
/// Validation outputs check doesn't pass.
|
||||
InvalidOutputs,
|
||||
/// Failed to execute.`validate_block`. This includes function panicking.
|
||||
ExecutionError,
|
||||
/// Execution timeout.
|
||||
Timeout,
|
||||
/// Validation input is over the limit.
|
||||
ParamsTooLarge,
|
||||
/// Code size is over the limit.
|
||||
CodeTooLarge,
|
||||
/// PoV does not decompress correctly.
|
||||
POVDecompressionFailure,
|
||||
/// Validation function returned invalid data.
|
||||
BadReturn,
|
||||
/// Invalid relay chain parent.
|
||||
BadParent,
|
||||
/// POV hash does not match.
|
||||
POVHashMismatch,
|
||||
/// Bad collator signature.
|
||||
BadSignature,
|
||||
/// Para head hash does not match.
|
||||
ParaHeadHashMismatch,
|
||||
/// Validation code hash does not match.
|
||||
CodeHashMismatch,
|
||||
}
|
||||
|
||||
impl Into<InvalidCandidate> for FakeCandidateValidationError {
|
||||
fn into(self) -> InvalidCandidate {
|
||||
match self {
|
||||
FakeCandidateValidationError::ExecutionError =>
|
||||
InvalidCandidate::ExecutionError("Malus".into()),
|
||||
FakeCandidateValidationError::InvalidOutputs => InvalidCandidate::InvalidOutputs,
|
||||
FakeCandidateValidationError::Timeout => InvalidCandidate::Timeout,
|
||||
FakeCandidateValidationError::ParamsTooLarge => InvalidCandidate::ParamsTooLarge(666),
|
||||
FakeCandidateValidationError::CodeTooLarge => InvalidCandidate::CodeTooLarge(666),
|
||||
FakeCandidateValidationError::POVDecompressionFailure =>
|
||||
InvalidCandidate::PoVDecompressionFailure,
|
||||
FakeCandidateValidationError::BadReturn => InvalidCandidate::BadReturn,
|
||||
FakeCandidateValidationError::BadParent => InvalidCandidate::BadParent,
|
||||
FakeCandidateValidationError::POVHashMismatch => InvalidCandidate::PoVHashMismatch,
|
||||
FakeCandidateValidationError::BadSignature => InvalidCandidate::BadSignature,
|
||||
FakeCandidateValidationError::ParaHeadHashMismatch =>
|
||||
InvalidCandidate::ParaHeadHashMismatch,
|
||||
FakeCandidateValidationError::CodeHashMismatch => InvalidCandidate::CodeHashMismatch,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
/// An interceptor which fakes validation result with a preconfigured result.
|
||||
/// Replaces `CandidateValidationSubsystem`.
|
||||
pub struct ReplaceValidationResult {
|
||||
fake_validation: FakeCandidateValidation,
|
||||
fake_validation_error: FakeCandidateValidationError,
|
||||
distribution: Bernoulli,
|
||||
}
|
||||
|
||||
impl ReplaceValidationResult {
|
||||
pub fn new(
|
||||
fake_validation: FakeCandidateValidation,
|
||||
fake_validation_error: FakeCandidateValidationError,
|
||||
percentage: f64,
|
||||
) -> Self {
|
||||
let distribution = Bernoulli::new(percentage / 100.0)
|
||||
.expect("Invalid probability! Percentage must be in range [0..=100].");
|
||||
Self { fake_validation, fake_validation_error, distribution }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_fake_candidate_commitments(
|
||||
persisted_validation_data: &PersistedValidationData,
|
||||
) -> CandidateCommitments {
|
||||
// Backing rejects candidates which output the same head as the parent,
|
||||
// therefore we must create a new head which is not equal to the parent.
|
||||
let mut head_data = persisted_validation_data.parent_head.clone();
|
||||
if head_data.0.is_empty() {
|
||||
head_data.0.push(0);
|
||||
} else {
|
||||
head_data.0[0] = head_data.0[0].wrapping_add(1);
|
||||
};
|
||||
|
||||
CandidateCommitments {
|
||||
upward_messages: Default::default(),
|
||||
horizontal_messages: Default::default(),
|
||||
new_validation_code: None,
|
||||
head_data,
|
||||
processed_downward_messages: 0,
|
||||
hrmp_watermark: persisted_validation_data.relay_parent_number,
|
||||
}
|
||||
}
|
||||
|
||||
// Create and send validation response. This function needs the persistent validation data.
|
||||
fn create_validation_response(
|
||||
persisted_validation_data: PersistedValidationData,
|
||||
descriptor: CandidateDescriptor,
|
||||
response_sender: oneshot::Sender<Result<ValidationResult, ValidationFailed>>,
|
||||
) {
|
||||
let commitments = create_fake_candidate_commitments(&persisted_validation_data);
|
||||
|
||||
// Craft the new malicious candidate.
|
||||
let candidate_receipt = CandidateReceipt { descriptor, commitments_hash: commitments.hash() };
|
||||
|
||||
let result = Ok(ValidationResult::Valid(commitments, persisted_validation_data));
|
||||
|
||||
gum::debug!(
|
||||
target: MALUS,
|
||||
para_id = ?candidate_receipt.descriptor.para_id(),
|
||||
candidate_hash = ?candidate_receipt.hash(),
|
||||
"ValidationResult: {:?}",
|
||||
&result
|
||||
);
|
||||
|
||||
response_sender.send(result).unwrap();
|
||||
}
|
||||
|
||||
impl<Sender> MessageInterceptor<Sender> for ReplaceValidationResult
|
||||
where
|
||||
Sender: overseer::CandidateValidationSenderTrait + Clone + Send + 'static,
|
||||
{
|
||||
type Message = CandidateValidationMessage;
|
||||
|
||||
// Capture all (approval and backing) candidate validation requests and depending on
|
||||
// configuration fail them.
|
||||
fn intercept_incoming(
|
||||
&self,
|
||||
_subsystem_sender: &mut Sender,
|
||||
msg: FromOrchestra<Self::Message>,
|
||||
) -> Option<FromOrchestra<Self::Message>> {
|
||||
match msg {
|
||||
// Message sent by the approval voting subsystem
|
||||
FromOrchestra::Communication {
|
||||
msg:
|
||||
CandidateValidationMessage::ValidateFromExhaustive {
|
||||
validation_data,
|
||||
validation_code,
|
||||
candidate_receipt,
|
||||
pov,
|
||||
executor_params,
|
||||
exec_kind,
|
||||
response_sender,
|
||||
..
|
||||
},
|
||||
} => {
|
||||
match self.fake_validation {
|
||||
x if x.misbehaves_valid() && x.should_misbehave(exec_kind.into()) => {
|
||||
// Behave normally if the `PoV` is not known to be malicious.
|
||||
if pov.block_data.0.as_slice() != MALICIOUS_POV {
|
||||
return Some(FromOrchestra::Communication {
|
||||
msg: CandidateValidationMessage::ValidateFromExhaustive {
|
||||
validation_data,
|
||||
validation_code,
|
||||
candidate_receipt,
|
||||
pov,
|
||||
executor_params,
|
||||
exec_kind,
|
||||
response_sender,
|
||||
},
|
||||
});
|
||||
}
|
||||
// Create the fake response with probability `p` if the `PoV` is malicious,
|
||||
// where 'p' defaults to 100% for suggest-garbage-candidate variant.
|
||||
let behave_maliciously = self.distribution.sample(&mut rand::thread_rng());
|
||||
match behave_maliciously {
|
||||
true => {
|
||||
gum::info!(
|
||||
target: MALUS,
|
||||
?behave_maliciously,
|
||||
"😈 Creating malicious ValidationResult::Valid message with fake candidate commitments.",
|
||||
);
|
||||
|
||||
create_validation_response(
|
||||
validation_data,
|
||||
candidate_receipt.descriptor,
|
||||
response_sender,
|
||||
);
|
||||
None
|
||||
},
|
||||
false => {
|
||||
// Behave normally with probability `(1-p)` for a malicious `PoV`.
|
||||
gum::info!(
|
||||
target: MALUS,
|
||||
?behave_maliciously,
|
||||
"😈 Passing CandidateValidationMessage::ValidateFromExhaustive to the candidate validation subsystem.",
|
||||
);
|
||||
|
||||
Some(FromOrchestra::Communication {
|
||||
msg: CandidateValidationMessage::ValidateFromExhaustive {
|
||||
validation_data,
|
||||
validation_code,
|
||||
candidate_receipt,
|
||||
pov,
|
||||
executor_params,
|
||||
exec_kind,
|
||||
response_sender,
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
},
|
||||
x if x.misbehaves_invalid() && x.should_misbehave(exec_kind.into()) => {
|
||||
// Set the validation result to invalid with probability `p` and trigger a
|
||||
// dispute
|
||||
let behave_maliciously = self.distribution.sample(&mut rand::thread_rng());
|
||||
match behave_maliciously {
|
||||
true => {
|
||||
let validation_result =
|
||||
ValidationResult::Invalid(self.fake_validation_error.into());
|
||||
|
||||
gum::info!(
|
||||
target: MALUS,
|
||||
?behave_maliciously,
|
||||
para_id = ?candidate_receipt.descriptor.para_id(),
|
||||
"😈 Maliciously sending invalid validation result: {:?}.",
|
||||
&validation_result,
|
||||
);
|
||||
|
||||
// We're not even checking the candidate, this makes us appear
|
||||
// faster than honest validators.
|
||||
response_sender.send(Ok(validation_result)).unwrap();
|
||||
None
|
||||
},
|
||||
false => {
|
||||
// Behave normally with probability `(1-p)`
|
||||
gum::info!(target: MALUS, "😈 'Decided' to not act maliciously.",);
|
||||
|
||||
Some(FromOrchestra::Communication {
|
||||
msg: CandidateValidationMessage::ValidateFromExhaustive {
|
||||
validation_data,
|
||||
validation_code,
|
||||
candidate_receipt,
|
||||
pov,
|
||||
executor_params,
|
||||
exec_kind,
|
||||
response_sender,
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
},
|
||||
// Handle FakeCandidateValidation::Disabled
|
||||
_ => Some(FromOrchestra::Communication {
|
||||
msg: CandidateValidationMessage::ValidateFromExhaustive {
|
||||
validation_data,
|
||||
validation_code,
|
||||
candidate_receipt,
|
||||
pov,
|
||||
executor_params,
|
||||
exec_kind,
|
||||
response_sender,
|
||||
},
|
||||
}),
|
||||
}
|
||||
},
|
||||
msg => Some(msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi 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.
|
||||
|
||||
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! A malicious node variant that attempts to dispute finalized candidates.
|
||||
//!
|
||||
//! This malus variant behaves honestly in backing and approval voting.
|
||||
//! The maliciousness comes from emitting an extra dispute statement on top of the other ones.
|
||||
//!
|
||||
//! Some extra quirks which generally should be insignificant:
|
||||
//! - The malus node will not dispute at session boundaries
|
||||
//! - The malus node will not dispute blocks it backed itself
|
||||
//! - Be cautious about the size of the network to make sure disputes are not auto-confirmed
|
||||
//! (7 validators is the smallest network size as it needs [(7-1)//3]+1 = 3 votes to get
|
||||
//! confirmed but it only gets 1 from backing and 1 from malus so 2 in total)
|
||||
//!
|
||||
//!
|
||||
//! Attention: For usage with `zombienet` only!
|
||||
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use futures::channel::oneshot;
|
||||
use pezkuwi_cli::{
|
||||
service::{
|
||||
AuxStore, Error, ExtendedOverseerGenArgs, Overseer, OverseerConnector, OverseerGen,
|
||||
OverseerGenArgs, OverseerHandle,
|
||||
},
|
||||
validator_overseer_builder, Cli,
|
||||
};
|
||||
use pezkuwi_node_subsystem::SpawnGlue;
|
||||
use pezkuwi_node_subsystem_types::{ChainApiBackend, OverseerSignal, RuntimeApiSubsystemClient};
|
||||
use pezkuwi_node_subsystem_util::request_candidate_events;
|
||||
use pezkuwi_primitives::CandidateEvent;
|
||||
use sp_core::traits::SpawnNamed;
|
||||
|
||||
// Filter wrapping related types.
|
||||
use crate::{interceptor::*, shared::MALUS};
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Wraps around ApprovalVotingSubsystem and replaces it.
|
||||
/// Listens to finalization messages and if possible triggers disputes for their ancestors.
|
||||
#[derive(Clone)]
|
||||
struct AncestorDisputer<Spawner> {
|
||||
spawner: Spawner, //stores the actual ApprovalVotingSubsystem spawner
|
||||
dispute_offset: u32, /* relative depth of the disputed block to the finalized block,
|
||||
* 0=finalized, 1=parent of finalized etc */
|
||||
}
|
||||
|
||||
impl<Sender, Spawner> MessageInterceptor<Sender> for AncestorDisputer<Spawner>
|
||||
where
|
||||
Sender: overseer::ApprovalVotingSenderTrait + Clone + Send + 'static,
|
||||
Spawner: overseer::gen::Spawner + Clone + 'static,
|
||||
{
|
||||
type Message = ApprovalVotingMessage;
|
||||
|
||||
/// Intercept incoming `OverseerSignal::BlockFinalized' and pass the rest as normal.
|
||||
fn intercept_incoming(
|
||||
&self,
|
||||
subsystem_sender: &mut Sender,
|
||||
msg: FromOrchestra<Self::Message>,
|
||||
) -> Option<FromOrchestra<Self::Message>> {
|
||||
match msg {
|
||||
FromOrchestra::Communication { msg } => Some(FromOrchestra::Communication { msg }),
|
||||
FromOrchestra::Signal(OverseerSignal::BlockFinalized(
|
||||
finalized_hash,
|
||||
finalized_height,
|
||||
)) => {
|
||||
gum::debug!(
|
||||
target: MALUS,
|
||||
"😈 Block Finalization Interception! Block: {:?}", finalized_hash,
|
||||
);
|
||||
|
||||
//Ensure that the chain is long enough for the target ancestor to exist
|
||||
if finalized_height <= self.dispute_offset {
|
||||
return Some(FromOrchestra::Signal(OverseerSignal::BlockFinalized(
|
||||
finalized_hash,
|
||||
finalized_height,
|
||||
)));
|
||||
}
|
||||
|
||||
let dispute_offset = self.dispute_offset;
|
||||
let mut sender = subsystem_sender.clone();
|
||||
self.spawner.spawn_blocking(
|
||||
"malus-dispute-finalized-block",
|
||||
Some("malus"),
|
||||
Box::pin(async move {
|
||||
// Query chain for the block hash at the target depth
|
||||
let (tx, rx) = oneshot::channel();
|
||||
sender
|
||||
.send_message(ChainApiMessage::FinalizedBlockHash(
|
||||
finalized_height - dispute_offset,
|
||||
tx,
|
||||
))
|
||||
.await;
|
||||
let disputable_hash = match rx.await {
|
||||
Ok(Ok(Some(hash))) => {
|
||||
gum::debug!(
|
||||
target: MALUS,
|
||||
"😈 Time to search {:?}`th ancestor! Block: {:?}", dispute_offset, hash,
|
||||
);
|
||||
hash
|
||||
},
|
||||
_ => {
|
||||
gum::debug!(
|
||||
target: MALUS,
|
||||
"😈 Seems the target is not yet finalized! Nothing to dispute."
|
||||
);
|
||||
return; // Early return from the async block
|
||||
},
|
||||
};
|
||||
|
||||
// Fetch all candidate events for the target ancestor
|
||||
let events =
|
||||
request_candidate_events(disputable_hash, &mut sender).await.await;
|
||||
let events = match events {
|
||||
Ok(Ok(events)) => events,
|
||||
Ok(Err(e)) => {
|
||||
gum::error!(
|
||||
target: MALUS,
|
||||
"😈 Failed to fetch candidate events: {:?}", e
|
||||
);
|
||||
return; // Early return from the async block
|
||||
},
|
||||
Err(e) => {
|
||||
gum::error!(
|
||||
target: MALUS,
|
||||
"😈 Failed to fetch candidate events: {:?}", e
|
||||
);
|
||||
return; // Early return from the async block
|
||||
},
|
||||
};
|
||||
|
||||
// Extract a token candidate from the events to use for disputing
|
||||
let event = events.iter().find(|event| {
|
||||
matches!(event, CandidateEvent::CandidateIncluded(_, _, _, _))
|
||||
});
|
||||
let candidate = match event {
|
||||
Some(CandidateEvent::CandidateIncluded(candidate, _, _, _)) =>
|
||||
candidate,
|
||||
_ => {
|
||||
gum::error!(
|
||||
target: MALUS,
|
||||
"😈 No candidate included event found! Nothing to dispute."
|
||||
);
|
||||
return; // Early return from the async block
|
||||
},
|
||||
};
|
||||
|
||||
// Extract the candidate hash from the candidate
|
||||
let candidate_hash = candidate.hash();
|
||||
|
||||
// Fetch the session index for the candidate
|
||||
let (tx, rx) = oneshot::channel();
|
||||
sender
|
||||
.send_message(RuntimeApiMessage::Request(
|
||||
disputable_hash,
|
||||
RuntimeApiRequest::SessionIndexForChild(tx),
|
||||
))
|
||||
.await;
|
||||
let session_index = match rx.await {
|
||||
Ok(Ok(session_index)) => session_index,
|
||||
_ => {
|
||||
gum::error!(
|
||||
target: MALUS,
|
||||
"😈 Failed to fetch session index for candidate."
|
||||
);
|
||||
return; // Early return from the async block
|
||||
},
|
||||
};
|
||||
gum::info!(
|
||||
target: MALUS,
|
||||
"😈 Disputing candidate with hash: {:?} in session {:?}", candidate_hash, session_index,
|
||||
);
|
||||
|
||||
// Start dispute
|
||||
sender.send_unbounded_message(
|
||||
DisputeCoordinatorMessage::IssueLocalStatement(
|
||||
session_index,
|
||||
candidate_hash,
|
||||
candidate.clone(),
|
||||
false, // indicates candidate is invalid -> dispute starts
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
// Passthrough the finalization signal as usual (using it as hook only)
|
||||
Some(FromOrchestra::Signal(OverseerSignal::BlockFinalized(
|
||||
finalized_hash,
|
||||
finalized_height,
|
||||
)))
|
||||
},
|
||||
FromOrchestra::Signal(signal) => Some(FromOrchestra::Signal(signal)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, clap::Parser)]
|
||||
#[clap(rename_all = "kebab-case")]
|
||||
#[allow(missing_docs)]
|
||||
pub struct DisputeFinalizedCandidatesOptions {
|
||||
/// relative depth of the disputed block to the finalized block, 0=finalized, 1=parent of
|
||||
/// finalized etc
|
||||
#[clap(long, ignore_case = true, default_value_t = 2, value_parser = clap::value_parser!(u32).range(0..=50))]
|
||||
pub dispute_offset: u32,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub cli: Cli,
|
||||
}
|
||||
|
||||
/// DisputeFinalizedCandidates implementation wrapper which implements `OverseerGen` glue.
|
||||
pub(crate) struct DisputeFinalizedCandidates {
|
||||
/// relative depth of the disputed block to the finalized block, 0=finalized, 1=parent of
|
||||
/// finalized etc
|
||||
pub dispute_offset: u32,
|
||||
}
|
||||
|
||||
impl OverseerGen for DisputeFinalizedCandidates {
|
||||
fn generate<Spawner, RuntimeClient>(
|
||||
&self,
|
||||
connector: OverseerConnector,
|
||||
args: OverseerGenArgs<'_, Spawner, RuntimeClient>,
|
||||
ext_args: Option<ExtendedOverseerGenArgs>,
|
||||
) -> Result<(Overseer<SpawnGlue<Spawner>, Arc<RuntimeClient>>, OverseerHandle), Error>
|
||||
where
|
||||
RuntimeClient: RuntimeApiSubsystemClient + ChainApiBackend + AuxStore + 'static,
|
||||
Spawner: 'static + SpawnNamed + Clone + Unpin,
|
||||
{
|
||||
gum::info!(
|
||||
target: MALUS,
|
||||
"😈 Started Malus node that disputes finalized blocks after they are {:?} finalizations deep.",
|
||||
&self.dispute_offset,
|
||||
);
|
||||
|
||||
let ancestor_disputer = AncestorDisputer {
|
||||
spawner: SpawnGlue(args.spawner.clone()),
|
||||
dispute_offset: self.dispute_offset,
|
||||
};
|
||||
|
||||
validator_overseer_builder(
|
||||
args,
|
||||
ext_args.expect("Extended arguments required to build validator overseer are provided"),
|
||||
)?
|
||||
.replace_approval_voting(move |cb| InterceptedSubsystem::new(cb, ancestor_disputer))
|
||||
.build_with_connector(connector)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi 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.
|
||||
|
||||
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! A malicious node that replaces approvals with invalid disputes
|
||||
//! against valid candidates. Additionally, the malus node can be configured to
|
||||
//! fake candidate validation and return a static result for candidate checking.
|
||||
//!
|
||||
//! Attention: For usage with `zombienet` only!
|
||||
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use pezkuwi_cli::{
|
||||
service::{
|
||||
AuxStore, Error, ExtendedOverseerGenArgs, Overseer, OverseerConnector, OverseerGen,
|
||||
OverseerGenArgs, OverseerHandle,
|
||||
},
|
||||
validator_overseer_builder, Cli,
|
||||
};
|
||||
use pezkuwi_node_subsystem::SpawnGlue;
|
||||
use pezkuwi_node_subsystem_types::{ChainApiBackend, RuntimeApiSubsystemClient};
|
||||
use sp_core::traits::SpawnNamed;
|
||||
|
||||
// Filter wrapping related types.
|
||||
use super::common::{FakeCandidateValidation, FakeCandidateValidationError};
|
||||
use crate::{interceptor::*, variants::ReplaceValidationResult};
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, clap::Parser)]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
#[allow(missing_docs)]
|
||||
pub struct DisputeAncestorOptions {
|
||||
/// Malicious candidate validation subsystem configuration. When enabled, node PVF execution is
|
||||
/// skipped during backing and/or approval and it's result can by specified by this option and
|
||||
/// `--fake-validation-error` for invalid candidate outcomes.
|
||||
#[arg(long, value_enum, ignore_case = true, default_value_t = FakeCandidateValidation::BackingAndApprovalInvalid)]
|
||||
pub fake_validation: FakeCandidateValidation,
|
||||
|
||||
/// Applies only when `--fake-validation` is configured to reject candidates as invalid. It
|
||||
/// allows to specify the exact error to return from the malicious candidate validation
|
||||
/// subsystem.
|
||||
#[arg(long, value_enum, ignore_case = true, default_value_t = FakeCandidateValidationError::InvalidOutputs)]
|
||||
pub fake_validation_error: FakeCandidateValidationError,
|
||||
|
||||
/// Determines the percentage of candidates that should be disputed. Allows for fine-tuning
|
||||
/// the intensity of the behavior of the malicious node. Value must be in the range [0..=100].
|
||||
#[clap(short, long, ignore_case = true, default_value_t = 100, value_parser = clap::value_parser!(u8).range(0..=100))]
|
||||
pub percentage: u8,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub cli: Cli,
|
||||
}
|
||||
|
||||
pub(crate) struct DisputeValidCandidates {
|
||||
/// Fake validation config (applies to disputes as well).
|
||||
pub fake_validation: FakeCandidateValidation,
|
||||
/// Fake validation error config.
|
||||
pub fake_validation_error: FakeCandidateValidationError,
|
||||
/// The probability of behaving maliciously.
|
||||
pub percentage: u8,
|
||||
}
|
||||
|
||||
impl OverseerGen for DisputeValidCandidates {
|
||||
fn generate<Spawner, RuntimeClient>(
|
||||
&self,
|
||||
connector: OverseerConnector,
|
||||
args: OverseerGenArgs<'_, Spawner, RuntimeClient>,
|
||||
ext_args: Option<ExtendedOverseerGenArgs>,
|
||||
) -> Result<(Overseer<SpawnGlue<Spawner>, Arc<RuntimeClient>>, OverseerHandle), Error>
|
||||
where
|
||||
RuntimeClient: RuntimeApiSubsystemClient + ChainApiBackend + AuxStore + 'static,
|
||||
Spawner: 'static + SpawnNamed + Clone + Unpin,
|
||||
{
|
||||
let validation_filter = ReplaceValidationResult::new(
|
||||
self.fake_validation,
|
||||
self.fake_validation_error,
|
||||
f64::from(self.percentage),
|
||||
);
|
||||
|
||||
validator_overseer_builder(
|
||||
args,
|
||||
ext_args.expect("Extended arguments required to build validator overseer are provided"),
|
||||
)?
|
||||
.replace_candidate_validation(move |cv_subsystem| {
|
||||
InterceptedSubsystem::new(cv_subsystem, validation_filter)
|
||||
})
|
||||
.build_with_connector(connector)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi 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.
|
||||
|
||||
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Collection of behavior variants.
|
||||
|
||||
mod back_garbage_candidate;
|
||||
mod common;
|
||||
mod dispute_finalized_candidates;
|
||||
mod dispute_valid_candidates;
|
||||
mod spam_statement_requests;
|
||||
mod suggest_garbage_candidate;
|
||||
mod support_disabled;
|
||||
|
||||
pub(crate) use self::{
|
||||
back_garbage_candidate::{BackGarbageCandidateOptions, BackGarbageCandidates},
|
||||
dispute_finalized_candidates::{DisputeFinalizedCandidates, DisputeFinalizedCandidatesOptions},
|
||||
dispute_valid_candidates::{DisputeAncestorOptions, DisputeValidCandidates},
|
||||
spam_statement_requests::{SpamStatementRequests, SpamStatementRequestsOptions},
|
||||
suggest_garbage_candidate::{SuggestGarbageCandidateOptions, SuggestGarbageCandidates},
|
||||
support_disabled::{SupportDisabled, SupportDisabledOptions},
|
||||
};
|
||||
pub(crate) use common::*;
|
||||
@@ -0,0 +1,155 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi 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.
|
||||
|
||||
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! A malicious node variant that attempts spam statement requests.
|
||||
//!
|
||||
//! This malus variant behaves honestly in everything except when propagating statement distribution
|
||||
//! requests through the network bridge subsystem. Instead of sending a single request when it needs
|
||||
//! something it attempts to spam the peer with multiple requests.
|
||||
//!
|
||||
//! Attention: For usage with `zombienet` only!
|
||||
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use pezkuwi_cli::{
|
||||
service::{
|
||||
AuxStore, Error, ExtendedOverseerGenArgs, Overseer, OverseerConnector, OverseerGen,
|
||||
OverseerGenArgs, OverseerHandle,
|
||||
},
|
||||
validator_overseer_builder, Cli,
|
||||
};
|
||||
use pezkuwi_node_network_protocol::request_response::{outgoing::Requests, OutgoingRequest};
|
||||
use pezkuwi_node_subsystem::{messages::NetworkBridgeTxMessage, SpawnGlue};
|
||||
use pezkuwi_node_subsystem_types::{ChainApiBackend, RuntimeApiSubsystemClient};
|
||||
use sp_core::traits::SpawnNamed;
|
||||
|
||||
// Filter wrapping related types.
|
||||
use crate::{interceptor::*, shared::MALUS};
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Wraps around network bridge and replaces it.
|
||||
#[derive(Clone)]
|
||||
struct RequestSpammer {
|
||||
spam_factor: u32, // How many statement distribution requests to send.
|
||||
}
|
||||
|
||||
impl<Sender> MessageInterceptor<Sender> for RequestSpammer
|
||||
where
|
||||
Sender: overseer::NetworkBridgeTxSenderTrait + Clone + Send + 'static,
|
||||
{
|
||||
type Message = NetworkBridgeTxMessage;
|
||||
|
||||
/// Intercept NetworkBridgeTxMessage::SendRequests with Requests::AttestedCandidateV2 inside and
|
||||
/// duplicate that request
|
||||
fn intercept_incoming(
|
||||
&self,
|
||||
_subsystem_sender: &mut Sender,
|
||||
msg: FromOrchestra<Self::Message>,
|
||||
) -> Option<FromOrchestra<Self::Message>> {
|
||||
match msg {
|
||||
FromOrchestra::Communication {
|
||||
msg: NetworkBridgeTxMessage::SendRequests(requests, if_disconnected),
|
||||
} => {
|
||||
let mut new_requests = Vec::new();
|
||||
|
||||
for request in requests {
|
||||
match request {
|
||||
Requests::AttestedCandidateV2(ref req) => {
|
||||
// Temporarily store peer and payload for duplication
|
||||
let peer_to_duplicate = req.peer.clone();
|
||||
let payload_to_duplicate = req.payload.clone();
|
||||
// Push the original request
|
||||
new_requests.push(request);
|
||||
|
||||
// Duplicate for spam purposes
|
||||
gum::info!(
|
||||
target: MALUS,
|
||||
"😈 Duplicating AttestedCandidateV2 request extra {:?} times to peer: {:?}.", self.spam_factor, peer_to_duplicate,
|
||||
);
|
||||
new_requests.extend((0..self.spam_factor - 1).map(|_| {
|
||||
let (new_outgoing_request, _) = OutgoingRequest::new(
|
||||
peer_to_duplicate.clone(),
|
||||
payload_to_duplicate.clone(),
|
||||
);
|
||||
Requests::AttestedCandidateV2(new_outgoing_request)
|
||||
}));
|
||||
},
|
||||
_ => {
|
||||
new_requests.push(request);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Passthrough the message with a potentially modified number of requests
|
||||
Some(FromOrchestra::Communication {
|
||||
msg: NetworkBridgeTxMessage::SendRequests(new_requests, if_disconnected),
|
||||
})
|
||||
},
|
||||
FromOrchestra::Communication { msg } => Some(FromOrchestra::Communication { msg }),
|
||||
FromOrchestra::Signal(signal) => Some(FromOrchestra::Signal(signal)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, clap::Parser)]
|
||||
#[clap(rename_all = "kebab-case")]
|
||||
#[allow(missing_docs)]
|
||||
pub struct SpamStatementRequestsOptions {
|
||||
/// How many statement distribution requests to send.
|
||||
#[clap(long, ignore_case = true, default_value_t = 1000, value_parser = clap::value_parser!(u32).range(0..=10000000))]
|
||||
pub spam_factor: u32,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub cli: Cli,
|
||||
}
|
||||
|
||||
/// SpamStatementRequests implementation wrapper which implements `OverseerGen` glue.
|
||||
pub(crate) struct SpamStatementRequests {
|
||||
/// How many statement distribution requests to send.
|
||||
pub spam_factor: u32,
|
||||
}
|
||||
|
||||
impl OverseerGen for SpamStatementRequests {
|
||||
fn generate<Spawner, RuntimeClient>(
|
||||
&self,
|
||||
connector: OverseerConnector,
|
||||
args: OverseerGenArgs<'_, Spawner, RuntimeClient>,
|
||||
ext_args: Option<ExtendedOverseerGenArgs>,
|
||||
) -> Result<(Overseer<SpawnGlue<Spawner>, Arc<RuntimeClient>>, OverseerHandle), Error>
|
||||
where
|
||||
RuntimeClient: RuntimeApiSubsystemClient + ChainApiBackend + AuxStore + 'static,
|
||||
Spawner: 'static + SpawnNamed + Clone + Unpin,
|
||||
{
|
||||
gum::info!(
|
||||
target: MALUS,
|
||||
"😈 Started Malus node that duplicates each statement distribution request spam_factor = {:?} times.",
|
||||
&self.spam_factor,
|
||||
);
|
||||
|
||||
let request_spammer = RequestSpammer { spam_factor: self.spam_factor };
|
||||
|
||||
validator_overseer_builder(
|
||||
args,
|
||||
ext_args.expect("Extended arguments required to build validator overseer are provided"),
|
||||
)?
|
||||
.replace_network_bridge_tx(move |cb| InterceptedSubsystem::new(cb, request_spammer))
|
||||
.build_with_connector(connector)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi 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.
|
||||
|
||||
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! A malicious node that stores bogus availability chunks, preventing others from
|
||||
//! doing approval voting. This should lead to disputes depending if the validator
|
||||
//! has fetched a malicious chunk.
|
||||
//!
|
||||
//! Attention: For usage with `zombienet` only!
|
||||
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use futures::channel::oneshot;
|
||||
use pezkuwi_cli::{
|
||||
service::{
|
||||
AuxStore, Error, ExtendedOverseerGenArgs, Overseer, OverseerConnector, OverseerGen,
|
||||
OverseerGenArgs, OverseerHandle,
|
||||
},
|
||||
validator_overseer_builder, Cli,
|
||||
};
|
||||
use pezkuwi_node_primitives::{AvailableData, BlockData, PoV};
|
||||
use pezkuwi_node_subsystem_types::{ChainApiBackend, RuntimeApiSubsystemClient};
|
||||
use pezkuwi_primitives::{CandidateDescriptorV2, CandidateReceiptV2, CoreIndex};
|
||||
|
||||
use pezkuwi_node_subsystem_util::request_validators;
|
||||
use sp_core::traits::SpawnNamed;
|
||||
|
||||
use rand::distributions::{Bernoulli, Distribution};
|
||||
|
||||
// Filter wrapping related types.
|
||||
use crate::{
|
||||
interceptor::*,
|
||||
shared::{MALICIOUS_POV, MALUS},
|
||||
variants::{
|
||||
create_fake_candidate_commitments, FakeCandidateValidation, FakeCandidateValidationError,
|
||||
ReplaceValidationResult,
|
||||
},
|
||||
};
|
||||
|
||||
// Import extra types relevant to the particular
|
||||
// subsystem.
|
||||
use pezkuwi_node_subsystem::SpawnGlue;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Replace outgoing approval messages with disputes.
|
||||
#[derive(Clone)]
|
||||
struct NoteCandidate<Spawner> {
|
||||
spawner: Spawner,
|
||||
percentage: f64,
|
||||
}
|
||||
|
||||
impl<Sender, Spawner> MessageInterceptor<Sender> for NoteCandidate<Spawner>
|
||||
where
|
||||
Sender: overseer::CandidateBackingSenderTrait + Clone + Send + 'static,
|
||||
Spawner: overseer::gen::Spawner + Clone + 'static,
|
||||
{
|
||||
type Message = CandidateBackingMessage;
|
||||
|
||||
/// Intercept incoming `Second` requests from the `collator-protocol` subsystem.
|
||||
fn intercept_incoming(
|
||||
&self,
|
||||
subsystem_sender: &mut Sender,
|
||||
msg: FromOrchestra<Self::Message>,
|
||||
) -> Option<FromOrchestra<Self::Message>> {
|
||||
match msg {
|
||||
FromOrchestra::Communication {
|
||||
msg:
|
||||
CandidateBackingMessage::Second(
|
||||
relay_parent,
|
||||
ref candidate,
|
||||
ref validation_data,
|
||||
ref _pov,
|
||||
),
|
||||
} => {
|
||||
gum::debug!(
|
||||
target: MALUS,
|
||||
candidate_hash = ?candidate.hash(),
|
||||
?relay_parent,
|
||||
"Received request to second candidate",
|
||||
);
|
||||
|
||||
// Need to draw value from Bernoulli distribution with given probability of success
|
||||
// defined by the clap parameter. Note that clap parameter must be f64 since this is
|
||||
// expected by the Bernoulli::new() function. It must be converted from u8, due to
|
||||
// the lack of support for the .range() call on u64 in the clap crate.
|
||||
let distribution = Bernoulli::new(self.percentage / 100.0)
|
||||
.expect("Invalid probability! Percentage must be in range [0..=100].");
|
||||
|
||||
// Draw a random boolean from the Bernoulli distribution with probability of true
|
||||
// equal to `p`. We use `rand::thread_rng` as the source of randomness.
|
||||
let generate_malicious_candidate = distribution.sample(&mut rand::thread_rng());
|
||||
|
||||
if generate_malicious_candidate {
|
||||
gum::debug!(target: MALUS, "😈 Suggesting malicious candidate.",);
|
||||
|
||||
let pov = PoV { block_data: BlockData(MALICIOUS_POV.into()) };
|
||||
|
||||
let (sender, receiver) = std::sync::mpsc::channel();
|
||||
let mut new_sender = subsystem_sender.clone();
|
||||
let _candidate = candidate.clone();
|
||||
let validation_data = validation_data.clone();
|
||||
self.spawner.spawn_blocking(
|
||||
"malus-get-validation-data",
|
||||
Some("malus"),
|
||||
Box::pin(async move {
|
||||
gum::trace!(target: MALUS, "Requesting validators");
|
||||
let n_validators = request_validators(relay_parent, &mut new_sender)
|
||||
.await
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.len();
|
||||
gum::trace!(target: MALUS, "Validators {}", n_validators);
|
||||
|
||||
let validation_code = {
|
||||
let validation_code_hash =
|
||||
_candidate.descriptor().validation_code_hash();
|
||||
let (tx, rx) = oneshot::channel();
|
||||
new_sender
|
||||
.send_message(RuntimeApiMessage::Request(
|
||||
relay_parent,
|
||||
RuntimeApiRequest::ValidationCodeByHash(
|
||||
validation_code_hash,
|
||||
tx,
|
||||
),
|
||||
))
|
||||
.await;
|
||||
|
||||
let code = rx.await.expect("Querying the RuntimeApi should work");
|
||||
match code {
|
||||
Err(e) => {
|
||||
gum::error!(
|
||||
target: MALUS,
|
||||
?validation_code_hash,
|
||||
error = %e,
|
||||
"Failed to fetch validation code",
|
||||
);
|
||||
|
||||
sender.send(None).expect("channel is still open");
|
||||
return;
|
||||
},
|
||||
Ok(None) => {
|
||||
gum::debug!(
|
||||
target: MALUS,
|
||||
?validation_code_hash,
|
||||
"Could not find validation code on chain",
|
||||
);
|
||||
|
||||
sender.send(None).expect("channel is still open");
|
||||
return;
|
||||
},
|
||||
Ok(Some(c)) => c,
|
||||
}
|
||||
};
|
||||
|
||||
sender
|
||||
.send(Some((validation_data, validation_code, n_validators)))
|
||||
.expect("channel is still open");
|
||||
}),
|
||||
);
|
||||
|
||||
let (validation_data, validation_code, n_validators) =
|
||||
receiver.recv().unwrap()?;
|
||||
|
||||
let validation_data_hash = validation_data.hash();
|
||||
let validation_code_hash = validation_code.hash();
|
||||
let validation_data_relay_parent_number = validation_data.relay_parent_number;
|
||||
|
||||
gum::trace!(
|
||||
target: MALUS,
|
||||
candidate_hash = ?candidate.hash(),
|
||||
?relay_parent,
|
||||
?n_validators,
|
||||
?validation_data_hash,
|
||||
?validation_code_hash,
|
||||
?validation_data_relay_parent_number,
|
||||
"Fetched validation data."
|
||||
);
|
||||
|
||||
let malicious_available_data = AvailableData {
|
||||
pov: Arc::new(pov.clone()),
|
||||
validation_data: validation_data.clone(),
|
||||
};
|
||||
|
||||
let pov_hash = pov.hash();
|
||||
let erasure_root = {
|
||||
let chunks = pezkuwi_erasure_coding::obtain_chunks_v1(
|
||||
n_validators as usize,
|
||||
&malicious_available_data,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let branches = pezkuwi_erasure_coding::branches(chunks.as_ref());
|
||||
branches.root()
|
||||
};
|
||||
|
||||
let malicious_commitments = create_fake_candidate_commitments(
|
||||
&malicious_available_data.validation_data,
|
||||
);
|
||||
|
||||
let malicious_candidate = CandidateReceiptV2 {
|
||||
descriptor: CandidateDescriptorV2::new(
|
||||
candidate.descriptor.para_id(),
|
||||
relay_parent,
|
||||
candidate.descriptor.core_index().unwrap_or(CoreIndex(0)),
|
||||
candidate.descriptor.session_index().unwrap_or(0),
|
||||
validation_data_hash,
|
||||
pov_hash,
|
||||
erasure_root,
|
||||
malicious_commitments.head_data.hash(),
|
||||
validation_code_hash,
|
||||
),
|
||||
commitments_hash: malicious_commitments.hash(),
|
||||
};
|
||||
let malicious_candidate_hash = malicious_candidate.hash();
|
||||
|
||||
let message = FromOrchestra::Communication {
|
||||
msg: CandidateBackingMessage::Second(
|
||||
relay_parent,
|
||||
malicious_candidate,
|
||||
validation_data,
|
||||
pov,
|
||||
),
|
||||
};
|
||||
|
||||
gum::info!(
|
||||
target: MALUS,
|
||||
candidate_hash = ?candidate.hash(),
|
||||
"😈 Intercepted CandidateBackingMessage::Second and created malicious candidate with hash: {:?}",
|
||||
&malicious_candidate_hash
|
||||
);
|
||||
Some(message)
|
||||
} else {
|
||||
Some(msg)
|
||||
}
|
||||
},
|
||||
FromOrchestra::Communication { msg } => Some(FromOrchestra::Communication { msg }),
|
||||
FromOrchestra::Signal(signal) => Some(FromOrchestra::Signal(signal)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, clap::Parser)]
|
||||
#[clap(rename_all = "kebab-case")]
|
||||
#[allow(missing_docs)]
|
||||
pub struct SuggestGarbageCandidateOptions {
|
||||
/// Determines the percentage of malicious candidates that are suggested by malus,
|
||||
/// based on the total number of intercepted CandidateBacking
|
||||
/// Must be in the range [0..=100].
|
||||
#[clap(short, long, ignore_case = true, default_value_t = 100, value_parser = clap::value_parser!(u8).range(0..=100))]
|
||||
pub percentage: u8,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub cli: Cli,
|
||||
}
|
||||
|
||||
/// Garbage candidate implementation wrapper which implements `OverseerGen` glue.
|
||||
pub(crate) struct SuggestGarbageCandidates {
|
||||
/// The probability of behaving maliciously.
|
||||
pub percentage: u8,
|
||||
}
|
||||
|
||||
impl OverseerGen for SuggestGarbageCandidates {
|
||||
fn generate<Spawner, RuntimeClient>(
|
||||
&self,
|
||||
connector: OverseerConnector,
|
||||
args: OverseerGenArgs<'_, Spawner, RuntimeClient>,
|
||||
ext_args: Option<ExtendedOverseerGenArgs>,
|
||||
) -> Result<(Overseer<SpawnGlue<Spawner>, Arc<RuntimeClient>>, OverseerHandle), Error>
|
||||
where
|
||||
RuntimeClient: RuntimeApiSubsystemClient + ChainApiBackend + AuxStore + 'static,
|
||||
Spawner: 'static + SpawnNamed + Clone + Unpin,
|
||||
{
|
||||
gum::info!(
|
||||
target: MALUS,
|
||||
"😈 Started Malus node with a {:?} percent chance of behaving maliciously for a given candidate.",
|
||||
&self.percentage,
|
||||
);
|
||||
|
||||
let note_candidate = NoteCandidate {
|
||||
spawner: SpawnGlue(args.spawner.clone()),
|
||||
percentage: f64::from(self.percentage),
|
||||
};
|
||||
let fake_valid_probability = 100.0;
|
||||
let validation_filter = ReplaceValidationResult::new(
|
||||
FakeCandidateValidation::BackingAndApprovalValid,
|
||||
FakeCandidateValidationError::InvalidOutputs,
|
||||
fake_valid_probability,
|
||||
);
|
||||
|
||||
validator_overseer_builder(
|
||||
args,
|
||||
ext_args.expect("Extended arguments required to build validator overseer are provided"),
|
||||
)?
|
||||
.replace_candidate_backing(move |cb| InterceptedSubsystem::new(cb, note_candidate))
|
||||
.replace_candidate_validation(move |cb| InterceptedSubsystem::new(cb, validation_filter))
|
||||
.build_with_connector(connector)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi 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.
|
||||
|
||||
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! This variant of Malus overrides the `disabled_validators` runtime API
|
||||
//! to always return an empty set of disabled validators.
|
||||
|
||||
use pezkuwi_cli::{
|
||||
service::{
|
||||
AuxStore, Error, ExtendedOverseerGenArgs, Overseer, OverseerConnector, OverseerGen,
|
||||
OverseerGenArgs, OverseerHandle,
|
||||
},
|
||||
validator_overseer_builder, Cli,
|
||||
};
|
||||
use pezkuwi_node_subsystem::SpawnGlue;
|
||||
use pezkuwi_node_subsystem_types::{ChainApiBackend, RuntimeApiSubsystemClient};
|
||||
use sp_core::traits::SpawnNamed;
|
||||
|
||||
use crate::interceptor::*;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, clap::Parser)]
|
||||
#[clap(rename_all = "kebab-case")]
|
||||
#[allow(missing_docs)]
|
||||
pub struct SupportDisabledOptions {
|
||||
#[clap(flatten)]
|
||||
pub cli: Cli,
|
||||
}
|
||||
|
||||
/// Generates an overseer with a custom runtime API subsystem.
|
||||
pub(crate) struct SupportDisabled;
|
||||
|
||||
impl OverseerGen for SupportDisabled {
|
||||
fn generate<Spawner, RuntimeClient>(
|
||||
&self,
|
||||
connector: OverseerConnector,
|
||||
args: OverseerGenArgs<'_, Spawner, RuntimeClient>,
|
||||
ext_args: Option<ExtendedOverseerGenArgs>,
|
||||
) -> Result<(Overseer<SpawnGlue<Spawner>, Arc<RuntimeClient>>, OverseerHandle), Error>
|
||||
where
|
||||
RuntimeClient: RuntimeApiSubsystemClient + ChainApiBackend + AuxStore + 'static,
|
||||
Spawner: 'static + SpawnNamed + Clone + Unpin,
|
||||
{
|
||||
validator_overseer_builder(
|
||||
args,
|
||||
ext_args.expect("Extended arguments required to build validator overseer are provided"),
|
||||
)?
|
||||
.replace_runtime_api(move |ra_subsystem| {
|
||||
InterceptedSubsystem::new(ra_subsystem, IgnoreDisabled)
|
||||
})
|
||||
.build_with_connector(connector)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct IgnoreDisabled;
|
||||
|
||||
impl<Sender> MessageInterceptor<Sender> for IgnoreDisabled
|
||||
where
|
||||
Sender: overseer::RuntimeApiSenderTrait + Clone + Send + 'static,
|
||||
{
|
||||
type Message = RuntimeApiMessage;
|
||||
|
||||
/// Intercept incoming runtime api requests.
|
||||
fn intercept_incoming(
|
||||
&self,
|
||||
_subsystem_sender: &mut Sender,
|
||||
msg: FromOrchestra<Self::Message>,
|
||||
) -> Option<FromOrchestra<Self::Message>> {
|
||||
match msg {
|
||||
FromOrchestra::Communication {
|
||||
msg:
|
||||
RuntimeApiMessage::Request(_relay_parent, RuntimeApiRequest::DisabledValidators(tx)),
|
||||
} => {
|
||||
let _ = tx.send(Ok(Vec::new()));
|
||||
None
|
||||
},
|
||||
FromOrchestra::Communication { msg } => Some(FromOrchestra::Communication { msg }),
|
||||
FromOrchestra::Signal(signal) => Some(FromOrchestra::Signal(signal)),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user