-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Accept PVF code hashes in validation host #3655
base: master
Are you sure you want to change the base?
Changes from 20 commits
2880898
2c6a031
da26bf0
162be9b
427bc1a
7311c27
094d164
50fcfca
2c39c37
8d40599
c7cf9f5
2d0d047
828af04
d2a2dbf
8da537a
bec11a3
33560ca
0a93b6a
e58b1ec
0e9b0fe
a11d53a
e4edd44
9c91729
cb82bd4
fb7e4e7
c38eb81
2a48e29
24c043c
c3cf617
249fc28
47e9e7e
0a847ab
bccbbc3
3668cfa
32680ca
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -40,8 +40,8 @@ use polkadot_node_subsystem::{ | |
| use polkadot_node_subsystem_util::metrics::{self, prometheus}; | ||
| use polkadot_parachain::primitives::{ValidationParams, ValidationResult as WasmValidationResult}; | ||
| use polkadot_primitives::v1::{ | ||
| CandidateCommitments, CandidateDescriptor, Hash, OccupiedCoreAssumption, | ||
| PersistedValidationData, ValidationCode, ValidationCodeHash, | ||
| BlockNumber, CandidateCommitments, CandidateDescriptor, Hash, PersistedValidationData, | ||
| ValidationCode, ValidationCodeHash, | ||
| }; | ||
|
|
||
| use parity_scale_codec::Encode; | ||
|
|
@@ -171,15 +171,17 @@ where | |
| response_sender, | ||
| ) => { | ||
| let bg = { | ||
| let mut sender = ctx.sender().clone(); | ||
| let metrics = metrics.clone(); | ||
| let validation_host = validation_host.clone(); | ||
|
|
||
| async move { | ||
| let _timer = metrics.time_validate_from_exhaustive(); | ||
| let res = validate_candidate_exhaustive( | ||
| &mut sender, | ||
| validation_host, | ||
| persisted_validation_data, | ||
| validation_code, | ||
| Some(validation_code), | ||
| descriptor, | ||
| pov, | ||
| timeout, | ||
|
|
@@ -199,6 +201,7 @@ where | |
| } | ||
| } | ||
|
|
||
| #[derive(Debug)] | ||
| struct RuntimeRequestFailed; | ||
|
|
||
| async fn runtime_api_request<T, Sender>( | ||
|
|
@@ -235,95 +238,50 @@ where | |
| }) | ||
| } | ||
|
|
||
| #[derive(Debug)] | ||
| enum AssumptionCheckOutcome { | ||
| Matches(PersistedValidationData, ValidationCode), | ||
| DoesNotMatch, | ||
| BadRequest, | ||
| } | ||
|
|
||
| async fn check_assumption_validation_data<Sender>( | ||
| async fn request_validation_code_by_hash<Sender>( | ||
| sender: &mut Sender, | ||
| descriptor: &CandidateDescriptor, | ||
| assumption: OccupiedCoreAssumption, | ||
| ) -> AssumptionCheckOutcome | ||
| ) -> Result<Option<ValidationCode>, RuntimeRequestFailed> | ||
| where | ||
| Sender: SubsystemSender, | ||
| { | ||
| let validation_data = { | ||
| let (tx, rx) = oneshot::channel(); | ||
| let d = runtime_api_request( | ||
| sender, | ||
| descriptor.relay_parent, | ||
| RuntimeApiRequest::PersistedValidationData(descriptor.para_id, assumption, tx), | ||
| rx, | ||
| ) | ||
| .await; | ||
|
|
||
| match d { | ||
| Ok(None) | Err(RuntimeRequestFailed) => return AssumptionCheckOutcome::BadRequest, | ||
| Ok(Some(d)) => d, | ||
| } | ||
| }; | ||
|
|
||
| let persisted_validation_data_hash = validation_data.hash(); | ||
|
|
||
| if descriptor.persisted_validation_data_hash == persisted_validation_data_hash { | ||
| let (code_tx, code_rx) = oneshot::channel(); | ||
| let validation_code = runtime_api_request( | ||
| sender, | ||
| descriptor.relay_parent, | ||
| RuntimeApiRequest::ValidationCode(descriptor.para_id, assumption, code_tx), | ||
| code_rx, | ||
| ) | ||
| .await; | ||
|
|
||
| match validation_code { | ||
| Ok(None) | Err(RuntimeRequestFailed) => AssumptionCheckOutcome::BadRequest, | ||
| Ok(Some(v)) => AssumptionCheckOutcome::Matches(validation_data, v), | ||
| } | ||
| } else { | ||
| AssumptionCheckOutcome::DoesNotMatch | ||
| } | ||
| let (tx, rx) = oneshot::channel(); | ||
| runtime_api_request( | ||
| sender, | ||
| descriptor.relay_parent, | ||
| RuntimeApiRequest::ValidationCodeByHash(descriptor.validation_code_hash, tx), | ||
| rx, | ||
| ) | ||
| .await | ||
| } | ||
|
|
||
| async fn find_assumed_validation_data<Sender>( | ||
| async fn request_assumed_validation_data<Sender>( | ||
| sender: &mut Sender, | ||
| descriptor: &CandidateDescriptor, | ||
| ) -> AssumptionCheckOutcome | ||
| ) -> Result< | ||
| Option<(PersistedValidationData<Hash, BlockNumber>, ValidationCodeHash)>, | ||
| RuntimeRequestFailed, | ||
| > | ||
| where | ||
| Sender: SubsystemSender, | ||
| { | ||
| // The candidate descriptor has a `persisted_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 persisted validation data | ||
| // based on the different `OccupiedCoreAssumption`s. | ||
|
|
||
| const ASSUMPTIONS: &[OccupiedCoreAssumption] = &[ | ||
| OccupiedCoreAssumption::Included, | ||
| OccupiedCoreAssumption::TimedOut, | ||
| // `TimedOut` and `Free` both don't perform any speculation and therefore should be the same | ||
| // for our purposes here. In other words, if `TimedOut` matched then the `Free` must be | ||
| // matched as well. | ||
| ]; | ||
|
|
||
| // Consider running these checks in parallel to reduce validation latency. | ||
| for assumption in ASSUMPTIONS { | ||
| let outcome = check_assumption_validation_data(sender, descriptor, *assumption).await; | ||
|
|
||
| match outcome { | ||
| AssumptionCheckOutcome::Matches(_, _) => return outcome, | ||
| AssumptionCheckOutcome::BadRequest => return outcome, | ||
| AssumptionCheckOutcome::DoesNotMatch => continue, | ||
| } | ||
| } | ||
|
|
||
| AssumptionCheckOutcome::DoesNotMatch | ||
| let (tx, rx) = oneshot::channel(); | ||
| runtime_api_request( | ||
| sender, | ||
| descriptor.relay_parent, | ||
| RuntimeApiRequest::AssumedValidationData( | ||
| descriptor.para_id, | ||
| descriptor.persisted_validation_data_hash, | ||
| tx, | ||
| ), | ||
| rx, | ||
| ) | ||
| .await | ||
| } | ||
|
|
||
| async fn validate_from_chain_state<Sender>( | ||
| sender: &mut Sender, | ||
| validation_host: ValidationHost, | ||
| validation_host: impl ValidationBackend, | ||
| descriptor: CandidateDescriptor, | ||
| pov: Arc<PoV>, | ||
| timeout: Duration, | ||
|
|
@@ -332,24 +290,31 @@ async fn validate_from_chain_state<Sender>( | |
| where | ||
| Sender: SubsystemSender, | ||
| { | ||
| let (validation_data, validation_code) = | ||
| match find_assumed_validation_data(sender, &descriptor).await { | ||
| AssumptionCheckOutcome::Matches(validation_data, validation_code) => | ||
| (validation_data, validation_code), | ||
| AssumptionCheckOutcome::DoesNotMatch => { | ||
| let (validation_data, validation_code_hash) = | ||
| match request_assumed_validation_data(sender, &descriptor).await { | ||
| Ok(Some((data, code_hash))) => (data, code_hash), | ||
| Ok(None) => { | ||
| // TODO: update comment. | ||
| // 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 persisted_validation_data_hash in the descriptor | ||
| // is not based on the relay parent and is thus invalid. | ||
| return Ok(ValidationResult::Invalid(InvalidCandidate::BadParent)) | ||
| }, | ||
| AssumptionCheckOutcome::BadRequest => | ||
| return Err(ValidationFailed("Assumption Check: Bad request".into())), | ||
| Err(_) => | ||
| return Err(ValidationFailed( | ||
| "Failed to request persisted validation data from the Runtime API".into(), | ||
| )), | ||
| }; | ||
|
|
||
| if descriptor.validation_code_hash != validation_code_hash { | ||
| return Ok(ValidationResult::Invalid(InvalidCandidate::CodeHashMismatch)) | ||
| } | ||
|
|
||
| let validation_result = validate_candidate_exhaustive( | ||
| sender, | ||
| validation_host, | ||
| validation_data, | ||
| validation_code, | ||
| None, | ||
| descriptor.clone(), | ||
| pov, | ||
| timeout, | ||
|
|
@@ -377,18 +342,22 @@ where | |
| validation_result | ||
| } | ||
|
|
||
| async fn validate_candidate_exhaustive( | ||
| async fn validate_candidate_exhaustive<Sender>( | ||
| sender: &mut Sender, | ||
| mut validation_backend: impl ValidationBackend, | ||
| persisted_validation_data: PersistedValidationData, | ||
| validation_code: ValidationCode, | ||
| validation_code: Option<ValidationCode>, | ||
| descriptor: CandidateDescriptor, | ||
| pov: Arc<PoV>, | ||
| timeout: Duration, | ||
| metrics: &Metrics, | ||
| ) -> Result<ValidationResult, ValidationFailed> { | ||
| ) -> Result<ValidationResult, ValidationFailed> | ||
| where | ||
| Sender: SubsystemSender, | ||
| { | ||
| let _timer = metrics.time_validate_candidate_exhaustive(); | ||
|
|
||
| let validation_code_hash = validation_code.hash(); | ||
| let validation_code_hash = validation_code.as_ref().map(ValidationCode::hash); | ||
| tracing::debug!( | ||
| target: LOG_TARGET, | ||
| ?validation_code_hash, | ||
|
|
@@ -400,22 +369,30 @@ async fn validate_candidate_exhaustive( | |
| &descriptor, | ||
| persisted_validation_data.max_pov_size, | ||
| &*pov, | ||
| &validation_code_hash, | ||
| validation_code_hash.as_ref(), | ||
| ) { | ||
| return Ok(ValidationResult::Invalid(e)) | ||
| } | ||
|
|
||
| let raw_validation_code = match sp_maybe_compressed_blob::decompress( | ||
| &validation_code.0, | ||
| VALIDATION_CODE_BOMB_LIMIT, | ||
| ) { | ||
| Ok(code) => code, | ||
| Err(e) => { | ||
| tracing::debug!(target: LOG_TARGET, err=?e, "Invalid validation code"); | ||
| let validation_code = if let Some(validation_code) = validation_code { | ||
| let raw_code = match sp_maybe_compressed_blob::decompress( | ||
| &validation_code.0, | ||
| VALIDATION_CODE_BOMB_LIMIT, | ||
| ) { | ||
| Ok(code) => code, | ||
| Err(e) => { | ||
| tracing::debug!(target: LOG_TARGET, err=?e, "Code decompression failed"); | ||
|
|
||
| // If the validation code is invalid, the candidate certainly is. | ||
| return Ok(ValidationResult::Invalid(InvalidCandidate::CodeDecompressionFailure)) | ||
| }, | ||
| // If the validation code is invalid, the candidate certainly is. | ||
| return Ok(ValidationResult::Invalid(InvalidCandidate::CodeDecompressionFailure)) | ||
| }, | ||
| } | ||
| .to_vec(); | ||
| Pvf::from_code(raw_code) | ||
| } else { | ||
| // In case validation code is not provided, ask the backend to obtain | ||
| // it from the cache using the hash. | ||
| Pvf::Hash(ValidationCodeHash::from(descriptor.validation_code_hash)) | ||
| }; | ||
|
|
||
| let raw_block_data = | ||
|
|
@@ -436,9 +413,45 @@ async fn validate_candidate_exhaustive( | |
| relay_parent_storage_root: persisted_validation_data.relay_parent_storage_root, | ||
| }; | ||
|
|
||
| let result = validation_backend | ||
| .validate_candidate(raw_validation_code.to_vec(), timeout, params) | ||
| .await; | ||
| let result = match validation_backend | ||
| .validate_candidate(validation_code, timeout, params.clone()) | ||
| .await | ||
| { | ||
| Err(ValidationError::ArtifactNotFound) => { | ||
| // In case preimage for the supplied code hash was not found by the | ||
| // validation host, request the code from Runtime API and try again. | ||
| tracing::debug!( | ||
| target: LOG_TARGET, | ||
| "Validation host failed to find artifact by provided hash", | ||
| ); | ||
|
|
||
| let validation_code = match request_validation_code_by_hash(sender, &descriptor).await { | ||
| Ok(Some(validation_code)) => validation_code, | ||
| Ok(None) => | ||
| // TODO: code not found by hash by the runtime, is this candidate invalid? | ||
|
slumber marked this conversation as resolved.
Outdated
|
||
| return Err(ValidationFailed( | ||
| "Runtime API didn't return validation code by hash".into(), | ||
| )), | ||
| Err(_) => return Err(ValidationFailed("Runtime API request failed".into())), | ||
| }; | ||
|
|
||
| let raw_code = match sp_maybe_compressed_blob::decompress( | ||
| &validation_code.0, | ||
| VALIDATION_CODE_BOMB_LIMIT, | ||
| ) { | ||
| Ok(code) => code, | ||
| Err(e) => { | ||
| tracing::debug!(target: LOG_TARGET, err=?e, "Code decompression failed"); | ||
|
|
||
| // If the validation code is invalid, the candidate certainly is. | ||
| return Ok(ValidationResult::Invalid(InvalidCandidate::CodeDecompressionFailure)) | ||
| }, | ||
| }; | ||
| let validation_code = Pvf::from_code(raw_code.to_vec()); | ||
| validation_backend.validate_candidate(validation_code, timeout, params).await | ||
| }, | ||
| result => result, | ||
| }; | ||
|
|
||
| if let Err(ref e) = result { | ||
| tracing::debug!( | ||
|
|
@@ -459,7 +472,15 @@ async fn validate_candidate_exhaustive( | |
| Ok(ValidationResult::Invalid(InvalidCandidate::ExecutionError( | ||
| "ambigious worker death".to_string(), | ||
| ))), | ||
|
|
||
| Err(ValidationError::ArtifactNotFound) => { | ||
| // The code was supplied on the second attempt, this | ||
| // error should be unreachable. | ||
| let err = ValidationFailed( | ||
| "Validation host failed to find artifact even though it was supplied".to_string(), | ||
| ); | ||
| tracing::error!(target: LOG_TARGET, error = ?err, "Unexpected error reported by the validation backend"); | ||
|
slumber marked this conversation as resolved.
Outdated
|
||
| Err(err) | ||
|
Comment on lines
+478
to
+579
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Imho there are too many strings involved and
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In fact, this could be replaced with The PR is on ice due to dependency on the recent runtime upgrade, so probably I could fix
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What my comment boils down to, is that exists which is an error antipattern, and makes the code less legible than it could be. It should be an error enum with explicit variants and annotations using |
||
| }, | ||
| Ok(res) => | ||
| if res.head_data.hash() != descriptor.para_head { | ||
| Ok(ValidationResult::Invalid(InvalidCandidate::ParaHeadHashMismatch)) | ||
|
|
@@ -481,7 +502,7 @@ async fn validate_candidate_exhaustive( | |
| trait ValidationBackend { | ||
| async fn validate_candidate( | ||
| &mut self, | ||
| raw_validation_code: Vec<u8>, | ||
| validation_code: Pvf, | ||
| timeout: Duration, | ||
| params: ValidationParams, | ||
| ) -> Result<WasmValidationResult, ValidationError>; | ||
|
|
@@ -491,14 +512,14 @@ trait ValidationBackend { | |
| impl ValidationBackend for ValidationHost { | ||
| async fn validate_candidate( | ||
| &mut self, | ||
| raw_validation_code: Vec<u8>, | ||
| validation_code: Pvf, | ||
|
drahnr marked this conversation as resolved.
Outdated
|
||
| timeout: Duration, | ||
| params: ValidationParams, | ||
| ) -> Result<WasmValidationResult, ValidationError> { | ||
| let (tx, rx) = oneshot::channel(); | ||
| if let Err(err) = self | ||
| .execute_pvf( | ||
| Pvf::from_code(raw_validation_code), | ||
| validation_code, | ||
| timeout, | ||
| params.encode(), | ||
| polkadot_node_core_pvf::Priority::Normal, | ||
|
|
@@ -526,7 +547,7 @@ fn perform_basic_checks( | |
| candidate: &CandidateDescriptor, | ||
| max_pov_size: u32, | ||
| pov: &PoV, | ||
| validation_code_hash: &ValidationCodeHash, | ||
| validation_code_hash: Option<&ValidationCodeHash>, | ||
| ) -> Result<(), InvalidCandidate> { | ||
| let pov_hash = pov.hash(); | ||
|
|
||
|
|
@@ -539,7 +560,7 @@ fn perform_basic_checks( | |
| return Err(InvalidCandidate::PoVHashMismatch) | ||
| } | ||
|
|
||
| if *validation_code_hash != candidate.validation_code_hash { | ||
| if let Some(false) = validation_code_hash.map(|hash| *hash == candidate.validation_code_hash) { | ||
|
pepyakin marked this conversation as resolved.
slumber marked this conversation as resolved.
|
||
| return Err(InvalidCandidate::CodeHashMismatch) | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.