Skip to content

Commit c3bb006

Browse files
RomarQshawntabrizi
andauthored
Migrate pallet-im-online to TransactionExtension API (#11235)
## Summary Part of #2415 (follow-up to #10150) Migrate `pallet-im-online` from the deprecated `ValidateUnsigned` trait to `#[pallet::authorize]`, following the pattern established in #10716. ## Changes - Replace `ValidateUnsigned` impl with `#[pallet::authorize]` on the `heartbeat` call. - Change Config bound from `CreateBare<Call<Self>>` to `CreateAuthorizedTransaction<Call<Self>>`. - `ensure_none` → `ensure_authorized`, `create_bare` → `create_authorized_transaction`. - Add tests covering all authorize validation paths and dispatch with `RawOrigin::Authorized`. ## Migration The `Config` trait now requires `CreateAuthorizedTransaction<Call<Self>>` instead of `CreateBare<Call<Self>>`. Runtimes with a blanket `impl<C> CreateAuthorizedTransaction<C> for Runtime` (like kitchensink) need no changes. --------- Co-authored-by: Shawn Tabrizi <shawntabrizi@gmail.com>
1 parent bde47ea commit c3bb006

8 files changed

Lines changed: 218 additions & 150 deletions

File tree

prdoc/pr_11235.prdoc

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
title: Migrate `pallet-im-online` to use `TransactionExtension` API
2+
3+
doc:
4+
- audience: Runtime Dev
5+
description: |-
6+
Migrates `pallet-im-online` from the deprecated `ValidateUnsigned` trait to the modern
7+
`TransactionExtension` API using the `#[pallet::authorize]` attribute.
8+
9+
Runtime integrators: the pallet's `Config` trait now requires
10+
`CreateAuthorizedTransaction<Call<Self>>` instead of `CreateBare<Call<Self>>`.
11+
The runtime must include `frame_system::AuthorizeCall` in its transaction extension pipeline.
12+
13+
crates:
14+
- name: pallet-im-online
15+
bump: major
16+
- name: pallet-offences-benchmarking
17+
bump: patch
18+
- name: kitchensink-runtime
19+
bump: patch

substrate/bin/node/cli/tests/submit_transaction.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,21 @@
1717
// along with this program. If not, see <https://www.gnu.org/licenses/>.
1818

1919
use codec::Decode;
20-
use frame_system::offchain::{SendSignedTransaction, Signer, SubmitTransaction};
20+
use frame_system::offchain::{
21+
CreateAuthorizedTransaction, SendSignedTransaction, Signer, SubmitTransaction,
22+
};
2123
use kitchensink_runtime::{Executive, ExistentialDeposit, Indices, Runtime, UncheckedExtrinsic};
2224
use polkadot_sdk::*;
2325
use sp_application_crypto::AppCrypto;
2426
use sp_core::offchain::{testing::TestTransactionPoolExt, TransactionPoolExt};
2527
use sp_keyring::sr25519::Keyring::Alice;
2628
use sp_keystore::{testing::MemoryKeystore, Keystore, KeystoreExt};
27-
use sp_runtime::generic;
2829

2930
pub mod common;
3031
use self::common::*;
3132

3233
#[test]
33-
fn should_submit_unsigned_transaction() {
34+
fn should_submit_authorized_transaction() {
3435
let mut t = new_test_ext(compact_code_unwrap());
3536
let (pool, state) = TestTransactionPoolExt::new();
3637
t.register_extension(TransactionPoolExt::new(pool));
@@ -46,7 +47,9 @@ fn should_submit_unsigned_transaction() {
4647
};
4748

4849
let call = pallet_im_online::Call::heartbeat { heartbeat: heartbeat_data, signature };
49-
let xt = generic::UncheckedExtrinsic::new_bare(call.into()).into();
50+
let xt = <Runtime as CreateAuthorizedTransaction<
51+
pallet_im_online::Call<Runtime>,
52+
>>::create_authorized_transaction(call.into());
5053
SubmitTransaction::<Runtime, pallet_im_online::Call<Runtime>>::submit_transaction(xt)
5154
.unwrap();
5255

substrate/frame/im-online/src/benchmarking.rs

Lines changed: 10 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
#![cfg(feature = "runtime-benchmarks")]
2121

2222
use frame_benchmarking::v2::*;
23-
use frame_support::{traits::UnfilteredDispatchable, WeakBoundedVec};
23+
use frame_support::{traits::Authorize, WeakBoundedVec};
2424
use frame_system::RawOrigin;
2525
use sp_runtime::{traits::Zero, transaction_validity::TransactionSource};
2626

@@ -57,53 +57,33 @@ pub fn create_heartbeat<T: Config>(
5757
Ok((input_heartbeat, signature))
5858
}
5959

60-
#[benchmarks]
60+
#[benchmarks(where <T as frame_system::Config>::RuntimeCall: From<Call<T>>)]
6161
mod benchmarks {
6262
use super::*;
6363

64-
#[benchmark(extra)]
64+
#[benchmark]
6565
fn heartbeat(k: Linear<1, { <T as Config>::MaxKeys::get() }>) -> Result<(), BenchmarkError> {
6666
let (input_heartbeat, signature) = create_heartbeat::<T>(k)?;
6767

6868
#[extrinsic_call]
69-
_(RawOrigin::None, input_heartbeat, signature);
70-
71-
Ok(())
72-
}
73-
74-
#[benchmark(extra)]
75-
fn validate_unsigned(
76-
k: Linear<1, { <T as Config>::MaxKeys::get() }>,
77-
) -> Result<(), BenchmarkError> {
78-
let (input_heartbeat, signature) = create_heartbeat::<T>(k)?;
79-
let call = Call::heartbeat { heartbeat: input_heartbeat, signature };
80-
81-
#[block]
82-
{
83-
#[allow(deprecated)]
84-
Pallet::<T>::validate_unsigned(TransactionSource::InBlock, &call)
85-
.map_err(<&str>::from)?;
86-
}
69+
_(RawOrigin::Authorized, input_heartbeat, signature);
8770

8871
Ok(())
8972
}
9073

9174
#[benchmark]
92-
fn validate_unsigned_and_then_heartbeat(
75+
fn authorize_heartbeat(
9376
k: Linear<1, { <T as Config>::MaxKeys::get() }>,
9477
) -> Result<(), BenchmarkError> {
9578
let (input_heartbeat, signature) = create_heartbeat::<T>(k)?;
96-
let call = Call::heartbeat { heartbeat: input_heartbeat, signature };
97-
let call_enc = call.encode();
79+
let call: <T as frame_system::Config>::RuntimeCall =
80+
Call::heartbeat { heartbeat: input_heartbeat, signature }.into();
9881

9982
#[block]
10083
{
101-
#[allow(deprecated)]
102-
Pallet::<T>::validate_unsigned(TransactionSource::InBlock, &call)
103-
.map_err(<&str>::from)?;
104-
<Call<T> as Decode>::decode(&mut &*call_enc)
105-
.expect("call is encoded above, encoding must be correct")
106-
.dispatch_bypass_filter(RawOrigin::None.into())?;
84+
call.authorize(TransactionSource::InBlock)
85+
.expect("heartbeat call should have authorize logic")
86+
.map_err(|_| "authorize failed")?;
10787
}
10888

10989
Ok(())

substrate/frame/im-online/src/lib.rs

Lines changed: 69 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
//!
2828
//! The heartbeat is a signed transaction, which was signed using the session key
2929
//! and includes the recent best block number of the local validators chain.
30-
//! It is submitted as an Unsigned Transaction via off-chain workers.
30+
//! It is submitted as an Authorized Transaction via off-chain workers.
3131
//!
3232
//! - [`Config`]
3333
//! - [`Call`]
@@ -95,7 +95,7 @@ use frame_support::{
9595
BoundedSlice, WeakBoundedVec,
9696
};
9797
use frame_system::{
98-
offchain::{CreateBare, SubmitTransaction},
98+
offchain::{CreateAuthorizedTransaction, SubmitTransaction},
9999
pallet_prelude::*,
100100
};
101101
pub use pallet::*;
@@ -104,6 +104,7 @@ use sp_application_crypto::RuntimeAppPublic;
104104
use sp_runtime::{
105105
offchain::storage::{MutateStorageError, StorageRetrievalError, StorageValueRef},
106106
traits::{AtLeast32BitUnsigned, Convert, Saturating, TrailingZeroInput},
107+
transaction_validity::TransactionValidityWithRefund,
107108
Debug, PerThing, Perbill, Permill, SaturatedConversion,
108109
};
109110
use sp_staking::{
@@ -261,7 +262,11 @@ pub mod pallet {
261262
pub struct Pallet<T>(_);
262263

263264
#[pallet::config]
264-
pub trait Config: CreateBare<Call<Self>> + frame_system::Config {
265+
/// # Requirements
266+
///
267+
/// This pallet requires `frame_system::AuthorizeCall` to be included in the runtime's
268+
/// transaction extension pipeline.
269+
pub trait Config: CreateAuthorizedTransaction<Call<Self>> + frame_system::Config {
265270
/// The identifier type for an authority.
266271
type AuthorityId: Member
267272
+ Parameter
@@ -384,20 +389,22 @@ pub mod pallet {
384389
/// ## Complexity:
385390
/// - `O(K)` where K is length of `Keys` (heartbeat.validators_len)
386391
/// - `O(K)`: decoding of length `K`
387-
// NOTE: the weight includes the cost of validate_unsigned as it is part of the cost to
388-
// import block with such an extrinsic.
389392
#[pallet::call_index(0)]
390-
#[pallet::weight(<T as Config>::WeightInfo::validate_unsigned_and_then_heartbeat(
393+
#[pallet::weight(<T as Config>::WeightInfo::heartbeat(
394+
heartbeat.validators_len,
395+
))]
396+
#[pallet::weight_of_authorize(<T as Config>::WeightInfo::authorize_heartbeat(
391397
heartbeat.validators_len,
392398
))]
399+
#[pallet::authorize(Self::authorize_heartbeat_call)]
393400
pub fn heartbeat(
394401
origin: OriginFor<T>,
395402
heartbeat: Heartbeat<BlockNumberFor<T>>,
396-
// since signature verification is done in `validate_unsigned`
403+
// since signature verification is done in `authorize`
397404
// we can skip doing it here again.
398405
_signature: <T::AuthorityId as RuntimeAppPublic>::Signature,
399406
) -> DispatchResult {
400-
ensure_none(origin)?;
407+
ensure_authorized(origin)?;
401408

402409
let current_session = T::ValidatorSet::session_index();
403410
let exists =
@@ -446,60 +453,6 @@ pub mod pallet {
446453
/// Invalid transaction custom error. Returned when validators_len field in heartbeat is
447454
/// incorrect.
448455
pub(crate) const INVALID_VALIDATORS_LEN: u8 = 10;
449-
450-
#[allow(deprecated)]
451-
#[pallet::validate_unsigned]
452-
impl<T: Config> ValidateUnsigned for Pallet<T> {
453-
type Call = Call<T>;
454-
455-
fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity {
456-
if let Call::heartbeat { heartbeat, signature } = call {
457-
if <Pallet<T>>::is_online(heartbeat.authority_index) {
458-
// we already received a heartbeat for this authority
459-
return InvalidTransaction::Stale.into();
460-
}
461-
462-
// check if session index from heartbeat is recent
463-
let current_session = T::ValidatorSet::session_index();
464-
if heartbeat.session_index != current_session {
465-
return InvalidTransaction::Stale.into();
466-
}
467-
468-
// verify that the incoming (unverified) pubkey is actually an authority id
469-
let keys = Keys::<T>::get();
470-
if keys.len() as u32 != heartbeat.validators_len {
471-
return InvalidTransaction::Custom(INVALID_VALIDATORS_LEN).into();
472-
}
473-
let authority_id = match keys.get(heartbeat.authority_index as usize) {
474-
Some(id) => id,
475-
None => return InvalidTransaction::BadProof.into(),
476-
};
477-
478-
// check signature (this is expensive so we do it last).
479-
let signature_valid = heartbeat.using_encoded(|encoded_heartbeat| {
480-
authority_id.verify(&encoded_heartbeat, signature)
481-
});
482-
483-
if !signature_valid {
484-
return InvalidTransaction::BadProof.into();
485-
}
486-
487-
ValidTransaction::with_tag_prefix("ImOnline")
488-
.priority(T::UnsignedPriority::get())
489-
.and_provides((current_session, authority_id))
490-
.longevity(
491-
TryInto::<u64>::try_into(
492-
T::NextSessionRotation::average_session_length() / 2u32.into(),
493-
)
494-
.unwrap_or(64_u64),
495-
)
496-
.propagate(true)
497-
.build()
498-
} else {
499-
InvalidTransaction::Call.into()
500-
}
501-
}
502-
}
503456
}
504457

505458
/// Keep track of number of authored blocks per authority, uncles are counted as
@@ -644,7 +597,7 @@ impl<T: Config> Pallet<T> {
644597
call,
645598
);
646599

647-
let xt = T::create_bare(call.into());
600+
let xt = T::create_authorized_transaction(call.into());
648601
SubmitTransaction::<T, Call<T>>::submit_transaction(xt)
649602
.map_err(|_| OffchainErr::SubmitTransaction)?;
650603

@@ -730,6 +683,59 @@ impl<T: Config> Pallet<T> {
730683
}
731684
}
732685

686+
/// Authorization callback for the `heartbeat` call.
687+
///
688+
/// Validates that the heartbeat is from a recognized authority in the current session
689+
/// and that the signature is correct. Cheap checks (staleness, session index, authority
690+
/// membership) run first; the expensive signature verification runs last.
691+
fn authorize_heartbeat_call(
692+
_source: TransactionSource,
693+
heartbeat: &Heartbeat<BlockNumberFor<T>>,
694+
signature: &<T::AuthorityId as RuntimeAppPublic>::Signature,
695+
) -> TransactionValidityWithRefund {
696+
if Pallet::<T>::is_online(heartbeat.authority_index) {
697+
// we already received a heartbeat for this authority
698+
return Err(InvalidTransaction::Stale.into());
699+
}
700+
701+
// check if session index from heartbeat is recent
702+
let current_session = T::ValidatorSet::session_index();
703+
if heartbeat.session_index != current_session {
704+
return Err(InvalidTransaction::Stale.into());
705+
}
706+
707+
// verify that the incoming (unverified) pubkey is actually an authority id
708+
let keys = Keys::<T>::get();
709+
if keys.len() as u32 != heartbeat.validators_len {
710+
return Err(InvalidTransaction::Custom(INVALID_VALIDATORS_LEN).into());
711+
}
712+
let authority_id = match keys.get(heartbeat.authority_index as usize) {
713+
Some(id) => id,
714+
None => return Err(InvalidTransaction::BadProof.into()),
715+
};
716+
717+
// check signature (this is expensive so we do it last).
718+
let signature_valid = heartbeat
719+
.using_encoded(|encoded_heartbeat| authority_id.verify(&encoded_heartbeat, signature));
720+
721+
if !signature_valid {
722+
return Err(InvalidTransaction::BadProof.into());
723+
}
724+
725+
ValidTransaction::with_tag_prefix("ImOnline")
726+
.priority(T::UnsignedPriority::get())
727+
.and_provides((current_session, authority_id))
728+
.longevity(
729+
TryInto::<u64>::try_into(
730+
T::NextSessionRotation::average_session_length() / 2u32.into(),
731+
)
732+
.unwrap_or(64_u64),
733+
)
734+
.propagate(true)
735+
.build()
736+
.map(|v| (v, Weight::zero()))
737+
}
738+
733739
#[cfg(test)]
734740
fn set_keys(keys: Vec<T::AuthorityId>) {
735741
let bounded_keys = WeakBoundedVec::<_, T::MaxKeys>::try_from(keys)

substrate/frame/im-online/src/mock.rs

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ use frame_support::{
2525
weights::Weight,
2626
};
2727
use pallet_session::historical as pallet_session_historical;
28-
use sp_runtime::{testing::UintAuthorityId, traits::ConvertInto, BuildStorage, Permill};
28+
use sp_runtime::{
29+
testing::UintAuthorityId,
30+
traits::{BlakeTwo256, ConvertInto},
31+
BuildStorage, Permill,
32+
};
2933
use sp_staking::{
3034
offence::{OffenceError, ReportOffence},
3135
SessionIndex,
@@ -34,7 +38,10 @@ use sp_staking::{
3438
use crate as imonline;
3539
use crate::Config;
3640

37-
type Block = frame_system::mocking::MockBlock<Runtime>;
41+
type TxExtension = frame_system::AuthorizeCall<Runtime>;
42+
/// An extrinsic type used for tests.
43+
pub type Extrinsic = sp_runtime::testing::TestXt<RuntimeCall, TxExtension>;
44+
type Block = sp_runtime::generic::Block<sp_runtime::generic::Header<u64, BlakeTwo256>, Extrinsic>;
3845

3946
frame_support::construct_runtime!(
4047
pub enum Runtime {
@@ -73,8 +80,6 @@ impl pallet_session::historical::SessionManager<u64, u64> for TestSessionManager
7380
fn start_session(_: SessionIndex) {}
7481
}
7582

76-
/// An extrinsic type used for tests.
77-
pub type Extrinsic = sp_runtime::testing::TestXt<RuntimeCall, ()>;
7883
type IdentificationTuple = (u64, u64);
7984
type Offence = crate::UnresponsivenessOffence<IdentificationTuple>;
8085

@@ -206,12 +211,22 @@ where
206211
type Extrinsic = Extrinsic;
207212
}
208213

209-
impl<LocalCall> frame_system::offchain::CreateBare<LocalCall> for Runtime
214+
impl<LocalCall> frame_system::offchain::CreateTransaction<LocalCall> for Runtime
215+
where
216+
RuntimeCall: From<LocalCall>,
217+
{
218+
type Extension = TxExtension;
219+
fn create_transaction(call: RuntimeCall, extension: Self::Extension) -> Extrinsic {
220+
Extrinsic::new_transaction(call, extension)
221+
}
222+
}
223+
224+
impl<LocalCall> frame_system::offchain::CreateAuthorizedTransaction<LocalCall> for Runtime
210225
where
211226
RuntimeCall: From<LocalCall>,
212227
{
213-
fn create_bare(call: Self::RuntimeCall) -> Self::Extrinsic {
214-
Extrinsic::new_bare(call)
228+
fn create_extension() -> Self::Extension {
229+
TxExtension::new()
215230
}
216231
}
217232

0 commit comments

Comments
 (0)