1#![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
42use crate::{
44 interceptor::*,
45 shared::{MALICIOUS_POV, MALUS},
46 variants::{
47 create_fake_candidate_commitments, FakeCandidateValidation, FakeCandidateValidationError,
48 ReplaceValidationResult,
49 },
50};
51
52use polkadot_node_subsystem::SpawnGlue;
55
56use std::sync::Arc;
57
58#[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 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 let distribution = Bernoulli::new(self.percentage / 100.0)
100 .expect("Invalid probability! Percentage must be in range [0..=100].");
101
102 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 #[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
270pub(crate) struct SuggestGarbageCandidates {
272 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}