// This file is part of Bizinikiwi. // 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. //! This pezpallet is responsible for determining the current mixnet session and phase, and the //! mixnode set for each session. #![warn(missing_docs)] #![cfg_attr(not(feature = "std"), no_std)] extern crate alloc; pub use pezpallet::*; use alloc::vec::Vec; use core::cmp::Ordering; use frame::{ deps::{ pezsp_io::{self, MultiRemovalResults}, pezsp_runtime, }, prelude::*, }; use pezsp_application_crypto::RuntimeAppPublic; use pezsp_mixnet::types::{ AuthorityId, AuthoritySignature, KxPublic, Mixnode, MixnodesErr, PeerId, SessionIndex, SessionPhase, SessionStatus, KX_PUBLIC_SIZE, }; use serde::{Deserialize, Serialize}; const LOG_TARGET: &str = "runtime::mixnet"; /// Index of an authority in the authority list for a session. pub type AuthorityIndex = u32; //////////////////////////////////////////////////////////////////////////////// // Bounded mixnode type //////////////////////////////////////////////////////////////////////////////// /// Like [`Mixnode`], but encoded size is bounded. #[derive( Clone, Decode, DecodeWithMemTracking, Encode, MaxEncodedLen, PartialEq, TypeInfo, RuntimeDebug, Serialize, Deserialize, )] pub struct BoundedMixnode { /// Key-exchange public key for the mixnode. pub kx_public: KxPublic, /// libp2p peer ID of the mixnode. pub peer_id: PeerId, /// External addresses for the mixnode, in multiaddr format, UTF-8 encoded. pub external_addresses: ExternalAddresses, } impl Into for BoundedMixnode, 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, MaxExternalAddresses: Get> From for BoundedMixnode, 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::>() .try_into() .expect("Excess external addresses discarded with take()"), } } } /// [`BoundedMixnode`] type for the given configuration. pub type BoundedMixnodeFor = BoundedMixnode< BoundedVec< BoundedVec::MaxExternalAddressSize>, ::MaxExternalAddressesPerMixnode, >, >; //////////////////////////////////////////////////////////////////////////////// // Registration type //////////////////////////////////////////////////////////////////////////////// /// A mixnode registration. A registration transaction is formed from one of these plus an /// [`AuthoritySignature`]. #[derive(Clone, Decode, DecodeWithMemTracking, Encode, PartialEq, TypeInfo, RuntimeDebug)] pub struct Registration { /// Block number at the time of creation. When a registration transaction fails to make it on /// to the chain for whatever reason, we send out another one. We want this one to have a /// different hash in case the earlier transaction got banned somewhere; including the block /// number is a simple way of achieving this. pub block_number: BlockNumber, /// The session during which this registration should be processed. Note that on success the /// mixnode is registered for the _following_ session. pub session_index: SessionIndex, /// The index in the next session's authority list of the authority registering the mixnode. pub authority_index: AuthorityIndex, /// Mixnode information to register for the following session. pub mixnode: BoundedMixnode, } /// [`Registration`] type for the given configuration. pub type RegistrationFor = Registration, BoundedMixnodeFor>; //////////////////////////////////////////////////////////////////////////////// // Misc helper funcs //////////////////////////////////////////////////////////////////////////////// fn check_removed_all(res: MultiRemovalResults) { debug_assert!(res.maybe_cursor.is_none()); } fn twox>( 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(pezsp_io::hashing::twox_64(&data)) } //////////////////////////////////////////////////////////////////////////////// // The pezpallet //////////////////////////////////////////////////////////////////////////////// #[frame::pezpallet(dev_mode)] pub mod pezpallet { use super::*; #[pezpallet::pezpallet] pub struct Pezpallet(_); #[pezpallet::config] pub trait Config: pezframe_system::Config + CreateBare> { /// The maximum number of authorities per session. #[pezpallet::constant] type MaxAuthorities: Get; /// The maximum size of one of a mixnode's external addresses. #[pezpallet::constant] type MaxExternalAddressSize: Get; /// The maximum number of external addresses for a mixnode. #[pezpallet::constant] type MaxExternalAddressesPerMixnode: Get; /// Session progress/length estimation. Used to determine when to send registration /// transactions and the longevity of these transactions. type NextSessionRotation: EstimateNextSessionRotation>; /// Length of the first phase of each session (`CoverToCurrent`), in blocks. #[pezpallet::constant] type NumCoverToCurrentBlocks: Get>; /// Length of the second phase of each session (`RequestsToCurrent`), in blocks. #[pezpallet::constant] type NumRequestsToCurrentBlocks: Get>; /// Length of the third phase of each session (`CoverToPrev`), in blocks. #[pezpallet::constant] type NumCoverToPrevBlocks: Get>; /// The number of "slack" blocks at the start of each session, during which /// [`maybe_register`](Pezpallet::maybe_register) will not attempt to post registration /// transactions. #[pezpallet::constant] type NumRegisterStartSlackBlocks: Get>; /// The number of "slack" blocks at the end of each session. /// [`maybe_register`](Pezpallet::maybe_register) will try to register before this slack /// period, but may post registration transactions during the slack period as a last /// resort. #[pezpallet::constant] type NumRegisterEndSlackBlocks: Get>; /// Priority of unsigned transactions used to register mixnodes. #[pezpallet::constant] type RegistrationPriority: Get; /// Minimum number of mixnodes. If there are fewer than this many mixnodes registered for a /// session, the mixnet will not be active during the session. #[pezpallet::constant] type MinMixnodes: Get; } /// Index of the current session. This may be offset relative to the session index tracked by /// eg `pezpallet_session`; mixnet session indices are independent. #[pezpallet::storage] pub(crate) type CurrentSessionIndex = StorageValue<_, SessionIndex, ValueQuery>; /// Block in which the current session started. #[pezpallet::storage] pub(crate) type CurrentSessionStartBlock = StorageValue<_, BlockNumberFor, ValueQuery>; /// Authority list for the next session. #[pezpallet::storage] pub(crate) type NextAuthorityIds = StorageMap<_, Identity, AuthorityIndex, AuthorityId>; /// Mixnode sets by session index. Only the mixnode sets for the previous, current, and next /// sessions are kept; older sets are discarded. /// /// The mixnodes in each set are keyed by authority index so we can easily check if an /// authority has registered a mixnode. The authority indices should only be used during /// registration; the authority indices for the very first session are made up. #[pezpallet::storage] pub(crate) type Mixnodes = StorageDoubleMap<_, Identity, SessionIndex, Identity, AuthorityIndex, BoundedMixnodeFor>; #[pezpallet::genesis_config] #[derive(DefaultNoBound)] pub struct GenesisConfig { /// The mixnode set for the very first session. pub mixnodes: BoundedVec, T::MaxAuthorities>, } #[pezpallet::genesis_build] impl BuildGenesisConfig for GenesisConfig { fn build(&self) { assert!( Mixnodes::::iter_prefix_values(0).next().is_none(), "Initial mixnodes already set" ); for (i, mixnode) in self.mixnodes.iter().enumerate() { // We just make up authority indices here. This doesn't matter as authority indices // are only used during registration to check an authority doesn't register twice. Mixnodes::::insert(0, i as AuthorityIndex, mixnode); } } } #[pezpallet::call] impl Pezpallet { /// Register a mixnode for the following session. #[pezpallet::call_index(0)] #[pezpallet::weight(1)] // TODO pub fn register( origin: OriginFor, registration: RegistrationFor, _signature: AuthoritySignature, ) -> DispatchResult { ensure_none(origin)?; // Checked by ValidateUnsigned debug_assert_eq!(registration.session_index, CurrentSessionIndex::::get()); debug_assert!(registration.authority_index < T::MaxAuthorities::get()); Mixnodes::::insert( // Registering for the _following_ session registration.session_index + 1, registration.authority_index, registration.mixnode, ); Ok(()) } } #[pezpallet::validate_unsigned] impl ValidateUnsigned for Pezpallet { type Call = Call; fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity { let Self::Call::register { registration, signature } = call else { return InvalidTransaction::Call.into(); }; // Check session index matches match registration.session_index.cmp(&CurrentSessionIndex::::get()) { Ordering::Greater => return InvalidTransaction::Future.into(), Ordering::Less => return InvalidTransaction::Stale.into(), Ordering::Equal => (), } // Check authority index is valid if registration.authority_index >= T::MaxAuthorities::get() { return InvalidTransaction::BadProof.into(); } let Some(authority_id) = NextAuthorityIds::::get(registration.authority_index) else { return 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(); } // Check signature. Note that we don't use regular signed transactions for registration // as we don't want validators to have to pay to register. Spam is prevented by only // allowing one registration per session per validator (see above). 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()) // Include both authority index _and_ ID in tag in case of forks with different // authority lists .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 Pezpallet { /// Returns the phase of the current session. fn session_phase() -> SessionPhase { let block_in_phase = pezframe_system::Pezpallet::::block_number() .saturating_sub(CurrentSessionStartBlock::::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 } } /// Returns the index and phase of the current session. pub fn session_status() -> SessionStatus { SessionStatus { current_index: CurrentSessionIndex::::get(), phase: Self::session_phase(), } } /// Returns the mixnode set for the given session (which should be either the previous or the /// current session). fn mixnodes(session_index: SessionIndex) -> Result, MixnodesErr> { let mixnodes: Vec<_> = Mixnodes::::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) } } /// Returns the mixnode set for the previous session. pub fn prev_mixnodes() -> Result, MixnodesErr> { let Some(prev_session_index) = CurrentSessionIndex::::get().checked_sub(1) else { return Err(MixnodesErr::InsufficientRegistrations { num: 0, min: T::MinMixnodes::get(), }); }; Self::mixnodes(prev_session_index) } /// Returns the mixnode set for the current session. pub fn current_mixnodes() -> Result, MixnodesErr> { Self::mixnodes(CurrentSessionIndex::::get()) } /// Is now a good time to register, considering only session progress? fn should_register_by_session_progress( block_number: BlockNumberFor, mixnode: &Mixnode, ) -> bool { // At the start of each session there are some "slack" blocks during which we avoid // registering let block_in_session = block_number.saturating_sub(CurrentSessionStartBlock::::get()); if block_in_session < T::NumRegisterStartSlackBlocks::get() { return false; } let (Some(end_block), _weight) = T::NextSessionRotation::estimate_next_session_rotation(block_number) else { // Things aren't going to work terribly well in this case as all the authorities will // just pile in after the slack period... return true; }; let remaining_blocks = end_block .saturating_sub(block_number) .saturating_sub(T::NumRegisterEndSlackBlocks::get()); if remaining_blocks.is_zero() { // Into the slack time at the end of the session. Not necessarily too late; // registrations are accepted right up until the session ends. return true; } // Want uniform distribution over the remaining blocks, so pick this block with probability // 1/remaining_blocks. maybe_register may be called multiple times per block; ensure the // same decision gets made each time by using a hash of the block number and the mixnode's // public key as the "random" source. This is slightly biased as remaining_blocks most // likely won't divide into 2^64, but it doesn't really matter... 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)> { // In the case where multiple local IDs are in the next authority set, we just return the // first one. There's (currently at least) no point in registering multiple times. let mut local_ids = AuthorityId::all(); local_ids.sort(); NextAuthorityIds::::iter().find(|(_index, id)| local_ids.binary_search(id).is_ok()) } /// `session_index` should be the index of the current session. `authority_index` is the /// authority index in the _next_ session. fn already_registered(session_index: SessionIndex, authority_index: AuthorityIndex) -> bool { Mixnodes::::contains_key(session_index + 1, authority_index) } /// Try to register a mixnode for the next session. /// /// If a registration extrinsic is submitted, `true` is returned. The caller should avoid /// calling `maybe_register` again for a few blocks, to give the submitted extrinsic a chance /// to get included. /// /// With the above exception, `maybe_register` is designed to be called every block. Most of /// the time it will not do anything, for example: /// /// - If it is not an appropriate time to submit a registration extrinsic. /// - If the local node has already registered a mixnode for the next session. /// - If the local node is not permitted to register a mixnode for the next session. /// /// `session_index` should match `session_status().current_index`; if it does not, `false` is /// returned immediately. pub fn maybe_register(session_index: SessionIndex, mixnode: Mixnode) -> bool { let current_session_index = CurrentSessionIndex::::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 = pezframe_system::Pezpallet::::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_bare(call.into()); match SubmitTransaction::>::submit_transaction(xt) { Ok(()) => true, Err(()) => { log::debug!( target: LOG_TARGET, "Failed to submit registration transaction", ); false }, } } } impl pezsp_runtime::BoundToRuntimeAppPublic for Pezpallet { type Public = AuthorityId; } impl OneSessionHandler for Pezpallet { type Key = AuthorityId; fn on_genesis_session<'a, I: 'a>(validators: I) where I: Iterator, { assert!( NextAuthorityIds::::iter().next().is_none(), "Initial authority IDs already set" ); for (i, (_, authority_id)) in validators.enumerate() { NextAuthorityIds::::insert(i as AuthorityIndex, authority_id); } } fn on_new_session<'a, I: 'a>(changed: bool, _validators: I, queued_validators: I) where I: Iterator, { let session_index = CurrentSessionIndex::::mutate(|index| { *index += 1; *index }); CurrentSessionStartBlock::::put(pezframe_system::Pezpallet::::block_number()); // Discard the previous previous mixnode set, which we don't need any more if let Some(prev_prev_session_index) = session_index.checked_sub(2) { check_removed_all(Mixnodes::::clear_prefix( prev_prev_session_index, T::MaxAuthorities::get(), None, )); } if changed { // Save authority set for the next session. Note that we don't care about the authority // set for the current session; we just care about the key-exchange public keys that // were registered and are stored in Mixnodes. check_removed_all(NextAuthorityIds::::clear(T::MaxAuthorities::get(), None)); for (i, (_, authority_id)) in queued_validators.enumerate() { NextAuthorityIds::::insert(i as AuthorityIndex, authority_id); } } } fn on_disabled(_i: u32) { // For now, to keep things simple, just ignore // TODO } }