mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-13 05:51:02 +00:00
Update disputes prioritisation in dispute-coordinator (#6130)
* Scraper processes CandidateBacked events * Change definition of best-effort * Fix `dispute-coordinator` tests * Unit test for dispute filtering * Clarification comment * Add tests * Fix logic If a dispute is not backed, not included and not confirmed we don't participate but we do import votes. * Add metrics for refrained participations * Revert "Add tests" This reverts commit 7b8391a087922ced942cde9cd2b50ff3f633efc0. * Revert "Unit test for dispute filtering" This reverts commit 92ba5fe678214ab360306313a33c781338e600a0. * fix dispute-coordinator tests * Fix scraping * new tests * Small fixes in guide * Apply suggestions from code review Co-authored-by: Andrei Sandu <54316454+sandreim@users.noreply.github.com> * Fix some comments and remove a pointless test * Code review feedback * Clarification comment in tests * Some tests * Reference counted `CandidateHash` in scraper * Proper handling for Backed and Included candidates in scraper Backed candidates which are not included should be kept for a predetermined window of finalized blocks. E.g. if a candidate is backed but not included in block 2, and the window size is 2, the same candidate should be cleaned after block 4 is finalized. Add reference counting for candidates in scraper. A candidate can be added on multiple block heights so we have to make sure we don't clean it prematurely from the scraper. Add tests. * Update comments in tests * Guide update * Fix cleanup logic for `backed_candidates_by_block_number` * Simplify cleanup * Make spellcheck happy * Update tests * Extract candidate backing logic in separate struct * Code review feedback * Treat backed and included candidates in the same fashion * Update some comments * Small improvements in test * spell check * Fix some more comments * clean -> prune * Code review feedback * Reword comment * spelling Co-authored-by: Andrei Sandu <54316454+sandreim@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
0cea4eedce
commit
ff09b18d31
@@ -869,12 +869,21 @@ impl Initialized {
|
||||
}
|
||||
}
|
||||
|
||||
let has_own_vote = new_state.has_own_vote();
|
||||
let is_disputed = new_state.is_disputed();
|
||||
let has_controlled_indices = !env.controlled_indices().is_empty();
|
||||
let is_backed = self.scraper.is_candidate_backed(&candidate_hash);
|
||||
let is_confirmed = new_state.is_confirmed();
|
||||
// We participate only in disputes which are included, backed or confirmed
|
||||
let allow_participation = is_included || is_backed || is_confirmed;
|
||||
|
||||
// Participate in dispute if we did not cast a vote before and actually have keys to cast a
|
||||
// local vote:
|
||||
if !new_state.has_own_vote() &&
|
||||
new_state.is_disputed() &&
|
||||
!env.controlled_indices().is_empty()
|
||||
{
|
||||
// local vote. Disputes should fall in one of the categories below, otherwise we will refrain
|
||||
// from participation:
|
||||
// - `is_included` lands in prioritised queue
|
||||
// - `is_confirmed` | `is_backed` lands in best effort queue
|
||||
// We don't participate in disputes on finalized candidates.
|
||||
if !has_own_vote && is_disputed && has_controlled_indices && allow_participation {
|
||||
let priority = ParticipationPriority::with_priority_if(is_included);
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
@@ -896,6 +905,23 @@ impl Initialized {
|
||||
)
|
||||
.await;
|
||||
log_error(r)?;
|
||||
} else {
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
?candidate_hash,
|
||||
?is_confirmed,
|
||||
?has_own_vote,
|
||||
?is_disputed,
|
||||
?has_controlled_indices,
|
||||
?allow_participation,
|
||||
?is_included,
|
||||
?is_backed,
|
||||
"Will not queue participation for candidate"
|
||||
);
|
||||
|
||||
if !allow_participation {
|
||||
self.metrics.on_refrained_participation();
|
||||
}
|
||||
}
|
||||
|
||||
// Also send any already existing approval vote on new disputes:
|
||||
|
||||
@@ -286,7 +286,7 @@ impl DisputeCoordinatorSubsystem {
|
||||
|
||||
let mut participation_requests = Vec::new();
|
||||
let mut unconfirmed_disputes: UnconfirmedDisputes = UnconfirmedDisputes::new();
|
||||
let (mut scraper, votes) = ChainScraper::new(ctx.sender(), initial_head).await?;
|
||||
let (scraper, votes) = ChainScraper::new(ctx.sender(), initial_head).await?;
|
||||
for ((session, ref candidate_hash), status) in active_disputes {
|
||||
let votes: CandidateVotes =
|
||||
match overlay_db.load_candidate_votes(session, candidate_hash) {
|
||||
|
||||
@@ -30,6 +30,8 @@ struct MetricsInner {
|
||||
queued_participations: prometheus::CounterVec<prometheus::U64>,
|
||||
/// How long vote cleanup batches take.
|
||||
vote_cleanup_time: prometheus::Histogram,
|
||||
/// Number of refrained participations.
|
||||
refrained_participations: prometheus::Counter<prometheus::U64>,
|
||||
}
|
||||
|
||||
/// Candidate validation metrics.
|
||||
@@ -88,6 +90,12 @@ impl Metrics {
|
||||
pub(crate) fn time_vote_cleanup(&self) -> Option<prometheus::prometheus::HistogramTimer> {
|
||||
self.0.as_ref().map(|metrics| metrics.vote_cleanup_time.start_timer())
|
||||
}
|
||||
|
||||
pub(crate) fn on_refrained_participation(&self) {
|
||||
if let Some(metrics) = &self.0 {
|
||||
metrics.refrained_participations.inc();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl metrics::Metrics for Metrics {
|
||||
@@ -147,6 +155,14 @@ impl metrics::Metrics for Metrics {
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
refrained_participations: prometheus::register(
|
||||
prometheus::Counter::with_opts(
|
||||
prometheus::Opts::new(
|
||||
"polkadot_parachain_dispute_refrained_participations",
|
||||
"Number of refrained participations. We refrain from participation if all of the following conditions are met: disputed candidate is not included, not backed and not confirmed.",
|
||||
))?,
|
||||
registry,
|
||||
)?,
|
||||
};
|
||||
Ok(Metrics(Some(metrics)))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
use polkadot_primitives::v2::{BlockNumber, CandidateHash};
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
|
||||
/// Keeps `CandidateHash` in reference counted way.
|
||||
/// Each `insert` saves a value with `reference count == 1` or increases the reference
|
||||
/// count if the value already exists.
|
||||
/// Each `remove` decreases the reference count for the corresponding `CandidateHash`.
|
||||
/// If the reference count reaches 0 - the value is removed.
|
||||
struct RefCountedCandidates {
|
||||
candidates: HashMap<CandidateHash, usize>,
|
||||
}
|
||||
|
||||
impl RefCountedCandidates {
|
||||
pub fn new() -> Self {
|
||||
Self { candidates: HashMap::new() }
|
||||
}
|
||||
// If `CandidateHash` doesn't exist in the `HashMap` it is created and its reference
|
||||
// count is set to 1.
|
||||
// If `CandidateHash` already exists in the `HashMap` its reference count is increased.
|
||||
pub fn insert(&mut self, candidate: CandidateHash) {
|
||||
*self.candidates.entry(candidate).or_default() += 1;
|
||||
}
|
||||
|
||||
// If a `CandidateHash` with reference count equals to 1 is about to be removed - the
|
||||
// candidate is dropped from the container too.
|
||||
// If a `CandidateHash` with reference count biger than 1 is about to be removed - the
|
||||
// reference count is decreased and the candidate remains in the container.
|
||||
pub fn remove(&mut self, candidate: &CandidateHash) {
|
||||
match self.candidates.get_mut(candidate) {
|
||||
Some(v) if *v > 1 => *v -= 1,
|
||||
Some(v) => {
|
||||
assert!(*v == 1);
|
||||
self.candidates.remove(candidate);
|
||||
},
|
||||
None => {},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contains(&self, candidate: &CandidateHash) -> bool {
|
||||
self.candidates.contains_key(&candidate)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod ref_counted_candidates_tests {
|
||||
use super::*;
|
||||
use polkadot_primitives::v2::{BlakeTwo256, HashT};
|
||||
|
||||
#[test]
|
||||
fn element_is_removed_when_refcount_reaches_zero() {
|
||||
let mut container = RefCountedCandidates::new();
|
||||
|
||||
let zero = CandidateHash(BlakeTwo256::hash(&vec![0]));
|
||||
let one = CandidateHash(BlakeTwo256::hash(&vec![1]));
|
||||
// add two separate candidates
|
||||
container.insert(zero); // refcount == 1
|
||||
container.insert(one);
|
||||
|
||||
// and increase the reference count for the first
|
||||
container.insert(zero); // refcount == 2
|
||||
|
||||
assert!(container.contains(&zero));
|
||||
assert!(container.contains(&one));
|
||||
|
||||
// remove once -> refcount == 1
|
||||
container.remove(&zero);
|
||||
assert!(container.contains(&zero));
|
||||
assert!(container.contains(&one));
|
||||
|
||||
// remove once -> refcount == 0
|
||||
container.remove(&zero);
|
||||
assert!(!container.contains(&zero));
|
||||
assert!(container.contains(&one));
|
||||
|
||||
// remove the other element
|
||||
container.remove(&one);
|
||||
assert!(!container.contains(&zero));
|
||||
assert!(!container.contains(&one));
|
||||
}
|
||||
}
|
||||
|
||||
/// Keeps track of scraped candidates. Supports `insert`, `remove_up_to_height` and `contains`
|
||||
/// operations.
|
||||
pub struct ScrapedCandidates {
|
||||
/// Main data structure which keeps the candidates we know about. `contains` does lookups only here.
|
||||
candidates: RefCountedCandidates,
|
||||
/// Keeps track at which block number a candidate was inserted. Used in `remove_up_to_height`.
|
||||
/// Without this tracking we won't be able to remove all candidates before block X.
|
||||
candidates_by_block_number: BTreeMap<BlockNumber, HashSet<CandidateHash>>,
|
||||
}
|
||||
|
||||
impl ScrapedCandidates {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
candidates: RefCountedCandidates::new(),
|
||||
candidates_by_block_number: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contains(&self, candidate_hash: &CandidateHash) -> bool {
|
||||
self.candidates.contains(candidate_hash)
|
||||
}
|
||||
|
||||
// Removes all candidates up to a given height. The candidates at the block height are NOT removed.
|
||||
pub fn remove_up_to_height(&mut self, height: &BlockNumber) {
|
||||
let not_stale = self.candidates_by_block_number.split_off(&height);
|
||||
let stale = std::mem::take(&mut self.candidates_by_block_number);
|
||||
self.candidates_by_block_number = not_stale;
|
||||
for candidates in stale.values() {
|
||||
for c in candidates {
|
||||
self.candidates.remove(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, block_number: BlockNumber, candidate_hash: CandidateHash) {
|
||||
self.candidates.insert(candidate_hash);
|
||||
self.candidates_by_block_number
|
||||
.entry(block_number)
|
||||
.or_default()
|
||||
.insert(candidate_hash);
|
||||
}
|
||||
|
||||
// Used only for tests to verify the pruning doesn't leak data.
|
||||
#[cfg(test)]
|
||||
pub fn candidates_by_block_number_is_empty(&self) -> bool {
|
||||
self.candidates_by_block_number.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod scraped_candidates_tests {
|
||||
use super::*;
|
||||
use polkadot_primitives::v2::{BlakeTwo256, HashT};
|
||||
|
||||
#[test]
|
||||
fn stale_candidates_are_removed() {
|
||||
let mut candidates = ScrapedCandidates::new();
|
||||
let target = CandidateHash(BlakeTwo256::hash(&vec![1, 2, 3]));
|
||||
candidates.insert(1, target);
|
||||
|
||||
assert!(candidates.contains(&target));
|
||||
|
||||
candidates.remove_up_to_height(&2);
|
||||
assert!(!candidates.contains(&target));
|
||||
assert!(candidates.candidates_by_block_number_is_empty());
|
||||
}
|
||||
}
|
||||
@@ -14,10 +14,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, HashSet},
|
||||
num::NonZeroUsize,
|
||||
};
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use futures::channel::oneshot;
|
||||
use lru::LruCache;
|
||||
@@ -40,6 +37,8 @@ use crate::{
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
mod candidates;
|
||||
|
||||
/// Number of hashes to keep in the LRU.
|
||||
///
|
||||
///
|
||||
@@ -58,18 +57,21 @@ const LRU_OBSERVED_BLOCKS_CAPACITY: NonZeroUsize = match NonZeroUsize::new(20) {
|
||||
///
|
||||
/// Concretely:
|
||||
///
|
||||
/// - Monitors for inclusion events to keep track of candidates that have been included on chains.
|
||||
/// - Monitors for `CandidateIncluded` events to keep track of candidates that have been
|
||||
/// included on chains.
|
||||
/// - Monitors for `CandidateBacked` events to keep track of all backed candidates.
|
||||
/// - Calls `FetchOnChainVotes` for each block to gather potentially missed votes from chain.
|
||||
///
|
||||
/// With this information it provides a `CandidateComparator` and as a return value of
|
||||
/// `process_active_leaves_update` any scraped votes.
|
||||
///
|
||||
/// Scraped candidates are available `CANDIDATE_LIFETIME_AFTER_FINALIZATION` more blocks
|
||||
/// after finalization as a precaution not to prune them prematurely.
|
||||
pub struct ChainScraper {
|
||||
/// All candidates we have seen included, which not yet have been finalized.
|
||||
included_candidates: HashSet<CandidateHash>,
|
||||
/// including block -> `CandidateHash`
|
||||
///
|
||||
/// We need this to clean up `included_candidates` on finalization.
|
||||
candidates_by_block_number: BTreeMap<BlockNumber, HashSet<CandidateHash>>,
|
||||
included_candidates: candidates::ScrapedCandidates,
|
||||
/// All candidates we have seen backed
|
||||
backed_candidates: candidates::ScrapedCandidates,
|
||||
/// Latest relay blocks observed by the provider.
|
||||
///
|
||||
/// We assume that ancestors of cached blocks are already processed, i.e. we have saved
|
||||
@@ -85,6 +87,16 @@ impl ChainScraper {
|
||||
/// As long as we have `MAX_FINALITY_LAG` this makes sense as a value.
|
||||
pub(crate) const ANCESTRY_SIZE_LIMIT: u32 = MAX_FINALITY_LAG;
|
||||
|
||||
/// How many blocks after finalization a backed/included candidate should be kept.
|
||||
/// We don't want to remove scraped candidates on finalization because we want to
|
||||
/// be sure that disputes will conclude on abandoned forks.
|
||||
/// Removing the candidate on finalization creates a possibility for an attacker to
|
||||
/// avoid slashing. If a bad fork is abandoned too quickly because in the same another
|
||||
/// better one gets finalized the entries for the bad fork will be pruned and we
|
||||
/// will never participate in a dispute for it. We want such disputes to conclude
|
||||
/// in a timely manner so that the offenders are slashed.
|
||||
pub(crate) const CANDIDATE_LIFETIME_AFTER_FINALIZATION: BlockNumber = 2;
|
||||
|
||||
/// Create a properly initialized `OrderingProvider`.
|
||||
///
|
||||
/// Returns: `Self` and any scraped votes.
|
||||
@@ -96,8 +108,8 @@ impl ChainScraper {
|
||||
Sender: overseer::DisputeCoordinatorSenderTrait,
|
||||
{
|
||||
let mut s = Self {
|
||||
included_candidates: HashSet::new(),
|
||||
candidates_by_block_number: BTreeMap::new(),
|
||||
included_candidates: candidates::ScrapedCandidates::new(),
|
||||
backed_candidates: candidates::ScrapedCandidates::new(),
|
||||
last_observed_blocks: LruCache::new(LRU_OBSERVED_BLOCKS_CAPACITY),
|
||||
};
|
||||
let update =
|
||||
@@ -107,10 +119,15 @@ impl ChainScraper {
|
||||
}
|
||||
|
||||
/// Check whether we have seen a candidate included on any chain.
|
||||
pub fn is_candidate_included(&mut self, candidate_hash: &CandidateHash) -> bool {
|
||||
pub fn is_candidate_included(&self, candidate_hash: &CandidateHash) -> bool {
|
||||
self.included_candidates.contains(candidate_hash)
|
||||
}
|
||||
|
||||
/// Check whether the candidate is backed
|
||||
pub fn is_candidate_backed(&self, candidate_hash: &CandidateHash) -> bool {
|
||||
self.backed_candidates.contains(candidate_hash)
|
||||
}
|
||||
|
||||
/// Query active leaves for any candidate `CandidateEvent::CandidateIncluded` events.
|
||||
///
|
||||
/// and updates current heads, so we can query candidates for all non finalized blocks.
|
||||
@@ -120,7 +137,7 @@ impl ChainScraper {
|
||||
&mut self,
|
||||
sender: &mut Sender,
|
||||
update: &ActiveLeavesUpdate,
|
||||
) -> crate::error::Result<Vec<ScrapedOnChainVotes>>
|
||||
) -> Result<Vec<ScrapedOnChainVotes>>
|
||||
where
|
||||
Sender: overseer::DisputeCoordinatorSenderTrait,
|
||||
{
|
||||
@@ -158,21 +175,27 @@ impl ChainScraper {
|
||||
|
||||
/// Prune finalized candidates.
|
||||
///
|
||||
/// Once a candidate lives in a relay chain block that's behind the finalized chain/got
|
||||
/// finalized, we can treat it as low priority.
|
||||
pub fn process_finalized_block(&mut self, finalized: &BlockNumber) {
|
||||
let not_finalized = self.candidates_by_block_number.split_off(finalized);
|
||||
let finalized = std::mem::take(&mut self.candidates_by_block_number);
|
||||
self.candidates_by_block_number = not_finalized;
|
||||
// Clean up finalized:
|
||||
for finalized_candidate in finalized.into_values().flatten() {
|
||||
self.included_candidates.remove(&finalized_candidate);
|
||||
/// We keep each candidate for `CANDIDATE_LIFETIME_AFTER_FINALIZATION` blocks after finalization.
|
||||
/// After that we treat it as low priority.
|
||||
pub fn process_finalized_block(&mut self, finalized_block_number: &BlockNumber) {
|
||||
// `CANDIDATE_LIFETIME_AFTER_FINALIZATION - 1` because `finalized_block_number`counts to the
|
||||
// candidate lifetime.
|
||||
match finalized_block_number.checked_sub(Self::CANDIDATE_LIFETIME_AFTER_FINALIZATION - 1) {
|
||||
Some(key_to_prune) => {
|
||||
self.backed_candidates.remove_up_to_height(&key_to_prune);
|
||||
self.included_candidates.remove_up_to_height(&key_to_prune);
|
||||
},
|
||||
None => {
|
||||
// Nothing to prune. We are still in the beginning of the chain and there are not
|
||||
// enough finalized blocks yet.
|
||||
},
|
||||
}
|
||||
{}
|
||||
}
|
||||
|
||||
/// Process candidate events of a block.
|
||||
///
|
||||
/// Keep track of all included candidates.
|
||||
/// Keep track of all included and backed candidates.
|
||||
async fn process_candidate_events<Sender>(
|
||||
&mut self,
|
||||
sender: &mut Sender,
|
||||
@@ -182,28 +205,33 @@ impl ChainScraper {
|
||||
where
|
||||
Sender: overseer::DisputeCoordinatorSenderTrait,
|
||||
{
|
||||
// Get included events:
|
||||
let included =
|
||||
get_candidate_events(sender, block_hash)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter_map(|ev| match ev {
|
||||
CandidateEvent::CandidateIncluded(receipt, _, _, _) => Some(receipt),
|
||||
_ => None,
|
||||
});
|
||||
for receipt in included {
|
||||
let candidate_hash = receipt.hash();
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
?candidate_hash,
|
||||
?block_number,
|
||||
"Processing included event"
|
||||
);
|
||||
self.included_candidates.insert(candidate_hash);
|
||||
self.candidates_by_block_number
|
||||
.entry(block_number)
|
||||
.or_default()
|
||||
.insert(candidate_hash);
|
||||
// Get included and backed events:
|
||||
for ev in get_candidate_events(sender, block_hash).await? {
|
||||
match ev {
|
||||
CandidateEvent::CandidateIncluded(receipt, _, _, _) => {
|
||||
let candidate_hash = receipt.hash();
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
?candidate_hash,
|
||||
?block_number,
|
||||
"Processing included event"
|
||||
);
|
||||
self.included_candidates.insert(block_number, candidate_hash);
|
||||
},
|
||||
CandidateEvent::CandidateBacked(receipt, _, _, _) => {
|
||||
let candidate_hash = receipt.hash();
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
?candidate_hash,
|
||||
?block_number,
|
||||
"Processing backed event"
|
||||
);
|
||||
self.backed_candidates.insert(block_number, candidate_hash);
|
||||
},
|
||||
_ => {
|
||||
// skip the rest
|
||||
},
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -73,7 +73,12 @@ impl TestState {
|
||||
assert_finalized_block_number_request(&mut ctx_handle, finalized_block_number).await;
|
||||
gum::trace!(target: LOG_TARGET, "After assert_finalized_block_number");
|
||||
// No ancestors requests, as list would be empty.
|
||||
assert_candidate_events_request(&mut ctx_handle, &chain).await;
|
||||
assert_candidate_events_request(
|
||||
&mut ctx_handle,
|
||||
&chain,
|
||||
get_backed_and_included_candidate_events,
|
||||
)
|
||||
.await;
|
||||
assert_chain_vote_request(&mut ctx_handle, &chain).await;
|
||||
};
|
||||
|
||||
@@ -112,6 +117,10 @@ async fn process_active_leaves_update(
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn process_finalized_block(scraper: &mut ChainScraper, finalized: &BlockNumber) {
|
||||
scraper.process_finalized_block(&finalized)
|
||||
}
|
||||
|
||||
fn make_candidate_receipt(relay_parent: Hash) -> CandidateReceipt {
|
||||
let zeros = dummy_hash();
|
||||
let descriptor = CandidateDescriptor {
|
||||
@@ -145,16 +154,66 @@ fn get_block_number_hash(n: BlockNumber) -> Hash {
|
||||
}
|
||||
|
||||
/// Get a dummy event that corresponds to candidate inclusion for the given block number.
|
||||
fn get_candidate_included_events(block_number: BlockNumber) -> Vec<CandidateEvent> {
|
||||
vec![CandidateEvent::CandidateIncluded(
|
||||
make_candidate_receipt(get_block_number_hash(block_number)),
|
||||
fn get_backed_and_included_candidate_events(block_number: BlockNumber) -> Vec<CandidateEvent> {
|
||||
let candidate_receipt = make_candidate_receipt(get_block_number_hash(block_number));
|
||||
vec![
|
||||
CandidateEvent::CandidateIncluded(
|
||||
candidate_receipt.clone(),
|
||||
HeadData::default(),
|
||||
CoreIndex::from(0),
|
||||
GroupIndex::from(0),
|
||||
),
|
||||
CandidateEvent::CandidateBacked(
|
||||
candidate_receipt,
|
||||
HeadData::default(),
|
||||
CoreIndex::from(0),
|
||||
GroupIndex::from(0),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
fn get_backed_candidate_event(block_number: BlockNumber) -> Vec<CandidateEvent> {
|
||||
let candidate_receipt = make_candidate_receipt(get_block_number_hash(block_number));
|
||||
vec![CandidateEvent::CandidateBacked(
|
||||
candidate_receipt,
|
||||
HeadData::default(),
|
||||
CoreIndex::from(0),
|
||||
GroupIndex::from(0),
|
||||
)]
|
||||
}
|
||||
/// Hash for a 'magic' candidate. This is meant to be a special candidate used to verify special cases.
|
||||
fn get_magic_candidate_hash() -> Hash {
|
||||
BlakeTwo256::hash(&"abc".encode())
|
||||
}
|
||||
/// Get a dummy event that corresponds to candidate inclusion for a hardcoded block number.
|
||||
/// Used to simulate candidates included multiple times at different block heights.
|
||||
fn get_backed_and_included_magic_candidate_events(
|
||||
_block_number: BlockNumber,
|
||||
) -> Vec<CandidateEvent> {
|
||||
let candidate_receipt = make_candidate_receipt(get_magic_candidate_hash());
|
||||
vec![
|
||||
CandidateEvent::CandidateIncluded(
|
||||
candidate_receipt.clone(),
|
||||
HeadData::default(),
|
||||
CoreIndex::from(0),
|
||||
GroupIndex::from(0),
|
||||
),
|
||||
CandidateEvent::CandidateBacked(
|
||||
candidate_receipt,
|
||||
HeadData::default(),
|
||||
CoreIndex::from(0),
|
||||
GroupIndex::from(0),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
async fn assert_candidate_events_request(virtual_overseer: &mut VirtualOverseer, chain: &[Hash]) {
|
||||
async fn assert_candidate_events_request<F>(
|
||||
virtual_overseer: &mut VirtualOverseer,
|
||||
chain: &[Hash],
|
||||
event_generator: F,
|
||||
) where
|
||||
F: Fn(u32) -> Vec<CandidateEvent>,
|
||||
{
|
||||
assert_matches!(
|
||||
overseer_recv(virtual_overseer).await,
|
||||
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
|
||||
@@ -163,7 +222,7 @@ async fn assert_candidate_events_request(virtual_overseer: &mut VirtualOverseer,
|
||||
)) => {
|
||||
let maybe_block_number = chain.iter().position(|h| *h == hash);
|
||||
let response = maybe_block_number
|
||||
.map(|num| get_candidate_included_events(num as u32))
|
||||
.map(|num| event_generator(num as u32))
|
||||
.unwrap_or_default();
|
||||
tx.send(Ok(response)).unwrap();
|
||||
}
|
||||
@@ -207,12 +266,15 @@ async fn assert_block_ancestors_request(virtual_overseer: &mut VirtualOverseer,
|
||||
);
|
||||
}
|
||||
|
||||
async fn overseer_process_active_leaves_update(
|
||||
async fn overseer_process_active_leaves_update<F>(
|
||||
virtual_overseer: &mut VirtualOverseer,
|
||||
chain: &[Hash],
|
||||
finalized_block: BlockNumber,
|
||||
expected_ancestry_len: usize,
|
||||
) {
|
||||
event_generator: F,
|
||||
) where
|
||||
F: Fn(u32) -> Vec<CandidateEvent> + Clone,
|
||||
{
|
||||
// Before walking through ancestors provider requests latest finalized block number.
|
||||
assert_finalized_block_number_request(virtual_overseer, finalized_block).await;
|
||||
// Expect block ancestors requests with respect to the ancestry step.
|
||||
@@ -221,7 +283,7 @@ async fn overseer_process_active_leaves_update(
|
||||
}
|
||||
// For each ancestry and the head return corresponding candidates inclusions.
|
||||
for _ in 0..expected_ancestry_len {
|
||||
assert_candidate_events_request(virtual_overseer, chain).await;
|
||||
assert_candidate_events_request(virtual_overseer, chain, event_generator.clone()).await;
|
||||
assert_chain_vote_request(virtual_overseer, chain).await;
|
||||
}
|
||||
}
|
||||
@@ -236,7 +298,9 @@ fn scraper_provides_included_state_when_initialized() {
|
||||
let TestState { mut chain, mut scraper, mut ctx } = state;
|
||||
|
||||
assert!(!scraper.is_candidate_included(&candidate_2.hash()));
|
||||
assert!(!scraper.is_candidate_backed(&candidate_2.hash()));
|
||||
assert!(scraper.is_candidate_included(&candidate_1.hash()));
|
||||
assert!(scraper.is_candidate_backed(&candidate_1.hash()));
|
||||
|
||||
// After next active leaves update we should see the candidate included.
|
||||
let next_update = next_leaf(&mut chain);
|
||||
@@ -248,11 +312,13 @@ fn scraper_provides_included_state_when_initialized() {
|
||||
&chain,
|
||||
finalized_block_number,
|
||||
expected_ancestry_len,
|
||||
get_backed_and_included_candidate_events,
|
||||
);
|
||||
join(process_active_leaves_update(ctx.sender(), &mut scraper, next_update), overseer_fut)
|
||||
.await;
|
||||
|
||||
assert!(scraper.is_candidate_included(&candidate_2.hash()));
|
||||
assert!(scraper.is_candidate_backed(&candidate_2.hash()));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -274,6 +340,7 @@ fn scraper_requests_candidates_of_leaf_ancestors() {
|
||||
&chain,
|
||||
finalized_block_number,
|
||||
BLOCKS_TO_SKIP,
|
||||
get_backed_and_included_candidate_events,
|
||||
);
|
||||
join(process_active_leaves_update(ctx.sender(), &mut scraper, next_update), overseer_fut)
|
||||
.await;
|
||||
@@ -282,6 +349,7 @@ fn scraper_requests_candidates_of_leaf_ancestors() {
|
||||
for block_number in 1..next_block_number {
|
||||
let candidate = make_candidate_receipt(get_block_number_hash(block_number));
|
||||
assert!(scraper.is_candidate_included(&candidate.hash()));
|
||||
assert!(scraper.is_candidate_backed(&candidate.hash()));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -304,6 +372,7 @@ fn scraper_requests_candidates_of_non_cached_ancestors() {
|
||||
&chain,
|
||||
finalized_block_number,
|
||||
BLOCKS_TO_SKIP[0],
|
||||
get_backed_and_included_candidate_events,
|
||||
);
|
||||
join(process_active_leaves_update(ctx.sender(), &mut ordering, next_update), overseer_fut)
|
||||
.await;
|
||||
@@ -315,6 +384,7 @@ fn scraper_requests_candidates_of_non_cached_ancestors() {
|
||||
&chain,
|
||||
finalized_block_number,
|
||||
BLOCKS_TO_SKIP[1],
|
||||
get_backed_and_included_candidate_events,
|
||||
);
|
||||
join(process_active_leaves_update(ctx.sender(), &mut ordering, next_update), overseer_fut)
|
||||
.await;
|
||||
@@ -340,8 +410,170 @@ fn scraper_requests_candidates_of_non_finalized_ancestors() {
|
||||
&chain,
|
||||
finalized_block_number,
|
||||
BLOCKS_TO_SKIP - finalized_block_number as usize, // Expect the provider not to go past finalized block.
|
||||
get_backed_and_included_candidate_events,
|
||||
);
|
||||
join(process_active_leaves_update(ctx.sender(), &mut ordering, next_update), overseer_fut)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scraper_prunes_finalized_candidates() {
|
||||
const TEST_TARGET_BLOCK_NUMBER: BlockNumber = 2;
|
||||
|
||||
// How many blocks should we skip before sending a leaf update.
|
||||
const BLOCKS_TO_SKIP: usize = 3;
|
||||
|
||||
futures::executor::block_on(async {
|
||||
let (state, mut virtual_overseer) = TestState::new().await;
|
||||
|
||||
let TestState { mut chain, mut scraper, mut ctx } = state;
|
||||
|
||||
// 1 because `TestState` starts at leaf 1.
|
||||
let next_update = (1..BLOCKS_TO_SKIP).map(|_| next_leaf(&mut chain)).last().unwrap();
|
||||
|
||||
let mut finalized_block_number = 1;
|
||||
let expected_ancestry_len = BLOCKS_TO_SKIP - finalized_block_number as usize;
|
||||
let overseer_fut = overseer_process_active_leaves_update(
|
||||
&mut virtual_overseer,
|
||||
&chain,
|
||||
finalized_block_number,
|
||||
expected_ancestry_len,
|
||||
|block_num| {
|
||||
if block_num == TEST_TARGET_BLOCK_NUMBER {
|
||||
get_backed_and_included_candidate_events(block_num)
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
},
|
||||
);
|
||||
join(process_active_leaves_update(ctx.sender(), &mut scraper, next_update), overseer_fut)
|
||||
.await;
|
||||
|
||||
let candidate = make_candidate_receipt(get_block_number_hash(TEST_TARGET_BLOCK_NUMBER));
|
||||
|
||||
// After `CANDIDATE_LIFETIME_AFTER_FINALIZATION` blocks the candidate should be removed
|
||||
finalized_block_number =
|
||||
TEST_TARGET_BLOCK_NUMBER + ChainScraper::CANDIDATE_LIFETIME_AFTER_FINALIZATION;
|
||||
process_finalized_block(&mut scraper, &finalized_block_number);
|
||||
|
||||
assert!(!scraper.is_candidate_backed(&candidate.hash()));
|
||||
assert!(!scraper.is_candidate_included(&candidate.hash()));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scraper_handles_backed_but_not_included_candidate() {
|
||||
const TEST_TARGET_BLOCK_NUMBER: BlockNumber = 2;
|
||||
|
||||
// How many blocks should we skip before sending a leaf update.
|
||||
const BLOCKS_TO_SKIP: usize = 3;
|
||||
|
||||
futures::executor::block_on(async {
|
||||
let (state, mut virtual_overseer) = TestState::new().await;
|
||||
|
||||
let TestState { mut chain, mut scraper, mut ctx } = state;
|
||||
|
||||
let next_update = (1..BLOCKS_TO_SKIP as BlockNumber)
|
||||
.map(|_| next_leaf(&mut chain))
|
||||
.last()
|
||||
.unwrap();
|
||||
|
||||
// Add `ActiveLeavesUpdate` containing `CandidateBacked` event for block `BLOCK_WITH_EVENTS`
|
||||
let mut finalized_block_number = 1;
|
||||
let expected_ancestry_len = BLOCKS_TO_SKIP - finalized_block_number as usize;
|
||||
let overseer_fut = overseer_process_active_leaves_update(
|
||||
&mut virtual_overseer,
|
||||
&chain,
|
||||
finalized_block_number,
|
||||
expected_ancestry_len,
|
||||
|block_num| {
|
||||
if block_num == TEST_TARGET_BLOCK_NUMBER {
|
||||
get_backed_candidate_event(block_num)
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
},
|
||||
);
|
||||
join(process_active_leaves_update(ctx.sender(), &mut scraper, next_update), overseer_fut)
|
||||
.await;
|
||||
|
||||
// Finalize blocks to enforce pruning of scraped events
|
||||
finalized_block_number += 1;
|
||||
process_finalized_block(&mut scraper, &finalized_block_number);
|
||||
|
||||
// `FIRST_TEST_BLOCK` is finalized, which is within `BACKED_CANDIDATE_LIFETIME_AFTER_FINALIZATION` window.
|
||||
// The candidate should still be backed.
|
||||
let candidate = make_candidate_receipt(get_block_number_hash(TEST_TARGET_BLOCK_NUMBER));
|
||||
assert!(!scraper.is_candidate_included(&candidate.hash()));
|
||||
assert!(scraper.is_candidate_backed(&candidate.hash()));
|
||||
|
||||
// Bump the finalized block outside `BACKED_CANDIDATE_LIFETIME_AFTER_FINALIZATION`.
|
||||
// The candidate should be removed.
|
||||
assert!(
|
||||
finalized_block_number <
|
||||
TEST_TARGET_BLOCK_NUMBER + ChainScraper::CANDIDATE_LIFETIME_AFTER_FINALIZATION
|
||||
);
|
||||
finalized_block_number +=
|
||||
TEST_TARGET_BLOCK_NUMBER + ChainScraper::CANDIDATE_LIFETIME_AFTER_FINALIZATION;
|
||||
process_finalized_block(&mut scraper, &finalized_block_number);
|
||||
|
||||
assert!(!scraper.is_candidate_included(&candidate.hash()));
|
||||
assert!(!scraper.is_candidate_backed(&candidate.hash()));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scraper_handles_the_same_candidate_incuded_in_two_different_block_heights() {
|
||||
// Same candidate will be inclued in these two leaves
|
||||
let test_targets = vec![2, 3];
|
||||
|
||||
// How many blocks should we skip before sending a leaf update.
|
||||
const BLOCKS_TO_SKIP: usize = 3;
|
||||
|
||||
futures::executor::block_on(async {
|
||||
let (state, mut virtual_overseer) = TestState::new().await;
|
||||
|
||||
let TestState { mut chain, mut scraper, mut ctx } = state;
|
||||
|
||||
// 1 because `TestState` starts at leaf 1.
|
||||
let next_update = (1..BLOCKS_TO_SKIP).map(|_| next_leaf(&mut chain)).last().unwrap();
|
||||
|
||||
// Now we will add the same magic candidate at two different block heights.
|
||||
// Check `get_backed_and_included_magic_candidate_event` implementation
|
||||
let mut finalized_block_number = 1;
|
||||
let expected_ancestry_len = BLOCKS_TO_SKIP - finalized_block_number as usize;
|
||||
let overseer_fut = overseer_process_active_leaves_update(
|
||||
&mut virtual_overseer,
|
||||
&chain,
|
||||
finalized_block_number,
|
||||
expected_ancestry_len,
|
||||
|block_num| {
|
||||
if test_targets.contains(&block_num) {
|
||||
get_backed_and_included_magic_candidate_events(block_num)
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
},
|
||||
);
|
||||
join(process_active_leaves_update(ctx.sender(), &mut scraper, next_update), overseer_fut)
|
||||
.await;
|
||||
|
||||
// Finalize blocks to enforce pruning of scraped events.
|
||||
// The magic candidate was added twice, so it shouldn't be removed if we finalize two more blocks.
|
||||
finalized_block_number = test_targets.first().expect("there are two block nums") +
|
||||
ChainScraper::CANDIDATE_LIFETIME_AFTER_FINALIZATION;
|
||||
process_finalized_block(&mut scraper, &finalized_block_number);
|
||||
|
||||
let magic_candidate = make_candidate_receipt(get_magic_candidate_hash());
|
||||
assert!(scraper.is_candidate_backed(&magic_candidate.hash()));
|
||||
assert!(scraper.is_candidate_included(&magic_candidate.hash()));
|
||||
|
||||
// On the next finalization the magic candidate should be removed
|
||||
finalized_block_number += 1;
|
||||
process_finalized_block(&mut scraper, &finalized_block_number);
|
||||
|
||||
assert!(!scraper.is_candidate_backed(&magic_candidate.hash()));
|
||||
assert!(!scraper.is_candidate_included(&magic_candidate.hash()));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -57,10 +57,10 @@ use polkadot_node_subsystem::{
|
||||
};
|
||||
use polkadot_node_subsystem_test_helpers::{make_subsystem_context, TestSubsystemContextHandle};
|
||||
use polkadot_primitives::v2::{
|
||||
ApprovalVote, BlockNumber, CandidateCommitments, CandidateHash, CandidateReceipt,
|
||||
DisputeStatement, GroupIndex, Hash, Header, IndexedVec, MultiDisputeStatementSet,
|
||||
ScrapedOnChainVotes, SessionIndex, SessionInfo, SigningContext, ValidDisputeStatementKind,
|
||||
ValidatorId, ValidatorIndex, ValidatorSignature,
|
||||
ApprovalVote, BlockNumber, CandidateCommitments, CandidateEvent, CandidateHash,
|
||||
CandidateReceipt, CoreIndex, DisputeStatement, GroupIndex, Hash, HeadData, Header, IndexedVec,
|
||||
MultiDisputeStatementSet, ScrapedOnChainVotes, SessionIndex, SessionInfo, SigningContext,
|
||||
ValidDisputeStatementKind, ValidatorId, ValidatorIndex, ValidatorSignature,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -212,6 +212,7 @@ impl TestState {
|
||||
virtual_overseer: &mut VirtualOverseer,
|
||||
session: SessionIndex,
|
||||
block_number: BlockNumber,
|
||||
candidate_events: Vec<CandidateEvent>,
|
||||
) {
|
||||
assert!(block_number > 0);
|
||||
|
||||
@@ -239,8 +240,14 @@ impl TestState {
|
||||
)))
|
||||
.await;
|
||||
|
||||
self.handle_sync_queries(virtual_overseer, block_hash, block_number, session)
|
||||
.await;
|
||||
self.handle_sync_queries(
|
||||
virtual_overseer,
|
||||
block_hash,
|
||||
block_number,
|
||||
session,
|
||||
candidate_events,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn handle_sync_queries(
|
||||
@@ -249,6 +256,7 @@ impl TestState {
|
||||
block_hash: Hash,
|
||||
block_number: BlockNumber,
|
||||
session: SessionIndex,
|
||||
candidate_events: Vec<CandidateEvent>,
|
||||
) {
|
||||
// Order of messages is not fixed (different on initializing):
|
||||
#[derive(Debug)]
|
||||
@@ -352,7 +360,7 @@ impl TestState {
|
||||
_new_leaf,
|
||||
RuntimeApiRequest::CandidateEvents(tx),
|
||||
)) => {
|
||||
tx.send(Ok(Vec::new())).unwrap();
|
||||
tx.send(Ok(candidate_events.clone())).unwrap();
|
||||
}
|
||||
);
|
||||
gum::trace!("After answering runtime api request");
|
||||
@@ -401,8 +409,14 @@ impl TestState {
|
||||
)))
|
||||
.await;
|
||||
|
||||
self.handle_sync_queries(virtual_overseer, *leaf, n as BlockNumber, session)
|
||||
.await;
|
||||
self.handle_sync_queries(
|
||||
virtual_overseer,
|
||||
*leaf,
|
||||
n as BlockNumber,
|
||||
session,
|
||||
Vec::new(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -556,6 +570,26 @@ fn make_invalid_candidate_receipt() -> CandidateReceipt {
|
||||
dummy_candidate_receipt_bad_sig(Default::default(), Some(Default::default()))
|
||||
}
|
||||
|
||||
// Generate a `CandidateBacked` event from a `CandidateReceipt`. The rest is dummy data.
|
||||
fn make_candidate_backed_event(candidate_receipt: CandidateReceipt) -> CandidateEvent {
|
||||
CandidateEvent::CandidateBacked(
|
||||
candidate_receipt,
|
||||
HeadData(Vec::new()),
|
||||
CoreIndex(0),
|
||||
GroupIndex(0),
|
||||
)
|
||||
}
|
||||
|
||||
// Generate a `CandidateIncluded` event from a `CandidateReceipt`. The rest is dummy data.
|
||||
fn make_candidate_included_event(candidate_receipt: CandidateReceipt) -> CandidateEvent {
|
||||
CandidateEvent::CandidateIncluded(
|
||||
candidate_receipt,
|
||||
HeadData(Vec::new()),
|
||||
CoreIndex(0),
|
||||
GroupIndex(0),
|
||||
)
|
||||
}
|
||||
|
||||
/// Handle request for approval votes:
|
||||
pub async fn handle_approval_vote_request(
|
||||
ctx_handle: &mut VirtualOverseer,
|
||||
@@ -587,7 +621,9 @@ fn too_many_unconfirmed_statements_are_considered_spam() {
|
||||
let candidate_receipt2 = make_invalid_candidate_receipt();
|
||||
let candidate_hash2 = candidate_receipt2.hash();
|
||||
|
||||
test_state.activate_leaf_at_session(&mut virtual_overseer, session, 1).await;
|
||||
test_state
|
||||
.activate_leaf_at_session(&mut virtual_overseer, session, 1, Vec::new())
|
||||
.await;
|
||||
|
||||
let valid_vote1 = test_state
|
||||
.issue_backing_statement_with_index(ValidatorIndex(3), candidate_hash1, session)
|
||||
@@ -634,8 +670,9 @@ fn too_many_unconfirmed_statements_are_considered_spam() {
|
||||
handle_approval_vote_request(&mut virtual_overseer, &candidate_hash1, HashMap::new())
|
||||
.await;
|
||||
|
||||
// Participation has to fail, otherwise the dispute will be confirmed.
|
||||
participation_missing_availability(&mut virtual_overseer).await;
|
||||
// Participation has to fail here, otherwise the dispute will be confirmed. However
|
||||
// participation won't happen at all because the dispute is neither backed, not confirmed
|
||||
// nor the candidate is included. Or in other words - we'll refrain from participation.
|
||||
|
||||
{
|
||||
let (tx, rx) = oneshot::channel();
|
||||
@@ -718,7 +755,9 @@ fn approval_vote_import_works() {
|
||||
let candidate_receipt1 = make_valid_candidate_receipt();
|
||||
let candidate_hash1 = candidate_receipt1.hash();
|
||||
|
||||
test_state.activate_leaf_at_session(&mut virtual_overseer, session, 1).await;
|
||||
test_state
|
||||
.activate_leaf_at_session(&mut virtual_overseer, session, 1, Vec::new())
|
||||
.await;
|
||||
|
||||
let valid_vote1 = test_state
|
||||
.issue_backing_statement_with_index(ValidatorIndex(3), candidate_hash1, session)
|
||||
@@ -762,8 +801,8 @@ fn approval_vote_import_works() {
|
||||
handle_approval_vote_request(&mut virtual_overseer, &candidate_hash1, approval_votes)
|
||||
.await;
|
||||
|
||||
// Participation has to fail, otherwise the dispute will be confirmed.
|
||||
participation_missing_availability(&mut virtual_overseer).await;
|
||||
// Participation won't happen here because the dispute is neither backed, not confirmed
|
||||
// nor the candidate is included. Or in other words - we'll refrain from participation.
|
||||
|
||||
{
|
||||
let (tx, rx) = oneshot::channel();
|
||||
@@ -814,7 +853,17 @@ fn dispute_gets_confirmed_via_participation() {
|
||||
let candidate_receipt2 = make_invalid_candidate_receipt();
|
||||
let candidate_hash2 = candidate_receipt2.hash();
|
||||
|
||||
test_state.activate_leaf_at_session(&mut virtual_overseer, session, 1).await;
|
||||
test_state
|
||||
.activate_leaf_at_session(
|
||||
&mut virtual_overseer,
|
||||
session,
|
||||
1,
|
||||
vec![
|
||||
make_candidate_backed_event(candidate_receipt1.clone()),
|
||||
make_candidate_backed_event(candidate_receipt2.clone()),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
let valid_vote1 = test_state
|
||||
.issue_explicit_statement_with_index(
|
||||
@@ -963,7 +1012,9 @@ fn dispute_gets_confirmed_at_byzantine_threshold() {
|
||||
let candidate_receipt2 = make_invalid_candidate_receipt();
|
||||
let candidate_hash2 = candidate_receipt2.hash();
|
||||
|
||||
test_state.activate_leaf_at_session(&mut virtual_overseer, session, 1).await;
|
||||
test_state
|
||||
.activate_leaf_at_session(&mut virtual_overseer, session, 1, Vec::new())
|
||||
.await;
|
||||
|
||||
let valid_vote1 = test_state
|
||||
.issue_explicit_statement_with_index(
|
||||
@@ -1037,7 +1088,8 @@ fn dispute_gets_confirmed_at_byzantine_threshold() {
|
||||
handle_approval_vote_request(&mut virtual_overseer, &candidate_hash1, HashMap::new())
|
||||
.await;
|
||||
|
||||
participation_missing_availability(&mut virtual_overseer).await;
|
||||
// Participation won't happen here because the dispute is neither backed, not confirmed
|
||||
// nor the candidate is included. Or in other words - we'll refrain from participation.
|
||||
|
||||
{
|
||||
let (tx, rx) = oneshot::channel();
|
||||
@@ -1124,7 +1176,9 @@ fn backing_statements_import_works_and_no_spam() {
|
||||
let candidate_receipt = make_valid_candidate_receipt();
|
||||
let candidate_hash = candidate_receipt.hash();
|
||||
|
||||
test_state.activate_leaf_at_session(&mut virtual_overseer, session, 1).await;
|
||||
test_state
|
||||
.activate_leaf_at_session(&mut virtual_overseer, session, 1, Vec::new())
|
||||
.await;
|
||||
|
||||
let valid_vote1 = test_state
|
||||
.issue_backing_statement_with_index(ValidatorIndex(3), candidate_hash, session)
|
||||
@@ -1187,6 +1241,15 @@ fn backing_statements_import_works_and_no_spam() {
|
||||
.issue_backing_statement_with_index(ValidatorIndex(4), candidate_hash, session)
|
||||
.await;
|
||||
|
||||
test_state
|
||||
.activate_leaf_at_session(
|
||||
&mut virtual_overseer,
|
||||
session,
|
||||
1,
|
||||
vec![make_candidate_backed_event(candidate_receipt.clone())],
|
||||
)
|
||||
.await;
|
||||
|
||||
let (pending_confirmation, confirmation_rx) = oneshot::channel();
|
||||
// Backing vote import should not have accounted to spam slots, so this should succeed
|
||||
// as well:
|
||||
@@ -1228,7 +1291,14 @@ fn conflicting_votes_lead_to_dispute_participation() {
|
||||
let candidate_receipt = make_valid_candidate_receipt();
|
||||
let candidate_hash = candidate_receipt.hash();
|
||||
|
||||
test_state.activate_leaf_at_session(&mut virtual_overseer, session, 1).await;
|
||||
test_state
|
||||
.activate_leaf_at_session(
|
||||
&mut virtual_overseer,
|
||||
session,
|
||||
1,
|
||||
vec![make_candidate_backed_event(candidate_receipt.clone())],
|
||||
)
|
||||
.await;
|
||||
|
||||
let valid_vote = test_state
|
||||
.issue_explicit_statement_with_index(
|
||||
@@ -1353,7 +1423,14 @@ fn positive_votes_dont_trigger_participation() {
|
||||
let candidate_receipt = make_valid_candidate_receipt();
|
||||
let candidate_hash = candidate_receipt.hash();
|
||||
|
||||
test_state.activate_leaf_at_session(&mut virtual_overseer, session, 1).await;
|
||||
test_state
|
||||
.activate_leaf_at_session(
|
||||
&mut virtual_overseer,
|
||||
session,
|
||||
1,
|
||||
vec![make_candidate_backed_event(candidate_receipt.clone())],
|
||||
)
|
||||
.await;
|
||||
|
||||
let valid_vote = test_state
|
||||
.issue_explicit_statement_with_index(
|
||||
@@ -1466,7 +1543,9 @@ fn wrong_validator_index_is_ignored() {
|
||||
let candidate_receipt = make_valid_candidate_receipt();
|
||||
let candidate_hash = candidate_receipt.hash();
|
||||
|
||||
test_state.activate_leaf_at_session(&mut virtual_overseer, session, 1).await;
|
||||
test_state
|
||||
.activate_leaf_at_session(&mut virtual_overseer, session, 1, Vec::new())
|
||||
.await;
|
||||
|
||||
let valid_vote = test_state
|
||||
.issue_explicit_statement_with_index(
|
||||
@@ -1544,7 +1623,14 @@ fn finality_votes_ignore_disputed_candidates() {
|
||||
let candidate_receipt = make_valid_candidate_receipt();
|
||||
let candidate_hash = candidate_receipt.hash();
|
||||
|
||||
test_state.activate_leaf_at_session(&mut virtual_overseer, session, 1).await;
|
||||
test_state
|
||||
.activate_leaf_at_session(
|
||||
&mut virtual_overseer,
|
||||
session,
|
||||
1,
|
||||
vec![make_candidate_backed_event(candidate_receipt.clone())],
|
||||
)
|
||||
.await;
|
||||
|
||||
let valid_vote = test_state
|
||||
.issue_explicit_statement_with_index(
|
||||
@@ -1654,7 +1740,14 @@ fn supermajority_valid_dispute_may_be_finalized() {
|
||||
let candidate_receipt = make_valid_candidate_receipt();
|
||||
let candidate_hash = candidate_receipt.hash();
|
||||
|
||||
test_state.activate_leaf_at_session(&mut virtual_overseer, session, 1).await;
|
||||
test_state
|
||||
.activate_leaf_at_session(
|
||||
&mut virtual_overseer,
|
||||
session,
|
||||
1,
|
||||
vec![make_candidate_backed_event(candidate_receipt.clone())],
|
||||
)
|
||||
.await;
|
||||
|
||||
let supermajority_threshold =
|
||||
polkadot_primitives::v2::supermajority_threshold(test_state.validators.len());
|
||||
@@ -1794,7 +1887,14 @@ fn concluded_supermajority_for_non_active_after_time() {
|
||||
let candidate_receipt = make_valid_candidate_receipt();
|
||||
let candidate_hash = candidate_receipt.hash();
|
||||
|
||||
test_state.activate_leaf_at_session(&mut virtual_overseer, session, 1).await;
|
||||
test_state
|
||||
.activate_leaf_at_session(
|
||||
&mut virtual_overseer,
|
||||
session,
|
||||
1,
|
||||
vec![make_candidate_backed_event(candidate_receipt.clone())],
|
||||
)
|
||||
.await;
|
||||
|
||||
let supermajority_threshold =
|
||||
polkadot_primitives::v2::supermajority_threshold(test_state.validators.len());
|
||||
@@ -1912,7 +2012,14 @@ fn concluded_supermajority_against_non_active_after_time() {
|
||||
|
||||
let candidate_hash = candidate_receipt.hash();
|
||||
|
||||
test_state.activate_leaf_at_session(&mut virtual_overseer, session, 1).await;
|
||||
test_state
|
||||
.activate_leaf_at_session(
|
||||
&mut virtual_overseer,
|
||||
session,
|
||||
1,
|
||||
vec![make_candidate_backed_event(candidate_receipt.clone())],
|
||||
)
|
||||
.await;
|
||||
|
||||
let supermajority_threshold =
|
||||
polkadot_primitives::v2::supermajority_threshold(test_state.validators.len());
|
||||
@@ -2036,7 +2143,9 @@ fn resume_dispute_without_local_statement() {
|
||||
let candidate_receipt = make_valid_candidate_receipt();
|
||||
let candidate_hash = candidate_receipt.hash();
|
||||
|
||||
test_state.activate_leaf_at_session(&mut virtual_overseer, session, 1).await;
|
||||
test_state
|
||||
.activate_leaf_at_session(&mut virtual_overseer, session, 1, Vec::new())
|
||||
.await;
|
||||
|
||||
let valid_vote = test_state
|
||||
.issue_explicit_statement_with_index(
|
||||
@@ -2074,7 +2183,8 @@ fn resume_dispute_without_local_statement() {
|
||||
.await;
|
||||
|
||||
// Missing availability -> No local vote.
|
||||
participation_missing_availability(&mut virtual_overseer).await;
|
||||
// Participation won't happen here because the dispute is neither backed, not confirmed
|
||||
// nor the candidate is included. Or in other words - we'll refrain from participation.
|
||||
|
||||
assert_eq!(confirmation_rx.await, Ok(ImportStatementsResult::ValidImport));
|
||||
|
||||
@@ -2216,7 +2326,14 @@ fn resume_dispute_with_local_statement() {
|
||||
let candidate_receipt = make_valid_candidate_receipt();
|
||||
let candidate_hash = candidate_receipt.hash();
|
||||
|
||||
test_state.activate_leaf_at_session(&mut virtual_overseer, session, 1).await;
|
||||
test_state
|
||||
.activate_leaf_at_session(
|
||||
&mut virtual_overseer,
|
||||
session,
|
||||
1,
|
||||
vec![make_candidate_backed_event(candidate_receipt.clone())],
|
||||
)
|
||||
.await;
|
||||
|
||||
let local_valid_vote = test_state
|
||||
.issue_explicit_statement_with_index(
|
||||
@@ -2315,7 +2432,9 @@ fn resume_dispute_without_local_statement_or_local_key() {
|
||||
let candidate_receipt = make_valid_candidate_receipt();
|
||||
let candidate_hash = candidate_receipt.hash();
|
||||
|
||||
test_state.activate_leaf_at_session(&mut virtual_overseer, session, 1).await;
|
||||
test_state
|
||||
.activate_leaf_at_session(&mut virtual_overseer, session, 1, Vec::new())
|
||||
.await;
|
||||
|
||||
let valid_vote = test_state
|
||||
.issue_explicit_statement_with_index(
|
||||
@@ -2408,7 +2527,9 @@ fn resume_dispute_with_local_statement_without_local_key() {
|
||||
let candidate_receipt = make_valid_candidate_receipt();
|
||||
let candidate_hash = candidate_receipt.hash();
|
||||
|
||||
test_state.activate_leaf_at_session(&mut virtual_overseer, session, 1).await;
|
||||
test_state
|
||||
.activate_leaf_at_session(&mut virtual_overseer, session, 1, Vec::new())
|
||||
.await;
|
||||
|
||||
let local_valid_vote = test_state
|
||||
.issue_explicit_statement_with_index(
|
||||
@@ -2517,7 +2638,9 @@ fn issue_local_statement_does_cause_distribution_but_not_duplicate_participation
|
||||
let candidate_receipt = make_valid_candidate_receipt();
|
||||
let candidate_hash = candidate_receipt.hash();
|
||||
|
||||
test_state.activate_leaf_at_session(&mut virtual_overseer, session, 1).await;
|
||||
test_state
|
||||
.activate_leaf_at_session(&mut virtual_overseer, session, 1, Vec::new())
|
||||
.await;
|
||||
|
||||
let other_vote = test_state
|
||||
.issue_explicit_statement_with_index(
|
||||
@@ -2590,7 +2713,9 @@ fn own_approval_vote_gets_distributed_on_dispute() {
|
||||
let candidate_receipt = make_valid_candidate_receipt();
|
||||
let candidate_hash = candidate_receipt.hash();
|
||||
|
||||
test_state.activate_leaf_at_session(&mut virtual_overseer, session, 1).await;
|
||||
test_state
|
||||
.activate_leaf_at_session(&mut virtual_overseer, session, 1, Vec::new())
|
||||
.await;
|
||||
|
||||
let statement = test_state.issue_approval_vote_with_index(
|
||||
ValidatorIndex(0),
|
||||
@@ -2681,7 +2806,9 @@ fn negative_issue_local_statement_only_triggers_import() {
|
||||
let candidate_receipt = make_invalid_candidate_receipt();
|
||||
let candidate_hash = candidate_receipt.hash();
|
||||
|
||||
test_state.activate_leaf_at_session(&mut virtual_overseer, session, 1).await;
|
||||
test_state
|
||||
.activate_leaf_at_session(&mut virtual_overseer, session, 1, Vec::new())
|
||||
.await;
|
||||
|
||||
virtual_overseer
|
||||
.send(FromOrchestra::Communication {
|
||||
@@ -2729,7 +2856,9 @@ fn redundant_votes_ignored() {
|
||||
let candidate_receipt = make_valid_candidate_receipt();
|
||||
let candidate_hash = candidate_receipt.hash();
|
||||
|
||||
test_state.activate_leaf_at_session(&mut virtual_overseer, session, 1).await;
|
||||
test_state
|
||||
.activate_leaf_at_session(&mut virtual_overseer, session, 1, Vec::new())
|
||||
.await;
|
||||
|
||||
let valid_vote = test_state
|
||||
.issue_backing_statement_with_index(ValidatorIndex(1), candidate_hash, session)
|
||||
@@ -2787,3 +2916,195 @@ fn redundant_votes_ignored() {
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refrain_from_participation() {
|
||||
test_harness(|mut test_state, mut virtual_overseer| {
|
||||
Box::pin(async move {
|
||||
let session = 1;
|
||||
|
||||
test_state.handle_resume_sync(&mut virtual_overseer, session).await;
|
||||
|
||||
let candidate_receipt = make_valid_candidate_receipt();
|
||||
let candidate_hash = candidate_receipt.hash();
|
||||
|
||||
// activate leaf - no backing/included event
|
||||
test_state
|
||||
.activate_leaf_at_session(&mut virtual_overseer, session, 1, Vec::new())
|
||||
.await;
|
||||
|
||||
// generate two votes
|
||||
let valid_vote = test_state
|
||||
.issue_explicit_statement_with_index(
|
||||
ValidatorIndex(1),
|
||||
candidate_hash,
|
||||
session,
|
||||
true,
|
||||
)
|
||||
.await;
|
||||
|
||||
let invalid_vote = test_state
|
||||
.issue_explicit_statement_with_index(
|
||||
ValidatorIndex(2),
|
||||
candidate_hash,
|
||||
session,
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
|
||||
virtual_overseer
|
||||
.send(FromOrchestra::Communication {
|
||||
msg: DisputeCoordinatorMessage::ImportStatements {
|
||||
candidate_receipt: candidate_receipt.clone(),
|
||||
session,
|
||||
statements: vec![
|
||||
(valid_vote, ValidatorIndex(1)),
|
||||
(invalid_vote, ValidatorIndex(2)),
|
||||
],
|
||||
pending_confirmation: None,
|
||||
},
|
||||
})
|
||||
.await;
|
||||
|
||||
handle_approval_vote_request(&mut virtual_overseer, &candidate_hash, HashMap::new())
|
||||
.await;
|
||||
|
||||
{
|
||||
let (tx, rx) = oneshot::channel();
|
||||
virtual_overseer
|
||||
.send(FromOrchestra::Communication {
|
||||
msg: DisputeCoordinatorMessage::ActiveDisputes(tx),
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(rx.await.unwrap().len(), 1);
|
||||
|
||||
let (tx, rx) = oneshot::channel();
|
||||
virtual_overseer
|
||||
.send(FromOrchestra::Communication {
|
||||
msg: DisputeCoordinatorMessage::QueryCandidateVotes(
|
||||
vec![(session, candidate_hash)],
|
||||
tx,
|
||||
),
|
||||
})
|
||||
.await;
|
||||
|
||||
let (_, _, votes) = rx.await.unwrap().get(0).unwrap().clone();
|
||||
assert_eq!(votes.valid.len(), 1);
|
||||
assert_eq!(votes.invalid.len(), 1);
|
||||
}
|
||||
|
||||
// activate leaf - no backing event
|
||||
test_state
|
||||
.activate_leaf_at_session(&mut virtual_overseer, session, 1, Vec::new())
|
||||
.await;
|
||||
|
||||
virtual_overseer.send(FromOrchestra::Signal(OverseerSignal::Conclude)).await;
|
||||
|
||||
// confirm that no participation request is made.
|
||||
assert!(virtual_overseer.try_recv().await.is_none());
|
||||
|
||||
test_state
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/// We have got no `participation_for_backed_candidates` test because most of the other tests (e.g.
|
||||
/// `dispute_gets_confirmed_via_participation`, `backing_statements_import_works_and_no_spam`) use
|
||||
/// candidate backing event to trigger participation. If they pass - that case works.
|
||||
#[test]
|
||||
fn participation_for_included_candidates() {
|
||||
test_harness(|mut test_state, mut virtual_overseer| {
|
||||
Box::pin(async move {
|
||||
let session = 1;
|
||||
|
||||
test_state.handle_resume_sync(&mut virtual_overseer, session).await;
|
||||
|
||||
let candidate_receipt = make_valid_candidate_receipt();
|
||||
let candidate_hash = candidate_receipt.hash();
|
||||
|
||||
// activate leaf - with candidate included event
|
||||
test_state
|
||||
.activate_leaf_at_session(
|
||||
&mut virtual_overseer,
|
||||
session,
|
||||
1,
|
||||
vec![make_candidate_included_event(candidate_receipt.clone())],
|
||||
)
|
||||
.await;
|
||||
|
||||
// generate two votes
|
||||
let valid_vote = test_state
|
||||
.issue_explicit_statement_with_index(
|
||||
ValidatorIndex(1),
|
||||
candidate_hash,
|
||||
session,
|
||||
true,
|
||||
)
|
||||
.await;
|
||||
|
||||
let invalid_vote = test_state
|
||||
.issue_explicit_statement_with_index(
|
||||
ValidatorIndex(2),
|
||||
candidate_hash,
|
||||
session,
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
|
||||
virtual_overseer
|
||||
.send(FromOrchestra::Communication {
|
||||
msg: DisputeCoordinatorMessage::ImportStatements {
|
||||
candidate_receipt: candidate_receipt.clone(),
|
||||
session,
|
||||
statements: vec![
|
||||
(valid_vote, ValidatorIndex(1)),
|
||||
(invalid_vote, ValidatorIndex(2)),
|
||||
],
|
||||
pending_confirmation: None,
|
||||
},
|
||||
})
|
||||
.await;
|
||||
|
||||
handle_approval_vote_request(&mut virtual_overseer, &candidate_hash, HashMap::new())
|
||||
.await;
|
||||
|
||||
participation_with_distribution(
|
||||
&mut virtual_overseer,
|
||||
&candidate_hash,
|
||||
candidate_receipt.commitments_hash,
|
||||
)
|
||||
.await;
|
||||
|
||||
{
|
||||
let (tx, rx) = oneshot::channel();
|
||||
virtual_overseer
|
||||
.send(FromOrchestra::Communication {
|
||||
msg: DisputeCoordinatorMessage::ActiveDisputes(tx),
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(rx.await.unwrap().len(), 1);
|
||||
|
||||
// check if we have participated (casted a vote)
|
||||
let (tx, rx) = oneshot::channel();
|
||||
virtual_overseer
|
||||
.send(FromOrchestra::Communication {
|
||||
msg: DisputeCoordinatorMessage::QueryCandidateVotes(
|
||||
vec![(session, candidate_hash)],
|
||||
tx,
|
||||
),
|
||||
})
|
||||
.await;
|
||||
|
||||
let (_, _, votes) = rx.await.unwrap().get(0).unwrap().clone();
|
||||
assert_eq!(votes.valid.len(), 2); // 2 => we have participated
|
||||
assert_eq!(votes.invalid.len(), 1);
|
||||
}
|
||||
|
||||
virtual_overseer.send(FromOrchestra::Signal(OverseerSignal::Conclude)).await;
|
||||
|
||||
test_state
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ There are two potential caveats with this though:
|
||||
dispute concludes in all cases? The answer is nuanced, but in general we
|
||||
cannot rely on it. The problem is first, that finalization and
|
||||
approval-voting is an off-chain process so there is no global consensus: As
|
||||
soon as at least f+1 honest (f= n/3, where n is the number of
|
||||
soon as at least f+1 honest (f=n/3, where n is the number of
|
||||
validators/nodes) nodes have seen the dispute conclude, finalization will
|
||||
take place and approval votes will be cleared. This would still be fine, if
|
||||
we had some guarantees that those honest nodes will be able to include those
|
||||
@@ -214,7 +214,7 @@ among nodes which includes current block producers (current authority set) which
|
||||
is an important property: If the dispute carries on across an era change, we
|
||||
need to ensure that the new validator set will learn about any disputes and
|
||||
their votes, so they can put that information on chain. Dispute-distribution
|
||||
luckily has this property and sends votes to the current authority set always.
|
||||
luckily has this property and always sends votes to the current authority set.
|
||||
The issue is, for dispute-distribution, nodes send only their own explicit (or
|
||||
in some cases their approval vote) in addition to some opposing vote. This
|
||||
guarantees that at least some backing or approval vote will be present at the
|
||||
@@ -327,7 +327,10 @@ participation at all on any _vote import_ if any of the following holds true:
|
||||
- We have seen the disputed candidate backed in some not yet finalized block on
|
||||
at least one fork of the chain. This ensures the candidate is at least not
|
||||
completely made up and there has been some effort already flown into that
|
||||
candidate.
|
||||
candidate. Generally speaking a dispute shouldn't be raised for a candidate
|
||||
which is backed but is not yet included. Disputes are raised during approval
|
||||
checking. We participate on such disputes as a precaution - maybe we haven't
|
||||
seen the `CandidateIncluded` event yet?
|
||||
- The dispute is already confirmed: Meaning that 1/3+1 nodes already
|
||||
participated, as this suggests in our threat model that there was at least one
|
||||
honest node that already voted, so the dispute must be genuine.
|
||||
|
||||
Reference in New Issue
Block a user