gear_core/
reservation.rs

1// This file is part of Gear.
2
3// Copyright (C) 2022-2025 Gear Technologies Inc.
4// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
5
6// This program is free software: you can redistribute it and/or modify
7// it under the terms of the GNU General Public License as published by
8// the Free Software Foundation, either version 3 of the License, or
9// (at your option) any later version.
10
11// This program is distributed in the hope that it will be useful,
12// but WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14// GNU General Public License for more details.
15
16// You should have received a copy of the GNU General Public License
17// along with this program. If not, see <https://www.gnu.org/licenses/>.
18
19//! Gas reservation structures.
20
21use crate::{
22    ids::{MessageId, ReservationId, prelude::*},
23    message::IncomingDispatch,
24};
25use alloc::{collections::BTreeMap, format};
26use gear_core_errors::ReservationError;
27use scale_decode::DecodeAsType;
28use scale_encode::EncodeAsType;
29use scale_info::{
30    TypeInfo,
31    scale::{Decode, Encode},
32};
33
34/// An unchangeable wrapper over u64 value, which is required
35/// to be used as a "view-only" reservations nonce in a message
36/// execution context.
37///
38/// ### Note:
39/// By contract, It must be instantiated only once, when message execution
40/// context is created. Also the latter is required to be instantiated only
41/// once, when incoming dispatch is created.
42#[derive(
43    Clone,
44    Copy,
45    Default,
46    Debug,
47    Eq,
48    Hash,
49    Ord,
50    PartialEq,
51    PartialOrd,
52    Decode,
53    DecodeAsType,
54    Encode,
55    EncodeAsType,
56    TypeInfo,
57)]
58#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
59pub struct ReservationNonce(u64);
60
61impl From<&InnerNonce> for ReservationNonce {
62    fn from(nonce: &InnerNonce) -> Self {
63        ReservationNonce(nonce.0)
64    }
65}
66
67/// A changeable wrapper over u64 value, which is required
68/// to be used as an "active" reservations nonce in a gas reserver.
69#[derive(Debug, Clone, Encode, EncodeAsType, Decode, DecodeAsType, PartialEq, Eq)]
70struct InnerNonce(u64);
71
72impl InnerNonce {
73    /// Fetches current state of the nonce and
74    /// updates its state by incrementing it.
75    fn fetch_inc(&mut self) -> u64 {
76        let current = self.0;
77        self.0 = self.0.saturating_add(1);
78
79        current
80    }
81}
82
83impl From<ReservationNonce> for InnerNonce {
84    fn from(frozen_nonce: ReservationNonce) -> Self {
85        InnerNonce(frozen_nonce.0)
86    }
87}
88
89/// Gas reserver.
90///
91/// Controls gas reservations states.
92#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
93pub struct GasReserver {
94    /// Message id within which reservations are created
95    /// by the current instance of [`GasReserver`].
96    message_id: MessageId,
97    /// Nonce used to generate [`ReservationId`]s.
98    ///
99    /// It's really important that if gas reserver is created
100    /// several times with the same `message_id`, value of this
101    /// field is re-used. This property is guaranteed by instantiating
102    /// gas reserver from the [`IncomingDispatch`].
103    nonce: InnerNonce,
104    /// Gas reservations states.
105    states: GasReservationStates,
106    /// Maximum allowed reservations to be stored in `states`.
107    ///
108    /// This field is used not only to control `states` during
109    /// one execution, but also during several execution using
110    /// gas reserver for the actor. To reach that `states` must
111    /// be set with reservation from previous executions of the
112    /// actor.
113    max_reservations: u64,
114}
115
116impl GasReserver {
117    /// Creates a new gas reserver.
118    ///
119    /// `map`, which is a [`BTreeMap`] of [`GasReservationSlot`]s,
120    /// will be converted to the [`BTreeMap`] of [`GasReservationState`]s.
121    pub fn new(
122        incoming_dispatch: &IncomingDispatch,
123        map: GasReservationMap,
124        max_reservations: u64,
125    ) -> Self {
126        let message_id = incoming_dispatch.id();
127        let nonce = incoming_dispatch
128            .context()
129            .as_ref()
130            .map(|c| c.reservation_nonce())
131            .unwrap_or_default()
132            .into();
133        Self {
134            message_id,
135            nonce,
136            states: {
137                let mut states = BTreeMap::new();
138                states.extend(map.into_iter().map(|(id, slot)| (id, slot.into())));
139                states
140            },
141            max_reservations,
142        }
143    }
144
145    /// Returns bool defining if gas reserver is empty.
146    pub fn is_empty(&self) -> bool {
147        self.states.is_empty()
148    }
149
150    /// Checks that the number of existing and newly created reservations
151    /// in the `states` is less than `max_reservations`. Removed reservations,
152    /// which are stored with the [`GasReservationState::Removed`] state in the
153    /// `states`, aren't excluded from the check.
154    fn check_execution_limit(&self) -> Result<(), ReservationError> {
155        // operation might very expensive in the future
156        // so we will store 2 numerics to optimize it maybe
157        let current_reservations = self
158            .states
159            .values()
160            .map(|state| {
161                matches!(
162                    state,
163                    GasReservationState::Exists { .. } | GasReservationState::Created { .. }
164                ) as u64
165            })
166            .sum::<u64>();
167        if current_reservations > self.max_reservations {
168            Err(ReservationError::ReservationsLimitReached)
169        } else {
170            Ok(())
171        }
172    }
173
174    /// Returns amount of gas in reservation, if exists.
175    pub fn limit_of(&self, reservation_id: &ReservationId) -> Option<u64> {
176        self.states.get(reservation_id).and_then(|v| match v {
177            GasReservationState::Exists { amount, .. }
178            | GasReservationState::Created { amount, .. } => Some(*amount),
179            _ => None,
180        })
181    }
182
183    /// Reserves gas.
184    ///
185    /// Creates a new reservation and returns its id.
186    ///
187    /// Returns an error if maximum limit of reservations is reached.
188    pub fn reserve(
189        &mut self,
190        amount: u64,
191        duration: u32,
192    ) -> Result<ReservationId, ReservationError> {
193        self.check_execution_limit()?;
194
195        let id = ReservationId::generate(self.message_id, self.nonce.fetch_inc());
196
197        let maybe_reservation = self.states.insert(
198            id,
199            GasReservationState::Created {
200                amount,
201                duration,
202                used: false,
203            },
204        );
205
206        if maybe_reservation.is_some() {
207            let err_msg = format!(
208                "GasReserver::reserve: created a duplicate reservation. \
209                Message id  - {message_id}, nonce - {nonce}",
210                message_id = self.message_id,
211                nonce = self.nonce.0
212            );
213
214            log::error!("{err_msg}");
215            unreachable!("{err_msg}");
216        }
217
218        Ok(id)
219    }
220
221    /// Unreserves gas reserved within `id` reservation.
222    ///
223    /// Return error if:
224    /// 1. Reservation doesn't exist.
225    /// 2. Reservation was "unreserved", so in [`GasReservationState::Removed`] state.
226    /// 3. Reservation was marked used.
227    pub fn unreserve(
228        &mut self,
229        id: ReservationId,
230    ) -> Result<(u64, Option<UnreservedReimbursement>), ReservationError> {
231        // Docs error case #1.
232        let state = self
233            .states
234            .get(&id)
235            .ok_or(ReservationError::InvalidReservationId)?;
236
237        if matches!(
238            state,
239            // Docs error case #2.
240            GasReservationState::Removed { .. } |
241            // Docs error case #3.
242            GasReservationState::Exists { used: true, .. } |
243            GasReservationState::Created { used: true, .. }
244        ) {
245            return Err(ReservationError::InvalidReservationId);
246        }
247
248        let state = self.states.remove(&id).unwrap();
249
250        Ok(match state {
251            GasReservationState::Exists { amount, finish, .. } => {
252                self.states
253                    .insert(id, GasReservationState::Removed { expiration: finish });
254                (amount, None)
255            }
256            GasReservationState::Created {
257                amount, duration, ..
258            } => (amount, Some(UnreservedReimbursement(duration))),
259            GasReservationState::Removed { .. } => {
260                let err_msg =
261                    "GasReserver::unreserve: `Removed` variant is unreachable, checked above";
262
263                log::error!("{err_msg}");
264                unreachable!("{err_msg}")
265            }
266        })
267    }
268
269    /// Marks reservation as used.
270    ///
271    /// This allows to avoid double usage of the reservation
272    /// for sending a new message from execution of `message_id`
273    /// of current gas reserver.
274    pub fn mark_used(&mut self, id: ReservationId) -> Result<(), ReservationError> {
275        let used = self.check_not_used(id)?;
276        *used = true;
277        Ok(())
278    }
279
280    /// Check if reservation is not used.
281    ///
282    /// If reservation does not exist returns `InvalidReservationId` error.
283    pub fn check_not_used(&mut self, id: ReservationId) -> Result<&mut bool, ReservationError> {
284        if let Some(
285            GasReservationState::Created { used, .. } | GasReservationState::Exists { used, .. },
286        ) = self.states.get_mut(&id)
287        {
288            if *used {
289                Err(ReservationError::InvalidReservationId)
290            } else {
291                Ok(used)
292            }
293        } else {
294            Err(ReservationError::InvalidReservationId)
295        }
296    }
297
298    /// Returns gas reservations current nonce.
299    pub fn nonce(&self) -> ReservationNonce {
300        (&self.nonce).into()
301    }
302
303    /// Gets gas reservations states.
304    pub fn states(&self) -> &GasReservationStates {
305        &self.states
306    }
307
308    /// Converts current gas reserver into gas reservation map.
309    pub fn into_map<F>(
310        self,
311        current_block_height: u32,
312        duration_into_expiration: F,
313    ) -> GasReservationMap
314    where
315        F: Fn(u32) -> u32,
316    {
317        self.states
318            .into_iter()
319            .flat_map(|(id, state)| match state {
320                GasReservationState::Exists {
321                    amount,
322                    start,
323                    finish,
324                    ..
325                } => Some((
326                    id,
327                    GasReservationSlot {
328                        amount,
329                        start,
330                        finish,
331                    },
332                )),
333                GasReservationState::Created {
334                    amount, duration, ..
335                } => {
336                    let expiration = duration_into_expiration(duration);
337                    Some((
338                        id,
339                        GasReservationSlot {
340                            amount,
341                            start: current_block_height,
342                            finish: expiration,
343                        },
344                    ))
345                }
346                GasReservationState::Removed { .. } => None,
347            })
348            .collect()
349    }
350}
351
352/// Safety token returned when unreserved gas can be returned back to the gas counter.
353///
354/// Wraps duration for the newly created reservation.
355#[derive(Debug, PartialEq, Eq)]
356pub struct UnreservedReimbursement(u32);
357
358impl UnreservedReimbursement {
359    /// Returns duration for the newly created unreserved reservation.
360    pub fn duration(&self) -> u32 {
361        self.0
362    }
363}
364
365/// Gas reservations states.
366pub type GasReservationStates = BTreeMap<ReservationId, GasReservationState>;
367
368/// Gas reservation state.
369///
370/// Used to control whether reservation was created, removed or nothing happened.
371#[derive(Debug, Clone, Copy, Eq, PartialEq, Encode, EncodeAsType, Decode, DecodeAsType)]
372pub enum GasReservationState {
373    /// Reservation exists.
374    Exists {
375        /// Amount of reserved gas.
376        amount: u64,
377        /// Block number when reservation is created.
378        start: u32,
379        /// Block number when reservation will expire.
380        finish: u32,
381        /// Flag signalizing whether reservation is used.
382        used: bool,
383    },
384    /// Reservation will be created.
385    Created {
386        /// Amount of reserved gas.
387        amount: u64,
388        /// How many blocks reservation will live.
389        duration: u32,
390        /// Flag signalizing whether reservation is used.
391        used: bool,
392    },
393    /// Reservation will be removed.
394    Removed {
395        /// Block number when reservation will expire.
396        expiration: u32,
397    },
398}
399
400impl From<GasReservationSlot> for GasReservationState {
401    fn from(slot: GasReservationSlot) -> Self {
402        Self::Exists {
403            amount: slot.amount,
404            start: slot.start,
405            finish: slot.finish,
406            used: false,
407        }
408    }
409}
410
411/// Gas reservations map.
412///
413/// Used across execution and is stored to storage.
414pub type GasReservationMap = BTreeMap<ReservationId, GasReservationSlot>;
415
416/// Gas reservation slot.
417#[derive(Debug, Clone, Eq, PartialEq, Encode, EncodeAsType, Decode, DecodeAsType, TypeInfo)]
418pub struct GasReservationSlot {
419    /// Amount of reserved gas.
420    pub amount: u64,
421    /// Block number when reservation is created.
422    pub start: u32,
423    /// Block number when reservation will expire.
424    pub finish: u32,
425}
426
427#[cfg(test)]
428mod tests {
429    use super::*;
430
431    const MAX_RESERVATIONS: u64 = 256;
432
433    fn new_reserver() -> GasReserver {
434        let d = IncomingDispatch::default();
435        GasReserver::new(&d, Default::default(), MAX_RESERVATIONS)
436    }
437
438    #[test]
439    fn max_reservations_limit_works() {
440        let mut reserver = new_reserver();
441        for n in 0..(MAX_RESERVATIONS * 10) {
442            let res = reserver.reserve(100, 10);
443            if n > MAX_RESERVATIONS {
444                assert_eq!(res, Err(ReservationError::ReservationsLimitReached));
445            } else {
446                assert!(res.is_ok());
447            }
448        }
449    }
450
451    #[test]
452    fn mark_used_for_unreserved_fails() {
453        let mut reserver = new_reserver();
454        let id = reserver.reserve(1, 1).unwrap();
455        reserver.unreserve(id).unwrap();
456
457        assert_eq!(
458            reserver.mark_used(id),
459            Err(ReservationError::InvalidReservationId)
460        );
461    }
462
463    #[test]
464    fn mark_used_twice_fails() {
465        let mut reserver = new_reserver();
466        let id = reserver.reserve(1, 1).unwrap();
467        reserver.mark_used(id).unwrap();
468        assert_eq!(
469            reserver.mark_used(id),
470            Err(ReservationError::InvalidReservationId)
471        );
472
473        // not found
474        assert_eq!(
475            reserver.mark_used(ReservationId::default()),
476            Err(ReservationError::InvalidReservationId)
477        );
478    }
479
480    #[test]
481    fn remove_reservation_twice_fails() {
482        let mut reserver = new_reserver();
483        let id = reserver.reserve(1, 1).unwrap();
484        reserver.unreserve(id).unwrap();
485        assert_eq!(
486            reserver.unreserve(id),
487            Err(ReservationError::InvalidReservationId)
488        );
489    }
490
491    #[test]
492    fn remove_non_existing_reservation_fails() {
493        let id = ReservationId::from([0xff; 32]);
494
495        let mut map = GasReservationMap::new();
496        map.insert(
497            id,
498            GasReservationSlot {
499                amount: 1,
500                start: 1,
501                finish: 100,
502            },
503        );
504
505        let mut reserver = GasReserver::new(&Default::default(), map, 256);
506        reserver.unreserve(id).unwrap();
507
508        assert_eq!(
509            reserver.unreserve(id),
510            Err(ReservationError::InvalidReservationId)
511        );
512    }
513
514    #[test]
515    fn fresh_reserve_unreserve() {
516        let mut reserver = new_reserver();
517        let id = reserver.reserve(10_000, 5).unwrap();
518        reserver.mark_used(id).unwrap();
519        assert_eq!(
520            reserver.unreserve(id),
521            Err(ReservationError::InvalidReservationId)
522        );
523    }
524
525    #[test]
526    fn existing_reserve_unreserve() {
527        let id = ReservationId::from([0xff; 32]);
528
529        let mut map = GasReservationMap::new();
530        map.insert(
531            id,
532            GasReservationSlot {
533                amount: 1,
534                start: 1,
535                finish: 100,
536            },
537        );
538
539        let mut reserver = GasReserver::new(&Default::default(), map, 256);
540        reserver.mark_used(id).unwrap();
541        assert_eq!(
542            reserver.unreserve(id),
543            Err(ReservationError::InvalidReservationId)
544        );
545    }
546
547    #[test]
548    fn unreserving_unreserved() {
549        let id = ReservationId::from([0xff; 32]);
550        let slot = GasReservationSlot {
551            amount: 1,
552            start: 2,
553            finish: 3,
554        };
555
556        let mut map = GasReservationMap::new();
557        map.insert(id, slot.clone());
558
559        let mut reserver = GasReserver::new(&Default::default(), map, 256);
560
561        let (amount, _) = reserver.unreserve(id).expect("Shouldn't fail");
562        assert_eq!(amount, slot.amount);
563
564        assert!(reserver.unreserve(id).is_err());
565        assert_eq!(
566            reserver.states().get(&id).cloned(),
567            Some(GasReservationState::Removed {
568                expiration: slot.finish
569            })
570        );
571    }
572}