referrerpolicy=no-referrer-when-downgrade

equivocation_detector/
block_checker.rs

1// Copyright (C) Parity Technologies (UK) Ltd.
2// This file is part of Parity Bridges Common.
3
4// Parity Bridges Common 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// Parity Bridges Common 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 Parity Bridges Common.  If not, see <http://www.gnu.org/licenses/>.
16
17use crate::{
18	handle_client_error, reporter::EquivocationsReporter, EquivocationDetectionPipeline,
19	EquivocationReportingContext, HeaderFinalityInfo, SourceClient, TargetClient,
20};
21
22use bp_header_chain::{FinalityProof, FindEquivocations as FindEquivocationsT};
23use finality_relay::FinalityProofsBuf;
24use futures::future::{BoxFuture, FutureExt};
25use num_traits::Saturating;
26
27/// First step in the block checking state machine.
28///
29/// Getting the finality info associated to the source headers synced with the target chain
30/// at the specified block.
31#[cfg_attr(test, derive(Debug, PartialEq))]
32pub struct ReadSyncedHeaders<P: EquivocationDetectionPipeline> {
33	pub target_block_num: P::TargetNumber,
34}
35
36impl<P: EquivocationDetectionPipeline> ReadSyncedHeaders<P> {
37	pub async fn next<TC: TargetClient<P>>(
38		self,
39		target_client: &mut TC,
40	) -> Result<ReadContext<P>, Self> {
41		match target_client.synced_headers_finality_info(self.target_block_num).await {
42			Ok(synced_headers) =>
43				Ok(ReadContext { target_block_num: self.target_block_num, synced_headers }),
44			Err(e) => {
45				log::error!(
46					target: "bridge",
47					"Could not get {} headers synced to {} at block {}: {e:?}",
48					P::SOURCE_NAME,
49					P::TARGET_NAME,
50					self.target_block_num
51				);
52
53				// Reconnect target client in case of a connection error.
54				handle_client_error(target_client, e).await;
55
56				Err(self)
57			},
58		}
59	}
60}
61
62/// Second step in the block checking state machine.
63///
64/// Reading the equivocation reporting context from the target chain.
65#[cfg_attr(test, derive(Debug))]
66pub struct ReadContext<P: EquivocationDetectionPipeline> {
67	target_block_num: P::TargetNumber,
68	synced_headers: Vec<HeaderFinalityInfo<P>>,
69}
70
71impl<P: EquivocationDetectionPipeline> ReadContext<P> {
72	pub async fn next<TC: TargetClient<P>>(
73		self,
74		target_client: &mut TC,
75	) -> Result<Option<FindEquivocations<P>>, Self> {
76		match EquivocationReportingContext::try_read_from_target::<TC>(
77			target_client,
78			self.target_block_num.saturating_sub(1.into()),
79		)
80		.await
81		{
82			Ok(Some(context)) => Ok(Some(FindEquivocations {
83				target_block_num: self.target_block_num,
84				synced_headers: self.synced_headers,
85				context,
86			})),
87			Ok(None) => Ok(None),
88			Err(e) => {
89				log::error!(
90					target: "bridge",
91					"Could not read {} `EquivocationReportingContext` from {} at block {}: {e:?}",
92					P::SOURCE_NAME,
93					P::TARGET_NAME,
94					self.target_block_num.saturating_sub(1.into()),
95				);
96
97				// Reconnect target client in case of a connection error.
98				handle_client_error(target_client, e).await;
99
100				Err(self)
101			},
102		}
103	}
104}
105
106/// Third step in the block checking state machine.
107///
108/// Searching for equivocations in the source headers synced with the target chain.
109#[cfg_attr(test, derive(Debug))]
110pub struct FindEquivocations<P: EquivocationDetectionPipeline> {
111	target_block_num: P::TargetNumber,
112	synced_headers: Vec<HeaderFinalityInfo<P>>,
113	context: EquivocationReportingContext<P>,
114}
115
116impl<P: EquivocationDetectionPipeline> FindEquivocations<P> {
117	pub fn next(
118		mut self,
119		finality_proofs_buf: &mut FinalityProofsBuf<P>,
120	) -> Vec<ReportEquivocations<P>> {
121		let mut result = vec![];
122		for synced_header in self.synced_headers {
123			match P::EquivocationsFinder::find_equivocations(
124				&self.context.synced_verification_context,
125				&synced_header.finality_proof,
126				finality_proofs_buf.buf().as_slice(),
127			) {
128				Ok(equivocations) =>
129					if !equivocations.is_empty() {
130						result.push(ReportEquivocations {
131							source_block_hash: self.context.synced_header_hash,
132							equivocations,
133						})
134					},
135				Err(e) => {
136					log::error!(
137						target: "bridge",
138						"Could not search for equivocations in the finality proof \
139						for source header {:?} synced at target block {}: {e:?}",
140						synced_header.finality_proof.target_header_hash(),
141						self.target_block_num
142					);
143				},
144			};
145
146			finality_proofs_buf.prune(synced_header.finality_proof.target_header_number(), None);
147			self.context.update(synced_header);
148		}
149
150		result
151	}
152}
153
154/// Fourth step in the block checking state machine.
155///
156/// Reporting the detected equivocations (if any).
157#[cfg_attr(test, derive(Debug))]
158pub struct ReportEquivocations<P: EquivocationDetectionPipeline> {
159	source_block_hash: P::Hash,
160	equivocations: Vec<P::EquivocationProof>,
161}
162
163impl<P: EquivocationDetectionPipeline> ReportEquivocations<P> {
164	pub async fn next<SC: SourceClient<P>>(
165		mut self,
166		source_client: &mut SC,
167		reporter: &mut EquivocationsReporter<'_, P, SC>,
168	) -> Result<(), Self> {
169		let mut unprocessed_equivocations = vec![];
170		for equivocation in self.equivocations {
171			match reporter
172				.submit_report(source_client, self.source_block_hash, equivocation.clone())
173				.await
174			{
175				Ok(_) => {},
176				Err(e) => {
177					log::error!(
178						target: "bridge",
179						"Could not submit equivocation report to {} for {equivocation:?}: {e:?}",
180						P::SOURCE_NAME,
181					);
182
183					// Mark the equivocation as unprocessed
184					unprocessed_equivocations.push(equivocation);
185					// Reconnect source client in case of a connection error.
186					handle_client_error(source_client, e).await;
187				},
188			}
189		}
190
191		self.equivocations = unprocessed_equivocations;
192		if !self.equivocations.is_empty() {
193			return Err(self)
194		}
195
196		Ok(())
197	}
198}
199
200/// Block checking state machine.
201#[cfg_attr(test, derive(Debug))]
202pub enum BlockChecker<P: EquivocationDetectionPipeline> {
203	ReadSyncedHeaders(ReadSyncedHeaders<P>),
204	ReadContext(ReadContext<P>),
205	ReportEquivocations(Vec<ReportEquivocations<P>>),
206}
207
208impl<P: EquivocationDetectionPipeline> BlockChecker<P> {
209	pub fn new(target_block_num: P::TargetNumber) -> Self {
210		Self::ReadSyncedHeaders(ReadSyncedHeaders { target_block_num })
211	}
212
213	pub fn run<'a, SC: SourceClient<P>, TC: TargetClient<P>>(
214		self,
215		source_client: &'a mut SC,
216		target_client: &'a mut TC,
217		finality_proofs_buf: &'a mut FinalityProofsBuf<P>,
218		reporter: &'a mut EquivocationsReporter<P, SC>,
219	) -> BoxFuture<'a, Result<(), Self>> {
220		async move {
221			match self {
222				Self::ReadSyncedHeaders(state) => {
223					let read_context =
224						state.next(target_client).await.map_err(Self::ReadSyncedHeaders)?;
225					Self::ReadContext(read_context)
226						.run(source_client, target_client, finality_proofs_buf, reporter)
227						.await
228				},
229				Self::ReadContext(state) => {
230					let maybe_find_equivocations =
231						state.next(target_client).await.map_err(Self::ReadContext)?;
232					let find_equivocations = match maybe_find_equivocations {
233						Some(find_equivocations) => find_equivocations,
234						None => return Ok(()),
235					};
236					Self::ReportEquivocations(find_equivocations.next(finality_proofs_buf))
237						.run(source_client, target_client, finality_proofs_buf, reporter)
238						.await
239				},
240				Self::ReportEquivocations(state) => {
241					let mut failures = vec![];
242					for report_equivocations in state {
243						if let Err(failure) =
244							report_equivocations.next(source_client, reporter).await
245						{
246							failures.push(failure);
247						}
248					}
249
250					if !failures.is_empty() {
251						return Err(Self::ReportEquivocations(failures))
252					}
253
254					Ok(())
255				},
256			}
257		}
258		.boxed()
259	}
260}
261
262#[cfg(test)]
263mod tests {
264	use super::*;
265	use crate::mock::*;
266	use std::collections::HashMap;
267
268	impl PartialEq for ReadContext<TestEquivocationDetectionPipeline> {
269		fn eq(&self, other: &Self) -> bool {
270			self.target_block_num == other.target_block_num &&
271				self.synced_headers == other.synced_headers
272		}
273	}
274
275	impl PartialEq for FindEquivocations<TestEquivocationDetectionPipeline> {
276		fn eq(&self, other: &Self) -> bool {
277			self.target_block_num == other.target_block_num &&
278				self.synced_headers == other.synced_headers &&
279				self.context == other.context
280		}
281	}
282
283	impl PartialEq for ReportEquivocations<TestEquivocationDetectionPipeline> {
284		fn eq(&self, other: &Self) -> bool {
285			self.source_block_hash == other.source_block_hash &&
286				self.equivocations == other.equivocations
287		}
288	}
289
290	impl PartialEq for BlockChecker<TestEquivocationDetectionPipeline> {
291		fn eq(&self, _other: &Self) -> bool {
292			matches!(self, _other)
293		}
294	}
295
296	#[async_std::test]
297	async fn block_checker_works() {
298		let mut source_client = TestSourceClient { ..Default::default() };
299		let mut target_client = TestTargetClient {
300			best_synced_header_hash: HashMap::from([(9, Ok(Some(5)))]),
301			finality_verification_context: HashMap::from([(
302				9,
303				Ok(TestFinalityVerificationContext { check_equivocations: true }),
304			)]),
305			synced_headers_finality_info: HashMap::from([(
306				10,
307				Ok(vec![
308					new_header_finality_info(6, None),
309					new_header_finality_info(7, Some(false)),
310					new_header_finality_info(8, None),
311					new_header_finality_info(9, Some(true)),
312					new_header_finality_info(10, None),
313					new_header_finality_info(11, None),
314					new_header_finality_info(12, None),
315				]),
316			)]),
317			..Default::default()
318		};
319		let mut reporter =
320			EquivocationsReporter::<TestEquivocationDetectionPipeline, TestSourceClient>::new();
321
322		let block_checker = BlockChecker::new(10);
323		assert!(block_checker
324			.run(
325				&mut source_client,
326				&mut target_client,
327				&mut FinalityProofsBuf::new(vec![
328					TestFinalityProof(6, vec!["6-1"]),
329					TestFinalityProof(7, vec![]),
330					TestFinalityProof(8, vec!["8-1"]),
331					TestFinalityProof(9, vec!["9-1"]),
332					TestFinalityProof(10, vec![]),
333					TestFinalityProof(11, vec!["11-1", "11-2"]),
334					TestFinalityProof(12, vec!["12-1"])
335				]),
336				&mut reporter
337			)
338			.await
339			.is_ok());
340		assert_eq!(
341			*source_client.reported_equivocations.lock().unwrap(),
342			HashMap::from([(5, vec!["6-1"]), (9, vec!["11-1", "11-2", "12-1"])])
343		);
344	}
345
346	#[async_std::test]
347	async fn block_checker_works_with_empty_context() {
348		let mut target_client = TestTargetClient {
349			best_synced_header_hash: HashMap::from([(9, Ok(None))]),
350			finality_verification_context: HashMap::from([(
351				9,
352				Ok(TestFinalityVerificationContext { check_equivocations: true }),
353			)]),
354			synced_headers_finality_info: HashMap::from([(
355				10,
356				Ok(vec![new_header_finality_info(6, None)]),
357			)]),
358			..Default::default()
359		};
360		let mut source_client = TestSourceClient { ..Default::default() };
361		let mut reporter =
362			EquivocationsReporter::<TestEquivocationDetectionPipeline, TestSourceClient>::new();
363
364		let block_checker = BlockChecker::new(10);
365		assert!(block_checker
366			.run(
367				&mut source_client,
368				&mut target_client,
369				&mut FinalityProofsBuf::new(vec![TestFinalityProof(6, vec!["6-1"])]),
370				&mut reporter
371			)
372			.await
373			.is_ok());
374		assert_eq!(*source_client.reported_equivocations.lock().unwrap(), HashMap::default());
375	}
376
377	#[async_std::test]
378	async fn read_synced_headers_handles_errors() {
379		let mut target_client = TestTargetClient {
380			synced_headers_finality_info: HashMap::from([
381				(10, Err(TestClientError::NonConnection)),
382				(11, Err(TestClientError::Connection)),
383			]),
384			..Default::default()
385		};
386		let mut source_client = TestSourceClient { ..Default::default() };
387		let mut reporter =
388			EquivocationsReporter::<TestEquivocationDetectionPipeline, TestSourceClient>::new();
389
390		// NonConnection error
391		let block_checker = BlockChecker::new(10);
392		assert_eq!(
393			block_checker
394				.run(
395					&mut source_client,
396					&mut target_client,
397					&mut FinalityProofsBuf::new(vec![]),
398					&mut reporter
399				)
400				.await,
401			Err(BlockChecker::ReadSyncedHeaders(ReadSyncedHeaders { target_block_num: 10 }))
402		);
403		assert_eq!(target_client.num_reconnects, 0);
404
405		// Connection error
406		let block_checker = BlockChecker::new(11);
407		assert_eq!(
408			block_checker
409				.run(
410					&mut source_client,
411					&mut target_client,
412					&mut FinalityProofsBuf::new(vec![]),
413					&mut reporter
414				)
415				.await,
416			Err(BlockChecker::ReadSyncedHeaders(ReadSyncedHeaders { target_block_num: 11 }))
417		);
418		assert_eq!(target_client.num_reconnects, 1);
419	}
420
421	#[async_std::test]
422	async fn read_context_handles_errors() {
423		let mut target_client = TestTargetClient {
424			synced_headers_finality_info: HashMap::from([(10, Ok(vec![])), (11, Ok(vec![]))]),
425			best_synced_header_hash: HashMap::from([
426				(9, Err(TestClientError::NonConnection)),
427				(10, Err(TestClientError::Connection)),
428			]),
429			..Default::default()
430		};
431		let mut source_client = TestSourceClient { ..Default::default() };
432		let mut reporter =
433			EquivocationsReporter::<TestEquivocationDetectionPipeline, TestSourceClient>::new();
434
435		// NonConnection error
436		let block_checker = BlockChecker::new(10);
437		assert_eq!(
438			block_checker
439				.run(
440					&mut source_client,
441					&mut target_client,
442					&mut FinalityProofsBuf::new(vec![]),
443					&mut reporter
444				)
445				.await,
446			Err(BlockChecker::ReadContext(ReadContext {
447				target_block_num: 10,
448				synced_headers: vec![]
449			}))
450		);
451		assert_eq!(target_client.num_reconnects, 0);
452
453		// Connection error
454		let block_checker = BlockChecker::new(11);
455		assert_eq!(
456			block_checker
457				.run(
458					&mut source_client,
459					&mut target_client,
460					&mut FinalityProofsBuf::new(vec![]),
461					&mut reporter
462				)
463				.await,
464			Err(BlockChecker::ReadContext(ReadContext {
465				target_block_num: 11,
466				synced_headers: vec![]
467			}))
468		);
469		assert_eq!(target_client.num_reconnects, 1);
470	}
471}