referrerpolicy=no-referrer-when-downgrade

malus/variants/
suggest_garbage_candidate.rs

1// Copyright (C) Parity Technologies (UK) Ltd.
2// This file is part of Polkadot.
3
4// Polkadot is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// Polkadot is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
16
17//! A malicious node that stores bogus availability chunks, preventing others from
18//! doing approval voting. This should lead to disputes depending if the validator
19//! has fetched a malicious chunk.
20//!
21//! Attention: For usage with `zombienet` only!
22
23#![allow(missing_docs)]
24
25use futures::channel::oneshot;
26use polkadot_cli::{
27	service::{
28		AuxStore, Error, ExtendedOverseerGenArgs, Overseer, OverseerConnector, OverseerGen,
29		OverseerGenArgs, OverseerHandle,
30	},
31	validator_overseer_builder, Cli,
32};
33use polkadot_node_primitives::{AvailableData, BlockData, PoV};
34use polkadot_node_subsystem_types::{ChainApiBackend, RuntimeApiSubsystemClient};
35use polkadot_primitives::{CandidateDescriptorV2, CandidateReceiptV2, CoreIndex};
36
37use polkadot_node_subsystem_util::request_validators;
38use sp_core::traits::SpawnNamed;
39
40use rand::distributions::{Bernoulli, Distribution};
41
42// Filter wrapping related types.
43use crate::{
44	interceptor::*,
45	shared::{MALICIOUS_POV, MALUS},
46	variants::{
47		create_fake_candidate_commitments, FakeCandidateValidation, FakeCandidateValidationError,
48		ReplaceValidationResult,
49	},
50};
51
52// Import extra types relevant to the particular
53// subsystem.
54use polkadot_node_subsystem::SpawnGlue;
55
56use std::sync::Arc;
57
58/// Replace outgoing approval messages with disputes.
59#[derive(Clone)]
60struct NoteCandidate<Spawner> {
61	spawner: Spawner,
62	percentage: f64,
63}
64
65impl<Sender, Spawner> MessageInterceptor<Sender> for NoteCandidate<Spawner>
66where
67	Sender: overseer::CandidateBackingSenderTrait + Clone + Send + 'static,
68	Spawner: overseer::gen::Spawner + Clone + 'static,
69{
70	type Message = CandidateBackingMessage;
71
72	/// Intercept incoming `Second` requests from the `collator-protocol` subsystem.
73	fn intercept_incoming(
74		&self,
75		subsystem_sender: &mut Sender,
76		msg: FromOrchestra<Self::Message>,
77	) -> Option<FromOrchestra<Self::Message>> {
78		match msg {
79			FromOrchestra::Communication {
80				msg:
81					CandidateBackingMessage::Second(
82						relay_parent,
83						ref candidate,
84						ref validation_data,
85						ref _pov,
86					),
87			} => {
88				gum::debug!(
89					target: MALUS,
90					candidate_hash = ?candidate.hash(),
91					?relay_parent,
92					"Received request to second candidate",
93				);
94
95				// Need to draw value from Bernoulli distribution with given probability of success
96				// defined by the clap parameter. Note that clap parameter must be f64 since this is
97				// expected by the Bernoulli::new() function. It must be converted from u8, due to
98				// the lack of support for the .range() call on u64 in the clap crate.
99				let distribution = Bernoulli::new(self.percentage / 100.0)
100					.expect("Invalid probability! Percentage must be in range [0..=100].");
101
102				// Draw a random boolean from the Bernoulli distribution with probability of true
103				// equal to `p`. We use `rand::thread_rng` as the source of randomness.
104				let generate_malicious_candidate = distribution.sample(&mut rand::thread_rng());
105
106				if generate_malicious_candidate {
107					gum::debug!(target: MALUS, "๐Ÿ˜ˆ Suggesting malicious candidate.",);
108
109					let pov = PoV { block_data: BlockData(MALICIOUS_POV.into()) };
110
111					let (sender, receiver) = std::sync::mpsc::channel();
112					let mut new_sender = subsystem_sender.clone();
113					let _candidate = candidate.clone();
114					let validation_data = validation_data.clone();
115					self.spawner.spawn_blocking(
116						"malus-get-validation-data",
117						Some("malus"),
118						Box::pin(async move {
119							gum::trace!(target: MALUS, "Requesting validators");
120							let n_validators = request_validators(relay_parent, &mut new_sender)
121								.await
122								.await
123								.unwrap()
124								.unwrap()
125								.len();
126							gum::trace!(target: MALUS, "Validators {}", n_validators);
127
128							let validation_code = {
129								let validation_code_hash =
130									_candidate.descriptor().validation_code_hash();
131								let (tx, rx) = oneshot::channel();
132								new_sender
133									.send_message(RuntimeApiMessage::Request(
134										relay_parent,
135										RuntimeApiRequest::ValidationCodeByHash(
136											validation_code_hash,
137											tx,
138										),
139									))
140									.await;
141
142								let code = rx.await.expect("Querying the RuntimeApi should work");
143								match code {
144									Err(e) => {
145										gum::error!(
146											target: MALUS,
147											?validation_code_hash,
148											error = %e,
149											"Failed to fetch validation code",
150										);
151
152										sender.send(None).expect("channel is still open");
153										return
154									},
155									Ok(None) => {
156										gum::debug!(
157											target: MALUS,
158											?validation_code_hash,
159											"Could not find validation code on chain",
160										);
161
162										sender.send(None).expect("channel is still open");
163										return
164									},
165									Ok(Some(c)) => c,
166								}
167							};
168
169							sender
170								.send(Some((validation_data, validation_code, n_validators)))
171								.expect("channel is still open");
172						}),
173					);
174
175					let (validation_data, validation_code, n_validators) =
176						receiver.recv().unwrap()?;
177
178					let validation_data_hash = validation_data.hash();
179					let validation_code_hash = validation_code.hash();
180					let validation_data_relay_parent_number = validation_data.relay_parent_number;
181
182					gum::trace!(
183						target: MALUS,
184						candidate_hash = ?candidate.hash(),
185						?relay_parent,
186						?n_validators,
187						?validation_data_hash,
188						?validation_code_hash,
189						?validation_data_relay_parent_number,
190						"Fetched validation data."
191					);
192
193					let malicious_available_data = AvailableData {
194						pov: Arc::new(pov.clone()),
195						validation_data: validation_data.clone(),
196					};
197
198					let pov_hash = pov.hash();
199					let erasure_root = {
200						let chunks = polkadot_erasure_coding::obtain_chunks_v1(
201							n_validators as usize,
202							&malicious_available_data,
203						)
204						.unwrap();
205
206						let branches = polkadot_erasure_coding::branches(chunks.as_ref());
207						branches.root()
208					};
209
210					let malicious_commitments = create_fake_candidate_commitments(
211						&malicious_available_data.validation_data,
212					);
213
214					let malicious_candidate = CandidateReceiptV2 {
215						descriptor: CandidateDescriptorV2::new(
216							candidate.descriptor.para_id(),
217							relay_parent,
218							candidate.descriptor.core_index().unwrap_or(CoreIndex(0)),
219							candidate.descriptor.session_index().unwrap_or(0),
220							validation_data_hash,
221							pov_hash,
222							erasure_root,
223							malicious_commitments.head_data.hash(),
224							validation_code_hash,
225						),
226						commitments_hash: malicious_commitments.hash(),
227					};
228					let malicious_candidate_hash = malicious_candidate.hash();
229
230					let message = FromOrchestra::Communication {
231						msg: CandidateBackingMessage::Second(
232							relay_parent,
233							malicious_candidate,
234							validation_data,
235							pov,
236						),
237					};
238
239					gum::info!(
240						target: MALUS,
241						candidate_hash = ?candidate.hash(),
242						"๐Ÿ˜ˆ Intercepted CandidateBackingMessage::Second and created malicious candidate with hash: {:?}",
243						&malicious_candidate_hash
244					);
245					Some(message)
246				} else {
247					Some(msg)
248				}
249			},
250			FromOrchestra::Communication { msg } => Some(FromOrchestra::Communication { msg }),
251			FromOrchestra::Signal(signal) => Some(FromOrchestra::Signal(signal)),
252		}
253	}
254}
255
256#[derive(Debug, clap::Parser)]
257#[clap(rename_all = "kebab-case")]
258#[allow(missing_docs)]
259pub struct SuggestGarbageCandidateOptions {
260	/// Determines the percentage of malicious candidates that are suggested by malus,
261	/// based on the total number of intercepted CandidateBacking
262	/// Must be in the range [0..=100].
263	#[clap(short, long, ignore_case = true, default_value_t = 100, value_parser = clap::value_parser!(u8).range(0..=100))]
264	pub percentage: u8,
265
266	#[clap(flatten)]
267	pub cli: Cli,
268}
269
270/// Garbage candidate implementation wrapper which implements `OverseerGen` glue.
271pub(crate) struct SuggestGarbageCandidates {
272	/// The probability of behaving maliciously.
273	pub percentage: u8,
274}
275
276impl OverseerGen for SuggestGarbageCandidates {
277	fn generate<Spawner, RuntimeClient>(
278		&self,
279		connector: OverseerConnector,
280		args: OverseerGenArgs<'_, Spawner, RuntimeClient>,
281		ext_args: Option<ExtendedOverseerGenArgs>,
282	) -> Result<(Overseer<SpawnGlue<Spawner>, Arc<RuntimeClient>>, OverseerHandle), Error>
283	where
284		RuntimeClient: RuntimeApiSubsystemClient + ChainApiBackend + AuxStore + 'static,
285		Spawner: 'static + SpawnNamed + Clone + Unpin,
286	{
287		gum::info!(
288			target: MALUS,
289			"๐Ÿ˜ˆ Started Malus node with a {:?} percent chance of behaving maliciously for a given candidate.",
290			&self.percentage,
291		);
292
293		let note_candidate = NoteCandidate {
294			spawner: SpawnGlue(args.spawner.clone()),
295			percentage: f64::from(self.percentage),
296		};
297		let fake_valid_probability = 100.0;
298		let validation_filter = ReplaceValidationResult::new(
299			FakeCandidateValidation::BackingAndApprovalValid,
300			FakeCandidateValidationError::InvalidOutputs,
301			fake_valid_probability,
302		);
303
304		validator_overseer_builder(
305			args,
306			ext_args.expect("Extended arguments required to build validator overseer are provided"),
307		)?
308		.replace_candidate_backing(move |cb| InterceptedSubsystem::new(cb, note_candidate))
309		.replace_candidate_validation(move |cb| InterceptedSubsystem::new(cb, validation_filter))
310		.build_with_connector(connector)
311		.map_err(|e| e.into())
312	}
313}