From 7c386ecfc46b47890e5b43ddfc27fb6881d5f43d Mon Sep 17 00:00:00 2001 From: Branislav Kontur <bkontur@gmail.com> Date: Mon, 3 Mar 2025 15:41:19 +0100 Subject: [PATCH] Add `validate_unsigned` validation for `apply_authorized_code` --- polkadot/runtime/parachains/src/paras/mod.rs | 172 +++++++++++------- .../runtime/parachains/src/paras/tests.rs | 77 ++++++-- 2 files changed, 173 insertions(+), 76 deletions(-) diff --git a/polkadot/runtime/parachains/src/paras/mod.rs b/polkadot/runtime/parachains/src/paras/mod.rs index 191c6d9ff33..ef3df278e90 100644 --- a/polkadot/runtime/parachains/src/paras/mod.rs +++ b/polkadot/runtime/parachains/src/paras/mod.rs @@ -590,8 +590,8 @@ impl<N> CodeHashAuthorization<N> { use CodeHashAuthorization::*; match (self, other) { - (ForceSetCurrentCode { para_id: a, .. }, ForceSetCurrentCode { para_id: b, .. }) | - ( + (ForceSetCurrentCode { para_id: a, .. }, ForceSetCurrentCode { para_id: b, .. }) + | ( ForceScheduleCodeUpgrade { para_id: a, .. }, ForceScheduleCodeUpgrade { para_id: b, .. }, ) if a == b => true, @@ -608,9 +608,9 @@ impl<N> CodeHashAuthorization<N> { /// Compares the stored `code_hash` with the hash of the provided validation code. fn code_matches(&self, code: &ValidationCode) -> bool { let code_hash = match self { - CodeHashAuthorization::ForceSetCurrentCode { code_hash, .. } | - CodeHashAuthorization::ForceScheduleCodeUpgrade { code_hash, .. } | - CodeHashAuthorization::AddTrustedValidationCode { code_hash } => code_hash, + CodeHashAuthorization::ForceSetCurrentCode { code_hash, .. } + | CodeHashAuthorization::ForceScheduleCodeUpgrade { code_hash, .. } + | CodeHashAuthorization::AddTrustedValidationCode { code_hash } => code_hash, }; code_hash == &code.hash() @@ -1269,21 +1269,20 @@ pub mod pallet { // no need to ensure, anybody can do this // Ensure `new_code` is authorized - let authorized_code_hash = AuthorizedCodeHash::<T>::try_mutate(|authorizations| { - if let Some(idx) = authorizations.iter().position(|(a, _)| a == &authorization) { - Ok(authorizations.swap_remove(idx).0) - } else { - Err(Error::<T>::NothingAuthorized) - } - })?; - ensure!(authorized_code_hash.code_matches(&code), Error::<T>::Unauthorized); + let (authorized_code_hash, _) = Self::validate_authorization(authorization, &code)?; + // remove + AuthorizedCodeHash::<T>::mutate(|authorizations| { + authorizations.retain(|(auth, _)| auth != &authorized_code_hash); + }); // apply/dispatch match authorized_code_hash { - CodeHashAuthorization::ForceSetCurrentCode { para_id, .. } => - Self::do_force_set_current_code_update(para_id, code), - CodeHashAuthorization::AddTrustedValidationCode { .. } => - Self::do_add_trusted_validation_code(code), + CodeHashAuthorization::ForceSetCurrentCode { para_id, .. } => { + Self::do_force_set_current_code_update(para_id, code) + }, + CodeHashAuthorization::AddTrustedValidationCode { .. } => { + Self::do_add_trusted_validation_code(code) + }, CodeHashAuthorization::ForceScheduleCodeUpgrade { para_id, relay_parent_number, @@ -1307,52 +1306,81 @@ pub mod pallet { type Call = Call<T>; fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity { - let (stmt, signature) = match call { - Call::include_pvf_check_statement { stmt, signature } => (stmt, signature), - _ => return InvalidTransaction::Call.into(), - }; + match call { + Call::include_pvf_check_statement { stmt, signature } => { + let current_session = shared::CurrentSessionIndex::<T>::get(); + if stmt.session_index < current_session { + return InvalidTransaction::Stale.into() + } else if stmt.session_index > current_session { + return InvalidTransaction::Future.into() + } - let current_session = shared::CurrentSessionIndex::<T>::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::<T>::get(); + let validator_public = match validators.get(validator_index) { + Some(pk) => pk, + None => { + return InvalidTransaction::Custom(INVALID_TX_BAD_VALIDATOR_IDX).into() + }, + }; - let validator_index = stmt.validator_index.0 as usize; - let validators = shared::ActiveValidatorKeys::<T>::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 signing_payload = stmt.signing_payload(); - if !signature.verify(&signing_payload[..], &validator_public) { - return InvalidTransaction::BadProof.into() - } + let active_vote = match PvfActiveVoteMap::<T>::get(&stmt.subject) { + Some(v) => v, + None => return InvalidTransaction::Custom(INVALID_TX_BAD_SUBJECT).into(), + }; - let active_vote = match PvfActiveVoteMap::<T>::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() + }, + } - 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::<u64>::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_code { authorization, code } => { + match Self::validate_authorization(authorization.clone(), &code) { + Ok((authorization, expire_at)) => { + let now = frame_system::Pallet::<T>::block_number(); + if expire_at < now { + // this should not happen, because `validate_authorization` validates `expire_at` + return InvalidTransaction::Stale.into(); + } + let longevity = + expire_at.saturating_sub(frame_system::Pallet::<T>::block_number()); + + ValidTransaction::with_tag_prefix("ApplyAuthorizedCode") + .priority(T::UnsignedPriority::get()) + .longevity(TryInto::<u64>::try_into(longevity).unwrap_or(64_u64)) + .and_provides(authorization) + .propagate(true) + .build() + }, + Err(_) => { + return InvalidTransaction::Custom(INVALID_TX_UNAUTHORIZED_CODE).into() + }, + } + }, + _ => InvalidTransaction::Call.into(), } - - ValidTransaction::with_tag_prefix("PvfPreCheckingVote") - .priority(T::UnsignedPriority::get()) - .longevity( - TryInto::<u64>::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() } fn pre_dispatch(_call: &Self::Call) -> Result<(), TransactionValidityError> { @@ -1372,6 +1400,7 @@ pub mod pallet { 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: /// <https://github.com/paritytech/polkadot-sdk/issues/4737> @@ -1415,10 +1444,10 @@ impl<T: Config> Pallet<T> { /// Called by the initializer to initialize the paras pallet. pub(crate) fn initializer_initialize(now: BlockNumberFor<T>) -> Weight { - Self::prune_old_code(now) + - Self::process_scheduled_upgrade_changes(now) + - Self::process_future_code_upgrades_at(now) + - Self::prune_expired_authorizations(now) + 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 pallet. @@ -2549,6 +2578,27 @@ impl<T: Config> Pallet<T> { ) -> Option<PvfCheckActiveVoteState<BlockNumberFor<T>>> { PvfActiveVoteMap::<T>::get(code_hash) } + + /// This function checks whether the given `requested_authorization` exists in the list of + /// authorized code hashes. If found, it verifies that the associated code matches the provided + /// `code`. If the validation is successful, it returns the authorized `CodeHashAuthorization`. + pub(crate) fn validate_authorization( + requested_authorization: CodeHashAuthorization<BlockNumberFor<T>>, + code: &ValidationCode, + ) -> Result<(CodeHashAuthorization<BlockNumberFor<T>>, BlockNumberFor<T>), Error<T>> { + let Some((authorized_code_hash, expire_at)) = AuthorizedCodeHash::<T>::get() + .into_iter() + .find(|(a, _)| a == &requested_authorization) + else { + return Err(Error::<T>::NothingAuthorized); + }; + ensure!(authorized_code_hash.code_matches(code), Error::<T>::Unauthorized); + ensure!( + expire_at > frame_system::Pallet::<T>::block_number(), + Error::<T>::InvalidBlockNumber + ); + Ok((authorized_code_hash, expire_at)) + } } /// An overlay over the `Parachains` storage entry that provides a convenient interface for adding diff --git a/polkadot/runtime/parachains/src/paras/tests.rs b/polkadot/runtime/parachains/src/paras/tests.rs index 83b1f2fa5ee..2b429acb452 100644 --- a/polkadot/runtime/parachains/src/paras/tests.rs +++ b/polkadot/runtime/parachains/src/paras/tests.rs @@ -2140,6 +2140,23 @@ fn authorize_code_hash_works() { #[test] fn apply_authorized_code_works() { + let apply_code = |origin, + authorization: CodeHashAuthorization<_>, + code: ValidationCode| + -> (Result<_, _>, DispatchResultWithPostInfo) { + let call = Call::apply_authorized_code { + authorization: authorization.clone(), + code: code.clone(), + }; + let validate_unsigned = + <Paras as ValidateUnsigned>::validate_unsigned(TransactionSource::InBlock, &call) + .map(|_| ()); + + let dispatch_result = Paras::apply_authorized_code(origin, authorization, code); + + (validate_unsigned, dispatch_result) + }; + new_test_ext(MockGenesisConfig::default()).execute_with(|| { let para_a = ParaId::from(111); let code_1 = ValidationCode(vec![1]); @@ -2156,21 +2173,27 @@ fn apply_authorized_code_works() { assert!(AuthorizedCodeHash::<Test>::get().is_empty()); // cannot apply code when nothing authorized - assert_err!( - Paras::apply_authorized_code( + assert_eq!( + apply_code( RuntimeOrigin::signed(1), authorize_force_set_current_code_1_for_para_a.clone(), code_1.clone() ), - Error::<Test>::NothingAuthorized, + ( + Err(InvalidTransaction::Custom(INVALID_TX_UNAUTHORIZED_CODE).into()), + Err(Error::<Test>::NothingAuthorized.into()) + ), ); - assert_err!( - Paras::apply_authorized_code( + assert_eq!( + apply_code( RuntimeOrigin::signed(1), add_trusted_validation_code_2.clone(), code_2.clone() ), - Error::<Test>::NothingAuthorized, + ( + Err(InvalidTransaction::Custom(INVALID_TX_UNAUTHORIZED_CODE).into()), + Err(Error::<Test>::NothingAuthorized.into()) + ), ); // authorize @@ -2180,34 +2203,58 @@ fn apply_authorized_code_works() { ]); // cannot apply unauthorized code_2 - assert_err!( - Paras::apply_authorized_code( + assert_eq!( + apply_code( RuntimeOrigin::signed(1), authorize_force_set_current_code_1_for_para_a.clone(), code_2.clone() ), - Error::<Test>::Unauthorized, + ( + Err(InvalidTransaction::Custom(INVALID_TX_UNAUTHORIZED_CODE).into()), + Err(Error::<Test>::Unauthorized.into()) + ), ); + // cannot apply obsolete authorization + frame_system::Pallet::<Test>::set_block_number(expire_at + 1); + assert_eq!( + apply_code( + RuntimeOrigin::signed(1), + authorize_force_set_current_code_1_for_para_a.clone(), + code_1.clone(), + ), + ( + Err(InvalidTransaction::Custom(INVALID_TX_UNAUTHORIZED_CODE).into()), + Err(Error::<Test>::InvalidBlockNumber.into()) + ), + ); + frame_system::Pallet::<Test>::set_block_number(expire_at - 1); + // ok - can apply authorized code - assert_ok!(Paras::apply_authorized_code( + let (validate_unsigned, dispatch_result) = apply_code( RuntimeOrigin::signed(1), authorize_force_set_current_code_1_for_para_a.clone(), - code_1.clone() - )); + code_1.clone(), + ); + assert_ok!(validate_unsigned); + assert_ok!(dispatch_result); + assert_eq!( AuthorizedCodeHash::<Test>::get(), vec![(add_trusted_validation_code_2.clone(), expire_at),] ); // cannot apply previously authorized code again - assert_err!( - Paras::apply_authorized_code( + assert_eq!( + apply_code( RuntimeOrigin::signed(1), authorize_force_set_current_code_1_for_para_a, code_1, ), - Error::<Test>::NothingAuthorized + ( + Err(InvalidTransaction::Custom(INVALID_TX_UNAUTHORIZED_CODE).into()), + Err(Error::<Test>::NothingAuthorized.into()) + ), ); }) } -- GitLab