From 90fa441b0d46b007f781198fad209920cc3cd4de Mon Sep 17 00:00:00 2001 From: Branislav Kontur <bkontur@gmail.com> Date: Mon, 17 Feb 2025 11:37:30 +0100 Subject: [PATCH] Added `authorize_force_set_current_code_hash` feature --- .../parachains/src/paras/benchmarking.rs | 24 ++++ polkadot/runtime/parachains/src/paras/mod.rs | 112 +++++++++++++++++- .../runtime/parachains/src/paras/tests.rs | 100 ++++++++++++++++ 3 files changed, 231 insertions(+), 5 deletions(-) diff --git a/polkadot/runtime/parachains/src/paras/benchmarking.rs b/polkadot/runtime/parachains/src/paras/benchmarking.rs index 4d617cbb05b..8f5b48b0521 100644 --- a/polkadot/runtime/parachains/src/paras/benchmarking.rs +++ b/polkadot/runtime/parachains/src/paras/benchmarking.rs @@ -249,6 +249,30 @@ mod benchmarks { } } + #[benchmark] + fn authorize_force_set_current_code_hash() { + let para_id = ParaId::from(1000); + let code_hash = [0; 32].into(); + + #[extrinsic_call] + _(RawOrigin::Root, para_id, code_hash); + + assert_last_event::<T>(Event::CodeAuthorized { para_id, code_hash }.into()); + } + + #[benchmark] + fn apply_authorized_force_set_current_code(c: Linear<MIN_CODE_SIZE, MAX_CODE_SIZE>) { + let new_code = ValidationCode(vec![0; c as usize]); + let para_id = ParaId::from(c as u32); + AuthorizedCodeHash::<T>::insert(¶_id, new_code.hash()); + generate_disordered_pruning::<T>(); + + #[extrinsic_call] + _(RawOrigin::Root, para_id, new_code); + + assert_last_event::<T>(Event::CurrentCodeUpdated(para_id).into()); + } + impl_benchmark_test_suite!( Pallet, crate::mock::new_test_ext(Default::default()), diff --git a/polkadot/runtime/parachains/src/paras/mod.rs b/polkadot/runtime/parachains/src/paras/mod.rs index e0f244dbd86..622969539dc 100644 --- a/polkadot/runtime/parachains/src/paras/mod.rs +++ b/polkadot/runtime/parachains/src/paras/mod.rs @@ -117,7 +117,10 @@ 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 frame_support::{pallet_prelude::*, traits::EstimateNextSessionRotation, DefaultNoBound}; +use frame_support::{ + dispatch::PostDispatchInfo, pallet_prelude::*, traits::EstimateNextSessionRotation, + DefaultNoBound +}; use frame_system::pallet_prelude::*; use polkadot_primitives::{ ConsensusLog, HeadData, Id as ParaId, PvfCheckStatement, SessionIndex, UpgradeGoAhead, @@ -551,6 +554,8 @@ pub trait WeightInfo { 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; @@ -596,6 +601,12 @@ impl WeightInfo for TestWeightInfo { // This special value is to distinguish from the finalizing variants above in tests. Weight::MAX - Weight::from_parts(1, 1) } + fn authorize_force_set_current_code_hash() -> Weight { + Weight::MAX + } + fn apply_authorized_force_set_current_code(_c: u32) -> Weight { + Weight::MAX + } } #[frame_support::pallet] @@ -649,6 +660,11 @@ pub mod pallet { pub enum Event { /// Current code has been updated for a Para. `para_id` CurrentCodeUpdated(ParaId), + /// New code hash has been authorized for a Para. + CodeAuthorized { + code_hash: ValidationCodeHash, + para_id: ParaId, + }, /// Current head has been updated for a Para. `para_id` CurrentHeadUpdated(ParaId), /// A code upgrade has been scheduled for a Para. `para_id` @@ -791,6 +807,10 @@ pub mod pallet { #[pallet::storage] pub type FutureCodeHash<T: Config> = StorageMap<_, Twox64Concat, ParaId, ValidationCodeHash>; + /// The code hash of a para which is authorized. + #[pallet::storage] + pub(super) type AuthorizedCodeHash<T: Config> = StorageMap<_, Twox64Concat, ParaId, ValidationCodeHash>; + /// This is used by the relay-chain to communicate to a parachain a go-ahead with in the upgrade /// procedure. /// @@ -895,10 +915,7 @@ pub mod pallet { new_code: ValidationCode, ) -> DispatchResult { ensure_root(origin)?; - let new_code_hash = new_code.hash(); - Self::increase_code_ref(&new_code_hash, &new_code); - Self::set_current_code(para, new_code_hash, frame_system::Pallet::<T>::block_number()); - Self::deposit_event(Event::CurrentCodeUpdated(para)); + Self::do_force_set_current_code_update(para, new_code); Ok(()) } @@ -1149,6 +1166,80 @@ pub mod pallet { MostRecentContext::<T>::insert(¶, context); Ok(()) } + + /// Sets the storage for the authorized current code hash of the parachain. + /// + /// This can be useful when we want to trigger `Paras::force_set_current_code(para, code)` + /// from a different chain than the one where the `Paras` pallet is deployed. + /// + /// The main reason is to avoid transferring the entire `new_code` wasm blob between chains. + /// Instead, we authorize `new_code_hash` with `root`, which can later be applied by + /// `Paras::apply_authorized_force_set_current_code(para, new_code)` by anyone. + #[pallet::call_index(9)] + #[pallet::weight(<T as Config>::WeightInfo::authorize_force_set_current_code_hash())] + pub fn authorize_force_set_current_code_hash( + origin: OriginFor<T>, + para: ParaId, + new_code_hash: ValidationCodeHash, + ) -> DispatchResult { + ensure_root(origin)?; + + // if one is already authorized, means it has not been applied yet, so we just replace it. + if let Some(already_authorized) = AuthorizedCodeHash::<T>::take(para) { + log::warn!( + target: LOG_TARGET, + "Already authorized code hash: {:?} found for para {:?}, just removing it!", + already_authorized, para + ); + } + + // TODO: FAIL-CI - more validations? + // do we need to check against `FutureCodeHash`, `CodeHashRef`, + // `PastCodeHash`,... code hashes? + + // insert authorized code hash. + AuthorizedCodeHash::<T>::insert(¶, new_code_hash); + Self::deposit_event(Event::CodeAuthorized { + para_id: para, + code_hash: new_code_hash, + }); + + Ok(()) + } + + /// Applies the already authorized current code for the parachain, + /// triggering the same functionality as `force_set_current_code`. + #[pallet::call_index(10)] + #[pallet::weight(<T as Config>::WeightInfo::apply_authorized_force_set_current_code(new_code.0.len() as u32))] + pub fn apply_authorized_force_set_current_code( + _origin: OriginFor<T>, + para: ParaId, + new_code: ValidationCode, + ) -> DispatchResultWithPostInfo { + // no need to ensure, anybody can do this + + // check `new_code` is authorized + let Some(authorized_code_hash) = AuthorizedCodeHash::<T>::take(¶) else { + log::error!(target: LOG_TARGET, "No authorized code hash found for para {:?}!", para); + return Err(Error::<T>::CannotUpgradeCode.into()) + }; + let new_code_hash = new_code.hash(); + ensure!(new_code_hash == authorized_code_hash, Error::<T>::CannotUpgradeCode); + + // TODO: FAIL-CI - more validations? + + // set current code + Self::do_force_set_current_code_update(para, new_code); + + // if ok, then allows "for free" + let post = PostDispatchInfo { + // consume the rest of the block to prevent further transactions + actual_weight: Some(T::BlockWeights::get().max_block), + // no fee for valid upgrade + pays_fee: Pays::No, + }; + Ok(post) + } } #[pallet::validate_unsigned] @@ -2159,6 +2250,17 @@ impl<T: Config> Pallet<T> { weight + T::DbWeight::get().writes(1) } + /// Force set the current code for the given parachain. + fn do_force_set_current_code_update( + para: ParaId, + new_code: ValidationCode, + ) { + let new_code_hash = new_code.hash(); + Self::increase_code_ref(&new_code_hash, &new_code); + Self::set_current_code(para, new_code_hash, frame_system::Pallet::<T>::block_number()); + Self::deposit_event(Event::CurrentCodeUpdated(para)); + } + /// Returns the list of PVFs (aka validation code) that require casting a vote by a validator in /// the active validator set. pub(crate) fn pvfs_require_precheck() -> Vec<ValidationCodeHash> { diff --git a/polkadot/runtime/parachains/src/paras/tests.rs b/polkadot/runtime/parachains/src/paras/tests.rs index 6e4f99aa3d8..2581dedda26 100644 --- a/polkadot/runtime/parachains/src/paras/tests.rs +++ b/polkadot/runtime/parachains/src/paras/tests.rs @@ -2013,3 +2013,103 @@ fn parachains_cache_preserves_order() { assert_eq!(Parachains::<Test>::get(), vec![a, c]); }); } + +#[test] +fn authorize_and_apply_set_current_code_works() { + new_test_ext(MockGenesisConfig::default()).execute_with(|| { + let para_a = ParaId::from(111); + let code_1 = ValidationCode(vec![1]); + let code_2 = ValidationCode(vec![2]); + let code_3 = ValidationCode(vec![3]); + let code_1_hash = code_1.hash(); + let code_2_hash = code_2.hash(); + let code_3_hash = code_3.hash(); + let root = RuntimeOrigin::root(); + let non_root = RuntimeOrigin::signed(1); + + // check before + assert!(AuthorizedCodeHash::<Test>::get(para_a).is_none()); + assert!(CurrentCodeHash::<Test>::get(para_a).is_none()); + check_code_is_not_stored(&code_1); + check_code_is_not_stored(&code_2); + check_code_is_not_stored(&code_3); + + // cannot apply non-authorized code + assert_err!( + Paras::apply_authorized_force_set_current_code(non_root.clone(), para_a, code_1.clone()), + Error::<Test>::CannotUpgradeCode, + ); + + // non-root user cannot authorize + assert_err!( + Paras::authorize_force_set_current_code_hash(non_root.clone(), para_a, code_1_hash), + DispatchError::BadOrigin, + ); + // root can authorize + assert_ok!(Paras::authorize_force_set_current_code_hash(root.clone(), para_a, code_1_hash)); + + // check authorized code hash stored + assert_eq!(AuthorizedCodeHash::<Test>::get(para_a), Some(code_1_hash)); + assert!(CurrentCodeHash::<Test>::get(para_a).is_none()); + check_code_is_not_stored(&code_1); + check_code_is_not_stored(&code_2); + check_code_is_not_stored(&code_3); + + // non-root cannot apply unauthorized code + assert_err!( + Paras::apply_authorized_force_set_current_code(non_root.clone(), para_a, code_2.clone()), + Error::<Test>::CannotUpgradeCode, + ); + assert_eq!(AuthorizedCodeHash::<Test>::get(para_a), Some(code_1_hash)); + assert!(CurrentCodeHash::<Test>::get(para_a).is_none()); + check_code_is_not_stored(&code_1); + check_code_is_not_stored(&code_2); + check_code_is_not_stored(&code_3); + + // non-root can apply authorized code + assert_ok!(Paras::apply_authorized_force_set_current_code(non_root.clone(), para_a, code_1.clone())); + + // check authorized code was applied + assert!(AuthorizedCodeHash::<Test>::get(para_a).is_none()); + assert_eq!(CurrentCodeHash::<Test>::get(para_a), Some(code_1_hash)); + check_code_is_stored(&code_1); + check_code_is_not_stored(&code_2); + check_code_is_not_stored(&code_3); + + // authorize multiple without apply: + // authorize code_2_hash + assert_ok!(Paras::authorize_force_set_current_code_hash(root.clone(), para_a, code_2_hash)); + assert_eq!(AuthorizedCodeHash::<Test>::get(para_a), Some(code_2_hash)); + assert_eq!(CurrentCodeHash::<Test>::get(para_a), Some(code_1_hash)); + check_code_is_stored(&code_1); + check_code_is_not_stored(&code_2); + check_code_is_not_stored(&code_3); + // authorize code_3_hash + assert_ok!(Paras::authorize_force_set_current_code_hash(root, para_a, code_3_hash)); + assert_eq!(AuthorizedCodeHash::<Test>::get(para_a), Some(code_3_hash)); + assert_eq!(CurrentCodeHash::<Test>::get(para_a), Some(code_1_hash)); + check_code_is_stored(&code_1); + check_code_is_not_stored(&code_2); + check_code_is_not_stored(&code_3); + + // cannot apply older ones + assert_err!( + Paras::apply_authorized_force_set_current_code(non_root.clone(), para_a, code_1.clone()), + Error::<Test>::CannotUpgradeCode, + ); + assert_err!( + Paras::apply_authorized_force_set_current_code(non_root.clone(), para_a, code_2.clone()), + Error::<Test>::CannotUpgradeCode, + ); + + // apply just authorized + assert_ok!(Paras::apply_authorized_force_set_current_code(non_root.clone(), para_a, code_3.clone())); + assert!(AuthorizedCodeHash::<Test>::get(para_a).is_none()); + assert_eq!(CurrentCodeHash::<Test>::get(para_a), Some(code_3_hash)); + check_code_is_stored(&code_1); + check_code_is_not_stored(&code_2); + check_code_is_stored(&code_3); + }) +} + }) +} -- GitLab