From 05a30802d77d119c13f5a4df3cfabfede640c561 Mon Sep 17 00:00:00 2001 From: Rodrigo Quelhas Date: Tue, 30 Dec 2025 21:30:11 +0000 Subject: [PATCH 01/11] Migrate pallet-example-offchain-worker to TransactionExtension API This commit migrates `pallet-example-offchain-worker` from the deprecated `ValidateUnsigned` trait to the modern `TransactionExtension` API using the `#[pallet::authorize]` attribute. Changes: - Replaced `#[pallet::validate_unsigned]` implementation with `#[pallet::authorize]` attributes on unsigned transaction calls - Updated `submit_price_unsigned` to use authorization attribute - Updated `submit_price_unsigned_with_signed_payload` to use authorization with signature verification - Changed `ensure_none` to `ensure_authorized` in unsigned transaction handlers - Updated documentation to reflect the new validation approach - Added comprehensive documentation about unsigned transaction validation This change demonstrates the recommended pattern for validating unsigned transactions and serves as a reference for other pallets migrating away from `ValidateUnsigned`. --- prdoc/pr_10716.prdoc | 21 ++++ .../frame/examples/offchain-worker/README.md | 22 ++++- .../frame/examples/offchain-worker/src/lib.rs | 99 +++++++++++-------- 3 files changed, 97 insertions(+), 45 deletions(-) create mode 100644 prdoc/pr_10716.prdoc diff --git a/prdoc/pr_10716.prdoc b/prdoc/pr_10716.prdoc new file mode 100644 index 0000000000000..9b630b59c4641 --- /dev/null +++ b/prdoc/pr_10716.prdoc @@ -0,0 +1,21 @@ +title: Migrate `pallet-example-offchain-worker` to use `TransactionExtension` API + +doc: + - audience: [Runtime Dev] + description: | + Migrates `pallet-example-offchain-worker` from the deprecated `ValidateUnsigned` trait + to the modern `TransactionExtension` API using the `#[pallet::authorize]` attribute. + + This change demonstrates how to validate unsigned transactions using the new approach, + which provides better composability and flexibility for transaction validation. + + The pallet now uses `#[pallet::authorize]` on unsigned transaction calls to validate: + - Block number is within the expected window + - Price data is valid + - For signed payloads, signature verification using the authority's public key + + This serves as a reference example for other pallets migrating away from `ValidateUnsigned`. + +crates: + - name: pallet-example-offchain-worker + bump: major diff --git a/substrate/frame/examples/offchain-worker/README.md b/substrate/frame/examples/offchain-worker/README.md index 7b8905cda3074..2c99d657b2f8a 100644 --- a/substrate/frame/examples/offchain-worker/README.md +++ b/substrate/frame/examples/offchain-worker/README.md @@ -9,7 +9,8 @@ documentation. - [`pallet_example_offchain_worker::Trait`](./trait.Trait.html) - [`Call`](./enum.Call.html) -- [`Module`](./struct.Module.html) +- [`Pallet`](./struct.Pallet.html) +- [`ValidateUnsignedPriceSubmission`](./struct.ValidateUnsignedPriceSubmission.html) **This pallet serves as an example showcasing Substrate off-chain worker and is not meant to be used in production.** @@ -23,7 +24,22 @@ and prepare either signed or unsigned transaction to feed the result back on cha The on-chain logic will simply aggregate the results and store last `64` values to compute the average price. Additional logic in OCW is put in place to prevent spamming the network with both signed -and unsigned transactions, and custom `UnsignedValidator` makes sure that there is only -one unsigned transaction floating in the network. +and unsigned transactions. The pallet uses the `#[pallet::authorize]` attribute to validate +unsigned transactions, ensuring that only one unsigned transaction can be accepted per +`UnsignedInterval` blocks. + +## Unsigned Transaction Validation + +This pallet demonstrates how to validate unsigned transactions using the modern +`#[pallet::authorize]` attribute instead of the deprecated `ValidateUnsigned` trait. + +The `#[pallet::authorize]` attribute is used on the unsigned transaction calls: +- `submit_price_unsigned` - Validates a simple unsigned transaction +- `submit_price_unsigned_with_signed_payload` - Validates an unsigned transaction with a signed payload + +The authorization logic checks: +1. Block number is within the expected window (via `NextUnsignedAt` storage) +2. The price data is valid +3. For signed payloads, verifies the signature using the authority's public key License: MIT-0 diff --git a/substrate/frame/examples/offchain-worker/src/lib.rs b/substrate/frame/examples/offchain-worker/src/lib.rs index 58a21a8395b4a..ec71e851810e2 100644 --- a/substrate/frame/examples/offchain-worker/src/lib.rs +++ b/substrate/frame/examples/offchain-worker/src/lib.rs @@ -46,8 +46,9 @@ //! The on-chain logic will simply aggregate the results and store last `64` values to compute //! the average price. //! Additional logic in OCW is put in place to prevent spamming the network with both signed -//! and unsigned transactions, and custom `UnsignedValidator` makes sure that there is only -//! one unsigned transaction floating in the network. +//! and unsigned transactions. The pallet uses the `#[pallet::authorize]` attribute to validate +//! unsigned transactions, ensuring that only one unsigned transaction can be accepted per +//! `UnsignedInterval` blocks. #![cfg_attr(not(feature = "std"), no_std)] @@ -55,7 +56,7 @@ extern crate alloc; use alloc::vec::Vec; use codec::{Decode, DecodeWithMemTracking, Encode}; -use frame_support::traits::Get; +use frame_support::{traits::Get, weights::Weight}; use frame_system::{ self as system, offchain::{ @@ -73,7 +74,10 @@ use sp_runtime::{ Duration, }, traits::Zero, - transaction_validity::{InvalidTransaction, TransactionValidity, ValidTransaction}, + transaction_validity::{ + InvalidTransaction, TransactionSource, TransactionValidity, TransactionValidityWithRefund, + ValidTransaction, + }, Debug, }; @@ -252,9 +256,6 @@ pub mod pallet { /// transaction without a signature, and hence without paying any fees, /// we need a way to make sure that only some transactions are accepted. /// This function can be called only once every `T::UnsignedInterval` blocks. - /// Transactions that call that function are de-duplicated on the pool level - /// via `validate_unsigned` implementation and also are rendered invalid if - /// the function has already been called in current "session". /// /// It's important to specify `weight` for unsigned calls as well, because even though /// they don't charge fees, we still don't want a single block to contain unlimited @@ -264,13 +265,33 @@ pub mod pallet { /// purpose is to showcase offchain worker capabilities. #[pallet::call_index(1)] #[pallet::weight({0})] + // Minimal weight since validation is lightweight. But in real-world scenarios, this should + // be benchmarked. + #[pallet::weight_of_authorize(Weight::from_parts(5_000, 0))] + #[pallet::authorize(| + _source: TransactionSource, + block_number: &BlockNumberFor, + new_price: &u32, + | -> TransactionValidityWithRefund { + let validity = Pallet::::validate_transaction_parameters(block_number, new_price); + match validity { + Ok(valid_tx) => { + // This is the amount to refund, here we refund nothing. + let refund = Weight::zero(); + + Ok((valid_tx, refund)) + }, + Err(e) => Err(e), + } + })] pub fn submit_price_unsigned( origin: OriginFor, _block_number: BlockNumberFor, price: u32, ) -> DispatchResultWithPostInfo { - // This ensures that the function can only be called via unsigned transaction. - ensure_none(origin)?; + // This ensures that the function can only be called via unsigned transaction + // after being validated in `pallet::authorize`. + ensure_authorized(origin)?; // Add the price to the on-chain list, but mark it as coming from an empty address. Self::add_price(None, price); // now increment the block number at which we expect next unsigned transaction. @@ -281,13 +302,37 @@ pub mod pallet { #[pallet::call_index(2)] #[pallet::weight({0})] + // Minimal weight since validation is lightweight. But in real-world scenarios, this should + // be benchmarked. + #[pallet::weight_of_authorize(Weight::from_parts(5_000, 0))] + #[pallet::authorize(| + _source: TransactionSource, + price_payload: &PricePayload>, + signature: &T::Signature, + | -> TransactionValidityWithRefund { + let signature_valid = SignedPayload::::verify::(price_payload, signature.clone()); + if !signature_valid { + return Err(InvalidTransaction::BadProof.into()) + } + let validity = Pallet::::validate_transaction_parameters(&price_payload.block_number, &price_payload.price); + match validity { + Ok(valid_tx) => { + // This is the amount to refund, here we refund nothing. + let refund = Weight::zero(); + + Ok((valid_tx, refund)) + }, + Err(e) => Err(e), + } + })] pub fn submit_price_unsigned_with_signed_payload( origin: OriginFor, price_payload: PricePayload>, _signature: T::Signature, ) -> DispatchResultWithPostInfo { - // This ensures that the function can only be called via unsigned transaction. - ensure_none(origin)?; + // This ensures that the function can only be called via unsigned transaction + // after being validated in `pallet::authorize`. + ensure_authorized(origin)?; // Add the price to the on-chain list, but mark it as coming from an empty address. Self::add_price(None, price_payload.price); // now increment the block number at which we expect next unsigned transaction. @@ -305,36 +350,6 @@ pub mod pallet { NewPrice { price: u32, maybe_who: Option }, } - #[pallet::validate_unsigned] - impl ValidateUnsigned for Pallet { - type Call = Call; - - /// Validate unsigned call to this module. - /// - /// By default unsigned transactions are disallowed, but implementing the validator - /// here we make sure that some particular calls (the ones produced by offchain worker) - /// are being whitelisted and marked as valid. - fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity { - // Firstly let's check that we call the right function. - if let Call::submit_price_unsigned_with_signed_payload { - price_payload: ref payload, - ref signature, - } = call - { - let signature_valid = - SignedPayload::::verify::(payload, signature.clone()); - if !signature_valid { - return InvalidTransaction::BadProof.into() - } - Self::validate_transaction_parameters(&payload.block_number, &payload.price) - } else if let Call::submit_price_unsigned { block_number, price: new_price } = call { - Self::validate_transaction_parameters(block_number, new_price) - } else { - InvalidTransaction::Call.into() - } - } - } - /// A vector of recently submitted prices. /// /// This is used to calculate average price, should have bounded size. @@ -504,7 +519,7 @@ impl Pallet { // Here we showcase two ways to send an unsigned transaction / unsigned payload (raw) // // By default unsigned transactions are disallowed, so we need to whitelist this case - // by writing `UnsignedValidator`. Note that it's EXTREMELY important to carefully + // by implementing a `TransactionExtension`. Note that it's EXTREMELY important to carefully // implement unsigned validation logic, as any mistakes can lead to opening DoS or spam // attack vectors. See validation logic docs for more details. // From 78b4e426c1b8ccb0df5d8410b3ad1081521d86cb Mon Sep 17 00:00:00 2001 From: Rodrigo Quelhas Date: Wed, 31 Dec 2025 14:43:08 +0000 Subject: [PATCH 02/11] Fix incorrect documentation references in README - Remove non-existent ValidateUnsignedPriceSubmission struct reference - Update Trait reference to Config (FRAME v2 syntax) These structs don't exist in the migrated code and would cause broken documentation links. --- substrate/frame/examples/offchain-worker/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/substrate/frame/examples/offchain-worker/README.md b/substrate/frame/examples/offchain-worker/README.md index 2c99d657b2f8a..65b48132ae46d 100644 --- a/substrate/frame/examples/offchain-worker/README.md +++ b/substrate/frame/examples/offchain-worker/README.md @@ -7,10 +7,9 @@ concepts, APIs and structures common to most offchain workers. Run `cargo doc --package pallet-example-offchain-worker --open` to view this module's documentation. -- [`pallet_example_offchain_worker::Trait`](./trait.Trait.html) +- [`Config`](./trait.Config.html) - [`Call`](./enum.Call.html) - [`Pallet`](./struct.Pallet.html) -- [`ValidateUnsignedPriceSubmission`](./struct.ValidateUnsignedPriceSubmission.html) **This pallet serves as an example showcasing Substrate off-chain worker and is not meant to be used in production.** From 0c154b2f2df10147ad5133745d225add779387ae Mon Sep 17 00:00:00 2001 From: Rodrigo Quelhas Date: Wed, 31 Dec 2025 17:41:13 +0000 Subject: [PATCH 03/11] fix pr doc --- prdoc/pr_10716.prdoc | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/prdoc/pr_10716.prdoc b/prdoc/pr_10716.prdoc index 9b630b59c4641..f3d39829b4080 100644 --- a/prdoc/pr_10716.prdoc +++ b/prdoc/pr_10716.prdoc @@ -1,21 +1,21 @@ title: Migrate `pallet-example-offchain-worker` to use `TransactionExtension` API doc: - - audience: [Runtime Dev] - description: | - Migrates `pallet-example-offchain-worker` from the deprecated `ValidateUnsigned` trait - to the modern `TransactionExtension` API using the `#[pallet::authorize]` attribute. +- audience: Runtime Dev + description: |- + Migrates `pallet-example-offchain-worker` from the deprecated `ValidateUnsigned` trait + to the modern `TransactionExtension` API using the `#[pallet::authorize]` attribute. - This change demonstrates how to validate unsigned transactions using the new approach, - which provides better composability and flexibility for transaction validation. + This change demonstrates how to validate unsigned transactions using the new approach, + which provides better composability and flexibility for transaction validation. - The pallet now uses `#[pallet::authorize]` on unsigned transaction calls to validate: - - Block number is within the expected window - - Price data is valid - - For signed payloads, signature verification using the authority's public key + The pallet now uses `#[pallet::authorize]` on unsigned transaction calls to validate: + - Block number is within the expected window + - Price data is valid + - For signed payloads, signature verification using the authority's public key - This serves as a reference example for other pallets migrating away from `ValidateUnsigned`. + This serves as a reference example for other pallets migrating away from `ValidateUnsigned`. crates: - - name: pallet-example-offchain-worker - bump: major +- name: pallet-example-offchain-worker + bump: major From 0cc992ae8de1231906d244665a2056b6530bc476 Mon Sep 17 00:00:00 2001 From: Rodrigo Quelhas Date: Thu, 1 Jan 2026 09:22:34 +0000 Subject: [PATCH 04/11] Address PR review feedback - Simplify authorize closures using .map() for cleaner code - Replace create_bare() with create_authorized_transaction() - create_bare only works for inherents and deprecated ValidateUnsigned - General transactions need transaction extensions for validation - Update Config trait bound from CreateBare to CreateAuthorizedTransaction - Update terminology from 'unsigned transaction' to 'general transaction' - Improve comments explaining custom validation and AuthorizeCall extension - Add clarification about transaction de-duplication via provides tag --- .../frame/examples/offchain-worker/src/lib.rs | 51 ++++++++----------- 1 file changed, 22 insertions(+), 29 deletions(-) diff --git a/substrate/frame/examples/offchain-worker/src/lib.rs b/substrate/frame/examples/offchain-worker/src/lib.rs index ec71e851810e2..290c9d532a6fc 100644 --- a/substrate/frame/examples/offchain-worker/src/lib.rs +++ b/substrate/frame/examples/offchain-worker/src/lib.rs @@ -60,7 +60,7 @@ use frame_support::{traits::Get, weights::Weight}; use frame_system::{ self as system, offchain::{ - AppCrypto, CreateBare, CreateSignedTransaction, SendSignedTransaction, + AppCrypto, CreateAuthorizedTransaction, CreateSignedTransaction, SendSignedTransaction, SendUnsignedTransaction, SignedPayload, Signer, SigningTypes, SubmitTransaction, }, pallet_prelude::BlockNumberFor, @@ -135,7 +135,9 @@ pub mod pallet { /// This pallet's configuration trait #[pallet::config] pub trait Config: - CreateSignedTransaction> + CreateBare> + frame_system::Config + CreateSignedTransaction> + + CreateAuthorizedTransaction> + + frame_system::Config { /// The identifier type for an offchain worker. type AuthorityId: AppCrypto; @@ -257,6 +259,10 @@ pub mod pallet { /// we need a way to make sure that only some transactions are accepted. /// This function can be called only once every `T::UnsignedInterval` blocks. /// + /// Transactions are de-duplicated on the pool level via the `provides` tag in the + /// validation logic (`validate_transaction_parameters`), which returns `next_unsigned_at`. + /// This ensures only one transaction per interval can be included in a block. + /// /// It's important to specify `weight` for unsigned calls as well, because even though /// they don't charge fees, we still don't want a single block to contain unlimited /// number of such transactions. @@ -273,16 +279,8 @@ pub mod pallet { block_number: &BlockNumberFor, new_price: &u32, | -> TransactionValidityWithRefund { - let validity = Pallet::::validate_transaction_parameters(block_number, new_price); - match validity { - Ok(valid_tx) => { - // This is the amount to refund, here we refund nothing. - let refund = Weight::zero(); - - Ok((valid_tx, refund)) - }, - Err(e) => Err(e), - } + Pallet::::validate_transaction_parameters(block_number, new_price) + .map(|v| (v, /* no refund */ Weight::zero())) })] pub fn submit_price_unsigned( origin: OriginFor, @@ -314,16 +312,8 @@ pub mod pallet { if !signature_valid { return Err(InvalidTransaction::BadProof.into()) } - let validity = Pallet::::validate_transaction_parameters(&price_payload.block_number, &price_payload.price); - match validity { - Ok(valid_tx) => { - // This is the amount to refund, here we refund nothing. - let refund = Weight::zero(); - - Ok((valid_tx, refund)) - }, - Err(e) => Err(e), - } + Pallet::::validate_transaction_parameters(&price_payload.block_number, &price_payload.price) + .map(|v| (v, /* no refund */ Weight::zero())) })] pub fn submit_price_unsigned_with_signed_payload( origin: OriginFor, @@ -516,16 +506,19 @@ impl Pallet { let call = Call::submit_price_unsigned { block_number, price }; // Now let's create a transaction out of this call and submit it to the pool. - // Here we showcase two ways to send an unsigned transaction / unsigned payload (raw) + // Here we showcase two ways to send a general transaction / unsigned payload (raw) // - // By default unsigned transactions are disallowed, so we need to whitelist this case - // by implementing a `TransactionExtension`. Note that it's EXTREMELY important to carefully - // implement unsigned validation logic, as any mistakes can lead to opening DoS or spam - // attack vectors. See validation logic docs for more details. + // By default general transactions start the transaction extension pipeline with the origin + // `None`. We define custom validation logic using the `#[pallet::authorize]` attribute. + // This custom validation is executed by the transaction extension `AuthorizeCall`, which + // will change the origin to `frame_system::Origin::Authorized` if validation succeeds. + // Note that it's EXTREMELY important to carefully implement custom validation logic, as + // any mistakes can lead to opening DoS or spam attack vectors. See validation logic docs + // for more details. // - let xt = T::create_bare(call.into()); + let xt = T::create_authorized_transaction(call.into()); SubmitTransaction::>::submit_transaction(xt) - .map_err(|()| "Unable to submit unsigned transaction.")?; + .map_err(|()| "Unable to submit transaction.")?; Ok(()) } From 0c67460344007dc520766cf970c5577cc56a7faf Mon Sep 17 00:00:00 2001 From: Rodrigo Quelhas Date: Thu, 1 Jan 2026 10:25:50 +0000 Subject: [PATCH 05/11] Address PR review feedback - Simplify authorize closures using .map() - Remove CreateBare trait bound and implementation (no longer needed) - Replace send_unsigned_transaction with manual transaction creation using accounts_from_keys() and create_authorized_transaction() - Add CreateAuthorizedTransaction implementation in tests - Update test assertions to check for general transactions - Use fully qualified paths for SignedPayload::sign() to resolve type inference --- .../frame/examples/offchain-worker/src/lib.rs | 94 +++++++++++++------ .../examples/offchain-worker/src/tests.rs | 15 +-- 2 files changed, 73 insertions(+), 36 deletions(-) diff --git a/substrate/frame/examples/offchain-worker/src/lib.rs b/substrate/frame/examples/offchain-worker/src/lib.rs index 290c9d532a6fc..ac621bbc039a7 100644 --- a/substrate/frame/examples/offchain-worker/src/lib.rs +++ b/substrate/frame/examples/offchain-worker/src/lib.rs @@ -60,8 +60,8 @@ use frame_support::{traits::Get, weights::Weight}; use frame_system::{ self as system, offchain::{ - AppCrypto, CreateAuthorizedTransaction, CreateSignedTransaction, SendSignedTransaction, - SendUnsignedTransaction, SignedPayload, Signer, SigningTypes, SubmitTransaction, + AppCrypto, CreateAuthorizedTransaction, CreateSignedTransaction, + SendSignedTransaction, SignedPayload, Signer, SigningTypes, SubmitTransaction, }, pallet_prelude::BlockNumberFor, }; @@ -287,8 +287,6 @@ pub mod pallet { _block_number: BlockNumberFor, price: u32, ) -> DispatchResultWithPostInfo { - // This ensures that the function can only be called via unsigned transaction - // after being validated in `pallet::authorize`. ensure_authorized(origin)?; // Add the price to the on-chain list, but mark it as coming from an empty address. Self::add_price(None, price); @@ -320,8 +318,6 @@ pub mod pallet { price_payload: PricePayload>, _signature: T::Signature, ) -> DispatchResultWithPostInfo { - // This ensures that the function can only be called via unsigned transaction - // after being validated in `pallet::authorize`. ensure_authorized(origin)?; // Add the price to the on-chain list, but mark it as coming from an empty address. Self::add_price(None, price_payload.price); @@ -538,17 +534,39 @@ impl Pallet { // Note this call will block until response is received. let price = Self::fetch_price().map_err(|_| "Failed to fetch price")?; - // -- Sign using any account - let (_, result) = Signer::::any_account() - .send_unsigned_transaction( - |account| PricePayload { price, block_number, public: account.public.clone() }, - |payload, signature| Call::submit_price_unsigned_with_signed_payload { - price_payload: payload, - signature, - }, - ) - .ok_or("No local accounts accounts available.")?; - result.map_err(|()| "Unable to submit transaction")?; + // -- Sign using any account and create an authorized transaction + let signer = Signer::::any_account(); + + // Get the first available account + let account = signer + .accounts_from_keys() + .next() + .ok_or("No local accounts available.")?; + + // Create the payload to sign + let payload = PricePayload { + price, + block_number, + public: account.public.clone(), + }; + + // Sign the payload + let signature = ::Public, + BlockNumberFor, + > as SignedPayload>::sign::(&payload) + .ok_or("Failed to sign payload")?; + + // Create the call with the signed payload + let call = Call::submit_price_unsigned_with_signed_payload { + price_payload: payload, + signature, + }; + + // Create an authorized transaction and submit it + let xt = T::create_authorized_transaction(call.into()); + SubmitTransaction::>::submit_transaction(xt) + .map_err(|()| "Unable to submit transaction")?; Ok(()) } @@ -568,19 +586,35 @@ impl Pallet { // Note this call will block until response is received. let price = Self::fetch_price().map_err(|_| "Failed to fetch price")?; - // -- Sign using all accounts - let transaction_results = Signer::::all_accounts() - .send_unsigned_transaction( - |account| PricePayload { price, block_number, public: account.public.clone() }, - |payload, signature| Call::submit_price_unsigned_with_signed_payload { - price_payload: payload, - signature, - }, - ); - for (_account_id, result) in transaction_results.into_iter() { - if result.is_err() { - return Err("Unable to submit transaction") - } + // -- Sign using all accounts and create authorized transactions + let signer = Signer::::all_accounts(); + + // Iterate over all available accounts + for account in signer.accounts_from_keys() { + // Create the payload to sign + let payload = PricePayload { + price, + block_number, + public: account.public.clone(), + }; + + // Sign the payload + let signature = ::Public, + BlockNumberFor, + > as SignedPayload>::sign::(&payload) + .ok_or("Failed to sign payload")?; + + // Create the call with the signed payload + let call = Call::submit_price_unsigned_with_signed_payload { + price_payload: payload, + signature, + }; + + // Create an authorized transaction and submit it + let xt = T::create_authorized_transaction(call.into()); + SubmitTransaction::>::submit_transaction(xt) + .map_err(|()| "Unable to submit transaction")?; } Ok(()) diff --git a/substrate/frame/examples/offchain-worker/src/tests.rs b/substrate/frame/examples/offchain-worker/src/tests.rs index 230873a2dca57..fd9c127ea3303 100644 --- a/substrate/frame/examples/offchain-worker/src/tests.rs +++ b/substrate/frame/examples/offchain-worker/src/tests.rs @@ -121,12 +121,12 @@ where } } -impl frame_system::offchain::CreateBare for Test +impl frame_system::offchain::CreateAuthorizedTransaction for Test where RuntimeCall: From, { - fn create_bare(call: Self::RuntimeCall) -> Self::Extrinsic { - Extrinsic::new_bare(call) + fn create_extension() -> Self::Extension { + () } } @@ -285,7 +285,8 @@ fn should_submit_unsigned_transaction_on_chain_for_any_account() { // then let tx = pool_state.write().transactions.pop().unwrap(); let tx = Extrinsic::decode(&mut &*tx).unwrap(); - assert!(tx.is_inherent()); + // General transactions are neither inherent nor signed (old-school) + assert!(!tx.is_inherent() && !tx.is_signed()); if let RuntimeCall::Example(crate::Call::submit_price_unsigned_with_signed_payload { price_payload: body, signature, @@ -340,7 +341,8 @@ fn should_submit_unsigned_transaction_on_chain_for_all_accounts() { // then let tx = pool_state.write().transactions.pop().unwrap(); let tx = Extrinsic::decode(&mut &*tx).unwrap(); - assert!(tx.is_inherent()); + // General transactions are neither inherent nor signed (old-school) + assert!(!tx.is_inherent() && !tx.is_signed()); if let RuntimeCall::Example(crate::Call::submit_price_unsigned_with_signed_payload { price_payload: body, signature, @@ -381,7 +383,8 @@ fn should_submit_raw_unsigned_transaction_on_chain() { let tx = pool_state.write().transactions.pop().unwrap(); assert!(pool_state.read().transactions.is_empty()); let tx = Extrinsic::decode(&mut &*tx).unwrap(); - assert!(tx.is_inherent()); + // General transactions are neither inherent nor signed (old-school) + assert!(!tx.is_inherent() && !tx.is_signed()); assert_eq!( tx.function, RuntimeCall::Example(crate::Call::submit_price_unsigned { From d5b040bfc29fe0c20cf2a7d682ef70d0a4363768 Mon Sep 17 00:00:00 2001 From: Rodrigo Quelhas Date: Thu, 1 Jan 2026 13:34:32 +0000 Subject: [PATCH 06/11] align naming --- .../frame/examples/offchain-worker/README.md | 20 +-- .../frame/examples/offchain-worker/src/lib.rs | 126 +++++++++--------- .../examples/offchain-worker/src/tests.rs | 18 +-- 3 files changed, 82 insertions(+), 82 deletions(-) diff --git a/substrate/frame/examples/offchain-worker/README.md b/substrate/frame/examples/offchain-worker/README.md index 65b48132ae46d..0de5cde5b04dd 100644 --- a/substrate/frame/examples/offchain-worker/README.md +++ b/substrate/frame/examples/offchain-worker/README.md @@ -19,25 +19,25 @@ used in production.** In this example we are going to build a very simplistic, naive and definitely NOT production-ready oracle for BTC/USD price. Offchain Worker (OCW) will be triggered after every block, fetch the current price -and prepare either signed or unsigned transaction to feed the result back on chain. +and prepare either signed or general transaction to feed the result back on chain. The on-chain logic will simply aggregate the results and store last `64` values to compute the average price. Additional logic in OCW is put in place to prevent spamming the network with both signed -and unsigned transactions. The pallet uses the `#[pallet::authorize]` attribute to validate -unsigned transactions, ensuring that only one unsigned transaction can be accepted per -`UnsignedInterval` blocks. +and general transactions. The pallet uses the `#[pallet::authorize]` attribute to validate +general transactions, ensuring that only one general transaction can be accepted per +`AuthorizedTxInterval` blocks. -## Unsigned Transaction Validation +## General Transaction Validation -This pallet demonstrates how to validate unsigned transactions using the modern +This pallet demonstrates how to validate general transactions using the modern `#[pallet::authorize]` attribute instead of the deprecated `ValidateUnsigned` trait. -The `#[pallet::authorize]` attribute is used on the unsigned transaction calls: -- `submit_price_unsigned` - Validates a simple unsigned transaction -- `submit_price_unsigned_with_signed_payload` - Validates an unsigned transaction with a signed payload +The `#[pallet::authorize]` attribute is used on the general transaction calls: +- `submit_price_authorized` - Validates a simple general transaction +- `submit_price_authorized_with_signed_payload` - Validates a general transaction with a signed payload The authorization logic checks: -1. Block number is within the expected window (via `NextUnsignedAt` storage) +1. Block number is within the expected window (via `NextAuthorizedAt` storage) 2. The price data is valid 3. For signed payloads, verifies the signature using the authority's public key diff --git a/substrate/frame/examples/offchain-worker/src/lib.rs b/substrate/frame/examples/offchain-worker/src/lib.rs index ac621bbc039a7..71e63e77ee230 100644 --- a/substrate/frame/examples/offchain-worker/src/lib.rs +++ b/substrate/frame/examples/offchain-worker/src/lib.rs @@ -42,13 +42,13 @@ //! In this example we are going to build a very simplistic, naive and definitely NOT //! production-ready oracle for BTC/USD price. //! Offchain Worker (OCW) will be triggered after every block, fetch the current price -//! and prepare either signed or unsigned transaction to feed the result back on chain. +//! and prepare either signed or general transaction to feed the result back on chain. //! The on-chain logic will simply aggregate the results and store last `64` values to compute //! the average price. //! Additional logic in OCW is put in place to prevent spamming the network with both signed -//! and unsigned transactions. The pallet uses the `#[pallet::authorize]` attribute to validate -//! unsigned transactions, ensuring that only one unsigned transaction can be accepted per -//! `UnsignedInterval` blocks. +//! and general transactions. The pallet uses the `#[pallet::authorize]` attribute to validate +//! general transactions, ensuring that only one general transaction can be accepted per +//! `AuthorizedTxInterval` blocks. #![cfg_attr(not(feature = "std"), no_std)] @@ -152,19 +152,19 @@ pub mod pallet { #[pallet::constant] type GracePeriod: Get>; - /// Number of blocks of cooldown after unsigned transaction is included. + /// Number of blocks of cooldown after an authorized transaction is included. /// - /// This ensures that we only accept unsigned transactions once, every `UnsignedInterval` + /// This ensures that we only accept authorized transactions once, every `AuthorizedTxInterval` /// blocks. #[pallet::constant] - type UnsignedInterval: Get>; + type AuthorizedTxInterval: Get>; - /// A configuration for base priority of unsigned transactions. + /// A configuration for base priority of authorized transactions. /// /// This is exposed so that it can be tuned for particular runtime, when - /// multiple pallets send unsigned transactions. + /// multiple pallets send authorized transactions. #[pallet::constant] - type UnsignedPriority: Get; + type AuthorizedTxPriority: Get; /// Maximum number of prices. #[pallet::constant] @@ -206,17 +206,17 @@ pub mod pallet { let average: Option = Self::average_price(); log::debug!("Current price: {:?}", average); - // For this example we are going to send both signed and unsigned transactions + // For this example we are going to send both signed and general transactions // depending on the block number. // Usually it's enough to choose one or the other. let should_send = Self::choose_transaction_type(block_number); let res = match should_send { TransactionType::Signed => Self::fetch_price_and_send_signed(), - TransactionType::UnsignedForAny => - Self::fetch_price_and_send_unsigned_for_any_account(block_number), - TransactionType::UnsignedForAll => - Self::fetch_price_and_send_unsigned_for_all_accounts(block_number), - TransactionType::Raw => Self::fetch_price_and_send_raw_unsigned(block_number), + TransactionType::AuthorizedForAny => + Self::fetch_price_and_send_authorized_tx_for_any_account(block_number), + TransactionType::AuthorizedForAll => + Self::fetch_price_and_send_authorized_tx_for_all_accounts(block_number), + TransactionType::Raw => Self::fetch_price_and_send_raw_authorized(block_number), TransactionType::None => Ok(()), }; if let Err(e) = res { @@ -252,15 +252,15 @@ pub mod pallet { Ok(().into()) } - /// Submit new price to the list via unsigned transaction. + /// Submit new price to the list via general transaction. /// /// Works exactly like the `submit_price` function, but since we allow sending the /// transaction without a signature, and hence without paying any fees, /// we need a way to make sure that only some transactions are accepted. - /// This function can be called only once every `T::UnsignedInterval` blocks. + /// This function can be called only once every `T::AuthorizedTxInterval` blocks. /// /// Transactions are de-duplicated on the pool level via the `provides` tag in the - /// validation logic (`validate_transaction_parameters`), which returns `next_unsigned_at`. + /// validation logic (`validate_transaction_parameters`), which returns `next_authorized_at`. /// This ensures only one transaction per interval can be included in a block. /// /// It's important to specify `weight` for unsigned calls as well, because even though @@ -282,7 +282,7 @@ pub mod pallet { Pallet::::validate_transaction_parameters(block_number, new_price) .map(|v| (v, /* no refund */ Weight::zero())) })] - pub fn submit_price_unsigned( + pub fn submit_price_authorized( origin: OriginFor, _block_number: BlockNumberFor, price: u32, @@ -290,9 +290,9 @@ pub mod pallet { ensure_authorized(origin)?; // Add the price to the on-chain list, but mark it as coming from an empty address. Self::add_price(None, price); - // now increment the block number at which we expect next unsigned transaction. + // now increment the block number at which we expect next authorized transaction. let current_block = >::block_number(); - >::put(current_block + T::UnsignedInterval::get()); + >::put(current_block + T::AuthorizedTxInterval::get()); Ok(().into()) } @@ -313,7 +313,7 @@ pub mod pallet { Pallet::::validate_transaction_parameters(&price_payload.block_number, &price_payload.price) .map(|v| (v, /* no refund */ Weight::zero())) })] - pub fn submit_price_unsigned_with_signed_payload( + pub fn submit_price_authorized_with_signed_payload( origin: OriginFor, price_payload: PricePayload>, _signature: T::Signature, @@ -321,9 +321,9 @@ pub mod pallet { ensure_authorized(origin)?; // Add the price to the on-chain list, but mark it as coming from an empty address. Self::add_price(None, price_payload.price); - // now increment the block number at which we expect next unsigned transaction. + // now increment the block number at which we expect next authorized transaction. let current_block = >::block_number(); - >::put(current_block + T::UnsignedInterval::get()); + >::put(current_block + T::AuthorizedTxInterval::get()); Ok(().into()) } } @@ -342,13 +342,13 @@ pub mod pallet { #[pallet::storage] pub(super) type Prices = StorageValue<_, BoundedVec, ValueQuery>; - /// Defines the block when next unsigned transaction will be accepted. + /// Defines the block when next authorized transaction will be accepted. /// - /// To prevent spam of unsigned (and unpaid!) transactions on the network, - /// we only allow one transaction every `T::UnsignedInterval` blocks. + /// To prevent spam of authorized (and unpaid!) transactions on the network, + /// we only allow one transaction every `T::AuthorizedTxInterval` blocks. /// This storage entry defines when new transaction is going to be accepted. #[pallet::storage] - pub(super) type NextUnsignedAt = StorageValue<_, BlockNumberFor, ValueQuery>; + pub(super) type NextAuthorizedAt = StorageValue<_, BlockNumberFor, ValueQuery>; } /// Payload used by this example crate to hold price @@ -370,8 +370,8 @@ impl SignedPayload for PricePayload Pallet { if transaction_type == Zero::zero() { TransactionType::Signed } else if transaction_type == BlockNumberFor::::from(1u32) { - TransactionType::UnsignedForAny + TransactionType::AuthorizedForAny } else if transaction_type == BlockNumberFor::::from(2u32) { - TransactionType::UnsignedForAll + TransactionType::AuthorizedForAll } else { TransactionType::Raw } @@ -481,25 +481,25 @@ impl Pallet { Ok(()) } - /// A helper function to fetch the price and send a raw unsigned transaction. - fn fetch_price_and_send_raw_unsigned( + /// A helper function to fetch the price and send a raw authorized transaction. + fn fetch_price_and_send_raw_authorized( block_number: BlockNumberFor, ) -> Result<(), &'static str> { - // Make sure we don't fetch the price if unsigned transaction is going to be rejected + // Make sure we don't fetch the price if the authorized transaction is going to be rejected // anyway. - let next_unsigned_at = NextUnsignedAt::::get(); - if next_unsigned_at > block_number { - return Err("Too early to send unsigned transaction") + let next_authorized_at = NextAuthorizedAt::::get(); + if next_authorized_at > block_number { + return Err("Too early to send authorized transaction") } // Make an external HTTP request to fetch the current price. // Note this call will block until response is received. let price = Self::fetch_price().map_err(|_| "Failed to fetch price")?; - // Received price is wrapped into a call to `submit_price_unsigned` public function of this + // Received price is wrapped into a call to `submit_price_authorized` public function of this // pallet. This means that the transaction, when executed, will simply call that function // passing `price` as an argument. - let call = Call::submit_price_unsigned { block_number, price }; + let call = Call::submit_price_authorized { block_number, price }; // Now let's create a transaction out of this call and submit it to the pool. // Here we showcase two ways to send a general transaction / unsigned payload (raw) @@ -519,22 +519,22 @@ impl Pallet { Ok(()) } - /// A helper function to fetch the price, sign payload and send an unsigned transaction - fn fetch_price_and_send_unsigned_for_any_account( + /// A helper function to fetch the price, sign payload and send an authorized transaction + fn fetch_price_and_send_authorized_tx_for_any_account( block_number: BlockNumberFor, ) -> Result<(), &'static str> { - // Make sure we don't fetch the price if unsigned transaction is going to be rejected + // Make sure we don't fetch the price if the authorized transaction is going to be rejected // anyway. - let next_unsigned_at = NextUnsignedAt::::get(); - if next_unsigned_at > block_number { - return Err("Too early to send unsigned transaction") + let next_authorized_at = NextAuthorizedAt::::get(); + if next_authorized_at > block_number { + return Err("Too early to send authorized transaction") } // Make an external HTTP request to fetch the current price. // Note this call will block until response is received. let price = Self::fetch_price().map_err(|_| "Failed to fetch price")?; - // -- Sign using any account and create an authorized transaction + // Sign using any account and create an authorized transaction let signer = Signer::::any_account(); // Get the first available account @@ -558,7 +558,7 @@ impl Pallet { .ok_or("Failed to sign payload")?; // Create the call with the signed payload - let call = Call::submit_price_unsigned_with_signed_payload { + let call = Call::submit_price_authorized_with_signed_payload { price_payload: payload, signature, }; @@ -571,15 +571,15 @@ impl Pallet { Ok(()) } - /// A helper function to fetch the price, sign payload and send an unsigned transaction - fn fetch_price_and_send_unsigned_for_all_accounts( + /// A helper function to fetch the price, sign payload and send an authorized transaction + fn fetch_price_and_send_authorized_tx_for_all_accounts( block_number: BlockNumberFor, ) -> Result<(), &'static str> { - // Make sure we don't fetch the price if unsigned transaction is going to be rejected + // Make sure we don't fetch the price if the authorized transaction is going to be rejected // anyway. - let next_unsigned_at = NextUnsignedAt::::get(); - if next_unsigned_at > block_number { - return Err("Too early to send unsigned transaction") + let next_authorized_at = NextAuthorizedAt::::get(); + if next_authorized_at > block_number { + return Err("Too early to send authorized transaction") } // Make an external HTTP request to fetch the current price. @@ -606,7 +606,7 @@ impl Pallet { .ok_or("Failed to sign payload")?; // Create the call with the signed payload - let call = Call::submit_price_unsigned_with_signed_payload { + let call = Call::submit_price_authorized_with_signed_payload { price_payload: payload, signature, }; @@ -727,8 +727,8 @@ impl Pallet { new_price: &u32, ) -> TransactionValidity { // Now let's check if the transaction has any chance to succeed. - let next_unsigned_at = NextUnsignedAt::::get(); - if &next_unsigned_at > block_number { + let next_authorized_at = NextAuthorizedAt::::get(); + if &next_authorized_at > block_number { return InvalidTransaction::Stale.into() } // Let's make sure to reject transactions from the future. @@ -751,17 +751,17 @@ impl Pallet { // transactions in the pool. Next we tweak the priority depending on how much // it differs from the current average. (the more it differs the more priority it // has). - .priority(T::UnsignedPriority::get().saturating_add(avg_price as _)) + .priority(T::AuthorizedTxPriority::get().saturating_add(avg_price as _)) // This transaction does not require anything else to go before into the pool. - // In theory we could require `previous_unsigned_at` transaction to go first, + // In theory we could require `previous_authorized_at` transaction to go first, // but it's not necessary in our case. //.and_requires() - // We set the `provides` tag to be the same as `next_unsigned_at`. This makes - // sure only one transaction produced after `next_unsigned_at` will ever + // We set the `provides` tag to be the same as `next_authorized_at`. This makes + // sure only one transaction produced after `next_authorized_at` will ever // get to the transaction pool and will end up in the block. // We can still have multiple transactions compete for the same "spot", // and the one with higher priority will replace other one in the pool. - .and_provides(next_unsigned_at) + .and_provides(next_authorized_at) // The transaction is only valid for next 5 blocks. After that it's // going to be revalidated by the pool. .longevity(5) diff --git a/substrate/frame/examples/offchain-worker/src/tests.rs b/substrate/frame/examples/offchain-worker/src/tests.rs index fd9c127ea3303..741e7cd889411 100644 --- a/substrate/frame/examples/offchain-worker/src/tests.rs +++ b/substrate/frame/examples/offchain-worker/src/tests.rs @@ -131,14 +131,14 @@ where } parameter_types! { - pub const UnsignedPriority: u64 = 1 << 20; + pub const AuthorizedTxPriority: u64 = 1 << 20; } impl Config for Test { type AuthorityId = crypto::TestAuthId; type GracePeriod = ConstU64<5>; - type UnsignedInterval = ConstU64<128>; - type UnsignedPriority = UnsignedPriority; + type AuthorizedTxInterval = ConstU64<128>; + type AuthorizedTxPriority = AuthorizedTxPriority; type MaxPrices = ConstU32<64>; } @@ -281,13 +281,13 @@ fn should_submit_unsigned_transaction_on_chain_for_any_account() { // let signature = price_payload.sign::().unwrap(); t.execute_with(|| { // when - Example::fetch_price_and_send_unsigned_for_any_account(1).unwrap(); + Example::fetch_price_and_send_authorized_tx_for_any_account(1).unwrap(); // then let tx = pool_state.write().transactions.pop().unwrap(); let tx = Extrinsic::decode(&mut &*tx).unwrap(); // General transactions are neither inherent nor signed (old-school) assert!(!tx.is_inherent() && !tx.is_signed()); - if let RuntimeCall::Example(crate::Call::submit_price_unsigned_with_signed_payload { + if let RuntimeCall::Example(crate::Call::submit_price_authorized_with_signed_payload { price_payload: body, signature, }) = tx.function @@ -337,13 +337,13 @@ fn should_submit_unsigned_transaction_on_chain_for_all_accounts() { // let signature = price_payload.sign::().unwrap(); t.execute_with(|| { // when - Example::fetch_price_and_send_unsigned_for_all_accounts(1).unwrap(); + Example::fetch_price_and_send_authorized_tx_for_all_accounts(1).unwrap(); // then let tx = pool_state.write().transactions.pop().unwrap(); let tx = Extrinsic::decode(&mut &*tx).unwrap(); // General transactions are neither inherent nor signed (old-school) assert!(!tx.is_inherent() && !tx.is_signed()); - if let RuntimeCall::Example(crate::Call::submit_price_unsigned_with_signed_payload { + if let RuntimeCall::Example(crate::Call::submit_price_authorized_with_signed_payload { price_payload: body, signature, }) = tx.function @@ -378,7 +378,7 @@ fn should_submit_raw_unsigned_transaction_on_chain() { t.execute_with(|| { // when - Example::fetch_price_and_send_raw_unsigned(1).unwrap(); + Example::fetch_price_and_send_raw_authorized(1).unwrap(); // then let tx = pool_state.write().transactions.pop().unwrap(); assert!(pool_state.read().transactions.is_empty()); @@ -387,7 +387,7 @@ fn should_submit_raw_unsigned_transaction_on_chain() { assert!(!tx.is_inherent() && !tx.is_signed()); assert_eq!( tx.function, - RuntimeCall::Example(crate::Call::submit_price_unsigned { + RuntimeCall::Example(crate::Call::submit_price_authorized { block_number: 1, price: 15523 }) From edd69813f5fdb6e913fff77b7d468a961658c36c Mon Sep 17 00:00:00 2001 From: Rodrigo Quelhas Date: Thu, 1 Jan 2026 13:59:20 +0000 Subject: [PATCH 07/11] fix formatting --- .../frame/examples/offchain-worker/src/lib.rs | 54 ++++++++----------- 1 file changed, 21 insertions(+), 33 deletions(-) diff --git a/substrate/frame/examples/offchain-worker/src/lib.rs b/substrate/frame/examples/offchain-worker/src/lib.rs index 71e63e77ee230..914e2fc8adf52 100644 --- a/substrate/frame/examples/offchain-worker/src/lib.rs +++ b/substrate/frame/examples/offchain-worker/src/lib.rs @@ -60,8 +60,8 @@ use frame_support::{traits::Get, weights::Weight}; use frame_system::{ self as system, offchain::{ - AppCrypto, CreateAuthorizedTransaction, CreateSignedTransaction, - SendSignedTransaction, SignedPayload, Signer, SigningTypes, SubmitTransaction, + AppCrypto, CreateAuthorizedTransaction, CreateSignedTransaction, SendSignedTransaction, + SignedPayload, Signer, SigningTypes, SubmitTransaction, }, pallet_prelude::BlockNumberFor, }; @@ -136,8 +136,8 @@ pub mod pallet { #[pallet::config] pub trait Config: CreateSignedTransaction> - + CreateAuthorizedTransaction> - + frame_system::Config + + CreateAuthorizedTransaction> + + frame_system::Config { /// The identifier type for an offchain worker. type AuthorityId: AppCrypto; @@ -154,8 +154,8 @@ pub mod pallet { /// Number of blocks of cooldown after an authorized transaction is included. /// - /// This ensures that we only accept authorized transactions once, every `AuthorizedTxInterval` - /// blocks. + /// This ensures that we only accept authorized transactions once, every + /// `AuthorizedTxInterval` blocks. #[pallet::constant] type AuthorizedTxInterval: Get>; @@ -260,8 +260,9 @@ pub mod pallet { /// This function can be called only once every `T::AuthorizedTxInterval` blocks. /// /// Transactions are de-duplicated on the pool level via the `provides` tag in the - /// validation logic (`validate_transaction_parameters`), which returns `next_authorized_at`. - /// This ensures only one transaction per interval can be included in a block. + /// validation logic (`validate_transaction_parameters`), which returns + /// `next_authorized_at`. This ensures only one transaction per interval can be included + /// in a block. /// /// It's important to specify `weight` for unsigned calls as well, because even though /// they don't charge fees, we still don't want a single block to contain unlimited @@ -496,9 +497,9 @@ impl Pallet { // Note this call will block until response is received. let price = Self::fetch_price().map_err(|_| "Failed to fetch price")?; - // Received price is wrapped into a call to `submit_price_authorized` public function of this - // pallet. This means that the transaction, when executed, will simply call that function - // passing `price` as an argument. + // Received price is wrapped into a call to `submit_price_authorized` public function of + // this pallet. This means that the transaction, when executed, will simply call that + // function passing `price` as an argument. let call = Call::submit_price_authorized { block_number, price }; // Now let's create a transaction out of this call and submit it to the pool. @@ -538,17 +539,10 @@ impl Pallet { let signer = Signer::::any_account(); // Get the first available account - let account = signer - .accounts_from_keys() - .next() - .ok_or("No local accounts available.")?; + let account = signer.accounts_from_keys().next().ok_or("No local accounts available.")?; // Create the payload to sign - let payload = PricePayload { - price, - block_number, - public: account.public.clone(), - }; + let payload = PricePayload { price, block_number, public: account.public.clone() }; // Sign the payload let signature = Pallet { .ok_or("Failed to sign payload")?; // Create the call with the signed payload - let call = Call::submit_price_authorized_with_signed_payload { - price_payload: payload, - signature, - }; + let call = + Call::submit_price_authorized_with_signed_payload { price_payload: payload, signature }; // Create an authorized transaction and submit it let xt = T::create_authorized_transaction(call.into()); @@ -592,17 +584,13 @@ impl Pallet { // Iterate over all available accounts for account in signer.accounts_from_keys() { // Create the payload to sign - let payload = PricePayload { - price, - block_number, - public: account.public.clone(), - }; + let payload = PricePayload { price, block_number, public: account.public.clone() }; // Sign the payload - let signature = ::Public, - BlockNumberFor, - > as SignedPayload>::sign::(&payload) + let signature = + ::Public, BlockNumberFor> as SignedPayload< + T, + >>::sign::(&payload) .ok_or("Failed to sign payload")?; // Create the call with the signed payload From 1e9e48712f776caf20a607ee1e28c8d35dcdba45 Mon Sep 17 00:00:00 2001 From: Rodrigo Quelhas Date: Thu, 1 Jan 2026 14:08:04 +0000 Subject: [PATCH 08/11] align naming --- substrate/frame/examples/offchain-worker/src/lib.rs | 2 +- substrate/frame/examples/offchain-worker/src/tests.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/substrate/frame/examples/offchain-worker/src/lib.rs b/substrate/frame/examples/offchain-worker/src/lib.rs index 914e2fc8adf52..3fd2eb298af1a 100644 --- a/substrate/frame/examples/offchain-worker/src/lib.rs +++ b/substrate/frame/examples/offchain-worker/src/lib.rs @@ -264,7 +264,7 @@ pub mod pallet { /// `next_authorized_at`. This ensures only one transaction per interval can be included /// in a block. /// - /// It's important to specify `weight` for unsigned calls as well, because even though + /// It's important to specify `weight` for authorized calls as well, because even though /// they don't charge fees, we still don't want a single block to contain unlimited /// number of such transactions. /// diff --git a/substrate/frame/examples/offchain-worker/src/tests.rs b/substrate/frame/examples/offchain-worker/src/tests.rs index 741e7cd889411..82f0e594c09b7 100644 --- a/substrate/frame/examples/offchain-worker/src/tests.rs +++ b/substrate/frame/examples/offchain-worker/src/tests.rs @@ -251,7 +251,7 @@ fn should_submit_signed_transaction_on_chain() { } #[test] -fn should_submit_unsigned_transaction_on_chain_for_any_account() { +fn should_submit_authorized_transaction_on_chain_for_any_account() { const PHRASE: &str = "news slush supreme milk chapter athlete soap sausage put clutch what kitten"; let (offchain, offchain_state) = testing::TestOffchainExt::new(); @@ -307,7 +307,7 @@ fn should_submit_unsigned_transaction_on_chain_for_any_account() { } #[test] -fn should_submit_unsigned_transaction_on_chain_for_all_accounts() { +fn should_submit_authorized_transaction_on_chain_for_all_accounts() { const PHRASE: &str = "news slush supreme milk chapter athlete soap sausage put clutch what kitten"; let (offchain, offchain_state) = testing::TestOffchainExt::new(); @@ -363,7 +363,7 @@ fn should_submit_unsigned_transaction_on_chain_for_all_accounts() { } #[test] -fn should_submit_raw_unsigned_transaction_on_chain() { +fn should_submit_raw_authorized_transaction_on_chain() { let (offchain, offchain_state) = testing::TestOffchainExt::new(); let (pool, pool_state) = testing::TestTransactionPoolExt::new(); From ad0ed6f8a34da54e5d067a8cda3e68887f130867 Mon Sep 17 00:00:00 2001 From: Rodrigo Quelhas Date: Thu, 1 Jan 2026 22:28:48 +0000 Subject: [PATCH 09/11] improve tests --- .../examples/offchain-worker/src/tests.rs | 125 ++++++++++++++++-- 1 file changed, 111 insertions(+), 14 deletions(-) diff --git a/substrate/frame/examples/offchain-worker/src/tests.rs b/substrate/frame/examples/offchain-worker/src/tests.rs index 82f0e594c09b7..c1cecb1bc61e6 100644 --- a/substrate/frame/examples/offchain-worker/src/tests.rs +++ b/substrate/frame/examples/offchain-worker/src/tests.rs @@ -41,7 +41,16 @@ use sp_runtime::{ RuntimeAppPublic, }; -type Block = frame_system::mocking::MockBlock; +// Use AuthorizeCall as the transaction extension to properly test the #[pallet::authorize] +// validation +type TxExtension = frame_system::AuthorizeCall; +type Extrinsic = TestXt; + +// Define a custom Block that uses our Extrinsic with AuthorizeCall extension +type Block = sp_runtime::generic::Block< + sp_runtime::generic::Header, + Extrinsic, +>; // For testing the module, we construct a mock runtime. frame_support::construct_runtime!( @@ -78,7 +87,6 @@ impl frame_system::Config for Test { type MaxConsumers = ConstU32<16>; } -type Extrinsic = TestXt; type AccountId = <::Signer as IdentifyAccount>::AccountId; impl frame_system::offchain::SigningTypes for Test { @@ -98,10 +106,10 @@ impl frame_system::offchain::CreateTransaction for Test where RuntimeCall: From, { - type Extension = (); + type Extension = TxExtension; - fn create_transaction(call: RuntimeCall, _extension: Self::Extension) -> Extrinsic { - Extrinsic::new_transaction(call, ()) + fn create_transaction(call: RuntimeCall, extension: Self::Extension) -> Extrinsic { + Extrinsic::new_transaction(call, extension) } } @@ -117,7 +125,9 @@ where _account: AccountId, nonce: u64, ) -> Option { - Some(Extrinsic::new_signed(call, nonce, (), ())) + // For signed transactions, AuthorizeCall will detect that the origin is already + // authorized by the signature and will pass through without calling authorize() + Some(Extrinsic::new_signed(call, nonce, (), TxExtension::new())) } } @@ -126,7 +136,8 @@ where RuntimeCall: From, { fn create_extension() -> Self::Extension { - () + // This will trigger the #[pallet::authorize] validation + TxExtension::new() } } @@ -245,7 +256,8 @@ fn should_submit_signed_transaction_on_chain() { let tx = pool_state.write().transactions.pop().unwrap(); assert!(pool_state.read().transactions.is_empty()); let tx = Extrinsic::decode(&mut &*tx).unwrap(); - assert!(matches!(tx.preamble, sp_runtime::generic::Preamble::Signed(0, (), (),))); + // For signed transactions, the preamble includes the signature and bypass AuthorizeCall + assert!(matches!(tx.preamble, sp_runtime::generic::Preamble::Signed(0, (), _))); assert_eq!(tx.function, RuntimeCall::Example(crate::Call::submit_price { price: 15523 })); }); } @@ -278,15 +290,32 @@ fn should_submit_authorized_transaction_on_chain_for_any_account() { public: ::Public::from(public_key), }; - // let signature = price_payload.sign::().unwrap(); t.execute_with(|| { + // Set up the block number to match the transaction + System::set_block_number(1); + // when - Example::fetch_price_and_send_authorized_tx_for_any_account(1).unwrap(); + assert_ok!(Example::fetch_price_and_send_authorized_tx_for_any_account(1)); // then let tx = pool_state.write().transactions.pop().unwrap(); let tx = Extrinsic::decode(&mut &*tx).unwrap(); - // General transactions are neither inherent nor signed (old-school) + // General transactions are neither inherent nor signed assert!(!tx.is_inherent() && !tx.is_signed()); + + // Actually validate the transaction through the authorize logic + use frame_support::traits::Authorize; + use sp_runtime::transaction_validity::TransactionSource; + + let authorize_result = tx.function.authorize(TransactionSource::External); + assert!( + authorize_result.is_some(), + "Transaction should have authorization logic from #[pallet::authorize]" + ); + assert!( + authorize_result.unwrap().is_ok(), + "Transaction should pass #[pallet::authorize] validation" + ); + if let RuntimeCall::Example(crate::Call::submit_price_authorized_with_signed_payload { price_payload: body, signature, @@ -334,15 +363,32 @@ fn should_submit_authorized_transaction_on_chain_for_all_accounts() { public: ::Public::from(public_key), }; - // let signature = price_payload.sign::().unwrap(); t.execute_with(|| { + // Set up the block number to match the transaction + System::set_block_number(1); + // when Example::fetch_price_and_send_authorized_tx_for_all_accounts(1).unwrap(); // then let tx = pool_state.write().transactions.pop().unwrap(); let tx = Extrinsic::decode(&mut &*tx).unwrap(); - // General transactions are neither inherent nor signed (old-school) + // General transactions are neither inherent nor signed assert!(!tx.is_inherent() && !tx.is_signed()); + + // Actually validate the transaction through the authorize logic + use frame_support::traits::Authorize; + use sp_runtime::transaction_validity::TransactionSource; + + let authorize_result = tx.function.authorize(TransactionSource::External); + assert!( + authorize_result.is_some(), + "Transaction should have authorization logic from #[pallet::authorize]" + ); + assert!( + authorize_result.unwrap().is_ok(), + "Transaction should pass #[pallet::authorize] validation" + ); + if let RuntimeCall::Example(crate::Call::submit_price_authorized_with_signed_payload { price_payload: body, signature, @@ -377,14 +423,32 @@ fn should_submit_raw_authorized_transaction_on_chain() { price_oracle_response(&mut offchain_state.write()); t.execute_with(|| { + // Set up the block number to match the transaction + System::set_block_number(1); + // when Example::fetch_price_and_send_raw_authorized(1).unwrap(); // then let tx = pool_state.write().transactions.pop().unwrap(); assert!(pool_state.read().transactions.is_empty()); let tx = Extrinsic::decode(&mut &*tx).unwrap(); - // General transactions are neither inherent nor signed (old-school) + // General transactions are neither inherent nor signed assert!(!tx.is_inherent() && !tx.is_signed()); + + // Actually validate the transaction through the authorize logic + use frame_support::traits::Authorize; + use sp_runtime::transaction_validity::TransactionSource; + + let authorize_result = tx.function.authorize(TransactionSource::External); + assert!( + authorize_result.is_some(), + "Transaction should have authorization logic from #[pallet::authorize]" + ); + assert!( + authorize_result.unwrap().is_ok(), + "Transaction should pass #[pallet::authorize] validation" + ); + assert_eq!( tx.function, RuntimeCall::Example(crate::Call::submit_price_authorized { @@ -395,6 +459,39 @@ fn should_submit_raw_authorized_transaction_on_chain() { }); } +#[test] +fn should_reject_invalid_authorized_transaction() { + let mut t = sp_io::TestExternalities::default(); + + t.execute_with(|| { + // Set NextAuthorizedAt to block 100, so any transaction at block 1 should be stale + crate::NextAuthorizedAt::::put(100u64); + + // Try to create a general transaction with an old block number (should be rejected) + let call = RuntimeCall::Example(crate::Call::submit_price_authorized { + block_number: 1, + price: 100, + }); + + // Try to validate the call's authorization - this should FAIL because the block number is + // too old + use frame_support::traits::Authorize; + use sp_runtime::transaction_validity::TransactionSource; + + let authorize_result = call.authorize(TransactionSource::External); + assert!( + authorize_result.is_some(), + "Transaction should have authorization logic from #[pallet::authorize]" + ); + + // Verify that validation failed due to stale block number + assert!( + authorize_result.unwrap().is_err(), + "Transaction with stale block number should be rejected by #[pallet::authorize] validation" + ); + }); +} + fn price_oracle_response(state: &mut testing::OffchainState) { state.expect_request(testing::PendingRequest { method: "GET".into(), From ced11f55d311e6bd9e7534e0ff3f742f12e4cb2a Mon Sep 17 00:00:00 2001 From: Rodrigo Quelhas Date: Fri, 2 Jan 2026 09:13:43 +0000 Subject: [PATCH 10/11] add integrity test --- .../frame/examples/offchain-worker/src/lib.rs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/substrate/frame/examples/offchain-worker/src/lib.rs b/substrate/frame/examples/offchain-worker/src/lib.rs index 3fd2eb298af1a..42a115573d874 100644 --- a/substrate/frame/examples/offchain-worker/src/lib.rs +++ b/substrate/frame/examples/offchain-worker/src/lib.rs @@ -133,6 +133,12 @@ pub mod pallet { use frame_system::pallet_prelude::*; /// This pallet's configuration trait + /// + /// # Requirements + /// + /// This pallet requires `frame_system::AuthorizeCall` to be included in the runtime's + /// transaction extension pipeline (configured via `frame_system::Config::Block`). + /// The integrity test will verify this at runtime. #[pallet::config] pub trait Config: CreateSignedTransaction> @@ -176,6 +182,32 @@ pub mod pallet { #[pallet::hooks] impl Hooks> for Pallet { + /// Integrity test to ensure AuthorizeCall is configured in the runtime. + /// + /// This pallet uses the `#[pallet::authorize]` attribute on calls that require + /// authorization validation. For these calls to work properly, the runtime must + /// include `frame_system::AuthorizeCall` as part of its transaction extension pipeline. + /// + /// This test validates that the Block type's Extrinsic includes the necessary + /// transaction extension structure by checking the type name. + fn integrity_test() { + use sp_runtime::traits::Block as BlockT; + + // Get the full type name of the Block's Extrinsic type + let extrinsic_type_name = core::any::type_name::<::Extrinsic>(); + + // Verify that AuthorizeCall is present in the extrinsic type + // The extrinsic should contain the AuthorizeCall extension in its structure + assert!( + extrinsic_type_name.contains("frame_system::extensions::authorize_call::AuthorizeCall"), + "The runtime must include `frame_system::AuthorizeCall` in its transaction extension \ + pipeline for this pallet to work correctly. The pallet uses `#[pallet::authorize]` \ + which requires AuthorizeCall to validate authorized transactions. \ + Current extrinsic type: {}", + extrinsic_type_name + ); + } + /// Offchain Worker entry point. /// /// By implementing `fn offchain_worker` you declare a new offchain worker. From 4e4302646006487512493e914033c1177cd5eba0 Mon Sep 17 00:00:00 2001 From: Rodrigo Quelhas Date: Wed, 7 Jan 2026 13:38:41 +0000 Subject: [PATCH 11/11] address remarks --- substrate/frame/examples/offchain-worker/src/lib.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/substrate/frame/examples/offchain-worker/src/lib.rs b/substrate/frame/examples/offchain-worker/src/lib.rs index 42a115573d874..411b2f43ff02c 100644 --- a/substrate/frame/examples/offchain-worker/src/lib.rs +++ b/substrate/frame/examples/offchain-worker/src/lib.rs @@ -137,7 +137,7 @@ pub mod pallet { /// # Requirements /// /// This pallet requires `frame_system::AuthorizeCall` to be included in the runtime's - /// transaction extension pipeline (configured via `frame_system::Config::Block`). + /// transaction extension pipeline. /// The integrity test will verify this at runtime. #[pallet::config] pub trait Config: @@ -195,11 +195,12 @@ pub mod pallet { // Get the full type name of the Block's Extrinsic type let extrinsic_type_name = core::any::type_name::<::Extrinsic>(); + let extension_type_name = core::any::type_name::>(); // Verify that AuthorizeCall is present in the extrinsic type // The extrinsic should contain the AuthorizeCall extension in its structure assert!( - extrinsic_type_name.contains("frame_system::extensions::authorize_call::AuthorizeCall"), + extrinsic_type_name.contains(extension_type_name), "The runtime must include `frame_system::AuthorizeCall` in its transaction extension \ pipeline for this pallet to work correctly. The pallet uses `#[pallet::authorize]` \ which requires AuthorizeCall to validate authorized transactions. \