feat: initialize Kurdistan SDK - independent fork of Polkadot SDK

This commit is contained in:
2025-12-13 15:44:15 +03:00
commit e4778b4576
6838 changed files with 1847450 additions and 0 deletions
+333
View File
@@ -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,
)
}
}
+243
View File
@@ -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);
});
}
}
+49
View File
@@ -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;
}),
);
}
+140
View File
@@ -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())
}
}
+356
View File
@@ -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())
}
}
+35
View File
@@ -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)),
}
}
}