referrerpolicy=no-referrer-when-downgrade

malus/variants/
common.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//! Implements common code for nemesis. Currently, only `ReplaceValidationResult`
18//! interceptor is implemented.
19use crate::{
20	interceptor::*,
21	shared::{MALICIOUS_POV, MALUS},
22};
23
24use polkadot_node_primitives::{InvalidCandidate, ValidationResult};
25
26use polkadot_primitives::{
27	CandidateCommitments, CandidateDescriptorV2 as CandidateDescriptor,
28	CandidateReceiptV2 as CandidateReceipt, PersistedValidationData, PvfExecKind,
29};
30
31use futures::channel::oneshot;
32use rand::distributions::{Bernoulli, Distribution};
33
34#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq)]
35#[value(rename_all = "kebab-case")]
36#[non_exhaustive]
37pub enum FakeCandidateValidation {
38	Disabled,
39	BackingInvalid,
40	ApprovalInvalid,
41	BackingAndApprovalInvalid,
42	BackingValid,
43	ApprovalValid,
44	BackingAndApprovalValid,
45}
46
47impl FakeCandidateValidation {
48	fn misbehaves_valid(&self) -> bool {
49		use FakeCandidateValidation::*;
50
51		match *self {
52			BackingValid | ApprovalValid | BackingAndApprovalValid => true,
53			_ => false,
54		}
55	}
56
57	fn misbehaves_invalid(&self) -> bool {
58		use FakeCandidateValidation::*;
59
60		match *self {
61			BackingInvalid | ApprovalInvalid | BackingAndApprovalInvalid => true,
62			_ => false,
63		}
64	}
65
66	fn includes_backing(&self) -> bool {
67		use FakeCandidateValidation::*;
68
69		match *self {
70			BackingInvalid | BackingAndApprovalInvalid | BackingValid | BackingAndApprovalValid =>
71				true,
72			_ => false,
73		}
74	}
75
76	fn includes_approval(&self) -> bool {
77		use FakeCandidateValidation::*;
78
79		match *self {
80			ApprovalInvalid |
81			BackingAndApprovalInvalid |
82			ApprovalValid |
83			BackingAndApprovalValid => true,
84			_ => false,
85		}
86	}
87
88	fn should_misbehave(&self, timeout: PvfExecKind) -> bool {
89		match timeout {
90			PvfExecKind::Backing => self.includes_backing(),
91			PvfExecKind::Approval => self.includes_approval(),
92		}
93	}
94}
95
96/// Candidate invalidity details
97#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq)]
98#[value(rename_all = "kebab-case")]
99pub enum FakeCandidateValidationError {
100	/// Validation outputs check doesn't pass.
101	InvalidOutputs,
102	/// Failed to execute.`validate_block`. This includes function panicking.
103	ExecutionError,
104	/// Execution timeout.
105	Timeout,
106	/// Validation input is over the limit.
107	ParamsTooLarge,
108	/// Code size is over the limit.
109	CodeTooLarge,
110	/// PoV does not decompress correctly.
111	POVDecompressionFailure,
112	/// Validation function returned invalid data.
113	BadReturn,
114	/// Invalid relay chain parent.
115	BadParent,
116	/// POV hash does not match.
117	POVHashMismatch,
118	/// Bad collator signature.
119	BadSignature,
120	/// Para head hash does not match.
121	ParaHeadHashMismatch,
122	/// Validation code hash does not match.
123	CodeHashMismatch,
124}
125
126impl Into<InvalidCandidate> for FakeCandidateValidationError {
127	fn into(self) -> InvalidCandidate {
128		match self {
129			FakeCandidateValidationError::ExecutionError =>
130				InvalidCandidate::ExecutionError("Malus".into()),
131			FakeCandidateValidationError::InvalidOutputs => InvalidCandidate::InvalidOutputs,
132			FakeCandidateValidationError::Timeout => InvalidCandidate::Timeout,
133			FakeCandidateValidationError::ParamsTooLarge => InvalidCandidate::ParamsTooLarge(666),
134			FakeCandidateValidationError::CodeTooLarge => InvalidCandidate::CodeTooLarge(666),
135			FakeCandidateValidationError::POVDecompressionFailure =>
136				InvalidCandidate::PoVDecompressionFailure,
137			FakeCandidateValidationError::BadReturn => InvalidCandidate::BadReturn,
138			FakeCandidateValidationError::BadParent => InvalidCandidate::BadParent,
139			FakeCandidateValidationError::POVHashMismatch => InvalidCandidate::PoVHashMismatch,
140			FakeCandidateValidationError::BadSignature => InvalidCandidate::BadSignature,
141			FakeCandidateValidationError::ParaHeadHashMismatch =>
142				InvalidCandidate::ParaHeadHashMismatch,
143			FakeCandidateValidationError::CodeHashMismatch => InvalidCandidate::CodeHashMismatch,
144		}
145	}
146}
147
148#[derive(Clone, Debug)]
149/// An interceptor which fakes validation result with a preconfigured result.
150/// Replaces `CandidateValidationSubsystem`.
151pub struct ReplaceValidationResult {
152	fake_validation: FakeCandidateValidation,
153	fake_validation_error: FakeCandidateValidationError,
154	distribution: Bernoulli,
155}
156
157impl ReplaceValidationResult {
158	pub fn new(
159		fake_validation: FakeCandidateValidation,
160		fake_validation_error: FakeCandidateValidationError,
161		percentage: f64,
162	) -> Self {
163		let distribution = Bernoulli::new(percentage / 100.0)
164			.expect("Invalid probability! Percentage must be in range [0..=100].");
165		Self { fake_validation, fake_validation_error, distribution }
166	}
167}
168
169pub fn create_fake_candidate_commitments(
170	persisted_validation_data: &PersistedValidationData,
171) -> CandidateCommitments {
172	// Backing rejects candidates which output the same head as the parent,
173	// therefore we must create a new head which is not equal to the parent.
174	let mut head_data = persisted_validation_data.parent_head.clone();
175	if head_data.0.is_empty() {
176		head_data.0.push(0);
177	} else {
178		head_data.0[0] = head_data.0[0].wrapping_add(1);
179	};
180
181	CandidateCommitments {
182		upward_messages: Default::default(),
183		horizontal_messages: Default::default(),
184		new_validation_code: None,
185		head_data,
186		processed_downward_messages: 0,
187		hrmp_watermark: persisted_validation_data.relay_parent_number,
188	}
189}
190
191// Create and send validation response. This function needs the persistent validation data.
192fn create_validation_response(
193	persisted_validation_data: PersistedValidationData,
194	descriptor: CandidateDescriptor,
195	response_sender: oneshot::Sender<Result<ValidationResult, ValidationFailed>>,
196) {
197	let commitments = create_fake_candidate_commitments(&persisted_validation_data);
198
199	// Craft the new malicious candidate.
200	let candidate_receipt = CandidateReceipt { descriptor, commitments_hash: commitments.hash() };
201
202	let result = Ok(ValidationResult::Valid(commitments, persisted_validation_data));
203
204	gum::debug!(
205		target: MALUS,
206		para_id = ?candidate_receipt.descriptor.para_id(),
207		candidate_hash = ?candidate_receipt.hash(),
208		"ValidationResult: {:?}",
209		&result
210	);
211
212	response_sender.send(result).unwrap();
213}
214
215impl<Sender> MessageInterceptor<Sender> for ReplaceValidationResult
216where
217	Sender: overseer::CandidateValidationSenderTrait + Clone + Send + 'static,
218{
219	type Message = CandidateValidationMessage;
220
221	// Capture all (approval and backing) candidate validation requests and depending on
222	// configuration fail them.
223	fn intercept_incoming(
224		&self,
225		_subsystem_sender: &mut Sender,
226		msg: FromOrchestra<Self::Message>,
227	) -> Option<FromOrchestra<Self::Message>> {
228		match msg {
229			// Message sent by the approval voting subsystem
230			FromOrchestra::Communication {
231				msg:
232					CandidateValidationMessage::ValidateFromExhaustive {
233						validation_data,
234						validation_code,
235						candidate_receipt,
236						pov,
237						executor_params,
238						exec_kind,
239						response_sender,
240						..
241					},
242			} => {
243				match self.fake_validation {
244					x if x.misbehaves_valid() && x.should_misbehave(exec_kind.into()) => {
245						// Behave normally if the `PoV` is not known to be malicious.
246						if pov.block_data.0.as_slice() != MALICIOUS_POV {
247							return Some(FromOrchestra::Communication {
248								msg: CandidateValidationMessage::ValidateFromExhaustive {
249									validation_data,
250									validation_code,
251									candidate_receipt,
252									pov,
253									executor_params,
254									exec_kind,
255									response_sender,
256								},
257							})
258						}
259						// Create the fake response with probability `p` if the `PoV` is malicious,
260						// where 'p' defaults to 100% for suggest-garbage-candidate variant.
261						let behave_maliciously = self.distribution.sample(&mut rand::thread_rng());
262						match behave_maliciously {
263							true => {
264								gum::info!(
265									target: MALUS,
266									?behave_maliciously,
267									"๐Ÿ˜ˆ Creating malicious ValidationResult::Valid message with fake candidate commitments.",
268								);
269
270								create_validation_response(
271									validation_data,
272									candidate_receipt.descriptor,
273									response_sender,
274								);
275								None
276							},
277							false => {
278								// Behave normally with probability `(1-p)` for a malicious `PoV`.
279								gum::info!(
280									target: MALUS,
281									?behave_maliciously,
282									"๐Ÿ˜ˆ Passing CandidateValidationMessage::ValidateFromExhaustive to the candidate validation subsystem.",
283								);
284
285								Some(FromOrchestra::Communication {
286									msg: CandidateValidationMessage::ValidateFromExhaustive {
287										validation_data,
288										validation_code,
289										candidate_receipt,
290										pov,
291										executor_params,
292										exec_kind,
293										response_sender,
294									},
295								})
296							},
297						}
298					},
299					x if x.misbehaves_invalid() && x.should_misbehave(exec_kind.into()) => {
300						// Set the validation result to invalid with probability `p` and trigger a
301						// dispute
302						let behave_maliciously = self.distribution.sample(&mut rand::thread_rng());
303						match behave_maliciously {
304							true => {
305								let validation_result =
306									ValidationResult::Invalid(self.fake_validation_error.into());
307
308								gum::info!(
309									target: MALUS,
310									?behave_maliciously,
311									para_id = ?candidate_receipt.descriptor.para_id(),
312									"๐Ÿ˜ˆ Maliciously sending invalid validation result: {:?}.",
313									&validation_result,
314								);
315
316								// We're not even checking the candidate, this makes us appear
317								// faster than honest validators.
318								response_sender.send(Ok(validation_result)).unwrap();
319								None
320							},
321							false => {
322								// Behave normally with probability `(1-p)`
323								gum::info!(target: MALUS, "๐Ÿ˜ˆ 'Decided' to not act maliciously.",);
324
325								Some(FromOrchestra::Communication {
326									msg: CandidateValidationMessage::ValidateFromExhaustive {
327										validation_data,
328										validation_code,
329										candidate_receipt,
330										pov,
331										executor_params,
332										exec_kind,
333										response_sender,
334									},
335								})
336							},
337						}
338					},
339					// Handle FakeCandidateValidation::Disabled
340					_ => Some(FromOrchestra::Communication {
341						msg: CandidateValidationMessage::ValidateFromExhaustive {
342							validation_data,
343							validation_code,
344							candidate_receipt,
345							pov,
346							executor_params,
347							exec_kind,
348							response_sender,
349						},
350					}),
351				}
352			},
353			msg => Some(msg),
354		}
355	}
356}