Unverified Commit 4a17a2bc authored by Sergey Pepyakin's avatar Sergey Pepyakin Committed by GitHub
Browse files

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.
parent 8f882afd
Pipeline #111715 passed with stages
in 22 minutes and 47 seconds
...@@ -32,11 +32,10 @@ use polkadot_primitives::v1::{ ...@@ -32,11 +32,10 @@ use polkadot_primitives::v1::{
CommittedCandidateReceipt, BackedCandidate, Id as ParaId, ValidatorId, CommittedCandidateReceipt, BackedCandidate, Id as ParaId, ValidatorId,
ValidatorIndex, SigningContext, PoV, ValidatorIndex, SigningContext, PoV,
CandidateDescriptor, AvailableData, ValidatorSignature, Hash, CandidateReceipt, CandidateDescriptor, AvailableData, ValidatorSignature, Hash, CandidateReceipt,
CandidateCommitments, CoreState, CoreIndex, CollatorId, CandidateCommitments, CoreState, CoreIndex, CollatorId, ValidationOutputs,
}; };
use polkadot_node_primitives::{ use polkadot_node_primitives::{
FromTableMisbehavior, Statement, SignedFullStatement, MisbehaviorReport, FromTableMisbehavior, Statement, SignedFullStatement, MisbehaviorReport, ValidationResult,
ValidationOutputs, ValidationResult,
}; };
use polkadot_subsystem::{ use polkadot_subsystem::{
messages::{ messages::{
...@@ -287,7 +286,7 @@ impl CandidateBackingJob { ...@@ -287,7 +286,7 @@ impl CandidateBackingJob {
let candidate_hash = candidate.hash(); let candidate_hash = candidate.hash();
let statement = match valid { let statement = match valid {
ValidationResult::Valid(outputs) => { ValidationResult::Valid(outputs, validation_data) => {
// make PoV available for later distribution. Send data to the availability // make PoV available for later distribution. Send data to the availability
// store to keep. Sign and dispatch `valid` statement to network if we // store to keep. Sign and dispatch `valid` statement to network if we
// have not seconded the given candidate. // have not seconded the given candidate.
...@@ -296,6 +295,7 @@ impl CandidateBackingJob { ...@@ -296,6 +295,7 @@ impl CandidateBackingJob {
// the collator, do not make available and report the collator. // the collator, do not make available and report the collator.
let commitments_check = self.make_pov_available( let commitments_check = self.make_pov_available(
pov, pov,
validation_data,
outputs, outputs,
|commitments| if commitments.hash() == candidate.commitments_hash { |commitments| if commitments.hash() == candidate.commitments_hash {
Ok(CommittedCandidateReceipt { Ok(CommittedCandidateReceipt {
...@@ -510,10 +510,11 @@ impl CandidateBackingJob { ...@@ -510,10 +510,11 @@ impl CandidateBackingJob {
let v = self.request_candidate_validation(descriptor, pov.clone()).await?; let v = self.request_candidate_validation(descriptor, pov.clone()).await?;
let statement = match v { 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. // If validation produces a new set of commitments, we vote the candidate as invalid.
let commitments_check = self.make_pov_available( let commitments_check = self.make_pov_available(
(&*pov).clone(), (&*pov).clone(),
validation_data,
outputs, outputs,
|commitments| if commitments == expected_commitments { |commitments| if commitments == expected_commitments {
Ok(()) Ok(())
...@@ -652,12 +653,13 @@ impl CandidateBackingJob { ...@@ -652,12 +653,13 @@ impl CandidateBackingJob {
async fn make_pov_available<T, E>( async fn make_pov_available<T, E>(
&mut self, &mut self,
pov: PoV, pov: PoV,
validation_data: polkadot_primitives::v1::PersistedValidationData,
outputs: ValidationOutputs, outputs: ValidationOutputs,
with_commitments: impl FnOnce(CandidateCommitments) -> Result<T, E>, with_commitments: impl FnOnce(CandidateCommitments) -> Result<T, E>,
) -> Result<Result<T, E>, Error> { ) -> Result<Result<T, E>, Error> {
let available_data = AvailableData { let available_data = AvailableData {
pov, pov,
validation_data: outputs.validation_data, validation_data,
}; };
let chunks = erasure_coding::obtain_chunks_v1( let chunks = erasure_coding::obtain_chunks_v1(
...@@ -1147,12 +1149,11 @@ mod tests { ...@@ -1147,12 +1149,11 @@ mod tests {
) if pov == pov && &c == candidate.descriptor() => { ) if pov == pov && &c == candidate.descriptor() => {
tx.send(Ok( tx.send(Ok(
ValidationResult::Valid(ValidationOutputs { ValidationResult::Valid(ValidationOutputs {
validation_data: test_state.validation_data.persisted,
head_data: expected_head_data.clone(), head_data: expected_head_data.clone(),
upward_messages: Vec::new(), upward_messages: Vec::new(),
fees: Default::default(), fees: Default::default(),
new_validation_code: None, new_validation_code: None,
}), }, test_state.validation_data.persisted),
)).unwrap(); )).unwrap();
} }
); );
...@@ -1267,12 +1268,11 @@ mod tests { ...@@ -1267,12 +1268,11 @@ mod tests {
) if pov == pov && &c == candidate_a.descriptor() => { ) if pov == pov && &c == candidate_a.descriptor() => {
tx.send(Ok( tx.send(Ok(
ValidationResult::Valid(ValidationOutputs { ValidationResult::Valid(ValidationOutputs {
validation_data: test_state.validation_data.persisted,
head_data: expected_head_data.clone(), head_data: expected_head_data.clone(),
upward_messages: Vec::new(), upward_messages: Vec::new(),
fees: Default::default(), fees: Default::default(),
new_validation_code: None, new_validation_code: None,
}), }, test_state.validation_data.persisted),
)).unwrap(); )).unwrap();
} }
); );
...@@ -1406,12 +1406,11 @@ mod tests { ...@@ -1406,12 +1406,11 @@ mod tests {
) if pov == pov && &c == candidate_a.descriptor() => { ) if pov == pov && &c == candidate_a.descriptor() => {
tx.send(Ok( tx.send(Ok(
ValidationResult::Valid(ValidationOutputs { ValidationResult::Valid(ValidationOutputs {
validation_data: test_state.validation_data.persisted,
head_data: expected_head_data.clone(), head_data: expected_head_data.clone(),
upward_messages: Vec::new(), upward_messages: Vec::new(),
fees: Default::default(), fees: Default::default(),
new_validation_code: None, new_validation_code: None,
}), }, test_state.validation_data.persisted),
)).unwrap(); )).unwrap();
} }
); );
...@@ -1562,12 +1561,11 @@ mod tests { ...@@ -1562,12 +1561,11 @@ mod tests {
) if pov == pov && &c == candidate_b.descriptor() => { ) if pov == pov && &c == candidate_b.descriptor() => {
tx.send(Ok( tx.send(Ok(
ValidationResult::Valid(ValidationOutputs { ValidationResult::Valid(ValidationOutputs {
validation_data: test_state.validation_data.persisted,
head_data: expected_head_data.clone(), head_data: expected_head_data.clone(),
upward_messages: Vec::new(), upward_messages: Vec::new(),
fees: Default::default(), fees: Default::default(),
new_validation_code: None, new_validation_code: None,
}), }, test_state.validation_data.persisted),
)).unwrap(); )).unwrap();
} }
); );
......
...@@ -343,7 +343,10 @@ async fn candidate_is_valid_inner( ...@@ -343,7 +343,10 @@ async fn candidate_is_valid_inner(
CandidateValidationMessage::ValidateFromChainState(candidate_descriptor, pov, tx), CandidateValidationMessage::ValidateFromChainState(candidate_descriptor, pov, tx),
)) ))
.await?; .await?;
Ok(std::matches!(rx.await, Ok(Ok(ValidationResult::Valid(_))))) Ok(std::matches!(
rx.await,
Ok(Ok(ValidationResult::Valid(_, _)))
))
} }
async fn second_candidate( async fn second_candidate(
...@@ -445,8 +448,7 @@ delegated_subsystem!(CandidateSelectionJob((), Metrics) <- ToJob as CandidateSel ...@@ -445,8 +448,7 @@ delegated_subsystem!(CandidateSelectionJob((), Metrics) <- ToJob as CandidateSel
mod tests { mod tests {
use super::*; use super::*;
use futures::lock::Mutex; use futures::lock::Mutex;
use polkadot_node_primitives::ValidationOutputs; use polkadot_primitives::v1::{BlockData, HeadData, PersistedValidationData, ValidationOutputs};
use polkadot_primitives::v1::{BlockData, HeadData, PersistedValidationData};
use sp_core::crypto::Public; use sp_core::crypto::Public;
fn test_harness<Preconditions, TestBuilder, Test, Postconditions>( fn test_harness<Preconditions, TestBuilder, Test, Postconditions>(
...@@ -478,7 +480,7 @@ mod tests { ...@@ -478,7 +480,7 @@ mod tests {
postconditions(job, job_result); 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 head_data: Vec<u8> = (0..32).rev().cycle().take(256).collect();
let parent_head_data = head_data let parent_head_data = head_data
.iter() .iter()
...@@ -486,17 +488,19 @@ mod tests { ...@@ -486,17 +488,19 @@ mod tests {
.map(|x| x.saturating_sub(1)) .map(|x| x.saturating_sub(1))
.collect(); .collect();
ValidationOutputs { (
head_data: HeadData(head_data), ValidationOutputs {
validation_data: PersistedValidationData { head_data: HeadData(head_data),
upward_messages: Vec::new(),
fees: 0,
new_validation_code: None,
},
PersistedValidationData {
parent_head: HeadData(parent_head_data), parent_head: HeadData(parent_head_data),
block_number: 123, block_number: 123,
hrmp_mqc_heads: Vec::new(), 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 /// when nothing is seconded so far, the collation is fetched and seconded
...@@ -556,8 +560,9 @@ mod tests { ...@@ -556,8 +560,9 @@ mod tests {
assert_eq!(got_candidate_descriptor, candidate_receipt.descriptor); assert_eq!(got_candidate_descriptor, candidate_receipt.descriptor);
assert_eq!(got_pov.as_ref(), &pov); assert_eq!(got_pov.as_ref(), &pov);
let (outputs, data) = default_validation_outputs_and_data();
return_sender return_sender
.send(Ok(ValidationResult::Valid(default_validation_outputs()))) .send(Ok(ValidationResult::Valid(outputs, data)))
.unwrap(); .unwrap();
} }
FromJob::Backing(CandidateBackingMessage::Second( FromJob::Backing(CandidateBackingMessage::Second(
......
...@@ -32,10 +32,10 @@ use polkadot_node_subsystem_util::{ ...@@ -32,10 +32,10 @@ use polkadot_node_subsystem_util::{
metrics::{self, prometheus}, metrics::{self, prometheus},
}; };
use polkadot_subsystem::errors::RuntimeApiError; use polkadot_subsystem::errors::RuntimeApiError;
use polkadot_node_primitives::{ValidationResult, ValidationOutputs, InvalidCandidate}; use polkadot_node_primitives::{ValidationResult, InvalidCandidate};
use polkadot_primitives::v1::{ use polkadot_primitives::v1::{
ValidationCode, PoV, CandidateDescriptor, ValidationData, PersistedValidationData, ValidationCode, PoV, CandidateDescriptor, PersistedValidationData,
TransientValidationData, OccupiedCoreAssumption, Hash, OccupiedCoreAssumption, Hash, ValidationOutputs,
}; };
use polkadot_parachain::wasm_executor::{ use polkadot_parachain::wasm_executor::{
self, ValidationPool, ExecutionMode, ValidationError, self, ValidationPool, ExecutionMode, ValidationError,
...@@ -72,7 +72,7 @@ impl Metrics { ...@@ -72,7 +72,7 @@ impl Metrics {
fn on_validation_event(&self, event: &Result<ValidationResult, ValidationFailed>) { fn on_validation_event(&self, event: &Result<ValidationResult, ValidationFailed>) {
if let Some(metrics) = &self.0 { if let Some(metrics) = &self.0 {
match event { match event {
Ok(ValidationResult::Valid(_)) => { Ok(ValidationResult::Valid(_, _)) => {
metrics.validation_requests.with_label_values(&["valid"]).inc(); metrics.validation_requests.with_label_values(&["valid"]).inc();
}, },
Ok(ValidationResult::Invalid(_)) => { Ok(ValidationResult::Invalid(_)) => {
...@@ -161,7 +161,6 @@ async fn run( ...@@ -161,7 +161,6 @@ async fn run(
} }
CandidateValidationMessage::ValidateFromExhaustive( CandidateValidationMessage::ValidateFromExhaustive(
persisted_validation_data, persisted_validation_data,
transient_validation_data,
validation_code, validation_code,
descriptor, descriptor,
pov, pov,
...@@ -171,7 +170,6 @@ async fn run( ...@@ -171,7 +170,6 @@ async fn run(
&mut ctx, &mut ctx,
execution_mode.clone(), execution_mode.clone(),
persisted_validation_data, persisted_validation_data,
transient_validation_data,
validation_code, validation_code,
descriptor, descriptor,
pov, pov,
...@@ -214,7 +212,7 @@ async fn runtime_api_request<T>( ...@@ -214,7 +212,7 @@ async fn runtime_api_request<T>(
#[derive(Debug)] #[derive(Debug)]
enum AssumptionCheckOutcome { enum AssumptionCheckOutcome {
Matches(ValidationData, ValidationCode), Matches(PersistedValidationData, ValidationCode),
DoesNotMatch, DoesNotMatch,
BadRequest, BadRequest,
} }
...@@ -229,7 +227,7 @@ async fn check_assumption_validation_data( ...@@ -229,7 +227,7 @@ async fn check_assumption_validation_data(
let d = runtime_api_request( let d = runtime_api_request(
ctx, ctx,
descriptor.relay_parent, descriptor.relay_parent,
RuntimeApiRequest::FullValidationData( RuntimeApiRequest::PersistedValidationData(
descriptor.para_id, descriptor.para_id,
assumption, assumption,
tx, tx,
...@@ -245,7 +243,7 @@ async fn check_assumption_validation_data( ...@@ -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 { SubsystemResult::Ok(if descriptor.persisted_validation_data_hash == persisted_validation_data_hash {
let (code_tx, code_rx) = oneshot::channel(); let (code_tx, code_rx) = oneshot::channel();
...@@ -269,70 +267,100 @@ async fn check_assumption_validation_data( ...@@ -269,70 +267,100 @@ async fn check_assumption_validation_data(
}) })
} }
async fn spawn_validate_from_chain_state( async fn find_assumed_validation_data(
ctx: &mut impl SubsystemContext<Message = CandidateValidationMessage>, ctx: &mut impl SubsystemContext<Message = CandidateValidationMessage>,
execution_mode: ExecutionMode, descriptor: &CandidateDescriptor,
descriptor: CandidateDescriptor, ) -> SubsystemResult<AssumptionCheckOutcome> {
pov: Arc<PoV>,
spawn: impl SpawnNamed + 'static,
) -> SubsystemResult<Result<ValidationResult, ValidationFailed>> {
// The candidate descriptor has a `persisted_validation_data_hash` which corresponds to // 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 // 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 // relay-parent. We can fetch these values by getting the persisted validation data
// based on the different `OccupiedCoreAssumption`s. // based on the different `OccupiedCoreAssumption`s.
match check_assumption_validation_data(
ctx, const ASSUMPTIONS: &[OccupiedCoreAssumption] = &[
&descriptor,
OccupiedCoreAssumption::Included, OccupiedCoreAssumption::Included,
).await? { OccupiedCoreAssumption::TimedOut,
AssumptionCheckOutcome::Matches(validation_data, validation_code) => { // TODO: Why don't we check `Free`? The guide assumes there are only two possible assumptions.
return spawn_validate_exhaustive( //
ctx, // Source that info and leave a comment here.
execution_mode, ];
validation_data.persisted,
Some(validation_data.transient), // Consider running these checks in parallel to reduce validation latency.
validation_code, for assumption in ASSUMPTIONS {
descriptor, let outcome = check_assumption_validation_data(ctx, descriptor, *assumption).await?;
pov,
spawn, let () = match outcome {
).await; AssumptionCheckOutcome::Matches(_, _) => return Ok(outcome),
} AssumptionCheckOutcome::BadRequest => return Ok(outcome),
AssumptionCheckOutcome::DoesNotMatch => {}, AssumptionCheckOutcome::DoesNotMatch => continue,
AssumptionCheckOutcome::BadRequest => return Ok(Err(ValidationFailed("Bad request".into()))), };
} }
match check_assumption_validation_data( Ok(AssumptionCheckOutcome::DoesNotMatch)
}
async fn spawn_validate_from_chain_state(
ctx: &mut impl SubsystemContext<Message = CandidateValidationMessage>,
execution_mode: ExecutionMode,
descriptor: CandidateDescriptor,
pov: Arc<PoV>,
spawn: impl SpawnNamed + 'static,
) -> SubsystemResult<Result<ValidationResult, ValidationFailed>> {
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, ctx,
&descriptor, execution_mode,
OccupiedCoreAssumption::TimedOut, validation_data,
).await? { validation_code,
AssumptionCheckOutcome::Matches(validation_data, validation_code) => { descriptor.clone(),
return spawn_validate_exhaustive( pov,
ctx, spawn,
execution_mode, )
validation_data.persisted, .await;
Some(validation_data.transient),
validation_code, if let Ok(Ok(ValidationResult::Valid(ref outputs, _))) = validation_result {
descriptor, let (tx, rx) = oneshot::channel();
pov, match runtime_api_request(
spawn, ctx,
).await; 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()))),
} }
// If neither the assumption of the occupied core having the para included or the assumption validation_result
// 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)))
} }
async fn spawn_validate_exhaustive( async fn spawn_validate_exhaustive(
ctx: &mut impl SubsystemContext<Message = CandidateValidationMessage>, ctx: &mut impl SubsystemContext<Message = CandidateValidationMessage>,
execution_mode: ExecutionMode, execution_mode: ExecutionMode,
persisted_validation_data: PersistedValidationData, persisted_validation_data: PersistedValidationData,
transient_validation_data: Option<TransientValidationData>,
validation_code: ValidationCode, validation_code: ValidationCode,
descriptor: CandidateDescriptor, descriptor: CandidateDescriptor,
pov: Arc<PoV>, pov: Arc<PoV>,
...@@ -343,7 +371,6 @@ async fn spawn_validate_exhaustive( ...@@ -343,7 +371,6 @@ async fn spawn_validate_exhaustive(
let res = validate_candidate_exhaustive::<RealValidationBackend, _>( let res = validate_candidate_exhaustive::<RealValidationBackend, _>(
execution_mode, execution_mode,
persisted_validation_data, persisted_validation_data,
transient_validation_data,
validation_code, validation_code,
descriptor, descriptor,
pov, pov,
...@@ -384,30 +411,6 @@ fn perform_basic_checks( ...@@ -384,30 +411,6 @@ fn perform_basic_checks(
Ok(()) 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 { trait ValidationBackend {
type Arg; type Arg;
...@@ -445,7 +448,6 @@ impl ValidationBackend for RealValidationBackend { ...@@ -445,7 +448,6 @@ impl ValidationBackend for RealValidationBackend {
fn validate_candidate_exhaustive<B: ValidationBackend, S: SpawnNamed + 'static>( fn validate_candidate_exhaustive<B: ValidationBackend, S: SpawnNamed + 'static>(
backend_arg: B::Arg, backend_arg: B::Arg,
persisted_validation_data: PersistedValidationData, persisted_validation_data: PersistedValidationData,
transient_validation_data: Option<TransientValidationData>,
validation_code: ValidationCode, validation_code: ValidationCode,
descriptor: CandidateDescriptor, descriptor: CandidateDescriptor,
pov: Arc<PoV>, pov: Arc<PoV>,
...@@ -477,25 +479,13 @@ fn validate_candidate_exhaustive<B: ValidationBackend, S: SpawnNamed + 'static>( ...@@ -477,25 +479,13 @@ fn validate_candidate_exhaustive<B: ValidationBackend, S: SpawnNamed + 'static>(
Ok(ValidationResult::Invalid(InvalidCandidate::ExecutionError(e.to_string()))), Ok(ValidationResult::Invalid(InvalidCandidate::ExecutionError(e.to_string()))),
Err(ValidationError::Internal(e)) => Err(ValidationFailed(e.to_string())), Err(ValidationError::Internal(e)) => Err(ValidationFailed(e.to_string())),
Ok(res) => { Ok(res) => {
let post_check_result = if let Some(transient) = transient_validation_data { let outputs = ValidationOutputs {
check_wasm_result_against_constraints( head_data: res.head_data,
&transient, upward_messages: res.upward_messages,
&res,