forked from paritytech/polkadot-sdk
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Zombienet tests - disputes on finalized blocks (paritytech#2184)
**Overview:** Adding an extra malus variant focusing on disputing finalized blocks. It will: - wrap around approval-voting - listen to `OverseerSignal::BlockFinalized` and when encountered start a dispute for the `dispute_offset`th ancestor - simply pass through all other messages and signals Add zombienet tests testing various edgecases: - disputing freshly finalized blocks - disputing stale finalized blocks - disputing eagerly pruned finalized blocks (might be separate PR) **TODO:** - [x] Register new malus variant - [x] Simple pass through wrapper (approval-voting) - [x] Simple network definition - [x] Listen to block finalizations - [x] Fetch ancestor hash - [x] Fetch session index - [x] Fetch candidate - [x] Construct and send dispute message - [x] zndsl test 1 checking that disputes on fresh finalizations resolve valid Closes paritytech#1365 - [x] zndsl test 2 checking that disputes for too old finalized blocks are not possible Closes paritytech#1364 - [ ] zndsl test 3 checking that disputes for candidates with eagerly pruned relay parent state are handled correctly paritytech#1359 (deferred to a separate PR) - [x] Unit tests for new malus variant (testing cli etc) - [x] Clean/streamline error handling - [ ] ~~Ensure it tests properly on session boundaries~~ --------- Co-authored-by: Javier Viola <[email protected]> Co-authored-by: Marcin S. <[email protected]> Co-authored-by: Tsvetomir Dimitrov <[email protected]>
- Loading branch information
1 parent
4f8048b
commit dc69dbb
Showing
10 changed files
with
462 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
265 changes: 265 additions & 0 deletions
265
polkadot/node/malus/src/variants/dispute_finalized_candidates.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,265 @@ | ||
// 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::{ | ||
prepared_overseer_builder, | ||
service::{ | ||
AuthorityDiscoveryApi, AuxStore, BabeApi, Block, Error, HeaderBackend, Overseer, | ||
OverseerConnector, OverseerGen, OverseerGenArgs, OverseerHandle, ParachainHost, | ||
ProvideRuntimeApi, | ||
}, | ||
Cli, | ||
}; | ||
use polkadot_node_subsystem::{messages::ApprovalVotingMessage, SpawnGlue}; | ||
use polkadot_node_subsystem_types::{DefaultSubsystemClient, OverseerSignal}; | ||
use polkadot_node_subsystem_util::request_candidate_events; | ||
use polkadot_primitives::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( | ||
"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>, | ||
) -> Result< | ||
(Overseer<SpawnGlue<Spawner>, Arc<DefaultSubsystemClient<RuntimeClient>>>, OverseerHandle), | ||
Error, | ||
> | ||
where | ||
RuntimeClient: 'static + ProvideRuntimeApi<Block> + HeaderBackend<Block> + AuxStore, | ||
RuntimeClient::Api: ParachainHost<Block> + BabeApi<Block> + AuthorityDiscoveryApi<Block>, | ||
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, | ||
}; | ||
|
||
prepared_overseer_builder(args)? | ||
.replace_approval_voting(move |cb| InterceptedSubsystem::new(cb, ancestor_disputer)) | ||
.build_with_connector(connector) | ||
.map_err(|e| e.into()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.