pallet_staking_reward_curve/
lib.rs1mod log;
21
22use log::log2;
23use proc_macro::TokenStream;
24use proc_macro2::{Span, TokenStream as TokenStream2};
25use proc_macro_crate::{crate_name, FoundCrate};
26use quote::{quote, ToTokens};
27use syn::parse::{Parse, ParseStream};
28
29#[proc_macro]
74pub fn build(input: TokenStream) -> TokenStream {
75 let input = syn::parse_macro_input!(input as INposInput);
76
77 let points = compute_points(&input);
78
79 let declaration = generate_piecewise_linear(points);
80 let test_module = generate_test_module(&input);
81
82 let imports = match crate_name("sp-runtime") {
83 Ok(FoundCrate::Itself) => quote!(
84 #[doc(hidden)]
85 pub use sp_runtime as _sp_runtime;
86 ),
87 Ok(FoundCrate::Name(sp_runtime)) => {
88 let ident = syn::Ident::new(&sp_runtime, Span::call_site());
89 quote!( #[doc(hidden)] pub use #ident as _sp_runtime; )
90 },
91 Err(e) => match crate_name("polkadot-sdk") {
92 Ok(FoundCrate::Name(polkadot_sdk)) => {
93 let ident = syn::Ident::new(&polkadot_sdk, Span::call_site());
94 quote!( #[doc(hidden)] pub use #ident::sp_runtime as _sp_runtime; )
95 },
96 _ => syn::Error::new(Span::call_site(), e).to_compile_error(),
97 },
98 };
99
100 let const_name = input.ident;
101 let const_type = input.typ;
102
103 quote!(
104 const #const_name: #const_type = {
105 #imports
106 #declaration
107 };
108 #test_module
109 )
110 .into()
111}
112
113const MILLION: u32 = 1_000_000;
114
115mod keyword {
116 syn::custom_keyword!(curve);
117 syn::custom_keyword!(min_inflation);
118 syn::custom_keyword!(max_inflation);
119 syn::custom_keyword!(ideal_stake);
120 syn::custom_keyword!(falloff);
121 syn::custom_keyword!(max_piece_count);
122 syn::custom_keyword!(test_precision);
123}
124
125struct INposInput {
126 ident: syn::Ident,
127 typ: syn::Type,
128 min_inflation: u32,
129 ideal_stake: u32,
130 max_inflation: u32,
131 falloff: u32,
132 max_piece_count: u32,
133 test_precision: u32,
134}
135
136struct Bounds {
137 min: u32,
138 min_strict: bool,
139 max: u32,
140 max_strict: bool,
141}
142
143impl Bounds {
144 fn check(&self, value: u32) -> bool {
145 let wrong = (self.min_strict && value <= self.min) ||
146 (!self.min_strict && value < self.min) ||
147 (self.max_strict && value >= self.max) ||
148 (!self.max_strict && value > self.max);
149
150 !wrong
151 }
152}
153
154impl core::fmt::Display for Bounds {
155 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
156 write!(
157 f,
158 "{}{:07}; {:07}{}",
159 if self.min_strict { "]" } else { "[" },
160 self.min,
161 self.max,
162 if self.max_strict { "[" } else { "]" },
163 )
164 }
165}
166
167fn parse_field<Token: Parse + Default + ToTokens>(
168 input: ParseStream,
169 bounds: Bounds,
170) -> syn::Result<u32> {
171 <Token>::parse(input)?;
172 <syn::Token![:]>::parse(input)?;
173 let value_lit = syn::LitInt::parse(input)?;
174 let value: u32 = value_lit.base10_parse()?;
175 if !bounds.check(value) {
176 return Err(syn::Error::new(
177 value_lit.span(),
178 format!(
179 "Invalid {}: {}, must be in {}",
180 Token::default().to_token_stream(),
181 value,
182 bounds,
183 ),
184 ))
185 }
186
187 Ok(value)
188}
189
190impl Parse for INposInput {
191 fn parse(input: ParseStream) -> syn::Result<Self> {
192 let args_input;
193
194 <syn::Token![const]>::parse(input)?;
195 let ident = <syn::Ident>::parse(input)?;
196 <syn::Token![:]>::parse(input)?;
197 let typ = <syn::Type>::parse(input)?;
198 <syn::Token![=]>::parse(input)?;
199 <keyword::curve>::parse(input)?;
200 <syn::Token![!]>::parse(input)?;
201 syn::parenthesized!(args_input in input);
202 <syn::Token![;]>::parse(input)?;
203
204 if !input.is_empty() {
205 return Err(input.error("expected end of input stream, no token expected"))
206 }
207
208 let min_inflation = parse_field::<keyword::min_inflation>(
209 &args_input,
210 Bounds { min: 0, min_strict: true, max: 1_000_000, max_strict: false },
211 )?;
212 <syn::Token![,]>::parse(&args_input)?;
213 let max_inflation = parse_field::<keyword::max_inflation>(
214 &args_input,
215 Bounds { min: min_inflation, min_strict: true, max: 1_000_000, max_strict: false },
216 )?;
217 <syn::Token![,]>::parse(&args_input)?;
218 let ideal_stake = parse_field::<keyword::ideal_stake>(
219 &args_input,
220 Bounds { min: 0_100_000, min_strict: false, max: 0_900_000, max_strict: false },
221 )?;
222 <syn::Token![,]>::parse(&args_input)?;
223 let falloff = parse_field::<keyword::falloff>(
224 &args_input,
225 Bounds { min: 0_010_000, min_strict: false, max: 1_000_000, max_strict: false },
226 )?;
227 <syn::Token![,]>::parse(&args_input)?;
228 let max_piece_count = parse_field::<keyword::max_piece_count>(
229 &args_input,
230 Bounds { min: 2, min_strict: false, max: 1_000, max_strict: false },
231 )?;
232 <syn::Token![,]>::parse(&args_input)?;
233 let test_precision = parse_field::<keyword::test_precision>(
234 &args_input,
235 Bounds { min: 0, min_strict: false, max: 1_000_000, max_strict: false },
236 )?;
237 <Option<syn::Token![,]>>::parse(&args_input)?;
238
239 if !args_input.is_empty() {
240 return Err(args_input.error("expected end of input stream, no token expected"))
241 }
242
243 Ok(Self {
244 ident,
245 typ,
246 min_inflation,
247 ideal_stake,
248 max_inflation,
249 falloff,
250 max_piece_count,
251 test_precision,
252 })
253 }
254}
255
256struct INPoS {
257 i_0: u32,
258 i_ideal_times_x_ideal: u32,
259 i_ideal: u32,
260 x_ideal: u32,
261 d: u32,
262}
263
264impl INPoS {
265 fn from_input(input: &INposInput) -> Self {
266 INPoS {
267 i_0: input.min_inflation,
268 i_ideal: (input.max_inflation as u64 * MILLION as u64 / input.ideal_stake as u64)
269 .try_into()
270 .unwrap(),
271 i_ideal_times_x_ideal: input.max_inflation,
272 x_ideal: input.ideal_stake,
273 d: input.falloff,
274 }
275 }
276
277 fn compute_opposite_after_x_ideal(&self, y: u32) -> u32 {
281 if y == self.i_0 {
282 return u32::MAX
283 }
284 let log = log2(self.i_ideal_times_x_ideal - self.i_0, y - self.i_0);
286
287 let term: u32 = ((self.d as u64 * log as u64) / 1_000_000).try_into().unwrap();
288
289 self.x_ideal + term
290 }
291}
292
293fn compute_points(input: &INposInput) -> Vec<(u32, u32)> {
294 let inpos = INPoS::from_input(input);
295
296 let mut points = vec![(0, inpos.i_0), (inpos.x_ideal, inpos.i_ideal_times_x_ideal)];
297
298 let max_length = (input.max_inflation - input.min_inflation + 1_000_000 - inpos.x_ideal) /
301 (input.max_piece_count - 1);
302
303 let mut delta_y = max_length;
304 let mut y = input.max_inflation;
305
306 while delta_y != 0 {
309 let next_y = y - delta_y;
310
311 if next_y <= input.min_inflation {
312 delta_y = delta_y.saturating_sub(1);
313 continue
314 }
315
316 let next_x = inpos.compute_opposite_after_x_ideal(next_y);
317
318 if (next_x - points.last().unwrap().0) > max_length {
319 delta_y = delta_y.saturating_sub(1);
320 continue
321 }
322
323 if next_x >= 1_000_000 {
324 let prev = points.last().unwrap();
325 let delta_y: u32 = ((next_x - 1_000_000) as u64 * (prev.1 - next_y) as u64 /
328 (next_x - prev.0) as u64)
329 .try_into()
330 .unwrap();
331
332 let y = next_y + delta_y;
333
334 points.push((1_000_000, y));
335 return points
336 }
337 points.push((next_x, next_y));
338 y = next_y;
339 }
340
341 points.push((1_000_000, inpos.i_0));
342
343 points
344}
345
346fn generate_piecewise_linear(points: Vec<(u32, u32)>) -> TokenStream2 {
347 let mut points_tokens = quote!();
348
349 let max = points
350 .iter()
351 .map(|&(_, x)| x)
352 .max()
353 .unwrap_or(0)
354 .checked_mul(1_000)
355 .unwrap_or(1_000_000_000);
357
358 for (x, y) in points {
359 let error = || {
360 panic!(
361 "Generated reward curve approximation doesn't fit into [0, 1] -> [0, 1] because \
362 of point:
363 x = {:07} per million
364 y = {:07} per million",
365 x, y
366 )
367 };
368
369 let x_perbill = x.checked_mul(1_000).unwrap_or_else(error);
370 let y_perbill = y.checked_mul(1_000).unwrap_or_else(error);
371
372 points_tokens.extend(quote!(
373 (
374 _sp_runtime::Perbill::from_parts(#x_perbill),
375 _sp_runtime::Perbill::from_parts(#y_perbill),
376 ),
377 ));
378 }
379
380 quote!(
381 _sp_runtime::curve::PiecewiseLinear::<'static> {
382 points: & [ #points_tokens ],
383 maximum: _sp_runtime::Perbill::from_parts(#max),
384 }
385 )
386}
387
388fn generate_test_module(input: &INposInput) -> TokenStream2 {
389 let inpos = INPoS::from_input(input);
390
391 let ident = &input.ident;
392 let precision = input.test_precision;
393 let i_0 = inpos.i_0 as f64 / MILLION as f64;
394 let i_ideal_times_x_ideal = inpos.i_ideal_times_x_ideal as f64 / MILLION as f64;
395 let i_ideal = inpos.i_ideal as f64 / MILLION as f64;
396 let x_ideal = inpos.x_ideal as f64 / MILLION as f64;
397 let d = inpos.d as f64 / MILLION as f64;
398 let max_piece_count = input.max_piece_count;
399
400 quote!(
401 #[cfg(test)]
402 mod __pallet_staking_reward_curve_test_module {
403 fn i_npos(x: f64) -> f64 {
404 if x <= #x_ideal {
405 #i_0 + x * (#i_ideal - #i_0 / #x_ideal)
406 } else {
407 #i_0 + (#i_ideal_times_x_ideal - #i_0) * 2_f64.powf((#x_ideal - x) / #d)
408 }
409 }
410
411 const MILLION: u32 = 1_000_000;
412
413 #[test]
414 fn reward_curve_precision() {
415 for &base in [MILLION, u32::MAX].iter() {
416 let number_of_check = 100_000.min(base);
417 for check_index in 0..=number_of_check {
418 let i = (check_index as u64 * base as u64 / number_of_check as u64) as u32;
419 let x = i as f64 / base as f64;
420 let float_res = (i_npos(x) * base as f64).round() as u32;
421 let int_res = super::#ident.calculate_for_fraction_times_denominator(i, base);
422 let err = (
423 (float_res.max(int_res) - float_res.min(int_res)) as u64
424 * MILLION as u64
425 / float_res as u64
426 ) as u32;
427 if err > #precision {
428 panic!("\n\
429 Generated reward curve approximation differ from real one:\n\t\
430 for i = {} and base = {}, f(i/base) * base = {},\n\t\
431 but approximation = {},\n\t\
432 err = {:07} millionth,\n\t\
433 try increase the number of segment: {} or the test_error: {}.\n",
434 i, base, float_res, int_res, err, #max_piece_count, #precision
435 );
436 }
437 }
438 }
439 }
440
441 #[test]
442 fn reward_curve_piece_count() {
443 assert!(
444 super::#ident.points.len() as u32 - 1 <= #max_piece_count,
445 "Generated reward curve approximation is invalid: \
446 has more points than specified, please fill an issue."
447 );
448 }
449 }
450 )
451}