feat: initialize Kurdistan SDK - independent fork of Polkadot SDK

This commit is contained in:
2025-12-13 15:44:15 +03:00
commit e4778b4576
6838 changed files with 1847450 additions and 0 deletions
@@ -0,0 +1,48 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! The disputes module is responsible for selecting dispute votes to be sent with the inherent
//! data.
use crate::LOG_TARGET;
use futures::channel::oneshot;
use pezkuwi_node_primitives::CandidateVotes;
use pezkuwi_node_subsystem::{messages::DisputeCoordinatorMessage, overseer};
use pezkuwi_primitives::{CandidateHash, SessionIndex};
/// Request the relevant dispute statements for a set of disputes identified by `CandidateHash` and
/// the `SessionIndex`.
async fn request_votes(
sender: &mut impl overseer::ProvisionerSenderTrait,
disputes_to_query: Vec<(SessionIndex, CandidateHash)>,
) -> Vec<(SessionIndex, CandidateHash, CandidateVotes)> {
let (tx, rx) = oneshot::channel();
// Bounded by block production - `ProvisionerMessage::RequestInherentData`.
sender.send_unbounded_message(DisputeCoordinatorMessage::QueryCandidateVotes(
disputes_to_query,
tx,
));
match rx.await {
Ok(v) => v,
Err(oneshot::Canceled) => {
gum::warn!(target: LOG_TARGET, "Unable to query candidate votes");
Vec::new()
},
}
}
pub(crate) mod prioritized_selection;
@@ -0,0 +1,501 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! This module uses different approach for selecting dispute votes. It queries the Runtime
//! about the votes already known onchain and tries to select only relevant votes. Refer to
//! the documentation of `select_disputes` for more details about the actual implementation.
use crate::{error::GetOnchainDisputesError, metrics, LOG_TARGET};
use futures::channel::oneshot;
use pezkuwi_node_primitives::{dispute_is_inactive, CandidateVotes, DisputeStatus, Timestamp};
use pezkuwi_node_subsystem::{
errors::RuntimeApiError,
messages::{DisputeCoordinatorMessage, RuntimeApiMessage, RuntimeApiRequest},
overseer, ActivatedLeaf,
};
use pezkuwi_primitives::{
supermajority_threshold, CandidateHash, DisputeState, DisputeStatement, DisputeStatementSet,
Hash, MultiDisputeStatementSet, SessionIndex, ValidDisputeStatementKind, ValidatorIndex,
};
use std::{
collections::{BTreeMap, HashMap},
time::{SystemTime, UNIX_EPOCH},
};
#[cfg(test)]
mod tests;
/// The maximum number of disputes Provisioner will include in the inherent data.
/// Serves as a protection not to flood the Runtime with excessive data.
#[cfg(not(test))]
pub const MAX_DISPUTE_VOTES_FORWARDED_TO_RUNTIME: usize = 200_000;
#[cfg(test)]
pub const MAX_DISPUTE_VOTES_FORWARDED_TO_RUNTIME: usize = 200;
/// Controls how much dispute votes to be fetched from the `dispute-coordinator` per iteration in
/// `fn vote_selection`. The purpose is to fetch the votes in batches until
/// `MAX_DISPUTE_VOTES_FORWARDED_TO_RUNTIME` is reached. If all votes are fetched in single call
/// we might fetch votes which we never use. This will create unnecessary load on
/// `dispute-coordinator`.
///
/// This value should be less than `MAX_DISPUTE_VOTES_FORWARDED_TO_RUNTIME`. Increase it in case
/// `provisioner` sends too many `QueryCandidateVotes` messages to `dispute-coordinator`.
#[cfg(not(test))]
const VOTES_SELECTION_BATCH_SIZE: usize = 1_100;
#[cfg(test)]
const VOTES_SELECTION_BATCH_SIZE: usize = 11;
/// Implements the `select_disputes` function which selects dispute votes which should
/// be sent to the Runtime.
///
/// # How the prioritization works
///
/// Generally speaking disputes can be described as:
/// * Active vs Inactive
/// * Known vs Unknown onchain
/// * Offchain vs Onchain
/// * Concluded onchain vs Unconcluded onchain
///
/// Provisioner fetches all disputes from `dispute-coordinator` and separates them in multiple
/// partitions. Please refer to `struct PartitionedDisputes` for details about the actual
/// partitions. Each partition has got a priority implicitly assigned to it and the disputes are
/// selected based on this priority (e.g. disputes in partition 1, then if there is space - disputes
/// from partition 2 and so on).
///
/// # Votes selection
///
/// Besides the prioritization described above the votes in each partition are filtered too.
/// Provisioner fetches all onchain votes and filters them out from all partitions. As a result the
/// Runtime receives only fresh votes (votes it didn't know about).
///
/// # How the onchain votes are fetched
///
/// The logic outlined above relies on `RuntimeApiRequest::Disputes` message from the Runtime. The
/// user check the Runtime version before calling `select_disputes`. If the function is used with
/// old runtime an error is logged and the logic will continue with empty onchain votes `HashMap`.
pub async fn select_disputes<Sender>(
sender: &mut Sender,
metrics: &metrics::Metrics,
leaf: &ActivatedLeaf,
) -> MultiDisputeStatementSet
where
Sender: overseer::ProvisionerSenderTrait,
{
gum::trace!(
target: LOG_TARGET,
?leaf,
"Selecting disputes for inherent data using prioritized selection"
);
// Fetch the onchain disputes. We'll do a prioritization based on them.
let onchain = match get_onchain_disputes(sender, leaf.hash).await {
Ok(r) => {
gum::trace!(
target: LOG_TARGET,
?leaf,
"Successfully fetched {} onchain disputes",
r.len()
);
r
},
Err(GetOnchainDisputesError::NotSupported(runtime_api_err, relay_parent)) => {
// Runtime version is checked before calling this method, so the error below should
// never happen!
gum::error!(
target: LOG_TARGET,
?runtime_api_err,
?relay_parent,
"Can't fetch onchain disputes, because TeyrchainHost runtime api version is old. Will continue with empty onchain disputes set.",
);
HashMap::new()
},
Err(GetOnchainDisputesError::Channel) => {
// This error usually means the node is shutting down. Log just in case.
gum::debug!(
target: LOG_TARGET,
"Channel error occurred while fetching onchain disputes. Will continue with empty onchain disputes set.",
);
HashMap::new()
},
Err(GetOnchainDisputesError::Execution(runtime_api_err, parent_hash)) => {
gum::warn!(
target: LOG_TARGET,
?runtime_api_err,
?parent_hash,
"Unexpected execution error occurred while fetching onchain votes. Will continue with empty onchain disputes set.",
);
HashMap::new()
},
};
metrics.on_fetched_onchain_disputes(onchain.keys().len() as u64);
gum::trace!(target: LOG_TARGET, ?leaf, "Fetching recent disputes");
let recent_disputes = request_disputes(sender).await;
gum::trace!(
target: LOG_TARGET,
?leaf,
"Got {} recent disputes and {} onchain disputes.",
recent_disputes.len(),
onchain.len(),
);
gum::trace!(target: LOG_TARGET, ?leaf, "Filtering recent disputes");
// Filter out unconfirmed disputes. However if the dispute is already onchain - don't skip it.
// In this case we'd better push as much fresh votes as possible to bring it to conclusion
// faster.
let recent_disputes = recent_disputes
.into_iter()
.filter(|(key, dispute_status)| {
dispute_status.is_confirmed_concluded() || onchain.contains_key(key)
})
.collect::<BTreeMap<_, _>>();
gum::trace!(target: LOG_TARGET, ?leaf, "Partitioning recent disputes");
let partitioned = partition_recent_disputes(recent_disputes, &onchain);
metrics.on_partition_recent_disputes(&partitioned);
if partitioned.inactive_unknown_onchain.len() > 0 {
gum::warn!(
target: LOG_TARGET,
?leaf,
"Got {} inactive unknown onchain disputes. This should not happen in normal conditions!",
partitioned.inactive_unknown_onchain.len()
);
}
gum::trace!(target: LOG_TARGET, ?leaf, "Vote selection for recent disputes");
let result = vote_selection(sender, partitioned, &onchain).await;
gum::trace!(target: LOG_TARGET, ?leaf, "Convert to multi dispute statement set");
make_multi_dispute_statement_set(metrics, result)
}
/// Selects dispute votes from `PartitionedDisputes` which should be sent to the runtime. Votes
/// which are already onchain are filtered out. Result should be sorted by `(SessionIndex,
/// CandidateHash)` which is enforced by the `BTreeMap`. This is a requirement from the runtime.
async fn vote_selection<Sender>(
sender: &mut Sender,
partitioned: PartitionedDisputes,
onchain: &HashMap<(SessionIndex, CandidateHash), DisputeState>,
) -> BTreeMap<(SessionIndex, CandidateHash), CandidateVotes>
where
Sender: overseer::ProvisionerSenderTrait,
{
// fetch in batches until there are enough votes
let mut disputes = partitioned.into_iter().collect::<Vec<_>>();
let mut total_votes_len = 0;
let mut result = BTreeMap::new();
let mut request_votes_counter = 0;
while !disputes.is_empty() {
gum::trace!(target: LOG_TARGET, "has to process {} disputes left", disputes.len());
let batch_size = std::cmp::min(VOTES_SELECTION_BATCH_SIZE, disputes.len());
let batch = Vec::from_iter(disputes.drain(0..batch_size));
// Filter votes which are already onchain
request_votes_counter += 1;
gum::trace!(target: LOG_TARGET, "requesting onchain votes",);
let votes = super::request_votes(sender, batch)
.await
.into_iter()
.map(|(session_index, candidate_hash, mut votes)| {
let onchain_state =
if let Some(onchain_state) = onchain.get(&(session_index, candidate_hash)) {
onchain_state
} else {
// onchain knows nothing about this dispute - add all votes
return (session_index, candidate_hash, votes);
};
votes.valid.retain(|validator_idx, (statement_kind, _)| {
is_vote_worth_to_keep(
validator_idx,
DisputeStatement::Valid(statement_kind.clone()),
&onchain_state,
)
});
votes.invalid.retain(|validator_idx, (statement_kind, _)| {
is_vote_worth_to_keep(
validator_idx,
DisputeStatement::Invalid(*statement_kind),
&onchain_state,
)
});
(session_index, candidate_hash, votes)
})
.collect::<Vec<_>>();
gum::trace!(target: LOG_TARGET, "got {} onchain votes after processing", votes.len());
// Check if votes are within the limit
for (session_index, candidate_hash, selected_votes) in votes {
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. Importantly, we don't add any votes for
// a dispute here if we can't fit them all. This gives us an important invariant,
// that backing votes for disputes make it into the provisioned vote set.
gum::trace!(
target: LOG_TARGET,
?request_votes_counter,
?total_votes_len,
"vote_selection DisputeCoordinatorMessage::QueryCandidateVotes counter",
);
return result;
}
result.insert((session_index, candidate_hash), selected_votes);
total_votes_len += votes_len
}
}
gum::trace!(
target: LOG_TARGET,
?request_votes_counter,
?total_votes_len,
"vote_selection DisputeCoordinatorMessage::QueryCandidateVotes counter",
);
result
}
/// Contains disputes by partitions. Check the field comments for further details.
#[derive(Default)]
pub(crate) struct PartitionedDisputes {
/// Concluded and inactive disputes which are completely unknown for the Runtime.
/// Hopefully this should never happen.
/// Will be sent to the Runtime with FIRST priority.
pub inactive_unknown_onchain: Vec<(SessionIndex, CandidateHash)>,
/// Disputes which are INACTIVE locally but they are unconcluded for the Runtime.
/// A dispute can have enough local vote to conclude and at the same time the
/// Runtime knows nothing about them at treats it as unconcluded. This discrepancy
/// should be treated with high priority.
/// Will be sent to the Runtime with SECOND priority.
pub inactive_unconcluded_onchain: Vec<(SessionIndex, CandidateHash)>,
/// Active disputes completely unknown onchain.
/// Will be sent to the Runtime with THIRD priority.
pub active_unknown_onchain: Vec<(SessionIndex, CandidateHash)>,
/// Active disputes unconcluded onchain.
/// Will be sent to the Runtime with FOURTH priority.
pub active_unconcluded_onchain: Vec<(SessionIndex, CandidateHash)>,
/// Active disputes concluded onchain. New votes are not that important for
/// this partition.
/// Will be sent to the Runtime with FIFTH priority.
pub active_concluded_onchain: Vec<(SessionIndex, CandidateHash)>,
/// Inactive disputes which has concluded onchain. These are not interesting and
/// won't be sent to the Runtime.
/// Will be DROPPED
pub inactive_concluded_onchain: Vec<(SessionIndex, CandidateHash)>,
}
impl PartitionedDisputes {
fn new() -> PartitionedDisputes {
Default::default()
}
fn into_iter(self) -> impl Iterator<Item = (SessionIndex, CandidateHash)> {
self.inactive_unknown_onchain
.into_iter()
.chain(self.inactive_unconcluded_onchain.into_iter())
.chain(self.active_unknown_onchain.into_iter())
.chain(self.active_unconcluded_onchain.into_iter())
.chain(self.active_concluded_onchain.into_iter())
// inactive_concluded_onchain is dropped on purpose
}
}
fn secs_since_epoch() -> Timestamp {
match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(d) => d.as_secs(),
Err(e) => {
gum::warn!(
target: LOG_TARGET,
err = ?e,
"Error getting system time."
);
0
},
}
}
fn concluded_onchain(onchain_state: &DisputeState) -> bool {
// Check if there are enough onchain votes for or against to conclude the dispute
let supermajority = supermajority_threshold(onchain_state.validators_for.len());
onchain_state.validators_for.count_ones() >= supermajority ||
onchain_state.validators_against.count_ones() >= supermajority
}
fn partition_recent_disputes(
recent: BTreeMap<(SessionIndex, CandidateHash), DisputeStatus>,
onchain: &HashMap<(SessionIndex, CandidateHash), DisputeState>,
) -> PartitionedDisputes {
let mut partitioned = PartitionedDisputes::new();
let time_now = &secs_since_epoch();
for ((session_index, candidate_hash), dispute_state) in recent {
let key = (session_index, candidate_hash);
if dispute_is_inactive(&dispute_state, time_now) {
match onchain.get(&key) {
Some(onchain_state) =>
if concluded_onchain(onchain_state) {
partitioned
.inactive_concluded_onchain
.push((session_index, candidate_hash));
} else {
partitioned
.inactive_unconcluded_onchain
.push((session_index, candidate_hash));
},
None => partitioned.inactive_unknown_onchain.push((session_index, candidate_hash)),
}
} else {
match onchain.get(&(session_index, candidate_hash)) {
Some(d) => match concluded_onchain(d) {
true =>
partitioned.active_concluded_onchain.push((session_index, candidate_hash)),
false =>
partitioned.active_unconcluded_onchain.push((session_index, candidate_hash)),
},
None => partitioned.active_unknown_onchain.push((session_index, candidate_hash)),
}
}
}
partitioned
}
/// Determines if a vote is worth to be kept, based on the onchain disputes
fn is_vote_worth_to_keep(
validator_index: &ValidatorIndex,
dispute_statement: DisputeStatement,
onchain_state: &DisputeState,
) -> bool {
let (offchain_vote, valid_kind) = match dispute_statement {
DisputeStatement::Valid(kind) => (true, Some(kind)),
DisputeStatement::Invalid(_) => (false, None),
};
// We want to keep all backing votes. This maximizes the number of backers
// punished when misbehaving.
if let Some(kind) = valid_kind {
match kind {
ValidDisputeStatementKind::BackingValid(_) |
ValidDisputeStatementKind::BackingSeconded(_) => return true,
_ => (),
}
}
let in_validators_for = onchain_state
.validators_for
.get(validator_index.0 as usize)
.as_deref()
.copied()
.unwrap_or(false);
let in_validators_against = onchain_state
.validators_against
.get(validator_index.0 as usize)
.as_deref()
.copied()
.unwrap_or(false);
if in_validators_for && in_validators_against {
// The validator has double voted and runtime knows about this. Ignore this vote.
return false;
}
if offchain_vote && in_validators_against || !offchain_vote && in_validators_for {
// offchain vote differs from the onchain vote
// we need this vote to punish the offending validator
return true;
}
// The vote is valid. Return true if it is not seen onchain.
!in_validators_for && !in_validators_against
}
/// Request disputes identified by `CandidateHash` and the `SessionIndex`.
async fn request_disputes(
sender: &mut impl overseer::ProvisionerSenderTrait,
) -> BTreeMap<(SessionIndex, CandidateHash), DisputeStatus> {
let (tx, rx) = oneshot::channel();
let msg = DisputeCoordinatorMessage::RecentDisputes(tx);
// Bounded by block production - `ProvisionerMessage::RequestInherentData`.
sender.send_unbounded_message(msg);
let recent_disputes = rx.await.unwrap_or_else(|err| {
gum::warn!(target: LOG_TARGET, err=?err, "Unable to gather recent disputes");
BTreeMap::new()
});
recent_disputes
}
// This function produces the return value for `pub fn select_disputes()`
fn make_multi_dispute_statement_set(
metrics: &metrics::Metrics,
dispute_candidate_votes: BTreeMap<(SessionIndex, CandidateHash), CandidateVotes>,
) -> MultiDisputeStatementSet {
// Transform all `CandidateVotes` into `MultiDisputeStatementSet`.
dispute_candidate_votes
.into_iter()
.map(|((session_index, candidate_hash), votes)| {
let valid_statements = votes
.valid
.into_iter()
.map(|(i, (s, sig))| (DisputeStatement::Valid(s), i, sig));
let invalid_statements = votes
.invalid
.into_iter()
.map(|(i, (s, sig))| (DisputeStatement::Invalid(s), i, sig));
metrics.inc_valid_statements_by(valid_statements.len());
metrics.inc_invalid_statements_by(invalid_statements.len());
metrics.inc_dispute_statement_sets_by(1);
DisputeStatementSet {
candidate_hash,
session: session_index,
statements: valid_statements.chain(invalid_statements).collect(),
}
})
.collect()
}
/// Gets the on-chain disputes at a given block number and returns them as a `HashMap` so that
/// searching in them is cheap.
pub async fn get_onchain_disputes<Sender>(
sender: &mut Sender,
relay_parent: Hash,
) -> Result<HashMap<(SessionIndex, CandidateHash), DisputeState>, GetOnchainDisputesError>
where
Sender: overseer::ProvisionerSenderTrait,
{
gum::trace!(target: LOG_TARGET, ?relay_parent, "Fetching on-chain disputes");
let (tx, rx) = oneshot::channel();
sender
.send_message(RuntimeApiMessage::Request(relay_parent, RuntimeApiRequest::Disputes(tx)))
.await;
rx.await
.map_err(|_| GetOnchainDisputesError::Channel)
.and_then(|res| {
res.map_err(|e| match e {
RuntimeApiError::Execution { .. } =>
GetOnchainDisputesError::Execution(e, relay_parent),
RuntimeApiError::NotSupported { .. } =>
GetOnchainDisputesError::NotSupported(e, relay_parent),
})
})
.map(|v| v.into_iter().map(|e| ((e.0, e.1), e.2)).collect())
}
@@ -0,0 +1,764 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
use super::super::{
super::{tests::common::test_harness, *},
prioritized_selection::*,
};
use bitvec::prelude::*;
use futures::channel::mpsc;
use pezkuwi_node_primitives::{CandidateVotes, DisputeStatus, ACTIVE_DURATION_SECS};
use pezkuwi_node_subsystem::messages::{
AllMessages, DisputeCoordinatorMessage, RuntimeApiMessage, RuntimeApiRequest,
};
use pezkuwi_node_subsystem_test_helpers::{mock::new_leaf, TestSubsystemSender};
use pezkuwi_primitives::{
CandidateHash, CandidateReceiptV2 as CandidateReceipt, DisputeState,
InvalidDisputeStatementKind, SessionIndex, ValidDisputeStatementKind, ValidatorSignature,
};
//
// Unit tests for various functions
//
#[test]
fn should_keep_vote_behaves() {
let onchain_state = DisputeState {
validators_for: bitvec![u8, Lsb0; 1, 0, 1, 0, 1],
validators_against: bitvec![u8, Lsb0; 0, 1, 0, 0, 1],
start: 1,
concluded_at: None,
};
let local_valid_known = (ValidatorIndex(0), ValidDisputeStatementKind::Explicit);
let local_valid_unknown = (ValidatorIndex(3), ValidDisputeStatementKind::Explicit);
let local_invalid_known = (ValidatorIndex(1), InvalidDisputeStatementKind::Explicit);
let local_invalid_unknown = (ValidatorIndex(3), InvalidDisputeStatementKind::Explicit);
assert_eq!(
is_vote_worth_to_keep(
&local_valid_known.0,
DisputeStatement::Valid(local_valid_known.1),
&onchain_state
),
false
);
assert_eq!(
is_vote_worth_to_keep(
&local_valid_unknown.0,
DisputeStatement::Valid(local_valid_unknown.1),
&onchain_state
),
true
);
assert_eq!(
is_vote_worth_to_keep(
&local_invalid_known.0,
DisputeStatement::Invalid(local_invalid_known.1),
&onchain_state
),
false
);
assert_eq!(
is_vote_worth_to_keep(
&local_invalid_unknown.0,
DisputeStatement::Invalid(local_invalid_unknown.1),
&onchain_state
),
true
);
//double voting - onchain knows
let local_double_vote_onchain_knows =
(ValidatorIndex(4), InvalidDisputeStatementKind::Explicit);
assert_eq!(
is_vote_worth_to_keep(
&local_double_vote_onchain_knows.0,
DisputeStatement::Invalid(local_double_vote_onchain_knows.1),
&onchain_state
),
false
);
//double voting - onchain doesn't know
let local_double_vote_onchain_doesnt_knows =
(ValidatorIndex(0), InvalidDisputeStatementKind::Explicit);
assert_eq!(
is_vote_worth_to_keep(
&local_double_vote_onchain_doesnt_knows.0,
DisputeStatement::Invalid(local_double_vote_onchain_doesnt_knows.1),
&onchain_state
),
true
);
// empty onchain state
let empty_onchain_state = DisputeState {
validators_for: BitVec::new(),
validators_against: BitVec::new(),
start: 1,
concluded_at: None,
};
assert_eq!(
is_vote_worth_to_keep(
&local_double_vote_onchain_doesnt_knows.0,
DisputeStatement::Invalid(local_double_vote_onchain_doesnt_knows.1),
&empty_onchain_state
),
true
);
}
#[test]
fn partitioning_happy_case() {
let mut input = BTreeMap::<(SessionIndex, CandidateHash), DisputeStatus>::new();
let mut onchain = HashMap::<(u32, CandidateHash), DisputeState>::new();
let time_now = secs_since_epoch();
// Create one dispute for each partition
let inactive_unknown_onchain = (
(0, CandidateHash(Hash::random())),
DisputeStatus::ConcludedFor(time_now - ACTIVE_DURATION_SECS * 2),
);
input.insert(inactive_unknown_onchain.0, inactive_unknown_onchain.1);
let inactive_unconcluded_onchain = (
(1, CandidateHash(Hash::random())),
DisputeStatus::ConcludedFor(time_now - ACTIVE_DURATION_SECS * 2),
);
input.insert(inactive_unconcluded_onchain.0, inactive_unconcluded_onchain.1);
onchain.insert(
inactive_unconcluded_onchain.0,
DisputeState {
validators_for: bitvec![u8, Lsb0; 1, 1, 1, 0, 0, 0, 0, 0, 0],
validators_against: bitvec![u8, Lsb0; 0, 0, 0, 0, 0, 0, 0, 0, 0],
start: 1,
concluded_at: None,
},
);
let active_unknown_onchain = ((2, CandidateHash(Hash::random())), DisputeStatus::Active);
input.insert(active_unknown_onchain.0, active_unknown_onchain.1);
let active_unconcluded_onchain = ((3, CandidateHash(Hash::random())), DisputeStatus::Active);
input.insert(active_unconcluded_onchain.0, active_unconcluded_onchain.1);
onchain.insert(
active_unconcluded_onchain.0,
DisputeState {
validators_for: bitvec![u8, Lsb0; 1, 1, 1, 0, 0, 0, 0, 0, 0],
validators_against: bitvec![u8, Lsb0; 0, 0, 0, 0, 0, 0, 0, 0, 0],
start: 1,
concluded_at: None,
},
);
let active_concluded_onchain = ((4, CandidateHash(Hash::random())), DisputeStatus::Active);
input.insert(active_concluded_onchain.0, active_concluded_onchain.1);
onchain.insert(
active_concluded_onchain.0,
DisputeState {
validators_for: bitvec![u8, Lsb0; 1, 1, 1, 1, 1, 1, 1, 1, 0],
validators_against: bitvec![u8, Lsb0; 0, 0, 0, 0, 0, 0, 0, 0, 0],
start: 1,
concluded_at: Some(3),
},
);
let inactive_concluded_onchain = (
(5, CandidateHash(Hash::random())),
DisputeStatus::ConcludedFor(time_now - ACTIVE_DURATION_SECS * 2),
);
input.insert(inactive_concluded_onchain.0, inactive_concluded_onchain.1);
onchain.insert(
inactive_concluded_onchain.0,
DisputeState {
validators_for: bitvec![u8, Lsb0; 1, 1, 1, 1, 1, 1, 1, 0, 0],
validators_against: bitvec![u8, Lsb0; 0, 0, 0, 0, 0, 0, 0, 0, 0],
start: 1,
concluded_at: Some(3),
},
);
let result = partition_recent_disputes(input, &onchain);
// Check results
assert_eq!(result.inactive_unknown_onchain.len(), 1);
assert_eq!(result.inactive_unknown_onchain.get(0).unwrap(), &inactive_unknown_onchain.0);
assert_eq!(result.inactive_unconcluded_onchain.len(), 1);
assert_eq!(
result.inactive_unconcluded_onchain.get(0).unwrap(),
&inactive_unconcluded_onchain.0
);
assert_eq!(result.active_unknown_onchain.len(), 1);
assert_eq!(result.active_unknown_onchain.get(0).unwrap(), &active_unknown_onchain.0);
assert_eq!(result.active_unconcluded_onchain.len(), 1);
assert_eq!(result.active_unconcluded_onchain.get(0).unwrap(), &active_unconcluded_onchain.0);
assert_eq!(result.active_concluded_onchain.len(), 1);
assert_eq!(result.active_concluded_onchain.get(0).unwrap(), &active_concluded_onchain.0);
assert_eq!(result.inactive_concluded_onchain.len(), 1);
assert_eq!(result.inactive_concluded_onchain.get(0).unwrap(), &inactive_concluded_onchain.0);
}
// This test verifies the double voting behavior. Currently we don't care if a supermajority is
// achieved with or without the 'help' of a double vote (a validator voting for and against at the
// same time). This makes the test a bit pointless but anyway I'm leaving it here to make this
// decision explicit and have the test code ready in case this behavior needs to be further tested
// in the future. Link to the PR with the discussions: https://github.com/paritytech/polkadot/pull/5567
#[test]
fn partitioning_doubled_onchain_vote() {
let mut input = BTreeMap::<(SessionIndex, CandidateHash), DisputeStatus>::new();
let mut onchain = HashMap::<(u32, CandidateHash), DisputeState>::new();
// Dispute A relies on a 'double onchain vote' to conclude. Validator with index 0 has voted
// both `for` and `against`. Despite that this dispute should be considered 'can conclude
// onchain'.
let dispute_a = ((3, CandidateHash(Hash::random())), DisputeStatus::Active);
// Dispute B has supermajority + 1 votes, so the doubled onchain vote doesn't affect it. It
// should be considered as 'can conclude onchain'.
let dispute_b = ((4, CandidateHash(Hash::random())), DisputeStatus::Active);
input.insert(dispute_a.0, dispute_a.1);
input.insert(dispute_b.0, dispute_b.1);
onchain.insert(
dispute_a.0,
DisputeState {
validators_for: bitvec![u8, Lsb0; 1, 1, 1, 1, 1, 1, 1, 0, 0],
validators_against: bitvec![u8, Lsb0; 1, 0, 0, 0, 0, 0, 0, 0, 0],
start: 1,
concluded_at: None,
},
);
onchain.insert(
dispute_b.0,
DisputeState {
validators_for: bitvec![u8, Lsb0; 1, 1, 1, 1, 1, 1, 1, 1, 0],
validators_against: bitvec![u8, Lsb0; 1, 0, 0, 0, 0, 0, 0, 0, 0],
start: 1,
concluded_at: None,
},
);
let result = partition_recent_disputes(input, &onchain);
assert_eq!(result.active_unconcluded_onchain.len(), 0);
assert_eq!(result.active_concluded_onchain.len(), 2);
}
#[test]
fn partitioning_duplicated_dispute() {
let mut input = BTreeMap::<(SessionIndex, CandidateHash), DisputeStatus>::new();
let mut onchain = HashMap::<(u32, CandidateHash), DisputeState>::new();
let some_dispute = ((3, CandidateHash(Hash::random())), DisputeStatus::Active);
input.insert(some_dispute.0, some_dispute.1);
input.insert(some_dispute.0, some_dispute.1);
onchain.insert(
some_dispute.0,
DisputeState {
validators_for: bitvec![u8, Lsb0; 1, 1, 1, 0, 0, 0, 0, 0, 0],
validators_against: bitvec![u8, Lsb0; 0, 0, 0, 0, 0, 0, 0, 0, 0],
start: 1,
concluded_at: None,
},
);
let result = partition_recent_disputes(input, &onchain);
assert_eq!(result.active_unconcluded_onchain.len(), 1);
assert_eq!(result.active_unconcluded_onchain.get(0).unwrap(), &some_dispute.0);
}
//
// end-to-end tests for select_disputes()
//
async fn mock_overseer(
mut receiver: mpsc::UnboundedReceiver<AllMessages>,
disputes_db: &mut TestDisputes,
vote_queries_count: &mut usize,
) {
while let Some(from_job) = receiver.next().await {
match from_job {
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
_,
RuntimeApiRequest::Disputes(sender),
)) => {
let _ = sender.send(Ok(disputes_db
.onchain_disputes
.clone()
.into_iter()
.map(|(k, v)| (k.0, k.1, v))
.collect::<Vec<_>>()));
},
AllMessages::RuntimeApi(_) => panic!("Unexpected RuntimeApi request"),
AllMessages::DisputeCoordinator(DisputeCoordinatorMessage::RecentDisputes(sender)) => {
let _ = sender.send(disputes_db.local_disputes.clone());
},
AllMessages::DisputeCoordinator(DisputeCoordinatorMessage::QueryCandidateVotes(
disputes,
sender,
)) => {
*vote_queries_count += 1;
let mut res = Vec::new();
for d in disputes.iter() {
let v = disputes_db.votes_db.get(d).unwrap().clone();
res.push((d.0, d.1, v));
}
let _ = sender.send(res);
},
_ => panic!("Unexpected message: {:?}", from_job),
}
}
}
fn leaf() -> ActivatedLeaf {
new_leaf(Hash::repeat_byte(0xAA), 0xAA)
}
struct TestDisputes {
pub local_disputes: BTreeMap<(SessionIndex, CandidateHash), DisputeStatus>,
pub votes_db: HashMap<(SessionIndex, CandidateHash), CandidateVotes>,
pub onchain_disputes: HashMap<(u32, CandidateHash), DisputeState>,
validators_count: usize,
}
impl TestDisputes {
pub fn new(validators_count: usize) -> TestDisputes {
TestDisputes {
local_disputes: BTreeMap::<(SessionIndex, CandidateHash), DisputeStatus>::new(),
votes_db: HashMap::<(SessionIndex, CandidateHash), CandidateVotes>::new(),
onchain_disputes: HashMap::<(u32, CandidateHash), DisputeState>::new(),
validators_count,
}
}
// Offchain disputes are on node side
fn add_offchain_dispute(
&mut self,
dispute: (SessionIndex, CandidateHash, DisputeStatus),
local_votes_count: usize,
dummy_receipt: CandidateReceipt,
) {
self.local_disputes.insert((dispute.0, dispute.1), dispute.2);
self.votes_db.insert(
(dispute.0, dispute.1),
CandidateVotes {
candidate_receipt: dummy_receipt,
valid: TestDisputes::generate_local_votes(
ValidDisputeStatementKind::Explicit,
0,
local_votes_count,
)
.into_iter()
.collect(),
invalid: BTreeMap::new(),
},
);
}
fn add_onchain_dispute(
&mut self,
dispute: (SessionIndex, CandidateHash, DisputeStatus),
onchain_votes_count: usize,
) {
let concluded_at = match dispute.2 {
DisputeStatus::Active | DisputeStatus::Confirmed => None,
DisputeStatus::ConcludedAgainst(_) | DisputeStatus::ConcludedFor(_) => Some(1),
};
self.onchain_disputes.insert(
(dispute.0, dispute.1),
DisputeState {
validators_for: TestDisputes::generate_bitvec(
self.validators_count,
0,
onchain_votes_count,
),
validators_against: bitvec![u8, Lsb0; 0; self.validators_count],
start: 1,
concluded_at,
},
);
}
pub fn add_unconfirmed_disputes_concluded_onchain(
&mut self,
dispute_count: usize,
) -> (SessionIndex, usize) {
let local_votes_count = self.validators_count * 90 / 100;
let onchain_votes_count = self.validators_count * 80 / 100;
let session_idx = 0;
let lf = leaf();
let dummy_receipt = pezkuwi_primitives_test_helpers::dummy_candidate_receipt_v2(lf.hash);
for _ in 0..dispute_count {
let d = (session_idx, CandidateHash(Hash::random()), DisputeStatus::Active);
self.add_offchain_dispute(d, local_votes_count, dummy_receipt.clone());
self.add_onchain_dispute(d, onchain_votes_count);
}
(session_idx, (local_votes_count - onchain_votes_count) * dispute_count)
}
pub fn add_unconfirmed_disputes_unconcluded_onchain(
&mut self,
dispute_count: usize,
) -> (SessionIndex, usize) {
let local_votes_count = self.validators_count * 90 / 100;
let onchain_votes_count = self.validators_count * 40 / 100;
let session_idx = 1;
let lf = leaf();
let dummy_receipt = pezkuwi_primitives_test_helpers::dummy_candidate_receipt_v2(lf.hash);
for _ in 0..dispute_count {
let d = (session_idx, CandidateHash(Hash::random()), DisputeStatus::Active);
self.add_offchain_dispute(d, local_votes_count, dummy_receipt.clone());
self.add_onchain_dispute(d, onchain_votes_count);
}
(session_idx, (local_votes_count - onchain_votes_count) * dispute_count)
}
pub fn add_confirmed_disputes_unknown_onchain(
&mut self,
dispute_count: usize,
) -> (SessionIndex, usize) {
let local_votes_count = self.validators_count * 90 / 100;
let session_idx = 2;
let lf = leaf();
let dummy_receipt = pezkuwi_primitives_test_helpers::dummy_candidate_receipt_v2(lf.hash);
for _ in 0..dispute_count {
let d = (session_idx, CandidateHash(Hash::random()), DisputeStatus::Confirmed);
self.add_offchain_dispute(d, local_votes_count, dummy_receipt.clone());
}
(session_idx, local_votes_count * dispute_count)
}
pub fn add_concluded_disputes_known_onchain(
&mut self,
dispute_count: usize,
) -> (SessionIndex, usize) {
let local_votes_count = self.validators_count * 90 / 100;
let onchain_votes_count = self.validators_count * 75 / 100;
let session_idx = 3;
let lf = leaf();
let dummy_receipt = pezkuwi_primitives_test_helpers::dummy_candidate_receipt_v2(lf.hash);
for _ in 0..dispute_count {
let d = (session_idx, CandidateHash(Hash::random()), DisputeStatus::ConcludedFor(0));
self.add_offchain_dispute(d, local_votes_count, dummy_receipt.clone());
self.add_onchain_dispute(d, onchain_votes_count);
}
(session_idx, (local_votes_count - onchain_votes_count) * dispute_count)
}
pub fn add_concluded_disputes_unknown_onchain(
&mut self,
dispute_count: usize,
) -> (SessionIndex, usize) {
let local_votes_count = self.validators_count * 90 / 100;
let session_idx = 4;
let lf = leaf();
let dummy_receipt = pezkuwi_primitives_test_helpers::dummy_candidate_receipt_v2(lf.hash);
for _ in 0..dispute_count {
let d = (session_idx, CandidateHash(Hash::random()), DisputeStatus::ConcludedFor(0));
self.add_offchain_dispute(d, local_votes_count, dummy_receipt.clone());
}
(session_idx, local_votes_count * dispute_count)
}
pub fn add_unconfirmed_disputes_known_onchain(
&mut self,
dispute_count: usize,
) -> (SessionIndex, usize) {
let local_votes_count = self.validators_count * 10 / 100;
let onchain_votes_count = self.validators_count * 10 / 100;
let session_idx = 5;
let lf = leaf();
let dummy_receipt = pezkuwi_primitives_test_helpers::dummy_candidate_receipt_v2(lf.hash);
for _ in 0..dispute_count {
let d = (session_idx, CandidateHash(Hash::random()), DisputeStatus::Active);
self.add_offchain_dispute(d, local_votes_count, dummy_receipt.clone());
self.add_onchain_dispute(d, onchain_votes_count);
}
(session_idx, (local_votes_count - onchain_votes_count) * dispute_count)
}
pub fn add_unconfirmed_disputes_unknown_onchain(
&mut self,
dispute_count: usize,
) -> (SessionIndex, usize) {
let local_votes_count = self.validators_count * 10 / 100;
let session_idx = 6;
let lf = leaf();
let dummy_receipt = pezkuwi_primitives_test_helpers::dummy_candidate_receipt_v2(lf.hash);
for _ in 0..dispute_count {
let d = (session_idx, CandidateHash(Hash::random()), DisputeStatus::Active);
self.add_offchain_dispute(d, local_votes_count, dummy_receipt.clone());
}
(session_idx, local_votes_count * dispute_count)
}
fn generate_local_votes<T: Clone>(
statement_kind: T,
start_idx: usize,
count: usize,
) -> BTreeMap<ValidatorIndex, (T, ValidatorSignature)> {
assert!(start_idx < count);
(start_idx..count)
.map(|idx| {
(
ValidatorIndex(idx as u32),
(statement_kind.clone(), pezkuwi_primitives_test_helpers::dummy_signature()),
)
})
.collect::<BTreeMap<_, _>>()
}
fn generate_bitvec(
validator_count: usize,
start_idx: usize,
count: usize,
) -> BitVec<u8, bitvec::order::Lsb0> {
assert!(start_idx < count);
assert!(start_idx + count < validator_count);
let mut res = bitvec![u8, Lsb0; 0; validator_count];
for idx in start_idx..count {
res.set(idx, true);
}
res
}
}
#[test]
fn normal_flow() {
const VALIDATOR_COUNT: usize = 10;
const DISPUTES_PER_BATCH: usize = 2;
const ACCEPTABLE_RUNTIME_VOTES_QUERIES_COUNT: usize = 1;
let mut input = TestDisputes::new(VALIDATOR_COUNT);
// active, concluded onchain
let (third_idx, third_votes) =
input.add_unconfirmed_disputes_concluded_onchain(DISPUTES_PER_BATCH);
// active unconcluded onchain
let (first_idx, first_votes) =
input.add_unconfirmed_disputes_unconcluded_onchain(DISPUTES_PER_BATCH);
//concluded disputes unknown onchain
let (fifth_idx, fifth_votes) = input.add_concluded_disputes_unknown_onchain(DISPUTES_PER_BATCH);
// concluded disputes known onchain - these should be ignored
let (_, _) = input.add_concluded_disputes_known_onchain(DISPUTES_PER_BATCH);
// confirmed disputes unknown onchain
let (second_idx, second_votes) =
input.add_confirmed_disputes_unknown_onchain(DISPUTES_PER_BATCH);
let metrics = metrics::Metrics::new_dummy();
let mut vote_queries: usize = 0;
test_harness(
|r| mock_overseer(r, &mut input, &mut vote_queries),
|mut tx: TestSubsystemSender| async move {
let lf = leaf();
let result = select_disputes(&mut tx, &metrics, &lf).await;
assert!(!result.is_empty());
assert_eq!(result.len(), 4 * DISPUTES_PER_BATCH);
// Naive checks that the result is partitioned correctly
let (first_batch, rest): (Vec<DisputeStatementSet>, Vec<DisputeStatementSet>) =
result.into_iter().partition(|d| d.session == first_idx);
assert_eq!(first_batch.len(), DISPUTES_PER_BATCH);
let (second_batch, rest): (Vec<DisputeStatementSet>, Vec<DisputeStatementSet>) =
rest.into_iter().partition(|d| d.session == second_idx);
assert_eq!(second_batch.len(), DISPUTES_PER_BATCH);
let (third_batch, rest): (Vec<DisputeStatementSet>, Vec<DisputeStatementSet>) =
rest.into_iter().partition(|d| d.session == third_idx);
assert_eq!(third_batch.len(), DISPUTES_PER_BATCH);
let (fifth_batch, rest): (Vec<DisputeStatementSet>, Vec<DisputeStatementSet>) =
rest.into_iter().partition(|d| d.session == fifth_idx);
assert_eq!(fifth_batch.len(), DISPUTES_PER_BATCH);
// Ensure there are no more disputes - fourth_batch should be dropped
assert_eq!(rest.len(), 0);
assert_eq!(
first_batch.iter().map(|d| d.statements.len()).fold(0, |acc, v| acc + v),
first_votes
);
assert_eq!(
second_batch.iter().map(|d| d.statements.len()).fold(0, |acc, v| acc + v),
second_votes
);
assert_eq!(
third_batch.iter().map(|d| d.statements.len()).fold(0, |acc, v| acc + v),
third_votes
);
assert_eq!(
fifth_batch.iter().map(|d| d.statements.len()).fold(0, |acc, v| acc + v),
fifth_votes
);
},
);
assert!(vote_queries <= ACCEPTABLE_RUNTIME_VOTES_QUERIES_COUNT);
}
#[test]
fn many_batches() {
const VALIDATOR_COUNT: usize = 10;
const DISPUTES_PER_PARTITION: usize = 10;
// 10 disputes per partition * 4 partitions = 40 disputes
// BATCH_SIZE = 11
// => There should be no more than 40 / 11 queries ( ~4 )
const ACCEPTABLE_RUNTIME_VOTES_QUERIES_COUNT: usize = 4;
let mut input = TestDisputes::new(VALIDATOR_COUNT);
// active which can conclude onchain
input.add_unconfirmed_disputes_concluded_onchain(DISPUTES_PER_PARTITION);
// active which can't conclude onchain
input.add_unconfirmed_disputes_unconcluded_onchain(DISPUTES_PER_PARTITION);
//concluded disputes unknown onchain
input.add_concluded_disputes_unknown_onchain(DISPUTES_PER_PARTITION);
// concluded disputes known onchain
input.add_concluded_disputes_known_onchain(DISPUTES_PER_PARTITION);
// confirmed disputes unknown onchain
input.add_confirmed_disputes_unknown_onchain(DISPUTES_PER_PARTITION);
let metrics = metrics::Metrics::new_dummy();
let mut vote_queries: usize = 0;
test_harness(
|r| mock_overseer(r, &mut input, &mut vote_queries),
|mut tx: TestSubsystemSender| async move {
let lf = leaf();
let result = select_disputes(&mut tx, &metrics, &lf).await;
assert!(!result.is_empty());
let vote_count = result.iter().map(|d| d.statements.len()).fold(0, |acc, v| acc + v);
assert!(
MAX_DISPUTE_VOTES_FORWARDED_TO_RUNTIME - VALIDATOR_COUNT <= vote_count &&
vote_count <= MAX_DISPUTE_VOTES_FORWARDED_TO_RUNTIME,
"vote_count: {}",
vote_count
);
},
);
assert!(
vote_queries <= ACCEPTABLE_RUNTIME_VOTES_QUERIES_COUNT,
"vote_queries: {} ACCEPTABLE_RUNTIME_VOTES_QUERIES_COUNT: {}",
vote_queries,
ACCEPTABLE_RUNTIME_VOTES_QUERIES_COUNT
);
}
#[test]
fn votes_above_limit() {
const VALIDATOR_COUNT: usize = 10;
const DISPUTES_PER_PARTITION: usize = 50;
const ACCEPTABLE_RUNTIME_VOTES_QUERIES_COUNT: usize = 4;
let mut input = TestDisputes::new(VALIDATOR_COUNT);
// active which can conclude onchain
let (_, second_votes) =
input.add_unconfirmed_disputes_concluded_onchain(DISPUTES_PER_PARTITION);
// active which can't conclude onchain
let (_, first_votes) =
input.add_unconfirmed_disputes_unconcluded_onchain(DISPUTES_PER_PARTITION);
//concluded disputes unknown onchain
let (_, third_votes) = input.add_concluded_disputes_unknown_onchain(DISPUTES_PER_PARTITION);
assert!(
first_votes + second_votes + third_votes > 3 * MAX_DISPUTE_VOTES_FORWARDED_TO_RUNTIME,
"Total relevant votes generated: {}",
first_votes + second_votes + third_votes
);
let metrics = metrics::Metrics::new_dummy();
let mut vote_queries: usize = 0;
test_harness(
|r| mock_overseer(r, &mut input, &mut vote_queries),
|mut tx: TestSubsystemSender| async move {
let lf = leaf();
let result = select_disputes(&mut tx, &metrics, &lf).await;
assert!(!result.is_empty());
let vote_count = result.iter().map(|d| d.statements.len()).fold(0, |acc, v| acc + v);
assert!(
MAX_DISPUTE_VOTES_FORWARDED_TO_RUNTIME - VALIDATOR_COUNT <= vote_count &&
vote_count <= MAX_DISPUTE_VOTES_FORWARDED_TO_RUNTIME,
"vote_count: {}",
vote_count
);
},
);
assert!(
vote_queries <= ACCEPTABLE_RUNTIME_VOTES_QUERIES_COUNT,
"vote_queries: {} ACCEPTABLE_RUNTIME_VOTES_QUERIES_COUNT: {}",
vote_queries,
ACCEPTABLE_RUNTIME_VOTES_QUERIES_COUNT
);
}
#[test]
fn unconfirmed_are_handled_correctly() {
const VALIDATOR_COUNT: usize = 10;
const DISPUTES_PER_PARTITION: usize = 50;
let mut input = TestDisputes::new(VALIDATOR_COUNT);
// Add unconfirmed known onchain -> this should be pushed
let (pushed_idx, _) = input.add_unconfirmed_disputes_known_onchain(DISPUTES_PER_PARTITION);
// Add unconfirmed unknown onchain -> this should be ignored
input.add_unconfirmed_disputes_unknown_onchain(DISPUTES_PER_PARTITION);
let metrics = metrics::Metrics::new_dummy();
let mut vote_queries: usize = 0;
test_harness(
|r| mock_overseer(r, &mut input, &mut vote_queries),
|mut tx: TestSubsystemSender| async move {
let lf = leaf();
let result = select_disputes(&mut tx, &metrics, &lf).await;
assert!(result.len() == DISPUTES_PER_PARTITION);
result.iter().for_each(|d| assert!(d.session == pushed_idx));
},
);
}