mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-04-26 22:47:56 +00:00
Runtime API for checking validation outputs (#1842)
* annoying whitespaces * update guide Add `CheckValidationOutputs` runtime api and also change the candidate-validation stuff * promote ValidationOutputs to global primitives i.e. move it from node specific primitives to global v1 primitives. This will be needed when we share it later in the runtime inclusion module * refactor acceptance checks in the inclusion module factor out the common code to share it during the block inclusion and for the forthcoming CheckValidationOutputs runtime api. Also note that the acceptance criteria was updated to incorporate checks that exist now in candidate-validation * plumb the runtime api outside * extract validation_data from ValidationOutputs * use runtime-api to check validation outputs apart from that refactor, update docs and tidy a bit * Update the maxium code size This is to fix a test that performs an upgrade.
This commit is contained in:
@@ -32,11 +32,10 @@ use polkadot_primitives::v1::{
|
||||
CommittedCandidateReceipt, BackedCandidate, Id as ParaId, ValidatorId,
|
||||
ValidatorIndex, SigningContext, PoV,
|
||||
CandidateDescriptor, AvailableData, ValidatorSignature, Hash, CandidateReceipt,
|
||||
CandidateCommitments, CoreState, CoreIndex, CollatorId,
|
||||
CandidateCommitments, CoreState, CoreIndex, CollatorId, ValidationOutputs,
|
||||
};
|
||||
use polkadot_node_primitives::{
|
||||
FromTableMisbehavior, Statement, SignedFullStatement, MisbehaviorReport,
|
||||
ValidationOutputs, ValidationResult,
|
||||
FromTableMisbehavior, Statement, SignedFullStatement, MisbehaviorReport, ValidationResult,
|
||||
};
|
||||
use polkadot_subsystem::{
|
||||
messages::{
|
||||
@@ -287,7 +286,7 @@ impl CandidateBackingJob {
|
||||
let candidate_hash = candidate.hash();
|
||||
|
||||
let statement = match valid {
|
||||
ValidationResult::Valid(outputs) => {
|
||||
ValidationResult::Valid(outputs, validation_data) => {
|
||||
// make PoV available for later distribution. Send data to the availability
|
||||
// store to keep. Sign and dispatch `valid` statement to network if we
|
||||
// have not seconded the given candidate.
|
||||
@@ -296,6 +295,7 @@ impl CandidateBackingJob {
|
||||
// the collator, do not make available and report the collator.
|
||||
let commitments_check = self.make_pov_available(
|
||||
pov,
|
||||
validation_data,
|
||||
outputs,
|
||||
|commitments| if commitments.hash() == candidate.commitments_hash {
|
||||
Ok(CommittedCandidateReceipt {
|
||||
@@ -510,10 +510,11 @@ impl CandidateBackingJob {
|
||||
let v = self.request_candidate_validation(descriptor, pov.clone()).await?;
|
||||
|
||||
let statement = match v {
|
||||
ValidationResult::Valid(outputs) => {
|
||||
ValidationResult::Valid(outputs, validation_data) => {
|
||||
// If validation produces a new set of commitments, we vote the candidate as invalid.
|
||||
let commitments_check = self.make_pov_available(
|
||||
(&*pov).clone(),
|
||||
validation_data,
|
||||
outputs,
|
||||
|commitments| if commitments == expected_commitments {
|
||||
Ok(())
|
||||
@@ -652,12 +653,13 @@ impl CandidateBackingJob {
|
||||
async fn make_pov_available<T, E>(
|
||||
&mut self,
|
||||
pov: PoV,
|
||||
validation_data: polkadot_primitives::v1::PersistedValidationData,
|
||||
outputs: ValidationOutputs,
|
||||
with_commitments: impl FnOnce(CandidateCommitments) -> Result<T, E>,
|
||||
) -> Result<Result<T, E>, Error> {
|
||||
let available_data = AvailableData {
|
||||
pov,
|
||||
validation_data: outputs.validation_data,
|
||||
validation_data,
|
||||
};
|
||||
|
||||
let chunks = erasure_coding::obtain_chunks_v1(
|
||||
@@ -1147,12 +1149,11 @@ mod tests {
|
||||
) if pov == pov && &c == candidate.descriptor() => {
|
||||
tx.send(Ok(
|
||||
ValidationResult::Valid(ValidationOutputs {
|
||||
validation_data: test_state.validation_data.persisted,
|
||||
head_data: expected_head_data.clone(),
|
||||
upward_messages: Vec::new(),
|
||||
fees: Default::default(),
|
||||
new_validation_code: None,
|
||||
}),
|
||||
}, test_state.validation_data.persisted),
|
||||
)).unwrap();
|
||||
}
|
||||
);
|
||||
@@ -1267,12 +1268,11 @@ mod tests {
|
||||
) if pov == pov && &c == candidate_a.descriptor() => {
|
||||
tx.send(Ok(
|
||||
ValidationResult::Valid(ValidationOutputs {
|
||||
validation_data: test_state.validation_data.persisted,
|
||||
head_data: expected_head_data.clone(),
|
||||
upward_messages: Vec::new(),
|
||||
fees: Default::default(),
|
||||
new_validation_code: None,
|
||||
}),
|
||||
}, test_state.validation_data.persisted),
|
||||
)).unwrap();
|
||||
}
|
||||
);
|
||||
@@ -1406,12 +1406,11 @@ mod tests {
|
||||
) if pov == pov && &c == candidate_a.descriptor() => {
|
||||
tx.send(Ok(
|
||||
ValidationResult::Valid(ValidationOutputs {
|
||||
validation_data: test_state.validation_data.persisted,
|
||||
head_data: expected_head_data.clone(),
|
||||
upward_messages: Vec::new(),
|
||||
fees: Default::default(),
|
||||
new_validation_code: None,
|
||||
}),
|
||||
}, test_state.validation_data.persisted),
|
||||
)).unwrap();
|
||||
}
|
||||
);
|
||||
@@ -1562,12 +1561,11 @@ mod tests {
|
||||
) if pov == pov && &c == candidate_b.descriptor() => {
|
||||
tx.send(Ok(
|
||||
ValidationResult::Valid(ValidationOutputs {
|
||||
validation_data: test_state.validation_data.persisted,
|
||||
head_data: expected_head_data.clone(),
|
||||
upward_messages: Vec::new(),
|
||||
fees: Default::default(),
|
||||
new_validation_code: None,
|
||||
}),
|
||||
}, test_state.validation_data.persisted),
|
||||
)).unwrap();
|
||||
}
|
||||
);
|
||||
|
||||
@@ -343,7 +343,10 @@ async fn candidate_is_valid_inner(
|
||||
CandidateValidationMessage::ValidateFromChainState(candidate_descriptor, pov, tx),
|
||||
))
|
||||
.await?;
|
||||
Ok(std::matches!(rx.await, Ok(Ok(ValidationResult::Valid(_)))))
|
||||
Ok(std::matches!(
|
||||
rx.await,
|
||||
Ok(Ok(ValidationResult::Valid(_, _)))
|
||||
))
|
||||
}
|
||||
|
||||
async fn second_candidate(
|
||||
@@ -445,8 +448,7 @@ delegated_subsystem!(CandidateSelectionJob((), Metrics) <- ToJob as CandidateSel
|
||||
mod tests {
|
||||
use super::*;
|
||||
use futures::lock::Mutex;
|
||||
use polkadot_node_primitives::ValidationOutputs;
|
||||
use polkadot_primitives::v1::{BlockData, HeadData, PersistedValidationData};
|
||||
use polkadot_primitives::v1::{BlockData, HeadData, PersistedValidationData, ValidationOutputs};
|
||||
use sp_core::crypto::Public;
|
||||
|
||||
fn test_harness<Preconditions, TestBuilder, Test, Postconditions>(
|
||||
@@ -478,7 +480,7 @@ mod tests {
|
||||
postconditions(job, job_result);
|
||||
}
|
||||
|
||||
fn default_validation_outputs() -> ValidationOutputs {
|
||||
fn default_validation_outputs_and_data() -> (ValidationOutputs, polkadot_primitives::v1::PersistedValidationData) {
|
||||
let head_data: Vec<u8> = (0..32).rev().cycle().take(256).collect();
|
||||
let parent_head_data = head_data
|
||||
.iter()
|
||||
@@ -486,17 +488,19 @@ mod tests {
|
||||
.map(|x| x.saturating_sub(1))
|
||||
.collect();
|
||||
|
||||
ValidationOutputs {
|
||||
head_data: HeadData(head_data),
|
||||
validation_data: PersistedValidationData {
|
||||
(
|
||||
ValidationOutputs {
|
||||
head_data: HeadData(head_data),
|
||||
upward_messages: Vec::new(),
|
||||
fees: 0,
|
||||
new_validation_code: None,
|
||||
},
|
||||
PersistedValidationData {
|
||||
parent_head: HeadData(parent_head_data),
|
||||
block_number: 123,
|
||||
hrmp_mqc_heads: Vec::new(),
|
||||
},
|
||||
upward_messages: Vec::new(),
|
||||
fees: 0,
|
||||
new_validation_code: None,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/// when nothing is seconded so far, the collation is fetched and seconded
|
||||
@@ -556,8 +560,9 @@ mod tests {
|
||||
assert_eq!(got_candidate_descriptor, candidate_receipt.descriptor);
|
||||
assert_eq!(got_pov.as_ref(), &pov);
|
||||
|
||||
let (outputs, data) = default_validation_outputs_and_data();
|
||||
return_sender
|
||||
.send(Ok(ValidationResult::Valid(default_validation_outputs())))
|
||||
.send(Ok(ValidationResult::Valid(outputs, data)))
|
||||
.unwrap();
|
||||
}
|
||||
FromJob::Backing(CandidateBackingMessage::Second(
|
||||
|
||||
@@ -32,10 +32,10 @@ use polkadot_node_subsystem_util::{
|
||||
metrics::{self, prometheus},
|
||||
};
|
||||
use polkadot_subsystem::errors::RuntimeApiError;
|
||||
use polkadot_node_primitives::{ValidationResult, ValidationOutputs, InvalidCandidate};
|
||||
use polkadot_node_primitives::{ValidationResult, InvalidCandidate};
|
||||
use polkadot_primitives::v1::{
|
||||
ValidationCode, PoV, CandidateDescriptor, ValidationData, PersistedValidationData,
|
||||
TransientValidationData, OccupiedCoreAssumption, Hash,
|
||||
ValidationCode, PoV, CandidateDescriptor, PersistedValidationData,
|
||||
OccupiedCoreAssumption, Hash, ValidationOutputs,
|
||||
};
|
||||
use polkadot_parachain::wasm_executor::{
|
||||
self, ValidationPool, ExecutionMode, ValidationError,
|
||||
@@ -72,7 +72,7 @@ impl Metrics {
|
||||
fn on_validation_event(&self, event: &Result<ValidationResult, ValidationFailed>) {
|
||||
if let Some(metrics) = &self.0 {
|
||||
match event {
|
||||
Ok(ValidationResult::Valid(_)) => {
|
||||
Ok(ValidationResult::Valid(_, _)) => {
|
||||
metrics.validation_requests.with_label_values(&["valid"]).inc();
|
||||
},
|
||||
Ok(ValidationResult::Invalid(_)) => {
|
||||
@@ -161,7 +161,6 @@ async fn run(
|
||||
}
|
||||
CandidateValidationMessage::ValidateFromExhaustive(
|
||||
persisted_validation_data,
|
||||
transient_validation_data,
|
||||
validation_code,
|
||||
descriptor,
|
||||
pov,
|
||||
@@ -171,7 +170,6 @@ async fn run(
|
||||
&mut ctx,
|
||||
execution_mode.clone(),
|
||||
persisted_validation_data,
|
||||
transient_validation_data,
|
||||
validation_code,
|
||||
descriptor,
|
||||
pov,
|
||||
@@ -214,7 +212,7 @@ async fn runtime_api_request<T>(
|
||||
|
||||
#[derive(Debug)]
|
||||
enum AssumptionCheckOutcome {
|
||||
Matches(ValidationData, ValidationCode),
|
||||
Matches(PersistedValidationData, ValidationCode),
|
||||
DoesNotMatch,
|
||||
BadRequest,
|
||||
}
|
||||
@@ -229,7 +227,7 @@ async fn check_assumption_validation_data(
|
||||
let d = runtime_api_request(
|
||||
ctx,
|
||||
descriptor.relay_parent,
|
||||
RuntimeApiRequest::FullValidationData(
|
||||
RuntimeApiRequest::PersistedValidationData(
|
||||
descriptor.para_id,
|
||||
assumption,
|
||||
tx,
|
||||
@@ -245,7 +243,7 @@ async fn check_assumption_validation_data(
|
||||
}
|
||||
};
|
||||
|
||||
let persisted_validation_data_hash = validation_data.persisted.hash();
|
||||
let persisted_validation_data_hash = validation_data.hash();
|
||||
|
||||
SubsystemResult::Ok(if descriptor.persisted_validation_data_hash == persisted_validation_data_hash {
|
||||
let (code_tx, code_rx) = oneshot::channel();
|
||||
@@ -269,6 +267,37 @@ async fn check_assumption_validation_data(
|
||||
})
|
||||
}
|
||||
|
||||
async fn find_assumed_validation_data(
|
||||
ctx: &mut impl SubsystemContext<Message = CandidateValidationMessage>,
|
||||
descriptor: &CandidateDescriptor,
|
||||
) -> SubsystemResult<AssumptionCheckOutcome> {
|
||||
// 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,
|
||||
// TODO: Why don't we check `Free`? The guide assumes there are only two possible assumptions.
|
||||
//
|
||||
// Source that info and leave a comment here.
|
||||
];
|
||||
|
||||
// Consider running these checks in parallel to reduce validation latency.
|
||||
for assumption in ASSUMPTIONS {
|
||||
let outcome = check_assumption_validation_data(ctx, descriptor, *assumption).await?;
|
||||
|
||||
let () = match outcome {
|
||||
AssumptionCheckOutcome::Matches(_, _) => return Ok(outcome),
|
||||
AssumptionCheckOutcome::BadRequest => return Ok(outcome),
|
||||
AssumptionCheckOutcome::DoesNotMatch => continue,
|
||||
};
|
||||
}
|
||||
|
||||
Ok(AssumptionCheckOutcome::DoesNotMatch)
|
||||
}
|
||||
|
||||
async fn spawn_validate_from_chain_state(
|
||||
ctx: &mut impl SubsystemContext<Message = CandidateValidationMessage>,
|
||||
execution_mode: ExecutionMode,
|
||||
@@ -276,63 +305,62 @@ async fn spawn_validate_from_chain_state(
|
||||
pov: Arc<PoV>,
|
||||
spawn: impl SpawnNamed + 'static,
|
||||
) -> SubsystemResult<Result<ValidationResult, ValidationFailed>> {
|
||||
// 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.
|
||||
match check_assumption_validation_data(
|
||||
let (validation_data, validation_code) =
|
||||
match find_assumed_validation_data(ctx, &descriptor).await? {
|
||||
AssumptionCheckOutcome::Matches(validation_data, validation_code) => {
|
||||
(validation_data, validation_code)
|
||||
}
|
||||
AssumptionCheckOutcome::DoesNotMatch => {
|
||||
// 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(Ok(ValidationResult::Invalid(InvalidCandidate::BadParent)));
|
||||
}
|
||||
AssumptionCheckOutcome::BadRequest => {
|
||||
return Ok(Err(ValidationFailed("Assumption Check: Bad request".into())));
|
||||
}
|
||||
};
|
||||
|
||||
let validation_result = spawn_validate_exhaustive(
|
||||
ctx,
|
||||
&descriptor,
|
||||
OccupiedCoreAssumption::Included,
|
||||
).await? {
|
||||
AssumptionCheckOutcome::Matches(validation_data, validation_code) => {
|
||||
return spawn_validate_exhaustive(
|
||||
ctx,
|
||||
execution_mode,
|
||||
validation_data.persisted,
|
||||
Some(validation_data.transient),
|
||||
validation_code,
|
||||
descriptor,
|
||||
pov,
|
||||
spawn,
|
||||
).await;
|
||||
execution_mode,
|
||||
validation_data,
|
||||
validation_code,
|
||||
descriptor.clone(),
|
||||
pov,
|
||||
spawn,
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Ok(Ok(ValidationResult::Valid(ref outputs, _))) = validation_result {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
match runtime_api_request(
|
||||
ctx,
|
||||
descriptor.relay_parent,
|
||||
RuntimeApiRequest::CheckValidationOutputs(descriptor.para_id, outputs.clone(), tx),
|
||||
rx,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Ok(true) => {}
|
||||
Ok(false) => {
|
||||
return Ok(Ok(ValidationResult::Invalid(
|
||||
InvalidCandidate::InvalidOutputs,
|
||||
)));
|
||||
}
|
||||
Err(_) => {
|
||||
return Ok(Err(ValidationFailed("Check Validation Outputs: Bad request".into())));
|
||||
}
|
||||
}
|
||||
AssumptionCheckOutcome::DoesNotMatch => {},
|
||||
AssumptionCheckOutcome::BadRequest => return Ok(Err(ValidationFailed("Bad request".into()))),
|
||||
}
|
||||
|
||||
match check_assumption_validation_data(
|
||||
ctx,
|
||||
&descriptor,
|
||||
OccupiedCoreAssumption::TimedOut,
|
||||
).await? {
|
||||
AssumptionCheckOutcome::Matches(validation_data, validation_code) => {
|
||||
return spawn_validate_exhaustive(
|
||||
ctx,
|
||||
execution_mode,
|
||||
validation_data.persisted,
|
||||
Some(validation_data.transient),
|
||||
validation_code,
|
||||
descriptor,
|
||||
pov,
|
||||
spawn,
|
||||
).await;
|
||||
}
|
||||
AssumptionCheckOutcome::DoesNotMatch => {},
|
||||
AssumptionCheckOutcome::BadRequest => return Ok(Err(ValidationFailed("Bad request".into()))),
|
||||
}
|
||||
|
||||
// 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.
|
||||
Ok(Ok(ValidationResult::Invalid(InvalidCandidate::BadParent)))
|
||||
validation_result
|
||||
}
|
||||
|
||||
async fn spawn_validate_exhaustive(
|
||||
ctx: &mut impl SubsystemContext<Message = CandidateValidationMessage>,
|
||||
execution_mode: ExecutionMode,
|
||||
persisted_validation_data: PersistedValidationData,
|
||||
transient_validation_data: Option<TransientValidationData>,
|
||||
validation_code: ValidationCode,
|
||||
descriptor: CandidateDescriptor,
|
||||
pov: Arc<PoV>,
|
||||
@@ -343,7 +371,6 @@ async fn spawn_validate_exhaustive(
|
||||
let res = validate_candidate_exhaustive::<RealValidationBackend, _>(
|
||||
execution_mode,
|
||||
persisted_validation_data,
|
||||
transient_validation_data,
|
||||
validation_code,
|
||||
descriptor,
|
||||
pov,
|
||||
@@ -384,30 +411,6 @@ fn perform_basic_checks(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check the result of Wasm execution against the constraints given by the relay-chain.
|
||||
///
|
||||
/// Returns `Ok(())` if checks pass, error otherwise.
|
||||
fn check_wasm_result_against_constraints(
|
||||
transient_params: &TransientValidationData,
|
||||
result: &WasmValidationResult,
|
||||
) -> Result<(), InvalidCandidate> {
|
||||
if result.head_data.0.len() > transient_params.max_head_data_size as _ {
|
||||
return Err(InvalidCandidate::HeadDataTooLarge(result.head_data.0.len() as u64))
|
||||
}
|
||||
|
||||
if let Some(ref code) = result.new_validation_code {
|
||||
if transient_params.code_upgrade_allowed.is_none() {
|
||||
return Err(InvalidCandidate::CodeUpgradeNotAllowed)
|
||||
}
|
||||
|
||||
if code.0.len() > transient_params.max_code_size as _ {
|
||||
return Err(InvalidCandidate::NewCodeTooLarge(code.0.len() as u64))
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
trait ValidationBackend {
|
||||
type Arg;
|
||||
|
||||
@@ -445,7 +448,6 @@ impl ValidationBackend for RealValidationBackend {
|
||||
fn validate_candidate_exhaustive<B: ValidationBackend, S: SpawnNamed + 'static>(
|
||||
backend_arg: B::Arg,
|
||||
persisted_validation_data: PersistedValidationData,
|
||||
transient_validation_data: Option<TransientValidationData>,
|
||||
validation_code: ValidationCode,
|
||||
descriptor: CandidateDescriptor,
|
||||
pov: Arc<PoV>,
|
||||
@@ -477,25 +479,13 @@ fn validate_candidate_exhaustive<B: ValidationBackend, S: SpawnNamed + 'static>(
|
||||
Ok(ValidationResult::Invalid(InvalidCandidate::ExecutionError(e.to_string()))),
|
||||
Err(ValidationError::Internal(e)) => Err(ValidationFailed(e.to_string())),
|
||||
Ok(res) => {
|
||||
let post_check_result = if let Some(transient) = transient_validation_data {
|
||||
check_wasm_result_against_constraints(
|
||||
&transient,
|
||||
&res,
|
||||
)
|
||||
} else {
|
||||
Ok(())
|
||||
let outputs = ValidationOutputs {
|
||||
head_data: res.head_data,
|
||||
upward_messages: res.upward_messages,
|
||||
fees: 0,
|
||||
new_validation_code: res.new_validation_code,
|
||||
};
|
||||
|
||||
Ok(match post_check_result {
|
||||
Ok(()) => ValidationResult::Valid(ValidationOutputs {
|
||||
head_data: res.head_data,
|
||||
validation_data: persisted_validation_data,
|
||||
upward_messages: res.upward_messages,
|
||||
fees: 0,
|
||||
new_validation_code: res.new_validation_code,
|
||||
}),
|
||||
Err(e) => ValidationResult::Invalid(e),
|
||||
})
|
||||
Ok(ValidationResult::Valid(outputs, persisted_validation_data))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -544,10 +534,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn correctly_checks_included_assumption() {
|
||||
let validation_data: ValidationData = Default::default();
|
||||
let validation_data: PersistedValidationData = Default::default();
|
||||
let validation_code: ValidationCode = vec![1, 2, 3].into();
|
||||
|
||||
let persisted_validation_data_hash = validation_data.persisted.hash();
|
||||
let persisted_validation_data_hash = validation_data.hash();
|
||||
let relay_parent = [2; 32].into();
|
||||
let para_id = 5.into();
|
||||
|
||||
@@ -570,7 +560,11 @@ mod tests {
|
||||
ctx_handle.recv().await,
|
||||
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
|
||||
rp,
|
||||
RuntimeApiRequest::FullValidationData(p, OccupiedCoreAssumption::Included, tx)
|
||||
RuntimeApiRequest::PersistedValidationData(
|
||||
p,
|
||||
OccupiedCoreAssumption::Included,
|
||||
tx
|
||||
),
|
||||
)) => {
|
||||
assert_eq!(rp, relay_parent);
|
||||
assert_eq!(p, para_id);
|
||||
@@ -604,10 +598,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn correctly_checks_timed_out_assumption() {
|
||||
let validation_data: ValidationData = Default::default();
|
||||
let validation_data: PersistedValidationData = Default::default();
|
||||
let validation_code: ValidationCode = vec![1, 2, 3].into();
|
||||
|
||||
let persisted_validation_data_hash = validation_data.persisted.hash();
|
||||
let persisted_validation_data_hash = validation_data.hash();
|
||||
let relay_parent = [2; 32].into();
|
||||
let para_id = 5.into();
|
||||
|
||||
@@ -630,7 +624,11 @@ mod tests {
|
||||
ctx_handle.recv().await,
|
||||
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
|
||||
rp,
|
||||
RuntimeApiRequest::FullValidationData(p, OccupiedCoreAssumption::TimedOut, tx)
|
||||
RuntimeApiRequest::PersistedValidationData(
|
||||
p,
|
||||
OccupiedCoreAssumption::TimedOut,
|
||||
tx
|
||||
),
|
||||
)) => {
|
||||
assert_eq!(rp, relay_parent);
|
||||
assert_eq!(p, para_id);
|
||||
@@ -664,8 +662,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn check_is_bad_request_if_no_validation_data() {
|
||||
let validation_data: ValidationData = Default::default();
|
||||
let persisted_validation_data_hash = validation_data.persisted.hash();
|
||||
let validation_data: PersistedValidationData = Default::default();
|
||||
let persisted_validation_data_hash = validation_data.hash();
|
||||
let relay_parent = [2; 32].into();
|
||||
let para_id = 5.into();
|
||||
|
||||
@@ -688,7 +686,11 @@ mod tests {
|
||||
ctx_handle.recv().await,
|
||||
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
|
||||
rp,
|
||||
RuntimeApiRequest::FullValidationData(p, OccupiedCoreAssumption::Included, tx)
|
||||
RuntimeApiRequest::PersistedValidationData(
|
||||
p,
|
||||
OccupiedCoreAssumption::Included,
|
||||
tx
|
||||
),
|
||||
)) => {
|
||||
assert_eq!(rp, relay_parent);
|
||||
assert_eq!(p, para_id);
|
||||
@@ -706,8 +708,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn check_is_bad_request_if_no_validation_code() {
|
||||
let validation_data: ValidationData = Default::default();
|
||||
let persisted_validation_data_hash = validation_data.persisted.hash();
|
||||
let validation_data: PersistedValidationData = Default::default();
|
||||
let persisted_validation_data_hash = validation_data.hash();
|
||||
let relay_parent = [2; 32].into();
|
||||
let para_id = 5.into();
|
||||
|
||||
@@ -730,7 +732,11 @@ mod tests {
|
||||
ctx_handle.recv().await,
|
||||
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
|
||||
rp,
|
||||
RuntimeApiRequest::FullValidationData(p, OccupiedCoreAssumption::TimedOut, tx)
|
||||
RuntimeApiRequest::PersistedValidationData(
|
||||
p,
|
||||
OccupiedCoreAssumption::TimedOut,
|
||||
tx
|
||||
),
|
||||
)) => {
|
||||
assert_eq!(rp, relay_parent);
|
||||
assert_eq!(p, para_id);
|
||||
@@ -761,7 +767,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn check_does_not_match() {
|
||||
let validation_data: ValidationData = Default::default();
|
||||
let validation_data: PersistedValidationData = Default::default();
|
||||
let relay_parent = [2; 32].into();
|
||||
let para_id = 5.into();
|
||||
|
||||
@@ -784,7 +790,11 @@ mod tests {
|
||||
ctx_handle.recv().await,
|
||||
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
|
||||
rp,
|
||||
RuntimeApiRequest::FullValidationData(p, OccupiedCoreAssumption::Included, tx)
|
||||
RuntimeApiRequest::PersistedValidationData(
|
||||
p,
|
||||
OccupiedCoreAssumption::Included,
|
||||
tx
|
||||
),
|
||||
)) => {
|
||||
assert_eq!(rp, relay_parent);
|
||||
assert_eq!(p, para_id);
|
||||
@@ -802,10 +812,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn candidate_validation_ok_is_ok() {
|
||||
let mut validation_data: ValidationData = Default::default();
|
||||
validation_data.transient.max_head_data_size = 1024;
|
||||
validation_data.transient.max_code_size = 1024;
|
||||
validation_data.transient.code_upgrade_allowed = Some(20);
|
||||
let validation_data: PersistedValidationData = Default::default();
|
||||
|
||||
let pov = PoV { block_data: BlockData(vec![1; 32]) };
|
||||
|
||||
@@ -822,37 +829,27 @@ mod tests {
|
||||
processed_downward_messages: 0,
|
||||
};
|
||||
|
||||
assert!(check_wasm_result_against_constraints(
|
||||
&validation_data.transient,
|
||||
&validation_result,
|
||||
).is_ok());
|
||||
|
||||
let v = validate_candidate_exhaustive::<MockValidationBackend, _>(
|
||||
MockValidationArg { result: Ok(validation_result) },
|
||||
validation_data.persisted.clone(),
|
||||
Some(validation_data.transient),
|
||||
validation_data.clone(),
|
||||
vec![1, 2, 3].into(),
|
||||
descriptor,
|
||||
Arc::new(pov),
|
||||
TaskExecutor::new(),
|
||||
).unwrap();
|
||||
|
||||
assert_matches!(v, ValidationResult::Valid(outputs) => {
|
||||
assert_matches!(v, ValidationResult::Valid(outputs, used_validation_data) => {
|
||||
assert_eq!(outputs.head_data, HeadData(vec![1, 1, 1]));
|
||||
assert_eq!(outputs.validation_data, validation_data.persisted);
|
||||
assert_eq!(outputs.upward_messages, Vec::new());
|
||||
assert_eq!(outputs.fees, 0);
|
||||
assert_eq!(outputs.new_validation_code, Some(vec![2, 2, 2].into()));
|
||||
assert_eq!(used_validation_data, validation_data);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn candidate_validation_bad_return_is_invalid() {
|
||||
let mut validation_data: ValidationData = Default::default();
|
||||
|
||||
validation_data.transient.max_head_data_size = 1024;
|
||||
validation_data.transient.max_code_size = 1024;
|
||||
validation_data.transient.code_upgrade_allowed = Some(20);
|
||||
let validation_data: PersistedValidationData = Default::default();
|
||||
|
||||
let pov = PoV { block_data: BlockData(vec![1; 32]) };
|
||||
|
||||
@@ -862,26 +859,13 @@ mod tests {
|
||||
|
||||
assert!(perform_basic_checks(&descriptor, Some(1024), &pov).is_ok());
|
||||
|
||||
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(
|
||||
&validation_data.transient,
|
||||
&validation_result,
|
||||
).is_ok());
|
||||
|
||||
let v = validate_candidate_exhaustive::<MockValidationBackend, _>(
|
||||
MockValidationArg {
|
||||
result: Err(ValidationError::InvalidCandidate(
|
||||
WasmInvalidCandidate::BadReturn
|
||||
))
|
||||
},
|
||||
validation_data.persisted,
|
||||
Some(validation_data.transient),
|
||||
validation_data,
|
||||
vec![1, 2, 3].into(),
|
||||
descriptor,
|
||||
Arc::new(pov),
|
||||
@@ -891,14 +875,9 @@ mod tests {
|
||||
assert_matches!(v, ValidationResult::Invalid(InvalidCandidate::BadReturn));
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn candidate_validation_timeout_is_internal_error() {
|
||||
let mut validation_data: ValidationData = Default::default();
|
||||
|
||||
validation_data.transient.max_head_data_size = 1024;
|
||||
validation_data.transient.max_code_size = 1024;
|
||||
validation_data.transient.code_upgrade_allowed = Some(20);
|
||||
let validation_data: PersistedValidationData = Default::default();
|
||||
|
||||
let pov = PoV { block_data: BlockData(vec![1; 32]) };
|
||||
|
||||
@@ -908,26 +887,13 @@ mod tests {
|
||||
|
||||
assert!(perform_basic_checks(&descriptor, Some(1024), &pov).is_ok());
|
||||
|
||||
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(
|
||||
&validation_data.transient,
|
||||
&validation_result,
|
||||
).is_ok());
|
||||
|
||||
let v = validate_candidate_exhaustive::<MockValidationBackend, _>(
|
||||
MockValidationArg {
|
||||
result: Err(ValidationError::InvalidCandidate(
|
||||
WasmInvalidCandidate::Timeout
|
||||
))
|
||||
},
|
||||
validation_data.persisted,
|
||||
Some(validation_data.transient),
|
||||
validation_data,
|
||||
vec![1, 2, 3].into(),
|
||||
descriptor,
|
||||
Arc::new(pov),
|
||||
@@ -936,49 +902,4 @@ mod tests {
|
||||
|
||||
assert_matches!(v, Ok(ValidationResult::Invalid(InvalidCandidate::Timeout)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn candidate_validation_ok_does_not_validate_outputs_if_no_transient() {
|
||||
let mut validation_data: ValidationData = Default::default();
|
||||
validation_data.transient.max_head_data_size = 1;
|
||||
validation_data.transient.max_code_size = 1;
|
||||
|
||||
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!(perform_basic_checks(&descriptor, Some(1024), &pov).is_ok());
|
||||
|
||||
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(
|
||||
&validation_data.transient,
|
||||
&validation_result,
|
||||
).is_err());
|
||||
|
||||
let v = validate_candidate_exhaustive::<MockValidationBackend, _>(
|
||||
MockValidationArg { result: Ok(validation_result) },
|
||||
validation_data.persisted.clone(),
|
||||
None,
|
||||
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.validation_data, validation_data.persisted);
|
||||
assert_eq!(outputs.upward_messages, Vec::new());
|
||||
assert_eq!(outputs.fees, 0);
|
||||
assert_eq!(outputs.new_validation_code, Some(vec![2, 2, 2].into()));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +114,8 @@ fn make_runtime_api_request<Client>(
|
||||
query!(persisted_validation_data(para, assumption), sender),
|
||||
Request::FullValidationData(para, assumption, sender) =>
|
||||
query!(full_validation_data(para, assumption), sender),
|
||||
Request::CheckValidationOutputs(para, commitments, sender) =>
|
||||
query!(check_validation_outputs(para, commitments), sender),
|
||||
Request::SessionIndexForChild(sender) => query!(session_index_for_child(), sender),
|
||||
Request::ValidationCode(para, assumption, sender) =>
|
||||
query!(validation_code(para, assumption), sender),
|
||||
@@ -186,6 +188,7 @@ mod tests {
|
||||
validation_data: HashMap<ParaId, ValidationData>,
|
||||
session_index_for_child: SessionIndex,
|
||||
validation_code: HashMap<ParaId, ValidationCode>,
|
||||
validation_outputs_results: HashMap<ParaId, bool>,
|
||||
candidate_pending_availability: HashMap<ParaId, CommittedCandidateReceipt>,
|
||||
candidate_events: Vec<CandidateEvent>,
|
||||
}
|
||||
@@ -237,6 +240,19 @@ mod tests {
|
||||
self.validation_data.get(¶).map(|l| l.clone())
|
||||
}
|
||||
|
||||
fn check_validation_outputs(
|
||||
&self,
|
||||
para_id: ParaId,
|
||||
_commitments: polkadot_primitives::v1::ValidationOutputs,
|
||||
) -> bool {
|
||||
self.validation_outputs_results
|
||||
.get(¶_id)
|
||||
.cloned()
|
||||
.expect(
|
||||
"`check_validation_outputs` called but the expected result hasn't been supplied"
|
||||
)
|
||||
}
|
||||
|
||||
fn session_index_for_child(&self) -> SessionIndex {
|
||||
self.session_index_for_child.clone()
|
||||
}
|
||||
@@ -415,6 +431,60 @@ mod tests {
|
||||
futures::executor::block_on(future::join(subsystem_task, test_task));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requests_check_validation_outputs() {
|
||||
let (ctx, mut ctx_handle) = test_helpers::make_subsystem_context(TaskExecutor::new());
|
||||
let mut runtime_api = MockRuntimeApi::default();
|
||||
let relay_parent = [1; 32].into();
|
||||
let para_a = 5.into();
|
||||
let para_b = 6.into();
|
||||
let commitments = polkadot_primitives::v1::ValidationOutputs::default();
|
||||
|
||||
runtime_api.validation_outputs_results.insert(para_a, false);
|
||||
runtime_api.validation_outputs_results.insert(para_b, true);
|
||||
|
||||
let subsystem = RuntimeApiSubsystem::new(runtime_api.clone(), Metrics(None));
|
||||
let subsystem_task = run(ctx, subsystem).map(|x| x.unwrap());
|
||||
let test_task = async move {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
ctx_handle.send(FromOverseer::Communication {
|
||||
msg: RuntimeApiMessage::Request(
|
||||
relay_parent,
|
||||
Request::CheckValidationOutputs(
|
||||
para_a,
|
||||
commitments.clone(),
|
||||
tx,
|
||||
),
|
||||
)
|
||||
}).await;
|
||||
assert_eq!(
|
||||
rx.await.unwrap().unwrap(),
|
||||
runtime_api.validation_outputs_results[¶_a],
|
||||
);
|
||||
|
||||
let (tx, rx) = oneshot::channel();
|
||||
ctx_handle.send(FromOverseer::Communication {
|
||||
msg: RuntimeApiMessage::Request(
|
||||
relay_parent,
|
||||
Request::CheckValidationOutputs(
|
||||
para_b,
|
||||
commitments,
|
||||
tx,
|
||||
),
|
||||
)
|
||||
}).await;
|
||||
assert_eq!(
|
||||
rx.await.unwrap().unwrap(),
|
||||
runtime_api.validation_outputs_results[¶_b],
|
||||
);
|
||||
|
||||
ctx_handle.send(FromOverseer::Signal(OverseerSignal::Conclude)).await;
|
||||
};
|
||||
|
||||
futures::executor::block_on(future::join(subsystem_task, test_task));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requests_session_index_for_child() {
|
||||
let (ctx, mut ctx_handle) = test_helpers::make_subsystem_context(TaskExecutor::new());
|
||||
|
||||
@@ -26,7 +26,7 @@ use polkadot_primitives::v1::{
|
||||
Hash, CommittedCandidateReceipt, CandidateReceipt, CompactStatement,
|
||||
EncodeAs, Signed, SigningContext, ValidatorIndex, ValidatorId,
|
||||
UpwardMessage, Balance, ValidationCode, PersistedValidationData, ValidationData,
|
||||
HeadData, PoV, CollatorPair, Id as ParaId,
|
||||
HeadData, PoV, CollatorPair, Id as ParaId, ValidationOutputs,
|
||||
};
|
||||
use polkadot_statement_table::{
|
||||
generic::{
|
||||
@@ -114,26 +114,13 @@ pub struct FromTableMisbehavior {
|
||||
pub key: ValidatorId,
|
||||
}
|
||||
|
||||
/// Outputs of validating a candidate.
|
||||
#[derive(Debug)]
|
||||
pub struct ValidationOutputs {
|
||||
/// The head-data produced by validation.
|
||||
pub head_data: HeadData,
|
||||
/// The persisted validation data.
|
||||
pub validation_data: PersistedValidationData,
|
||||
/// Upward messages to the relay chain.
|
||||
pub upward_messages: Vec<UpwardMessage>,
|
||||
/// Fees paid to the validators of the relay-chain.
|
||||
pub fees: Balance,
|
||||
/// The new validation code submitted by the execution, if any.
|
||||
pub new_validation_code: Option<ValidationCode>,
|
||||
}
|
||||
|
||||
/// Candidate invalidity details
|
||||
#[derive(Debug)]
|
||||
pub enum InvalidCandidate {
|
||||
/// Failed to execute.`validate_block`. This includes function panicking.
|
||||
ExecutionError(String),
|
||||
/// Validation outputs check doesn't pass.
|
||||
InvalidOutputs,
|
||||
/// Execution timeout.
|
||||
Timeout,
|
||||
/// Validation input is over the limit.
|
||||
@@ -148,19 +135,14 @@ pub enum InvalidCandidate {
|
||||
HashMismatch,
|
||||
/// Bad collator signature.
|
||||
BadSignature,
|
||||
/// Output code is too large
|
||||
NewCodeTooLarge(u64),
|
||||
/// Head-data is over the limit.
|
||||
HeadDataTooLarge(u64),
|
||||
/// Code upgrade triggered but not allowed.
|
||||
CodeUpgradeNotAllowed,
|
||||
}
|
||||
|
||||
/// Result of the validation of the candidate.
|
||||
#[derive(Debug)]
|
||||
pub enum ValidationResult {
|
||||
/// Candidate is valid. The validation process yields these outputs.
|
||||
Valid(ValidationOutputs),
|
||||
/// Candidate is valid. The validation process yields these outputs and the persisted validation
|
||||
/// data used to form inputs.
|
||||
Valid(ValidationOutputs, PersistedValidationData),
|
||||
/// Candidate is invalid.
|
||||
Invalid(InvalidCandidate),
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ use polkadot_primitives::v1::{
|
||||
CollatorId, CommittedCandidateReceipt, CoreState, ErasureChunk,
|
||||
GroupRotationInfo, Hash, Id as ParaId, OccupiedCoreAssumption,
|
||||
PersistedValidationData, PoV, SessionIndex, SignedAvailabilityBitfield,
|
||||
TransientValidationData, ValidationCode, ValidatorId, ValidationData,
|
||||
ValidationCode, ValidatorId, ValidationData,
|
||||
ValidatorIndex, ValidatorSignature,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
@@ -115,6 +115,8 @@ pub enum CandidateValidationMessage {
|
||||
/// from the runtime API of the chain, based on the `relay_parent`
|
||||
/// of the `CandidateDescriptor`.
|
||||
///
|
||||
/// This will also perform checking of validation outputs against the acceptance criteria.
|
||||
///
|
||||
/// 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(
|
||||
@@ -125,11 +127,14 @@ pub enum CandidateValidationMessage {
|
||||
/// Validate a candidate with provided, exhaustive parameters for validation.
|
||||
///
|
||||
/// Explicitly provide the `PersistedValidationData` and `ValidationCode` so this can do full
|
||||
/// validation without needing to access the state of the relay-chain. Optionally provide the
|
||||
/// `TransientValidationData` for further checks on the outputs.
|
||||
/// validation without needing to access the state of the relay-chain.
|
||||
///
|
||||
/// This request doesn't involve acceptance criteria checking, therefore only useful for the
|
||||
/// cases where the validity of the candidate is established. This is the case for the typical
|
||||
/// use-case: secondary checkers would use this request relying on the full prior checks
|
||||
/// performed by the relay-chain.
|
||||
ValidateFromExhaustive(
|
||||
PersistedValidationData,
|
||||
Option<TransientValidationData>,
|
||||
ValidationCode,
|
||||
CandidateDescriptor,
|
||||
Arc<PoV>,
|
||||
@@ -142,7 +147,7 @@ impl CandidateValidationMessage {
|
||||
pub fn relay_parent(&self) -> Option<Hash> {
|
||||
match self {
|
||||
Self::ValidateFromChainState(_, _, _) => None,
|
||||
Self::ValidateFromExhaustive(_, _, _, _, _, _) => None,
|
||||
Self::ValidateFromExhaustive(_, _, _, _, _) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -402,6 +407,12 @@ pub enum RuntimeApiRequest {
|
||||
OccupiedCoreAssumption,
|
||||
RuntimeApiSender<Option<ValidationData>>,
|
||||
),
|
||||
/// Sends back `true` if the validation outputs pass all acceptance criteria checks.
|
||||
CheckValidationOutputs(
|
||||
ParaId,
|
||||
polkadot_primitives::v1::ValidationOutputs,
|
||||
RuntimeApiSender<bool>,
|
||||
),
|
||||
/// Get the session index that a child of the block will have.
|
||||
SessionIndexForChild(RuntimeApiSender<SessionIndex>),
|
||||
/// Get the validation code for a para, taking the given `OccupiedCoreAssumption`, which
|
||||
|
||||
@@ -303,6 +303,20 @@ pub struct TransientValidationData<N = BlockNumber> {
|
||||
pub code_upgrade_allowed: Option<N>,
|
||||
}
|
||||
|
||||
/// Outputs of validating a candidate.
|
||||
#[derive(Encode, Decode)]
|
||||
#[cfg_attr(feature = "std", derive(Clone, Debug, Default))]
|
||||
pub struct ValidationOutputs {
|
||||
/// The head-data produced by validation.
|
||||
pub head_data: HeadData,
|
||||
/// Upward messages to the relay chain.
|
||||
pub upward_messages: Vec<UpwardMessage>,
|
||||
/// Fees paid to the validators of the relay-chain.
|
||||
pub fees: Balance,
|
||||
/// The new validation code submitted by the execution, if any.
|
||||
pub new_validation_code: Option<ValidationCode>,
|
||||
}
|
||||
|
||||
/// Commitments made in a `CandidateReceipt`. Many of these are outputs of validation.
|
||||
#[derive(PartialEq, Eq, Clone, Encode, Decode)]
|
||||
#[cfg_attr(feature = "std", derive(Debug, Default, Hash))]
|
||||
@@ -666,6 +680,10 @@ sp_api::decl_runtime_apis! {
|
||||
fn persisted_validation_data(para_id: Id, assumption: OccupiedCoreAssumption)
|
||||
-> Option<PersistedValidationData<N>>;
|
||||
|
||||
// TODO: Adding a Runtime API should be backwards compatible... right?
|
||||
/// Checks if the given validation outputs pass the acceptance criteria.
|
||||
fn check_validation_outputs(para_id: Id, outputs: ValidationOutputs) -> bool;
|
||||
|
||||
/// Returns the session index expected at a child of the block.
|
||||
///
|
||||
/// This can be used to instantiate a `SigningContext`.
|
||||
@@ -689,7 +707,7 @@ sp_api::decl_runtime_apis! {
|
||||
fn candidate_events() -> Vec<CandidateEvent<H>>;
|
||||
|
||||
/// Get the `AuthorityDiscoveryId`s corresponding to the given `ValidatorId`s.
|
||||
/// Currently this request is limited to validators in the current session.
|
||||
/// Currently this request is limited to validators in the current session.
|
||||
///
|
||||
/// We assume that every validator runs authority discovery,
|
||||
/// which would allow us to establish point-to-point connection to given validators.
|
||||
|
||||
@@ -24,7 +24,7 @@ Upon receiving a validation request, the first thing the candidate validation su
|
||||
|
||||
### Determining Parameters
|
||||
|
||||
For a [`CandidateValidationMessage`][CVM]`::ValidateFromExhaustive`, these parameters are exhaustively provided. The [`TransientValidationData`](../../types/candidate.md#transientvalidationdata) is optional, and is used to perform further checks on the outputs of validation.
|
||||
For a [`CandidateValidationMessage`][CVM]`::ValidateFromExhaustive`, these parameters are exhaustively provided.
|
||||
|
||||
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.
|
||||
|
||||
@@ -40,9 +40,8 @@ Once we have all parameters, we can spin up a background task to perform the val
|
||||
* 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, if available, we do some final checks on the output using the `TransientValidationData`:
|
||||
* 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.
|
||||
### Checking Validation Outputs
|
||||
|
||||
If we can assume the presence of the relay-chain state (that is, during processing [`CandidateValidationMessage`][CVM]`::ValidateFromChainState`) we can run all the checks that the relay-chain would run at the inclusion time thus confirming that the candidate will be accepted.
|
||||
|
||||
[CVM]: ../../types/overseer-protocol.md#validationrequesttype
|
||||
|
||||
@@ -348,6 +348,12 @@ enum RuntimeApiRequest {
|
||||
OccupiedCoreAssumption,
|
||||
ResponseChannel<Option<ValidationData>>,
|
||||
),
|
||||
/// Sends back `true` if the commitments pass all acceptance criteria checks.
|
||||
CheckValidationOutputs(
|
||||
ParaId,
|
||||
CandidateCommitments,
|
||||
RuntimeApiSender<bool>,
|
||||
),
|
||||
/// Get information about all availability cores.
|
||||
AvailabilityCores(ResponseChannel<Vec<CoreState>>),
|
||||
/// Get a committed candidate receipt for all candidates pending availability.
|
||||
@@ -392,39 +398,52 @@ Various modules request that the [Candidate Validation subsystem](../node/utilit
|
||||
|
||||
/// Result of the validation of the candidate.
|
||||
enum ValidationResult {
|
||||
/// Candidate is valid, and here are the outputs. In practice, this should be a shared type
|
||||
/// so that validation caching can be done.
|
||||
Valid(ValidationOutputs),
|
||||
/// Candidate is valid, and here are the outputs and the validation data used to form inputs.
|
||||
/// In practice, this should be a shared type so that validation caching can be done.
|
||||
Valid(ValidationOutputs, PersistedValidationData),
|
||||
/// Candidate is invalid.
|
||||
Invalid,
|
||||
}
|
||||
|
||||
/// Messages issued to the candidate validation subsystem.
|
||||
/// Messages received by the Validation subsystem.
|
||||
///
|
||||
/// ## Validation Requests
|
||||
///
|
||||
/// Validation requests made to the subsystem should return an error only on internal error.
|
||||
/// Otherwise, they should return either `Ok(ValidationResult::Valid(_))` or `Ok(ValidationResult::Invalid)`.
|
||||
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`.
|
||||
/// Otherwise, they should return either `Ok(ValidationResult::Valid(_))`
|
||||
/// or `Ok(ValidationResult::Invalid)`.
|
||||
#[derive(Debug)]
|
||||
pub enum CandidateValidationMessage {
|
||||
/// Validate a candidate with provided parameters using relay-chain state.
|
||||
///
|
||||
/// This will implicitly attempt to gather the `PersistedValidationData` and `ValidationCode`
|
||||
/// from the runtime API of the chain, based on the `relay_parent`
|
||||
/// of the `CandidateDescriptor`.
|
||||
///
|
||||
/// This will also perform checking of validation outputs against the acceptance criteria.
|
||||
///
|
||||
/// 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 `PersistedValidationData`
|
||||
/// and `ValidationCode` so this can do full validation without needing to access the state of
|
||||
/// the relay-chain. Optionally provide the `TransientValidationData` which will lead to checks
|
||||
/// on the output.
|
||||
ValidateFromChainState(
|
||||
CandidateDescriptor,
|
||||
Arc<PoV>,
|
||||
oneshot::Sender<Result<ValidationResult, ValidationFailed>>,
|
||||
),
|
||||
/// Validate a candidate with provided, exhaustive parameters for validation.
|
||||
///
|
||||
/// Explicitly provide the `PersistedValidationData` and `ValidationCode` so this can do full
|
||||
/// validation without needing to access the state of the relay-chain.
|
||||
///
|
||||
/// This request doesn't involve acceptance criteria checking, therefore only useful for the
|
||||
/// cases where the validity of the candidate is established. This is the case for the typical
|
||||
/// use-case: secondary checkers would use this request relying on the full prior checks
|
||||
/// performed by the relay-chain.
|
||||
ValidateFromExhaustive(
|
||||
PersistedValidationData,
|
||||
Option<TransientValidationData>,
|
||||
ValidationCode,
|
||||
CandidateDescriptor,
|
||||
PoV,
|
||||
ResponseChannel<Result<ValidationResult>>,
|
||||
Arc<PoV>,
|
||||
oneshot::Sender<Result<ValidationResult, ValidationFailed>>,
|
||||
),
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1108,6 +1108,12 @@ sp_api::impl_runtime_apis! {
|
||||
-> Option<PersistedValidationData<BlockNumber>> {
|
||||
None
|
||||
}
|
||||
fn check_validation_outputs(
|
||||
_: Id,
|
||||
_: primitives::v1::ValidationOutputs,
|
||||
) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn session_index_for_child() -> SessionIndex {
|
||||
0
|
||||
|
||||
@@ -131,8 +131,12 @@ decl_error! {
|
||||
WrongCollator,
|
||||
/// Scheduled cores out of order.
|
||||
ScheduledOutOfOrder,
|
||||
/// Head data exceeds the configured maximum.
|
||||
HeadDataTooLarge,
|
||||
/// Code upgrade prematurely.
|
||||
PrematureCodeUpgrade,
|
||||
/// Output code is too large
|
||||
NewCodeTooLarge,
|
||||
/// Candidate not in parent context.
|
||||
CandidateNotInParentContext,
|
||||
/// The bitfield contains a bit relating to an unassigned availability core.
|
||||
@@ -345,9 +349,7 @@ impl<T: Trait> Module<T> {
|
||||
candidates: Vec<BackedCandidate<T::Hash>>,
|
||||
scheduled: Vec<CoreAssignment>,
|
||||
group_validators: impl Fn(GroupIndex) -> Option<Vec<ValidatorIndex>>,
|
||||
)
|
||||
-> Result<Vec<CoreIndex>, DispatchError>
|
||||
{
|
||||
) -> Result<Vec<CoreIndex>, DispatchError> {
|
||||
ensure!(candidates.len() <= scheduled.len(), Error::<T>::UnscheduledCandidate);
|
||||
|
||||
if scheduled.is_empty() {
|
||||
@@ -356,9 +358,7 @@ impl<T: Trait> Module<T> {
|
||||
|
||||
let validators = Validators::get();
|
||||
let parent_hash = <frame_system::Module<T>>::parent_hash();
|
||||
let config = <configuration::Module<T>>::config();
|
||||
let now = <frame_system::Module<T>>::block_number();
|
||||
let relay_parent_number = now - One::one();
|
||||
let check_cx = CandidateCheckContext::<T>::new();
|
||||
|
||||
// do all checks before writing storage.
|
||||
let core_indices = {
|
||||
@@ -400,27 +400,17 @@ impl<T: Trait> Module<T> {
|
||||
candidate.descriptor().relay_parent == parent_hash,
|
||||
Error::<T>::CandidateNotInParentContext,
|
||||
);
|
||||
|
||||
// if any, the code upgrade attempt is allowed.
|
||||
let valid_upgrade_attempt =
|
||||
candidate.candidate.commitments.new_validation_code.is_none() ||
|
||||
<paras::Module<T>>::last_code_upgrade(para_id, true)
|
||||
.map_or(
|
||||
true,
|
||||
|last| last <= relay_parent_number &&
|
||||
relay_parent_number.saturating_sub(last)
|
||||
>= config.validation_upgrade_frequency,
|
||||
);
|
||||
|
||||
ensure!(
|
||||
valid_upgrade_attempt,
|
||||
Error::<T>::PrematureCodeUpgrade,
|
||||
);
|
||||
ensure!(
|
||||
candidate.descriptor().check_collator_signature().is_ok(),
|
||||
Error::<T>::NotCollatorSigned,
|
||||
);
|
||||
|
||||
check_cx.check_validation_outputs(
|
||||
para_id,
|
||||
&candidate.candidate.commitments.head_data,
|
||||
&candidate.candidate.commitments.new_validation_code,
|
||||
)?;
|
||||
|
||||
for (i, assignment) in scheduled[skip..].iter().enumerate() {
|
||||
check_assignment_in_order(assignment)?;
|
||||
|
||||
@@ -498,7 +488,7 @@ impl<T: Trait> Module<T> {
|
||||
false,
|
||||
Error::<T>::UnscheduledCandidate,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// check remainder of scheduled cores, if any.
|
||||
for assignment in scheduled[skip..].iter() {
|
||||
@@ -530,8 +520,8 @@ impl<T: Trait> Module<T> {
|
||||
core,
|
||||
descriptor,
|
||||
availability_votes,
|
||||
relay_parent_number,
|
||||
backed_in_number: now,
|
||||
relay_parent_number: check_cx.relay_parent_number,
|
||||
backed_in_number: check_cx.now,
|
||||
});
|
||||
<PendingAvailabilityCommitments>::insert(¶_id, commitments);
|
||||
}
|
||||
@@ -539,6 +529,20 @@ impl<T: Trait> Module<T> {
|
||||
Ok(core_indices)
|
||||
}
|
||||
|
||||
/// Run the acceptance criteria checks on the given candidate commitments.
|
||||
///
|
||||
/// Returns an 'Err` if any of the checks doesn't pass.
|
||||
pub(crate) fn check_validation_outputs(
|
||||
para_id: ParaId,
|
||||
validation_outputs: primitives::v1::ValidationOutputs,
|
||||
) -> Result<(), DispatchError> {
|
||||
CandidateCheckContext::<T>::new().check_validation_outputs(
|
||||
para_id,
|
||||
&validation_outputs.head_data,
|
||||
&validation_outputs.new_validation_code,
|
||||
)
|
||||
}
|
||||
|
||||
fn enact_candidate(
|
||||
relay_parent_number: T::BlockNumber,
|
||||
receipt: CommittedCandidateReceipt<T::Hash>,
|
||||
@@ -654,6 +658,57 @@ const fn availability_threshold(n_validators: usize) -> usize {
|
||||
threshold
|
||||
}
|
||||
|
||||
/// A collection of data required for checking a candidate.
|
||||
struct CandidateCheckContext<T: Trait> {
|
||||
config: configuration::HostConfiguration<T::BlockNumber>,
|
||||
now: T::BlockNumber,
|
||||
relay_parent_number: T::BlockNumber,
|
||||
}
|
||||
|
||||
impl<T: Trait> CandidateCheckContext<T> {
|
||||
fn new() -> Self {
|
||||
let now = <frame_system::Module<T>>::block_number();
|
||||
Self {
|
||||
config: <configuration::Module<T>>::config(),
|
||||
now,
|
||||
relay_parent_number: now - One::one(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check the given outputs after candidate validation on whether it passes the acceptance
|
||||
/// criteria.
|
||||
fn check_validation_outputs(
|
||||
&self,
|
||||
para_id: ParaId,
|
||||
head_data: &HeadData,
|
||||
new_validation_code: &Option<primitives::v1::ValidationCode>,
|
||||
) -> Result<(), DispatchError> {
|
||||
ensure!(
|
||||
head_data.0.len() <= self.config.max_head_data_size as _,
|
||||
Error::<T>::HeadDataTooLarge
|
||||
);
|
||||
|
||||
// if any, the code upgrade attempt is allowed.
|
||||
if let Some(new_validation_code) = new_validation_code {
|
||||
let valid_upgrade_attempt = <paras::Module<T>>::last_code_upgrade(para_id, true)
|
||||
.map_or(true, |last| {
|
||||
last <= self.relay_parent_number
|
||||
&& self.relay_parent_number.saturating_sub(last)
|
||||
>= self.config.validation_upgrade_frequency
|
||||
});
|
||||
ensure!(valid_upgrade_attempt, Error::<T>::PrematureCodeUpgrade);
|
||||
ensure!(
|
||||
new_validation_code.0.len() <= self.config.max_code_size as _,
|
||||
Error::<T>::NewCodeTooLarge
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: messaging acceptance criteria rules will go here.
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -682,6 +737,7 @@ mod tests {
|
||||
fn default_config() -> HostConfiguration<BlockNumber> {
|
||||
let mut config = HostConfiguration::default();
|
||||
config.parathread_cores = 1;
|
||||
config.max_code_size = 3;
|
||||
config
|
||||
}
|
||||
|
||||
|
||||
@@ -216,6 +216,15 @@ pub fn persisted_validation_data<T: initializer::Trait>(
|
||||
)
|
||||
}
|
||||
|
||||
/// Implementation for the `check_validation_outputs` function of the runtime API.
|
||||
pub fn check_validation_outputs<T: initializer::Trait>(
|
||||
para_id: ParaId,
|
||||
outputs: primitives::v1::ValidationOutputs,
|
||||
) -> bool {
|
||||
// we strip detailed information from error here for the sake of simplicity of runtime API.
|
||||
<inclusion::Module<T>>::check_validation_outputs(para_id, outputs).is_ok()
|
||||
}
|
||||
|
||||
/// Implementation for the `session_index_for_child` function of the runtime API.
|
||||
pub fn session_index_for_child<T: initializer::Trait>() -> SessionIndex {
|
||||
// Just returns the session index from `inclusion`. Runtime APIs follow
|
||||
@@ -268,7 +277,7 @@ where
|
||||
}
|
||||
|
||||
/// Get the `AuthorityDiscoveryId`s corresponding to the given `ValidatorId`s.
|
||||
/// Currently this request is limited to validators in the current session.
|
||||
/// Currently this request is limited to validators in the current session.
|
||||
///
|
||||
/// We assume that every validator runs authority discovery,
|
||||
/// which would allow us to establish point-to-point connection to given validators.
|
||||
|
||||
@@ -1105,6 +1105,10 @@ sp_api::impl_runtime_apis! {
|
||||
None
|
||||
}
|
||||
|
||||
fn check_validation_outputs(_: Id, _: primitives::v1::ValidationOutputs) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn session_index_for_child() -> SessionIndex {
|
||||
0
|
||||
}
|
||||
|
||||
@@ -191,6 +191,13 @@ sp_api::impl_runtime_apis! {
|
||||
runtime_api_impl::persisted_validation_data::<Runtime>(para_id, assumption)
|
||||
}
|
||||
|
||||
fn check_validation_outputs(
|
||||
para_id: Id,
|
||||
outputs: primitives::v1::ValidationOutputs,
|
||||
) -> bool {
|
||||
runtime_api_impl::check_validation_outputs::<Runtime>(para_id, outputs)
|
||||
}
|
||||
|
||||
fn session_index_for_child() -> SessionIndex {
|
||||
runtime_api_impl::session_index_for_child::<Runtime>()
|
||||
}
|
||||
|
||||
@@ -620,6 +620,13 @@ sp_api::impl_runtime_apis! {
|
||||
runtime_impl::persisted_validation_data::<Runtime>(para_id, assumption)
|
||||
}
|
||||
|
||||
fn check_validation_outputs(
|
||||
para_id: ParaId,
|
||||
outputs: primitives::v1::ValidationOutputs,
|
||||
) -> bool {
|
||||
runtime_impl::check_validation_outputs::<Runtime>(para_id, outputs)
|
||||
}
|
||||
|
||||
fn session_index_for_child() -> SessionIndex {
|
||||
runtime_impl::session_index_for_child::<Runtime>()
|
||||
}
|
||||
|
||||
@@ -821,6 +821,13 @@ sp_api::impl_runtime_apis! {
|
||||
None
|
||||
}
|
||||
|
||||
fn check_validation_outputs(
|
||||
_: Id,
|
||||
_: primitives::v1::ValidationOutputs
|
||||
) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn session_index_for_child() -> SessionIndex {
|
||||
0
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user