Skip to content
Open
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 33 additions & 0 deletions prdoc/pr_11010.prdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
title: Migrate `pallet-mixnet` to use `TransactionExtension` API

doc:
- audience: Runtime Dev
description: |-
Migrates `pallet-mixnet` from the deprecated `ValidateUnsigned` trait to the modern
`TransactionExtension` API using the `#[pallet::authorize]` attribute.

This change uses the `#[pallet::authorize]` attribute to validate mixnet registration
transactions, providing better composability and flexibility.

The pallet now uses `#[pallet::authorize]` on the `register` call to validate:
- Session index matches the current session (not future/stale)
- Authority index is valid and within bounds
- Authority ID exists in the authority set
- Authority hasn't already registered for this session
- Signature is valid for the registration data

Runtime integrators: the pallet's `Config` trait now requires
`CreateAuthorizedTransaction<Call<Self>>` instead of `CreateBare<Call<Self>>`.
If your runtime already has a blanket `impl<C> CreateAuthorizedTransaction<C> for Runtime`
(as kitchensink does), no extra changes are needed.

This serves as a reference example for other consensus/authority-based pallets
migrating away from `ValidateUnsigned`.

crates:
- name: pallet-mixnet
bump: major
- name: kitchensink-runtime
bump: patch
- name: polkadot-sdk
bump: patch
2 changes: 2 additions & 0 deletions substrate/bin/node/runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2467,6 +2467,7 @@ parameter_types! {
}

impl pallet_mixnet::Config for Runtime {
type WeightInfo = pallet_mixnet::weights::SubstrateWeight<Runtime>;
type MaxAuthorities = MaxAuthorities;
type MaxExternalAddressSize = ConstU32<128>;
type MaxExternalAddressesPerMixnode = ConstU32<16>;
Expand Down Expand Up @@ -3207,6 +3208,7 @@ mod benches {
[pallet_asset_conversion_ops, AssetConversionMigration]
[pallet_verify_signature, VerifySignature]
[pallet_meta_tx, MetaTx]
[pallet_mixnet, Mixnet]
);
}

Expand Down
6 changes: 6 additions & 0 deletions substrate/frame/mixnet/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ serde = { features = ["derive"], workspace = true }
sp-application-crypto = { workspace = true }
sp-mixnet = { workspace = true }

[dev-dependencies]
sp-keystore = { workspace = true, default-features = true }

[features]
default = ["std"]
std = [
Expand All @@ -35,6 +38,9 @@ std = [
"sp-application-crypto/std",
"sp-mixnet/std",
]
runtime-benchmarks = [
"frame/runtime-benchmarks",
]
try-runtime = [
"frame/try-runtime",
]
88 changes: 88 additions & 0 deletions substrate/frame/mixnet/src/benchmarking.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// This file is part of Substrate.

// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Benchmarking for `pallet-mixnet`.

#![cfg(feature = "runtime-benchmarks")]

use super::*;
use frame::{benchmarking::prelude::*, deps::frame_support::traits::Authorize};
use sp_mixnet::types::{AuthorityId, AuthoritySignature};

fn setup_registration<T: Config>() -> (RegistrationFor<T>, AuthoritySignature) {
let session_index = 0u32;
let authority_index = 0u32;

// Use RuntimeAppPublic to generate a keypair in the keystore.
// This works in both std and no_std (via host functions), unlike
// sp_core::Pair which requires the `full_crypto` feature (std only).
let authority_id = AuthorityId::generate_pair(None);

pallet::CurrentSessionIndex::<T>::put(session_index);
pallet::NextAuthorityIds::<T>::insert(authority_index, authority_id.clone());

let registration = Registration {
block_number: 1u32.into(),
session_index,
authority_index,
mixnode: BoundedMixnode {
kx_public: [1u8; 32],
peer_id: [2u8; 32],
external_addresses: Default::default(),
},
};
let signature = authority_id
.sign(&registration.encode())
.expect("Key was just generated and is in keystore; qed");

(registration, signature)
}

#[benchmarks]
mod benchmarks {
use super::*;

/// Measures the execution time of `register` dispatch.
#[benchmark]
fn register() {
let (registration, signature) = setup_registration::<T>();
let session_index = registration.session_index;
let authority_index = registration.authority_index;

#[extrinsic_call]
_(RawOrigin::Authorized, registration, signature);

assert!(pallet::Mixnodes::<T>::contains_key(session_index + 1, authority_index));
}

/// Measures the weight of the authorize closure for `register`.
#[benchmark]
fn authorize_register() {
let (registration, signature) = setup_registration::<T>();
let call = pallet::Call::<T>::register { registration, signature };
let source = TransactionSource::External;

#[block]
{
call.authorize(source)
.expect("Call should have authorize logic")
.expect("Authorization should succeed");
}
}

impl_benchmark_test_suite!(Pallet, crate::tests::new_test_ext(), crate::tests::Test);
}
96 changes: 53 additions & 43 deletions substrate/frame/mixnet/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,21 @@

extern crate alloc;

#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
#[cfg(test)]
mod tests;
pub mod weights;
pub use weights::*;

pub use pallet::*;

use alloc::vec::Vec;
use core::cmp::Ordering;
use frame::{
deps::{
sp_io::{self, MultiRemovalResults},
sp_runtime,
sp_runtime::{self, transaction_validity::TransactionValidityWithRefund},
},
prelude::*,
};
Expand Down Expand Up @@ -178,8 +185,15 @@ pub mod pallet {
#[pallet::pallet]
pub struct Pallet<T>(_);

/// # Requirements
///
/// This pallet requires `frame_system::AuthorizeCall` to be included in the runtime's
/// transaction extension pipeline.
#[pallet::config]
pub trait Config: frame_system::Config + CreateBare<Call<Self>> {
pub trait Config: frame_system::Config + CreateAuthorizedTransaction<Call<Self>> {
/// Weight functions for this pallet.
type WeightInfo: WeightInfo;

/// The maximum number of authorities per session.
#[pallet::constant]
type MaxAuthorities: Get<AuthorityIndex>;
Expand Down Expand Up @@ -221,7 +235,7 @@ pub mod pallet {
#[pallet::constant]
type NumRegisterEndSlackBlocks: Get<BlockNumberFor<Self>>;

/// Priority of unsigned transactions used to register mixnodes.
/// Priority of authorized transactions used to register mixnodes.
#[pallet::constant]
type RegistrationPriority: Get<TransactionPriority>;

Expand Down Expand Up @@ -280,57 +294,32 @@ pub mod pallet {
impl<T: Config> Pallet<T> {
/// Register a mixnode for the following session.
#[pallet::call_index(0)]
#[pallet::weight(1)] // TODO
pub fn register(
origin: OriginFor<T>,
registration: RegistrationFor<T>,
_signature: AuthoritySignature,
) -> DispatchResult {
ensure_none(origin)?;

// Checked by ValidateUnsigned
debug_assert_eq!(registration.session_index, CurrentSessionIndex::<T>::get());
debug_assert!(registration.authority_index < T::MaxAuthorities::get());

Mixnodes::<T>::insert(
// Registering for the _following_ session
registration.session_index + 1,
registration.authority_index,
registration.mixnode,
);

Ok(())
}
}

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

fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity {
let Self::Call::register { registration, signature } = call else {
return InvalidTransaction::Call.into();
};

#[pallet::weight(T::WeightInfo::register())]
#[pallet::weight_of_authorize(T::WeightInfo::authorize_register())]
#[pallet::authorize(|
_source: TransactionSource,
registration: &RegistrationFor<T>,
signature: &AuthoritySignature,
| -> TransactionValidityWithRefund {
// Check session index matches
match registration.session_index.cmp(&CurrentSessionIndex::<T>::get()) {
Ordering::Greater => return InvalidTransaction::Future.into(),
Ordering::Less => return InvalidTransaction::Stale.into(),
Ordering::Greater => return Err(InvalidTransaction::Future.into()),
Ordering::Less => return Err(InvalidTransaction::Stale.into()),
Ordering::Equal => (),
}

// Check authority index is valid
if registration.authority_index >= T::MaxAuthorities::get() {
return InvalidTransaction::BadProof.into();
return Err(InvalidTransaction::BadProof.into());
}
let Some(authority_id) = NextAuthorityIds::<T>::get(registration.authority_index)
else {
return InvalidTransaction::BadProof.into();
return Err(InvalidTransaction::BadProof.into());
};

// Check the authority hasn't registered a mixnode yet
if Self::already_registered(registration.session_index, registration.authority_index) {
return InvalidTransaction::Stale.into();
if Pallet::<T>::already_registered(registration.session_index, registration.authority_index) {
return Err(InvalidTransaction::Stale.into());
}

// Check signature. Note that we don't use regular signed transactions for registration
Expand All @@ -340,7 +329,7 @@ pub mod pallet {
authority_id.verify(&encoded_registration, signature)
});
if !signature_ok {
return InvalidTransaction::BadProof.into();
return Err(InvalidTransaction::BadProof.into());
}

ValidTransaction::with_tag_prefix("MixnetRegistration")
Expand All @@ -358,6 +347,27 @@ pub mod pallet {
.unwrap_or(64_u64),
)
.build()
.map(|v| (v, /* no refund */ Weight::zero()))
})]
pub fn register(
origin: OriginFor<T>,
registration: RegistrationFor<T>,
_signature: AuthoritySignature,
) -> DispatchResult {
ensure_authorized(origin)?;

// Checked by authorize
debug_assert_eq!(registration.session_index, CurrentSessionIndex::<T>::get());
debug_assert!(registration.authority_index < T::MaxAuthorities::get());

Mixnodes::<T>::insert(
// Registering for the _following_ session
registration.session_index + 1,
registration.authority_index,
registration.mixnode,
);

Ok(())
}
}
}
Expand Down Expand Up @@ -532,7 +542,7 @@ impl<T: Config> Pallet<T> {
return false;
};
let call = Call::register { registration, signature };
let xt = T::create_bare(call.into());
let xt = T::create_authorized_transaction(call.into());
match SubmitTransaction::<T, Call<T>>::submit_transaction(xt) {
Ok(()) => true,
Err(()) => {
Expand Down
Loading
Loading