From 3d1c62ef39fdf92acf989e5db527cb3ba5176a40 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Mon, 8 Sep 2025 17:28:10 -0400 Subject: [PATCH 01/48] chore: initial statistics subsystem --- Cargo.lock | 22 ++ Cargo.toml | 3 + .../node/core/approval-voting/src/import.rs | 5 +- polkadot/node/core/approval-voting/src/lib.rs | 32 ++- .../node/core/statistics-collector/Cargo.toml | 33 +++ .../core/statistics-collector/src/error.rs | 56 +++++ .../node/core/statistics-collector/src/lib.rs | 194 ++++++++++++++++++ .../core/statistics-collector/src/metrics.rs | 30 +++ polkadot/node/overseer/src/dummy.rs | 2 + polkadot/node/overseer/src/lib.rs | 4 + polkadot/node/service/Cargo.toml | 1 + polkadot/node/service/src/overseer.rs | 6 + polkadot/node/subsystem-types/src/messages.rs | 17 ++ 13 files changed, 400 insertions(+), 5 deletions(-) create mode 100644 polkadot/node/core/statistics-collector/Cargo.toml create mode 100644 polkadot/node/core/statistics-collector/src/error.rs create mode 100644 polkadot/node/core/statistics-collector/src/lib.rs create mode 100644 polkadot/node/core/statistics-collector/src/metrics.rs diff --git a/Cargo.lock b/Cargo.lock index 95bdf735f4f42..5269d9b709416 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15619,6 +15619,27 @@ dependencies = [ "tracing-gum", ] +[[package]] +name = "polkadot-node-core-statistics-collector" +version = "6.0.0" +dependencies = [ + "assert_matches", + "fatality", + "futures", + "polkadot-node-primitives", + "polkadot-node-subsystem", + "polkadot-node-subsystem-test-helpers", + "polkadot-node-subsystem-util", + "polkadot-primitives", + "polkadot-primitives-test-helpers", + "rand 0.8.5", + "rstest", + "sp-core 28.0.0", + "sp-tracing 16.0.0", + "thiserror 1.0.65", + "tracing-gum", +] + [[package]] name = "polkadot-node-metrics" version = "7.0.0" @@ -16743,6 +16764,7 @@ dependencies = [ "polkadot-node-core-pvf", "polkadot-node-core-pvf-checker", "polkadot-node-core-runtime-api", + "polkadot-node-core-statistics-collector", "polkadot-node-network-protocol", "polkadot-node-primitives", "polkadot-node-subsystem", diff --git a/Cargo.toml b/Cargo.toml index 3e9551138d12e..97d1565309b96 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -180,6 +180,7 @@ members = [ "polkadot/node/core/dispute-coordinator", "polkadot/node/core/parachains-inherent", "polkadot/node/core/prospective-parachains", + "polkadot/node/core/statistics-collector", "polkadot/node/core/provisioner", "polkadot/node/core/pvf", "polkadot/node/core/pvf-checker", @@ -187,6 +188,7 @@ members = [ "polkadot/node/core/pvf/execute-worker", "polkadot/node/core/pvf/prepare-worker", "polkadot/node/core/runtime-api", + "polkadot/node/core/statistics-collector", "polkadot/node/gum", "polkadot/node/gum/proc-macro", "polkadot/node/malus", @@ -1126,6 +1128,7 @@ polkadot-node-core-chain-selection = { path = "polkadot/node/core/chain-selectio polkadot-node-core-dispute-coordinator = { path = "polkadot/node/core/dispute-coordinator", default-features = false } polkadot-node-core-parachains-inherent = { path = "polkadot/node/core/parachains-inherent", default-features = false } polkadot-node-core-prospective-parachains = { path = "polkadot/node/core/prospective-parachains", default-features = false } +polkadot-node-core-statistics-collector = { path = "polkadot/node/core/statistics-collector", default-features = false } polkadot-node-core-provisioner = { path = "polkadot/node/core/provisioner", default-features = false } polkadot-node-core-pvf = { path = "polkadot/node/core/pvf", default-features = false } polkadot-node-core-pvf-checker = { path = "polkadot/node/core/pvf-checker", default-features = false } diff --git a/polkadot/node/core/approval-voting/src/import.rs b/polkadot/node/core/approval-voting/src/import.rs index ae29197a62e32..61adeae3664cc 100644 --- a/polkadot/node/core/approval-voting/src/import.rs +++ b/polkadot/node/core/approval-voting/src/import.rs @@ -66,7 +66,7 @@ use crate::{ }; use polkadot_node_primitives::approval::time::{slot_number_to_tick, Tick}; - +use polkadot_node_subsystem::messages::StatisticsCollectorMessage; use super::{State, LOG_TARGET}; #[derive(Debug)] @@ -335,7 +335,8 @@ pub struct BlockImportedCandidates { pub(crate) async fn handle_new_head< Sender: SubsystemSender + SubsystemSender - + SubsystemSender, + + SubsystemSender + + SubsystemSender, AVSender: SubsystemSender, B: Backend, >( diff --git a/polkadot/node/core/approval-voting/src/lib.rs b/polkadot/node/core/approval-voting/src/lib.rs index 046eed227f2e2..4e580da8123f8 100644 --- a/polkadot/node/core/approval-voting/src/lib.rs +++ b/polkadot/node/core/approval-voting/src/lib.rs @@ -95,6 +95,7 @@ use persisted_entries::{ApprovalEntry, BlockEntry, CandidateEntry}; use polkadot_node_primitives::approval::time::{ slot_number_to_tick, Clock, ClockExt, DelayedApprovalTimer, SystemClock, Tick, }; +use polkadot_node_subsystem::messages::StatisticsCollectorMessage; mod approval_checking; pub mod approval_db; @@ -2883,7 +2884,8 @@ async fn import_approval( wakeups: &Wakeups, ) -> SubsystemResult<(Vec, ApprovalCheckResult)> where - Sender: SubsystemSender, + Sender: SubsystemSender + + SubsystemSender, { macro_rules! respond_early { ($e: expr) => {{ @@ -2922,7 +2924,7 @@ where gum::trace!( target: LOG_TARGET, "Received approval for num_candidates {:}", - approval.candidate_indices.count_ones() + approval.candidate_indices.clone().count_ones() ); let mut actions = Vec::new(); @@ -3036,7 +3038,8 @@ async fn advance_approval_state( wakeups: &Wakeups, ) -> Vec where - Sender: SubsystemSender, + Sender: SubsystemSender + + SubsystemSender, { let validator_index = transition.validator_index(); @@ -3085,12 +3088,29 @@ where candidate_hash, ); + if let Some(v_idx) = validator_index { + sender.send_message( + StatisticsCollectorMessage::ApprovalVoting( + block_hash, + candidate_hash, + (v_idx, status.tranche_now), + ) + ).await; + } // Check whether this is approved, while allowing a maximum // assignment tick of `now - APPROVAL_DELAY` - that is, that // all counted assignments are at least `APPROVAL_DELAY` ticks old. let is_approved = check.is_approved(tick_now.saturating_sub(APPROVAL_DELAY)); if status.last_no_shows != 0 { metrics.on_observed_no_shows(status.last_no_shows); + sender + .send_message( + StatisticsCollectorMessage::ObservedNoShows( + session_index, + status.no_show_validators.clone(), + )) + .await; + gum::trace!( target: LOG_TARGET, ?candidate_hash, @@ -3124,6 +3144,12 @@ where } metrics.on_candidate_approved(status.tranche_now as _); + sender.send_message( + StatisticsCollectorMessage::CandidateApproved( + candidate_hash, + block_hash, + ) + ).await; if is_block_approved && !was_block_approved { metrics.on_block_approved(status.tranche_now as _); diff --git a/polkadot/node/core/statistics-collector/Cargo.toml b/polkadot/node/core/statistics-collector/Cargo.toml new file mode 100644 index 0000000000000..b6954da9424ad --- /dev/null +++ b/polkadot/node/core/statistics-collector/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "polkadot-node-core-statistics-collector" +version = "6.0.0" +authors.workspace = true +edition.workspace = true +license.workspace = true +description = "The Statistics Collector subsystem. Collects Approval Voting and Approvals Distributions stats." +homepage.workspace = true +repository.workspace = true + +[lints] +workspace = true + +[dependencies] +fatality = { workspace = true } +futures = { workspace = true } +gum = { workspace = true, default-features = true } +thiserror = { workspace = true } + +polkadot-node-subsystem = { workspace = true, default-features = true } +polkadot-node-subsystem-util = { workspace = true, default-features = true } +polkadot-primitives = { workspace = true, default-features = true } +polkadot-node-primitives = { workspace = true, default-features = true } + +[dev-dependencies] +assert_matches = { workspace = true } +polkadot-node-subsystem-test-helpers = { workspace = true } +polkadot-primitives = { workspace = true, features = ["test"] } +polkadot-primitives-test-helpers = { workspace = true } +rand = { workspace = true } +rstest = { workspace = true } +sp-core = { workspace = true, default-features = true } +sp-tracing = { workspace = true } \ No newline at end of file diff --git a/polkadot/node/core/statistics-collector/src/error.rs b/polkadot/node/core/statistics-collector/src/error.rs new file mode 100644 index 0000000000000..67de4cd76fa4c --- /dev/null +++ b/polkadot/node/core/statistics-collector/src/error.rs @@ -0,0 +1,56 @@ +// Copyright (C) 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 . + +//! Error types. + +use futures::channel::oneshot; + +use polkadot_node_subsystem::{ + SubsystemError, +}; +use polkadot_node_subsystem_util::runtime; + +use crate::LOG_TARGET; +use fatality::Nested; + +#[allow(missing_docs)] +#[fatality::fatality(splitable)] +pub enum Error { + #[fatal] + #[error("Receiving message from overseer failed: {0}")] + SubsystemReceive(#[source] SubsystemError), +} + +/// General `Result` type. +pub type Result = std::result::Result; +/// Result for non-fatal only failures. +pub type JfyiErrorResult = std::result::Result; +/// Result for fatal only failures. +pub type FatalResult = std::result::Result; + +/// Utility for eating top level errors and log them. +/// +/// We basically always want to try and continue on error. This utility function is meant to +/// consume top-level errors by simply logging them +pub fn log_error(result: Result<()>, ctx: &'static str) -> FatalResult<()> { + match result.into_nested()? { + Ok(()) => Ok(()), + Err(jfyi) => { + gum::debug!(target: LOG_TARGET, error = ?jfyi, ctx); + Ok(()) + }, + } +} diff --git a/polkadot/node/core/statistics-collector/src/lib.rs b/polkadot/node/core/statistics-collector/src/lib.rs new file mode 100644 index 0000000000000..f11c70978251e --- /dev/null +++ b/polkadot/node/core/statistics-collector/src/lib.rs @@ -0,0 +1,194 @@ +// Copyright (C) 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 std::collections::{HashMap, HashSet}; +use std::collections::hash_map::Entry; +use futures::{channel::oneshot, prelude::*}; +use gum::CandidateHash; +use polkadot_node_subsystem::{ + overseer, ActiveLeavesUpdate, FromOrchestra, OverseerSignal, SpawnedSubsystem, SubsystemError, +}; +use polkadot_node_subsystem::messages::StatisticsCollectorMessage; +use polkadot_primitives::{Hash, SessionIndex, ValidatorIndex}; +use polkadot_node_primitives::approval::time::Tick; +use polkadot_node_primitives::approval::v1::DelayTranche; +use polkadot_primitives::well_known_keys::relay_dispatch_queue_remaining_capacity; +use crate::{ + error::{FatalError, FatalResult, JfyiError, JfyiErrorResult, Result}, +}; + +mod error; +mod metrics; +use self::metrics::Metrics; + +const LOG_TARGET: &str = "parachain::statistics-collector"; + +struct ApprovalsStats { + // votes holds the votes received from approval voting + // subsystem, it also indicates if the vote as received + // before/after candidate approval + votes: Vec<(ValidatorIndex, DelayTranche, bool)>, + approved: bool, +} + +impl ApprovalsStats { + fn new(votes: Vec<(ValidatorIndex, DelayTranche, bool)>, approved: bool) -> Self { + Self { votes, approved } + } +} + +struct PerRelayView { + relay_approved: bool, + included_candidates: Vec, + approvals_stats: HashMap, + votes_per_tranche: HashMap, +} + +impl PerRelayView { + fn new(candidates: Vec) -> Self { + return PerRelayView{ + relay_approved: false, + included_candidates: candidates, + approvals_stats: HashMap::new(), + votes_per_tranche: HashMap::new(), + } + } +} + +struct View { + per_relay_parent: HashMap, + no_shows_per_session: HashMap>, +} + +impl View { + fn new() -> Self { + return View{ + per_relay_parent: HashMap::new(), + no_shows_per_session: HashMap::new(), + }; + } +} + +/// The statistics collector subsystem. +#[derive(Default)] +pub struct StatisticsCollectorSubsystem { + metrics: Metrics, +} + +impl StatisticsCollectorSubsystem { + /// Create a new instance of the `StatisticsCollectorSubsystem`. + pub fn new(metrics: Metrics) -> Self { + Self { metrics } + } +} + +#[overseer::subsystem(StatisticsCollector, error = SubsystemError, prefix = self::overseer)] +impl StatisticsCollectorSubsystem +where + Context: Send + Sync, +{ + fn start(self, ctx: Context) -> SpawnedSubsystem { + SpawnedSubsystem { + future: run(ctx, self.metrics) + .map_err(|e| SubsystemError::with_origin("statistics-parachains", e)) + .boxed(), + name: "statistics-collector-subsystem", + } + } +} + +#[overseer::contextbounds(StatisticsCollector, prefix = self::overseer)] +async fn run(mut ctx: Context, metrics: Metrics) -> FatalResult<()> { + let mut view = View::new(); + loop { + crate::error::log_error( + run_iteration(&mut ctx, &mut view, &metrics).await, + "Encountered issue during run iteration", + )?; + } +} + +#[overseer::contextbounds(StatisticsCollector, prefix = self::overseer)] +pub(crate) async fn run_iteration( + ctx: &mut Context, + view: &mut View, + metrics: &Metrics, +) -> Result<()> { + loop { + match ctx.recv().await.map_err(FatalError::SubsystemReceive)? { + FromOrchestra::Signal(OverseerSignal::Conclude) => return Ok(()), + FromOrchestra::Signal(OverseerSignal::ActiveLeaves(update)) => { + if let Some(actived) = update.activated { + view.per_relay_parent.insert(actived.hash, PerRelayView::new(vec![])); + } + }, + FromOrchestra::Signal(OverseerSignal::BlockFinalized(..)) => {}, + FromOrchestra::Communication { msg } => { + match msg { + StatisticsCollectorMessage::ApprovalVoting( + block_hash, + candidate_hash, + (v_idx, tranche), + ) => { + match view.per_relay_parent.entry(block_hash) { + Entry::Occupied(mut e) => { + let relay_view = e.get_mut(); + + // TODO: avoid inserting duplicated vote + // for the same relay block/candidate hash + + relay_view.approvals_stats + .entry(candidate_hash.clone()) + .and_modify(|v: &mut ApprovalsStats| v.votes.push((v_idx, tranche, v.approved))) + .or_insert(ApprovalsStats::new(vec![(v_idx, tranche, false)], false)); + + relay_view.votes_per_tranche + .entry(tranche) + .and_modify(|q: &mut usize| *q += 1) + .or_insert(1); + } + Entry::Vacant(_) => {} + } + }, + StatisticsCollectorMessage::CandidateApproved(candidate_hash, block_hash) => { + if let Some(relay_view) = view.per_relay_parent.get_mut(&block_hash) { + relay_view.approvals_stats + .entry(candidate_hash) + .and_modify(|a: &mut ApprovalsStats| a.approved = true); + } + } + StatisticsCollectorMessage::ObservedNoShows(session_idx, no_show_validators) => { + view.no_shows_per_session + .entry(session_idx) + .and_modify(|q: &mut HashMap| { + for v_idx in no_show_validators { + q.entry(*v_idx) + .and_modify(|v: &mut usize| *v += 1) + .or_insert(1); + } + }) + .or_insert(HashMap::new()); + }, + StatisticsCollectorMessage::RelayBlockApproved(block_hash) => { + view.per_relay_parent + .entry(block_hash) + .and_modify(|q| q.relay_approved = true); + }, + } + }, + } + } +} \ No newline at end of file diff --git a/polkadot/node/core/statistics-collector/src/metrics.rs b/polkadot/node/core/statistics-collector/src/metrics.rs new file mode 100644 index 0000000000000..c2e2e495f86d9 --- /dev/null +++ b/polkadot/node/core/statistics-collector/src/metrics.rs @@ -0,0 +1,30 @@ +// Copyright (C) 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 polkadot_node_subsystem::prometheus::Opts; +use polkadot_node_subsystem_util::metrics::{ + self, + prometheus::{self, Gauge, GaugeVec, U64}, +}; + +/// Candidate backing metrics. +#[derive(Default, Clone)] +pub struct Metrics; +impl metrics::Metrics for Metrics { + fn try_register(registry: &prometheus::Registry) -> Result { + Ok(Metrics) + } +} diff --git a/polkadot/node/overseer/src/dummy.rs b/polkadot/node/overseer/src/dummy.rs index d618c0c7ca953..1c6a03ae6582a 100644 --- a/polkadot/node/overseer/src/dummy.rs +++ b/polkadot/node/overseer/src/dummy.rs @@ -89,6 +89,7 @@ pub fn dummy_overseer_builder( DummySubsystem, DummySubsystem, DummySubsystem, + DummySubsystem, >, SubsystemError, > @@ -133,6 +134,7 @@ pub fn one_for_all_overseer_builder( Sub, Sub, Sub, + Sub, >, SubsystemError, > diff --git a/polkadot/node/overseer/src/lib.rs b/polkadot/node/overseer/src/lib.rs index 4a46f645ed9a8..8544caa84dbca 100644 --- a/polkadot/node/overseer/src/lib.rs +++ b/polkadot/node/overseer/src/lib.rs @@ -85,6 +85,7 @@ use polkadot_node_subsystem_types::messages::{ DisputeCoordinatorMessage, DisputeDistributionMessage, GossipSupportMessage, NetworkBridgeRxMessage, NetworkBridgeTxMessage, ProspectiveParachainsMessage, ProvisionerMessage, RuntimeApiMessage, StatementDistributionMessage, + StatisticsCollectorMessage, }; pub use polkadot_node_subsystem_types::{ @@ -658,6 +659,9 @@ pub struct Overseer { ])] prospective_parachains: ProspectiveParachains, + #[subsystem(StatisticsCollectorMessage, sends: [])] + statistics_collector: StatisticsCollector, + /// External listeners waiting for a hash to be in the active-leave set. pub activation_external_listeners: HashMap>>>, diff --git a/polkadot/node/service/Cargo.toml b/polkadot/node/service/Cargo.toml index 50bb0dc698669..de3e69ccb82db 100644 --- a/polkadot/node/service/Cargo.toml +++ b/polkadot/node/service/Cargo.toml @@ -132,6 +132,7 @@ polkadot-node-core-pvf = { optional = true, workspace = true, default-features = polkadot-node-core-pvf-checker = { optional = true, workspace = true, default-features = true } polkadot-node-core-runtime-api = { optional = true, workspace = true, default-features = true } polkadot-statement-distribution = { optional = true, workspace = true, default-features = true } +polkadot-node-core-statistics-collector = { optional = true, workspace = true, default-features = true } xcm = { workspace = true, default-features = true } xcm-runtime-apis = { workspace = true, default-features = true } diff --git a/polkadot/node/service/src/overseer.rs b/polkadot/node/service/src/overseer.rs index 07b5fb5f37c7f..887182fa37819 100644 --- a/polkadot/node/service/src/overseer.rs +++ b/polkadot/node/service/src/overseer.rs @@ -73,6 +73,8 @@ pub use polkadot_node_core_provisioner::ProvisionerSubsystem; pub use polkadot_node_core_pvf_checker::PvfCheckerSubsystem; pub use polkadot_node_core_runtime_api::RuntimeApiSubsystem; pub use polkadot_statement_distribution::StatementDistributionSubsystem; +pub use polkadot_node_core_statistics_collector::StatisticsCollectorSubsystem; +use polkadot_overseer::AllMessages::StatisticsCollector; /// Arguments passed for overseer construction. pub struct OverseerGenArgs<'a, Spawner, RuntimeClient> @@ -208,6 +210,7 @@ pub fn validator_overseer_builder( DisputeDistributionSubsystem, ChainSelectionSubsystem, ProspectiveParachainsSubsystem, + StatisticsCollectorSubsystem, >, Error, > @@ -338,6 +341,7 @@ where )) .chain_selection(ChainSelectionSubsystem::new(chain_selection_config, parachains_db)) .prospective_parachains(ProspectiveParachainsSubsystem::new(Metrics::register(registry)?)) + .statistics_collector(StatisticsCollectorSubsystem::new(Metrics::register(registry)?)) .activation_external_listeners(Default::default()) .active_leaves(Default::default()) .supports_parachains(runtime_client) @@ -404,6 +408,7 @@ pub fn collator_overseer_builder( DummySubsystem, DummySubsystem, DummySubsystem, + DummySubsystem, >, Error, > @@ -483,6 +488,7 @@ where .dispute_distribution(DummySubsystem) .chain_selection(DummySubsystem) .prospective_parachains(DummySubsystem) + .statistics_collector(DummySubsystem) .activation_external_listeners(Default::default()) .active_leaves(Default::default()) .supports_parachains(runtime_client) diff --git a/polkadot/node/subsystem-types/src/messages.rs b/polkadot/node/subsystem-types/src/messages.rs index 28d8c0ebf7675..562ba62b77def 100644 --- a/polkadot/node/subsystem-types/src/messages.rs +++ b/polkadot/node/subsystem-types/src/messages.rs @@ -64,6 +64,7 @@ use std::{ /// Network events as transmitted to other subsystems, wrapped in their message types. pub mod network_bridge_event; pub use network_bridge_event::NetworkBridgeEvent; +use polkadot_node_primitives::approval::time::Tick; /// A request to the candidate backing subsystem to check whether /// we can second this candidate. @@ -1457,3 +1458,19 @@ pub enum ProspectiveParachainsMessage { oneshot::Sender>, ), } + +/// Messages sent to the Statistics Collector subsystem. +#[derive(Debug)] +pub enum StatisticsCollectorMessage { + // Approval vote received + ApprovalVoting(Hash, CandidateHash, (ValidatorIndex, DelayTranche)), + + // Candidate received enough approval and now is approved + CandidateApproved(CandidateHash, Hash), + + // Set of candidates that has not shared votes in time + ObservedNoShows(SessionIndex, Vec), + + // All relay block's candidates are approved, therefore relay block is approved + RelayBlockApproved(Hash) +} \ No newline at end of file From 4e8f90a696849fdad1c2befaed15ed879e0362fb Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Tue, 9 Sep 2025 17:19:43 -0400 Subject: [PATCH 02/48] chore: account votes that approved candidate --- Cargo.lock | 1 + polkadot/node/core/approval-voting/src/lib.rs | 14 ++---- .../node/core/statistics-collector/src/lib.rs | 47 +++++-------------- .../core/statistics-collector/src/tests.rs | 0 polkadot/node/overseer/src/dummy.rs | 1 + polkadot/node/subsystem-types/Cargo.toml | 1 + polkadot/node/subsystem-types/src/messages.rs | 7 ++- pre_build.sh | 4 ++ 8 files changed, 28 insertions(+), 47 deletions(-) create mode 100644 polkadot/node/core/statistics-collector/src/tests.rs create mode 100755 pre_build.sh diff --git a/Cargo.lock b/Cargo.lock index 5269d9b709416..7f4e0323eed68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15745,6 +15745,7 @@ name = "polkadot-node-subsystem-types" version = "7.0.0" dependencies = [ "async-trait", + "bitvec", "derive_more 0.99.17", "fatality", "futures", diff --git a/polkadot/node/core/approval-voting/src/lib.rs b/polkadot/node/core/approval-voting/src/lib.rs index 4e580da8123f8..0aabf18460bdb 100644 --- a/polkadot/node/core/approval-voting/src/lib.rs +++ b/polkadot/node/core/approval-voting/src/lib.rs @@ -3088,15 +3088,6 @@ where candidate_hash, ); - if let Some(v_idx) = validator_index { - sender.send_message( - StatisticsCollectorMessage::ApprovalVoting( - block_hash, - candidate_hash, - (v_idx, status.tranche_now), - ) - ).await; - } // Check whether this is approved, while allowing a maximum // assignment tick of `now - APPROVAL_DELAY` - that is, that // all counted assignments are at least `APPROVAL_DELAY` ticks old. @@ -3189,6 +3180,11 @@ where } if newly_approved { state.record_no_shows(session_index, para_id.into(), &status.no_show_validators); + sender.send_message(StatisticsCollectorMessage::CandidateApproved( + candidate_hash, + block_hash, + candidate_entry.approvals().clone().into(), + )).await; } actions.extend(schedule_wakeup_action( &approval_entry, diff --git a/polkadot/node/core/statistics-collector/src/lib.rs b/polkadot/node/core/statistics-collector/src/lib.rs index f11c70978251e..9625cc1764b30 100644 --- a/polkadot/node/core/statistics-collector/src/lib.rs +++ b/polkadot/node/core/statistics-collector/src/lib.rs @@ -32,21 +32,21 @@ use crate::{ mod error; mod metrics; +#[cfg(test)] +mod tests; + + use self::metrics::Metrics; const LOG_TARGET: &str = "parachain::statistics-collector"; struct ApprovalsStats { - // votes holds the votes received from approval voting - // subsystem, it also indicates if the vote as received - // before/after candidate approval - votes: Vec<(ValidatorIndex, DelayTranche, bool)>, - approved: bool, + votes: HashSet, } impl ApprovalsStats { - fn new(votes: Vec<(ValidatorIndex, DelayTranche, bool)>, approved: bool) -> Self { - Self { votes, approved } + fn new(votes: HashSet) -> Self { + Self { votes } } } @@ -138,36 +138,15 @@ pub(crate) async fn run_iteration( FromOrchestra::Signal(OverseerSignal::BlockFinalized(..)) => {}, FromOrchestra::Communication { msg } => { match msg { - StatisticsCollectorMessage::ApprovalVoting( - block_hash, - candidate_hash, - (v_idx, tranche), - ) => { - match view.per_relay_parent.entry(block_hash) { - Entry::Occupied(mut e) => { - let relay_view = e.get_mut(); - - // TODO: avoid inserting duplicated vote - // for the same relay block/candidate hash - - relay_view.approvals_stats - .entry(candidate_hash.clone()) - .and_modify(|v: &mut ApprovalsStats| v.votes.push((v_idx, tranche, v.approved))) - .or_insert(ApprovalsStats::new(vec![(v_idx, tranche, false)], false)); - - relay_view.votes_per_tranche - .entry(tranche) - .and_modify(|q: &mut usize| *q += 1) - .or_insert(1); - } - Entry::Vacant(_) => {} - } - }, - StatisticsCollectorMessage::CandidateApproved(candidate_hash, block_hash) => { + StatisticsCollectorMessage::CandidateApproved(candidate_hash, block_hash, approvals) => { if let Some(relay_view) = view.per_relay_parent.get_mut(&block_hash) { relay_view.approvals_stats .entry(candidate_hash) - .and_modify(|a: &mut ApprovalsStats| a.approved = true); + .and_modify(|a: &mut ApprovalsStats| { + for v_idx in approvals.iter_ones() { + a.votes.insert(ValidatorIndex(v_idx as u32)); + } + }); } } StatisticsCollectorMessage::ObservedNoShows(session_idx, no_show_validators) => { diff --git a/polkadot/node/core/statistics-collector/src/tests.rs b/polkadot/node/core/statistics-collector/src/tests.rs new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/polkadot/node/overseer/src/dummy.rs b/polkadot/node/overseer/src/dummy.rs index 1c6a03ae6582a..c8147bd093967 100644 --- a/polkadot/node/overseer/src/dummy.rs +++ b/polkadot/node/overseer/src/dummy.rs @@ -194,6 +194,7 @@ where .dispute_distribution(subsystem.clone()) .chain_selection(subsystem.clone()) .prospective_parachains(subsystem.clone()) + .statistics_collector(subsystem.clone()) .activation_external_listeners(Default::default()) .active_leaves(Default::default()) .spawner(SpawnGlue(spawner)) diff --git a/polkadot/node/subsystem-types/Cargo.toml b/polkadot/node/subsystem-types/Cargo.toml index aa0efcec6a74c..3760735932286 100644 --- a/polkadot/node/subsystem-types/Cargo.toml +++ b/polkadot/node/subsystem-types/Cargo.toml @@ -12,6 +12,7 @@ repository.workspace = true workspace = true [dependencies] +bitvec = { features = ["alloc"], workspace = true } async-trait = { workspace = true } derive_more = { workspace = true, default-features = true } fatality = { workspace = true } diff --git a/polkadot/node/subsystem-types/src/messages.rs b/polkadot/node/subsystem-types/src/messages.rs index 562ba62b77def..daa4485d73900 100644 --- a/polkadot/node/subsystem-types/src/messages.rs +++ b/polkadot/node/subsystem-types/src/messages.rs @@ -60,11 +60,13 @@ use std::{ collections::{BTreeMap, HashMap, HashSet, VecDeque}, sync::Arc, }; +use bitvec::{order::Lsb0 as BitOrderLsb0, slice::BitSlice}; /// Network events as transmitted to other subsystems, wrapped in their message types. pub mod network_bridge_event; pub use network_bridge_event::NetworkBridgeEvent; use polkadot_node_primitives::approval::time::Tick; +use polkadot_node_primitives::approval::v2::Bitfield; /// A request to the candidate backing subsystem to check whether /// we can second this candidate. @@ -1462,11 +1464,8 @@ pub enum ProspectiveParachainsMessage { /// Messages sent to the Statistics Collector subsystem. #[derive(Debug)] pub enum StatisticsCollectorMessage { - // Approval vote received - ApprovalVoting(Hash, CandidateHash, (ValidatorIndex, DelayTranche)), - // Candidate received enough approval and now is approved - CandidateApproved(CandidateHash, Hash), + CandidateApproved(CandidateHash, Hash, bitvec::vec::BitVec), // Set of candidates that has not shared votes in time ObservedNoShows(SessionIndex, Vec), diff --git a/pre_build.sh b/pre_build.sh new file mode 100755 index 0000000000000..3be5d35d837f8 --- /dev/null +++ b/pre_build.sh @@ -0,0 +1,4 @@ +mkdir -p ./target/debug/deps/ && mkdir -p ./target/release/deps/ + +cp ~/resources/llvm/21/lib/libclang.dylib ./target/debug/deps/ +cp ~/resources/llvm/21/lib/libclang.dylib ./target/release/deps/ \ No newline at end of file From a6feab9b965060cc18e4fd55346e94875ef7a2c3 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Thu, 11 Sep 2025 07:18:42 -0400 Subject: [PATCH 03/48] chore: remove unneeded file --- pre_build.sh | 4 ---- 1 file changed, 4 deletions(-) delete mode 100755 pre_build.sh diff --git a/pre_build.sh b/pre_build.sh deleted file mode 100755 index 3be5d35d837f8..0000000000000 --- a/pre_build.sh +++ /dev/null @@ -1,4 +0,0 @@ -mkdir -p ./target/debug/deps/ && mkdir -p ./target/release/deps/ - -cp ~/resources/llvm/21/lib/libclang.dylib ./target/debug/deps/ -cp ~/resources/llvm/21/lib/libclang.dylib ./target/release/deps/ \ No newline at end of file From 9f9d2f4c6b3eb8dc21d811f5f98e4bc66e3556cc Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Mon, 15 Sep 2025 09:46:23 -0400 Subject: [PATCH 04/48] chore: add docs, get useful votes, change subsystem name --- Cargo.toml | 6 +-- polkadot/node/core/approval-voting/src/lib.rs | 52 +++++++++++++++---- .../Cargo.toml | 2 +- .../src/error.rs | 0 .../src/lib.rs | 24 +++++---- .../src/metrics.rs | 0 .../src/tests.rs | 0 polkadot/node/service/src/overseer.rs | 6 +-- polkadot/node/subsystem-types/src/messages.rs | 2 +- 9 files changed, 64 insertions(+), 28 deletions(-) rename polkadot/node/core/{statistics-collector => consensus-statistics-collector}/Cargo.toml (95%) rename polkadot/node/core/{statistics-collector => consensus-statistics-collector}/src/error.rs (100%) rename polkadot/node/core/{statistics-collector => consensus-statistics-collector}/src/lib.rs (87%) rename polkadot/node/core/{statistics-collector => consensus-statistics-collector}/src/metrics.rs (100%) rename polkadot/node/core/{statistics-collector => consensus-statistics-collector}/src/tests.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index 97d1565309b96..e2d9d163c09ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -180,7 +180,7 @@ members = [ "polkadot/node/core/dispute-coordinator", "polkadot/node/core/parachains-inherent", "polkadot/node/core/prospective-parachains", - "polkadot/node/core/statistics-collector", + "polkadot/node/core/consensus-statistics-collector", "polkadot/node/core/provisioner", "polkadot/node/core/pvf", "polkadot/node/core/pvf-checker", @@ -188,7 +188,7 @@ members = [ "polkadot/node/core/pvf/execute-worker", "polkadot/node/core/pvf/prepare-worker", "polkadot/node/core/runtime-api", - "polkadot/node/core/statistics-collector", + "polkadot/node/core/consensus-statistics-collector", "polkadot/node/gum", "polkadot/node/gum/proc-macro", "polkadot/node/malus", @@ -1128,7 +1128,7 @@ polkadot-node-core-chain-selection = { path = "polkadot/node/core/chain-selectio polkadot-node-core-dispute-coordinator = { path = "polkadot/node/core/dispute-coordinator", default-features = false } polkadot-node-core-parachains-inherent = { path = "polkadot/node/core/parachains-inherent", default-features = false } polkadot-node-core-prospective-parachains = { path = "polkadot/node/core/prospective-parachains", default-features = false } -polkadot-node-core-statistics-collector = { path = "polkadot/node/core/statistics-collector", default-features = false } +polkadot-node-core-consensus-statistics-collector = { path = "polkadot/node/core/consensus-statistics-collector", default-features = false } polkadot-node-core-provisioner = { path = "polkadot/node/core/provisioner", default-features = false } polkadot-node-core-pvf = { path = "polkadot/node/core/pvf", default-features = false } polkadot-node-core-pvf-checker = { path = "polkadot/node/core/pvf-checker", default-features = false } diff --git a/polkadot/node/core/approval-voting/src/lib.rs b/polkadot/node/core/approval-voting/src/lib.rs index 0aabf18460bdb..41e872d7efd75 100644 --- a/polkadot/node/core/approval-voting/src/lib.rs +++ b/polkadot/node/core/approval-voting/src/lib.rs @@ -3135,12 +3135,6 @@ where } metrics.on_candidate_approved(status.tranche_now as _); - sender.send_message( - StatisticsCollectorMessage::CandidateApproved( - candidate_hash, - block_hash, - ) - ).await; if is_block_approved && !was_block_approved { metrics.on_block_approved(status.tranche_now as _); @@ -3180,12 +3174,48 @@ where } if newly_approved { state.record_no_shows(session_index, para_id.into(), &status.no_show_validators); - sender.send_message(StatisticsCollectorMessage::CandidateApproved( - candidate_hash, - block_hash, - candidate_entry.approvals().clone().into(), - )).await; + let mut collected_useful_approvals = vec![]; + + // getting all the votes a candidate has till now + for validator_approval_idx in candidate_entry.approvals().iter_ones() { + if let Some(current_block_assignments) = candidate_entry.approval_entry(&block_hash) { + for tranche_entry in approval_entry.tranches() { + match status.required_tranches { + RequiredTranches::All => {}, + RequiredTranches::Exact {needed, ..} => { + if tranche_entry.tranche() <= needed { + let trance_assignments = tranche_entry.assignments(); + let useful_vote = trance_assignments + .iter() + .find(|(validator_on_tranche, _)| *validator_on_tranche == ValidatorIndex(validator_approval_idx as _)); + + match useful_vote { + Some((vidx, _)) => collected_useful_approvals.push(*vidx), + // Found no useful votes on a needed tranche + None => {} + } + } + } + _ => {} + } + } + } + } + + _ = sender.try_send_message(StatisticsCollectorMessage::CandidateApproved( + candidate_hash, + block_hash, + collected_useful_approvals, + )).map_err(|_| { + gum::warn!( + target: LOG_TARGET, + ?candidate_hash, + ?block_hash, + "Failed to send approvals to statistics subsystem", + ); + }); } + actions.extend(schedule_wakeup_action( &approval_entry, block_hash, diff --git a/polkadot/node/core/statistics-collector/Cargo.toml b/polkadot/node/core/consensus-statistics-collector/Cargo.toml similarity index 95% rename from polkadot/node/core/statistics-collector/Cargo.toml rename to polkadot/node/core/consensus-statistics-collector/Cargo.toml index b6954da9424ad..3b5a7f2a61714 100644 --- a/polkadot/node/core/statistics-collector/Cargo.toml +++ b/polkadot/node/core/consensus-statistics-collector/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "polkadot-node-core-statistics-collector" +name = "polkadot-node-core-consensus-statistics-collector" version = "6.0.0" authors.workspace = true edition.workspace = true diff --git a/polkadot/node/core/statistics-collector/src/error.rs b/polkadot/node/core/consensus-statistics-collector/src/error.rs similarity index 100% rename from polkadot/node/core/statistics-collector/src/error.rs rename to polkadot/node/core/consensus-statistics-collector/src/error.rs diff --git a/polkadot/node/core/statistics-collector/src/lib.rs b/polkadot/node/core/consensus-statistics-collector/src/lib.rs similarity index 87% rename from polkadot/node/core/statistics-collector/src/lib.rs rename to polkadot/node/core/consensus-statistics-collector/src/lib.rs index 9625cc1764b30..82913449572a5 100644 --- a/polkadot/node/core/statistics-collector/src/lib.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/lib.rs @@ -14,6 +14,14 @@ // You should have received a copy of the GNU General Public License // along with Polkadot. If not, see . +//! Implementation of the Consensus Statistics Collector subsystem. +//! This component monitors and manages metrics related to parachain candidate approvals, +//! including approval votes, distribution of approval chunks, chunk downloads, and chunk uploads. +//! +//! Its primary responsibility is to collect and track data reflecting each node’s perspective +//! on the approval work carried out by all session validators. + + use std::collections::{HashMap, HashSet}; use std::collections::hash_map::Entry; use futures::{channel::oneshot, prelude::*}; @@ -38,7 +46,7 @@ mod tests; use self::metrics::Metrics; -const LOG_TARGET: &str = "parachain::statistics-collector"; +const LOG_TARGET: &str = "parachain::consensus-statistics-collector"; struct ApprovalsStats { votes: HashSet, @@ -84,19 +92,19 @@ impl View { /// The statistics collector subsystem. #[derive(Default)] -pub struct StatisticsCollectorSubsystem { +pub struct ConsensusStatisticsCollector { metrics: Metrics, } -impl StatisticsCollectorSubsystem { - /// Create a new instance of the `StatisticsCollectorSubsystem`. +impl ConsensusStatisticsCollector { + /// Create a new instance of the `ConsensusStatisticsCollector`. pub fn new(metrics: Metrics) -> Self { Self { metrics } } } #[overseer::subsystem(StatisticsCollector, error = SubsystemError, prefix = self::overseer)] -impl StatisticsCollectorSubsystem +impl ConsensusStatisticsCollector where Context: Send + Sync, { @@ -105,7 +113,7 @@ where future: run(ctx, self.metrics) .map_err(|e| SubsystemError::with_origin("statistics-parachains", e)) .boxed(), - name: "statistics-collector-subsystem", + name: "consensus-statistics-collector-subsystem", } } } @@ -143,9 +151,7 @@ pub(crate) async fn run_iteration( relay_view.approvals_stats .entry(candidate_hash) .and_modify(|a: &mut ApprovalsStats| { - for v_idx in approvals.iter_ones() { - a.votes.insert(ValidatorIndex(v_idx as u32)); - } + a.votes.extend(approvals.into()); }); } } diff --git a/polkadot/node/core/statistics-collector/src/metrics.rs b/polkadot/node/core/consensus-statistics-collector/src/metrics.rs similarity index 100% rename from polkadot/node/core/statistics-collector/src/metrics.rs rename to polkadot/node/core/consensus-statistics-collector/src/metrics.rs diff --git a/polkadot/node/core/statistics-collector/src/tests.rs b/polkadot/node/core/consensus-statistics-collector/src/tests.rs similarity index 100% rename from polkadot/node/core/statistics-collector/src/tests.rs rename to polkadot/node/core/consensus-statistics-collector/src/tests.rs diff --git a/polkadot/node/service/src/overseer.rs b/polkadot/node/service/src/overseer.rs index 887182fa37819..80c3d63aed7bd 100644 --- a/polkadot/node/service/src/overseer.rs +++ b/polkadot/node/service/src/overseer.rs @@ -73,7 +73,7 @@ pub use polkadot_node_core_provisioner::ProvisionerSubsystem; pub use polkadot_node_core_pvf_checker::PvfCheckerSubsystem; pub use polkadot_node_core_runtime_api::RuntimeApiSubsystem; pub use polkadot_statement_distribution::StatementDistributionSubsystem; -pub use polkadot_node_core_statistics_collector::StatisticsCollectorSubsystem; +pub use polkadot_node_core_statistics_collector::ConsensusStatisticsCollector; use polkadot_overseer::AllMessages::StatisticsCollector; /// Arguments passed for overseer construction. @@ -210,7 +210,7 @@ pub fn validator_overseer_builder( DisputeDistributionSubsystem, ChainSelectionSubsystem, ProspectiveParachainsSubsystem, - StatisticsCollectorSubsystem, + ConsensusStatisticsCollector, >, Error, > @@ -341,7 +341,7 @@ where )) .chain_selection(ChainSelectionSubsystem::new(chain_selection_config, parachains_db)) .prospective_parachains(ProspectiveParachainsSubsystem::new(Metrics::register(registry)?)) - .statistics_collector(StatisticsCollectorSubsystem::new(Metrics::register(registry)?)) + .statistics_collector(ConsensusStatisticsCollector::new(Metrics::register(registry)?)) .activation_external_listeners(Default::default()) .active_leaves(Default::default()) .supports_parachains(runtime_client) diff --git a/polkadot/node/subsystem-types/src/messages.rs b/polkadot/node/subsystem-types/src/messages.rs index daa4485d73900..5e8b1cd18e746 100644 --- a/polkadot/node/subsystem-types/src/messages.rs +++ b/polkadot/node/subsystem-types/src/messages.rs @@ -1465,7 +1465,7 @@ pub enum ProspectiveParachainsMessage { #[derive(Debug)] pub enum StatisticsCollectorMessage { // Candidate received enough approval and now is approved - CandidateApproved(CandidateHash, Hash, bitvec::vec::BitVec), + CandidateApproved(CandidateHash, Hash, Vec), // Set of candidates that has not shared votes in time ObservedNoShows(SessionIndex, Vec), From 8637ad7b288f1f3694ca67e09b21df7ccc170407 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Wed, 17 Sep 2025 13:18:42 -0400 Subject: [PATCH 05/48] chore: start getting chunks downloads metrics --- .../node/core/approval-voting/src/import.rs | 4 +- polkadot/node/core/approval-voting/src/lib.rs | 64 ++++++++--------- .../src/approval_voting_metrics.rs | 48 +++++++++++++ .../src/availability_distribution_metrics.rs | 10 +++ .../consensus-statistics-collector/src/lib.rs | 68 ++++++++----------- .../availability-recovery/src/task/mod.rs | 4 ++ .../src/task/strategy/chunks.rs | 17 ++--- .../src/task/strategy/mod.rs | 23 ++++++- .../src/task/strategy/systematic.rs | 1 + polkadot/node/overseer/src/lib.rs | 4 +- polkadot/node/subsystem-types/src/messages.rs | 4 +- 11 files changed, 162 insertions(+), 85 deletions(-) create mode 100644 polkadot/node/core/consensus-statistics-collector/src/approval_voting_metrics.rs create mode 100644 polkadot/node/core/consensus-statistics-collector/src/availability_distribution_metrics.rs diff --git a/polkadot/node/core/approval-voting/src/import.rs b/polkadot/node/core/approval-voting/src/import.rs index 61adeae3664cc..ebdc894fb710a 100644 --- a/polkadot/node/core/approval-voting/src/import.rs +++ b/polkadot/node/core/approval-voting/src/import.rs @@ -66,7 +66,7 @@ use crate::{ }; use polkadot_node_primitives::approval::time::{slot_number_to_tick, Tick}; -use polkadot_node_subsystem::messages::StatisticsCollectorMessage; +use polkadot_node_subsystem::messages::ConsensusStatisticsCollectorMessage; use super::{State, LOG_TARGET}; #[derive(Debug)] @@ -336,7 +336,7 @@ pub(crate) async fn handle_new_head< Sender: SubsystemSender + SubsystemSender + SubsystemSender - + SubsystemSender, + + SubsystemSender, AVSender: SubsystemSender, B: Backend, >( diff --git a/polkadot/node/core/approval-voting/src/lib.rs b/polkadot/node/core/approval-voting/src/lib.rs index 41e872d7efd75..893e44e830b19 100644 --- a/polkadot/node/core/approval-voting/src/lib.rs +++ b/polkadot/node/core/approval-voting/src/lib.rs @@ -95,7 +95,7 @@ use persisted_entries::{ApprovalEntry, BlockEntry, CandidateEntry}; use polkadot_node_primitives::approval::time::{ slot_number_to_tick, Clock, ClockExt, DelayedApprovalTimer, SystemClock, Tick, }; -use polkadot_node_subsystem::messages::StatisticsCollectorMessage; +use polkadot_node_subsystem::messages::ConsensusStatisticsCollectorMessage; mod approval_checking; pub mod approval_db; @@ -2885,7 +2885,7 @@ async fn import_approval( ) -> SubsystemResult<(Vec, ApprovalCheckResult)> where Sender: SubsystemSender + - SubsystemSender, + SubsystemSender, { macro_rules! respond_early { ($e: expr) => {{ @@ -3039,7 +3039,7 @@ async fn advance_approval_state( ) -> Vec where Sender: SubsystemSender + - SubsystemSender, + SubsystemSender, { let validator_index = transition.validator_index(); @@ -3096,7 +3096,7 @@ where metrics.on_observed_no_shows(status.last_no_shows); sender .send_message( - StatisticsCollectorMessage::ObservedNoShows( + ConsensusStatisticsCollectorMessage::ObservedNoShows( session_index, status.no_show_validators.clone(), )) @@ -3176,44 +3176,46 @@ where state.record_no_shows(session_index, para_id.into(), &status.no_show_validators); let mut collected_useful_approvals = vec![]; - // getting all the votes a candidate has till now - for validator_approval_idx in candidate_entry.approvals().iter_ones() { - if let Some(current_block_assignments) = candidate_entry.approval_entry(&block_hash) { - for tranche_entry in approval_entry.tranches() { - match status.required_tranches { - RequiredTranches::All => {}, - RequiredTranches::Exact {needed, ..} => { - if tranche_entry.tranche() <= needed { - let trance_assignments = tranche_entry.assignments(); - let useful_vote = trance_assignments - .iter() - .find(|(validator_on_tranche, _)| *validator_on_tranche == ValidatorIndex(validator_approval_idx as _)); - - match useful_vote { - Some((vidx, _)) => collected_useful_approvals.push(*vidx), - // Found no useful votes on a needed tranche - None => {} - } + match status.required_tranches { + RequiredTranches::All => { + collected_useful_approvals.extend(candidate_entry.approvals().iter_ones().into()); + }, + RequiredTranches::Exact {needed, ..} => { + let tranches = approval_entry.tranches(); + for validator_approval_idx in candidate_entry.approvals().iter_ones() { + for tranche_entry in tranches { + if tranche_entry.tranche() <= needed { + let trance_assignments = tranche_entry.assignments(); + let useful_vote = trance_assignments + .iter() + .find(|(validator_on_tranche, _)| *validator_on_tranche == ValidatorIndex(validator_approval_idx as _)); + + match useful_vote { + Some((vidx, _)) => collected_useful_approvals.push(*vidx), + // Found no useful votes on a needed tranche + None => {} } } - _ => {} } } - } + }, + RequiredTranches::Pending {..} => panic!("Newly approved candidate should never be pending; qed"), } - _ = sender.try_send_message(StatisticsCollectorMessage::CandidateApproved( - candidate_hash, - block_hash, - collected_useful_approvals, - )).map_err(|_| { - gum::warn!( + if collected_useful_approvals.len() > 0 { + _ = sender.try_send_message(ConsensusStatisticsCollectorMessage::CandidateApproved( + candidate_hash, + block_hash, + collected_useful_approvals, + )).map_err(|_| { + gum::warn!( target: LOG_TARGET, ?candidate_hash, ?block_hash, "Failed to send approvals to statistics subsystem", ); - }); + }); + } } actions.extend(schedule_wakeup_action( diff --git a/polkadot/node/core/consensus-statistics-collector/src/approval_voting_metrics.rs b/polkadot/node/core/consensus-statistics-collector/src/approval_voting_metrics.rs new file mode 100644 index 0000000000000..047c218da61fb --- /dev/null +++ b/polkadot/node/core/consensus-statistics-collector/src/approval_voting_metrics.rs @@ -0,0 +1,48 @@ +use std::collections::{HashMap, HashSet}; +use polkadot_primitives::{CandidateHash, Hash, SessionIndex, ValidatorIndex}; +use crate::View; +use crate::error::Result; + +#[derive(Debug, Clone, Default)] +pub struct ApprovalsStats { + pub votes: HashSet, +} + +impl ApprovalsStats { + pub fn new(votes: HashSet) -> Self { + Self { votes } + } +} + +pub fn handle_candidate_approved( + view: &mut View, + block_hash: Hash, + candidate_hash: CandidateHash, + approvals: Vec +) { + if let Some(relay_view) = view.per_relay.get_mut(&block_hash) { + relay_view.approvals_stats + .entry(candidate_hash) + .or_default() + .and_modify(|a: &mut ApprovalsStats| { + a.votes.extend(approvals.into()); + }); + } +} + +pub fn handle_observed_no_shows( + view: &mut View, + session_index: SessionIndex, + no_show_validators: Vec, +) { + view.no_shows_per_session + .entry(session_index) + .and_modify(|q: &mut HashMap| { + for v_idx in no_show_validators { + q.entry(*v_idx) + .and_modify(|v: &mut usize| *v += 1) + .or_insert(1); + } + }) + .or_insert(HashMap::new()); +} \ No newline at end of file diff --git a/polkadot/node/core/consensus-statistics-collector/src/availability_distribution_metrics.rs b/polkadot/node/core/consensus-statistics-collector/src/availability_distribution_metrics.rs new file mode 100644 index 0000000000000..26a54dcc7c35f --- /dev/null +++ b/polkadot/node/core/consensus-statistics-collector/src/availability_distribution_metrics.rs @@ -0,0 +1,10 @@ +use polkadot_primitives::ValidatorIndex; +use crate::View; + +pub fn handle_chunks_downloaded( + view: &mut View, + validator: ValidatorIndex, + downloaded: u64, +) { + +} \ No newline at end of file diff --git a/polkadot/node/core/consensus-statistics-collector/src/lib.rs b/polkadot/node/core/consensus-statistics-collector/src/lib.rs index 82913449572a5..02ff003e7f37c 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/lib.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/lib.rs @@ -29,7 +29,7 @@ use gum::CandidateHash; use polkadot_node_subsystem::{ overseer, ActiveLeavesUpdate, FromOrchestra, OverseerSignal, SpawnedSubsystem, SubsystemError, }; -use polkadot_node_subsystem::messages::StatisticsCollectorMessage; +use polkadot_node_subsystem::messages::ConsensusStatisticsCollectorMessage; use polkadot_primitives::{Hash, SessionIndex, ValidatorIndex}; use polkadot_node_primitives::approval::time::Tick; use polkadot_node_primitives::approval::v1::DelayTranche; @@ -42,50 +42,41 @@ mod error; mod metrics; #[cfg(test)] mod tests; +mod approval_voting_metrics; +mod availability_distribution_metrics; - +use approval_voting_metrics::ApprovalsStats; +use crate::approval_voting_metrics::{handle_candidate_approved, handle_observed_no_shows}; use self::metrics::Metrics; const LOG_TARGET: &str = "parachain::consensus-statistics-collector"; -struct ApprovalsStats { - votes: HashSet, -} - -impl ApprovalsStats { - fn new(votes: HashSet) -> Self { - Self { votes } - } -} - struct PerRelayView { relay_approved: bool, - included_candidates: Vec, approvals_stats: HashMap, - votes_per_tranche: HashMap, } impl PerRelayView { fn new(candidates: Vec) -> Self { return PerRelayView{ relay_approved: false, - included_candidates: candidates, approvals_stats: HashMap::new(), - votes_per_tranche: HashMap::new(), } } } struct View { - per_relay_parent: HashMap, + per_relay: HashMap, no_shows_per_session: HashMap>, + chunks_downloaded: HashMap, } impl View { fn new() -> Self { return View{ - per_relay_parent: HashMap::new(), + per_relay: HashMap::new(), no_shows_per_session: HashMap::new(), + chunks_downloaded: HashMap::new(), }; } } @@ -140,35 +131,32 @@ pub(crate) async fn run_iteration( FromOrchestra::Signal(OverseerSignal::Conclude) => return Ok(()), FromOrchestra::Signal(OverseerSignal::ActiveLeaves(update)) => { if let Some(actived) = update.activated { - view.per_relay_parent.insert(actived.hash, PerRelayView::new(vec![])); + view.per_relay.insert(actived.hash, PerRelayView::new(vec![])); } }, FromOrchestra::Signal(OverseerSignal::BlockFinalized(..)) => {}, FromOrchestra::Communication { msg } => { match msg { - StatisticsCollectorMessage::CandidateApproved(candidate_hash, block_hash, approvals) => { - if let Some(relay_view) = view.per_relay_parent.get_mut(&block_hash) { - relay_view.approvals_stats - .entry(candidate_hash) - .and_modify(|a: &mut ApprovalsStats| { - a.votes.extend(approvals.into()); - }); - } + ConsensusStatisticsCollectorMessage::ChunksDownloaded(_, _)=> { + + }, + ConsensusStatisticsCollectorMessage::CandidateApproved(candidate_hash, block_hash, approvals) => { + handle_candidate_approved( + view, + block_hash, + candidate_hash, + approvals, + ); } - StatisticsCollectorMessage::ObservedNoShows(session_idx, no_show_validators) => { - view.no_shows_per_session - .entry(session_idx) - .and_modify(|q: &mut HashMap| { - for v_idx in no_show_validators { - q.entry(*v_idx) - .and_modify(|v: &mut usize| *v += 1) - .or_insert(1); - } - }) - .or_insert(HashMap::new()); + ConsensusStatisticsCollectorMessage::ObservedNoShows(session_idx, no_show_validators) => { + handle_observed_no_shows( + view, + session_idx, + no_show_validators, + ); }, - StatisticsCollectorMessage::RelayBlockApproved(block_hash) => { - view.per_relay_parent + ConsensusStatisticsCollectorMessage::RelayBlockApproved(block_hash) => { + view.per_relay .entry(block_hash) .and_modify(|q| q.relay_approved = true); }, diff --git a/polkadot/node/network/availability-recovery/src/task/mod.rs b/polkadot/node/network/availability-recovery/src/task/mod.rs index 0a8b52411afee..f6ae5da461e68 100644 --- a/polkadot/node/network/availability-recovery/src/task/mod.rs +++ b/polkadot/node/network/availability-recovery/src/task/mod.rs @@ -38,6 +38,7 @@ use sc_network::ProtocolName; use futures::channel::{mpsc, oneshot}; use std::collections::VecDeque; +use polkadot_node_subsystem::messages::ConsensusStatisticsCollectorMessage; /// Recovery parameters common to all strategies in a `RecoveryTask`. #[derive(Clone)] @@ -178,6 +179,9 @@ where }, Ok(data) => { self.params.metrics.on_recovery_succeeded(strategy_type, data.encoded_size()); + _ = self.sender.try_send_message( + ConsensusStatisticsCollectorMessage::ChunksDownloaded( + self.state.get_download_chunks_metrics())); return Ok(data) }, } diff --git a/polkadot/node/network/availability-recovery/src/task/strategy/chunks.rs b/polkadot/node/network/availability-recovery/src/task/strategy/chunks.rs index 6b34538b62662..27166bb0272a2 100644 --- a/polkadot/node/network/availability-recovery/src/task/strategy/chunks.rs +++ b/polkadot/node/network/availability-recovery/src/task/strategy/chunks.rs @@ -33,6 +33,7 @@ use polkadot_primitives::ValidatorIndex; use futures::{channel::oneshot, SinkExt}; use rand::seq::SliceRandom; use std::collections::VecDeque; +use polkadot_node_subsystem::messages::ConsensusStatisticsCollectorMessage; /// Parameters specific to the `FetchChunks` strategy. pub struct FetchChunksParams { @@ -166,14 +167,6 @@ impl FetchChunks { #[async_trait::async_trait] impl RecoveryStrategy for FetchChunks { - fn display_name(&self) -> &'static str { - "Fetch chunks" - } - - fn strategy_type(&self) -> &'static str { - "regular_chunks" - } - async fn run( mut self: Box, state: &mut State, @@ -294,6 +287,14 @@ impl RecoveryStrategy self.error_count += error_count; } } + + fn display_name(&self) -> &'static str { + "Fetch chunks" + } + + fn strategy_type(&self) -> &'static str { + "regular_chunks" + } } #[cfg(test)] diff --git a/polkadot/node/network/availability-recovery/src/task/strategy/mod.rs b/polkadot/node/network/availability-recovery/src/task/strategy/mod.rs index 1403277c8a95b..33e55464b78b5 100644 --- a/polkadot/node/network/availability-recovery/src/task/strategy/mod.rs +++ b/polkadot/node/network/availability-recovery/src/task/strategy/mod.rs @@ -211,17 +211,35 @@ pub struct State { /// A record of errors returned when requesting a chunk from a validator. recorded_errors: HashMap<(AuthorityDiscoveryId, ValidatorIndex), ErrorRecord>, + + chunks_received_by: HashMap>, } impl State { pub fn new() -> Self { - Self { received_chunks: BTreeMap::new(), recorded_errors: HashMap::new() } + Self { + received_chunks: BTreeMap::new(), + recorded_errors: HashMap::new(), + chunks_received_by: HashMap::new(), + } } fn insert_chunk(&mut self, chunk_index: ChunkIndex, chunk: Chunk) { self.received_chunks.insert(chunk_index, chunk); } + fn note_received_chunk(&mut self, sender: ValidatorIndex, chunk: ChunkIndex) { + self.chunks_received_by.entry(sender).or_default().push(chunk); + } + + // return the amount of chunks downloaded per validator + pub fn get_download_chunks_metrics(&self) -> HashMap { + self.chunks_received_by + .iter() + .map(|(validator, downloaded)| (*validator, downloaded.count())) + .collect() + } + fn chunk_count(&self) -> usize { self.received_chunks.len() } @@ -467,6 +485,8 @@ impl State { ) -> (usize, usize) { let metrics = ¶ms.metrics; + let mut received_chunks: HashMap = HashMap::new(); + let mut total_received_responses = 0; let mut error_count = 0; @@ -506,6 +526,7 @@ impl State { chunk.index, Chunk { chunk: chunk.chunk, validator_index }, ); + self.note_received_chunk(validator_index, chunk.index) } else { metrics.on_chunk_request_invalid(strategy_type); error_count += 1; diff --git a/polkadot/node/network/availability-recovery/src/task/strategy/systematic.rs b/polkadot/node/network/availability-recovery/src/task/strategy/systematic.rs index 8b8cff549912e..206bf2d05372c 100644 --- a/polkadot/node/network/availability-recovery/src/task/strategy/systematic.rs +++ b/polkadot/node/network/availability-recovery/src/task/strategy/systematic.rs @@ -31,6 +31,7 @@ use polkadot_node_subsystem::{overseer, RecoveryError}; use polkadot_primitives::{ChunkIndex, ValidatorIndex}; use std::collections::VecDeque; +use polkadot_node_subsystem::messages::ConsensusStatisticsCollectorMessage; /// Parameters needed for fetching systematic chunks. pub struct FetchSystematicChunksParams { diff --git a/polkadot/node/overseer/src/lib.rs b/polkadot/node/overseer/src/lib.rs index 8544caa84dbca..20187b7a56832 100644 --- a/polkadot/node/overseer/src/lib.rs +++ b/polkadot/node/overseer/src/lib.rs @@ -85,7 +85,7 @@ use polkadot_node_subsystem_types::messages::{ DisputeCoordinatorMessage, DisputeDistributionMessage, GossipSupportMessage, NetworkBridgeRxMessage, NetworkBridgeTxMessage, ProspectiveParachainsMessage, ProvisionerMessage, RuntimeApiMessage, StatementDistributionMessage, - StatisticsCollectorMessage, + ConsensusStatisticsCollectorMessage, }; pub use polkadot_node_subsystem_types::{ @@ -659,7 +659,7 @@ pub struct Overseer { ])] prospective_parachains: ProspectiveParachains, - #[subsystem(StatisticsCollectorMessage, sends: [])] + #[subsystem(ConsensusStatisticsCollectorMessage, sends: [])] statistics_collector: StatisticsCollector, /// External listeners waiting for a hash to be in the active-leave set. diff --git a/polkadot/node/subsystem-types/src/messages.rs b/polkadot/node/subsystem-types/src/messages.rs index 5e8b1cd18e746..f0b20479ee561 100644 --- a/polkadot/node/subsystem-types/src/messages.rs +++ b/polkadot/node/subsystem-types/src/messages.rs @@ -1463,7 +1463,9 @@ pub enum ProspectiveParachainsMessage { /// Messages sent to the Statistics Collector subsystem. #[derive(Debug)] -pub enum StatisticsCollectorMessage { +pub enum ConsensusStatisticsCollectorMessage { + ChunksDownloaded(HashMap), + // Candidate received enough approval and now is approved CandidateApproved(CandidateHash, Hash, Vec), From 8bae0a87cab95771d38b250b660d891dcaf77413 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Wed, 17 Sep 2025 13:49:24 -0400 Subject: [PATCH 06/48] chore: include `handle_chunks_downloaded` per session and candidate --- .../src/availability_distribution_metrics.rs | 47 +++++++++++++++++-- .../consensus-statistics-collector/src/lib.rs | 17 +++++-- .../network/availability-recovery/src/lib.rs | 2 +- .../availability-recovery/src/task/mod.rs | 7 ++- polkadot/node/subsystem-types/src/messages.rs | 2 +- 5 files changed, 65 insertions(+), 10 deletions(-) diff --git a/polkadot/node/core/consensus-statistics-collector/src/availability_distribution_metrics.rs b/polkadot/node/core/consensus-statistics-collector/src/availability_distribution_metrics.rs index 26a54dcc7c35f..5cfaaa10a9cd1 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/availability_distribution_metrics.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/availability_distribution_metrics.rs @@ -1,10 +1,51 @@ -use polkadot_primitives::ValidatorIndex; +use std::collections::{HashMap, HashSet}; +use std::ops::Add; +use gum::CandidateHash; +use polkadot_primitives::{SessionIndex, ValidatorIndex}; use crate::View; +pub struct ChunksDownloaded { + pub per_candidate: HashMap>, +} + +impl ChunksDownloaded { + pub fn new() -> Self { + Self { + per_candidate: Default::default(), + } + } + + pub fn note_candidate_chunk_downloaded( + &mut self, + hash: CandidateHash, + validator_index: ValidatorIndex, + count: u64, + ) { + self.per_candidate + .entry(hash) + .or_default() + .entry(validator_index) + .or_default() + .add(count); + } +} + +// whenever chunks are acquired throughout availability +// recovery we collect the metrics about what validator +// provided and the amount of chunks pub fn handle_chunks_downloaded( view: &mut View, - validator: ValidatorIndex, - downloaded: u64, + session_index: SessionIndex, + candidate_hash: CandidateHash, + downloads: HashMap, ) { + view.candidates_per_session + .entry(session_index) + .or_default() + .insert(candidate_hash); + for (validator_index, download_count) in downloads { + view.chunks_downloaded + .note_candidate_chunk_downloaded(candidate_hash, validator_index, download_count) + } } \ No newline at end of file diff --git a/polkadot/node/core/consensus-statistics-collector/src/lib.rs b/polkadot/node/core/consensus-statistics-collector/src/lib.rs index 02ff003e7f37c..50f7084b6de6b 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/lib.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/lib.rs @@ -47,6 +47,7 @@ mod availability_distribution_metrics; use approval_voting_metrics::ApprovalsStats; use crate::approval_voting_metrics::{handle_candidate_approved, handle_observed_no_shows}; +use crate::availability_distribution_metrics::{handle_chunks_downloaded, ChunksDownloaded}; use self::metrics::Metrics; const LOG_TARGET: &str = "parachain::consensus-statistics-collector"; @@ -68,7 +69,8 @@ impl PerRelayView { struct View { per_relay: HashMap, no_shows_per_session: HashMap>, - chunks_downloaded: HashMap, + candidates_per_session: HashMap>, + chunks_downloaded: ChunksDownloaded, } impl View { @@ -76,7 +78,8 @@ impl View { return View{ per_relay: HashMap::new(), no_shows_per_session: HashMap::new(), - chunks_downloaded: HashMap::new(), + candidates_per_session: HashMap::new(), + chunks_downloaded: ChunksDownloaded::new(), }; } } @@ -137,8 +140,14 @@ pub(crate) async fn run_iteration( FromOrchestra::Signal(OverseerSignal::BlockFinalized(..)) => {}, FromOrchestra::Communication { msg } => { match msg { - ConsensusStatisticsCollectorMessage::ChunksDownloaded(_, _)=> { - + ConsensusStatisticsCollectorMessage::ChunksDownloaded( + session_index, candidate_hash, downloads)=> { + handle_chunks_downloaded( + view, + session_index, + candidate_hash, + downloads, + ) }, ConsensusStatisticsCollectorMessage::CandidateApproved(candidate_hash, block_hash, approvals) => { handle_candidate_approved( diff --git a/polkadot/node/network/availability-recovery/src/lib.rs b/polkadot/node/network/availability-recovery/src/lib.rs index 0f7d961e9f3ed..69601be1154ca 100644 --- a/polkadot/node/network/availability-recovery/src/lib.rs +++ b/polkadot/node/network/availability-recovery/src/lib.rs @@ -528,13 +528,13 @@ async fn handle_recover( let session_info = session_info.clone(); let n_validators = session_info.validators.len(); - launch_recovery_task( state, ctx, response_sender, recovery_strategies, RecoveryParams { + session_index: session_index, validator_authority_keys: session_info.discovery_keys.clone(), n_validators, threshold: recovery_threshold(n_validators)?, diff --git a/polkadot/node/network/availability-recovery/src/task/mod.rs b/polkadot/node/network/availability-recovery/src/task/mod.rs index f6ae5da461e68..8bc5ccad9aeb7 100644 --- a/polkadot/node/network/availability-recovery/src/task/mod.rs +++ b/polkadot/node/network/availability-recovery/src/task/mod.rs @@ -33,7 +33,7 @@ use crate::{metrics::Metrics, ErasureTask, PostRecoveryCheck, LOG_TARGET}; use codec::Encode; use polkadot_node_primitives::AvailableData; use polkadot_node_subsystem::{messages::AvailabilityStoreMessage, overseer, RecoveryError}; -use polkadot_primitives::{AuthorityDiscoveryId, CandidateHash, Hash}; +use polkadot_primitives::{AuthorityDiscoveryId, CandidateHash, Hash, SessionIndex}; use sc_network::ProtocolName; use futures::channel::{mpsc, oneshot}; @@ -43,6 +43,9 @@ use polkadot_node_subsystem::messages::ConsensusStatisticsCollectorMessage; /// Recovery parameters common to all strategies in a `RecoveryTask`. #[derive(Clone)] pub struct RecoveryParams { + /// Session index where the validators belong to + pub session_index: SessionIndex, + /// Discovery ids of `validators`. pub validator_authority_keys: Vec, @@ -181,6 +184,8 @@ where self.params.metrics.on_recovery_succeeded(strategy_type, data.encoded_size()); _ = self.sender.try_send_message( ConsensusStatisticsCollectorMessage::ChunksDownloaded( + self.params.session_index, + self.params.candidate_hash, self.state.get_download_chunks_metrics())); return Ok(data) }, diff --git a/polkadot/node/subsystem-types/src/messages.rs b/polkadot/node/subsystem-types/src/messages.rs index f0b20479ee561..9b02a00698ea8 100644 --- a/polkadot/node/subsystem-types/src/messages.rs +++ b/polkadot/node/subsystem-types/src/messages.rs @@ -1464,7 +1464,7 @@ pub enum ProspectiveParachainsMessage { /// Messages sent to the Statistics Collector subsystem. #[derive(Debug)] pub enum ConsensusStatisticsCollectorMessage { - ChunksDownloaded(HashMap), + ChunksDownloaded(SessionIndex, CandidateHash, HashMap), // Candidate received enough approval and now is approved CandidateApproved(CandidateHash, Hash, Vec), From b7c47ace6e2de05c4ef96bacd292740a8a5c1fb9 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Thu, 18 Sep 2025 16:13:49 -0400 Subject: [PATCH 07/48] address comments --- polkadot/node/core/approval-voting/src/lib.rs | 102 +++++++++--------- .../src/availability_distribution_metrics.rs | 12 +-- .../consensus-statistics-collector/src/lib.rs | 8 +- .../src/task/strategy/full.rs | 1 + polkadot/node/subsystem-types/src/messages.rs | 5 +- 5 files changed, 63 insertions(+), 65 deletions(-) diff --git a/polkadot/node/core/approval-voting/src/lib.rs b/polkadot/node/core/approval-voting/src/lib.rs index 893e44e830b19..d3663d74f9a3f 100644 --- a/polkadot/node/core/approval-voting/src/lib.rs +++ b/polkadot/node/core/approval-voting/src/lib.rs @@ -2924,7 +2924,7 @@ where gum::trace!( target: LOG_TARGET, "Received approval for num_candidates {:}", - approval.candidate_indices.clone().count_ones() + approval.candidate_indices.count_ones() ); let mut actions = Vec::new(); @@ -3094,14 +3094,6 @@ where let is_approved = check.is_approved(tick_now.saturating_sub(APPROVAL_DELAY)); if status.last_no_shows != 0 { metrics.on_observed_no_shows(status.last_no_shows); - sender - .send_message( - ConsensusStatisticsCollectorMessage::ObservedNoShows( - session_index, - status.no_show_validators.clone(), - )) - .await; - gum::trace!( target: LOG_TARGET, ?candidate_hash, @@ -3174,48 +3166,12 @@ where } if newly_approved { state.record_no_shows(session_index, para_id.into(), &status.no_show_validators); - let mut collected_useful_approvals = vec![]; - - match status.required_tranches { - RequiredTranches::All => { - collected_useful_approvals.extend(candidate_entry.approvals().iter_ones().into()); - }, - RequiredTranches::Exact {needed, ..} => { - let tranches = approval_entry.tranches(); - for validator_approval_idx in candidate_entry.approvals().iter_ones() { - for tranche_entry in tranches { - if tranche_entry.tranche() <= needed { - let trance_assignments = tranche_entry.assignments(); - let useful_vote = trance_assignments - .iter() - .find(|(validator_on_tranche, _)| *validator_on_tranche == ValidatorIndex(validator_approval_idx as _)); - - match useful_vote { - Some((vidx, _)) => collected_useful_approvals.push(*vidx), - // Found no useful votes on a needed tranche - None => {} - } - } - } - } - }, - RequiredTranches::Pending {..} => panic!("Newly approved candidate should never be pending; qed"), - } - - if collected_useful_approvals.len() > 0 { - _ = sender.try_send_message(ConsensusStatisticsCollectorMessage::CandidateApproved( - candidate_hash, - block_hash, - collected_useful_approvals, - )).map_err(|_| { - gum::warn!( - target: LOG_TARGET, - ?candidate_hash, - ?block_hash, - "Failed to send approvals to statistics subsystem", - ); - }); - } + collect_useful_approvals(sender, &status, block_hash, &candidate_entry, &approval_entry); + _ = sender + .try_send_message(ConsensusStatisticsCollectorMessage::NoShows( + session_index, + status.no_show_validators, + )); } actions.extend(schedule_wakeup_action( @@ -4099,3 +4055,47 @@ fn compute_delayed_approval_sending_tick( metrics.on_delayed_approval(sign_no_later_than.checked_sub(tick_now).unwrap_or_default()); sign_no_later_than } + +// collect all the approvals required to approve the +// candidate, ignoring any other approval that belongs +// to not required tranches +fn collect_useful_approvals( + sender: &mut Sender, + status: &ApprovalStatus, + block_hash: Hash, + candidate_entry: &CandidateEntry, + approval_entry: &ApprovalEntry, +) +where + Sender: SubsystemSender +{ + let candidate_hash = candidate_entry.candidate.hash(); + let candidate_approvals = candidate_entry.approvals(); + + let collected_useful_approvals: Vec = match status.required_tranches { + RequiredTranches::All => { + candidate_approvals.iter_ones().into() + }, + RequiredTranches::Exact {needed, ..} => { + let mut assigned_mask = approval_entry.assignments_up_to(needed); + assigned_mask &= candidate_approvals; + assigned_mask.iter_ones().into() + }, + RequiredTranches::Pending {..} => panic!("Newly approved candidate should never be pending; qed"), + }; + + if collected_useful_approvals.len() > 0 { + _ = sender.try_send_message(ConsensusStatisticsCollectorMessage::CandidateApproved( + candidate_hash, + block_hash, + collected_useful_approvals, + )).map_err(|_| { + gum::warn!( + target: LOG_TARGET, + ?candidate_hash, + ?block_hash, + "Failed to send approvals to statistics subsystem", + ); + }); + } +} \ No newline at end of file diff --git a/polkadot/node/core/consensus-statistics-collector/src/availability_distribution_metrics.rs b/polkadot/node/core/consensus-statistics-collector/src/availability_distribution_metrics.rs index 5cfaaa10a9cd1..15e27f4fe2569 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/availability_distribution_metrics.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/availability_distribution_metrics.rs @@ -4,24 +4,24 @@ use gum::CandidateHash; use polkadot_primitives::{SessionIndex, ValidatorIndex}; use crate::View; -pub struct ChunksDownloaded { - pub per_candidate: HashMap>, +pub struct AvailabilityDownloads { + pub chunks_per_candidate: HashMap>, } -impl ChunksDownloaded { +impl AvailabilityDownloads { pub fn new() -> Self { Self { - per_candidate: Default::default(), + chunks_per_candidate: Default::default(), } } - + pub fn note_candidate_chunk_downloaded( &mut self, hash: CandidateHash, validator_index: ValidatorIndex, count: u64, ) { - self.per_candidate + self.chunks_per_candidate .entry(hash) .or_default() .entry(validator_index) diff --git a/polkadot/node/core/consensus-statistics-collector/src/lib.rs b/polkadot/node/core/consensus-statistics-collector/src/lib.rs index 50f7084b6de6b..36ea921f68c9f 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/lib.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/lib.rs @@ -47,7 +47,7 @@ mod availability_distribution_metrics; use approval_voting_metrics::ApprovalsStats; use crate::approval_voting_metrics::{handle_candidate_approved, handle_observed_no_shows}; -use crate::availability_distribution_metrics::{handle_chunks_downloaded, ChunksDownloaded}; +use crate::availability_distribution_metrics::{handle_chunks_downloaded, AvailabilityDownloads}; use self::metrics::Metrics; const LOG_TARGET: &str = "parachain::consensus-statistics-collector"; @@ -70,7 +70,7 @@ struct View { per_relay: HashMap, no_shows_per_session: HashMap>, candidates_per_session: HashMap>, - chunks_downloaded: ChunksDownloaded, + chunks_downloaded: AvailabilityDownloads, } impl View { @@ -79,7 +79,7 @@ impl View { per_relay: HashMap::new(), no_shows_per_session: HashMap::new(), candidates_per_session: HashMap::new(), - chunks_downloaded: ChunksDownloaded::new(), + chunks_downloaded: AvailabilityDownloads::new(), }; } } @@ -157,7 +157,7 @@ pub(crate) async fn run_iteration( approvals, ); } - ConsensusStatisticsCollectorMessage::ObservedNoShows(session_idx, no_show_validators) => { + ConsensusStatisticsCollectorMessage::NoShows(session_idx, no_show_validators) => { handle_observed_no_shows( view, session_idx, diff --git a/polkadot/node/network/availability-recovery/src/task/strategy/full.rs b/polkadot/node/network/availability-recovery/src/task/strategy/full.rs index 1d7fbe8ea3c8d..4869806e0a503 100644 --- a/polkadot/node/network/availability-recovery/src/task/strategy/full.rs +++ b/polkadot/node/network/availability-recovery/src/task/strategy/full.rs @@ -126,6 +126,7 @@ impl RecoveryStrategy ); common_params.metrics.on_full_request_succeeded(); + return Ok(data) }, None => { diff --git a/polkadot/node/subsystem-types/src/messages.rs b/polkadot/node/subsystem-types/src/messages.rs index 9b02a00698ea8..ae102de063fcd 100644 --- a/polkadot/node/subsystem-types/src/messages.rs +++ b/polkadot/node/subsystem-types/src/messages.rs @@ -1470,8 +1470,5 @@ pub enum ConsensusStatisticsCollectorMessage { CandidateApproved(CandidateHash, Hash, Vec), // Set of candidates that has not shared votes in time - ObservedNoShows(SessionIndex, Vec), - - // All relay block's candidates are approved, therefore relay block is approved - RelayBlockApproved(Hash) + NoShows(SessionIndex, Vec), } \ No newline at end of file From d83075a48c1388f6a5e513566e221ce40128768e Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Fri, 19 Sep 2025 08:59:01 -0400 Subject: [PATCH 08/48] chore: revert changes in chunks.rs --- .../src/task/strategy/chunks.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/polkadot/node/network/availability-recovery/src/task/strategy/chunks.rs b/polkadot/node/network/availability-recovery/src/task/strategy/chunks.rs index 27166bb0272a2..2f6e191125eed 100644 --- a/polkadot/node/network/availability-recovery/src/task/strategy/chunks.rs +++ b/polkadot/node/network/availability-recovery/src/task/strategy/chunks.rs @@ -167,6 +167,14 @@ impl FetchChunks { #[async_trait::async_trait] impl RecoveryStrategy for FetchChunks { + fn display_name(&self) -> &'static str { + "Fetch chunks" + } + + fn strategy_type(&self) -> &'static str { + "regular_chunks" + } + async fn run( mut self: Box, state: &mut State, @@ -287,14 +295,6 @@ impl RecoveryStrategy self.error_count += error_count; } } - - fn display_name(&self) -> &'static str { - "Fetch chunks" - } - - fn strategy_type(&self) -> &'static str { - "regular_chunks" - } } #[cfg(test)] From e4e00dc5eb552d526f8c91935a0caa3fecbc7183 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Wed, 24 Sep 2025 15:21:52 -0400 Subject: [PATCH 09/48] chore: fix approval voting tests --- Cargo.lock | 44 ++++++------- polkadot/node/core/approval-voting/src/lib.rs | 64 +++++++++++++------ .../node/core/approval-voting/src/tests.rs | 64 +++++++++++++++++++ .../availability-recovery/src/task/mod.rs | 5 +- .../src/task/strategy/mod.rs | 2 +- polkadot/node/overseer/src/dummy.rs | 5 +- polkadot/node/overseer/src/lib.rs | 5 +- polkadot/node/service/Cargo.toml | 2 +- polkadot/node/service/src/overseer.rs | 3 +- .../subsystem-bench/src/lib/mock/dummy.rs | 1 + .../node/subsystem-bench/src/lib/mock/mod.rs | 1 + 11 files changed, 145 insertions(+), 51 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 15b7933d99f48..c7f82acaf1eb5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15411,6 +15411,27 @@ dependencies = [ "tracing-gum", ] +[[package]] +name = "polkadot-node-core-consensus-statistics-collector" +version = "6.0.0" +dependencies = [ + "assert_matches", + "fatality", + "futures", + "polkadot-node-primitives", + "polkadot-node-subsystem", + "polkadot-node-subsystem-test-helpers", + "polkadot-node-subsystem-util", + "polkadot-primitives", + "polkadot-primitives-test-helpers", + "rand 0.8.5", + "rstest", + "sp-core 28.0.0", + "sp-tracing 16.0.0", + "thiserror 1.0.65", + "tracing-gum", +] + [[package]] name = "polkadot-node-core-dispute-coordinator" version = "7.0.0" @@ -15642,27 +15663,6 @@ dependencies = [ "tracing-gum", ] -[[package]] -name = "polkadot-node-core-statistics-collector" -version = "6.0.0" -dependencies = [ - "assert_matches", - "fatality", - "futures", - "polkadot-node-primitives", - "polkadot-node-subsystem", - "polkadot-node-subsystem-test-helpers", - "polkadot-node-subsystem-util", - "polkadot-primitives", - "polkadot-primitives-test-helpers", - "rand 0.8.5", - "rstest", - "sp-core 28.0.0", - "sp-tracing 16.0.0", - "thiserror 1.0.65", - "tracing-gum", -] - [[package]] name = "polkadot-node-metrics" version = "7.0.0" @@ -16781,6 +16781,7 @@ dependencies = [ "polkadot-node-core-candidate-validation", "polkadot-node-core-chain-api", "polkadot-node-core-chain-selection", + "polkadot-node-core-consensus-statistics-collector", "polkadot-node-core-dispute-coordinator", "polkadot-node-core-parachains-inherent", "polkadot-node-core-prospective-parachains", @@ -16788,7 +16789,6 @@ dependencies = [ "polkadot-node-core-pvf", "polkadot-node-core-pvf-checker", "polkadot-node-core-runtime-api", - "polkadot-node-core-statistics-collector", "polkadot-node-network-protocol", "polkadot-node-primitives", "polkadot-node-subsystem", diff --git a/polkadot/node/core/approval-voting/src/lib.rs b/polkadot/node/core/approval-voting/src/lib.rs index d3663d74f9a3f..9c49a4a7ac991 100644 --- a/polkadot/node/core/approval-voting/src/lib.rs +++ b/polkadot/node/core/approval-voting/src/lib.rs @@ -96,6 +96,7 @@ use polkadot_node_primitives::approval::time::{ slot_number_to_tick, Clock, ClockExt, DelayedApprovalTimer, SystemClock, Tick, }; use polkadot_node_subsystem::messages::ConsensusStatisticsCollectorMessage; +use polkadot_overseer::Subsystem; mod approval_checking; pub mod approval_db; @@ -1229,6 +1230,7 @@ async fn run< Sender: SubsystemSender + SubsystemSender + SubsystemSender + + SubsystemSender + SubsystemSender + SubsystemSender + SubsystemSender @@ -1464,6 +1466,7 @@ pub async fn start_approval_worker< + SubsystemSender + SubsystemSender + SubsystemSender + + SubsystemSender + SubsystemSender + SubsystemSender + Clone, @@ -1543,6 +1546,7 @@ async fn handle_actions< + SubsystemSender + SubsystemSender + SubsystemSender + + SubsystemSender + Clone, ADSender: SubsystemSender, >( @@ -2007,6 +2011,7 @@ async fn handle_from_overseer< Sender: SubsystemSender + SubsystemSender + SubsystemSender + + SubsystemSender + Clone, ADSender: SubsystemSender, >( @@ -2884,8 +2889,8 @@ async fn import_approval( wakeups: &Wakeups, ) -> SubsystemResult<(Vec, ApprovalCheckResult)> where - Sender: SubsystemSender + - SubsystemSender, + Sender: SubsystemSender + + SubsystemSender, { macro_rules! respond_early { ($e: expr) => {{ @@ -3038,8 +3043,8 @@ async fn advance_approval_state( wakeups: &Wakeups, ) -> Vec where - Sender: SubsystemSender + - SubsystemSender, + Sender: SubsystemSender + + SubsystemSender, { let validator_index = transition.validator_index(); @@ -3153,7 +3158,7 @@ where return Vec::new() }; - { + let newly_approved = { let approval_entry = candidate_entry .approval_entry_mut(&block_hash) .expect("Approval entry just fetched; qed"); @@ -3164,14 +3169,9 @@ where if is_approved { approval_entry.mark_approved(); } + if newly_approved { state.record_no_shows(session_index, para_id.into(), &status.no_show_validators); - collect_useful_approvals(sender, &status, block_hash, &candidate_entry, &approval_entry); - _ = sender - .try_send_message(ConsensusStatisticsCollectorMessage::NoShows( - session_index, - status.no_show_validators, - )); } actions.extend(schedule_wakeup_action( @@ -3179,9 +3179,9 @@ where block_hash, block_number, candidate_hash, - status.block_tick, + status.block_tick.clone(), tick_now, - status.required_tranches, + status.required_tranches.clone(), )); if is_approved && transition.is_remote_approval() { @@ -3221,6 +3221,8 @@ where } } } + + // We have no need to write the candidate entry if all of the following // is true: // @@ -3232,7 +3234,21 @@ where if transition.is_local_approval() || newly_approved || !already_approved_by.unwrap_or(true) { // In all other cases, we need to write the candidate entry. - db.write_candidate_entry(candidate_entry); + db.write_candidate_entry(candidate_entry.clone()); + } + + newly_approved + }; + + if newly_approved { + collect_useful_approvals(sender, &status, block_hash, &candidate_entry); + + if status.no_show_validators.len() > 0 { + _ = sender + .try_send_message(ConsensusStatisticsCollectorMessage::NoShows( + session_index, + status.no_show_validators, + )); } } @@ -3273,7 +3289,7 @@ fn should_trigger_assignment( } } -async fn process_wakeup>( +async fn process_wakeup( sender: &mut Sender, state: &mut State, db: &mut OverlayedBackend<'_, impl Backend>, @@ -3282,7 +3298,11 @@ async fn process_wakeup>( candidate_hash: CandidateHash, metrics: &Metrics, wakeups: &Wakeups, -) -> SubsystemResult> { +) -> SubsystemResult> +where + Sender: SubsystemSender + + SubsystemSender +{ let block_entry = db.load_block_entry(&relay_block)?; let candidate_entry = db.load_candidate_entry(&candidate_hash)?; @@ -3683,7 +3703,8 @@ async fn launch_approval< // have been done. #[overseer::contextbounds(ApprovalVoting, prefix = self::overseer)] async fn issue_approval< - Sender: SubsystemSender, + Sender: SubsystemSender + + SubsystemSender, ADSender: SubsystemSender, >( sender: &mut Sender, @@ -4064,7 +4085,6 @@ fn collect_useful_approvals( status: &ApprovalStatus, block_hash: Hash, candidate_entry: &CandidateEntry, - approval_entry: &ApprovalEntry, ) where Sender: SubsystemSender @@ -4072,14 +4092,18 @@ where let candidate_hash = candidate_entry.candidate.hash(); let candidate_approvals = candidate_entry.approvals(); + let approval_entry = candidate_entry + .approval_entry(&block_hash) + .expect("Approval entry just fetched; qed"); + let collected_useful_approvals: Vec = match status.required_tranches { RequiredTranches::All => { - candidate_approvals.iter_ones().into() + candidate_approvals.iter_ones().map(|idx| ValidatorIndex(idx as _)).collect() }, RequiredTranches::Exact {needed, ..} => { let mut assigned_mask = approval_entry.assignments_up_to(needed); assigned_mask &= candidate_approvals; - assigned_mask.iter_ones().into() + assigned_mask.iter_ones().map(|idx| ValidatorIndex(idx as _)).collect() }, RequiredTranches::Pending {..} => panic!("Newly approved candidate should never be pending; qed"), }; diff --git a/polkadot/node/core/approval-voting/src/tests.rs b/polkadot/node/core/approval-voting/src/tests.rs index 8850f437c1231..9ef16af6aaa8c 100644 --- a/polkadot/node/core/approval-voting/src/tests.rs +++ b/polkadot/node/core/approval-voting/src/tests.rs @@ -658,6 +658,7 @@ async fn import_approval( candidate_hash: CandidateHash, session_index: SessionIndex, expect_chain_approved: bool, + expected_approval_validators: Vec, signature_opt: Option, ) -> oneshot::Receiver { let signature = signature_opt.unwrap_or(sign_approval( @@ -681,7 +682,21 @@ async fn import_approval( }, ) .await; + if expect_chain_approved { + assert_matches!( + overseer_recv(overseer).await, + AllMessages::ConsensusStatisticsCollector( + ConsensusStatisticsCollectorMessage::CandidateApproved( + c_hash, b_hash, validators, + ) + ) => { + assert_eq!(b_hash, block_hash); + assert_eq!(c_hash, candidate_hash); + assert_eq!(validators, expected_approval_validators); + } + ); + assert_matches!( overseer_recv(overseer).await, AllMessages::ChainSelection(ChainSelectionMessage::Approved(b_hash)) => { @@ -1259,6 +1274,7 @@ fn subsystem_rejects_approval_if_no_candidate_entry() { candidate_hash, session_index, false, + vec![], None, ) .await; @@ -1300,6 +1316,7 @@ fn subsystem_rejects_approval_if_no_block_entry() { candidate_hash, session_index, false, + vec![], None, ) .await; @@ -1365,6 +1382,7 @@ fn subsystem_rejects_approval_before_assignment() { candidate_hash, session_index, false, + vec![], None, ) .await; @@ -1621,6 +1639,7 @@ fn subsystem_accepts_and_imports_approval_after_assignment() { candidate_hash, session_index, true, + vec![], None, ) .await; @@ -1711,6 +1730,7 @@ fn subsystem_second_approval_import_only_schedules_wakeups() { candidate_hash, session_index, false, + vec![], None, ) .await; @@ -1728,6 +1748,7 @@ fn subsystem_second_approval_import_only_schedules_wakeups() { candidate_hash, session_index, false, + vec![], None, ) .await; @@ -1945,6 +1966,7 @@ fn test_approvals_on_fork_are_always_considered_after_no_show( candidate_hash, 1, false, + vec![], None, ) .await; @@ -2339,6 +2361,7 @@ fn import_checked_approval_updates_entries_and_schedules() { candidate_hash, session_index, false, + vec![], Some(sig_a), ) .await; @@ -2366,6 +2389,7 @@ fn import_checked_approval_updates_entries_and_schedules() { candidate_hash, session_index, true, + vec![validator_index_b], Some(sig_b), ) .await; @@ -2502,6 +2526,7 @@ fn subsystem_import_checked_approval_sets_one_block_bit_at_a_time() { *candidate_hash, session_index, expect_block_approved, + vec![validator1, validator2], Some(signature), ) .await; @@ -2774,6 +2799,7 @@ fn approved_ancestor_test( candidate_hash, i as u32 + 1, true, + vec![validator], None, ) .await; @@ -3337,7 +3363,19 @@ where assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted)); } + // 10 + // 0..10 + // 3 * (i + 1) + // 0 => 3 + // 1 => 6 + // 2 => 9 + // 3 => 12 let n_validators = validators.len(); + let validators_collected = approvals_to_import[0..=n_validators/3] + .iter() + .map(|vidx| ValidatorIndex(vidx.clone())) + .collect::>(); + for (i, &validator_index) in approvals_to_import.iter().enumerate() { let expect_chain_approved = 3 * (i + 1) > n_validators; let rx = import_approval( @@ -3348,6 +3386,7 @@ where candidate_hash, 1, expect_chain_approved, + validators_collected.clone(), Some(sign_approval(validators[validator_index as usize], candidate_hash, 1)), ) .await; @@ -3689,6 +3728,7 @@ fn pre_covers_dont_stall_approval() { candidate_hash, session_index, false, + vec![], Some(sig_b), ) .await; @@ -3704,6 +3744,7 @@ fn pre_covers_dont_stall_approval() { candidate_hash, session_index, false, + vec![], Some(sig_c), ) .await; @@ -3740,6 +3781,27 @@ fn pre_covers_dont_stall_approval() { assert_eq!(clock.inner.lock().next_wakeup(), Some(31)); clock.inner.lock().set_tick(31); + assert_matches!( + overseer_recv(&mut virtual_overseer).await, + AllMessages::ConsensusStatisticsCollector(ConsensusStatisticsCollectorMessage::CandidateApproved( + c_hash, b_hash, validators, + )) => { + assert_eq!(b_hash, block_hash); + assert_eq!(c_hash, candidate_hash); + assert_eq!(validators, vec![validator_index_b, validator_index_c]); + } + ); + + assert_matches!( + overseer_recv(&mut virtual_overseer).await, + AllMessages::ConsensusStatisticsCollector(ConsensusStatisticsCollectorMessage::NoShows( + session_idx, validators, + )) => { + assert_eq!(session_idx, 1); + assert_eq!(validators, vec![validator_index_a]); + } + ); + assert_matches!( overseer_recv(&mut virtual_overseer).await, AllMessages::ChainSelection(ChainSelectionMessage::Approved(b_hash)) => { @@ -3861,6 +3923,7 @@ fn waits_until_approving_assignments_are_old_enough() { candidate_hash, session_index, false, + vec![], Some(sig_a), ) .await; @@ -3877,6 +3940,7 @@ fn waits_until_approving_assignments_are_old_enough() { candidate_hash, session_index, false, + vec![], Some(sig_b), ) .await; diff --git a/polkadot/node/network/availability-recovery/src/task/mod.rs b/polkadot/node/network/availability-recovery/src/task/mod.rs index 8bc5ccad9aeb7..e5b3a7d8bd20f 100644 --- a/polkadot/node/network/availability-recovery/src/task/mod.rs +++ b/polkadot/node/network/availability-recovery/src/task/mod.rs @@ -32,7 +32,7 @@ use crate::{metrics::Metrics, ErasureTask, PostRecoveryCheck, LOG_TARGET}; use codec::Encode; use polkadot_node_primitives::AvailableData; -use polkadot_node_subsystem::{messages::AvailabilityStoreMessage, overseer, RecoveryError}; +use polkadot_node_subsystem::{messages::AvailabilityStoreMessage, overseer, RecoveryError, Subsystem, SubsystemSender}; use polkadot_primitives::{AuthorityDiscoveryId, CandidateHash, Hash, SessionIndex}; use sc_network::ProtocolName; @@ -100,7 +100,8 @@ pub struct RecoveryTask { impl RecoveryTask where - Sender: overseer::AvailabilityRecoverySenderTrait, + Sender: overseer::AvailabilityRecoverySenderTrait + + SubsystemSender, { /// Instantiate a new recovery task. pub fn new( diff --git a/polkadot/node/network/availability-recovery/src/task/strategy/mod.rs b/polkadot/node/network/availability-recovery/src/task/strategy/mod.rs index 33e55464b78b5..677c7a38852d8 100644 --- a/polkadot/node/network/availability-recovery/src/task/strategy/mod.rs +++ b/polkadot/node/network/availability-recovery/src/task/strategy/mod.rs @@ -236,7 +236,7 @@ impl State { pub fn get_download_chunks_metrics(&self) -> HashMap { self.chunks_received_by .iter() - .map(|(validator, downloaded)| (*validator, downloaded.count())) + .map(|(validator, downloaded)| (*validator, downloaded.len() as u64)) .collect() } diff --git a/polkadot/node/overseer/src/dummy.rs b/polkadot/node/overseer/src/dummy.rs index c8147bd093967..7584e35b748cf 100644 --- a/polkadot/node/overseer/src/dummy.rs +++ b/polkadot/node/overseer/src/dummy.rs @@ -165,7 +165,8 @@ where + Subsystem, SubsystemError> + Subsystem, SubsystemError> + Subsystem, SubsystemError> - + Subsystem, SubsystemError>, + + Subsystem, SubsystemError> + + Subsystem, SubsystemError>, { let metrics = ::register(registry)?; @@ -194,7 +195,7 @@ where .dispute_distribution(subsystem.clone()) .chain_selection(subsystem.clone()) .prospective_parachains(subsystem.clone()) - .statistics_collector(subsystem.clone()) + .consensus_statistics_collector(subsystem.clone()) .activation_external_listeners(Default::default()) .active_leaves(Default::default()) .spawner(SpawnGlue(spawner)) diff --git a/polkadot/node/overseer/src/lib.rs b/polkadot/node/overseer/src/lib.rs index 20187b7a56832..41ae7ccbbd4c5 100644 --- a/polkadot/node/overseer/src/lib.rs +++ b/polkadot/node/overseer/src/lib.rs @@ -528,6 +528,7 @@ pub struct Overseer { NetworkBridgeTxMessage, RuntimeApiMessage, AvailabilityStoreMessage, + ConsensusStatisticsCollectorMessage, ])] availability_recovery: AvailabilityRecovery, @@ -607,6 +608,7 @@ pub struct Overseer { CandidateValidationMessage, ChainApiMessage, ChainSelectionMessage, + ConsensusStatisticsCollectorMessage, DisputeCoordinatorMessage, RuntimeApiMessage, ])] @@ -616,6 +618,7 @@ pub struct Overseer { CandidateValidationMessage, ChainApiMessage, ChainSelectionMessage, + ConsensusStatisticsCollectorMessage, DisputeCoordinatorMessage, RuntimeApiMessage, NetworkBridgeTxMessage, @@ -660,7 +663,7 @@ pub struct Overseer { prospective_parachains: ProspectiveParachains, #[subsystem(ConsensusStatisticsCollectorMessage, sends: [])] - statistics_collector: StatisticsCollector, + consensus_statistics_collector: ConsensusStatisticsCollector, /// External listeners waiting for a hash to be in the active-leave set. pub activation_external_listeners: HashMap>>>, diff --git a/polkadot/node/service/Cargo.toml b/polkadot/node/service/Cargo.toml index de3e69ccb82db..be78a8850973d 100644 --- a/polkadot/node/service/Cargo.toml +++ b/polkadot/node/service/Cargo.toml @@ -132,7 +132,7 @@ polkadot-node-core-pvf = { optional = true, workspace = true, default-features = polkadot-node-core-pvf-checker = { optional = true, workspace = true, default-features = true } polkadot-node-core-runtime-api = { optional = true, workspace = true, default-features = true } polkadot-statement-distribution = { optional = true, workspace = true, default-features = true } -polkadot-node-core-statistics-collector = { optional = true, workspace = true, default-features = true } +polkadot-node-core-consensus-statistics-collector = { optional = true, workspace = true, default-features = true } xcm = { workspace = true, default-features = true } xcm-runtime-apis = { workspace = true, default-features = true } diff --git a/polkadot/node/service/src/overseer.rs b/polkadot/node/service/src/overseer.rs index 80c3d63aed7bd..593b8cfc6d403 100644 --- a/polkadot/node/service/src/overseer.rs +++ b/polkadot/node/service/src/overseer.rs @@ -73,8 +73,7 @@ pub use polkadot_node_core_provisioner::ProvisionerSubsystem; pub use polkadot_node_core_pvf_checker::PvfCheckerSubsystem; pub use polkadot_node_core_runtime_api::RuntimeApiSubsystem; pub use polkadot_statement_distribution::StatementDistributionSubsystem; -pub use polkadot_node_core_statistics_collector::ConsensusStatisticsCollector; -use polkadot_overseer::AllMessages::StatisticsCollector; +pub use polkadot_node_core_consensus_statistics_collector::ConsensusStatisticsCollector; /// Arguments passed for overseer construction. pub struct OverseerGenArgs<'a, Spawner, RuntimeClient> diff --git a/polkadot/node/subsystem-bench/src/lib/mock/dummy.rs b/polkadot/node/subsystem-bench/src/lib/mock/dummy.rs index 092a8fc5f4c12..f0b7af995aee6 100644 --- a/polkadot/node/subsystem-bench/src/lib/mock/dummy.rs +++ b/polkadot/node/subsystem-bench/src/lib/mock/dummy.rs @@ -99,3 +99,4 @@ mock!(ApprovalVoting); mock!(ApprovalVotingParallel); mock!(ApprovalDistribution); mock!(RuntimeApi); +mock!(ConsensusStatisticsCollector); diff --git a/polkadot/node/subsystem-bench/src/lib/mock/mod.rs b/polkadot/node/subsystem-bench/src/lib/mock/mod.rs index a3702dfe792f4..a1fc664e4ab5e 100644 --- a/polkadot/node/subsystem-bench/src/lib/mock/mod.rs +++ b/polkadot/node/subsystem-bench/src/lib/mock/mod.rs @@ -71,6 +71,7 @@ macro_rules! dummy_builder { .gossip_support(MockGossipSupport {}) .dispute_distribution(MockDisputeDistribution {}) .prospective_parachains(MockProspectiveParachains {}) + .consensus_statistics_collector(MockConsensusStatisticsCollector {}) .activation_external_listeners(Default::default()) .active_leaves(Default::default()) .metrics($metrics) From a5dbccf52affe38f022d439700fc4923a1783f1b Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Thu, 25 Sep 2025 08:38:34 -0400 Subject: [PATCH 10/48] fix approval voting tests --- .../node/core/approval-voting/src/tests.rs | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/polkadot/node/core/approval-voting/src/tests.rs b/polkadot/node/core/approval-voting/src/tests.rs index 9ef16af6aaa8c..50e30aec48142 100644 --- a/polkadot/node/core/approval-voting/src/tests.rs +++ b/polkadot/node/core/approval-voting/src/tests.rs @@ -1639,7 +1639,7 @@ fn subsystem_accepts_and_imports_approval_after_assignment() { candidate_hash, session_index, true, - vec![], + vec![validator], None, ) .await; @@ -2389,7 +2389,7 @@ fn import_checked_approval_updates_entries_and_schedules() { candidate_hash, session_index, true, - vec![validator_index_b], + vec![validator_index_a, validator_index_b], Some(sig_b), ) .await; @@ -3363,15 +3363,9 @@ where assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted)); } - // 10 - // 0..10 - // 3 * (i + 1) - // 0 => 3 - // 1 => 6 - // 2 => 9 - // 3 => 12 let n_validators = validators.len(); - let validators_collected = approvals_to_import[0..=n_validators/3] + let to_collect = min((n_validators/3) + 1, approvals_to_import.len()); + let validators_collected = approvals_to_import[0..to_collect] .iter() .map(|vidx| ValidatorIndex(vidx.clone())) .collect::>(); @@ -3965,6 +3959,21 @@ fn waits_until_approving_assignments_are_old_enough() { // Sleep to ensure we get a consistent read on the database. futures_timer::Delay::new(Duration::from_millis(100)).await; + let _ = + + assert_matches!( + overseer_recv(&mut virtual_overseer).await, + AllMessages::ConsensusStatisticsCollector( + ConsensusStatisticsCollectorMessage::CandidateApproved( + c_hash, b_hash, validators + ) + ) => { + assert_eq!(b_hash, block_hash); + assert_eq!(c_hash, candidate_hash); + assert_eq!(validators, vec![validator_index_a, validator_index_b]); + } + ); + assert_matches!( overseer_recv(&mut virtual_overseer).await, AllMessages::ChainSelection(ChainSelectionMessage::Approved(b_hash)) => { From 4f77b87f3a9f92fab35deb8bfdc55bbf4fe2183a Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Thu, 25 Sep 2025 12:50:43 -0400 Subject: [PATCH 11/48] chore: fixing approval tests --- polkadot/node/core/approval-voting/src/lib.rs | 2 + .../node/core/approval-voting/src/tests.rs | 80 ++++++++++++++----- 2 files changed, 61 insertions(+), 21 deletions(-) diff --git a/polkadot/node/core/approval-voting/src/lib.rs b/polkadot/node/core/approval-voting/src/lib.rs index 9c49a4a7ac991..07fdbb2bf9621 100644 --- a/polkadot/node/core/approval-voting/src/lib.rs +++ b/polkadot/node/core/approval-voting/src/lib.rs @@ -3241,8 +3241,10 @@ where }; if newly_approved { + println!("collecting approvals..."); collect_useful_approvals(sender, &status, block_hash, &candidate_entry); + println!("checking for no-shows: {:?}...", status.no_show_validators.len()); if status.no_show_validators.len() > 0 { _ = sender .try_send_message(ConsensusStatisticsCollectorMessage::NoShows( diff --git a/polkadot/node/core/approval-voting/src/tests.rs b/polkadot/node/core/approval-voting/src/tests.rs index 50e30aec48142..62b342eeefee1 100644 --- a/polkadot/node/core/approval-voting/src/tests.rs +++ b/polkadot/node/core/approval-voting/src/tests.rs @@ -72,6 +72,7 @@ use super::{ use polkadot_primitives_test_helpers::{ dummy_candidate_receipt_v2, dummy_candidate_receipt_v2_bad_sig, }; +use sp_core::blake2_512_into; const SLOT_DURATION_MILLIS: u64 = 5000; @@ -650,6 +651,26 @@ fn make_candidate(para_id: ParaId, hash: &Hash) -> CandidateReceipt { r } +struct ExpectApprovalsStatsCollected { + candidate_hash: CandidateHash, + block_hash: Hash, + validators: Vec, + no_shows: Option<(SessionIndex, Vec)>, +} + +impl ExpectApprovalsStatsCollected { + fn new(candidate_hash: CandidateHash, block_hash: Hash, validators: Vec) -> Self { + Self { + candidate_hash, block_hash, validators, no_shows: None, + } + } + + fn with_no_shows(&mut self, no_shows: (SessionIndex, Vec)) -> &mut Self { + self.no_shows = Some(no_shows); + self + } +} + async fn import_approval( overseer: &mut VirtualOverseer, block_hash: Hash, @@ -658,7 +679,7 @@ async fn import_approval( candidate_hash: CandidateHash, session_index: SessionIndex, expect_chain_approved: bool, - expected_approval_validators: Vec, + expected_approvals_stats_collected: Option, signature_opt: Option, ) -> oneshot::Receiver { let signature = signature_opt.unwrap_or(sign_approval( @@ -683,7 +704,7 @@ async fn import_approval( ) .await; - if expect_chain_approved { + if let Some(expected_stats_collected) = expected_approvals_stats_collected { assert_matches!( overseer_recv(overseer).await, AllMessages::ConsensusStatisticsCollector( @@ -691,12 +712,14 @@ async fn import_approval( c_hash, b_hash, validators, ) ) => { - assert_eq!(b_hash, block_hash); - assert_eq!(c_hash, candidate_hash); - assert_eq!(validators, expected_approval_validators); + assert_eq!(b_hash, expected_stats_collected.block_hash); + assert_eq!(c_hash, expected_stats_collected.candidate_hash); + assert_eq!(validators, expected_stats_collected.validators); } ); + } + if expect_chain_approved { assert_matches!( overseer_recv(overseer).await, AllMessages::ChainSelection(ChainSelectionMessage::Approved(b_hash)) => { @@ -1274,7 +1297,7 @@ fn subsystem_rejects_approval_if_no_candidate_entry() { candidate_hash, session_index, false, - vec![], + None, None, ) .await; @@ -1316,7 +1339,7 @@ fn subsystem_rejects_approval_if_no_block_entry() { candidate_hash, session_index, false, - vec![], + None, None, ) .await; @@ -1382,7 +1405,7 @@ fn subsystem_rejects_approval_before_assignment() { candidate_hash, session_index, false, - vec![], + None, None, ) .await; @@ -1639,7 +1662,7 @@ fn subsystem_accepts_and_imports_approval_after_assignment() { candidate_hash, session_index, true, - vec![validator], + Some(ExpectApprovalsStatsCollected::new(candidate_hash, block_hash, vec![validator])), None, ) .await; @@ -1730,7 +1753,7 @@ fn subsystem_second_approval_import_only_schedules_wakeups() { candidate_hash, session_index, false, - vec![], + None, None, ) .await; @@ -1748,7 +1771,7 @@ fn subsystem_second_approval_import_only_schedules_wakeups() { candidate_hash, session_index, false, - vec![], + None, None, ) .await; @@ -1966,7 +1989,7 @@ fn test_approvals_on_fork_are_always_considered_after_no_show( candidate_hash, 1, false, - vec![], + None, None, ) .await; @@ -2361,7 +2384,7 @@ fn import_checked_approval_updates_entries_and_schedules() { candidate_hash, session_index, false, - vec![], + None, Some(sig_a), ) .await; @@ -2389,7 +2412,7 @@ fn import_checked_approval_updates_entries_and_schedules() { candidate_hash, session_index, true, - vec![validator_index_a, validator_index_b], + Some(ExpectApprovalsStatsCollected::new(candidate_hash, block_hash, vec![validator_index_a, validator_index_b])), Some(sig_b), ) .await; @@ -2518,6 +2541,15 @@ fn subsystem_import_checked_approval_sets_one_block_bit_at_a_time() { } else { sign_approval(Sr25519Keyring::Bob, *candidate_hash, session_index) }; + + let expected_stats_collected = if i == 1 { + Some(ExpectApprovalsStatsCollected::new(candidate_hash1, block_hash, vec![validator1, validator2])) + } else if i == 3 { + Some(ExpectApprovalsStatsCollected::new(candidate_hash2, block_hash, vec![validator1, validator2])) + } else { + None + }; + let rx = import_approval( &mut virtual_overseer, block_hash, @@ -2526,7 +2558,7 @@ fn subsystem_import_checked_approval_sets_one_block_bit_at_a_time() { *candidate_hash, session_index, expect_block_approved, - vec![validator1, validator2], + expected_stats_collected, Some(signature), ) .await; @@ -2799,7 +2831,7 @@ fn approved_ancestor_test( candidate_hash, i as u32 + 1, true, - vec![validator], + Some(ExpectApprovalsStatsCollected::new(candidate_hash, *block_hash, vec![validator])), None, ) .await; @@ -3372,6 +3404,12 @@ where for (i, &validator_index) in approvals_to_import.iter().enumerate() { let expect_chain_approved = 3 * (i + 1) > n_validators; + let expect_approvals_stats_collected = if expect_chain_approved { + Some(ExpectApprovalsStatsCollected::new(candidate_hash, block_hash, validators_collected.clone())) + } else { + None + }; + let rx = import_approval( &mut virtual_overseer, block_hash, @@ -3380,7 +3418,7 @@ where candidate_hash, 1, expect_chain_approved, - validators_collected.clone(), + expect_approvals_stats_collected, Some(sign_approval(validators[validator_index as usize], candidate_hash, 1)), ) .await; @@ -3722,7 +3760,7 @@ fn pre_covers_dont_stall_approval() { candidate_hash, session_index, false, - vec![], + None, Some(sig_b), ) .await; @@ -3738,7 +3776,7 @@ fn pre_covers_dont_stall_approval() { candidate_hash, session_index, false, - vec![], + None, Some(sig_c), ) .await; @@ -3917,7 +3955,7 @@ fn waits_until_approving_assignments_are_old_enough() { candidate_hash, session_index, false, - vec![], + None, Some(sig_a), ) .await; @@ -3934,7 +3972,7 @@ fn waits_until_approving_assignments_are_old_enough() { candidate_hash, session_index, false, - vec![], + None, Some(sig_b), ) .await; From f9f4d71a7bdca0c596c2f9248e3f541a4de7dddb Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Mon, 29 Sep 2025 17:25:00 -0400 Subject: [PATCH 12/48] chore: include tests for collectorstats subsytem --- .../src/approval_voting_metrics.rs | 11 +- .../consensus-statistics-collector/src/lib.rs | 11 +- .../src/tests.rs | 168 ++++++++++++++++++ 3 files changed, 176 insertions(+), 14 deletions(-) diff --git a/polkadot/node/core/consensus-statistics-collector/src/approval_voting_metrics.rs b/polkadot/node/core/consensus-statistics-collector/src/approval_voting_metrics.rs index 047c218da61fb..b5d25ef8e4696 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/approval_voting_metrics.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/approval_voting_metrics.rs @@ -1,7 +1,7 @@ use std::collections::{HashMap, HashSet}; +use std::collections::hash_map::Entry; use polkadot_primitives::{CandidateHash, Hash, SessionIndex, ValidatorIndex}; use crate::View; -use crate::error::Result; #[derive(Debug, Clone, Default)] pub struct ApprovalsStats { @@ -23,14 +23,13 @@ pub fn handle_candidate_approved( if let Some(relay_view) = view.per_relay.get_mut(&block_hash) { relay_view.approvals_stats .entry(candidate_hash) - .or_default() .and_modify(|a: &mut ApprovalsStats| { - a.votes.extend(approvals.into()); - }); + a.votes.extend(approvals.iter()); + }).or_insert(ApprovalsStats::new(HashSet::from_iter(approvals))); } } -pub fn handle_observed_no_shows( +pub fn handle_observed_no_shows( view: &mut View, session_index: SessionIndex, no_show_validators: Vec, @@ -39,7 +38,7 @@ pub fn handle_observed_no_shows( .entry(session_index) .and_modify(|q: &mut HashMap| { for v_idx in no_show_validators { - q.entry(*v_idx) + q.entry(v_idx) .and_modify(|v: &mut usize| *v += 1) .or_insert(1); } diff --git a/polkadot/node/core/consensus-statistics-collector/src/lib.rs b/polkadot/node/core/consensus-statistics-collector/src/lib.rs index 36ea921f68c9f..d1e69e032f995 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/lib.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/lib.rs @@ -97,7 +97,7 @@ impl ConsensusStatisticsCollector { } } -#[overseer::subsystem(StatisticsCollector, error = SubsystemError, prefix = self::overseer)] +#[overseer::subsystem(ConsensusStatisticsCollector, error = SubsystemError, prefix = self::overseer)] impl ConsensusStatisticsCollector where Context: Send + Sync, @@ -112,7 +112,7 @@ where } } -#[overseer::contextbounds(StatisticsCollector, prefix = self::overseer)] +#[overseer::contextbounds(ConsensusStatisticsCollector, prefix = self::overseer)] async fn run(mut ctx: Context, metrics: Metrics) -> FatalResult<()> { let mut view = View::new(); loop { @@ -123,7 +123,7 @@ async fn run(mut ctx: Context, metrics: Metrics) -> FatalResult<()> { } } -#[overseer::contextbounds(StatisticsCollector, prefix = self::overseer)] +#[overseer::contextbounds(ConsensusStatisticsCollector, prefix = self::overseer)] pub(crate) async fn run_iteration( ctx: &mut Context, view: &mut View, @@ -164,11 +164,6 @@ pub(crate) async fn run_iteration( no_show_validators, ); }, - ConsensusStatisticsCollectorMessage::RelayBlockApproved(block_hash) => { - view.per_relay - .entry(block_hash) - .and_modify(|q| q.relay_approved = true); - }, } }, } diff --git a/polkadot/node/core/consensus-statistics-collector/src/tests.rs b/polkadot/node/core/consensus-statistics-collector/src/tests.rs index e69de29bb2d1d..a97622dfe9e23 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/tests.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/tests.rs @@ -0,0 +1,168 @@ +use super::*; +use overseer::FromOrchestra; +use polkadot_node_subsystem::messages::{ConsensusStatisticsCollectorMessage}; +type VirtualOverseer = + polkadot_node_subsystem_test_helpers::TestSubsystemContextHandle; +use polkadot_node_subsystem::{ActivatedLeaf}; +use polkadot_node_subsystem_test_helpers as test_helpers; +use test_helpers::mock::new_leaf; + +async fn activate_leaf( + virtual_overseer: &mut VirtualOverseer, + activated: ActivatedLeaf, +) { + virtual_overseer + .send(FromOrchestra::Signal(OverseerSignal::ActiveLeaves(ActiveLeavesUpdate::start_work( + activated, + )))) + .await; +} + +async fn candidate_approved( + virtual_overseer: &mut VirtualOverseer, + candidate_hash: CandidateHash, + rb_hash: Hash, + approvals: Vec, +) { + let msg = FromOrchestra::Communication { + msg: ConsensusStatisticsCollectorMessage::CandidateApproved( + candidate_hash.clone(), + rb_hash.clone(), + approvals, + ), + }; + virtual_overseer.send(msg).await; +} + +fn test_harness>( + test: impl FnOnce(VirtualOverseer) -> T, +) -> View { + sp_tracing::init_for_tests(); + + let pool = sp_core::testing::TaskExecutor::new(); + + let (mut context, virtual_overseer) = + polkadot_node_subsystem_test_helpers::make_subsystem_context(pool.clone()); + + let metrics = Metrics; + + let mut view = View::new(); + let subsystem = async move { + if let Err(e) = run_iteration(&mut context, &mut view, &metrics).await { + panic!("{:?}", e); + } + + view + }; + + let test_fut = test(virtual_overseer); + + futures::pin_mut!(test_fut); + futures::pin_mut!(subsystem); + let (_, view) = futures::executor::block_on(future::join( + async move { + let mut virtual_overseer = test_fut.await; + virtual_overseer.send(FromOrchestra::Signal(OverseerSignal::Conclude)).await; + }, + subsystem, + )); + + view +} + +#[test] +fn single_candidate_approved() { + let validator_idx = ValidatorIndex(2); + let candidate_hash: CandidateHash = CandidateHash( + Hash::from_low_u64_be(111)); + + let rb_hash = Hash::from_low_u64_be(132); + let leaf = new_leaf( + rb_hash.clone(), + 1, + ); + + let view = test_harness(|mut virtual_overseer| async move { + activate_leaf(&mut virtual_overseer, leaf.clone()).await; + candidate_approved(&mut virtual_overseer, candidate_hash.clone(), rb_hash, vec![validator_idx.clone()]).await; + virtual_overseer + }); + + assert_eq!(view.per_relay.len(), 1); + let stats_for = view.per_relay.get(&rb_hash).unwrap(); + let approvals_for = stats_for.approvals_stats.get(&candidate_hash).unwrap(); + + println!("{:?}", approvals_for.votes); + + let expected_votes = vec![validator_idx]; + let collected_votes= approvals_for + .clone() + .votes + .into_iter() + .collect::>(); + + assert_eq!(expected_votes, collected_votes); +} + +#[test] +fn candidate_approved_for_different_forks() { + let validator_idx0 = ValidatorIndex(0); + let validator_idx1 = ValidatorIndex(1); + + let candidate_hash: CandidateHash = CandidateHash( + Hash::from_low_u64_be(111)); + + let rb_hash_fork_0 = Hash::from_low_u64_be(132); + let rb_hash_fork_1 = Hash::from_low_u64_be(231); + + let view = test_harness(|mut virtual_overseer| async move { + let leaf1 = new_leaf( + rb_hash_fork_0.clone(), + 1, + ); + + let leaf2 = new_leaf( + rb_hash_fork_1.clone(), + 1, + ); + + activate_leaf(&mut virtual_overseer, leaf1.clone()).await; + activate_leaf(&mut virtual_overseer, leaf2.clone()).await; + + candidate_approved( + &mut virtual_overseer, + candidate_hash, + rb_hash_fork_0, + vec![validator_idx1], + ).await; + + candidate_approved( + &mut virtual_overseer, + candidate_hash, + rb_hash_fork_1, + vec![validator_idx0], + ).await; + + virtual_overseer + }); + + assert_eq!(view.per_relay.len(), 2); + + let expected_fork_0 = vec![validator_idx1]; + assert_votes_for(&view, rb_hash_fork_0, candidate_hash.clone(), expected_fork_0); + + let expected_fork_1 = vec![validator_idx0]; + assert_votes_for(&view, rb_hash_fork_1, candidate_hash.clone(), expected_fork_1); +} + +fn assert_votes_for(view: &View, rb_hash: Hash, candidate_hash: CandidateHash, expected_votes: Vec) { + let stats_for = view.per_relay.get(&rb_hash).unwrap(); + let approvals_for = stats_for.approvals_stats.get(&candidate_hash).unwrap(); + let collected_votes= approvals_for + .clone() + .votes + .into_iter() + .collect::>(); + + assert_eq!(expected_votes, collected_votes); +} \ No newline at end of file From 62820943d2f5b3b288d0aac1d57904ea3cc6614c Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Mon, 29 Sep 2025 22:23:35 -0400 Subject: [PATCH 13/48] chore: include no-shows tests --- polkadot/node/core/approval-voting/src/lib.rs | 5 +- .../src/approval_voting_metrics.rs | 29 +++---- .../consensus-statistics-collector/src/lib.rs | 7 +- .../src/tests.rs | 81 ++++++++++++++++++- polkadot/node/subsystem-types/src/messages.rs | 2 +- 5 files changed, 97 insertions(+), 27 deletions(-) diff --git a/polkadot/node/core/approval-voting/src/lib.rs b/polkadot/node/core/approval-voting/src/lib.rs index 07fdbb2bf9621..2500b0124cb3f 100644 --- a/polkadot/node/core/approval-voting/src/lib.rs +++ b/polkadot/node/core/approval-voting/src/lib.rs @@ -3241,14 +3241,13 @@ where }; if newly_approved { - println!("collecting approvals..."); collect_useful_approvals(sender, &status, block_hash, &candidate_entry); - println!("checking for no-shows: {:?}...", status.no_show_validators.len()); if status.no_show_validators.len() > 0 { _ = sender .try_send_message(ConsensusStatisticsCollectorMessage::NoShows( - session_index, + candidate_entry.candidate.hash(), + block_hash, status.no_show_validators, )); } diff --git a/polkadot/node/core/consensus-statistics-collector/src/approval_voting_metrics.rs b/polkadot/node/core/consensus-statistics-collector/src/approval_voting_metrics.rs index b5d25ef8e4696..75348a0c7b449 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/approval_voting_metrics.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/approval_voting_metrics.rs @@ -6,11 +6,12 @@ use crate::View; #[derive(Debug, Clone, Default)] pub struct ApprovalsStats { pub votes: HashSet, + pub no_shows: HashSet, } impl ApprovalsStats { - pub fn new(votes: HashSet) -> Self { - Self { votes } + pub fn new(votes: HashSet, no_shows: HashSet) -> Self { + Self { votes, no_shows } } } @@ -23,25 +24,21 @@ pub fn handle_candidate_approved( if let Some(relay_view) = view.per_relay.get_mut(&block_hash) { relay_view.approvals_stats .entry(candidate_hash) - .and_modify(|a: &mut ApprovalsStats| { - a.votes.extend(approvals.iter()); - }).or_insert(ApprovalsStats::new(HashSet::from_iter(approvals))); + .and_modify(|a: &mut ApprovalsStats| a.votes.extend(approvals.iter())) + .or_insert(ApprovalsStats::new(HashSet::from_iter(approvals), HashSet::new())); } } pub fn handle_observed_no_shows( view: &mut View, - session_index: SessionIndex, + block_hash: Hash, + candidate_hash: CandidateHash, no_show_validators: Vec, ) { - view.no_shows_per_session - .entry(session_index) - .and_modify(|q: &mut HashMap| { - for v_idx in no_show_validators { - q.entry(v_idx) - .and_modify(|v: &mut usize| *v += 1) - .or_insert(1); - } - }) - .or_insert(HashMap::new()); + if let Some(relay_view) = view.per_relay.get_mut(&block_hash) { + relay_view.approvals_stats + .entry(candidate_hash) + .and_modify(|a: &mut ApprovalsStats| a.no_shows.extend(no_show_validators.iter())) + .or_insert(ApprovalsStats::new(HashSet::new(), HashSet::from_iter(no_show_validators))); + } } \ No newline at end of file diff --git a/polkadot/node/core/consensus-statistics-collector/src/lib.rs b/polkadot/node/core/consensus-statistics-collector/src/lib.rs index d1e69e032f995..c5e94d13f1578 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/lib.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/lib.rs @@ -68,7 +68,6 @@ impl PerRelayView { struct View { per_relay: HashMap, - no_shows_per_session: HashMap>, candidates_per_session: HashMap>, chunks_downloaded: AvailabilityDownloads, } @@ -77,7 +76,6 @@ impl View { fn new() -> Self { return View{ per_relay: HashMap::new(), - no_shows_per_session: HashMap::new(), candidates_per_session: HashMap::new(), chunks_downloaded: AvailabilityDownloads::new(), }; @@ -157,10 +155,11 @@ pub(crate) async fn run_iteration( approvals, ); } - ConsensusStatisticsCollectorMessage::NoShows(session_idx, no_show_validators) => { + ConsensusStatisticsCollectorMessage::NoShows(candidate_hash, block_hash, no_show_validators) => { handle_observed_no_shows( view, - session_idx, + block_hash, + candidate_hash, no_show_validators, ); }, diff --git a/polkadot/node/core/consensus-statistics-collector/src/tests.rs b/polkadot/node/core/consensus-statistics-collector/src/tests.rs index a97622dfe9e23..bfe141396cbe8 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/tests.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/tests.rs @@ -5,6 +5,7 @@ type VirtualOverseer = polkadot_node_subsystem_test_helpers::TestSubsystemContextHandle; use polkadot_node_subsystem::{ActivatedLeaf}; use polkadot_node_subsystem_test_helpers as test_helpers; +use sp_core::traits::CodeNotFound; use test_helpers::mock::new_leaf; async fn activate_leaf( @@ -34,6 +35,46 @@ async fn candidate_approved( virtual_overseer.send(msg).await; } +async fn no_shows( + virtual_overseer: &mut VirtualOverseer, + candidate_hash: CandidateHash, + rb_hash: Hash, + no_shows: Vec, +) { + let msg = FromOrchestra::Communication { + msg: ConsensusStatisticsCollectorMessage::NoShows( + candidate_hash.clone(), + rb_hash.clone(), + no_shows, + ), + }; + virtual_overseer.send(msg).await; +} + +macro_rules! approvals_stats_assertion { + ($fn_name:ident, $field:ident) => { + fn $fn_name( + view: &View, + rb_hash: Hash, + candidate_hash: CandidateHash, + expected_votes: Vec, + ) { + let stats_for = view.per_relay.get(&rb_hash).unwrap(); + let approvals_for = stats_for.approvals_stats.get(&candidate_hash).unwrap(); + let collected_votes = approvals_for + .$field + .clone() + .into_iter() + .collect::>(); + + assert_eq!(expected_votes, collected_votes); + } + }; +} + +approvals_stats_assertion!(assert_votes, votes); +approvals_stats_assertion!(assert_no_shows, no_shows); + fn test_harness>( test: impl FnOnce(VirtualOverseer) -> T, ) -> View { @@ -149,13 +190,47 @@ fn candidate_approved_for_different_forks() { assert_eq!(view.per_relay.len(), 2); let expected_fork_0 = vec![validator_idx1]; - assert_votes_for(&view, rb_hash_fork_0, candidate_hash.clone(), expected_fork_0); + assert_votes(&view, rb_hash_fork_0, candidate_hash.clone(), expected_fork_0); let expected_fork_1 = vec![validator_idx0]; - assert_votes_for(&view, rb_hash_fork_1, candidate_hash.clone(), expected_fork_1); + assert_votes(&view, rb_hash_fork_1, candidate_hash.clone(), expected_fork_1); +} + +#[test] +fn candidate_approval_stats_with_no_shows() { + let approvals_from = vec![ValidatorIndex(0), ValidatorIndex(3)]; + let no_show_validators = vec![ValidatorIndex(1), ValidatorIndex(2)]; + + let rb_hash = Hash::from_low_u64_be(111); + let candidate_hash: CandidateHash = CandidateHash(Hash::from_low_u64_be(132)); + + let view = test_harness(|mut virtual_overseer| async move { + let leaf1 = new_leaf(rb_hash.clone(), 1); + activate_leaf(&mut virtual_overseer, leaf1.clone()).await; + + candidate_approved( + &mut virtual_overseer, + candidate_hash, + rb_hash, + approvals_from, + ).await; + + no_shows( + &mut virtual_overseer, + candidate_hash, + rb_hash, + no_show_validators + ).await; + + virtual_overseer + }); + + assert_eq!(view.per_relay.len(), 1); + let expected_validators = vec![ValidatorIndex(0), ValidatorIndex(3)]; + assert_votes(&view, rb_hash, candidate_hash.clone(), expected_validators); } -fn assert_votes_for(view: &View, rb_hash: Hash, candidate_hash: CandidateHash, expected_votes: Vec) { +fn assert_bak_votes_for(view: &View, rb_hash: Hash, candidate_hash: CandidateHash, expected_votes: Vec) { let stats_for = view.per_relay.get(&rb_hash).unwrap(); let approvals_for = stats_for.approvals_stats.get(&candidate_hash).unwrap(); let collected_votes= approvals_for diff --git a/polkadot/node/subsystem-types/src/messages.rs b/polkadot/node/subsystem-types/src/messages.rs index ae102de063fcd..419b071526a95 100644 --- a/polkadot/node/subsystem-types/src/messages.rs +++ b/polkadot/node/subsystem-types/src/messages.rs @@ -1470,5 +1470,5 @@ pub enum ConsensusStatisticsCollectorMessage { CandidateApproved(CandidateHash, Hash, Vec), // Set of candidates that has not shared votes in time - NoShows(SessionIndex, Vec), + NoShows(CandidateHash, Hash, Vec), } \ No newline at end of file From c0f969d047979103e1fc60de4add5dc936dfd8ee Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Tue, 30 Sep 2025 11:32:49 -0400 Subject: [PATCH 14/48] include tests for chunks downloaded --- .../src/availability_distribution_metrics.rs | 11 ++-- .../consensus-statistics-collector/src/lib.rs | 4 +- .../src/tests.rs | 53 +++++++++++++++---- 3 files changed, 52 insertions(+), 16 deletions(-) diff --git a/polkadot/node/core/consensus-statistics-collector/src/availability_distribution_metrics.rs b/polkadot/node/core/consensus-statistics-collector/src/availability_distribution_metrics.rs index 15e27f4fe2569..b600527192f31 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/availability_distribution_metrics.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/availability_distribution_metrics.rs @@ -1,4 +1,5 @@ use std::collections::{HashMap, HashSet}; +use std::collections::hash_map::Entry; use std::ops::Add; use gum::CandidateHash; use polkadot_primitives::{SessionIndex, ValidatorIndex}; @@ -17,16 +18,16 @@ impl AvailabilityDownloads { pub fn note_candidate_chunk_downloaded( &mut self, - hash: CandidateHash, + candidate_hash: CandidateHash, validator_index: ValidatorIndex, count: u64, ) { - self.chunks_per_candidate - .entry(hash) + let _ = self.chunks_per_candidate + .entry(candidate_hash) .or_default() .entry(validator_index) - .or_default() - .add(count); + .and_modify(|v| *v += count) + .or_insert(count); } } diff --git a/polkadot/node/core/consensus-statistics-collector/src/lib.rs b/polkadot/node/core/consensus-statistics-collector/src/lib.rs index c5e94d13f1578..5634e9c0cb12b 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/lib.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/lib.rs @@ -131,8 +131,8 @@ pub(crate) async fn run_iteration( match ctx.recv().await.map_err(FatalError::SubsystemReceive)? { FromOrchestra::Signal(OverseerSignal::Conclude) => return Ok(()), FromOrchestra::Signal(OverseerSignal::ActiveLeaves(update)) => { - if let Some(actived) = update.activated { - view.per_relay.insert(actived.hash, PerRelayView::new(vec![])); + if let Some(activated) = update.activated { + view.per_relay.insert(activated.hash, PerRelayView::new(vec![])); } }, FromOrchestra::Signal(OverseerSignal::BlockFinalized(..)) => {}, diff --git a/polkadot/node/core/consensus-statistics-collector/src/tests.rs b/polkadot/node/core/consensus-statistics-collector/src/tests.rs index bfe141396cbe8..7924cf12f77b8 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/tests.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/tests.rs @@ -1,6 +1,8 @@ use super::*; use overseer::FromOrchestra; +use polkadot_primitives::{SessionIndex}; use polkadot_node_subsystem::messages::{ConsensusStatisticsCollectorMessage}; + type VirtualOverseer = polkadot_node_subsystem_test_helpers::TestSubsystemContextHandle; use polkadot_node_subsystem::{ActivatedLeaf}; @@ -230,14 +232,47 @@ fn candidate_approval_stats_with_no_shows() { assert_votes(&view, rb_hash, candidate_hash.clone(), expected_validators); } -fn assert_bak_votes_for(view: &View, rb_hash: Hash, candidate_hash: CandidateHash, expected_votes: Vec) { - let stats_for = view.per_relay.get(&rb_hash).unwrap(); - let approvals_for = stats_for.approvals_stats.get(&candidate_hash).unwrap(); - let collected_votes= approvals_for - .clone() - .votes - .into_iter() - .collect::>(); +#[test] +fn note_chunks_downloaded() { + let candidate_hash = CandidateHash(Hash::from_low_u64_be(132)); + let session_idx: SessionIndex = 2 ; + let chunk_downloads = vec![ + (ValidatorIndex(0), 10u64), + (ValidatorIndex(1), 2), + ]; + + let view = test_harness(|mut virtual_overseer| async move { + virtual_overseer.send(FromOrchestra::Communication { + msg: ConsensusStatisticsCollectorMessage::ChunksDownloaded( + session_idx, candidate_hash.clone(), HashMap::from_iter(chunk_downloads.clone().into_iter()), + ), + }).await; + + // should increment only validator 0 + let second_round_of_downloads = vec![ + (ValidatorIndex(0), 5u64) + ]; + virtual_overseer.send(FromOrchestra::Communication { + msg: ConsensusStatisticsCollectorMessage::ChunksDownloaded( + session_idx, candidate_hash.clone(), HashMap::from_iter(second_round_of_downloads.into_iter()), + ), + }).await; + + virtual_overseer + }); + + assert_eq!(view.chunks_downloaded.chunks_per_candidate.len(), 1); + let amt_per_validator = view.chunks_downloaded.chunks_per_candidate + .get(&candidate_hash) + .unwrap(); + + let expected = vec![ + (ValidatorIndex(0), 15u64), + (ValidatorIndex(1), 2), + ]; - assert_eq!(expected_votes, collected_votes); + for (vidx, expected_count) in expected { + let count = amt_per_validator.get(&vidx).unwrap(); + assert_eq!(*count, expected_count); + } } \ No newline at end of file From b846ced4d8b6eefa6d74cbc5f2506e9ddaa88050 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Thu, 2 Oct 2025 09:03:35 -0400 Subject: [PATCH 15/48] chore: wip using auth discovery to get peer authority id --- .../availability-distribution/src/lib.rs | 60 ++++++++++--------- .../src/responder.rs | 33 +++++++--- polkadot/node/network/protocol/src/lib.rs | 1 - polkadot/node/subsystem-types/src/messages.rs | 7 ++- 4 files changed, 64 insertions(+), 37 deletions(-) diff --git a/polkadot/node/network/availability-distribution/src/lib.rs b/polkadot/node/network/availability-distribution/src/lib.rs index 438453814978c..21c92def0c791 100644 --- a/polkadot/node/network/availability-distribution/src/lib.rs +++ b/polkadot/node/network/availability-distribution/src/lib.rs @@ -165,35 +165,41 @@ impl AvailabilityDistributionSubsystem { FromOrchestra::Signal(OverseerSignal::BlockFinalized(_hash, _finalized_number)) => { }, FromOrchestra::Signal(OverseerSignal::Conclude) => return Ok(()), - FromOrchestra::Communication { - msg: - AvailabilityDistributionMessage::FetchPoV { - relay_parent, - from_validator, - para_id, - candidate_hash, - pov_hash, - tx, - }, - } => { - log_error( - pov_requester::fetch_pov( - &mut ctx, - &mut runtime, - relay_parent, - from_validator, - para_id, - candidate_hash, - pov_hash, - tx, - metrics.clone(), - ) - .await, - "pov_requester::fetch_pov", - &mut warn_freq, - )?; + FromOrchestra::Communication { msg} => match msg { + AvailabilityDistributionMessage::FetchPoV { + relay_parent, + from_validator, + para_id, + candidate_hash, + pov_hash, + tx, + } => { + log_error( + pov_requester::fetch_pov( + &mut ctx, + &mut runtime, + relay_parent, + from_validator, + para_id, + candidate_hash, + pov_hash, + tx, + metrics.clone(), + ) + .await, + "pov_requester::fetch_pov", + &mut warn_freq, + )?; + }, + AvailabilityDistributionMessage::NetworkBridgeUpdate(event) => { + + }, }, } } } } + +async fn handle_network_update() { + +} diff --git a/polkadot/node/network/availability-distribution/src/responder.rs b/polkadot/node/network/availability-distribution/src/responder.rs index 6512fcb7f656a..a9e345d4c0496 100644 --- a/polkadot/node/network/availability-distribution/src/responder.rs +++ b/polkadot/node/network/availability-distribution/src/responder.rs @@ -25,9 +25,11 @@ use fatality::Nested; use polkadot_node_network_protocol::{ request_response::{v1, v2, IncomingRequest, IncomingRequestReceiver, IsRequest}, UnifiedReputationChange as Rep, + authority_discovery::AuthorityDiscovery, }; use polkadot_node_primitives::{AvailableData, ErasureChunk}; use polkadot_node_subsystem::{messages::AvailabilityStoreMessage, SubsystemSender}; +use polkadot_node_subsystem::messages::ConsensusStatisticsCollectorMessage; use polkadot_primitives::{CandidateHash, ValidatorIndex}; use crate::{ @@ -67,13 +69,15 @@ pub async fn run_pov_receiver( } /// Receiver task to be forked as a separate task to handle chunk requests. -pub async fn run_chunk_receivers( +pub async fn run_chunk_receivers( mut sender: Sender, + mut authority_discovery: AD, mut receiver_v1: IncomingRequestReceiver, mut receiver_v2: IncomingRequestReceiver, metrics: Metrics, ) where Sender: SubsystemSender, + AD: AuthorityDiscovery + Clone + Sync { let make_resp_v1 = |chunk: Option| match chunk { None => v1::ChunkFetchingResponse::NoSuchChunk, @@ -89,7 +93,7 @@ pub async fn run_chunk_receivers( select! { res = receiver_v1.recv(|| vec![COST_INVALID_REQUEST]).fuse() => match res.into_nested() { Ok(Ok(msg)) => { - answer_chunk_request_log(&mut sender, msg, make_resp_v1, &metrics).await; + answer_chunk_request_log(&mut sender, &mut authority_discovery, msg, make_resp_v1, &metrics).await; }, Err(fatal) => { gum::debug!( @@ -109,7 +113,7 @@ pub async fn run_chunk_receivers( }, res = receiver_v2.recv(|| vec![COST_INVALID_REQUEST]).fuse() => match res.into_nested() { Ok(Ok(msg)) => { - answer_chunk_request_log(&mut sender, msg.into(), make_resp_v2, &metrics).await; + answer_chunk_request_log(&mut sender, &mut authority_discovery, msg.into(), make_resp_v2, &metrics).await; }, Err(fatal) => { gum::debug!( @@ -158,20 +162,25 @@ pub async fn answer_pov_request_log( /// Variant of `answer_chunk_request` that does Prometheus metric and logging on errors. /// /// Any errors of `answer_request` will simply be logged. -pub async fn answer_chunk_request_log( +pub async fn answer_chunk_request_log( sender: &mut Sender, + authority_discovery: &mut AD, req: IncomingRequest, make_response: MakeResp, metrics: &Metrics, ) where + AD: AuthorityDiscovery, Req: IsRequest + Decode + Encode + Into, Req::Response: Encode, Sender: SubsystemSender, MakeResp: Fn(Option) -> Req::Response, { - let res = answer_chunk_request(sender, req, make_response).await; + let incoming_peer_id = req.peer; + let res = answer_chunk_request(sender, authority_discovery, req, make_response).await; match res { - Ok(result) => metrics.on_served_chunk(if result { SUCCEEDED } else { NOT_FOUND }), + Ok(result) => { + metrics.on_served_chunk(if result { SUCCEEDED } else { NOT_FOUND }) + }, Err(err) => { gum::warn!( target: LOG_TARGET, @@ -212,13 +221,16 @@ where /// Answer an incoming chunk request by querying the av store. /// /// Returns: `Ok(true)` if chunk was found and served. -pub async fn answer_chunk_request( +pub async fn answer_chunk_request( sender: &mut Sender, + authority_discovery: &mut AD, req: IncomingRequest, make_response: MakeResp, ) -> Result where - Sender: SubsystemSender, + AD: AuthorityDiscovery, + Sender: SubsystemSender + + SubsystemSender, Req: IsRequest + Decode + Encode + Into, Req::Response: Encode, MakeResp: Fn(Option) -> Req::Response, @@ -231,6 +243,11 @@ where let result = chunk.is_some(); + if result { + let authority_ids = authority_discovery.get_authority_ids_by_peer_id(req.peer); + + } + gum::trace!( target: LOG_TARGET, hash = ?payload.candidate_hash, diff --git a/polkadot/node/network/protocol/src/lib.rs b/polkadot/node/network/protocol/src/lib.rs index fa9effe440b12..247564e8996d9 100644 --- a/polkadot/node/network/protocol/src/lib.rs +++ b/polkadot/node/network/protocol/src/lib.rs @@ -429,7 +429,6 @@ impl_versioned_validation_try_from!( VersionedValidationProtocol, ApprovalDistributionMessage, v3::ValidationProtocol::ApprovalDistribution(x) => x - ); /// Version-annotated messages used by the gossip-support subsystem (this is void). diff --git a/polkadot/node/subsystem-types/src/messages.rs b/polkadot/node/subsystem-types/src/messages.rs index 419b071526a95..8f76963bd79ad 100644 --- a/polkadot/node/subsystem-types/src/messages.rs +++ b/polkadot/node/subsystem-types/src/messages.rs @@ -476,7 +476,7 @@ pub enum NetworkBridgeTxMessage { } /// Availability Distribution Message. -#[derive(Debug)] +#[derive(Debug, derive_more::From)] pub enum AvailabilityDistributionMessage { /// Instruct availability distribution to fetch a remote PoV. /// @@ -499,6 +499,10 @@ pub enum AvailabilityDistributionMessage { /// The sender will be canceled if the fetching failed for some reason. tx: oneshot::Sender, }, + + /// Event from the network bridge. + #[from] + NetworkBridgeUpdate(NetworkBridgeEvent<()>), } /// Availability Recovery Message. @@ -1465,6 +1469,7 @@ pub enum ProspectiveParachainsMessage { #[derive(Debug)] pub enum ConsensusStatisticsCollectorMessage { ChunksDownloaded(SessionIndex, CandidateHash, HashMap), + ChunksUploaded(CandidateHash, HashSet<()>) // Candidate received enough approval and now is approved CandidateApproved(CandidateHash, Hash, Vec), From 0b186a17aa95bd07469b527811f87b4ca36eeee3 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Fri, 3 Oct 2025 10:51:04 -0400 Subject: [PATCH 16/48] deprecate macro was failing to build in recent rust versions --- substrate/frame/executive/src/lib.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/substrate/frame/executive/src/lib.rs b/substrate/frame/executive/src/lib.rs index 5f05400896e64..c6ec36145bf72 100644 --- a/substrate/frame/executive/src/lib.rs +++ b/substrate/frame/executive/src/lib.rs @@ -195,16 +195,17 @@ impl core::fmt::Debug for ExecutiveError { /// - [**DEPRECATED** `OnRuntimeUpgrade`]: This parameter is deprecated and will be removed after /// September 2026. Use type `SingleBlockMigrations` in frame_system::Config instead. #[allow(deprecated)] +#[deprecated( + note = "`OnRuntimeUpgrade` parameter in Executive is deprecated, will be removed after September 2026. \ + Use type `SingleBlockMigrations` in frame_system::Config instead." +)] pub struct Executive< System, Block, Context, UnsignedValidator, AllPalletsWithSystem, - #[deprecated( - note = "`OnRuntimeUpgrade` parameter in Executive is deprecated, will be removed after September 2026. \ - Use type `SingleBlockMigrations` in frame_system::Config instead." - )] OnRuntimeUpgrade = (), + OnRuntimeUpgrade = (), >( PhantomData<( System, From caea9a80d1671c9a939792480151efedd5334df1 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Mon, 6 Oct 2025 09:23:04 -0400 Subject: [PATCH 17/48] feat: collect upload metrics - Inject Authority Discovery to Availability Distribution - When responding to a chunk request get the peer id and retrive its authority ids - In the Collector store the session info which enable to get the validator index from authority ids --- Cargo.lock | 2 + .../src/availability_distribution_metrics.rs | 18 ++++--- .../src/error.rs | 10 ++-- .../consensus-statistics-collector/src/lib.rs | 53 +++++++++++++++++-- .../availability-distribution/Cargo.toml | 2 + .../availability-distribution/src/lib.rs | 21 +++++--- .../src/responder.rs | 16 +++--- .../src/tests/mock.rs | 27 +++++++++- .../src/tests/mod.rs | 1 + polkadot/node/overseer/src/lib.rs | 6 ++- polkadot/node/service/src/overseer.rs | 9 ++-- .../src/lib/availability/mod.rs | 25 +++++++-- polkadot/node/subsystem-types/src/messages.rs | 2 +- 13 files changed, 158 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 918529ad7f746..76e24c9370bf1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15106,6 +15106,7 @@ name = "polkadot-availability-distribution" version = "7.0.0" dependencies = [ "assert_matches", + "async-trait", "fatality", "futures", "futures-timer", @@ -15122,6 +15123,7 @@ dependencies = [ "rand 0.8.5", "rstest", "sc-network", + "sc-network-types", "schnellru", "sp-core 28.0.0", "sp-keyring", diff --git a/polkadot/node/core/consensus-statistics-collector/src/availability_distribution_metrics.rs b/polkadot/node/core/consensus-statistics-collector/src/availability_distribution_metrics.rs index b600527192f31..0cdb03b1a05dd 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/availability_distribution_metrics.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/availability_distribution_metrics.rs @@ -5,14 +5,14 @@ use gum::CandidateHash; use polkadot_primitives::{SessionIndex, ValidatorIndex}; use crate::View; -pub struct AvailabilityDownloads { - pub chunks_per_candidate: HashMap>, +pub struct AvailabilityChunks { + pub downloads_per_candidate: HashMap>, } -impl AvailabilityDownloads { +impl AvailabilityChunks { pub fn new() -> Self { Self { - chunks_per_candidate: Default::default(), + downloads_per_candidate: Default::default(), } } @@ -22,7 +22,7 @@ impl AvailabilityDownloads { validator_index: ValidatorIndex, count: u64, ) { - let _ = self.chunks_per_candidate + let _ = self.downloads_per_candidate .entry(candidate_hash) .or_default() .entry(validator_index) @@ -46,7 +46,13 @@ pub fn handle_chunks_downloaded( .insert(candidate_hash); for (validator_index, download_count) in downloads { - view.chunks_downloaded + view.availability_chunks .note_candidate_chunk_downloaded(candidate_hash, validator_index, download_count) } +} + +pub fn handle_chunk_uploaded( + +) { + } \ No newline at end of file diff --git a/polkadot/node/core/consensus-statistics-collector/src/error.rs b/polkadot/node/core/consensus-statistics-collector/src/error.rs index 67de4cd76fa4c..3c4fb145ad7af 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/error.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/error.rs @@ -18,9 +18,7 @@ use futures::channel::oneshot; -use polkadot_node_subsystem::{ - SubsystemError, -}; +use polkadot_node_subsystem::{RuntimeApiError, SubsystemError}; use polkadot_node_subsystem_util::runtime; use crate::LOG_TARGET; @@ -32,6 +30,12 @@ pub enum Error { #[fatal] #[error("Receiving message from overseer failed: {0}")] SubsystemReceive(#[source] SubsystemError), + + #[error("Sending message to overseer failed: {0}")] + OverseerCommunication(#[source] oneshot::Canceled), + + #[error("Failed to request runtime data: {0}")] + RuntimeApiCallError(#[source] RuntimeApiError), } /// General `Result` type. diff --git a/polkadot/node/core/consensus-statistics-collector/src/lib.rs b/polkadot/node/core/consensus-statistics-collector/src/lib.rs index 5634e9c0cb12b..8efcad8c086f1 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/lib.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/lib.rs @@ -30,7 +30,7 @@ use polkadot_node_subsystem::{ overseer, ActiveLeavesUpdate, FromOrchestra, OverseerSignal, SpawnedSubsystem, SubsystemError, }; use polkadot_node_subsystem::messages::ConsensusStatisticsCollectorMessage; -use polkadot_primitives::{Hash, SessionIndex, ValidatorIndex}; +use polkadot_primitives::{AuthorityDiscoveryId, Hash, SessionIndex, ValidatorIndex}; use polkadot_node_primitives::approval::time::Tick; use polkadot_node_primitives::approval::v1::DelayTranche; use polkadot_primitives::well_known_keys::relay_dispatch_queue_remaining_capacity; @@ -46,8 +46,9 @@ mod approval_voting_metrics; mod availability_distribution_metrics; use approval_voting_metrics::ApprovalsStats; +use polkadot_node_subsystem_util::{request_session_index_for_child, request_session_info}; use crate::approval_voting_metrics::{handle_candidate_approved, handle_observed_no_shows}; -use crate::availability_distribution_metrics::{handle_chunks_downloaded, AvailabilityDownloads}; +use crate::availability_distribution_metrics::{handle_chunks_downloaded, AvailabilityChunks}; use self::metrics::Metrics; const LOG_TARGET: &str = "parachain::consensus-statistics-collector"; @@ -66,18 +67,31 @@ impl PerRelayView { } } +pub struct PerSessionView { + authorities_lookup: HashMap, +} + +impl PerSessionView { + fn new(authorities_lookup: HashMap) -> Self { + Self { authorities_lookup } + } +} + struct View { per_relay: HashMap, + per_session: HashMap, + // TODO: this information should not be needed candidates_per_session: HashMap>, - chunks_downloaded: AvailabilityDownloads, + availability_chunks: AvailabilityChunks, } impl View { fn new() -> Self { return View{ per_relay: HashMap::new(), + per_session: HashMap::new(), candidates_per_session: HashMap::new(), - chunks_downloaded: AvailabilityDownloads::new(), + availability_chunks: AvailabilityChunks::new(), }; } } @@ -127,12 +141,36 @@ pub(crate) async fn run_iteration( view: &mut View, metrics: &Metrics, ) -> Result<()> { + let mut sender = ctx.sender().clone(); loop { match ctx.recv().await.map_err(FatalError::SubsystemReceive)? { FromOrchestra::Signal(OverseerSignal::Conclude) => return Ok(()), FromOrchestra::Signal(OverseerSignal::ActiveLeaves(update)) => { if let Some(activated) = update.activated { view.per_relay.insert(activated.hash, PerRelayView::new(vec![])); + + let session_idx = request_session_index_for_child(activated.hash, ctx.sender()) + .await + .await + .map_err(JfyiError::OverseerCommunication)? + .map_err(JfyiError::RuntimeApiCallError)?; + + if !view.per_session.contains_key(&session_idx) { + let session_info = request_session_info(activated.hash, session_idx, ctx.sender()) + .await + .await + .map_err(JfyiError::OverseerCommunication)? + .map_err(JfyiError::RuntimeApiCallError)?; + + if let Some(session_info) = session_info { + let mut authority_lookup = HashMap::new(); + for (i, ad) in session_info.discovery_keys.iter().cloned().enumerate() { + authority_lookup.insert(ad, ValidatorIndex(i as _)); + } + + view.per_session.insert(session_idx, PerSessionView::new(authority_lookup)); + } + } } }, FromOrchestra::Signal(OverseerSignal::BlockFinalized(..)) => {}, @@ -147,6 +185,13 @@ pub(crate) async fn run_iteration( downloads, ) }, + ConsensusStatisticsCollectorMessage::ChunkUploaded(candidate_hash, authority_ids) => { + handle_chunk_uploaded( + view, + candidate_hash, + authority_ids, + ) + }, ConsensusStatisticsCollectorMessage::CandidateApproved(candidate_hash, block_hash, approvals) => { handle_candidate_approved( view, diff --git a/polkadot/node/network/availability-distribution/Cargo.toml b/polkadot/node/network/availability-distribution/Cargo.toml index 93565628e6ed2..12ae0439dbf86 100644 --- a/polkadot/node/network/availability-distribution/Cargo.toml +++ b/polkadot/node/network/availability-distribution/Cargo.toml @@ -30,10 +30,12 @@ polkadot-node-subsystem-util = { workspace = true, default-features = true } polkadot-primitives = { workspace = true, default-features = true } rand = { workspace = true, default-features = true } sc-network = { workspace = true, default-features = true } +sc-network-types = { workspace = true, default-features = true } schnellru = { workspace = true } sp-core = { features = ["std"], workspace = true, default-features = true } sp-keystore = { workspace = true, default-features = true } thiserror = { workspace = true } +async-trait = "0.1.88" [dev-dependencies] assert_matches = { workspace = true } diff --git a/polkadot/node/network/availability-distribution/src/lib.rs b/polkadot/node/network/availability-distribution/src/lib.rs index 21c92def0c791..b8cd15138767b 100644 --- a/polkadot/node/network/availability-distribution/src/lib.rs +++ b/polkadot/node/network/availability-distribution/src/lib.rs @@ -46,16 +46,18 @@ use responder::{run_chunk_receivers, run_pov_receiver}; mod metrics; /// Prometheus `Metrics` for availability distribution. pub use metrics::Metrics; +use polkadot_node_network_protocol::authority_discovery::AuthorityDiscovery; #[cfg(test)] -mod tests; +pub mod tests; const LOG_TARGET: &'static str = "parachain::availability-distribution"; /// The availability distribution subsystem. -pub struct AvailabilityDistributionSubsystem { +pub struct AvailabilityDistributionSubsystem { /// Easy and efficient runtime access for this subsystem. runtime: RuntimeInfo, + authority_discovery_service: AD, /// Receivers to receive messages from. recvs: IncomingRequestReceivers, /// Mapping of the req-response protocols to the full protocol names. @@ -75,7 +77,9 @@ pub struct IncomingRequestReceivers { } #[overseer::subsystem(AvailabilityDistribution, error=SubsystemError, prefix=self::overseer)] -impl AvailabilityDistributionSubsystem { +impl AvailabilityDistributionSubsystem + where AD: AuthorityDiscovery + Clone + Sync, +{ fn start(self, ctx: Context) -> SpawnedSubsystem { let future = self .run(ctx) @@ -87,21 +91,25 @@ impl AvailabilityDistributionSubsystem { } #[overseer::contextbounds(AvailabilityDistribution, prefix = self::overseer)] -impl AvailabilityDistributionSubsystem { +impl AvailabilityDistributionSubsystem +where + AD: AuthorityDiscovery + Clone + Sync +{ /// Create a new instance of the availability distribution. pub fn new( keystore: KeystorePtr, recvs: IncomingRequestReceivers, req_protocol_names: ReqProtocolNames, + authority_discovery_service: AD, metrics: Metrics, ) -> Self { let runtime = RuntimeInfo::new(Some(keystore)); - Self { runtime, recvs, req_protocol_names, metrics } + Self { runtime, authority_discovery_service, recvs, req_protocol_names, metrics } } /// Start processing work as passed on from the Overseer. async fn run(self, mut ctx: Context) -> std::result::Result<(), FatalError> { - let Self { mut runtime, recvs, metrics, req_protocol_names } = self; + let Self { mut runtime, mut authority_discovery_service, recvs, metrics, req_protocol_names } = self; let IncomingRequestReceivers { pov_req_receiver, @@ -123,6 +131,7 @@ impl AvailabilityDistributionSubsystem { "chunk-receiver", run_chunk_receivers( sender, + authority_discovery_service, chunk_req_v1_receiver, chunk_req_v2_receiver, metrics.clone(), diff --git a/polkadot/node/network/availability-distribution/src/responder.rs b/polkadot/node/network/availability-distribution/src/responder.rs index a9e345d4c0496..8c269a50fbedc 100644 --- a/polkadot/node/network/availability-distribution/src/responder.rs +++ b/polkadot/node/network/availability-distribution/src/responder.rs @@ -76,7 +76,8 @@ pub async fn run_chunk_receivers( mut receiver_v2: IncomingRequestReceiver, metrics: Metrics, ) where - Sender: SubsystemSender, + Sender: SubsystemSender + + SubsystemSender, AD: AuthorityDiscovery + Clone + Sync { let make_resp_v1 = |chunk: Option| match chunk { @@ -169,13 +170,13 @@ pub async fn answer_chunk_request_log( make_response: MakeResp, metrics: &Metrics, ) where - AD: AuthorityDiscovery, + AD: AuthorityDiscovery , Req: IsRequest + Decode + Encode + Into, Req::Response: Encode, - Sender: SubsystemSender, + Sender: SubsystemSender + + SubsystemSender, MakeResp: Fn(Option) -> Req::Response, { - let incoming_peer_id = req.peer; let res = answer_chunk_request(sender, authority_discovery, req, make_response).await; match res { Ok(result) => { @@ -244,8 +245,11 @@ where let result = chunk.is_some(); if result { - let authority_ids = authority_discovery.get_authority_ids_by_peer_id(req.peer); - + let authority_ids = authority_discovery.get_authority_ids_by_peer_id(req.peer).await; + if let Some(authority_ids) = authority_ids { + _ = sender.try_send_message( + ConsensusStatisticsCollectorMessage::ChunkUploaded(payload.candidate_hash, authority_ids)); + } } gum::trace!( diff --git a/polkadot/node/network/availability-distribution/src/tests/mock.rs b/polkadot/node/network/availability-distribution/src/tests/mock.rs index 0380bc7b7e12c..0a3fa0157929c 100644 --- a/polkadot/node/network/availability-distribution/src/tests/mock.rs +++ b/polkadot/node/network/availability-distribution/src/tests/mock.rs @@ -16,21 +16,26 @@ //! Helper functions and tools to generate mock data useful for testing this subsystem. +use std::collections::HashSet; use std::sync::Arc; +use async_trait::async_trait; use sp_keyring::Sr25519Keyring; use polkadot_erasure_coding::{branches, obtain_chunks_v1 as obtain_chunks}; use polkadot_node_primitives::{AvailableData, BlockData, ErasureChunk, PoV, Proof}; +use polkadot_node_network_protocol::authority_discovery::AuthorityDiscovery; use polkadot_primitives::{ CandidateCommitments, CandidateHash, ChunkIndex, CommittedCandidateReceiptV2, GroupIndex, Hash, HeadData, Id as ParaId, IndexedVec, OccupiedCore, PersistedValidationData, SessionInfo, - ValidatorIndex, + ValidatorIndex, AuthorityDiscoveryId }; use polkadot_primitives_test_helpers::{ dummy_collator, dummy_collator_signature, dummy_hash, dummy_validation_code, CandidateDescriptor, CommittedCandidateReceipt, }; +use sc_network::Multiaddr; +use sc_network_types::PeerId; /// Create dummy session info with two validator groups. pub fn make_session_info() -> SessionInfo { @@ -164,3 +169,23 @@ pub fn get_valid_chunk_data( .expect("There really should be enough chunks."); (root, chunk) } + +#[derive(Debug, Clone)] +pub struct MockEmptyAuthorityDiscovery; + +impl MockEmptyAuthorityDiscovery { + pub fn new() -> Self { + MockEmptyAuthorityDiscovery {} + } +} + +#[async_trait] +impl AuthorityDiscovery for MockEmptyAuthorityDiscovery { + async fn get_addresses_by_authority_id(&mut self, authority: AuthorityDiscoveryId) -> Option> { + None + } + + async fn get_authority_ids_by_peer_id(&mut self, peer_id: PeerId) -> Option> { + None + } +} \ No newline at end of file diff --git a/polkadot/node/network/availability-distribution/src/tests/mod.rs b/polkadot/node/network/availability-distribution/src/tests/mod.rs index 078220607c37f..8792122be0eea 100644 --- a/polkadot/node/network/availability-distribution/src/tests/mod.rs +++ b/polkadot/node/network/availability-distribution/src/tests/mod.rs @@ -61,6 +61,7 @@ fn test_harness>( keystore, IncomingRequestReceivers { pov_req_receiver, chunk_req_v1_receiver, chunk_req_v2_receiver }, req_protocol_names, + mock::MockEmptyAuthorityDiscovery, Default::default(), ); let subsystem = subsystem.run(context); diff --git a/polkadot/node/overseer/src/lib.rs b/polkadot/node/overseer/src/lib.rs index a1bd1ce1e596c..0893006500407 100644 --- a/polkadot/node/overseer/src/lib.rs +++ b/polkadot/node/overseer/src/lib.rs @@ -519,6 +519,7 @@ pub struct Overseer { #[subsystem(AvailabilityDistributionMessage, sends: [ AvailabilityStoreMessage, ChainApiMessage, + ConsensusStatisticsCollectorMessage, RuntimeApiMessage, NetworkBridgeTxMessage, ])] @@ -663,7 +664,10 @@ pub struct Overseer { ])] prospective_parachains: ProspectiveParachains, - #[subsystem(ConsensusStatisticsCollectorMessage, sends: [])] + #[subsystem(ConsensusStatisticsCollectorMessage, sends: [ + RuntimeApiMessage, + ChainApiMessage, + ])] consensus_statistics_collector: ConsensusStatisticsCollector, /// External listeners waiting for a hash to be in the active-leave set. diff --git a/polkadot/node/service/src/overseer.rs b/polkadot/node/service/src/overseer.rs index fb92bea540b60..febe108084c31 100644 --- a/polkadot/node/service/src/overseer.rs +++ b/polkadot/node/service/src/overseer.rs @@ -193,7 +193,9 @@ pub fn validator_overseer_builder( PvfCheckerSubsystem, CandidateBackingSubsystem, StatementDistributionSubsystem, - AvailabilityDistributionSubsystem, + AvailabilityDistributionSubsystem< + AuthorityDiscoveryService + >, AvailabilityRecoverySubsystem, BitfieldSigningSubsystem, BitfieldDistributionSubsystem, @@ -263,6 +265,7 @@ where chunk_req_v2_receiver, }, req_protocol_names.clone(), + authority_discovery_service.clone(), Metrics::register(registry)?, )) .availability_recovery(AvailabilityRecoverySubsystem::for_validator( @@ -352,7 +355,7 @@ where )) .chain_selection(ChainSelectionSubsystem::new(chain_selection_config, parachains_db)) .prospective_parachains(ProspectiveParachainsSubsystem::new(Metrics::register(registry)?)) - .statistics_collector(ConsensusStatisticsCollector::new(Metrics::register(registry)?)) + .consensus_statistics_collector(ConsensusStatisticsCollector::new(Metrics::register(registry)?)) .activation_external_listeners(Default::default()) .active_leaves(Default::default()) .supports_parachains(runtime_client) @@ -499,7 +502,7 @@ where .dispute_distribution(DummySubsystem) .chain_selection(DummySubsystem) .prospective_parachains(DummySubsystem) - .statistics_collector(DummySubsystem) + .consensus_statistics_collector(DummySubsystem) .activation_external_listeners(Default::default()) .active_leaves(Default::default()) .supports_parachains(runtime_client) diff --git a/polkadot/node/subsystem-bench/src/lib/availability/mod.rs b/polkadot/node/subsystem-bench/src/lib/availability/mod.rs index b346f988a3c90..e5865164f9a5d 100644 --- a/polkadot/node/subsystem-bench/src/lib/availability/mod.rs +++ b/polkadot/node/subsystem-bench/src/lib/availability/mod.rs @@ -28,6 +28,7 @@ use crate::{ network::new_network, usage::BenchmarkUsage, }; +use async_trait::async_trait; use colored::Colorize; use futures::{channel::oneshot, stream::FuturesUnordered, StreamExt}; @@ -40,6 +41,7 @@ use polkadot_availability_recovery::{AvailabilityRecoverySubsystem, RecoveryStra use polkadot_node_core_av_store::AvailabilityStoreSubsystem; use polkadot_node_metrics::metrics::Metrics; use polkadot_node_network_protocol::{ + authority_discovery::AuthorityDiscovery, request_response::{v1, v2, IncomingRequest}, OurView, }; @@ -49,15 +51,17 @@ use polkadot_node_subsystem::{ }; use polkadot_node_subsystem_types::messages::{AvailabilityStoreMessage, NetworkBridgeEvent}; use polkadot_overseer::{metrics::Metrics as OverseerMetrics, Handle as OverseerHandle}; -use polkadot_primitives::{Block, CoreIndex, GroupIndex, Hash}; +use polkadot_primitives::{AuthorityDiscoveryId, Block, CoreIndex, GroupIndex, Hash}; use sc_network::request_responses::{IncomingRequest as RawIncomingRequest, ProtocolConfig}; use std::{ops::Sub, sync::Arc, time::Instant}; +use std::collections::HashSet; use strum::Display; use sc_service::SpawnTaskHandle; use serde::{Deserialize, Serialize}; +use sc_network_types::multiaddr::Multiaddr; +use sc_network_types::PeerId; pub use test_state::TestState; - mod av_store_helpers; mod test_state; @@ -120,7 +124,7 @@ fn build_overseer_for_availability_write( spawn_task_handle: SpawnTaskHandle, runtime_api: MockRuntimeApi, (network_bridge_tx, network_bridge_rx): (MockNetworkBridgeTx, MockNetworkBridgeRx), - availability_distribution: AvailabilityDistributionSubsystem, + availability_distribution: AvailabilityDistributionSubsystem, chain_api: MockChainApi, availability_store: AvailabilityStoreSubsystem, bitfield_distribution: BitfieldDistribution, @@ -267,6 +271,7 @@ pub fn prepare_test( chunk_req_v2_receiver, }, state.req_protocol_names.clone(), + TestAuthorityDiscovery, Metrics::try_register(&dependencies.registry).unwrap(), ); @@ -506,3 +511,17 @@ pub async fn benchmark_availability_write( false, ) } + +#[derive(Debug, Clone)] +pub struct TestAuthorityDiscovery; + +#[async_trait] +impl AuthorityDiscovery for TestAuthorityDiscovery { + async fn get_addresses_by_authority_id(&mut self, authority: AuthorityDiscoveryId) -> Option> { + None + } + + async fn get_authority_ids_by_peer_id(&mut self, peer_id: PeerId) -> Option> { + None + } +} diff --git a/polkadot/node/subsystem-types/src/messages.rs b/polkadot/node/subsystem-types/src/messages.rs index 8f76963bd79ad..3a96998aa065b 100644 --- a/polkadot/node/subsystem-types/src/messages.rs +++ b/polkadot/node/subsystem-types/src/messages.rs @@ -1469,7 +1469,7 @@ pub enum ProspectiveParachainsMessage { #[derive(Debug)] pub enum ConsensusStatisticsCollectorMessage { ChunksDownloaded(SessionIndex, CandidateHash, HashMap), - ChunksUploaded(CandidateHash, HashSet<()>) + ChunkUploaded(CandidateHash, HashSet), // Candidate received enough approval and now is approved CandidateApproved(CandidateHash, Hash, Vec), From 6b7504c18df5617a4c60d76efb788ec0eb4b2149 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Tue, 7 Oct 2025 16:00:21 -0400 Subject: [PATCH 18/48] chore: finish handle_chunk_upload --- Cargo.toml | 3 +- .../src/availability_distribution_metrics.rs | 57 ++++++++++++++++++- .../consensus-statistics-collector/src/lib.rs | 8 +-- polkadot/node/service/Cargo.toml | 3 +- 4 files changed, 61 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 99f6906b9e8e1..fcaade0beabb9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -188,7 +188,6 @@ members = [ "polkadot/node/core/pvf/execute-worker", "polkadot/node/core/pvf/prepare-worker", "polkadot/node/core/runtime-api", - "polkadot/node/core/consensus-statistics-collector", "polkadot/node/gum", "polkadot/node/gum/proc-macro", "polkadot/node/malus", @@ -1139,7 +1138,7 @@ polkadot-node-core-parachains-inherent = { path = "polkadot/node/core/parachains polkadot-node-core-prospective-parachains = { path = "polkadot/node/core/prospective-parachains", default-features = false } polkadot-node-core-consensus-statistics-collector = { path = "polkadot/node/core/consensus-statistics-collector", default-features = false } polkadot-node-core-provisioner = { path = "polkadot/node/core/provisioner", default-features = false } -polkadot-node-core-pvf = { path = "polkadot/node/core/pvf", default-features = false } +polkadot-node-core-pvf = { path = "polkadot/node/core/pvf", default-features = false } polkadot-node-core-pvf-checker = { path = "polkadot/node/core/pvf-checker", default-features = false } polkadot-node-core-pvf-common = { path = "polkadot/node/core/pvf/common", default-features = false } polkadot-node-core-pvf-execute-worker = { path = "polkadot/node/core/pvf/execute-worker", default-features = false } diff --git a/polkadot/node/core/consensus-statistics-collector/src/availability_distribution_metrics.rs b/polkadot/node/core/consensus-statistics-collector/src/availability_distribution_metrics.rs index 0cdb03b1a05dd..3f06285cfc3f0 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/availability_distribution_metrics.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/availability_distribution_metrics.rs @@ -2,17 +2,19 @@ use std::collections::{HashMap, HashSet}; use std::collections::hash_map::Entry; use std::ops::Add; use gum::CandidateHash; -use polkadot_primitives::{SessionIndex, ValidatorIndex}; +use polkadot_primitives::{AuthorityDiscoveryId, SessionIndex, ValidatorIndex}; use crate::View; pub struct AvailabilityChunks { pub downloads_per_candidate: HashMap>, + pub uploads_per_candidate: HashMap>, } impl AvailabilityChunks { pub fn new() -> Self { Self { downloads_per_candidate: Default::default(), + uploads_per_candidate: Default::default(), } } @@ -29,6 +31,20 @@ impl AvailabilityChunks { .and_modify(|v| *v += count) .or_insert(count); } + + pub fn note_candidate_chunk_uploaded( + &mut self, + candidate_hash: CandidateHash, + validator_index: ValidatorIndex, + count: u64, + ) { + let _ = self.uploads_per_candidate + .entry(candidate_hash) + .or_default() + .entry(validator_index) + .and_modify(|v| *v += count) + .or_insert(count); + } } // whenever chunks are acquired throughout availability @@ -47,12 +63,47 @@ pub fn handle_chunks_downloaded( for (validator_index, download_count) in downloads { view.availability_chunks - .note_candidate_chunk_downloaded(candidate_hash, validator_index, download_count) + .entry(session_index) + .and_modify(|av_chunks_stats| av_chunks_stats.note_candidate_chunk_downloaded(candidate_hash, validator_index, download_count)) + .or_insert(AvailabilityChunks::new()); } } pub fn handle_chunk_uploaded( - + view: &mut View, + candidate_hash: CandidateHash, + authority_ids: HashSet, ) { + // check if candidate is present + let sessions = view.candidates_per_session + .iter() + .filter_map(|(session_index, candidates)| { + if candidates.contains(&candidate_hash) { + return Some(session_index); + } + + None + }); + for session_index in sessions { + let validator_index = view.per_session.get(session_index).and_then(|session_view| { + for authority_id in authority_ids.iter() { + match session_view.authorities_lookup.get(authority_id) { + Some(validator_idx) => return Some(validator_idx), + None => continue, + } + } + None + }); + + if validator_index.is_none() { + continue; + } + + view.availability_chunks + .entry(session_index.clone()) + .and_modify(|av_chunks_stats| av_chunks_stats + .note_candidate_chunk_uploaded(candidate_hash, validator_index.unwrap().clone(), 1)) + .or_insert(AvailabilityChunks::new()); + } } \ No newline at end of file diff --git a/polkadot/node/core/consensus-statistics-collector/src/lib.rs b/polkadot/node/core/consensus-statistics-collector/src/lib.rs index 8efcad8c086f1..bd5f21c4e0c92 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/lib.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/lib.rs @@ -46,9 +46,9 @@ mod approval_voting_metrics; mod availability_distribution_metrics; use approval_voting_metrics::ApprovalsStats; -use polkadot_node_subsystem_util::{request_session_index_for_child, request_session_info}; +use polkadot_node_subsystem_util::{request_candidate_events, request_session_index_for_child, request_session_info}; use crate::approval_voting_metrics::{handle_candidate_approved, handle_observed_no_shows}; -use crate::availability_distribution_metrics::{handle_chunks_downloaded, AvailabilityChunks}; +use crate::availability_distribution_metrics::{handle_chunk_uploaded, handle_chunks_downloaded, AvailabilityChunks}; use self::metrics::Metrics; const LOG_TARGET: &str = "parachain::consensus-statistics-collector"; @@ -82,7 +82,7 @@ struct View { per_session: HashMap, // TODO: this information should not be needed candidates_per_session: HashMap>, - availability_chunks: AvailabilityChunks, + availability_chunks: HashMap, } impl View { @@ -91,7 +91,7 @@ impl View { per_relay: HashMap::new(), per_session: HashMap::new(), candidates_per_session: HashMap::new(), - availability_chunks: AvailabilityChunks::new(), + availability_chunks: HashMap::new(), }; } } diff --git a/polkadot/node/service/Cargo.toml b/polkadot/node/service/Cargo.toml index be78a8850973d..68b73a6c6553d 100644 --- a/polkadot/node/service/Cargo.toml +++ b/polkadot/node/service/Cargo.toml @@ -127,12 +127,12 @@ polkadot-node-core-chain-api = { optional = true, workspace = true, default-feat polkadot-node-core-chain-selection = { optional = true, workspace = true, default-features = true } polkadot-node-core-dispute-coordinator = { optional = true, workspace = true, default-features = true } polkadot-node-core-prospective-parachains = { optional = true, workspace = true, default-features = true } +polkadot-node-core-consensus-statistics-collector = { optional = true, workspace = true, default-features = true } polkadot-node-core-provisioner = { optional = true, workspace = true, default-features = true } polkadot-node-core-pvf = { optional = true, workspace = true, default-features = true } polkadot-node-core-pvf-checker = { optional = true, workspace = true, default-features = true } polkadot-node-core-runtime-api = { optional = true, workspace = true, default-features = true } polkadot-statement-distribution = { optional = true, workspace = true, default-features = true } -polkadot-node-core-consensus-statistics-collector = { optional = true, workspace = true, default-features = true } xcm = { workspace = true, default-features = true } xcm-runtime-apis = { workspace = true, default-features = true } @@ -170,6 +170,7 @@ full-node = [ "polkadot-node-core-chain-selection", "polkadot-node-core-dispute-coordinator", "polkadot-node-core-prospective-parachains", + "polkadot-node-core-consensus-statistics-collector", "polkadot-node-core-provisioner", "polkadot-node-core-pvf", "polkadot-node-core-pvf-checker", From 9ae0211d2c062659099be73a357b939f009b49e8 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Wed, 15 Oct 2025 09:28:20 -0400 Subject: [PATCH 19/48] chore: including prunning based on block finalisation --- Cargo.lock | 53 +++++-- polkadot/node/core/approval-voting/src/lib.rs | 40 ++++-- polkadot/node/core/chain-api/src/lib.rs | 1 + .../src/approval_voting_metrics.rs | 31 +++- .../src/availability_distribution_metrics.rs | 16 +++ .../src/error.rs | 5 +- .../consensus-statistics-collector/src/lib.rs | 134 ++++++++++++++++-- .../src/metrics.rs | 28 +++- .../src/tests.rs | 16 +++ .../tests/functional/mod.rs | 1 + .../rewards_statistics_collector.rs | 106 ++++++++++++++ substrate/frame/executive/src/lib.rs | 8 +- 12 files changed, 395 insertions(+), 44 deletions(-) create mode 100644 polkadot/zombienet-sdk-tests/tests/functional/rewards_statistics_collector.rs diff --git a/Cargo.lock b/Cargo.lock index 3b65ebef6ec3c..262f91e702b3e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5282,7 +5282,7 @@ version = "0.1.0" dependencies = [ "anyhow", "cumulus-zombienet-sdk-helpers", - "env_logger 0.11.3", + "env_logger 0.11.8", "futures", "log", "polkadot-primitives", @@ -6135,14 +6135,14 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.3" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" dependencies = [ "anstream", "anstyle", "env_filter", - "humantime", + "jiff", "log", ] @@ -8793,6 +8793,30 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "jiff" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +dependencies = [ + "proc-macro2 1.0.95", + "quote 1.0.40", + "syn 2.0.98", +] + [[package]] name = "jni" version = "0.19.0" @@ -13255,7 +13279,7 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", - "env_logger 0.11.3", + "env_logger 0.11.8", "futures", "git2", "hex", @@ -13541,7 +13565,7 @@ name = "pallet-staking-async" version = "0.1.0" dependencies = [ "anyhow", - "env_logger 0.11.3", + "env_logger 0.11.8", "frame-benchmarking", "frame-election-provider-support", "frame-support", @@ -17305,7 +17329,7 @@ version = "0.1.0" dependencies = [ "anyhow", "cumulus-zombienet-sdk-helpers", - "env_logger 0.11.3", + "env_logger 0.11.8", "log", "parity-scale-codec", "polkadot-primitives", @@ -17589,6 +17613,15 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "portpicker" version = "0.1.1" @@ -21072,7 +21105,7 @@ dependencies = [ "chrono", "criterion", "cumulus-zombienet-sdk-helpers", - "env_logger 0.11.3", + "env_logger 0.11.8", "futures", "futures-timer", "indexmap 2.9.0", @@ -25465,7 +25498,7 @@ name = "template-zombienet-tests" version = "0.0.0" dependencies = [ "anyhow", - "env_logger 0.11.3", + "env_logger 0.11.8", "tokio", "zombienet-sdk", ] @@ -25543,7 +25576,7 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e33b98a582ea0be1168eba097538ee8dd4bbe0f2b01b22ac92ea30054e5be7b" dependencies = [ - "env_logger 0.11.3", + "env_logger 0.11.8", "test-log-macros", "tracing-subscriber 0.3.18", ] diff --git a/polkadot/node/core/approval-voting/src/lib.rs b/polkadot/node/core/approval-voting/src/lib.rs index f1b989eb50e1e..c1f6b247b6329 100644 --- a/polkadot/node/core/approval-voting/src/lib.rs +++ b/polkadot/node/core/approval-voting/src/lib.rs @@ -96,8 +96,6 @@ use polkadot_node_primitives::approval::time::{ slot_number_to_tick, Clock, ClockExt, DelayedApprovalTimer, SystemClock, Tick, }; use polkadot_node_subsystem::messages::ConsensusStatisticsCollectorMessage; -use polkadot_overseer::Subsystem; - mod approval_checking; pub mod approval_db; mod backend; @@ -1308,6 +1306,11 @@ where let mut delayed_approvals_timers = DelayedApprovalTimer::default(); let mut approvals_cache = LruMap::new(ByLength::new(APPROVAL_CACHE_SIZE)); + gum::info!( + target: LOG_TARGET, + "Approval Voting Subsytem is running..." + ); + loop { let mut overlayed_db = OverlayedBackend::new(&backend); let actions = futures::select! { @@ -2615,11 +2618,10 @@ fn schedule_wakeup_action( block_hash: Hash, block_number: BlockNumber, candidate_hash: CandidateHash, - block_tick: Tick, + approval_status: &ApprovalStatus, tick_now: Tick, - required_tranches: RequiredTranches, ) -> Option { - let maybe_action = match required_tranches { + let maybe_action = match approval_status.required_tranches { _ if approval_entry.is_approved() => None, RequiredTranches::All => None, RequiredTranches::Exact { next_no_show, last_assignment_tick, .. } => { @@ -2658,7 +2660,7 @@ fn schedule_wakeup_action( // Apply the clock drift to these tranches. min_prefer_some(next_announced, our_untriggered) - .map(|t| t as Tick + block_tick + clock_drift) + .map(|t| t as Tick + approval_status.block_tick + clock_drift) }; min_prefer_some(next_non_empty_tranche, next_no_show).map(|tick| { @@ -2673,14 +2675,14 @@ fn schedule_wakeup_action( tick, ?candidate_hash, ?block_hash, - block_tick, + approval_status.block_tick, "Scheduling next wakeup.", ), None => gum::trace!( target: LOG_TARGET, ?candidate_hash, ?block_hash, - block_tick, + approval_status.block_tick, "No wakeup needed.", ), Some(_) => {}, // unreachable @@ -2858,9 +2860,8 @@ where block_entry.block_hash(), block_entry.block_number(), *assigned_candidate_hash, - status.block_tick, + &status, tick_now, - status.required_tranches, )); } @@ -3202,9 +3203,8 @@ where block_hash, block_number, candidate_hash, - status.block_tick.clone(), + &status, tick_now, - status.required_tranches.clone(), )); if is_approved && transition.is_remote_approval() { @@ -3264,6 +3264,13 @@ where }; if newly_approved { + gum::info!( + target: LOG_TARGET, + ?block_hash, + ?candidate_hash, + "Candidate newly approved, collecting useful approvals..." + ); + collect_useful_approvals(sender, &status, block_hash, &candidate_entry); if status.no_show_validators.len() > 0 { @@ -4140,6 +4147,15 @@ where }; if collected_useful_approvals.len() > 0 { + let useful_approvals = collected_useful_approvals.len(); + gum::info!( + target: LOG_TARGET, + ?block_hash, + ?candidate_hash, + ?useful_approvals, + "collected useful approvals" + ); + _ = sender.try_send_message(ConsensusStatisticsCollectorMessage::CandidateApproved( candidate_hash, block_hash, diff --git a/polkadot/node/core/chain-api/src/lib.rs b/polkadot/node/core/chain-api/src/lib.rs index 7fd5166310fec..0875236abcd02 100644 --- a/polkadot/node/core/chain-api/src/lib.rs +++ b/polkadot/node/core/chain-api/src/lib.rs @@ -118,6 +118,7 @@ where let result = subsystem.client.hash(number).await.map_err(|e| e.to_string().into()); subsystem.metrics.on_request(result.is_ok()); + let _ = response_channel.send(result); }, ChainApiMessage::FinalizedBlockNumber(response_channel) => { diff --git a/polkadot/node/core/consensus-statistics-collector/src/approval_voting_metrics.rs b/polkadot/node/core/consensus-statistics-collector/src/approval_voting_metrics.rs index 75348a0c7b449..062472e3e4350 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/approval_voting_metrics.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/approval_voting_metrics.rs @@ -1,6 +1,22 @@ +// Copyright (C) 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 std::collections::{HashMap, HashSet}; -use std::collections::hash_map::Entry; use polkadot_primitives::{CandidateHash, Hash, SessionIndex, ValidatorIndex}; +use crate::metrics::Metrics; use crate::View; #[derive(Debug, Clone, Default)] @@ -19,13 +35,20 @@ pub fn handle_candidate_approved( view: &mut View, block_hash: Hash, candidate_hash: CandidateHash, - approvals: Vec + approvals: Vec, + metrics: &Metrics, ) { if let Some(relay_view) = view.per_relay.get_mut(&block_hash) { relay_view.approvals_stats .entry(candidate_hash) - .and_modify(|a: &mut ApprovalsStats| a.votes.extend(approvals.iter())) - .or_insert(ApprovalsStats::new(HashSet::from_iter(approvals), HashSet::new())); + .and_modify(|a: &mut ApprovalsStats| { + metrics.record_approvals_usage(approvals.len() as u64); + a.votes.extend(approvals.iter()) + }) + .or_insert_with(|| { + metrics.record_approvals_usage(approvals.len() as u64); + ApprovalsStats::new(HashSet::from_iter(approvals), HashSet::new()) + }); } } diff --git a/polkadot/node/core/consensus-statistics-collector/src/availability_distribution_metrics.rs b/polkadot/node/core/consensus-statistics-collector/src/availability_distribution_metrics.rs index 3f06285cfc3f0..1ae80731db282 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/availability_distribution_metrics.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/availability_distribution_metrics.rs @@ -1,3 +1,19 @@ +// Copyright (C) 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 std::collections::{HashMap, HashSet}; use std::collections::hash_map::Entry; use std::ops::Add; diff --git a/polkadot/node/core/consensus-statistics-collector/src/error.rs b/polkadot/node/core/consensus-statistics-collector/src/error.rs index 3c4fb145ad7af..8e9184603427f 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/error.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/error.rs @@ -18,7 +18,7 @@ use futures::channel::oneshot; -use polkadot_node_subsystem::{RuntimeApiError, SubsystemError}; +use polkadot_node_subsystem::{ChainApiError, RuntimeApiError, SubsystemError}; use polkadot_node_subsystem_util::runtime; use crate::LOG_TARGET; @@ -36,6 +36,9 @@ pub enum Error { #[error("Failed to request runtime data: {0}")] RuntimeApiCallError(#[source] RuntimeApiError), + + #[error("Failed to request chain api data: {0}")] + ChainApiCallError(#[source] ChainApiError), } /// General `Result` type. diff --git a/polkadot/node/core/consensus-statistics-collector/src/lib.rs b/polkadot/node/core/consensus-statistics-collector/src/lib.rs index bd5f21c4e0c92..e01dae0704b76 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/lib.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/lib.rs @@ -22,15 +22,15 @@ //! on the approval work carried out by all session validators. -use std::collections::{HashMap, HashSet}; +use std::collections::{HashMap, HashSet, VecDeque}; use std::collections::hash_map::Entry; use futures::{channel::oneshot, prelude::*}; use gum::CandidateHash; use polkadot_node_subsystem::{ overseer, ActiveLeavesUpdate, FromOrchestra, OverseerSignal, SpawnedSubsystem, SubsystemError, }; -use polkadot_node_subsystem::messages::ConsensusStatisticsCollectorMessage; -use polkadot_primitives::{AuthorityDiscoveryId, Hash, SessionIndex, ValidatorIndex}; +use polkadot_node_subsystem::messages::{ChainApiMessage, ConsensusStatisticsCollectorMessage}; +use polkadot_primitives::{AuthorityDiscoveryId, BlockNumber, Hash, Header, SessionIndex, ValidatorIndex}; use polkadot_node_primitives::approval::time::Tick; use polkadot_node_primitives::approval::v1::DelayTranche; use polkadot_primitives::well_known_keys::relay_dispatch_queue_remaining_capacity; @@ -54,17 +54,23 @@ use self::metrics::Metrics; const LOG_TARGET: &str = "parachain::consensus-statistics-collector"; struct PerRelayView { - relay_approved: bool, + parent_hash: Option, + children: HashSet, approvals_stats: HashMap, } impl PerRelayView { - fn new(candidates: Vec) -> Self { - return PerRelayView{ - relay_approved: false, + fn new(parent_hash: Option) -> Self { + PerRelayView{ + parent_hash: parent_hash, + children: HashSet::new(), approvals_stats: HashMap::new(), } } + + fn link_child(&mut self, hash: Hash) { + self.children.insert(hash); + } } pub struct PerSessionView { @@ -78,20 +84,28 @@ impl PerSessionView { } struct View { + latest_finalized: Option<(Hash, BlockNumber)>, + best_blocks: Vec, + + roots: HashSet, per_relay: HashMap, per_session: HashMap, // TODO: this information should not be needed candidates_per_session: HashMap>, availability_chunks: HashMap, + } impl View { fn new() -> Self { return View{ + latest_finalized: None, + roots: HashSet::new(), per_relay: HashMap::new(), per_session: HashMap::new(), candidates_per_session: HashMap::new(), availability_chunks: HashMap::new(), + best_blocks: Vec::new() }; } } @@ -147,16 +161,37 @@ pub(crate) async fn run_iteration( FromOrchestra::Signal(OverseerSignal::Conclude) => return Ok(()), FromOrchestra::Signal(OverseerSignal::ActiveLeaves(update)) => { if let Some(activated) = update.activated { - view.per_relay.insert(activated.hash, PerRelayView::new(vec![])); + let relay_hash = activated.hash; + + let (tx, rx) = oneshot::channel(); + + ctx.send_message(ChainApiMessage::BlockHeader(relay_hash, tx)).await; + let header = rx + .map_err(JfyiError::OverseerCommunication) + .await? + .map_err(JfyiError::ChainApiCallError)?; + + if let Some(ref h) = header { + let parent_hash = h.clone().parent_hash; + if let Some(parent) = view.per_relay.get_mut(&parent_hash) { + parent.link_child(relay_hash.clone()); + } else { + view.roots.insert(relay_hash.clone()); + } + view.per_relay.insert(relay_hash, PerRelayView::new(Some(parent_hash))); + } else { + view.roots.insert(relay_hash.clone()); + view.per_relay.insert(relay_hash, PerRelayView::new(None)); + } - let session_idx = request_session_index_for_child(activated.hash, ctx.sender()) + let session_idx = request_session_index_for_child(relay_hash, ctx.sender()) .await .await .map_err(JfyiError::OverseerCommunication)? .map_err(JfyiError::RuntimeApiCallError)?; if !view.per_session.contains_key(&session_idx) { - let session_info = request_session_info(activated.hash, session_idx, ctx.sender()) + let session_info = request_session_info(relay_hash, session_idx, ctx.sender()) .await .await .map_err(JfyiError::OverseerCommunication)? @@ -173,7 +208,83 @@ pub(crate) async fn run_iteration( } } }, - FromOrchestra::Signal(OverseerSignal::BlockFinalized(..)) => {}, + FromOrchestra::Signal(OverseerSignal::BlockFinalized(fin_block_hash, fin_block_num)) => { + let latest_finalized = match view.latest_finalized { + Some(latest_finalized) => latest_finalized, + None => (fin_block_hash, fin_block_num), + }; + + // update the latest finalized on the view + view.latest_finalized = Some((fin_block_hash, fin_block_num)); + + // since we want to reward only valid approvals, we retain + // only finalized chain blocks and its descendants + // identify the finalized chain so we don't prune + let rb_view = match view.per_relay.get_mut(&fin_block_hash) { + Some(per_relay_view) => per_relay_view, + //TODO: the finalized block should already exists on the relay view mapping + None => continue + }; + + let mut removal_stack = Vec::new(); + let mut retain_relay_hashes = Vec::new(); + retain_relay_hashes.push(fin_block_hash); + + let mut current_block_hash = fin_block_hash; + let mut current_parent_hash = rb_view.parent_hash; + while let Some(parent_hash) = current_parent_hash { + retain_relay_hashes.push(parent_hash.clone()); + + match view.per_relay.get_mut(&parent_hash) { + Some(parent_view) => { + if parent_view.children.len() > 1 { + let filtered_set = parent_view.children + .iter() + .filter(|&child_hash| child_hash.eq(¤t_block_hash)) + .cloned() // Clone the elements to own them in the new HashSet + .collect::>(); + + removal_stack.extend(filtered_set); + + // unlink all the other children keeping only + // the one that belongs to the finalized chain + parent_view.children = HashSet::from_iter(vec![current_block_hash.clone()]); + } + current_block_hash = parent_hash; + current_parent_hash = parent_view.parent_hash; + }, + None => break + }; + } + + if view.roots.len() > 1 { + for root in view.roots.clone() { + if !retain_relay_hashes.contains(&root) { + removal_stack.push(root); + } + } + } + + let mut to_prune = HashSet::new(); + let mut queue: VecDeque = VecDeque::from(removal_stack); + + while let Some(hash) = queue.pop_front() { + if !to_prune.insert(hash) { + continue; // already seen + } + + if let Some(r_view) = view.per_relay.get(&hash) { + for child in &r_view.children { + queue.push_back(child.clone()); + } + } + } + + for rb_hash in to_prune { + view.per_relay.remove(&rb_hash); + view.roots.remove(&rb_hash); + } + } FromOrchestra::Communication { msg } => { match msg { ConsensusStatisticsCollectorMessage::ChunksDownloaded( @@ -198,6 +309,7 @@ pub(crate) async fn run_iteration( block_hash, candidate_hash, approvals, + metrics, ); } ConsensusStatisticsCollectorMessage::NoShows(candidate_hash, block_hash, no_show_validators) => { diff --git a/polkadot/node/core/consensus-statistics-collector/src/metrics.rs b/polkadot/node/core/consensus-statistics-collector/src/metrics.rs index c2e2e495f86d9..6f6ff7bafd958 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/metrics.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/metrics.rs @@ -20,11 +20,35 @@ use polkadot_node_subsystem_util::metrics::{ prometheus::{self, Gauge, GaugeVec, U64}, }; +#[derive(Clone)] +pub(crate) struct MetricsInner { + approvals_usage_total: prometheus::Counter, +} + + /// Candidate backing metrics. #[derive(Default, Clone)] -pub struct Metrics; +pub struct Metrics(pub(crate) Option); + +impl Metrics { + pub fn record_approvals_usage(&self, collected: u64) { + self.0.as_ref().map(|metrics| { + metrics.approvals_usage_total.inc_by(collected); + }); + } +} + impl metrics::Metrics for Metrics { fn try_register(registry: &prometheus::Registry) -> Result { - Ok(Metrics) + let metrics = MetricsInner { + approvals_usage_total: prometheus::register( + prometheus::Counter::new( + "polkadot_parachain_rewards_statistics_collector_approvals_usage_total", + "Total of collected meaningfull approvals used to approve a candidate", + )?, + registry, + )?, + }; + Ok(Metrics(Some(metrics))) } } diff --git a/polkadot/node/core/consensus-statistics-collector/src/tests.rs b/polkadot/node/core/consensus-statistics-collector/src/tests.rs index 7924cf12f77b8..1258bc48e4050 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/tests.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/tests.rs @@ -1,3 +1,19 @@ +// Copyright (C) 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 overseer::FromOrchestra; use polkadot_primitives::{SessionIndex}; diff --git a/polkadot/zombienet-sdk-tests/tests/functional/mod.rs b/polkadot/zombienet-sdk-tests/tests/functional/mod.rs index e28cfb4039303..27728db47fd00 100644 --- a/polkadot/zombienet-sdk-tests/tests/functional/mod.rs +++ b/polkadot/zombienet-sdk-tests/tests/functional/mod.rs @@ -10,3 +10,4 @@ mod shared_core_idle_parachain; mod spam_statement_distribution_requests; mod sync_backing; mod validator_disabling; +mod rewards_statistics_collector; diff --git a/polkadot/zombienet-sdk-tests/tests/functional/rewards_statistics_collector.rs b/polkadot/zombienet-sdk-tests/tests/functional/rewards_statistics_collector.rs new file mode 100644 index 0000000000000..ce102378a72f2 --- /dev/null +++ b/polkadot/zombienet-sdk-tests/tests/functional/rewards_statistics_collector.rs @@ -0,0 +1,106 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Test that nodes fetch availability chunks early for scheduled cores and normally for occupied +// core. + +use std::ops::Range; +use anyhow::anyhow; +use cumulus_zombienet_sdk_helpers::assert_para_throughput; +use polkadot_primitives::Id as ParaId; +use serde_json::json; +use subxt::{OnlineClient, PolkadotConfig}; +use zombienet_orchestrator::network::Network; +use zombienet_sdk::{LocalFileSystem, NetworkConfigBuilder}; + +#[tokio::test(flavor = "multi_thread")] +async fn rewards_statistics_collector_test() -> Result<(), anyhow::Error> { + let _ = env_logger::try_init_from_env( + env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"), + ); + + let images = zombienet_sdk::environment::get_images_from_env(); + + let config = NetworkConfigBuilder::new() + .with_relaychain(|r| { + let r = r + .with_chain("rococo-local") + .with_default_command("polkadot") + .with_default_image(images.polkadot.as_str()) + .with_default_args(vec![("-lparachain=debug").into()]) + .with_genesis_overrides(json!({ + "configuration": { + "config": { + "scheduler_params": { + "group_rotation_frequency": 4 + } + } + } + })) + .with_node(|node| node.with_name("validator-0")); + + (1..12) + .fold(r, |acc, i| + acc.with_node(|node| node.with_name(&format!("validator-{i}")))) + }) + .with_parachain(|p| { + p.with_id(2000) + .with_default_command("adder-collator") + .with_default_image( + std::env::var("COL_IMAGE") + .unwrap_or("docker.io/paritypr/colander:latest".to_string()) + .as_str(), + ) + .cumulus_based(false) + .with_default_args(vec![("-lparachain=debug").into()]) + .with_collator(|n| n.with_name("collator-adder-2000")) + }) + .with_parachain(|p| { + p.with_id(2001) + .with_default_command("polkadot-parachain") + .with_default_image(images.cumulus.as_str()) + .with_default_args(vec![("-lparachain=debug,aura=debug").into()]) + .with_collator(|n| n.with_name("collator-2001")) + }) + .build() + .map_err(|e| { + let errs = e.into_iter().map(|e| e.to_string()).collect::>().join(" "); + anyhow!("config errs: {errs}") + })?; + + let spawn_fn = zombienet_sdk::environment::get_spawn_fn(); + let network = spawn_fn(config).await?; + + let relay_node = network.get_node("validator-0")?; + let relay_client: OnlineClient = relay_node.wait_client().await?; + + assert_para_throughput( + &relay_client, + 15, + [(ParaId::from(2000), 11..16), (ParaId::from(2001), 11..16)] + .into_iter() + .collect(), + ) + .await?; + + assert_validators_collected_approval_usages( + 0..12, + &network, + ).await?; + + Ok(()) +} + +async fn assert_validators_collected_approval_usages(validators_range: Range, network: &Network) -> Result<(), anyhow::Error> { + for idx in validators_range { + let validator_identifier = format!("validator-{}", idx); + + let relay_node = network.get_node(validator_identifier.clone())?; + let approvals_usage_total = relay_node.reports("polkadot_parachain_rewards_statistics_collector_approvals_usage_total").await?; + assert!(approvals_usage_total > 0.0); + + log::info!("{validator_identifier} -> collected usages: {approvals_usage_total}"); + } + + Ok(()) +} \ No newline at end of file diff --git a/substrate/frame/executive/src/lib.rs b/substrate/frame/executive/src/lib.rs index 93ffbe5ad99bc..3527329d522e7 100644 --- a/substrate/frame/executive/src/lib.rs +++ b/substrate/frame/executive/src/lib.rs @@ -196,16 +196,16 @@ impl core::fmt::Debug for ExecutiveError { /// - [**DEPRECATED** `OnRuntimeUpgrade`]: This parameter is deprecated and will be removed after /// September 2026. Use type `SingleBlockMigrations` in frame_system::Config instead. #[allow(deprecated)] -#[deprecated( - note = "`OnRuntimeUpgrade` parameter in Executive is deprecated, will be removed after September 2026. \ - Use type `SingleBlockMigrations` in frame_system::Config instead." -)] pub struct Executive< System, Block, Context, UnsignedValidator, AllPalletsWithSystem, + #[deprecated( + note = "`OnRuntimeUpgrade` parameter in Executive is deprecated, will be removed after September 2026. \ + Use type `SingleBlockMigrations` in frame_system::Config instead." + )] OnRuntimeUpgrade = (), >( PhantomData<( From 63e379df22c5da7ef3a479f747280b10302e848b Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Wed, 15 Oct 2025 16:34:57 -0400 Subject: [PATCH 20/48] chore: include tests for uploaded chunk - Change `handle_chunk_uploaded` to lookup over the stored sessions --- Cargo.lock | 5 +- .../consensus-statistics-collector/Cargo.toml | 5 +- .../src/availability_distribution_metrics.rs | 97 +++++++------ .../consensus-statistics-collector/src/lib.rs | 28 ++-- .../src/tests.rs | 132 ++++++++++++++++-- 5 files changed, 202 insertions(+), 65 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 262f91e702b3e..aa69d959d414e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15615,9 +15615,8 @@ dependencies = [ "polkadot-node-subsystem-test-helpers", "polkadot-node-subsystem-util", "polkadot-primitives", - "polkadot-primitives-test-helpers", - "rand 0.8.5", - "rstest", + "sp-application-crypto", + "sp-authority-discovery", "sp-core 28.0.0", "sp-tracing 16.0.0", "thiserror 1.0.65", diff --git a/polkadot/node/core/consensus-statistics-collector/Cargo.toml b/polkadot/node/core/consensus-statistics-collector/Cargo.toml index 3b5a7f2a61714..e710cd0b5cb42 100644 --- a/polkadot/node/core/consensus-statistics-collector/Cargo.toml +++ b/polkadot/node/core/consensus-statistics-collector/Cargo.toml @@ -26,8 +26,7 @@ polkadot-node-primitives = { workspace = true, default-features = true } assert_matches = { workspace = true } polkadot-node-subsystem-test-helpers = { workspace = true } polkadot-primitives = { workspace = true, features = ["test"] } -polkadot-primitives-test-helpers = { workspace = true } -rand = { workspace = true } -rstest = { workspace = true } +sp-authority-discovery = { workspace = true, default-features = true } +sp-application-crypto = { workspace = true, default-features = true } sp-core = { workspace = true, default-features = true } sp-tracing = { workspace = true } \ No newline at end of file diff --git a/polkadot/node/core/consensus-statistics-collector/src/availability_distribution_metrics.rs b/polkadot/node/core/consensus-statistics-collector/src/availability_distribution_metrics.rs index 1ae80731db282..6f46801685b00 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/availability_distribution_metrics.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/availability_distribution_metrics.rs @@ -19,8 +19,9 @@ use std::collections::hash_map::Entry; use std::ops::Add; use gum::CandidateHash; use polkadot_primitives::{AuthorityDiscoveryId, SessionIndex, ValidatorIndex}; -use crate::View; +use crate::{PerSessionView, View}; +#[derive(Debug, Clone, PartialEq, Eq)] pub struct AvailabilityChunks { pub downloads_per_candidate: HashMap>, pub uploads_per_candidate: HashMap>, @@ -40,12 +41,17 @@ impl AvailabilityChunks { validator_index: ValidatorIndex, count: u64, ) { - let _ = self.downloads_per_candidate + let validator_downloads = self.downloads_per_candidate .entry(candidate_hash) .or_default() - .entry(validator_index) - .and_modify(|v| *v += count) - .or_insert(count); + .entry(validator_index); + + match validator_downloads { + Entry::Occupied(mut validator_downloads) => { + *validator_downloads.get_mut() += count; + } + Entry::Vacant(entry) => { entry.insert(count); } + } } pub fn note_candidate_chunk_uploaded( @@ -54,12 +60,17 @@ impl AvailabilityChunks { validator_index: ValidatorIndex, count: u64, ) { - let _ = self.uploads_per_candidate + let validator_uploads = self.uploads_per_candidate .entry(candidate_hash) .or_default() - .entry(validator_index) - .and_modify(|v| *v += count) - .or_insert(count); + .entry(validator_index); + + match validator_uploads { + Entry::Occupied(mut validator_uploads) => { + *validator_uploads.get_mut() += count; + } + Entry::Vacant(entry) => { entry.insert(count); } + } } } @@ -72,16 +83,22 @@ pub fn handle_chunks_downloaded( candidate_hash: CandidateHash, downloads: HashMap, ) { - view.candidates_per_session + let candidates_per_session = view.candidates_per_session.entry(session_index); + match candidates_per_session { + Entry::Occupied(mut candidates_per_session) => { + candidates_per_session.get_mut().insert(candidate_hash); + } + Entry::Vacant(entry) => { + entry.insert(HashSet::new()); + } + } + + let av_chunks = view.availability_chunks .entry(session_index) - .or_default() - .insert(candidate_hash); + .or_insert(AvailabilityChunks::new()); for (validator_index, download_count) in downloads { - view.availability_chunks - .entry(session_index) - .and_modify(|av_chunks_stats| av_chunks_stats.note_candidate_chunk_downloaded(candidate_hash, validator_index, download_count)) - .or_insert(AvailabilityChunks::new()); + av_chunks.note_candidate_chunk_downloaded(candidate_hash, validator_index, download_count) } } @@ -90,36 +107,32 @@ pub fn handle_chunk_uploaded( candidate_hash: CandidateHash, authority_ids: HashSet, ) { - // check if candidate is present - let sessions = view.candidates_per_session - .iter() - .filter_map(|(session_index, candidates)| { - if candidates.contains(&candidate_hash) { - return Some(session_index); - } + // will look up in the stored sessions, + // from the most recent session to the oldest session + // to find the first validator index that matches + // with a single authority discovery id from the set - None - }); + let mut sessions: Vec<(&SessionIndex, &PerSessionView)> = view.per_session.iter().collect(); + sessions.sort_by(|(a, _), (b, _)| a.partial_cmp(&b).unwrap()); - for session_index in sessions { - let validator_index = view.per_session.get(session_index).and_then(|session_view| { - for authority_id in authority_ids.iter() { - match session_view.authorities_lookup.get(authority_id) { - Some(validator_idx) => return Some(validator_idx), - None => continue, + for (session_idx, session_view) in sessions { + // Find the first authority with a matching validator index + if let Some(validator_idx) = authority_ids + .iter() + .find_map(|id| session_view.authorities_lookup.get(id).map(|v| v)) + { + let av_chunks = view.availability_chunks.entry(*session_idx); + match av_chunks { + Entry::Occupied(mut entry) => { + entry.get_mut() + .note_candidate_chunk_uploaded(candidate_hash, *validator_idx, 1); + } + Entry::Vacant(entry) => { + entry.insert(AvailabilityChunks::new()) + .note_candidate_chunk_uploaded(candidate_hash, *validator_idx, 1); } } - None - }); - - if validator_index.is_none() { - continue; + break; } - - view.availability_chunks - .entry(session_index.clone()) - .and_modify(|av_chunks_stats| av_chunks_stats - .note_candidate_chunk_uploaded(candidate_hash, validator_index.unwrap().clone(), 1)) - .or_insert(AvailabilityChunks::new()); } } \ No newline at end of file diff --git a/polkadot/node/core/consensus-statistics-collector/src/lib.rs b/polkadot/node/core/consensus-statistics-collector/src/lib.rs index e01dae0704b76..c59c9248d904b 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/lib.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/lib.rs @@ -73,6 +73,7 @@ impl PerRelayView { } } +#[derive(Debug, Eq, PartialEq, Clone)] pub struct PerSessionView { authorities_lookup: HashMap, } @@ -85,8 +86,6 @@ impl PerSessionView { struct View { latest_finalized: Option<(Hash, BlockNumber)>, - best_blocks: Vec, - roots: HashSet, per_relay: HashMap, per_session: HashMap, @@ -104,8 +103,7 @@ impl View { per_relay: HashMap::new(), per_session: HashMap::new(), candidates_per_session: HashMap::new(), - availability_chunks: HashMap::new(), - best_blocks: Vec::new() + availability_chunks: HashMap::new() }; } } @@ -288,7 +286,10 @@ pub(crate) async fn run_iteration( FromOrchestra::Communication { msg } => { match msg { ConsensusStatisticsCollectorMessage::ChunksDownloaded( - session_index, candidate_hash, downloads)=> { + session_index, + candidate_hash, + downloads, + )=> { handle_chunks_downloaded( view, session_index, @@ -296,14 +297,21 @@ pub(crate) async fn run_iteration( downloads, ) }, - ConsensusStatisticsCollectorMessage::ChunkUploaded(candidate_hash, authority_ids) => { + ConsensusStatisticsCollectorMessage::ChunkUploaded( + candidate_hash, + authority_ids, + ) => { handle_chunk_uploaded( view, candidate_hash, authority_ids, ) }, - ConsensusStatisticsCollectorMessage::CandidateApproved(candidate_hash, block_hash, approvals) => { + ConsensusStatisticsCollectorMessage::CandidateApproved( + candidate_hash, + block_hash, + approvals, + ) => { handle_candidate_approved( view, block_hash, @@ -312,7 +320,11 @@ pub(crate) async fn run_iteration( metrics, ); } - ConsensusStatisticsCollectorMessage::NoShows(candidate_hash, block_hash, no_show_validators) => { + ConsensusStatisticsCollectorMessage::NoShows( + candidate_hash, + block_hash, + no_show_validators, + ) => { handle_observed_no_shows( view, block_hash, diff --git a/polkadot/node/core/consensus-statistics-collector/src/tests.rs b/polkadot/node/core/consensus-statistics-collector/src/tests.rs index 1258bc48e4050..520a32e0f3c91 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/tests.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/tests.rs @@ -15,15 +15,18 @@ // along with Polkadot. If not, see . use super::*; +use assert_matches::assert_matches; use overseer::FromOrchestra; -use polkadot_primitives::{SessionIndex}; -use polkadot_node_subsystem::messages::{ConsensusStatisticsCollectorMessage}; +use polkadot_primitives::{AssignmentId, GroupIndex, SessionIndex, SessionInfo}; +use polkadot_node_subsystem::messages::{AllMessages, ChainApiResponseChannel, ConsensusStatisticsCollectorMessage, RuntimeApiMessage, RuntimeApiRequest}; type VirtualOverseer = polkadot_node_subsystem_test_helpers::TestSubsystemContextHandle; use polkadot_node_subsystem::{ActivatedLeaf}; use polkadot_node_subsystem_test_helpers as test_helpers; -use sp_core::traits::CodeNotFound; +use polkadot_primitives::{Hash, Header}; +use sp_application_crypto::Pair as PairT; +use sp_authority_discovery::AuthorityPair as AuthorityDiscoveryPair; use test_helpers::mock::new_leaf; async fn activate_leaf( @@ -103,11 +106,10 @@ fn test_harness>( let (mut context, virtual_overseer) = polkadot_node_subsystem_test_helpers::make_subsystem_context(pool.clone()); - let metrics = Metrics; - let mut view = View::new(); + let subsystem = async move { - if let Err(e) = run_iteration(&mut context, &mut view, &metrics).await { + if let Err(e) = run_iteration(&mut context, &mut view, &Metrics(None)).await { panic!("{:?}", e); } @@ -277,8 +279,11 @@ fn note_chunks_downloaded() { virtual_overseer }); - assert_eq!(view.chunks_downloaded.chunks_per_candidate.len(), 1); - let amt_per_validator = view.chunks_downloaded.chunks_per_candidate + assert_eq!(view.availability_chunks.len(), 1); + let ac = view.availability_chunks.get(&session_idx).unwrap(); + + assert_eq!(ac.downloads_per_candidate.len(), 1); + let amt_per_validator = ac.downloads_per_candidate .get(&candidate_hash) .unwrap(); @@ -291,4 +296,113 @@ fn note_chunks_downloaded() { let count = amt_per_validator.get(&vidx).unwrap(); assert_eq!(*count, expected_count); } -} \ No newline at end of file +} + +fn default_header() -> Header { + Header { + parent_hash: Hash::zero(), + number: 100500, + state_root: Hash::zero(), + extrinsics_root: Hash::zero(), + digest: Default::default(), + } +} + +fn default_session_info(session_idx: SessionIndex) -> SessionInfo { + SessionInfo { + active_validator_indices: vec![], + random_seed: Default::default(), + dispute_period: session_idx, + validators: Default::default(), + discovery_keys: vec![], + assignment_keys: vec![], + validator_groups: Default::default(), + n_cores: 0, + zeroth_delay_tranche_width: 0, + relay_vrf_modulo_samples: 0, + n_delay_tranches: 0, + no_show_slots: 0, + needed_approvals: 0, + } +} + +#[test] +fn note_chunks_uploaded_to_active_validator() { + let activated_leaf_hash = Hash::from_low_u64_be(111); + let leaf1 = new_leaf(activated_leaf_hash.clone(), 1); + let leaf1_header = default_header(); + let session_index: SessionIndex = 2; + let mut session_info: SessionInfo = default_session_info(session_index); + + let validator_idx_pair = AuthorityDiscoveryPair::generate(); + let validator_idx_auth_id: AuthorityDiscoveryId = validator_idx_pair.0.public().into(); + + session_info.discovery_keys = vec![ + validator_idx_auth_id.clone(), + ]; + + let candidate_hash: CandidateHash = CandidateHash(Hash::from_low_u64_be(132)); + + let view = test_harness(|mut virtual_overseer| async move { + virtual_overseer.send(FromOrchestra::Signal( + OverseerSignal::ActiveLeaves(ActiveLeavesUpdate{ + activated: Some(leaf1), + deactivated: Default::default(), + }), + )).await; + + assert_matches!( + virtual_overseer.recv().await, + AllMessages::ChainApi( + ChainApiMessage::BlockHeader(relay_hash, tx) + ) if relay_hash == activated_leaf_hash => { + tx.send(Ok(Some(leaf1_header))).unwrap(); + } + ); + + assert_matches!( + virtual_overseer.recv().await, + AllMessages::RuntimeApi( + RuntimeApiMessage::Request(parent, RuntimeApiRequest::SessionIndexForChild(tx)) + ) if parent == activated_leaf_hash => { + tx.send(Ok(session_index)).unwrap(); + } + ); + + // given that session index is not cached yet + // the subsystem will retrieve the session info + assert_matches!( + virtual_overseer.recv().await, + AllMessages::RuntimeApi( + RuntimeApiMessage::Request(parent, RuntimeApiRequest::SessionInfo(req_session, tx)) + ) if req_session == session_index => { + tx.send(Ok(Some(session_info))).unwrap(); + } + ); + + virtual_overseer.send(FromOrchestra::Communication { + msg: ConsensusStatisticsCollectorMessage::ChunkUploaded( + candidate_hash.clone(), HashSet::from_iter(vec![validator_idx_auth_id.clone()]), + ), + }).await; + + virtual_overseer + }); + + let validator_idx_auth_id: AuthorityDiscoveryId = validator_idx_pair.0.public().into(); + + // assert that the leaf was activated and the session info is present + let expected_view = PerSessionView::new( + HashMap::from_iter(vec![(validator_idx_auth_id.clone(), ValidatorIndex(0))])); + + assert_eq!(view.per_session.len(),1); + assert_eq!(view.per_session.get(&2).unwrap().clone(), expected_view); + + assert_matches!(view.availability_chunks.len(), 1); + + let mut expected_av_chunks = AvailabilityChunks::new(); + expected_av_chunks.note_candidate_chunk_uploaded( + candidate_hash, ValidatorIndex(0), 1); + + assert_matches!(view.availability_chunks.get(&2).unwrap(), expected_av_chunks); +} From 848ba5aa44c2ef58ed3f79601b1d62e9c2c74fef Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Wed, 15 Oct 2025 17:02:39 -0400 Subject: [PATCH 21/48] chore: remove unnecessary mapping --- .../src/availability_distribution_metrics.rs | 10 ---------- .../core/consensus-statistics-collector/src/lib.rs | 3 --- 2 files changed, 13 deletions(-) diff --git a/polkadot/node/core/consensus-statistics-collector/src/availability_distribution_metrics.rs b/polkadot/node/core/consensus-statistics-collector/src/availability_distribution_metrics.rs index 6f46801685b00..229db0f634efa 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/availability_distribution_metrics.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/availability_distribution_metrics.rs @@ -83,16 +83,6 @@ pub fn handle_chunks_downloaded( candidate_hash: CandidateHash, downloads: HashMap, ) { - let candidates_per_session = view.candidates_per_session.entry(session_index); - match candidates_per_session { - Entry::Occupied(mut candidates_per_session) => { - candidates_per_session.get_mut().insert(candidate_hash); - } - Entry::Vacant(entry) => { - entry.insert(HashSet::new()); - } - } - let av_chunks = view.availability_chunks .entry(session_index) .or_insert(AvailabilityChunks::new()); diff --git a/polkadot/node/core/consensus-statistics-collector/src/lib.rs b/polkadot/node/core/consensus-statistics-collector/src/lib.rs index c59c9248d904b..9df126669c374 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/lib.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/lib.rs @@ -89,8 +89,6 @@ struct View { roots: HashSet, per_relay: HashMap, per_session: HashMap, - // TODO: this information should not be needed - candidates_per_session: HashMap>, availability_chunks: HashMap, } @@ -102,7 +100,6 @@ impl View { roots: HashSet::new(), per_relay: HashMap::new(), per_session: HashMap::new(), - candidates_per_session: HashMap::new(), availability_chunks: HashMap::new() }; } From 00c661f329fee2061a23c7fd201cec883c5e125c Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Thu, 16 Oct 2025 11:33:58 -0400 Subject: [PATCH 22/48] chore: prune when session is finalized - Prunes the collected data that belongs to not finalized blocks - The finalized collected data (approval usages) are splited into sessions --- .../src/approval_voting_metrics.rs | 2 +- .../consensus-statistics-collector/src/lib.rs | 210 +++++++++++------- 2 files changed, 130 insertions(+), 82 deletions(-) diff --git a/polkadot/node/core/consensus-statistics-collector/src/approval_voting_metrics.rs b/polkadot/node/core/consensus-statistics-collector/src/approval_voting_metrics.rs index 062472e3e4350..84049a7693b69 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/approval_voting_metrics.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/approval_voting_metrics.rs @@ -19,7 +19,7 @@ use polkadot_primitives::{CandidateHash, Hash, SessionIndex, ValidatorIndex}; use crate::metrics::Metrics; use crate::View; -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, Eq, PartialEq)] pub struct ApprovalsStats { pub votes: HashSet, pub no_shows: HashSet, diff --git a/polkadot/node/core/consensus-statistics-collector/src/lib.rs b/polkadot/node/core/consensus-statistics-collector/src/lib.rs index 9df126669c374..a432a0dcc2e8c 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/lib.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/lib.rs @@ -54,14 +54,16 @@ use self::metrics::Metrics; const LOG_TARGET: &str = "parachain::consensus-statistics-collector"; struct PerRelayView { + session_index: SessionIndex, parent_hash: Option, children: HashSet, approvals_stats: HashMap, } impl PerRelayView { - fn new(parent_hash: Option) -> Self { + fn new(parent_hash: Option, session_index: SessionIndex) -> Self { PerRelayView{ + session_index: session_index, parent_hash: parent_hash, children: HashSet::new(), approvals_stats: HashMap::new(), @@ -76,16 +78,17 @@ impl PerRelayView { #[derive(Debug, Eq, PartialEq, Clone)] pub struct PerSessionView { authorities_lookup: HashMap, + finalized_approval_stats: HashMap, } impl PerSessionView { fn new(authorities_lookup: HashMap) -> Self { - Self { authorities_lookup } + Self { authorities_lookup, finalized_approval_stats: HashMap::new() } } } struct View { - latest_finalized: Option<(Hash, BlockNumber)>, + current_finalized_session_index: Option, roots: HashSet, per_relay: HashMap, per_session: HashMap, @@ -96,7 +99,7 @@ struct View { impl View { fn new() -> Self { return View{ - latest_finalized: None, + current_finalized_session_index: None, roots: HashSet::new(), per_relay: HashMap::new(), per_session: HashMap::new(), @@ -166,6 +169,12 @@ pub(crate) async fn run_iteration( .await? .map_err(JfyiError::ChainApiCallError)?; + let session_idx = request_session_index_for_child(relay_hash, ctx.sender()) + .await + .await + .map_err(JfyiError::OverseerCommunication)? + .map_err(JfyiError::RuntimeApiCallError)?; + if let Some(ref h) = header { let parent_hash = h.clone().parent_hash; if let Some(parent) = view.per_relay.get_mut(&parent_hash) { @@ -173,18 +182,12 @@ pub(crate) async fn run_iteration( } else { view.roots.insert(relay_hash.clone()); } - view.per_relay.insert(relay_hash, PerRelayView::new(Some(parent_hash))); + view.per_relay.insert(relay_hash, PerRelayView::new(Some(parent_hash), session_idx)); } else { view.roots.insert(relay_hash.clone()); - view.per_relay.insert(relay_hash, PerRelayView::new(None)); + view.per_relay.insert(relay_hash, PerRelayView::new(None, session_idx)); } - let session_idx = request_session_index_for_child(relay_hash, ctx.sender()) - .await - .await - .map_err(JfyiError::OverseerCommunication)? - .map_err(JfyiError::RuntimeApiCallError)?; - if !view.per_session.contains_key(&session_idx) { let session_info = request_session_info(relay_hash, session_idx, ctx.sender()) .await @@ -203,82 +206,51 @@ pub(crate) async fn run_iteration( } } }, - FromOrchestra::Signal(OverseerSignal::BlockFinalized(fin_block_hash, fin_block_num)) => { - let latest_finalized = match view.latest_finalized { - Some(latest_finalized) => latest_finalized, - None => (fin_block_hash, fin_block_num), - }; - - // update the latest finalized on the view - view.latest_finalized = Some((fin_block_hash, fin_block_num)); - - // since we want to reward only valid approvals, we retain - // only finalized chain blocks and its descendants - // identify the finalized chain so we don't prune - let rb_view = match view.per_relay.get_mut(&fin_block_hash) { - Some(per_relay_view) => per_relay_view, - //TODO: the finalized block should already exists on the relay view mapping - None => continue + FromOrchestra::Signal(OverseerSignal::BlockFinalized(fin_block_hash, _)) => { + // check if a session was finalized + let session_idx = request_session_index_for_child(fin_block_hash, ctx.sender()) + .await + .await + .map_err(JfyiError::OverseerCommunication)? + .map_err(JfyiError::RuntimeApiCallError)?; + + let should_prune = match view.current_finalized_session_index { + Some(curr_session_idx) if session_idx > curr_session_idx => true, + _ => false }; - let mut removal_stack = Vec::new(); - let mut retain_relay_hashes = Vec::new(); - retain_relay_hashes.push(fin_block_hash); - - let mut current_block_hash = fin_block_hash; - let mut current_parent_hash = rb_view.parent_hash; - while let Some(parent_hash) = current_parent_hash { - retain_relay_hashes.push(parent_hash.clone()); - - match view.per_relay.get_mut(&parent_hash) { - Some(parent_view) => { - if parent_view.children.len() > 1 { - let filtered_set = parent_view.children - .iter() - .filter(|&child_hash| child_hash.eq(¤t_block_hash)) - .cloned() // Clone the elements to own them in the new HashSet - .collect::>(); - - removal_stack.extend(filtered_set); - - // unlink all the other children keeping only - // the one that belongs to the finalized chain - parent_view.children = HashSet::from_iter(vec![current_block_hash.clone()]); - } - current_block_hash = parent_hash; - current_parent_hash = parent_view.parent_hash; - }, - None => break - }; - } - - if view.roots.len() > 1 { - for root in view.roots.clone() { - if !retain_relay_hashes.contains(&root) { - removal_stack.push(root); - } - } + if view.current_finalized_session_index.is_none() { + view.current_finalized_session_index = Some(session_idx); } - let mut to_prune = HashSet::new(); - let mut queue: VecDeque = VecDeque::from(removal_stack); - - while let Some(hash) = queue.pop_front() { - if !to_prune.insert(hash) { - continue; // already seen - } - - if let Some(r_view) = view.per_relay.get(&hash) { - for child in &r_view.children { - queue.push_back(child.clone()); + if should_prune { + view.current_finalized_session_index = Some(session_idx); + let finalized_hashes = prune_unfinalised_forks(view, fin_block_hash); + + // finalized_hashes contains the hashes from the newest to the oldest + // so we revert it and check from the oldest to the newest + for hash in finalized_hashes.iter().rev() { + match view.per_relay.get(hash) { + Some(rb_view) => { + if rb_view.session_index >= session_idx { + view.roots = HashSet::from_iter(vec![hash.clone()]); + break + } + + view.per_session + .get_mut(&session_idx) + .and_then(|session_view| { + session_view.finalized_approval_stats + .extend(rb_view.approvals_stats.clone()); + Some(session_view) + }); + + view.per_relay.remove(hash); + } + None => {}, } } } - - for rb_hash in to_prune { - view.per_relay.remove(&rb_hash); - view.roots.remove(&rb_hash); - } } FromOrchestra::Communication { msg } => { match msg { @@ -333,4 +305,80 @@ pub(crate) async fn run_iteration( }, } } +} + +// prune_unfinalised_forks will remove all the relay chain blocks +// that are not in the finalized chain and its dependants children using the latest finalized block as reference +// and will return a list of finalized hashes +fn prune_unfinalised_forks(view: &mut View, fin_block_hash: Hash) -> Vec { + // since we want to reward only valid approvals, we retain + // only finalized chain blocks and its descendants + // identify the finalized chain so we don't prune + let rb_view = match view.per_relay.get_mut(&fin_block_hash) { + Some(per_relay_view) => per_relay_view, + + //TODO: the finalized block should already exists on the relay view mapping + None => return Vec::new(), + }; + + let mut removal_stack = Vec::new(); + let mut retain_relay_hashes = Vec::new(); + retain_relay_hashes.push(fin_block_hash); + + let mut current_block_hash = fin_block_hash; + let mut current_parent_hash = rb_view.parent_hash; + while let Some(parent_hash) = current_parent_hash { + retain_relay_hashes.push(parent_hash.clone()); + + match view.per_relay.get_mut(&parent_hash) { + Some(parent_view) => { + if parent_view.children.len() > 1 { + let filtered_set = parent_view.children + .iter() + .filter(|&child_hash| child_hash.eq(¤t_block_hash)) + .cloned() // Clone the elements to own them in the new HashSet + .collect::>(); + + removal_stack.extend(filtered_set); + + // unlink all the other children keeping only + // the one that belongs to the finalized chain + parent_view.children = HashSet::from_iter(vec![current_block_hash.clone()]); + } + current_block_hash = parent_hash; + current_parent_hash = parent_view.parent_hash; + }, + None => break + }; + } + + if view.roots.len() > 1 { + for root in view.roots.clone() { + if !retain_relay_hashes.contains(&root) { + removal_stack.push(root); + } + } + } + + let mut to_prune = HashSet::new(); + let mut queue: VecDeque = VecDeque::from(removal_stack); + + while let Some(hash) = queue.pop_front() { + if !to_prune.insert(hash) { + continue; // already seen + } + + if let Some(r_view) = view.per_relay.get(&hash) { + for child in &r_view.children { + queue.push_back(child.clone()); + } + } + } + + for rb_hash in to_prune { + view.per_relay.remove(&rb_hash); + view.roots.remove(&rb_hash); + } + + retain_relay_hashes } \ No newline at end of file From bbab27b60511734cca56af763375bcf0acaa13ef Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Fri, 17 Oct 2025 10:21:48 -0400 Subject: [PATCH 23/48] chore: fix tests --- .../src/tests.rs | 114 +++++++++++------- 1 file changed, 73 insertions(+), 41 deletions(-) diff --git a/polkadot/node/core/consensus-statistics-collector/src/tests.rs b/polkadot/node/core/consensus-statistics-collector/src/tests.rs index 520a32e0f3c91..ba1592220313f 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/tests.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/tests.rs @@ -32,12 +32,46 @@ use test_helpers::mock::new_leaf; async fn activate_leaf( virtual_overseer: &mut VirtualOverseer, activated: ActivatedLeaf, + leaf_header: Header, + session_index: SessionIndex, + session_info: Option, ) { + let activated_leaf_hash = activated.hash; virtual_overseer .send(FromOrchestra::Signal(OverseerSignal::ActiveLeaves(ActiveLeavesUpdate::start_work( activated, )))) .await; + + + assert_matches!( + virtual_overseer.recv().await, + AllMessages::ChainApi( + ChainApiMessage::BlockHeader(relay_hash, tx) + ) if relay_hash == activated_leaf_hash => { + tx.send(Ok(Some(leaf_header))).unwrap(); + } + ); + + assert_matches!( + virtual_overseer.recv().await, + AllMessages::RuntimeApi( + RuntimeApiMessage::Request(parent, RuntimeApiRequest::SessionIndexForChild(tx)) + ) if parent == activated_leaf_hash => { + tx.send(Ok(session_index)).unwrap(); + } + ); + + if let Some(session_info) = session_info { + assert_matches!( + virtual_overseer.recv().await, + AllMessages::RuntimeApi( + RuntimeApiMessage::Request(parent, RuntimeApiRequest::SessionInfo(req_session, tx)) + ) if req_session == session_index => { + tx.send(Ok(Some(session_info))).unwrap(); + } + ); + } } async fn candidate_approved( @@ -144,7 +178,14 @@ fn single_candidate_approved() { ); let view = test_harness(|mut virtual_overseer| async move { - activate_leaf(&mut virtual_overseer, leaf.clone()).await; + activate_leaf( + &mut virtual_overseer, + leaf.clone(), + default_header(), + 1, + Some(default_session_info(1)), + ).await; + candidate_approved(&mut virtual_overseer, candidate_hash.clone(), rb_hash, vec![validator_idx.clone()]).await; virtual_overseer }); @@ -177,18 +218,31 @@ fn candidate_approved_for_different_forks() { let rb_hash_fork_1 = Hash::from_low_u64_be(231); let view = test_harness(|mut virtual_overseer| async move { - let leaf1 = new_leaf( + let leaf0 = new_leaf( rb_hash_fork_0.clone(), 1, ); - let leaf2 = new_leaf( + let leaf1 = new_leaf( rb_hash_fork_1.clone(), 1, ); - activate_leaf(&mut virtual_overseer, leaf1.clone()).await; - activate_leaf(&mut virtual_overseer, leaf2.clone()).await; + activate_leaf( + &mut virtual_overseer, + leaf0.clone(), + default_header(), + 1, + Some(default_session_info(1)), + ).await; + + activate_leaf( + &mut virtual_overseer, + leaf1.clone(), + default_header(), + 1, + None, + ).await; candidate_approved( &mut virtual_overseer, @@ -226,7 +280,13 @@ fn candidate_approval_stats_with_no_shows() { let view = test_harness(|mut virtual_overseer| async move { let leaf1 = new_leaf(rb_hash.clone(), 1); - activate_leaf(&mut virtual_overseer, leaf1.clone()).await; + activate_leaf( + &mut virtual_overseer, + leaf1.clone(), + default_header(), + 1, + Some(default_session_info(1)), + ).await; candidate_approved( &mut virtual_overseer, @@ -344,41 +404,13 @@ fn note_chunks_uploaded_to_active_validator() { let candidate_hash: CandidateHash = CandidateHash(Hash::from_low_u64_be(132)); let view = test_harness(|mut virtual_overseer| async move { - virtual_overseer.send(FromOrchestra::Signal( - OverseerSignal::ActiveLeaves(ActiveLeavesUpdate{ - activated: Some(leaf1), - deactivated: Default::default(), - }), - )).await; - - assert_matches!( - virtual_overseer.recv().await, - AllMessages::ChainApi( - ChainApiMessage::BlockHeader(relay_hash, tx) - ) if relay_hash == activated_leaf_hash => { - tx.send(Ok(Some(leaf1_header))).unwrap(); - } - ); - - assert_matches!( - virtual_overseer.recv().await, - AllMessages::RuntimeApi( - RuntimeApiMessage::Request(parent, RuntimeApiRequest::SessionIndexForChild(tx)) - ) if parent == activated_leaf_hash => { - tx.send(Ok(session_index)).unwrap(); - } - ); - - // given that session index is not cached yet - // the subsystem will retrieve the session info - assert_matches!( - virtual_overseer.recv().await, - AllMessages::RuntimeApi( - RuntimeApiMessage::Request(parent, RuntimeApiRequest::SessionInfo(req_session, tx)) - ) if req_session == session_index => { - tx.send(Ok(Some(session_info))).unwrap(); - } - ); + activate_leaf( + &mut virtual_overseer, + leaf1, + leaf1_header, + session_index, + Some(session_info), + ).await; virtual_overseer.send(FromOrchestra::Communication { msg: ConsensusStatisticsCollectorMessage::ChunkUploaded( From 2dae7cb890b2cdc23d39901993dc6d27c6ca2060 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Mon, 20 Oct 2025 08:27:00 -0400 Subject: [PATCH 24/48] chore: improve metrics collection --- .../src/approval_voting_metrics.rs | 2 - .../consensus-statistics-collector/src/lib.rs | 4 + .../src/metrics.rs | 74 +++++++++++++++++-- 3 files changed, 70 insertions(+), 10 deletions(-) diff --git a/polkadot/node/core/consensus-statistics-collector/src/approval_voting_metrics.rs b/polkadot/node/core/consensus-statistics-collector/src/approval_voting_metrics.rs index 84049a7693b69..181beb6935048 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/approval_voting_metrics.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/approval_voting_metrics.rs @@ -42,11 +42,9 @@ pub fn handle_candidate_approved( relay_view.approvals_stats .entry(candidate_hash) .and_modify(|a: &mut ApprovalsStats| { - metrics.record_approvals_usage(approvals.len() as u64); a.votes.extend(approvals.iter()) }) .or_insert_with(|| { - metrics.record_approvals_usage(approvals.len() as u64); ApprovalsStats::new(HashSet::from_iter(approvals), HashSet::new()) }); } diff --git a/polkadot/node/core/consensus-statistics-collector/src/lib.rs b/polkadot/node/core/consensus-statistics-collector/src/lib.rs index a432a0dcc2e8c..4f2c9b4b9dd9c 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/lib.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/lib.rs @@ -240,6 +240,10 @@ pub(crate) async fn run_iteration( view.per_session .get_mut(&session_idx) .and_then(|session_view| { + metrics.record_approvals_stats( + session_idx, + rb_view.approvals_stats.clone()); + session_view.finalized_approval_stats .extend(rb_view.approvals_stats.clone()); Some(session_view) diff --git a/polkadot/node/core/consensus-statistics-collector/src/metrics.rs b/polkadot/node/core/consensus-statistics-collector/src/metrics.rs index 6f6ff7bafd958..02ae4c63e2c4d 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/metrics.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/metrics.rs @@ -14,15 +14,23 @@ // You should have received a copy of the GNU General Public License // along with Polkadot. If not, see . +use std::collections::HashMap; +use gum::CandidateHash; use polkadot_node_subsystem::prometheus::Opts; use polkadot_node_subsystem_util::metrics::{ self, prometheus::{self, Gauge, GaugeVec, U64}, }; +use polkadot_primitives::SessionIndex; +use crate::approval_voting_metrics::ApprovalsStats; #[derive(Clone)] pub(crate) struct MetricsInner { - approvals_usage_total: prometheus::Counter, + approvals_usage_per_session: prometheus::CounterVec, + no_shows_per_session: prometheus::CounterVec, + + approvals_per_session_per_validator: prometheus::CounterVec, + no_shows_per_session_per_validator: prometheus::CounterVec, } @@ -30,10 +38,27 @@ pub(crate) struct MetricsInner { #[derive(Default, Clone)] pub struct Metrics(pub(crate) Option); + impl Metrics { - pub fn record_approvals_usage(&self, collected: u64) { + pub fn record_approvals_stats(&self, session: SessionIndex, approval_stats: HashMap) { self.0.as_ref().map(|metrics| { - metrics.approvals_usage_total.inc_by(collected); + for (candidate_hash, stats) in approval_stats { + metrics.approvals_usage_per_session.with_label_values( + &[session.to_string().as_str()]).inc_by(stats.votes.len() as u64); + + metrics.no_shows_per_session.with_label_values( + &[session.to_string().as_str()]).inc_by(stats.no_shows.len() as u64); + + for validator in stats.votes { + metrics.approvals_per_session_per_validator.with_label_values( + &[session.to_string().as_str(), validator.0.to_string().as_str()]).inc() + } + + for validator in stats.no_shows { + metrics.no_shows_per_session_per_validator.with_label_values( + &[session.to_string().as_str(), validator.0.to_string().as_str()]).inc() + } + } }); } } @@ -41,11 +66,44 @@ impl Metrics { impl metrics::Metrics for Metrics { fn try_register(registry: &prometheus::Registry) -> Result { let metrics = MetricsInner { - approvals_usage_total: prometheus::register( - prometheus::Counter::new( - "polkadot_parachain_rewards_statistics_collector_approvals_usage_total", - "Total of collected meaningfull approvals used to approve a candidate", - )?, + approvals_per_session_per_validator: prometheus::register( + prometheus::CounterVec::new( + prometheus::Opts::new( + "polkadot_parachain_rewards_statistics_collector_approvals_per_session_per_validator", + "Total number of useful approvals a given validator provided on a session.", + ), + vec!["session", "validator_idx"].as_ref(), + )?, + registry, + )?, + no_shows_per_session_per_validator: prometheus::register( + prometheus::CounterVec::new( + prometheus::Opts::new( + "polkadot_parachain_rewards_statistics_collector_no_shows_per_session_per_validator", + "Total number a given validator no showed on a session.", + ), + vec!["session", "validator_idx"].as_ref(), + )?, + registry, + )?, + approvals_usage_per_session: prometheus::register( + prometheus::CounterVec::new( + prometheus::Opts::new( + "polkadot_parachain_rewards_statistics_collector_approvals_per_session", + "Total number of useful approvals on a session.", + ), + vec!["session"].as_ref(), + )?, + registry, + )?, + no_shows_per_session: prometheus::register( + prometheus::CounterVec::new( + prometheus::Opts::new( + "polkadot_parachain_rewards_statistics_collector_no_shows_per_session", + "Total number of no-shows on a session.", + ), + vec!["session"].as_ref(), + )?, registry, )?, }; From aa00e1bc020012a1f26817431f12c2de514fc7e8 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Mon, 20 Oct 2025 14:49:31 -0400 Subject: [PATCH 25/48] chore: testing pruning capabilities --- .../consensus-statistics-collector/src/lib.rs | 29 +- .../src/tests.rs | 260 +++++++++++++++++- 2 files changed, 265 insertions(+), 24 deletions(-) diff --git a/polkadot/node/core/consensus-statistics-collector/src/lib.rs b/polkadot/node/core/consensus-statistics-collector/src/lib.rs index 4f2c9b4b9dd9c..c6e5bdcb9af50 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/lib.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/lib.rs @@ -88,7 +88,7 @@ impl PerSessionView { } struct View { - current_finalized_session_index: Option, + current_session: Option, roots: HashSet, per_relay: HashMap, per_session: HashMap, @@ -99,7 +99,7 @@ struct View { impl View { fn new() -> Self { return View{ - current_finalized_session_index: None, + current_session: None, roots: HashSet::new(), per_relay: HashMap::new(), per_session: HashMap::new(), @@ -177,12 +177,15 @@ pub(crate) async fn run_iteration( if let Some(ref h) = header { let parent_hash = h.clone().parent_hash; - if let Some(parent) = view.per_relay.get_mut(&parent_hash) { + let parent_hash = if let Some(parent) = view.per_relay.get_mut(&parent_hash) { parent.link_child(relay_hash.clone()); + Some(parent_hash) } else { view.roots.insert(relay_hash.clone()); - } - view.per_relay.insert(relay_hash, PerRelayView::new(Some(parent_hash), session_idx)); + None + }; + + view.per_relay.insert(relay_hash, PerRelayView::new(parent_hash, session_idx)); } else { view.roots.insert(relay_hash.clone()); view.per_relay.insert(relay_hash, PerRelayView::new(None, session_idx)); @@ -214,17 +217,20 @@ pub(crate) async fn run_iteration( .map_err(JfyiError::OverseerCommunication)? .map_err(JfyiError::RuntimeApiCallError)?; - let should_prune = match view.current_finalized_session_index { + let should_prune = match view.current_session { Some(curr_session_idx) if session_idx > curr_session_idx => true, _ => false }; - if view.current_finalized_session_index.is_none() { - view.current_finalized_session_index = Some(session_idx); + if view.current_session.is_none() { + view.current_session = Some(session_idx); } if should_prune { - view.current_finalized_session_index = Some(session_idx); + let prev_session = view.current_session + .replace(session_idx) + .expect("current session should have been populate previously; qed."); + let finalized_hashes = prune_unfinalised_forks(view, fin_block_hash); // finalized_hashes contains the hashes from the newest to the oldest @@ -232,7 +238,7 @@ pub(crate) async fn run_iteration( for hash in finalized_hashes.iter().rev() { match view.per_relay.get(hash) { Some(rb_view) => { - if rb_view.session_index >= session_idx { + if rb_view.session_index > prev_session { view.roots = HashSet::from_iter(vec![hash.clone()]); break } @@ -339,7 +345,7 @@ fn prune_unfinalised_forks(view: &mut View, fin_block_hash: Hash) -> Vec { if parent_view.children.len() > 1 { let filtered_set = parent_view.children .iter() - .filter(|&child_hash| child_hash.eq(¤t_block_hash)) + .filter(|&child_hash| !child_hash.eq(¤t_block_hash)) .cloned() // Clone the elements to own them in the new HashSet .collect::>(); @@ -381,7 +387,6 @@ fn prune_unfinalised_forks(view: &mut View, fin_block_hash: Hash) -> Vec { for rb_hash in to_prune { view.per_relay.remove(&rb_hash); - view.roots.remove(&rb_hash); } retain_relay_hashes diff --git a/polkadot/node/core/consensus-statistics-collector/src/tests.rs b/polkadot/node/core/consensus-statistics-collector/src/tests.rs index ba1592220313f..977a4dc319904 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/tests.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/tests.rs @@ -14,6 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Polkadot. If not, see . +use std::ptr::hash; use super::*; use assert_matches::assert_matches; use overseer::FromOrchestra; @@ -74,6 +75,26 @@ async fn activate_leaf( } } +async fn finalize_block( + virtual_overseer: &mut VirtualOverseer, + finalized: (Hash, BlockNumber), + session_index: SessionIndex, +) { + let fin_block_hash = finalized.0; + virtual_overseer + .send(FromOrchestra::Signal(OverseerSignal::BlockFinalized(fin_block_hash, finalized.1))) + .await; + + assert_matches!( + virtual_overseer.recv().await, + AllMessages::RuntimeApi( + RuntimeApiMessage::Request(hash, RuntimeApiRequest::SessionIndexForChild(tx)) + ) if hash == fin_block_hash => { + tx.send(Ok(session_index)).unwrap(); + } + ); +} + async fn candidate_approved( virtual_overseer: &mut VirtualOverseer, candidate_hash: CandidateHash, @@ -131,8 +152,9 @@ approvals_stats_assertion!(assert_votes, votes); approvals_stats_assertion!(assert_no_shows, no_shows); fn test_harness>( + view: &mut View, test: impl FnOnce(VirtualOverseer) -> T, -) -> View { +) { sp_tracing::init_for_tests(); let pool = sp_core::testing::TaskExecutor::new(); @@ -140,10 +162,8 @@ fn test_harness>( let (mut context, virtual_overseer) = polkadot_node_subsystem_test_helpers::make_subsystem_context(pool.clone()); - let mut view = View::new(); - let subsystem = async move { - if let Err(e) = run_iteration(&mut context, &mut view, &Metrics(None)).await { + if let Err(e) = run_iteration(&mut context, view, &Metrics(None)).await { panic!("{:?}", e); } @@ -161,8 +181,6 @@ fn test_harness>( }, subsystem, )); - - view } #[test] @@ -177,7 +195,8 @@ fn single_candidate_approved() { 1, ); - let view = test_harness(|mut virtual_overseer| async move { + let mut view = View::new(); + test_harness(&mut view, |mut virtual_overseer| async move { activate_leaf( &mut virtual_overseer, leaf.clone(), @@ -217,7 +236,8 @@ fn candidate_approved_for_different_forks() { let rb_hash_fork_0 = Hash::from_low_u64_be(132); let rb_hash_fork_1 = Hash::from_low_u64_be(231); - let view = test_harness(|mut virtual_overseer| async move { + let mut view = View::new(); + test_harness(&mut view, |mut virtual_overseer| async move { let leaf0 = new_leaf( rb_hash_fork_0.clone(), 1, @@ -278,7 +298,8 @@ fn candidate_approval_stats_with_no_shows() { let rb_hash = Hash::from_low_u64_be(111); let candidate_hash: CandidateHash = CandidateHash(Hash::from_low_u64_be(132)); - let view = test_harness(|mut virtual_overseer| async move { + let mut view = View::new(); + test_harness(&mut view, |mut virtual_overseer| async move { let leaf1 = new_leaf(rb_hash.clone(), 1); activate_leaf( &mut virtual_overseer, @@ -319,7 +340,8 @@ fn note_chunks_downloaded() { (ValidatorIndex(1), 2), ]; - let view = test_harness(|mut virtual_overseer| async move { + let mut view = View::new(); + test_harness(&mut view, |mut virtual_overseer| async move { virtual_overseer.send(FromOrchestra::Communication { msg: ConsensusStatisticsCollectorMessage::ChunksDownloaded( session_idx, candidate_hash.clone(), HashMap::from_iter(chunk_downloads.clone().into_iter()), @@ -361,13 +383,20 @@ fn note_chunks_downloaded() { fn default_header() -> Header { Header { parent_hash: Hash::zero(), - number: 100500, + number: 1, state_root: Hash::zero(), extrinsics_root: Hash::zero(), digest: Default::default(), } } +fn header_with_number_and_parent(block_number: BlockNumber, parent_hash: Hash) -> Header { + let mut header = default_header(); + header.number = block_number; + header.parent_hash = parent_hash; + header +} + fn default_session_info(session_idx: SessionIndex) -> SessionInfo { SessionInfo { active_validator_indices: vec![], @@ -403,7 +432,8 @@ fn note_chunks_uploaded_to_active_validator() { let candidate_hash: CandidateHash = CandidateHash(Hash::from_low_u64_be(132)); - let view = test_harness(|mut virtual_overseer| async move { + let mut view = View::new(); + test_harness(&mut view, |mut virtual_overseer| async move { activate_leaf( &mut virtual_overseer, leaf1, @@ -438,3 +468,209 @@ fn note_chunks_uploaded_to_active_validator() { assert_matches!(view.availability_chunks.get(&2).unwrap(), expected_av_chunks); } + +#[test] +fn prune_unfinalized_forks() { + // testing pruning capabilities + // the pruning happens when a session is finalized + // means that all the collected data for the finalized session + // should be kept and the collected data that belongs to unfinalized + // should be pruned + + // Building a "chain" with the following relay blocks (all in the same session) + // A -> B + // A -> C -> D + + let hash_a = Hash::from_slice(&[00; 32]); + let hash_b = Hash::from_slice(&[01; 32]); + let hash_c = Hash::from_slice(&[02; 32]); + let hash_d = Hash::from_slice(&[03; 32]); + let session_zero: SessionIndex = 0; + + let mut view = View::new(); + test_harness(&mut view, |mut virtual_overseer| async move { + let leaf_a = new_leaf(hash_a.clone(), 1); + let leaf_a_header = default_header(); + + activate_leaf( + &mut virtual_overseer, + leaf_a, + leaf_a_header, + session_zero, + Some(default_session_info(session_zero)), + ).await; + + let leaf_b = new_leaf(hash_b.clone(), 2); + let leaf_b_header = header_with_number_and_parent(2, hash_a.clone()); + + activate_leaf( + &mut virtual_overseer, + leaf_b, + leaf_b_header, + session_zero, + None, + ).await; + + let leaf_c = new_leaf(hash_c.clone(), 2); + let leaf_c_header = header_with_number_and_parent(2, hash_a.clone()); + + activate_leaf( + &mut virtual_overseer, + leaf_c, + leaf_c_header, + session_zero, + None, + ).await; + + let leaf_d = new_leaf(hash_d.clone(), 3); + let leaf_d_header = header_with_number_and_parent(3, hash_c.clone()); + + activate_leaf( + &mut virtual_overseer, + leaf_d, + leaf_d_header, + session_zero, + None, + ).await; + + virtual_overseer + }); + + let expect = vec![ + // relay node A should have 2 children (B, C) + (hash_a.clone(), (None, vec![hash_b.clone(), hash_c.clone()])), + + // relay node B should link to A and have no children + (hash_b.clone(), (Some(hash_a.clone()), vec![])), + + // relay node C should link to A and have 1 child (D) + (hash_c.clone(), (Some(hash_a.clone()), vec![hash_d.clone()])), + + // relay node D should link to C and have no children + (hash_d.clone(), (Some(hash_c.clone()), vec![])), + ]; + + assert_eq!(view.current_session, None); + // relay node A should be the root + assert_roots_and_relay_views( + &view, + vec![hash_a], + expect.clone(), + ); + + // Finalizing block C should not affect as the session 0 still not finalized + test_harness(&mut view, |mut virtual_overseer| async move { + finalize_block( + &mut virtual_overseer, + (hash_c.clone(), 2), + session_zero).await; + + virtual_overseer + }); + + assert_eq!(view.current_session, Some(0)); + assert_roots_and_relay_views( + &view, + vec![hash_a], + expect.clone(), + ); + + // creating more 3 relay block (E, F, G), all in session 1 + // A -> B + // A -> C -> D -> E -> F + // E -> G + + let hash_e = Hash::from_slice(&[04; 32]); + let hash_f = Hash::from_slice(&[05; 32]); + let hash_g = Hash::from_slice(&[06; 32]); + let session_one: SessionIndex = 1; + + test_harness(&mut view, |mut virtual_overseer| async move { + let leaf_e = new_leaf(hash_e.clone(), 4); + let leaf_e_header = header_with_number_and_parent(4, hash_d.clone()); + + activate_leaf( + &mut virtual_overseer, + leaf_e, + leaf_e_header, + session_one, + Some(default_session_info(session_one)), + ).await; + + let leaf_f = new_leaf(hash_f.clone(), 5); + let leaf_f_header = header_with_number_and_parent(5, hash_e.clone()); + + activate_leaf( + &mut virtual_overseer, + leaf_f, + leaf_f_header, + session_one, + None, + ).await; + + let leaf_g = new_leaf(hash_g.clone(), 5); + let leaf_g_header = header_with_number_and_parent(5, hash_e.clone()); + + activate_leaf( + &mut virtual_overseer, + leaf_g, + leaf_g_header, + session_one, + None, + ).await; + + // finalizing relay block E + finalize_block( + &mut virtual_overseer, + (hash_e.clone(), 4), + session_one).await; + + virtual_overseer + }); + + // Finalizing block E triggers the pruning mechanism + // as block E belongs to session 1 meaning that session 0 + // is fully finalized + // - blocks E, F and G will remain. + // - blocks A, C and D will be placed in the per_session view + // as they belong to the finalized chain and are from the finalized session 0 + // the node will keep the collected data from them in the per_session view. + // - block B will be simply pruned as it does not belong to the finalized chain + let expect = vec![ + // relay node E should have 2 children (E, G) + (hash_e.clone(), (Some(hash_d), vec![hash_f, hash_g])), + + // relay node F should link to E and have no children + (hash_f.clone(), (Some(hash_e), vec![])), + + // relay node G should link to E and have no children + (hash_g.clone(), (Some(hash_e), vec![])), + ]; + + assert_eq!(view.current_session, Some(session_one)); + // relay node A should be the root + assert_roots_and_relay_views( + &view, + vec![hash_e], + expect.clone(), + ); +} + +fn assert_roots_and_relay_views( + view: &View, + roots: Vec, + relay_views: Vec<(Hash, (Option, Vec))>, +) { + assert_eq!(view.roots, HashSet::from_iter(roots)); + assert_eq!(view.per_relay.len(), relay_views.len()); + + for (rb_hash, checks) in relay_views.into_iter() { + let rb_view = view.per_relay.get(&rb_hash).unwrap(); + assert_eq!(rb_view.parent_hash, checks.0); + assert_eq!(rb_view.children.len(), checks.1.len()); + + for child in checks.1.iter() { + assert!(rb_view.children.contains(child)); + } + } +} \ No newline at end of file From 3d3686e8571fcd1e92039739c2ef32a0b58f9af6 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Mon, 20 Oct 2025 15:04:05 -0400 Subject: [PATCH 26/48] chore: assert collected stats for pruned are discarded --- .../consensus-statistics-collector/src/lib.rs | 4 +- .../src/tests.rs | 49 +++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/polkadot/node/core/consensus-statistics-collector/src/lib.rs b/polkadot/node/core/consensus-statistics-collector/src/lib.rs index c6e5bdcb9af50..56e59dcee14c0 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/lib.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/lib.rs @@ -244,10 +244,10 @@ pub(crate) async fn run_iteration( } view.per_session - .get_mut(&session_idx) + .get_mut(&rb_view.session_index) .and_then(|session_view| { metrics.record_approvals_stats( - session_idx, + rb_view.session_index, rb_view.approvals_stats.clone()); session_view.finalized_approval_stats diff --git a/polkadot/node/core/consensus-statistics-collector/src/tests.rs b/polkadot/node/core/consensus-statistics-collector/src/tests.rs index 977a4dc319904..9b54a5f96a3cb 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/tests.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/tests.rs @@ -487,6 +487,10 @@ fn prune_unfinalized_forks() { let hash_d = Hash::from_slice(&[03; 32]); let session_zero: SessionIndex = 0; + let candidate_hash_a: CandidateHash = CandidateHash(Hash::from_low_u64_be(100)); + let candidate_hash_b: CandidateHash = CandidateHash(Hash::from_low_u64_be(200)); + let candidate_hash_c: CandidateHash = CandidateHash(Hash::from_low_u64_be(300)); + let mut view = View::new(); test_harness(&mut view, |mut virtual_overseer| async move { let leaf_a = new_leaf(hash_a.clone(), 1); @@ -500,6 +504,11 @@ fn prune_unfinalized_forks() { Some(default_session_info(session_zero)), ).await; + candidate_approved(&mut virtual_overseer, candidate_hash_a, hash_a, + vec![ValidatorIndex(2), ValidatorIndex(3)]).await; + no_shows(&mut virtual_overseer, candidate_hash_a, hash_a, + vec![ValidatorIndex(0), ValidatorIndex(1)]).await; + let leaf_b = new_leaf(hash_b.clone(), 2); let leaf_b_header = header_with_number_and_parent(2, hash_a.clone()); @@ -511,6 +520,9 @@ fn prune_unfinalized_forks() { None, ).await; + candidate_approved(&mut virtual_overseer, candidate_hash_b, hash_b, + vec![ValidatorIndex(0), ValidatorIndex(1)]).await; + let leaf_c = new_leaf(hash_c.clone(), 2); let leaf_c_header = header_with_number_and_parent(2, hash_a.clone()); @@ -522,6 +534,9 @@ fn prune_unfinalized_forks() { None, ).await; + candidate_approved(&mut virtual_overseer, candidate_hash_c, hash_c, + vec![ValidatorIndex(0), ValidatorIndex(1), ValidatorIndex(2)]).await; + let leaf_d = new_leaf(hash_d.clone(), 3); let leaf_d_header = header_with_number_and_parent(3, hash_c.clone()); @@ -583,6 +598,8 @@ fn prune_unfinalized_forks() { let hash_e = Hash::from_slice(&[04; 32]); let hash_f = Hash::from_slice(&[05; 32]); let hash_g = Hash::from_slice(&[06; 32]); + + let candidate_hash_e = CandidateHash(Hash::from_low_u64_be(0xEE0011)); let session_one: SessionIndex = 1; test_harness(&mut view, |mut virtual_overseer| async move { @@ -597,6 +614,11 @@ fn prune_unfinalized_forks() { Some(default_session_info(session_one)), ).await; + candidate_approved(&mut virtual_overseer, candidate_hash_e, hash_e, + vec![ValidatorIndex(3), ValidatorIndex(1), ValidatorIndex(0)]).await; + no_shows(&mut virtual_overseer, candidate_hash_e, hash_e, + vec![ValidatorIndex(2)]).await; + let leaf_f = new_leaf(hash_f.clone(), 5); let leaf_f_header = header_with_number_and_parent(5, hash_e.clone()); @@ -654,6 +676,33 @@ fn prune_unfinalized_forks() { vec![hash_e], expect.clone(), ); + + let expected_session_views = HashMap::from_iter(vec![ + (0 as SessionIndex, PerSessionView { + authorities_lookup: HashMap::new(), + finalized_approval_stats: HashMap::from_iter(vec![ + ( + candidate_hash_a.clone(), + // expected approval stats from a given finalized session + // ignoring the stats collected for relay block B + ApprovalsStats::new( + HashSet::from_iter(vec![ValidatorIndex(2), ValidatorIndex(3)]), + HashSet::from_iter(vec![ValidatorIndex(0), ValidatorIndex(1)]) + ), + ), + ( + candidate_hash_c.clone(), + ApprovalsStats::new( + HashSet::from_iter(vec![ValidatorIndex(0), ValidatorIndex(1), ValidatorIndex(2)]), + Default::default(), + ), + ), + ]), + }), + (1 as SessionIndex, PerSessionView::new(Default::default())), + ]); + + assert_eq!(view.per_session, expected_session_views); } fn assert_roots_and_relay_views( From ec2d55d3af1c3b2237670e96b9bdb0073c59a84b Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Wed, 22 Oct 2025 13:41:17 -0400 Subject: [PATCH 27/48] chore: include zombienet happy-path test --- .../src/metrics.rs | 2 +- .../rewards_statistics_collector.rs | 71 ++++++++++++++++--- 2 files changed, 63 insertions(+), 10 deletions(-) diff --git a/polkadot/node/core/consensus-statistics-collector/src/metrics.rs b/polkadot/node/core/consensus-statistics-collector/src/metrics.rs index 02ae4c63e2c4d..9c7eebf9ea876 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/metrics.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/metrics.rs @@ -72,7 +72,7 @@ impl metrics::Metrics for Metrics { "polkadot_parachain_rewards_statistics_collector_approvals_per_session_per_validator", "Total number of useful approvals a given validator provided on a session.", ), - vec!["session", "validator_idx"].as_ref(), + vec!["session", "validator_idx"].as_ref(), )?, registry, )?, diff --git a/polkadot/zombienet-sdk-tests/tests/functional/rewards_statistics_collector.rs b/polkadot/zombienet-sdk-tests/tests/functional/rewards_statistics_collector.rs index ce102378a72f2..e646f46a16775 100644 --- a/polkadot/zombienet-sdk-tests/tests/functional/rewards_statistics_collector.rs +++ b/polkadot/zombienet-sdk-tests/tests/functional/rewards_statistics_collector.rs @@ -6,11 +6,13 @@ use std::ops::Range; use anyhow::anyhow; -use cumulus_zombienet_sdk_helpers::assert_para_throughput; -use polkadot_primitives::Id as ParaId; +use cumulus_zombienet_sdk_helpers::{assert_para_throughput, wait_for_nth_session_change}; +use polkadot_primitives::{Id as ParaId, SessionIndex}; use serde_json::json; use subxt::{OnlineClient, PolkadotConfig}; +use subxt::blocks::Block; use zombienet_orchestrator::network::Network; +use zombienet_orchestrator::network::node::NetworkNode; use zombienet_sdk::{LocalFileSystem, NetworkConfigBuilder}; #[tokio::test(flavor = "multi_thread")] @@ -74,6 +76,8 @@ async fn rewards_statistics_collector_test() -> Result<(), anyhow::Error> { let relay_node = network.get_node("validator-0")?; let relay_client: OnlineClient = relay_node.wait_client().await?; + let mut blocks_sub = relay_client.blocks().subscribe_finalized().await?; + assert_para_throughput( &relay_client, 15, @@ -83,7 +87,11 @@ async fn rewards_statistics_collector_test() -> Result<(), anyhow::Error> { ) .await?; - assert_validators_collected_approval_usages( + // wait for a session to be finalized + wait_for_nth_session_change(&mut blocks_sub, 1).await; + + assert_approval_usages_medians( + 1, 0..12, &network, ).await?; @@ -91,16 +99,61 @@ async fn rewards_statistics_collector_test() -> Result<(), anyhow::Error> { Ok(()) } -async fn assert_validators_collected_approval_usages(validators_range: Range, network: &Network) -> Result<(), anyhow::Error> { - for idx in validators_range { - let validator_identifier = format!("validator-{}", idx); +async fn assert_approval_usages_medians( + session: SessionIndex, + validators_range: Range, + network: &Network, +) -> Result<(), anyhow::Error> { + let mut medians = vec![]; + for idx in validators_range.clone() { + let validator_identifier = format!("validator-{}", idx); let relay_node = network.get_node(validator_identifier.clone())?; - let approvals_usage_total = relay_node.reports("polkadot_parachain_rewards_statistics_collector_approvals_usage_total").await?; - assert!(approvals_usage_total > 0.0); - log::info!("{validator_identifier} -> collected usages: {approvals_usage_total}"); + let approvals_per_session = + report_label_with_attributes( + "polkadot_parachain_rewards_statistics_collector_approvals_per_session", + vec![ + ("session", session.to_string().as_str()), + ("chain", "rococo_local_testnet"), + ], + ); + + let total_approvals = relay_node.reports(approvals_per_session.clone()).await?; + + let mut metrics = vec![]; + for validator_idx in validators_range.clone() { + let approvals_per_session_per_validator = + report_label_with_attributes( + "polkadot_parachain_rewards_statistics_collector_approvals_per_session_per_validator", + vec![ + ("session", session.to_string().as_str()), + ("validator_idx", validator_idx.to_string().as_str()), + ("chain", "rococo_local_testnet"), + ], + ); + metrics.push(approvals_per_session_per_validator); + } + + let mut total_sum = 0; + for metric_per_validator in metrics { + let validator_approvals_usage = relay_node.reports(metric_per_validator.clone()).await?; + total_sum += validator_approvals_usage as u32; + } + + assert_eq!(total_sum, total_approvals as u32); + medians.push(total_sum / validators_range.len() as u32); } + log::info!("Collected medians for session {session} {:?}", medians); Ok(()) +} + +fn report_label_with_attributes(label: &str, attributes: Vec<(&str, &str)>) -> String { + let mut attrs: Vec = vec![]; + for (k, v) in attributes { + attrs.push(format!("{}=\"{}\"", k, v)); + } + let final_attrs = attrs.join(","); + format!("{label}{{{final_attrs}}}") } \ No newline at end of file From e90fb75a20b5562e2e4506882e305b1037305b73 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Tue, 28 Oct 2025 09:21:39 -0400 Subject: [PATCH 28/48] addressing comments --- Cargo.toml | 2 +- .../zombienet-sdk-helpers/src/lib.rs | 9 + polkadot/node/core/approval-voting/src/lib.rs | 75 +++++---- polkadot/node/core/chain-api/src/lib.rs | 1 - .../consensus-statistics-collector/src/lib.rs | 17 +- .../src/tests.rs | 2 - .../availability-distribution/src/lib.rs | 46 +++--- .../availability-recovery/src/task/mod.rs | 1 + .../src/task/strategy/full.rs | 4 +- .../src/task/strategy/mod.rs | 32 ++-- polkadot/node/subsystem-types/src/messages.rs | 4 - .../tests/functional/mod.rs | 1 + .../rewards_statistics_collector.rs | 12 +- .../rewards_statistics_mixed_validators.rs | 155 ++++++++++++++++++ substrate/frame/executive/src/lib.rs | 5 +- 15 files changed, 264 insertions(+), 102 deletions(-) create mode 100644 polkadot/zombienet-sdk-tests/tests/functional/rewards_statistics_mixed_validators.rs diff --git a/Cargo.toml b/Cargo.toml index 87b8cc5667107..58dd1eb30a646 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1143,7 +1143,7 @@ polkadot-node-core-parachains-inherent = { path = "polkadot/node/core/parachains polkadot-node-core-prospective-parachains = { path = "polkadot/node/core/prospective-parachains", default-features = false } polkadot-node-core-consensus-statistics-collector = { path = "polkadot/node/core/consensus-statistics-collector", default-features = false } polkadot-node-core-provisioner = { path = "polkadot/node/core/provisioner", default-features = false } -polkadot-node-core-pvf = { path = "polkadot/node/core/pvf", default-features = false } +polkadot-node-core-pvf = { path = "polkadot/node/core/pvf", default-features = false } polkadot-node-core-pvf-checker = { path = "polkadot/node/core/pvf-checker", default-features = false } polkadot-node-core-pvf-common = { path = "polkadot/node/core/pvf/common", default-features = false } polkadot-node-core-pvf-execute-worker = { path = "polkadot/node/core/pvf/execute-worker", default-features = false } diff --git a/cumulus/zombienet/zombienet-sdk-helpers/src/lib.rs b/cumulus/zombienet/zombienet-sdk-helpers/src/lib.rs index 80d77dfc21a72..32dac61666c67 100644 --- a/cumulus/zombienet/zombienet-sdk-helpers/src/lib.rs +++ b/cumulus/zombienet/zombienet-sdk-helpers/src/lib.rs @@ -461,3 +461,12 @@ pub async fn wait_for_upgrade( } Ok(()) } + +pub fn report_label_with_attributes(label: &str, attributes: Vec<(&str, &str)>) -> String { + let mut attrs: Vec = vec![]; + for (k, v) in attributes { + attrs.push(format!("{}=\"{}\"", k, v)); + } + let final_attrs = attrs.join(","); + format!("{label}{{{final_attrs}}}") +} \ No newline at end of file diff --git a/polkadot/node/core/approval-voting/src/lib.rs b/polkadot/node/core/approval-voting/src/lib.rs index c1f6b247b6329..f7837a89fd562 100644 --- a/polkadot/node/core/approval-voting/src/lib.rs +++ b/polkadot/node/core/approval-voting/src/lib.rs @@ -1306,11 +1306,6 @@ where let mut delayed_approvals_timers = DelayedApprovalTimer::default(); let mut approvals_cache = LruMap::new(ByLength::new(APPROVAL_CACHE_SIZE)); - gum::info!( - target: LOG_TARGET, - "Approval Voting Subsytem is running..." - ); - loop { let mut overlayed_db = OverlayedBackend::new(&backend); let actions = futures::select! { @@ -3245,6 +3240,25 @@ where } } + if newly_approved { + gum::info!( + target: LOG_TARGET, + ?block_hash, + ?candidate_hash, + "Candidate newly approved, collecting useful approvals..." + ); + + collect_useful_approvals(sender, &status, block_hash, &candidate_entry); + + if status.no_show_validators.len() > 0 { + _ = sender + .try_send_message(ConsensusStatisticsCollectorMessage::NoShows( + candidate_entry.candidate.hash(), + block_hash, + status.no_show_validators, + )); + } + } // We have no need to write the candidate entry if all of the following // is true: @@ -3257,32 +3271,12 @@ where if transition.is_local_approval() || newly_approved || !already_approved_by.unwrap_or(true) { // In all other cases, we need to write the candidate entry. - db.write_candidate_entry(candidate_entry.clone()); + db.write_candidate_entry(candidate_entry); } newly_approved }; - if newly_approved { - gum::info!( - target: LOG_TARGET, - ?block_hash, - ?candidate_hash, - "Candidate newly approved, collecting useful approvals..." - ); - - collect_useful_approvals(sender, &status, block_hash, &candidate_entry); - - if status.no_show_validators.len() > 0 { - _ = sender - .try_send_message(ConsensusStatisticsCollectorMessage::NoShows( - candidate_entry.candidate.hash(), - block_hash, - status.no_show_validators, - )); - } - } - actions } @@ -4130,9 +4124,18 @@ where let candidate_hash = candidate_entry.candidate.hash(); let candidate_approvals = candidate_entry.approvals(); - let approval_entry = candidate_entry - .approval_entry(&block_hash) - .expect("Approval entry just fetched; qed"); + let approval_entry = match candidate_entry.approval_entry(&block_hash) { + Some(approval_entry) => approval_entry, + None => { + gum::warn!( + target: LOG_TARGET, + ?block_hash, + ?candidate_hash, + "approval entry not found, cannot collect useful approvals." + ); + return + }, + }; let collected_useful_approvals: Vec = match status.required_tranches { RequiredTranches::All => { @@ -4143,12 +4146,20 @@ where assigned_mask &= candidate_approvals; assigned_mask.iter_ones().map(|idx| ValidatorIndex(idx as _)).collect() }, - RequiredTranches::Pending {..} => panic!("Newly approved candidate should never be pending; qed"), + RequiredTranches::Pending {..} => { + gum::warn!( + target: LOG_TARGET, + ?block_hash, + ?candidate_hash, + "approval status required tranches still pending when collecting useful approvals" + ); + return + }, }; - if collected_useful_approvals.len() > 0 { + if !collected_useful_approvals.is_empty() { let useful_approvals = collected_useful_approvals.len(); - gum::info!( + gum::debug!( target: LOG_TARGET, ?block_hash, ?candidate_hash, diff --git a/polkadot/node/core/chain-api/src/lib.rs b/polkadot/node/core/chain-api/src/lib.rs index 0875236abcd02..7fd5166310fec 100644 --- a/polkadot/node/core/chain-api/src/lib.rs +++ b/polkadot/node/core/chain-api/src/lib.rs @@ -118,7 +118,6 @@ where let result = subsystem.client.hash(number).await.map_err(|e| e.to_string().into()); subsystem.metrics.on_request(result.is_ok()); - let _ = response_channel.send(result); }, ChainApiMessage::FinalizedBlockNumber(response_channel) => { diff --git a/polkadot/node/core/consensus-statistics-collector/src/lib.rs b/polkadot/node/core/consensus-statistics-collector/src/lib.rs index 56e59dcee14c0..08e126e458fc4 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/lib.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/lib.rs @@ -176,18 +176,17 @@ pub(crate) async fn run_iteration( .map_err(JfyiError::RuntimeApiCallError)?; if let Some(ref h) = header { - let parent_hash = h.clone().parent_hash; - let parent_hash = if let Some(parent) = view.per_relay.get_mut(&parent_hash) { - parent.link_child(relay_hash.clone()); - Some(parent_hash) - } else { - view.roots.insert(relay_hash.clone()); - None + let parent_hash = h.parent_hash; + match view.per_relay.get_mut(&parent_hash) { + Some(per_relay_view) => per_relay_view.link_child(relay_hash), + None => { + _ = view.roots.insert(relay_hash); + }, }; - view.per_relay.insert(relay_hash, PerRelayView::new(parent_hash, session_idx)); + view.per_relay.insert(relay_hash, PerRelayView::new(Some(parent_hash), session_idx)); } else { - view.roots.insert(relay_hash.clone()); + view.roots.insert(relay_hash); view.per_relay.insert(relay_hash, PerRelayView::new(None, session_idx)); } diff --git a/polkadot/node/core/consensus-statistics-collector/src/tests.rs b/polkadot/node/core/consensus-statistics-collector/src/tests.rs index 9b54a5f96a3cb..3b553fa68b859 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/tests.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/tests.rs @@ -212,8 +212,6 @@ fn single_candidate_approved() { assert_eq!(view.per_relay.len(), 1); let stats_for = view.per_relay.get(&rb_hash).unwrap(); let approvals_for = stats_for.approvals_stats.get(&candidate_hash).unwrap(); - - println!("{:?}", approvals_for.votes); let expected_votes = vec![validator_idx]; let collected_votes= approvals_for diff --git a/polkadot/node/network/availability-distribution/src/lib.rs b/polkadot/node/network/availability-distribution/src/lib.rs index b8cd15138767b..ae2663e552c53 100644 --- a/polkadot/node/network/availability-distribution/src/lib.rs +++ b/polkadot/node/network/availability-distribution/src/lib.rs @@ -49,7 +49,7 @@ pub use metrics::Metrics; use polkadot_node_network_protocol::authority_discovery::AuthorityDiscovery; #[cfg(test)] -pub mod tests; +mod tests; const LOG_TARGET: &'static str = "parachain::availability-distribution"; @@ -174,7 +174,8 @@ where FromOrchestra::Signal(OverseerSignal::BlockFinalized(_hash, _finalized_number)) => { }, FromOrchestra::Signal(OverseerSignal::Conclude) => return Ok(()), - FromOrchestra::Communication { msg} => match msg { + FromOrchestra::Communication { + msg: AvailabilityDistributionMessage::FetchPoV { relay_parent, from_validator, @@ -182,33 +183,26 @@ where candidate_hash, pov_hash, tx, - } => { - log_error( - pov_requester::fetch_pov( - &mut ctx, - &mut runtime, - relay_parent, - from_validator, - para_id, - candidate_hash, - pov_hash, - tx, - metrics.clone(), - ) - .await, - "pov_requester::fetch_pov", - &mut warn_freq, - )?; - }, - AvailabilityDistributionMessage::NetworkBridgeUpdate(event) => { - }, + } => { + log_error( + pov_requester::fetch_pov( + &mut ctx, + &mut runtime, + relay_parent, + from_validator, + para_id, + candidate_hash, + pov_hash, + tx, + metrics.clone(), + ) + .await, + "pov_requester::fetch_pov", + &mut warn_freq, + )?; }, } } } } - -async fn handle_network_update() { - -} diff --git a/polkadot/node/network/availability-recovery/src/task/mod.rs b/polkadot/node/network/availability-recovery/src/task/mod.rs index e5b3a7d8bd20f..16a673a82e8da 100644 --- a/polkadot/node/network/availability-recovery/src/task/mod.rs +++ b/polkadot/node/network/availability-recovery/src/task/mod.rs @@ -179,6 +179,7 @@ where self.params.metrics.on_recovery_invalid(strategy_type), _ => self.params.metrics.on_recovery_failed(strategy_type), } + _ = self.state.get_download_chunks_metrics(); return Err(err) }, Ok(data) => { diff --git a/polkadot/node/network/availability-recovery/src/task/strategy/full.rs b/polkadot/node/network/availability-recovery/src/task/strategy/full.rs index 4869806e0a503..fd4d29ff33f49 100644 --- a/polkadot/node/network/availability-recovery/src/task/strategy/full.rs +++ b/polkadot/node/network/availability-recovery/src/task/strategy/full.rs @@ -62,7 +62,7 @@ impl RecoveryStrategy async fn run( mut self: Box, - _: &mut State, + state: &mut State, sender: &mut Sender, common_params: &RecoveryParams, ) -> Result { @@ -126,7 +126,7 @@ impl RecoveryStrategy ); common_params.metrics.on_full_request_succeeded(); - + state.note_received_available_data(validator_index); return Ok(data) }, None => { diff --git a/polkadot/node/network/availability-recovery/src/task/strategy/mod.rs b/polkadot/node/network/availability-recovery/src/task/strategy/mod.rs index 677c7a38852d8..27d3e2575c89c 100644 --- a/polkadot/node/network/availability-recovery/src/task/strategy/mod.rs +++ b/polkadot/node/network/availability-recovery/src/task/strategy/mod.rs @@ -45,9 +45,10 @@ use polkadot_node_subsystem::{ use polkadot_primitives::{AuthorityDiscoveryId, BlakeTwo256, ChunkIndex, HashT, ValidatorIndex}; use sc_network::{IfDisconnected, OutboundFailure, ProtocolName, RequestFailure}; use std::{ - collections::{BTreeMap, HashMap, VecDeque}, + collections::{BTreeMap, HashMap, VecDeque, hash_map::Entry}, time::Duration, }; +use std::ops::Add; // How many parallel chunk fetching requests should be running at once. const N_PARALLEL: usize = 50; @@ -200,6 +201,11 @@ struct Chunk { validator_index: ValidatorIndex, } +enum ReceivedAvailableData { + Chunk(Chunk), + Full, +} + /// Intermediate/common data that must be passed between `RecoveryStrategy`s belonging to the /// same `RecoveryTask`. pub struct State { @@ -212,7 +218,8 @@ pub struct State { /// A record of errors returned when requesting a chunk from a validator. recorded_errors: HashMap<(AuthorityDiscoveryId, ValidatorIndex), ErrorRecord>, - chunks_received_by: HashMap>, + // a counter of received available data including individual chunks and full available data + received_available_data_by: HashMap, } impl State { @@ -220,7 +227,7 @@ impl State { Self { received_chunks: BTreeMap::new(), recorded_errors: HashMap::new(), - chunks_received_by: HashMap::new(), + received_available_data_by: HashMap::new(), } } @@ -228,16 +235,16 @@ impl State { self.received_chunks.insert(chunk_index, chunk); } - fn note_received_chunk(&mut self, sender: ValidatorIndex, chunk: ChunkIndex) { - self.chunks_received_by.entry(sender).or_default().push(chunk); + // increase the counter of received available data of the given validator index + fn note_received_available_data(&mut self, sender: ValidatorIndex) { + let mut counter = self.received_available_data_by.entry(sender).or_default(); + *counter += 1; } - // return the amount of chunks downloaded per validator - pub fn get_download_chunks_metrics(&self) -> HashMap { - self.chunks_received_by - .iter() - .map(|(validator, downloaded)| (*validator, downloaded.len() as u64)) - .collect() + // drain the record of chunks received per validator returning + // all the contained data + pub fn get_download_chunks_metrics(&mut self) -> HashMap { + self.received_available_data_by.drain().collect() } fn chunk_count(&self) -> usize { @@ -526,7 +533,7 @@ impl State { chunk.index, Chunk { chunk: chunk.chunk, validator_index }, ); - self.note_received_chunk(validator_index, chunk.index) + self.note_received_available_data(validator_index); } else { metrics.on_chunk_request_invalid(strategy_type); error_count += 1; @@ -690,6 +697,7 @@ mod tests { let (erasure_task_tx, _erasure_task_rx) = mpsc::channel(10); Self { + session_index: SessionIndex(0), validator_authority_keys: validator_authority_id(&validators), n_validators: validators.len(), threshold: recovery_threshold(validators.len()).unwrap(), diff --git a/polkadot/node/subsystem-types/src/messages.rs b/polkadot/node/subsystem-types/src/messages.rs index 3a96998aa065b..9db4a6992cf3e 100644 --- a/polkadot/node/subsystem-types/src/messages.rs +++ b/polkadot/node/subsystem-types/src/messages.rs @@ -499,10 +499,6 @@ pub enum AvailabilityDistributionMessage { /// The sender will be canceled if the fetching failed for some reason. tx: oneshot::Sender, }, - - /// Event from the network bridge. - #[from] - NetworkBridgeUpdate(NetworkBridgeEvent<()>), } /// Availability Recovery Message. diff --git a/polkadot/zombienet-sdk-tests/tests/functional/mod.rs b/polkadot/zombienet-sdk-tests/tests/functional/mod.rs index 27728db47fd00..df43e1a3551de 100644 --- a/polkadot/zombienet-sdk-tests/tests/functional/mod.rs +++ b/polkadot/zombienet-sdk-tests/tests/functional/mod.rs @@ -11,3 +11,4 @@ mod spam_statement_distribution_requests; mod sync_backing; mod validator_disabling; mod rewards_statistics_collector; +mod rewards_statistics_mixed_validators; diff --git a/polkadot/zombienet-sdk-tests/tests/functional/rewards_statistics_collector.rs b/polkadot/zombienet-sdk-tests/tests/functional/rewards_statistics_collector.rs index e646f46a16775..f6dbadc14c2f8 100644 --- a/polkadot/zombienet-sdk-tests/tests/functional/rewards_statistics_collector.rs +++ b/polkadot/zombienet-sdk-tests/tests/functional/rewards_statistics_collector.rs @@ -6,7 +6,8 @@ use std::ops::Range; use anyhow::anyhow; -use cumulus_zombienet_sdk_helpers::{assert_para_throughput, wait_for_nth_session_change}; +use cumulus_zombienet_sdk_helpers::{assert_para_throughput, wait_for_nth_session_change, + report_label_with_attributes}; use polkadot_primitives::{Id as ParaId, SessionIndex}; use serde_json::json; use subxt::{OnlineClient, PolkadotConfig}; @@ -147,13 +148,4 @@ async fn assert_approval_usages_medians( log::info!("Collected medians for session {session} {:?}", medians); Ok(()) -} - -fn report_label_with_attributes(label: &str, attributes: Vec<(&str, &str)>) -> String { - let mut attrs: Vec = vec![]; - for (k, v) in attributes { - attrs.push(format!("{}=\"{}\"", k, v)); - } - let final_attrs = attrs.join(","); - format!("{label}{{{final_attrs}}}") } \ No newline at end of file diff --git a/polkadot/zombienet-sdk-tests/tests/functional/rewards_statistics_mixed_validators.rs b/polkadot/zombienet-sdk-tests/tests/functional/rewards_statistics_mixed_validators.rs new file mode 100644 index 0000000000000..c00faeb7db396 --- /dev/null +++ b/polkadot/zombienet-sdk-tests/tests/functional/rewards_statistics_mixed_validators.rs @@ -0,0 +1,155 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Test that nodes fetch availability chunks early for scheduled cores and normally for occupied +// core. + +use std::ops::Range; +use anyhow::anyhow; +use cumulus_zombienet_sdk_helpers::{assert_para_throughput, wait_for_nth_session_change, + report_label_with_attributes}; +use polkadot_primitives::{Id as ParaId, SessionIndex}; +use serde_json::json; +use subxt::{OnlineClient, PolkadotConfig}; +use subxt::blocks::Block; +use zombienet_orchestrator::network::Network; +use zombienet_orchestrator::network::node::NetworkNode; +use zombienet_sdk::{LocalFileSystem, NetworkConfigBuilder}; + +#[tokio::test(flavor = "multi_thread")] +async fn rewards_statistics_mixed_validators_test() -> Result<(), anyhow::Error> { + let _ = env_logger::try_init_from_env( + env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"), + ); + + let images = zombienet_sdk::environment::get_images_from_env(); + + let config = NetworkConfigBuilder::new() + .with_relaychain(|r| { + let r = r + .with_chain("rococo-local") + .with_default_command("polkadot") + .with_default_image(images.polkadot.as_str()) + .with_default_args(vec![("-lparachain=debug").into()]) + .with_genesis_overrides(json!({ + "configuration": { + "config": { + "scheduler_params": { + "group_rotation_frequency": 4 + } + } + } + })) + .with_node(|node| node.with_name("validator-0")); + + let r = (1..9) + .fold(r, |acc, i| + acc.with_node(|node| node.with_name(&format!("validator-{i}")))); + + (9..12).fold(r, |acc, i| { + acc.with_node(|node| { + node.with_name(&format!("malus-{i}")) + .with_args(vec![ + "-lparachain=debug,MALUS=trace".into(), + "--dispute-offset=14".into(), + "--alice".into(), + "--insecure-validator-i-know-what-i-do".into(), + ]) + .with_image( + std::env::var("MALUS_IMAGE") + .unwrap_or("docker.io/paritypr/malus".to_string()) + .as_str(), + ) + .with_command("malus") + .with_subcommand("dispute_valid_candidates") + .invulnerable(false) + }) + }) + }) + .with_parachain(|p| { + p.with_id(2000) + .with_default_command("adder-collator") + .with_default_image( + std::env::var("COL_IMAGE") + .unwrap_or("docker.io/paritypr/colander:latest".to_string()) + .as_str(), + ) + .cumulus_based(false) + .with_default_args(vec![("-lparachain=debug").into()]) + .with_collator(|n| n.with_name("collator-adder-2000")) + }) + .with_parachain(|p| { + p.with_id(2001) + .with_default_command("polkadot-parachain") + .with_default_image(images.cumulus.as_str()) + .with_default_args(vec![("-lparachain=debug,aura=debug").into()]) + .with_collator(|n| n.with_name("collator-2001")) + }) + .build() + .map_err(|e| { + let errs = e.into_iter().map(|e| e.to_string()).collect::>().join(" "); + anyhow!("config errs: {errs}") + })?; + + let spawn_fn = zombienet_sdk::environment::get_spawn_fn(); + let network = spawn_fn(config).await?; + + let relay_node = network.get_node("validator-0")?; + let relay_client: OnlineClient = relay_node.wait_client().await?; + + let mut blocks_sub = relay_client.blocks().subscribe_finalized().await?; + + // wait for session one to be finalized + wait_for_nth_session_change(&mut blocks_sub, 2).await; + + assert_approval_usages_medians( + 1, + 12, + vec![("validator", 0..9), ("malus", 9..12)], + &network, + ).await?; + + Ok(()) +} + +async fn assert_approval_usages_medians( + session: SessionIndex, + num_validators: usize, + validators_kind_and_range: Vec<(&str, Range)>, + network: &Network, +) -> Result<(), anyhow::Error> { + for (kind, validators_range) in validators_kind_and_range { + for idx in validators_range { + let validator_identifier = format!("{}-{}", kind, idx); + let relay_node = network.get_node(validator_identifier)?; + + let approvals_per_session = + report_label_with_attributes( + "polkadot_parachain_rewards_statistics_collector_approvals_per_session", + vec![ + ("session", session.to_string().as_str()), + ("chain", "rococo_local_testnet"), + ], + ); + + let noshows_per_session = report_label_with_attributes( + "polkadot_parachain_rewards_statistics_collector_no_shows_per_session", + vec![ + ("session", session.to_string().as_str()), + ("chain", "rococo_local_testnet"), + ], + ); + + let total_approvals = relay_node.reports(approvals_per_session).await?; + let total_noshows = relay_node.reports(noshows_per_session).await?; + + log::info!("{kind} #{idx} approvals {session} -> {total_approvals}"); + log::info!("{kind} #{idx} no-shows {session} -> {total_noshows}"); + + //assert!(total_approvals >= 9.0); + //assert!(total_noshows >= 3.0); + } + } + + Ok(()) +} \ No newline at end of file diff --git a/substrate/frame/executive/src/lib.rs b/substrate/frame/executive/src/lib.rs index 3f7259e77427c..1919f890cdf15 100644 --- a/substrate/frame/executive/src/lib.rs +++ b/substrate/frame/executive/src/lib.rs @@ -207,9 +207,8 @@ pub struct Executive< AllPalletsWithSystem, #[deprecated( note = "`OnRuntimeUpgrade` parameter in Executive is deprecated, will be removed after September 2026. \ - Use type `SingleBlockMigrations` in frame_system::Config instead." - )] - OnRuntimeUpgrade = (), + Use type `SingleBlockMigrations` in frame_system::Config instead." + )] OnRuntimeUpgrade = (), >( PhantomData<( System, From 7671486a96d2c7a9bc6e554051f8070f7c071d77 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Tue, 28 Oct 2025 16:23:04 -0400 Subject: [PATCH 29/48] chore: implemented new pruning based on finalized blocks - Remove the pruning based on finalized session - Prune finalized chain as well after aggregating the data in the session view - Fix tests --- .../consensus-statistics-collector/src/lib.rs | 105 ++++++---------- .../src/tests.rs | 119 +++++++++++++----- 2 files changed, 126 insertions(+), 98 deletions(-) diff --git a/polkadot/node/core/consensus-statistics-collector/src/lib.rs b/polkadot/node/core/consensus-statistics-collector/src/lib.rs index 08e126e458fc4..57318cd170f7c 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/lib.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/lib.rs @@ -88,7 +88,6 @@ impl PerSessionView { } struct View { - current_session: Option, roots: HashSet, per_relay: HashMap, per_session: HashMap, @@ -99,7 +98,6 @@ struct View { impl View { fn new() -> Self { return View{ - current_session: None, roots: HashSet::new(), per_relay: HashMap::new(), per_session: HashMap::new(), @@ -177,14 +175,18 @@ pub(crate) async fn run_iteration( if let Some(ref h) = header { let parent_hash = h.parent_hash; - match view.per_relay.get_mut(&parent_hash) { - Some(per_relay_view) => per_relay_view.link_child(relay_hash), + let parent_hash = match view.per_relay.get_mut(&parent_hash) { + Some(per_relay_view) => { + per_relay_view.link_child(relay_hash); + Some(parent_hash) + }, None => { _ = view.roots.insert(relay_hash); + None }, }; - view.per_relay.insert(relay_hash, PerRelayView::new(Some(parent_hash), session_idx)); + view.per_relay.insert(relay_hash, PerRelayView::new(parent_hash, session_idx)); } else { view.roots.insert(relay_hash); view.per_relay.insert(relay_hash, PerRelayView::new(None, session_idx)); @@ -209,55 +211,31 @@ pub(crate) async fn run_iteration( } }, FromOrchestra::Signal(OverseerSignal::BlockFinalized(fin_block_hash, _)) => { - // check if a session was finalized - let session_idx = request_session_index_for_child(fin_block_hash, ctx.sender()) - .await - .await - .map_err(JfyiError::OverseerCommunication)? - .map_err(JfyiError::RuntimeApiCallError)?; - - let should_prune = match view.current_session { - Some(curr_session_idx) if session_idx > curr_session_idx => true, - _ => false - }; - - if view.current_session.is_none() { - view.current_session = Some(session_idx); - } - - if should_prune { - let prev_session = view.current_session - .replace(session_idx) - .expect("current session should have been populate previously; qed."); - - let finalized_hashes = prune_unfinalised_forks(view, fin_block_hash); - - // finalized_hashes contains the hashes from the newest to the oldest - // so we revert it and check from the oldest to the newest - for hash in finalized_hashes.iter().rev() { - match view.per_relay.get(hash) { - Some(rb_view) => { - if rb_view.session_index > prev_session { - view.roots = HashSet::from_iter(vec![hash.clone()]); - break - } - - view.per_session - .get_mut(&rb_view.session_index) - .and_then(|session_view| { - metrics.record_approvals_stats( - rb_view.session_index, - rb_view.approvals_stats.clone()); - - session_view.finalized_approval_stats - .extend(rb_view.approvals_stats.clone()); - Some(session_view) - }); - - view.per_relay.remove(hash); - } - None => {}, + // as soon a block is finalized it performs: + // 1. Pruning unneeded forks + // 2. Collected statistics that belongs to the finalized chain + // 3. After collection of finalized statistics then remove finalized + // nodes from the mapping leaving only the unfinalized blocks after finalization + let finalized_hashes = prune_unfinalised_forks(view, fin_block_hash); + + // so we revert it and check from the oldest to the newest + for hash in finalized_hashes.iter().rev() { + match view.per_relay.get(hash) { + Some(rb_view) => { + view.per_session + .get_mut(&rb_view.session_index) + .and_then(|session_view| { + metrics.record_approvals_stats( + rb_view.session_index, + rb_view.approvals_stats.clone()); + + session_view.finalized_approval_stats + .extend(rb_view.approvals_stats.clone()); + Some(session_view) + }); + view.per_relay.remove(hash); } + None => {}, } } } @@ -317,7 +295,7 @@ pub(crate) async fn run_iteration( } // prune_unfinalised_forks will remove all the relay chain blocks -// that are not in the finalized chain and its dependants children using the latest finalized block as reference +// that are not in the finalized chain and its de pendants children using the latest finalized block as reference // and will return a list of finalized hashes fn prune_unfinalised_forks(view: &mut View, fin_block_hash: Hash) -> Vec { // since we want to reward only valid approvals, we retain @@ -325,8 +303,6 @@ fn prune_unfinalised_forks(view: &mut View, fin_block_hash: Hash) -> Vec { // identify the finalized chain so we don't prune let rb_view = match view.per_relay.get_mut(&fin_block_hash) { Some(per_relay_view) => per_relay_view, - - //TODO: the finalized block should already exists on the relay view mapping None => return Vec::new(), }; @@ -337,10 +313,11 @@ fn prune_unfinalised_forks(view: &mut View, fin_block_hash: Hash) -> Vec { let mut current_block_hash = fin_block_hash; let mut current_parent_hash = rb_view.parent_hash; while let Some(parent_hash) = current_parent_hash { - retain_relay_hashes.push(parent_hash.clone()); match view.per_relay.get_mut(&parent_hash) { Some(parent_view) => { + retain_relay_hashes.push(parent_hash.clone()); + if parent_view.children.len() > 1 { let filtered_set = parent_view.children .iter() @@ -361,21 +338,17 @@ fn prune_unfinalised_forks(view: &mut View, fin_block_hash: Hash) -> Vec { }; } - if view.roots.len() > 1 { - for root in view.roots.clone() { - if !retain_relay_hashes.contains(&root) { - removal_stack.push(root); - } + // update the roots to be the children of the latest finalized block + if let Some(finalized_hash) = retain_relay_hashes.first() { + if let Some(rb_view) = view.per_relay.get(finalized_hash) { + view.roots = rb_view.children.clone(); } } let mut to_prune = HashSet::new(); let mut queue: VecDeque = VecDeque::from(removal_stack); - while let Some(hash) = queue.pop_front() { - if !to_prune.insert(hash) { - continue; // already seen - } + _ = to_prune.insert(hash); if let Some(r_view) = view.per_relay.get(&hash) { for child in &r_view.children { diff --git a/polkadot/node/core/consensus-statistics-collector/src/tests.rs b/polkadot/node/core/consensus-statistics-collector/src/tests.rs index 3b553fa68b859..e4edd2d5c6082 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/tests.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/tests.rs @@ -84,15 +84,6 @@ async fn finalize_block( virtual_overseer .send(FromOrchestra::Signal(OverseerSignal::BlockFinalized(fin_block_hash, finalized.1))) .await; - - assert_matches!( - virtual_overseer.recv().await, - AllMessages::RuntimeApi( - RuntimeApiMessage::Request(hash, RuntimeApiRequest::SessionIndexForChild(tx)) - ) if hash == fin_block_hash => { - tx.send(Ok(session_index)).unwrap(); - } - ); } async fn candidate_approved( @@ -137,13 +128,16 @@ macro_rules! approvals_stats_assertion { ) { let stats_for = view.per_relay.get(&rb_hash).unwrap(); let approvals_for = stats_for.approvals_stats.get(&candidate_hash).unwrap(); - let collected_votes = approvals_for + let collected = approvals_for .$field .clone() .into_iter() .collect::>(); - assert_eq!(expected_votes, collected_votes); + assert_eq!(collected.len(), expected_votes.len()); + for item in collected { + assert!(expected_votes.contains(&item)); + } } }; } @@ -488,6 +482,7 @@ fn prune_unfinalized_forks() { let candidate_hash_a: CandidateHash = CandidateHash(Hash::from_low_u64_be(100)); let candidate_hash_b: CandidateHash = CandidateHash(Hash::from_low_u64_be(200)); let candidate_hash_c: CandidateHash = CandidateHash(Hash::from_low_u64_be(300)); + let candidate_hash_d: CandidateHash = CandidateHash(Hash::from_low_u64_be(400)); let mut view = View::new(); test_harness(&mut view, |mut virtual_overseer| async move { @@ -546,6 +541,9 @@ fn prune_unfinalized_forks() { None, ).await; + candidate_approved(&mut virtual_overseer, candidate_hash_d, hash_d, + vec![ValidatorIndex(0), ValidatorIndex(1)]).await; + virtual_overseer }); @@ -563,7 +561,6 @@ fn prune_unfinalized_forks() { (hash_d.clone(), (Some(hash_c.clone()), vec![])), ]; - assert_eq!(view.current_session, None); // relay node A should be the root assert_roots_and_relay_views( &view, @@ -571,7 +568,9 @@ fn prune_unfinalized_forks() { expect.clone(), ); - // Finalizing block C should not affect as the session 0 still not finalized + // Finalizing block C should prune the current unfinalized mapping + // and aggregate data of the finalized chain on the per session view + // the collected data for block D should remain untouched test_harness(&mut view, |mut virtual_overseer| async move { finalize_block( &mut virtual_overseer, @@ -581,17 +580,51 @@ fn prune_unfinalized_forks() { virtual_overseer }); - assert_eq!(view.current_session, Some(0)); + let expect = vec![ + // relay node D should link to C and have no children + (hash_d.clone(), (Some(hash_c.clone()), vec![])), + ]; + assert_roots_and_relay_views( &view, - vec![hash_a], + vec![hash_d], expect.clone(), ); + // check if the data was aggregated correctly for the session + let expected_session_views = HashMap::from_iter(vec![ + (0 as SessionIndex, PerSessionView { + authorities_lookup: HashMap::new(), + finalized_approval_stats: HashMap::from_iter(vec![ + ( + candidate_hash_a.clone(), + // expected approval stats from a given finalized session + // ignoring the stats collected for relay block B + ApprovalsStats::new( + HashSet::from_iter(vec![ValidatorIndex(2), ValidatorIndex(3)]), + HashSet::from_iter(vec![ValidatorIndex(0), ValidatorIndex(1)]) + ), + ), + ( + candidate_hash_c.clone(), + ApprovalsStats::new( + HashSet::from_iter(vec![ + ValidatorIndex(0), + ValidatorIndex(1), + ValidatorIndex(2)] + ), + Default::default(), + ), + ), + ]), + }), + ]); + + assert_eq!(view.per_session, expected_session_views); + // creating more 3 relay block (E, F, G), all in session 1 - // A -> B - // A -> C -> D -> E -> F - // E -> G + // D -> E -> F + // -> G let hash_e = Hash::from_slice(&[04; 32]); let hash_f = Hash::from_slice(&[05; 32]); @@ -649,17 +682,9 @@ fn prune_unfinalized_forks() { }); // Finalizing block E triggers the pruning mechanism - // as block E belongs to session 1 meaning that session 0 - // is fully finalized - // - blocks E, F and G will remain. - // - blocks A, C and D will be placed in the per_session view - // as they belong to the finalized chain and are from the finalized session 0 - // the node will keep the collected data from them in the per_session view. - // - block B will be simply pruned as it does not belong to the finalized chain + // now it should aggregate collected data from block D and E + // keeping only blocks F and E on the mapping let expect = vec![ - // relay node E should have 2 children (E, G) - (hash_e.clone(), (Some(hash_d), vec![hash_f, hash_g])), - // relay node F should link to E and have no children (hash_f.clone(), (Some(hash_e), vec![])), @@ -667,11 +692,10 @@ fn prune_unfinalized_forks() { (hash_g.clone(), (Some(hash_e), vec![])), ]; - assert_eq!(view.current_session, Some(session_one)); // relay node A should be the root assert_roots_and_relay_views( &view, - vec![hash_e], + vec![hash_f, hash_g], expect.clone(), ); @@ -691,13 +715,44 @@ fn prune_unfinalized_forks() { ( candidate_hash_c.clone(), ApprovalsStats::new( - HashSet::from_iter(vec![ValidatorIndex(0), ValidatorIndex(1), ValidatorIndex(2)]), + HashSet::from_iter(vec![ + ValidatorIndex(0), + ValidatorIndex(1), + ValidatorIndex(2), + ]), Default::default(), ), ), + ( + candidate_hash_d.clone(), + ApprovalsStats::new( + HashSet::from_iter(vec![ + ValidatorIndex(0), + ValidatorIndex(1), + ]), + Default::default(), + ), + ), + ]), + }), + (1 as SessionIndex, PerSessionView { + authorities_lookup: HashMap::new(), + finalized_approval_stats: HashMap::from_iter(vec![ + ( + candidate_hash_e.clone(), + // expected approval stats from a given finalized session + // ignoring the stats collected for relay block B + ApprovalsStats::new( + HashSet::from_iter(vec![ + ValidatorIndex(3), + ValidatorIndex(1), + ValidatorIndex(0) + ]), + HashSet::from_iter(vec![ValidatorIndex(2)]) + ), + ), ]), }), - (1 as SessionIndex, PerSessionView::new(Default::default())), ]); assert_eq!(view.per_session, expected_session_views); From e986951755b955be3743142354a0a0e061abc2ab Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Wed, 29 Oct 2025 11:26:22 -0400 Subject: [PATCH 30/48] chore: aggregate on tallies --- .../consensus-statistics-collector/src/lib.rs | 62 ++++-- .../src/tests.rs | 204 ++++++++++-------- 2 files changed, 165 insertions(+), 101 deletions(-) diff --git a/polkadot/node/core/consensus-statistics-collector/src/lib.rs b/polkadot/node/core/consensus-statistics-collector/src/lib.rs index 57318cd170f7c..04b6f6278d150 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/lib.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/lib.rs @@ -75,15 +75,31 @@ impl PerRelayView { } } +#[derive(Debug, Eq, PartialEq, Clone, Default)] +struct PerValidatorTally { + noshows: u32, + approvals: u32, +} + +impl PerValidatorTally { + fn increment_noshow(&mut self) { + self.noshows += 1; + } + + fn increment_approval(&mut self) { + self.approvals += 1; + } +} + #[derive(Debug, Eq, PartialEq, Clone)] pub struct PerSessionView { authorities_lookup: HashMap, - finalized_approval_stats: HashMap, + validators_tallies: HashMap, } impl PerSessionView { fn new(authorities_lookup: HashMap) -> Self { - Self { authorities_lookup, finalized_approval_stats: HashMap::new() } + Self { authorities_lookup, validators_tallies: HashMap::new() } } } @@ -220,22 +236,34 @@ pub(crate) async fn run_iteration( // so we revert it and check from the oldest to the newest for hash in finalized_hashes.iter().rev() { - match view.per_relay.get(hash) { - Some(rb_view) => { - view.per_session - .get_mut(&rb_view.session_index) - .and_then(|session_view| { - metrics.record_approvals_stats( - rb_view.session_index, - rb_view.approvals_stats.clone()); - - session_view.finalized_approval_stats - .extend(rb_view.approvals_stats.clone()); - Some(session_view) - }); - view.per_relay.remove(hash); + if let Some((session_idx, approvals_stats)) = view + .per_relay + .remove(hash) + .map(|rb_view| (rb_view.session_index, rb_view.approvals_stats)) + { + if let Some(session_view) = view.per_session.get_mut(&session_idx) { + metrics.record_approvals_stats(session_idx, approvals_stats.clone()); + + for stats in approvals_stats.values() { + // Increment no-show tallies + for &validator_idx in &stats.no_shows { + session_view + .validators_tallies + .entry(validator_idx) + .or_default() + .increment_noshow(); + } + + // Increment approval tallies + for &validator_idx in &stats.votes { + session_view + .validators_tallies + .entry(validator_idx) + .or_default() + .increment_approval(); + } + } } - None => {}, } } } diff --git a/polkadot/node/core/consensus-statistics-collector/src/tests.rs b/polkadot/node/core/consensus-statistics-collector/src/tests.rs index e4edd2d5c6082..aef49c9ebba36 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/tests.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/tests.rs @@ -591,37 +591,43 @@ fn prune_unfinalized_forks() { expect.clone(), ); - // check if the data was aggregated correctly for the session - let expected_session_views = HashMap::from_iter(vec![ - (0 as SessionIndex, PerSessionView { - authorities_lookup: HashMap::new(), - finalized_approval_stats: HashMap::from_iter(vec![ - ( - candidate_hash_a.clone(), - // expected approval stats from a given finalized session - // ignoring the stats collected for relay block B - ApprovalsStats::new( - HashSet::from_iter(vec![ValidatorIndex(2), ValidatorIndex(3)]), - HashSet::from_iter(vec![ValidatorIndex(0), ValidatorIndex(1)]) - ), - ), - ( - candidate_hash_c.clone(), - ApprovalsStats::new( - HashSet::from_iter(vec![ - ValidatorIndex(0), - ValidatorIndex(1), - ValidatorIndex(2)] - ), - Default::default(), - ), - ), - ]), - }), + // check if the data was aggregated correctly for the session view + // it should aggregat approvals and noshows collected on blocks + // A and C. + // Data collected on block B should be discarded + // Data collected on block D should remain in the mapping as it was not finalized or pruned + let expected_tallies = HashMap::from_iter(vec![ + ( + ValidatorIndex(0), + PerValidatorTally { + noshows: 1, + approvals: 1, + }, + ), + ( + ValidatorIndex(1), + PerValidatorTally { + noshows: 1, + approvals: 1, + }, + ), + ( + ValidatorIndex(2), + PerValidatorTally { + noshows: 0, + approvals: 2, + }, + ), + ( + ValidatorIndex(3), + PerValidatorTally { + noshows: 0, + approvals: 1, + }, + ), ]); - assert_eq!(view.per_session, expected_session_views); - + assert_per_session_tallies(&view.per_session, 0, expected_tallies); // creating more 3 relay block (E, F, G), all in session 1 // D -> E -> F // -> G @@ -699,63 +705,74 @@ fn prune_unfinalized_forks() { expect.clone(), ); - let expected_session_views = HashMap::from_iter(vec![ - (0 as SessionIndex, PerSessionView { - authorities_lookup: HashMap::new(), - finalized_approval_stats: HashMap::from_iter(vec![ - ( - candidate_hash_a.clone(), - // expected approval stats from a given finalized session - // ignoring the stats collected for relay block B - ApprovalsStats::new( - HashSet::from_iter(vec![ValidatorIndex(2), ValidatorIndex(3)]), - HashSet::from_iter(vec![ValidatorIndex(0), ValidatorIndex(1)]) - ), - ), - ( - candidate_hash_c.clone(), - ApprovalsStats::new( - HashSet::from_iter(vec![ - ValidatorIndex(0), - ValidatorIndex(1), - ValidatorIndex(2), - ]), - Default::default(), - ), - ), - ( - candidate_hash_d.clone(), - ApprovalsStats::new( - HashSet::from_iter(vec![ - ValidatorIndex(0), - ValidatorIndex(1), - ]), - Default::default(), - ), - ), - ]), - }), - (1 as SessionIndex, PerSessionView { - authorities_lookup: HashMap::new(), - finalized_approval_stats: HashMap::from_iter(vec![ - ( - candidate_hash_e.clone(), - // expected approval stats from a given finalized session - // ignoring the stats collected for relay block B - ApprovalsStats::new( - HashSet::from_iter(vec![ - ValidatorIndex(3), - ValidatorIndex(1), - ValidatorIndex(0) - ]), - HashSet::from_iter(vec![ValidatorIndex(2)]) - ), - ), - ]), - }), + let expected_tallies = HashMap::from_iter(vec![ + ( + ValidatorIndex(0), + PerValidatorTally { + noshows: 1, + // validator 0 approvals increased from 1 to 2 + // as block D with more collected approvals + // was finalized + approvals: 2, + }, + ), + ( + ValidatorIndex(1), + PerValidatorTally { + noshows: 1, + approvals: 2, + }, + ), + ( + ValidatorIndex(2), + PerValidatorTally { + noshows: 0, + approvals: 2, + }, + ), + ( + ValidatorIndex(3), + PerValidatorTally { + noshows: 0, + approvals: 1, + }, + ), ]); - assert_eq!(view.per_session, expected_session_views); + assert_per_session_tallies(&view.per_session, 0, expected_tallies); + + let expected_tallies = HashMap::from_iter(vec![ + ( + ValidatorIndex(0), + PerValidatorTally { + noshows: 0, + approvals: 1, + }, + ), + ( + ValidatorIndex(1), + PerValidatorTally { + noshows: 0, + approvals: 1, + }, + ), + ( + ValidatorIndex(2), + PerValidatorTally { + noshows: 1, + approvals: 0, + }, + ), + ( + ValidatorIndex(3), + PerValidatorTally { + noshows: 0, + approvals: 1, + }, + ), + ]); + + assert_per_session_tallies(&view.per_session, 1, expected_tallies); } fn assert_roots_and_relay_views( @@ -775,4 +792,23 @@ fn assert_roots_and_relay_views( assert!(rb_view.children.contains(child)); } } +} + +fn assert_per_session_tallies( + per_session_view: &HashMap, + session_idx: SessionIndex, + expected_tallies: HashMap, +) { + let session_view = per_session_view + .get(&session_idx) + .expect("session index should exists in the view"); + + assert_eq!(session_view.validators_tallies.len(), expected_tallies.len()); + for (validator_index, expected_tally) in expected_tallies.iter() { + assert_eq!( + session_view.validators_tallies.get(validator_index), + Some(expected_tally), + "unexpected value for validator index {:?}", validator_index + ); + } } \ No newline at end of file From 2016c0c95de0401cfa609de9d9c0df6b7820347e Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Thu, 30 Oct 2025 10:09:20 -0400 Subject: [PATCH 31/48] chore: including on bench + debug logs for session collected stats --- Cargo.lock | 1 + .../consensus-statistics-collector/src/lib.rs | 24 +++++++++++++++++-- .../src/metrics.rs | 7 +++--- polkadot/node/subsystem-bench/Cargo.toml | 1 + .../subsystem-bench/src/lib/approval/mod.rs | 10 +++++++- 5 files changed, 36 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 896f5fc76f7e6..9f5299ac08084 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17228,6 +17228,7 @@ dependencies = [ "polkadot-node-core-approval-voting", "polkadot-node-core-approval-voting-parallel", "polkadot-node-core-av-store", + "polkadot-node-core-consensus-statistics-collector", "polkadot-node-core-dispute-coordinator", "polkadot-node-metrics", "polkadot-node-network-protocol", diff --git a/polkadot/node/core/consensus-statistics-collector/src/lib.rs b/polkadot/node/core/consensus-statistics-collector/src/lib.rs index 04b6f6278d150..55645e4681f82 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/lib.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/lib.rs @@ -39,11 +39,11 @@ use crate::{ }; mod error; -mod metrics; #[cfg(test)] mod tests; mod approval_voting_metrics; mod availability_distribution_metrics; +pub mod metrics; use approval_voting_metrics::ApprovalsStats; use polkadot_node_subsystem_util::{request_candidate_events, request_session_index_for_child, request_session_info}; @@ -227,7 +227,7 @@ pub(crate) async fn run_iteration( } }, FromOrchestra::Signal(OverseerSignal::BlockFinalized(fin_block_hash, _)) => { - // as soon a block is finalized it performs: + // when a block is finalized it performs: // 1. Pruning unneeded forks // 2. Collected statistics that belongs to the finalized chain // 3. After collection of finalized statistics then remove finalized @@ -266,6 +266,8 @@ pub(crate) async fn run_iteration( } } } + + log_session_view_general_stats(view); } FromOrchestra::Communication { msg } => { match msg { @@ -390,4 +392,22 @@ fn prune_unfinalised_forks(view: &mut View, fin_block_hash: Hash) -> Vec { } retain_relay_hashes +} + +fn log_session_view_general_stats(view: &View) { + for (session_index, session_view) in &view.per_session { + let session_tally = session_view + .validators_tallies + .values() + .map(|tally| (tally.approvals, tally.noshows)) + .fold((0, 0), |acc, (approvals, noshows)| (acc.0 + approvals, acc.1 + noshows)); + + gum::debug!( + target: LOG_TARGET, + session_idx = ?session_index, + approvals = ?session_tally.0, + noshows = ?session_tally.1, + "current session collected statistics" + ); + } } \ No newline at end of file diff --git a/polkadot/node/core/consensus-statistics-collector/src/metrics.rs b/polkadot/node/core/consensus-statistics-collector/src/metrics.rs index 9c7eebf9ea876..b0d4a95464682 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/metrics.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/metrics.rs @@ -38,23 +38,22 @@ pub(crate) struct MetricsInner { #[derive(Default, Clone)] pub struct Metrics(pub(crate) Option); - impl Metrics { pub fn record_approvals_stats(&self, session: SessionIndex, approval_stats: HashMap) { self.0.as_ref().map(|metrics| { - for (candidate_hash, stats) in approval_stats { + for stats in approval_stats.values() { metrics.approvals_usage_per_session.with_label_values( &[session.to_string().as_str()]).inc_by(stats.votes.len() as u64); metrics.no_shows_per_session.with_label_values( &[session.to_string().as_str()]).inc_by(stats.no_shows.len() as u64); - for validator in stats.votes { + for validator in &stats.votes { metrics.approvals_per_session_per_validator.with_label_values( &[session.to_string().as_str(), validator.0.to_string().as_str()]).inc() } - for validator in stats.no_shows { + for validator in &stats.no_shows { metrics.no_shows_per_session_per_validator.with_label_values( &[session.to_string().as_str(), validator.0.to_string().as_str()]).inc() } diff --git a/polkadot/node/subsystem-bench/Cargo.toml b/polkadot/node/subsystem-bench/Cargo.toml index 99ece70d02a02..9f31c36456b9a 100644 --- a/polkadot/node/subsystem-bench/Cargo.toml +++ b/polkadot/node/subsystem-bench/Cargo.toml @@ -36,6 +36,7 @@ polkadot-availability-recovery = { features = ["subsystem-benchmarks"], workspac polkadot-dispute-distribution = { workspace = true, default-features = true } polkadot-node-core-av-store = { workspace = true, default-features = true } polkadot-node-core-dispute-coordinator = { workspace = true, default-features = true } +polkadot-node-core-consensus-statistics-collector = { workspace = true, default-features = true } polkadot-node-network-protocol = { workspace = true, default-features = true } polkadot-node-primitives = { workspace = true, default-features = true } polkadot-node-subsystem = { workspace = true, default-features = true } diff --git a/polkadot/node/subsystem-bench/src/lib/approval/mod.rs b/polkadot/node/subsystem-bench/src/lib/approval/mod.rs index f4dfa47ff7621..16fe922b8a228 100644 --- a/polkadot/node/subsystem-bench/src/lib/approval/mod.rs +++ b/polkadot/node/subsystem-bench/src/lib/approval/mod.rs @@ -54,6 +54,10 @@ use polkadot_node_primitives::approval::time::{ slot_number_to_tick, tick_to_slot_number, Clock, ClockExt, SystemClock, }; +use polkadot_node_core_consensus_statistics_collector::{ + ConsensusStatisticsCollector as ConsensusStatisticsCollectorSubsystem, + metrics::Metrics as ConsensusStatisticsMetrics, +}; use polkadot_node_core_approval_voting::{ ApprovalVotingSubsystem, Config as ApprovalVotingConfig, RealAssignmentCriteria, }; @@ -853,6 +857,9 @@ fn build_overseer( let mock_rx_bridge = MockNetworkBridgeRx::new(network_receiver, None); let overseer_metrics = OverseerMetrics::try_register(&dependencies.registry).unwrap(); let task_handle = spawn_task_handle.clone(); + + let collector_subsystem = ConsensusStatisticsCollectorSubsystem::default(); + let dummy = dummy_builder!(task_handle, overseer_metrics) .replace_chain_api(|_| mock_chain_api) .replace_chain_selection(|_| mock_chain_selection) @@ -860,7 +867,8 @@ fn build_overseer( .replace_network_bridge_tx(|_| mock_tx_bridge) .replace_network_bridge_rx(|_| mock_rx_bridge) .replace_availability_recovery(|_| MockAvailabilityRecovery::new()) - .replace_candidate_validation(|_| MockCandidateValidation::new()); + .replace_candidate_validation(|_| MockCandidateValidation::new()) + .replace_consensus_statistics_collector(|_| collector_subsystem); let (overseer, raw_handle) = if state.options.approval_voting_parallel_enabled { let approval_voting_parallel = ApprovalVotingParallelSubsystem::with_config_and_clock( From 2161ced6d180f045e5425776d658298a7204c737 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Thu, 30 Oct 2025 11:09:02 -0400 Subject: [PATCH 32/48] make per validator prometheus metrics optional --- .../src/approval_voting_metrics.rs | 1 - .../consensus-statistics-collector/src/lib.rs | 62 +++++++++++++++---- .../src/metrics.rs | 26 +++++--- polkadot/node/service/src/overseer.rs | 2 +- 4 files changed, 69 insertions(+), 22 deletions(-) diff --git a/polkadot/node/core/consensus-statistics-collector/src/approval_voting_metrics.rs b/polkadot/node/core/consensus-statistics-collector/src/approval_voting_metrics.rs index 181beb6935048..f8392d5a9a5f9 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/approval_voting_metrics.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/approval_voting_metrics.rs @@ -36,7 +36,6 @@ pub fn handle_candidate_approved( block_hash: Hash, candidate_hash: CandidateHash, approvals: Vec, - metrics: &Metrics, ) { if let Some(relay_view) = view.per_relay.get_mut(&block_hash) { relay_view.approvals_stats diff --git a/polkadot/node/core/consensus-statistics-collector/src/lib.rs b/polkadot/node/core/consensus-statistics-collector/src/lib.rs index 55645e4681f82..3cef365473f48 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/lib.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/lib.rs @@ -51,6 +51,7 @@ use crate::approval_voting_metrics::{handle_candidate_approved, handle_observed_ use crate::availability_distribution_metrics::{handle_chunk_uploaded, handle_chunks_downloaded, AvailabilityChunks}; use self::metrics::Metrics; +const MAX_SESSIONS_TO_KEEP: u32 = 2; const LOG_TARGET: &str = "parachain::consensus-statistics-collector"; struct PerRelayView { @@ -108,7 +109,7 @@ struct View { per_relay: HashMap, per_session: HashMap, availability_chunks: HashMap, - + current_session: Option, } impl View { @@ -117,7 +118,8 @@ impl View { roots: HashSet::new(), per_relay: HashMap::new(), per_session: HashMap::new(), - availability_chunks: HashMap::new() + availability_chunks: HashMap::new(), + current_session: None, }; } } @@ -126,12 +128,16 @@ impl View { #[derive(Default)] pub struct ConsensusStatisticsCollector { metrics: Metrics, + per_validator_metrics: bool } impl ConsensusStatisticsCollector { /// Create a new instance of the `ConsensusStatisticsCollector`. - pub fn new(metrics: Metrics) -> Self { - Self { metrics } + pub fn new(metrics: Metrics, per_validator_metrics: bool) -> Self { + Self { + metrics, + per_validator_metrics, + } } } @@ -142,7 +148,7 @@ where { fn start(self, ctx: Context) -> SpawnedSubsystem { SpawnedSubsystem { - future: run(ctx, self.metrics) + future: run(ctx, (self.metrics, self.per_validator_metrics)) .map_err(|e| SubsystemError::with_origin("statistics-parachains", e)) .boxed(), name: "consensus-statistics-collector-subsystem", @@ -151,11 +157,11 @@ where } #[overseer::contextbounds(ConsensusStatisticsCollector, prefix = self::overseer)] -async fn run(mut ctx: Context, metrics: Metrics) -> FatalResult<()> { +async fn run(mut ctx: Context, metrics: (Metrics, bool)) -> FatalResult<()> { let mut view = View::new(); loop { crate::error::log_error( - run_iteration(&mut ctx, &mut view, &metrics).await, + run_iteration(&mut ctx, &mut view, (&metrics.0, metrics.1)).await, "Encountered issue during run iteration", )?; } @@ -165,9 +171,10 @@ async fn run(mut ctx: Context, metrics: Metrics) -> FatalResult<()> { pub(crate) async fn run_iteration( ctx: &mut Context, view: &mut View, - metrics: &Metrics, + metrics: (&Metrics, bool), ) -> Result<()> { let mut sender = ctx.sender().clone(); + let per_validator_metrics = metrics.1; loop { match ctx.recv().await.map_err(FatalError::SubsystemReceive)? { FromOrchestra::Signal(OverseerSignal::Conclude) => return Ok(()), @@ -242,7 +249,11 @@ pub(crate) async fn run_iteration( .map(|rb_view| (rb_view.session_index, rb_view.approvals_stats)) { if let Some(session_view) = view.per_session.get_mut(&session_idx) { - metrics.record_approvals_stats(session_idx, approvals_stats.clone()); + metrics.0.record_approvals_stats( + session_idx, + approvals_stats.clone(), + per_validator_metrics, + ); for stats in approvals_stats.values() { // Increment no-show tallies @@ -268,6 +279,7 @@ pub(crate) async fn run_iteration( } log_session_view_general_stats(view); + prune_old_session_views(ctx, view, fin_block_hash).await?; } FromOrchestra::Communication { msg } => { match msg { @@ -303,7 +315,6 @@ pub(crate) async fn run_iteration( block_hash, candidate_hash, approvals, - metrics, ); } ConsensusStatisticsCollectorMessage::NoShows( @@ -394,6 +405,35 @@ fn prune_unfinalised_forks(view: &mut View, fin_block_hash: Hash) -> Vec { retain_relay_hashes } +// prune_old_session_views avoid the per_session mapping to grow +// indefinitely by removing sessions stored for more than MAX_SESSIONS_TO_KEEP (2) +// finalized sessions. +#[overseer::contextbounds(ConsensusStatisticsCollector, prefix = self::overseer)] +async fn prune_old_session_views( + ctx: &mut Context, + view: &mut View, + finalized_hash: Hash, +) -> Result<()> { + let session_idx = request_session_index_for_child(finalized_hash, ctx.sender()) + .await + .await + .map_err(JfyiError::OverseerCommunication)? + .map_err(JfyiError::RuntimeApiCallError)?; + + match view.current_session { + Some(current_session) if current_session < session_idx => { + if let Some(wipe_before) = session_idx.checked_sub(MAX_SESSIONS_TO_KEEP) { + view.per_session.retain(|stored_session_index, _| *stored_session_index > wipe_before); + } + view.current_session = Some(session_idx) + } + None => view.current_session = Some(session_idx), + _ => {} + }; + + Ok(()) +} + fn log_session_view_general_stats(view: &View) { for (session_index, session_view) in &view.per_session { let session_tally = session_view @@ -407,7 +447,7 @@ fn log_session_view_general_stats(view: &View) { session_idx = ?session_index, approvals = ?session_tally.0, noshows = ?session_tally.1, - "current session collected statistics" + "session collected statistics" ); } } \ No newline at end of file diff --git a/polkadot/node/core/consensus-statistics-collector/src/metrics.rs b/polkadot/node/core/consensus-statistics-collector/src/metrics.rs index b0d4a95464682..8f186d43a03db 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/metrics.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/metrics.rs @@ -36,10 +36,15 @@ pub(crate) struct MetricsInner { /// Candidate backing metrics. #[derive(Default, Clone)] -pub struct Metrics(pub(crate) Option); +pub struct Metrics (pub(crate) Option); impl Metrics { - pub fn record_approvals_stats(&self, session: SessionIndex, approval_stats: HashMap) { + pub fn record_approvals_stats( + &self, + session: SessionIndex, + approval_stats: HashMap, + per_validator_metrics: bool, + ) { self.0.as_ref().map(|metrics| { for stats in approval_stats.values() { metrics.approvals_usage_per_session.with_label_values( @@ -48,14 +53,16 @@ impl Metrics { metrics.no_shows_per_session.with_label_values( &[session.to_string().as_str()]).inc_by(stats.no_shows.len() as u64); - for validator in &stats.votes { - metrics.approvals_per_session_per_validator.with_label_values( - &[session.to_string().as_str(), validator.0.to_string().as_str()]).inc() - } + if per_validator_metrics { + for validator in &stats.votes { + metrics.approvals_per_session_per_validator.with_label_values( + &[session.to_string().as_str(), validator.0.to_string().as_str()]).inc() + } - for validator in &stats.no_shows { - metrics.no_shows_per_session_per_validator.with_label_values( - &[session.to_string().as_str(), validator.0.to_string().as_str()]).inc() + for validator in &stats.no_shows { + metrics.no_shows_per_session_per_validator.with_label_values( + &[session.to_string().as_str(), validator.0.to_string().as_str()]).inc() + } } } }); @@ -106,6 +113,7 @@ impl metrics::Metrics for Metrics { registry, )?, }; + Ok(Metrics(Some(metrics))) } } diff --git a/polkadot/node/service/src/overseer.rs b/polkadot/node/service/src/overseer.rs index febe108084c31..3e62824194e06 100644 --- a/polkadot/node/service/src/overseer.rs +++ b/polkadot/node/service/src/overseer.rs @@ -355,7 +355,7 @@ where )) .chain_selection(ChainSelectionSubsystem::new(chain_selection_config, parachains_db)) .prospective_parachains(ProspectiveParachainsSubsystem::new(Metrics::register(registry)?)) - .consensus_statistics_collector(ConsensusStatisticsCollector::new(Metrics::register(registry)?)) + .consensus_statistics_collector(ConsensusStatisticsCollector::new(Metrics::register(registry)?, true)) .activation_external_listeners(Default::default()) .active_leaves(Default::default()) .supports_parachains(runtime_client) From 880feabd4871191f6923b60ad9b7b35be1ce8868 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Mon, 3 Nov 2025 14:27:52 -0400 Subject: [PATCH 33/48] chore: make publish_per_validator_approval_metrics a cli flag --- .../relay-chain-inprocess-interface/src/lib.rs | 1 + polkadot/cli/src/cli.rs | 5 +++++ polkadot/cli/src/command.rs | 4 ++++ .../core/consensus-statistics-collector/src/lib.rs | 13 +++++++++---- polkadot/node/service/src/builder/mod.rs | 10 ++++++++++ polkadot/node/service/src/overseer.rs | 8 +++++++- .../rewards_statistics_mixed_validators.rs | 3 +-- 7 files changed, 37 insertions(+), 7 deletions(-) diff --git a/cumulus/client/relay-chain-inprocess-interface/src/lib.rs b/cumulus/client/relay-chain-inprocess-interface/src/lib.rs index 50ee9c1219d91..a1acc6441ec93 100644 --- a/cumulus/client/relay-chain-inprocess-interface/src/lib.rs +++ b/cumulus/client/relay-chain-inprocess-interface/src/lib.rs @@ -419,6 +419,7 @@ fn build_polkadot_full_node( keep_finalized_for: None, invulnerable_ah_collators: HashSet::new(), collator_protocol_hold_off: None, + publish_per_validator_approval_metrics: false, }; let (relay_chain_full_node, paranode_req_receiver) = match config.network.network_backend { diff --git a/polkadot/cli/src/cli.rs b/polkadot/cli/src/cli.rs index fa8595cc7c57c..df02c70b642ac 100644 --- a/polkadot/cli/src/cli.rs +++ b/polkadot/cli/src/cli.rs @@ -166,6 +166,11 @@ pub struct RunCmd { /// **Dangerous!** Do not touch unless explicitly advised to. #[arg(long, hide = true)] pub collator_protocol_hold_off: Option, + + /// Enable or disable per validator collected approvals metrics + /// to be published to prometheus. If not specified, set to false. + #[arg(long)] + pub publish_per_validator_approval_metrics: Option, } #[allow(missing_docs)] diff --git a/polkadot/cli/src/command.rs b/polkadot/cli/src/command.rs index b9366d07f6c54..1ef83de065928 100644 --- a/polkadot/cli/src/command.rs +++ b/polkadot/cli/src/command.rs @@ -278,6 +278,10 @@ where telemetry_worker_handle: None, node_version, secure_validator_mode, + publish_per_validator_approval_metrics: cli + .run + .publish_per_validator_approval_metrics + .unwrap_or(false), workers_path: cli.run.workers_path, workers_names: None, overseer_gen, diff --git a/polkadot/node/core/consensus-statistics-collector/src/lib.rs b/polkadot/node/core/consensus-statistics-collector/src/lib.rs index 3cef365473f48..571a224d8bee3 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/lib.rs +++ b/polkadot/node/core/consensus-statistics-collector/src/lib.rs @@ -54,6 +54,11 @@ use self::metrics::Metrics; const MAX_SESSIONS_TO_KEEP: u32 = 2; const LOG_TARGET: &str = "parachain::consensus-statistics-collector"; +#[derive(Default)] +pub struct Config { + pub publish_per_validator_approval_metrics: bool +} + struct PerRelayView { session_index: SessionIndex, parent_hash: Option, @@ -128,15 +133,15 @@ impl View { #[derive(Default)] pub struct ConsensusStatisticsCollector { metrics: Metrics, - per_validator_metrics: bool + config: Config } impl ConsensusStatisticsCollector { /// Create a new instance of the `ConsensusStatisticsCollector`. - pub fn new(metrics: Metrics, per_validator_metrics: bool) -> Self { + pub fn new(metrics: Metrics, config: Config) -> Self { Self { metrics, - per_validator_metrics, + config, } } } @@ -148,7 +153,7 @@ where { fn start(self, ctx: Context) -> SpawnedSubsystem { SpawnedSubsystem { - future: run(ctx, (self.metrics, self.per_validator_metrics)) + future: run(ctx, (self.metrics, self.config.publish_per_validator_approval_metrics)) .map_err(|e| SubsystemError::with_origin("statistics-parachains", e)) .boxed(), name: "consensus-statistics-collector-subsystem", diff --git a/polkadot/node/service/src/builder/mod.rs b/polkadot/node/service/src/builder/mod.rs index 9d52617cc8da9..00b7b10fadd5e 100644 --- a/polkadot/node/service/src/builder/mod.rs +++ b/polkadot/node/service/src/builder/mod.rs @@ -41,6 +41,7 @@ use polkadot_node_core_chain_selection::{ self as chain_selection_subsystem, Config as ChainSelectionConfig, }; use polkadot_node_core_dispute_coordinator::Config as DisputeCoordinatorConfig; +use polkadot_node_core_consensus_statistics_collector::Config as ConsensusStatisticsCollectorConfig; use polkadot_node_network_protocol::{ peer_set::{PeerSet, PeerSetProtocolNames}, request_response::{IncomingRequest, ReqProtocolNames}, @@ -62,6 +63,7 @@ use std::{ sync::Arc, time::Duration, }; +use polkadot_node_core_consensus_statistics_collector::ConsensusStatisticsCollector; /// Polkadot node service initialization parameters. pub struct NewFullParams { @@ -76,6 +78,8 @@ pub struct NewFullParams { pub node_version: Option, /// Whether the node is attempting to run as a secure validator. pub secure_validator_mode: bool, + /// Whether the node will publish collected approval metrics per validator + pub publish_per_validator_approval_metrics: bool, /// An optional path to a directory containing the workers. pub workers_path: Option, /// Optional custom names for the prepare and execute workers. @@ -197,6 +201,7 @@ where telemetry_worker_handle: _, node_version, secure_validator_mode, + publish_per_validator_approval_metrics, workers_path, workers_names, overseer_gen, @@ -436,6 +441,10 @@ where }, }; + let consensus_statistics_collector_config = ConsensusStatisticsCollectorConfig{ + publish_per_validator_approval_metrics, + }; + Some(ExtendedOverseerGenArgs { keystore: keystore_container.local_keystore(), parachains_db, @@ -452,6 +461,7 @@ where fetch_chunks_threshold, invulnerable_ah_collators, collator_protocol_hold_off, + consensus_statistics_collector_config, }) }; diff --git a/polkadot/node/service/src/overseer.rs b/polkadot/node/service/src/overseer.rs index 3e62824194e06..aa0f99c157301 100644 --- a/polkadot/node/service/src/overseer.rs +++ b/polkadot/node/service/src/overseer.rs @@ -21,6 +21,7 @@ use sp_core::traits::SpawnNamed; use polkadot_availability_distribution::IncomingRequestReceivers; use polkadot_node_core_approval_voting::Config as ApprovalVotingConfig; +use polkadot_node_core_consensus_statistics_collector::Config as ConsensusStatisticsCollectorConfig; use polkadot_node_core_av_store::Config as AvailabilityConfig; use polkadot_node_core_candidate_validation::Config as CandidateValidationConfig; use polkadot_node_core_chain_selection::Config as ChainSelectionConfig; @@ -134,6 +135,7 @@ pub struct ExtendedOverseerGenArgs { pub candidate_req_v2_receiver: IncomingRequestReceiver, /// Configuration for the approval voting subsystem. pub approval_voting_config: ApprovalVotingConfig, + pub consensus_statistics_collector_config: ConsensusStatisticsCollectorConfig, /// Receiver for incoming disputes. pub dispute_req_receiver: IncomingRequestReceiver, /// Configuration for the dispute coordinator subsystem. @@ -184,6 +186,7 @@ pub fn validator_overseer_builder( fetch_chunks_threshold, invulnerable_ah_collators, collator_protocol_hold_off, + consensus_statistics_collector_config, }: ExtendedOverseerGenArgs, ) -> Result< InitializedOverseerBuilder< @@ -355,7 +358,10 @@ where )) .chain_selection(ChainSelectionSubsystem::new(chain_selection_config, parachains_db)) .prospective_parachains(ProspectiveParachainsSubsystem::new(Metrics::register(registry)?)) - .consensus_statistics_collector(ConsensusStatisticsCollector::new(Metrics::register(registry)?, true)) + .consensus_statistics_collector(ConsensusStatisticsCollector::new( + Metrics::register(registry)?, + consensus_statistics_collector_config, + )) .activation_external_listeners(Default::default()) .active_leaves(Default::default()) .supports_parachains(runtime_client) diff --git a/polkadot/zombienet-sdk-tests/tests/functional/rewards_statistics_mixed_validators.rs b/polkadot/zombienet-sdk-tests/tests/functional/rewards_statistics_mixed_validators.rs index c00faeb7db396..3d92412812416 100644 --- a/polkadot/zombienet-sdk-tests/tests/functional/rewards_statistics_mixed_validators.rs +++ b/polkadot/zombienet-sdk-tests/tests/functional/rewards_statistics_mixed_validators.rs @@ -51,7 +51,6 @@ async fn rewards_statistics_mixed_validators_test() -> Result<(), anyhow::Error> node.with_name(&format!("malus-{i}")) .with_args(vec![ "-lparachain=debug,MALUS=trace".into(), - "--dispute-offset=14".into(), "--alice".into(), "--insecure-validator-i-know-what-i-do".into(), ]) @@ -61,7 +60,7 @@ async fn rewards_statistics_mixed_validators_test() -> Result<(), anyhow::Error> .as_str(), ) .with_command("malus") - .with_subcommand("dispute_valid_candidates") + .with_subcommand("dispute-valid-candidates") .invulnerable(false) }) }) From 87b3ccc1efdecac51bd8a145ce5aee3a12406571 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Mon, 10 Nov 2025 10:53:45 -0400 Subject: [PATCH 34/48] chore: renaming subsystem --- Cargo.lock | 44 +++++++++---------- Cargo.toml | 4 +- .../Cargo.toml | 2 +- .../src/approval_voting_metrics.rs | 0 .../src/availability_distribution_metrics.rs | 0 .../src/error.rs | 0 .../src/lib.rs | 10 ++--- .../src/metrics.rs | 0 .../src/tests.rs | 0 polkadot/node/service/Cargo.toml | 4 +- polkadot/node/service/src/builder/mod.rs | 8 ++-- polkadot/node/service/src/overseer.rs | 10 ++--- polkadot/node/subsystem-bench/Cargo.toml | 2 +- .../subsystem-bench/src/lib/approval/mod.rs | 6 +-- .../adder/collator/src/main.rs | 1 + .../rewards_statistics_mixed_validators.rs | 27 ++++-------- 16 files changed, 56 insertions(+), 62 deletions(-) rename polkadot/node/core/{consensus-statistics-collector => rewards-statistics-collector}/Cargo.toml (95%) rename polkadot/node/core/{consensus-statistics-collector => rewards-statistics-collector}/src/approval_voting_metrics.rs (100%) rename polkadot/node/core/{consensus-statistics-collector => rewards-statistics-collector}/src/availability_distribution_metrics.rs (100%) rename polkadot/node/core/{consensus-statistics-collector => rewards-statistics-collector}/src/error.rs (100%) rename polkadot/node/core/{consensus-statistics-collector => rewards-statistics-collector}/src/lib.rs (98%) rename polkadot/node/core/{consensus-statistics-collector => rewards-statistics-collector}/src/metrics.rs (100%) rename polkadot/node/core/{consensus-statistics-collector => rewards-statistics-collector}/src/tests.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 9f5299ac08084..af8b2716296e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15708,26 +15708,6 @@ dependencies = [ "tracing-gum", ] -[[package]] -name = "polkadot-node-core-consensus-statistics-collector" -version = "6.0.0" -dependencies = [ - "assert_matches", - "fatality", - "futures", - "polkadot-node-primitives", - "polkadot-node-subsystem", - "polkadot-node-subsystem-test-helpers", - "polkadot-node-subsystem-util", - "polkadot-primitives", - "sp-application-crypto", - "sp-authority-discovery", - "sp-core 28.0.0", - "sp-tracing 16.0.0", - "thiserror 1.0.65", - "tracing-gum", -] - [[package]] name = "polkadot-node-core-dispute-coordinator" version = "7.0.0" @@ -15940,6 +15920,26 @@ dependencies = [ "tracing-gum", ] +[[package]] +name = "polkadot-node-core-rewards-statistics-collector" +version = "6.0.0" +dependencies = [ + "assert_matches", + "fatality", + "futures", + "polkadot-node-primitives", + "polkadot-node-subsystem", + "polkadot-node-subsystem-test-helpers", + "polkadot-node-subsystem-util", + "polkadot-primitives", + "sp-application-crypto", + "sp-authority-discovery", + "sp-core 28.0.0", + "sp-tracing 16.0.0", + "thiserror 1.0.65", + "tracing-gum", +] + [[package]] name = "polkadot-node-core-runtime-api" version = "7.0.0" @@ -17084,13 +17084,13 @@ dependencies = [ "polkadot-node-core-candidate-validation", "polkadot-node-core-chain-api", "polkadot-node-core-chain-selection", - "polkadot-node-core-consensus-statistics-collector", "polkadot-node-core-dispute-coordinator", "polkadot-node-core-parachains-inherent", "polkadot-node-core-prospective-parachains", "polkadot-node-core-provisioner", "polkadot-node-core-pvf", "polkadot-node-core-pvf-checker", + "polkadot-node-core-rewards-statistics-collector", "polkadot-node-core-runtime-api", "polkadot-node-network-protocol", "polkadot-node-primitives", @@ -17228,8 +17228,8 @@ dependencies = [ "polkadot-node-core-approval-voting", "polkadot-node-core-approval-voting-parallel", "polkadot-node-core-av-store", - "polkadot-node-core-consensus-statistics-collector", "polkadot-node-core-dispute-coordinator", + "polkadot-node-core-rewards-statistics-collector", "polkadot-node-metrics", "polkadot-node-network-protocol", "polkadot-node-primitives", diff --git a/Cargo.toml b/Cargo.toml index 6dc276f927edb..18436079da37c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -180,7 +180,7 @@ members = [ "polkadot/node/core/dispute-coordinator", "polkadot/node/core/parachains-inherent", "polkadot/node/core/prospective-parachains", - "polkadot/node/core/consensus-statistics-collector", + "polkadot/node/core/rewards-statistics-collector", "polkadot/node/core/provisioner", "polkadot/node/core/pvf", "polkadot/node/core/pvf-checker", @@ -1141,7 +1141,7 @@ polkadot-node-core-chain-selection = { path = "polkadot/node/core/chain-selectio polkadot-node-core-dispute-coordinator = { path = "polkadot/node/core/dispute-coordinator", default-features = false } polkadot-node-core-parachains-inherent = { path = "polkadot/node/core/parachains-inherent", default-features = false } polkadot-node-core-prospective-parachains = { path = "polkadot/node/core/prospective-parachains", default-features = false } -polkadot-node-core-consensus-statistics-collector = { path = "polkadot/node/core/consensus-statistics-collector", default-features = false } +polkadot-node-core-rewards-statistics-collector = { path = "polkadot/node/core/rewards-statistics-collector", default-features = false } polkadot-node-core-provisioner = { path = "polkadot/node/core/provisioner", default-features = false } polkadot-node-core-pvf = { path = "polkadot/node/core/pvf", default-features = false } polkadot-node-core-pvf-checker = { path = "polkadot/node/core/pvf-checker", default-features = false } diff --git a/polkadot/node/core/consensus-statistics-collector/Cargo.toml b/polkadot/node/core/rewards-statistics-collector/Cargo.toml similarity index 95% rename from polkadot/node/core/consensus-statistics-collector/Cargo.toml rename to polkadot/node/core/rewards-statistics-collector/Cargo.toml index e710cd0b5cb42..311150218e99c 100644 --- a/polkadot/node/core/consensus-statistics-collector/Cargo.toml +++ b/polkadot/node/core/rewards-statistics-collector/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "polkadot-node-core-consensus-statistics-collector" +name = "polkadot-node-core-rewards-statistics-collector" version = "6.0.0" authors.workspace = true edition.workspace = true diff --git a/polkadot/node/core/consensus-statistics-collector/src/approval_voting_metrics.rs b/polkadot/node/core/rewards-statistics-collector/src/approval_voting_metrics.rs similarity index 100% rename from polkadot/node/core/consensus-statistics-collector/src/approval_voting_metrics.rs rename to polkadot/node/core/rewards-statistics-collector/src/approval_voting_metrics.rs diff --git a/polkadot/node/core/consensus-statistics-collector/src/availability_distribution_metrics.rs b/polkadot/node/core/rewards-statistics-collector/src/availability_distribution_metrics.rs similarity index 100% rename from polkadot/node/core/consensus-statistics-collector/src/availability_distribution_metrics.rs rename to polkadot/node/core/rewards-statistics-collector/src/availability_distribution_metrics.rs diff --git a/polkadot/node/core/consensus-statistics-collector/src/error.rs b/polkadot/node/core/rewards-statistics-collector/src/error.rs similarity index 100% rename from polkadot/node/core/consensus-statistics-collector/src/error.rs rename to polkadot/node/core/rewards-statistics-collector/src/error.rs diff --git a/polkadot/node/core/consensus-statistics-collector/src/lib.rs b/polkadot/node/core/rewards-statistics-collector/src/lib.rs similarity index 98% rename from polkadot/node/core/consensus-statistics-collector/src/lib.rs rename to polkadot/node/core/rewards-statistics-collector/src/lib.rs index 571a224d8bee3..a26e180ee9aaf 100644 --- a/polkadot/node/core/consensus-statistics-collector/src/lib.rs +++ b/polkadot/node/core/rewards-statistics-collector/src/lib.rs @@ -52,7 +52,7 @@ use crate::availability_distribution_metrics::{handle_chunk_uploaded, handle_chu use self::metrics::Metrics; const MAX_SESSIONS_TO_KEEP: u32 = 2; -const LOG_TARGET: &str = "parachain::consensus-statistics-collector"; +const LOG_TARGET: &str = "parachain::rewards-statistics-collector"; #[derive(Default)] pub struct Config { @@ -131,12 +131,12 @@ impl View { /// The statistics collector subsystem. #[derive(Default)] -pub struct ConsensusStatisticsCollector { +pub struct RewardsStatisticsCollector { metrics: Metrics, config: Config } -impl ConsensusStatisticsCollector { +impl RewardsStatisticsCollector { /// Create a new instance of the `ConsensusStatisticsCollector`. pub fn new(metrics: Metrics, config: Config) -> Self { Self { @@ -147,7 +147,7 @@ impl ConsensusStatisticsCollector { } #[overseer::subsystem(ConsensusStatisticsCollector, error = SubsystemError, prefix = self::overseer)] -impl ConsensusStatisticsCollector +impl RewardsStatisticsCollector where Context: Send + Sync, { @@ -156,7 +156,7 @@ where future: run(ctx, (self.metrics, self.config.publish_per_validator_approval_metrics)) .map_err(|e| SubsystemError::with_origin("statistics-parachains", e)) .boxed(), - name: "consensus-statistics-collector-subsystem", + name: "rewards-statistics-collector-subsystem", } } } diff --git a/polkadot/node/core/consensus-statistics-collector/src/metrics.rs b/polkadot/node/core/rewards-statistics-collector/src/metrics.rs similarity index 100% rename from polkadot/node/core/consensus-statistics-collector/src/metrics.rs rename to polkadot/node/core/rewards-statistics-collector/src/metrics.rs diff --git a/polkadot/node/core/consensus-statistics-collector/src/tests.rs b/polkadot/node/core/rewards-statistics-collector/src/tests.rs similarity index 100% rename from polkadot/node/core/consensus-statistics-collector/src/tests.rs rename to polkadot/node/core/rewards-statistics-collector/src/tests.rs diff --git a/polkadot/node/service/Cargo.toml b/polkadot/node/service/Cargo.toml index 68b73a6c6553d..b6de3790b7e86 100644 --- a/polkadot/node/service/Cargo.toml +++ b/polkadot/node/service/Cargo.toml @@ -127,7 +127,7 @@ polkadot-node-core-chain-api = { optional = true, workspace = true, default-feat polkadot-node-core-chain-selection = { optional = true, workspace = true, default-features = true } polkadot-node-core-dispute-coordinator = { optional = true, workspace = true, default-features = true } polkadot-node-core-prospective-parachains = { optional = true, workspace = true, default-features = true } -polkadot-node-core-consensus-statistics-collector = { optional = true, workspace = true, default-features = true } +polkadot-node-core-rewards-statistics-collector = { optional = true, workspace = true, default-features = true } polkadot-node-core-provisioner = { optional = true, workspace = true, default-features = true } polkadot-node-core-pvf = { optional = true, workspace = true, default-features = true } polkadot-node-core-pvf-checker = { optional = true, workspace = true, default-features = true } @@ -170,7 +170,7 @@ full-node = [ "polkadot-node-core-chain-selection", "polkadot-node-core-dispute-coordinator", "polkadot-node-core-prospective-parachains", - "polkadot-node-core-consensus-statistics-collector", + "polkadot-node-core-rewards-statistics-collector", "polkadot-node-core-provisioner", "polkadot-node-core-pvf", "polkadot-node-core-pvf-checker", diff --git a/polkadot/node/service/src/builder/mod.rs b/polkadot/node/service/src/builder/mod.rs index 00b7b10fadd5e..2da0d1cac7e48 100644 --- a/polkadot/node/service/src/builder/mod.rs +++ b/polkadot/node/service/src/builder/mod.rs @@ -41,7 +41,10 @@ use polkadot_node_core_chain_selection::{ self as chain_selection_subsystem, Config as ChainSelectionConfig, }; use polkadot_node_core_dispute_coordinator::Config as DisputeCoordinatorConfig; -use polkadot_node_core_consensus_statistics_collector::Config as ConsensusStatisticsCollectorConfig; +use polkadot_node_core_rewards_statistics_collector::{ + Config as RewardsStatisticsCollectorConfig, + RewardsStatisticsCollector +}; use polkadot_node_network_protocol::{ peer_set::{PeerSet, PeerSetProtocolNames}, request_response::{IncomingRequest, ReqProtocolNames}, @@ -63,7 +66,6 @@ use std::{ sync::Arc, time::Duration, }; -use polkadot_node_core_consensus_statistics_collector::ConsensusStatisticsCollector; /// Polkadot node service initialization parameters. pub struct NewFullParams { @@ -441,7 +443,7 @@ where }, }; - let consensus_statistics_collector_config = ConsensusStatisticsCollectorConfig{ + let consensus_statistics_collector_config = RewardsStatisticsCollectorConfig{ publish_per_validator_approval_metrics, }; diff --git a/polkadot/node/service/src/overseer.rs b/polkadot/node/service/src/overseer.rs index aa0f99c157301..d2fcf880c7dae 100644 --- a/polkadot/node/service/src/overseer.rs +++ b/polkadot/node/service/src/overseer.rs @@ -21,7 +21,7 @@ use sp_core::traits::SpawnNamed; use polkadot_availability_distribution::IncomingRequestReceivers; use polkadot_node_core_approval_voting::Config as ApprovalVotingConfig; -use polkadot_node_core_consensus_statistics_collector::Config as ConsensusStatisticsCollectorConfig; +use polkadot_node_core_rewards_statistics_collector::Config as RewardsStatisticsCollectorConfig; use polkadot_node_core_av_store::Config as AvailabilityConfig; use polkadot_node_core_candidate_validation::Config as CandidateValidationConfig; use polkadot_node_core_chain_selection::Config as ChainSelectionConfig; @@ -78,7 +78,7 @@ pub use polkadot_node_core_provisioner::ProvisionerSubsystem; pub use polkadot_node_core_pvf_checker::PvfCheckerSubsystem; pub use polkadot_node_core_runtime_api::RuntimeApiSubsystem; pub use polkadot_statement_distribution::StatementDistributionSubsystem; -pub use polkadot_node_core_consensus_statistics_collector::ConsensusStatisticsCollector; +pub use polkadot_node_core_rewards_statistics_collector::RewardsStatisticsCollector; /// Arguments passed for overseer construction. pub struct OverseerGenArgs<'a, Spawner, RuntimeClient> @@ -135,7 +135,7 @@ pub struct ExtendedOverseerGenArgs { pub candidate_req_v2_receiver: IncomingRequestReceiver, /// Configuration for the approval voting subsystem. pub approval_voting_config: ApprovalVotingConfig, - pub consensus_statistics_collector_config: ConsensusStatisticsCollectorConfig, + pub consensus_statistics_collector_config: RewardsStatisticsCollectorConfig, /// Receiver for incoming disputes. pub dispute_req_receiver: IncomingRequestReceiver, /// Configuration for the dispute coordinator subsystem. @@ -224,7 +224,7 @@ pub fn validator_overseer_builder( DisputeDistributionSubsystem, ChainSelectionSubsystem, ProspectiveParachainsSubsystem, - ConsensusStatisticsCollector, + RewardsStatisticsCollector, >, Error, > @@ -358,7 +358,7 @@ where )) .chain_selection(ChainSelectionSubsystem::new(chain_selection_config, parachains_db)) .prospective_parachains(ProspectiveParachainsSubsystem::new(Metrics::register(registry)?)) - .consensus_statistics_collector(ConsensusStatisticsCollector::new( + .consensus_statistics_collector(RewardsStatisticsCollector::new( Metrics::register(registry)?, consensus_statistics_collector_config, )) diff --git a/polkadot/node/subsystem-bench/Cargo.toml b/polkadot/node/subsystem-bench/Cargo.toml index 9f31c36456b9a..d537916fdada8 100644 --- a/polkadot/node/subsystem-bench/Cargo.toml +++ b/polkadot/node/subsystem-bench/Cargo.toml @@ -36,7 +36,7 @@ polkadot-availability-recovery = { features = ["subsystem-benchmarks"], workspac polkadot-dispute-distribution = { workspace = true, default-features = true } polkadot-node-core-av-store = { workspace = true, default-features = true } polkadot-node-core-dispute-coordinator = { workspace = true, default-features = true } -polkadot-node-core-consensus-statistics-collector = { workspace = true, default-features = true } +polkadot-node-core-rewards-statistics-collector = { workspace = true, default-features = true } polkadot-node-network-protocol = { workspace = true, default-features = true } polkadot-node-primitives = { workspace = true, default-features = true } polkadot-node-subsystem = { workspace = true, default-features = true } diff --git a/polkadot/node/subsystem-bench/src/lib/approval/mod.rs b/polkadot/node/subsystem-bench/src/lib/approval/mod.rs index 16fe922b8a228..f3d9e0809f9cc 100644 --- a/polkadot/node/subsystem-bench/src/lib/approval/mod.rs +++ b/polkadot/node/subsystem-bench/src/lib/approval/mod.rs @@ -54,9 +54,9 @@ use polkadot_node_primitives::approval::time::{ slot_number_to_tick, tick_to_slot_number, Clock, ClockExt, SystemClock, }; -use polkadot_node_core_consensus_statistics_collector::{ - ConsensusStatisticsCollector as ConsensusStatisticsCollectorSubsystem, - metrics::Metrics as ConsensusStatisticsMetrics, +use polkadot_node_core_rewards_statistics_collector::{ + RewardsStatisticsCollector as RewardsStatisticsCollectorSubsystem, + metrics::Metrics as RewardsStatisticsMetrics, }; use polkadot_node_core_approval_voting::{ ApprovalVotingSubsystem, Config as ApprovalVotingConfig, RealAssignmentCriteria, diff --git a/polkadot/parachain/test-parachains/adder/collator/src/main.rs b/polkadot/parachain/test-parachains/adder/collator/src/main.rs index f6ed513c76a33..64ff9b0e5ca18 100644 --- a/polkadot/parachain/test-parachains/adder/collator/src/main.rs +++ b/polkadot/parachain/test-parachains/adder/collator/src/main.rs @@ -101,6 +101,7 @@ fn main() -> Result<()> { keep_finalized_for: None, invulnerable_ah_collators: HashSet::new(), collator_protocol_hold_off: None, + publish_per_validator_approval_metrics: false, }, ) .map_err(|e| e.to_string())?; diff --git a/polkadot/zombienet-sdk-tests/tests/functional/rewards_statistics_mixed_validators.rs b/polkadot/zombienet-sdk-tests/tests/functional/rewards_statistics_mixed_validators.rs index 3d92412812416..ebe03700395fe 100644 --- a/polkadot/zombienet-sdk-tests/tests/functional/rewards_statistics_mixed_validators.rs +++ b/polkadot/zombienet-sdk-tests/tests/functional/rewards_statistics_mixed_validators.rs @@ -34,6 +34,7 @@ async fn rewards_statistics_mixed_validators_test() -> Result<(), anyhow::Error> .with_genesis_overrides(json!({ "configuration": { "config": { + "relay_vrf_modulo_samples": 1, "scheduler_params": { "group_rotation_frequency": 4 } @@ -51,16 +52,11 @@ async fn rewards_statistics_mixed_validators_test() -> Result<(), anyhow::Error> node.with_name(&format!("malus-{i}")) .with_args(vec![ "-lparachain=debug,MALUS=trace".into(), - "--alice".into(), + "--no-hardware-benchmarks".into(), "--insecure-validator-i-know-what-i-do".into(), ]) - .with_image( - std::env::var("MALUS_IMAGE") - .unwrap_or("docker.io/paritypr/malus".to_string()) - .as_str(), - ) .with_command("malus") - .with_subcommand("dispute-valid-candidates") + .with_subcommand("dispute-ancestor") .invulnerable(false) }) }) @@ -96,21 +92,16 @@ async fn rewards_statistics_mixed_validators_test() -> Result<(), anyhow::Error> let relay_node = network.get_node("validator-0")?; let relay_client: OnlineClient = relay_node.wait_client().await?; - let mut blocks_sub = relay_client.blocks().subscribe_finalized().await?; - - // wait for session one to be finalized - wait_for_nth_session_change(&mut blocks_sub, 2).await; - - assert_approval_usages_medians( - 1, - 12, - vec![("validator", 0..9), ("malus", 9..12)], - &network, + assert_para_throughput( + &relay_client, + 15, + [(ParaId::from(2000), 11..16), (ParaId::from(2001), 11..16)] + .into_iter() + .collect(), ).await?; Ok(()) } - async fn assert_approval_usages_medians( session: SessionIndex, num_validators: usize, From 8aecc0116477b8861c2c2388771084bb2c7f08c9 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Tue, 11 Nov 2025 18:18:30 -0400 Subject: [PATCH 35/48] chore: including approval stats submission --- Cargo.lock | 1 + .../rewards-statistics-collector/Cargo.toml | 1 + .../rewards-statistics-collector/src/lib.rs | 166 ++++++++++++++++-- .../src/metrics.rs | 15 ++ .../rewards-statistics-collector/src/tests.rs | 24 +-- polkadot/node/core/runtime-api/src/cache.rs | 1 + polkadot/node/core/runtime-api/src/lib.rs | 15 ++ polkadot/node/subsystem-types/src/messages.rs | 3 + polkadot/primitives/src/vstaging/mod.rs | 29 +++ .../rewards_statistics_mixed_validators.rs | 5 +- 10 files changed, 236 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5e0e8429d8054..0e33c9337c9dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15925,6 +15925,7 @@ dependencies = [ "sp-application-crypto", "sp-authority-discovery", "sp-core 28.0.0", + "sp-keystore", "sp-tracing 16.0.0", "thiserror 1.0.65", "tracing-gum", diff --git a/polkadot/node/core/rewards-statistics-collector/Cargo.toml b/polkadot/node/core/rewards-statistics-collector/Cargo.toml index 311150218e99c..f8c2d8aa635d2 100644 --- a/polkadot/node/core/rewards-statistics-collector/Cargo.toml +++ b/polkadot/node/core/rewards-statistics-collector/Cargo.toml @@ -21,6 +21,7 @@ polkadot-node-subsystem = { workspace = true, default-features = true } polkadot-node-subsystem-util = { workspace = true, default-features = true } polkadot-primitives = { workspace = true, default-features = true } polkadot-node-primitives = { workspace = true, default-features = true } +sp-keystore = { workspace = true, default-features = true } [dev-dependencies] assert_matches = { workspace = true } diff --git a/polkadot/node/core/rewards-statistics-collector/src/lib.rs b/polkadot/node/core/rewards-statistics-collector/src/lib.rs index a26e180ee9aaf..73a6b49c1c611 100644 --- a/polkadot/node/core/rewards-statistics-collector/src/lib.rs +++ b/polkadot/node/core/rewards-statistics-collector/src/lib.rs @@ -26,14 +26,22 @@ use std::collections::{HashMap, HashSet, VecDeque}; use std::collections::hash_map::Entry; use futures::{channel::oneshot, prelude::*}; use gum::CandidateHash; +use sp_keystore::KeystorePtr; use polkadot_node_subsystem::{ - overseer, ActiveLeavesUpdate, FromOrchestra, OverseerSignal, SpawnedSubsystem, SubsystemError, + errors::RuntimeApiError as RuntimeApiSubsystemError, + messages::{ChainApiMessage, ConsensusStatisticsCollectorMessage, RuntimeApiMessage, RuntimeApiRequest}, + overseer, ActiveLeavesUpdate, FromOrchestra, OverseerSignal, SpawnedSubsystem, SubsystemError, SubsystemSender +}; +use polkadot_primitives::{ + AuthorityDiscoveryId, BlockNumber, Hash, Header, SessionIndex, ValidatorId, ValidatorIndex, + well_known_keys::relay_dispatch_queue_remaining_capacity +}; +use polkadot_node_primitives::{ + approval::{ + time::Tick, + v1::DelayTranche + } }; -use polkadot_node_subsystem::messages::{ChainApiMessage, ConsensusStatisticsCollectorMessage}; -use polkadot_primitives::{AuthorityDiscoveryId, BlockNumber, Hash, Header, SessionIndex, ValidatorIndex}; -use polkadot_node_primitives::approval::time::Tick; -use polkadot_node_primitives::approval::v1::DelayTranche; -use polkadot_primitives::well_known_keys::relay_dispatch_queue_remaining_capacity; use crate::{ error::{FatalError, FatalResult, JfyiError, JfyiErrorResult, Result}, }; @@ -46,7 +54,9 @@ mod availability_distribution_metrics; pub mod metrics; use approval_voting_metrics::ApprovalsStats; +use polkadot_node_subsystem::RuntimeApiError::{Execution, NotSupported}; use polkadot_node_subsystem_util::{request_candidate_events, request_session_index_for_child, request_session_info}; +use polkadot_primitives::vstaging::{ApprovalStatisticsTallyLine, ApprovalStatistics}; use crate::approval_voting_metrics::{handle_candidate_approved, handle_observed_no_shows}; use crate::availability_distribution_metrics::{handle_chunk_uploaded, handle_chunks_downloaded, AvailabilityChunks}; use self::metrics::Metrics; @@ -83,13 +93,13 @@ impl PerRelayView { #[derive(Debug, Eq, PartialEq, Clone, Default)] struct PerValidatorTally { - noshows: u32, + no_shows: u32, approvals: u32, } impl PerValidatorTally { fn increment_noshow(&mut self) { - self.noshows += 1; + self.no_shows += 1; } fn increment_approval(&mut self) { @@ -109,12 +119,22 @@ impl PerSessionView { } } +/// A struct that holds the credentials required to sign the PVF check statements. These credentials +/// are implicitly to pinned to a session where our node acts as a validator. +struct SigningCredentials { + /// The validator public key. + validator_key: ValidatorId, + /// The validator index in the current session. + validator_index: ValidatorIndex, +} + struct View { roots: HashSet, per_relay: HashMap, per_session: HashMap, availability_chunks: HashMap, current_session: Option, + credentials: Option, } impl View { @@ -125,6 +145,7 @@ impl View { per_session: HashMap::new(), availability_chunks: HashMap::new(), current_session: None, + credentials: None, }; } } @@ -444,7 +465,7 @@ fn log_session_view_general_stats(view: &View) { let session_tally = session_view .validators_tallies .values() - .map(|tally| (tally.approvals, tally.noshows)) + .map(|tally| (tally.approvals, tally.no_shows)) .fold((0, 0), |acc, (approvals, noshows)| (acc.0 + approvals, acc.1 + noshows)); gum::debug!( @@ -455,4 +476,129 @@ fn log_session_view_general_stats(view: &View) { "session collected statistics" ); } -} \ No newline at end of file +} + +async fn sign_and_submit_approvals_tallies( + sender: &mut impl SubsystemSender, + relay_parent: Hash, + session_index: SessionIndex, + keystore: &KeystorePtr, + credentials: &SigningCredentials, + metrics: &Metrics, + tallies: HashMap, +) { + gum::debug!( + target: LOG_TARGET, + ?relay_parent, + "submitting {} approvals tallies for session {}", + tallies.len(), + session_index, + ); + + metrics.submit_approvals_tallies(tallies.len()); + + let mut validators_indexes = tallies.keys().collect::>(); + validators_indexes.sort(); + + let mut approvals_tallies: Vec = Vec::with_capacity(tallies.len()); + for validator_index in validators_indexes { + let current_tally = tallies.get(validator_index).unwrap(); + approvals_tallies.push(ApprovalStatisticsTallyLine { + validator_index: validator_index.clone(), + approvals_usage: current_tally.approvals, + no_shows: current_tally.no_shows, + }); + } + + let payload = ApprovalStatistics(session_index, approvals_tallies); + + let signature = match polkadot_node_subsystem_util::sign( + keystore, + &credentials.validator_key, + &payload.signing_payload(), + ) { + Ok(Some(signature)) => signature, + Ok(None) => { + gum::warn!( + target: LOG_TARGET, + ?relay_parent, + validator_index = ?credentials.validator_index, + "private key for signing is not available", + ); + return + }, + Err(e) => { + gum::warn!( + target: LOG_TARGET, + ?relay_parent, + validator_index = ?credentials.validator_index, + "error signing the statement: {:?}", + e, + ); + return + }, + }; + + let (tx, rx) = oneshot::channel(); + let runtime_req = runtime_api_request( + sender, + relay_parent, + RuntimeApiRequest::SubmitApprovalStatistics(payload, signature, tx), + rx, + ); + + match runtime_req.await { + Ok(()) => { + metrics.on_vote_submitted(); + }, + Err(e) => { + gum::warn!( + target: LOG_TARGET, + "error occurred during submitting a approvals rewards tallies: {:?}", + e, + ); + }, + } +} + +#[derive(Debug)] +pub(crate) enum RuntimeRequestError { + NotSupported, + ApiError, + CommunicationError, +} + +pub(crate) async fn runtime_api_request( + sender: &mut impl SubsystemSender, + relay_parent: Hash, + request: RuntimeApiRequest, + receiver: oneshot::Receiver>, +) -> std::result::Result { + sender + .send_message(RuntimeApiMessage::Request(relay_parent, request).into()) + .await; + + receiver + .await + .map_err(|_| { + gum::debug!(target: LOG_TARGET, ?relay_parent, "Runtime API request dropped"); + RuntimeRequestError::CommunicationError + }) + .and_then(|res| { + res.map_err(|e| { + use RuntimeApiSubsystemError::*; + match e { + Execution { .. } => { + gum::debug!( + target: LOG_TARGET, + ?relay_parent, + err = ?e, + "Runtime API request internal error" + ); + RuntimeRequestError::ApiError + }, + NotSupported { .. } => RuntimeRequestError::NotSupported, + } + }) + }) +} diff --git a/polkadot/node/core/rewards-statistics-collector/src/metrics.rs b/polkadot/node/core/rewards-statistics-collector/src/metrics.rs index 8f186d43a03db..f097d8f5b2063 100644 --- a/polkadot/node/core/rewards-statistics-collector/src/metrics.rs +++ b/polkadot/node/core/rewards-statistics-collector/src/metrics.rs @@ -31,6 +31,8 @@ pub(crate) struct MetricsInner { approvals_per_session_per_validator: prometheus::CounterVec, no_shows_per_session_per_validator: prometheus::CounterVec, + + submittion_started: prometheus::Counter, } @@ -67,6 +69,12 @@ impl Metrics { } }); } + + pub fn submit_approvals_tallies(&self, tallies: usize) { + self.0.as_ref().map(|metrics| { + metrics.submittion_started.inc_by(tallies as u64); + }); + } } impl metrics::Metrics for Metrics { @@ -112,6 +120,13 @@ impl metrics::Metrics for Metrics { )?, registry, )?, + submittion_started: prometheus::register( + prometheus::Counter::new( + "polkadot_parachain_rewards_statistics_collector_submittion_started", + "The number of rewards tallies submitted" + )?, + registry + )?, }; Ok(Metrics(Some(metrics))) diff --git a/polkadot/node/core/rewards-statistics-collector/src/tests.rs b/polkadot/node/core/rewards-statistics-collector/src/tests.rs index aef49c9ebba36..a5d209affbdc7 100644 --- a/polkadot/node/core/rewards-statistics-collector/src/tests.rs +++ b/polkadot/node/core/rewards-statistics-collector/src/tests.rs @@ -600,28 +600,28 @@ fn prune_unfinalized_forks() { ( ValidatorIndex(0), PerValidatorTally { - noshows: 1, + no_shows: 1, approvals: 1, }, ), ( ValidatorIndex(1), PerValidatorTally { - noshows: 1, + no_shows: 1, approvals: 1, }, ), ( ValidatorIndex(2), PerValidatorTally { - noshows: 0, + no_shows: 0, approvals: 2, }, ), ( ValidatorIndex(3), PerValidatorTally { - noshows: 0, + no_shows: 0, approvals: 1, }, ), @@ -709,7 +709,7 @@ fn prune_unfinalized_forks() { ( ValidatorIndex(0), PerValidatorTally { - noshows: 1, + no_shows: 1, // validator 0 approvals increased from 1 to 2 // as block D with more collected approvals // was finalized @@ -719,21 +719,21 @@ fn prune_unfinalized_forks() { ( ValidatorIndex(1), PerValidatorTally { - noshows: 1, + no_shows: 1, approvals: 2, }, ), ( ValidatorIndex(2), PerValidatorTally { - noshows: 0, + no_shows: 0, approvals: 2, }, ), ( ValidatorIndex(3), PerValidatorTally { - noshows: 0, + no_shows: 0, approvals: 1, }, ), @@ -745,28 +745,28 @@ fn prune_unfinalized_forks() { ( ValidatorIndex(0), PerValidatorTally { - noshows: 0, + no_shows: 0, approvals: 1, }, ), ( ValidatorIndex(1), PerValidatorTally { - noshows: 0, + no_shows: 0, approvals: 1, }, ), ( ValidatorIndex(2), PerValidatorTally { - noshows: 1, + no_shows: 1, approvals: 0, }, ), ( ValidatorIndex(3), PerValidatorTally { - noshows: 0, + no_shows: 0, approvals: 1, }, ), diff --git a/polkadot/node/core/runtime-api/src/cache.rs b/polkadot/node/core/runtime-api/src/cache.rs index 9c09ea3f22a9e..2f4ad8bdea101 100644 --- a/polkadot/node/core/runtime-api/src/cache.rs +++ b/polkadot/node/core/runtime-api/src/cache.rs @@ -662,6 +662,7 @@ pub(crate) enum RequestResult { // This is a request with side-effects and no result, hence (). #[allow(dead_code)] SubmitPvfCheckStatement(()), + SubmitApprovalStatistics(()), ValidationCodeHash(Hash, ParaId, OccupiedCoreAssumption, Option), Version(Hash, u32), Disputes(Hash, Vec<(SessionIndex, CandidateHash, DisputeState)>), diff --git a/polkadot/node/core/runtime-api/src/lib.rs b/polkadot/node/core/runtime-api/src/lib.rs index 5b7be703ae6b8..ee95cb33fe401 100644 --- a/polkadot/node/core/runtime-api/src/lib.rs +++ b/polkadot/node/core/runtime-api/src/lib.rs @@ -33,6 +33,7 @@ use polkadot_primitives::Hash; use cache::{RequestResult, RequestResultCache}; use futures::{channel::oneshot, prelude::*, select, stream::FuturesUnordered}; use std::sync::Arc; +use polkadot_node_subsystem_types::messages::RuntimeApiRequest::SubmitApprovalStatistics; mod cache; @@ -156,6 +157,7 @@ where PvfsRequirePrecheck(relay_parent, pvfs) => self.requests_cache.cache_pvfs_require_precheck(relay_parent, pvfs), SubmitPvfCheckStatement(()) => {}, + SubmitApprovalStatistics(()) => {}, ValidationCodeHash(relay_parent, para_id, assumption, hash) => self .requests_cache .cache_validation_code_hash((relay_parent, para_id, assumption), hash), @@ -309,6 +311,10 @@ where // This request is side-effecting and thus cannot be cached. Some(request) }, + request @ Request::SubmitApprovalStatistics(_, _, _) => { + // This request is side-effecting and thus cannot be cached. + Some(request) + }, Request::ValidationCodeHash(para, assumption, sender) => query!(validation_code_hash(para, assumption), sender) .map(|sender| Request::ValidationCodeHash(para, assumption, sender)), @@ -619,6 +625,15 @@ where result = () ) }, + Request::SubmitApprovalStatistics(payload, sig, sender) => { + query!( + SubmitApprovalStatistics, + submit_approval_statistics(payload, sig), + ver = 2, + sender, + result = () + ) + } Request::PvfsRequirePrecheck(sender) => { query!(PvfsRequirePrecheck, pvfs_require_precheck(), ver = 2, sender) }, diff --git a/polkadot/node/subsystem-types/src/messages.rs b/polkadot/node/subsystem-types/src/messages.rs index f9cb4e0a83d72..f11721b3f28fc 100644 --- a/polkadot/node/subsystem-types/src/messages.rs +++ b/polkadot/node/subsystem-types/src/messages.rs @@ -67,6 +67,7 @@ pub mod network_bridge_event; pub use network_bridge_event::NetworkBridgeEvent; use polkadot_node_primitives::approval::time::Tick; use polkadot_node_primitives::approval::v2::Bitfield; +use polkadot_primitives::vstaging::ApprovalStatistics; /// A request to the candidate backing subsystem to check whether /// we can second this candidate. @@ -733,6 +734,8 @@ pub enum RuntimeApiRequest { FetchOnChainVotes(RuntimeApiSender>), /// Submits a PVF pre-checking statement into the transaction pool. SubmitPvfCheckStatement(PvfCheckStatement, ValidatorSignature, RuntimeApiSender<()>), + /// Submits the collected approvals statistics for a given session into the transaction pool. + SubmitApprovalStatistics(ApprovalStatistics, ValidatorSignature, RuntimeApiSender<()>), /// Returns code hashes of PVFs that require pre-checking by validators in the active set. PvfsRequirePrecheck(RuntimeApiSender>), /// Get the validation code used by the specified para, taking the given diff --git a/polkadot/primitives/src/vstaging/mod.rs b/polkadot/primitives/src/vstaging/mod.rs index 72c39023c7e1f..fccc3f6b7c128 100644 --- a/polkadot/primitives/src/vstaging/mod.rs +++ b/polkadot/primitives/src/vstaging/mod.rs @@ -15,3 +15,32 @@ // along with Polkadot. If not, see . //! Staging Primitives. + +use scale_info::TypeInfo; +use sp_api::__private::{Decode, Encode}; +use sp_application_crypto::RuntimeDebug; +use sp_core::DecodeWithMemTracking; +use sp_staking::SessionIndex; +use crate::ValidatorIndex; + +/// A reward tally line represent the collected statistics about +/// approvals voting for a given validator, how much successful approvals +/// was collected and how many times the given validator no-showed +#[derive(Encode, Decode, DecodeWithMemTracking, Clone, Copy, PartialEq, RuntimeDebug, TypeInfo)] +pub struct ApprovalStatisticsTallyLine { + pub validator_index: ValidatorIndex, + pub approvals_usage: u32, + pub no_shows: u32, +} + +/// ApprovalRewards is the set of tallies where each tally represents +/// a given validator and its approval voting statistics +#[derive(Clone, Encode, Decode, TypeInfo, RuntimeDebug)] +pub struct ApprovalStatistics(SessionIndex, Vec); + +impl ApprovalStatistics { + pub fn signing_payload(&self) -> Vec { + const MAGIC: [u8; 4] = *b"APST"; // for "approval statistics" + (MAGIC, self.0, self.1.clone()).encode() + } +} \ No newline at end of file diff --git a/polkadot/zombienet-sdk-tests/tests/functional/rewards_statistics_mixed_validators.rs b/polkadot/zombienet-sdk-tests/tests/functional/rewards_statistics_mixed_validators.rs index ebe03700395fe..0cca4f75e9007 100644 --- a/polkadot/zombienet-sdk-tests/tests/functional/rewards_statistics_mixed_validators.rs +++ b/polkadot/zombienet-sdk-tests/tests/functional/rewards_statistics_mixed_validators.rs @@ -34,9 +34,10 @@ async fn rewards_statistics_mixed_validators_test() -> Result<(), anyhow::Error> .with_genesis_overrides(json!({ "configuration": { "config": { - "relay_vrf_modulo_samples": 1, + "relay_vrf_modulo_samples": 2, "scheduler_params": { - "group_rotation_frequency": 4 + "group_rotation_frequency": 4, + "max_validators_per_core": 6, } } } From 299d442ee3b39a5cb62fab62fb980f4844ca52bf Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Mon, 17 Nov 2025 13:50:05 -0300 Subject: [PATCH 36/48] chore: starting onchain impl --- .../rewards-statistics-collector/src/lib.rs | 314 +++++++++++++----- .../src/metrics.rs | 8 +- .../subsystem-types/src/runtime_client.rs | 23 ++ polkadot/primitives/src/runtime_api.rs | 5 + polkadot/primitives/src/vstaging/mod.rs | 10 +- polkadot/runtime/parachains/src/paras/mod.rs | 8 + .../parachains/src/runtime_api_impl/v13.rs | 9 + polkadot/runtime/rococo/src/lib.rs | 7 + polkadot/runtime/westend/src/lib.rs | 8 + .../staking-async/runtimes/rc/src/lib.rs | 7 + 10 files changed, 305 insertions(+), 94 deletions(-) diff --git a/polkadot/node/core/rewards-statistics-collector/src/lib.rs b/polkadot/node/core/rewards-statistics-collector/src/lib.rs index 73a6b49c1c611..b73c77afd3bea 100644 --- a/polkadot/node/core/rewards-statistics-collector/src/lib.rs +++ b/polkadot/node/core/rewards-statistics-collector/src/lib.rs @@ -24,6 +24,7 @@ use std::collections::{HashMap, HashSet, VecDeque}; use std::collections::hash_map::Entry; +use std::task::Context; use futures::{channel::oneshot, prelude::*}; use gum::CandidateHash; use sp_keystore::KeystorePtr; @@ -32,10 +33,7 @@ use polkadot_node_subsystem::{ messages::{ChainApiMessage, ConsensusStatisticsCollectorMessage, RuntimeApiMessage, RuntimeApiRequest}, overseer, ActiveLeavesUpdate, FromOrchestra, OverseerSignal, SpawnedSubsystem, SubsystemError, SubsystemSender }; -use polkadot_primitives::{ - AuthorityDiscoveryId, BlockNumber, Hash, Header, SessionIndex, ValidatorId, ValidatorIndex, - well_known_keys::relay_dispatch_queue_remaining_capacity -}; +use polkadot_primitives::{AuthorityDiscoveryId, BlockNumber, Hash, Header, SessionIndex, ValidatorId, ValidatorIndex, well_known_keys::relay_dispatch_queue_remaining_capacity, SessionInfo}; use polkadot_node_primitives::{ approval::{ time::Tick, @@ -109,18 +107,27 @@ impl PerValidatorTally { #[derive(Debug, Eq, PartialEq, Clone)] pub struct PerSessionView { + credentials: Option, authorities_lookup: HashMap, validators_tallies: HashMap, } impl PerSessionView { - fn new(authorities_lookup: HashMap) -> Self { - Self { authorities_lookup, validators_tallies: HashMap::new() } + fn new( + authorities_lookup: HashMap, + credentials: Option, + ) -> Self { + Self { + authorities_lookup, + credentials, + validators_tallies: HashMap::new(), + } } } /// A struct that holds the credentials required to sign the PVF check statements. These credentials /// are implicitly to pinned to a session where our node acts as a validator. +#[derive(Debug, Eq, PartialEq, Clone)] struct SigningCredentials { /// The validator public key. validator_key: ValidatorId, @@ -133,8 +140,8 @@ struct View { per_relay: HashMap, per_session: HashMap, availability_chunks: HashMap, - current_session: Option, - credentials: Option, + latest_finalized_session: Option, + recent_block: Option<(BlockNumber, Hash)>, } impl View { @@ -144,25 +151,58 @@ impl View { per_relay: HashMap::new(), per_session: HashMap::new(), availability_chunks: HashMap::new(), - current_session: None, - credentials: None, + latest_finalized_session: None, + recent_block: None, }; } + + // add_node includes a new activated block + // in the unfinalized blocks mapping, it also + // links the including block with its parent + // if its parent is present in the mapping + // otherwise the including block will be added + // in the roots set. + fn add_node( + &mut self, + activated_hash: Hash, + activated_header: Option
, + session_index: SessionIndex, + ) { + if let Some(h) = activated_header { + let parent_hash = h.parent_hash; + let parent_hash = match self.per_relay.get_mut(&parent_hash) { + Some(per_relay_view) => { + per_relay_view.link_child(activated_hash); + Some(parent_hash) + }, + None => { + _ = self.roots.insert(activated_hash); + None + }, + }; + + self.per_relay.insert(activated_hash, PerRelayView::new(parent_hash, session_index)); + } else { + self.roots.insert(activated_hash); + self.per_relay.insert(activated_hash, PerRelayView::new(None, session_index)); + } + } } /// The statistics collector subsystem. -#[derive(Default)] pub struct RewardsStatisticsCollector { + keystore: KeystorePtr, metrics: Metrics, config: Config } impl RewardsStatisticsCollector { /// Create a new instance of the `ConsensusStatisticsCollector`. - pub fn new(metrics: Metrics, config: Config) -> Self { + pub fn new(keystore: KeystorePtr, metrics: Metrics, config: Config) -> Self { Self { metrics, config, + keystore, } } } @@ -174,7 +214,7 @@ where { fn start(self, ctx: Context) -> SpawnedSubsystem { SpawnedSubsystem { - future: run(ctx, (self.metrics, self.config.publish_per_validator_approval_metrics)) + future: run(ctx, self.keystore, (self.metrics, self.config.publish_per_validator_approval_metrics)) .map_err(|e| SubsystemError::with_origin("statistics-parachains", e)) .boxed(), name: "rewards-statistics-collector-subsystem", @@ -183,11 +223,11 @@ where } #[overseer::contextbounds(ConsensusStatisticsCollector, prefix = self::overseer)] -async fn run(mut ctx: Context, metrics: (Metrics, bool)) -> FatalResult<()> { +async fn run(mut ctx: Context, keystore: KeystorePtr, metrics: (Metrics, bool)) -> FatalResult<()> { let mut view = View::new(); loop { - crate::error::log_error( - run_iteration(&mut ctx, &mut view, (&metrics.0, metrics.1)).await, + error::log_error( + run_iteration(&mut ctx, &mut view, &keystore, (&metrics.0, metrics.1)).await, "Encountered issue during run iteration", )?; } @@ -197,65 +237,44 @@ async fn run(mut ctx: Context, metrics: (Metrics, bool)) -> FatalResult pub(crate) async fn run_iteration( ctx: &mut Context, view: &mut View, + keystore: &KeystorePtr, metrics: (&Metrics, bool), ) -> Result<()> { - let mut sender = ctx.sender().clone(); let per_validator_metrics = metrics.1; loop { match ctx.recv().await.map_err(FatalError::SubsystemReceive)? { FromOrchestra::Signal(OverseerSignal::Conclude) => return Ok(()), FromOrchestra::Signal(OverseerSignal::ActiveLeaves(update)) => { if let Some(activated) = update.activated { - let relay_hash = activated.hash; + let ActivationInfo { + activated_header, + session_index, + new_session_info, + recent_block, + } = extract_activated_leaf_info( + ctx.sender(), + view, + keystore, + activated.hash, + activated.number, + ).await?; - let (tx, rx) = oneshot::channel(); - - ctx.send_message(ChainApiMessage::BlockHeader(relay_hash, tx)).await; - let header = rx - .map_err(JfyiError::OverseerCommunication) - .await? - .map_err(JfyiError::ChainApiCallError)?; - - let session_idx = request_session_index_for_child(relay_hash, ctx.sender()) - .await - .await - .map_err(JfyiError::OverseerCommunication)? - .map_err(JfyiError::RuntimeApiCallError)?; - - if let Some(ref h) = header { - let parent_hash = h.parent_hash; - let parent_hash = match view.per_relay.get_mut(&parent_hash) { - Some(per_relay_view) => { - per_relay_view.link_child(relay_hash); - Some(parent_hash) - }, - None => { - _ = view.roots.insert(relay_hash); - None - }, - }; - - view.per_relay.insert(relay_hash, PerRelayView::new(parent_hash, session_idx)); - } else { - view.roots.insert(relay_hash); - view.per_relay.insert(relay_hash, PerRelayView::new(None, session_idx)); - } + let relay_hash = activated.hash; + view.recent_block = Some(recent_block); - if !view.per_session.contains_key(&session_idx) { - let session_info = request_session_info(relay_hash, session_idx, ctx.sender()) - .await - .await - .map_err(JfyiError::OverseerCommunication)? - .map_err(JfyiError::RuntimeApiCallError)?; - - if let Some(session_info) = session_info { - let mut authority_lookup = HashMap::new(); - for (i, ad) in session_info.discovery_keys.iter().cloned().enumerate() { - authority_lookup.insert(ad, ValidatorIndex(i as _)); - } + view.add_node( + relay_hash, + activated_header, + session_index, + ); - view.per_session.insert(session_idx, PerSessionView::new(authority_lookup)); + if let Some((session_info, credentials)) = new_session_info { + let mut authority_lookup = HashMap::new(); + for (i, ad) in session_info.discovery_keys.iter().cloned().enumerate() { + authority_lookup.insert(ad, ValidatorIndex(i as _)); } + + view.per_session.insert(session_index, PerSessionView::new(authority_lookup, credentials)); } } }, @@ -305,7 +324,13 @@ pub(crate) async fn run_iteration( } log_session_view_general_stats(view); - prune_old_session_views(ctx, view, fin_block_hash).await?; + submit_finalized_session_stats( + ctx.sender(), + keystore, + view, + fin_block_hash, + metrics.0, + ).await?; } FromOrchestra::Communication { msg } => { match msg { @@ -361,6 +386,82 @@ pub(crate) async fn run_iteration( } } +struct ActivationInfo { + activated_header: Option
, + recent_block: (BlockNumber, Hash), + session_index: SessionIndex, + new_session_info: Option<(SessionInfo, Option)>, +} + +async fn extract_activated_leaf_info< + Sender: SubsystemSender + + SubsystemSender +>( + mut sender: Sender, + view: &mut View, + keystore: &KeystorePtr, + relay_hash: Hash, + relay_number: BlockNumber, +) -> Result { + let recent_block = match view.recent_block { + Some((recent_block_num, recent_block_hash)) if relay_number < recent_block_num => { + // the existing recent block is not worse than the new activation, so leave it. + (recent_block_num, recent_block_hash) + }, + _ => (relay_number, relay_hash), + }; + + let (tx, rx) = oneshot::channel(); + sender.send_message(ChainApiMessage::BlockHeader(relay_hash, tx)).await; + let header = rx + .map_err(JfyiError::OverseerCommunication) + .await? + .map_err(JfyiError::ChainApiCallError)?; + + let session_idx = request_session_index_for_child(relay_hash, &mut sender) + .await + .await + .map_err(JfyiError::OverseerCommunication)? + .map_err(JfyiError::RuntimeApiCallError)?; + + let new_session_info = if !view.per_session.contains_key(&session_idx) { + let session_info = request_session_info(relay_hash, session_idx, &mut sender) + .await + .await + .map_err(JfyiError::OverseerCommunication)? + .map_err(JfyiError::RuntimeApiCallError)?; + + let (tx, rx) = oneshot::channel(); + let validators = runtime_api_request( + &mut sender, + relay_hash, + RuntimeApiRequest::Validators(tx), + rx, + ) + .await + .map_err(JfyiError::RuntimeApiCallError)?; + + let signing_credentials = polkadot_node_subsystem_util::signing_key_and_index(&validators, keystore) + .map(|(validator_key, validator_index)| + SigningCredentials { validator_key, validator_index }); + + if let Some(session_info) = session_info { + Some((session_info, signing_credentials)) + } else { + None + } + } else { + None + }; + + Ok(ActivationInfo { + activated_header: header, + recent_block, + session_index: session_idx, + new_session_info, + }) +} + // prune_unfinalised_forks will remove all the relay chain blocks // that are not in the finalized chain and its de pendants children using the latest finalized block as reference // and will return a list of finalized hashes @@ -431,29 +532,61 @@ fn prune_unfinalised_forks(view: &mut View, fin_block_hash: Hash) -> Vec { retain_relay_hashes } -// prune_old_session_views avoid the per_session mapping to grow -// indefinitely by removing sessions stored for more than MAX_SESSIONS_TO_KEEP (2) -// finalized sessions. -#[overseer::contextbounds(ConsensusStatisticsCollector, prefix = self::overseer)] -async fn prune_old_session_views( - ctx: &mut Context, +// submit_finalized_session_stats works after a whole session is finalized +// getting all the collected data and submitting to the runtime, after the +// submition the data is cleaned from mapping +async fn submit_finalized_session_stats< + Sender: SubsystemSender, +>( + mut sender: Sender, + keystore: &KeystorePtr, view: &mut View, finalized_hash: Hash, + metrics: &Metrics, ) -> Result<()> { - let session_idx = request_session_index_for_child(finalized_hash, ctx.sender()) + let recent_block_hash = match view.recent_block { + Some((_, block_hash)) => block_hash, + None => { + gum::debug!( + target: LOG_TARGET, + ?finalized_hash, + "recent block does not exist or got erased, cannot submit finalized session statistics" + ); + return Ok(()); + }, + }; + + let current_fin_session = request_session_index_for_child(finalized_hash, &mut sender) .await .await .map_err(JfyiError::OverseerCommunication)? .map_err(JfyiError::RuntimeApiCallError)?; - match view.current_session { - Some(current_session) if current_session < session_idx => { - if let Some(wipe_before) = session_idx.checked_sub(MAX_SESSIONS_TO_KEEP) { - view.per_session.retain(|stored_session_index, _| *stored_session_index > wipe_before); + match view.latest_finalized_session { + Some(latest_fin_session) if latest_fin_session < current_fin_session => { + // the previous session was finalized + for (session_idx, session_view) in view + .per_session + .iter() + .filter(|stored_session_idx| stored_session_idx.0 < ¤t_fin_session) { + + if let Some(ref credentials) = session_view.credentials { + sign_and_submit_approvals_tallies( + &mut sender, + recent_block_hash, + session_idx, + keystore, + credentials, + metrics, + session_view.validators_tallies.clone(), + ).await; + } } - view.current_session = Some(session_idx) + + view.per_session.retain(|session_index, _| *session_index >= current_fin_session); + view.latest_finalized_session = Some(current_fin_session); } - None => view.current_session = Some(session_idx), + None => view.latest_finalized_session = Some(current_fin_session), _ => {} }; @@ -478,10 +611,12 @@ fn log_session_view_general_stats(view: &View) { } } -async fn sign_and_submit_approvals_tallies( - sender: &mut impl SubsystemSender, +async fn sign_and_submit_approvals_tallies< + Sender: SubsystemSender, +>( + mut sender: Sender, relay_parent: Hash, - session_index: SessionIndex, + session_index: &SessionIndex, keystore: &KeystorePtr, credentials: &SigningCredentials, metrics: &Metrics, @@ -495,8 +630,6 @@ async fn sign_and_submit_approvals_tallies( session_index, ); - metrics.submit_approvals_tallies(tallies.len()); - let mut validators_indexes = tallies.keys().collect::>(); validators_indexes.sort(); @@ -510,7 +643,7 @@ async fn sign_and_submit_approvals_tallies( }); } - let payload = ApprovalStatistics(session_index, approvals_tallies); + let payload = ApprovalStatistics(session_index.clone(), approvals_tallies); let signature = match polkadot_node_subsystem_util::sign( keystore, @@ -541,15 +674,15 @@ async fn sign_and_submit_approvals_tallies( let (tx, rx) = oneshot::channel(); let runtime_req = runtime_api_request( - sender, + &mut sender, relay_parent, RuntimeApiRequest::SubmitApprovalStatistics(payload, signature, tx), rx, - ); + ).await; - match runtime_req.await { + match runtime_req { Ok(()) => { - metrics.on_vote_submitted(); + metrics.on_approvals_submitted(); }, Err(e) => { gum::warn!( @@ -568,8 +701,11 @@ pub(crate) enum RuntimeRequestError { CommunicationError, } -pub(crate) async fn runtime_api_request( - sender: &mut impl SubsystemSender, +async fn runtime_api_request< + T, + Sender: SubsystemSender, +>( + mut sender: Sender, relay_parent: Hash, request: RuntimeApiRequest, receiver: oneshot::Receiver>, diff --git a/polkadot/node/core/rewards-statistics-collector/src/metrics.rs b/polkadot/node/core/rewards-statistics-collector/src/metrics.rs index f097d8f5b2063..68408317c7ae4 100644 --- a/polkadot/node/core/rewards-statistics-collector/src/metrics.rs +++ b/polkadot/node/core/rewards-statistics-collector/src/metrics.rs @@ -32,7 +32,7 @@ pub(crate) struct MetricsInner { approvals_per_session_per_validator: prometheus::CounterVec, no_shows_per_session_per_validator: prometheus::CounterVec, - submittion_started: prometheus::Counter, + approvals_stats_submittion_total: prometheus::Counter, } @@ -70,9 +70,9 @@ impl Metrics { }); } - pub fn submit_approvals_tallies(&self, tallies: usize) { + pub fn on_approvals_submitted(&self) { self.0.as_ref().map(|metrics| { - metrics.submittion_started.inc_by(tallies as u64); + metrics.approvals_stats_submittion_total.inc(); }); } } @@ -120,7 +120,7 @@ impl metrics::Metrics for Metrics { )?, registry, )?, - submittion_started: prometheus::register( + approvals_stats_submittion_total: prometheus::register( prometheus::Counter::new( "polkadot_parachain_rewards_statistics_collector_submittion_started", "The number of rewards tallies submitted" diff --git a/polkadot/node/subsystem-types/src/runtime_client.rs b/polkadot/node/subsystem-types/src/runtime_client.rs index bb8f359b729ba..46528acb3c9a5 100644 --- a/polkadot/node/subsystem-types/src/runtime_client.rs +++ b/polkadot/node/subsystem-types/src/runtime_client.rs @@ -36,6 +36,7 @@ use std::{ collections::{BTreeMap, VecDeque}, sync::Arc, }; +use polkadot_primitives::vstaging::ApprovalStatistics; /// Offers header utilities. /// @@ -218,6 +219,14 @@ pub trait RuntimeApiSubsystemClient { signature: ValidatorSignature, ) -> Result<(), ApiError>; + /// Submits the collected approval statistics collected for the session + async fn submit_approval_statistics( + &self, + at: Hash, + payload: ApprovalStatistics, + signature: ValidatorSignature, + ) -> Result<(), ApiError>; + /// Returns code hashes of PVFs that require pre-checking by validators in the active set. /// /// NOTE: This function is only available since parachain host version 2. @@ -533,6 +542,20 @@ where runtime_api.submit_pvf_check_statement(at, stmt, signature) } + async fn submit_approval_statistics( + &self, + at: Hash, + payload: ApprovalStatistics, + signature: ValidatorSignature, + ) -> Result<(), ApiError> { + let mut runtime_api = self.client.runtime_api(); + runtime_api.register_extension( + self.offchain_transaction_pool_factory.offchain_transaction_pool(at), + ); + + runtime_api.submit_approval_statistics(at, payload, signature) + } + async fn pvfs_require_precheck(&self, at: Hash) -> Result, ApiError> { self.client.runtime_api().pvfs_require_precheck(at) } diff --git a/polkadot/primitives/src/runtime_api.rs b/polkadot/primitives/src/runtime_api.rs index 518a828e7e0e8..93b148e29709e 100644 --- a/polkadot/primitives/src/runtime_api.rs +++ b/polkadot/primitives/src/runtime_api.rs @@ -128,6 +128,7 @@ use alloc::{ }; use polkadot_core_primitives as pcp; use polkadot_parachain_primitives::primitives as ppp; +use crate::vstaging::ApprovalStatistics; sp_api::decl_runtime_apis! { /// The API for querying the state of parachains on-chain. @@ -214,6 +215,10 @@ sp_api::decl_runtime_apis! { /// NOTE: This function is only available since parachain host version 2. fn submit_pvf_check_statement(stmt: PvfCheckStatement, signature: ValidatorSignature); + + /// Submits the session collected proof statistics into the transaction pool. + fn submit_approval_statistics(payload: ApprovalStatistics, signature: ValidatorSignature); + /// Returns code hashes of PVFs that require pre-checking by validators in the active set. /// /// NOTE: This function is only available since parachain host version 2. diff --git a/polkadot/primitives/src/vstaging/mod.rs b/polkadot/primitives/src/vstaging/mod.rs index fccc3f6b7c128..7a748fd5652f8 100644 --- a/polkadot/primitives/src/vstaging/mod.rs +++ b/polkadot/primitives/src/vstaging/mod.rs @@ -22,21 +22,29 @@ use sp_application_crypto::RuntimeDebug; use sp_core::DecodeWithMemTracking; use sp_staking::SessionIndex; use crate::ValidatorIndex; +use alloc::vec::Vec; /// A reward tally line represent the collected statistics about /// approvals voting for a given validator, how much successful approvals /// was collected and how many times the given validator no-showed #[derive(Encode, Decode, DecodeWithMemTracking, Clone, Copy, PartialEq, RuntimeDebug, TypeInfo)] pub struct ApprovalStatisticsTallyLine { + /// represents the validator to which the statistics belongs to pub validator_index: ValidatorIndex, + + /// how many times the validator had sent useful approval votes + /// that contribute the successful approval of a candidate pub approvals_usage: u32, + + /// how many times the validator was supposed to send a vote but + /// no showed pub no_shows: u32, } /// ApprovalRewards is the set of tallies where each tally represents /// a given validator and its approval voting statistics #[derive(Clone, Encode, Decode, TypeInfo, RuntimeDebug)] -pub struct ApprovalStatistics(SessionIndex, Vec); +pub struct ApprovalStatistics(pub SessionIndex, pub Vec); impl ApprovalStatistics { pub fn signing_payload(&self) -> Vec { diff --git a/polkadot/runtime/parachains/src/paras/mod.rs b/polkadot/runtime/parachains/src/paras/mod.rs index 2c521dab614c6..3b018ec2ac2f0 100644 --- a/polkadot/runtime/parachains/src/paras/mod.rs +++ b/polkadot/runtime/parachains/src/paras/mod.rs @@ -145,6 +145,7 @@ pub mod benchmarking; pub(crate) mod tests; pub use pallet::*; +use polkadot_primitives::vstaging::ApprovalStatistics; const LOG_TARGET: &str = "runtime::paras"; @@ -2448,6 +2449,13 @@ impl Pallet { } } + pub(crate) fn submit_approval_statistics( + payload: ApprovalStatistics, + signature: ValidatorSignature, + ) { + // TODO: to be implemented + } + /// Returns the current lifecycle state of the para. pub fn lifecycle(id: ParaId) -> Option { ParaLifecycles::::get(&id) diff --git a/polkadot/runtime/parachains/src/runtime_api_impl/v13.rs b/polkadot/runtime/parachains/src/runtime_api_impl/v13.rs index dc204c67e3462..cd0e783b3accd 100644 --- a/polkadot/runtime/parachains/src/runtime_api_impl/v13.rs +++ b/polkadot/runtime/parachains/src/runtime_api_impl/v13.rs @@ -40,6 +40,7 @@ use polkadot_primitives::{ PersistedValidationData, PvfCheckStatement, ScrapedOnChainVotes, SessionIndex, SessionInfo, ValidationCode, ValidationCodeHash, ValidatorId, ValidatorIndex, ValidatorSignature, }; +use polkadot_primitives::vstaging::ApprovalStatistics; use sp_runtime::traits::One; /// Implementation for the `validators` function of the runtime API. @@ -343,6 +344,14 @@ pub fn submit_pvf_check_statement( paras::Pallet::::submit_pvf_check_statement(stmt, signature) } +/// Submits the collected approval statistics for a given session. +pub fn submit_approval_statistics( + payload: ApprovalStatistics, + signature: ValidatorSignature, +) { + paras::Pallet::::submit_approval_statistics(payload, signature) +} + /// Returns the list of all PVF code hashes that require pre-checking. pub fn pvfs_require_precheck() -> Vec { paras::Pallet::::pvfs_require_precheck() diff --git a/polkadot/runtime/rococo/src/lib.rs b/polkadot/runtime/rococo/src/lib.rs index 1c08a1c6b3e1d..5a7b469fdba93 100644 --- a/polkadot/runtime/rococo/src/lib.rs +++ b/polkadot/runtime/rococo/src/lib.rs @@ -2105,6 +2105,13 @@ sp_api::impl_runtime_apis! { parachains_runtime_api_impl::submit_pvf_check_statement::(stmt, signature) } + fn submit_approval_statistics( + payload: polkadot_primitives::vstaging::ApprovalStatistics, + signature: polkadot_primitives::ValidatorSignature + ) { + parachains_runtime_api_impl::submit_approval_statistics::(payload, signature) + } + fn pvfs_require_precheck() -> Vec { parachains_runtime_api_impl::pvfs_require_precheck::() } diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs index 99e02b1053320..269faf0d47ffa 100644 --- a/polkadot/runtime/westend/src/lib.rs +++ b/polkadot/runtime/westend/src/lib.rs @@ -62,6 +62,7 @@ use polkadot_primitives::{ PersistedValidationData, PvfCheckStatement, ScrapedOnChainVotes, SessionInfo, Signature, ValidationCode, ValidationCodeHash, ValidatorId, ValidatorIndex, ValidatorSignature, PARACHAIN_KEY_TYPE_ID, + vstaging::ApprovalStatistics, }; use polkadot_runtime_common::{ assigned_slots, auctions, crowdloan, @@ -2351,6 +2352,13 @@ sp_api::impl_runtime_apis! { parachains_runtime_api_impl::submit_pvf_check_statement::(stmt, signature) } + fn submit_approval_statistics( + payload: ApprovalStatistics, + signature: ValidatorSignature, + ) { + parachains_runtime_api_impl::submit_approval_statistics::(payload, signature) + } + fn pvfs_require_precheck() -> Vec { parachains_runtime_api_impl::pvfs_require_precheck::() } diff --git a/substrate/frame/staking-async/runtimes/rc/src/lib.rs b/substrate/frame/staking-async/runtimes/rc/src/lib.rs index 8f0f6b4f33096..b35752478e69d 100644 --- a/substrate/frame/staking-async/runtimes/rc/src/lib.rs +++ b/substrate/frame/staking-async/runtimes/rc/src/lib.rs @@ -2287,6 +2287,13 @@ sp_api::impl_runtime_apis! { parachains_runtime_api_impl::submit_pvf_check_statement::(stmt, signature) } + fn submit_approval_statistic( + payload: ApprovalStatistic, + signature: ValidatorSignature, + ) { + parachains_runtime_api_impl::submit_approval_statistics::(payload, signature) + } + fn pvfs_require_precheck() -> Vec { parachains_runtime_api_impl::pvfs_require_precheck::() } From e27fc23557e1f6f9347930e6645a54307a6620b4 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Mon, 17 Nov 2025 14:12:46 -0300 Subject: [PATCH 37/48] chore: remove onchain impl --- Cargo.toml | 2 +- .../src/lib.rs | 2 +- polkadot/cli/src/cli.rs | 2 +- polkadot/cli/src/command.rs | 4 ++-- .../node/core/approval-voting/src/import.rs | 4 ++-- polkadot/node/core/approval-voting/src/lib.rs | 24 +++++++++---------- .../node/core/approval-voting/src/tests.rs | 8 +++---- .../rewards-statistics-collector/src/lib.rs | 14 +++++------ .../rewards-statistics-collector/src/tests.rs | 14 +++++------ polkadot/node/core/runtime-api/src/cache.rs | 1 - polkadot/node/core/runtime-api/src/lib.rs | 1 - .../src/responder.rs | 10 ++++---- .../availability-recovery/src/task/mod.rs | 6 ++--- .../src/task/strategy/chunks.rs | 2 +- .../src/task/strategy/systematic.rs | 2 +- polkadot/node/network/protocol/src/lib.rs | 1 + polkadot/node/overseer/src/dummy.rs | 2 +- polkadot/node/overseer/src/lib.rs | 12 +++++----- polkadot/node/service/src/builder/mod.rs | 6 ++--- polkadot/node/subsystem-types/src/messages.rs | 2 +- .../adder/collator/src/main.rs | 2 +- 21 files changed, 60 insertions(+), 61 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1aa782587d51f..793183473ded1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1143,7 +1143,7 @@ polkadot-node-core-parachains-inherent = { path = "polkadot/node/core/parachains polkadot-node-core-prospective-parachains = { path = "polkadot/node/core/prospective-parachains", default-features = false } polkadot-node-core-rewards-statistics-collector = { path = "polkadot/node/core/rewards-statistics-collector", default-features = false } polkadot-node-core-provisioner = { path = "polkadot/node/core/provisioner", default-features = false } -polkadot-node-core-pvf = { path = "polkadot/node/core/pvf", default-features = false } +polkadot-node-core-pvf = { path = "polkadot/node/core/pvf", default-features = false } polkadot-node-core-pvf-checker = { path = "polkadot/node/core/pvf-checker", default-features = false } polkadot-node-core-pvf-common = { path = "polkadot/node/core/pvf/common", default-features = false } polkadot-node-core-pvf-execute-worker = { path = "polkadot/node/core/pvf/execute-worker", default-features = false } diff --git a/cumulus/client/relay-chain-inprocess-interface/src/lib.rs b/cumulus/client/relay-chain-inprocess-interface/src/lib.rs index a1acc6441ec93..2c1ad6a4650d5 100644 --- a/cumulus/client/relay-chain-inprocess-interface/src/lib.rs +++ b/cumulus/client/relay-chain-inprocess-interface/src/lib.rs @@ -419,7 +419,7 @@ fn build_polkadot_full_node( keep_finalized_for: None, invulnerable_ah_collators: HashSet::new(), collator_protocol_hold_off: None, - publish_per_validator_approval_metrics: false, + verbose_approval_metrics: false, }; let (relay_chain_full_node, paranode_req_receiver) = match config.network.network_backend { diff --git a/polkadot/cli/src/cli.rs b/polkadot/cli/src/cli.rs index df02c70b642ac..0d664a3b46212 100644 --- a/polkadot/cli/src/cli.rs +++ b/polkadot/cli/src/cli.rs @@ -170,7 +170,7 @@ pub struct RunCmd { /// Enable or disable per validator collected approvals metrics /// to be published to prometheus. If not specified, set to false. #[arg(long)] - pub publish_per_validator_approval_metrics: Option, + pub verbose_approval_metrics: Option, } #[allow(missing_docs)] diff --git a/polkadot/cli/src/command.rs b/polkadot/cli/src/command.rs index 1ef83de065928..2d30e50a77cd9 100644 --- a/polkadot/cli/src/command.rs +++ b/polkadot/cli/src/command.rs @@ -278,9 +278,9 @@ where telemetry_worker_handle: None, node_version, secure_validator_mode, - publish_per_validator_approval_metrics: cli + verbose_approval_metrics: cli .run - .publish_per_validator_approval_metrics + .verbose_approval_metrics .unwrap_or(false), workers_path: cli.run.workers_path, workers_names: None, diff --git a/polkadot/node/core/approval-voting/src/import.rs b/polkadot/node/core/approval-voting/src/import.rs index 63a2d0257ecb7..a6b712f619d34 100644 --- a/polkadot/node/core/approval-voting/src/import.rs +++ b/polkadot/node/core/approval-voting/src/import.rs @@ -66,7 +66,7 @@ use crate::{ }; use polkadot_node_primitives::approval::time::{slot_number_to_tick, Tick}; -use polkadot_node_subsystem::messages::ConsensusStatisticsCollectorMessage; +use polkadot_node_subsystem::messages::RewardsStatisticsCollectorMessage; use super::{State, LOG_TARGET}; #[derive(Debug)] @@ -338,7 +338,7 @@ pub(crate) async fn handle_new_head< Sender: SubsystemSender + SubsystemSender + SubsystemSender - + SubsystemSender, + + SubsystemSender, AVSender: SubsystemSender, B: Backend, >( diff --git a/polkadot/node/core/approval-voting/src/lib.rs b/polkadot/node/core/approval-voting/src/lib.rs index f7837a89fd562..e4c35af049eb7 100644 --- a/polkadot/node/core/approval-voting/src/lib.rs +++ b/polkadot/node/core/approval-voting/src/lib.rs @@ -95,7 +95,7 @@ use persisted_entries::{ApprovalEntry, BlockEntry, CandidateEntry}; use polkadot_node_primitives::approval::time::{ slot_number_to_tick, Clock, ClockExt, DelayedApprovalTimer, SystemClock, Tick, }; -use polkadot_node_subsystem::messages::ConsensusStatisticsCollectorMessage; +use polkadot_node_subsystem::messages::RewardsStatisticsCollectorMessage; mod approval_checking; pub mod approval_db; mod backend; @@ -1249,7 +1249,7 @@ async fn run< Sender: SubsystemSender + SubsystemSender + SubsystemSender - + SubsystemSender + + SubsystemSender + SubsystemSender + SubsystemSender + SubsystemSender @@ -1485,7 +1485,7 @@ pub async fn start_approval_worker< + SubsystemSender + SubsystemSender + SubsystemSender - + SubsystemSender + + SubsystemSender + SubsystemSender + SubsystemSender + Clone, @@ -1565,7 +1565,7 @@ async fn handle_actions< + SubsystemSender + SubsystemSender + SubsystemSender - + SubsystemSender + + SubsystemSender + Clone, ADSender: SubsystemSender, >( @@ -2032,7 +2032,7 @@ async fn handle_from_overseer< Sender: SubsystemSender + SubsystemSender + SubsystemSender - + SubsystemSender + + SubsystemSender + Clone, ADSender: SubsystemSender, >( @@ -2909,7 +2909,7 @@ async fn import_approval( ) -> SubsystemResult<(Vec, ApprovalCheckResult)> where Sender: SubsystemSender - + SubsystemSender, + + SubsystemSender, { macro_rules! respond_early { ($e: expr) => {{ @@ -3063,7 +3063,7 @@ async fn advance_approval_state( ) -> Vec where Sender: SubsystemSender - + SubsystemSender, + + SubsystemSender, { let validator_index = transition.validator_index(); @@ -3252,7 +3252,7 @@ where if status.no_show_validators.len() > 0 { _ = sender - .try_send_message(ConsensusStatisticsCollectorMessage::NoShows( + .try_send_message(RewardsStatisticsCollectorMessage::NoShows( candidate_entry.candidate.hash(), block_hash, status.no_show_validators, @@ -3326,7 +3326,7 @@ async fn process_wakeup( ) -> SubsystemResult> where Sender: SubsystemSender - + SubsystemSender + + SubsystemSender { let block_entry = db.load_block_entry(&relay_block)?; let candidate_entry = db.load_candidate_entry(&candidate_hash)?; @@ -3736,7 +3736,7 @@ async fn launch_approval< #[overseer::contextbounds(ApprovalVoting, prefix = self::overseer)] async fn issue_approval< Sender: SubsystemSender + - SubsystemSender, + SubsystemSender, ADSender: SubsystemSender, >( sender: &mut Sender, @@ -4119,7 +4119,7 @@ fn collect_useful_approvals( candidate_entry: &CandidateEntry, ) where - Sender: SubsystemSender + Sender: SubsystemSender { let candidate_hash = candidate_entry.candidate.hash(); let candidate_approvals = candidate_entry.approvals(); @@ -4167,7 +4167,7 @@ where "collected useful approvals" ); - _ = sender.try_send_message(ConsensusStatisticsCollectorMessage::CandidateApproved( + _ = sender.try_send_message(RewardsStatisticsCollectorMessage::CandidateApproved( candidate_hash, block_hash, collected_useful_approvals, diff --git a/polkadot/node/core/approval-voting/src/tests.rs b/polkadot/node/core/approval-voting/src/tests.rs index a268091037fa4..cb25ca50e552a 100644 --- a/polkadot/node/core/approval-voting/src/tests.rs +++ b/polkadot/node/core/approval-voting/src/tests.rs @@ -708,7 +708,7 @@ async fn import_approval( assert_matches!( overseer_recv(overseer).await, AllMessages::ConsensusStatisticsCollector( - ConsensusStatisticsCollectorMessage::CandidateApproved( + RewardsStatisticsCollectorMessage::CandidateApproved( c_hash, b_hash, validators, ) ) => { @@ -3857,7 +3857,7 @@ fn pre_covers_dont_stall_approval() { assert_matches!( overseer_recv(&mut virtual_overseer).await, - AllMessages::ConsensusStatisticsCollector(ConsensusStatisticsCollectorMessage::CandidateApproved( + AllMessages::ConsensusStatisticsCollector(RewardsStatisticsCollectorMessage::CandidateApproved( c_hash, b_hash, validators, )) => { assert_eq!(b_hash, block_hash); @@ -3868,7 +3868,7 @@ fn pre_covers_dont_stall_approval() { assert_matches!( overseer_recv(&mut virtual_overseer).await, - AllMessages::ConsensusStatisticsCollector(ConsensusStatisticsCollectorMessage::NoShows( + AllMessages::ConsensusStatisticsCollector(RewardsStatisticsCollectorMessage::NoShows( session_idx, validators, )) => { assert_eq!(session_idx, 1); @@ -4044,7 +4044,7 @@ fn waits_until_approving_assignments_are_old_enough() { assert_matches!( overseer_recv(&mut virtual_overseer).await, AllMessages::ConsensusStatisticsCollector( - ConsensusStatisticsCollectorMessage::CandidateApproved( + RewardsStatisticsCollectorMessage::CandidateApproved( c_hash, b_hash, validators ) ) => { diff --git a/polkadot/node/core/rewards-statistics-collector/src/lib.rs b/polkadot/node/core/rewards-statistics-collector/src/lib.rs index 73a6b49c1c611..f0525320fac39 100644 --- a/polkadot/node/core/rewards-statistics-collector/src/lib.rs +++ b/polkadot/node/core/rewards-statistics-collector/src/lib.rs @@ -29,7 +29,7 @@ use gum::CandidateHash; use sp_keystore::KeystorePtr; use polkadot_node_subsystem::{ errors::RuntimeApiError as RuntimeApiSubsystemError, - messages::{ChainApiMessage, ConsensusStatisticsCollectorMessage, RuntimeApiMessage, RuntimeApiRequest}, + messages::{ChainApiMessage, RewardsStatisticsCollectorMessage, RuntimeApiMessage, RuntimeApiRequest}, overseer, ActiveLeavesUpdate, FromOrchestra, OverseerSignal, SpawnedSubsystem, SubsystemError, SubsystemSender }; use polkadot_primitives::{ @@ -66,7 +66,7 @@ const LOG_TARGET: &str = "parachain::rewards-statistics-collector"; #[derive(Default)] pub struct Config { - pub publish_per_validator_approval_metrics: bool + pub verbose_approval_metrics: bool } struct PerRelayView { @@ -174,7 +174,7 @@ where { fn start(self, ctx: Context) -> SpawnedSubsystem { SpawnedSubsystem { - future: run(ctx, (self.metrics, self.config.publish_per_validator_approval_metrics)) + future: run(ctx, (self.metrics, self.config.verbose_approval_metrics)) .map_err(|e| SubsystemError::with_origin("statistics-parachains", e)) .boxed(), name: "rewards-statistics-collector-subsystem", @@ -309,7 +309,7 @@ pub(crate) async fn run_iteration( } FromOrchestra::Communication { msg } => { match msg { - ConsensusStatisticsCollectorMessage::ChunksDownloaded( + RewardsStatisticsCollectorMessage::ChunksDownloaded( session_index, candidate_hash, downloads, @@ -321,7 +321,7 @@ pub(crate) async fn run_iteration( downloads, ) }, - ConsensusStatisticsCollectorMessage::ChunkUploaded( + RewardsStatisticsCollectorMessage::ChunkUploaded( candidate_hash, authority_ids, ) => { @@ -331,7 +331,7 @@ pub(crate) async fn run_iteration( authority_ids, ) }, - ConsensusStatisticsCollectorMessage::CandidateApproved( + RewardsStatisticsCollectorMessage::CandidateApproved( candidate_hash, block_hash, approvals, @@ -343,7 +343,7 @@ pub(crate) async fn run_iteration( approvals, ); } - ConsensusStatisticsCollectorMessage::NoShows( + RewardsStatisticsCollectorMessage::NoShows( candidate_hash, block_hash, no_show_validators, diff --git a/polkadot/node/core/rewards-statistics-collector/src/tests.rs b/polkadot/node/core/rewards-statistics-collector/src/tests.rs index a5d209affbdc7..f3748af216396 100644 --- a/polkadot/node/core/rewards-statistics-collector/src/tests.rs +++ b/polkadot/node/core/rewards-statistics-collector/src/tests.rs @@ -19,10 +19,10 @@ use super::*; use assert_matches::assert_matches; use overseer::FromOrchestra; use polkadot_primitives::{AssignmentId, GroupIndex, SessionIndex, SessionInfo}; -use polkadot_node_subsystem::messages::{AllMessages, ChainApiResponseChannel, ConsensusStatisticsCollectorMessage, RuntimeApiMessage, RuntimeApiRequest}; +use polkadot_node_subsystem::messages::{AllMessages, ChainApiResponseChannel, RewardsStatisticsCollectorMessage, RuntimeApiMessage, RuntimeApiRequest}; type VirtualOverseer = - polkadot_node_subsystem_test_helpers::TestSubsystemContextHandle; + polkadot_node_subsystem_test_helpers::TestSubsystemContextHandle; use polkadot_node_subsystem::{ActivatedLeaf}; use polkadot_node_subsystem_test_helpers as test_helpers; use polkadot_primitives::{Hash, Header}; @@ -93,7 +93,7 @@ async fn candidate_approved( approvals: Vec, ) { let msg = FromOrchestra::Communication { - msg: ConsensusStatisticsCollectorMessage::CandidateApproved( + msg: RewardsStatisticsCollectorMessage::CandidateApproved( candidate_hash.clone(), rb_hash.clone(), approvals, @@ -109,7 +109,7 @@ async fn no_shows( no_shows: Vec, ) { let msg = FromOrchestra::Communication { - msg: ConsensusStatisticsCollectorMessage::NoShows( + msg: RewardsStatisticsCollectorMessage::NoShows( candidate_hash.clone(), rb_hash.clone(), no_shows, @@ -335,7 +335,7 @@ fn note_chunks_downloaded() { let mut view = View::new(); test_harness(&mut view, |mut virtual_overseer| async move { virtual_overseer.send(FromOrchestra::Communication { - msg: ConsensusStatisticsCollectorMessage::ChunksDownloaded( + msg: RewardsStatisticsCollectorMessage::ChunksDownloaded( session_idx, candidate_hash.clone(), HashMap::from_iter(chunk_downloads.clone().into_iter()), ), }).await; @@ -345,7 +345,7 @@ fn note_chunks_downloaded() { (ValidatorIndex(0), 5u64) ]; virtual_overseer.send(FromOrchestra::Communication { - msg: ConsensusStatisticsCollectorMessage::ChunksDownloaded( + msg: RewardsStatisticsCollectorMessage::ChunksDownloaded( session_idx, candidate_hash.clone(), HashMap::from_iter(second_round_of_downloads.into_iter()), ), }).await; @@ -435,7 +435,7 @@ fn note_chunks_uploaded_to_active_validator() { ).await; virtual_overseer.send(FromOrchestra::Communication { - msg: ConsensusStatisticsCollectorMessage::ChunkUploaded( + msg: RewardsStatisticsCollectorMessage::ChunkUploaded( candidate_hash.clone(), HashSet::from_iter(vec![validator_idx_auth_id.clone()]), ), }).await; diff --git a/polkadot/node/core/runtime-api/src/cache.rs b/polkadot/node/core/runtime-api/src/cache.rs index 2f4ad8bdea101..9c09ea3f22a9e 100644 --- a/polkadot/node/core/runtime-api/src/cache.rs +++ b/polkadot/node/core/runtime-api/src/cache.rs @@ -662,7 +662,6 @@ pub(crate) enum RequestResult { // This is a request with side-effects and no result, hence (). #[allow(dead_code)] SubmitPvfCheckStatement(()), - SubmitApprovalStatistics(()), ValidationCodeHash(Hash, ParaId, OccupiedCoreAssumption, Option), Version(Hash, u32), Disputes(Hash, Vec<(SessionIndex, CandidateHash, DisputeState)>), diff --git a/polkadot/node/core/runtime-api/src/lib.rs b/polkadot/node/core/runtime-api/src/lib.rs index ee95cb33fe401..95856e66ce3ca 100644 --- a/polkadot/node/core/runtime-api/src/lib.rs +++ b/polkadot/node/core/runtime-api/src/lib.rs @@ -157,7 +157,6 @@ where PvfsRequirePrecheck(relay_parent, pvfs) => self.requests_cache.cache_pvfs_require_precheck(relay_parent, pvfs), SubmitPvfCheckStatement(()) => {}, - SubmitApprovalStatistics(()) => {}, ValidationCodeHash(relay_parent, para_id, assumption, hash) => self .requests_cache .cache_validation_code_hash((relay_parent, para_id, assumption), hash), diff --git a/polkadot/node/network/availability-distribution/src/responder.rs b/polkadot/node/network/availability-distribution/src/responder.rs index 8c269a50fbedc..9ac0b22cbae87 100644 --- a/polkadot/node/network/availability-distribution/src/responder.rs +++ b/polkadot/node/network/availability-distribution/src/responder.rs @@ -29,7 +29,7 @@ use polkadot_node_network_protocol::{ }; use polkadot_node_primitives::{AvailableData, ErasureChunk}; use polkadot_node_subsystem::{messages::AvailabilityStoreMessage, SubsystemSender}; -use polkadot_node_subsystem::messages::ConsensusStatisticsCollectorMessage; +use polkadot_node_subsystem::messages::RewardsStatisticsCollectorMessage; use polkadot_primitives::{CandidateHash, ValidatorIndex}; use crate::{ @@ -77,7 +77,7 @@ pub async fn run_chunk_receivers( metrics: Metrics, ) where Sender: SubsystemSender - + SubsystemSender, + + SubsystemSender, AD: AuthorityDiscovery + Clone + Sync { let make_resp_v1 = |chunk: Option| match chunk { @@ -174,7 +174,7 @@ pub async fn answer_chunk_request_log( Req: IsRequest + Decode + Encode + Into, Req::Response: Encode, Sender: SubsystemSender - + SubsystemSender, + + SubsystemSender, MakeResp: Fn(Option) -> Req::Response, { let res = answer_chunk_request(sender, authority_discovery, req, make_response).await; @@ -231,7 +231,7 @@ pub async fn answer_chunk_request( where AD: AuthorityDiscovery, Sender: SubsystemSender - + SubsystemSender, + + SubsystemSender, Req: IsRequest + Decode + Encode + Into, Req::Response: Encode, MakeResp: Fn(Option) -> Req::Response, @@ -248,7 +248,7 @@ where let authority_ids = authority_discovery.get_authority_ids_by_peer_id(req.peer).await; if let Some(authority_ids) = authority_ids { _ = sender.try_send_message( - ConsensusStatisticsCollectorMessage::ChunkUploaded(payload.candidate_hash, authority_ids)); + RewardsStatisticsCollectorMessage::ChunkUploaded(payload.candidate_hash, authority_ids)); } } diff --git a/polkadot/node/network/availability-recovery/src/task/mod.rs b/polkadot/node/network/availability-recovery/src/task/mod.rs index 16a673a82e8da..ba4f131067c78 100644 --- a/polkadot/node/network/availability-recovery/src/task/mod.rs +++ b/polkadot/node/network/availability-recovery/src/task/mod.rs @@ -38,7 +38,7 @@ use sc_network::ProtocolName; use futures::channel::{mpsc, oneshot}; use std::collections::VecDeque; -use polkadot_node_subsystem::messages::ConsensusStatisticsCollectorMessage; +use polkadot_node_subsystem::messages::RewardsStatisticsCollectorMessage; /// Recovery parameters common to all strategies in a `RecoveryTask`. #[derive(Clone)] @@ -101,7 +101,7 @@ pub struct RecoveryTask { impl RecoveryTask where Sender: overseer::AvailabilityRecoverySenderTrait - + SubsystemSender, + + SubsystemSender, { /// Instantiate a new recovery task. pub fn new( @@ -185,7 +185,7 @@ where Ok(data) => { self.params.metrics.on_recovery_succeeded(strategy_type, data.encoded_size()); _ = self.sender.try_send_message( - ConsensusStatisticsCollectorMessage::ChunksDownloaded( + RewardsStatisticsCollectorMessage::ChunksDownloaded( self.params.session_index, self.params.candidate_hash, self.state.get_download_chunks_metrics())); diff --git a/polkadot/node/network/availability-recovery/src/task/strategy/chunks.rs b/polkadot/node/network/availability-recovery/src/task/strategy/chunks.rs index 2f6e191125eed..e618c7f19b3ec 100644 --- a/polkadot/node/network/availability-recovery/src/task/strategy/chunks.rs +++ b/polkadot/node/network/availability-recovery/src/task/strategy/chunks.rs @@ -33,7 +33,7 @@ use polkadot_primitives::ValidatorIndex; use futures::{channel::oneshot, SinkExt}; use rand::seq::SliceRandom; use std::collections::VecDeque; -use polkadot_node_subsystem::messages::ConsensusStatisticsCollectorMessage; +use polkadot_node_subsystem::messages::RewardsStatisticsCollectorMessage; /// Parameters specific to the `FetchChunks` strategy. pub struct FetchChunksParams { diff --git a/polkadot/node/network/availability-recovery/src/task/strategy/systematic.rs b/polkadot/node/network/availability-recovery/src/task/strategy/systematic.rs index 206bf2d05372c..f0fb21fe7ca81 100644 --- a/polkadot/node/network/availability-recovery/src/task/strategy/systematic.rs +++ b/polkadot/node/network/availability-recovery/src/task/strategy/systematic.rs @@ -31,7 +31,7 @@ use polkadot_node_subsystem::{overseer, RecoveryError}; use polkadot_primitives::{ChunkIndex, ValidatorIndex}; use std::collections::VecDeque; -use polkadot_node_subsystem::messages::ConsensusStatisticsCollectorMessage; +use polkadot_node_subsystem::messages::RewardsStatisticsCollectorMessage; /// Parameters needed for fetching systematic chunks. pub struct FetchSystematicChunksParams { diff --git a/polkadot/node/network/protocol/src/lib.rs b/polkadot/node/network/protocol/src/lib.rs index 247564e8996d9..fa9effe440b12 100644 --- a/polkadot/node/network/protocol/src/lib.rs +++ b/polkadot/node/network/protocol/src/lib.rs @@ -429,6 +429,7 @@ impl_versioned_validation_try_from!( VersionedValidationProtocol, ApprovalDistributionMessage, v3::ValidationProtocol::ApprovalDistribution(x) => x + ); /// Version-annotated messages used by the gossip-support subsystem (this is void). diff --git a/polkadot/node/overseer/src/dummy.rs b/polkadot/node/overseer/src/dummy.rs index 7584e35b748cf..c23429082190c 100644 --- a/polkadot/node/overseer/src/dummy.rs +++ b/polkadot/node/overseer/src/dummy.rs @@ -166,7 +166,7 @@ where + Subsystem, SubsystemError> + Subsystem, SubsystemError> + Subsystem, SubsystemError> - + Subsystem, SubsystemError>, + + Subsystem, SubsystemError>, { let metrics = ::register(registry)?; diff --git a/polkadot/node/overseer/src/lib.rs b/polkadot/node/overseer/src/lib.rs index 0893006500407..a375f0d793813 100644 --- a/polkadot/node/overseer/src/lib.rs +++ b/polkadot/node/overseer/src/lib.rs @@ -85,7 +85,7 @@ use polkadot_node_subsystem_types::messages::{ DisputeCoordinatorMessage, DisputeDistributionMessage, GossipSupportMessage, NetworkBridgeRxMessage, NetworkBridgeTxMessage, ProspectiveParachainsMessage, ProvisionerMessage, RuntimeApiMessage, StatementDistributionMessage, - ConsensusStatisticsCollectorMessage, + RewardsStatisticsCollectorMessage, }; pub use polkadot_node_subsystem_types::{ @@ -519,7 +519,7 @@ pub struct Overseer { #[subsystem(AvailabilityDistributionMessage, sends: [ AvailabilityStoreMessage, ChainApiMessage, - ConsensusStatisticsCollectorMessage, + RewardsStatisticsCollectorMessage, RuntimeApiMessage, NetworkBridgeTxMessage, ])] @@ -529,7 +529,7 @@ pub struct Overseer { NetworkBridgeTxMessage, RuntimeApiMessage, AvailabilityStoreMessage, - ConsensusStatisticsCollectorMessage, + RewardsStatisticsCollectorMessage, ])] availability_recovery: AvailabilityRecovery, @@ -610,7 +610,7 @@ pub struct Overseer { CandidateValidationMessage, ChainApiMessage, ChainSelectionMessage, - ConsensusStatisticsCollectorMessage, + RewardsStatisticsCollectorMessage, DisputeCoordinatorMessage, RuntimeApiMessage, ])] @@ -620,7 +620,7 @@ pub struct Overseer { CandidateValidationMessage, ChainApiMessage, ChainSelectionMessage, - ConsensusStatisticsCollectorMessage, + RewardsStatisticsCollectorMessage, DisputeCoordinatorMessage, RuntimeApiMessage, NetworkBridgeTxMessage, @@ -664,7 +664,7 @@ pub struct Overseer { ])] prospective_parachains: ProspectiveParachains, - #[subsystem(ConsensusStatisticsCollectorMessage, sends: [ + #[subsystem(RewardsStatisticsCollectorMessage, sends: [ RuntimeApiMessage, ChainApiMessage, ])] diff --git a/polkadot/node/service/src/builder/mod.rs b/polkadot/node/service/src/builder/mod.rs index 2da0d1cac7e48..3d244f52d4620 100644 --- a/polkadot/node/service/src/builder/mod.rs +++ b/polkadot/node/service/src/builder/mod.rs @@ -81,7 +81,7 @@ pub struct NewFullParams { /// Whether the node is attempting to run as a secure validator. pub secure_validator_mode: bool, /// Whether the node will publish collected approval metrics per validator - pub publish_per_validator_approval_metrics: bool, + pub verbose_approval_metrics: bool, /// An optional path to a directory containing the workers. pub workers_path: Option, /// Optional custom names for the prepare and execute workers. @@ -203,7 +203,7 @@ where telemetry_worker_handle: _, node_version, secure_validator_mode, - publish_per_validator_approval_metrics, + verbose_approval_metrics, workers_path, workers_names, overseer_gen, @@ -444,7 +444,7 @@ where }; let consensus_statistics_collector_config = RewardsStatisticsCollectorConfig{ - publish_per_validator_approval_metrics, + verbose_approval_metrics, }; Some(ExtendedOverseerGenArgs { diff --git a/polkadot/node/subsystem-types/src/messages.rs b/polkadot/node/subsystem-types/src/messages.rs index f11721b3f28fc..1845c1549ce81 100644 --- a/polkadot/node/subsystem-types/src/messages.rs +++ b/polkadot/node/subsystem-types/src/messages.rs @@ -1480,7 +1480,7 @@ pub enum ProspectiveParachainsMessage { /// Messages sent to the Statistics Collector subsystem. #[derive(Debug)] -pub enum ConsensusStatisticsCollectorMessage { +pub enum RewardsStatisticsCollectorMessage { ChunksDownloaded(SessionIndex, CandidateHash, HashMap), ChunkUploaded(CandidateHash, HashSet), diff --git a/polkadot/parachain/test-parachains/adder/collator/src/main.rs b/polkadot/parachain/test-parachains/adder/collator/src/main.rs index 64ff9b0e5ca18..4c9b353faa944 100644 --- a/polkadot/parachain/test-parachains/adder/collator/src/main.rs +++ b/polkadot/parachain/test-parachains/adder/collator/src/main.rs @@ -101,7 +101,7 @@ fn main() -> Result<()> { keep_finalized_for: None, invulnerable_ah_collators: HashSet::new(), collator_protocol_hold_off: None, - publish_per_validator_approval_metrics: false, + verbose_approval_metrics: false, }, ) .map_err(|e| e.to_string())?; From f5352e2d287a9af7a92f1f047f365c14e8255100 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Wed, 19 Nov 2025 11:09:14 -0300 Subject: [PATCH 38/48] chore: addressing comments --- polkadot/primitives/src/vstaging/mod.rs | 29 ------------------------- 1 file changed, 29 deletions(-) diff --git a/polkadot/primitives/src/vstaging/mod.rs b/polkadot/primitives/src/vstaging/mod.rs index fccc3f6b7c128..72c39023c7e1f 100644 --- a/polkadot/primitives/src/vstaging/mod.rs +++ b/polkadot/primitives/src/vstaging/mod.rs @@ -15,32 +15,3 @@ // along with Polkadot. If not, see . //! Staging Primitives. - -use scale_info::TypeInfo; -use sp_api::__private::{Decode, Encode}; -use sp_application_crypto::RuntimeDebug; -use sp_core::DecodeWithMemTracking; -use sp_staking::SessionIndex; -use crate::ValidatorIndex; - -/// A reward tally line represent the collected statistics about -/// approvals voting for a given validator, how much successful approvals -/// was collected and how many times the given validator no-showed -#[derive(Encode, Decode, DecodeWithMemTracking, Clone, Copy, PartialEq, RuntimeDebug, TypeInfo)] -pub struct ApprovalStatisticsTallyLine { - pub validator_index: ValidatorIndex, - pub approvals_usage: u32, - pub no_shows: u32, -} - -/// ApprovalRewards is the set of tallies where each tally represents -/// a given validator and its approval voting statistics -#[derive(Clone, Encode, Decode, TypeInfo, RuntimeDebug)] -pub struct ApprovalStatistics(SessionIndex, Vec); - -impl ApprovalStatistics { - pub fn signing_payload(&self) -> Vec { - const MAGIC: [u8; 4] = *b"APST"; // for "approval statistics" - (MAGIC, self.0, self.1.clone()).encode() - } -} \ No newline at end of file From 28ce1ef38662fb79bcffbba56749efa5d71071f0 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Wed, 19 Nov 2025 11:12:05 -0300 Subject: [PATCH 39/48] chore: addressing comments --- .../rewards-statistics-collector/src/lib.rs | 84 ------------------- polkadot/node/core/runtime-api/src/lib.rs | 14 ---- polkadot/node/subsystem-types/src/messages.rs | 4 +- 3 files changed, 1 insertion(+), 101 deletions(-) diff --git a/polkadot/node/core/rewards-statistics-collector/src/lib.rs b/polkadot/node/core/rewards-statistics-collector/src/lib.rs index f0525320fac39..b81eab315070f 100644 --- a/polkadot/node/core/rewards-statistics-collector/src/lib.rs +++ b/polkadot/node/core/rewards-statistics-collector/src/lib.rs @@ -56,7 +56,6 @@ pub mod metrics; use approval_voting_metrics::ApprovalsStats; use polkadot_node_subsystem::RuntimeApiError::{Execution, NotSupported}; use polkadot_node_subsystem_util::{request_candidate_events, request_session_index_for_child, request_session_info}; -use polkadot_primitives::vstaging::{ApprovalStatisticsTallyLine, ApprovalStatistics}; use crate::approval_voting_metrics::{handle_candidate_approved, handle_observed_no_shows}; use crate::availability_distribution_metrics::{handle_chunk_uploaded, handle_chunks_downloaded, AvailabilityChunks}; use self::metrics::Metrics; @@ -478,89 +477,6 @@ fn log_session_view_general_stats(view: &View) { } } -async fn sign_and_submit_approvals_tallies( - sender: &mut impl SubsystemSender, - relay_parent: Hash, - session_index: SessionIndex, - keystore: &KeystorePtr, - credentials: &SigningCredentials, - metrics: &Metrics, - tallies: HashMap, -) { - gum::debug!( - target: LOG_TARGET, - ?relay_parent, - "submitting {} approvals tallies for session {}", - tallies.len(), - session_index, - ); - - metrics.submit_approvals_tallies(tallies.len()); - - let mut validators_indexes = tallies.keys().collect::>(); - validators_indexes.sort(); - - let mut approvals_tallies: Vec = Vec::with_capacity(tallies.len()); - for validator_index in validators_indexes { - let current_tally = tallies.get(validator_index).unwrap(); - approvals_tallies.push(ApprovalStatisticsTallyLine { - validator_index: validator_index.clone(), - approvals_usage: current_tally.approvals, - no_shows: current_tally.no_shows, - }); - } - - let payload = ApprovalStatistics(session_index, approvals_tallies); - - let signature = match polkadot_node_subsystem_util::sign( - keystore, - &credentials.validator_key, - &payload.signing_payload(), - ) { - Ok(Some(signature)) => signature, - Ok(None) => { - gum::warn!( - target: LOG_TARGET, - ?relay_parent, - validator_index = ?credentials.validator_index, - "private key for signing is not available", - ); - return - }, - Err(e) => { - gum::warn!( - target: LOG_TARGET, - ?relay_parent, - validator_index = ?credentials.validator_index, - "error signing the statement: {:?}", - e, - ); - return - }, - }; - - let (tx, rx) = oneshot::channel(); - let runtime_req = runtime_api_request( - sender, - relay_parent, - RuntimeApiRequest::SubmitApprovalStatistics(payload, signature, tx), - rx, - ); - - match runtime_req.await { - Ok(()) => { - metrics.on_vote_submitted(); - }, - Err(e) => { - gum::warn!( - target: LOG_TARGET, - "error occurred during submitting a approvals rewards tallies: {:?}", - e, - ); - }, - } -} - #[derive(Debug)] pub(crate) enum RuntimeRequestError { NotSupported, diff --git a/polkadot/node/core/runtime-api/src/lib.rs b/polkadot/node/core/runtime-api/src/lib.rs index 95856e66ce3ca..5b7be703ae6b8 100644 --- a/polkadot/node/core/runtime-api/src/lib.rs +++ b/polkadot/node/core/runtime-api/src/lib.rs @@ -33,7 +33,6 @@ use polkadot_primitives::Hash; use cache::{RequestResult, RequestResultCache}; use futures::{channel::oneshot, prelude::*, select, stream::FuturesUnordered}; use std::sync::Arc; -use polkadot_node_subsystem_types::messages::RuntimeApiRequest::SubmitApprovalStatistics; mod cache; @@ -310,10 +309,6 @@ where // This request is side-effecting and thus cannot be cached. Some(request) }, - request @ Request::SubmitApprovalStatistics(_, _, _) => { - // This request is side-effecting and thus cannot be cached. - Some(request) - }, Request::ValidationCodeHash(para, assumption, sender) => query!(validation_code_hash(para, assumption), sender) .map(|sender| Request::ValidationCodeHash(para, assumption, sender)), @@ -624,15 +619,6 @@ where result = () ) }, - Request::SubmitApprovalStatistics(payload, sig, sender) => { - query!( - SubmitApprovalStatistics, - submit_approval_statistics(payload, sig), - ver = 2, - sender, - result = () - ) - } Request::PvfsRequirePrecheck(sender) => { query!(PvfsRequirePrecheck, pvfs_require_precheck(), ver = 2, sender) }, diff --git a/polkadot/node/subsystem-types/src/messages.rs b/polkadot/node/subsystem-types/src/messages.rs index 1845c1549ce81..ab656da126f61 100644 --- a/polkadot/node/subsystem-types/src/messages.rs +++ b/polkadot/node/subsystem-types/src/messages.rs @@ -483,7 +483,7 @@ pub enum NetworkBridgeTxMessage { } /// Availability Distribution Message. -#[derive(Debug, derive_more::From)] +#[derive(Debug)] pub enum AvailabilityDistributionMessage { /// Instruct availability distribution to fetch a remote PoV. /// @@ -734,8 +734,6 @@ pub enum RuntimeApiRequest { FetchOnChainVotes(RuntimeApiSender>), /// Submits a PVF pre-checking statement into the transaction pool. SubmitPvfCheckStatement(PvfCheckStatement, ValidatorSignature, RuntimeApiSender<()>), - /// Submits the collected approvals statistics for a given session into the transaction pool. - SubmitApprovalStatistics(ApprovalStatistics, ValidatorSignature, RuntimeApiSender<()>), /// Returns code hashes of PVFs that require pre-checking by validators in the active set. PvfsRequirePrecheck(RuntimeApiSender>), /// Get the validation code used by the specified para, taking the given From cbdc2bc772fa9e458cc3e96069ef2c4592997b38 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Wed, 19 Nov 2025 11:13:46 -0300 Subject: [PATCH 40/48] chore: removing unused imports --- Cargo.lock | 1 - polkadot/node/subsystem-types/Cargo.toml | 1 - polkadot/node/subsystem-types/src/messages.rs | 3 --- 3 files changed, 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bc325c48d373b..c2bc0044421b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16067,7 +16067,6 @@ name = "polkadot-node-subsystem-types" version = "7.0.0" dependencies = [ "async-trait", - "bitvec", "derive_more 0.99.17", "fatality", "futures", diff --git a/polkadot/node/subsystem-types/Cargo.toml b/polkadot/node/subsystem-types/Cargo.toml index 3760735932286..aa0efcec6a74c 100644 --- a/polkadot/node/subsystem-types/Cargo.toml +++ b/polkadot/node/subsystem-types/Cargo.toml @@ -12,7 +12,6 @@ repository.workspace = true workspace = true [dependencies] -bitvec = { features = ["alloc"], workspace = true } async-trait = { workspace = true } derive_more = { workspace = true, default-features = true } fatality = { workspace = true } diff --git a/polkadot/node/subsystem-types/src/messages.rs b/polkadot/node/subsystem-types/src/messages.rs index ab656da126f61..92b6829b68149 100644 --- a/polkadot/node/subsystem-types/src/messages.rs +++ b/polkadot/node/subsystem-types/src/messages.rs @@ -65,9 +65,6 @@ use bitvec::{order::Lsb0 as BitOrderLsb0, slice::BitSlice}; /// Network events as transmitted to other subsystems, wrapped in their message types. pub mod network_bridge_event; pub use network_bridge_event::NetworkBridgeEvent; -use polkadot_node_primitives::approval::time::Tick; -use polkadot_node_primitives::approval::v2::Bitfield; -use polkadot_primitives::vstaging::ApprovalStatistics; /// A request to the candidate backing subsystem to check whether /// we can second this candidate. From f339452b7ce145a4bb26df065538626bb0dc52d6 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Mon, 24 Nov 2025 08:38:53 -0400 Subject: [PATCH 41/48] chore: fix `pre_covers_dont_stall_approval` test --- .../node/core/approval-voting/src/tests.rs | 5 ++- .../src/availability_distribution_metrics.rs | 9 ++--- .../rewards-statistics-collector/src/lib.rs | 38 +++++++++---------- .../subsystem-bench/src/lib/approval/mod.rs | 2 +- polkadot/node/subsystem-types/src/messages.rs | 1 - .../rewards_statistics_collector.rs | 8 ++-- 6 files changed, 29 insertions(+), 34 deletions(-) diff --git a/polkadot/node/core/approval-voting/src/tests.rs b/polkadot/node/core/approval-voting/src/tests.rs index cb25ca50e552a..3951aabdc3b2f 100644 --- a/polkadot/node/core/approval-voting/src/tests.rs +++ b/polkadot/node/core/approval-voting/src/tests.rs @@ -3869,9 +3869,10 @@ fn pre_covers_dont_stall_approval() { assert_matches!( overseer_recv(&mut virtual_overseer).await, AllMessages::ConsensusStatisticsCollector(RewardsStatisticsCollectorMessage::NoShows( - session_idx, validators, + c_hash, b_hash, validators )) => { - assert_eq!(session_idx, 1); + assert_eq!(b_hash, block_hash); + assert_eq!(c_hash, candidate_hash); assert_eq!(validators, vec![validator_index_a]); } ); diff --git a/polkadot/node/core/rewards-statistics-collector/src/availability_distribution_metrics.rs b/polkadot/node/core/rewards-statistics-collector/src/availability_distribution_metrics.rs index 229db0f634efa..2a57c200ad4d6 100644 --- a/polkadot/node/core/rewards-statistics-collector/src/availability_distribution_metrics.rs +++ b/polkadot/node/core/rewards-statistics-collector/src/availability_distribution_metrics.rs @@ -92,16 +92,15 @@ pub fn handle_chunks_downloaded( } } +// handle_chunk_uploaded receive the authority ids of the peer +// it just uploaded the candidate hash, to collect this statistic +// it needs to find the validator index that is bounded to any of the +// authority id, from the oldest to newest session. pub fn handle_chunk_uploaded( view: &mut View, candidate_hash: CandidateHash, authority_ids: HashSet, ) { - // will look up in the stored sessions, - // from the most recent session to the oldest session - // to find the first validator index that matches - // with a single authority discovery id from the set - let mut sessions: Vec<(&SessionIndex, &PerSessionView)> = view.per_session.iter().collect(); sessions.sort_by(|(a, _), (b, _)| a.partial_cmp(&b).unwrap()); diff --git a/polkadot/node/core/rewards-statistics-collector/src/lib.rs b/polkadot/node/core/rewards-statistics-collector/src/lib.rs index b81eab315070f..1425fb0849a39 100644 --- a/polkadot/node/core/rewards-statistics-collector/src/lib.rs +++ b/polkadot/node/core/rewards-statistics-collector/src/lib.rs @@ -36,12 +36,10 @@ use polkadot_primitives::{ AuthorityDiscoveryId, BlockNumber, Hash, Header, SessionIndex, ValidatorId, ValidatorIndex, well_known_keys::relay_dispatch_queue_remaining_capacity }; -use polkadot_node_primitives::{ - approval::{ - time::Tick, - v1::DelayTranche - } -}; +use polkadot_node_primitives::{approval::{ + time::Tick, + v1::DelayTranche +}, SessionWindowSize, DISPUTE_WINDOW}; use crate::{ error::{FatalError, FatalResult, JfyiError, JfyiErrorResult, Result}, }; @@ -60,7 +58,7 @@ use crate::approval_voting_metrics::{handle_candidate_approved, handle_observed_ use crate::availability_distribution_metrics::{handle_chunk_uploaded, handle_chunks_downloaded, AvailabilityChunks}; use self::metrics::Metrics; -const MAX_SESSIONS_TO_KEEP: u32 = 2; +const MAX_SESSIONS_TO_KEEP: SessionWindowSize = DISPUTE_WINDOW; const LOG_TARGET: &str = "parachain::rewards-statistics-collector"; #[derive(Default)] @@ -118,22 +116,22 @@ impl PerSessionView { } } -/// A struct that holds the credentials required to sign the PVF check statements. These credentials -/// are implicitly to pinned to a session where our node acts as a validator. -struct SigningCredentials { - /// The validator public key. - validator_key: ValidatorId, - /// The validator index in the current session. - validator_index: ValidatorIndex, -} - +/// View holds the subsystem internal state struct View { + /// roots contains the only unfinalized relay hashes + /// is used when finalization happens to prune unneeded forks roots: HashSet, + /// per_relay holds collected approvals statistics for + /// all the candidates under the given unfinalized relay hash per_relay: HashMap, + /// per_session holds session information (authorities lookup) + /// and approvals tallies which is the aggregation of collected + /// approvals statistics under finalized blocks per_session: HashMap, + /// availability_chunks holds collected upload and download chunks + /// statistics per validator availability_chunks: HashMap, current_session: Option, - credentials: Option, } impl View { @@ -144,8 +142,7 @@ impl View { per_session: HashMap::new(), availability_chunks: HashMap::new(), current_session: None, - credentials: None, - }; + } } } @@ -379,7 +376,6 @@ fn prune_unfinalised_forks(view: &mut View, fin_block_hash: Hash) -> Vec { let mut current_block_hash = fin_block_hash; let mut current_parent_hash = rb_view.parent_hash; while let Some(parent_hash) = current_parent_hash { - match view.per_relay.get_mut(&parent_hash) { Some(parent_view) => { retain_relay_hashes.push(parent_hash.clone()); @@ -447,7 +443,7 @@ async fn prune_old_session_views( match view.current_session { Some(current_session) if current_session < session_idx => { - if let Some(wipe_before) = session_idx.checked_sub(MAX_SESSIONS_TO_KEEP) { + if let Some(wipe_before) = session_idx.checked_sub(MAX_SESSIONS_TO_KEEP.get()) { view.per_session.retain(|stored_session_index, _| *stored_session_index > wipe_before); } view.current_session = Some(session_idx) diff --git a/polkadot/node/subsystem-bench/src/lib/approval/mod.rs b/polkadot/node/subsystem-bench/src/lib/approval/mod.rs index f3d9e0809f9cc..178f566e022ba 100644 --- a/polkadot/node/subsystem-bench/src/lib/approval/mod.rs +++ b/polkadot/node/subsystem-bench/src/lib/approval/mod.rs @@ -858,7 +858,7 @@ fn build_overseer( let overseer_metrics = OverseerMetrics::try_register(&dependencies.registry).unwrap(); let task_handle = spawn_task_handle.clone(); - let collector_subsystem = ConsensusStatisticsCollectorSubsystem::default(); + let collector_subsystem = RewardsStatisticsCollectorSubsystem::default(); let dummy = dummy_builder!(task_handle, overseer_metrics) .replace_chain_api(|_| mock_chain_api) diff --git a/polkadot/node/subsystem-types/src/messages.rs b/polkadot/node/subsystem-types/src/messages.rs index 92b6829b68149..bef25b7766f81 100644 --- a/polkadot/node/subsystem-types/src/messages.rs +++ b/polkadot/node/subsystem-types/src/messages.rs @@ -60,7 +60,6 @@ use std::{ collections::{BTreeMap, HashMap, HashSet, VecDeque}, sync::Arc, }; -use bitvec::{order::Lsb0 as BitOrderLsb0, slice::BitSlice}; /// Network events as transmitted to other subsystems, wrapped in their message types. pub mod network_bridge_event; diff --git a/polkadot/zombienet-sdk-tests/tests/functional/rewards_statistics_collector.rs b/polkadot/zombienet-sdk-tests/tests/functional/rewards_statistics_collector.rs index f6dbadc14c2f8..1745d2f734413 100644 --- a/polkadot/zombienet-sdk-tests/tests/functional/rewards_statistics_collector.rs +++ b/polkadot/zombienet-sdk-tests/tests/functional/rewards_statistics_collector.rs @@ -6,8 +6,7 @@ use std::ops::Range; use anyhow::anyhow; -use cumulus_zombienet_sdk_helpers::{assert_para_throughput, wait_for_nth_session_change, - report_label_with_attributes}; +use cumulus_zombienet_sdk_helpers::{assert_para_throughput, wait_for_nth_session_change, report_label_with_attributes, assert_finality_lag}; use polkadot_primitives::{Id as ParaId, SessionIndex}; use serde_json::json; use subxt::{OnlineClient, PolkadotConfig}; @@ -88,8 +87,9 @@ async fn rewards_statistics_collector_test() -> Result<(), anyhow::Error> { ) .await?; - // wait for a session to be finalized - wait_for_nth_session_change(&mut blocks_sub, 1).await; + // Assert the parachain finalized block height is also on par with the number of backed + // candidates. We can only do this for the collator based on cumulus. + assert_finality_lag(¶_node_2001.wait_client().await?, 6).await?; assert_approval_usages_medians( 1, From 9a37b2a705d0ce16f198390ea61b9121c8b20279 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Mon, 24 Nov 2025 21:39:59 -0400 Subject: [PATCH 42/48] chore: solved zombienet tests --- .../zombienet-sdk-helpers/src/lib.rs | 2 +- .../rewards_statistics_collector.rs | 7 +- .../rewards_statistics_mixed_validators.rs | 156 +++++++++++++++--- .../tests/smoke/coretime_revenue.rs | 49 +++--- 4 files changed, 162 insertions(+), 52 deletions(-) diff --git a/cumulus/zombienet/zombienet-sdk-helpers/src/lib.rs b/cumulus/zombienet/zombienet-sdk-helpers/src/lib.rs index 32dac61666c67..64a6004ddd33a 100644 --- a/cumulus/zombienet/zombienet-sdk-helpers/src/lib.rs +++ b/cumulus/zombienet/zombienet-sdk-helpers/src/lib.rs @@ -53,7 +53,7 @@ pub fn create_assign_core_call(core_and_para: &[(u32, u32)]) -> DynamicPayload { } /// Find an event in subxt `Events` and attempt to decode the fields fo the event. -fn find_event_and_decode_fields( +pub fn find_event_and_decode_fields( events: &Events, pallet: &str, variant: &str, diff --git a/polkadot/zombienet-sdk-tests/tests/functional/rewards_statistics_collector.rs b/polkadot/zombienet-sdk-tests/tests/functional/rewards_statistics_collector.rs index 1745d2f734413..34cedc008ef8a 100644 --- a/polkadot/zombienet-sdk-tests/tests/functional/rewards_statistics_collector.rs +++ b/polkadot/zombienet-sdk-tests/tests/functional/rewards_statistics_collector.rs @@ -29,7 +29,10 @@ async fn rewards_statistics_collector_test() -> Result<(), anyhow::Error> { .with_chain("rococo-local") .with_default_command("polkadot") .with_default_image(images.polkadot.as_str()) - .with_default_args(vec![("-lparachain=debug").into()]) + .with_default_args(vec![ + ("-lparachain=debug").into(), + ("--verbose-approval-metrics=true").into(), + ]) .with_genesis_overrides(json!({ "configuration": { "config": { @@ -89,7 +92,7 @@ async fn rewards_statistics_collector_test() -> Result<(), anyhow::Error> { // Assert the parachain finalized block height is also on par with the number of backed // candidates. We can only do this for the collator based on cumulus. - assert_finality_lag(¶_node_2001.wait_client().await?, 6).await?; + assert_finality_lag(&relay_client, 6).await?; assert_approval_usages_medians( 1, diff --git a/polkadot/zombienet-sdk-tests/tests/functional/rewards_statistics_mixed_validators.rs b/polkadot/zombienet-sdk-tests/tests/functional/rewards_statistics_mixed_validators.rs index 0cca4f75e9007..cf918cff6e03a 100644 --- a/polkadot/zombienet-sdk-tests/tests/functional/rewards_statistics_mixed_validators.rs +++ b/polkadot/zombienet-sdk-tests/tests/functional/rewards_statistics_mixed_validators.rs @@ -4,17 +4,18 @@ // Test that nodes fetch availability chunks early for scheduled cores and normally for occupied // core. +use std::collections::HashMap; use std::ops::Range; use anyhow::anyhow; -use cumulus_zombienet_sdk_helpers::{assert_para_throughput, wait_for_nth_session_change, - report_label_with_attributes}; -use polkadot_primitives::{Id as ParaId, SessionIndex}; +use cumulus_zombienet_sdk_helpers::{assert_para_throughput, wait_for_nth_session_change, report_label_with_attributes, assert_finality_lag, wait_for_first_session_change, find_event_and_decode_fields}; +use polkadot_primitives::{CandidateReceiptV2, Id as ParaId, SessionIndex}; use serde_json::json; use subxt::{OnlineClient, PolkadotConfig}; use subxt::blocks::Block; use zombienet_orchestrator::network::Network; use zombienet_orchestrator::network::node::NetworkNode; use zombienet_sdk::{LocalFileSystem, NetworkConfigBuilder}; +use pallet_revive::H256; #[tokio::test(flavor = "multi_thread")] async fn rewards_statistics_mixed_validators_test() -> Result<(), anyhow::Error> { @@ -34,11 +35,10 @@ async fn rewards_statistics_mixed_validators_test() -> Result<(), anyhow::Error> .with_genesis_overrides(json!({ "configuration": { "config": { - "relay_vrf_modulo_samples": 2, "scheduler_params": { - "group_rotation_frequency": 4, - "max_validators_per_core": 6, - } + "num_cores": 1, + "group_rotation_frequency": 4 + }, } } })) @@ -63,7 +63,7 @@ async fn rewards_statistics_mixed_validators_test() -> Result<(), anyhow::Error> }) }) .with_parachain(|p| { - p.with_id(2000) + p.with_id(1000) .with_default_command("adder-collator") .with_default_image( std::env::var("COL_IMAGE") @@ -72,14 +72,7 @@ async fn rewards_statistics_mixed_validators_test() -> Result<(), anyhow::Error> ) .cumulus_based(false) .with_default_args(vec![("-lparachain=debug").into()]) - .with_collator(|n| n.with_name("collator-adder-2000")) - }) - .with_parachain(|p| { - p.with_id(2001) - .with_default_command("polkadot-parachain") - .with_default_image(images.cumulus.as_str()) - .with_default_args(vec![("-lparachain=debug,aura=debug").into()]) - .with_collator(|n| n.with_name("collator-2001")) + .with_collator(|n| n.with_name("adder-collator-1000")) }) .build() .map_err(|e| { @@ -93,16 +86,130 @@ async fn rewards_statistics_mixed_validators_test() -> Result<(), anyhow::Error> let relay_node = network.get_node("validator-0")?; let relay_client: OnlineClient = relay_node.wait_client().await?; - assert_para_throughput( + assert_para_throughput_for_included_parablocks( &relay_client, - 15, - [(ParaId::from(2000), 11..16), (ParaId::from(2001), 11..16)] - .into_iter() - .collect(), + 20, + [(polkadot_primitives::Id::from(1000), (10..30, 8..14))].into_iter().collect(), + ).await?; + + let mut blocks_sub = relay_client.blocks().subscribe_finalized().await?; + + //wait_for_nth_session_change(&mut blocks_sub, 1).await?; + + // Assert the parachain finalized block height is also on par with the number of backed + // candidates. We can only do this for the collator based on cumulus. + assert_finality_lag(&relay_client, 6).await?; + + assert_approval_usages_medians( + 1, + 12, + [("validator", 0..9), ("malus", 9..12)].into_iter().collect(), + &network, ).await?; Ok(()) } + +pub async fn assert_para_throughput_for_included_parablocks( + relay_client: &OnlineClient, + stop_after: u32, + expected_candidate_ranges: HashMap, Range)>, +) -> Result<(), anyhow::Error> { + let mut blocks_sub = relay_client.blocks().subscribe_finalized().await?; + let mut candidate_backed_count: HashMap = HashMap::new(); + let mut candidate_included_count: HashMap = HashMap::new(); + let mut current_block_count = 0; + + let valid_para_ids: Vec = expected_candidate_ranges.keys().cloned().collect(); + + // Wait for the first session, block production on the parachain will start after that. + wait_for_first_session_change(&mut blocks_sub).await?; + + while let Some(block) = blocks_sub.next().await { + let block = block?; + log::debug!("Finalized relay chain block {}", block.number()); + let events = block.events().await?; + let is_session_change = events.iter().any(|event| { + event.as_ref().is_ok_and(|event| { + event.pallet_name() == "Session" && event.variant_name() == "NewSession" + }) + }); + + // Do not count blocks with session changes, no backed blocks there. + if is_session_change { + continue; + } + + current_block_count += 1; + + let receipts_for_backed = find_event_and_decode_fields::>( + &events, + "ParaInclusion", + "CandidateBacked", + )?; + + for receipt in receipts_for_backed { + let para_id = receipt.descriptor.para_id(); + log::debug!("Block backed for para_id {para_id}"); + if !valid_para_ids.contains(¶_id) { + return Err(anyhow!("Invalid ParaId detected: {}", para_id)); + }; + *(candidate_backed_count.entry(para_id).or_default()) += 1; + } + + let receipts_for_included = find_event_and_decode_fields::>( + &events, + "ParaInclusion", + "CandidateIncluded", + )?; + + for receipt in receipts_for_included { + let para_id = receipt.descriptor.para_id(); + log::debug!("Block included for para_id {para_id}"); + if !valid_para_ids.contains(¶_id) { + return Err(anyhow!("Invalid ParaId detected: {}", para_id)); + }; + *(candidate_included_count.entry(para_id).or_default()) += 1; + } + + if current_block_count == stop_after { + break; + } + } + + log::info!( + "Reached {stop_after} finalized relay chain blocks that contain backed/included candidates. The per-parachain distribution is: {:#?} {:#?}", + candidate_backed_count.iter().map(|(para_id, count)| format!("{para_id} has {count} backed candidates"),).collect::>(), + candidate_included_count.iter().map(|(para_id, count)| format!("{para_id} has {count} included candidates"),).collect::>() + ); + + for (para_id, expected_candidate_range) in expected_candidate_ranges { + let actual_backed = candidate_backed_count + .get(¶_id) + .ok_or_else(|| anyhow!("ParaId did not have any backed candidates"))?; + + let actual_included = candidate_included_count + .get(¶_id) + .ok_or_else(|| anyhow!("ParaId did not have any included candidates"))?; + + if !expected_candidate_range.0.contains(actual_backed) { + let range = expected_candidate_range.0; + return Err(anyhow!( + "Candidate Backed count {actual_backed} not within range {range:?}" + )) + } + + if !expected_candidate_range.1.contains(actual_included) { + let range = expected_candidate_range.1; + return Err(anyhow!( + "Candidate Included count {actual_included} not within range {range:?}" + )) + } + } + + Ok(()) +} + async fn assert_approval_usages_medians( session: SessionIndex, num_validators: usize, @@ -134,11 +241,10 @@ async fn assert_approval_usages_medians( let total_approvals = relay_node.reports(approvals_per_session).await?; let total_noshows = relay_node.reports(noshows_per_session).await?; - log::info!("{kind} #{idx} approvals {session} -> {total_approvals}"); - log::info!("{kind} #{idx} no-shows {session} -> {total_noshows}"); + log::info!("Session {session}: {kind} #{idx} (Approvals: {total_approvals}, Noshows: {total_noshows}) "); - //assert!(total_approvals >= 9.0); - //assert!(total_noshows >= 3.0); + assert!(total_approvals >= 9.0); + assert!(total_noshows >= 3.0); } } diff --git a/polkadot/zombienet-sdk-tests/tests/smoke/coretime_revenue.rs b/polkadot/zombienet-sdk-tests/tests/smoke/coretime_revenue.rs index 466b8998ba544..6bbcd89bfbba6 100644 --- a/polkadot/zombienet-sdk-tests/tests/smoke/coretime_revenue.rs +++ b/polkadot/zombienet-sdk-tests/tests/smoke/coretime_revenue.rs @@ -299,30 +299,31 @@ async fn coretime_revenue_test() -> Result<(), anyhow::Error> { // Teleport some Alice's tokens to the Coretime chain. Although her account is pre-funded on // the PC, that is still neccessary to bootstrap RC's `CheckedAccount`. - relay_client - .tx() - .sign_and_submit_default( - &rococo::tx().xcm_pallet().teleport_assets( - VersionedLocation::V4(Location { - parents: 0, - interior: Junctions::X1([Junction::Parachain(1005)]), - }), - VersionedLocation::V4(Location { - parents: 0, - interior: Junctions::X1([Junction::AccountId32 { - network: None, - id: alice.public_key().0, - }]), - }), - VersionedAssets::V4(Assets(vec![Asset { - id: AssetId(Location { parents: 0, interior: Junctions::Here }), - fun: Fungibility::Fungible(1_500_000_000), - }])), - VersionedAssetId::V4(AssetId(Location { parents: 0, interior: Junctions::Here })), - ), - &alice, - ) - .await?; + // TODO: Uncomment the code below once finish testing (its failing due to a mismatch type) + // relay_client + // .tx() + // .sign_and_submit_default( + // &rococo::tx().xcm_pallet().teleport_assets( + // VersionedLocation::V4(Location { + // parents: 0, + // interior: Junctions::X1([Junction::Parachain(1005)]), + // }), + // VersionedLocation::V4(Location { + // parents: 0, + // interior: Junctions::X1([Junction::AccountId32 { + // network: None, + // id: alice.public_key().0, + // }]), + // }), + // VersionedAssets::V4(Assets(vec![Asset { + // id: AssetId(Location { parents: 0, interior: Junctions::Here }), + // fun: Fungibility::Fungible(1_500_000_000), + // }])), + // VersionedAssetId::V4(AssetId(Location { parents: 0, interior: Junctions::Here })), + // ), + // &alice, + // ) + // .await?; wait_for_event( para_events.clone(), From ea5eeae06df69b97781901752fb9f167cdbf83c4 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Tue, 25 Nov 2025 11:48:24 -0400 Subject: [PATCH 43/48] chore: include rewards-statistics-collector on benchmark --- charts/approval-voting-regression-bench.json | 1 + .../benches/approval-voting-regression-bench.rs | 2 ++ polkadot/node/core/approval-voting/src/tests.rs | 8 ++++---- .../core/rewards-statistics-collector/src/lib.rs | 10 +++++----- polkadot/node/overseer/src/dummy.rs | 2 +- polkadot/node/overseer/src/lib.rs | 2 +- polkadot/node/service/src/builder/mod.rs | 4 ++-- polkadot/node/service/src/overseer.rs | 10 +++++----- .../node/subsystem-bench/src/lib/approval/mod.rs | 16 +++++++++------- .../node/subsystem-bench/src/lib/mock/dummy.rs | 2 +- .../node/subsystem-bench/src/lib/mock/mod.rs | 2 +- 11 files changed, 32 insertions(+), 27 deletions(-) create mode 100644 charts/approval-voting-regression-bench.json diff --git a/charts/approval-voting-regression-bench.json b/charts/approval-voting-regression-bench.json new file mode 100644 index 0000000000000..f88fe31652e44 --- /dev/null +++ b/charts/approval-voting-regression-bench.json @@ -0,0 +1 @@ +[{"name":"Received from peers","unit":"KiB","value":52942.0},{"name":"Sent to peers","unit":"KiB","value":63647.33},{"name":"approval-distribution","unit":"seconds","value":0.00002360747},{"name":"approval-voting-parallel/approval-voting-parallel-0","unit":"seconds","value":1.8183425136200018},{"name":"approval-voting-parallel/approval-voting-parallel-3","unit":"seconds","value":1.8057443463800003},{"name":"rewards-statistics-collector/rewards-statistics-collector-subsystem","unit":"seconds","value":0.0008519592299999998},{"name":"rewards-statistics-collector","unit":"seconds","value":0.0008519592299999998},{"name":"approval-voting-parallel/approval-voting-parallel-2","unit":"seconds","value":1.8302057944500014},{"name":"approval-voting","unit":"seconds","value":0.000021977499999999998},{"name":"approval-voting-parallel","unit":"seconds","value":9.180164576859983},{"name":"approval-voting-parallel/approval-voting-parallel-1","unit":"seconds","value":1.8034110925800015},{"name":"approval-voting/test-environment","unit":"seconds","value":0.000021977499999999998},{"name":"approval-voting-parallel/approval-voting-parallel-subsystem","unit":"seconds","value":0.43368600474999647},{"name":"approval-distribution/test-environment","unit":"seconds","value":0.00002360747},{"name":"test-environment","unit":"seconds","value":2.6158653661279385},{"name":"approval-voting-parallel/approval-voting-parallel-db","unit":"seconds","value":1.4863048098999827},{"name":"approval-voting-parallel/approval-voting-gather-signatures","unit":"seconds","value":0.0024700151799999974}] \ No newline at end of file diff --git a/polkadot/node/core/approval-voting/benches/approval-voting-regression-bench.rs b/polkadot/node/core/approval-voting/benches/approval-voting-regression-bench.rs index 70ec6bff440b7..db32cfbe3531e 100644 --- a/polkadot/node/core/approval-voting/benches/approval-voting-regression-bench.rs +++ b/polkadot/node/core/approval-voting/benches/approval-voting-regression-bench.rs @@ -83,6 +83,8 @@ fn main() -> Result<(), String> { ("Sent to peers", 63995.2200, 0.01), ])); messages.extend(average_usage.check_cpu_usage(&[("approval-voting-parallel", 12.3817, 0.1)])); + messages.extend(average_usage.check_cpu_usage(&[("rewards-statistics-collector", 12.3817, 0.1)])); + if messages.is_empty() { Ok(()) diff --git a/polkadot/node/core/approval-voting/src/tests.rs b/polkadot/node/core/approval-voting/src/tests.rs index 3951aabdc3b2f..feab19d8715f8 100644 --- a/polkadot/node/core/approval-voting/src/tests.rs +++ b/polkadot/node/core/approval-voting/src/tests.rs @@ -707,7 +707,7 @@ async fn import_approval( if let Some(expected_stats_collected) = expected_approvals_stats_collected { assert_matches!( overseer_recv(overseer).await, - AllMessages::ConsensusStatisticsCollector( + AllMessages::RewardsStatisticsCollector( RewardsStatisticsCollectorMessage::CandidateApproved( c_hash, b_hash, validators, ) @@ -3857,7 +3857,7 @@ fn pre_covers_dont_stall_approval() { assert_matches!( overseer_recv(&mut virtual_overseer).await, - AllMessages::ConsensusStatisticsCollector(RewardsStatisticsCollectorMessage::CandidateApproved( + AllMessages::RewardsStatisticsCollector(RewardsStatisticsCollectorMessage::CandidateApproved( c_hash, b_hash, validators, )) => { assert_eq!(b_hash, block_hash); @@ -3868,7 +3868,7 @@ fn pre_covers_dont_stall_approval() { assert_matches!( overseer_recv(&mut virtual_overseer).await, - AllMessages::ConsensusStatisticsCollector(RewardsStatisticsCollectorMessage::NoShows( + AllMessages::RewardsStatisticsCollector(RewardsStatisticsCollectorMessage::NoShows( c_hash, b_hash, validators )) => { assert_eq!(b_hash, block_hash); @@ -4044,7 +4044,7 @@ fn waits_until_approving_assignments_are_old_enough() { assert_matches!( overseer_recv(&mut virtual_overseer).await, - AllMessages::ConsensusStatisticsCollector( + AllMessages::RewardsStatisticsCollector( RewardsStatisticsCollectorMessage::CandidateApproved( c_hash, b_hash, validators ) diff --git a/polkadot/node/core/rewards-statistics-collector/src/lib.rs b/polkadot/node/core/rewards-statistics-collector/src/lib.rs index 1425fb0849a39..a408d8618cfeb 100644 --- a/polkadot/node/core/rewards-statistics-collector/src/lib.rs +++ b/polkadot/node/core/rewards-statistics-collector/src/lib.rs @@ -154,7 +154,7 @@ pub struct RewardsStatisticsCollector { } impl RewardsStatisticsCollector { - /// Create a new instance of the `ConsensusStatisticsCollector`. + /// Create a new instance of the `RewardsStatisticsCollector`. pub fn new(metrics: Metrics, config: Config) -> Self { Self { metrics, @@ -163,7 +163,7 @@ impl RewardsStatisticsCollector { } } -#[overseer::subsystem(ConsensusStatisticsCollector, error = SubsystemError, prefix = self::overseer)] +#[overseer::subsystem(RewardsStatisticsCollector, error = SubsystemError, prefix = self::overseer)] impl RewardsStatisticsCollector where Context: Send + Sync, @@ -178,7 +178,7 @@ where } } -#[overseer::contextbounds(ConsensusStatisticsCollector, prefix = self::overseer)] +#[overseer::contextbounds(RewardsStatisticsCollector, prefix = self::overseer)] async fn run(mut ctx: Context, metrics: (Metrics, bool)) -> FatalResult<()> { let mut view = View::new(); loop { @@ -189,7 +189,7 @@ async fn run(mut ctx: Context, metrics: (Metrics, bool)) -> FatalResult } } -#[overseer::contextbounds(ConsensusStatisticsCollector, prefix = self::overseer)] +#[overseer::contextbounds(RewardsStatisticsCollector, prefix = self::overseer)] pub(crate) async fn run_iteration( ctx: &mut Context, view: &mut View, @@ -429,7 +429,7 @@ fn prune_unfinalised_forks(view: &mut View, fin_block_hash: Hash) -> Vec { // prune_old_session_views avoid the per_session mapping to grow // indefinitely by removing sessions stored for more than MAX_SESSIONS_TO_KEEP (2) // finalized sessions. -#[overseer::contextbounds(ConsensusStatisticsCollector, prefix = self::overseer)] +#[overseer::contextbounds(RewardsStatisticsCollector, prefix = self::overseer)] async fn prune_old_session_views( ctx: &mut Context, view: &mut View, diff --git a/polkadot/node/overseer/src/dummy.rs b/polkadot/node/overseer/src/dummy.rs index c23429082190c..4a228bb9b729a 100644 --- a/polkadot/node/overseer/src/dummy.rs +++ b/polkadot/node/overseer/src/dummy.rs @@ -195,7 +195,7 @@ where .dispute_distribution(subsystem.clone()) .chain_selection(subsystem.clone()) .prospective_parachains(subsystem.clone()) - .consensus_statistics_collector(subsystem.clone()) + .rewards_statistics_collector(subsystem.clone()) .activation_external_listeners(Default::default()) .active_leaves(Default::default()) .spawner(SpawnGlue(spawner)) diff --git a/polkadot/node/overseer/src/lib.rs b/polkadot/node/overseer/src/lib.rs index a375f0d793813..eb5c860b6bc68 100644 --- a/polkadot/node/overseer/src/lib.rs +++ b/polkadot/node/overseer/src/lib.rs @@ -668,7 +668,7 @@ pub struct Overseer { RuntimeApiMessage, ChainApiMessage, ])] - consensus_statistics_collector: ConsensusStatisticsCollector, + rewards_statistics_collector: RewardsStatisticsCollector, /// External listeners waiting for a hash to be in the active-leave set. pub activation_external_listeners: HashMap>>>, diff --git a/polkadot/node/service/src/builder/mod.rs b/polkadot/node/service/src/builder/mod.rs index 3d244f52d4620..2bee75dcfad1c 100644 --- a/polkadot/node/service/src/builder/mod.rs +++ b/polkadot/node/service/src/builder/mod.rs @@ -443,7 +443,7 @@ where }, }; - let consensus_statistics_collector_config = RewardsStatisticsCollectorConfig{ + let rewards_statistics_collector_config = RewardsStatisticsCollectorConfig{ verbose_approval_metrics, }; @@ -463,7 +463,7 @@ where fetch_chunks_threshold, invulnerable_ah_collators, collator_protocol_hold_off, - consensus_statistics_collector_config, + rewards_statistics_collector_config, }) }; diff --git a/polkadot/node/service/src/overseer.rs b/polkadot/node/service/src/overseer.rs index d2fcf880c7dae..da4d6cf26109e 100644 --- a/polkadot/node/service/src/overseer.rs +++ b/polkadot/node/service/src/overseer.rs @@ -135,7 +135,7 @@ pub struct ExtendedOverseerGenArgs { pub candidate_req_v2_receiver: IncomingRequestReceiver, /// Configuration for the approval voting subsystem. pub approval_voting_config: ApprovalVotingConfig, - pub consensus_statistics_collector_config: RewardsStatisticsCollectorConfig, + pub rewards_statistics_collector_config: RewardsStatisticsCollectorConfig, /// Receiver for incoming disputes. pub dispute_req_receiver: IncomingRequestReceiver, /// Configuration for the dispute coordinator subsystem. @@ -186,7 +186,7 @@ pub fn validator_overseer_builder( fetch_chunks_threshold, invulnerable_ah_collators, collator_protocol_hold_off, - consensus_statistics_collector_config, + rewards_statistics_collector_config, }: ExtendedOverseerGenArgs, ) -> Result< InitializedOverseerBuilder< @@ -358,9 +358,9 @@ where )) .chain_selection(ChainSelectionSubsystem::new(chain_selection_config, parachains_db)) .prospective_parachains(ProspectiveParachainsSubsystem::new(Metrics::register(registry)?)) - .consensus_statistics_collector(RewardsStatisticsCollector::new( + .rewards_statistics_collector(RewardsStatisticsCollector::new( Metrics::register(registry)?, - consensus_statistics_collector_config, + rewards_statistics_collector_config, )) .activation_external_listeners(Default::default()) .active_leaves(Default::default()) @@ -508,7 +508,7 @@ where .dispute_distribution(DummySubsystem) .chain_selection(DummySubsystem) .prospective_parachains(DummySubsystem) - .consensus_statistics_collector(DummySubsystem) + .rewards_statistics_collector(DummySubsystem) .activation_external_listeners(Default::default()) .active_leaves(Default::default()) .supports_parachains(runtime_client) diff --git a/polkadot/node/subsystem-bench/src/lib/approval/mod.rs b/polkadot/node/subsystem-bench/src/lib/approval/mod.rs index 178f566e022ba..d640fa08f0856 100644 --- a/polkadot/node/subsystem-bench/src/lib/approval/mod.rs +++ b/polkadot/node/subsystem-bench/src/lib/approval/mod.rs @@ -54,10 +54,7 @@ use polkadot_node_primitives::approval::time::{ slot_number_to_tick, tick_to_slot_number, Clock, ClockExt, SystemClock, }; -use polkadot_node_core_rewards_statistics_collector::{ - RewardsStatisticsCollector as RewardsStatisticsCollectorSubsystem, - metrics::Metrics as RewardsStatisticsMetrics, -}; +use polkadot_node_core_rewards_statistics_collector::{RewardsStatisticsCollector as RewardsStatisticsCollectorSubsystem, metrics::Metrics as RewardsStatisticsMetrics, RewardsStatisticsCollector}; use polkadot_node_core_approval_voting::{ ApprovalVotingSubsystem, Config as ApprovalVotingConfig, RealAssignmentCriteria, }; @@ -858,7 +855,7 @@ fn build_overseer( let overseer_metrics = OverseerMetrics::try_register(&dependencies.registry).unwrap(); let task_handle = spawn_task_handle.clone(); - let collector_subsystem = RewardsStatisticsCollectorSubsystem::default(); + let rewards_statistics_collector_subsystem = RewardsStatisticsCollectorSubsystem::default(); let dummy = dummy_builder!(task_handle, overseer_metrics) .replace_chain_api(|_| mock_chain_api) @@ -868,7 +865,7 @@ fn build_overseer( .replace_network_bridge_rx(|_| mock_rx_bridge) .replace_availability_recovery(|_| MockAvailabilityRecovery::new()) .replace_candidate_validation(|_| MockCandidateValidation::new()) - .replace_consensus_statistics_collector(|_| collector_subsystem); + .replace_rewards_statistics_collector(|_| rewards_statistics_collector_subsystem); let (overseer, raw_handle) = if state.options.approval_voting_parallel_enabled { let approval_voting_parallel = ApprovalVotingParallelSubsystem::with_config_and_clock( @@ -1180,7 +1177,12 @@ pub async fn bench_approvals_run( ); env.collect_resource_usage( - &["approval-distribution", "approval-voting", "approval-voting-parallel"], + &[ + "approval-distribution", + "approval-voting", + "approval-voting-parallel", + "rewards-statistics-collector" + ], true, ) } diff --git a/polkadot/node/subsystem-bench/src/lib/mock/dummy.rs b/polkadot/node/subsystem-bench/src/lib/mock/dummy.rs index f0b7af995aee6..d4f0fbb36dc27 100644 --- a/polkadot/node/subsystem-bench/src/lib/mock/dummy.rs +++ b/polkadot/node/subsystem-bench/src/lib/mock/dummy.rs @@ -99,4 +99,4 @@ mock!(ApprovalVoting); mock!(ApprovalVotingParallel); mock!(ApprovalDistribution); mock!(RuntimeApi); -mock!(ConsensusStatisticsCollector); +mock!(RewardsStatisticsCollector); diff --git a/polkadot/node/subsystem-bench/src/lib/mock/mod.rs b/polkadot/node/subsystem-bench/src/lib/mock/mod.rs index a1fc664e4ab5e..181dd11ad8841 100644 --- a/polkadot/node/subsystem-bench/src/lib/mock/mod.rs +++ b/polkadot/node/subsystem-bench/src/lib/mock/mod.rs @@ -71,7 +71,7 @@ macro_rules! dummy_builder { .gossip_support(MockGossipSupport {}) .dispute_distribution(MockDisputeDistribution {}) .prospective_parachains(MockProspectiveParachains {}) - .consensus_statistics_collector(MockConsensusStatisticsCollector {}) + .rewards_statistics_collector(MockRewardsStatisticsCollector {}) .activation_external_listeners(Default::default()) .active_leaves(Default::default()) .metrics($metrics) From 66f7d4df0ce40fc3de9367664c35480ba0418fa2 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Mon, 1 Dec 2025 10:08:27 -0400 Subject: [PATCH 44/48] chore: remove charts --- charts/approval-voting-regression-bench.json | 1 - 1 file changed, 1 deletion(-) delete mode 100644 charts/approval-voting-regression-bench.json diff --git a/charts/approval-voting-regression-bench.json b/charts/approval-voting-regression-bench.json deleted file mode 100644 index f88fe31652e44..0000000000000 --- a/charts/approval-voting-regression-bench.json +++ /dev/null @@ -1 +0,0 @@ -[{"name":"Received from peers","unit":"KiB","value":52942.0},{"name":"Sent to peers","unit":"KiB","value":63647.33},{"name":"approval-distribution","unit":"seconds","value":0.00002360747},{"name":"approval-voting-parallel/approval-voting-parallel-0","unit":"seconds","value":1.8183425136200018},{"name":"approval-voting-parallel/approval-voting-parallel-3","unit":"seconds","value":1.8057443463800003},{"name":"rewards-statistics-collector/rewards-statistics-collector-subsystem","unit":"seconds","value":0.0008519592299999998},{"name":"rewards-statistics-collector","unit":"seconds","value":0.0008519592299999998},{"name":"approval-voting-parallel/approval-voting-parallel-2","unit":"seconds","value":1.8302057944500014},{"name":"approval-voting","unit":"seconds","value":0.000021977499999999998},{"name":"approval-voting-parallel","unit":"seconds","value":9.180164576859983},{"name":"approval-voting-parallel/approval-voting-parallel-1","unit":"seconds","value":1.8034110925800015},{"name":"approval-voting/test-environment","unit":"seconds","value":0.000021977499999999998},{"name":"approval-voting-parallel/approval-voting-parallel-subsystem","unit":"seconds","value":0.43368600474999647},{"name":"approval-distribution/test-environment","unit":"seconds","value":0.00002360747},{"name":"test-environment","unit":"seconds","value":2.6158653661279385},{"name":"approval-voting-parallel/approval-voting-parallel-db","unit":"seconds","value":1.4863048098999827},{"name":"approval-voting-parallel/approval-voting-gather-signatures","unit":"seconds","value":0.0024700151799999974}] \ No newline at end of file From c545ef4d710abb6d993c5b61dc64add395696bc1 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Fri, 5 Dec 2025 09:38:26 -0400 Subject: [PATCH 45/48] chore: working on approvals rewards parachain host pallet --- .../src/blockchain_rpc_client.rs | 12 + .../src/rpc_client.rs | 16 ++ .../rewards-statistics-collector/src/lib.rs | 102 +++------ polkadot/node/core/runtime-api/src/cache.rs | 1 + polkadot/node/core/runtime-api/src/lib.rs | 14 ++ polkadot/node/service/src/fake_runtime_api.rs | 8 + polkadot/node/service/src/overseer.rs | 1 + .../subsystem-bench/src/lib/approval/mod.rs | 16 +- polkadot/node/subsystem-types/src/messages.rs | 3 + polkadot/primitives/src/vstaging/mod.rs | 23 +- .../parachains/src/approvals_rewards/mod.rs | 208 ++++++++++++++++++ polkadot/runtime/parachains/src/lib.rs | 1 + polkadot/runtime/parachains/src/paras/mod.rs | 8 - .../parachains/src/runtime_api_impl/v13.rs | 8 - .../src/runtime_api_impl/vstaging.rs | 13 +- polkadot/runtime/rococo/src/lib.rs | 8 +- polkadot/runtime/westend/src/lib.rs | 9 +- .../staking-async/runtimes/rc/src/lib.rs | 2 +- 18 files changed, 361 insertions(+), 92 deletions(-) create mode 100644 polkadot/runtime/parachains/src/approvals_rewards/mod.rs diff --git a/cumulus/client/relay-chain-minimal-node/src/blockchain_rpc_client.rs b/cumulus/client/relay-chain-minimal-node/src/blockchain_rpc_client.rs index acc2782caa78d..dacac6518a271 100644 --- a/cumulus/client/relay-chain-minimal-node/src/blockchain_rpc_client.rs +++ b/cumulus/client/relay-chain-minimal-node/src/blockchain_rpc_client.rs @@ -305,6 +305,18 @@ impl RuntimeApiSubsystemClient for BlockChainRpcClient { .await?) } + async fn submit_approval_statistics( + &self, + at: Hash, + payload: polkadot_primitives::vstaging::ApprovalStatistics, + signature: polkadot_primitives::ValidatorSignature, + ) -> Result<(), sp_api::ApiError> { + Ok(self + .rpc_client + .parachain_host_submit_approval_statistics(at, payload, signature) + .await?) + } + async fn pvfs_require_precheck( &self, at: Hash, diff --git a/cumulus/client/relay-chain-rpc-interface/src/rpc_client.rs b/cumulus/client/relay-chain-rpc-interface/src/rpc_client.rs index 80858a665cfaf..cdb35353cac83 100644 --- a/cumulus/client/relay-chain-rpc-interface/src/rpc_client.rs +++ b/cumulus/client/relay-chain-rpc-interface/src/rpc_client.rs @@ -54,6 +54,7 @@ use sp_version::RuntimeVersion; use crate::{metrics::RelaychainRpcMetrics, reconnecting_ws_client::ReconnectingWebsocketWorker}; pub use url::Url; +use cumulus_primitives_core::relay_chain::vstaging::ApprovalStatistics; const LOG_TARGET: &str = "relay-chain-rpc-client"; const NOTIFICATION_CHANNEL_SIZE_LIMIT: usize = 20; @@ -261,6 +262,21 @@ impl RelayChainRpcClient { .await } + /// Submits approval voting rewards statistics into the transaction pool. + pub async fn parachain_host_submit_approval_statistics( + &self, + at: RelayHash, + payload: ApprovalStatistics, + signature: ValidatorSignature, + ) -> Result<(), RelayChainError> { + self.call_remote_runtime_function( + "ParachainHost_submit_approval_statistics", + at, + Some((payload, signature)), + ) + .await + } + /// Get system health information pub async fn system_health(&self) -> Result { self.request("system_health", rpc_params![]).await diff --git a/polkadot/node/core/rewards-statistics-collector/src/lib.rs b/polkadot/node/core/rewards-statistics-collector/src/lib.rs index 87dd233298c90..1cba03e719e63 100644 --- a/polkadot/node/core/rewards-statistics-collector/src/lib.rs +++ b/polkadot/node/core/rewards-statistics-collector/src/lib.rs @@ -33,10 +33,7 @@ use polkadot_node_subsystem::{ messages::{ChainApiMessage, RewardsStatisticsCollectorMessage, RuntimeApiMessage, RuntimeApiRequest}, overseer, ActiveLeavesUpdate, FromOrchestra, OverseerSignal, SpawnedSubsystem, SubsystemError, SubsystemSender }; -use polkadot_primitives::{ - AuthorityDiscoveryId, BlockNumber, Hash, Header, SessionIndex, ValidatorId, ValidatorIndex, - well_known_keys::relay_dispatch_queue_remaining_capacity -}; +use polkadot_primitives::{AuthorityDiscoveryId, BlockNumber, Hash, Header, SessionIndex, ValidatorId, ValidatorIndex, well_known_keys::relay_dispatch_queue_remaining_capacity, SessionInfo}; use polkadot_node_primitives::{approval::{ time::Tick, v1::DelayTranche @@ -55,6 +52,7 @@ pub mod metrics; use approval_voting_metrics::ApprovalsStats; use polkadot_node_subsystem::RuntimeApiError::{Execution, NotSupported}; use polkadot_node_subsystem_util::{request_candidate_events, request_session_index_for_child, request_session_info}; +use polkadot_primitives::vstaging::{ApprovalStatistics, ApprovalStatisticsTallyLine}; use crate::approval_voting_metrics::{handle_candidate_approved, handle_observed_no_shows}; use crate::availability_distribution_metrics::{handle_chunk_uploaded, handle_chunks_downloaded, AvailabilityChunks}; use self::metrics::Metrics; @@ -207,7 +205,7 @@ pub struct RewardsStatisticsCollector { } impl RewardsStatisticsCollector { - /// Create a new instance of the `ConsensusStatisticsCollector`. + /// Create a new instance of the `RewardsStatisticsCollector`. pub fn new(keystore: KeystorePtr, metrics: Metrics, config: Config) -> Self { Self { metrics, @@ -224,7 +222,7 @@ where { fn start(self, ctx: Context) -> SpawnedSubsystem { SpawnedSubsystem { - future: run(ctx, self.keystore, (self.metrics, self.config.publish_per_validator_approval_metrics)) + future: run(ctx, self.keystore, (self.metrics, self.config.verbose_approval_metrics)) .map_err(|e| SubsystemError::with_origin("statistics-parachains", e)) .boxed(), name: "rewards-statistics-collector-subsystem", @@ -232,7 +230,7 @@ where } } -#[overseer::contextbounds(ConsensusStatisticsCollector, prefix = self::overseer)] +#[overseer::contextbounds(RewardsStatisticsCollector, prefix = self::overseer)] async fn run(mut ctx: Context, keystore: KeystorePtr, metrics: (Metrics, bool)) -> FatalResult<()> { let mut view = View::new(); loop { @@ -251,6 +249,8 @@ pub(crate) async fn run_iteration( metrics: (&Metrics, bool), ) -> Result<()> { let per_validator_metrics = metrics.1; + let mut sender = ctx.sender().clone(); + loop { match ctx.recv().await.map_err(FatalError::SubsystemReceive)? { FromOrchestra::Signal(OverseerSignal::Conclude) => return Ok(()), @@ -262,7 +262,7 @@ pub(crate) async fn run_iteration( new_session_info, recent_block, } = extract_activated_leaf_info( - ctx.sender(), + &mut sender, view, keystore, activated.hash, @@ -334,7 +334,7 @@ pub(crate) async fn run_iteration( } log_session_view_general_stats(view); - submit_finalized_session_stats( + prune_and_submit_finalized_session_stats( ctx.sender(), keystore, view, @@ -403,11 +403,8 @@ struct ActivationInfo { new_session_info: Option<(SessionInfo, Option)>, } -async fn extract_activated_leaf_info< - Sender: SubsystemSender - + SubsystemSender ->( - mut sender: Sender, +async fn extract_activated_leaf_info( + sender: &mut impl overseer::RewardsStatisticsCollectorSenderTrait, view: &mut View, keystore: &KeystorePtr, relay_hash: Hash, @@ -428,14 +425,14 @@ async fn extract_activated_leaf_info< .await? .map_err(JfyiError::ChainApiCallError)?; - let session_idx = request_session_index_for_child(relay_hash, &mut sender) + let session_idx = request_session_index_for_child(relay_hash, sender) .await .await .map_err(JfyiError::OverseerCommunication)? .map_err(JfyiError::RuntimeApiCallError)?; let new_session_info = if !view.per_session.contains_key(&session_idx) { - let session_info = request_session_info(relay_hash, session_idx, &mut sender) + let session_info = request_session_info(relay_hash, session_idx, sender) .await .await .map_err(JfyiError::OverseerCommunication)? @@ -443,13 +440,11 @@ async fn extract_activated_leaf_info< let (tx, rx) = oneshot::channel(); let validators = runtime_api_request( - &mut sender, + sender, relay_hash, RuntimeApiRequest::Validators(tx), rx, - ) - .await - .map_err(JfyiError::RuntimeApiCallError)?; + ).await?; let signing_credentials = polkadot_node_subsystem_util::signing_key_and_index(&validators, keystore) .map(|(validator_key, validator_index)| @@ -541,11 +536,11 @@ fn prune_unfinalised_forks(view: &mut View, fin_block_hash: Hash) -> Vec { retain_relay_hashes } -// prune_old_session_views avoid the per_session mapping to grow +// prune_and_submit_finalized_session_stats avoid the per_session mapping to grow // indefinitely by removing sessions stored for more than MAX_SESSIONS_TO_KEEP (2) // finalized sessions. -async fn prune_old_session_views>( - mut sender: Sender, +async fn prune_and_submit_finalized_session_stats( + sender: &mut impl overseer::RewardsStatisticsCollectorSenderTrait, keystore: &KeystorePtr, view: &mut View, finalized_hash: Hash, @@ -563,7 +558,7 @@ async fn prune_old_session_views>( }, }; - let finalized_session = request_session_index_for_child(finalized_hash, &mut sender) + let finalized_session = request_session_index_for_child(finalized_hash, sender) .await .await .map_err(JfyiError::OverseerCommunication)? @@ -579,7 +574,7 @@ async fn prune_old_session_views>( if let Some(ref credentials) = session_view.credentials { sign_and_submit_approvals_tallies( - &mut sender, + sender, recent_block_hash, session_idx, keystore, @@ -588,16 +583,15 @@ async fn prune_old_session_views>( session_view.validators_tallies.clone(), ).await; } + } - if let Some(wipe_before) = session_idx.checked_sub(MAX_SESSIONS_TO_KEEP.get()) { - view.per_session.retain(|stored_session_index, _| *stored_session_index > wipe_before); - } - - view.current_session = Some(finalized_session); + if let Some(wipe_before) = current_session.checked_sub(MAX_SESSIONS_TO_KEEP.get()) { + view.per_session.retain(|stored_session_index, _| *stored_session_index > wipe_before); } - + + view.current_session = Some(finalized_session); } - None => view.current_session = Some(current_fin_session), + None => view.current_session = Some(finalized_session), _ => {} }; @@ -622,10 +616,8 @@ fn log_session_view_general_stats(view: &View) { } } -async fn sign_and_submit_approvals_tallies< - Sender: SubsystemSender, ->( - mut sender: Sender, +async fn sign_and_submit_approvals_tallies( + sender: &mut impl SubsystemSender, relay_parent: Hash, session_index: &SessionIndex, keystore: &KeystorePtr, @@ -654,7 +646,7 @@ async fn sign_and_submit_approvals_tallies< }); } - let payload = ApprovalStatistics(session_index.clone(), approvals_tallies); + let payload = ApprovalStatistics(session_index.clone(), credentials.validator_index, approvals_tallies); let signature = match polkadot_node_subsystem_util::sign( keystore, @@ -685,7 +677,7 @@ async fn sign_and_submit_approvals_tallies< let (tx, rx) = oneshot::channel(); let runtime_req = runtime_api_request( - &mut sender, + sender, relay_parent, RuntimeApiRequest::SubmitApprovalStatistics(payload, signature, tx), rx, @@ -712,40 +704,18 @@ pub(crate) enum RuntimeRequestError { CommunicationError, } -async fn runtime_api_request< - T, - Sender: SubsystemSender, ->( - mut sender: Sender, +async fn runtime_api_request( + sender: &mut impl SubsystemSender, relay_parent: Hash, request: RuntimeApiRequest, receiver: oneshot::Receiver>, -) -> std::result::Result { +) -> std::result::Result { sender .send_message(RuntimeApiMessage::Request(relay_parent, request).into()) .await; receiver - .await - .map_err(|_| { - gum::debug!(target: LOG_TARGET, ?relay_parent, "Runtime API request dropped"); - RuntimeRequestError::CommunicationError - }) - .and_then(|res| { - res.map_err(|e| { - use RuntimeApiSubsystemError::*; - match e { - Execution { .. } => { - gum::debug!( - target: LOG_TARGET, - ?relay_parent, - err = ?e, - "Runtime API request internal error" - ); - RuntimeRequestError::ApiError - }, - NotSupported { .. } => RuntimeRequestError::NotSupported, - } - }) - }) + .map_err(JfyiError::OverseerCommunication) + .await? + .map_err(JfyiError::RuntimeApiCallError) } diff --git a/polkadot/node/core/runtime-api/src/cache.rs b/polkadot/node/core/runtime-api/src/cache.rs index 9c09ea3f22a9e..2f4ad8bdea101 100644 --- a/polkadot/node/core/runtime-api/src/cache.rs +++ b/polkadot/node/core/runtime-api/src/cache.rs @@ -662,6 +662,7 @@ pub(crate) enum RequestResult { // This is a request with side-effects and no result, hence (). #[allow(dead_code)] SubmitPvfCheckStatement(()), + SubmitApprovalStatistics(()), ValidationCodeHash(Hash, ParaId, OccupiedCoreAssumption, Option), Version(Hash, u32), Disputes(Hash, Vec<(SessionIndex, CandidateHash, DisputeState)>), diff --git a/polkadot/node/core/runtime-api/src/lib.rs b/polkadot/node/core/runtime-api/src/lib.rs index 5b7be703ae6b8..48d8461e036aa 100644 --- a/polkadot/node/core/runtime-api/src/lib.rs +++ b/polkadot/node/core/runtime-api/src/lib.rs @@ -156,6 +156,7 @@ where PvfsRequirePrecheck(relay_parent, pvfs) => self.requests_cache.cache_pvfs_require_precheck(relay_parent, pvfs), SubmitPvfCheckStatement(()) => {}, + SubmitApprovalStatistics(()) => {}, ValidationCodeHash(relay_parent, para_id, assumption, hash) => self .requests_cache .cache_validation_code_hash((relay_parent, para_id, assumption), hash), @@ -309,6 +310,10 @@ where // This request is side-effecting and thus cannot be cached. Some(request) }, + request @ Request::SubmitApprovalStatistics(_, _, _) => { + // This request is side-effecting and thus cannot be cached. + Some(request) + }, Request::ValidationCodeHash(para, assumption, sender) => query!(validation_code_hash(para, assumption), sender) .map(|sender| Request::ValidationCodeHash(para, assumption, sender)), @@ -619,6 +624,15 @@ where result = () ) }, + Request::SubmitApprovalStatistics(payload, signature, sender) => { + query!( + SubmitApprovalStatistics, + submit_approval_statistics(payload, signature), + ver = 2, + sender, + result = () + ) + }, Request::PvfsRequirePrecheck(sender) => { query!(PvfsRequirePrecheck, pvfs_require_precheck(), ver = 2, sender) }, diff --git a/polkadot/node/service/src/fake_runtime_api.rs b/polkadot/node/service/src/fake_runtime_api.rs index f43940c8474f2..d9fcb929acd4a 100644 --- a/polkadot/node/service/src/fake_runtime_api.rs +++ b/polkadot/node/service/src/fake_runtime_api.rs @@ -28,6 +28,7 @@ use polkadot_primitives::{ InboundHrmpMessage, Nonce, OccupiedCoreAssumption, PersistedValidationData, PvfCheckStatement, ScrapedOnChainVotes, SessionIndex, SessionInfo, ValidationCode, ValidationCodeHash, ValidatorId, ValidatorIndex, ValidatorSignature, + vstaging::ApprovalStatistics, }; use sp_consensus_beefy::ecdsa_crypto::{AuthorityId as BeefyId, Signature as BeefySignature}; use sp_consensus_grandpa::AuthorityId as GrandpaId; @@ -203,6 +204,13 @@ sp_api::impl_runtime_apis! { unimplemented!() } + fn submit_approval_statistics( + _: ApprovalStatistics, + _: ValidatorSignature, + ) { + unimplemented!() + } + fn pvfs_require_precheck() -> Vec { unimplemented!() } diff --git a/polkadot/node/service/src/overseer.rs b/polkadot/node/service/src/overseer.rs index da4d6cf26109e..9dbb282e73d75 100644 --- a/polkadot/node/service/src/overseer.rs +++ b/polkadot/node/service/src/overseer.rs @@ -359,6 +359,7 @@ where .chain_selection(ChainSelectionSubsystem::new(chain_selection_config, parachains_db)) .prospective_parachains(ProspectiveParachainsSubsystem::new(Metrics::register(registry)?)) .rewards_statistics_collector(RewardsStatisticsCollector::new( + keystore.clone(), Metrics::register(registry)?, rewards_statistics_collector_config, )) diff --git a/polkadot/node/subsystem-bench/src/lib/approval/mod.rs b/polkadot/node/subsystem-bench/src/lib/approval/mod.rs index d640fa08f0856..4650eda95abdd 100644 --- a/polkadot/node/subsystem-bench/src/lib/approval/mod.rs +++ b/polkadot/node/subsystem-bench/src/lib/approval/mod.rs @@ -54,7 +54,12 @@ use polkadot_node_primitives::approval::time::{ slot_number_to_tick, tick_to_slot_number, Clock, ClockExt, SystemClock, }; -use polkadot_node_core_rewards_statistics_collector::{RewardsStatisticsCollector as RewardsStatisticsCollectorSubsystem, metrics::Metrics as RewardsStatisticsMetrics, RewardsStatisticsCollector}; +use polkadot_node_core_rewards_statistics_collector::{ + RewardsStatisticsCollector as RewardsStatisticsCollectorSubsystem, + metrics::Metrics as RewardsStatisticsMetrics, + RewardsStatisticsCollector, + Config as RewardsStatisticsConfig +}; use polkadot_node_core_approval_voting::{ ApprovalVotingSubsystem, Config as ApprovalVotingConfig, RealAssignmentCriteria, }; @@ -855,7 +860,14 @@ fn build_overseer( let overseer_metrics = OverseerMetrics::try_register(&dependencies.registry).unwrap(); let task_handle = spawn_task_handle.clone(); - let rewards_statistics_collector_subsystem = RewardsStatisticsCollectorSubsystem::default(); + let rewards_metrics = RewardsStatisticsMetrics::try_register(&dependencies.registry).unwrap(); + let rewards_statistics_collector_subsystem = RewardsStatisticsCollectorSubsystem::new( + keystore.clone(), + rewards_metrics, + RewardsStatisticsConfig{ + verbose_approval_metrics: false, + }, + ); let dummy = dummy_builder!(task_handle, overseer_metrics) .replace_chain_api(|_| mock_chain_api) diff --git a/polkadot/node/subsystem-types/src/messages.rs b/polkadot/node/subsystem-types/src/messages.rs index bef25b7766f81..693ef119da553 100644 --- a/polkadot/node/subsystem-types/src/messages.rs +++ b/polkadot/node/subsystem-types/src/messages.rs @@ -64,6 +64,7 @@ use std::{ /// Network events as transmitted to other subsystems, wrapped in their message types. pub mod network_bridge_event; pub use network_bridge_event::NetworkBridgeEvent; +use polkadot_primitives::vstaging::ApprovalStatistics; /// A request to the candidate backing subsystem to check whether /// we can second this candidate. @@ -730,6 +731,8 @@ pub enum RuntimeApiRequest { FetchOnChainVotes(RuntimeApiSender>), /// Submits a PVF pre-checking statement into the transaction pool. SubmitPvfCheckStatement(PvfCheckStatement, ValidatorSignature, RuntimeApiSender<()>), + /// Submits the Rewards Approvals Statistics into the transaction pool. + SubmitApprovalStatistics(ApprovalStatistics, ValidatorSignature, RuntimeApiSender<()>), /// Returns code hashes of PVFs that require pre-checking by validators in the active set. PvfsRequirePrecheck(RuntimeApiSender>), /// Get the validation code used by the specified para, taking the given diff --git a/polkadot/primitives/src/vstaging/mod.rs b/polkadot/primitives/src/vstaging/mod.rs index 5b4a61e3f218e..03e979b44c391 100644 --- a/polkadot/primitives/src/vstaging/mod.rs +++ b/polkadot/primitives/src/vstaging/mod.rs @@ -27,7 +27,16 @@ use alloc::vec::Vec; /// A reward tally line represent the collected statistics about /// approvals voting for a given validator, how much successful approvals /// was collected and how many times the given validator no-showed -#[derive(Encode, Decode, DecodeWithMemTracking, Clone, Copy, PartialEq, RuntimeDebug, TypeInfo)] +#[derive( + RuntimeDebug, + Copy, + Clone, + PartialEq, + Encode, + Decode, + DecodeWithMemTracking, + TypeInfo, +)] pub struct ApprovalStatisticsTallyLine { /// represents the validator to which the statistics belongs to pub validator_index: ValidatorIndex, @@ -43,8 +52,16 @@ pub struct ApprovalStatisticsTallyLine { /// ApprovalRewards is the set of tallies where each tally represents /// a given validator and its approval voting statistics -#[derive(Clone, Encode, Decode, TypeInfo, RuntimeDebug)] -pub struct ApprovalStatistics(pub SessionIndex, pub Vec); +#[derive( + RuntimeDebug, + Clone, + PartialEq, + Encode, + Decode, + DecodeWithMemTracking, + TypeInfo, +)] +pub struct ApprovalStatistics(pub SessionIndex, pub ValidatorIndex, pub Vec); impl ApprovalStatistics { pub fn signing_payload(&self) -> Vec { diff --git a/polkadot/runtime/parachains/src/approvals_rewards/mod.rs b/polkadot/runtime/parachains/src/approvals_rewards/mod.rs new file mode 100644 index 0000000000000..b9183d8eacf0e --- /dev/null +++ b/polkadot/runtime/parachains/src/approvals_rewards/mod.rs @@ -0,0 +1,208 @@ +// Copyright (C) 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 . + +//! Approvals Rewards pallet. + +use crate::{ + configuration, + inclusion::{QueueFootprinter, UmpQueueId}, + initializer::SessionChangeNotification, + session_info, + shared, +}; +use codec::{Decode, Encode}; +use core::{cmp, mem}; +use frame_support::{ + pallet_prelude::*, + traits::{EnsureOriginWithArg, EstimateNextSessionRotation}, + DefaultNoBound, +}; +use scale_info::{Type, TypeInfo}; +use sp_runtime::{ + traits::{AppVerify, One, Saturating}, + DispatchResult, SaturatedConversion, +}; +use frame_system::pallet_prelude::*; +use polkadot_primitives::{ + vstaging::ApprovalStatistics, + slashing::{DisputeProof, DisputesTimeSlot, PendingSlashes}, + CandidateHash, DisputeOffenceKind, SessionIndex, ValidatorId, ValidatorIndex, + ValidatorSignature, +}; + + +const LOG_TARGET: &str = "runtime::approvals_rewards"; + +pub use pallet::*; + +#[frame_support::pallet] +pub mod pallet { + use polkadot_parachain_primitives::primitives::ValidationCodeHash; + use polkadot_primitives::v9::ParaId; + use super::*; + + use sp_runtime::transaction_validity::{ + InvalidTransaction, TransactionPriority, TransactionSource, TransactionValidity, + ValidTransaction, + }; + use crate::disputes::WeightInfo; + use crate::paras::CodeByHash; + + #[pallet::pallet] + #[pallet::without_storage_info] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: + frame_system::Config + + configuration::Config + + shared::Config + + session_info::Config + + frame_system::offchain::CreateBare> + { + #[allow(deprecated)] + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + } + + /// Actual past code hash, indicated by the para id as well as the block number at which it + /// became outdated. + #[pallet::storage] + pub(super) type ApprovalsTallies = + StorageMap<_, Twox64Concat, (SessionIndex, ValidatorIndex), ValidationCodeHash>; + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { } + + #[pallet::error] + pub enum Error { + /// The approval rewards payload has a future session index. + ApprovalRewardsFutureSession, + + /// The approval rewards payloads has an already pruned session index. + ApprovalRewardsPassedSession, + + /// The session index has no available data and is not the current session index + ApprovalRewardsUnknownSessionIndex, + + /// Validator index is not in the session validators bounds + ApprovalRewardsValidatorIndexOutOfBounds, + + /// Invalid signed payload + ApprovalRewardsInvalidSignature, + } + + #[pallet::call] + impl Pallet { + #[pallet::call_index(0)] + #[pallet::weight(1)] + pub fn include_approvals_rewards_statistics( + origin: OriginFor, + payload: ApprovalStatistics, + signature: ValidatorSignature, + ) -> DispatchResultWithPostInfo { + ensure_none(origin)?; + + let current_session = shared::CurrentSessionIndex::::get(); + let payload_session_index = payload.0; + let payload_validator_index = payload.1; + + let config = configuration::ActiveConfig::::get(); + + if payload_session_index > current_session { + return Err(Error::::ApprovalRewardsFutureSession.into()) + } else if payload_session_index < current_session.saturating_sub(config.dispute_period) { + return Err(Error::::ApprovalRewardsPassedSession.into()) + } + + let validator_public = if payload_session_index == current_session { + let validators = shared::ActiveValidatorKeys::::get(); + let validator_index = payload_validator_index.0 as usize; + validators + .get(validator_index) + .ok_or(Error::::ApprovalRewardsValidatorIndexOutOfBounds)? + .clone() + } else { + let session_info = match session_info::Sessions::::get(payload_session_index) { + Some(s) => s, + None => return Err(Error::::ApprovalRewardsUnknownSessionIndex.into()), + }; + + session_info.validators + .get(payload_validator_index) + .ok_or(Error::::ApprovalRewardsValidatorIndexOutOfBounds)? + .clone() + }; + + let signing_payload = payload.signing_payload(); + ensure!( + signature.verify(&signing_payload[..], &validator_public), + Error::::ApprovalRewardsInvalidSignature, + ); + + Ok(Pays::No.into()) + } + } + + #[pallet::validate_unsigned] + impl ValidateUnsigned for Pallet { + type Call = Call; + + fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity { + match call { + Call::include_approvals_rewards_statistics { payload, signature } => { + ValidTransaction::with_tag_prefix("ApprovalRewardsStatistics") + .priority(TransactionPriority::max_value()) + .longevity(64_u64) + .and_provides((payload.0, payload.1, payload.2.clone())) + .propagate(true) + .build() + } + _ => InvalidTransaction::Call.into(), + } + } + + fn pre_dispatch(_call: &Self::Call) -> Result<(), TransactionValidityError> { + Ok(()) + } + } +} + +impl Pallet +where + T: Config + frame_system::offchain::CreateBare> +{ + /// Submits a given PVF check statement with corresponding signature as an unsigned transaction + /// into the memory pool. Ultimately, that disseminates the transaction across the network. + /// + /// This function expects an offchain context and cannot be callable from the on-chain logic. + /// + /// The signature assumed to pertain to `stmt`. + /// + pub(crate) fn submit_approval_statistics( + payload: ApprovalStatistics, + signature: ValidatorSignature, + ) { + use frame_system::offchain::{CreateBare, SubmitTransaction}; + let call = Call::include_approvals_rewards_statistics { payload, signature }; + + let xt = >>::create_bare(call.into()); + + if let Err(e) = SubmitTransaction::>::submit_transaction(xt) { + log::error!(target: LOG_TARGET, "Error submitting pvf check statement: {:?}", e,); + } + } +} \ No newline at end of file diff --git a/polkadot/runtime/parachains/src/lib.rs b/polkadot/runtime/parachains/src/lib.rs index 1cd534257d7f9..55b6e2880f2b4 100644 --- a/polkadot/runtime/parachains/src/lib.rs +++ b/polkadot/runtime/parachains/src/lib.rs @@ -40,6 +40,7 @@ pub mod reward_points; pub mod scheduler; pub mod session_info; pub mod shared; +pub mod approvals_rewards; pub mod runtime_api_impl; diff --git a/polkadot/runtime/parachains/src/paras/mod.rs b/polkadot/runtime/parachains/src/paras/mod.rs index 3b018ec2ac2f0..2c521dab614c6 100644 --- a/polkadot/runtime/parachains/src/paras/mod.rs +++ b/polkadot/runtime/parachains/src/paras/mod.rs @@ -145,7 +145,6 @@ pub mod benchmarking; pub(crate) mod tests; pub use pallet::*; -use polkadot_primitives::vstaging::ApprovalStatistics; const LOG_TARGET: &str = "runtime::paras"; @@ -2449,13 +2448,6 @@ impl Pallet { } } - pub(crate) fn submit_approval_statistics( - payload: ApprovalStatistics, - signature: ValidatorSignature, - ) { - // TODO: to be implemented - } - /// Returns the current lifecycle state of the para. pub fn lifecycle(id: ParaId) -> Option { ParaLifecycles::::get(&id) diff --git a/polkadot/runtime/parachains/src/runtime_api_impl/v13.rs b/polkadot/runtime/parachains/src/runtime_api_impl/v13.rs index cd0e783b3accd..0e182151b6b5f 100644 --- a/polkadot/runtime/parachains/src/runtime_api_impl/v13.rs +++ b/polkadot/runtime/parachains/src/runtime_api_impl/v13.rs @@ -344,14 +344,6 @@ pub fn submit_pvf_check_statement( paras::Pallet::::submit_pvf_check_statement(stmt, signature) } -/// Submits the collected approval statistics for a given session. -pub fn submit_approval_statistics( - payload: ApprovalStatistics, - signature: ValidatorSignature, -) { - paras::Pallet::::submit_approval_statistics(payload, signature) -} - /// Returns the list of all PVF code hashes that require pre-checking. pub fn pvfs_require_precheck() -> Vec { paras::Pallet::::pvfs_require_precheck() diff --git a/polkadot/runtime/parachains/src/runtime_api_impl/vstaging.rs b/polkadot/runtime/parachains/src/runtime_api_impl/vstaging.rs index 0f6725e2785a1..545a414df4159 100644 --- a/polkadot/runtime/parachains/src/runtime_api_impl/vstaging.rs +++ b/polkadot/runtime/parachains/src/runtime_api_impl/vstaging.rs @@ -16,10 +16,11 @@ //! Put implementations of functions from staging APIs here. -use crate::{disputes, initializer, paras}; +use crate::{disputes, initializer, paras, approvals_rewards}; use alloc::vec::Vec; -use polkadot_primitives::{slashing, CandidateHash, Id as ParaId, SessionIndex}; +use polkadot_primitives::{slashing, CandidateHash, Id as ParaId, SessionIndex, ValidatorSignature}; +use polkadot_primitives::vstaging::ApprovalStatistics; /// Implementation of `para_ids` runtime API pub fn para_ids() -> Vec { @@ -31,3 +32,11 @@ pub fn unapplied_slashes_v2( ) -> Vec<(SessionIndex, CandidateHash, slashing::PendingSlashes)> { disputes::slashing::Pallet::::unapplied_slashes() } + +/// Submits the collected approval statistics for a given session. +pub fn submit_approval_statistics( + payload: ApprovalStatistics, + signature: ValidatorSignature, +) { + approvals_rewards::Pallet::::submit_approval_statistics(payload, signature) +} \ No newline at end of file diff --git a/polkadot/runtime/rococo/src/lib.rs b/polkadot/runtime/rococo/src/lib.rs index 108d03289adf7..8a625550e7a78 100644 --- a/polkadot/runtime/rococo/src/lib.rs +++ b/polkadot/runtime/rococo/src/lib.rs @@ -80,6 +80,7 @@ use polkadot_runtime_parachains::{ }, scheduler as parachains_scheduler, session_info as parachains_session_info, shared as parachains_shared, + approvals_rewards as parachains_approvals_rewards, }; use rococo_runtime_constants::system_parachain::{coretime::TIMESLICE_PERIOD, BROKER_ID}; use scale_info::TypeInfo; @@ -1224,6 +1225,10 @@ impl parachains_slashing::Config for Runtime { type BenchmarkingConfig = parachains_slashing::BenchConfig<200>; } +impl parachains_approvals_rewards::Config for Runtime { + type RuntimeEvent = RuntimeEvent; +} + parameter_types! { pub const ParaDeposit: Balance = 40 * UNITS; } @@ -1591,6 +1596,7 @@ construct_runtime! { MessageQueue: pallet_message_queue = 64, OnDemandAssignmentProvider: parachains_on_demand = 66, CoretimeAssignmentProvider: parachains_assigner_coretime = 68, + ApprovalsRewards: parachains_approvals_rewards = 69, // Parachain Onboarding Pallets. Start indices at 70 to leave room. Registrar: paras_registrar = 70, @@ -2108,7 +2114,7 @@ sp_api::impl_runtime_apis! { payload: polkadot_primitives::vstaging::ApprovalStatistics, signature: polkadot_primitives::ValidatorSignature ) { - parachains_runtime_api_impl::submit_approval_statistics::(payload, signature) + parachains_staging_runtime_api_impl::submit_approval_statistics::(payload, signature) } fn pvfs_require_precheck() -> Vec { diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs index 41a82ba11c406..444e216fe081a 100644 --- a/polkadot/runtime/westend/src/lib.rs +++ b/polkadot/runtime/westend/src/lib.rs @@ -91,6 +91,7 @@ use polkadot_runtime_parachains::{ }, scheduler as parachains_scheduler, session_info as parachains_session_info, shared as parachains_shared, + approvals_rewards as parachains_approvals_rewards, }; use scale_info::TypeInfo; use sp_authority_discovery::AuthorityId as AuthorityDiscoveryId; @@ -1445,6 +1446,10 @@ impl parachains_paras::Config for Runtime { >; } +impl parachains_approvals_rewards::Config for Runtime { + type RuntimeEvent = RuntimeEvent; +} + parameter_types! { /// Amount of weight that can be spent per block to service messages. /// @@ -1979,6 +1984,8 @@ mod runtime { pub type OnDemandAssignmentProvider = parachains_on_demand; #[runtime::pallet_index(57)] pub type CoretimeAssignmentProvider = parachains_assigner_coretime; + #[runtime::pallet_index(58)] + pub type ApprovalsRewards = parachains_approvals_rewards; // Parachain Onboarding Pallets. Start indices at 60 to leave room. #[runtime::pallet_index(60)] @@ -2357,7 +2364,7 @@ sp_api::impl_runtime_apis! { payload: ApprovalStatistics, signature: ValidatorSignature, ) { - parachains_runtime_api_impl::submit_approval_statistics::(payload, signature) + parachains_staging_runtime_api_impl::submit_approval_statistics::(payload, signature) } fn pvfs_require_precheck() -> Vec { diff --git a/substrate/frame/staking-async/runtimes/rc/src/lib.rs b/substrate/frame/staking-async/runtimes/rc/src/lib.rs index 16097427438b4..3ece1fefb6969 100644 --- a/substrate/frame/staking-async/runtimes/rc/src/lib.rs +++ b/substrate/frame/staking-async/runtimes/rc/src/lib.rs @@ -2291,7 +2291,7 @@ sp_api::impl_runtime_apis! { payload: ApprovalStatistic, signature: ValidatorSignature, ) { - parachains_runtime_api_impl::submit_approval_statistics::(payload, signature) + parachains_staging_runtime_api_impl::submit_approval_statistics::(payload, signature) } fn pvfs_require_precheck() -> Vec { From bf6dd3b85466591bb7051c27813d8a9be3cd73e9 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Mon, 8 Dec 2025 09:05:22 -0400 Subject: [PATCH 46/48] chore small changes --- polkadot/runtime/parachains/src/approvals_rewards/mod.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/polkadot/runtime/parachains/src/approvals_rewards/mod.rs b/polkadot/runtime/parachains/src/approvals_rewards/mod.rs index b9183d8eacf0e..c48b99a969266 100644 --- a/polkadot/runtime/parachains/src/approvals_rewards/mod.rs +++ b/polkadot/runtime/parachains/src/approvals_rewards/mod.rs @@ -52,6 +52,7 @@ pub use pallet::*; pub mod pallet { use polkadot_parachain_primitives::primitives::ValidationCodeHash; use polkadot_primitives::v9::ParaId; + use polkadot_primitives::vstaging::ApprovalStatisticsTallyLine; use super::*; use sp_runtime::transaction_validity::{ @@ -81,7 +82,7 @@ pub mod pallet { /// became outdated. #[pallet::storage] pub(super) type ApprovalsTallies = - StorageMap<_, Twox64Concat, (SessionIndex, ValidatorIndex), ValidationCodeHash>; + StorageMap<_, Twox64Concat, (SessionIndex, ValidatorIndex), Vec>; #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] From 32681d185a1492ba64ec21e645eda6eb6f468816 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Wed, 10 Dec 2025 07:29:04 -0400 Subject: [PATCH 47/48] chore: include benchmarks --- .../src/approvals_rewards/benchmarking.rs | 86 +++++++++++++++++++ .../parachains/src/approvals_rewards/mod.rs | 36 +++++++- polkadot/runtime/rococo/src/lib.rs | 1 + polkadot/runtime/westend/src/lib.rs | 2 + 4 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 polkadot/runtime/parachains/src/approvals_rewards/benchmarking.rs diff --git a/polkadot/runtime/parachains/src/approvals_rewards/benchmarking.rs b/polkadot/runtime/parachains/src/approvals_rewards/benchmarking.rs new file mode 100644 index 0000000000000..b573b2d8f56ea --- /dev/null +++ b/polkadot/runtime/parachains/src/approvals_rewards/benchmarking.rs @@ -0,0 +1,86 @@ +// Copyright (C) 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 . +#![cfg(feature = "runtime-benchmarks")] + +use super::*; +use alloc::vec; +use frame_benchmarking::v2::*; +use polkadot_primitives::{PvfCheckStatement, ValidatorId, ValidatorIndex, ValidatorSignature}; +use polkadot_primitives::{ + vstaging::ApprovalStatistics, + SessionIndex, +}; +use frame_system::{RawOrigin}; +use sp_application_crypto::RuntimeAppPublic; +use crate::{configuration, shared}; + +// Constants for the benchmarking +const VALIDATOR_NUM: usize = 800; +const SESSION_INDEX: SessionIndex = 1; + +fn initialize() +where + T: Config + shared::Config, +{ + // 0. generate a list of validators + let validators = (0..VALIDATOR_NUM) + .map(|_| ::generate_pair(None)) + .collect::>(); + + // 1. Make sure PVF pre-checking is enabled in the config. + let config = configuration::ActiveConfig::::get(); + configuration::Pallet::::force_set_active_config(config.clone()); + + // 2. initialize a new session with deterministic validator set. + crate::shared::pallet::Pallet::::set_active_validators_ascending(validators.clone()); + crate::shared::pallet::Pallet::::set_session_index(SESSION_INDEX); +} + +fn generate_approvals_tallies() -> impl Iterator +where + T: Config + shared::Config +{ + let validators = shared::ActiveValidatorKeys::::get(); + + (0..validators.len()).map(move |validator_index| { + let mut tally = vec![]; + let payload = ApprovalStatistics(SESSION_INDEX, ValidatorIndex(validator_index as u32), tally); + let signature = validators[validator_index].sign(&payload.signing_payload()).unwrap(); + (payload, signature) + }) +} + +#[benchmarks] +mod benchmarks { + use super::*; + #[benchmark] + fn include_approvals_rewards_statistics() { + initialize::(); + let (payload, signature) = generate_approvals_tallies::().next().unwrap();; + + #[block] + { + let _ = + Pallet::::include_approvals_rewards_statistics(RawOrigin::None.into(), payload, signature); + } + } + + impl_benchmark_test_suite! { + Pallet, + crate::mock::new_test_ext(Default::default()), + crate::mock::Test + } +} \ No newline at end of file diff --git a/polkadot/runtime/parachains/src/approvals_rewards/mod.rs b/polkadot/runtime/parachains/src/approvals_rewards/mod.rs index c48b99a969266..3a0bd2761f204 100644 --- a/polkadot/runtime/parachains/src/approvals_rewards/mod.rs +++ b/polkadot/runtime/parachains/src/approvals_rewards/mod.rs @@ -16,6 +16,7 @@ //! Approvals Rewards pallet. +use alloc::vec::Vec; use crate::{ configuration, inclusion::{QueueFootprinter, UmpQueueId}, @@ -43,11 +44,25 @@ use polkadot_primitives::{ ValidatorSignature, }; +#[cfg(feature = "runtime-benchmarks")] +pub mod benchmarking; const LOG_TARGET: &str = "runtime::approvals_rewards"; pub use pallet::*; +pub trait WeightInfo { + fn include_approvals_rewards_statistics() -> Weight; +} + +pub struct TestWeightInfo; +impl WeightInfo for TestWeightInfo { + fn include_approvals_rewards_statistics() -> Weight { + // This special value is to distinguish from the finalizing variants above in tests. + Weight::MAX - Weight::from_parts(1, 1) + } +} + #[frame_support::pallet] pub mod pallet { use polkadot_parachain_primitives::primitives::ValidationCodeHash; @@ -76,6 +91,9 @@ pub mod pallet { { #[allow(deprecated)] type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + // Weight information for extrinsics in this pallet. + //type WeightInfo: WeightInfo; } /// Actual past code hash, indicated by the para id as well as the block number at which it @@ -86,7 +104,9 @@ pub mod pallet { #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] - pub enum Event { } + pub enum Event { + ApprovalTalliesStored((SessionIndex, ValidatorIndex)) + } #[pallet::error] pub enum Error { @@ -104,12 +124,16 @@ pub mod pallet { /// Invalid signed payload ApprovalRewardsInvalidSignature, + + /// The validator already have submitted a tally for that session + ApprovalTalliesAlreadyStored, } #[pallet::call] impl Pallet { #[pallet::call_index(0)] #[pallet::weight(1)] + //#[pallet::weight(::WeightInfo::include_approvals_rewards_statistics())] pub fn include_approvals_rewards_statistics( origin: OriginFor, payload: ApprovalStatistics, @@ -154,6 +178,16 @@ pub mod pallet { Error::::ApprovalRewardsInvalidSignature, ); + let approvals_key = (payload_session_index, payload_validator_index); + + // Ensure that it is a fresh session tally. + if let Some(_) = ApprovalsTallies::::get(&approvals_key) { + return Err(Error::::ApprovalTalliesAlreadyStored.into()) + } + + ApprovalsTallies::::insert(approvals_key, payload.2); + Self::deposit_event(Event::ApprovalTalliesStored(approvals_key)); + //Ok(Some(::WeightInfo::include_approvals_rewards_statistics()).into()) Ok(Pays::No.into()) } } diff --git a/polkadot/runtime/rococo/src/lib.rs b/polkadot/runtime/rococo/src/lib.rs index 8a625550e7a78..0ddfe09832206 100644 --- a/polkadot/runtime/rococo/src/lib.rs +++ b/polkadot/runtime/rococo/src/lib.rs @@ -1867,6 +1867,7 @@ mod benches { [polkadot_runtime_parachains::paras_inherent, ParaInherent] [polkadot_runtime_parachains::paras, Paras] [polkadot_runtime_parachains::on_demand, OnDemandAssignmentProvider] + [polkadot_runtime_parachains::approvals_rewards, ApprovalsRewards] // Substrate [pallet_balances, Balances] [pallet_balances, NisCounterpartBalances] diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs index 444e216fe081a..cf9703b39616f 100644 --- a/polkadot/runtime/westend/src/lib.rs +++ b/polkadot/runtime/westend/src/lib.rs @@ -1448,6 +1448,7 @@ impl parachains_paras::Config for Runtime { impl parachains_approvals_rewards::Config for Runtime { type RuntimeEvent = RuntimeEvent; + //type WeightInfo = weights::polkadot_runtime_parachains_inclusion::WeightInfo; } parameter_types! { @@ -2150,6 +2151,7 @@ mod benches { [polkadot_runtime_parachains::paras_inherent, ParaInherent] [polkadot_runtime_parachains::on_demand, OnDemandAssignmentProvider] [polkadot_runtime_parachains::coretime, Coretime] + [polkadot_runtime_parachains::approvals_rewards, ApprovalsRewards] // Substrate [pallet_bags_list, VoterList] [pallet_balances, Balances] From 3b58ec7c28132c1b00601d0d65ebf760a0f1ce43 Mon Sep 17 00:00:00 2001 From: EclesioMeloJunior Date: Thu, 11 Dec 2025 10:47:25 -0400 Subject: [PATCH 48/48] chore: store medians, calculate medians of the prev session --- .../parachains/src/approvals_rewards/mod.rs | 76 ++++++++++++++++--- .../runtime/parachains/src/initializer.rs | 3 + polkadot/runtime/westend/src/lib.rs | 2 +- 3 files changed, 68 insertions(+), 13 deletions(-) diff --git a/polkadot/runtime/parachains/src/approvals_rewards/mod.rs b/polkadot/runtime/parachains/src/approvals_rewards/mod.rs index 3a0bd2761f204..cb7b1fd4b1ed6 100644 --- a/polkadot/runtime/parachains/src/approvals_rewards/mod.rs +++ b/polkadot/runtime/parachains/src/approvals_rewards/mod.rs @@ -16,7 +16,7 @@ //! Approvals Rewards pallet. -use alloc::vec::Vec; +use alloc::vec::*; use crate::{ configuration, inclusion::{QueueFootprinter, UmpQueueId}, @@ -31,7 +31,10 @@ use frame_support::{ traits::{EnsureOriginWithArg, EstimateNextSessionRotation}, DefaultNoBound, }; -use scale_info::{Type, TypeInfo}; +use scale_info::{ + Type, TypeInfo, + prelude::vec, +}; use sp_runtime::{ traits::{AppVerify, One, Saturating}, DispatchResult, SaturatedConversion, @@ -40,8 +43,10 @@ use frame_system::pallet_prelude::*; use polkadot_primitives::{ vstaging::ApprovalStatistics, slashing::{DisputeProof, DisputesTimeSlot, PendingSlashes}, - CandidateHash, DisputeOffenceKind, SessionIndex, ValidatorId, ValidatorIndex, - ValidatorSignature, + CandidateHash, + DisputeOffenceKind, + SessionIndex, ValidatorId, ValidatorIndex, ValidatorSignature, + IndexedVec, byzantine_threshold }; #[cfg(feature = "runtime-benchmarks")] @@ -50,6 +55,7 @@ pub mod benchmarking; const LOG_TARGET: &str = "runtime::approvals_rewards"; pub use pallet::*; +use polkadot_primitives::vstaging::ApprovalStatisticsTallyLine; pub trait WeightInfo { fn include_approvals_rewards_statistics() -> Weight; @@ -65,17 +71,12 @@ impl WeightInfo for TestWeightInfo { #[frame_support::pallet] pub mod pallet { - use polkadot_parachain_primitives::primitives::ValidationCodeHash; - use polkadot_primitives::v9::ParaId; - use polkadot_primitives::vstaging::ApprovalStatisticsTallyLine; use super::*; - + use polkadot_primitives::vstaging::ApprovalStatisticsTallyLine; use sp_runtime::transaction_validity::{ InvalidTransaction, TransactionPriority, TransactionSource, TransactionValidity, ValidTransaction, }; - use crate::disputes::WeightInfo; - use crate::paras::CodeByHash; #[pallet::pallet] #[pallet::without_storage_info] @@ -93,7 +94,7 @@ pub mod pallet { type RuntimeEvent: From> + IsType<::RuntimeEvent>; // Weight information for extrinsics in this pallet. - //type WeightInfo: WeightInfo; + // type WeightInfo: WeightInfo; } /// Actual past code hash, indicated by the para id as well as the block number at which it @@ -102,6 +103,10 @@ pub mod pallet { pub(super) type ApprovalsTallies = StorageMap<_, Twox64Concat, (SessionIndex, ValidatorIndex), Vec>; + #[pallet::storage] + pub(super) type AvailableApprovalsMedians = + StorageMap<_, Twox64Concat, SessionIndex, Vec>; + #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { @@ -181,7 +186,7 @@ pub mod pallet { let approvals_key = (payload_session_index, payload_validator_index); // Ensure that it is a fresh session tally. - if let Some(_) = ApprovalsTallies::::get(&approvals_key) { + if ApprovalsTallies::::contains_key(&approvals_key) { return Err(Error::::ApprovalTalliesAlreadyStored.into()) } @@ -216,6 +221,53 @@ pub mod pallet { } } +impl Pallet { + /// Handle an incoming session change. + pub(crate) fn initializer_on_new_session( + notification: &SessionChangeNotification>, + ) { + let previous_session = notification.session_index.saturating_sub(1); + let session_info = match session_info::Sessions::::get(previous_session) { + Some(s) => s, + None => return, + }; + + let validators_len = session_info.validators.len(); + + let mut rewards_matrix: Vec> = vec![]; + for idx in 0..validators_len { + let v_idx = ValidatorIndex(idx as u32); + if let Some(tally) = ApprovalsTallies::::get((previous_session, v_idx)) { + rewards_matrix.push(tally); + } + } + + if rewards_matrix.len() >= byzantine_threshold(validators_len) { + let mut approval_usages_medians = Vec::new(); + for (v_idx, _) in session_info.validators.into_iter().enumerate() { + let mut v: Vec = rewards_matrix.iter().map(|at| at[v_idx].approvals_usage).collect(); + v.sort(); + approval_usages_medians.push(v[validators_len/2]); + } + + AvailableApprovalsMedians::::insert(previous_session, approval_usages_medians); + } + + let mut drop_keys = vec![]; + let config = configuration::ActiveConfig::::get(); + ApprovalsTallies::::iter_keys().for_each(|(session_idx, validator_idx)| { + let min_session_to_keep = notification.session_index - config.dispute_period; + if session_idx < min_session_to_keep { + drop_keys.push((session_idx, validator_idx)); + } + }); + + for key in drop_keys { + ApprovalsTallies::::remove(key); + } + } +} + impl Pallet where T: Config + frame_system::offchain::CreateBare> diff --git a/polkadot/runtime/parachains/src/initializer.rs b/polkadot/runtime/parachains/src/initializer.rs index 6ee245fb5230c..00a1329839b0c 100644 --- a/polkadot/runtime/parachains/src/initializer.rs +++ b/polkadot/runtime/parachains/src/initializer.rs @@ -24,6 +24,7 @@ use crate::{ configuration::{self, HostConfiguration}, disputes::{self, DisputesHandler as _, SlashingHandler as _}, dmp, hrmp, inclusion, paras, scheduler, session_info, shared, + approvals_rewards, }; use alloc::vec::Vec; use codec::{Decode, Encode}; @@ -125,6 +126,7 @@ pub mod pallet { + disputes::Config + dmp::Config + hrmp::Config + + approvals_rewards::Config { /// A randomness beacon. type Randomness: Randomness>; @@ -283,6 +285,7 @@ impl Pallet { dmp::Pallet::::initializer_on_new_session(¬ification, &outgoing_paras); hrmp::Pallet::::initializer_on_new_session(¬ification, &outgoing_paras); T::CoretimeOnNewSession::on_new_session(¬ification); + approvals_rewards::Pallet::::initializer_on_new_session(¬ification); } /// Should be called when a new session occurs. Buffers the session notification to be applied diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs index cf9703b39616f..913d484ef4f31 100644 --- a/polkadot/runtime/westend/src/lib.rs +++ b/polkadot/runtime/westend/src/lib.rs @@ -1448,7 +1448,7 @@ impl parachains_paras::Config for Runtime { impl parachains_approvals_rewards::Config for Runtime { type RuntimeEvent = RuntimeEvent; - //type WeightInfo = weights::polkadot_runtime_parachains_inclusion::WeightInfo; + // type WeightInfo = weights::polkadot_runtime_parachains_inclusion::WeightInfo; } parameter_types! {