|
| 1 | +#![cfg_attr(not(feature = "std"), no_std)] |
| 2 | + |
| 3 | +//! Read-only interface for querying rate limits and last-seen usage. |
| 4 | +
|
| 5 | +use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; |
| 6 | +use frame_support::traits::GetCallMetadata; |
| 7 | +use scale_info::TypeInfo; |
| 8 | +use serde::{Deserialize, Serialize}; |
| 9 | +use sp_std::vec::Vec; |
| 10 | + |
| 11 | +/// Read-only queries for rate-limiting configuration and usage tracking. |
| 12 | +pub trait RateLimitingInfo { |
| 13 | + /// Group id type used by rate-limiting targets. |
| 14 | + type GroupId; |
| 15 | + /// Call type used for name/index resolution. |
| 16 | + type CallMetadata: GetCallMetadata; |
| 17 | + /// Numeric type used for returned values (commonly a block number / block span type). |
| 18 | + type Limit; |
| 19 | + /// Optional configuration scope (for example per-network `netuid`). |
| 20 | + type Scope; |
| 21 | + /// Optional usage key used to refine "last seen" tracking. |
| 22 | + type UsageKey; |
| 23 | + |
| 24 | + /// Returns the configured limit for `target` and optional `scope`. |
| 25 | + fn rate_limit<TargetArg>(target: TargetArg, scope: Option<Self::Scope>) -> Option<Self::Limit> |
| 26 | + where |
| 27 | + TargetArg: TryIntoRateLimitTarget<Self::GroupId>; |
| 28 | + |
| 29 | + /// Returns when `target` was last observed for the optional `usage_key`. |
| 30 | + fn last_seen<TargetArg>( |
| 31 | + target: TargetArg, |
| 32 | + usage_key: Option<Self::UsageKey>, |
| 33 | + ) -> Option<Self::Limit> |
| 34 | + where |
| 35 | + TargetArg: TryIntoRateLimitTarget<Self::GroupId>; |
| 36 | +} |
| 37 | + |
| 38 | +/// Target identifier for rate limit and usage configuration. |
| 39 | +#[derive( |
| 40 | + Serialize, |
| 41 | + Deserialize, |
| 42 | + Clone, |
| 43 | + Copy, |
| 44 | + PartialEq, |
| 45 | + Eq, |
| 46 | + PartialOrd, |
| 47 | + Ord, |
| 48 | + Encode, |
| 49 | + Decode, |
| 50 | + DecodeWithMemTracking, |
| 51 | + TypeInfo, |
| 52 | + MaxEncodedLen, |
| 53 | + Debug, |
| 54 | +)] |
| 55 | +pub enum RateLimitTarget<GroupId> { |
| 56 | + /// Per-transaction configuration keyed by pallet/extrinsic indices. |
| 57 | + Transaction(TransactionIdentifier), |
| 58 | + /// Shared configuration for a named group. |
| 59 | + Group(GroupId), |
| 60 | +} |
| 61 | + |
| 62 | +impl<GroupId> RateLimitTarget<GroupId> { |
| 63 | + /// Returns the transaction identifier when the target represents a single extrinsic. |
| 64 | + pub fn as_transaction(&self) -> Option<&TransactionIdentifier> { |
| 65 | + match self { |
| 66 | + RateLimitTarget::Transaction(identifier) => Some(identifier), |
| 67 | + RateLimitTarget::Group(_) => None, |
| 68 | + } |
| 69 | + } |
| 70 | + |
| 71 | + /// Returns the group identifier when the target represents a group configuration. |
| 72 | + pub fn as_group(&self) -> Option<&GroupId> { |
| 73 | + match self { |
| 74 | + RateLimitTarget::Transaction(_) => None, |
| 75 | + RateLimitTarget::Group(id) => Some(id), |
| 76 | + } |
| 77 | + } |
| 78 | +} |
| 79 | + |
| 80 | +impl<GroupId> From<TransactionIdentifier> for RateLimitTarget<GroupId> { |
| 81 | + fn from(identifier: TransactionIdentifier) -> Self { |
| 82 | + Self::Transaction(identifier) |
| 83 | + } |
| 84 | +} |
| 85 | + |
| 86 | +/// Identifies a runtime call by pallet and extrinsic indices. |
| 87 | +#[derive( |
| 88 | + Serialize, |
| 89 | + Deserialize, |
| 90 | + Clone, |
| 91 | + Copy, |
| 92 | + PartialEq, |
| 93 | + Eq, |
| 94 | + PartialOrd, |
| 95 | + Ord, |
| 96 | + Encode, |
| 97 | + Decode, |
| 98 | + DecodeWithMemTracking, |
| 99 | + TypeInfo, |
| 100 | + MaxEncodedLen, |
| 101 | + Debug, |
| 102 | +)] |
| 103 | +pub struct TransactionIdentifier { |
| 104 | + /// Pallet variant index. |
| 105 | + pub pallet_index: u8, |
| 106 | + /// Call variant index within the pallet. |
| 107 | + pub extrinsic_index: u8, |
| 108 | +} |
| 109 | + |
| 110 | +impl TransactionIdentifier { |
| 111 | + /// Builds a new identifier from pallet/extrinsic indices. |
| 112 | + pub const fn new(pallet_index: u8, extrinsic_index: u8) -> Self { |
| 113 | + Self { |
| 114 | + pallet_index, |
| 115 | + extrinsic_index, |
| 116 | + } |
| 117 | + } |
| 118 | + |
| 119 | + /// Attempts to build an identifier from a SCALE-encoded call by reading the first two bytes. |
| 120 | + pub fn from_call<Call: codec::Encode>(call: &Call) -> Option<Self> { |
| 121 | + call.using_encoded(|encoded| { |
| 122 | + let pallet_index = *encoded.get(0)?; |
| 123 | + let extrinsic_index = *encoded.get(1)?; |
| 124 | + Some(Self::new(pallet_index, extrinsic_index)) |
| 125 | + }) |
| 126 | + } |
| 127 | + |
| 128 | + /// Resolves pallet/extrinsic names for this identifier using call metadata. |
| 129 | + pub fn names<Call: GetCallMetadata>(&self) -> Option<(&'static str, &'static str)> { |
| 130 | + let modules = Call::get_module_names(); |
| 131 | + let pallet_name = *modules.get(self.pallet_index as usize)?; |
| 132 | + let call_names = Call::get_call_names(pallet_name); |
| 133 | + let extrinsic_name = *call_names.get(self.extrinsic_index as usize)?; |
| 134 | + Some((pallet_name, extrinsic_name)) |
| 135 | + } |
| 136 | + |
| 137 | + /// Resolves a pallet/extrinsic name pair into a transaction identifier. |
| 138 | + pub fn for_call_names<Call: GetCallMetadata>( |
| 139 | + pallet_name: &str, |
| 140 | + extrinsic_name: &str, |
| 141 | + ) -> Option<Self> { |
| 142 | + let modules = Call::get_module_names(); |
| 143 | + let pallet_pos = modules.iter().position(|name| *name == pallet_name)?; |
| 144 | + let call_names = Call::get_call_names(pallet_name); |
| 145 | + let extrinsic_pos = call_names.iter().position(|name| *name == extrinsic_name)?; |
| 146 | + let pallet_index = u8::try_from(pallet_pos).ok()?; |
| 147 | + let extrinsic_index = u8::try_from(extrinsic_pos).ok()?; |
| 148 | + Some(Self::new(pallet_index, extrinsic_index)) |
| 149 | + } |
| 150 | +} |
| 151 | + |
| 152 | +/// Conversion into a concrete [`RateLimitTarget`]. |
| 153 | +pub trait TryIntoRateLimitTarget<GroupId> { |
| 154 | + type Error; |
| 155 | + |
| 156 | + fn try_into_rate_limit_target<Call: GetCallMetadata>( |
| 157 | + self, |
| 158 | + ) -> Result<RateLimitTarget<GroupId>, Self::Error>; |
| 159 | +} |
| 160 | + |
| 161 | +#[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 162 | +pub enum RateLimitTargetConversionError { |
| 163 | + InvalidUtf8, |
| 164 | + UnknownCall, |
| 165 | +} |
| 166 | + |
| 167 | +impl<GroupId> TryIntoRateLimitTarget<GroupId> for RateLimitTarget<GroupId> { |
| 168 | + type Error = core::convert::Infallible; |
| 169 | + |
| 170 | + fn try_into_rate_limit_target<Call: GetCallMetadata>( |
| 171 | + self, |
| 172 | + ) -> Result<RateLimitTarget<GroupId>, Self::Error> { |
| 173 | + Ok(self) |
| 174 | + } |
| 175 | +} |
| 176 | + |
| 177 | +impl<GroupId> TryIntoRateLimitTarget<GroupId> for GroupId { |
| 178 | + type Error = core::convert::Infallible; |
| 179 | + |
| 180 | + fn try_into_rate_limit_target<Call: GetCallMetadata>( |
| 181 | + self, |
| 182 | + ) -> Result<RateLimitTarget<GroupId>, Self::Error> { |
| 183 | + Ok(RateLimitTarget::Group(self)) |
| 184 | + } |
| 185 | +} |
| 186 | + |
| 187 | +impl TryIntoRateLimitTarget<u32> for (Vec<u8>, Vec<u8>) { |
| 188 | + type Error = RateLimitTargetConversionError; |
| 189 | + |
| 190 | + fn try_into_rate_limit_target<Call: GetCallMetadata>( |
| 191 | + self, |
| 192 | + ) -> Result<RateLimitTarget<u32>, Self::Error> { |
| 193 | + let (pallet, extrinsic) = self; |
| 194 | + let pallet_name = sp_std::str::from_utf8(&pallet) |
| 195 | + .map_err(|_| RateLimitTargetConversionError::InvalidUtf8)?; |
| 196 | + let extrinsic_name = sp_std::str::from_utf8(&extrinsic) |
| 197 | + .map_err(|_| RateLimitTargetConversionError::InvalidUtf8)?; |
| 198 | + |
| 199 | + let identifier = TransactionIdentifier::for_call_names::<Call>(pallet_name, extrinsic_name) |
| 200 | + .ok_or(RateLimitTargetConversionError::UnknownCall)?; |
| 201 | + |
| 202 | + Ok(RateLimitTarget::Transaction(identifier)) |
| 203 | + } |
| 204 | +} |
| 205 | + |
| 206 | +#[cfg(test)] |
| 207 | +mod tests { |
| 208 | + use super::*; |
| 209 | + use codec::Encode; |
| 210 | + use frame_support::traits::CallMetadata; |
| 211 | + |
| 212 | + #[derive(Clone, Copy, Debug, Encode)] |
| 213 | + struct DummyCall(u8, u8); |
| 214 | + |
| 215 | + impl GetCallMetadata for DummyCall { |
| 216 | + fn get_module_names() -> &'static [&'static str] { |
| 217 | + &["P0", "P1"] |
| 218 | + } |
| 219 | + |
| 220 | + fn get_call_names(module: &str) -> &'static [&'static str] { |
| 221 | + match module { |
| 222 | + "P0" => &["C0"], |
| 223 | + "P1" => &["C0", "C1", "C2", "C3", "C4"], |
| 224 | + _ => &[], |
| 225 | + } |
| 226 | + } |
| 227 | + |
| 228 | + fn get_call_metadata(&self) -> CallMetadata { |
| 229 | + CallMetadata { |
| 230 | + function_name: "unused", |
| 231 | + pallet_name: "unused", |
| 232 | + } |
| 233 | + } |
| 234 | + } |
| 235 | + |
| 236 | + #[test] |
| 237 | + fn transaction_identifier_from_call_reads_first_two_bytes() { |
| 238 | + let id = TransactionIdentifier::from_call(&DummyCall(1, 4)).expect("identifier"); |
| 239 | + assert_eq!(id, TransactionIdentifier::new(1, 4)); |
| 240 | + } |
| 241 | + |
| 242 | + #[test] |
| 243 | + fn transaction_identifier_names_resolves_metadata() { |
| 244 | + let id = TransactionIdentifier::new(1, 4); |
| 245 | + assert_eq!(id.names::<DummyCall>(), Some(("P1", "C4"))); |
| 246 | + } |
| 247 | + |
| 248 | + #[test] |
| 249 | + fn transaction_identifier_for_call_names_resolves_indices() { |
| 250 | + let id = TransactionIdentifier::for_call_names::<DummyCall>("P1", "C4").expect("id"); |
| 251 | + assert_eq!(id, TransactionIdentifier::new(1, 4)); |
| 252 | + } |
| 253 | + |
| 254 | + #[test] |
| 255 | + fn rate_limit_target_accessors_work() { |
| 256 | + let tx = RateLimitTarget::<u32>::Transaction(TransactionIdentifier::new(1, 4)); |
| 257 | + assert!(tx.as_group().is_none()); |
| 258 | + assert_eq!( |
| 259 | + tx.as_transaction().copied(), |
| 260 | + Some(TransactionIdentifier::new(1, 4)) |
| 261 | + ); |
| 262 | + |
| 263 | + let group = RateLimitTarget::<u32>::Group(7); |
| 264 | + assert!(group.as_transaction().is_none()); |
| 265 | + assert_eq!(group.as_group().copied(), Some(7)); |
| 266 | + } |
| 267 | +} |
0 commit comments