Skip to content

Commit 5c8a8ab

Browse files
committed
Add rate limit adjuster
1 parent fc60465 commit 5c8a8ab

File tree

5 files changed

+115
-31
lines changed

5 files changed

+115
-31
lines changed

pallets/rate-limiting/src/lib.rs

Lines changed: 52 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,9 @@
4848
//! The pallet relies on two resolvers:
4949
//!
5050
//! - [`Config::LimitScopeResolver`], which determines how limits are stored (for example by
51-
//! returning a `netuid`). When this resolver returns `None`, the configuration is stored as a
52-
//! global fallback.
51+
//! returning a `netuid`). The resolver can also signal that a call should bypass rate limiting or
52+
//! adjust the effective span at validation time. When it returns `None`, the configuration is
53+
//! stored as a global fallback.
5354
//! - [`Config::UsageResolver`], which decides how executions are tracked in
5455
//! [`LastSeen`](pallet::LastSeen). This can refine the limit scope (for example by returning a
5556
//! tuple of `(netuid, hyperparameter)`).
@@ -63,7 +64,7 @@
6364
//!
6465
//! // Limits are scoped per netuid.
6566
//! pub struct ScopeResolver;
66-
//! impl pallet_rate_limiting::RateLimitScopeResolver<RuntimeCall, NetUid> for ScopeResolver {
67+
//! impl pallet_rate_limiting::RateLimitScopeResolver<RuntimeCall, NetUid, BlockNumber> for ScopeResolver {
6768
//! fn context(call: &RuntimeCall) -> Option<NetUid> {
6869
//! match call {
6970
//! RuntimeCall::Subtensor(pallet_subtensor::Call::set_weights { netuid, .. }) => {
@@ -72,12 +73,15 @@
7273
//! _ => None,
7374
//! }
7475
//! }
76+
//!
77+
//! fn adjust_span(_call: &RuntimeCall, span: BlockNumber) -> BlockNumber {
78+
//! span
79+
//! }
7580
//! }
7681
//!
7782
//! // Usage tracking distinguishes hyperparameter + netuid.
7883
//! pub struct UsageResolver;
79-
//! impl pallet_rate_limiting::RateLimitUsageResolver<RuntimeCall, (NetUid, HyperParam)>
80-
//! for UsageResolver {
84+
//! impl pallet_rate_limiting::RateLimitUsageResolver<RuntimeCall, (NetUid, HyperParam)> for UsageResolver {
8185
//! fn context(call: &RuntimeCall) -> Option<(NetUid, HyperParam)> {
8286
//! match call {
8387
//! RuntimeCall::Subtensor(pallet_subtensor::Call::set_hyperparam {
@@ -156,7 +160,11 @@ pub mod pallet {
156160
type LimitScope: Parameter + Clone + PartialEq + Eq + Ord + MaybeSerializeDeserialize;
157161

158162
/// Resolves the scope for the given runtime call when configuring limits.
159-
type LimitScopeResolver: RateLimitScopeResolver<<Self as Config<I>>::RuntimeCall, Self::LimitScope>;
163+
type LimitScopeResolver: RateLimitScopeResolver<
164+
<Self as Config<I>>::RuntimeCall,
165+
Self::LimitScope,
166+
BlockNumberFor<Self>,
167+
>;
160168

161169
/// Usage key tracked in [`LastSeen`] for rate-limited calls.
162170
type UsageKey: Parameter + Clone + PartialEq + Eq + Ord + MaybeSerializeDeserialize;
@@ -306,21 +314,17 @@ pub mod pallet {
306314
identifier: &TransactionIdentifier,
307315
scope: &Option<<T as Config<I>>::LimitScope>,
308316
usage_key: &Option<<T as Config<I>>::UsageKey>,
317+
call: &<T as Config<I>>::RuntimeCall,
309318
) -> Result<bool, DispatchError> {
310-
let Some(block_span) = Self::resolved_limit(identifier, scope) else {
319+
if <T as Config<I>>::LimitScopeResolver::should_bypass(call) {
311320
return Ok(true);
312-
};
313-
314-
let current = frame_system::Pallet::<T>::block_number();
315-
316-
if let Some(last) = LastSeen::<T, I>::get(identifier, usage_key) {
317-
let delta = current.saturating_sub(last);
318-
if delta < block_span {
319-
return Ok(false);
320-
}
321321
}
322322

323-
Ok(true)
323+
let Some(block_span) = Self::effective_span(call, identifier, scope) else {
324+
return Ok(true);
325+
};
326+
327+
Ok(Self::within_span(identifier, usage_key, block_span))
324328
}
325329

326330
pub(crate) fn resolved_limit(
@@ -335,6 +339,37 @@ pub mod pallet {
335339
})
336340
}
337341

342+
pub(crate) fn effective_span(
343+
call: &<T as Config<I>>::RuntimeCall,
344+
identifier: &TransactionIdentifier,
345+
scope: &Option<<T as Config<I>>::LimitScope>,
346+
) -> Option<BlockNumberFor<T>> {
347+
let span = Self::resolved_limit(identifier, scope)?;
348+
Some(<T as Config<I>>::LimitScopeResolver::adjust_span(
349+
call, span,
350+
))
351+
}
352+
353+
pub(crate) fn within_span(
354+
identifier: &TransactionIdentifier,
355+
usage_key: &Option<<T as Config<I>>::UsageKey>,
356+
block_span: BlockNumberFor<T>,
357+
) -> bool {
358+
if block_span.is_zero() {
359+
return true;
360+
}
361+
362+
if let Some(last) = LastSeen::<T, I>::get(identifier, usage_key) {
363+
let current = frame_system::Pallet::<T>::block_number();
364+
let delta = current.saturating_sub(last);
365+
if delta < block_span {
366+
return false;
367+
}
368+
}
369+
370+
true
371+
}
372+
338373
/// Returns the configured limit for the specified pallet/extrinsic names, if any.
339374
pub fn limit_for_call_names(
340375
pallet_name: &str,

pallets/rate-limiting/src/mock.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ pub type UsageKey = u16;
6161
pub struct TestScopeResolver;
6262
pub struct TestUsageResolver;
6363

64-
impl pallet_rate_limiting::RateLimitScopeResolver<RuntimeCall, LimitScope> for TestScopeResolver {
64+
impl pallet_rate_limiting::RateLimitScopeResolver<RuntimeCall, LimitScope, u64>
65+
for TestScopeResolver
66+
{
6567
fn context(call: &RuntimeCall) -> Option<LimitScope> {
6668
match call {
6769
RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span }) => {
@@ -78,6 +80,17 @@ impl pallet_rate_limiting::RateLimitScopeResolver<RuntimeCall, LimitScope> for T
7880
RuntimeCall::RateLimiting(RateLimitingCall::clear_rate_limit { .. })
7981
)
8082
}
83+
84+
fn adjust_span(call: &RuntimeCall, span: u64) -> u64 {
85+
if matches!(
86+
call,
87+
RuntimeCall::RateLimiting(RateLimitingCall::clear_all_rate_limits { .. })
88+
) {
89+
span.saturating_mul(2)
90+
} else {
91+
span
92+
}
93+
}
8194
}
8295

8396
impl pallet_rate_limiting::RateLimitUsageResolver<RuntimeCall, UsageKey> for TestUsageResolver {

pallets/rate-limiting/src/tests.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ fn is_within_limit_is_true_when_no_limit() {
121121
RuntimeCall::RateLimiting(RateLimitingCall::set_default_rate_limit { block_span: 0 });
122122
let identifier = identifier_for(&call);
123123

124-
let result = RateLimiting::is_within_limit(&identifier, &None, &None);
124+
let result = RateLimiting::is_within_limit(&identifier, &None, &None, &call);
125125
assert_eq!(result.expect("no error expected"), true);
126126
});
127127
}
@@ -144,6 +144,7 @@ fn is_within_limit_false_when_rate_limited() {
144144
&identifier,
145145
&Some(1 as LimitScope),
146146
&Some(1 as UsageKey),
147+
&call,
147148
)
148149
.expect("call succeeds");
149150
assert!(!within);
@@ -168,6 +169,7 @@ fn is_within_limit_true_after_required_span() {
168169
&identifier,
169170
&Some(2 as LimitScope),
170171
&Some(2 as UsageKey),
172+
&call,
171173
)
172174
.expect("call succeeds");
173175
assert!(within);

pallets/rate-limiting/src/tx_extension.rs

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,16 +109,15 @@ where
109109
let scope = <T as Config<I>>::LimitScopeResolver::context(call);
110110
let usage = <T as Config<I>>::UsageResolver::context(call);
111111

112-
let Some(block_span) = Pallet::<T, I>::resolved_limit(&identifier, &scope) else {
112+
let Some(block_span) = Pallet::<T, I>::effective_span(call, &identifier, &scope) else {
113113
return Ok((ValidTransaction::default(), None, origin));
114114
};
115115

116116
if block_span.is_zero() {
117117
return Ok((ValidTransaction::default(), None, origin));
118118
}
119119

120-
let within_limit = Pallet::<T, I>::is_within_limit(&identifier, &scope, &usage)
121-
.map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::Call))?;
120+
let within_limit = Pallet::<T, I>::within_span(&identifier, &usage, block_span);
122121

123122
if !within_limit {
124123
return Err(TransactionValidityError::Invalid(
@@ -188,6 +187,12 @@ mod tests {
188187
})
189188
}
190189

190+
fn adjustable_call() -> RuntimeCall {
191+
RuntimeCall::RateLimiting(RateLimitingCall::clear_all_rate_limits {
192+
call: Box::new(remark_call()),
193+
})
194+
}
195+
191196
fn new_tx_extension() -> RateLimitTransactionExtension<Test> {
192197
RateLimitTransactionExtension(Default::default())
193198
}
@@ -273,6 +278,29 @@ mod tests {
273278
});
274279
}
275280

281+
#[test]
282+
fn tx_extension_applies_adjusted_span() {
283+
new_test_ext().execute_with(|| {
284+
let extension = new_tx_extension();
285+
let call = adjustable_call();
286+
let identifier = identifier_for(&call);
287+
Limits::<Test, ()>::insert(identifier, RateLimit::global(RateLimitKind::Exact(4)));
288+
LastSeen::<Test, ()>::insert(identifier, Some(1u16), 10);
289+
290+
System::set_block_number(14);
291+
292+
// Stored span (4) would allow the call, but adjusted span (8) should block it.
293+
let err = validate_with_tx_extension(&extension, &call)
294+
.expect_err("adjusted span should apply");
295+
match err {
296+
TransactionValidityError::Invalid(InvalidTransaction::Custom(code)) => {
297+
assert_eq!(code, RATE_LIMIT_DENIED);
298+
}
299+
other => panic!("unexpected error: {:?}", other),
300+
}
301+
});
302+
}
303+
276304
#[test]
277305
fn tx_extension_records_last_seen_for_successful_call() {
278306
new_test_ext().execute_with(|| {

pallets/rate-limiting/src/types.rs

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,30 @@ use frame_support::{pallet_prelude::DispatchError, traits::GetCallMetadata};
33
use scale_info::TypeInfo;
44
use sp_std::collections::btree_map::BTreeMap;
55

6-
/// Resolves the optional identifier within which a rate limit applies and can optionally bypass
7-
/// enforcement.
8-
pub trait RateLimitScopeResolver<Call, Scope> {
9-
/// Returns `Some(scope)` when the limit should be applied per-scope, or `None` for global
10-
/// limits.
6+
/// Resolves the optional identifier within which a rate limit applies and can optionally adjust
7+
/// enforcement behaviour.
8+
pub trait RateLimitScopeResolver<Call, Scope, Span> {
9+
/// Returns `Some(scope)` when the limit should be applied per-scope, or `None` for global
10+
/// limits.
1111
fn context(call: &Call) -> Option<Scope>;
1212

13-
/// Returns `true` when the rate limit should be bypassed for the provided call. Defaults to
14-
/// `false`.
13+
/// Returns `true` when the rate limit should be bypassed for the provided call. Defaults to
14+
/// `false`.
1515
fn should_bypass(_call: &Call) -> bool {
1616
false
1717
}
18+
19+
/// Optionally adjusts the effective span used during enforcement. Defaults to the original
20+
/// `span`.
21+
fn adjust_span(_call: &Call, span: Span) -> Span {
22+
span
23+
}
1824
}
1925

2026
/// Resolves the optional usage tracking key applied when enforcing limits.
2127
pub trait RateLimitUsageResolver<Call, Usage> {
22-
/// Returns `Some(usage)` when usage should be tracked per-key, or `None` for global usage
23-
/// tracking.
28+
/// Returns `Some(usage)` when usage should be tracked per-key, or `None` for global usage
29+
/// tracking.
2430
fn context(call: &Call) -> Option<Usage>;
2531
}
2632

0 commit comments

Comments
 (0)