Inform the PVF with the latest relevant relay chain state (#279)

* Update polkadot

* Extend cumulus primitives with some relay chain exports

Follow https://github.com/paritytech/polkadot/pull/2194 to see the
polkadot PR

* collator: collect the state proof

This commit changes cumulus-collator so that it takes the relay chain
state at the relay parent and creates a storage proof that contains all
the required data for PVF.

* parachain-upgrade: use the proofs instead

This change is needed to make cumulus logic to not longer depend on the
transient validation data. As part of this change, in order to preserve
the current behavior `code_upgrade_allowed` now is computed on the
parachain side, rather than provided by polkadot.

Turned out that this requires to know the self parachain id so it was
added where needed.

* message-broker: use relay state to track limits

this should make sending messages safe from accidentally running over
the relay chain limits that were previously unknown.

* Update polkadot

So that `relay_storage_root` is available through `ValidationParams`

* Check `relay_storage_root` matches expected

Check that `relay_storage_root` submitted by the collator matches the
one that we receive in `validate_block` through `ValidationParams`

* Add a missing check for `dmq_mqc_head` while we are at it

* Update polkadot

* Fix tests that use the relay storage root

* Apply suggestions from code review

Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>

* Update message-broker/src/lib.rs

Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>

* Remove unneeded (&_)

* Fix unwraps

* Polish basti's suggestion

* Fix merge

* Bring back the System::can_set_code check

Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>
This commit is contained in:
Sergei Shulepov
2021-01-13 14:40:26 +01:00
committed by GitHub
parent 322eda8f69
commit 464e54affd
19 changed files with 793 additions and 136 deletions
+5
View File
@@ -24,15 +24,18 @@ sp-runtime = { git = "https://github.com/paritytech/substrate", default-features
sp-version = { git = "https://github.com/paritytech/substrate", default-features = false , branch = "master" }
frame-system = { git = "https://github.com/paritytech/substrate", default-features = false , branch = "master" }
sp-state-machine = { git = "https://github.com/paritytech/substrate", default-features = false , branch = "master" }
sp-trie = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "master" }
# Other Dependencies
codec = { package = "parity-scale-codec", version = "1.0.0", default-features = false, features = ["derive"]}
serde = { version = "1.0.101", optional = true, features = ["derive"] }
hash-db = { version = "0.15.2", default-features = false }
[dev-dependencies]
sp-externalities = { git = "https://github.com/paritytech/substrate", default-features = false , branch = "master" }
substrate-test-runtime-client = { git = "https://github.com/paritytech/substrate", default-features = false , branch = "master" }
sp-version = { git = "https://github.com/paritytech/substrate", default-features = false , branch = "master" }
cumulus-test-relay-sproof-builder = { path = "../test/relay-sproof-builder" }
[features]
default = ['std']
@@ -46,7 +49,9 @@ std = [
'sp-runtime/std',
'sp-io/std',
'sp-std/std',
'hash-db/std',
'sp-state-machine/std',
'sp-trie/std',
'frame-system/std',
'cumulus-primitives/std',
]
+122 -65
View File
@@ -31,12 +31,12 @@
use cumulus_primitives::{
inherents::{ValidationDataType, VALIDATION_DATA_IDENTIFIER as INHERENT_IDENTIFIER},
well_known_keys::{NEW_VALIDATION_CODE, VALIDATION_DATA},
OnValidationData, ValidationData,
well_known_keys::{NEW_VALIDATION_CODE, VALIDATION_DATA}, AbridgedHostConfiguration,
OnValidationData, ValidationData, ParaId, relay_chain,
};
use frame_support::{
decl_error, decl_event, decl_module, decl_storage, ensure, storage,
weights::{DispatchClass, Weight},
weights::{DispatchClass, Weight}, dispatch::DispatchResult, traits::Get,
};
use frame_system::{ensure_none, ensure_root};
use parachain::primitives::RelayChainBlockNumber;
@@ -44,7 +44,9 @@ use sp_core::storage::well_known_keys;
use sp_inherents::{InherentData, InherentIdentifier, ProvideInherent};
use sp_std::vec::Vec;
type System<T> = frame_system::Module<T>;
mod relay_state_snapshot;
pub use relay_state_snapshot::MessagingStateSnapshot;
/// The pallet's configuration trait.
pub trait Config: frame_system::Config {
@@ -53,6 +55,9 @@ pub trait Config: frame_system::Config {
/// Something which can be notified when the validation data is set.
type OnValidationData: OnValidationData;
/// Returns the parachain ID we are running with.
type SelfParaId: Get<ParaId>;
}
// This pallet's storage items.
@@ -68,6 +73,25 @@ decl_storage! {
/// Were the validation data set to notify the relay chain?
DidSetValidationCode: bool;
/// The last relay parent block number at which we signalled the code upgrade.
LastUpgrade: relay_chain::BlockNumber;
/// The snapshot of some state related to messaging relevant to the current parachain as per
/// the relay parent.
///
/// This field is meant to be updated each block with the validation data inherent. Therefore,
/// before processing of the inherent, e.g. in `on_initialize` this data may be stale.
///
/// This data is also absent from the genesis.
RelevantMessagingState get(fn relevant_messaging_state): Option<MessagingStateSnapshot>;
/// The parachain host configuration that was obtained from the relay parent.
///
/// This field is meant to be updated each block with the validation data inherent. Therefore,
/// before processing of the inherent, e.g. in `on_initialize` this data may be stale.
///
/// This data is also absent from the genesis.
HostConfiguration get(fn host_configuration): Option<AbridgedHostConfiguration>;
}
}
@@ -82,7 +106,7 @@ decl_module! {
#[weight = (0, DispatchClass::Operational)]
pub fn schedule_upgrade(origin, validation_function: Vec<u8>) {
ensure_root(origin)?;
System::<T>::can_set_code(&validation_function)?;
<frame_system::Module<T>>::can_set_code(&validation_function)?;
Self::schedule_upgrade_impl(validation_function)?;
}
@@ -106,7 +130,7 @@ decl_module! {
/// As a side effect, this function upgrades the current validation function
/// if the appropriate time has come.
#[weight = (0, DispatchClass::Mandatory)]
fn set_validation_data(origin, data: ValidationDataType) {
fn set_validation_data(origin, data: ValidationDataType) -> DispatchResult {
ensure_none(origin)?;
assert!(!DidUpdateValidationData::exists(), "ValidationData must be updated only once in a block");
@@ -121,18 +145,31 @@ decl_module! {
if let Some((apply_block, validation_function)) = PendingValidationFunction::get() {
if vfp.persisted.block_number >= apply_block {
PendingValidationFunction::kill();
LastUpgrade::put(&apply_block);
Self::put_parachain_code(&validation_function);
Self::deposit_event(Event::ValidationFunctionApplied(vfp.persisted.block_number));
}
}
let (host_config, relevant_messaging_state) =
relay_state_snapshot::extract_from_proof(
T::SelfParaId::get(),
vfp.persisted.relay_storage_root,
relay_chain_state
)
.map_err(|err| {
frame_support::debug::print!("invalid relay chain merkle proof: {:?}", err);
Error::<T>::InvalidRelayChainMerkleProof
})?;
storage::unhashed::put(VALIDATION_DATA, &vfp);
DidUpdateValidationData::put(true);
// TODO: here we should extract the key value pairs out of the storage proof.
drop(relay_chain_state);
RelevantMessagingState::put(relevant_messaging_state);
HostConfiguration::put(host_config);
<T::OnValidationData as OnValidationData>::on_validation_data(vfp);
Ok(())
}
fn on_finalize() {
@@ -177,35 +214,53 @@ impl<T: Config> Module<T> {
storage::unhashed::put_raw(well_known_keys::CODE, code);
}
/// `true` when a code upgrade is currently legal
pub fn can_set_code() -> bool {
Self::validation_data()
.map(|vfp| vfp.transient.code_upgrade_allowed.is_some())
.unwrap_or_default()
/// The maximum code size permitted, in bytes.
///
/// Returns `None` if the relay chain parachain host configuration hasn't been submitted yet.
pub fn max_code_size() -> Option<u32> {
HostConfiguration::get().map(|cfg| cfg.max_code_size)
}
/// The maximum code size permitted, in bytes.
pub fn max_code_size() -> Option<u32> {
Self::validation_data().map(|vfp| vfp.transient.max_code_size)
/// Returns if a PVF/runtime upgrade could be signalled at the current block, and if so
/// when the new code will take the effect.
fn code_upgrade_allowed(
vfp: &ValidationData,
cfg: &AbridgedHostConfiguration,
) -> Option<relay_chain::BlockNumber> {
if PendingValidationFunction::get().is_some() {
// There is already upgrade scheduled. Upgrade is not allowed.
return None;
}
let relay_blocks_since_last_upgrade = vfp
.persisted
.block_number
.saturating_sub(LastUpgrade::get());
if relay_blocks_since_last_upgrade <= cfg.validation_upgrade_frequency {
// The cooldown after the last upgrade hasn't elapsed yet. Upgrade is not allowed.
return None;
}
Some(vfp.persisted.block_number + cfg.validation_upgrade_delay)
}
/// The implementation of the runtime upgrade scheduling.
fn schedule_upgrade_impl(
validation_function: Vec<u8>,
) -> frame_support::dispatch::DispatchResult {
) -> DispatchResult {
ensure!(
!PendingValidationFunction::exists(),
Error::<T>::OverlappingUpgrades
);
let vfp = Self::validation_data().ok_or(Error::<T>::ValidationDataNotAvailable)?;
let cfg = Self::host_configuration().ok_or(Error::<T>::HostConfigurationNotAvailable)?;
ensure!(
validation_function.len() <= vfp.transient.max_code_size as usize,
validation_function.len() <= cfg.max_code_size as usize,
Error::<T>::TooBig
);
let apply_block = vfp
.transient
.code_upgrade_allowed
.ok_or(Error::<T>::ProhibitedByPolkadot)?;
let apply_block =
Self::code_upgrade_allowed(&vfp, &cfg).ok_or(Error::<T>::ProhibitedByPolkadot)?;
// When a code upgrade is scheduled, it has to be applied in two
// places, synchronized: both polkadot and the individual parachain
@@ -257,6 +312,10 @@ decl_error! {
TooBig,
/// The inherent which supplies the validation data did not run this block
ValidationDataNotAvailable,
/// The inherent which supplies the host configuration did not run this block
HostConfigurationNotAvailable,
/// Invalid relay-chain storage merkle proof
InvalidRelayChainMerkleProof,
}
}
@@ -267,6 +326,7 @@ mod tests {
use codec::Encode;
use cumulus_primitives::{PersistedValidationData, TransientValidationData};
use cumulus_test_relay_sproof_builder::RelayStateSproofBuilder;
use frame_support::{
assert_ok,
dispatch::UnfilteredDispatchable,
@@ -312,6 +372,7 @@ mod tests {
apis: sp_version::create_apis_vec!([]),
transaction_version: 1,
};
pub const ParachainId: ParaId = ParaId::new(200);
}
impl frame_system::Config for Test {
type Origin = Origin;
@@ -340,9 +401,11 @@ mod tests {
impl Config for Test {
type Event = TestEvent;
type OnValidationData = ();
type SelfParaId = ParachainId;
}
type ParachainUpgrade = Module<Test>;
type System = frame_system::Module<Test>;
// This function basically just builds a genesis storage key/value store according to
// our desired mockup.
@@ -398,7 +461,9 @@ mod tests {
tests: Vec<BlockTest>,
pending_upgrade: Option<RelayChainBlockNumber>,
ran: bool,
vfp_maker: Option<Box<dyn Fn(&BlockTests, RelayChainBlockNumber) -> ValidationData>>,
relay_sproof_builder_hook: Option<
Box<dyn Fn(&BlockTests, RelayChainBlockNumber, &mut RelayStateSproofBuilder)>
>,
}
impl BlockTests {
@@ -439,11 +504,11 @@ mod tests {
})
}
fn with_validation_data<F>(mut self, f: F) -> Self
fn with_relay_sproof_builder<F>(mut self, f: F) -> Self
where
F: 'static + Fn(&BlockTests, RelayChainBlockNumber) -> ValidationData,
F: 'static + Fn(&BlockTests, RelayChainBlockNumber, &mut RelayStateSproofBuilder)
{
self.vfp_maker = Some(Box::new(f));
self.relay_sproof_builder_hook = Some(Box::new(f));
self
}
@@ -464,7 +529,7 @@ mod tests {
}
// begin initialization
System::<Test>::initialize(
System::initialize(
&n,
&Default::default(),
&Default::default(),
@@ -472,24 +537,21 @@ mod tests {
);
// now mess with the storage the way validate_block does
let vfp = match self.vfp_maker {
None => ValidationData {
persisted: PersistedValidationData {
block_number: *n as RelayChainBlockNumber,
..Default::default()
},
transient: TransientValidationData {
max_code_size: 10 * 1024 * 1024, // 10 mb
code_upgrade_allowed: if self.pending_upgrade.is_some() {
None
} else {
Some(*n as RelayChainBlockNumber + 1000)
},
..Default::default()
},
let mut sproof_builder = RelayStateSproofBuilder::default();
if let Some(ref hook) = self.relay_sproof_builder_hook {
hook(self, *n as RelayChainBlockNumber, &mut sproof_builder);
}
let (relay_storage_root, relay_chain_state) =
sproof_builder.into_state_root_and_proof();
let vfp = ValidationData {
persisted: PersistedValidationData {
block_number: *n as RelayChainBlockNumber,
relay_storage_root,
..Default::default()
},
Some(ref maker) => maker(self, *n as RelayChainBlockNumber),
transient: TransientValidationData::default(),
};
storage::unhashed::put(VALIDATION_DATA, &vfp);
storage::unhashed::kill(NEW_VALIDATION_CODE);
@@ -498,13 +560,10 @@ mod tests {
let inherent_data = {
let mut inherent_data = InherentData::default();
inherent_data
.put_data(
INHERENT_IDENTIFIER,
&ValidationDataType {
validation_data: vfp.clone(),
relay_chain_state: sp_state_machine::StorageProof::empty(),
},
)
.put_data(INHERENT_IDENTIFIER, &ValidationDataType {
validation_data: vfp.clone(),
relay_chain_state,
})
.expect("failed to put VFP inherent");
inherent_data
};
@@ -527,7 +586,7 @@ mod tests {
}
// clean up
System::<Test>::finalize();
System::finalize();
if let Some(after_block) = after_block {
after_block();
}
@@ -575,6 +634,9 @@ mod tests {
#[test]
fn events() {
BlockTests::new()
.with_relay_sproof_builder(|_, _, builder| {
builder.host_config.validation_upgrade_delay = 1000;
})
.add_with_post_test(
123,
|| {
@@ -584,7 +646,7 @@ mod tests {
));
},
|| {
let events = System::<Test>::events();
let events = System::events();
assert_eq!(
events[0].event,
TestEvent::parachain_upgrade(Event::ValidationFunctionStored(1123))
@@ -595,7 +657,7 @@ mod tests {
1234,
|| {},
|| {
let events = System::<Test>::events();
let events = System::events();
assert_eq!(
events[0].event,
TestEvent::parachain_upgrade(Event::ValidationFunctionApplied(1234))
@@ -607,6 +669,9 @@ mod tests {
#[test]
fn non_overlapping() {
BlockTests::new()
.with_relay_sproof_builder(|_, _, builder| {
builder.host_config.validation_upgrade_delay = 1000;
})
.add(123, || {
assert_ok!(ParachainUpgrade::schedule_upgrade(
RawOrigin::Root.into(),
@@ -615,7 +680,7 @@ mod tests {
})
.add(234, || {
assert_eq!(
ParachainUpgrade::schedule_upgrade(RawOrigin::Root.into(), Default::default(),),
ParachainUpgrade::schedule_upgrade(RawOrigin::Root.into(), Default::default()),
Err(Error::<Test>::OverlappingUpgrades.into()),
)
});
@@ -653,16 +718,8 @@ mod tests {
#[test]
fn checks_size() {
BlockTests::new()
.with_validation_data(|_, n| ValidationData {
persisted: PersistedValidationData {
block_number: n,
..Default::default()
},
transient: TransientValidationData {
max_code_size: 32,
code_upgrade_allowed: Some(n + 1000),
..Default::default()
},
.with_relay_sproof_builder(|_, _, builder| {
builder.host_config.max_code_size = 8;
})
.add(123, || {
assert_eq!(
@@ -0,0 +1,150 @@
// Copyright 2021 Parity Technologies (UK) Ltd.
// This file is part of Cumulus.
// Cumulus is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Cumulus is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Cumulus. If not, see <http://www.gnu.org/licenses/>.
use codec::{Encode, Decode};
use cumulus_primitives::{relay_chain, AbridgedHostConfiguration, AbridgedHrmpChannel, ParaId};
use hash_db::{HashDB, EMPTY_PREFIX};
use sp_runtime::traits::HashFor;
use sp_state_machine::{Backend, TrieBackend};
use sp_trie::StorageProof;
use sp_std::vec::Vec;
/// A snapshot of some messaging related state of relay chain pertaining to the current parachain.
///
/// This data is essential for making sure that the parachain is aware of current resource use on
/// the relay chain and that the candidates produced for this parachain do not exceed any of these
/// limits.
#[derive(Encode, Decode)]
pub struct MessagingStateSnapshot {
/// The current capacity of the upward message queue of the current parachain on the relay chain.
///
/// The capacity is represented by a tuple that consist of the `count` of the messages and the
/// `total_size` expressed as the sum of byte sizes of all messages in the queue.
pub relay_dispatch_queue_size: (u32, u32),
/// Information about all the outbound HRMP channels.
///
/// These are structured as a list of tuples. The para id in the tuple specifies the recipient
/// of the channel. Obviously, the sender is the current parachain.
///
/// The channels are sorted by the recipient para id ascension.
pub egress_channels: Vec<(ParaId, AbridgedHrmpChannel)>,
}
#[derive(Debug)]
pub enum Error {
/// The provided proof was created against unexpected storage root.
RootMismatch,
/// The host configuration cannot be extracted.
Config(ReadEntryErr),
/// Relay dispatch queue cannot be extracted.
RelayDispatchQueueSize(ReadEntryErr),
/// The hrmp egress channel index cannot be extracted.
HrmpEgressChannelIndex(ReadEntryErr),
/// The hrmp channel for the given recipient cannot be extracted.
HrmpChannel(ParaId, ReadEntryErr),
}
#[derive(Debug)]
pub enum ReadEntryErr {
/// The value cannot be extracted from the proof.
Proof,
/// The value cannot be decoded.
Decode,
/// The value is expected to be present on the relay chain, but it doesn't exist.
Absent,
}
/// Read an entry given by the key and try to decode it. If the value specified by the key according
/// to the proof is empty, the `fallback` value will be returned.
///
/// Returns `Err` in case the backend can't return the value under the specific key (likely due to
/// a malformed proof), in case the decoding fails, or in case where the value is empty in the relay
/// chain state and no fallback was provided.
fn read_entry<T, B>(backend: &B, key: &[u8], fallback: Option<T>) -> Result<T, ReadEntryErr>
where
T: Decode,
B: Backend<HashFor<relay_chain::Block>>,
{
backend
.storage(key)
.map_err(|_| ReadEntryErr::Proof)?
.map(|raw_entry| T::decode(&mut &raw_entry[..]).map_err(|_| ReadEntryErr::Decode))
.transpose()?
.or(fallback)
.ok_or(ReadEntryErr::Absent)
}
/// Extract the relay chain state from the given storage proof. This function accepts the `para_id`
/// of the current parachain and the expected storage root the proof should stem from.
pub fn extract_from_proof(
para_id: ParaId,
relay_storage_root: relay_chain::v1::Hash,
proof: StorageProof,
) -> Result<(AbridgedHostConfiguration, MessagingStateSnapshot), Error> {
let db = proof.into_memory_db::<HashFor<relay_chain::Block>>();
if !db.contains(&relay_storage_root, EMPTY_PREFIX) {
return Err(Error::RootMismatch);
}
let backend = TrieBackend::new(db, relay_storage_root);
let host_config: AbridgedHostConfiguration = read_entry(
&backend,
relay_chain::well_known_keys::ACTIVE_CONFIG,
None,
)
.map_err(Error::Config)?;
let relay_dispatch_queue_size: (u32, u32) = read_entry(
&backend,
&relay_chain::well_known_keys::relay_dispatch_queue_size(para_id),
Some((0, 0)),
)
.map_err(Error::RelayDispatchQueueSize)?;
let egress_channel_index: Vec<ParaId> = read_entry(
&backend,
&relay_chain::well_known_keys::hrmp_egress_channel_index(para_id),
Some(Vec::new()),
)
.map_err(Error::HrmpEgressChannelIndex)?;
let mut egress_channels = Vec::with_capacity(egress_channel_index.len());
for recipient in egress_channel_index {
let channel_id = relay_chain::v1::HrmpChannelId {
sender: para_id,
recipient,
};
let hrmp_channel: AbridgedHrmpChannel = read_entry(
&backend,
&relay_chain::well_known_keys::hrmp_channels(channel_id),
None,
)
.map_err(|read_err| Error::HrmpChannel(recipient, read_err))?;
egress_channels.push((recipient, hrmp_channel));
}
// NOTE that egress_channels promises to be sorted. We satisfy this property by relying on
// the fact that `egress_channel_index` is itself sorted.
Ok((
host_config,
MessagingStateSnapshot {
relay_dispatch_queue_size,
egress_channels,
},
))
}