#![cfg_attr(not(feature = "std"), no_std)] mod benchmarking; #[cfg(test)] mod tests; #[cfg(test)] mod mock; pub mod types; pub mod weights; use ark_serialize::CanonicalDeserialize; use codec::Encode; use frame_support::IterableStorageDoubleMap; use frame_support::weights::WeightMeter; use frame_support::{ BoundedVec, traits::{Currency, Get}, }; use frame_system::pallet_prelude::BlockNumberFor; pub use pallet::*; use scale_info::prelude::collections::BTreeSet; use sp_runtime::SaturatedConversion; use sp_runtime::{Saturating, Weight, traits::Zero}; use sp_std::{boxed::Box, vec::Vec}; use subtensor_runtime_common::{NetUid, clear_prefix_with_meter}; use tle::{ curves::drand::TinyBLS381, stream_ciphers::AESGCMStreamCipherProvider, tlock::{TLECiphertext, tld}, }; pub use types::*; use w3f_bls::EngineBLS; pub use weights::WeightInfo; type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; #[deny(missing_docs)] #[frame_support::pallet] #[allow(clippy::expect_used)] pub mod pallet { use super::*; use frame_support::{pallet_prelude::*, traits::ReservableCurrency}; use frame_system::pallet_prelude::{BlockNumberFor, *}; #[pallet::pallet] #[pallet::without_storage_info] pub struct Pallet(_); // Configure the pallet by specifying the parameters and types on which it depends. #[pallet::config] pub trait Config: frame_system::Config + pallet_drand::Config { ///Currency type that will be used to reserve deposits for commitments type Currency: ReservableCurrency + Send + Sync; /// Weight information for extrinsics in this pallet. type WeightInfo: WeightInfo; /// Interface to access-limit metadata commitments type CanCommit: CanCommit; /// Interface to trigger other pallets when metadata is committed type OnMetadataCommitment: OnMetadataCommitment; /// The maximum number of additional fields that can be added to a commitment #[pallet::constant] type MaxFields: Get + TypeInfo + 'static; /// The amount held on deposit for a registered identity #[pallet::constant] type InitialDeposit: Get>; /// The amount held on deposit per additional field for a registered identity. #[pallet::constant] type FieldDeposit: Get>; /// Used to retrieve the given subnet's tempo type TempoInterface: GetTempoInterface; } /// Used to retrieve the given subnet's tempo pub trait GetTempoInterface { /// Used to retreive the epoch index for the given subnet. fn get_epoch_index(netuid: NetUid, cur_block: u64) -> u64; } #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { /// A commitment was set Commitment { /// The netuid of the commitment netuid: NetUid, /// The account who: T::AccountId, }, /// A timelock-encrypted commitment was set TimelockCommitment { /// The netuid of the commitment netuid: NetUid, /// The account who: T::AccountId, /// The drand round to reveal reveal_round: u64, }, /// A timelock-encrypted commitment was auto-revealed CommitmentRevealed { /// The netuid of the commitment netuid: NetUid, /// The account who: T::AccountId, }, } #[pallet::error] pub enum Error { /// Account passed too many additional fields to their commitment TooManyFieldsInCommitmentInfo, /// Account is not allowed to make commitments to the chain AccountNotAllowedCommit, /// Space Limit Exceeded for the current interval SpaceLimitExceeded, /// Indicates that unreserve returned a leftover, which is unexpected. UnexpectedUnreserveLeftover, } /// Tracks all CommitmentOf that have at least one timelocked field. #[pallet::storage] #[pallet::getter(fn timelocked_index)] pub type TimelockedIndex = StorageValue<_, BTreeSet<(NetUid, T::AccountId)>, ValueQuery>; /// Identity data by account #[pallet::storage] #[pallet::getter(fn commitment_of)] pub(super) type CommitmentOf = StorageDoubleMap< _, Identity, NetUid, Twox64Concat, T::AccountId, Registration, T::MaxFields, BlockNumberFor>, OptionQuery, >; #[pallet::storage] #[pallet::getter(fn last_commitment)] pub(super) type LastCommitment = StorageDoubleMap< _, Identity, NetUid, Twox64Concat, T::AccountId, BlockNumberFor, OptionQuery, >; #[pallet::storage] #[pallet::getter(fn last_bonds_reset)] pub(super) type LastBondsReset = StorageDoubleMap< _, Identity, NetUid, Twox64Concat, T::AccountId, BlockNumberFor, OptionQuery, >; #[pallet::storage] #[pallet::getter(fn revealed_commitments)] pub(super) type RevealedCommitments = StorageDoubleMap< _, Identity, NetUid, Twox64Concat, T::AccountId, Vec<(Vec, u64)>, // Reveals<(Data, RevealBlock)> OptionQuery, >; /// Maps (netuid, who) -> usage (how many “bytes” they've committed) /// in the RateLimit window #[pallet::storage] #[pallet::getter(fn used_space_of)] pub type UsedSpaceOf = StorageDoubleMap< _, Identity, NetUid, Twox64Concat, T::AccountId, UsageTracker, OptionQuery, >; #[pallet::type_value] /// The default Maximum Space pub fn DefaultMaxSpace() -> u32 { 3100 } #[pallet::storage] #[pallet::getter(fn max_space_per_user_per_rate_limit)] pub type MaxSpace = StorageValue<_, u32, ValueQuery, DefaultMaxSpace>; #[pallet::call] impl Pallet { #![deny(clippy::expect_used)] /// Set the commitment for a given netuid #[pallet::call_index(0)] #[pallet::weight(( ::WeightInfo::set_commitment(), DispatchClass::Normal, Pays::No ))] pub fn set_commitment( origin: OriginFor, netuid: NetUid, info: Box>, ) -> DispatchResult { let who = ensure_signed(origin.clone())?; ensure!( T::CanCommit::can_commit(netuid, &who), Error::::AccountNotAllowedCommit ); let extra_fields = info.fields.len() as u32; ensure!( extra_fields <= T::MaxFields::get(), Error::::TooManyFieldsInCommitmentInfo ); let cur_block = >::block_number(); let min_used_space: u64 = 100; let required_space: u64 = info .fields .iter() .map(|field| field.len_for_rate_limit()) .sum::() .max(min_used_space); let mut usage = UsedSpaceOf::::get(netuid, &who).unwrap_or_default(); let cur_block_u64 = cur_block.saturated_into::(); let current_epoch = T::TempoInterface::get_epoch_index(netuid, cur_block_u64); if usage.last_epoch != current_epoch { usage.last_epoch = current_epoch; usage.used_space = 0; } // check if ResetBondsFlag is set in the fields for field in info.fields.iter() { if let Data::ResetBondsFlag = field { // track when bonds reset was last triggered >::insert(netuid, &who, cur_block); T::OnMetadataCommitment::on_metadata_commitment(netuid, &who); break; } } let max_allowed = MaxSpace::::get() as u64; ensure!( usage.used_space.saturating_add(required_space) <= max_allowed, Error::::SpaceLimitExceeded ); usage.used_space = usage.used_space.saturating_add(required_space); UsedSpaceOf::::insert(netuid, &who, usage); let mut id = match >::get(netuid, &who) { Some(mut id) => { id.info = *info.clone(); id.block = cur_block; id } None => Registration { info: *info.clone(), block: cur_block, deposit: Zero::zero(), }, }; let old_deposit = id.deposit; let fd = >::from(extra_fields).saturating_mul(T::FieldDeposit::get()); id.deposit = T::InitialDeposit::get().saturating_add(fd); if id.deposit > old_deposit { T::Currency::reserve(&who, id.deposit.saturating_sub(old_deposit))?; } if old_deposit > id.deposit { let err_amount = T::Currency::unreserve(&who, old_deposit.saturating_sub(id.deposit)); if !err_amount.is_zero() { return Err(Error::::UnexpectedUnreserveLeftover.into()); } } >::insert(netuid, &who, id); >::insert(netuid, &who, cur_block); if let Some(Data::TimelockEncrypted { reveal_round, .. }) = info .fields .iter() .find(|data| matches!(data, Data::TimelockEncrypted { .. })) { Self::deposit_event(Event::TimelockCommitment { netuid, who: who.clone(), reveal_round: *reveal_round, }); TimelockedIndex::::mutate(|index| { index.insert((netuid, who.clone())); }); } else { Self::deposit_event(Event::Commitment { netuid, who: who.clone(), }); TimelockedIndex::::mutate(|index| { index.remove(&(netuid, who.clone())); }); } Ok(()) } /// Sudo-set MaxSpace #[pallet::call_index(2)] #[pallet::weight(::WeightInfo::set_max_space())] pub fn set_max_space(origin: OriginFor, new_limit: u32) -> DispatchResult { ensure_root(origin)?; MaxSpace::::set(new_limit); Ok(()) } } #[pallet::hooks] impl Hooks> for Pallet { fn on_initialize(n: BlockNumberFor) -> Weight { match Self::reveal_timelocked_commitments() { Ok(w) => w, Err(e) => { log::debug!("Failed to unveil matured commitments on block {n:?}: {e:?}"); Weight::from_parts(0, 0) } } } } } // Interfaces to interact with other pallets pub trait CanCommit { fn can_commit(netuid: NetUid, who: &AccountId) -> bool; } impl CanCommit for () { fn can_commit(_: NetUid, _: &A) -> bool { false } } pub trait OnMetadataCommitment { fn on_metadata_commitment(netuid: NetUid, account: &AccountId); } impl OnMetadataCommitment for () { fn on_metadata_commitment(_: NetUid, _: &A) {} } /************************************************************ CallType definition ************************************************************/ #[derive(Debug, PartialEq, Default)] pub enum CallType { SetCommitment, #[default] Other, } use frame_support::{dispatch::DispatchResult, pallet_prelude::TypeInfo}; impl Pallet { pub fn reveal_timelocked_commitments() -> Result { let mut total_weight = Weight::from_parts(0, 0); let index = TimelockedIndex::::get(); total_weight = total_weight.saturating_add(T::DbWeight::get().reads(1)); for (netuid, who) in index.clone() { let maybe_registration = >::get(netuid, &who); total_weight = total_weight.saturating_add(T::DbWeight::get().reads(1)); let Some(mut registration) = maybe_registration else { TimelockedIndex::::mutate(|idx| { idx.remove(&(netuid, who.clone())); }); total_weight = total_weight.saturating_add(T::DbWeight::get().reads_writes(1, 1)); continue; }; let original_fields = registration.info.fields.clone(); let mut remain_fields = Vec::new(); let mut revealed_fields = Vec::new(); let mut saw_timelock = false; let mut processed_timelock = false; for data in original_fields { match data { Data::TimelockEncrypted { encrypted, reveal_round, } => { saw_timelock = true; total_weight = total_weight.saturating_add(T::DbWeight::get().reads(1)); let pulse = match pallet_drand::Pulses::::get(reveal_round) { Some(p) => p, None => { remain_fields.push(Data::TimelockEncrypted { encrypted, reveal_round, }); continue; } }; processed_timelock = true; let signature_bytes = pulse .signature .strip_prefix(b"0x") .unwrap_or(&pulse.signature); let sig_reader = &mut &signature_bytes[..]; let sig = ::SignatureGroup::deserialize_compressed( sig_reader, ) .map_err(|e| { log::warn!( "Failed to deserialize drand signature for {who:?}: {e:?}" ) }) .ok(); let Some(sig) = sig else { log::warn!("No sig after deserialization"); continue; }; let reader = &mut &encrypted[..]; let commit = TLECiphertext::::deserialize_compressed(reader) .map_err(|e| { log::warn!("Failed to deserialize TLECiphertext for {who:?}: {e:?}") }) .ok(); let Some(commit) = commit else { log::warn!("No commit after deserialization"); continue; }; let decrypted_bytes: Vec = tld::(commit, sig) .map_err(|e| { log::warn!("Failed to decrypt timelock for {who:?}: {e:?}") }) .ok() .unwrap_or_default(); if decrypted_bytes.is_empty() { log::warn!("Bytes were decrypted for {who:?} but they are empty"); continue; } revealed_fields.push(decrypted_bytes); } other => remain_fields.push(other), } } if !saw_timelock { TimelockedIndex::::mutate(|idx| { idx.remove(&(netuid, who.clone())); }); total_weight = total_weight.saturating_add(T::DbWeight::get().reads_writes(1, 1)); continue; } // Do not rewrite CommitmentOf every block for entries whose reveal round is // not yet available in the drand pulse storage. The hook has only performed // the index, commitment, and pulse reads accounted above. if !processed_timelock { continue; } let Ok(remaining_fields) = BoundedVec::try_from(remain_fields) else { log::error!( "Failed to build BoundedVec for remain_fields; this should be impossible \ because remain_fields is a subset of the original commitment fields" ); continue; }; if !revealed_fields.is_empty() { let mut existing_reveals = RevealedCommitments::::get(netuid, &who).unwrap_or_default(); total_weight = total_weight.saturating_add(T::DbWeight::get().reads(1)); let current_block = >::block_number(); let block_u64 = current_block.saturated_into::(); // Push newly revealed items onto the tail of existing_reveals and emit the event for revealed_bytes in revealed_fields { existing_reveals.push((revealed_bytes, block_u64)); Self::deposit_event(Event::CommitmentRevealed { netuid, who: who.clone(), }); } const MAX_REVEALS: usize = 10; if existing_reveals.len() > MAX_REVEALS { let remove_count = existing_reveals.len().saturating_sub(MAX_REVEALS); existing_reveals.drain(0..remove_count); } RevealedCommitments::::insert(netuid, &who, existing_reveals); total_weight = total_weight.saturating_add(T::DbWeight::get().writes(1)); } registration.info.fields = remaining_fields; match registration.info.fields.is_empty() { true => { >::remove(netuid, &who); total_weight = total_weight.saturating_add(T::DbWeight::get().writes(1)); TimelockedIndex::::mutate(|idx| { idx.remove(&(netuid, who.clone())); }); total_weight = total_weight.saturating_add(T::DbWeight::get().reads_writes(1, 1)); } false => { >::insert(netuid, &who, ®istration); total_weight = total_weight.saturating_add(T::DbWeight::get().writes(1)); let has_timelock = registration .info .fields .iter() .any(|f| matches!(f, Data::TimelockEncrypted { .. })); if !has_timelock { TimelockedIndex::::mutate(|idx| { idx.remove(&(netuid, who.clone())); }); total_weight = total_weight.saturating_add(T::DbWeight::get().reads_writes(1, 1)); } } } } Ok(total_weight) } pub fn get_commitments(netuid: NetUid) -> Vec<(T::AccountId, Vec)> { let commitments: Vec<(T::AccountId, Vec)> = as IterableStorageDoubleMap< NetUid, T::AccountId, Registration, T::MaxFields, BlockNumberFor>, >>::iter_prefix(netuid) .map(|(account, registration)| { let bytes = registration.encode(); (account, bytes) }) .collect(); commitments } pub fn purge_netuid(netuid: NetUid, weight_meter: &mut WeightMeter) -> bool { let write_weight = T::DbWeight::get().writes(1); let result = clear_prefix_with_meter(weight_meter, write_weight, |limit| { CommitmentOf::::clear_prefix(netuid, limit, None) }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { LastCommitment::::clear_prefix(netuid, limit, None) }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { LastBondsReset::::clear_prefix(netuid, limit, None) }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { RevealedCommitments::::clear_prefix(netuid, limit, None) }) && clear_prefix_with_meter(weight_meter, write_weight, |limit| { UsedSpaceOf::::clear_prefix(netuid, limit, None) }); if !result { return false; } if weight_meter.can_consume(write_weight) { TimelockedIndex::::mutate(|index| { index.retain(|(n, _)| *n != netuid); }); weight_meter.consume(write_weight); true } else { false } } } pub trait GetCommitments { fn get_commitments(netuid: NetUid) -> Vec<(AccountId, Vec)>; } impl GetCommitments for () { fn get_commitments(_netuid: NetUid) -> Vec<(AccountId, Vec)> { Vec::new() } }