From 5253a055556862b5ea9018c9baeae2b617bd93ea Mon Sep 17 00:00:00 2001 From: Robert Habermeier <rphmeier@gmail.com> Date: Thu, 30 Jul 2020 17:50:11 -0400 Subject: [PATCH] Candidate Validation Subsystem (#1432) * skeleton for candidate-validation * add to workspace * implement candidate validation logic * guide: note occupied-core assumption for candidate validation * adjust message doc * wire together `run` asynchronously * add a Subsystem implementation * clean up a couple warnings * fix compilation errors due to merge * improve candidate-validation.md * remove old reference to subsystem-test helpers crate * update Cargo.lock * add a couple new Runtime API methods * add a candidate validation message * fetch validation data from the chain state * some tests for assumption checking * make spawn_validate_exhaustive mockable * more tests on the error handling side * fix all other grumbles except for wasm validation API change * wrap a SpawnNamed in candidate-validation * warn * amend guide * squanch warning * remove duplicate after merge --- polkadot/Cargo.lock | 18 + polkadot/Cargo.toml | 3 +- .../node/core/candidate-validation/Cargo.toml | 24 + .../node/core/candidate-validation/src/lib.rs | 918 ++++++++++++++++++ polkadot/node/subsystem/src/messages.rs | 4 +- .../src/node/utility/candidate-validation.md | 40 +- .../src/types/overseer-protocol.md | 4 +- 7 files changed, 1001 insertions(+), 10 deletions(-) create mode 100644 polkadot/node/core/candidate-validation/Cargo.toml create mode 100644 polkadot/node/core/candidate-validation/src/lib.rs diff --git a/polkadot/Cargo.lock b/polkadot/Cargo.lock index 3ac38104668..47386ba2d4c 100644 --- a/polkadot/Cargo.lock +++ b/polkadot/Cargo.lock @@ -4635,6 +4635,24 @@ dependencies = [ "wasm-timer", ] +[[package]] +name = "polkadot-node-core-candidate-validation" +version = "0.1.0" +dependencies = [ + "assert_matches", + "derive_more 0.99.9", + "futures 0.3.5", + "log 0.4.8", + "parity-scale-codec", + "polkadot-node-primitives", + "polkadot-node-subsystem", + "polkadot-parachain", + "polkadot-primitives", + "sp-blockchain", + "sp-core", + "sp-keyring", +] + [[package]] name = "polkadot-node-core-proposer" version = "0.1.0" diff --git a/polkadot/Cargo.toml b/polkadot/Cargo.toml index 595936f357c..fa56b0ec424 100644 --- a/polkadot/Cargo.toml +++ b/polkadot/Cargo.toml @@ -44,7 +44,9 @@ members = [ "validation", "node/core/av-store", + "node/core/backing", "node/core/bitfield-signing", + "node/core/candidate-validation", "node/core/proposer", "node/core/runtime-api", "node/network/bridge", @@ -54,7 +56,6 @@ members = [ "node/overseer", "node/primitives", "node/service", - "node/core/backing", "node/subsystem", "node/test-service", diff --git a/polkadot/node/core/candidate-validation/Cargo.toml b/polkadot/node/core/candidate-validation/Cargo.toml new file mode 100644 index 00000000000..3c36b167585 --- /dev/null +++ b/polkadot/node/core/candidate-validation/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "polkadot-node-core-candidate-validation" +version = "0.1.0" +authors = ["Parity Technologies <admin@parity.io>"] +edition = "2018" + +[dependencies] +futures = "0.3.5" +sp-blockchain = { git = "https://github.com/paritytech/substrate", branch = "master" } +sp-core = { package = "sp-core", git = "https://github.com/paritytech/substrate", branch = "master" } +parity-scale-codec = { version = "1.3.0", default-features = false, features = ["bit-vec", "derive"] } + +polkadot-primitives = { path = "../../../primitives" } +polkadot-parachain = { path = "../../../parachain" } +polkadot-node-primitives = { path = "../../primitives" } +polkadot-subsystem = { package = "polkadot-node-subsystem", path = "../../subsystem" } +derive_more = "0.99.9" +log = "0.4.8" + +[dev-dependencies] +sp-keyring = { git = "https://github.com/paritytech/substrate", branch = "master" } +futures = { version = "0.3.5", features = ["thread-pool"] } +assert_matches = "1.3.0" +polkadot-subsystem = { package = "polkadot-node-subsystem", path = "../../subsystem", features = ["test-helpers"] } diff --git a/polkadot/node/core/candidate-validation/src/lib.rs b/polkadot/node/core/candidate-validation/src/lib.rs new file mode 100644 index 00000000000..c040090faa9 --- /dev/null +++ b/polkadot/node/core/candidate-validation/src/lib.rs @@ -0,0 +1,918 @@ +// Copyright 2020 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 <http://www.gnu.org/licenses/>. + +//! The Candidate Validation subsystem. +//! +//! This handles incoming requests from other subsystems to validate candidates +//! according to a validation function. This delegates validation to an underlying +//! pool of processes used for execution of the Wasm. + +use polkadot_subsystem::{ + Subsystem, SubsystemContext, SpawnedSubsystem, SubsystemResult, + FromOverseer, OverseerSignal, +}; +use polkadot_subsystem::messages::{ + AllMessages, CandidateValidationMessage, RuntimeApiMessage, ValidationFailed, RuntimeApiRequest, + RuntimeApiError, +}; +use polkadot_node_primitives::{ValidationResult, ValidationOutputs}; +use polkadot_primitives::v1::{ + ValidationCode, OmittedValidationData, PoV, CandidateDescriptor, LocalValidationData, + GlobalValidationData, OccupiedCoreAssumption, Hash, validation_data_hash, +}; +use polkadot_parachain::wasm_executor::{self, ValidationPool, ExecutionMode}; +use polkadot_parachain::primitives::{ValidationResult as WasmValidationResult, ValidationParams}; + +use parity_scale_codec::Encode; +use sp_core::traits::SpawnNamed; + +use futures::channel::oneshot; +use futures::prelude::*; + +use std::sync::Arc; + +/// The candidate validation subsystem. +pub struct CandidateValidationSubsystem<S>(S); + +impl<S> CandidateValidationSubsystem<S> { + /// Create a new `CandidateValidationSubsystem` with the given task spawner. + pub fn new(spawn: S) -> Self { + CandidateValidationSubsystem(spawn) + } +} + +impl<S, C> Subsystem<C> for CandidateValidationSubsystem<S> where + C: SubsystemContext<Message = CandidateValidationMessage>, + S: SpawnNamed + Clone + 'static, +{ + fn start(self, ctx: C) -> SpawnedSubsystem { + SpawnedSubsystem { + name: "candidate-validation-subsystem", + future: run(ctx, self.0).map(|_| ()).boxed(), + } + } +} + +async fn run( + mut ctx: impl SubsystemContext<Message = CandidateValidationMessage>, + spawn: impl SpawnNamed + Clone + 'static, +) + -> SubsystemResult<()> +{ + let pool = ValidationPool::new(); + + loop { + match ctx.recv().await? { + FromOverseer::Signal(OverseerSignal::ActiveLeaves(_)) => {} + FromOverseer::Signal(OverseerSignal::BlockFinalized(_)) => {} + FromOverseer::Signal(OverseerSignal::Conclude) => return Ok(()), + FromOverseer::Communication { msg } => match msg { + CandidateValidationMessage::ValidateFromChainState( + descriptor, + pov, + response_sender, + ) => { + let res = spawn_validate_from_chain_state( + &mut ctx, + Some(pool.clone()), + descriptor, + pov, + spawn.clone(), + ).await; + + match res { + Ok(x) => { let _ = response_sender.send(x); } + Err(e)=> return Err(e), + } + } + CandidateValidationMessage::ValidateFromExhaustive( + omitted_validation, + validation_code, + descriptor, + pov, + response_sender, + ) => { + let res = spawn_validate_exhaustive( + &mut ctx, + Some(pool.clone()), + omitted_validation, + validation_code, + descriptor, + pov, + spawn.clone(), + ).await; + + match res { + Ok(x) => if let Err(_e) = response_sender.send(x) { + log::warn!( + target: "candidate_validation", + "Requester of candidate validation dropped", + ) + }, + Err(e)=> return Err(e), + } + } + } + } + } +} + +async fn runtime_api_request<T>( + ctx: &mut impl SubsystemContext<Message = CandidateValidationMessage>, + relay_parent: Hash, + request: RuntimeApiRequest, + receiver: oneshot::Receiver<Result<T, RuntimeApiError>>, +) -> SubsystemResult<Result<T, RuntimeApiError>> { + ctx.send_message( + AllMessages::RuntimeApi(RuntimeApiMessage::Request( + relay_parent, + request, + )) + ).await?; + + receiver.await.map_err(Into::into) +} + +#[derive(Debug)] +enum AssumptionCheckOutcome { + Matches(OmittedValidationData, ValidationCode), + DoesNotMatch, + BadRequest, +} + +async fn check_assumption_validation_data( + ctx: &mut impl SubsystemContext<Message = CandidateValidationMessage>, + descriptor: &CandidateDescriptor, + global_validation_data: &GlobalValidationData, + assumption: OccupiedCoreAssumption, +) -> SubsystemResult<AssumptionCheckOutcome> { + let local_validation_data = { + let (tx, rx) = oneshot::channel(); + let d = runtime_api_request( + ctx, + descriptor.relay_parent, + RuntimeApiRequest::LocalValidationData( + descriptor.para_id, + assumption, + tx, + ), + rx, + ).await?; + + match d { + Ok(None) | Err(_) => { + return Ok(AssumptionCheckOutcome::BadRequest); + } + Ok(Some(d)) => d, + } + }; + + let validation_data_hash = validation_data_hash( + &global_validation_data, + &local_validation_data, + ); + + SubsystemResult::Ok(if descriptor.validation_data_hash == validation_data_hash { + let omitted_validation = OmittedValidationData { + global_validation: global_validation_data.clone(), + local_validation: local_validation_data, + }; + + let (code_tx, code_rx) = oneshot::channel(); + let validation_code = runtime_api_request( + ctx, + descriptor.relay_parent, + RuntimeApiRequest::ValidationCode( + descriptor.para_id, + OccupiedCoreAssumption::Included, + code_tx, + ), + code_rx, + ).await?; + + match validation_code { + Ok(None) | Err(_) => AssumptionCheckOutcome::BadRequest, + Ok(Some(v)) => AssumptionCheckOutcome::Matches(omitted_validation, v), + } + } else { + AssumptionCheckOutcome::DoesNotMatch + }) +} + +async fn spawn_validate_from_chain_state( + ctx: &mut impl SubsystemContext<Message = CandidateValidationMessage>, + validation_pool: Option<ValidationPool>, + descriptor: CandidateDescriptor, + pov: Arc<PoV>, + spawn: impl SpawnNamed + 'static, +) -> SubsystemResult<Result<ValidationResult, ValidationFailed>> { + // The candidate descriptor has a `validation_data_hash` which corresponds to + // one of up to two possible values that we can derive from the state of the + // relay-parent. We can fetch these values by getting the `global_validation_data`, + // and both `local_validation_data` based on the different `OccupiedCoreAssumption`s. + let global_validation_data = { + let (tx, rx) = oneshot::channel(); + let res = runtime_api_request( + ctx, + descriptor.relay_parent, + RuntimeApiRequest::GlobalValidationData(tx), + rx, + ).await?; + + match res { + Ok(g) => g, + Err(e) => { + log::warn!( + target: "candidate_validation", + "Error making runtime API request: {:?}", + e, + ); + + return Ok(Err(ValidationFailed)); + } + } + }; + + match check_assumption_validation_data( + ctx, + &descriptor, + &global_validation_data, + OccupiedCoreAssumption::Included, + ).await? { + AssumptionCheckOutcome::Matches(omitted_validation, validation_code) => { + return spawn_validate_exhaustive( + ctx, + validation_pool, + omitted_validation, + validation_code, + descriptor, + pov, + spawn, + ).await; + } + AssumptionCheckOutcome::DoesNotMatch => {}, + AssumptionCheckOutcome::BadRequest => return Ok(Err(ValidationFailed)), + } + + match check_assumption_validation_data( + ctx, + &descriptor, + &global_validation_data, + OccupiedCoreAssumption::TimedOut, + ).await? { + AssumptionCheckOutcome::Matches(omitted_validation, validation_code) => { + return spawn_validate_exhaustive( + ctx, + validation_pool, + omitted_validation, + validation_code, + descriptor, + pov, + spawn, + ).await; + } + AssumptionCheckOutcome::DoesNotMatch => {}, + AssumptionCheckOutcome::BadRequest => return Ok(Err(ValidationFailed)), + } + + // If neither the assumption of the occupied core having the para included or the assumption + // of the occupied core timing out are valid, then the validation_data_hash in the descriptor + // is not based on the relay parent and is thus invalid. + Ok(Ok(ValidationResult::Invalid)) +} + +async fn spawn_validate_exhaustive( + ctx: &mut impl SubsystemContext<Message = CandidateValidationMessage>, + validation_pool: Option<ValidationPool>, + omitted_validation: OmittedValidationData, + validation_code: ValidationCode, + descriptor: CandidateDescriptor, + pov: Arc<PoV>, + spawn: impl SpawnNamed + 'static, +) -> SubsystemResult<Result<ValidationResult, ValidationFailed>> { + let (tx, rx) = oneshot::channel(); + let fut = async move { + let res = validate_candidate_exhaustive::<RealValidationBackend, _>( + validation_pool, + omitted_validation, + validation_code, + descriptor, + pov, + spawn, + ); + + let _ = tx.send(res); + }; + + ctx.spawn("blocking-candidate-validation-task", fut.boxed()).await?; + rx.await.map_err(Into::into) +} + +/// Does basic checks of a candidate. Provide the encoded PoV-block. Returns `true` if basic checks +/// are passed, false otherwise. +fn passes_basic_checks( + candidate: &CandidateDescriptor, + max_block_data_size: Option<u64>, + pov: &PoV, +) -> bool { + let encoded_pov = pov.encode(); + let hash = pov.hash(); + + if let Some(max_size) = max_block_data_size { + if encoded_pov.len() as u64 > max_size { + return false; + } + } + + if hash != candidate.pov_hash { + return false; + } + + if let Err(()) = candidate.check_collator_signature() { + return false; + } + + true +} + +/// Check the result of Wasm execution against the constraints given by the relay-chain. +/// +/// Returns `true` if checks pass, false otherwise. +fn check_wasm_result_against_constraints( + global_validation_data: &GlobalValidationData, + _local_validation_data: &LocalValidationData, + result: &WasmValidationResult, +) -> bool { + if result.head_data.0.len() > global_validation_data.max_head_data_size as _ { + return false + } + + if let Some(ref code) = result.new_validation_code { + if code.0.len() > global_validation_data.max_code_size as _ { + return false + } + } + + true +} + +trait ValidationBackend { + type Arg; + + fn validate<S: SpawnNamed + 'static>( + arg: Self::Arg, + validation_code: &ValidationCode, + params: ValidationParams, + spawn: S, + ) -> Result<WasmValidationResult, wasm_executor::Error>; +} + +struct RealValidationBackend; + +impl ValidationBackend for RealValidationBackend { + type Arg = Option<ValidationPool>; + + fn validate<S: SpawnNamed + 'static>( + pool: Option<ValidationPool>, + validation_code: &ValidationCode, + params: ValidationParams, + spawn: S, + ) -> Result<WasmValidationResult, wasm_executor::Error> { + let execution_mode = pool.as_ref() + .map(ExecutionMode::Remote) + .unwrap_or(ExecutionMode::Local); + + wasm_executor::validate_candidate( + &validation_code.0, + params, + execution_mode, + spawn, + ) + } +} + +/// Validates the candidate from exhaustive parameters. +/// +/// Sends the result of validation on the channel once complete. +fn validate_candidate_exhaustive<B: ValidationBackend, S: SpawnNamed + 'static>( + backend_arg: B::Arg, + omitted_validation: OmittedValidationData, + validation_code: ValidationCode, + descriptor: CandidateDescriptor, + pov: Arc<PoV>, + spawn: S, +) -> Result<ValidationResult, ValidationFailed> { + if !passes_basic_checks(&descriptor, None, &*pov) { + return Ok(ValidationResult::Invalid); + } + + let OmittedValidationData { global_validation, local_validation } = omitted_validation; + + let params = ValidationParams { + parent_head: local_validation.parent_head.clone(), + block_data: pov.block_data.clone(), + max_code_size: global_validation.max_code_size, + max_head_data_size: global_validation.max_head_data_size, + relay_chain_height: global_validation.block_number, + code_upgrade_allowed: local_validation.code_upgrade_allowed, + }; + + match B::validate(backend_arg, &validation_code, params, spawn) { + Err(wasm_executor::Error::BadReturn) => Ok(ValidationResult::Invalid), + Err(_) => Err(ValidationFailed), + Ok(res) => { + let passes_post_checks = check_wasm_result_against_constraints( + &global_validation, + &local_validation, + &res, + ); + + Ok(if passes_post_checks { + ValidationResult::Valid(ValidationOutputs { + head_data: res.head_data, + global_validation_data: global_validation, + local_validation_data: local_validation, + upward_messages: res.upward_messages, + fees: 0, + new_validation_code: res.new_validation_code, + }) + } else { + ValidationResult::Invalid + }) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use polkadot_subsystem::test_helpers; + use polkadot_primitives::v1::{HeadData, BlockData}; + use sp_core::testing::TaskExecutor; + use futures::executor; + use assert_matches::assert_matches; + use sp_keyring::Sr25519Keyring; + + struct MockValidationBackend; + + struct MockValidationArg { + result: Result<WasmValidationResult, wasm_executor::Error>, + } + + impl ValidationBackend for MockValidationBackend { + type Arg = MockValidationArg; + + fn validate<S: SpawnNamed + 'static>( + arg: Self::Arg, + _validation_code: &ValidationCode, + _params: ValidationParams, + _spawn: S, + ) -> Result<WasmValidationResult, wasm_executor::Error> { + arg.result + } + } + + fn collator_sign(descriptor: &mut CandidateDescriptor, collator: Sr25519Keyring) { + descriptor.collator = collator.public().into(); + let payload = polkadot_primitives::v1::collator_signature_payload( + &descriptor.relay_parent, + &descriptor.para_id, + &descriptor.validation_data_hash, + &descriptor.pov_hash, + ); + + descriptor.signature = collator.sign(&payload[..]).into(); + assert!(descriptor.check_collator_signature().is_ok()); + } + + #[test] + fn correctly_checks_included_assumption() { + let local_validation_data = LocalValidationData::default(); + let global_validation_data = GlobalValidationData::default(); + let validation_code: ValidationCode = vec![1, 2, 3].into(); + + let validation_data_hash = validation_data_hash(&global_validation_data, &local_validation_data); + let relay_parent = [2; 32].into(); + let para_id = 5.into(); + + let mut candidate = CandidateDescriptor::default(); + candidate.relay_parent = relay_parent; + candidate.validation_data_hash = validation_data_hash; + candidate.para_id = para_id; + + let pool = TaskExecutor::new(); + let (mut ctx, mut ctx_handle) = test_helpers::make_subsystem_context(pool.clone()); + + let (check_fut, check_result) = check_assumption_validation_data( + &mut ctx, + &candidate, + &global_validation_data, + OccupiedCoreAssumption::Included, + ).remote_handle(); + + let global_validation_data = global_validation_data.clone(); + let test_fut = async move { + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request( + rp, + RuntimeApiRequest::LocalValidationData(p, OccupiedCoreAssumption::Included, tx) + )) => { + assert_eq!(rp, relay_parent); + assert_eq!(p, para_id); + + let _ = tx.send(Ok(Some(local_validation_data.clone()))); + } + ); + + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request( + rp, + RuntimeApiRequest::ValidationCode(p, OccupiedCoreAssumption::Included, tx) + )) => { + assert_eq!(rp, relay_parent); + assert_eq!(p, para_id); + + let _ = tx.send(Ok(Some(validation_code.clone()))); + } + ); + + assert_matches!(check_result.await.unwrap(), AssumptionCheckOutcome::Matches(o, v) => { + assert_eq!(o, OmittedValidationData { + local_validation: local_validation_data, + global_validation: global_validation_data, + }); + + assert_eq!(v, validation_code); + }); + }; + + let test_fut = future::join(test_fut, check_fut); + executor::block_on(test_fut); + } + + #[test] + fn correctly_checks_timed_out_assumption() { + let local_validation_data = LocalValidationData::default(); + let global_validation_data = GlobalValidationData::default(); + let validation_code: ValidationCode = vec![1, 2, 3].into(); + + let validation_data_hash = validation_data_hash(&global_validation_data, &local_validation_data); + let relay_parent = [2; 32].into(); + let para_id = 5.into(); + + let mut candidate = CandidateDescriptor::default(); + candidate.relay_parent = relay_parent; + candidate.validation_data_hash = validation_data_hash; + candidate.para_id = para_id; + + let pool = TaskExecutor::new(); + let (mut ctx, mut ctx_handle) = test_helpers::make_subsystem_context(pool.clone()); + + let (check_fut, check_result) = check_assumption_validation_data( + &mut ctx, + &candidate, + &global_validation_data, + OccupiedCoreAssumption::TimedOut, + ).remote_handle(); + + let global_validation_data = global_validation_data.clone(); + let test_fut = async move { + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request( + rp, + RuntimeApiRequest::LocalValidationData(p, OccupiedCoreAssumption::TimedOut, tx) + )) => { + assert_eq!(rp, relay_parent); + assert_eq!(p, para_id); + + let _ = tx.send(Ok(Some(local_validation_data.clone()))); + } + ); + + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request( + rp, + RuntimeApiRequest::ValidationCode(p, OccupiedCoreAssumption::Included, tx) + )) => { + assert_eq!(rp, relay_parent); + assert_eq!(p, para_id); + + let _ = tx.send(Ok(Some(validation_code.clone()))); + } + ); + + assert_matches!(check_result.await.unwrap(), AssumptionCheckOutcome::Matches(o, v) => { + assert_eq!(o, OmittedValidationData { + local_validation: local_validation_data, + global_validation: global_validation_data, + }); + + assert_eq!(v, validation_code); + }); + }; + + let test_fut = future::join(test_fut, check_fut); + executor::block_on(test_fut); + } + + #[test] + fn check_is_bad_request_if_no_validation_data() { + let local_validation_data = LocalValidationData::default(); + let global_validation_data = GlobalValidationData::default(); + + let validation_data_hash = validation_data_hash(&global_validation_data, &local_validation_data); + let relay_parent = [2; 32].into(); + let para_id = 5.into(); + + let mut candidate = CandidateDescriptor::default(); + candidate.relay_parent = relay_parent; + candidate.validation_data_hash = validation_data_hash; + candidate.para_id = para_id; + + let pool = TaskExecutor::new(); + let (mut ctx, mut ctx_handle) = test_helpers::make_subsystem_context(pool.clone()); + + let (check_fut, check_result) = check_assumption_validation_data( + &mut ctx, + &candidate, + &global_validation_data, + OccupiedCoreAssumption::Included, + ).remote_handle(); + + let test_fut = async move { + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request( + rp, + RuntimeApiRequest::LocalValidationData(p, OccupiedCoreAssumption::Included, tx) + )) => { + assert_eq!(rp, relay_parent); + assert_eq!(p, para_id); + + let _ = tx.send(Ok(None)); + } + ); + + assert_matches!(check_result.await.unwrap(), AssumptionCheckOutcome::BadRequest); + }; + + let test_fut = future::join(test_fut, check_fut); + executor::block_on(test_fut); + } + + #[test] + fn check_is_bad_request_if_no_validation_code() { + let local_validation_data = LocalValidationData::default(); + let global_validation_data = GlobalValidationData::default(); + + let validation_data_hash = validation_data_hash(&global_validation_data, &local_validation_data); + let relay_parent = [2; 32].into(); + let para_id = 5.into(); + + let mut candidate = CandidateDescriptor::default(); + candidate.relay_parent = relay_parent; + candidate.validation_data_hash = validation_data_hash; + candidate.para_id = para_id; + + let pool = TaskExecutor::new(); + let (mut ctx, mut ctx_handle) = test_helpers::make_subsystem_context(pool.clone()); + + let (check_fut, check_result) = check_assumption_validation_data( + &mut ctx, + &candidate, + &global_validation_data, + OccupiedCoreAssumption::TimedOut, + ).remote_handle(); + + let test_fut = async move { + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request( + rp, + RuntimeApiRequest::LocalValidationData(p, OccupiedCoreAssumption::TimedOut, tx) + )) => { + assert_eq!(rp, relay_parent); + assert_eq!(p, para_id); + + let _ = tx.send(Ok(Some(local_validation_data.clone()))); + } + ); + + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request( + rp, + RuntimeApiRequest::ValidationCode(p, OccupiedCoreAssumption::Included, tx) + )) => { + assert_eq!(rp, relay_parent); + assert_eq!(p, para_id); + + let _ = tx.send(Ok(None)); + } + ); + + assert_matches!(check_result.await.unwrap(), AssumptionCheckOutcome::BadRequest); + }; + + let test_fut = future::join(test_fut, check_fut); + executor::block_on(test_fut); + } + + #[test] + fn check_does_not_match() { + let local_validation_data = LocalValidationData::default(); + let global_validation_data = GlobalValidationData::default(); + + let relay_parent = [2; 32].into(); + let para_id = 5.into(); + + let mut candidate = CandidateDescriptor::default(); + candidate.relay_parent = relay_parent; + candidate.validation_data_hash = [3; 32].into(); + candidate.para_id = para_id; + + let pool = TaskExecutor::new(); + let (mut ctx, mut ctx_handle) = test_helpers::make_subsystem_context(pool.clone()); + + let (check_fut, check_result) = check_assumption_validation_data( + &mut ctx, + &candidate, + &global_validation_data, + OccupiedCoreAssumption::Included, + ).remote_handle(); + + let test_fut = async move { + assert_matches!( + ctx_handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request( + rp, + RuntimeApiRequest::LocalValidationData(p, OccupiedCoreAssumption::Included, tx) + )) => { + assert_eq!(rp, relay_parent); + assert_eq!(p, para_id); + + let _ = tx.send(Ok(Some(local_validation_data.clone()))); + } + ); + + assert_matches!(check_result.await.unwrap(), AssumptionCheckOutcome::DoesNotMatch); + }; + + let test_fut = future::join(test_fut, check_fut); + executor::block_on(test_fut); + } + + #[test] + fn candidate_validation_ok_is_ok() { + let mut omitted_validation = OmittedValidationData { + local_validation: Default::default(), + global_validation: Default::default(), + }; + + omitted_validation.global_validation.max_head_data_size = 1024; + omitted_validation.global_validation.max_code_size = 1024; + + let pov = PoV { block_data: BlockData(vec![1; 32]) }; + + let mut descriptor = CandidateDescriptor::default(); + descriptor.pov_hash = pov.hash(); + collator_sign(&mut descriptor, Sr25519Keyring::Alice); + + assert!(passes_basic_checks(&descriptor, Some(1024), &pov)); + + let validation_result = WasmValidationResult { + head_data: HeadData(vec![1, 1, 1]), + new_validation_code: Some(vec![2, 2, 2].into()), + upward_messages: Vec::new(), + processed_downward_messages: 0, + }; + + assert!(check_wasm_result_against_constraints( + &omitted_validation.global_validation, + &omitted_validation.local_validation, + &validation_result, + )); + + let v = validate_candidate_exhaustive::<MockValidationBackend, _>( + MockValidationArg { result: Ok(validation_result) }, + omitted_validation.clone(), + vec![1, 2, 3].into(), + descriptor, + Arc::new(pov), + TaskExecutor::new(), + ).unwrap(); + + assert_matches!(v, ValidationResult::Valid(outputs) => { + assert_eq!(outputs.head_data, HeadData(vec![1, 1, 1])); + assert_eq!(outputs.global_validation_data, omitted_validation.global_validation); + assert_eq!(outputs.local_validation_data, omitted_validation.local_validation); + assert_eq!(outputs.upward_messages, Vec::new()); + assert_eq!(outputs.fees, 0); + assert_eq!(outputs.new_validation_code, Some(vec![2, 2, 2].into())); + }); + } + + #[test] + fn candidate_validation_bad_return_is_invalid() { + let mut omitted_validation = OmittedValidationData { + local_validation: Default::default(), + global_validation: Default::default(), + }; + + omitted_validation.global_validation.max_head_data_size = 1024; + omitted_validation.global_validation.max_code_size = 1024; + + let pov = PoV { block_data: BlockData(vec![1; 32]) }; + + let mut descriptor = CandidateDescriptor::default(); + descriptor.pov_hash = pov.hash(); + collator_sign(&mut descriptor, Sr25519Keyring::Alice); + + assert!(passes_basic_checks(&descriptor, Some(1024), &pov)); + + let validation_result = WasmValidationResult { + head_data: HeadData(vec![1, 1, 1]), + new_validation_code: Some(vec![2, 2, 2].into()), + upward_messages: Vec::new(), + processed_downward_messages: 0, + }; + + assert!(check_wasm_result_against_constraints( + &omitted_validation.global_validation, + &omitted_validation.local_validation, + &validation_result, + )); + + let v = validate_candidate_exhaustive::<MockValidationBackend, _>( + MockValidationArg { result: Err(wasm_executor::Error::BadReturn) }, + omitted_validation.clone(), + vec![1, 2, 3].into(), + descriptor, + Arc::new(pov), + TaskExecutor::new(), + ).unwrap(); + + assert_matches!(v, ValidationResult::Invalid); + } + + + #[test] + fn candidate_validation_timeout_is_internal_error() { + let mut omitted_validation = OmittedValidationData { + local_validation: Default::default(), + global_validation: Default::default(), + }; + + omitted_validation.global_validation.max_head_data_size = 1024; + omitted_validation.global_validation.max_code_size = 1024; + + let pov = PoV { block_data: BlockData(vec![1; 32]) }; + + let mut descriptor = CandidateDescriptor::default(); + descriptor.pov_hash = pov.hash(); + collator_sign(&mut descriptor, Sr25519Keyring::Alice); + + assert!(passes_basic_checks(&descriptor, Some(1024), &pov)); + + let validation_result = WasmValidationResult { + head_data: HeadData(vec![1, 1, 1]), + new_validation_code: Some(vec![2, 2, 2].into()), + upward_messages: Vec::new(), + processed_downward_messages: 0, + }; + + assert!(check_wasm_result_against_constraints( + &omitted_validation.global_validation, + &omitted_validation.local_validation, + &validation_result, + )); + + let v = validate_candidate_exhaustive::<MockValidationBackend, _>( + MockValidationArg { result: Err(wasm_executor::Error::Timeout) }, + omitted_validation.clone(), + vec![1, 2, 3].into(), + descriptor, + Arc::new(pov), + TaskExecutor::new(), + ); + + assert_matches!(v, Err(ValidationFailed)); + } +} diff --git a/polkadot/node/subsystem/src/messages.rs b/polkadot/node/subsystem/src/messages.rs index c049b9d199c..3cd06863d4f 100644 --- a/polkadot/node/subsystem/src/messages.rs +++ b/polkadot/node/subsystem/src/messages.rs @@ -105,7 +105,9 @@ pub enum CandidateValidationMessage { /// This will implicitly attempt to gather the `OmittedValidationData` and `ValidationCode` /// from the runtime API of the chain, based on the `relay_parent` /// of the `CandidateDescriptor`. - /// If there is no state available which can provide this data, an error is returned. + /// + /// If there is no state available which can provide this data or the core for + /// the para is not free at the relay-parent, an error is returned. ValidateFromChainState( CandidateDescriptor, Arc<PoV>, diff --git a/polkadot/roadmap/implementers-guide/src/node/utility/candidate-validation.md b/polkadot/roadmap/implementers-guide/src/node/utility/candidate-validation.md index ffeaa7a37e5..36c89b7d2a1 100644 --- a/polkadot/roadmap/implementers-guide/src/node/utility/candidate-validation.md +++ b/polkadot/roadmap/implementers-guide/src/node/utility/candidate-validation.md @@ -12,12 +12,38 @@ Output: Validation result via the provided response side-channel. ## Functionality -Given the hashes of a relay parent and a parachain candidate block, and either its PoV or the information with which to retrieve the PoV from the network, spawn a short-lived async job to determine whether the candidate is valid. +This subsystem answers two types of requests: one which draws out validation data from the state, and another which accepts all validation data exhaustively. The goal of both request types is to validate a candidate. There are three possible outputs of validation: either the candidate is valid, the candidate is invalid, or an internal error occurred. Whatever the end result is, it will be returned on the response channel to the requestor. -Each job follows this process: +Parachain candidates are validated against their validation function: A piece of Wasm code that is describes the state-transition of the parachain. Validation function execution is not metered. This means that an execution which is an infinite loop or simply takes too long must be forcibly exited by some other means. For this reason, we recommend dispatching candidate validation to be done on subprocesses which can be killed if they time-out. -- Get the full candidate from the current relay chain state -- Check the candidate's proof - > TODO: that's extremely hand-wavey. What does that actually entail? -- Generate either `Statement::Valid` or `Statement::Invalid`. Note that this never generates `Statement::Seconded`; Candidate Backing is the only subsystem which upgrades valid to seconded. -- Return the statement on the provided channel. +Upon receiving a validation request, the first thing the candidate validation subsystem should do is make sure it has all the necessary parameters to the validation function. These are: + * The Validation Function itself. + * The [`CandidateDescriptor`](../../types/candidate.md#candidatedescriptor). + * The [`LocalValidationData`](../../types/candidate.md#localvalidationdata). + * The [`GlobalValidationSchedule](../../types/candidate.md#globalvalidationschedule). + * The [`PoV`](../../types/availability.md#proofofvalidity). + +### Determining Parameters + +For a [`CandidateValidationMessage`][CVM]`::ValidateFromExhaustive`, these parameters are exhaustively provided. The [`OmittedValidationData`](../../types/availability.md#omittedvalidationdata) can be deconstructed into the validation data. + +For a [`CandidateValidationMessage`][CVM]`::ValidateFromChainState`, some more work needs to be done. Due to the uncertainty of Availability Cores (implemented in the [`Scheduler`](../../runtime/scheduler.md) module of the runtime), a candidate at a particular relay-parent and for a particular para may have two different valid validation-data to be executed under depending on what is assumed to happen if the para is occupying a core at the onset of the new block. This is encoded as an `OccupiedCoreAssumption` in the runtime API. + +The way that we can determine which assumption the candidate is meant to be executed under is simply to do an exhaustive check of both possibilities based on the state of the relay-parent. First we fetch the validation data under the assumption that the block occupying becomes available. If the `validation_data_hash` of the `CandidateDescriptor` matches this validation data, we use that. Otherwise, if the `validation_data_hash` matches the validation data fetched under the `TimedOut` assumption, we use that. Otherwise, we return a `ValidationResult::Invalid` response and conclude. + +Then, we can fetch the validation code from the runtime based on which type of candidate this is. This gives us all the parameters. The descriptor and PoV come from the request itself, and the other parameters have been derived from the state. + +> TODO: This would be a great place for caching to avoid making lots of runtime requests. That would need a job, though. + +### Execution of the Parachain Wasm + +Once we have all parameters, we can spin up a background task to perform the validation in a way that doesn't hold up the entire event loop. Before invoking the validation function itself, this should first do some basic checks: + * The collator signature is valid + * The PoV provided matches the `pov_hash` field of the descriptor + +After that, we can invoke the validation function. Lastly, we do some final checks on the output: + * The produced head-data is no larger than the maximum allowed. + * The produced code upgrade, if any, is no larger than the maximum allowed, and a code upgrade was allowed to be signaled. + * The amount and size of produced upward messages is not too large. + +[CVM]: ../../types/overseer-protocol.md#validationrequesttype diff --git a/polkadot/roadmap/implementers-guide/src/types/overseer-protocol.md b/polkadot/roadmap/implementers-guide/src/types/overseer-protocol.md index 41ae58bdc61..c3e0e9c2305 100644 --- a/polkadot/roadmap/implementers-guide/src/types/overseer-protocol.md +++ b/polkadot/roadmap/implementers-guide/src/types/overseer-protocol.md @@ -324,7 +324,9 @@ enum CandidateValidationMessage { /// Validate a candidate with provided parameters. This will implicitly attempt to gather the /// `OmittedValidationData` and `ValidationCode` from the runtime API of the chain, /// based on the `relay_parent` of the `CandidateDescriptor`. - /// If there is no state available which can provide this data, an error is returned. + /// + /// If there is no state available which can provide this data or the core for + /// the para is not free at the relay-parent, an error is returned. ValidateFromChainState(CandidateDescriptor, PoV, ResponseChannel<Result<ValidationResult>>), /// Validate a candidate with provided parameters. Explicitly provide the `OmittedValidationData` -- GitLab