referrerpolicy=no-referrer-when-downgrade
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Polkadot.

// Polkadot 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.

// Polkadot 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 Polkadot.  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 polkadot_cli::{
	service::{
		AuxStore, Error, ExtendedOverseerGenArgs, Overseer, OverseerConnector, OverseerGen,
		OverseerGenArgs, OverseerHandle,
	},
	validator_overseer_builder, Cli,
};
use polkadot_node_subsystem::SpawnGlue;
use polkadot_node_subsystem_types::{ChainApiBackend, OverseerSignal, RuntimeApiSubsystemClient};
use polkadot_node_subsystem_util::request_candidate_events;
use polkadot_primitives::vstaging::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())
	}
}