// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see .
//! The paras pezpallet acts as the main registry of paras.
//!
//! # Tracking State of Paras
//!
//! The most important responsibility of this module is to track which teyrchains
//! are active and what their current state is. The current state of a para consists of the current
//! head data and the current validation code (AKA Teyrchain Validation Function (PVF)).
//!
//! A para is not considered live until it is registered and activated in this pezpallet.
//!
//! The set of teyrchains cannot change except at session boundaries. This is primarily to ensure
//! that the number and meaning of bits required for the availability bitfields does not change
//! except at session boundaries.
//!
//! # Validation Code Upgrades
//!
//! When a para signals the validation code upgrade it will be processed by this module. This can
//! be in turn split into more fine grained items:
//!
//! - Part of the acceptance criteria checks if the para can indeed signal an upgrade,
//!
//! - When the candidate is enacted, this module schedules code upgrade, storing the prospective
//! validation code.
//!
//! - Actually assign the prospective validation code to be the current one after all conditions are
//! fulfilled.
//!
//! The conditions that must be met before the para can use the new validation code are:
//!
//! 1. The validation code should have been "soaked" in the storage for a given number of blocks.
//! That is, the validation code should have been stored in on-chain storage for some time, so
//! that in case of a revert with a non-extreme height difference, that validation code can still
//! be found on-chain.
//!
//! 2. The validation code was vetted by the validators and declared as non-malicious in a processes
//! known as PVF pre-checking.
//!
//! # Validation Code Management
//!
//! Potentially, one validation code can be used by several different paras. For example, during
//! initial stages of deployment several paras can use the same "shell" validation code, or
//! there can be shards of the same para that use the same validation code.
//!
//! In case a validation code ceases to have any users it must be pruned from the on-chain storage.
//!
//! # Para Lifecycle Management
//!
//! A para can be in one of the two stable states: it is either a lease holding teyrchain or an
//! on-demand teyrchain.
//!
//! However, in order to get into one of those two states, it must first be onboarded. Onboarding
//! can be only enacted at session boundaries. Onboarding must take at least one full session.
//! Moreover, a brand new validation code should go through the PVF pre-checking process.
//!
//! Once the para is in one of the two stable states, it can switch to the other stable state or to
//! initiate offboarding process. The result of offboarding is removal of all data related to that
//! para.
//!
//! # PVF Pre-checking
//!
//! As was mentioned above, a brand new validation code should go through a process of approval. As
//! part of this process, validators from the active set will take the validation code and check if
//! it is malicious. Once they did that and have their judgement, either accept or reject, they
//! issue a statement in a form of an unsigned extrinsic. This extrinsic is processed by this
//! pezpallet. Once supermajority is gained for accept, then the process that initiated the check is
//! resumed (as mentioned before this can be either upgrading of validation code or onboarding). If
//! getting a supermajority becomes impossible (>1/3 of validators have already voted against), then
//! we reject.
//!
//! Below is a state diagram that depicts states of a single PVF pre-checking vote.
//!
//! ```text
//! ┌──────────┐
//! supermajority │ │
//! ┌────────for───────────▶│ accepted │
//! vote────┐ │ │ │
//! │ │ │ └──────────┘
//! │ │ │
//! │ ┌───────┐
//! │ │ │
//! └─▶│ init │──── >1/3 against ┌──────────┐
//! │ │ │ │ │
//! └───────┘ └──────────▶│ rejected │
//! ▲ │ │ │
//! │ │ session └──────────┘
//! │ └──change
//! │ │
//! │ ▼
//! ┌─────┐
//! start──────▶│reset│
//! └─────┘
//! ```
use crate::{
configuration,
inclusion::{QueueFootprinter, UmpQueueId},
initializer::SessionChangeNotification,
shared,
};
use alloc::{collections::btree_set::BTreeSet, vec::Vec};
use bitvec::{order::Lsb0 as BitOrderLsb0, vec::BitVec};
use codec::{Decode, Encode};
use core::{cmp, mem};
use pezframe_support::{
pezpallet_prelude::*,
traits::{EnsureOriginWithArg, EstimateNextSessionRotation},
DefaultNoBound,
};
use pezframe_system::pezpallet_prelude::*;
use pezkuwi_primitives::{
ConsensusLog, HeadData, Id as ParaId, PvfCheckStatement, SessionIndex, UpgradeGoAhead,
UpgradeRestriction, ValidationCode, ValidationCodeHash, ValidatorSignature, MIN_CODE_SIZE,
};
use pezsp_core::RuntimeDebug;
use pezsp_runtime::{
traits::{AppVerify, One, Saturating},
DispatchResult, SaturatedConversion,
};
use scale_info::{Type, TypeInfo};
use serde::{Deserialize, Serialize};
pub use crate::Origin as TeyrchainOrigin;
#[cfg(feature = "runtime-benchmarks")]
pub mod benchmarking;
#[cfg(test)]
pub(crate) mod tests;
pub use pezpallet::*;
const LOG_TARGET: &str = "runtime::paras";
// the two key times necessary to track for every code replacement.
#[derive(Default, Encode, Decode, TypeInfo)]
#[cfg_attr(test, derive(Debug, Clone, PartialEq))]
pub struct ReplacementTimes {
/// The relay-chain block number that the code upgrade was expected to be activated.
/// This is when the code change occurs from the para's perspective - after the
/// first parablock included with a relay-parent with number >= this value.
expected_at: N,
/// The relay-chain block number at which the parablock activating the code upgrade was
/// actually included. This means considered included and available, so this is the time at
/// which that parablock enters the acceptance period in this fork of the relay-chain.
activated_at: N,
}
/// Metadata used to track previous teyrchain validation code that we keep in
/// the state.
#[derive(Default, Encode, Decode, TypeInfo)]
#[cfg_attr(test, derive(Debug, Clone, PartialEq))]
pub struct ParaPastCodeMeta {
/// Block numbers where the code was expected to be replaced and where the code
/// was actually replaced, respectively. The first is used to do accurate look-ups
/// of historic code in historic contexts, whereas the second is used to do
/// pruning on an accurate timeframe. These can be used as indices
/// into the `PastCodeHash` map along with the `ParaId` to fetch the code itself.
upgrade_times: Vec>,
/// Tracks the highest pruned code-replacement, if any. This is the `activated_at` value,
/// not the `expected_at` value.
last_pruned: Option,
}
/// The possible states of a para, to take into account delayed lifecycle changes.
///
/// If the para is in a "transition state", it is expected that the teyrchain is
/// queued in the `ActionsQueue` to transition it into a stable state. Its lifecycle
/// state will be used to determine the state transition to apply to the para.
#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)]
pub enum ParaLifecycle {
/// Para is new and is onboarding as an on-demand or lease holding Teyrchain.
Onboarding,
/// Para is a Parathread (on-demand teyrchain).
Parathread,
/// Para is a lease holding Teyrchain.
Teyrchain,
/// Para is a Parathread (on-demand teyrchain) which is upgrading to a lease holding Teyrchain.
UpgradingParathread,
/// Para is a lease holding Teyrchain which is downgrading to an on-demand teyrchain.
DowngradingTeyrchain,
/// Parathread (on-demand teyrchain) is queued to be offboarded.
OffboardingParathread,
/// Teyrchain is queued to be offboarded.
OffboardingTeyrchain,
}
impl ParaLifecycle {
/// Returns true if teyrchain is currently onboarding. To learn if the
/// teyrchain is onboarding as a lease holding or on-demand teyrchain, look at the
/// `UpcomingGenesis` storage item.
pub fn is_onboarding(&self) -> bool {
matches!(self, ParaLifecycle::Onboarding)
}
/// Returns true if para is in a stable state, i.e. it is currently
/// a lease holding or on-demand teyrchain, and not in any transition state.
pub fn is_stable(&self) -> bool {
matches!(self, ParaLifecycle::Parathread | ParaLifecycle::Teyrchain)
}
/// Returns true if para is currently treated as a teyrchain.
/// This also includes transitioning states, so you may want to combine
/// this check with `is_stable` if you specifically want `Paralifecycle::Teyrchain`.
pub fn is_teyrchain(&self) -> bool {
matches!(
self,
ParaLifecycle::Teyrchain |
ParaLifecycle::DowngradingTeyrchain |
ParaLifecycle::OffboardingTeyrchain
)
}
/// Returns true if para is currently treated as a parathread (on-demand teyrchain).
/// This also includes transitioning states, so you may want to combine
/// this check with `is_stable` if you specifically want `Paralifecycle::Parathread`.
pub fn is_parathread(&self) -> bool {
matches!(
self,
ParaLifecycle::Parathread |
ParaLifecycle::UpgradingParathread |
ParaLifecycle::OffboardingParathread
)
}
/// Returns true if para is currently offboarding.
pub fn is_offboarding(&self) -> bool {
matches!(self, ParaLifecycle::OffboardingParathread | ParaLifecycle::OffboardingTeyrchain)
}
/// Returns true if para is in any transitionary state.
pub fn is_transitioning(&self) -> bool {
!Self::is_stable(self)
}
}
impl ParaPastCodeMeta {
// note a replacement has occurred at a given block number.
pub(crate) fn note_replacement(&mut self, expected_at: N, activated_at: N) {
self.upgrade_times.push(ReplacementTimes { expected_at, activated_at })
}
/// Returns `true` if the upgrade logs list is empty.
fn is_empty(&self) -> bool {
self.upgrade_times.is_empty()
}
// The block at which the most recently tracked code change occurred, from the perspective
// of the para.
#[cfg(test)]
fn most_recent_change(&self) -> Option {
self.upgrade_times.last().map(|x| x.expected_at)
}
// prunes all code upgrade logs occurring at or before `max`.
// note that code replaced at `x` is the code used to validate all blocks before
// `x`. Thus, `max` should be outside of the slashing window when this is invoked.
//
// Since we don't want to prune anything inside the acceptance period, and the parablock only
// enters the acceptance period after being included, we prune based on the activation height of
// the code change, not the expected height of the code change.
//
// returns an iterator of block numbers at which code was replaced, where the replaced
// code should be now pruned, in ascending order.
fn prune_up_to(&'_ mut self, max: N) -> impl Iterator- + '_ {
let to_prune = self.upgrade_times.iter().take_while(|t| t.activated_at <= max).count();
let drained = if to_prune == 0 {
// no-op prune.
self.upgrade_times.drain(self.upgrade_times.len()..)
} else {
// if we are actually pruning something, update the `last_pruned` member.
self.last_pruned = Some(self.upgrade_times[to_prune - 1].activated_at);
self.upgrade_times.drain(..to_prune)
};
drained.map(|times| times.expected_at)
}
}
/// Arguments for initializing a para.
#[derive(
PartialEq,
Eq,
Clone,
Encode,
Decode,
DecodeWithMemTracking,
RuntimeDebug,
TypeInfo,
Serialize,
Deserialize,
)]
pub struct ParaGenesisArgs {
/// The initial head data to use.
pub genesis_head: HeadData,
/// The initial validation code to use.
pub validation_code: ValidationCode,
/// Lease holding or on-demand teyrchain.
#[serde(rename = "teyrchain")]
pub para_kind: ParaKind,
}
/// Distinguishes between lease holding Teyrchain and Parathread (on-demand teyrchain)
#[derive(DecodeWithMemTracking, PartialEq, Eq, Clone, RuntimeDebug)]
pub enum ParaKind {
Parathread,
Teyrchain,
}
impl Serialize for ParaKind {
fn serialize
(&self, serializer: S) -> Result
where
S: serde::Serializer,
{
match self {
ParaKind::Teyrchain => serializer.serialize_bool(true),
ParaKind::Parathread => serializer.serialize_bool(false),
}
}
}
impl<'de> Deserialize<'de> for ParaKind {
fn deserialize(deserializer: D) -> Result
where
D: serde::Deserializer<'de>,
{
match serde::de::Deserialize::deserialize(deserializer) {
Ok(true) => Ok(ParaKind::Teyrchain),
Ok(false) => Ok(ParaKind::Parathread),
_ => Err(serde::de::Error::custom("invalid ParaKind serde representation")),
}
}
}
// Manual encoding, decoding, and TypeInfo as the parakind field in ParaGenesisArgs used to be a
// bool
impl Encode for ParaKind {
fn size_hint(&self) -> usize {
true.size_hint()
}
fn using_encoded R>(&self, f: F) -> R {
match self {
ParaKind::Teyrchain => true.using_encoded(f),
ParaKind::Parathread => false.using_encoded(f),
}
}
}
impl Decode for ParaKind {
fn decode(input: &mut I) -> Result {
match bool::decode(input) {
Ok(true) => Ok(ParaKind::Teyrchain),
Ok(false) => Ok(ParaKind::Parathread),
_ => Err("Invalid ParaKind representation".into()),
}
}
}
impl TypeInfo for ParaKind {
type Identity = bool;
fn type_info() -> Type {
bool::type_info()
}
}
/// This enum describes a reason why a particular PVF pre-checking vote was initiated. When the
/// PVF vote in question is concluded, this enum indicates what changes should be performed.
#[derive(Debug, Encode, Decode, TypeInfo)]
pub(crate) enum PvfCheckCause {
/// PVF vote was initiated by the initial onboarding process of the given para.
Onboarding(ParaId),
/// PVF vote was initiated by signalling of an upgrade by the given para.
Upgrade {
/// The ID of the teyrchain that initiated or is waiting for the conclusion of
/// pre-checking.
id: ParaId,
/// The relay-chain block number of **inclusion** of candidate that that initiated the
/// upgrade.
///
/// It's important to count upgrade enactment delay from the inclusion of this candidate
/// instead of its relay parent -- in order to keep PVF available in case of chain
/// reversions.
///
/// See https://github.com/pezkuwichain/pezkuwi-sdk/issues/151 for detailed explanation.
included_at: BlockNumber,
/// Whether or not the upgrade should be enacted directly.
///
/// If set to `Yes` it means that no `GoAheadSignal` will be set and the teyrchain code
/// will also be overwritten directly.
upgrade_strategy: UpgradeStrategy,
},
}
/// The strategy on how to handle a validation code upgrade.
///
/// When scheduling a teyrchain code upgrade the upgrade first is checked by all validators. The
/// validators ensure that the new validation code can be compiled and instantiated. After the
/// majority of the validators have reported their checking result the upgrade is either scheduled
/// or aborted. This strategy then comes into play around the relay chain block this upgrade was
/// scheduled in.
#[derive(Debug, Copy, Clone, PartialEq, TypeInfo, Decode, Encode)]
pub enum UpgradeStrategy {
/// Set the `GoAhead` signal to inform the teyrchain that it is time to upgrade.
///
/// The upgrade will then be applied after the first teyrchain block was enacted that must have
/// observed the `GoAhead` signal.
SetGoAheadSignal,
/// Apply the upgrade directly at the expected relay chain block.
///
/// This doesn't wait for the teyrchain to make any kind of progress.
ApplyAtExpectedBlock,
}
impl PvfCheckCause {
/// Returns the ID of the para that initiated or subscribed to the pre-checking vote.
fn para_id(&self) -> ParaId {
match *self {
PvfCheckCause::Onboarding(id) => id,
PvfCheckCause::Upgrade { id, .. } => id,
}
}
}
/// Specifies what was the outcome of a PVF pre-checking vote.
#[derive(Copy, Clone, Encode, Decode, RuntimeDebug, TypeInfo)]
enum PvfCheckOutcome {
Accepted,
Rejected,
}
/// This struct describes the current state of an in-progress PVF pre-checking vote.
#[derive(Encode, Decode, TypeInfo)]
pub(crate) struct PvfCheckActiveVoteState {
// The two following vectors have their length equal to the number of validators in the active
// set. They start with all zeroes. A 1 is set at an index when the validator at the that index
// makes a vote. Once a 1 is set for either of the vectors, that validator cannot vote anymore.
// Since the active validator set changes each session, the bit vectors are reinitialized as
// well: zeroed and resized so that each validator gets its own bit.
votes_accept: BitVec,
votes_reject: BitVec,
/// The number of session changes this PVF vote has observed. Therefore, this number is
/// increased at each session boundary. When created, it is initialized with 0.
age: SessionIndex,
/// The block number at which this PVF vote was created.
created_at: BlockNumber,
/// A list of causes for this PVF pre-checking. Has at least one.
causes: Vec>,
}
impl PvfCheckActiveVoteState {
/// Returns a new instance of vote state, started at the specified block `now`, with the
/// number of validators in the current session `n_validators` and the originating `cause`.
fn new(now: BlockNumber, n_validators: usize, cause: PvfCheckCause) -> Self {
let mut causes = Vec::with_capacity(1);
causes.push(cause);
Self {
created_at: now,
votes_accept: bitvec::bitvec![u8, BitOrderLsb0; 0; n_validators],
votes_reject: bitvec::bitvec![u8, BitOrderLsb0; 0; n_validators],
age: 0,
causes,
}
}
/// Resets all votes and resizes the votes vectors corresponding to the number of validators
/// in the new session.
fn reinitialize_ballots(&mut self, n_validators: usize) {
let clear_and_resize = |v: &mut BitVec<_, _>| {
v.clear();
v.resize(n_validators, false);
};
clear_and_resize(&mut self.votes_accept);
clear_and_resize(&mut self.votes_reject);
}
/// Returns `Some(true)` if the validator at the given index has already cast their vote within
/// the ongoing session. Returns `None` in case the index is out of bounds.
fn has_vote(&self, validator_index: usize) -> Option {
let accept_vote = self.votes_accept.get(validator_index)?;
let reject_vote = self.votes_reject.get(validator_index)?;
Some(*accept_vote || *reject_vote)
}
/// Returns `None` if the quorum is not reached, or the direction of the decision.
fn quorum(&self, n_validators: usize) -> Option {
let accept_threshold = pezkuwi_primitives::supermajority_threshold(n_validators);
// At this threshold, a supermajority is no longer possible, so we reject.
let reject_threshold = n_validators - accept_threshold;
if self.votes_accept.count_ones() >= accept_threshold {
Some(PvfCheckOutcome::Accepted)
} else if self.votes_reject.count_ones() > reject_threshold {
Some(PvfCheckOutcome::Rejected)
} else {
None
}
}
#[cfg(test)]
pub(crate) fn causes(&self) -> &[PvfCheckCause] {
self.causes.as_slice()
}
}
/// Runtime hook for when a teyrchain head is updated.
pub trait OnNewHead {
/// Called when a teyrchain head is updated.
/// Returns the weight consumed by this function.
fn on_new_head(id: ParaId, head: &HeadData) -> Weight;
}
#[impl_trait_for_tuples::impl_for_tuples(30)]
impl OnNewHead for Tuple {
fn on_new_head(id: ParaId, head: &HeadData) -> Weight {
let mut weight: Weight = Default::default();
for_tuples!( #( weight.saturating_accrue(Tuple::on_new_head(id, head)); )* );
weight
}
}
/// Assign coretime to some teyrchain.
///
/// This assigns coretime to a teyrchain without using the coretime chain. Thus, this should only be
/// used for testing purposes.
pub trait AssignCoretime {
/// ONLY USE FOR TESTING OR GENESIS.
fn assign_coretime(id: ParaId) -> DispatchResult;
}
impl AssignCoretime for () {
fn assign_coretime(_: ParaId) -> DispatchResult {
Ok(())
}
}
/// Holds an authorized validation code hash along with its expiry timestamp.
#[derive(Debug, Encode, Decode, DecodeWithMemTracking, TypeInfo)]
#[cfg_attr(test, derive(PartialEq))]
pub struct AuthorizedCodeHashAndExpiry {
code_hash: ValidationCodeHash,
expire_at: T,
}
impl From<(ValidationCodeHash, T)> for AuthorizedCodeHashAndExpiry {
fn from(value: (ValidationCodeHash, T)) -> Self {
AuthorizedCodeHashAndExpiry { code_hash: value.0, expire_at: value.1 }
}
}
pub trait WeightInfo {
fn force_set_current_code(c: u32) -> Weight;
fn force_set_current_head(s: u32) -> Weight;
fn force_set_most_recent_context() -> Weight;
fn force_schedule_code_upgrade(c: u32) -> Weight;
fn force_note_new_head(s: u32) -> Weight;
fn force_queue_action() -> Weight;
fn add_trusted_validation_code(c: u32) -> Weight;
fn poke_unused_validation_code() -> Weight;
fn remove_upgrade_cooldown() -> Weight;
fn include_pvf_check_statement_finalize_upgrade_accept() -> Weight;
fn include_pvf_check_statement_finalize_upgrade_reject() -> Weight;
fn include_pvf_check_statement_finalize_onboarding_accept() -> Weight;
fn include_pvf_check_statement_finalize_onboarding_reject() -> Weight;
fn include_pvf_check_statement() -> Weight;
fn authorize_force_set_current_code_hash() -> Weight;
fn apply_authorized_force_set_current_code(c: u32) -> Weight;
}
pub struct TestWeightInfo;
impl WeightInfo for TestWeightInfo {
fn force_set_current_code(_c: u32) -> Weight {
Weight::MAX
}
fn force_set_current_head(_s: u32) -> Weight {
Weight::MAX
}
fn force_set_most_recent_context() -> Weight {
Weight::MAX
}
fn force_schedule_code_upgrade(_c: u32) -> Weight {
Weight::MAX
}
fn force_note_new_head(_s: u32) -> Weight {
Weight::MAX
}
fn force_queue_action() -> Weight {
Weight::MAX
}
fn add_trusted_validation_code(_c: u32) -> Weight {
// Called during integration tests for para initialization.
Weight::zero()
}
fn poke_unused_validation_code() -> Weight {
Weight::MAX
}
fn include_pvf_check_statement_finalize_upgrade_accept() -> Weight {
Weight::MAX
}
fn include_pvf_check_statement_finalize_upgrade_reject() -> Weight {
Weight::MAX
}
fn include_pvf_check_statement_finalize_onboarding_accept() -> Weight {
Weight::MAX
}
fn include_pvf_check_statement_finalize_onboarding_reject() -> Weight {
Weight::MAX
}
fn include_pvf_check_statement() -> Weight {
// This special value is to distinguish from the finalizing variants above in tests.
Weight::MAX - Weight::from_parts(1, 1)
}
fn remove_upgrade_cooldown() -> Weight {
Weight::MAX
}
fn authorize_force_set_current_code_hash() -> Weight {
Weight::MAX
}
fn apply_authorized_force_set_current_code(_c: u32) -> Weight {
Weight::MAX
}
}
#[pezframe_support::pezpallet]
pub mod pezpallet {
use super::*;
use pezframe_support::traits::{
fungible::{Inspect, Mutate},
tokens::{Fortitude, Precision, Preservation},
};
use pezsp_runtime::transaction_validity::{
InvalidTransaction, TransactionPriority, TransactionSource, TransactionValidity,
ValidTransaction,
};
type BalanceOf = <::Fungible as Inspect>>::Balance;
#[pezpallet::pezpallet]
#[pezpallet::without_storage_info]
pub struct Pezpallet(_);
#[pezpallet::config]
pub trait Config:
pezframe_system::Config
+ configuration::Config
+ shared::Config
+ pezframe_system::offchain::CreateBare>
{
#[allow(deprecated)]
type RuntimeEvent: From>
+ IsType<::RuntimeEvent>;
#[pezpallet::constant]
type UnsignedPriority: Get;
type NextSessionRotation: EstimateNextSessionRotation>;
/// Retrieve how many UMP messages are enqueued for this para-chain.
///
/// This is used to judge whether or not a para-chain can offboard. Per default this should
/// be set to the `ParaInclusion` pezpallet.
type QueueFootprinter: QueueFootprinter;
/// Runtime hook for when a teyrchain head is updated.
type OnNewHead: OnNewHead;
/// Weight information for extrinsics in this pezpallet.
type WeightInfo: WeightInfo;
/// Runtime hook for assigning coretime for a given teyrchain.
///
/// This is only used at genesis or by root.
///
/// TODO: Remove once coretime is the standard across all chains.
type AssignCoretime: AssignCoretime;
/// The fungible instance used by the runtime.
type Fungible: Mutate>>;
/// Multiplier to determine the cost of removing upgrade cooldown.
///
/// After a teyrchain upgrades their runtime, an upgrade cooldown is applied
/// ([`configuration::HostConfiguration::validation_upgrade_cooldown`]). This cooldown
/// exists to prevent spamming the relay chain with runtime upgrades. But as life is going
/// on, mistakes can happen and a consequent may be required. The cooldown period can be
/// removed by using [`Pezpallet::remove_upgrade_cooldown`]. This dispatchable will use this
/// multiplier to determine the cost for removing the upgrade cooldown. Time left for the
/// cooldown multiplied with this multiplier determines the cost.
type CooldownRemovalMultiplier: Get>;
/// The origin that can authorize [`Pezpallet::authorize_force_set_current_code_hash`].
///
/// In the end this allows [`Pezpallet::apply_authorized_force_set_current_code`] to force
/// set the current code without paying any fee. So, the origin should be chosen with
/// care.
type AuthorizeCurrentCodeOrigin: EnsureOriginWithArg;
}
#[pezpallet::event]
#[pezpallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event {
/// Current code has been updated for a Para. `para_id`
CurrentCodeUpdated(ParaId),
/// Current head has been updated for a Para. `para_id`
CurrentHeadUpdated(ParaId),
/// A code upgrade has been scheduled for a Para. `para_id`
CodeUpgradeScheduled(ParaId),
/// A new head has been noted for a Para. `para_id`
NewHeadNoted(ParaId),
/// A para has been queued to execute pending actions. `para_id`
ActionQueued(ParaId, SessionIndex),
/// The given para either initiated or subscribed to a PVF check for the given validation
/// code. `code_hash` `para_id`
PvfCheckStarted(ValidationCodeHash, ParaId),
/// The given validation code was accepted by the PVF pre-checking vote.
/// `code_hash` `para_id`
PvfCheckAccepted(ValidationCodeHash, ParaId),
/// The given validation code was rejected by the PVF pre-checking vote.
/// `code_hash` `para_id`
PvfCheckRejected(ValidationCodeHash, ParaId),
/// The upgrade cooldown was removed.
UpgradeCooldownRemoved {
/// The teyrchain for which the cooldown got removed.
para_id: ParaId,
},
/// A new code hash has been authorized for a Para.
CodeAuthorized {
/// Para
para_id: ParaId,
/// Authorized code hash.
code_hash: ValidationCodeHash,
/// Block at which authorization expires and will be removed.
expire_at: BlockNumberFor,
},
}
#[pezpallet::error]
pub enum Error {
/// Para is not registered in our system.
NotRegistered,
/// Para cannot be onboarded because it is already tracked by our system.
CannotOnboard,
/// Para cannot be offboarded at this time.
CannotOffboard,
/// Para cannot be upgraded to a lease holding teyrchain.
CannotUpgrade,
/// Para cannot be downgraded to an on-demand teyrchain.
CannotDowngrade,
/// The statement for PVF pre-checking is stale.
PvfCheckStatementStale,
/// The statement for PVF pre-checking is for a future session.
PvfCheckStatementFuture,
/// Claimed validator index is out of bounds.
PvfCheckValidatorIndexOutOfBounds,
/// The signature for the PVF pre-checking is invalid.
PvfCheckInvalidSignature,
/// The given validator already has cast a vote.
PvfCheckDoubleVote,
/// The given PVF does not exist at the moment of process a vote.
PvfCheckSubjectInvalid,
/// Teyrchain cannot currently schedule a code upgrade.
CannotUpgradeCode,
/// Invalid validation code size.
InvalidCode,
/// No upgrade authorized.
NothingAuthorized,
/// The submitted code is not authorized.
Unauthorized,
/// Invalid block number.
InvalidBlockNumber,
}
/// All currently active PVF pre-checking votes.
///
/// Invariant:
/// - There are no PVF pre-checking votes that exists in list but not in the set and vice versa.
#[pezpallet::storage]
pub(super) type PvfActiveVoteMap = StorageMap<
_,
Twox64Concat,
ValidationCodeHash,
PvfCheckActiveVoteState>,
OptionQuery,
>;
/// The list of all currently active PVF votes. Auxiliary to `PvfActiveVoteMap`.
#[pezpallet::storage]
pub(super) type PvfActiveVoteList =
StorageValue<_, Vec, ValueQuery>;
/// All lease holding teyrchains. Ordered ascending by `ParaId`. On demand teyrchains are not
/// included.
///
/// Consider using the [`TeyrchainsCache`] type of modifying.
#[pezpallet::storage]
pub type Teyrchains = StorageValue<_, Vec, ValueQuery>;
/// The current lifecycle of a all known Para IDs.
#[pezpallet::storage]
pub(super) type ParaLifecycles = StorageMap<_, Twox64Concat, ParaId, ParaLifecycle>;
/// The head-data of every registered para.
#[pezpallet::storage]
pub type Heads = StorageMap<_, Twox64Concat, ParaId, HeadData>;
/// The context (relay-chain block number) of the most recent teyrchain head.
#[pezpallet::storage]
pub type MostRecentContext = StorageMap<_, Twox64Concat, ParaId, BlockNumberFor>;
/// The validation code hash of every live para.
///
/// Corresponding code can be retrieved with [`CodeByHash`].
#[pezpallet::storage]
pub type CurrentCodeHash = StorageMap<_, Twox64Concat, ParaId, ValidationCodeHash>;
/// Actual past code hash, indicated by the para id as well as the block number at which it
/// became outdated.
///
/// Corresponding code can be retrieved with [`CodeByHash`].
#[pezpallet::storage]
pub(super) type PastCodeHash =
StorageMap<_, Twox64Concat, (ParaId, BlockNumberFor), ValidationCodeHash>;
/// Past code of teyrchains. The teyrchains themselves may not be registered anymore,
/// but we also keep their code on-chain for the same amount of time as outdated code
/// to keep it available for approval checkers.
#[pezpallet::storage]
pub type PastCodeMeta =
StorageMap<_, Twox64Concat, ParaId, ParaPastCodeMeta>, ValueQuery>;
/// Which paras have past code that needs pruning and the relay-chain block at which the code
/// was replaced. Note that this is the actual height of the included block, not the expected
/// height at which the code upgrade would be applied, although they may be equal.
/// This is to ensure the entire acceptance period is covered, not an offset acceptance period
/// starting from the time at which the teyrchain perceives a code upgrade as having occurred.
/// Multiple entries for a single para are permitted. Ordered ascending by block number.
#[pezpallet::storage]
pub(super) type PastCodePruning =
StorageValue<_, Vec<(ParaId, BlockNumberFor)>, ValueQuery>;
/// The block number at which the planned code change is expected for a teyrchain.
///
/// The change will be applied after the first parablock for this ID included which executes
/// in the context of a relay chain block with a number >= `expected_at`.
#[pezpallet::storage]
pub type FutureCodeUpgrades = StorageMap<_, Twox64Concat, ParaId, BlockNumberFor>;
/// The list of upcoming future code upgrades.
///
/// Each item is a pair of the teyrchain and the expected block at which the upgrade should be
/// applied. The upgrade will be applied at the given relay chain block. In contrast to
/// [`FutureCodeUpgrades`] this code upgrade will be applied regardless the teyrchain making any
/// progress or not.
///
/// Ordered ascending by block number.
#[pezpallet::storage]
pub(super) type FutureCodeUpgradesAt =
StorageValue<_, Vec<(ParaId, BlockNumberFor)>, ValueQuery>;
/// The actual future code hash of a para.
///
/// Corresponding code can be retrieved with [`CodeByHash`].
#[pezpallet::storage]
pub type FutureCodeHash = StorageMap<_, Twox64Concat, ParaId, ValidationCodeHash>;
/// The code hash authorizations for a para which will expire `expire_at` `BlockNumberFor`.
#[pezpallet::storage]
pub type AuthorizedCodeHash =
StorageMap<_, Twox64Concat, ParaId, AuthorizedCodeHashAndExpiry>>;
/// This is used by the relay-chain to communicate to a teyrchain a go-ahead with in the upgrade
/// procedure.
///
/// This value is absent when there are no upgrades scheduled or during the time the relay chain
/// performs the checks. It is set at the first relay-chain block when the corresponding
/// teyrchain can switch its upgrade function. As soon as the teyrchain's block is included, the
/// value gets reset to `None`.
///
/// NOTE that this field is used by teyrchains via merkle storage proofs, therefore changing
/// the format will require migration of teyrchains.
#[pezpallet::storage]
pub(super) type UpgradeGoAheadSignal =
StorageMap<_, Twox64Concat, ParaId, UpgradeGoAhead>;
/// This is used by the relay-chain to communicate that there are restrictions for performing
/// an upgrade for this teyrchain.
///
/// This may be a because the teyrchain waits for the upgrade cooldown to expire. Another
/// potential use case is when we want to perform some maintenance (such as storage migration)
/// we could restrict upgrades to make the process simpler.
///
/// NOTE that this field is used by teyrchains via merkle storage proofs, therefore changing
/// the format will require migration of teyrchains.
#[pezpallet::storage]
pub type UpgradeRestrictionSignal =
StorageMap<_, Twox64Concat, ParaId, UpgradeRestriction>;
/// The list of teyrchains that are awaiting for their upgrade restriction to cooldown.
///
/// Ordered ascending by block number.
#[pezpallet::storage]
pub(super) type UpgradeCooldowns =
StorageValue<_, Vec<(ParaId, BlockNumberFor)>, ValueQuery>;
/// The list of upcoming code upgrades.
///
/// Each item is a pair of which para performs a code upgrade and at which relay-chain block it
/// is expected at.
///
/// Ordered ascending by block number.
#[pezpallet::storage]
pub(super) type UpcomingUpgrades =
StorageValue<_, Vec<(ParaId, BlockNumberFor)>, ValueQuery>;
/// The actions to perform during the start of a specific session index.
#[pezpallet::storage]
pub type ActionsQueue =
StorageMap<_, Twox64Concat, SessionIndex, Vec, ValueQuery>;
/// Upcoming paras instantiation arguments.
///
/// NOTE that after PVF pre-checking is enabled the para genesis arg will have it's code set
/// to empty. Instead, the code will be saved into the storage right away via `CodeByHash`.
#[pezpallet::storage]
pub(super) type UpcomingParasGenesis =
StorageMap<_, Twox64Concat, ParaId, ParaGenesisArgs>;
/// The number of reference on the validation code in [`CodeByHash`] storage.
#[pezpallet::storage]
pub(super) type CodeByHashRefs =
StorageMap<_, Identity, ValidationCodeHash, u32, ValueQuery>;
/// Validation code stored by its hash.
///
/// This storage is consistent with [`FutureCodeHash`], [`CurrentCodeHash`] and
/// [`PastCodeHash`].
#[pezpallet::storage]
pub type CodeByHash = StorageMap<_, Identity, ValidationCodeHash, ValidationCode>;
#[pezpallet::genesis_config]
#[derive(DefaultNoBound)]
pub struct GenesisConfig {
#[serde(skip)]
pub _config: core::marker::PhantomData,
pub paras: Vec<(ParaId, ParaGenesisArgs)>,
}
#[pezpallet::genesis_build]
impl BuildGenesisConfig for GenesisConfig {
fn build(&self) {
let mut teyrchains = TeyrchainsCache::new();
for (id, genesis_args) in &self.paras {
if genesis_args.validation_code.0.is_empty() {
panic!("empty validation code is not allowed in genesis");
}
Pezpallet::::initialize_para_now(&mut teyrchains, *id, genesis_args);
if genesis_args.para_kind == ParaKind::Teyrchain {
T::AssignCoretime::assign_coretime(*id)
.expect("Assigning coretime works at genesis; qed");
}
}
// teyrchains are flushed on drop
}
}
#[pezpallet::call]
impl Pezpallet {
/// Set the storage for the teyrchain validation code immediately.
#[pezpallet::call_index(0)]
#[pezpallet::weight(::WeightInfo::force_set_current_code(new_code.0.len() as u32))]
pub fn force_set_current_code(
origin: OriginFor,
para: ParaId,
new_code: ValidationCode,
) -> DispatchResult {
ensure_root(origin)?;
Self::do_force_set_current_code_update(para, new_code);
Ok(())
}
/// Set the storage for the current teyrchain head data immediately.
#[pezpallet::call_index(1)]
#[pezpallet::weight(::WeightInfo::force_set_current_head(new_head.0.len() as u32))]
pub fn force_set_current_head(
origin: OriginFor,
para: ParaId,
new_head: HeadData,
) -> DispatchResult {
ensure_root(origin)?;
Self::set_current_head(para, new_head);
Ok(())
}
/// Schedule an upgrade as if it was scheduled in the given relay parent block.
#[pezpallet::call_index(2)]
#[pezpallet::weight(::WeightInfo::force_schedule_code_upgrade(new_code.0.len() as u32))]
pub fn force_schedule_code_upgrade(
origin: OriginFor,
para: ParaId,
new_code: ValidationCode,
relay_parent_number: BlockNumberFor,
) -> DispatchResult {
ensure_root(origin)?;
let config = configuration::ActiveConfig::::get();
Self::schedule_code_upgrade(
para,
new_code,
relay_parent_number,
&config,
UpgradeStrategy::ApplyAtExpectedBlock,
);
Self::deposit_event(Event::CodeUpgradeScheduled(para));
Ok(())
}
/// Note a new block head for para within the context of the current block.
#[pezpallet::call_index(3)]
#[pezpallet::weight(::WeightInfo::force_note_new_head(new_head.0.len() as u32))]
pub fn force_note_new_head(
origin: OriginFor,
para: ParaId,
new_head: HeadData,
) -> DispatchResult {
ensure_root(origin)?;
let now = pezframe_system::Pezpallet::::block_number();
Self::note_new_head(para, new_head, now);
Self::deposit_event(Event::NewHeadNoted(para));
Ok(())
}
/// Put a teyrchain directly into the next session's action queue.
/// We can't queue it any sooner than this without going into the
/// initializer...
#[pezpallet::call_index(4)]
#[pezpallet::weight(::WeightInfo::force_queue_action())]
pub fn force_queue_action(origin: OriginFor, para: ParaId) -> DispatchResult {
ensure_root(origin)?;
let next_session = shared::CurrentSessionIndex::::get().saturating_add(One::one());
ActionsQueue::::mutate(next_session, |v| {
if let Err(i) = v.binary_search(¶) {
v.insert(i, para);
}
});
Self::deposit_event(Event::ActionQueued(para, next_session));
Ok(())
}
/// Adds the validation code to the storage.
///
/// The code will not be added if it is already present. Additionally, if PVF pre-checking
/// is running for that code, it will be instantly accepted.
///
/// Otherwise, the code will be added into the storage. Note that the code will be added
/// into storage with reference count 0. This is to account the fact that there are no users
/// for this code yet. The caller will have to make sure that this code eventually gets
/// used by some teyrchain or removed from the storage to avoid storage leaks. For the
/// latter prefer to use the `poke_unused_validation_code` dispatchable to raw storage
/// manipulation.
///
/// This function is mainly meant to be used for upgrading teyrchains that do not follow
/// the go-ahead signal while the PVF pre-checking feature is enabled.
#[pezpallet::call_index(5)]
#[pezpallet::weight(::WeightInfo::add_trusted_validation_code(validation_code.0.len() as u32))]
pub fn add_trusted_validation_code(
origin: OriginFor,
validation_code: ValidationCode,
) -> DispatchResult {
ensure_root(origin)?;
let code_hash = validation_code.hash();
if let Some(vote) = PvfActiveVoteMap::::get(&code_hash) {
// Remove the existing vote.
PvfActiveVoteMap::::remove(&code_hash);
PvfActiveVoteList::::mutate(|l| {
if let Ok(i) = l.binary_search(&code_hash) {
l.remove(i);
}
});
let cfg = configuration::ActiveConfig::::get();
Self::enact_pvf_accepted(
pezframe_system::Pezpallet::::block_number(),
&code_hash,
&vote.causes,
vote.age,
&cfg,
);
return Ok(());
}
if CodeByHash::::contains_key(&code_hash) {
// There is no vote, but the code exists. Nothing to do here.
return Ok(());
}
// At this point the code is unknown and there is no PVF pre-checking vote for it, so we
// can just add the code into the storage.
//
// NOTE That we do not use `increase_code_ref` here, because the code is not yet used
// by any teyrchain.
CodeByHash::::insert(code_hash, &validation_code);
Ok(())
}
/// Remove the validation code from the storage iff the reference count is 0.
///
/// This is better than removing the storage directly, because it will not remove the code
/// that was suddenly got used by some teyrchain while this dispatchable was pending
/// dispatching.
#[pezpallet::call_index(6)]
#[pezpallet::weight(::WeightInfo::poke_unused_validation_code())]
pub fn poke_unused_validation_code(
origin: OriginFor,
validation_code_hash: ValidationCodeHash,
) -> DispatchResult {
ensure_root(origin)?;
if CodeByHashRefs::::get(&validation_code_hash) == 0 {
CodeByHash::::remove(&validation_code_hash);
}
Ok(())
}
/// Includes a statement for a PVF pre-checking vote. Potentially, finalizes the vote and
/// enacts the results if that was the last vote before achieving the supermajority.
#[pezpallet::call_index(7)]
#[pezpallet::weight(
::WeightInfo::include_pvf_check_statement_finalize_upgrade_accept()
.max(::WeightInfo::include_pvf_check_statement_finalize_upgrade_reject())
.max(::WeightInfo::include_pvf_check_statement_finalize_onboarding_accept()
.max(::WeightInfo::include_pvf_check_statement_finalize_onboarding_reject())
)
)]
pub fn include_pvf_check_statement(
origin: OriginFor,
stmt: PvfCheckStatement,
signature: ValidatorSignature,
) -> DispatchResultWithPostInfo {
ensure_none(origin)?;
let validators = shared::ActiveValidatorKeys::::get();
let current_session = shared::CurrentSessionIndex::::get();
if stmt.session_index < current_session {
return Err(Error::::PvfCheckStatementStale.into());
} else if stmt.session_index > current_session {
return Err(Error::::PvfCheckStatementFuture.into());
}
let validator_index = stmt.validator_index.0 as usize;
let validator_public = validators
.get(validator_index)
.ok_or(Error::::PvfCheckValidatorIndexOutOfBounds)?;
let signing_payload = stmt.signing_payload();
ensure!(
signature.verify(&signing_payload[..], &validator_public),
Error::::PvfCheckInvalidSignature,
);
let mut active_vote = PvfActiveVoteMap::::get(&stmt.subject)
.ok_or(Error::::PvfCheckSubjectInvalid)?;
// Ensure that the validator submitting this statement hasn't voted already.
ensure!(
!active_vote
.has_vote(validator_index)
.ok_or(Error::::PvfCheckValidatorIndexOutOfBounds)?,
Error::::PvfCheckDoubleVote,
);
// Finally, cast the vote and persist.
if stmt.accept {
active_vote.votes_accept.set(validator_index, true);
} else {
active_vote.votes_reject.set(validator_index, true);
}
if let Some(outcome) = active_vote.quorum(validators.len()) {
// The quorum has been achieved.
//
// Remove the PVF vote from the active map and finalize the PVF checking according
// to the outcome.
PvfActiveVoteMap::::remove(&stmt.subject);
PvfActiveVoteList::::mutate(|l| {
if let Ok(i) = l.binary_search(&stmt.subject) {
l.remove(i);
}
});
match outcome {
PvfCheckOutcome::Accepted => {
let cfg = configuration::ActiveConfig::::get();
Self::enact_pvf_accepted(
pezframe_system::Pezpallet::::block_number(),
&stmt.subject,
&active_vote.causes,
active_vote.age,
&cfg,
);
},
PvfCheckOutcome::Rejected => {
Self::enact_pvf_rejected(&stmt.subject, active_vote.causes);
},
}
// No weight refund since this statement was the last one and lead to finalization.
Ok(().into())
} else {
// No quorum has been achieved.
//
// - So just store the updated state back into the storage.
// - Only charge weight for simple vote inclusion.
PvfActiveVoteMap::::insert(&stmt.subject, active_vote);
Ok(Some(::WeightInfo::include_pvf_check_statement()).into())
}
}
/// Set the storage for the current teyrchain head data immediately.
#[pezpallet::call_index(8)]
#[pezpallet::weight(::WeightInfo::force_set_most_recent_context())]
pub fn force_set_most_recent_context(
origin: OriginFor,
para: ParaId,
context: BlockNumberFor,
) -> DispatchResult {
ensure_root(origin)?;
MostRecentContext::::insert(¶, context);
Ok(())
}
/// Remove an upgrade cooldown for a teyrchain.
///
/// The cost for removing the cooldown earlier depends on the time left for the cooldown
/// multiplied by [`Config::CooldownRemovalMultiplier`]. The paid tokens are burned.
#[pezpallet::call_index(9)]
#[pezpallet::weight(::WeightInfo::remove_upgrade_cooldown())]
pub fn remove_upgrade_cooldown(origin: OriginFor, para: ParaId) -> DispatchResult {
let who = ensure_signed(origin)?;
let removed = UpgradeCooldowns::::mutate(|cooldowns| {
let Some(pos) = cooldowns.iter().position(|(p, _)| p == ¶) else {
return Ok::<_, DispatchError>(false);
};
let (_, cooldown_until) = cooldowns.remove(pos);
let cost = Self::calculate_remove_upgrade_cooldown_cost(cooldown_until);
// burn...
T::Fungible::burn_from(
&who,
cost,
Preservation::Preserve,
Precision::Exact,
Fortitude::Polite,
)?;
Ok(true)
})?;
if removed {
UpgradeRestrictionSignal::::remove(para);
Self::deposit_event(Event::UpgradeCooldownRemoved { para_id: para });
}
Ok(())
}
/// Sets the storage for the authorized current code hash of the teyrchain.
/// If not applied, it will be removed at the `System::block_number() + valid_period` block.
///
/// This can be useful, when triggering `Paras::force_set_current_code(para, code)`
/// from a different chain than the one where the `Paras` pezpallet is deployed.
///
/// The main purpose is to avoid transferring the entire `code` Wasm blob between chains.
/// Instead, we authorize `code_hash` with `root`, which can later be applied by
/// `Paras::apply_authorized_force_set_current_code(para, code)` by anyone.
///
/// Authorizations are stored in an **overwriting manner**.
#[pezpallet::call_index(10)]
#[pezpallet::weight(::WeightInfo::authorize_force_set_current_code_hash())]
pub fn authorize_force_set_current_code_hash(
origin: OriginFor,
para: ParaId,
new_code_hash: ValidationCodeHash,
valid_period: BlockNumberFor,
) -> DispatchResult {
T::AuthorizeCurrentCodeOrigin::ensure_origin(origin, ¶)?;
// The requested para must be a valid para (neither onboarding nor offboarding).
ensure!(Self::is_valid_para(para), Error::::NotRegistered);
let now = pezframe_system::Pezpallet::::block_number();
let expire_at = now.saturating_add(valid_period);
// Insert the authorized code hash and ensure it overwrites the existing one for a para.
AuthorizedCodeHash::::insert(
¶,
AuthorizedCodeHashAndExpiry::from((new_code_hash, expire_at)),
);
Self::deposit_event(Event::CodeAuthorized {
para_id: para,
code_hash: new_code_hash,
expire_at,
});
Ok(())
}
/// Applies the already authorized current code for the teyrchain,
/// triggering the same functionality as `force_set_current_code`.
#[pezpallet::call_index(11)]
#[pezpallet::weight(::WeightInfo::apply_authorized_force_set_current_code(new_code.0.len() as u32))]
pub fn apply_authorized_force_set_current_code(
_origin: OriginFor,
para: ParaId,
new_code: ValidationCode,
) -> DispatchResultWithPostInfo {
// no need to ensure anybody can do this
// Ensure `new_code` is authorized
let _ = Self::validate_code_is_authorized(&new_code, ¶)?;
// Remove authorization
AuthorizedCodeHash::::remove(para);
// apply/dispatch
Self::do_force_set_current_code_update(para, new_code);
Ok(Pays::No.into())
}
}
impl Pezpallet {
pub(crate) fn calculate_remove_upgrade_cooldown_cost(
cooldown_until: BlockNumberFor,
) -> BalanceOf {
let time_left =
cooldown_until.saturating_sub(pezframe_system::Pezpallet::::block_number());
BalanceOf::::from(time_left).saturating_mul(T::CooldownRemovalMultiplier::get())
}
}
#[pezpallet::view_functions]
impl Pezpallet {
/// Returns the cost for removing an upgrade cooldown for the given `para`.
pub fn remove_upgrade_cooldown_cost(para: ParaId) -> BalanceOf {
UpgradeCooldowns::::get()
.iter()
.find(|(p, _)| p == ¶)
.map(|(_, c)| Self::calculate_remove_upgrade_cooldown_cost(*c))
.unwrap_or_default()
}
}
#[pezpallet::validate_unsigned]
impl ValidateUnsigned for Pezpallet {
type Call = Call;
fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity {
match call {
Call::include_pvf_check_statement { stmt, signature } => {
let current_session = shared::CurrentSessionIndex::::get();
if stmt.session_index < current_session {
return InvalidTransaction::Stale.into();
} else if stmt.session_index > current_session {
return InvalidTransaction::Future.into();
}
let validator_index = stmt.validator_index.0 as usize;
let validators = shared::ActiveValidatorKeys::::get();
let validator_public = match validators.get(validator_index) {
Some(pk) => pk,
None =>
return InvalidTransaction::Custom(INVALID_TX_BAD_VALIDATOR_IDX).into(),
};
let signing_payload = stmt.signing_payload();
if !signature.verify(&signing_payload[..], &validator_public) {
return InvalidTransaction::BadProof.into();
}
let active_vote = match PvfActiveVoteMap::::get(&stmt.subject) {
Some(v) => v,
None => return InvalidTransaction::Custom(INVALID_TX_BAD_SUBJECT).into(),
};
match active_vote.has_vote(validator_index) {
Some(false) => (),
Some(true) =>
return InvalidTransaction::Custom(INVALID_TX_DOUBLE_VOTE).into(),
None =>
return InvalidTransaction::Custom(INVALID_TX_BAD_VALIDATOR_IDX).into(),
}
ValidTransaction::with_tag_prefix("PvfPreCheckingVote")
.priority(T::UnsignedPriority::get())
.longevity(
TryInto::::try_into(
T::NextSessionRotation::average_session_length() / 2u32.into(),
)
.unwrap_or(64_u64),
)
.and_provides((stmt.session_index, stmt.validator_index, stmt.subject))
.propagate(true)
.build()
},
Call::apply_authorized_force_set_current_code { para, new_code } =>
match Self::validate_code_is_authorized(new_code, para) {
Ok(authorized_code) => {
let now = pezframe_system::Pezpallet::::block_number();
let longevity = authorized_code.expire_at.saturating_sub(now);
ValidTransaction::with_tag_prefix("ApplyAuthorizedForceSetCurrentCode")
.priority(T::UnsignedPriority::get())
.longevity(TryInto::::try_into(longevity).unwrap_or(64_u64))
.and_provides((para, authorized_code.code_hash))
.propagate(true)
.build()
},
Err(_) =>
return InvalidTransaction::Custom(INVALID_TX_UNAUTHORIZED_CODE).into(),
},
_ => InvalidTransaction::Call.into(),
}
}
fn pre_dispatch(_call: &Self::Call) -> Result<(), TransactionValidityError> {
// Return `Ok` here meaning that as soon as the transaction got into the block, it will
// always dispatched. This is OK, since the `include_pvf_check_statement` dispatchable
// will perform the same checks anyway, so there is no point doing it here.
//
// On the other hand, if we did not provide the implementation, then the default
// implementation would be used. The default implementation just delegates the
// pre-dispatch validation to `validate_unsigned`.
Ok(())
}
}
}
// custom transaction error codes
const INVALID_TX_BAD_VALIDATOR_IDX: u8 = 1;
const INVALID_TX_BAD_SUBJECT: u8 = 2;
const INVALID_TX_DOUBLE_VOTE: u8 = 3;
const INVALID_TX_UNAUTHORIZED_CODE: u8 = 4;
/// This is intermediate "fix" for this issue:
///
///
/// It does not actually fix it, but makes the worst case better. Without that limit someone
/// could completely DoS the relay chain by registering a ridiculously high amount of paras.
/// With this limit the same attack could lead to some teyrchains ceasing to being able to
/// communicate via offchain XCMP. Snowbridge will still work as it only cares about `BridgeHub`.
pub const MAX_PARA_HEADS: usize = 1024;
impl Pezpallet {
/// This is a call to schedule code upgrades for teyrchains which is safe to be called
/// outside of this module. That means this function does all checks necessary to ensure
/// that some external code is allowed to trigger a code upgrade. We do not do auth checks,
/// that should be handled by whomever calls this function.
pub(crate) fn schedule_code_upgrade_external(
id: ParaId,
new_code: ValidationCode,
upgrade_strategy: UpgradeStrategy,
) -> DispatchResult {
// Check that we can schedule an upgrade at all.
ensure!(Self::can_upgrade_validation_code(id), Error::::CannotUpgradeCode);
let config = configuration::ActiveConfig::::get();
// Validation code sanity checks:
ensure!(new_code.0.len() >= MIN_CODE_SIZE as usize, Error::::InvalidCode);
ensure!(new_code.0.len() <= config.max_code_size as usize, Error::::InvalidCode);
let current_block = pezframe_system::Pezpallet::::block_number();
// Schedule the upgrade with a delay just like if a teyrchain triggered the upgrade.
let upgrade_block = current_block.saturating_add(config.validation_upgrade_delay);
Self::schedule_code_upgrade(id, new_code, upgrade_block, &config, upgrade_strategy);
Self::deposit_event(Event::CodeUpgradeScheduled(id));
Ok(())
}
/// Set the current head of a teyrchain.
pub(crate) fn set_current_head(para: ParaId, new_head: HeadData) {
Heads::::insert(¶, new_head);
Self::deposit_event(Event::CurrentHeadUpdated(para));
}
/// Called by the initializer to initialize the paras pezpallet.
pub(crate) fn initializer_initialize(now: BlockNumberFor) -> Weight {
Self::prune_old_code(now) +
Self::process_scheduled_upgrade_changes(now) +
Self::process_future_code_upgrades_at(now) +
Self::prune_expired_authorizations(now)
}
/// Called by the initializer to finalize the paras pezpallet.
pub(crate) fn initializer_finalize(now: BlockNumberFor) {
Self::process_scheduled_upgrade_cooldowns(now);
}
/// Called by the initializer to note that a new session has started.
///
/// Returns the list of outgoing paras from the actions queue.
pub(crate) fn initializer_on_new_session(
notification: &SessionChangeNotification>,
) -> Vec {
let outgoing_paras = Self::apply_actions_queue(notification.session_index);
Self::groom_ongoing_pvf_votes(¬ification.new_config, notification.validators.len());
outgoing_paras
}
/// The validation code of live para.
pub(crate) fn current_code(para_id: &ParaId) -> Option {
CurrentCodeHash::::get(para_id).and_then(|code_hash| {
let code = CodeByHash::::get(&code_hash);
if code.is_none() {
log::error!(
"Pezpallet paras storage is inconsistent, code not found for hash {}",
code_hash,
);
debug_assert!(false, "inconsistent paras storages");
}
code
})
}
/// Get a list of the first [`MAX_PARA_HEADS`] para heads sorted by para_id.
/// This method is likely to be removed in the future.
pub fn sorted_para_heads() -> Vec<(u32, Vec)> {
let mut heads: Vec<(u32, Vec)> =
Heads::::iter().map(|(id, head)| (id.into(), head.0)).collect();
heads.sort_by_key(|(id, _)| *id);
heads.truncate(MAX_PARA_HEADS);
heads
}
// Apply all para actions queued for the given session index.
//
// The actions to take are based on the lifecycle of of the paras.
//
// The final state of any para after the actions queue should be as a
// lease holding teyrchain, on-demand teyrchain, or not registered. (stable states)
//
// Returns the list of outgoing paras from the actions queue.
fn apply_actions_queue(session: SessionIndex) -> Vec {
let actions = ActionsQueue::::take(session);
let mut teyrchains = TeyrchainsCache::new();
let now = pezframe_system::Pezpallet::::block_number();
let mut outgoing = Vec::new();
for para in actions {
let lifecycle = ParaLifecycles::::get(¶);
match lifecycle {
None | Some(ParaLifecycle::Parathread) | Some(ParaLifecycle::Teyrchain) => { /* Nothing to do... */
},
Some(ParaLifecycle::Onboarding) => {
if let Some(genesis_data) = UpcomingParasGenesis::::take(¶) {
Self::initialize_para_now(&mut teyrchains, para, &genesis_data);
}
},
// Upgrade an on-demand teyrchain to a lease holding teyrchain
Some(ParaLifecycle::UpgradingParathread) => {
teyrchains.add(para);
ParaLifecycles::::insert(¶, ParaLifecycle::Teyrchain);
},
// Downgrade a lease holding teyrchain to an on-demand teyrchain
Some(ParaLifecycle::DowngradingTeyrchain) => {
teyrchains.remove(para);
ParaLifecycles::::insert(¶, ParaLifecycle::Parathread);
},
// Offboard a lease holding or on-demand teyrchain from the system
Some(ParaLifecycle::OffboardingTeyrchain) |
Some(ParaLifecycle::OffboardingParathread) => {
teyrchains.remove(para);
Heads::::remove(¶);
MostRecentContext::::remove(¶);
FutureCodeUpgrades::::remove(¶);
UpgradeGoAheadSignal::::remove(¶);
UpgradeRestrictionSignal::::remove(¶);
ParaLifecycles::::remove(¶);
AuthorizedCodeHash::::remove(¶);
let removed_future_code_hash = FutureCodeHash::::take(¶);
if let Some(removed_future_code_hash) = removed_future_code_hash {
Self::decrease_code_ref(&removed_future_code_hash);
}
let removed_code_hash = CurrentCodeHash::::take(¶);
if let Some(removed_code_hash) = removed_code_hash {
Self::note_past_code(para, now, now, removed_code_hash);
}
outgoing.push(para);
},
}
}
if !outgoing.is_empty() {
// Filter offboarded teyrchains from the upcoming upgrades and upgrade cooldowns list.
//
// We do it after the offboarding to get away with only a single read/write per list.
//
// NOTE both of those iterates over the list and the outgoing. We do not expect either
// of these to be large. Thus should be fine.
UpcomingUpgrades::::mutate(|upcoming_upgrades| {
upcoming_upgrades.retain(|(para, _)| !outgoing.contains(para));
});
UpgradeCooldowns::::mutate(|upgrade_cooldowns| {
upgrade_cooldowns.retain(|(para, _)| !outgoing.contains(para));
});
FutureCodeUpgradesAt::::mutate(|future_upgrades| {
future_upgrades.retain(|(para, _)| !outgoing.contains(para));
});
}
// Persist teyrchains into the storage explicitly.
drop(teyrchains);
outgoing
}
// note replacement of the code of para with given `id`, which occurred in the
// context of the given relay-chain block number. provide the replaced code.
//
// `at` for para-triggered replacement is the block number of the relay-chain
// block in whose context the parablock was executed
// (i.e. number of `relay_parent` in the receipt)
fn note_past_code(
id: ParaId,
at: BlockNumberFor,
now: BlockNumberFor,
old_code_hash: ValidationCodeHash,
) -> Weight {
PastCodeMeta::::mutate(&id, |past_meta| {
past_meta.note_replacement(at, now);
});
PastCodeHash::::insert(&(id, at), old_code_hash);
// Schedule pruning for this past-code to be removed as soon as it
// exits the slashing window.
PastCodePruning::::mutate(|pruning| {
let insert_idx =
pruning.binary_search_by_key(&now, |&(_, b)| b).unwrap_or_else(|idx| idx);
pruning.insert(insert_idx, (id, now));
});
T::DbWeight::get().reads_writes(2, 3)
}
// looks at old code metadata, compares them to the current acceptance window, and prunes those
// that are too old.
fn prune_old_code(now: BlockNumberFor) -> Weight {
let config = configuration::ActiveConfig::::get();
let code_retention_period = config.code_retention_period;
if now <= code_retention_period {
let weight = T::DbWeight::get().reads_writes(1, 0);
return weight;
}
// The height of any changes we no longer should keep around.
let pruning_height = now - (code_retention_period + One::one());
let pruning_tasks_done =
PastCodePruning::::mutate(|pruning_tasks: &mut Vec<(_, BlockNumberFor)>| {
let (pruning_tasks_done, pruning_tasks_to_do) = {
// find all past code that has just exited the pruning window.
let up_to_idx =
pruning_tasks.iter().take_while(|&(_, at)| at <= &pruning_height).count();
(up_to_idx, pruning_tasks.drain(..up_to_idx))
};
for (para_id, _) in pruning_tasks_to_do {
let full_deactivate = PastCodeMeta::::mutate(¶_id, |meta| {
for pruned_repl_at in meta.prune_up_to(pruning_height) {
let removed_code_hash =
PastCodeHash::::take(&(para_id, pruned_repl_at));
if let Some(removed_code_hash) = removed_code_hash {
Self::decrease_code_ref(&removed_code_hash);
} else {
log::warn!(
target: LOG_TARGET,
"Missing code for removed hash {:?}",
removed_code_hash,
);
}
}
meta.is_empty() && Heads::::get(¶_id).is_none()
});
// This teyrchain has been removed and now the vestigial code
// has been removed from the state. clean up meta as well.
if full_deactivate {
PastCodeMeta::::remove(¶_id);
}
}
pruning_tasks_done as u64
});
// 1 read for the meta for each pruning task, 1 read for the config
// 2 writes: updating the meta and pruning the code
T::DbWeight::get().reads_writes(1 + pruning_tasks_done, 2 * pruning_tasks_done)
}
/// This function removes authorizations that have expired,
/// meaning their `expire_at` block is less than or equal to the current block (`now`).
fn prune_expired_authorizations(now: BlockNumberFor) -> Weight {
let mut weight = T::DbWeight::get().reads(1);
let to_remove = AuthorizedCodeHash::::iter().filter_map(
|(para, AuthorizedCodeHashAndExpiry { expire_at, .. })| {
if expire_at <= now {
Some(para)
} else {
None
}
},
);
for para in to_remove {
AuthorizedCodeHash::::remove(¶);
weight.saturating_accrue(T::DbWeight::get().writes(1));
}
weight
}
/// Process the future code upgrades that should be applied directly.
///
/// Upgrades that should not be applied directly are being processed in
/// [`Self::process_scheduled_upgrade_changes`].
fn process_future_code_upgrades_at(now: BlockNumberFor) -> Weight {
// account weight for `FutureCodeUpgradeAt::mutate`.
let mut weight = T::DbWeight::get().reads_writes(1, 1);
FutureCodeUpgradesAt::::mutate(
|upcoming_upgrades: &mut Vec<(ParaId, BlockNumberFor)>| {
let num = upcoming_upgrades.iter().take_while(|&(_, at)| at <= &now).count();
for (id, expected_at) in upcoming_upgrades.drain(..num) {
weight += T::DbWeight::get().reads_writes(1, 1);
// Both should always be `Some` in this case, since a code upgrade is scheduled.
let new_code_hash = if let Some(new_code_hash) = FutureCodeHash::::take(&id)
{
new_code_hash
} else {
log::error!(target: LOG_TARGET, "Missing future code hash for {:?}", &id);
continue;
};
weight += Self::set_current_code(id, new_code_hash, expected_at);
}
num
},
);
weight
}
/// Process the timers related to upgrades. Specifically, the upgrade go ahead signals toggle
/// and the upgrade cooldown restrictions. However, this function does not actually unset
/// the upgrade restriction, that will happen in the `initializer_finalize` function. However,
/// this function does count the number of cooldown timers expired so that we can reserve weight
/// for the `initializer_finalize` function.
fn process_scheduled_upgrade_changes(now: BlockNumberFor) -> Weight {
// account weight for `UpcomingUpgrades::mutate`.
let mut weight = T::DbWeight::get().reads_writes(1, 1);
let upgrades_signaled = UpcomingUpgrades::::mutate(
|upcoming_upgrades: &mut Vec<(ParaId, BlockNumberFor)>| {
let num = upcoming_upgrades.iter().take_while(|&(_, at)| at <= &now).count();
for (para, _) in upcoming_upgrades.drain(..num) {
UpgradeGoAheadSignal::::insert(¶, UpgradeGoAhead::GoAhead);
}
num
},
);
weight += T::DbWeight::get().writes(upgrades_signaled as u64);
// account weight for `UpgradeCooldowns::get`.
weight += T::DbWeight::get().reads(1);
let cooldowns_expired =
UpgradeCooldowns::::get().iter().take_while(|&(_, at)| at <= &now).count();
// reserve weight for `initializer_finalize`:
// - 1 read and 1 write for `UpgradeCooldowns::mutate`.
// - 1 write per expired cooldown.
weight += T::DbWeight::get().reads_writes(1, 1);
weight += T::DbWeight::get().reads(cooldowns_expired as u64);
weight
}
/// Actually perform unsetting the expired upgrade restrictions.
///
/// See `process_scheduled_upgrade_changes` for more details.
fn process_scheduled_upgrade_cooldowns(now: BlockNumberFor) {
UpgradeCooldowns::::mutate(
|upgrade_cooldowns: &mut Vec<(ParaId, BlockNumberFor)>| {
// Remove all expired signals and also prune the cooldowns.
upgrade_cooldowns.retain(|(para, at)| {
if at <= &now {
UpgradeRestrictionSignal::::remove(¶);
false
} else {
true
}
});
},
);
}
/// Goes over all PVF votes in progress, reinitializes ballots, increments ages and prunes the
/// active votes that reached their time-to-live.
fn groom_ongoing_pvf_votes(
cfg: &configuration::HostConfiguration>,
new_n_validators: usize,
) -> Weight {
let mut weight = T::DbWeight::get().reads(1);
let potentially_active_votes = PvfActiveVoteList::::get();
// Initially empty list which contains all the PVF active votes that made it through this
// session change.
//
// **Ordered** as well as `PvfActiveVoteList`.
let mut actually_active_votes = Vec::with_capacity(potentially_active_votes.len());
for vote_subject in potentially_active_votes {
let mut vote_state = match PvfActiveVoteMap::::take(&vote_subject) {
Some(v) => v,
None => {
// This branch should never be reached. This is due to the fact that the set of
// `PvfActiveVoteMap`'s keys is always equal to the set of items found in
// `PvfActiveVoteList`.
log::warn!(
target: LOG_TARGET,
"The PvfActiveVoteMap is out of sync with PvfActiveVoteList!",
);
debug_assert!(false);
continue;
},
};
vote_state.age += 1;
if vote_state.age < cfg.pvf_voting_ttl {
weight += T::DbWeight::get().writes(1);
vote_state.reinitialize_ballots(new_n_validators);
PvfActiveVoteMap::::insert(&vote_subject, vote_state);
// push maintaining the original order.
actually_active_votes.push(vote_subject);
} else {
// TTL is reached. Reject.
weight += Self::enact_pvf_rejected(&vote_subject, vote_state.causes);
}
}
weight += T::DbWeight::get().writes(1);
PvfActiveVoteList::::put(actually_active_votes);
weight
}
fn enact_pvf_accepted(
now: BlockNumberFor,
code_hash: &ValidationCodeHash,
causes: &[PvfCheckCause>],
sessions_observed: SessionIndex,
cfg: &configuration::HostConfiguration>,
) -> Weight {
let mut weight = Weight::zero();
for cause in causes {
weight += T::DbWeight::get().reads_writes(3, 2);
Self::deposit_event(Event::PvfCheckAccepted(*code_hash, cause.para_id()));
match cause {
PvfCheckCause::Onboarding(id) => {
weight += Self::proceed_with_onboarding(*id, sessions_observed);
},
PvfCheckCause::Upgrade { id, included_at, upgrade_strategy } => {
weight += Self::proceed_with_upgrade(
*id,
code_hash,
now,
*included_at,
cfg,
*upgrade_strategy,
);
},
}
}
weight
}
fn proceed_with_onboarding(id: ParaId, sessions_observed: SessionIndex) -> Weight {
let weight = T::DbWeight::get().reads_writes(2, 1);
// we should onboard only after `SESSION_DELAY` sessions but we should take
// into account the number of sessions the PVF pre-checking occupied.
//
// we cannot onboard at the current session, so it must be at least one
// session ahead.
let onboard_at: SessionIndex = shared::CurrentSessionIndex::::get() +
cmp::max(shared::SESSION_DELAY.saturating_sub(sessions_observed), 1);
ActionsQueue::::mutate(onboard_at, |v| {
if let Err(i) = v.binary_search(&id) {
v.insert(i, id);
}
});
weight
}
fn proceed_with_upgrade(
id: ParaId,
code_hash: &ValidationCodeHash,
now: BlockNumberFor,
relay_parent_number: BlockNumberFor,
cfg: &configuration::HostConfiguration