1use crate::{
22 ids::{MessageId, ReservationId, prelude::*},
23 message::IncomingDispatch,
24};
25use alloc::{collections::BTreeMap, format};
26use gear_core_errors::ReservationError;
27use scale_info::{
28 TypeInfo,
29 scale::{Decode, Encode},
30};
31
32#[derive(
41 Clone, Copy, Default, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Decode, Encode, TypeInfo,
42)]
43#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
44pub struct ReservationNonce(u64);
45
46impl From<&InnerNonce> for ReservationNonce {
47 fn from(nonce: &InnerNonce) -> Self {
48 ReservationNonce(nonce.0)
49 }
50}
51
52#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
55struct InnerNonce(u64);
56
57impl InnerNonce {
58 fn fetch_inc(&mut self) -> u64 {
61 let current = self.0;
62 self.0 = self.0.saturating_add(1);
63
64 current
65 }
66}
67
68impl From<ReservationNonce> for InnerNonce {
69 fn from(frozen_nonce: ReservationNonce) -> Self {
70 InnerNonce(frozen_nonce.0)
71 }
72}
73
74#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
78pub struct GasReserver {
79 message_id: MessageId,
82 nonce: InnerNonce,
89 states: GasReservationStates,
91 max_reservations: u64,
99}
100
101impl GasReserver {
102 pub fn new(
107 incoming_dispatch: &IncomingDispatch,
108 map: GasReservationMap,
109 max_reservations: u64,
110 ) -> Self {
111 let message_id = incoming_dispatch.id();
112 let nonce = incoming_dispatch
113 .context()
114 .as_ref()
115 .map(|c| c.reservation_nonce())
116 .unwrap_or_default()
117 .into();
118 Self {
119 message_id,
120 nonce,
121 states: {
122 let mut states = BTreeMap::new();
123 states.extend(map.into_iter().map(|(id, slot)| (id, slot.into())));
124 states
125 },
126 max_reservations,
127 }
128 }
129
130 pub fn is_empty(&self) -> bool {
132 self.states.is_empty()
133 }
134
135 fn check_execution_limit(&self) -> Result<(), ReservationError> {
140 let current_reservations = self
143 .states
144 .values()
145 .map(|state| {
146 matches!(
147 state,
148 GasReservationState::Exists { .. } | GasReservationState::Created { .. }
149 ) as u64
150 })
151 .sum::<u64>();
152 if current_reservations > self.max_reservations {
153 Err(ReservationError::ReservationsLimitReached)
154 } else {
155 Ok(())
156 }
157 }
158
159 pub fn limit_of(&self, reservation_id: &ReservationId) -> Option<u64> {
161 self.states.get(reservation_id).and_then(|v| match v {
162 GasReservationState::Exists { amount, .. }
163 | GasReservationState::Created { amount, .. } => Some(*amount),
164 _ => None,
165 })
166 }
167
168 pub fn reserve(
174 &mut self,
175 amount: u64,
176 duration: u32,
177 ) -> Result<ReservationId, ReservationError> {
178 self.check_execution_limit()?;
179
180 let id = ReservationId::generate(self.message_id, self.nonce.fetch_inc());
181
182 let maybe_reservation = self.states.insert(
183 id,
184 GasReservationState::Created {
185 amount,
186 duration,
187 used: false,
188 },
189 );
190
191 if maybe_reservation.is_some() {
192 let err_msg = format!(
193 "GasReserver::reserve: created a duplicate reservation. \
194 Message id - {message_id}, nonce - {nonce}",
195 message_id = self.message_id,
196 nonce = self.nonce.0
197 );
198
199 log::error!("{err_msg}");
200 unreachable!("{err_msg}");
201 }
202
203 Ok(id)
204 }
205
206 pub fn unreserve(
213 &mut self,
214 id: ReservationId,
215 ) -> Result<(u64, Option<UnreservedReimbursement>), ReservationError> {
216 let state = self
218 .states
219 .get(&id)
220 .ok_or(ReservationError::InvalidReservationId)?;
221
222 if matches!(
223 state,
224 GasReservationState::Removed { .. } |
226 GasReservationState::Exists { used: true, .. } |
228 GasReservationState::Created { used: true, .. }
229 ) {
230 return Err(ReservationError::InvalidReservationId);
231 }
232
233 let state = self.states.remove(&id).unwrap();
234
235 Ok(match state {
236 GasReservationState::Exists { amount, finish, .. } => {
237 self.states
238 .insert(id, GasReservationState::Removed { expiration: finish });
239 (amount, None)
240 }
241 GasReservationState::Created {
242 amount, duration, ..
243 } => (amount, Some(UnreservedReimbursement(duration))),
244 GasReservationState::Removed { .. } => {
245 let err_msg =
246 "GasReserver::unreserve: `Removed` variant is unreachable, checked above";
247
248 log::error!("{err_msg}");
249 unreachable!("{err_msg}")
250 }
251 })
252 }
253
254 pub fn mark_used(&mut self, id: ReservationId) -> Result<(), ReservationError> {
260 let used = self.check_not_used(id)?;
261 *used = true;
262 Ok(())
263 }
264
265 pub fn check_not_used(&mut self, id: ReservationId) -> Result<&mut bool, ReservationError> {
269 if let Some(
270 GasReservationState::Created { used, .. } | GasReservationState::Exists { used, .. },
271 ) = self.states.get_mut(&id)
272 {
273 if *used {
274 Err(ReservationError::InvalidReservationId)
275 } else {
276 Ok(used)
277 }
278 } else {
279 Err(ReservationError::InvalidReservationId)
280 }
281 }
282
283 pub fn nonce(&self) -> ReservationNonce {
285 (&self.nonce).into()
286 }
287
288 pub fn states(&self) -> &GasReservationStates {
290 &self.states
291 }
292
293 pub fn into_map<F>(
295 self,
296 current_block_height: u32,
297 duration_into_expiration: F,
298 ) -> GasReservationMap
299 where
300 F: Fn(u32) -> u32,
301 {
302 self.states
303 .into_iter()
304 .flat_map(|(id, state)| match state {
305 GasReservationState::Exists {
306 amount,
307 start,
308 finish,
309 ..
310 } => Some((
311 id,
312 GasReservationSlot {
313 amount,
314 start,
315 finish,
316 },
317 )),
318 GasReservationState::Created {
319 amount, duration, ..
320 } => {
321 let expiration = duration_into_expiration(duration);
322 Some((
323 id,
324 GasReservationSlot {
325 amount,
326 start: current_block_height,
327 finish: expiration,
328 },
329 ))
330 }
331 GasReservationState::Removed { .. } => None,
332 })
333 .collect()
334 }
335}
336
337#[derive(Debug, PartialEq, Eq)]
341pub struct UnreservedReimbursement(u32);
342
343impl UnreservedReimbursement {
344 pub fn duration(&self) -> u32 {
346 self.0
347 }
348}
349
350pub type GasReservationStates = BTreeMap<ReservationId, GasReservationState>;
352
353#[derive(Debug, Clone, Copy, Eq, PartialEq, Encode, Decode)]
357pub enum GasReservationState {
358 Exists {
360 amount: u64,
362 start: u32,
364 finish: u32,
366 used: bool,
368 },
369 Created {
371 amount: u64,
373 duration: u32,
375 used: bool,
377 },
378 Removed {
380 expiration: u32,
382 },
383}
384
385impl From<GasReservationSlot> for GasReservationState {
386 fn from(slot: GasReservationSlot) -> Self {
387 Self::Exists {
388 amount: slot.amount,
389 start: slot.start,
390 finish: slot.finish,
391 used: false,
392 }
393 }
394}
395
396pub type GasReservationMap = BTreeMap<ReservationId, GasReservationSlot>;
400
401#[derive(Debug, Clone, Eq, PartialEq, Encode, Decode, TypeInfo)]
403pub struct GasReservationSlot {
404 pub amount: u64,
406 pub start: u32,
408 pub finish: u32,
410}
411
412#[cfg(test)]
413mod tests {
414 use super::*;
415
416 const MAX_RESERVATIONS: u64 = 256;
417
418 fn new_reserver() -> GasReserver {
419 let d = IncomingDispatch::default();
420 GasReserver::new(&d, Default::default(), MAX_RESERVATIONS)
421 }
422
423 #[test]
424 fn max_reservations_limit_works() {
425 let mut reserver = new_reserver();
426 for n in 0..(MAX_RESERVATIONS * 10) {
427 let res = reserver.reserve(100, 10);
428 if n > MAX_RESERVATIONS {
429 assert_eq!(res, Err(ReservationError::ReservationsLimitReached));
430 } else {
431 assert!(res.is_ok());
432 }
433 }
434 }
435
436 #[test]
437 fn mark_used_for_unreserved_fails() {
438 let mut reserver = new_reserver();
439 let id = reserver.reserve(1, 1).unwrap();
440 reserver.unreserve(id).unwrap();
441
442 assert_eq!(
443 reserver.mark_used(id),
444 Err(ReservationError::InvalidReservationId)
445 );
446 }
447
448 #[test]
449 fn mark_used_twice_fails() {
450 let mut reserver = new_reserver();
451 let id = reserver.reserve(1, 1).unwrap();
452 reserver.mark_used(id).unwrap();
453 assert_eq!(
454 reserver.mark_used(id),
455 Err(ReservationError::InvalidReservationId)
456 );
457
458 assert_eq!(
460 reserver.mark_used(ReservationId::default()),
461 Err(ReservationError::InvalidReservationId)
462 );
463 }
464
465 #[test]
466 fn remove_reservation_twice_fails() {
467 let mut reserver = new_reserver();
468 let id = reserver.reserve(1, 1).unwrap();
469 reserver.unreserve(id).unwrap();
470 assert_eq!(
471 reserver.unreserve(id),
472 Err(ReservationError::InvalidReservationId)
473 );
474 }
475
476 #[test]
477 fn remove_non_existing_reservation_fails() {
478 let id = ReservationId::from([0xff; 32]);
479
480 let mut map = GasReservationMap::new();
481 map.insert(
482 id,
483 GasReservationSlot {
484 amount: 1,
485 start: 1,
486 finish: 100,
487 },
488 );
489
490 let mut reserver = GasReserver::new(&Default::default(), map, 256);
491 reserver.unreserve(id).unwrap();
492
493 assert_eq!(
494 reserver.unreserve(id),
495 Err(ReservationError::InvalidReservationId)
496 );
497 }
498
499 #[test]
500 fn fresh_reserve_unreserve() {
501 let mut reserver = new_reserver();
502 let id = reserver.reserve(10_000, 5).unwrap();
503 reserver.mark_used(id).unwrap();
504 assert_eq!(
505 reserver.unreserve(id),
506 Err(ReservationError::InvalidReservationId)
507 );
508 }
509
510 #[test]
511 fn existing_reserve_unreserve() {
512 let id = ReservationId::from([0xff; 32]);
513
514 let mut map = GasReservationMap::new();
515 map.insert(
516 id,
517 GasReservationSlot {
518 amount: 1,
519 start: 1,
520 finish: 100,
521 },
522 );
523
524 let mut reserver = GasReserver::new(&Default::default(), map, 256);
525 reserver.mark_used(id).unwrap();
526 assert_eq!(
527 reserver.unreserve(id),
528 Err(ReservationError::InvalidReservationId)
529 );
530 }
531
532 #[test]
533 fn unreserving_unreserved() {
534 let id = ReservationId::from([0xff; 32]);
535 let slot = GasReservationSlot {
536 amount: 1,
537 start: 2,
538 finish: 3,
539 };
540
541 let mut map = GasReservationMap::new();
542 map.insert(id, slot.clone());
543
544 let mut reserver = GasReserver::new(&Default::default(), map, 256);
545
546 let (amount, _) = reserver.unreserve(id).expect("Shouldn't fail");
547 assert_eq!(amount, slot.amount);
548
549 assert!(reserver.unreserve(id).is_err());
550 assert_eq!(
551 reserver.states().get(&id).cloned(),
552 Some(GasReservationState::Removed {
553 expiration: slot.finish
554 })
555 );
556 }
557}