feat: initialize Kurdistan SDK - independent fork of Polkadot SDK

This commit is contained in:
2025-12-13 15:44:15 +03:00
commit 286de54384
6841 changed files with 1848356 additions and 0 deletions
@@ -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)),
}
}
}