diff --git a/.gitlab/pipeline/zombienet/polkadot.yml b/.gitlab/pipeline/zombienet/polkadot.yml index 90251082077ce..b4ef4bb7446c8 100644 --- a/.gitlab/pipeline/zombienet/polkadot.yml +++ b/.gitlab/pipeline/zombienet/polkadot.yml @@ -276,6 +276,14 @@ zombienet-polkadot-smoke-0004-coretime-smoke-test: --local-dir="${LOCAL_DIR}/smoke" --test="0004-coretime-smoke-test.zndsl" +zombienet-polkadot-smoke-0005-precompile-pvf-smoke: + extends: + - .zombienet-polkadot-common + script: + - /home/nonroot/zombie-net/scripts/ci/run-test-local-env-manager.sh + --local-dir="${LOCAL_DIR}/smoke" + --test="0005-precompile-pvf-smoke.zndsl" + zombienet-polkadot-misc-0001-parachains-paritydb: extends: - .zombienet-polkadot-common diff --git a/Cargo.lock b/Cargo.lock index 0a12c1e524de0..49ecd4dd2bd5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13318,8 +13318,10 @@ dependencies = [ "polkadot-parachain-primitives", "polkadot-primitives", "polkadot-primitives-test-helpers", + "sp-application-crypto", "sp-core", "sp-keyring", + "sp-keystore", "sp-maybe-compressed-blob", "tracing-gum", ] diff --git a/polkadot/node/core/candidate-validation/Cargo.toml b/polkadot/node/core/candidate-validation/Cargo.toml index e1a98f80783fa..13ab3e3fba50a 100644 --- a/polkadot/node/core/candidate-validation/Cargo.toml +++ b/polkadot/node/core/candidate-validation/Cargo.toml @@ -15,6 +15,8 @@ futures = { workspace = true } futures-timer = { workspace = true } gum = { workspace = true, default-features = true } +sp-keystore = { workspace = true } +sp-application-crypto = { workspace = true } sp-maybe-compressed-blob = { workspace = true, default-features = true } codec = { features = ["bit-vec", "derive"], workspace = true } diff --git a/polkadot/node/core/candidate-validation/src/lib.rs b/polkadot/node/core/candidate-validation/src/lib.rs index 76619bd391f2b..1985964ebc512 100644 --- a/polkadot/node/core/candidate-validation/src/lib.rs +++ b/polkadot/node/core/candidate-validation/src/lib.rs @@ -39,7 +39,8 @@ use polkadot_node_subsystem::{ overseer, FromOrchestra, OverseerSignal, SpawnedSubsystem, SubsystemError, SubsystemResult, SubsystemSender, }; -use polkadot_node_subsystem_util::executor_params_at_relay_parent; +use polkadot_node_subsystem_util as util; +use polkadot_overseer::ActiveLeavesUpdate; use polkadot_parachain_primitives::primitives::{ ValidationParams, ValidationResult as WasmValidationResult, }; @@ -48,16 +49,19 @@ use polkadot_primitives::{ DEFAULT_APPROVAL_EXECUTION_TIMEOUT, DEFAULT_BACKING_EXECUTION_TIMEOUT, DEFAULT_LENIENT_PREPARATION_TIMEOUT, DEFAULT_PRECHECK_PREPARATION_TIMEOUT, }, - CandidateCommitments, CandidateDescriptor, CandidateReceipt, ExecutorParams, Hash, - OccupiedCoreAssumption, PersistedValidationData, PvfExecKind, PvfPrepKind, ValidationCode, - ValidationCodeHash, + AuthorityDiscoveryId, CandidateCommitments, CandidateDescriptor, CandidateEvent, + CandidateReceipt, ExecutorParams, Hash, OccupiedCoreAssumption, PersistedValidationData, + PvfExecKind, PvfPrepKind, SessionIndex, ValidationCode, ValidationCodeHash, }; +use sp_application_crypto::{AppCrypto, ByteArray}; +use sp_keystore::KeystorePtr; use codec::Encode; use futures::{channel::oneshot, prelude::*, stream::FuturesUnordered}; use std::{ + collections::HashSet, path::PathBuf, pin::Pin, sync::Arc, @@ -88,7 +92,7 @@ const PVF_APPROVAL_EXECUTION_RETRY_DELAY: Duration = Duration::from_millis(200); const TASK_LIMIT: usize = 30; /// Configuration for the candidate validation subsystem -#[derive(Clone)] +#[derive(Clone, Default)] pub struct Config { /// The path where candidate validation can store compiled artifacts for PVFs. pub artifacts_cache_path: PathBuf, @@ -111,6 +115,7 @@ pub struct Config { /// The candidate validation subsystem. pub struct CandidateValidationSubsystem { + keystore: KeystorePtr, #[allow(missing_docs)] pub metrics: Metrics, #[allow(missing_docs)] @@ -122,10 +127,11 @@ impl CandidateValidationSubsystem { /// Create a new `CandidateValidationSubsystem`. pub fn with_config( config: Option, + keystore: KeystorePtr, metrics: Metrics, pvf_metrics: polkadot_node_core_pvf::Metrics, ) -> Self { - CandidateValidationSubsystem { config, metrics, pvf_metrics } + CandidateValidationSubsystem { keystore, config, metrics, pvf_metrics } } } @@ -133,7 +139,7 @@ impl CandidateValidationSubsystem { impl CandidateValidationSubsystem { fn start(self, ctx: Context) -> SpawnedSubsystem { if let Some(config) = self.config { - let future = run(ctx, self.metrics, self.pvf_metrics, config) + let future = run(ctx, self.keystore, self.metrics, self.pvf_metrics, config) .map_err(|e| SubsystemError::with_origin("candidate-validation", e)) .boxed(); SpawnedSubsystem { name: "candidate-validation-subsystem", future } @@ -223,6 +229,7 @@ where #[overseer::contextbounds(CandidateValidation, prefix = self::overseer)] async fn run( mut ctx: Context, + keystore: KeystorePtr, metrics: Metrics, pvf_metrics: polkadot_node_core_pvf::Metrics, Config { @@ -253,13 +260,16 @@ async fn run( ctx.spawn_blocking("pvf-validation-host", task.boxed())?; let mut tasks = FuturesUnordered::new(); + let mut prepare_state = PrepareValidationState::default(); loop { loop { futures::select! { comm = ctx.recv().fuse() => { match comm { - Ok(FromOrchestra::Signal(OverseerSignal::ActiveLeaves(_))) => {}, + Ok(FromOrchestra::Signal(OverseerSignal::ActiveLeaves(update))) => { + maybe_prepare_validation(ctx.sender(), keystore.clone(), validation_host.clone(), update, &mut prepare_state).await; + }, Ok(FromOrchestra::Signal(OverseerSignal::BlockFinalized(..))) => {}, Ok(FromOrchestra::Signal(OverseerSignal::Conclude)) => return Ok(()), Ok(FromOrchestra::Communication { msg }) => { @@ -298,6 +308,246 @@ async fn run( } } +struct PrepareValidationState { + session_index: Option, + is_next_session_authority: bool, + // PVF host won't prepare the same code hash twice, so here we just avoid extra communication + already_prepared_code_hashes: HashSet, + // How many PVFs per block we take to prepare themselves for the next session validation + per_block_limit: usize, +} + +impl Default for PrepareValidationState { + fn default() -> Self { + Self { + session_index: None, + is_next_session_authority: false, + already_prepared_code_hashes: HashSet::new(), + per_block_limit: 1, + } + } +} + +async fn maybe_prepare_validation( + sender: &mut Sender, + keystore: KeystorePtr, + validation_backend: impl ValidationBackend, + update: ActiveLeavesUpdate, + state: &mut PrepareValidationState, +) where + Sender: SubsystemSender, +{ + let Some(leaf) = update.activated else { return }; + let new_session_index = new_session_index(sender, state.session_index, leaf.hash).await; + if new_session_index.is_some() { + state.session_index = new_session_index; + state.already_prepared_code_hashes.clear(); + state.is_next_session_authority = check_next_session_authority( + sender, + keystore, + leaf.hash, + state.session_index.expect("qed: just checked above"), + ) + .await; + } + + // On every active leaf check candidates and prepare PVFs our node doesn't have yet. + if state.is_next_session_authority { + let code_hashes = prepare_pvfs_for_backed_candidates( + sender, + validation_backend, + leaf.hash, + &state.already_prepared_code_hashes, + state.per_block_limit, + ) + .await; + state.already_prepared_code_hashes.extend(code_hashes.unwrap_or_default()); + } +} + +// Returns the new session index if it is greater than the current one. +async fn new_session_index( + sender: &mut Sender, + session_index: Option, + relay_parent: Hash, +) -> Option +where + Sender: SubsystemSender, +{ + let Ok(Ok(new_session_index)) = + util::request_session_index_for_child(relay_parent, sender).await.await + else { + gum::warn!( + target: LOG_TARGET, + ?relay_parent, + "cannot fetch session index from runtime API", + ); + return None + }; + + session_index.map_or(Some(new_session_index), |index| { + if new_session_index > index { + Some(new_session_index) + } else { + None + } + }) +} + +// Returns true if the node is an authority in the next session. +async fn check_next_session_authority( + sender: &mut Sender, + keystore: KeystorePtr, + relay_parent: Hash, + session_index: SessionIndex, +) -> bool +where + Sender: SubsystemSender, +{ + // In spite of function name here we request past, present and future authorities. + // It's ok to stil prepare PVFs in other cases, but better to request only future ones. + let Ok(Ok(authorities)) = util::request_authorities(relay_parent, sender).await.await else { + gum::warn!( + target: LOG_TARGET, + ?relay_parent, + "cannot fetch authorities from runtime API", + ); + return false + }; + + // We need to exclude at least current session authority from the previous request + let Ok(Ok(Some(session_info))) = + util::request_session_info(relay_parent, session_index, sender).await.await + else { + gum::warn!( + target: LOG_TARGET, + ?relay_parent, + "cannot fetch session info from runtime API", + ); + return false + }; + + let is_past_present_or_future_authority = authorities + .iter() + .any(|v| keystore.has_keys(&[(v.to_raw_vec(), AuthorityDiscoveryId::ID)])); + + let is_present_authority = session_info + .discovery_keys + .iter() + .any(|v| keystore.has_keys(&[(v.to_raw_vec(), AuthorityDiscoveryId::ID)])); + + // There is still a chance to be a previous session authority, but this extra work does not + // affect the finalization. + is_past_present_or_future_authority && !is_present_authority +} + +// Sends PVF with unknown code hashes to the validation host returning the list of code hashes sent. +async fn prepare_pvfs_for_backed_candidates( + sender: &mut Sender, + mut validation_backend: impl ValidationBackend, + relay_parent: Hash, + already_prepared: &HashSet, + per_block_limit: usize, +) -> Option> +where + Sender: SubsystemSender, +{ + let Ok(Ok(events)) = util::request_candidate_events(relay_parent, sender).await.await else { + gum::warn!( + target: LOG_TARGET, + ?relay_parent, + "cannot fetch candidate events from runtime API", + ); + return None + }; + let code_hashes = events + .into_iter() + .filter_map(|e| match e { + CandidateEvent::CandidateBacked(receipt, ..) => { + let h = receipt.descriptor.validation_code_hash; + if already_prepared.contains(&h) { + None + } else { + Some(h) + } + }, + _ => None, + }) + .take(per_block_limit) + .collect::>(); + + let Ok(executor_params) = util::executor_params_at_relay_parent(relay_parent, sender).await + else { + gum::warn!( + target: LOG_TARGET, + ?relay_parent, + "cannot fetch executor params for the session", + ); + return None + }; + let timeout = pvf_prep_timeout(&executor_params, PvfPrepKind::Prepare); + + let mut active_pvfs = vec![]; + let mut processed_code_hashes = vec![]; + for code_hash in code_hashes { + let Ok(Ok(Some(validation_code))) = + util::request_validation_code_by_hash(relay_parent, code_hash, sender) + .await + .await + else { + gum::warn!( + target: LOG_TARGET, + ?relay_parent, + ?code_hash, + "cannot fetch validation code hash from runtime API", + ); + continue; + }; + + let pvf = match sp_maybe_compressed_blob::decompress( + &validation_code.0, + VALIDATION_CODE_BOMB_LIMIT, + ) { + Ok(code) => PvfPrepData::from_code( + code.into_owned(), + executor_params.clone(), + timeout, + PrepareJobKind::Prechecking, + ), + Err(e) => { + gum::debug!(target: LOG_TARGET, err=?e, "cannot decompress validation code"); + continue + }, + }; + + active_pvfs.push(pvf); + processed_code_hashes.push(code_hash); + } + + if active_pvfs.is_empty() { + return None + } + + if let Err(err) = validation_backend.heads_up(active_pvfs).await { + gum::warn!( + target: LOG_TARGET, + ?relay_parent, + ?err, + "cannot prepare PVF for the next session", + ); + return None + }; + + gum::debug!( + target: LOG_TARGET, + ?relay_parent, + ?processed_code_hashes, + "Prepared PVF for the next session", + ); + + Some(processed_code_hashes) +} + struct RuntimeRequestFailed; async fn runtime_api_request( @@ -378,25 +628,26 @@ where }, }; - let executor_params = - if let Ok(executor_params) = executor_params_at_relay_parent(relay_parent, sender).await { - gum::debug!( - target: LOG_TARGET, - ?relay_parent, - ?validation_code_hash, - "precheck: acquired executor params for the session: {:?}", - executor_params, - ); - executor_params - } else { - gum::warn!( - target: LOG_TARGET, - ?relay_parent, - ?validation_code_hash, - "precheck: failed to acquire executor params for the session, thus voting against.", - ); - return PreCheckOutcome::Invalid - }; + let executor_params = if let Ok(executor_params) = + util::executor_params_at_relay_parent(relay_parent, sender).await + { + gum::debug!( + target: LOG_TARGET, + ?relay_parent, + ?validation_code_hash, + "precheck: acquired executor params for the session: {:?}", + executor_params, + ); + executor_params + } else { + gum::warn!( + target: LOG_TARGET, + ?relay_parent, + ?validation_code_hash, + "precheck: failed to acquire executor params for the session, thus voting against.", + ); + return PreCheckOutcome::Invalid + }; let timeout = pvf_prep_timeout(&executor_params, PvfPrepKind::Precheck); @@ -891,6 +1142,8 @@ trait ValidationBackend { } async fn precheck_pvf(&mut self, pvf: PvfPrepData) -> Result<(), PrepareError>; + + async fn heads_up(&mut self, active_pvfs: Vec) -> Result<(), String>; } #[async_trait] @@ -933,6 +1186,10 @@ impl ValidationBackend for ValidationHost { precheck_result } + + async fn heads_up(&mut self, active_pvfs: Vec) -> Result<(), String> { + self.heads_up(active_pvfs).await + } } /// Does basic checks of a candidate. Provide the encoded PoV-block. Returns `Ok` if basic checks diff --git a/polkadot/node/core/candidate-validation/src/tests.rs b/polkadot/node/core/candidate-validation/src/tests.rs index 491ed7a335d8f..86d855f78b454 100644 --- a/polkadot/node/core/candidate-validation/src/tests.rs +++ b/polkadot/node/core/candidate-validation/src/tests.rs @@ -14,16 +14,25 @@ // You should have received a copy of the GNU General Public License // along with Polkadot. If not, see . +use std::sync::atomic::{AtomicUsize, Ordering}; + use super::*; use assert_matches::assert_matches; use futures::executor; use polkadot_node_core_pvf::PrepareError; use polkadot_node_subsystem::messages::AllMessages; use polkadot_node_subsystem_util::reexports::SubsystemContext; -use polkadot_primitives::{HeadData, Id as ParaId, UpwardMessage}; -use polkadot_primitives_test_helpers::{dummy_hash, make_valid_candidate_descriptor}; +use polkadot_overseer::ActivatedLeaf; +use polkadot_primitives::{ + CoreIndex, GroupIndex, HeadData, Id as ParaId, IndexedVec, SessionInfo, UpwardMessage, + ValidatorId, ValidatorIndex, +}; +use polkadot_primitives_test_helpers::{ + dummy_collator, dummy_collator_signature, dummy_hash, make_valid_candidate_descriptor, +}; use sp_core::testing::TaskExecutor; use sp_keyring::Sr25519Keyring; +use sp_keystore::{testing::MemoryKeystore, Keystore}; #[test] fn correctly_checks_included_assumption() { @@ -390,6 +399,10 @@ impl ValidationBackend for MockValidateCandidateBackend { async fn precheck_pvf(&mut self, _pvf: PvfPrepData) -> Result<(), PrepareError> { unreachable!() } + + async fn heads_up(&mut self, _active_pvfs: Vec) -> Result<(), String> { + unreachable!() + } } #[test] @@ -1071,6 +1084,10 @@ impl ValidationBackend for MockPreCheckBackend { async fn precheck_pvf(&mut self, _pvf: PvfPrepData) -> Result<(), PrepareError> { self.result.clone() } + + async fn heads_up(&mut self, _active_pvfs: Vec) -> Result<(), String> { + unreachable!() + } } #[test] @@ -1263,3 +1280,628 @@ fn precheck_properly_classifies_outcomes() { inner(Err(PrepareError::TimedOut), PreCheckOutcome::Failed); inner(Err(PrepareError::IoErr("fizz".to_owned())), PreCheckOutcome::Failed); } + +#[derive(Default, Clone)] +struct MockHeadsUp { + heads_up_call_count: Arc, +} + +#[async_trait] +impl ValidationBackend for MockHeadsUp { + async fn validate_candidate( + &mut self, + _pvf: PvfPrepData, + _timeout: Duration, + _encoded_params: Vec, + _prepare_priority: polkadot_node_core_pvf::Priority, + ) -> Result { + unreachable!() + } + + async fn precheck_pvf(&mut self, _pvf: PvfPrepData) -> Result<(), PrepareError> { + unreachable!() + } + + async fn heads_up(&mut self, _active_pvfs: Vec) -> Result<(), String> { + let _ = self.heads_up_call_count.fetch_add(1, Ordering::SeqCst); + Ok(()) + } +} + +fn alice_keystore() -> KeystorePtr { + let keystore: KeystorePtr = Arc::new(MemoryKeystore::new()); + let _ = Keystore::sr25519_generate_new( + &*keystore, + ValidatorId::ID, + Some(&Sr25519Keyring::Alice.to_seed()), + ) + .unwrap(); + let _ = Keystore::sr25519_generate_new( + &*keystore, + AuthorityDiscoveryId::ID, + Some(&Sr25519Keyring::Alice.to_seed()), + ) + .unwrap(); + + keystore +} + +fn dummy_active_leaves_update(hash: Hash) -> ActiveLeavesUpdate { + ActiveLeavesUpdate { + activated: Some(ActivatedLeaf { + hash, + number: 10, + unpin_handle: polkadot_node_subsystem_test_helpers::mock::dummy_unpin_handle(hash), + span: Arc::new(overseer::jaeger::Span::Disabled), + }), + ..Default::default() + } +} + +fn dummy_candidate_backed( + relay_parent: Hash, + validation_code_hash: ValidationCodeHash, +) -> CandidateEvent { + let zeros = dummy_hash(); + let descriptor = CandidateDescriptor { + para_id: ParaId::from(0_u32), + relay_parent, + collator: dummy_collator(), + persisted_validation_data_hash: zeros, + pov_hash: zeros, + erasure_root: zeros, + signature: dummy_collator_signature(), + para_head: zeros, + validation_code_hash, + }; + + CandidateEvent::CandidateBacked( + CandidateReceipt { descriptor, commitments_hash: zeros }, + HeadData(Vec::new()), + CoreIndex(0), + GroupIndex(0), + ) +} + +fn dummy_session_info(discovery_keys: Vec) -> SessionInfo { + SessionInfo { + validators: IndexedVec::::from(vec![]), + discovery_keys, + assignment_keys: vec![], + validator_groups: Default::default(), + n_cores: 4u32, + zeroth_delay_tranche_width: 0u32, + relay_vrf_modulo_samples: 0u32, + n_delay_tranches: 2u32, + no_show_slots: 0u32, + needed_approvals: 1u32, + active_validator_indices: vec![], + dispute_period: 6, + random_seed: [0u8; 32], + } +} + +#[test] +fn maybe_prepare_validation_golden_path() { + let pool = TaskExecutor::new(); + let (mut ctx, mut ctx_handle) = + polkadot_node_subsystem_test_helpers::make_subsystem_context::(pool); + + let keystore = alice_keystore(); + let backend = MockHeadsUp::default(); + let activated_hash = Hash::random(); + let update = dummy_active_leaves_update(activated_hash); + let mut state = PrepareValidationState::default(); + + let check_fut = + maybe_prepare_validation(ctx.sender(), keystore, backend.clone(), update, &mut state); + + let test_fut = async move { + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::SessionIndexForChild(tx))) => { + let _ = tx.send(Ok(1)); + } + ); + + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::Authorities(tx))) => { + let _ = tx.send(Ok(vec![Sr25519Keyring::Alice.public().into()])); + } + ); + + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::SessionInfo(index, tx))) => { + assert_eq!(index, 1); + let _ = tx.send(Ok(Some(dummy_session_info(vec![Sr25519Keyring::Bob.public().into()])))); + } + ); + + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::CandidateEvents(tx))) => { + let _ = tx.send(Ok(vec![dummy_candidate_backed(activated_hash, dummy_hash().into())])); + } + ); + + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::SessionIndexForChild(tx))) => { + let _ = tx.send(Ok(1)); + } + ); + + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::SessionExecutorParams(index, tx))) => { + assert_eq!(index, 1); + let _ = tx.send(Ok(Some(ExecutorParams::default()))); + } + ); + + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::ValidationCodeByHash(hash, tx))) => { + assert_eq!(hash, dummy_hash().into()); + let _ = tx.send(Ok(Some(ValidationCode(Vec::new())))); + } + ); + }; + + let test_fut = future::join(test_fut, check_fut); + executor::block_on(test_fut); + + assert_eq!(backend.heads_up_call_count.load(Ordering::SeqCst), 1); + assert!(state.session_index.is_some()); + assert!(state.is_next_session_authority); +} + +#[test] +fn maybe_prepare_validation_checkes_authority_once_per_session() { + let pool = TaskExecutor::new(); + let (mut ctx, mut ctx_handle) = + polkadot_node_subsystem_test_helpers::make_subsystem_context::(pool); + + let keystore = alice_keystore(); + let backend = MockHeadsUp::default(); + let activated_hash = Hash::random(); + let update = dummy_active_leaves_update(activated_hash); + let mut state = PrepareValidationState { + session_index: Some(1), + is_next_session_authority: false, + ..Default::default() + }; + + let check_fut = + maybe_prepare_validation(ctx.sender(), keystore, backend.clone(), update, &mut state); + + let test_fut = async move { + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::SessionIndexForChild(tx))) => { + let _ = tx.send(Ok(1)); + } + ); + }; + + let test_fut = future::join(test_fut, check_fut); + executor::block_on(test_fut); + + assert_eq!(backend.heads_up_call_count.load(Ordering::SeqCst), 0); + assert!(state.session_index.is_some()); + assert!(!state.is_next_session_authority); +} + +#[test] +fn maybe_prepare_validation_resets_state_on_a_new_session() { + let pool = TaskExecutor::new(); + let (mut ctx, mut ctx_handle) = + polkadot_node_subsystem_test_helpers::make_subsystem_context::(pool); + + let keystore = alice_keystore(); + let backend = MockHeadsUp::default(); + let activated_hash = Hash::random(); + let update = dummy_active_leaves_update(activated_hash); + let mut state = PrepareValidationState { + session_index: Some(1), + is_next_session_authority: true, + already_prepared_code_hashes: HashSet::from_iter(vec![ValidationCode(vec![0; 16]).hash()]), + ..Default::default() + }; + + let check_fut = + maybe_prepare_validation(ctx.sender(), keystore, backend.clone(), update, &mut state); + + let test_fut = async move { + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::SessionIndexForChild(tx))) => { + let _ = tx.send(Ok(2)); + } + ); + + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::Authorities(tx))) => { + let _ = tx.send(Ok(vec![Sr25519Keyring::Bob.public().into()])); + } + ); + + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::SessionInfo(index, tx))) => { + assert_eq!(index, 2); + let _ = tx.send(Ok(Some(dummy_session_info(vec![Sr25519Keyring::Bob.public().into()])))); + } + ); + }; + + let test_fut = future::join(test_fut, check_fut); + executor::block_on(test_fut); + + assert_eq!(backend.heads_up_call_count.load(Ordering::SeqCst), 0); + assert_eq!(state.session_index.unwrap(), 2); + assert!(!state.is_next_session_authority); + assert!(state.already_prepared_code_hashes.is_empty()); +} + +#[test] +fn maybe_prepare_validation_does_not_prepare_pvfs_if_no_new_session_and_not_a_validator() { + let pool = TaskExecutor::new(); + let (mut ctx, mut ctx_handle) = + polkadot_node_subsystem_test_helpers::make_subsystem_context::(pool); + + let keystore = alice_keystore(); + let backend = MockHeadsUp::default(); + let activated_hash = Hash::random(); + let update = dummy_active_leaves_update(activated_hash); + let mut state = PrepareValidationState { session_index: Some(1), ..Default::default() }; + + let check_fut = + maybe_prepare_validation(ctx.sender(), keystore, backend.clone(), update, &mut state); + + let test_fut = async move { + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::SessionIndexForChild(tx))) => { + let _ = tx.send(Ok(1)); + } + ); + }; + + let test_fut = future::join(test_fut, check_fut); + executor::block_on(test_fut); + + assert_eq!(backend.heads_up_call_count.load(Ordering::SeqCst), 0); + assert!(state.session_index.is_some()); + assert!(!state.is_next_session_authority); +} + +#[test] +fn maybe_prepare_validation_does_not_prepare_pvfs_if_no_new_session_but_a_validator() { + let pool = TaskExecutor::new(); + let (mut ctx, mut ctx_handle) = + polkadot_node_subsystem_test_helpers::make_subsystem_context::(pool); + + let keystore = alice_keystore(); + let backend = MockHeadsUp::default(); + let activated_hash = Hash::random(); + let update = dummy_active_leaves_update(activated_hash); + let mut state = PrepareValidationState { + session_index: Some(1), + is_next_session_authority: true, + ..Default::default() + }; + + let check_fut = + maybe_prepare_validation(ctx.sender(), keystore, backend.clone(), update, &mut state); + + let test_fut = async move { + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::SessionIndexForChild(tx))) => { + let _ = tx.send(Ok(1)); + } + ); + + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::CandidateEvents(tx))) => { + let _ = tx.send(Ok(vec![dummy_candidate_backed(activated_hash, dummy_hash().into())])); + } + ); + + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::SessionIndexForChild(tx))) => { + let _ = tx.send(Ok(1)); + } + ); + + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::SessionExecutorParams(index, tx))) => { + assert_eq!(index, 1); + let _ = tx.send(Ok(Some(ExecutorParams::default()))); + } + ); + + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::ValidationCodeByHash(hash, tx))) => { + assert_eq!(hash, dummy_hash().into()); + let _ = tx.send(Ok(Some(ValidationCode(Vec::new())))); + } + ); + }; + + let test_fut = future::join(test_fut, check_fut); + executor::block_on(test_fut); + + assert_eq!(backend.heads_up_call_count.load(Ordering::SeqCst), 1); + assert!(state.session_index.is_some()); + assert!(state.is_next_session_authority); +} + +#[test] +fn maybe_prepare_validation_does_not_prepare_pvfs_if_not_a_validator_in_the_next_session() { + let pool = TaskExecutor::new(); + let (mut ctx, mut ctx_handle) = + polkadot_node_subsystem_test_helpers::make_subsystem_context::(pool); + + let keystore = alice_keystore(); + let backend = MockHeadsUp::default(); + let activated_hash = Hash::random(); + let update = dummy_active_leaves_update(activated_hash); + let mut state = PrepareValidationState::default(); + + let check_fut = + maybe_prepare_validation(ctx.sender(), keystore, backend.clone(), update, &mut state); + + let test_fut = async move { + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::SessionIndexForChild(tx))) => { + let _ = tx.send(Ok(1)); + } + ); + + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::Authorities(tx))) => { + let _ = tx.send(Ok(vec![Sr25519Keyring::Bob.public().into()])); + } + ); + + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::SessionInfo(index, tx))) => { + assert_eq!(index, 1); + let _ = tx.send(Ok(Some(dummy_session_info(vec![Sr25519Keyring::Bob.public().into()])))); + } + ); + }; + + let test_fut = future::join(test_fut, check_fut); + executor::block_on(test_fut); + + assert_eq!(backend.heads_up_call_count.load(Ordering::SeqCst), 0); + assert!(state.session_index.is_some()); + assert!(!state.is_next_session_authority); +} + +#[test] +fn maybe_prepare_validation_does_not_prepare_pvfs_if_a_validator_in_the_current_session() { + let pool = TaskExecutor::new(); + let (mut ctx, mut ctx_handle) = + polkadot_node_subsystem_test_helpers::make_subsystem_context::(pool); + + let keystore = alice_keystore(); + let backend = MockHeadsUp::default(); + let activated_hash = Hash::random(); + let update = dummy_active_leaves_update(activated_hash); + let mut state = PrepareValidationState::default(); + + let check_fut = + maybe_prepare_validation(ctx.sender(), keystore, backend.clone(), update, &mut state); + + let test_fut = async move { + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::SessionIndexForChild(tx))) => { + let _ = tx.send(Ok(1)); + } + ); + + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::Authorities(tx))) => { + let _ = tx.send(Ok(vec![Sr25519Keyring::Alice.public().into()])); + } + ); + + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::SessionInfo(index, tx))) => { + assert_eq!(index, 1); + let _ = tx.send(Ok(Some(dummy_session_info(vec![Sr25519Keyring::Alice.public().into()])))); + } + ); + }; + + let test_fut = future::join(test_fut, check_fut); + executor::block_on(test_fut); + + assert_eq!(backend.heads_up_call_count.load(Ordering::SeqCst), 0); + assert!(state.session_index.is_some()); + assert!(!state.is_next_session_authority); +} + +#[test] +fn maybe_prepare_validation_prepares_a_limited_number_of_pvfs() { + let pool = TaskExecutor::new(); + let (mut ctx, mut ctx_handle) = + polkadot_node_subsystem_test_helpers::make_subsystem_context::(pool); + + let keystore = alice_keystore(); + let backend = MockHeadsUp::default(); + let activated_hash = Hash::random(); + let update = dummy_active_leaves_update(activated_hash); + let mut state = PrepareValidationState { per_block_limit: 2, ..Default::default() }; + + let check_fut = + maybe_prepare_validation(ctx.sender(), keystore, backend.clone(), update, &mut state); + + let test_fut = async move { + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::SessionIndexForChild(tx))) => { + let _ = tx.send(Ok(1)); + } + ); + + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::Authorities(tx))) => { + let _ = tx.send(Ok(vec![Sr25519Keyring::Alice.public().into()])); + } + ); + + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::SessionInfo(index, tx))) => { + assert_eq!(index, 1); + let _ = tx.send(Ok(Some(dummy_session_info(vec![Sr25519Keyring::Bob.public().into()])))); + } + ); + + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::CandidateEvents(tx))) => { + let candidates = vec![ + dummy_candidate_backed(activated_hash, ValidationCode(vec![0; 16]).hash()), + dummy_candidate_backed(activated_hash, ValidationCode(vec![1; 16]).hash()), + dummy_candidate_backed(activated_hash, ValidationCode(vec![2; 16]).hash()), + ]; + let _ = tx.send(Ok(candidates)); + } + ); + + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::SessionIndexForChild(tx))) => { + let _ = tx.send(Ok(1)); + } + ); + + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::SessionExecutorParams(index, tx))) => { + assert_eq!(index, 1); + let _ = tx.send(Ok(Some(ExecutorParams::default()))); + } + ); + + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::ValidationCodeByHash(hash, tx))) => { + assert_eq!(hash, ValidationCode(vec![0; 16]).hash()); + let _ = tx.send(Ok(Some(ValidationCode(Vec::new())))); + } + ); + + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::ValidationCodeByHash(hash, tx))) => { + assert_eq!(hash, ValidationCode(vec![1; 16]).hash()); + let _ = tx.send(Ok(Some(ValidationCode(Vec::new())))); + } + ); + }; + + let test_fut = future::join(test_fut, check_fut); + executor::block_on(test_fut); + + assert_eq!(backend.heads_up_call_count.load(Ordering::SeqCst), 1); + assert!(state.session_index.is_some()); + assert!(state.is_next_session_authority); + assert_eq!(state.already_prepared_code_hashes.len(), 2); +} + +#[test] +fn maybe_prepare_validation_does_not_prepare_already_prepared_pvfs() { + let pool = TaskExecutor::new(); + let (mut ctx, mut ctx_handle) = + polkadot_node_subsystem_test_helpers::make_subsystem_context::(pool); + + let keystore = alice_keystore(); + let backend = MockHeadsUp::default(); + let activated_hash = Hash::random(); + let update = dummy_active_leaves_update(activated_hash); + let mut state = PrepareValidationState { + session_index: Some(1), + is_next_session_authority: true, + per_block_limit: 2, + already_prepared_code_hashes: HashSet::from_iter(vec![ + ValidationCode(vec![0; 16]).hash(), + ValidationCode(vec![1; 16]).hash(), + ]), + }; + + let check_fut = + maybe_prepare_validation(ctx.sender(), keystore, backend.clone(), update, &mut state); + + let test_fut = async move { + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::SessionIndexForChild(tx))) => { + let _ = tx.send(Ok(1)); + } + ); + + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::CandidateEvents(tx))) => { + let candidates = vec![ + dummy_candidate_backed(activated_hash, ValidationCode(vec![0; 16]).hash()), + dummy_candidate_backed(activated_hash, ValidationCode(vec![1; 16]).hash()), + dummy_candidate_backed(activated_hash, ValidationCode(vec![2; 16]).hash()), + ]; + let _ = tx.send(Ok(candidates)); + } + ); + + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::SessionIndexForChild(tx))) => { + let _ = tx.send(Ok(1)); + } + ); + + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::SessionExecutorParams(index, tx))) => { + assert_eq!(index, 1); + let _ = tx.send(Ok(Some(ExecutorParams::default()))); + } + ); + + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::ValidationCodeByHash(hash, tx))) => { + assert_eq!(hash, ValidationCode(vec![2; 16]).hash()); + let _ = tx.send(Ok(Some(ValidationCode(Vec::new())))); + } + ); + }; + + let test_fut = future::join(test_fut, check_fut); + executor::block_on(test_fut); + + assert_eq!(backend.heads_up_call_count.load(Ordering::SeqCst), 1); + assert!(state.session_index.is_some()); + assert!(state.is_next_session_authority); + assert_eq!(state.already_prepared_code_hashes.len(), 3); +} diff --git a/polkadot/node/service/src/overseer.rs b/polkadot/node/service/src/overseer.rs index 5f4db99b00ef9..add5d239364df 100644 --- a/polkadot/node/service/src/overseer.rs +++ b/polkadot/node/service/src/overseer.rs @@ -275,6 +275,7 @@ where )) .candidate_validation(CandidateValidationSubsystem::with_config( candidate_validation_config, + keystore.clone(), Metrics::register(registry)?, // candidate-validation metrics Metrics::register(registry)?, // validation host metrics )) diff --git a/polkadot/zombienet_tests/smoke/0005-precompile-pvf-smoke.js b/polkadot/zombienet_tests/smoke/0005-precompile-pvf-smoke.js new file mode 100644 index 0000000000000..9963ce74d64e8 --- /dev/null +++ b/polkadot/zombienet_tests/smoke/0005-precompile-pvf-smoke.js @@ -0,0 +1,46 @@ +const assert = require("assert"); + +function nameCase(string) { + return string.charAt(0).toUpperCase() + string.slice(1); +} + +async function run(nodeName, networkInfo, jsArgs) { + const {wsUri, userDefinedTypes} = networkInfo.nodesByName[nodeName]; + const api = await zombie.connect(wsUri, userDefinedTypes); + const action = jsArgs[0] === "register" ? "registerValidators" : "deregisterValidators" + const validatorName = jsArgs[1]; // used as seed + + await zombie.util.cryptoWaitReady(); + + // account to submit tx + const keyring = new zombie.Keyring({ type: "sr25519" }); + const alice = keyring.addFromUri("//Alice"); + const validatorStash = keyring.createFromUri(`//${nameCase(validatorName)}//stash`); + + await new Promise(async (resolve, reject) => { + const unsub = await api.tx.sudo + .sudo(api.tx.validatorManager[action]([validatorStash.address])) + .signAndSend(alice, (result) => { + console.log(`Current status is ${result.status}`); + if (result.status.isInBlock) { + console.log( + `Transaction included at blockHash ${result.status.asInBlock}` + ); + } else if (result.status.isFinalized) { + console.log( + `Transaction finalized at blockHash ${result.status.asFinalized}` + ); + unsub(); + return resolve(); + } else if (result.isError) { + console.log(`Transaction Error`); + unsub(); + return reject(); + } + }); + }); + + return 0; +} + +module.exports = { run } diff --git a/polkadot/zombienet_tests/smoke/0005-precompile-pvf-smoke.toml b/polkadot/zombienet_tests/smoke/0005-precompile-pvf-smoke.toml new file mode 100644 index 0000000000000..2baf21b96cc1a --- /dev/null +++ b/polkadot/zombienet_tests/smoke/0005-precompile-pvf-smoke.toml @@ -0,0 +1,32 @@ +[settings] +timeout = 1000 + +[relaychain] +default_image = "{{ZOMBIENET_INTEGRATION_TEST_IMAGE}}" +chain = "rococo-local" +command = "polkadot" + + [[relaychain.nodes]] + name = "alice" + args = ["-lruntime=debug,parachain=trace" ] + + [[relaychain.nodes]] + name = "bob" + args = [ "-lruntime=debug,parachain=trace" ] + + [[relaychain.nodes]] + name = "charlie" + args = [ "-lruntime=debug,parachain=trace" ] + + [[relaychain.nodes]] + name = "dave" + args = [ "-lruntime=debug,parachain=trace" ] + +[[parachains]] +id = 100 +add_to_genesis = true + + [parachains.collator] + name = "collator-100" + image = "{{CUMULUS_IMAGE}}" + command = "polkadot-parachain" diff --git a/polkadot/zombienet_tests/smoke/0005-precompile-pvf-smoke.zndsl b/polkadot/zombienet_tests/smoke/0005-precompile-pvf-smoke.zndsl new file mode 100644 index 0000000000000..3ac68caf7e1bf --- /dev/null +++ b/polkadot/zombienet_tests/smoke/0005-precompile-pvf-smoke.zndsl @@ -0,0 +1,43 @@ +Description: PVF precompilation +Network: ./0005-precompile-pvf-smoke.toml +Creds: config + +# Ensure the calidator is in the validator set +dave: reports polkadot_node_is_parachain_validator is 1 within 240 secs +dave: reports polkadot_node_is_active_validator is 1 within 240 secs + +# Deregister the validator +alice: js-script ./0005-register-deregister-validator.js with "deregister,dave" return is 0 within 30 secs + +# Wait 2 sessions. The authority set change is enacted at curent_session + 2. +sleep 120 seconds +dave: reports polkadot_node_is_parachain_validator is 0 within 180 secs +dave: reports polkadot_node_is_active_validator is 0 within 180 secs + +# register the parachain +alice: js-script ./0005-register-para.js with "100" return is 0 within 600 seconds + +# Ensure the parachain made progress. +alice: parachain 100 block height is at least 10 within 300 seconds + +# Ensure the validator didn't prepare pvf +dave: reports polkadot_pvf_preparation_time_count is 1 within 30 seconds + +# Register the validator again +alice: js-script ./0005-register-deregister-validator.js with "register,dave" return is 0 within 30 secs + +# Wait 1 session and check the pvf preparation +sleep 60 seconds +dave: reports polkadot_pvf_preparation_time_count is 1 within 30 seconds + +# Check the validator is still not in the validator set +dave: reports polkadot_node_is_parachain_validator is 0 within 30 secs +dave: reports polkadot_node_is_active_validator is 0 within 30 secs + +# Check the validator is in the validator set +dave: reports polkadot_node_is_parachain_validator is 1 within 60 secs +dave: reports polkadot_node_is_active_validator is 1 within 60 secs + +# Check the pvf preparation again. The authority set change is enacted at curent_session + 2. +sleep 60 seconds +dave: reports polkadot_pvf_preparation_time_count is 1 within 60 seconds diff --git a/polkadot/zombienet_tests/smoke/0005-register-deregister-validator.js b/polkadot/zombienet_tests/smoke/0005-register-deregister-validator.js new file mode 100644 index 0000000000000..16e7d87d0ffc4 --- /dev/null +++ b/polkadot/zombienet_tests/smoke/0005-register-deregister-validator.js @@ -0,0 +1,44 @@ +function nameCase(string) { + return string.charAt(0).toUpperCase() + string.slice(1); +} + +async function run(nodeName, networkInfo, args) { + const { wsUri, userDefinedTypes } = networkInfo.nodesByName[nodeName]; + const api = await zombie.connect(wsUri, userDefinedTypes); + const action = args[0] === "register" ? "registerValidators" : "deregisterValidators" + const validatorName = args[1]; // used as seed + + await zombie.util.cryptoWaitReady(); + + // account to submit tx + const keyring = new zombie.Keyring({ type: "sr25519" }); + const alice = keyring.addFromUri("//Alice"); + const validatorStash = keyring.createFromUri(`//${nameCase(validatorName)}//stash`); + + await new Promise(async (resolve, reject) => { + const unsub = await api.tx.sudo + .sudo(api.tx.validatorManager[action]([validatorStash.address])) + .signAndSend(alice, (result) => { + console.log(`Current status is ${result.status}`); + if (result.status.isInBlock) { + console.log( + `Transaction included at blockHash ${result.status.asInBlock}` + ); + } else if (result.status.isFinalized) { + console.log( + `Transaction finalized at blockHash ${result.status.asFinalized}` + ); + unsub(); + return resolve(); + } else if (result.isError) { + console.log(`Transaction Error`); + unsub(); + return reject(); + } + }); + }); + + return 0; +} + +module.exports = { run } diff --git a/polkadot/zombienet_tests/smoke/0005-register-para.js b/polkadot/zombienet_tests/smoke/0005-register-para.js new file mode 100644 index 0000000000000..aba7bc3601fbe --- /dev/null +++ b/polkadot/zombienet_tests/smoke/0005-register-para.js @@ -0,0 +1,63 @@ +async function run(nodeName, networkInfo, args) { + const init = networkInfo.nodesByName[nodeName]; + let wsUri = init.wsUri; + let userDefinedTypes = init.userDefinedTypes; + const api = await zombie.connect(wsUri, userDefinedTypes); + + // account to submit tx + const keyring = new zombie.Keyring({ type: "sr25519" }); + const alice = keyring.addFromUri("//Alice"); + + let calls = []; + + for (let i = 0; i < args.length; i++) { + let para = args[i]; + const sec = networkInfo.nodesByName["collator-" + para]; + const api_collator = await zombie.connect(sec.wsUri, sec.userDefinedTypes); + + await zombie.util.cryptoWaitReady(); + + // Get the genesis header and the validation code of the parachain + const genesis_header = await api_collator.rpc.chain.getHeader(); + const validation_code = await api_collator.rpc.state.getStorage("0x3A636F6465"); + + calls.push( + api.tx.paras.addTrustedValidationCode(validation_code.toHex()) + ); + calls.push( + api.tx.registrar.forceRegister( + alice.address, + 0, + Number(para), + genesis_header.toHex(), + validation_code.toHex(), + ) + ); + } + + const sudo_batch = api.tx.sudo.sudo(api.tx.utility.batch(calls)); + + await new Promise(async (resolve, reject) => { + const unsub = await sudo_batch + .signAndSend(alice, ({ status, isError }) => { + if (status.isInBlock) { + console.log( + `Transaction included at blockhash ${status.asInBlock}`, + ); + } else if (status.isFinalized) { + console.log( + `Transaction finalized at blockHash ${status.asFinalized}`, + ); + unsub(); + return resolve(); + } else if (isError) { + console.log(`Transaction error`); + reject(`Transaction error`); + } + }); + }); + + return 0; +} + +module.exports = { run }; diff --git a/prdoc/pr_4791.prdoc b/prdoc/pr_4791.prdoc new file mode 100644 index 0000000000000..9a7a9ca44e167 --- /dev/null +++ b/prdoc/pr_4791.prdoc @@ -0,0 +1,19 @@ +# Schema: Polkadot SDK PRDoc Schema (prdoc) v1.0.0 +# See doc at https://raw.githubusercontent.com/paritytech/polkadot-sdk/master/prdoc/schema_user.json + +title: Prepare PVFs if node is a validator in the next session + +doc: + - audience: Node Operator + description: | + - On every active leaf candidate-validation subsystem checks if the node is the next session authority. + - If it is, it fetches backed candidates and prepares unknown PVFs. + - Number of PVF preparations per block is limited to not overload subsystem. + +crates: + - name: polkadot + bump: patch + - name: polkadot-service + bump: patch + - name: polkadot-node-core-candidate-validation + bump: major