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_info::{
28    TypeInfo,
29    scale::{Decode, Encode},
30};
31
32/// An unchangeable wrapper over u64 value, which is required
33/// to be used as a "view-only" reservations nonce in a message
34/// execution context.
35///
36/// ### Note:
37/// By contract, It must be instantiated only once, when message execution
38/// context is created. Also the latter is required to be instantiated only
39/// once, when incoming dispatch is created.
40#[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/// A changeable wrapper over u64 value, which is required
53/// to be used as an "active" reservations nonce in a gas reserver.
54#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
55struct InnerNonce(u64);
56
57impl InnerNonce {
58    /// Fetches current state of the nonce and
59    /// updates its state by incrementing it.
60    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/// Gas reserver.
75///
76/// Controls gas reservations states.
77#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
78pub struct GasReserver {
79    /// Message id within which reservations are created
80    /// by the current instance of [`GasReserver`].
81    message_id: MessageId,
82    /// Nonce used to generate [`ReservationId`]s.
83    ///
84    /// It's really important that if gas reserver is created
85    /// several times with the same `message_id`, value of this
86    /// field is re-used. This property is guaranteed by instantiating
87    /// gas reserver from the [`IncomingDispatch`].
88    nonce: InnerNonce,
89    /// Gas reservations states.
90    states: GasReservationStates,
91    /// Maximum allowed reservations to be stored in `states`.
92    ///
93    /// This field is used not only to control `states` during
94    /// one execution, but also during several execution using
95    /// gas reserver for the actor. To reach that `states` must
96    /// be set with reservation from previous executions of the
97    /// actor.
98    max_reservations: u64,
99}
100
101impl GasReserver {
102    /// Creates a new gas reserver.
103    ///
104    /// `map`, which is a [`BTreeMap`] of [`GasReservationSlot`]s,
105    /// will be converted to the [`BTreeMap`] of [`GasReservationState`]s.
106    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    /// Returns bool defining if gas reserver is empty.
131    pub fn is_empty(&self) -> bool {
132        self.states.is_empty()
133    }
134
135    /// Checks that the number of existing and newly created reservations
136    /// in the `states` is less than `max_reservations`. Removed reservations,
137    /// which are stored with the [`GasReservationState::Removed`] state in the
138    /// `states`, aren't excluded from the check.
139    fn check_execution_limit(&self) -> Result<(), ReservationError> {
140        // operation might very expensive in the future
141        // so we will store 2 numerics to optimize it maybe
142        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    /// Returns amount of gas in reservation, if exists.
160    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    /// Reserves gas.
169    ///
170    /// Creates a new reservation and returns its id.
171    ///
172    /// Returns an error if maximum limit of reservations is reached.
173    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    /// Unreserves gas reserved within `id` reservation.
207    ///
208    /// Return error if:
209    /// 1. Reservation doesn't exist.
210    /// 2. Reservation was "unreserved", so in [`GasReservationState::Removed`] state.
211    /// 3. Reservation was marked used.
212    pub fn unreserve(
213        &mut self,
214        id: ReservationId,
215    ) -> Result<(u64, Option<UnreservedReimbursement>), ReservationError> {
216        // Docs error case #1.
217        let state = self
218            .states
219            .get(&id)
220            .ok_or(ReservationError::InvalidReservationId)?;
221
222        if matches!(
223            state,
224            // Docs error case #2.
225            GasReservationState::Removed { .. } |
226            // Docs error case #3.
227            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    /// Marks reservation as used.
255    ///
256    /// This allows to avoid double usage of the reservation
257    /// for sending a new message from execution of `message_id`
258    /// of current gas reserver.
259    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    /// Check if reservation is not used.
266    ///
267    /// If reservation does not exist returns `InvalidReservationId` error.
268    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    /// Returns gas reservations current nonce.
284    pub fn nonce(&self) -> ReservationNonce {
285        (&self.nonce).into()
286    }
287
288    /// Gets gas reservations states.
289    pub fn states(&self) -> &GasReservationStates {
290        &self.states
291    }
292
293    /// Converts current gas reserver into gas reservation map.
294    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/// Safety token returned when unreserved gas can be returned back to the gas counter.
338///
339/// Wraps duration for the newly created reservation.
340#[derive(Debug, PartialEq, Eq)]
341pub struct UnreservedReimbursement(u32);
342
343impl UnreservedReimbursement {
344    /// Returns duration for the newly created unreserved reservation.
345    pub fn duration(&self) -> u32 {
346        self.0
347    }
348}
349
350/// Gas reservations states.
351pub type GasReservationStates = BTreeMap<ReservationId, GasReservationState>;
352
353/// Gas reservation state.
354///
355/// Used to control whether reservation was created, removed or nothing happened.
356#[derive(Debug, Clone, Copy, Eq, PartialEq, Encode, Decode)]
357pub enum GasReservationState {
358    /// Reservation exists.
359    Exists {
360        /// Amount of reserved gas.
361        amount: u64,
362        /// Block number when reservation is created.
363        start: u32,
364        /// Block number when reservation will expire.
365        finish: u32,
366        /// Flag signalizing whether reservation is used.
367        used: bool,
368    },
369    /// Reservation will be created.
370    Created {
371        /// Amount of reserved gas.
372        amount: u64,
373        /// How many blocks reservation will live.
374        duration: u32,
375        /// Flag signalizing whether reservation is used.
376        used: bool,
377    },
378    /// Reservation will be removed.
379    Removed {
380        /// Block number when reservation will expire.
381        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
396/// Gas reservations map.
397///
398/// Used across execution and is stored to storage.
399pub type GasReservationMap = BTreeMap<ReservationId, GasReservationSlot>;
400
401/// Gas reservation slot.
402#[derive(Debug, Clone, Eq, PartialEq, Encode, Decode, TypeInfo)]
403pub struct GasReservationSlot {
404    /// Amount of reserved gas.
405    pub amount: u64,
406    /// Block number when reservation is created.
407    pub start: u32,
408    /// Block number when reservation will expire.
409    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        // not found
459        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}