#![warn(missing_docs)]
#![cfg_attr(not(feature = "std"), no_std)]
extern crate alloc;
use alloc::vec::Vec;
use codec::{Decode, Encode, MaxEncodedLen};
use core::cmp::Ordering;
use frame_support::{
traits::{EstimateNextSessionRotation, Get, OneSessionHandler},
BoundedVec,
};
use frame_system::{
offchain::{CreateInherent, SubmitTransaction},
pallet_prelude::BlockNumberFor,
};
pub use pallet::*;
use scale_info::TypeInfo;
use serde::{Deserialize, Serialize};
use sp_application_crypto::RuntimeAppPublic;
use sp_arithmetic::traits::{CheckedSub, Saturating, UniqueSaturatedInto, Zero};
use sp_io::MultiRemovalResults;
use sp_mixnet::types::{
AuthorityId, AuthoritySignature, KxPublic, Mixnode, MixnodesErr, PeerId, SessionIndex,
SessionPhase, SessionStatus, KX_PUBLIC_SIZE,
};
use sp_runtime::RuntimeDebug;
const LOG_TARGET: &str = "runtime::mixnet";
pub type AuthorityIndex = u32;
#[derive(
Clone, Decode, Encode, MaxEncodedLen, PartialEq, TypeInfo, RuntimeDebug, Serialize, Deserialize,
)]
pub struct BoundedMixnode<ExternalAddresses> {
pub kx_public: KxPublic,
pub peer_id: PeerId,
pub external_addresses: ExternalAddresses,
}
impl<MaxExternalAddressSize, MaxExternalAddresses> Into<Mixnode>
for BoundedMixnode<BoundedVec<BoundedVec<u8, MaxExternalAddressSize>, MaxExternalAddresses>>
{
fn into(self) -> Mixnode {
Mixnode {
kx_public: self.kx_public,
peer_id: self.peer_id,
external_addresses: self
.external_addresses
.into_iter()
.map(BoundedVec::into_inner)
.collect(),
}
}
}
impl<MaxExternalAddressSize: Get<u32>, MaxExternalAddresses: Get<u32>> From<Mixnode>
for BoundedMixnode<BoundedVec<BoundedVec<u8, MaxExternalAddressSize>, MaxExternalAddresses>>
{
fn from(mixnode: Mixnode) -> Self {
Self {
kx_public: mixnode.kx_public,
peer_id: mixnode.peer_id,
external_addresses: mixnode
.external_addresses
.into_iter()
.flat_map(|addr| match addr.try_into() {
Ok(addr) => Some(addr),
Err(addr) => {
log::debug!(
target: LOG_TARGET,
"Mixnode external address {addr:x?} too long; discarding",
);
None
},
})
.take(MaxExternalAddresses::get() as usize)
.collect::<Vec<_>>()
.try_into()
.expect("Excess external addresses discarded with take()"),
}
}
}
pub type BoundedMixnodeFor<T> = BoundedMixnode<
BoundedVec<
BoundedVec<u8, <T as Config>::MaxExternalAddressSize>,
<T as Config>::MaxExternalAddressesPerMixnode,
>,
>;
#[derive(Clone, Decode, Encode, PartialEq, TypeInfo, RuntimeDebug)]
pub struct Registration<BlockNumber, BoundedMixnode> {
pub block_number: BlockNumber,
pub session_index: SessionIndex,
pub authority_index: AuthorityIndex,
pub mixnode: BoundedMixnode,
}
pub type RegistrationFor<T> = Registration<BlockNumberFor<T>, BoundedMixnodeFor<T>>;
fn check_removed_all(res: MultiRemovalResults) {
debug_assert!(res.maybe_cursor.is_none());
}
fn twox<BlockNumber: UniqueSaturatedInto<u64>>(
block_number: BlockNumber,
kx_public: &KxPublic,
) -> u64 {
let block_number: u64 = block_number.unique_saturated_into();
let mut data = [0; 8 + KX_PUBLIC_SIZE];
data[..8].copy_from_slice(&block_number.to_le_bytes());
data[8..].copy_from_slice(kx_public);
u64::from_le_bytes(sp_io::hashing::twox_64(&data))
}
#[frame_support::pallet(dev_mode)]
pub mod pallet {
use super::*;
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;
#[pallet::pallet]
pub struct Pallet<T>(_);
#[pallet::config]
pub trait Config: frame_system::Config + CreateInherent<Call<Self>> {
#[pallet::constant]
type MaxAuthorities: Get<AuthorityIndex>;
#[pallet::constant]
type MaxExternalAddressSize: Get<u32>;
#[pallet::constant]
type MaxExternalAddressesPerMixnode: Get<u32>;
type NextSessionRotation: EstimateNextSessionRotation<BlockNumberFor<Self>>;
#[pallet::constant]
type NumCoverToCurrentBlocks: Get<BlockNumberFor<Self>>;
#[pallet::constant]
type NumRequestsToCurrentBlocks: Get<BlockNumberFor<Self>>;
#[pallet::constant]
type NumCoverToPrevBlocks: Get<BlockNumberFor<Self>>;
#[pallet::constant]
type NumRegisterStartSlackBlocks: Get<BlockNumberFor<Self>>;
#[pallet::constant]
type NumRegisterEndSlackBlocks: Get<BlockNumberFor<Self>>;
#[pallet::constant]
type RegistrationPriority: Get<TransactionPriority>;
#[pallet::constant]
type MinMixnodes: Get<u32>;
}
#[pallet::storage]
pub(crate) type CurrentSessionIndex<T> = StorageValue<_, SessionIndex, ValueQuery>;
#[pallet::storage]
pub(crate) type CurrentSessionStartBlock<T> = StorageValue<_, BlockNumberFor<T>, ValueQuery>;
#[pallet::storage]
pub(crate) type NextAuthorityIds<T> = StorageMap<_, Identity, AuthorityIndex, AuthorityId>;
#[pallet::storage]
pub(crate) type Mixnodes<T> =
StorageDoubleMap<_, Identity, SessionIndex, Identity, AuthorityIndex, BoundedMixnodeFor<T>>;
#[pallet::genesis_config]
#[derive(frame_support::DefaultNoBound)]
pub struct GenesisConfig<T: Config> {
pub mixnodes: BoundedVec<BoundedMixnodeFor<T>, T::MaxAuthorities>,
}
#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {
assert!(
Mixnodes::<T>::iter_prefix_values(0).next().is_none(),
"Initial mixnodes already set"
);
for (i, mixnode) in self.mixnodes.iter().enumerate() {
Mixnodes::<T>::insert(0, i as AuthorityIndex, mixnode);
}
}
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(1)] pub fn register(
origin: OriginFor<T>,
registration: RegistrationFor<T>,
_signature: AuthoritySignature,
) -> DispatchResult {
ensure_none(origin)?;
debug_assert_eq!(registration.session_index, CurrentSessionIndex::<T>::get());
debug_assert!(registration.authority_index < T::MaxAuthorities::get());
Mixnodes::<T>::insert(
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()
};
match registration.session_index.cmp(&CurrentSessionIndex::<T>::get()) {
Ordering::Greater => return InvalidTransaction::Future.into(),
Ordering::Less => return InvalidTransaction::Stale.into(),
Ordering::Equal => (),
}
if registration.authority_index >= T::MaxAuthorities::get() {
return InvalidTransaction::BadProof.into()
}
let Some(authority_id) = NextAuthorityIds::<T>::get(registration.authority_index)
else {
return InvalidTransaction::BadProof.into()
};
if Self::already_registered(registration.session_index, registration.authority_index) {
return InvalidTransaction::Stale.into()
}
let signature_ok = registration.using_encoded(|encoded_registration| {
authority_id.verify(&encoded_registration, signature)
});
if !signature_ok {
return InvalidTransaction::BadProof.into()
}
ValidTransaction::with_tag_prefix("MixnetRegistration")
.priority(T::RegistrationPriority::get())
.and_provides((
registration.session_index,
registration.authority_index,
authority_id,
))
.longevity(
(T::NextSessionRotation::average_session_length() / 2_u32.into())
.try_into()
.unwrap_or(64_u64),
)
.build()
}
}
}
impl<T: Config> Pallet<T> {
fn session_phase() -> SessionPhase {
let block_in_phase = frame_system::Pallet::<T>::block_number()
.saturating_sub(CurrentSessionStartBlock::<T>::get());
let Some(block_in_phase) = block_in_phase.checked_sub(&T::NumCoverToCurrentBlocks::get())
else {
return SessionPhase::CoverToCurrent
};
let Some(block_in_phase) =
block_in_phase.checked_sub(&T::NumRequestsToCurrentBlocks::get())
else {
return SessionPhase::RequestsToCurrent
};
if block_in_phase < T::NumCoverToPrevBlocks::get() {
SessionPhase::CoverToPrev
} else {
SessionPhase::DisconnectFromPrev
}
}
pub fn session_status() -> SessionStatus {
SessionStatus {
current_index: CurrentSessionIndex::<T>::get(),
phase: Self::session_phase(),
}
}
fn mixnodes(session_index: SessionIndex) -> Result<Vec<Mixnode>, MixnodesErr> {
let mixnodes: Vec<_> =
Mixnodes::<T>::iter_prefix_values(session_index).map(Into::into).collect();
if mixnodes.len() < T::MinMixnodes::get() as usize {
Err(MixnodesErr::InsufficientRegistrations {
num: mixnodes.len() as u32,
min: T::MinMixnodes::get(),
})
} else {
Ok(mixnodes)
}
}
pub fn prev_mixnodes() -> Result<Vec<Mixnode>, MixnodesErr> {
let Some(prev_session_index) = CurrentSessionIndex::<T>::get().checked_sub(1) else {
return Err(MixnodesErr::InsufficientRegistrations {
num: 0,
min: T::MinMixnodes::get(),
})
};
Self::mixnodes(prev_session_index)
}
pub fn current_mixnodes() -> Result<Vec<Mixnode>, MixnodesErr> {
Self::mixnodes(CurrentSessionIndex::<T>::get())
}
fn should_register_by_session_progress(
block_number: BlockNumberFor<T>,
mixnode: &Mixnode,
) -> bool {
let block_in_session = block_number.saturating_sub(CurrentSessionStartBlock::<T>::get());
if block_in_session < T::NumRegisterStartSlackBlocks::get() {
return false
}
let (Some(end_block), _weight) =
T::NextSessionRotation::estimate_next_session_rotation(block_number)
else {
return true
};
let remaining_blocks = end_block
.saturating_sub(block_number)
.saturating_sub(T::NumRegisterEndSlackBlocks::get());
if remaining_blocks.is_zero() {
return true
}
let random = twox(block_number, &mixnode.kx_public);
(random % remaining_blocks.try_into().unwrap_or(u64::MAX)) == 0
}
fn next_local_authority() -> Option<(AuthorityIndex, AuthorityId)> {
let mut local_ids = AuthorityId::all();
local_ids.sort();
NextAuthorityIds::<T>::iter().find(|(_index, id)| local_ids.binary_search(id).is_ok())
}
fn already_registered(session_index: SessionIndex, authority_index: AuthorityIndex) -> bool {
Mixnodes::<T>::contains_key(session_index + 1, authority_index)
}
pub fn maybe_register(session_index: SessionIndex, mixnode: Mixnode) -> bool {
let current_session_index = CurrentSessionIndex::<T>::get();
if session_index != current_session_index {
log::trace!(
target: LOG_TARGET,
"Session {session_index} registration attempted, \
but current session is {current_session_index}",
);
return false
}
let block_number = frame_system::Pallet::<T>::block_number();
if !Self::should_register_by_session_progress(block_number, &mixnode) {
log::trace!(
target: LOG_TARGET,
"Waiting for the session to progress further before registering",
);
return false
}
let Some((authority_index, authority_id)) = Self::next_local_authority() else {
log::trace!(
target: LOG_TARGET,
"Not an authority in the next session; cannot register a mixnode",
);
return false
};
if Self::already_registered(session_index, authority_index) {
log::trace!(
target: LOG_TARGET,
"Already registered a mixnode for the next session",
);
return false
}
let registration =
Registration { block_number, session_index, authority_index, mixnode: mixnode.into() };
let Some(signature) = authority_id.sign(®istration.encode()) else {
log::debug!(target: LOG_TARGET, "Failed to sign registration");
return false
};
let call = Call::register { registration, signature };
let xt = T::create_inherent(call.into());
match SubmitTransaction::<T, Call<T>>::submit_transaction(xt) {
Ok(()) => true,
Err(()) => {
log::debug!(
target: LOG_TARGET,
"Failed to submit registration transaction",
);
false
},
}
}
}
impl<T: Config> sp_runtime::BoundToRuntimeAppPublic for Pallet<T> {
type Public = AuthorityId;
}
impl<T: Config> OneSessionHandler<T::AccountId> for Pallet<T> {
type Key = AuthorityId;
fn on_genesis_session<'a, I: 'a>(validators: I)
where
I: Iterator<Item = (&'a T::AccountId, Self::Key)>,
{
assert!(
NextAuthorityIds::<T>::iter().next().is_none(),
"Initial authority IDs already set"
);
for (i, (_, authority_id)) in validators.enumerate() {
NextAuthorityIds::<T>::insert(i as AuthorityIndex, authority_id);
}
}
fn on_new_session<'a, I: 'a>(changed: bool, _validators: I, queued_validators: I)
where
I: Iterator<Item = (&'a T::AccountId, Self::Key)>,
{
let session_index = CurrentSessionIndex::<T>::mutate(|index| {
*index += 1;
*index
});
CurrentSessionStartBlock::<T>::put(frame_system::Pallet::<T>::block_number());
if let Some(prev_prev_session_index) = session_index.checked_sub(2) {
check_removed_all(Mixnodes::<T>::clear_prefix(
prev_prev_session_index,
T::MaxAuthorities::get(),
None,
));
}
if changed {
check_removed_all(NextAuthorityIds::<T>::clear(T::MaxAuthorities::get(), None));
for (i, (_, authority_id)) in queued_validators.enumerate() {
NextAuthorityIds::<T>::insert(i as AuthorityIndex, authority_id);
}
}
}
fn on_disabled(_i: u32) {
}
}