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} ;
9797use frame_system:: {
98- offchain:: { CreateBare , SubmitTransaction } ,
98+ offchain:: { CreateAuthorizedTransaction , SubmitTransaction } ,
9999 pallet_prelude:: * ,
100100} ;
101101pub use pallet:: * ;
@@ -104,6 +104,7 @@ use sp_application_crypto::RuntimeAppPublic;
104104use 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} ;
109110use 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)
0 commit comments