diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 91ac364a8a06..62e983e72c23 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -32,7 +32,7 @@ variables: CI_IMAGE: "paritytech/ci-linux:production" DOCKER_OS: "debian:stretch" ARCH: "x86_64" - ZOMBIENET_IMAGE: "docker.io/paritytech/zombienet:v1.2.56" + ZOMBIENET_IMAGE: "docker.io/paritytech/zombienet:v1.2.67" PIPELINE_SCRIPTS_TAG: "v0.4" default: @@ -231,6 +231,9 @@ build-linux-stable: - echo "Polkadot version = ${VERSION} (EXTRATAG = ${EXTRATAG})" - echo -n ${VERSION} > ./artifacts/VERSION - echo -n ${EXTRATAG} > ./artifacts/EXTRATAG + - echo -n ${CI_JOB_ID} > ./artifacts/BUILD_LINUX_JOB_ID + - RELEASE_VERSION=$(./artifacts/polkadot -V | awk '{print $2}'| awk -F "-" '{print $1}') + - echo -n "v${RELEASE_VERSION}" > ./artifacts/BUILD_RELEASE_VERSION - cp -r scripts/* ./artifacts test-linux-stable: @@ -868,6 +871,38 @@ zombienet-tests-misc-paritydb: tags: - zombienet-polkadot-integration-test +zombienet-tests-misc-upgrade-node: + stage: stage3 + image: "${ZOMBIENET_IMAGE}" + <<: *kubernetes-env + <<: *zombienet-refs + needs: + - job: publish-polkadot-debug-image + - job: publish-test-collators-image + - job: build-linux-stable + artifacts: true + variables: + GH_DIR: "https://github.com/paritytech/polkadot/tree/${CI_COMMIT_SHORT_SHA}/zombienet_tests/misc" + before_script: + - echo "Zombie-net Tests Config" + - echo "${ZOMBIENET_IMAGE_NAME}" + - echo "${PARACHAINS_IMAGE_NAME} ${PARACHAINS_IMAGE_TAG}" + - echo "${GH_DIR}" + - export DEBUG=zombie,zombie::network-node + - BUILD_RELEASE_VERSION="$(cat ./artifacts/BUILD_RELEASE_VERSION)" + - export ZOMBIENET_INTEGRATION_TEST_IMAGE="docker.io/parity/polkadot:${BUILD_RELEASE_VERSION}" + - export COL_IMAGE=${COLLATOR_IMAGE_NAME}:${COLLATOR_IMAGE_TAG} + - BUILD_LINUX_JOB_ID="$(cat ./artifacts/BUILD_LINUX_JOB_ID)" + - export POLKADOT_PR_BIN_URL="https://gitlab.parity.io/parity/mirrors/polkadot/-/jobs/${BUILD_LINUX_JOB_ID}/artifacts/raw/artifacts/polkadot" + script: + - /home/nonroot/zombie-net/scripts/ci/run-test-env-manager.sh + --github-remote-dir="${GH_DIR}" + --test="0002-upgrade-node.feature" + allow_failure: false + retry: 2 + tags: + - zombienet-polkadot-integration-test + zombienet-tests-malus-dispute-valid: stage: stage3 image: "${ZOMBIENET_IMAGE}" @@ -1054,8 +1089,6 @@ short-benchmark-westend: PR_NUM: "${PR_NUM}" trigger: project: "parity/infrastructure/ci_cd/pipeline-stopper" - # remove branch, when pipeline-stopper for polakdot is updated to the same branch - branch: "as-improve" cancel-pipeline-test-linux-stable: extends: .cancel-pipeline-template diff --git a/Cargo.lock b/Cargo.lock index 5847bf63f600..b9f15ad0ed18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6022,6 +6022,7 @@ dependencies = [ "polkadot-node-subsystem-test-helpers", "polkadot-node-subsystem-util", "polkadot-primitives", + "polkadot-primitives-test-helpers", "rand 0.8.5", "rand_chacha 0.3.1", "rand_core 0.5.1", @@ -6924,6 +6925,7 @@ dependencies = [ "polkadot-primitives", "rand 0.8.5", "sp-application-crypto", + "sp-core", "sp-keyring", "sp-runtime", ] diff --git a/Cargo.toml b/Cargo.toml index 4983b346225b..be2e04291370 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -199,7 +199,6 @@ try-runtime = [ "polkadot-cli/try-runtime" ] fast-runtime = [ "polkadot-cli/fast-runtime" ] runtime-metrics = [ "polkadot-cli/runtime-metrics" ] pyroscope = ["polkadot-cli/pyroscope"] -staging-client = ["polkadot-cli/staging-client"] # Configuration for building a .deb package - for use with `cargo-deb` [package.metadata.deb] diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 4cc97cf9af8d..1e770cd8715b 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -74,4 +74,3 @@ rococo-native = ["service/rococo-native"] malus = ["full-node", "service/malus"] runtime-metrics = ["service/runtime-metrics", "polkadot-node-metrics/runtime-metrics"] -staging-client = ["service/staging-client"] diff --git a/node/core/dispute-coordinator/src/db/v1.rs b/node/core/dispute-coordinator/src/db/v1.rs index 4d33949db644..2c643d341de2 100644 --- a/node/core/dispute-coordinator/src/db/v1.rs +++ b/node/core/dispute-coordinator/src/db/v1.rs @@ -16,6 +16,7 @@ //! `V1` database for the dispute coordinator. +use polkadot_node_primitives::DisputeStatus; use polkadot_node_subsystem::{SubsystemError, SubsystemResult}; use polkadot_node_subsystem_util::database::{DBTransaction, Database}; use polkadot_primitives::v2::{ @@ -31,7 +32,6 @@ use crate::{ backend::{Backend, BackendWriteOp, OverlayedBackend}, error::{FatalError, FatalResult}, metrics::Metrics, - status::DisputeStatus, DISPUTE_WINDOW, LOG_TARGET, }; diff --git a/node/core/dispute-coordinator/src/initialized.rs b/node/core/dispute-coordinator/src/initialized.rs index 075cfbb33c27..5f29245f33f8 100644 --- a/node/core/dispute-coordinator/src/initialized.rs +++ b/node/core/dispute-coordinator/src/initialized.rs @@ -26,8 +26,8 @@ use futures::{ use sc_keystore::LocalKeystore; use polkadot_node_primitives::{ - CandidateVotes, DisputeMessage, DisputeMessageCheckError, SignedDisputeStatement, - DISPUTE_WINDOW, + CandidateVotes, DisputeMessage, DisputeMessageCheckError, DisputeStatus, + SignedDisputeStatement, Timestamp, DISPUTE_WINDOW, }; use polkadot_node_subsystem::{ messages::{ @@ -49,7 +49,7 @@ use crate::{ error::{log_error, Error, FatalError, FatalResult, JfyiError, JfyiResult, Result}, import::{CandidateEnvironment, CandidateVoteState}, metrics::Metrics, - status::{get_active_with_status, Clock, DisputeStatus, Timestamp}, + status::{get_active_with_status, Clock}, DisputeCoordinatorSubsystem, LOG_TARGET, }; @@ -599,7 +599,9 @@ impl Initialized { }; gum::trace!(target: LOG_TARGET, "Loaded recent disputes from db"); - let _ = tx.send(recent_disputes.keys().cloned().collect()); + let _ = tx.send( + recent_disputes.into_iter().map(|(k, v)| (k.0, k.1, v)).collect::>(), + ); }, DisputeCoordinatorMessage::ActiveDisputes(tx) => { // Return error if session information is missing. diff --git a/node/core/dispute-coordinator/src/status.rs b/node/core/dispute-coordinator/src/status.rs index d2ad551bd9ad..6332c3653274 100644 --- a/node/core/dispute-coordinator/src/status.rs +++ b/node/core/dispute-coordinator/src/status.rs @@ -14,125 +14,18 @@ // You should have received a copy of the GNU General Public License // along with Polkadot. If not, see . -use std::time::{SystemTime, UNIX_EPOCH}; - -use parity_scale_codec::{Decode, Encode}; +use polkadot_node_primitives::{dispute_is_inactive, DisputeStatus, Timestamp}; use polkadot_primitives::v2::{CandidateHash, SessionIndex}; +use std::time::{SystemTime, UNIX_EPOCH}; use crate::LOG_TARGET; -/// The choice here is fairly arbitrary. But any dispute that concluded more than a few minutes ago -/// is not worth considering anymore. Changing this value has little to no bearing on consensus, -/// and really only affects the work that the node might do on startup during periods of many -/// disputes. -pub const ACTIVE_DURATION_SECS: Timestamp = 180; - -/// Timestamp based on the 1 Jan 1970 UNIX base, which is persistent across node restarts and OS reboots. -pub type Timestamp = u64; - -/// The status of dispute. This is a state machine which can be altered by the -/// helper methods. -#[derive(Debug, Clone, Copy, Encode, Decode, PartialEq)] -pub enum DisputeStatus { - /// The dispute is active and unconcluded. - #[codec(index = 0)] - Active, - /// The dispute has been concluded in favor of the candidate - /// since the given timestamp. - #[codec(index = 1)] - ConcludedFor(Timestamp), - /// The dispute has been concluded against the candidate - /// since the given timestamp. - /// - /// This takes precedence over `ConcludedFor` in the case that - /// both are true, which is impossible unless a large amount of - /// validators are participating on both sides. - #[codec(index = 2)] - ConcludedAgainst(Timestamp), - /// Dispute has been confirmed (more than `byzantine_threshold` have already participated/ or - /// we have seen the candidate included already/participated successfully ourselves). - #[codec(index = 3)] - Confirmed, -} - -impl DisputeStatus { - /// Initialize the status to the active state. - pub fn active() -> DisputeStatus { - DisputeStatus::Active - } - - /// Move status to confirmed status, if not yet concluded/confirmed already. - pub fn confirm(self) -> DisputeStatus { - match self { - DisputeStatus::Active => DisputeStatus::Confirmed, - DisputeStatus::Confirmed => DisputeStatus::Confirmed, - DisputeStatus::ConcludedFor(_) | DisputeStatus::ConcludedAgainst(_) => self, - } - } - - /// Check whether the dispute is not a spam dispute. - pub fn is_confirmed_concluded(&self) -> bool { - match self { - &DisputeStatus::Confirmed | - &DisputeStatus::ConcludedFor(_) | - DisputeStatus::ConcludedAgainst(_) => true, - &DisputeStatus::Active => false, - } - } - - /// Transition the status to a new status after observing the dispute has concluded for the candidate. - /// This may be a no-op if the status was already concluded. - pub fn concluded_for(self, now: Timestamp) -> DisputeStatus { - match self { - DisputeStatus::Active | DisputeStatus::Confirmed => DisputeStatus::ConcludedFor(now), - DisputeStatus::ConcludedFor(at) => DisputeStatus::ConcludedFor(std::cmp::min(at, now)), - against => against, - } - } - - /// Transition the status to a new status after observing the dispute has concluded against the candidate. - /// This may be a no-op if the status was already concluded. - pub fn concluded_against(self, now: Timestamp) -> DisputeStatus { - match self { - DisputeStatus::Active | DisputeStatus::Confirmed => - DisputeStatus::ConcludedAgainst(now), - DisputeStatus::ConcludedFor(at) => - DisputeStatus::ConcludedAgainst(std::cmp::min(at, now)), - DisputeStatus::ConcludedAgainst(at) => - DisputeStatus::ConcludedAgainst(std::cmp::min(at, now)), - } - } - - /// Whether the disputed candidate is possibly invalid. - pub fn is_possibly_invalid(&self) -> bool { - match self { - DisputeStatus::Active | - DisputeStatus::Confirmed | - DisputeStatus::ConcludedAgainst(_) => true, - DisputeStatus::ConcludedFor(_) => false, - } - } - - /// Yields the timestamp this dispute concluded at, if any. - pub fn concluded_at(&self) -> Option { - match self { - DisputeStatus::Active | DisputeStatus::Confirmed => None, - DisputeStatus::ConcludedFor(at) | DisputeStatus::ConcludedAgainst(at) => Some(*at), - } - } -} - /// Get active disputes as iterator, preserving its `DisputeStatus`. pub fn get_active_with_status( recent_disputes: impl Iterator, now: Timestamp, ) -> impl Iterator { - recent_disputes.filter_map(move |(disputed, status)| { - status - .concluded_at() - .filter(|at| *at + ACTIVE_DURATION_SECS < now) - .map_or(Some((disputed, status)), |_| None) - }) + recent_disputes.filter(move |(_, status)| !dispute_is_inactive(status, &now)) } pub trait Clock: Send + Sync { diff --git a/node/core/dispute-coordinator/src/tests.rs b/node/core/dispute-coordinator/src/tests.rs index 39fdc3a037e5..ff85319599ce 100644 --- a/node/core/dispute-coordinator/src/tests.rs +++ b/node/core/dispute-coordinator/src/tests.rs @@ -49,6 +49,7 @@ use sp_keyring::Sr25519Keyring; use sp_keystore::{SyncCryptoStore, SyncCryptoStorePtr}; use ::test_helpers::{dummy_candidate_receipt_bad_sig, dummy_digest, dummy_hash}; +use polkadot_node_primitives::{Timestamp, ACTIVE_DURATION_SECS}; use polkadot_node_subsystem::{ jaeger, messages::{AllMessages, BlockDescription, RuntimeApiMessage, RuntimeApiRequest}, @@ -66,7 +67,7 @@ use crate::{ backend::Backend, metrics::Metrics, participation::{participation_full_happy_path, participation_missing_availability}, - status::{Clock, Timestamp, ACTIVE_DURATION_SECS}, + status::Clock, Config, DisputeCoordinatorSubsystem, }; diff --git a/node/core/provisioner/Cargo.toml b/node/core/provisioner/Cargo.toml index 4f18c10aba35..77d0794754a6 100644 --- a/node/core/provisioner/Cargo.toml +++ b/node/core/provisioner/Cargo.toml @@ -13,8 +13,8 @@ polkadot-primitives = { path = "../../../primitives" } polkadot-node-primitives = { path = "../../primitives" } polkadot-node-subsystem = { path = "../../subsystem" } polkadot-node-subsystem-util = { path = "../../subsystem-util" } -futures-timer = "3.0.2" rand = "0.8.5" +futures-timer = "3.0.2" fatality = "0.0.6" [dev-dependencies] @@ -22,6 +22,3 @@ sp-application-crypto = { git = "https://github.com/paritytech/substrate", branc sp-keystore = { git = "https://github.com/paritytech/substrate", branch = "master" } polkadot-node-subsystem-test-helpers = { path = "../../subsystem-test-helpers" } test-helpers = { package = "polkadot-primitives-test-helpers", path = "../../../primitives/test-helpers" } - -[features] -staging-client = [] diff --git a/node/core/provisioner/src/disputes/mod.rs b/node/core/provisioner/src/disputes/mod.rs new file mode 100644 index 000000000000..404e800702b1 --- /dev/null +++ b/node/core/provisioner/src/disputes/mod.rs @@ -0,0 +1,53 @@ +// Copyright 2017-2022 Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Polkadot is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see . + +//! The disputes module is responsible for selecting dispute votes to be sent with the inherent data. It contains two +//! different implementations, extracted in two separate modules - `random_selection` and `prioritized_selection`. Which +//! implementation will be executed depends on the version of the runtime. Runtime v2 supports `random_selection`. Runtime +//! v3 and above - `prioritized_selection`. The entrypoint to these implementations is the `select_disputes` function. +//! prioritized_selection` is considered superior and will be the default one in the future. Refer to the documentation of +//! the modules for more details about each implementation. + +use crate::LOG_TARGET; +use futures::channel::oneshot; +use polkadot_node_primitives::CandidateVotes; +use polkadot_node_subsystem::{messages::DisputeCoordinatorMessage, overseer}; +use polkadot_primitives::v2::{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; + +pub(crate) mod random_selection; diff --git a/node/core/provisioner/src/disputes/prioritized_selection/mod.rs b/node/core/provisioner/src/disputes/prioritized_selection/mod.rs new file mode 100644 index 000000000000..6582f0a612ff --- /dev/null +++ b/node/core/provisioner/src/disputes/prioritized_selection/mod.rs @@ -0,0 +1,470 @@ +// Copyright 2017-2022 Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Polkadot is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see . + +//! 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 polkadot_node_primitives::{dispute_is_inactive, CandidateVotes, DisputeStatus, Timestamp}; +use polkadot_node_subsystem::{ + errors::RuntimeApiError, + messages::{DisputeCoordinatorMessage, RuntimeApiMessage, RuntimeApiRequest}, + overseer, ActivatedLeaf, +}; +use polkadot_primitives::v2::{ + supermajority_threshold, CandidateHash, DisputeState, DisputeStatement, DisputeStatementSet, + Hash, MultiDisputeStatementSet, SessionIndex, 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 runtime per iteration in `fn vote_selection`. +/// The purpose is to fetch the votes in batches until `MAX_DISPUTE_VOTES_FORWARDED_TO_RUNTIME` is +/// reached. This value should definitely be less than `MAX_DISPUTE_VOTES_FORWARDED_TO_RUNTIME`. +/// +/// The ratio `MAX_DISPUTE_VOTES_FORWARDED_TO_RUNTIME` / `VOTES_SELECTION_BATCH_SIZE` gives an +/// approximation about how many runtime requests will be issued to fetch votes from the runtime in +/// a single `select_disputes` call. Ideally we don't want to make more than 2-3 calls. In practice +/// it's hard to predict this number because we can't guess how many new votes (for the runtime) a +/// batch will contain. +/// +/// The value below is reached by: `MAX_DISPUTE_VOTES_FORWARDED_TO_RUNTIME` / 2 + 10% +/// The 10% makes approximately means '10% new votes'. Tweak this if provisioner makes excessive +/// number of runtime calls. +#[cfg(not(test))] +const VOTES_SELECTION_BATCH_SIZE: usize = 1_100; +#[cfg(test)] +const VOTES_SELECTION_BATCH_SIZE: usize = 11; // Just a small value for tests. Doesn't follow the rules above + +/// 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: &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.clone()).await { + Ok(r) => 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 ParachainHost 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() + }, + }; + + let recent_disputes = request_disputes(sender).await; + gum::trace!( + target: LOG_TARGET, + ?leaf, + "Got {} recent disputes and {} onchain disputes.", + recent_disputes.len(), + onchain.len(), + ); + + 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!", + partitioned.inactive_unknown_onchain.len() + ); + } + let result = vote_selection(sender, partitioned, &onchain).await; + + 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: &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::>(); + let mut total_votes_len = 0; + let mut result = BTreeMap::new(); + let mut request_votes_counter = 0; + while !disputes.is_empty() { + 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; + 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), + &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::>(); + + // Check if votes are within the limit + for (session_index, candidate_hash, selected_votes) in votes { + let votes_len = selected_votes.valid.len() + selected_votes.invalid.len(); + if votes_len + total_votes_len > MAX_DISPUTE_VOTES_FORWARDED_TO_RUNTIME { + // we are done - no more votes can be added + return result + } + result.insert((session_index, candidate_hash), selected_votes); + total_votes_len += votes_len + } + } + + gum::trace!( + target: LOG_TARGET, + ?request_votes_counter, + "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 { + 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: Vec<(SessionIndex, CandidateHash, DisputeStatus)>, + onchain: &HashMap<(SessionIndex, CandidateHash), DisputeState>, +) -> PartitionedDisputes { + let mut partitioned = PartitionedDisputes::new(); + + // Drop any duplicates + let unique_recent = recent + .into_iter() + .map(|(session_index, candidate_hash, dispute_state)| { + ((session_index, candidate_hash), dispute_state) + }) + .collect::>(); + + // Split recent disputes in ACTIVE and INACTIVE + let time_now = &secs_since_epoch(); + let (active, inactive): ( + Vec<(SessionIndex, CandidateHash, DisputeStatus)>, + Vec<(SessionIndex, CandidateHash, DisputeStatus)>, + ) = unique_recent + .into_iter() + .map(|((session_index, candidate_hash), dispute_state)| { + (session_index, candidate_hash, dispute_state) + }) + .partition(|(_, _, status)| !dispute_is_inactive(status, time_now)); + + // Split ACTIVE in three groups... + for (session_index, candidate_hash, _) in active { + 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)), + }; + } + + // ... and INACTIVE in three more + for (session_index, candidate_hash, _) in inactive { + match onchain.get(&(session_index, candidate_hash)) { + 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)), + } + } + + 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 = match dispute_statement { + DisputeStatement::Valid(_) => true, + DisputeStatement::Invalid(_) => false, + }; + 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, +) -> Vec<(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"); + Vec::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: &mut Sender, + relay_parent: Hash, +) -> Result, 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()) +} diff --git a/node/core/provisioner/src/disputes/prioritized_selection/tests.rs b/node/core/provisioner/src/disputes/prioritized_selection/tests.rs new file mode 100644 index 000000000000..f76107dc65d4 --- /dev/null +++ b/node/core/provisioner/src/disputes/prioritized_selection/tests.rs @@ -0,0 +1,722 @@ +// Copyright 2017-2022 Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Polkadot is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see . + +use super::super::{ + super::{tests::common::test_harness, *}, + prioritized_selection::*, +}; +use bitvec::prelude::*; +use futures::channel::mpsc; +use polkadot_node_primitives::{CandidateVotes, DisputeStatus, ACTIVE_DURATION_SECS}; +use polkadot_node_subsystem::messages::{ + AllMessages, DisputeCoordinatorMessage, RuntimeApiMessage, RuntimeApiRequest, +}; +use polkadot_node_subsystem_test_helpers::TestSubsystemSender; +use polkadot_primitives::v2::{ + CandidateHash, DisputeState, InvalidDisputeStatementKind, SessionIndex, + ValidDisputeStatementKind, ValidatorSignature, +}; +use std::sync::Arc; +use test_helpers; + +// +// 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 = Vec::<(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.push(inactive_unknown_onchain.clone()); + + let inactive_unconcluded_onchain = ( + 1, + CandidateHash(Hash::random()), + DisputeStatus::ConcludedFor(time_now - ACTIVE_DURATION_SECS * 2), + ); + input.push(inactive_unconcluded_onchain.clone()); + onchain.insert( + (inactive_unconcluded_onchain.0, inactive_unconcluded_onchain.1.clone()), + 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.push(active_unknown_onchain.clone()); + + let active_unconcluded_onchain = (3, CandidateHash(Hash::random()), DisputeStatus::Active); + input.push(active_unconcluded_onchain.clone()); + onchain.insert( + (active_unconcluded_onchain.0, active_unconcluded_onchain.1.clone()), + 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.push(active_concluded_onchain.clone()); + onchain.insert( + (active_concluded_onchain.0, active_concluded_onchain.1.clone()), + 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.push(inactive_concluded_onchain.clone()); + onchain.insert( + (inactive_concluded_onchain.0, inactive_concluded_onchain.1.clone()), + 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, inactive_unknown_onchain.1) + ); + + assert_eq!(result.inactive_unconcluded_onchain.len(), 1); + assert_eq!( + result.inactive_unconcluded_onchain.get(0).unwrap(), + &(inactive_unconcluded_onchain.0, inactive_unconcluded_onchain.1) + ); + + assert_eq!(result.active_unknown_onchain.len(), 1); + assert_eq!( + result.active_unknown_onchain.get(0).unwrap(), + &(active_unknown_onchain.0, active_unknown_onchain.1) + ); + + assert_eq!(result.active_unconcluded_onchain.len(), 1); + assert_eq!( + result.active_unconcluded_onchain.get(0).unwrap(), + &(active_unconcluded_onchain.0, active_unconcluded_onchain.1) + ); + + assert_eq!(result.active_concluded_onchain.len(), 1); + assert_eq!( + result.active_concluded_onchain.get(0).unwrap(), + &(active_concluded_onchain.0, active_concluded_onchain.1) + ); + + assert_eq!(result.inactive_concluded_onchain.len(), 1); + assert_eq!( + result.inactive_concluded_onchain.get(0).unwrap(), + &(inactive_concluded_onchain.0, inactive_concluded_onchain.1) + ); +} + +// 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 = Vec::<(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.push(dispute_a.clone()); + input.push(dispute_b.clone()); + onchain.insert( + (dispute_a.0, dispute_a.1.clone()), + 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, dispute_b.1.clone()), + 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 = Vec::<(SessionIndex, CandidateHash, DisputeStatus)>::new(); + let mut onchain = HashMap::<(u32, CandidateHash), DisputeState>::new(); + + let some_dispute = (3, CandidateHash(Hash::random()), DisputeStatus::Active); + input.push(some_dispute.clone()); + input.push(some_dispute.clone()); + onchain.insert( + (some_dispute.0, some_dispute.1.clone()), + 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, some_dispute.1) + ); +} + +// +// end-to-end tests for select_disputes() +// + +async fn mock_overseer( + mut receiver: mpsc::UnboundedReceiver, + 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::>())); + }, + 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 { + ActivatedLeaf { + hash: Hash::repeat_byte(0xAA), + number: 0xAA, + status: LeafStatus::Fresh, + span: Arc::new(jaeger::Span::Disabled), + } +} + +struct TestDisputes { + pub local_disputes: Vec<(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: Vec::<(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.push(dispute.clone()); + self.votes_db.insert( + (dispute.0, dispute.1), + CandidateVotes { + candidate_receipt: dummy_receipt, + valid: TestDisputes::generate_local_votes( + ValidDisputeStatementKind::Explicit, + 0, + local_votes_count, + ), + 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.clone()), + 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, + ) -> (u32, 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 = test_helpers::dummy_candidate_receipt(lf.hash.clone()); + for _ in 0..dispute_count { + let d = (session_idx, CandidateHash(Hash::random()), DisputeStatus::Active); + self.add_offchain_dispute(d.clone(), 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, + ) -> (u32, 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 = test_helpers::dummy_candidate_receipt(lf.hash.clone()); + for _ in 0..dispute_count { + let d = (session_idx, CandidateHash(Hash::random()), DisputeStatus::Active); + self.add_offchain_dispute(d.clone(), 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, + ) -> (u32, usize) { + let local_votes_count = self.validators_count * 90 / 100; + let session_idx = 2; + let lf = leaf(); + let dummy_receipt = test_helpers::dummy_candidate_receipt(lf.hash.clone()); + for _ in 0..dispute_count { + let d = (session_idx, CandidateHash(Hash::random()), DisputeStatus::Active); + self.add_offchain_dispute(d.clone(), 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) -> (u32, 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 = test_helpers::dummy_candidate_receipt(lf.hash.clone()); + for _ in 0..dispute_count { + let d = (session_idx, CandidateHash(Hash::random()), DisputeStatus::ConcludedFor(0)); + self.add_offchain_dispute(d.clone(), 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) -> (u32, usize) { + let local_votes_count = self.validators_count * 90 / 100; + let session_idx = 4; + let lf = leaf(); + let dummy_receipt = test_helpers::dummy_candidate_receipt(lf.hash.clone()); + for _ in 0..dispute_count { + let d = (session_idx, CandidateHash(Hash::random()), DisputeStatus::ConcludedFor(0)); + self.add_offchain_dispute(d.clone(), local_votes_count, dummy_receipt.clone()); + } + (session_idx, local_votes_count * dispute_count) + } + + fn generate_local_votes( + statement_kind: T, + start_idx: usize, + count: usize, + ) -> BTreeMap { + assert!(start_idx < count); + (start_idx..count) + .map(|idx| { + ( + ValidatorIndex(idx as u32), + (statement_kind.clone(), test_helpers::dummy_signature()), + ) + }) + .collect::>() + } + + fn generate_bitvec( + validator_count: usize, + start_idx: usize, + count: usize, + ) -> BitVec { + 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); + + // active disputes unknown onchain + let (second_idx, second_votes) = + input.add_unconfirmed_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, Vec) = + result.into_iter().partition(|d| d.session == first_idx); + assert_eq!(first_batch.len(), DISPUTES_PER_BATCH); + + let (second_batch, rest): (Vec, Vec) = + rest.into_iter().partition(|d| d.session == second_idx); + assert_eq!(second_batch.len(), DISPUTES_PER_BATCH); + + let (third_batch, rest): (Vec, Vec) = + rest.into_iter().partition(|d| d.session == third_idx); + assert_eq!(third_batch.len(), DISPUTES_PER_BATCH); + + let (fifth_batch, rest): (Vec, Vec) = + 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); + + // active disputes unknown onchain + 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.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 + ); +} diff --git a/node/core/provisioner/src/disputes/random_selection/mod.rs b/node/core/provisioner/src/disputes/random_selection/mod.rs new file mode 100644 index 000000000000..7af025700bae --- /dev/null +++ b/node/core/provisioner/src/disputes/random_selection/mod.rs @@ -0,0 +1,194 @@ +// Copyright 2017-2022 Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Polkadot is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see . + +//! This module selects all RECENT disputes, fetches the votes for them from dispute-coordinator and +//! returns them as MultiDisputeStatementSet. If the RECENT disputes are more than +//! `MAX_DISPUTES_FORWARDED_TO_RUNTIME` constant - the ACTIVE disputes plus a random selection of +//! RECENT disputes (up to `MAX_DISPUTES_FORWARDED_TO_RUNTIME`) are returned instead. +//! If the ACTIVE disputes are also above `MAX_DISPUTES_FORWARDED_TO_RUNTIME` limit - a random selection +//! of them is generated. + +use crate::{metrics, LOG_TARGET}; +use futures::channel::oneshot; +use polkadot_node_subsystem::{messages::DisputeCoordinatorMessage, overseer}; +use polkadot_primitives::v2::{ + CandidateHash, DisputeStatement, DisputeStatementSet, MultiDisputeStatementSet, SessionIndex, +}; +use std::collections::HashSet; + +/// The maximum number of disputes Provisioner will include in the inherent data. +/// Serves as a protection not to flood the Runtime with excessive data. +const MAX_DISPUTES_FORWARDED_TO_RUNTIME: usize = 1_000; + +#[derive(Debug)] +enum RequestType { + /// Query recent disputes, could be an excessive amount. + Recent, + /// Query the currently active and very recently concluded disputes. + Active, +} + +/// Request open disputes identified by `CandidateHash` and the `SessionIndex`. +async fn request_disputes( + sender: &mut impl overseer::ProvisionerSenderTrait, + active_or_recent: RequestType, +) -> Vec<(SessionIndex, CandidateHash)> { + let disputes = match active_or_recent { + RequestType::Recent => { + let (tx, rx) = oneshot::channel(); + let msg = DisputeCoordinatorMessage::RecentDisputes(tx); + sender.send_unbounded_message(msg); + let recent_disputes = match rx.await { + Ok(r) => r, + Err(oneshot::Canceled) => { + gum::warn!( + target: LOG_TARGET, + "Channel closed: unable to gather {:?} disputes", + active_or_recent + ); + Vec::new() + }, + }; + recent_disputes + .into_iter() + .map(|(sesion_idx, candodate_hash, _)| (sesion_idx, candodate_hash)) + .collect::>() + }, + RequestType::Active => { + let (tx, rx) = oneshot::channel(); + let msg = DisputeCoordinatorMessage::ActiveDisputes(tx); + sender.send_unbounded_message(msg); + let active_disputes = match rx.await { + Ok(r) => r, + Err(oneshot::Canceled) => { + gum::warn!( + target: LOG_TARGET, + "Unable to gather {:?} disputes", + active_or_recent + ); + Vec::new() + }, + }; + active_disputes + }, + }; + + disputes +} + +/// Extend `acc` by `n` random, picks of not-yet-present in `acc` items of `recent` without repetition and additions of recent. +fn extend_by_random_subset_without_repetition( + acc: &mut Vec<(SessionIndex, CandidateHash)>, + extension: Vec<(SessionIndex, CandidateHash)>, + n: usize, +) { + use rand::Rng; + + let lut = acc.iter().cloned().collect::>(); + + let mut unique_new = + extension.into_iter().filter(|recent| !lut.contains(recent)).collect::>(); + + // we can simply add all + if unique_new.len() <= n { + acc.extend(unique_new) + } else { + acc.reserve(n); + let mut rng = rand::thread_rng(); + for _ in 0..n { + let idx = rng.gen_range(0..unique_new.len()); + acc.push(unique_new.swap_remove(idx)); + } + } + // assure sorting stays candid according to session index + acc.sort_unstable_by(|a, b| a.0.cmp(&b.0)); +} + +pub async fn select_disputes( + sender: &mut Sender, + metrics: &metrics::Metrics, +) -> MultiDisputeStatementSet +where + Sender: overseer::ProvisionerSenderTrait, +{ + gum::trace!(target: LOG_TARGET, "Selecting disputes for inherent data using random selection"); + + // We use `RecentDisputes` instead of `ActiveDisputes` because redundancy is fine. + // It's heavier than `ActiveDisputes` but ensures that everything from the dispute + // window gets on-chain, unlike `ActiveDisputes`. + // In case of an overload condition, we limit ourselves to active disputes, and fill up to the + // upper bound of disputes to pass to wasm `fn create_inherent_data`. + // If the active ones are already exceeding the bounds, randomly select a subset. + let recent = request_disputes(sender, RequestType::Recent).await; + let disputes = if recent.len() > MAX_DISPUTES_FORWARDED_TO_RUNTIME { + gum::warn!( + target: LOG_TARGET, + "Recent disputes are excessive ({} > {}), reduce to active ones, and selected", + recent.len(), + MAX_DISPUTES_FORWARDED_TO_RUNTIME + ); + let mut active = request_disputes(sender, RequestType::Active).await; + let n_active = active.len(); + let active = if active.len() > MAX_DISPUTES_FORWARDED_TO_RUNTIME { + let mut picked = Vec::with_capacity(MAX_DISPUTES_FORWARDED_TO_RUNTIME); + extend_by_random_subset_without_repetition( + &mut picked, + active, + MAX_DISPUTES_FORWARDED_TO_RUNTIME, + ); + picked + } else { + extend_by_random_subset_without_repetition( + &mut active, + recent, + MAX_DISPUTES_FORWARDED_TO_RUNTIME.saturating_sub(n_active), + ); + active + }; + active + } else { + recent + }; + + // Load all votes for all disputes from the coordinator. + let dispute_candidate_votes = super::request_votes(sender, disputes).await; + + // 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() +} diff --git a/node/core/provisioner/src/error.rs b/node/core/provisioner/src/error.rs index 05e437854eac..9fb958c4f339 100644 --- a/node/core/provisioner/src/error.rs +++ b/node/core/provisioner/src/error.rs @@ -88,9 +88,7 @@ pub enum GetOnchainDisputesError { #[error("runtime execution error occurred while fetching onchain disputes for parent {1}")] Execution(#[source] RuntimeApiError, Hash), - #[error( - "runtime doesn't support RuntimeApiRequest::Disputes/RuntimeApiRequest::StagingDisputes for parent {1}" - )] + #[error("runtime doesn't support RuntimeApiRequest::Disputes for parent {1}")] NotSupported(#[source] RuntimeApiError, Hash), } diff --git a/node/core/provisioner/src/lib.rs b/node/core/provisioner/src/lib.rs index 0f3099c7df33..301aec32c15b 100644 --- a/node/core/provisioner/src/lib.rs +++ b/node/core/provisioner/src/lib.rs @@ -25,29 +25,27 @@ use futures::{ }; use futures_timer::Delay; -use polkadot_node_primitives::CandidateVotes; use polkadot_node_subsystem::{ jaeger, messages::{ - CandidateBackingMessage, ChainApiMessage, DisputeCoordinatorMessage, ProvisionableData, - ProvisionerInherentData, ProvisionerMessage, + CandidateBackingMessage, ChainApiMessage, ProvisionableData, ProvisionerInherentData, + ProvisionerMessage, RuntimeApiMessage, RuntimeApiRequest, }, overseer, ActivatedLeaf, ActiveLeavesUpdate, FromOrchestra, LeafStatus, OverseerSignal, - PerLeafSpan, SpawnedSubsystem, SubsystemError, + PerLeafSpan, RuntimeApiError, SpawnedSubsystem, SubsystemError, }; use polkadot_node_subsystem_util::{ request_availability_cores, request_persisted_validation_data, TimeoutExt, }; use polkadot_primitives::v2::{ - BackedCandidate, BlockNumber, CandidateHash, CandidateReceipt, CoreState, DisputeState, - DisputeStatement, DisputeStatementSet, Hash, MultiDisputeStatementSet, OccupiedCoreAssumption, - SessionIndex, SignedAvailabilityBitfield, ValidatorIndex, + BackedCandidate, BlockNumber, CandidateReceipt, CoreState, Hash, OccupiedCoreAssumption, + SignedAvailabilityBitfield, ValidatorIndex, }; -use std::collections::{BTreeMap, HashMap, HashSet}; +use std::collections::{BTreeMap, HashMap}; +mod disputes; mod error; mod metrics; -mod onchain_disputes; pub use self::metrics::*; use error::{Error, FatalResult}; @@ -62,6 +60,9 @@ const SEND_INHERENT_DATA_TIMEOUT: std::time::Duration = core::time::Duration::fr const LOG_TARGET: &str = "parachain::provisioner"; +const PRIORITIZED_SELECTION_RUNTIME_VERSION_REQUIREMENT: u32 = + RuntimeApiRequest::DISPUTES_RUNTIME_REQUIREMENT; + /// The provisioner subsystem. pub struct ProvisionerSubsystem { metrics: Metrics, @@ -361,7 +362,18 @@ async fn send_inherent_data( relay_parent = ?leaf.hash, "Selecting disputes" ); - let disputes = select_disputes(from_job, metrics, leaf).await?; + + let disputes = match has_required_runtime( + from_job, + leaf.hash.clone(), + PRIORITIZED_SELECTION_RUNTIME_VERSION_REQUIREMENT, + ) + .await + { + true => disputes::prioritized_selection::select_disputes(from_job, metrics, leaf).await, + false => disputes::random_selection::select_disputes(from_job, metrics).await, + }; + gum::trace!( target: LOG_TARGET, relay_parent = ?leaf.hash, @@ -677,275 +689,55 @@ fn bitfields_indicate_availability( 3 * availability.count_ones() >= 2 * availability.len() } -#[derive(Debug)] -enum RequestType { - /// Query recent disputes, could be an excessive amount. - Recent, - /// Query the currently active and very recently concluded disputes. - Active, -} - -/// Request open disputes identified by `CandidateHash` and the `SessionIndex`. -async fn request_disputes( - sender: &mut impl overseer::ProvisionerSenderTrait, - active_or_recent: RequestType, -) -> Vec<(SessionIndex, CandidateHash)> { - let (tx, rx) = oneshot::channel(); - let msg = match active_or_recent { - RequestType::Recent => DisputeCoordinatorMessage::RecentDisputes(tx), - RequestType::Active => DisputeCoordinatorMessage::ActiveDisputes(tx), - }; - // Bounded by block production - `ProvisionerMessage::RequestInherentData`. - sender.send_unbounded_message(msg); - - let recent_disputes = match rx.await { - Ok(r) => r, - Err(oneshot::Canceled) => { - gum::warn!(target: LOG_TARGET, "Unable to gather {:?} disputes", active_or_recent); - Vec::new() - }, - }; - recent_disputes -} - -/// Request the relevant dispute statements for a set of disputes identified by `CandidateHash` and the `SessionIndex`. -async fn request_votes( +// If we have to be absolutely precise here, this method gets the version of the `ParachainHost` api. +// For brevity we'll just call it 'runtime version'. +async fn has_required_runtime( sender: &mut impl overseer::ProvisionerSenderTrait, - disputes_to_query: Vec<(SessionIndex, CandidateHash)>, -) -> Vec<(SessionIndex, CandidateHash, CandidateVotes)> { - // No need to send dummy request, if nothing to request: - if disputes_to_query.is_empty() { - gum::trace!(target: LOG_TARGET, "No disputes, nothing to request - returning empty `Vec`."); + relay_parent: Hash, + required_runtime_version: u32, +) -> bool { + gum::trace!(target: LOG_TARGET, ?relay_parent, "Fetching ParachainHost runtime api version"); - return Vec::new() - } let (tx, rx) = oneshot::channel(); - // Bounded by block production - `ProvisionerMessage::RequestInherentData`. - sender.send_unbounded_message(DisputeCoordinatorMessage::QueryCandidateVotes( - disputes_to_query, - tx, - )); + sender + .send_message(RuntimeApiMessage::Request(relay_parent, RuntimeApiRequest::Version(tx))) + .await; match rx.await { - Ok(v) => v, - Err(oneshot::Canceled) => { - gum::warn!(target: LOG_TARGET, "Unable to query candidate votes"); - Vec::new() + Result::Ok(Ok(runtime_version)) => { + gum::trace!( + target: LOG_TARGET, + ?relay_parent, + ?runtime_version, + ?required_runtime_version, + "Fetched ParachainHost runtime api version" + ); + runtime_version >= required_runtime_version }, - } -} - -/// Extend `acc` by `n` random, picks of not-yet-present in `acc` items of `recent` without repetition and additions of recent. -fn extend_by_random_subset_without_repetition( - acc: &mut Vec<(SessionIndex, CandidateHash)>, - extension: Vec<(SessionIndex, CandidateHash)>, - n: usize, -) { - use rand::Rng; - - let lut = acc.iter().cloned().collect::>(); - - let mut unique_new = - extension.into_iter().filter(|recent| !lut.contains(recent)).collect::>(); - - // we can simply add all - if unique_new.len() <= n { - acc.extend(unique_new) - } else { - acc.reserve(n); - let mut rng = rand::thread_rng(); - for _ in 0..n { - let idx = rng.gen_range(0..unique_new.len()); - acc.push(unique_new.swap_remove(idx)); - } - } - // assure sorting stays candid according to session index - acc.sort_unstable_by(|a, b| a.0.cmp(&b.0)); -} - -/// The maximum number of disputes Provisioner will include in the inherent data. -/// Serves as a protection not to flood the Runtime with excessive data. -const MAX_DISPUTES_FORWARDED_TO_RUNTIME: usize = 1_000; - -async fn select_disputes( - sender: &mut impl overseer::ProvisionerSenderTrait, - metrics: &metrics::Metrics, - _leaf: &ActivatedLeaf, -) -> Result { - // Helper lambda - // Gets the active disputes as input and partitions it in seen and unseen disputes by the Runtime - // Returns as much unseen disputes as possible and optionally some seen disputes up to `MAX_DISPUTES_FORWARDED_TO_RUNTIME` limit. - let generate_unseen_active_subset = - |active: Vec<(SessionIndex, CandidateHash)>, - onchain: HashMap<(SessionIndex, CandidateHash), DisputeState>| - -> Vec<(SessionIndex, CandidateHash)> { - let (seen_onchain, mut unseen_onchain): ( - Vec<(SessionIndex, CandidateHash)>, - Vec<(SessionIndex, CandidateHash)>, - ) = active.into_iter().partition(|d| onchain.contains_key(d)); - - if unseen_onchain.len() > MAX_DISPUTES_FORWARDED_TO_RUNTIME { - // Even unseen on-chain don't fit within the limit. Add as many as possible. - let mut unseen_subset = Vec::with_capacity(MAX_DISPUTES_FORWARDED_TO_RUNTIME); - extend_by_random_subset_without_repetition( - &mut unseen_subset, - unseen_onchain, - MAX_DISPUTES_FORWARDED_TO_RUNTIME, - ); - unseen_subset - } else { - // Add all unseen onchain disputes and as much of the seen ones as there is space. - let n_unseen_onchain = unseen_onchain.len(); - extend_by_random_subset_without_repetition( - &mut unseen_onchain, - seen_onchain, - MAX_DISPUTES_FORWARDED_TO_RUNTIME.saturating_sub(n_unseen_onchain), - ); - unseen_onchain - } - }; - - // Helper lambda - // Extends the active disputes with recent ones up to `MAX_DISPUTES_FORWARDED_TO_RUNTIME` limit. Unseen recent disputes are prioritised. - let generate_active_and_unseen_recent_subset = - |recent: Vec<(SessionIndex, CandidateHash)>, - mut active: Vec<(SessionIndex, CandidateHash)>, - onchain: HashMap<(SessionIndex, CandidateHash), DisputeState>| - -> Vec<(SessionIndex, CandidateHash)> { - let mut n_active = active.len(); - // All active disputes can be sent. Fill the rest of the space with recent ones. - // We assume there is not enough space for all recent disputes. So we prioritise the unseen ones. - let (seen_onchain, unseen_onchain): ( - Vec<(SessionIndex, CandidateHash)>, - Vec<(SessionIndex, CandidateHash)>, - ) = recent.into_iter().partition(|d| onchain.contains_key(d)); - - extend_by_random_subset_without_repetition( - &mut active, - unseen_onchain, - MAX_DISPUTES_FORWARDED_TO_RUNTIME.saturating_sub(n_active), + Result::Ok(Err(RuntimeApiError::Execution { source: error, .. })) => { + gum::trace!( + target: LOG_TARGET, + ?relay_parent, + ?error, + "Execution error while fetching ParachainHost runtime api version" ); - n_active = active.len(); - - if n_active < MAX_DISPUTES_FORWARDED_TO_RUNTIME { - // Looks like we can add some of the seen disputes too - extend_by_random_subset_without_repetition( - &mut active, - seen_onchain, - MAX_DISPUTES_FORWARDED_TO_RUNTIME.saturating_sub(n_active), - ); - } - active - }; - - gum::trace!( - target: LOG_TARGET, - relay_parent = ?_leaf.hash, - "Request recent disputes" - ); - - // We use `RecentDisputes` instead of `ActiveDisputes` because redundancy is fine. - // It's heavier than `ActiveDisputes` but ensures that everything from the dispute - // window gets on-chain, unlike `ActiveDisputes`. - // In case of an overload condition, we limit ourselves to active disputes, and fill up to the - // upper bound of disputes to pass to wasm `fn create_inherent_data`. - // If the active ones are already exceeding the bounds, randomly select a subset. - let recent = request_disputes(sender, RequestType::Recent).await; - - gum::trace!( - target: LOG_TARGET, - relay_paent = ?_leaf.hash, - "Received recent disputes" - ); - - gum::trace!( - target: LOG_TARGET, - relay_paent = ?_leaf.hash, - "Request on chain disputes" - ); - - // On chain disputes are fetched from the runtime. We want to prioritise the inclusion of unknown - // disputes in the inherent data. The call relies on staging Runtime API. If the staging API is not - // enabled in the binary an empty set is generated which doesn't affect the rest of the logic. - let onchain = match onchain_disputes::get_onchain_disputes(sender, _leaf.hash.clone()).await { - Ok(r) => r, - Err(e) => { - gum::debug!( + false + }, + Result::Ok(Err(RuntimeApiError::NotSupported { .. })) => { + gum::trace!( target: LOG_TARGET, - ?e, - "Can't fetch onchain disputes. Will continue with empty onchain disputes set.", + ?relay_parent, + "NotSupported error while fetching ParachainHost runtime api version" ); - HashMap::new() + false }, - }; - - gum::trace!( - target: LOG_TARGET, - relay_paent = ?_leaf.hash, - "Received on chain disputes" - ); - - gum::trace!( - target: LOG_TARGET, - relay_paent = ?_leaf.hash, - "Filtering disputes" - ); - - let disputes = if recent.len() > MAX_DISPUTES_FORWARDED_TO_RUNTIME { - gum::warn!( - target: LOG_TARGET, - "Recent disputes are excessive ({} > {}), reduce to active ones, and selected", - recent.len(), - MAX_DISPUTES_FORWARDED_TO_RUNTIME - ); - let active = request_disputes(sender, RequestType::Active).await; - if active.len() > MAX_DISPUTES_FORWARDED_TO_RUNTIME { - generate_unseen_active_subset(active, onchain) - } else { - generate_active_and_unseen_recent_subset(recent, active, onchain) - } - } else { - recent - }; - - gum::trace!( - target: LOG_TARGET, - relay_paent = ?_leaf.hash, - "Calling `request_votes`" - ); - - // Load all votes for all disputes from the coordinator. - let dispute_candidate_votes = request_votes(sender, disputes).await; - - gum::trace!( - target: LOG_TARGET, - relay_paent = ?_leaf.hash, - "Finished `request_votes`" - ); - - // Transform all `CandidateVotes` into `MultiDisputeStatementSet`. - Ok(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()) + Result::Err(_) => { + gum::trace!( + target: LOG_TARGET, + ?relay_parent, + "Cancelled error while fetching ParachainHost runtime api version" + ); + false + }, + } } diff --git a/node/core/provisioner/src/metrics.rs b/node/core/provisioner/src/metrics.rs index 508c668f6e24..8b6bb37284cb 100644 --- a/node/core/provisioner/src/metrics.rs +++ b/node/core/provisioner/src/metrics.rs @@ -14,6 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Polkadot. If not, see . +use crate::disputes::prioritized_selection::PartitionedDisputes; use polkadot_node_subsystem_util::metrics::{self, prometheus}; #[derive(Clone)] @@ -32,6 +33,9 @@ struct MetricsInner { /// 4 hours on Polkadot. The metrics are updated only when the node authors a block, so values vary across nodes. inherent_data_dispute_statement_sets: prometheus::Counter, inherent_data_dispute_statements: prometheus::CounterVec, + + /// The disputes received from `disputes-coordinator` by partition + partitioned_disputes: prometheus::CounterVec, } /// Provisioner metrics. @@ -101,6 +105,44 @@ impl Metrics { .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)); + } + } } impl metrics::Metrics for Metrics { @@ -156,6 +198,16 @@ impl metrics::Metrics for Metrics { )?, registry, )?, + partitioned_disputes: prometheus::register( + prometheus::CounterVec::new( + prometheus::Opts::new( + "polkadot_parachain_provisioner_partitioned_disputes", + "some fancy description", + ), + &["partition"], + )?, + ®istry, + )?, }; Ok(Metrics(Some(metrics))) } diff --git a/node/core/provisioner/src/onchain_disputes.rs b/node/core/provisioner/src/onchain_disputes.rs deleted file mode 100644 index 6810f512173f..000000000000 --- a/node/core/provisioner/src/onchain_disputes.rs +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2017-2022 Parity Technologies (UK) Ltd. -// This file is part of Polkadot. - -// Polkadot is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Polkadot is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Polkadot. If not, see . - -use crate::error::GetOnchainDisputesError; -use polkadot_node_subsystem::overseer; -use polkadot_primitives::v2::{CandidateHash, DisputeState, Hash, SessionIndex}; -use std::collections::HashMap; - -pub async fn get_onchain_disputes( - _sender: &mut Sender, - _relay_parent: Hash, -) -> Result, GetOnchainDisputesError> -where - Sender: overseer::ProvisionerSenderTrait, -{ - let _onchain = Result::< - HashMap<(SessionIndex, CandidateHash), DisputeState>, - GetOnchainDisputesError, - >::Ok(HashMap::new()); - #[cfg(feature = "staging-client")] - let _onchain = self::staging_impl::get_onchain_disputes(_sender, _relay_parent).await; - - _onchain -} - -// Merge this module with the outer (current one) when promoting to stable -#[cfg(feature = "staging-client")] -mod staging_impl { - use super::*; // remove this when promoting to stable - use crate::LOG_TARGET; - use futures::channel::oneshot; - use polkadot_node_subsystem::{ - errors::RuntimeApiError, - messages::{RuntimeApiMessage, RuntimeApiRequest}, - SubsystemSender, - }; - - /// Gets the on-chain disputes at a given block number and returns them as a `HashSet` so that searching in them is cheap. - pub async fn get_onchain_disputes( - sender: &mut impl SubsystemSender, - relay_parent: Hash, - ) -> Result, GetOnchainDisputesError> { - gum::trace!(target: LOG_TARGET, ?relay_parent, "Fetching on-chain disputes"); - let (tx, rx) = oneshot::channel(); - sender - .send_message( - RuntimeApiMessage::Request(relay_parent, RuntimeApiRequest::StagingDisputes(tx)) - .into(), - ) - .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()) - } -} diff --git a/node/core/provisioner/src/tests.rs b/node/core/provisioner/src/tests.rs index d0ca425210ed..08eba8eabe80 100644 --- a/node/core/provisioner/src/tests.rs +++ b/node/core/provisioner/src/tests.rs @@ -195,7 +195,7 @@ mod select_availability_bitfields { } } -mod common { +pub(crate) mod common { use super::super::*; use futures::channel::mpsc; use polkadot_node_subsystem::messages::AllMessages; @@ -497,403 +497,3 @@ mod select_candidates { ) } } - -mod select_disputes { - use super::{super::*, common::test_harness}; - use futures::channel::mpsc; - use polkadot_node_subsystem::{ - messages::{AllMessages, DisputeCoordinatorMessage, RuntimeApiMessage, RuntimeApiRequest}, - RuntimeApiError, - }; - use polkadot_node_subsystem_test_helpers::TestSubsystemSender; - use polkadot_primitives::v2::DisputeState; - use std::sync::Arc; - use test_helpers; - - // Global Test Data - fn recent_disputes(len: usize) -> Vec<(SessionIndex, CandidateHash)> { - let mut res = Vec::with_capacity(len); - for _ in 0..len { - res.push((0, CandidateHash(Hash::random()))); - } - - res - } - - // same as recent_disputes() but with SessionIndex set to 1 - fn active_disputes(len: usize) -> Vec<(SessionIndex, CandidateHash)> { - let mut res = Vec::with_capacity(len); - for _ in 0..len { - res.push((1, CandidateHash(Hash::random()))); - } - - res - } - - fn leaf() -> ActivatedLeaf { - ActivatedLeaf { - hash: Hash::repeat_byte(0xAA), - number: 0xAA, - status: LeafStatus::Fresh, - span: Arc::new(jaeger::Span::Disabled), - } - } - - async fn mock_overseer( - leaf: ActivatedLeaf, - mut receiver: mpsc::UnboundedReceiver, - onchain_disputes: Result, RuntimeApiError>, - recent_disputes: Vec<(SessionIndex, CandidateHash)>, - active_disputes: Vec<(SessionIndex, CandidateHash)>, - ) { - while let Some(from_job) = receiver.next().await { - match from_job { - AllMessages::RuntimeApi(RuntimeApiMessage::Request( - _, - RuntimeApiRequest::StagingDisputes(sender), - )) => { - let _ = sender.send(onchain_disputes.clone()); - }, - AllMessages::RuntimeApi(_) => panic!("Unexpected RuntimeApi request"), - AllMessages::DisputeCoordinator(DisputeCoordinatorMessage::RecentDisputes( - sender, - )) => { - let _ = sender.send(recent_disputes.clone()); - }, - AllMessages::DisputeCoordinator(DisputeCoordinatorMessage::ActiveDisputes( - sender, - )) => { - let _ = sender.send(active_disputes.clone()); - }, - AllMessages::DisputeCoordinator( - DisputeCoordinatorMessage::QueryCandidateVotes(disputes, sender), - ) => { - let mut res = Vec::new(); - let v = CandidateVotes { - candidate_receipt: test_helpers::dummy_candidate_receipt(leaf.hash.clone()), - valid: BTreeMap::new(), - invalid: BTreeMap::new(), - }; - for r in disputes.iter() { - res.push((r.0, r.1, v.clone())); - } - - let _ = sender.send(res); - }, - _ => panic!("Unexpected message: {:?}", from_job), - } - } - } - - #[test] - fn recent_disputes_are_withing_onchain_limit() { - const RECENT_DISPUTES_SIZE: usize = 10; - let metrics = metrics::Metrics::new_dummy(); - let onchain_disputes = Ok(Vec::new()); - let active_disputes = Vec::new(); - let recent_disputes = recent_disputes(RECENT_DISPUTES_SIZE); - - let recent_disputes_overseer = recent_disputes.clone(); - test_harness( - |r| { - mock_overseer( - leaf(), - r, - onchain_disputes, - recent_disputes_overseer, - active_disputes, - ) - }, - |mut tx: TestSubsystemSender| async move { - let lf = leaf(); - let disputes = select_disputes(&mut tx, &metrics, &lf).await.unwrap(); - - assert!(!disputes.is_empty()); - - let result = disputes.iter().zip(recent_disputes.iter()); - // We should get all recent disputes. - for (d, r) in result { - assert_eq!(d.session, r.0); - assert_eq!(d.candidate_hash, r.1); - } - }, - ) - } - - #[test] - fn recent_disputes_are_too_much_but_active_are_within_limit() { - const RECENT_DISPUTES_SIZE: usize = MAX_DISPUTES_FORWARDED_TO_RUNTIME + 10; - const ACTIVE_DISPUTES_SIZE: usize = MAX_DISPUTES_FORWARDED_TO_RUNTIME; - let metrics = metrics::Metrics::new_dummy(); - let onchain_disputes = Ok(Vec::new()); - let recent_disputes = recent_disputes(RECENT_DISPUTES_SIZE); - let active_disputes = active_disputes(ACTIVE_DISPUTES_SIZE); - - let active_disputes_overseer = active_disputes.clone(); - test_harness( - |r| { - mock_overseer( - leaf(), - r, - onchain_disputes, - recent_disputes, - active_disputes_overseer, - ) - }, - |mut tx: TestSubsystemSender| async move { - let lf = leaf(); - let disputes = select_disputes(&mut tx, &metrics, &lf).await.unwrap(); - - assert!(!disputes.is_empty()); - - let result = disputes.iter().zip(active_disputes.iter()); - // We should get all active disputes. - for (d, r) in result { - assert_eq!(d.session, r.0); - assert_eq!(d.candidate_hash, r.1); - } - }, - ) - } - - #[test] - fn recent_disputes_are_too_much_but_active_are_less_than_the_limit() { - // In this case all active disputes + a random set of recent disputes should be returned - const RECENT_DISPUTES_SIZE: usize = MAX_DISPUTES_FORWARDED_TO_RUNTIME + 10; - const ACTIVE_DISPUTES_SIZE: usize = MAX_DISPUTES_FORWARDED_TO_RUNTIME - 10; - let metrics = metrics::Metrics::new_dummy(); - let onchain_disputes = Ok(Vec::new()); - let recent_disputes = recent_disputes(RECENT_DISPUTES_SIZE); - let active_disputes = active_disputes(ACTIVE_DISPUTES_SIZE); - - let active_disputes_overseer = active_disputes.clone(); - test_harness( - |r| { - mock_overseer( - leaf(), - r, - onchain_disputes, - recent_disputes, - active_disputes_overseer, - ) - }, - |mut tx: TestSubsystemSender| async move { - let lf = leaf(); - let disputes = select_disputes(&mut tx, &metrics, &lf).await.unwrap(); - - assert!(!disputes.is_empty()); - - // Recent disputes are generated with `SessionIndex` = 0 - let (res_recent, res_active): (Vec, Vec) = - disputes.into_iter().partition(|d| d.session == 0); - - // It should be good enough the count the number of active disputes and not compare them one by one. Checking the exact values is already covered by the previous tests. - assert_eq!(res_active.len(), active_disputes.len()); // We have got all active disputes - assert_eq!(res_active.len() + res_recent.len(), MAX_DISPUTES_FORWARDED_TO_RUNTIME); - // And some recent ones. - }, - ) - } - - //These tests rely on staging Runtime functions so they are separated and compiled conditionally. - #[cfg(feature = "staging-client")] - mod staging_tests { - use super::*; - - fn dummy_dispute_state() -> DisputeState { - DisputeState { - validators_for: BitVec::new(), - validators_against: BitVec::new(), - start: 0, - concluded_at: None, - } - } - - #[test] - fn recent_disputes_are_too_much_active_fits_test_recent_prioritisation() { - // In this case recent disputes are above `MAX_DISPUTES_FORWARDED_TO_RUNTIME` limit and the active ones are below it. - // The expected behaviour is to send all active disputes and extend the set with recent ones. During the extension the disputes unknown for the Runtime are added with priority. - const RECENT_DISPUTES_SIZE: usize = MAX_DISPUTES_FORWARDED_TO_RUNTIME + 10; - const ACTIVE_DISPUTES_SIZE: usize = MAX_DISPUTES_FORWARDED_TO_RUNTIME - 10; - const ONCHAIN_DISPUTE_SIZE: usize = RECENT_DISPUTES_SIZE - 9; - let metrics = metrics::Metrics::new_dummy(); - let recent_disputes = recent_disputes(RECENT_DISPUTES_SIZE); - let active_disputes = active_disputes(ACTIVE_DISPUTES_SIZE); - let onchain_disputes: Result< - Vec<(SessionIndex, CandidateHash, DisputeState)>, - RuntimeApiError, - > = Ok(Vec::from(&recent_disputes[0..ONCHAIN_DISPUTE_SIZE]) - .iter() - .map(|(session_index, candidate_hash)| { - (*session_index, candidate_hash.clone(), dummy_dispute_state()) - }) - .collect()); - let active_disputes_overseer = active_disputes.clone(); - let recent_disputes_overseer = recent_disputes.clone(); - test_harness( - |r| { - mock_overseer( - leaf(), - r, - onchain_disputes, - recent_disputes_overseer, - active_disputes_overseer, - ) - }, - |mut tx: TestSubsystemSender| async move { - let lf = leaf(); - let disputes = select_disputes(&mut tx, &metrics, &lf).await.unwrap(); - - assert!(!disputes.is_empty()); - - // Recent disputes are generated with `SessionIndex` = 0 - let (res_recent, res_active): ( - Vec, - Vec, - ) = disputes.into_iter().partition(|d| d.session == 0); - - // It should be good enough the count the number of the disputes and not compare them one by one as this was already covered in other tests. - assert_eq!(res_active.len(), active_disputes.len()); // We've got all active disputes. - assert_eq!( - res_recent.len(), - MAX_DISPUTES_FORWARDED_TO_RUNTIME - active_disputes.len() - ); // And some recent ones. - - // Check if the recent disputes were unknown for the Runtime. - let expected_recent_disputes = - Vec::from(&recent_disputes[ONCHAIN_DISPUTE_SIZE..]); - let res_recent_set: HashSet<(SessionIndex, CandidateHash)> = HashSet::from_iter( - res_recent.iter().map(|d| (d.session, d.candidate_hash)), - ); - - // Explicitly check that all unseen disputes are sent to the Runtime. - for d in &expected_recent_disputes { - assert_eq!(res_recent_set.contains(d), true); - } - }, - ) - } - - #[test] - fn active_disputes_are_too_much_test_active_prioritisation() { - // In this case the active disputes are above the `MAX_DISPUTES_FORWARDED_TO_RUNTIME` limit so the unseen ones should be prioritised. - const RECENT_DISPUTES_SIZE: usize = MAX_DISPUTES_FORWARDED_TO_RUNTIME + 10; - const ACTIVE_DISPUTES_SIZE: usize = MAX_DISPUTES_FORWARDED_TO_RUNTIME + 10; - const ONCHAIN_DISPUTE_SIZE: usize = ACTIVE_DISPUTES_SIZE - 9; - - let metrics = metrics::Metrics::new_dummy(); - let recent_disputes = recent_disputes(RECENT_DISPUTES_SIZE); - let active_disputes = active_disputes(ACTIVE_DISPUTES_SIZE); - let onchain_disputes: Result< - Vec<(SessionIndex, CandidateHash, DisputeState)>, - RuntimeApiError, - > = Ok(Vec::from(&active_disputes[0..ONCHAIN_DISPUTE_SIZE]) - .iter() - .map(|(session_index, candidate_hash)| { - (*session_index, candidate_hash.clone(), dummy_dispute_state()) - }) - .collect()); - let active_disputes_overseer = active_disputes.clone(); - let recent_disputes_overseer = recent_disputes.clone(); - test_harness( - |r| { - mock_overseer( - leaf(), - r, - onchain_disputes, - recent_disputes_overseer, - active_disputes_overseer, - ) - }, - |mut tx: TestSubsystemSender| async move { - let lf = leaf(); - let disputes = select_disputes(&mut tx, &metrics, &lf).await.unwrap(); - - assert!(!disputes.is_empty()); - - // Recent disputes are generated with `SessionIndex` = 0 - let (res_recent, res_active): ( - Vec, - Vec, - ) = disputes.into_iter().partition(|d| d.session == 0); - - // It should be good enough the count the number of the disputes and not compare them one by one - assert_eq!(res_recent.len(), 0); // We expect no recent disputes - assert_eq!(res_active.len(), MAX_DISPUTES_FORWARDED_TO_RUNTIME); - - let expected_active_disputes = - Vec::from(&active_disputes[ONCHAIN_DISPUTE_SIZE..]); - let res_active_set: HashSet<(SessionIndex, CandidateHash)> = HashSet::from_iter( - res_active.iter().map(|d| (d.session, d.candidate_hash)), - ); - - // Explicitly check that the unseen disputes are delivered to the Runtime. - for d in &expected_active_disputes { - assert_eq!(res_active_set.contains(d), true); - } - }, - ) - } - - #[test] - fn active_disputes_are_too_much_and_are_all_unseen() { - // In this case there are a lot of active disputes unseen by the Runtime. The focus of the test is to verify that in such cases known disputes are NOT sent to the Runtime. - const RECENT_DISPUTES_SIZE: usize = MAX_DISPUTES_FORWARDED_TO_RUNTIME + 10; - const ACTIVE_DISPUTES_SIZE: usize = MAX_DISPUTES_FORWARDED_TO_RUNTIME + 5; - const ONCHAIN_DISPUTE_SIZE: usize = 5; - - let metrics = metrics::Metrics::new_dummy(); - let recent_disputes = recent_disputes(RECENT_DISPUTES_SIZE); - let active_disputes = active_disputes(ACTIVE_DISPUTES_SIZE); - let onchain_disputes: Result< - Vec<(SessionIndex, CandidateHash, DisputeState)>, - RuntimeApiError, - > = Ok(Vec::from(&active_disputes[0..ONCHAIN_DISPUTE_SIZE]) - .iter() - .map(|(session_index, candidate_hash)| { - (*session_index, candidate_hash.clone(), dummy_dispute_state()) - }) - .collect()); - let active_disputes_overseer = active_disputes.clone(); - let recent_disputes_overseer = recent_disputes.clone(); - test_harness( - |r| { - mock_overseer( - leaf(), - r, - onchain_disputes, - recent_disputes_overseer, - active_disputes_overseer, - ) - }, - |mut tx: TestSubsystemSender| async move { - let lf = leaf(); - let disputes = select_disputes(&mut tx, &metrics, &lf).await.unwrap(); - assert!(!disputes.is_empty()); - - // Recent disputes are generated with `SessionIndex` = 0 - let (res_recent, res_active): ( - Vec, - Vec, - ) = disputes.into_iter().partition(|d| d.session == 0); - - // It should be good enough the count the number of the disputes and not compare them one by one - assert_eq!(res_recent.len(), 0); - assert_eq!(res_active.len(), MAX_DISPUTES_FORWARDED_TO_RUNTIME); - - // For sure we don't want to see any of this disputes in the result - let unexpected_active_disputes = - Vec::from(&active_disputes[0..ONCHAIN_DISPUTE_SIZE]); - let res_active_set: HashSet<(SessionIndex, CandidateHash)> = HashSet::from_iter( - res_active.iter().map(|d| (d.session, d.candidate_hash)), - ); - - // Verify that the result DOESN'T contain known disputes (because there is an excessive number of unknown onces). - for d in &unexpected_active_disputes { - assert_eq!(res_active_set.contains(d), false); - } - }, - ) - } - } -} diff --git a/node/core/runtime-api/src/cache.rs b/node/core/runtime-api/src/cache.rs index 6f5fdc5d4657..0fe9b74dc86d 100644 --- a/node/core/runtime-api/src/cache.rs +++ b/node/core/runtime-api/src/cache.rs @@ -463,5 +463,5 @@ pub(crate) enum RequestResult { SubmitPvfCheckStatement(Hash, PvfCheckStatement, ValidatorSignature, ()), ValidationCodeHash(Hash, ParaId, OccupiedCoreAssumption, Option), Version(Hash, u32), - StagingDisputes(Hash, Vec<(SessionIndex, CandidateHash, DisputeState)>), + Disputes(Hash, Vec<(SessionIndex, CandidateHash, DisputeState)>), } diff --git a/node/core/runtime-api/src/lib.rs b/node/core/runtime-api/src/lib.rs index a815b76a8d7c..36355b5759e6 100644 --- a/node/core/runtime-api/src/lib.rs +++ b/node/core/runtime-api/src/lib.rs @@ -153,7 +153,7 @@ where .cache_validation_code_hash((relay_parent, para_id, assumption), hash), Version(relay_parent, version) => self.requests_cache.cache_version(relay_parent, version), - StagingDisputes(relay_parent, disputes) => + Disputes(relay_parent, disputes) => self.requests_cache.cache_disputes(relay_parent, disputes), } } @@ -256,8 +256,8 @@ where Request::ValidationCodeHash(para, assumption, sender) => query!(validation_code_hash(para, assumption), sender) .map(|sender| Request::ValidationCodeHash(para, assumption, sender)), - Request::StagingDisputes(sender) => - query!(disputes(), sender).map(|sender| Request::StagingDisputes(sender)), + Request::Disputes(sender) => + query!(disputes(), sender).map(|sender| Request::Disputes(sender)), } } @@ -351,8 +351,9 @@ where let _timer = metrics.time_make_runtime_api_request(); macro_rules! query { - ($req_variant:ident, $api_name:ident ($($param:expr),*), ver = $version:literal, $sender:expr) => {{ + ($req_variant:ident, $api_name:ident ($($param:expr),*), ver = $version:expr, $sender:expr) => {{ let sender = $sender; + let version: u32 = $version; // enforce type for the version expression let runtime_version = client.api_version_parachain_host(relay_parent).await .unwrap_or_else(|e| { gum::warn!( @@ -370,7 +371,7 @@ where 0 }); - let res = if runtime_version >= $version { + let res = if runtime_version >= version { client.$api_name(relay_parent $(, $param.clone() )*).await .map_err(|e| RuntimeApiError::Execution { runtime_api_name: stringify!($api_name), @@ -499,7 +500,7 @@ where }, Request::ValidationCodeHash(para, assumption, sender) => query!(ValidationCodeHash, validation_code_hash(para, assumption), ver = 2, sender), - Request::StagingDisputes(sender) => - query!(StagingDisputes, staging_get_disputes(), ver = 2, sender), + Request::Disputes(sender) => + query!(Disputes, disputes(), ver = Request::DISPUTES_RUNTIME_REQUIREMENT, sender), } } diff --git a/node/core/runtime-api/src/tests.rs b/node/core/runtime-api/src/tests.rs index eccfbeaa17c4..2fab84179433 100644 --- a/node/core/runtime-api/src/tests.rs +++ b/node/core/runtime-api/src/tests.rs @@ -23,11 +23,11 @@ use polkadot_node_subsystem_test_helpers::make_subsystem_context; use polkadot_primitives::{ runtime_api::ParachainHost, v2::{ - AuthorityDiscoveryId, Block, BlockNumber, CandidateEvent, CandidateHash, - CommittedCandidateReceipt, CoreState, DisputeState, GroupRotationInfo, Id as ParaId, - InboundDownwardMessage, InboundHrmpMessage, OccupiedCoreAssumption, - PersistedValidationData, PvfCheckStatement, ScrapedOnChainVotes, SessionIndex, SessionInfo, - ValidationCode, ValidationCodeHash, ValidatorId, ValidatorIndex, ValidatorSignature, + AuthorityDiscoveryId, Block, CandidateEvent, CommittedCandidateReceipt, CoreState, + GroupRotationInfo, Id as ParaId, InboundDownwardMessage, InboundHrmpMessage, + OccupiedCoreAssumption, PersistedValidationData, PvfCheckStatement, ScrapedOnChainVotes, + SessionIndex, SessionInfo, ValidationCode, ValidationCodeHash, ValidatorId, ValidatorIndex, + ValidatorSignature, }, }; use sp_api::ProvideRuntimeApi; @@ -196,10 +196,6 @@ sp_api::mock_impl_runtime_apis! { ) -> Option { self.validation_code_hash.get(¶).map(|c| c.clone()) } - - fn staging_get_disputes() -> Vec<(SessionIndex, CandidateHash, DisputeState)> { - unimplemented!() - } } impl BabeApi for MockRuntimeApi { diff --git a/node/network/approval-distribution/Cargo.toml b/node/network/approval-distribution/Cargo.toml index ac34d57d586b..fa0e4fff2c91 100644 --- a/node/network/approval-distribution/Cargo.toml +++ b/node/network/approval-distribution/Cargo.toml @@ -21,6 +21,7 @@ sp-core = { git = "https://github.com/paritytech/substrate", branch = "master", polkadot-node-subsystem-util = { path = "../../subsystem-util" } polkadot-node-subsystem-test-helpers = { path = "../../subsystem-test-helpers" } +polkadot-primitives-test-helpers = { path = "../../../primitives/test-helpers" } assert_matches = "1.4.0" schnorrkel = { version = "0.9.1", default-features = false } diff --git a/node/network/approval-distribution/src/tests.rs b/node/network/approval-distribution/src/tests.rs index b3d44bfe8c1e..a96a89bb58eb 100644 --- a/node/network/approval-distribution/src/tests.rs +++ b/node/network/approval-distribution/src/tests.rs @@ -25,6 +25,7 @@ use polkadot_node_subsystem::messages::{network_bridge_event, AllMessages, Appro use polkadot_node_subsystem_test_helpers as test_helpers; use polkadot_node_subsystem_util::TimeoutExt as _; use polkadot_primitives::v2::{AuthorityDiscoveryId, BlakeTwo256, HashT}; +use polkadot_primitives_test_helpers::dummy_signature; use rand::SeedableRng; use sp_authority_discovery::AuthorityPair as AuthorityDiscoveryPair; use sp_core::crypto::Pair as PairT; @@ -32,10 +33,6 @@ use std::time::Duration; type VirtualOverseer = test_helpers::TestSubsystemContextHandle; -fn dummy_signature() -> polkadot_primitives::v2::ValidatorSignature { - sp_core::crypto::UncheckedFrom::unchecked_from([1u8; 64]) -} - fn test_harness>( mut state: State, test_fn: impl FnOnce(VirtualOverseer) -> T, diff --git a/node/network/gossip-support/src/lib.rs b/node/network/gossip-support/src/lib.rs index df90914b6f58..823835aa7638 100644 --- a/node/network/gossip-support/src/lib.rs +++ b/node/network/gossip-support/src/lib.rs @@ -249,11 +249,15 @@ where let mut connections = authorities_past_present_future(sender, leaf).await?; // Remove all of our locally controlled validator indices so we don't connect to ourself. - // If we control none of them, don't issue connection requests - we're outside - // of the 'clique' of recent validators. - if remove_all_controlled(&self.keystore, &mut connections).await != 0 { - self.issue_connection_request(sender, connections).await; - } + let connections = + if remove_all_controlled(&self.keystore, &mut connections).await != 0 { + connections + } else { + // If we control none of them, issue an empty connection request + // to clean up all connections. + Vec::new() + }; + self.issue_connection_request(sender, connections).await; } if is_new_session { @@ -353,7 +357,7 @@ where // issue another request for the same session // if at least a third of the authorities were not resolved. - if 3 * failures >= num { + if num != 0 && 3 * failures >= num { let timestamp = Instant::now(); match self.failure_start { None => self.failure_start = Some(timestamp), diff --git a/node/network/gossip-support/src/tests.rs b/node/network/gossip-support/src/tests.rs index 831f0aa94342..cde47e2ba977 100644 --- a/node/network/gossip-support/src/tests.rs +++ b/node/network/gossip-support/src/tests.rs @@ -475,6 +475,64 @@ fn issues_connection_request_to_past_present_future() { }); } +#[test] +fn disconnect_when_not_in_past_present_future() { + sp_tracing::try_init_simple(); + let hash = Hash::repeat_byte(0xAA); + test_harness(make_subsystem(), |mut virtual_overseer| async move { + let overseer = &mut virtual_overseer; + overseer_signal_active_leaves(overseer, hash).await; + assert_matches!( + overseer_recv(overseer).await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request( + relay_parent, + RuntimeApiRequest::SessionIndexForChild(tx), + )) => { + assert_eq!(relay_parent, hash); + tx.send(Ok(1)).unwrap(); + } + ); + + assert_matches!( + overseer_recv(overseer).await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request( + relay_parent, + RuntimeApiRequest::SessionInfo(s, tx), + )) => { + assert_eq!(relay_parent, hash); + assert_eq!(s, 1); + let mut heute_leider_nicht = make_session_info(); + heute_leider_nicht.discovery_keys = AUTHORITIES_WITHOUT_US.clone(); + tx.send(Ok(Some(heute_leider_nicht))).unwrap(); + } + ); + + assert_matches!( + overseer_recv(overseer).await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request( + relay_parent, + RuntimeApiRequest::Authorities(tx), + )) => { + assert_eq!(relay_parent, hash); + tx.send(Ok(AUTHORITIES_WITHOUT_US.clone())).unwrap(); + } + ); + + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ConnectToResolvedValidators { + validator_addrs, + peer_set, + }) => { + assert!(validator_addrs.is_empty()); + assert_eq!(peer_set, PeerSet::Validation); + } + ); + + virtual_overseer + }); +} + #[test] fn test_log_output() { sp_tracing::try_init_simple(); diff --git a/node/primitives/src/disputes/mod.rs b/node/primitives/src/disputes/mod.rs index ec7bb6abc3b7..ee047c7bcc22 100644 --- a/node/primitives/src/disputes/mod.rs +++ b/node/primitives/src/disputes/mod.rs @@ -30,6 +30,8 @@ use polkadot_primitives::v2::{ /// `DisputeMessage` and related types. mod message; pub use message::{DisputeMessage, Error as DisputeMessageCheckError, UncheckedDisputeMessage}; +mod status; +pub use status::{dispute_is_inactive, DisputeStatus, Timestamp, ACTIVE_DURATION_SECS}; /// A checked dispute statement from an associated validator. #[derive(Debug, Clone)] diff --git a/node/primitives/src/disputes/status.rs b/node/primitives/src/disputes/status.rs new file mode 100644 index 000000000000..44aed9b78e20 --- /dev/null +++ b/node/primitives/src/disputes/status.rs @@ -0,0 +1,125 @@ +// Copyright 2017-2022 Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Polkadot is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see . + +use parity_scale_codec::{Decode, Encode}; + +/// Timestamp based on the 1 Jan 1970 UNIX base, which is persistent across node restarts and OS reboots. +pub type Timestamp = u64; + +/// The status of dispute. This is a state machine which can be altered by the +/// helper methods. +#[derive(Debug, Clone, Copy, Encode, Decode, PartialEq)] +pub enum DisputeStatus { + /// The dispute is active and unconcluded. + #[codec(index = 0)] + Active, + /// The dispute has been concluded in favor of the candidate + /// since the given timestamp. + #[codec(index = 1)] + ConcludedFor(Timestamp), + /// The dispute has been concluded against the candidate + /// since the given timestamp. + /// + /// This takes precedence over `ConcludedFor` in the case that + /// both are true, which is impossible unless a large amount of + /// validators are participating on both sides. + #[codec(index = 2)] + ConcludedAgainst(Timestamp), + /// Dispute has been confirmed (more than `byzantine_threshold` have already participated/ or + /// we have seen the candidate included already/participated successfully ourselves). + #[codec(index = 3)] + Confirmed, +} + +impl DisputeStatus { + /// Initialize the status to the active state. + pub fn active() -> DisputeStatus { + DisputeStatus::Active + } + + /// Move status to confirmed status, if not yet concluded/confirmed already. + pub fn confirm(self) -> DisputeStatus { + match self { + DisputeStatus::Active => DisputeStatus::Confirmed, + DisputeStatus::Confirmed => DisputeStatus::Confirmed, + DisputeStatus::ConcludedFor(_) | DisputeStatus::ConcludedAgainst(_) => self, + } + } + + /// Check whether the dispute is not a spam dispute. + pub fn is_confirmed_concluded(&self) -> bool { + match self { + &DisputeStatus::Confirmed | + &DisputeStatus::ConcludedFor(_) | + DisputeStatus::ConcludedAgainst(_) => true, + &DisputeStatus::Active => false, + } + } + + /// Transition the status to a new status after observing the dispute has concluded for the candidate. + /// This may be a no-op if the status was already concluded. + pub fn concluded_for(self, now: Timestamp) -> DisputeStatus { + match self { + DisputeStatus::Active | DisputeStatus::Confirmed => DisputeStatus::ConcludedFor(now), + DisputeStatus::ConcludedFor(at) => DisputeStatus::ConcludedFor(std::cmp::min(at, now)), + against => against, + } + } + + /// Transition the status to a new status after observing the dispute has concluded against the candidate. + /// This may be a no-op if the status was already concluded. + pub fn concluded_against(self, now: Timestamp) -> DisputeStatus { + match self { + DisputeStatus::Active | DisputeStatus::Confirmed => + DisputeStatus::ConcludedAgainst(now), + DisputeStatus::ConcludedFor(at) => + DisputeStatus::ConcludedAgainst(std::cmp::min(at, now)), + DisputeStatus::ConcludedAgainst(at) => + DisputeStatus::ConcludedAgainst(std::cmp::min(at, now)), + } + } + + /// Whether the disputed candidate is possibly invalid. + pub fn is_possibly_invalid(&self) -> bool { + match self { + DisputeStatus::Active | + DisputeStatus::Confirmed | + DisputeStatus::ConcludedAgainst(_) => true, + DisputeStatus::ConcludedFor(_) => false, + } + } + + /// Yields the timestamp this dispute concluded at, if any. + pub fn concluded_at(&self) -> Option { + match self { + DisputeStatus::Active | DisputeStatus::Confirmed => None, + DisputeStatus::ConcludedFor(at) | DisputeStatus::ConcludedAgainst(at) => Some(*at), + } + } +} + +/// The choice here is fairly arbitrary. But any dispute that concluded more than a few minutes ago +/// is not worth considering anymore. Changing this value has little to no bearing on consensus, +/// and really only affects the work that the node might do on startup during periods of many +/// disputes. +pub const ACTIVE_DURATION_SECS: Timestamp = 180; + +/// Returns true if the dispute has concluded for longer than ACTIVE_DURATION_SECS +pub fn dispute_is_inactive(status: &DisputeStatus, now: &Timestamp) -> bool { + let at = status.concluded_at(); + + at.is_some() && at.unwrap() + ACTIVE_DURATION_SECS < *now +} diff --git a/node/primitives/src/lib.rs b/node/primitives/src/lib.rs index cbc2a132fc9a..4551ce9855e3 100644 --- a/node/primitives/src/lib.rs +++ b/node/primitives/src/lib.rs @@ -46,8 +46,9 @@ pub mod approval; /// Disputes related types. pub mod disputes; pub use disputes::{ - CandidateVotes, DisputeMessage, DisputeMessageCheckError, InvalidDisputeVote, - SignedDisputeStatement, UncheckedDisputeMessage, ValidDisputeVote, + dispute_is_inactive, CandidateVotes, DisputeMessage, DisputeMessageCheckError, DisputeStatus, + InvalidDisputeVote, SignedDisputeStatement, Timestamp, UncheckedDisputeMessage, + ValidDisputeVote, ACTIVE_DURATION_SECS, }; // For a 16-ary Merkle Prefix Trie, we can expect at most 16 32-byte hashes per node diff --git a/node/service/Cargo.toml b/node/service/Cargo.toml index 80c424e76e04..a9c9484b6eba 100644 --- a/node/service/Cargo.toml +++ b/node/service/Cargo.toml @@ -204,5 +204,3 @@ runtime-metrics = [ "polkadot-runtime?/runtime-metrics", "polkadot-runtime-parachains/runtime-metrics" ] - -staging-client = ["polkadot-node-core-provisioner/staging-client"] diff --git a/node/subsystem-types/src/messages.rs b/node/subsystem-types/src/messages.rs index 10a5201cc524..c37f773b3839 100644 --- a/node/subsystem-types/src/messages.rs +++ b/node/subsystem-types/src/messages.rs @@ -35,8 +35,8 @@ use polkadot_node_network_protocol::{ use polkadot_node_primitives::{ approval::{BlockApprovalMeta, IndirectAssignmentCert, IndirectSignedApprovalVote}, AvailableData, BabeEpoch, BlockWeight, CandidateVotes, CollationGenerationConfig, - CollationSecondedSignal, DisputeMessage, ErasureChunk, PoV, SignedDisputeStatement, - SignedFullStatement, ValidationResult, + CollationSecondedSignal, DisputeMessage, DisputeStatus, ErasureChunk, PoV, + SignedDisputeStatement, SignedFullStatement, ValidationResult, }; use polkadot_primitives::v2::{ AuthorityDiscoveryId, BackedCandidate, BlockNumber, CandidateEvent, CandidateHash, @@ -271,7 +271,7 @@ pub enum DisputeCoordinatorMessage { /// Fetch a list of all recent disputes the co-ordinator is aware of. /// These are disputes which have occurred any time in recent sessions, /// and which may have already concluded. - RecentDisputes(oneshot::Sender>), + RecentDisputes(oneshot::Sender>), /// Fetch a list of all active disputes that the coordinator is aware of. /// These disputes are either not yet concluded or recently concluded. ActiveDisputes(oneshot::Sender>), @@ -699,10 +699,15 @@ pub enum RuntimeApiRequest { OccupiedCoreAssumption, RuntimeApiSender>, ), - /// Returns all on-chain disputes at given block number. - StagingDisputes( - RuntimeApiSender)>>, - ), + /// Returns all on-chain disputes at given block number. Available in v3. + Disputes(RuntimeApiSender)>>), +} + +impl RuntimeApiRequest { + /// Runtime version requirements for each message + + /// `Disputes` + pub const DISPUTES_RUNTIME_REQUIREMENT: u32 = 3; } /// A message to the Runtime API subsystem. diff --git a/node/subsystem-types/src/runtime_client.rs b/node/subsystem-types/src/runtime_client.rs index 2aa9e2bffb82..259c94fd4e51 100644 --- a/node/subsystem-types/src/runtime_client.rs +++ b/node/subsystem-types/src/runtime_client.rs @@ -186,7 +186,7 @@ pub trait RuntimeApiSubsystemClient { /// Returns all onchain disputes. /// This is a staging method! Do not use on production runtimes! - async fn staging_get_disputes( + async fn disputes( &self, at: Hash, ) -> Result)>, ApiError>; @@ -375,10 +375,10 @@ where self.runtime_api().session_info_before_version_2(&BlockId::Hash(at), index) } - async fn staging_get_disputes( + async fn disputes( &self, at: Hash, ) -> Result)>, ApiError> { - self.runtime_api().staging_get_disputes(&BlockId::Hash(at)) + self.runtime_api().disputes(&BlockId::Hash(at)) } } diff --git a/primitives/src/lib.rs b/primitives/src/lib.rs index 121f7cb40d23..168b5795b040 100644 --- a/primitives/src/lib.rs +++ b/primitives/src/lib.rs @@ -22,9 +22,9 @@ // `v2` is currently the latest stable version of the runtime API. pub mod v2; -// The 'staging' version is special - while other versions are set in stone, -// the staging version is malleable. Once it's released, it gets the next -// version number. +// The 'staging' version is special - it contains primitives which are +// still in development. Once they are considered stable, they will be +// moved to a new versioned module. pub mod vstaging; // `runtime_api` contains the actual API implementation. It contains stable and diff --git a/primitives/src/runtime_api.rs b/primitives/src/runtime_api.rs index 84d2cf0ec4ca..d0d0b7220bb9 100644 --- a/primitives/src/runtime_api.rs +++ b/primitives/src/runtime_api.rs @@ -18,31 +18,97 @@ //! of the Runtime API exposed from the Runtime to the Host. //! //! The functions in trait ParachainHost` can be part of the stable API -//! (which is versioned) or they can be staging (aka unstable functions). +//! (which is versioned) or they can be staging (aka unstable/testing +//! functions). //! -//! All stable API functions should use primitives from the latest version. -//! In the time of writing of this document - this is v2. So for example: -//! ```ignore -//! fn validators() -> Vec; -//! ``` -//! indicates a function from the stable v2 API. +//! The separation outlined above is achieved with the versioned api feature +//! of `decl_runtime_apis!` and `impl_runtime_apis!`. Before moving on let's +//! see a quick example about how api versioning works. //! -//! On the other hand a staging function's name should be prefixed with -//! `staging_` like this: -//! ```ignore -//! fn staging_get_disputes() -> Vec<(vstaging::SessionIndex, vstaging::CandidateHash, vstaging::DisputeState)>; +//! # Runtime api versioning crash course +//! +//! The versioning is achieved with the `api_version` attribute. It can be +//! placed on: +//! * trait declaration - represents the base version of the api. +//! * method declaration (inside a trait declaration) - represents a versioned +//! method, which is not available in the base version. +//! * trait implementation - represents which version of the api is being +//! implemented. +//! +//! Let's see a quick example: +//! +//! ```rust(ignore) +//! sp_api::decl_runtime_apis! { +//! #[api_version(2)] +//! pub trait MyApi { +//! fn fn1(); +//! fn fn2(); +//! #[api_version(3)] +//! fn fn3(); +//! #[api_version(4)] +//! fn fn4(); +//! } +//! } +//! +//! struct Runtime {} +//! +//! sp_api::impl_runtime_apis! { +//! #[api_version(3)] +//! impl self::MyApi for Runtime { +//! fn fn1() {} +//! fn fn2() {} +//! fn fn3() {} +//! } +//! } //! ``` +//! A new api named `MyApi` is declared with `decl_runtime_apis!`. The trait declaration +//! has got an `api_version` attribute which represents its base version - 2 in this case. +//! +//! The api has got three methods - `fn1`, `fn2`, `fn3` and `fn4`. `fn3` and `fn4` has got +//! an `api_version` attribute which makes them versioned methods. These methods do not exist +//! in the base version of the api. Behind the scenes the declaration above creates three +//! runtime apis: +//! * MyApiV2 with `fn1` and `fn2` +//! * MyApiV3 with `fn1`, `fn2` and `fn3`. +//! * MyApiV4 with `fn1`, `fn2`, `fn3` and `fn4`. //! -//! How a staging function becomes stable? +//! Please note that v4 contains all methods from v3, v3 all methods from v2 and so on. //! -//! Once a staging function is ready to be versioned the `renamed` macro -//! should be used to rename it and version it. For the example above: +//! Back to our example. At the end runtime api is implemented for `struct Runtime` with +//! `impl_runtime_apis` macro. `api_version` attribute is attached to the impl block which +//! means that a version different from the base one is being implemented - in our case this +//! is v3. +//! +//! This version of the api contains three methods so the `impl` block has got definitions +//! for them. Note that `fn4` is not implemented as it is not part of this version of the api. +//! `impl_runtime_apis` generates a default implementation for it calling `unimplemented!()`. +//! +//! Hopefully this should be all you need to know in order to use versioned methods in the node. +//! For more details about how the api versioning works refer to `spi_api` +//! documentation [here](https://docs.substrate.io/rustdocs/latest/sp_api/macro.decl_runtime_apis.html). +//! +//! # How versioned methods are used for `ParachainHost` +//! +//! Let's introduce two types of `ParachainHost` api implementation: +//! * stable - used on stable production networks like Polkadot and Kusama. There is only one +//! stable api at a single point in time. +//! * staging - used on test networks like Westend or Rococo. Depending on the development needs +//! there can be zero, one or multiple staging apis. +//! +//! The stable version of `ParachainHost` is indicated by the base version of the api. Any staging +//! method must use `api_version` attribute so that it is assigned to a specific version of a +//! staging api. This way in a single declaration one can see what's the stable version of +//! `ParachainHost` and what staging versions/functions are available. +//! +//! All stable api functions should use primitives from the latest version. +//! In the time of writing of this document - this is v2. So for example: //! ```ignore -//! #[renamed("staging_get_session_disputes", 3)] -//! fn get_session_disputes() -> Vec<(v3::SessionIndex, v3::CandidateHash, v3::DisputeState)>; +//! fn validators() -> Vec; //! ``` -//! For more details about how the API versioning works refer to `spi_api` -//! documentation [here](https://docs.substrate.io/rustdocs/latest/sp_api/macro.decl_runtime_apis.html). +//! indicates a function from the stable v2 API. +//! +//! All staging api functions should use primitives from vstaging. They should be clearly separated +//! from the stable primitives. use crate::v2; use parity_scale_codec::{Decode, Encode}; @@ -153,7 +219,7 @@ sp_api::decl_runtime_apis! { /***** STAGING *****/ /// Returns all onchain disputes. - /// This is a staging method! Do not use on production runtimes! - fn staging_get_disputes() -> Vec<(v2::SessionIndex, v2::CandidateHash, v2::DisputeState)>; + #[api_version(3)] + fn disputes() -> Vec<(v2::SessionIndex, v2::CandidateHash, v2::DisputeState)>; } } diff --git a/primitives/src/vstaging/mod.rs b/primitives/src/vstaging/mod.rs index 2f29ffbe60b7..64671bd48a60 100644 --- a/primitives/src/vstaging/mod.rs +++ b/primitives/src/vstaging/mod.rs @@ -16,4 +16,4 @@ //! Staging Primitives. -// Put any primitives used by staging API functions here +// Put any primitives used by staging APIs functions here diff --git a/primitives/test-helpers/Cargo.toml b/primitives/test-helpers/Cargo.toml index ed086c87cfc9..bbd6f45a45bd 100644 --- a/primitives/test-helpers/Cargo.toml +++ b/primitives/test-helpers/Cargo.toml @@ -8,5 +8,6 @@ edition = "2021" sp-keyring = { git = "https://github.com/paritytech/substrate", branch = "master" } sp-application-crypto = { package = "sp-application-crypto", git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "master" } +sp-core = { git = "https://github.com/paritytech/substrate", branch = "master", features = ["std"] } polkadot-primitives = { path = "../" } rand = "0.8.5" diff --git a/primitives/test-helpers/src/lib.rs b/primitives/test-helpers/src/lib.rs index 02ba009b13cc..8873d69cdb2f 100644 --- a/primitives/test-helpers/src/lib.rs +++ b/primitives/test-helpers/src/lib.rs @@ -255,3 +255,7 @@ impl rand::RngCore for AlwaysZeroRng { Ok(()) } } + +pub fn dummy_signature() -> polkadot_primitives::v2::ValidatorSignature { + sp_core::crypto::UncheckedFrom::unchecked_from([1u8; 64]) +} diff --git a/runtime/kusama/src/lib.rs b/runtime/kusama/src/lib.rs index 352ccb2855e3..b5b1d45cffd4 100644 --- a/runtime/kusama/src/lib.rs +++ b/runtime/kusama/src/lib.rs @@ -22,11 +22,10 @@ use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use primitives::v2::{ - AccountId, AccountIndex, Balance, BlockNumber, CandidateEvent, CandidateHash, - CommittedCandidateReceipt, CoreState, DisputeState, GroupRotationInfo, Hash, Id as ParaId, - InboundDownwardMessage, InboundHrmpMessage, Moment, Nonce, OccupiedCoreAssumption, - PersistedValidationData, ScrapedOnChainVotes, SessionInfo, Signature, ValidationCode, - ValidationCodeHash, ValidatorId, ValidatorIndex, + AccountId, AccountIndex, Balance, BlockNumber, CandidateEvent, CommittedCandidateReceipt, + CoreState, GroupRotationInfo, Hash, Id as ParaId, InboundDownwardMessage, InboundHrmpMessage, + Moment, Nonce, OccupiedCoreAssumption, PersistedValidationData, ScrapedOnChainVotes, + SessionInfo, Signature, ValidationCode, ValidationCodeHash, ValidatorId, ValidatorIndex, }; use runtime_common::{ auctions, claims, crowdloan, impl_runtime_weights, impls::DealWithFees, paras_registrar, @@ -1175,7 +1174,7 @@ impl parachains_initializer::Config for Runtime { impl parachains_disputes::Config for Runtime { type RuntimeEvent = RuntimeEvent; type RewardValidators = parachains_reward_points::RewardValidatorsWithEraPoints; - type PunishValidators = (); + type SlashingHandler = (); type WeightInfo = weights::runtime_parachains_disputes::WeightInfo; } @@ -1684,10 +1683,6 @@ sp_api::impl_runtime_apis! { { parachains_runtime_api_impl::validation_code_hash::(para_id, assumption) } - - fn staging_get_disputes() -> Vec<(SessionIndex, CandidateHash, DisputeState)> { - unimplemented!() - } } impl beefy_primitives::BeefyApi for Runtime { diff --git a/runtime/parachains/Cargo.toml b/runtime/parachains/Cargo.toml index 1feeb3540b1c..3f34566013e6 100644 --- a/runtime/parachains/Cargo.toml +++ b/runtime/parachains/Cargo.toml @@ -95,6 +95,7 @@ runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", + "pallet-staking/runtime-benchmarks", "primitives/runtime-benchmarks", "static_assertions", "sp-application-crypto", @@ -109,4 +110,3 @@ try-runtime = [ "pallet-vesting/try-runtime", ] runtime-metrics = ["sp-tracing/with-tracing", "polkadot-runtime-metrics/runtime-metrics"] -vstaging = [] diff --git a/runtime/parachains/src/disputes.rs b/runtime/parachains/src/disputes.rs index a6fc7e75aa22..a26eccad0daf 100644 --- a/runtime/parachains/src/disputes.rs +++ b/runtime/parachains/src/disputes.rs @@ -39,6 +39,7 @@ use sp_std::{cmp::Ordering, prelude::*}; #[allow(unused_imports)] pub(crate) use self::tests::run_to_block; +pub mod slashing; #[cfg(test)] mod tests; @@ -73,35 +74,55 @@ impl RewardValidators for () { } /// Punishment hooks for disputes. -pub trait PunishValidators { - /// Punish a series of validators who were for an invalid parablock. This is expected to be a major - /// punishment. +pub trait SlashingHandler { + /// Punish a series of validators who were for an invalid parablock. This is + /// expected to be a major punishment. fn punish_for_invalid( session: SessionIndex, - validators: impl IntoIterator, + candidate_hash: CandidateHash, + losers: impl IntoIterator, ); - /// Punish a series of validators who were against a valid parablock. This is expected to be a minor - /// punishment. + /// Punish a series of validators who were against a valid parablock. This + /// is expected to be a minor punishment. fn punish_against_valid( session: SessionIndex, - validators: impl IntoIterator, + candidate_hash: CandidateHash, + losers: impl IntoIterator, ); - /// Punish a series of validators who were part of a dispute which never concluded. This is expected - /// to be a minor punishment. - fn punish_inconclusive( - session: SessionIndex, - validators: impl IntoIterator, - ); + /// Called by the initializer to initialize the slashing pallet. + fn initializer_initialize(now: BlockNumber) -> Weight; + + /// Called by the initializer to finalize the slashing pallet. + fn initializer_finalize(); + + /// Called by the initializer to note that a new session has started. + fn initializer_on_new_session(session_index: SessionIndex); } -impl PunishValidators for () { - fn punish_for_invalid(_: SessionIndex, _: impl IntoIterator) {} +impl SlashingHandler for () { + fn punish_for_invalid( + _: SessionIndex, + _: CandidateHash, + _: impl IntoIterator, + ) { + } + + fn punish_against_valid( + _: SessionIndex, + _: CandidateHash, + _: impl IntoIterator, + ) { + } - fn punish_against_valid(_: SessionIndex, _: impl IntoIterator) {} + fn initializer_initialize(_now: BlockNumber) -> Weight { + Weight::zero() + } + + fn initializer_finalize() {} - fn punish_inconclusive(_: SessionIndex, _: impl IntoIterator) {} + fn initializer_on_new_session(_: SessionIndex) {} } /// Binary discriminator to determine if the expensive signature @@ -412,7 +433,7 @@ pub mod pallet { pub trait Config: frame_system::Config + configuration::Config + session_info::Config { type RuntimeEvent: From> + IsType<::RuntimeEvent>; type RewardValidators: RewardValidators; - type PunishValidators: PunishValidators; + type SlashingHandler: SlashingHandler; /// Weight information for extrinsics in this pallet. type WeightInfo: WeightInfo; @@ -831,14 +852,7 @@ impl Pallet { // it would be unexpected for any change here to occur when the dispute has not concluded // in time, as a dispute guaranteed to have at least one honest participant should // conclude quickly. - let participating = decrement_spam(spam_slots, &dispute); - - // Slight punishment as these validators have failed to make data available to - // others in a timely manner. - T::PunishValidators::punish_inconclusive( - session_index, - participating.iter_ones().map(|i| ValidatorIndex(i as _)), - ); + let _participating = decrement_spam(spam_slots, &dispute); }); weight += T::DbWeight::get().reads_writes(2, 2); @@ -1187,7 +1201,9 @@ impl Pallet { Error::::SingleSidedDispute, ); - let DisputeStatementSet { session, candidate_hash, .. } = set.clone(); + let DisputeStatementSet { ref session, ref candidate_hash, .. } = set; + let session = *session; + let candidate_hash = *candidate_hash; // we can omit spam slot checks, `fn filter_disputes_data` is // always called before calling this `fn`. @@ -1227,10 +1243,14 @@ impl Pallet { // Slash participants on a losing side. { // a valid candidate, according to 2/3. Punish those on the 'against' side. - T::PunishValidators::punish_against_valid(session, summary.slash_against); + T::SlashingHandler::punish_against_valid( + session, + candidate_hash, + summary.slash_against, + ); // an invalid candidate, according to 2/3. Punish those on the 'for' side. - T::PunishValidators::punish_for_invalid(session, summary.slash_for); + T::SlashingHandler::punish_for_invalid(session, candidate_hash, summary.slash_for); } >::insert(&session, &candidate_hash, &summary.state); diff --git a/runtime/parachains/src/disputes/slashing.rs b/runtime/parachains/src/disputes/slashing.rs new file mode 100644 index 000000000000..fd6708de6ba0 --- /dev/null +++ b/runtime/parachains/src/disputes/slashing.rs @@ -0,0 +1,739 @@ +// Copyright 2022 Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Polkadot is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see . + +//! Dispute slashing pallet. +//! +//! Once a dispute is concluded, we want to slash validators +//! who were on the wrong side of the dispute. The slashing amount +//! depends on whether the candidate was valid (small) or invalid (big). +//! In addition to that, we might want to kick out the validators from the +//! active set. +//! +//! The `offences` pallet from Substrate provides us with a way to do both. +//! Currently, the interface expects us to provide staking information +//! including nominator exposure in order to submit an offence. +//! +//! Normally, we'd able to fetch this information from the runtime as soon as +//! the dispute is concluded. This is also what `im-online` pallet does. +//! However, since a dispute can conclude several sessions after the candidate +//! was backed (see `dispute_period` in `HostConfiguration`), we can't rely on +//! this information be available in the context of the current block. The +//! `babe` and `grandpa` equivocation handlers also have to deal +//! with this problem. +//! +//! Our implementation looks like a hybrid of `im-online` and `grandpa` +//! equivocation handlers. Meaning, we submit an `offence` for the concluded +//! disputes about the current session candidate directly from the runtime. +//! If, however, the dispute is about a past session, we record unapplied +//! slashes on chain, without `FullIdentification` of the offenders. +//! Later on, a block producer can submit an unsigned transaction with +//! `KeyOwnershipProof` of an offender and submit it to the runtime +//! to produce an offence. + +use crate::{disputes, initializer::ValidatorSetCount, session_info::IdentificationTuple}; +use frame_support::{ + dispatch::Pays, + traits::{Defensive, Get, KeyOwnerProofSystem, ValidatorSet, ValidatorSetWithIdentification}, + weights::Weight, +}; + +use parity_scale_codec::{Decode, Encode}; +use primitives::v2::{CandidateHash, SessionIndex, ValidatorId, ValidatorIndex}; +use scale_info::TypeInfo; +use sp_runtime::{ + traits::Convert, + transaction_validity::{ + InvalidTransaction, TransactionPriority, TransactionSource, TransactionValidity, + TransactionValidityError, ValidTransaction, + }, + DispatchResult, KeyTypeId, Perbill, RuntimeDebug, +}; +use sp_session::{GetSessionNumber, GetValidatorCount}; +use sp_staking::offence::{DisableStrategy, Kind, Offence, OffenceError, ReportOffence}; +use sp_std::{ + collections::btree_map::{BTreeMap, Entry}, + prelude::*, +}; + +const LOG_TARGET: &str = "runtime::parachains::slashing"; + +// These are constants, but we want to make them configurable +// via `HostConfiguration` in the future. +const SLASH_FOR_INVALID: Perbill = Perbill::from_percent(100); +const SLASH_AGAINST_VALID: Perbill = Perbill::from_perthousand(1); +const DEFENSIVE_PROOF: &'static str = "disputes module should bail on old session"; + +#[cfg(feature = "runtime-benchmarks")] +pub mod benchmarking; + +/// The benchmarking configuration. +pub trait BenchmarkingConfiguration { + const MAX_VALIDATORS: u32; +} + +pub struct BenchConfig; + +impl BenchmarkingConfiguration for BenchConfig { + const MAX_VALIDATORS: u32 = M; +} + +/// Timeslots should uniquely identify offences and are used for the offence +/// deduplication. +#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Encode, Decode, TypeInfo, RuntimeDebug)] +pub struct DisputesTimeSlot { + // The order of these matters for `derive(Ord)`. + session_index: SessionIndex, + candidate_hash: CandidateHash, +} + +impl DisputesTimeSlot { + pub fn new(session_index: SessionIndex, candidate_hash: CandidateHash) -> Self { + Self { session_index, candidate_hash } + } +} + +/// An offence that is filed when a series of validators lost a dispute. +#[derive(RuntimeDebug, TypeInfo)] +#[cfg_attr(feature = "std", derive(Clone, PartialEq, Eq))] +pub struct SlashingOffence { + /// The size of the validator set in that session. + pub validator_set_count: ValidatorSetCount, + /// Should be unique per dispute. + pub time_slot: DisputesTimeSlot, + /// Staking information about the validators that lost the dispute + /// needed for slashing. + pub offenders: Vec, + /// What fraction of the total exposure that should be slashed for + /// this offence. + pub slash_fraction: Perbill, + /// Whether the candidate was valid or invalid. + pub kind: SlashingOffenceKind, +} + +impl Offence for SlashingOffence +where + Offender: Clone, +{ + const ID: Kind = *b"disputes:slashin"; + + type TimeSlot = DisputesTimeSlot; + + fn offenders(&self) -> Vec { + self.offenders.clone() + } + + fn session_index(&self) -> SessionIndex { + self.time_slot.session_index + } + + fn validator_set_count(&self) -> ValidatorSetCount { + self.validator_set_count + } + + fn time_slot(&self) -> Self::TimeSlot { + self.time_slot.clone() + } + + fn disable_strategy(&self) -> DisableStrategy { + match self.kind { + SlashingOffenceKind::ForInvalid => DisableStrategy::Always, + // in the future we might change it based on number of disputes initiated: + // + SlashingOffenceKind::AgainstValid => DisableStrategy::Never, + } + } + + fn slash_fraction(&self, _offenders: u32) -> Perbill { + self.slash_fraction + } +} + +impl SlashingOffence { + fn new( + session_index: SessionIndex, + candidate_hash: CandidateHash, + validator_set_count: ValidatorSetCount, + offenders: Vec, + kind: SlashingOffenceKind, + ) -> Self { + let time_slot = DisputesTimeSlot::new(session_index, candidate_hash); + let slash_fraction = match kind { + SlashingOffenceKind::ForInvalid => SLASH_FOR_INVALID, + SlashingOffenceKind::AgainstValid => SLASH_AGAINST_VALID, + }; + Self { time_slot, validator_set_count, offenders, slash_fraction, kind } + } +} + +/// This type implements `SlashingHandler`. +pub struct SlashValidatorsForDisputes { + _phantom: sp_std::marker::PhantomData, +} + +impl Default for SlashValidatorsForDisputes { + fn default() -> Self { + Self { _phantom: Default::default() } + } +} + +impl SlashValidatorsForDisputes> +where + T: Config>, +{ + /// If in the current session, returns the identified validators. `None` + /// otherwise. + fn maybe_identify_validators( + session_index: SessionIndex, + validators: impl IntoIterator, + ) -> Option>> { + // We use `ValidatorSet::session_index` and not + // `shared::Pallet::session_index()` because at the first block of a new era, + // the `IdentificationOf` of a validator in the previous session might be + // missing, while `shared` pallet would return the same session index as being + // updated at the end of the block. + let current_session = T::ValidatorSet::session_index(); + if session_index == current_session { + let account_keys = crate::session_info::Pallet::::account_keys(session_index); + let account_ids = account_keys.defensive_unwrap_or_default(); + + let fully_identified = validators + .into_iter() + .flat_map(|i| account_ids.get(i.0 as usize).cloned()) + .filter_map(|id| { + >::IdentificationOf::convert( + id.clone() + ).map(|full_id| (id, full_id)) + }) + .collect::>>(); + return Some(fully_identified) + } + None + } + + fn do_punish( + session_index: SessionIndex, + candidate_hash: CandidateHash, + kind: SlashingOffenceKind, + losers: impl IntoIterator, + ) { + let losers: Vec = losers.into_iter().collect(); + if losers.is_empty() { + // Nothing to do + return + } + let session_info = crate::session_info::Pallet::::session_info(session_index); + let session_info = match session_info.defensive_proof(DEFENSIVE_PROOF) { + Some(info) => info, + None => return, + }; + let maybe = Self::maybe_identify_validators(session_index, losers.iter().cloned()); + if let Some(offenders) = maybe { + let validator_set_count = session_info.discovery_keys.len() as ValidatorSetCount; + let offence = SlashingOffence::new( + session_index, + candidate_hash, + validator_set_count, + offenders, + kind, + ); + // This is the first time we report an offence for this dispute, + // so it is not a duplicate. + let _ = T::HandleReports::report_offence(offence); + return + } + + let keys = losers + .into_iter() + .filter_map(|i| session_info.validators.get(i.0 as usize).cloned().map(|id| (i, id))) + .collect(); + let unapplied = PendingSlashes { keys, kind }; + >::insert(session_index, candidate_hash, unapplied); + } +} + +impl disputes::SlashingHandler for SlashValidatorsForDisputes> +where + T: Config>, +{ + fn punish_for_invalid( + session_index: SessionIndex, + candidate_hash: CandidateHash, + losers: impl IntoIterator, + ) { + let kind = SlashingOffenceKind::ForInvalid; + Self::do_punish(session_index, candidate_hash, kind, losers); + } + + fn punish_against_valid( + session_index: SessionIndex, + candidate_hash: CandidateHash, + losers: impl IntoIterator, + ) { + let kind = SlashingOffenceKind::AgainstValid; + Self::do_punish(session_index, candidate_hash, kind, losers); + } + + fn initializer_initialize(now: T::BlockNumber) -> Weight { + Pallet::::initializer_initialize(now) + } + + fn initializer_finalize() { + Pallet::::initializer_finalize() + } + + fn initializer_on_new_session(session_index: SessionIndex) { + Pallet::::initializer_on_new_session(session_index) + } +} + +#[derive(PartialEq, Eq, Clone, Copy, Encode, Decode, RuntimeDebug, TypeInfo)] +pub enum SlashingOffenceKind { + #[codec(index = 0)] + ForInvalid, + #[codec(index = 1)] + AgainstValid, +} + +/// We store most of the information about a lost dispute on chain. This struct +/// is required to identify and verify it. +#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)] +pub struct DisputeProof { + /// Time slot when the dispute occured. + pub time_slot: DisputesTimeSlot, + /// The dispute outcome. + pub kind: SlashingOffenceKind, + /// The index of the validator who lost a dispute. + pub validator_index: ValidatorIndex, + /// The parachain session key of the validator. + pub validator_id: ValidatorId, +} + +/// Slashes that are waiting to be applied once we have validator key +/// identification. +#[derive(Encode, Decode, RuntimeDebug, TypeInfo)] +pub struct PendingSlashes { + /// Indices and keys of the validators who lost a dispute and are pending + /// slashes. + pub keys: BTreeMap, + /// The dispute outcome. + pub kind: SlashingOffenceKind, +} + +/// A trait that defines methods to report an offence (after the slashing report +/// has been validated) and for submitting a transaction to report a slash (from +/// an offchain context). +pub trait HandleReports { + /// The longevity, in blocks, that the offence report is valid for. When + /// using the staking pallet this should be equal to the bonding duration + /// (in blocks, not eras). + type ReportLongevity: Get; + + /// Report an offence. + fn report_offence( + offence: SlashingOffence, + ) -> Result<(), OffenceError>; + + /// Returns true if the offenders at the given time slot has already been + /// reported. + fn is_known_offence( + offenders: &[T::KeyOwnerIdentification], + time_slot: &DisputesTimeSlot, + ) -> bool; + + /// Create and dispatch a slashing report extrinsic. + /// This should be called offchain. + fn submit_unsigned_slashing_report( + dispute_proof: DisputeProof, + key_owner_proof: T::KeyOwnerProof, + ) -> DispatchResult; +} + +impl HandleReports for () { + type ReportLongevity = (); + + fn report_offence( + _offence: SlashingOffence, + ) -> Result<(), OffenceError> { + Ok(()) + } + + fn is_known_offence( + _offenders: &[T::KeyOwnerIdentification], + _time_slot: &DisputesTimeSlot, + ) -> bool { + true + } + + fn submit_unsigned_slashing_report( + _dispute_proof: DisputeProof, + _key_owner_proof: T::KeyOwnerProof, + ) -> DispatchResult { + Ok(()) + } +} + +pub trait WeightInfo { + fn report_dispute_lost(validator_count: ValidatorSetCount) -> Weight; +} + +pub struct TestWeightInfo; +impl WeightInfo for TestWeightInfo { + fn report_dispute_lost(_validator_count: ValidatorSetCount) -> Weight { + Weight::zero() + } +} + +pub use pallet::*; +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + #[pallet::config] + pub trait Config: frame_system::Config + crate::disputes::Config { + /// The proof of key ownership, used for validating slashing reports. + /// The proof must include the session index and validator count of the + /// session at which the offence occurred. + type KeyOwnerProof: Parameter + GetSessionNumber + GetValidatorCount; + + /// The identification of a key owner, used when reporting slashes. + type KeyOwnerIdentification: Parameter; + + /// A system for proving ownership of keys, i.e. that a given key was + /// part of a validator set, needed for validating slashing reports. + type KeyOwnerProofSystem: KeyOwnerProofSystem< + (KeyTypeId, ValidatorId), + Proof = Self::KeyOwnerProof, + IdentificationTuple = Self::KeyOwnerIdentification, + >; + + /// The slashing report handling subsystem, defines methods to report an + /// offence (after the slashing report has been validated) and for + /// submitting a transaction to report a slash (from an offchain + /// context). NOTE: when enabling slashing report handling (i.e. this + /// type isn't set to `()`) you must use this pallet's + /// `ValidateUnsigned` in the runtime definition. + type HandleReports: HandleReports; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + + /// Benchmarking configuration. + type BenchmarkingConfig: BenchmarkingConfiguration; + } + + #[pallet::pallet] + #[pallet::without_storage_info] + pub struct Pallet(_); + + /// Validators pending dispute slashes. + #[pallet::storage] + pub(super) type UnappliedSlashes = StorageDoubleMap< + _, + Twox64Concat, + SessionIndex, + Blake2_128Concat, + CandidateHash, + PendingSlashes, + >; + + /// `ValidatorSetCount` per session. + #[pallet::storage] + pub(super) type ValidatorSetCounts = + StorageMap<_, Twox64Concat, SessionIndex, ValidatorSetCount>; + + #[pallet::error] + pub enum Error { + /// The key ownership proof is invalid. + InvalidKeyOwnershipProof, + /// The session index is too old or invalid. + InvalidSessionIndex, + /// The candidate hash is invalid. + InvalidCandidateHash, + /// There is no pending slash for the given validator index and time + /// slot. + InvalidValidatorIndex, + /// The validator index does not match the validator id. + ValidatorIndexIdMismatch, + /// The given slashing report is valid but already previously reported. + DuplicateSlashingReport, + } + + #[pallet::call] + impl Pallet { + #[pallet::weight(::WeightInfo::report_dispute_lost( + key_owner_proof.validator_count() + ))] + pub fn report_dispute_lost_unsigned( + origin: OriginFor, + // box to decrease the size of the call + dispute_proof: Box, + key_owner_proof: T::KeyOwnerProof, + ) -> DispatchResultWithPostInfo { + ensure_none(origin)?; + + // check the membership proof to extract the offender's id + let key = (primitives::v2::PARACHAIN_KEY_TYPE_ID, dispute_proof.validator_id.clone()); + let offender = T::KeyOwnerProofSystem::check_proof(key, key_owner_proof) + .ok_or(Error::::InvalidKeyOwnershipProof)?; + + let session_index = dispute_proof.time_slot.session_index; + let validator_set_count = crate::session_info::Pallet::::session_info(session_index) + .ok_or(Error::::InvalidSessionIndex)? + .discovery_keys + .len() as ValidatorSetCount; + + // check that there is a pending slash for the given + // validator index and candidate hash + let candidate_hash = dispute_proof.time_slot.candidate_hash; + let try_remove = |v: &mut Option| -> Result<(), DispatchError> { + let pending = v.as_mut().ok_or(Error::::InvalidCandidateHash)?; + if pending.kind != dispute_proof.kind { + return Err(Error::::InvalidCandidateHash.into()) + } + + match pending.keys.entry(dispute_proof.validator_index) { + Entry::Vacant(_) => return Err(Error::::InvalidValidatorIndex.into()), + // check that `validator_index` matches `validator_id` + Entry::Occupied(e) if e.get() != &dispute_proof.validator_id => + return Err(Error::::ValidatorIndexIdMismatch.into()), + Entry::Occupied(e) => { + e.remove(); // the report is correct + }, + } + + // if the last validator is slashed for this dispute, clean up the storage + if pending.keys.is_empty() { + *v = None; + } + + Ok(()) + }; + + >::try_mutate_exists(&session_index, &candidate_hash, try_remove)?; + + let offence = SlashingOffence::new( + session_index, + candidate_hash, + validator_set_count, + vec![offender], + dispute_proof.kind, + ); + + >::report_offence(offence) + .map_err(|_| Error::::DuplicateSlashingReport)?; + + Ok(Pays::No.into()) + } + } + + #[pallet::validate_unsigned] + impl ValidateUnsigned for Pallet { + type Call = Call; + fn validate_unsigned(source: TransactionSource, call: &Self::Call) -> TransactionValidity { + Self::validate_unsigned(source, call) + } + + fn pre_dispatch(call: &Self::Call) -> Result<(), TransactionValidityError> { + Self::pre_dispatch(call) + } + } +} + +impl Pallet { + /// Called by the initializer to initialize the disputes slashing module. + fn initializer_initialize(_now: T::BlockNumber) -> Weight { + Weight::zero() + } + + /// Called by the initializer to finalize the disputes slashing pallet. + fn initializer_finalize() {} + + /// Called by the initializer to note a new session in the disputes slashing + /// pallet. + fn initializer_on_new_session(session_index: SessionIndex) { + // This should be small, as disputes are limited by spam slots, so no limit is + // fine. + const REMOVE_LIMIT: u32 = u32::MAX; + + let config = >::config(); + if session_index <= config.dispute_period + 1 { + return + } + + let old_session = session_index - config.dispute_period - 1; + let _ = >::clear_prefix(old_session, REMOVE_LIMIT, None); + } +} + +/// Methods for the `ValidateUnsigned` implementation: +/// +/// It restricts calls to `report_dispute_lost_unsigned` to local calls (i.e. +/// extrinsics generated on this node) or that already in a block. This +/// guarantees that only block authors can include unsigned slashing reports. +impl Pallet { + pub fn validate_unsigned(source: TransactionSource, call: &Call) -> TransactionValidity { + if let Call::report_dispute_lost_unsigned { dispute_proof, key_owner_proof } = call { + // discard slashing report not coming from the local node + match source { + TransactionSource::Local | TransactionSource::InBlock => { /* allowed */ }, + _ => { + log::warn!( + target: LOG_TARGET, + "rejecting unsigned transaction because it is not local/in-block." + ); + + return InvalidTransaction::Call.into() + }, + } + + // check report staleness + is_known_offence::(dispute_proof, key_owner_proof)?; + + let longevity = >::ReportLongevity::get(); + + let tag_prefix = match dispute_proof.kind { + SlashingOffenceKind::ForInvalid => "DisputeForInvalid", + SlashingOffenceKind::AgainstValid => "DisputeAgainstValid", + }; + + ValidTransaction::with_tag_prefix(tag_prefix) + // We assign the maximum priority for any report. + .priority(TransactionPriority::max_value()) + // Only one report for the same offender at the same slot. + .and_provides((dispute_proof.time_slot.clone(), dispute_proof.validator_id.clone())) + .longevity(longevity) + // We don't propagate this. This can never be included on a remote node. + .propagate(false) + .build() + } else { + InvalidTransaction::Call.into() + } + } + + pub fn pre_dispatch(call: &Call) -> Result<(), TransactionValidityError> { + if let Call::report_dispute_lost_unsigned { dispute_proof, key_owner_proof } = call { + is_known_offence::(dispute_proof, key_owner_proof) + } else { + Err(InvalidTransaction::Call.into()) + } + } +} + +fn is_known_offence( + dispute_proof: &DisputeProof, + key_owner_proof: &T::KeyOwnerProof, +) -> Result<(), TransactionValidityError> { + // check the membership proof to extract the offender's id + let key = (primitives::v2::PARACHAIN_KEY_TYPE_ID, dispute_proof.validator_id.clone()); + + let offender = T::KeyOwnerProofSystem::check_proof(key, key_owner_proof.clone()) + .ok_or(InvalidTransaction::BadProof)?; + + // check if the offence has already been reported, + // and if so then we can discard the report. + let is_known_offence = >::is_known_offence( + &[offender], + &dispute_proof.time_slot, + ); + + if is_known_offence { + Err(InvalidTransaction::Stale.into()) + } else { + Ok(()) + } +} + +/// Actual `HandleReports` implemention. +/// +/// When configured properly, should be instantiated with +/// `T::KeyOwnerIdentification, Offences, ReportLongevity` parameters. +pub struct SlashingReportHandler { + _phantom: sp_std::marker::PhantomData<(I, R, L)>, +} + +impl Default for SlashingReportHandler { + fn default() -> Self { + Self { _phantom: Default::default() } + } +} + +impl HandleReports for SlashingReportHandler +where + T: Config + frame_system::offchain::SendTransactionTypes>, + R: ReportOffence< + T::AccountId, + T::KeyOwnerIdentification, + SlashingOffence, + >, + L: Get, +{ + type ReportLongevity = L; + + fn report_offence( + offence: SlashingOffence, + ) -> Result<(), OffenceError> { + let reporters = Vec::new(); + R::report_offence(reporters, offence) + } + + fn is_known_offence( + offenders: &[T::KeyOwnerIdentification], + time_slot: &DisputesTimeSlot, + ) -> bool { + , + >>::is_known_offence(offenders, time_slot) + } + + fn submit_unsigned_slashing_report( + dispute_proof: DisputeProof, + key_owner_proof: ::KeyOwnerProof, + ) -> DispatchResult { + use frame_system::offchain::SubmitTransaction; + + let session_index = dispute_proof.time_slot.session_index; + let validator_index = dispute_proof.validator_index.0; + let kind = dispute_proof.kind; + + let call = Call::report_dispute_lost_unsigned { + dispute_proof: Box::new(dispute_proof), + key_owner_proof, + }; + + match SubmitTransaction::>::submit_unsigned_transaction(call.into()) { + Ok(()) => log::info!( + target: LOG_TARGET, + "Submitted dispute slashing report, session({}), index({}), kind({:?})", + session_index, + validator_index, + kind, + ), + Err(()) => log::error!( + target: LOG_TARGET, + "Error submitting dispute slashing report, session({}), index({}), kind({:?})", + session_index, + validator_index, + kind, + ), + } + + Ok(()) + } +} diff --git a/runtime/parachains/src/disputes/slashing/benchmarking.rs b/runtime/parachains/src/disputes/slashing/benchmarking.rs new file mode 100644 index 000000000000..2a21c3a0f62b --- /dev/null +++ b/runtime/parachains/src/disputes/slashing/benchmarking.rs @@ -0,0 +1,156 @@ +// Copyright 2021 Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Polkadot is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see . + +use super::*; + +use crate::{disputes::SlashingHandler, initializer, shared}; +use frame_benchmarking::{benchmarks, whitelist_account}; +use frame_support::traits::{OnFinalize, OnInitialize}; +use frame_system::RawOrigin; +use pallet_staking::testing_utils::create_validators; +use primitives::v2::{Hash, PARACHAIN_KEY_TYPE_ID}; +use sp_runtime::traits::{One, StaticLookup}; +use sp_session::MembershipProof; + +// Candidate hash of the disputed candidate. +const CANDIDATE_HASH: CandidateHash = CandidateHash(Hash::zero()); + +pub trait Config: + pallet_session::Config + + pallet_session::historical::Config + + pallet_staking::Config + + super::Config + + shared::Config + + initializer::Config +{ +} + +fn setup_validator_set(n: u32) -> (SessionIndex, MembershipProof, ValidatorId) +where + T: Config, +{ + pallet_staking::ValidatorCount::::put(n); + + let balance_factor = 1000; + // create validators and set random session keys + for (n, who) in create_validators::(n, balance_factor).unwrap().into_iter().enumerate() { + use rand::{RngCore, SeedableRng}; + + let validator = T::Lookup::lookup(who).unwrap(); + let controller = pallet_staking::Pallet::::bonded(validator).unwrap(); + + let keys = { + const NUM_SESSION_KEYS: usize = 6; + const SESSION_KEY_LEN: usize = 32; + let mut keys = [0u8; NUM_SESSION_KEYS * SESSION_KEY_LEN]; + let mut rng = rand_chacha::ChaCha12Rng::seed_from_u64(n as u64); + rng.fill_bytes(&mut keys); + keys + }; + + let keys: T::Keys = Decode::decode(&mut &keys[..]).expect("wrong number of session keys?"); + let proof: Vec = vec![]; + + whitelist_account!(controller); + pallet_session::Pallet::::set_keys(RawOrigin::Signed(controller).into(), keys, proof) + .expect("session::set_keys should work"); + } + + pallet_session::Pallet::::on_initialize(T::BlockNumber::one()); + initializer::Pallet::::on_initialize(T::BlockNumber::one()); + // skip sessions until the new validator set is enacted + while pallet_session::Pallet::::validators().len() < n as usize { + pallet_session::Pallet::::rotate_session(); + } + initializer::Pallet::::on_finalize(T::BlockNumber::one()); + + let session_index = crate::shared::Pallet::::session_index(); + let session_info = crate::session_info::Pallet::::session_info(session_index); + let session_info = session_info.unwrap(); + let validator_id = session_info.validators[0].clone(); + let key = (PARACHAIN_KEY_TYPE_ID, validator_id.clone()); + let key_owner_proof = pallet_session::historical::Pallet::::prove(key).unwrap(); + + // rotate a session to make sure `key_owner_proof` is historical + initializer::Pallet::::on_initialize(T::BlockNumber::one()); + pallet_session::Pallet::::rotate_session(); + initializer::Pallet::::on_finalize(T::BlockNumber::one()); + + let idx = crate::shared::Pallet::::session_index(); + assert!( + idx > session_index, + "session rotation should work for parachain pallets: {} <= {}", + idx, + session_index, + ); + + (session_index, key_owner_proof, validator_id) +} + +fn setup_dispute(session_index: SessionIndex, validator_id: ValidatorId) -> DisputeProof +where + T: Config, +{ + let current_session = T::ValidatorSet::session_index(); + assert_ne!(session_index, current_session); + + let validator_index = ValidatorIndex(0); + let losers = [validator_index].into_iter(); + + T::SlashingHandler::punish_against_valid(session_index, CANDIDATE_HASH, losers); + + let unapplied = >::get(session_index, CANDIDATE_HASH); + assert_eq!(unapplied.unwrap().keys.len(), 1); + + dispute_proof(session_index, validator_id, validator_index) +} + +fn dispute_proof( + session_index: SessionIndex, + validator_id: ValidatorId, + validator_index: ValidatorIndex, +) -> DisputeProof { + let kind = SlashingOffenceKind::AgainstValid; + let time_slot = DisputesTimeSlot::new(session_index, CANDIDATE_HASH); + + DisputeProof { time_slot, kind, validator_index, validator_id } +} + +benchmarks! { + where_clause { + where T: Config, + } + + // in this setup we have a single `AgainstValid` dispute + // submitted for a past session + report_dispute_lost { + let n in 4..<::BenchmarkingConfig as BenchmarkingConfiguration>::MAX_VALIDATORS; + + let origin = RawOrigin::None.into(); + let (session_index, key_owner_proof, validator_id) = setup_validator_set::(n); + let dispute_proof = setup_dispute::(session_index, validator_id); + }: { + let result = Pallet::::report_dispute_lost_unsigned( + origin, + Box::new(dispute_proof), + key_owner_proof, + ); + assert!(result.is_ok()); + } verify { + let unapplied = >::get(session_index, CANDIDATE_HASH); + assert!(unapplied.is_none()); + } +} diff --git a/runtime/parachains/src/disputes/tests.rs b/runtime/parachains/src/disputes/tests.rs index 2897ced22ed0..4d8ac714cb7b 100644 --- a/runtime/parachains/src/disputes/tests.rs +++ b/runtime/parachains/src/disputes/tests.rs @@ -20,8 +20,7 @@ use crate::{ disputes::DisputesHandler, mock::{ new_test_ext, AccountId, AllPalletsWithSystem, Initializer, MockGenesisConfig, System, - Test, PUNISH_VALIDATORS_AGAINST, PUNISH_VALIDATORS_FOR, PUNISH_VALIDATORS_INCONCLUSIVE, - REWARD_VALIDATORS, + Test, PUNISH_VALIDATORS_AGAINST, PUNISH_VALIDATORS_FOR, REWARD_VALIDATORS, }, }; use assert_matches::assert_matches; @@ -500,9 +499,9 @@ fn dispute_statement_becoming_onesided_due_to_spamslots_is_accepted() { }); } -// Test that punish_inconclusive is correctly called. +// Test that dispute timeout is handled correctly. #[test] -fn test_initializer_initialize() { +fn test_dispute_timeout() { let dispute_conclusion_by_time_out_period = 3; let start = 10; @@ -602,10 +601,6 @@ fn test_initializer_initialize() { // Run to timeout + 1 in order to executive on_finalize(timeout) run_to_block(start + dispute_conclusion_by_time_out_period + 1, |_| None); assert_eq!(SpamSlots::::get(start - 1), Some(vec![0, 0, 0, 0, 0, 0, 0])); - assert_eq!( - PUNISH_VALIDATORS_INCONCLUSIVE.with(|r| r.borrow()[0].clone()), - (9, vec![ValidatorIndex(0), ValidatorIndex(6)]), - ); }); } diff --git a/runtime/parachains/src/initializer.rs b/runtime/parachains/src/initializer.rs index 027dd677bbba..a5634bf48419 100644 --- a/runtime/parachains/src/initializer.rs +++ b/runtime/parachains/src/initializer.rs @@ -21,7 +21,7 @@ use crate::{ configuration::{self, HostConfiguration}, - disputes::DisputesHandler, + disputes::{self, DisputesHandler as _, SlashingHandler as _}, dmp, hrmp, inclusion, paras, scheduler, session_info, shared, ump, }; use frame_support::{ @@ -59,6 +59,9 @@ pub struct SessionChangeNotification { pub session_index: SessionIndex, } +/// Number of validators (not only parachain) in a session. +pub type ValidatorSetCount = u32; + impl> Default for SessionChangeNotification { fn default() -> Self { Self { @@ -109,6 +112,7 @@ pub mod pallet { + scheduler::Config + inclusion::Config + session_info::Config + + disputes::Config + dmp::Config + ump::Config + hrmp::Config @@ -163,6 +167,7 @@ pub mod pallet { inclusion::Pallet::::initializer_initialize(now) + session_info::Pallet::::initializer_initialize(now) + T::DisputesHandler::initializer_initialize(now) + + T::SlashingHandler::initializer_initialize(now) + dmp::Pallet::::initializer_initialize(now) + ump::Pallet::::initializer_initialize(now) + hrmp::Pallet::::initializer_initialize(now); @@ -177,6 +182,7 @@ pub mod pallet { hrmp::Pallet::::initializer_finalize(); ump::Pallet::::initializer_finalize(); dmp::Pallet::::initializer_finalize(); + T::SlashingHandler::initializer_finalize(); T::DisputesHandler::initializer_finalize(); session_info::Pallet::::initializer_finalize(); inclusion::Pallet::::initializer_finalize(); @@ -260,6 +266,7 @@ impl Pallet { inclusion::Pallet::::initializer_on_new_session(¬ification); session_info::Pallet::::initializer_on_new_session(¬ification); T::DisputesHandler::initializer_on_new_session(¬ification); + T::SlashingHandler::initializer_on_new_session(session_index); dmp::Pallet::::initializer_on_new_session(¬ification, &outgoing_paras); ump::Pallet::::initializer_on_new_session(¬ification, &outgoing_paras); hrmp::Pallet::::initializer_on_new_session(¬ification, &outgoing_paras); diff --git a/runtime/parachains/src/mock.rs b/runtime/parachains/src/mock.rs index 8a1714070540..dc0eeb97e049 100644 --- a/runtime/parachains/src/mock.rs +++ b/runtime/parachains/src/mock.rs @@ -31,8 +31,8 @@ use frame_support::{ use frame_support_test::TestRandomness; use parity_scale_codec::Decode; use primitives::v2::{ - AuthorityDiscoveryId, Balance, BlockNumber, Header, Moment, SessionIndex, UpwardMessage, - ValidatorIndex, + AuthorityDiscoveryId, Balance, BlockNumber, CandidateHash, Header, Moment, SessionIndex, + UpwardMessage, ValidatorIndex, }; use sp_core::H256; use sp_io::TestExternalities; @@ -243,7 +243,7 @@ impl crate::hrmp::Config for Test { impl crate::disputes::Config for Test { type RuntimeEvent = RuntimeEvent; type RewardValidators = Self; - type PunishValidators = Self; + type SlashingHandler = Self; type WeightInfo = crate::disputes::TestWeightInfo; } @@ -251,7 +251,6 @@ thread_local! { pub static REWARD_VALIDATORS: RefCell)>> = RefCell::new(Vec::new()); pub static PUNISH_VALIDATORS_FOR: RefCell)>> = RefCell::new(Vec::new()); pub static PUNISH_VALIDATORS_AGAINST: RefCell)>> = RefCell::new(Vec::new()); - pub static PUNISH_VALIDATORS_INCONCLUSIVE: RefCell)>> = RefCell::new(Vec::new()); } impl crate::disputes::RewardValidators for Test { @@ -263,30 +262,31 @@ impl crate::disputes::RewardValidators for Test { } } -impl crate::disputes::PunishValidators for Test { +impl crate::disputes::SlashingHandler for Test { fn punish_for_invalid( session: SessionIndex, - validators: impl IntoIterator, + _: CandidateHash, + losers: impl IntoIterator, ) { - PUNISH_VALIDATORS_FOR - .with(|r| r.borrow_mut().push((session, validators.into_iter().collect()))) + PUNISH_VALIDATORS_FOR.with(|r| r.borrow_mut().push((session, losers.into_iter().collect()))) } fn punish_against_valid( session: SessionIndex, - validators: impl IntoIterator, + _: CandidateHash, + losers: impl IntoIterator, ) { PUNISH_VALIDATORS_AGAINST - .with(|r| r.borrow_mut().push((session, validators.into_iter().collect()))) + .with(|r| r.borrow_mut().push((session, losers.into_iter().collect()))) } - fn punish_inconclusive( - session: SessionIndex, - validators: impl IntoIterator, - ) { - PUNISH_VALIDATORS_INCONCLUSIVE - .with(|r| r.borrow_mut().push((session, validators.into_iter().collect()))) + fn initializer_initialize(_now: BlockNumber) -> Weight { + Weight::zero() } + + fn initializer_finalize() {} + + fn initializer_on_new_session(_: SessionIndex) {} } impl crate::scheduler::Config for Test {} diff --git a/runtime/parachains/src/runtime_api_impl/mod.rs b/runtime/parachains/src/runtime_api_impl/mod.rs index 603b6c4cb385..c045b4747868 100644 --- a/runtime/parachains/src/runtime_api_impl/mod.rs +++ b/runtime/parachains/src/runtime_api_impl/mod.rs @@ -17,9 +17,14 @@ //! Runtime API implementations for Parachains. //! //! These are exposed as different modules using different sets of primitives. -//! At the moment there is only a v2 module and it is not completely clear how migration -//! to a v2 would be done. - +//! At the moment there is a v2 module for the current stable api and +//! vstaging module for all staging methods. +//! When new version of the stable api is released it will be based on v2 and +//! will contain methods from vstaging. +//! The promotion consists of the following steps: +//! 1. Bump the version of the stable module (e.g. v2 becomes v3) +//! 2. Move methods from vstaging to v3. The new stable version should include +//! all methods from vstaging tagged with the new version number (e.g. all +//! v3 methods). pub mod v2; -#[cfg(feature = "vstaging")] pub mod vstaging; diff --git a/runtime/parachains/src/runtime_api_impl/vstaging.rs b/runtime/parachains/src/runtime_api_impl/vstaging.rs index 8715cdc53121..7ae235c8133a 100644 --- a/runtime/parachains/src/runtime_api_impl/vstaging.rs +++ b/runtime/parachains/src/runtime_api_impl/vstaging.rs @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Polkadot. If not, see . -// Put implementations of functions from staging API here. +//! Put implementations of functions from staging APIs here. use crate::disputes; use primitives::v2::{CandidateHash, DisputeState, SessionIndex}; diff --git a/runtime/polkadot/src/lib.rs b/runtime/polkadot/src/lib.rs index dd44d10351e3..ecde85c98125 100644 --- a/runtime/polkadot/src/lib.rs +++ b/runtime/polkadot/src/lib.rs @@ -51,11 +51,10 @@ use pallet_session::historical as session_historical; use pallet_transaction_payment::{FeeDetails, RuntimeDispatchInfo}; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use primitives::v2::{ - AccountId, AccountIndex, Balance, BlockNumber, CandidateEvent, CandidateHash, - CommittedCandidateReceipt, CoreState, DisputeState, GroupRotationInfo, Hash, Id as ParaId, - InboundDownwardMessage, InboundHrmpMessage, Moment, Nonce, OccupiedCoreAssumption, - PersistedValidationData, ScrapedOnChainVotes, SessionInfo, Signature, ValidationCode, - ValidationCodeHash, ValidatorId, ValidatorIndex, + AccountId, AccountIndex, Balance, BlockNumber, CandidateEvent, CommittedCandidateReceipt, + CoreState, GroupRotationInfo, Hash, Id as ParaId, InboundDownwardMessage, InboundHrmpMessage, + Moment, Nonce, OccupiedCoreAssumption, PersistedValidationData, ScrapedOnChainVotes, + SessionInfo, Signature, ValidationCode, ValidationCodeHash, ValidatorId, ValidatorIndex, }; use sp_core::OpaqueMetadata; use sp_mmr_primitives as mmr; @@ -1262,7 +1261,7 @@ impl parachains_initializer::Config for Runtime { impl parachains_disputes::Config for Runtime { type RuntimeEvent = RuntimeEvent; type RewardValidators = (); - type PunishValidators = (); + type SlashingHandler = (); type WeightInfo = weights::runtime_parachains_disputes::WeightInfo; } @@ -1772,10 +1771,6 @@ sp_api::impl_runtime_apis! { { parachains_runtime_api_impl::validation_code_hash::(para_id, assumption) } - - fn staging_get_disputes() -> Vec<(SessionIndex, CandidateHash, DisputeState)> { - unimplemented!() - } } impl beefy_primitives::BeefyApi for Runtime { diff --git a/runtime/rococo/src/lib.rs b/runtime/rococo/src/lib.rs index 4ac03b638cd1..f861594ae61e 100644 --- a/runtime/rococo/src/lib.rs +++ b/runtime/rococo/src/lib.rs @@ -37,8 +37,9 @@ use sp_std::{cmp::Ordering, collections::btree_map::BTreeMap, prelude::*}; use runtime_parachains::{ configuration as parachains_configuration, disputes as parachains_disputes, - dmp as parachains_dmp, hrmp as parachains_hrmp, inclusion as parachains_inclusion, - initializer as parachains_initializer, origin as parachains_origin, paras as parachains_paras, + disputes::slashing as parachains_slashing, dmp as parachains_dmp, hrmp as parachains_hrmp, + inclusion as parachains_inclusion, initializer as parachains_initializer, + origin as parachains_origin, paras as parachains_paras, paras_inherent as parachains_paras_inherent, runtime_api_impl::v2 as parachains_runtime_api_impl, scheduler as parachains_scheduler, session_info as parachains_session_info, shared as parachains_shared, ump as parachains_ump, @@ -1091,10 +1092,27 @@ impl parachains_initializer::Config for Runtime { impl parachains_disputes::Config for Runtime { type RuntimeEvent = RuntimeEvent; type RewardValidators = (); - type PunishValidators = (); + type SlashingHandler = parachains_slashing::SlashValidatorsForDisputes; type WeightInfo = weights::runtime_parachains_disputes::WeightInfo; } +impl parachains_slashing::Config for Runtime { + type KeyOwnerProofSystem = Historical; + type KeyOwnerProof = + >::Proof; + type KeyOwnerIdentification = >::IdentificationTuple; + type HandleReports = parachains_slashing::SlashingReportHandler< + Self::KeyOwnerIdentification, + Offences, + ReportLongevity, + >; + type WeightInfo = parachains_slashing::TestWeightInfo; + type BenchmarkingConfig = parachains_slashing::BenchConfig<200>; +} + parameter_types! { pub const ParaDeposit: Balance = 40 * UNITS; } @@ -1374,6 +1392,7 @@ construct_runtime! { Hrmp: parachains_hrmp::{Pallet, Call, Storage, Event, Config} = 60, ParaSessionInfo: parachains_session_info::{Pallet, Storage} = 61, ParasDisputes: parachains_disputes::{Pallet, Call, Storage, Event} = 62, + ParasSlashing: parachains_slashing::{Pallet, Call, Storage, ValidateUnsigned} = 63, // Parachain Onboarding Pallets. Start indices at 70 to leave room. Registrar: paras_registrar::{Pallet, Call, Storage, Event, Config} = 70, @@ -1552,6 +1571,7 @@ sp_api::impl_runtime_apis! { } } + #[api_version(3)] impl primitives::runtime_api::ParachainHost for Runtime { fn validators() -> Vec { parachains_runtime_api_impl::validators::() @@ -1650,8 +1670,8 @@ sp_api::impl_runtime_apis! { parachains_runtime_api_impl::validation_code_hash::(para_id, assumption) } - fn staging_get_disputes() -> Vec<(SessionIndex, CandidateHash, DisputeState)> { - unimplemented!() + fn disputes() -> Vec<(SessionIndex, CandidateHash, DisputeState)> { + runtime_parachains::runtime_api_impl::vstaging::get_session_disputes::() } } diff --git a/runtime/test-runtime/Cargo.toml b/runtime/test-runtime/Cargo.toml index 68c9b7116a45..c4f10e7d6db4 100644 --- a/runtime/test-runtime/Cargo.toml +++ b/runtime/test-runtime/Cargo.toml @@ -58,7 +58,7 @@ runtime-common = { package = "polkadot-runtime-common", path = "../common", defa primitives = { package = "polkadot-primitives", path = "../../primitives", default-features = false } pallet-xcm = { path = "../../xcm/pallet-xcm", default-features = false } polkadot-parachain = { path = "../../parachain", default-features = false } -polkadot-runtime-parachains = { path = "../parachains", default-features = false, features = ["vstaging"]} +polkadot-runtime-parachains = { path = "../parachains", default-features = false } xcm-builder = { path = "../../xcm/xcm-builder", default-features = false } xcm-executor = { path = "../../xcm/xcm-executor", default-features = false } xcm = { path = "../../xcm", default-features = false } diff --git a/runtime/test-runtime/src/lib.rs b/runtime/test-runtime/src/lib.rs index fe2dcdac3f11..ed83c3594f63 100644 --- a/runtime/test-runtime/src/lib.rs +++ b/runtime/test-runtime/src/lib.rs @@ -45,12 +45,11 @@ use pallet_session::historical as session_historical; use pallet_transaction_payment::{FeeDetails, RuntimeDispatchInfo}; use polkadot_runtime_parachains::reward_points::RewardValidatorsWithEraPoints; use primitives::v2::{ - AccountId, AccountIndex, Balance, BlockNumber, CandidateEvent, CandidateHash, - CommittedCandidateReceipt, CoreState, DisputeState, GroupRotationInfo, Hash as HashT, - Id as ParaId, InboundDownwardMessage, InboundHrmpMessage, Moment, Nonce, - OccupiedCoreAssumption, PersistedValidationData, ScrapedOnChainVotes, - SessionInfo as SessionInfoData, Signature, ValidationCode, ValidationCodeHash, ValidatorId, - ValidatorIndex, + AccountId, AccountIndex, Balance, BlockNumber, CandidateEvent, CommittedCandidateReceipt, + CoreState, GroupRotationInfo, Hash as HashT, Id as ParaId, InboundDownwardMessage, + InboundHrmpMessage, Moment, Nonce, OccupiedCoreAssumption, PersistedValidationData, + ScrapedOnChainVotes, SessionInfo as SessionInfoData, Signature, ValidationCode, + ValidationCodeHash, ValidatorId, ValidatorIndex, }; use runtime_common::{ claims, impl_runtime_weights, paras_sudo_wrapper, BlockHashCount, BlockLength, @@ -487,7 +486,7 @@ impl parachains_inclusion::Config for Runtime { impl parachains_disputes::Config for Runtime { type RuntimeEvent = RuntimeEvent; type RewardValidators = (); - type PunishValidators = (); + type SlashingHandler = (); type WeightInfo = parachains_disputes::TestWeightInfo; } @@ -907,10 +906,6 @@ sp_api::impl_runtime_apis! { { runtime_impl::validation_code_hash::(para_id, assumption) } - - fn staging_get_disputes() -> Vec<(SessionIndex, CandidateHash, DisputeState)> { - polkadot_runtime_parachains::runtime_api_impl::vstaging::get_session_disputes::() - } } impl beefy_primitives::BeefyApi for Runtime { diff --git a/runtime/westend/Cargo.toml b/runtime/westend/Cargo.toml index 892e812ae8d9..92ec42bfc98d 100644 --- a/runtime/westend/Cargo.toml +++ b/runtime/westend/Cargo.toml @@ -87,7 +87,7 @@ hex-literal = { version = "0.3.4", optional = true } runtime-common = { package = "polkadot-runtime-common", path = "../common", default-features = false } primitives = { package = "polkadot-primitives", path = "../../primitives", default-features = false } polkadot-parachain = { path = "../../parachain", default-features = false } -runtime-parachains = { package = "polkadot-runtime-parachains", path = "../parachains", default-features = false, features = ["vstaging"] } +runtime-parachains = { package = "polkadot-runtime-parachains", path = "../parachains", default-features = false } xcm = { package = "xcm", path = "../../xcm", default-features = false } xcm-executor = { package = "xcm-executor", path = "../../xcm/xcm-executor", default-features = false } diff --git a/runtime/westend/src/lib.rs b/runtime/westend/src/lib.rs index be1cb3a2615c..dbe498b1e9ee 100644 --- a/runtime/westend/src/lib.rs +++ b/runtime/westend/src/lib.rs @@ -49,8 +49,9 @@ use runtime_common::{ }; use runtime_parachains::{ configuration as parachains_configuration, disputes as parachains_disputes, - dmp as parachains_dmp, hrmp as parachains_hrmp, inclusion as parachains_inclusion, - initializer as parachains_initializer, origin as parachains_origin, paras as parachains_paras, + disputes::slashing as parachains_slashing, dmp as parachains_dmp, hrmp as parachains_hrmp, + inclusion as parachains_inclusion, initializer as parachains_initializer, + origin as parachains_origin, paras as parachains_paras, paras_inherent as parachains_paras_inherent, reward_points as parachains_reward_points, runtime_api_impl::v2 as parachains_runtime_api_impl, scheduler as parachains_scheduler, session_info as parachains_session_info, shared as parachains_shared, ump as parachains_ump, @@ -431,9 +432,9 @@ impl pallet_election_provider_multi_phase::Config for Runtime { type OffchainRepeat = OffchainRepeat; type MinerTxPriority = NposSolutionPriority; type DataProvider = Staking; - #[cfg(feature = "fast-runtime")] + #[cfg(any(feature = "fast-runtime", feature = "runtime-benchmarks"))] type Fallback = onchain::UnboundedExecution; - #[cfg(not(feature = "fast-runtime"))] + #[cfg(not(any(feature = "fast-runtime", feature = "runtime-benchmarks")))] type Fallback = pallet_election_provider_multi_phase::NoFallback; type GovernanceFallback = onchain::UnboundedExecution; type Solver = SequentialPhragmen< @@ -948,10 +949,27 @@ impl assigned_slots::Config for Runtime { impl parachains_disputes::Config for Runtime { type RuntimeEvent = RuntimeEvent; type RewardValidators = parachains_reward_points::RewardValidatorsWithEraPoints; - type PunishValidators = (); + type SlashingHandler = parachains_slashing::SlashValidatorsForDisputes; type WeightInfo = weights::runtime_parachains_disputes::WeightInfo; } +impl parachains_slashing::Config for Runtime { + type KeyOwnerProofSystem = Historical; + type KeyOwnerProof = + >::Proof; + type KeyOwnerIdentification = >::IdentificationTuple; + type HandleReports = parachains_slashing::SlashingReportHandler< + Self::KeyOwnerIdentification, + Offences, + ReportLongevity, + >; + type WeightInfo = weights::runtime_parachains_disputes_slashing::WeightInfo; + type BenchmarkingConfig = parachains_slashing::BenchConfig<300>; +} + parameter_types! { pub const ParaDeposit: Balance = 2000 * CENTS; pub const DataDepositPerByte: Balance = deposit(0, 1); @@ -1122,6 +1140,7 @@ construct_runtime! { Hrmp: parachains_hrmp::{Pallet, Call, Storage, Event, Config} = 51, ParaSessionInfo: parachains_session_info::{Pallet, Storage} = 52, ParasDisputes: parachains_disputes::{Pallet, Call, Storage, Event} = 53, + ParasSlashing: parachains_slashing::{Pallet, Call, Storage, ValidateUnsigned} = 54, // Parachain Onboarding Pallets. Start indices at 60 to leave room. Registrar: paras_registrar::{Pallet, Call, Storage, Event, Config} = 60, @@ -1204,6 +1223,7 @@ mod benches { [runtime_common::slots, Slots] [runtime_parachains::configuration, Configuration] [runtime_parachains::disputes, ParasDisputes] + [runtime_parachains::disputes::slashing, ParasSlashing] [runtime_parachains::hrmp, Hrmp] [runtime_parachains::initializer, Initializer] [runtime_parachains::paras, Paras] @@ -1296,6 +1316,7 @@ sp_api::impl_runtime_apis! { } } + #[api_version(3)] impl primitives::runtime_api::ParachainHost for Runtime { fn validators() -> Vec { parachains_runtime_api_impl::validators::() @@ -1394,7 +1415,7 @@ sp_api::impl_runtime_apis! { parachains_runtime_api_impl::validation_code_hash::(para_id, assumption) } - fn staging_get_disputes() -> Vec<(SessionIndex, CandidateHash, DisputeState)> { + fn disputes() -> Vec<(SessionIndex, CandidateHash, DisputeState)> { runtime_parachains::runtime_api_impl::vstaging::get_session_disputes::() } } @@ -1666,6 +1687,7 @@ sp_api::impl_runtime_apis! { impl pallet_election_provider_support_benchmarking::Config for Runtime {} impl frame_system_benchmarking::Config for Runtime {} impl pallet_nomination_pools_benchmarking::Config for Runtime {} + impl runtime_parachains::disputes::slashing::benchmarking::Config for Runtime {} use xcm::latest::{ AssetId::*, Fungibility::*, Junctions::*, MultiAsset, MultiAssets, MultiLocation, diff --git a/runtime/westend/src/weights/mod.rs b/runtime/westend/src/weights/mod.rs index 90394ea0fd16..df8b55067efc 100644 --- a/runtime/westend/src/weights/mod.rs +++ b/runtime/westend/src/weights/mod.rs @@ -39,6 +39,7 @@ pub mod runtime_common_paras_registrar; pub mod runtime_common_slots; pub mod runtime_parachains_configuration; pub mod runtime_parachains_disputes; +pub mod runtime_parachains_disputes_slashing; pub mod runtime_parachains_hrmp; pub mod runtime_parachains_initializer; pub mod runtime_parachains_paras; diff --git a/runtime/westend/src/weights/runtime_parachains_disputes_slashing.rs b/runtime/westend/src/weights/runtime_parachains_disputes_slashing.rs new file mode 100644 index 000000000000..868be3969728 --- /dev/null +++ b/runtime/westend/src/weights/runtime_parachains_disputes_slashing.rs @@ -0,0 +1,70 @@ +// Copyright 2017-2022 Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Polkadot is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see . +//! Autogenerated weights for `runtime_parachains::disputes::slashing` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2022-08-31, STEPS: `50`, REPEAT: 20, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! HOSTNAME: `bm3`, CPU: `Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("westend-dev"), DB CACHE: 1024 + +// Executed Command: +// /home/benchbot/cargo_target_dir/production/polkadot +// benchmark +// pallet +// --steps=50 +// --repeat=20 +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --pallet=runtime_parachains::disputes::slashing +// --chain=westend-dev +// --header=./file_header.txt +// --output=./runtime/westend/src/weights/runtime_parachains_disputes_slashing.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::Weight}; +use sp_std::marker::PhantomData; + +/// Weight functions for `runtime_parachains::disputes::slashing`. +pub struct WeightInfo(PhantomData); +impl runtime_parachains::disputes::slashing::WeightInfo for WeightInfo { + // Storage: Session CurrentIndex (r:1 w:0) + // Storage: Historical HistoricalSessions (r:1 w:0) + // Storage: ParaSessionInfo Sessions (r:1 w:0) + // Storage: ParasSlashing UnappliedSlashes (r:1 w:1) + // Storage: Authorship Author (r:1 w:0) + // Storage: System Digest (r:1 w:0) + // Storage: Offences ReportsByKindIndex (r:1 w:1) + // Storage: Offences ConcurrentReportsIndex (r:1 w:1) + // Storage: Offences Reports (r:1 w:1) + // Storage: Staking SlashRewardFraction (r:1 w:0) + // Storage: Staking ActiveEra (r:1 w:0) + // Storage: Staking ErasStartSessionIndex (r:1 w:0) + // Storage: Staking Invulnerables (r:1 w:0) + // Storage: Staking ValidatorSlashInEra (r:1 w:0) + /// The range of component `n` is `[4, 300]`. + fn report_dispute_lost(n: u32, ) -> Weight { + Weight::from_ref_time(97_366_000 as u64) + // Standard Error: 2_000 + .saturating_add(Weight::from_ref_time(467_000 as u64).saturating_mul(n as u64)) + .saturating_add(T::DbWeight::get().reads(14 as u64)) + .saturating_add(T::DbWeight::get().writes(4 as u64)) + } +} diff --git a/scripts/ci/gitlab/lingua.dic b/scripts/ci/gitlab/lingua.dic index 6e6d0acea6ad..ea2da595908e 100644 --- a/scripts/ci/gitlab/lingua.dic +++ b/scripts/ci/gitlab/lingua.dic @@ -51,6 +51,7 @@ Debian/M decodable/MS decrement deduplicated +deduplication deinitializing dequeue/SD dequeuing @@ -283,6 +284,7 @@ typesystem ubuntu/M UDP UI +unapplied unassign unconcluded unfinalize/B diff --git a/scripts/ci/run_benches_for_runtime.sh b/scripts/ci/run_benches_for_runtime.sh index cd9cb5be8e98..d88ca343d6cf 100755 --- a/scripts/ci/run_benches_for_runtime.sh +++ b/scripts/ci/run_benches_for_runtime.sh @@ -62,7 +62,8 @@ OUTPUT=$( --wasm-execution=compiled \ --weight-path="runtime/${runtime}/constants/src/weights/" \ --warmup=10 \ - --repeat=100 + --repeat=100 \ + --header=./file_header.txt ) if [ $? -ne 0 ]; then echo "$OUTPUT" >> "$ERR_FILE" diff --git a/zombienet_tests/functional/0002-parachains-disputes.feature b/zombienet_tests/functional/0002-parachains-disputes.feature index b56cd9b06c89..9386e07e209a 100644 --- a/zombienet_tests/functional/0002-parachains-disputes.feature +++ b/zombienet_tests/functional/0002-parachains-disputes.feature @@ -48,6 +48,9 @@ eve: reports parachain_candidate_disputes_total is at least 10 within 15 seconds eve: reports parachain_candidate_dispute_concluded{validity="valid"} is at least 10 within 15 seconds eve: reports parachain_candidate_dispute_concluded{validity="invalid"} is 0 within 15 seconds +# Check there is an offence report +alice: system event contains "There is an offence reported" within 60 seconds + # Check lag - approval alice: reports polkadot_parachain_approval_checking_finality_lag is 0 bob: reports polkadot_parachain_approval_checking_finality_lag is 0 diff --git a/zombienet_tests/functional/0002-parachains-disputes.toml b/zombienet_tests/functional/0002-parachains-disputes.toml index dc909726bdae..a0a87d60d4e3 100644 --- a/zombienet_tests/functional/0002-parachains-disputes.toml +++ b/zombienet_tests/functional/0002-parachains-disputes.toml @@ -26,12 +26,10 @@ requests = { memory = "2G", cpu = "1" } name = "bob" command = "malus dispute-ancestor --fake-validation approval-invalid" args = [ "--bob", "-lparachain=debug,MALUS=trace"] - + [[relaychain.nodes]] - image = "{{MALUS_IMAGE}}" name = "charlie" - command = "malus dispute-ancestor --fake-validation approval-invalid" - args = [ "--charlie", "-lparachain=debug,MALUS=trace" ] + args = [ "--charlie", "-lparachain=debug" ] [[relaychain.nodes]] name = "dave" diff --git a/zombienet_tests/misc/0002-download-polkadot-from-pr.sh b/zombienet_tests/misc/0002-download-polkadot-from-pr.sh new file mode 100644 index 000000000000..332435aaeebb --- /dev/null +++ b/zombienet_tests/misc/0002-download-polkadot-from-pr.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +set -euxo pipefail + +echo $@ + +CFG_DIR=/cfg + +# add CFG_DIR as first `looking dir` to allow to overrides commands. +mkdir -p $CFG_DIR +export PATH=$CFG_DIR:$PATH + +cd $CFG_DIR +# see 0002-upgrade-node.feature to view the args. +curl -L -O $1 +chmod +x $CFG_DIR/polkadot +echo $(polkadot --version) diff --git a/zombienet_tests/misc/0002-upgrade-node.feature b/zombienet_tests/misc/0002-upgrade-node.feature new file mode 100644 index 000000000000..6d0bd643c4f9 --- /dev/null +++ b/zombienet_tests/misc/0002-upgrade-node.feature @@ -0,0 +1,33 @@ +Description: Smoke / Upgrade Node +Network: ./0002-upgrade-node.toml +Creds: config + +alice: is up +bob: is up +charlie: is up +dave: is up + +alice: parachain 2000 block height is at least 10 within 200 seconds +bob: parachain 2001 block height is at least 10 within 200 seconds + +# upgrade both nodes +# For testing using native provider you should set this env var +# POLKADOT_PR_BIN_URL=https://gitlab.parity.io/parity/mirrors/polkadot/-/jobs/1842869/artifacts/raw/artifacts/polkadot +# with the version of polkadot you want to download. + +# avg 30s in our infra +alice: run ./0002-download-polkadot-from-pr.sh with "{{POLKADOT_PR_BIN_URL}}" within 40 seconds +bob: run ./0002-download-polkadot-from-pr.sh with "{{POLKADOT_PR_BIN_URL}}" within 40 seconds +alice: restart after 5 seconds +bob: restart after 5 seconds + +# process bootstrap +sleep 30 seconds + +alice: is up within 10 seconds +bob: is up within 10 seconds + + +alice: parachain 2000 block height is at least 30 within 300 seconds +bob: parachain 2001 block height is at least 30 within 120 seconds + diff --git a/zombienet_tests/misc/0002-upgrade-node.toml b/zombienet_tests/misc/0002-upgrade-node.toml new file mode 100644 index 000000000000..ef3fa54dc200 --- /dev/null +++ b/zombienet_tests/misc/0002-upgrade-node.toml @@ -0,0 +1,49 @@ +[settings] +timeout = 1000 + +[relaychain] +default_image = "{{ZOMBIENET_INTEGRATION_TEST_IMAGE}}" +chain = "rococo-local" + + + [[relaychain.nodes]] + name = "alice" + args = [ "-lparachain=debug,runtime=debug", "--db paritydb" ] + + [[relaychain.nodes]] + name = "bob" + args = [ "-lparachain=debug,runtime=debug", "--db rocksdb" ] + + [[relaychain.nodes]] + name = "charlie" + args = [ "-lparachain=debug,runtime=debug", "--db paritydb" ] + + [[relaychain.nodes]] + name = "dave" + args = [ "-lparachain=debug,runtime=debug", "--db rocksdb" ] + + +[[parachains]] +id = 2000 +addToGenesis = true + + [parachains.collator] + name = "collator01" + image = "{{COL_IMAGE}}" + command = "undying-collator" + args = ["-lparachain=debug"] + +[[parachains]] +id = 2001 +addToGenesis = true + + [parachains.collator] + name = "collator02" + image = "{{COL_IMAGE}}" + command = "undying-collator" + args = ["-lparachain=debug"] + +[types.Header] +number = "u64" +parent_hash = "Hash" +post_state = "Hash" \ No newline at end of file diff --git a/zombienet_tests/smoke/0003-deregister-register-validator-smoke.feature b/zombienet_tests/smoke/0003-deregister-register-validator-smoke.feature index 90fa4ef9d711..6a79c4cb071d 100644 --- a/zombienet_tests/smoke/0003-deregister-register-validator-smoke.feature +++ b/zombienet_tests/smoke/0003-deregister-register-validator-smoke.feature @@ -2,10 +2,10 @@ Description: Deregister / Register Validator Smoke Network: ./0003-deregister-register-validator-smoke.toml Creds: config -alice: is up -bob: is up -charlie: is up -dave: is up +alice: is up within 30 seconds +bob: is up within 30 seconds +charlie: is up within 30 seconds +dave: is up within 30 seconds # ensure is in the validator set dave: reports polkadot_node_is_parachain_validator is 1 within 240 secs