Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

Commit

Permalink
Make sure to preserve backing votes (#6382)
Browse files Browse the repository at this point in the history
* Guide updates

* Consider more dead forks.

* Ensure backing votes don't get overridden.

* Fix spelling.

* Fix comments.

* Update node/primitives/src/lib.rs

Co-authored-by: Tsvetomir Dimitrov <tsvetomir@parity.io>

Co-authored-by: eskimor <eskimor@no-such-url.com>
Co-authored-by: Tsvetomir Dimitrov <tsvetomir@parity.io>
  • Loading branch information
3 people committed Dec 7, 2022
1 parent ef17d4f commit b097577
Show file tree
Hide file tree
Showing 11 changed files with 303 additions and 185 deletions.
52 changes: 18 additions & 34 deletions node/core/dispute-coordinator/src/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@

use std::collections::{BTreeMap, HashMap, HashSet};

use polkadot_node_primitives::{CandidateVotes, DisputeStatus, SignedDisputeStatement, Timestamp};
use polkadot_node_primitives::{
disputes::ValidCandidateVotes, CandidateVotes, DisputeStatus, SignedDisputeStatement, Timestamp,
};
use polkadot_node_subsystem_util::rolling_session_window::RollingSessionWindow;
use polkadot_primitives::v2::{
CandidateReceipt, DisputeStatement, IndexedVec, SessionIndex, SessionInfo,
Expand Down Expand Up @@ -101,7 +103,7 @@ impl OwnVoteState {
let mut our_valid_votes = env
.controlled_indices()
.iter()
.filter_map(|i| votes.valid.get_key_value(i))
.filter_map(|i| votes.valid.raw().get_key_value(i))
.peekable();
let mut our_invalid_votes =
env.controlled_indices.iter().filter_map(|i| votes.invalid.get_key_value(i));
Expand Down Expand Up @@ -163,8 +165,11 @@ impl CandidateVoteState<CandidateVotes> {
///
/// in case there have not been any previous votes.
pub fn new_from_receipt(candidate_receipt: CandidateReceipt) -> Self {
let votes =
CandidateVotes { candidate_receipt, valid: BTreeMap::new(), invalid: BTreeMap::new() };
let votes = CandidateVotes {
candidate_receipt,
valid: ValidCandidateVotes::new(),
invalid: BTreeMap::new(),
};
Self { votes, own_vote: OwnVoteState::NoVote, dispute_status: None }
}

Expand All @@ -178,7 +183,7 @@ impl CandidateVoteState<CandidateVotes> {
polkadot_primitives::v2::supermajority_threshold(n_validators);

// We have a dispute, if we have votes on both sides:
let is_disputed = !votes.invalid.is_empty() && !votes.valid.is_empty();
let is_disputed = !votes.invalid.is_empty() && !votes.valid.raw().is_empty();

let dispute_status = if is_disputed {
let mut status = DisputeStatus::active();
Expand All @@ -187,7 +192,7 @@ impl CandidateVoteState<CandidateVotes> {
if is_confirmed {
status = status.confirm();
};
let concluded_for = votes.valid.len() >= supermajority_threshold;
let concluded_for = votes.valid.raw().len() >= supermajority_threshold;
if concluded_for {
status = status.conclude_for(now);
};
Expand Down Expand Up @@ -262,25 +267,20 @@ impl CandidateVoteState<CandidateVotes> {

match statement.statement() {
DisputeStatement::Valid(valid_kind) => {
let fresh = insert_into_statements(
&mut votes.valid,
*valid_kind,
let fresh = votes.valid.insert_vote(
val_index,
*valid_kind,
statement.into_validator_signature(),
);

if fresh {
imported_valid_votes += 1;
}
},
DisputeStatement::Invalid(invalid_kind) => {
let fresh = insert_into_statements(
&mut votes.invalid,
*invalid_kind,
val_index,
statement.into_validator_signature(),
);

let fresh = votes
.invalid
.insert(val_index, (*invalid_kind, statement.into_validator_signature()))
.is_none();
if fresh {
new_invalid_voters.push(val_index);
imported_invalid_votes += 1;
Expand Down Expand Up @@ -481,12 +481,7 @@ impl ImportResult {
},
"Signature check for imported approval votes failed! This is a serious bug. Session: {:?}, candidate hash: {:?}, validator index: {:?}", env.session_index(), votes.candidate_receipt.hash(), index
);
if insert_into_statements(
&mut votes.valid,
ValidDisputeStatementKind::ApprovalChecking,
index,
sig,
) {
if votes.valid.insert_vote(index, ValidDisputeStatementKind::ApprovalChecking, sig) {
imported_valid_votes += 1;
imported_approval_votes += 1;
}
Expand Down Expand Up @@ -535,14 +530,3 @@ fn find_controlled_validator_indices(

controlled
}

// Returns 'true' if no other vote by that validator was already
// present and 'false' otherwise. Same semantics as `HashSet`.
fn insert_into_statements<T>(
m: &mut BTreeMap<ValidatorIndex, (T, ValidatorSignature)>,
tag: T,
val_index: ValidatorIndex,
val_signature: ValidatorSignature,
) -> bool {
m.insert(val_index, (tag, val_signature)).is_none()
}
16 changes: 10 additions & 6 deletions node/core/dispute-coordinator/src/initialized.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ use futures::{
use sc_keystore::LocalKeystore;

use polkadot_node_primitives::{
CandidateVotes, DisputeMessage, DisputeMessageCheckError, DisputeStatus,
SignedDisputeStatement, Timestamp,
disputes::ValidCandidateVotes, CandidateVotes, DisputeMessage, DisputeMessageCheckError,
DisputeStatus, SignedDisputeStatement, Timestamp,
};
use polkadot_node_subsystem::{
messages::{
Expand Down Expand Up @@ -1024,7 +1024,7 @@ impl Initialized {
imported_approval_votes = ?import_result.imported_approval_votes(),
imported_valid_votes = ?import_result.imported_valid_votes(),
imported_invalid_votes = ?import_result.imported_invalid_votes(),
total_valid_votes = ?import_result.new_state().votes().valid.len(),
total_valid_votes = ?import_result.new_state().votes().valid.raw().len(),
total_invalid_votes = ?import_result.new_state().votes().invalid.len(),
confirmed = ?import_result.new_state().is_confirmed(),
"Import summary"
Expand Down Expand Up @@ -1099,7 +1099,7 @@ impl Initialized {
.map(CandidateVotes::from)
.unwrap_or_else(|| CandidateVotes {
candidate_receipt: candidate_receipt.clone(),
valid: BTreeMap::new(),
valid: ValidCandidateVotes::new(),
invalid: BTreeMap::new(),
});

Expand Down Expand Up @@ -1272,8 +1272,12 @@ fn make_dispute_message(
.map_err(|()| DisputeMessageCreationError::InvalidStoredStatement)?;
(our_vote, our_index, other_vote, *validator_index)
} else {
let (validator_index, (statement_kind, validator_signature)) =
votes.valid.iter().next().ok_or(DisputeMessageCreationError::NoOppositeVote)?;
let (validator_index, (statement_kind, validator_signature)) = votes
.valid
.raw()
.iter()
.next()
.ok_or(DisputeMessageCreationError::NoOppositeVote)?;
let other_vote = SignedDisputeStatement::new_checked(
DisputeStatement::Valid(*statement_kind),
*our_vote.candidate_hash(),
Expand Down
21 changes: 6 additions & 15 deletions node/core/dispute-coordinator/src/scraping/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use std::num::NonZeroUsize;
use futures::channel::oneshot;
use lru::LruCache;

use polkadot_node_primitives::MAX_FINALITY_LAG;
use polkadot_node_primitives::{DISPUTE_CANDIDATE_LIFETIME_AFTER_FINALIZATION, MAX_FINALITY_LAG};
use polkadot_node_subsystem::{
messages::ChainApiMessage, overseer, ActivatedLeaf, ActiveLeavesUpdate, ChainApiError,
SubsystemSender,
Expand Down Expand Up @@ -65,7 +65,7 @@ const LRU_OBSERVED_BLOCKS_CAPACITY: NonZeroUsize = match NonZeroUsize::new(20) {
/// 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
/// Scraped candidates are available `DISPUTE_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.
Expand All @@ -87,16 +87,6 @@ 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.
Expand Down Expand Up @@ -175,12 +165,13 @@ impl ChainScraper {

/// Prune finalized candidates.
///
/// We keep each candidate for `CANDIDATE_LIFETIME_AFTER_FINALIZATION` blocks after finalization.
/// We keep each candidate for `DISPUTE_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
// `DISPUTE_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) {
match finalized_block_number.checked_sub(DISPUTE_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);
Expand Down
11 changes: 6 additions & 5 deletions node/core/dispute-coordinator/src/scraping/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use parity_scale_codec::Encode;
use sp_core::testing::TaskExecutor;

use ::test_helpers::{dummy_collator, dummy_collator_signature, dummy_hash};
use polkadot_node_primitives::DISPUTE_CANDIDATE_LIFETIME_AFTER_FINALIZATION;
use polkadot_node_subsystem::{
jaeger,
messages::{
Expand Down Expand Up @@ -452,9 +453,9 @@ fn scraper_prunes_finalized_candidates() {

let candidate = make_candidate_receipt(get_block_number_hash(TEST_TARGET_BLOCK_NUMBER));

// After `CANDIDATE_LIFETIME_AFTER_FINALIZATION` blocks the candidate should be removed
// After `DISPUTE_CANDIDATE_LIFETIME_AFTER_FINALIZATION` blocks the candidate should be removed
finalized_block_number =
TEST_TARGET_BLOCK_NUMBER + ChainScraper::CANDIDATE_LIFETIME_AFTER_FINALIZATION;
TEST_TARGET_BLOCK_NUMBER + DISPUTE_CANDIDATE_LIFETIME_AFTER_FINALIZATION;
process_finalized_block(&mut scraper, &finalized_block_number);

assert!(!scraper.is_candidate_backed(&candidate.hash()));
Expand Down Expand Up @@ -512,10 +513,10 @@ fn scraper_handles_backed_but_not_included_candidate() {
// The candidate should be removed.
assert!(
finalized_block_number <
TEST_TARGET_BLOCK_NUMBER + ChainScraper::CANDIDATE_LIFETIME_AFTER_FINALIZATION
TEST_TARGET_BLOCK_NUMBER + DISPUTE_CANDIDATE_LIFETIME_AFTER_FINALIZATION
);
finalized_block_number +=
TEST_TARGET_BLOCK_NUMBER + ChainScraper::CANDIDATE_LIFETIME_AFTER_FINALIZATION;
TEST_TARGET_BLOCK_NUMBER + DISPUTE_CANDIDATE_LIFETIME_AFTER_FINALIZATION;
process_finalized_block(&mut scraper, &finalized_block_number);

assert!(!scraper.is_candidate_included(&candidate.hash()));
Expand Down Expand Up @@ -562,7 +563,7 @@ fn scraper_handles_the_same_candidate_incuded_in_two_different_block_heights() {
// 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;
DISPUTE_CANDIDATE_LIFETIME_AFTER_FINALIZATION;
process_finalized_block(&mut scraper, &finalized_block_number);

let magic_candidate = make_candidate_receipt(get_magic_candidate_hash());
Expand Down
31 changes: 17 additions & 14 deletions node/core/dispute-coordinator/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -706,7 +706,7 @@ fn too_many_unconfirmed_statements_are_considered_spam() {
.await;

let (_, _, votes) = rx.await.unwrap().get(0).unwrap().clone();
assert_eq!(votes.valid.len(), 1);
assert_eq!(votes.valid.raw().len(), 1);
assert_eq!(votes.invalid.len(), 1);
}

Expand Down Expand Up @@ -839,8 +839,11 @@ fn approval_vote_import_works() {
.await;

let (_, _, votes) = rx.await.unwrap().get(0).unwrap().clone();
assert_eq!(votes.valid.len(), 2);
assert!(votes.valid.get(&ValidatorIndex(4)).is_some(), "Approval vote is missing!");
assert_eq!(votes.valid.raw().len(), 2);
assert!(
votes.valid.raw().get(&ValidatorIndex(4)).is_some(),
"Approval vote is missing!"
);
assert_eq!(votes.invalid.len(), 1);
}

Expand Down Expand Up @@ -964,7 +967,7 @@ fn dispute_gets_confirmed_via_participation() {
.await;

let (_, _, votes) = rx.await.unwrap().get(0).unwrap().clone();
assert_eq!(votes.valid.len(), 2);
assert_eq!(votes.valid.raw().len(), 2);
assert_eq!(votes.invalid.len(), 1);
}

Expand Down Expand Up @@ -999,7 +1002,7 @@ fn dispute_gets_confirmed_via_participation() {
.await;

let (_, _, votes) = rx.await.unwrap().get(0).unwrap().clone();
assert_eq!(votes.valid.len(), 1);
assert_eq!(votes.valid.raw().len(), 1);
assert_eq!(votes.invalid.len(), 1);
}

Expand Down Expand Up @@ -1132,7 +1135,7 @@ fn dispute_gets_confirmed_at_byzantine_threshold() {
.await;

let (_, _, votes) = rx.await.unwrap().get(0).unwrap().clone();
assert_eq!(votes.valid.len(), 2);
assert_eq!(votes.valid.raw().len(), 2);
assert_eq!(votes.invalid.len(), 2);
}

Expand Down Expand Up @@ -1167,7 +1170,7 @@ fn dispute_gets_confirmed_at_byzantine_threshold() {
.await;

let (_, _, votes) = rx.await.unwrap().get(0).unwrap().clone();
assert_eq!(votes.valid.len(), 1);
assert_eq!(votes.valid.raw().len(), 1);
assert_eq!(votes.invalid.len(), 1);
}

Expand Down Expand Up @@ -1246,7 +1249,7 @@ fn backing_statements_import_works_and_no_spam() {
.await;

let (_, _, votes) = rx.await.unwrap().get(0).unwrap().clone();
assert_eq!(votes.valid.len(), 2);
assert_eq!(votes.valid.raw().len(), 2);
assert_eq!(votes.invalid.len(), 0);
}

Expand Down Expand Up @@ -1394,7 +1397,7 @@ fn conflicting_votes_lead_to_dispute_participation() {
.await;

let (_, _, votes) = rx.await.unwrap().get(0).unwrap().clone();
assert_eq!(votes.valid.len(), 2);
assert_eq!(votes.valid.raw().len(), 2);
assert_eq!(votes.invalid.len(), 1);
}

Expand All @@ -1421,7 +1424,7 @@ fn conflicting_votes_lead_to_dispute_participation() {
.await;

let (_, _, votes) = rx.await.unwrap().get(0).unwrap().clone();
assert_eq!(votes.valid.len(), 2);
assert_eq!(votes.valid.raw().len(), 2);
assert_eq!(votes.invalid.len(), 2);
}

Expand Down Expand Up @@ -1505,7 +1508,7 @@ fn positive_votes_dont_trigger_participation() {
.await;

let (_, _, votes) = rx.await.unwrap().get(0).unwrap().clone();
assert_eq!(votes.valid.len(), 1);
assert_eq!(votes.valid.raw().len(), 1);
assert!(votes.invalid.is_empty());
}

Expand Down Expand Up @@ -1541,7 +1544,7 @@ fn positive_votes_dont_trigger_participation() {
.await;

let (_, _, votes) = rx.await.unwrap().get(0).unwrap().clone();
assert_eq!(votes.valid.len(), 2);
assert_eq!(votes.valid.raw().len(), 2);
assert!(votes.invalid.is_empty());
}

Expand Down Expand Up @@ -3075,7 +3078,7 @@ fn refrain_from_participation() {
.await;

let (_, _, votes) = rx.await.unwrap().get(0).unwrap().clone();
assert_eq!(votes.valid.len(), 1);
assert_eq!(votes.valid.raw().len(), 1);
assert_eq!(votes.invalid.len(), 1);
}

Expand Down Expand Up @@ -3183,7 +3186,7 @@ fn participation_for_included_candidates() {
.await;

let (_, _, votes) = rx.await.unwrap().get(0).unwrap().clone();
assert_eq!(votes.valid.len(), 2); // 2 => we have participated
assert_eq!(votes.valid.raw().len(), 2); // 2 => we have participated
assert_eq!(votes.invalid.len(), 1);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ where

// Check if votes are within the limit
for (session_index, candidate_hash, selected_votes) in votes {
let votes_len = selected_votes.valid.len() + selected_votes.invalid.len();
let votes_len = selected_votes.valid.raw().len() + selected_votes.invalid.len();
if votes_len + total_votes_len > MAX_DISPUTE_VOTES_FORWARDED_TO_RUNTIME {
// we are done - no more votes can be added
return result
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,9 @@ impl TestDisputes {
ValidDisputeStatementKind::Explicit,
0,
local_votes_count,
),
)
.into_iter()
.collect(),
invalid: BTreeMap::new(),
},
);
Expand Down
Loading

0 comments on commit b097577

Please sign in to comment.