Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions prdoc/pr_10716.prdoc
Original file line number Diff line number Diff line change
@@ -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
22 changes: 19 additions & 3 deletions substrate/frame/examples/offchain-worker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.**
Expand All @@ -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
99 changes: 57 additions & 42 deletions substrate/frame/examples/offchain-worker/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,17 @@
//! 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)]

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::{
Expand All @@ -73,7 +74,10 @@ use sp_runtime::{
Duration,
},
traits::Zero,
transaction_validity::{InvalidTransaction, TransactionValidity, ValidTransaction},
transaction_validity::{
InvalidTransaction, TransactionSource, TransactionValidity, TransactionValidityWithRefund,
ValidTransaction,
},
Debug,
};

Expand Down Expand Up @@ -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
Expand All @@ -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<T>,
new_price: &u32,
| -> TransactionValidityWithRefund {
let validity = Pallet::<T>::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<T>,
_block_number: BlockNumberFor<T>,
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.
Expand All @@ -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<T::Public, BlockNumberFor<T>>,
signature: &T::Signature,
| -> TransactionValidityWithRefund {
let signature_valid = SignedPayload::<T>::verify::<T::AuthorityId>(price_payload, signature.clone());
if !signature_valid {
return Err(InvalidTransaction::BadProof.into())
}
let validity = Pallet::<T>::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<T>,
price_payload: PricePayload<T::Public, BlockNumberFor<T>>,
_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.
Expand All @@ -305,36 +350,6 @@ pub mod pallet {
NewPrice { price: u32, maybe_who: Option<T::AccountId> },
}

#[pallet::validate_unsigned]
impl<T: Config> ValidateUnsigned for Pallet<T> {
type Call = Call<T>;

/// 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::<T>::verify::<T::AuthorityId>(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.
Expand Down Expand Up @@ -504,7 +519,7 @@ impl<T: Config> Pallet<T> {
// 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.
//
Expand Down
Loading