Skip to content

Commit 483be0c

Browse files
committed
Add rate limiting interface
1 parent 30d5e32 commit 483be0c

File tree

17 files changed

+401
-186
lines changed

17 files changed

+401
-186
lines changed

Cargo.lock

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ subtensor-runtime-common = { default-features = false, path = "common" }
7575
subtensor-swap-interface = { default-features = false, path = "pallets/swap-interface" }
7676
subtensor-transaction-fee = { default-features = false, path = "pallets/transaction-fee" }
7777
subtensor-chain-extensions = { default-features = false, path = "chain-extensions" }
78+
rate-limiting-interface = { default-features = false, path = "pallets/rate-limiting-interface" }
7879

7980
ed25519-dalek = { version = "2.1.0", default-features = false }
8081
async-trait = "0.1"

common/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use subtensor_macros::freeze_struct;
1717
pub use currency::*;
1818

1919
mod currency;
20+
pub mod rate_limiting;
2021

2122
/// Balance of an account.
2223
pub type Balance = u64;

common/src/rate_limiting.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//! Shared rate-limiting types.
2+
3+
/// Identifier type for rate-limiting groups.
4+
pub type GroupId = u32;
5+
6+
/// Group id for serving-related calls.
7+
pub const GROUP_SERVE: GroupId = 0;
8+
/// Group id for delegate-take related calls.
9+
pub const GROUP_DELEGATE_TAKE: GroupId = 1;
10+
/// Group id for subnet weight-setting calls.
11+
pub const GROUP_WEIGHTS_SUBNET: GroupId = 2;
12+
/// Group id for network registration calls.
13+
pub const GROUP_REGISTER_NETWORK: GroupId = 3;
14+
/// Group id for owner hyperparameter calls.
15+
pub const GROUP_OWNER_HPARAMS: GroupId = 4;
16+
/// Group id for staking operations.
17+
pub const GROUP_STAKING_OPS: GroupId = 5;
18+
/// Group id for key swap calls.
19+
pub const GROUP_SWAP_KEYS: GroupId = 6;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[package]
2+
name = "rate-limiting-interface"
3+
version = "0.1.0"
4+
edition.workspace = true
5+
6+
[lints]
7+
workspace = true
8+
9+
[dependencies]
10+
codec = { workspace = true, features = ["derive"], default-features = false }
11+
frame-support = { workspace = true, default-features = false }
12+
scale-info = { workspace = true, features = ["derive"], default-features = false }
13+
serde = { workspace = true, features = ["derive"], default-features = false }
14+
sp-std = { workspace = true, default-features = false }
15+
16+
[features]
17+
default = ["std"]
18+
std = [
19+
"codec/std",
20+
"frame-support/std",
21+
"scale-info/std",
22+
"serde/std",
23+
"sp-std/std",
24+
]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# `rate-limiting-interface`
2+
3+
Small, `no_std`-friendly interface crate that defines [`RateLimitingInfo`](src/lib.rs).
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
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+
}

pallets/rate-limiting/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ scale-info = { workspace = true, features = ["derive"] }
1818
serde = { workspace = true, features = ["derive"] }
1919
sp-std.workspace = true
2020
sp-runtime.workspace = true
21+
rate-limiting-interface.workspace = true
2122
subtensor-runtime-common.workspace = true
2223

2324
[dev-dependencies]
@@ -32,6 +33,7 @@ std = [
3233
"frame-benchmarking?/std",
3334
"frame-support/std",
3435
"frame-system/std",
36+
"rate-limiting-interface/std",
3537
"scale-info/std",
3638
"serde/std",
3739
"sp-std/std",

0 commit comments

Comments
 (0)