feat: initialize Kurdistan SDK - independent fork of Polkadot SDK
This commit is contained in:
@@ -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));
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
// 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/>.
|
||||
|
||||
///! Error types for provisioner module
|
||||
use fatality::Nested;
|
||||
use futures::channel::{mpsc, oneshot};
|
||||
use pezkuwi_node_subsystem::errors::{ChainApiError, RuntimeApiError, SubsystemError};
|
||||
use pezkuwi_node_subsystem_util as util;
|
||||
use pezkuwi_primitives::Hash;
|
||||
|
||||
pub type FatalResult<T> = std::result::Result<T, FatalError>;
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Errors in the provisioner.
|
||||
#[allow(missing_docs)]
|
||||
#[fatality::fatality(splitable)]
|
||||
pub enum Error {
|
||||
#[fatal(forward)]
|
||||
#[error("Error while accessing runtime information")]
|
||||
Runtime(#[from] util::runtime::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Util(#[from] util::Error),
|
||||
|
||||
#[error("failed to get availability cores")]
|
||||
CanceledAvailabilityCores(#[source] oneshot::Canceled),
|
||||
|
||||
#[error("failed to get persisted validation data")]
|
||||
CanceledPersistedValidationData(#[source] oneshot::Canceled),
|
||||
|
||||
#[error("failed to get block number")]
|
||||
CanceledBlockNumber(#[source] oneshot::Canceled),
|
||||
|
||||
#[error("failed to get session index")]
|
||||
CanceledSessionIndex(#[source] oneshot::Canceled),
|
||||
|
||||
#[error("failed to get node features")]
|
||||
CanceledNodeFeatures(#[source] oneshot::Canceled),
|
||||
|
||||
#[error("failed to get backed candidates")]
|
||||
CanceledBackedCandidates(#[source] oneshot::Canceled),
|
||||
|
||||
#[error("failed to get votes on dispute")]
|
||||
CanceledCandidateVotes(#[source] oneshot::Canceled),
|
||||
|
||||
#[error("failed to get backable candidates from prospective teyrchains")]
|
||||
CanceledBackableCandidates(#[source] oneshot::Canceled),
|
||||
|
||||
#[error(transparent)]
|
||||
ChainApi(#[from] ChainApiError),
|
||||
|
||||
#[error(transparent)]
|
||||
RuntimeApi(#[from] RuntimeApiError),
|
||||
|
||||
#[error("failed to send message to ChainAPI")]
|
||||
ChainApiMessageSend(#[source] mpsc::SendError),
|
||||
|
||||
#[error("failed to send message to CandidateBacking to get backed candidates")]
|
||||
GetBackedCandidatesSend(#[source] mpsc::SendError),
|
||||
|
||||
#[error("Send inherent data timeout.")]
|
||||
SendInherentDataTimeout,
|
||||
|
||||
#[error("failed to send return message with Inherents")]
|
||||
InherentDataReturnChannel,
|
||||
|
||||
#[fatal]
|
||||
#[error("Failed to spawn background task")]
|
||||
FailedToSpawnBackgroundTask,
|
||||
|
||||
#[error(transparent)]
|
||||
SubsystemError(#[from] SubsystemError),
|
||||
|
||||
#[fatal]
|
||||
#[error(transparent)]
|
||||
OverseerExited(SubsystemError),
|
||||
}
|
||||
|
||||
/// Used by `get_onchain_disputes` to represent errors related to fetching on-chain disputes from
|
||||
/// the Runtime
|
||||
#[allow(dead_code)] // Remove when promoting to stable
|
||||
#[fatality::fatality]
|
||||
pub enum GetOnchainDisputesError {
|
||||
#[fatal]
|
||||
#[error("runtime subsystem is down")]
|
||||
Channel,
|
||||
|
||||
#[error("runtime execution error occurred while fetching onchain disputes for parent {1}")]
|
||||
Execution(#[source] RuntimeApiError, Hash),
|
||||
|
||||
#[error("runtime doesn't support RuntimeApiRequest::Disputes for parent {1}")]
|
||||
NotSupported(#[source] RuntimeApiError, Hash),
|
||||
}
|
||||
|
||||
pub fn log_error(result: Result<()>) -> std::result::Result<(), FatalError> {
|
||||
match result.into_nested()? {
|
||||
Ok(()) => Ok(()),
|
||||
Err(jfyi) => {
|
||||
jfyi.log();
|
||||
Ok(())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
impl JfyiError {
|
||||
/// Log a `JfyiError`.
|
||||
pub fn log(self) {
|
||||
gum::debug!(target: super::LOG_TARGET, error = ?self);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,802 @@
|
||||
// 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 provisioner is responsible for assembling a relay chain block
|
||||
//! from a set of available teyrchain candidates of its choice.
|
||||
|
||||
#![deny(missing_docs, unused_crate_dependencies)]
|
||||
|
||||
use bitvec::vec::BitVec;
|
||||
use futures::{
|
||||
channel::oneshot::{self, Canceled},
|
||||
future::BoxFuture,
|
||||
prelude::*,
|
||||
stream::FuturesUnordered,
|
||||
FutureExt,
|
||||
};
|
||||
use futures_timer::Delay;
|
||||
use pezkuwi_node_subsystem::{
|
||||
messages::{
|
||||
Ancestors, CandidateBackingMessage, ChainApiMessage, ProspectiveTeyrchainsMessage,
|
||||
ProvisionableData, ProvisionerInherentData, ProvisionerMessage,
|
||||
},
|
||||
overseer, ActivatedLeaf, ActiveLeavesUpdate, FromOrchestra, OverseerSignal, SpawnedSubsystem,
|
||||
SubsystemError,
|
||||
};
|
||||
use pezkuwi_node_subsystem_util::{request_availability_cores, TimeoutExt};
|
||||
use pezkuwi_primitives::{
|
||||
BackedCandidate, CandidateEvent, CandidateHash, CoreIndex, CoreState, Hash, Id as ParaId,
|
||||
SignedAvailabilityBitfield, ValidatorIndex,
|
||||
};
|
||||
use sc_consensus_slots::time_until_next_slot;
|
||||
use schnellru::{ByLength, LruMap};
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
time::Duration,
|
||||
};
|
||||
mod disputes;
|
||||
mod error;
|
||||
mod metrics;
|
||||
|
||||
pub use self::metrics::*;
|
||||
use error::{Error, FatalResult};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// How long to wait before proposing.
|
||||
const PRE_PROPOSE_TIMEOUT: std::time::Duration = core::time::Duration::from_millis(2000);
|
||||
/// Some timeout to ensure task won't hang around in the background forever on issues.
|
||||
const SEND_INHERENT_DATA_TIMEOUT: std::time::Duration = core::time::Duration::from_millis(500);
|
||||
|
||||
const LOG_TARGET: &str = "teyrchain::provisioner";
|
||||
|
||||
/// The provisioner subsystem.
|
||||
pub struct ProvisionerSubsystem {
|
||||
metrics: Metrics,
|
||||
}
|
||||
|
||||
impl ProvisionerSubsystem {
|
||||
/// Create a new instance of the `ProvisionerSubsystem`.
|
||||
pub fn new(metrics: Metrics) -> Self {
|
||||
Self { metrics }
|
||||
}
|
||||
}
|
||||
|
||||
/// A per-relay-parent state for the provisioning subsystem.
|
||||
pub struct PerRelayParent {
|
||||
leaf: ActivatedLeaf,
|
||||
signed_bitfields: Vec<SignedAvailabilityBitfield>,
|
||||
is_inherent_ready: bool,
|
||||
awaiting_inherent: Vec<oneshot::Sender<ProvisionerInherentData>>,
|
||||
}
|
||||
|
||||
impl PerRelayParent {
|
||||
fn new(leaf: ActivatedLeaf) -> Self {
|
||||
Self {
|
||||
leaf,
|
||||
signed_bitfields: Vec::new(),
|
||||
is_inherent_ready: false,
|
||||
awaiting_inherent: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type InherentDelays = FuturesUnordered<BoxFuture<'static, Hash>>;
|
||||
type SlotDelays = FuturesUnordered<BoxFuture<'static, Hash>>;
|
||||
type InherentReceivers =
|
||||
FuturesUnordered<BoxFuture<'static, (Hash, Result<ProvisionerInherentData, Canceled>)>>;
|
||||
|
||||
#[overseer::subsystem(Provisioner, error=SubsystemError, prefix=self::overseer)]
|
||||
impl<Context> ProvisionerSubsystem {
|
||||
fn start(self, ctx: Context) -> SpawnedSubsystem {
|
||||
let future = async move {
|
||||
run(ctx, self.metrics)
|
||||
.await
|
||||
.map_err(|e| SubsystemError::with_origin("provisioner", e))
|
||||
}
|
||||
.boxed();
|
||||
|
||||
SpawnedSubsystem { name: "provisioner-subsystem", future }
|
||||
}
|
||||
}
|
||||
|
||||
#[overseer::contextbounds(Provisioner, prefix = self::overseer)]
|
||||
async fn run<Context>(mut ctx: Context, metrics: Metrics) -> FatalResult<()> {
|
||||
let mut inherent_delays = InherentDelays::new();
|
||||
let mut inherent_receivers = InherentReceivers::new();
|
||||
let mut slot_delays = SlotDelays::new();
|
||||
let mut per_relay_parent = HashMap::new();
|
||||
let mut inherents = LruMap::new(ByLength::new(16));
|
||||
|
||||
loop {
|
||||
let result = run_iteration(
|
||||
&mut ctx,
|
||||
&mut per_relay_parent,
|
||||
&mut inherent_delays,
|
||||
&mut inherent_receivers,
|
||||
&mut inherents,
|
||||
&mut slot_delays,
|
||||
&metrics,
|
||||
)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(()) => break,
|
||||
err => crate::error::log_error(err)?,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[overseer::contextbounds(Provisioner, prefix = self::overseer)]
|
||||
async fn run_iteration<Context>(
|
||||
ctx: &mut Context,
|
||||
per_relay_parent: &mut HashMap<Hash, PerRelayParent>,
|
||||
inherent_delays: &mut InherentDelays,
|
||||
inherent_receivers: &mut InherentReceivers,
|
||||
inherents: &mut LruMap<Hash, ProvisionerInherentData>,
|
||||
slot_delays: &mut SlotDelays,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), Error> {
|
||||
loop {
|
||||
futures::select! {
|
||||
from_overseer = ctx.recv().fuse() => {
|
||||
// Map the error to ensure that the subsystem exits when the overseer is gone.
|
||||
match from_overseer.map_err(Error::OverseerExited)? {
|
||||
FromOrchestra::Signal(OverseerSignal::ActiveLeaves(update)) =>
|
||||
handle_active_leaves_update(ctx, update, per_relay_parent, inherent_delays, slot_delays, inherents, metrics).await?,
|
||||
FromOrchestra::Signal(OverseerSignal::BlockFinalized(..)) => {},
|
||||
FromOrchestra::Signal(OverseerSignal::Conclude) => return Ok(()),
|
||||
FromOrchestra::Communication { msg } => {
|
||||
handle_communication(ctx, per_relay_parent, msg, metrics).await?;
|
||||
},
|
||||
}
|
||||
},
|
||||
hash = slot_delays.select_next_some() => {
|
||||
gum::debug!(target: LOG_TARGET, leaf_hash=?hash, "Slot start, preparing debug inherent");
|
||||
|
||||
let Some(state) = per_relay_parent.get_mut(&hash) else {
|
||||
continue
|
||||
};
|
||||
|
||||
// Create the inherent data just to record the backed candidates.
|
||||
let (inherent_tx, inherent_rx) = oneshot::channel();
|
||||
let task = async move {
|
||||
match inherent_rx.await {
|
||||
Ok(res) => (hash, Ok(res)),
|
||||
Err(e) => (hash, Err(e)),
|
||||
}
|
||||
}
|
||||
.boxed();
|
||||
|
||||
inherent_receivers.push(task);
|
||||
|
||||
send_inherent_data_bg(ctx, &state, vec![inherent_tx], metrics.clone()).await?;
|
||||
},
|
||||
(hash, inherent_data) = inherent_receivers.select_next_some() => {
|
||||
let Ok(inherent_data) = inherent_data else {
|
||||
continue
|
||||
};
|
||||
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
relay_parent = ?hash,
|
||||
"Debug Inherent Data became ready"
|
||||
);
|
||||
inherents.insert(hash, inherent_data);
|
||||
}
|
||||
hash = inherent_delays.select_next_some() => {
|
||||
if let Some(state) = per_relay_parent.get_mut(&hash) {
|
||||
state.is_inherent_ready = true;
|
||||
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
relay_parent = ?hash,
|
||||
"Inherent Data became ready"
|
||||
);
|
||||
|
||||
let return_senders = std::mem::take(&mut state.awaiting_inherent);
|
||||
if !return_senders.is_empty() {
|
||||
send_inherent_data_bg(ctx, &state, return_senders, metrics.clone()).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[overseer::contextbounds(Provisioner, prefix = self::overseer)]
|
||||
async fn handle_active_leaves_update<Context>(
|
||||
ctx: &mut Context,
|
||||
update: ActiveLeavesUpdate,
|
||||
per_relay_parent: &mut HashMap<Hash, PerRelayParent>,
|
||||
inherent_delays: &mut InherentDelays,
|
||||
slot_delays: &mut SlotDelays,
|
||||
inherents: &mut LruMap<Hash, ProvisionerInherentData>,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), Error> {
|
||||
gum::trace!(target: LOG_TARGET, "Handle ActiveLeavesUpdate");
|
||||
for deactivated in &update.deactivated {
|
||||
per_relay_parent.remove(deactivated);
|
||||
}
|
||||
|
||||
let Some(leaf) = update.activated else { return Ok(()) };
|
||||
|
||||
gum::trace!(target: LOG_TARGET, leaf_hash=?leaf.hash, "Adding delay");
|
||||
let delay_fut = Delay::new(PRE_PROPOSE_TIMEOUT).map(move |_| leaf.hash).boxed();
|
||||
per_relay_parent.insert(leaf.hash, PerRelayParent::new(leaf.clone()));
|
||||
inherent_delays.push(delay_fut);
|
||||
|
||||
let slot_delay = time_until_next_slot(Duration::from_millis(6000));
|
||||
gum::debug!(target: LOG_TARGET, leaf_hash=?leaf.hash, "Expecting next slot in {}ms", slot_delay.as_millis());
|
||||
|
||||
let slot_delay_task =
|
||||
Delay::new(slot_delay + PRE_PROPOSE_TIMEOUT).map(move |_| leaf.hash).boxed();
|
||||
slot_delays.push(slot_delay_task);
|
||||
|
||||
let Ok(Ok(candidate_events)) =
|
||||
pezkuwi_node_subsystem_util::request_candidate_events(leaf.hash, ctx.sender())
|
||||
.await
|
||||
.await
|
||||
else {
|
||||
gum::warn!(target: LOG_TARGET, leaf_hash=?leaf.hash, "Failed to fetch candidate events");
|
||||
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let in_block_count = candidate_events
|
||||
.into_iter()
|
||||
.filter(|event| matches!(event, CandidateEvent::CandidateBacked(_, _, _, _)))
|
||||
.count() as isize;
|
||||
|
||||
let (tx, rx) = oneshot::channel();
|
||||
ctx.send_message(ChainApiMessage::BlockHeader(leaf.hash, tx)).await;
|
||||
|
||||
let Ok(Some(header)) = rx.await.unwrap_or_else(|err| {
|
||||
gum::warn!(target: LOG_TARGET, hash = ?leaf.hash, ?err, "Missing header for block");
|
||||
Ok(None)
|
||||
}) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
gum::trace!(target: LOG_TARGET, hash = ?header.parent_hash, "Looking up debug inherent");
|
||||
|
||||
// Now, let's get the candidate count from our own inherent built earlier.
|
||||
// The inherent is stored under the parent hash.
|
||||
let Some(inherent) = inherents.get(&header.parent_hash) else { return Ok(()) };
|
||||
|
||||
let diff = inherent.backed_candidates.len() as isize - in_block_count;
|
||||
gum::debug!(target: LOG_TARGET,
|
||||
?diff,
|
||||
?in_block_count,
|
||||
local_count = ?inherent.backed_candidates.len(),
|
||||
leaf_hash=?leaf.hash, "Offchain vs on-chain backing update");
|
||||
|
||||
metrics.observe_backable_vs_in_block(diff);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[overseer::contextbounds(Provisioner, prefix = self::overseer)]
|
||||
async fn handle_communication<Context>(
|
||||
ctx: &mut Context,
|
||||
per_relay_parent: &mut HashMap<Hash, PerRelayParent>,
|
||||
message: ProvisionerMessage,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), Error> {
|
||||
match message {
|
||||
ProvisionerMessage::RequestInherentData(relay_parent, return_sender) => {
|
||||
gum::trace!(target: LOG_TARGET, ?relay_parent, "Inherent data got requested.");
|
||||
|
||||
if let Some(state) = per_relay_parent.get_mut(&relay_parent) {
|
||||
if state.is_inherent_ready {
|
||||
gum::trace!(target: LOG_TARGET, ?relay_parent, "Calling send_inherent_data.");
|
||||
send_inherent_data_bg(ctx, &state, vec![return_sender], metrics.clone())
|
||||
.await?;
|
||||
} else {
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
?relay_parent,
|
||||
"Queuing inherent data request (inherent data not yet ready)."
|
||||
);
|
||||
state.awaiting_inherent.push(return_sender);
|
||||
}
|
||||
}
|
||||
},
|
||||
ProvisionerMessage::ProvisionableData(relay_parent, data) => {
|
||||
if let Some(state) = per_relay_parent.get_mut(&relay_parent) {
|
||||
let _timer = metrics.time_provisionable_data();
|
||||
|
||||
gum::trace!(target: LOG_TARGET, ?relay_parent, "Received provisionable data: {:?}", &data);
|
||||
|
||||
note_provisionable_data(state, data);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[overseer::contextbounds(Provisioner, prefix = self::overseer)]
|
||||
async fn send_inherent_data_bg<Context>(
|
||||
ctx: &mut Context,
|
||||
per_relay_parent: &PerRelayParent,
|
||||
return_senders: Vec<oneshot::Sender<ProvisionerInherentData>>,
|
||||
metrics: Metrics,
|
||||
) -> Result<(), Error> {
|
||||
let leaf = per_relay_parent.leaf.clone();
|
||||
let signed_bitfields = per_relay_parent.signed_bitfields.clone();
|
||||
let mut sender = ctx.sender().clone();
|
||||
|
||||
let bg = async move {
|
||||
let _timer = metrics.time_request_inherent_data();
|
||||
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
relay_parent = ?leaf.hash,
|
||||
"Sending inherent data in background."
|
||||
);
|
||||
|
||||
let send_result =
|
||||
send_inherent_data(&leaf, &signed_bitfields, return_senders, &mut sender, &metrics) // Make sure call is not taking forever:
|
||||
.timeout(SEND_INHERENT_DATA_TIMEOUT)
|
||||
.map(|v| match v {
|
||||
Some(r) => r,
|
||||
None => Err(Error::SendInherentDataTimeout),
|
||||
});
|
||||
|
||||
match send_result.await {
|
||||
Err(err) => {
|
||||
if let Error::CanceledBackedCandidates(_) = err {
|
||||
gum::debug!(
|
||||
target: LOG_TARGET,
|
||||
err = ?err,
|
||||
"Failed to assemble or send inherent data - block got likely obsoleted already."
|
||||
);
|
||||
} else {
|
||||
gum::warn!(target: LOG_TARGET, err = ?err, "failed to assemble or send inherent data");
|
||||
}
|
||||
metrics.on_inherent_data_request(Err(()));
|
||||
},
|
||||
Ok(()) => {
|
||||
metrics.on_inherent_data_request(Ok(()));
|
||||
gum::debug!(
|
||||
target: LOG_TARGET,
|
||||
signed_bitfield_count = signed_bitfields.len(),
|
||||
leaf_hash = ?leaf.hash,
|
||||
"inherent data sent successfully"
|
||||
);
|
||||
metrics.observe_inherent_data_bitfields_count(signed_bitfields.len());
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
ctx.spawn("send-inherent-data", bg.boxed())
|
||||
.map_err(|_| Error::FailedToSpawnBackgroundTask)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn note_provisionable_data(
|
||||
per_relay_parent: &mut PerRelayParent,
|
||||
provisionable_data: ProvisionableData,
|
||||
) {
|
||||
match provisionable_data {
|
||||
ProvisionableData::Bitfield(_, signed_bitfield) =>
|
||||
per_relay_parent.signed_bitfields.push(signed_bitfield),
|
||||
// We choose not to punish these forms of misbehavior for the time being.
|
||||
// Risks from misbehavior are sufficiently mitigated at the protocol level
|
||||
// via reputation changes. Punitive actions here may become desirable
|
||||
// enough to dedicate time to in the future.
|
||||
ProvisionableData::MisbehaviorReport(_, _, _) => {},
|
||||
// We wait and do nothing here, preferring to initiate a dispute after the
|
||||
// parablock candidate is included for the following reasons:
|
||||
//
|
||||
// 1. A dispute for a candidate triggered at any point before the candidate
|
||||
// has been made available, including the backing stage, can't be
|
||||
// guaranteed to conclude. Non-concluding disputes are unacceptable.
|
||||
// 2. Candidates which haven't been made available don't pose a security
|
||||
// risk as they can not be included, approved, or finalized.
|
||||
//
|
||||
// Currently we rely on approval checkers to trigger disputes for bad
|
||||
// parablocks once they are included. But we can do slightly better by
|
||||
// allowing disagreeing backers to record their disagreement and initiate a
|
||||
// dispute once the parablock in question has been included. This potential
|
||||
// change is tracked by: https://github.com/paritytech/polkadot/issues/3232
|
||||
ProvisionableData::Dispute(_, _) => {},
|
||||
}
|
||||
}
|
||||
|
||||
type CoreAvailability = BitVec<u8, bitvec::order::Lsb0>;
|
||||
|
||||
/// The provisioner is the subsystem best suited to choosing which specific
|
||||
/// backed candidates and availability bitfields should be assembled into the
|
||||
/// block. To engage this functionality, a
|
||||
/// `ProvisionerMessage::RequestInherentData` is sent; the response is a set of
|
||||
/// non-conflicting candidates and the appropriate bitfields. Non-conflicting
|
||||
/// means that there are never two distinct teyrchain candidates included for
|
||||
/// the same teyrchain and that new teyrchain candidates cannot be included
|
||||
/// until the previous one either gets declared available or expired.
|
||||
///
|
||||
/// The main complication here is going to be around handling
|
||||
/// occupied-core-assumptions. We might have candidates that are only
|
||||
/// includable when some bitfields are included. And we might have candidates
|
||||
/// that are not includable when certain bitfields are included.
|
||||
///
|
||||
/// When we're choosing bitfields to include, the rule should be simple:
|
||||
/// maximize availability. So basically, include all bitfields. And then
|
||||
/// choose a coherent set of candidates along with that.
|
||||
async fn send_inherent_data(
|
||||
leaf: &ActivatedLeaf,
|
||||
bitfields: &[SignedAvailabilityBitfield],
|
||||
return_senders: Vec<oneshot::Sender<ProvisionerInherentData>>,
|
||||
from_job: &mut impl overseer::ProvisionerSenderTrait,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), Error> {
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
relay_parent = ?leaf.hash,
|
||||
"Requesting availability cores"
|
||||
);
|
||||
let availability_cores = request_availability_cores(leaf.hash, from_job)
|
||||
.await
|
||||
.await
|
||||
.map_err(|err| Error::CanceledAvailabilityCores(err))??;
|
||||
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
relay_parent = ?leaf.hash,
|
||||
"Selecting disputes"
|
||||
);
|
||||
|
||||
let disputes = disputes::prioritized_selection::select_disputes(from_job, metrics, leaf).await;
|
||||
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
relay_parent = ?leaf.hash,
|
||||
"Selected disputes"
|
||||
);
|
||||
|
||||
let bitfields = select_availability_bitfields(&availability_cores, bitfields, &leaf.hash);
|
||||
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
relay_parent = ?leaf.hash,
|
||||
"Selected bitfields"
|
||||
);
|
||||
|
||||
let candidates = select_candidates(&availability_cores, &bitfields, leaf, from_job).await?;
|
||||
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
relay_parent = ?leaf.hash,
|
||||
"Selected candidates"
|
||||
);
|
||||
|
||||
gum::debug!(
|
||||
target: LOG_TARGET,
|
||||
availability_cores_len = availability_cores.len(),
|
||||
disputes_count = disputes.len(),
|
||||
bitfields_count = bitfields.len(),
|
||||
candidates_count = candidates.len(),
|
||||
leaf_hash = ?leaf.hash,
|
||||
"inherent data prepared",
|
||||
);
|
||||
|
||||
let inherent_data =
|
||||
ProvisionerInherentData { bitfields, backed_candidates: candidates, disputes };
|
||||
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
relay_parent = ?leaf.hash,
|
||||
"Sending back inherent data to requesters."
|
||||
);
|
||||
|
||||
for return_sender in return_senders {
|
||||
return_sender
|
||||
.send(inherent_data.clone())
|
||||
.map_err(|_data| Error::InherentDataReturnChannel)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// In general, we want to pick all the bitfields. However, we have the following constraints:
|
||||
///
|
||||
/// - not more than one per validator
|
||||
/// - each 1 bit must correspond to an occupied core
|
||||
///
|
||||
/// If we have too many, an arbitrary selection policy is fine. For purposes of maximizing
|
||||
/// availability, we pick the one with the greatest number of 1 bits.
|
||||
///
|
||||
/// Note: This does not enforce any sorting precondition on the output; the ordering there will be
|
||||
/// unrelated to the sorting of the input.
|
||||
fn select_availability_bitfields(
|
||||
cores: &[CoreState],
|
||||
bitfields: &[SignedAvailabilityBitfield],
|
||||
leaf_hash: &Hash,
|
||||
) -> Vec<SignedAvailabilityBitfield> {
|
||||
let mut selected: BTreeMap<ValidatorIndex, SignedAvailabilityBitfield> = BTreeMap::new();
|
||||
|
||||
gum::debug!(
|
||||
target: LOG_TARGET,
|
||||
bitfields_count = bitfields.len(),
|
||||
?leaf_hash,
|
||||
"bitfields count before selection"
|
||||
);
|
||||
|
||||
'a: for bitfield in bitfields.iter().cloned() {
|
||||
if bitfield.payload().0.len() != cores.len() {
|
||||
gum::debug!(target: LOG_TARGET, ?leaf_hash, "dropping bitfield due to length mismatch");
|
||||
continue;
|
||||
}
|
||||
|
||||
let is_better = selected
|
||||
.get(&bitfield.validator_index())
|
||||
.map_or(true, |b| b.payload().0.count_ones() < bitfield.payload().0.count_ones());
|
||||
|
||||
if !is_better {
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
val_idx = bitfield.validator_index().0,
|
||||
?leaf_hash,
|
||||
"dropping bitfield due to duplication - the better one is kept"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (idx, _) in cores.iter().enumerate().filter(|v| !v.1.is_occupied()) {
|
||||
// Bit is set for an unoccupied core - invalid
|
||||
if *bitfield.payload().0.get(idx).as_deref().unwrap_or(&false) {
|
||||
gum::debug!(
|
||||
target: LOG_TARGET,
|
||||
val_idx = bitfield.validator_index().0,
|
||||
?leaf_hash,
|
||||
"dropping invalid bitfield - bit is set for an unoccupied core"
|
||||
);
|
||||
continue 'a;
|
||||
}
|
||||
}
|
||||
|
||||
let _ = selected.insert(bitfield.validator_index(), bitfield);
|
||||
}
|
||||
|
||||
gum::debug!(
|
||||
target: LOG_TARGET,
|
||||
?leaf_hash,
|
||||
"selected {} of all {} bitfields (each bitfield is from a unique validator)",
|
||||
selected.len(),
|
||||
bitfields.len()
|
||||
);
|
||||
|
||||
selected.into_values().collect()
|
||||
}
|
||||
|
||||
/// Requests backable candidates from Prospective Teyrchains subsystem
|
||||
/// based on core states.
|
||||
async fn request_backable_candidates(
|
||||
availability_cores: &[CoreState],
|
||||
bitfields: &[SignedAvailabilityBitfield],
|
||||
relay_parent: &ActivatedLeaf,
|
||||
sender: &mut impl overseer::ProvisionerSenderTrait,
|
||||
) -> Result<HashMap<ParaId, Vec<(CandidateHash, Hash)>>, Error> {
|
||||
let block_number_under_construction = relay_parent.number + 1;
|
||||
|
||||
// Record how many cores are scheduled for each paraid. Use a BTreeMap because
|
||||
// we'll need to iterate through them.
|
||||
let mut scheduled_cores_per_para: BTreeMap<ParaId, usize> = BTreeMap::new();
|
||||
// The on-chain ancestors of a para present in availability-cores.
|
||||
let mut ancestors: HashMap<ParaId, Ancestors> =
|
||||
HashMap::with_capacity(availability_cores.len());
|
||||
|
||||
for (core_idx, core) in availability_cores.iter().enumerate() {
|
||||
let core_idx = CoreIndex(core_idx as u32);
|
||||
match core {
|
||||
CoreState::Scheduled(scheduled_core) => {
|
||||
*scheduled_cores_per_para.entry(scheduled_core.para_id).or_insert(0) += 1;
|
||||
},
|
||||
CoreState::Occupied(occupied_core) => {
|
||||
let is_available = bitfields_indicate_availability(
|
||||
core_idx.0 as usize,
|
||||
bitfields,
|
||||
&occupied_core.availability,
|
||||
);
|
||||
|
||||
if is_available {
|
||||
ancestors
|
||||
.entry(occupied_core.para_id())
|
||||
.or_default()
|
||||
.insert(occupied_core.candidate_hash);
|
||||
|
||||
if let Some(ref scheduled_core) = occupied_core.next_up_on_available {
|
||||
// Request a new backable candidate for the newly scheduled para id.
|
||||
*scheduled_cores_per_para.entry(scheduled_core.para_id).or_insert(0) += 1;
|
||||
}
|
||||
} else if occupied_core.time_out_at <= block_number_under_construction {
|
||||
// Timed out before being available.
|
||||
|
||||
if let Some(ref scheduled_core) = occupied_core.next_up_on_time_out {
|
||||
// Candidate's availability timed out, practically same as scheduled.
|
||||
*scheduled_cores_per_para.entry(scheduled_core.para_id).or_insert(0) += 1;
|
||||
}
|
||||
} else {
|
||||
// Not timed out and not available.
|
||||
ancestors
|
||||
.entry(occupied_core.para_id())
|
||||
.or_default()
|
||||
.insert(occupied_core.candidate_hash);
|
||||
}
|
||||
},
|
||||
CoreState::Free => continue,
|
||||
};
|
||||
}
|
||||
|
||||
let mut selected_candidates: HashMap<ParaId, Vec<(CandidateHash, Hash)>> =
|
||||
HashMap::with_capacity(scheduled_cores_per_para.len());
|
||||
|
||||
for (para_id, core_count) in scheduled_cores_per_para {
|
||||
let para_ancestors = ancestors.remove(¶_id).unwrap_or_default();
|
||||
|
||||
let response = get_backable_candidates(
|
||||
relay_parent.hash,
|
||||
para_id,
|
||||
para_ancestors,
|
||||
core_count as u32,
|
||||
sender,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if response.is_empty() {
|
||||
gum::debug!(
|
||||
target: LOG_TARGET,
|
||||
leaf_hash = ?relay_parent.hash,
|
||||
?para_id,
|
||||
"No backable candidate returned by prospective teyrchains",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
selected_candidates.insert(para_id, response);
|
||||
}
|
||||
|
||||
Ok(selected_candidates)
|
||||
}
|
||||
|
||||
/// Determine which cores are free, and then to the degree possible, pick a candidate appropriate to
|
||||
/// each free core.
|
||||
async fn select_candidates(
|
||||
availability_cores: &[CoreState],
|
||||
bitfields: &[SignedAvailabilityBitfield],
|
||||
leaf: &ActivatedLeaf,
|
||||
sender: &mut impl overseer::ProvisionerSenderTrait,
|
||||
) -> Result<Vec<BackedCandidate>, Error> {
|
||||
let relay_parent = leaf.hash;
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
leaf_hash=?relay_parent,
|
||||
"before GetBackedCandidates"
|
||||
);
|
||||
|
||||
let selected_candidates =
|
||||
request_backable_candidates(availability_cores, bitfields, leaf, sender).await?;
|
||||
gum::debug!(target: LOG_TARGET, ?selected_candidates, "Got backable candidates");
|
||||
|
||||
// now get the backed candidates corresponding to these candidate receipts
|
||||
let (tx, rx) = oneshot::channel();
|
||||
sender.send_unbounded_message(CandidateBackingMessage::GetBackableCandidates(
|
||||
selected_candidates.clone(),
|
||||
tx,
|
||||
));
|
||||
let candidates = rx.await.map_err(|err| Error::CanceledBackedCandidates(err))?;
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
leaf_hash=?relay_parent,
|
||||
"Got {} backed candidates", candidates.len()
|
||||
);
|
||||
|
||||
// keep only one candidate with validation code.
|
||||
let mut with_validation_code = false;
|
||||
// merge the candidates into a common collection, preserving the order
|
||||
let mut merged_candidates = Vec::with_capacity(availability_cores.len());
|
||||
|
||||
for para_candidates in candidates.into_values() {
|
||||
for candidate in para_candidates {
|
||||
if candidate.candidate().commitments.new_validation_code.is_some() {
|
||||
if with_validation_code {
|
||||
break;
|
||||
} else {
|
||||
with_validation_code = true;
|
||||
}
|
||||
}
|
||||
|
||||
merged_candidates.push(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
gum::debug!(
|
||||
target: LOG_TARGET,
|
||||
n_candidates = merged_candidates.len(),
|
||||
n_cores = availability_cores.len(),
|
||||
?relay_parent,
|
||||
"Selected backed candidates",
|
||||
);
|
||||
|
||||
Ok(merged_candidates)
|
||||
}
|
||||
|
||||
/// Requests backable candidates from Prospective Teyrchains based on
|
||||
/// the given ancestors in the fragment chain. The ancestors may not be ordered.
|
||||
async fn get_backable_candidates(
|
||||
relay_parent: Hash,
|
||||
para_id: ParaId,
|
||||
ancestors: Ancestors,
|
||||
count: u32,
|
||||
sender: &mut impl overseer::ProvisionerSenderTrait,
|
||||
) -> Result<Vec<(CandidateHash, Hash)>, Error> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
sender
|
||||
.send_message(ProspectiveTeyrchainsMessage::GetBackableCandidates(
|
||||
relay_parent,
|
||||
para_id,
|
||||
count,
|
||||
ancestors,
|
||||
tx,
|
||||
))
|
||||
.await;
|
||||
|
||||
rx.await.map_err(Error::CanceledBackableCandidates)
|
||||
}
|
||||
|
||||
/// The availability bitfield for a given core is the transpose
|
||||
/// of a set of signed availability bitfields. It goes like this:
|
||||
///
|
||||
/// - construct a transverse slice along `core_idx`
|
||||
/// - bitwise-or it with the availability slice
|
||||
/// - count the 1 bits, compare to the total length; true on 2/3+
|
||||
fn bitfields_indicate_availability(
|
||||
core_idx: usize,
|
||||
bitfields: &[SignedAvailabilityBitfield],
|
||||
availability: &CoreAvailability,
|
||||
) -> bool {
|
||||
let mut availability = availability.clone();
|
||||
let availability_len = availability.len();
|
||||
|
||||
for bitfield in bitfields {
|
||||
let validator_idx = bitfield.validator_index().0 as usize;
|
||||
match availability.get_mut(validator_idx) {
|
||||
None => {
|
||||
// in principle, this function might return a `Result<bool, Error>` so that we can
|
||||
// more clearly express this error condition however, in practice, that would just
|
||||
// push off an error-handling routine which would look a whole lot like this one.
|
||||
// simpler to just handle the error internally here.
|
||||
gum::warn!(
|
||||
target: LOG_TARGET,
|
||||
validator_idx = %validator_idx,
|
||||
availability_len = %availability_len,
|
||||
"attempted to set a transverse bit at idx {} which is greater than bitfield size {}",
|
||||
validator_idx,
|
||||
availability_len,
|
||||
);
|
||||
|
||||
return false;
|
||||
},
|
||||
Some(mut bit_mut) => *bit_mut |= bitfield.payload().0[core_idx],
|
||||
}
|
||||
}
|
||||
|
||||
3 * availability.count_ones() >= 2 * availability.len()
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
// 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 crate::disputes::prioritized_selection::PartitionedDisputes;
|
||||
use pezkuwi_node_subsystem_util::metrics::{self, prometheus};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct MetricsInner {
|
||||
/// Tracks successful/unsuccessful inherent data requests
|
||||
inherent_data_requests: prometheus::CounterVec<prometheus::U64>,
|
||||
/// How much time the `RequestInherentData` processing takes
|
||||
request_inherent_data_duration: prometheus::Histogram,
|
||||
/// How much time `ProvisionableData` processing takes
|
||||
provisionable_data_duration: prometheus::Histogram,
|
||||
/// Bitfields array length in `ProvisionerInherentData` (the result for `RequestInherentData`)
|
||||
inherent_data_response_bitfields: prometheus::Histogram,
|
||||
|
||||
/// The following metrics track how many disputes/votes the runtime will have to process. These
|
||||
/// will count all recent statements meaning every dispute from last sessions: 10 min on
|
||||
/// Pezkuwichain, 60 min on Kusama and 4 hours on Pezkuwi. The metrics are updated only when
|
||||
/// the node authors a block, so values vary across nodes.
|
||||
inherent_data_dispute_statement_sets: prometheus::Counter<prometheus::U64>,
|
||||
inherent_data_dispute_statements: prometheus::CounterVec<prometheus::U64>,
|
||||
|
||||
/// The disputes received from `disputes-coordinator` by partition
|
||||
partitioned_disputes: prometheus::CounterVec<prometheus::U64>,
|
||||
|
||||
/// The disputes fetched from the runtime.
|
||||
fetched_onchain_disputes: prometheus::Counter<prometheus::U64>,
|
||||
|
||||
/// The difference between the number of backed candidates in a block and the number of
|
||||
/// backable candidates on the node side.
|
||||
backable_vs_in_block: prometheus::Histogram,
|
||||
}
|
||||
|
||||
/// Provisioner metrics.
|
||||
#[derive(Default, Clone)]
|
||||
pub struct Metrics(Option<MetricsInner>);
|
||||
|
||||
impl Metrics {
|
||||
/// Creates new dummy `Metrics` instance. Used for testing only.
|
||||
#[cfg(test)]
|
||||
pub fn new_dummy() -> Metrics {
|
||||
Metrics(None)
|
||||
}
|
||||
|
||||
pub(crate) fn on_inherent_data_request(&self, response: Result<(), ()>) {
|
||||
if let Some(metrics) = &self.0 {
|
||||
match response {
|
||||
Ok(()) => metrics.inherent_data_requests.with_label_values(&["succeeded"]).inc(),
|
||||
Err(()) => metrics.inherent_data_requests.with_label_values(&["failed"]).inc(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Provide a timer for `request_inherent_data` which observes on drop.
|
||||
pub(crate) fn time_request_inherent_data(
|
||||
&self,
|
||||
) -> Option<metrics::prometheus::prometheus::HistogramTimer> {
|
||||
self.0
|
||||
.as_ref()
|
||||
.map(|metrics| metrics.request_inherent_data_duration.start_timer())
|
||||
}
|
||||
|
||||
/// Provide a timer for `provisionable_data` which observes on drop.
|
||||
pub(crate) fn time_provisionable_data(
|
||||
&self,
|
||||
) -> Option<metrics::prometheus::prometheus::HistogramTimer> {
|
||||
self.0.as_ref().map(|metrics| metrics.provisionable_data_duration.start_timer())
|
||||
}
|
||||
|
||||
pub(crate) fn observe_inherent_data_bitfields_count(&self, bitfields_count: usize) {
|
||||
self.0.as_ref().map(|metrics| {
|
||||
metrics.inherent_data_response_bitfields.observe(bitfields_count as f64)
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn inc_valid_statements_by(&self, votes: usize) {
|
||||
if let Some(metrics) = &self.0 {
|
||||
metrics
|
||||
.inherent_data_dispute_statements
|
||||
.with_label_values(&["valid"])
|
||||
.inc_by(votes.try_into().unwrap_or(0));
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn inc_invalid_statements_by(&self, votes: usize) {
|
||||
if let Some(metrics) = &self.0 {
|
||||
metrics
|
||||
.inherent_data_dispute_statements
|
||||
.with_label_values(&["invalid"])
|
||||
.inc_by(votes.try_into().unwrap_or(0));
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn inc_dispute_statement_sets_by(&self, disputes: usize) {
|
||||
if let Some(metrics) = &self.0 {
|
||||
metrics
|
||||
.inherent_data_dispute_statement_sets
|
||||
.inc_by(disputes.try_into().unwrap_or(0));
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn on_partition_recent_disputes(&self, disputes: &PartitionedDisputes) {
|
||||
if let Some(metrics) = &self.0 {
|
||||
let PartitionedDisputes {
|
||||
inactive_unknown_onchain,
|
||||
inactive_unconcluded_onchain: inactive_unconcluded_known_onchain,
|
||||
active_unknown_onchain,
|
||||
active_unconcluded_onchain,
|
||||
active_concluded_onchain,
|
||||
inactive_concluded_onchain: inactive_concluded_known_onchain,
|
||||
} = disputes;
|
||||
|
||||
metrics
|
||||
.partitioned_disputes
|
||||
.with_label_values(&["inactive_unknown_onchain"])
|
||||
.inc_by(inactive_unknown_onchain.len().try_into().unwrap_or(0));
|
||||
metrics
|
||||
.partitioned_disputes
|
||||
.with_label_values(&["inactive_unconcluded_known_onchain"])
|
||||
.inc_by(inactive_unconcluded_known_onchain.len().try_into().unwrap_or(0));
|
||||
metrics
|
||||
.partitioned_disputes
|
||||
.with_label_values(&["active_unknown_onchain"])
|
||||
.inc_by(active_unknown_onchain.len().try_into().unwrap_or(0));
|
||||
metrics
|
||||
.partitioned_disputes
|
||||
.with_label_values(&["active_unconcluded_onchain"])
|
||||
.inc_by(active_unconcluded_onchain.len().try_into().unwrap_or(0));
|
||||
metrics
|
||||
.partitioned_disputes
|
||||
.with_label_values(&["active_concluded_onchain"])
|
||||
.inc_by(active_concluded_onchain.len().try_into().unwrap_or(0));
|
||||
metrics
|
||||
.partitioned_disputes
|
||||
.with_label_values(&["inactive_concluded_known_onchain"])
|
||||
.inc_by(inactive_concluded_known_onchain.len().try_into().unwrap_or(0));
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn on_fetched_onchain_disputes(&self, onchain_count: u64) {
|
||||
if let Some(metrics) = &self.0 {
|
||||
metrics.fetched_onchain_disputes.inc_by(onchain_count);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn observe_backable_vs_in_block(&self, diff: isize) {
|
||||
self.0.as_ref().map(|metrics| metrics.backable_vs_in_block.observe(diff as f64));
|
||||
}
|
||||
}
|
||||
|
||||
impl metrics::Metrics for Metrics {
|
||||
fn try_register(registry: &prometheus::Registry) -> Result<Self, prometheus::PrometheusError> {
|
||||
let metrics = MetricsInner {
|
||||
inherent_data_requests: prometheus::register(
|
||||
prometheus::CounterVec::new(
|
||||
prometheus::Opts::new(
|
||||
"pezkuwi_teyrchain_inherent_data_requests_total",
|
||||
"Number of InherentData requests served by provisioner.",
|
||||
),
|
||||
&["success"],
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
request_inherent_data_duration: prometheus::register(
|
||||
prometheus::Histogram::with_opts(prometheus::HistogramOpts::new(
|
||||
"pezkuwi_teyrchain_provisioner_request_inherent_data_time",
|
||||
"Time spent within `provisioner::request_inherent_data`",
|
||||
))?,
|
||||
registry,
|
||||
)?,
|
||||
provisionable_data_duration: prometheus::register(
|
||||
prometheus::Histogram::with_opts(prometheus::HistogramOpts::new(
|
||||
"pezkuwi_teyrchain_provisioner_provisionable_data_time",
|
||||
"Time spent within `provisioner::provisionable_data`",
|
||||
))?,
|
||||
registry,
|
||||
)?,
|
||||
inherent_data_dispute_statements: prometheus::register(
|
||||
prometheus::CounterVec::new(
|
||||
prometheus::Opts::new(
|
||||
"pezkuwi_teyrchain_inherent_data_dispute_statements",
|
||||
"Number of dispute statements passed to `create_inherent()`.",
|
||||
),
|
||||
&["validity"],
|
||||
)?,
|
||||
®istry,
|
||||
)?,
|
||||
inherent_data_dispute_statement_sets: prometheus::register(
|
||||
prometheus::Counter::new(
|
||||
"pezkuwi_teyrchain_inherent_data_dispute_statement_sets",
|
||||
"Number of dispute statements sets passed to `create_inherent()`.",
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
inherent_data_response_bitfields: prometheus::register(
|
||||
prometheus::Histogram::with_opts(
|
||||
prometheus::HistogramOpts::new(
|
||||
"pezkuwi_teyrchain_provisioner_inherent_data_response_bitfields_sent",
|
||||
"Number of inherent bitfields sent in response to `ProvisionerMessage::RequestInherentData`.",
|
||||
).buckets(vec![0.0, 25.0, 50.0, 100.0, 150.0, 200.0, 250.0, 300.0, 400.0, 500.0, 600.0]),
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
backable_vs_in_block: prometheus::register(
|
||||
prometheus::Histogram::with_opts(
|
||||
prometheus::HistogramOpts::new(
|
||||
"pezkuwi_teyrchain_provisioner_backable_vs_in_block",
|
||||
"Difference between number of backable blocks and number of backed candidates in block",
|
||||
).buckets(vec![-100.0, -50.0, -40.0, -30.0, -20.0, -15.0, -10.0, -5.0, 0.0, 5.0, 10.0, 15.0, 20.0, 30.0, 40.0, 50.0, 100.0]),
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
partitioned_disputes: prometheus::register(
|
||||
prometheus::CounterVec::new(
|
||||
prometheus::Opts::new(
|
||||
"pezkuwi_teyrchain_provisioner_partitioned_disputes",
|
||||
"Number of disputes partitioned by type.",
|
||||
),
|
||||
&["partition"],
|
||||
)?,
|
||||
®istry,
|
||||
)?,
|
||||
fetched_onchain_disputes: prometheus::register(
|
||||
prometheus::Counter::new("pezkuwi_teyrchain_fetched_onchain_disputes", "Number of disputes fetched from the runtime"
|
||||
)?,
|
||||
®istry,
|
||||
)?,
|
||||
};
|
||||
Ok(Metrics(Some(metrics)))
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user