gstd/
reservations.rs

1// This file is part of Gear.
2
3// Copyright (C) 2023-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//! Gear reservation manager implementation.
20//!
21//! It is capable of managing multiple gas reservations
22//! providing simple interface to user.
23
24use crate::prelude::*;
25use gcore::errors::Result;
26use parity_scale_codec::{Decode, Encode};
27
28#[cfg(not(test))]
29use crate::exec;
30#[cfg(test)]
31use tests::exec_mock as exec;
32
33#[cfg(not(test))]
34use crate::ReservationId;
35#[cfg(test)]
36use tests::ReservationIdMock as ReservationId;
37
38/// Stores additional data along with [`ReservationId`] to track its state.
39#[derive(Clone, Copy, Debug, TypeInfo, Hash, Encode, Decode)]
40pub struct Reservation {
41    id: ReservationId,
42    amount: u64,
43    valid_until: u32,
44}
45
46impl From<Reservation> for ReservationId {
47    fn from(res: Reservation) -> Self {
48        res.id
49    }
50}
51
52impl Reservation {
53    /// Reserve the `amount` of gas for further usage.
54    ///
55    /// `duration` is the block count within which the reserve must be used.
56    ///
57    /// Refer to [`ReservationId`] for the more detailed description.
58    pub fn reserve(amount: u64, duration: u32) -> Result<Self> {
59        let block_height = exec::block_height();
60
61        Ok(Self {
62            id: ReservationId::reserve(amount, duration)?,
63            amount,
64            valid_until: duration.saturating_add(block_height),
65        })
66    }
67
68    /// Unreserve unused gas from the reservation.
69    ///
70    /// If successful, it returns the reserved amount of gas.
71    pub fn unreserve(self) -> Result<u64> {
72        self.id.unreserve()
73    }
74
75    /// `ReservationId` associated with current `Reservation`.
76    pub fn id(&self) -> ReservationId {
77        self.id
78    }
79
80    /// Amount of gas stored inside this `Reservation`.
81    pub fn amount(&self) -> u64 {
82        self.amount
83    }
84
85    /// Returns block number when this `Reservation` expires.
86    pub fn valid_until(&self) -> u32 {
87        self.valid_until
88    }
89}
90
91/// Reservation manager.
92///
93/// The manager is used to control multiple gas reservations
94/// across executions. It can be used when you only care about
95/// reserved amounts and not concrete [`ReservationId`]s.
96///
97/// # Examples
98///
99/// Create gas reservations inside `init` and use them inside `handle`.
100///
101/// ```rust,ignore
102/// use gstd::{msg, prelude::*, Reservations};
103///
104/// static mut RESERVATIONS: Reservations = Reservations::new();
105///
106/// #[unsafe(no_mangle)]
107/// extern "C" fn init() {
108///     unsafe {
109///         RESERVATIONS
110///             .reserve(200_000, 50)
111///             .expect("failed to reserve gas");
112///         RESERVATIONS
113///             .reserve(100_000, 100)
114///             .expect("failed to reserve gas");
115///         RESERVATIONS
116///             .reserve(50_000, 30)
117///             .expect("failed to reserve gas");
118///     }
119/// }
120///
121/// #[unsafe(no_mangle)]
122/// extern "C" fn handle() {
123///     let reservation = unsafe { RESERVATIONS.try_take_reservation(100_000) };
124///     if let Some(reservation) = reservation {
125///         msg::send_bytes_from_reservation(
126///             reservation.id(),
127///             msg::source(),
128///             "send_bytes_from_reservation",
129///             0,
130///         )
131///         .expect("Failed to send message from reservation");
132///     } else {
133///         msg::send_bytes(msg::source(), "send_bytes", 0).expect("Failed to send message");
134///     }
135/// }
136/// ```
137///
138/// # See also
139/// - [`ReservationId`](ReservationId) is used to reserve and unreserve gas for
140///   program execution later.
141/// - [`Reservation`] stores some additional data along with `ReservationId`.
142#[derive(Default, Clone, Debug, TypeInfo, Hash, Encode, Decode)]
143pub struct Reservations(Vec<Reservation>);
144
145impl Reservations {
146    /// Create a new [`Reservations`] struct.
147    pub const fn new() -> Self {
148        Reservations(Vec::new())
149    }
150
151    /// Reserve the `amount` of gas for further usage.
152    ///
153    /// `duration` is the block count within which the reservation must be used.
154    ///
155    /// # Underlying logics
156    ///
157    /// Executes for O(logN)..O(N), where N is a number of stored
158    /// reservations.
159    ///
160    /// All the reservations are kept sorted by amount in
161    /// ascending order(when amount is the same, they're sorted by time when
162    /// they expire) when inserted, so the closer inserted element to the
163    /// beginning of the underlying `Vec` the closer execution time will be
164    /// to O(N).
165    ///
166    /// Also, when the underlying `Vec` will allocate new memory
167    /// the attempt to clean expired reservations occurs to avoid memory
168    /// allocations.
169    pub fn reserve(&mut self, amount: u64, duration: u32) -> Result<()> {
170        let new_reservation = Reservation::reserve(amount, duration)?;
171
172        let insert_range_start = self
173            .0
174            .partition_point(|reservation| reservation.amount < amount);
175        let insert_range_end = self
176            .0
177            .partition_point(|reservation| reservation.amount <= amount);
178
179        let insert_to =
180            self.0[insert_range_start..insert_range_end].binary_search_by(|reservation| {
181                reservation.valid_until.cmp(&new_reservation.valid_until)
182            });
183        let insert_to = if insert_range_start == self.0.len() {
184            self.0.len()
185        } else {
186            match insert_to {
187                Ok(pos) => pos + 1,
188                Err(pos) => pos,
189            }
190        };
191
192        // self.0 will allocate new memory.
193        if self.0.capacity() == self.0.len() {
194            self.cleanup();
195        }
196
197        self.0.insert(insert_to, new_reservation);
198
199        Ok(())
200    }
201
202    /// Find the appropriate reservation with reserved amount greater than or
203    /// equal to `amount`.
204    ///
205    /// If such a reservation is found, [`Reservation`] is returned.
206    ///
207    /// # Underlying logics
208    ///
209    /// Executes for O(logN)..O(N), where N is amount of stored
210    /// reservations. When there's many expired reservations execution time
211    /// is closer to O(N).
212    ///
213    /// All the reservations are sorted by their amount and then by the time
214    /// when they'll expire (both are in ascending order) in underlying `Vec`,
215    /// so when one's trying to take reservation, reservation with the least
216    /// possible amount is found and if it's already expired, the search to the
217    /// end of underlying `Vec` occurs. After that, all the expired
218    /// reservations that were found in process of search are cleaned out.
219    ///
220    /// # See also
221    /// - [`ReservationId`] is used to reserve and unreserve gas amount for
222    ///   program execution later.
223    /// - [`Reservation`] stores some additional data along with
224    ///   `ReservationId`.
225    pub fn try_take_reservation(&mut self, amount: u64) -> Option<Reservation> {
226        let search_from = self
227            .0
228            .partition_point(|reservation| reservation.amount < amount);
229
230        if search_from < self.0.len() {
231            let block_height = exec::block_height();
232            for i in search_from..self.0.len() {
233                if self.0[i].valid_until > block_height {
234                    // All the checked reservations are already expired at this time.
235                    let suitable = self
236                        .0
237                        .drain(search_from..=i)
238                        .next_back()
239                        .expect("At least one element in range");
240
241                    return Some(suitable);
242                }
243            }
244        }
245
246        // All the checked reservations are already expired at this time.
247        self.0.drain(search_from..);
248
249        None
250    }
251
252    /// Returns an amount of the stored reservations
253    /// that aren't expired at this time.
254    ///
255    /// Executes for O(N) where N is amount of stored reservations.
256    pub fn count_valid(&self) -> usize {
257        let block_height = exec::block_height();
258        self.0
259            .iter()
260            .filter(|reservation| reservation.valid_until > block_height)
261            .count()
262    }
263
264    /// Returns an amount of all the stored reservations (including expired).
265    pub fn count_all(&self) -> usize {
266        self.0.len()
267    }
268
269    fn cleanup(&mut self) {
270        let block_height = exec::block_height();
271
272        self.0 = self
273            .0
274            .drain(..)
275            .filter(|reservation| reservation.valid_until > block_height)
276            .collect();
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use crate::{Reservations, prelude::*};
283    use gcore::errors::Result;
284    use parity_scale_codec::{Decode, Encode};
285
286    #[must_use]
287    #[derive(Clone, Copy, Debug, TypeInfo, Hash, Encode, Decode)]
288    pub struct ReservationIdMock {
289        pub valid_until: u32,
290        pub amount: u64,
291    }
292
293    impl ReservationIdMock {
294        pub fn reserve(amount: u64, duration: u32) -> Result<ReservationIdMock> {
295            Ok(ReservationIdMock {
296                valid_until: duration + exec_mock::block_height(),
297                amount,
298            })
299        }
300
301        pub fn unreserve(self) -> Result<u64> {
302            unreachable!()
303        }
304
305        fn is_valid(&self) -> bool {
306            self.valid_until > exec_mock::block_height()
307        }
308    }
309
310    pub mod exec_mock {
311        static mut BLOCK_HEIGHT: u32 = 0;
312
313        pub fn block_height() -> u32 {
314            unsafe { BLOCK_HEIGHT }
315        }
316
317        pub(super) fn set_block_height(block_height: u32) {
318            unsafe {
319                BLOCK_HEIGHT = block_height;
320            }
321        }
322    }
323
324    #[test]
325    fn reservations_expire() -> Result<()> {
326        exec_mock::set_block_height(0);
327
328        let mut reservations = Reservations::new();
329        reservations.reserve(10_000, 5)?;
330        reservations.reserve(10_000, 10)?;
331
332        exec_mock::set_block_height(5);
333
334        assert_eq!(reservations.count_all(), 2);
335        assert_eq!(reservations.count_valid(), 1);
336
337        let reservation = reservations.try_take_reservation(10_000);
338        assert_eq!(reservation.map(|res| res.id().is_valid()), Some(true));
339
340        Ok(())
341    }
342
343    #[test]
344    fn the_best_possible_reservation_taken() -> Result<()> {
345        exec_mock::set_block_height(0);
346
347        let mut reservations = Reservations::new();
348
349        reservations.reserve(10_000, 5)?;
350        reservations.reserve(10_000, 10)?;
351        reservations.reserve(10_000, 15)?;
352        exec_mock::set_block_height(7);
353
354        let reservation = reservations.try_take_reservation(10_000);
355        // The shortest possible living reservation taken.
356        assert_eq!(reservation.map(|res| res.id().valid_until), Some(10));
357
358        let reservation = reservations.try_take_reservation(10_000);
359        assert_eq!(reservation.map(|res| res.id().valid_until), Some(15));
360
361        assert_eq!(reservations.count_valid(), 0);
362
363        reservations.reserve(10_000, 100)?;
364        reservations.reserve(20_000, 100)?;
365        reservations.reserve(30_000, 100)?;
366
367        let reservation = reservations.try_take_reservation(1_000);
368        // Reservation with the smallest amount is taken.
369        assert_eq!(reservation.map(|res| res.id.amount), Some(10_000));
370
371        let reservation = reservations.try_take_reservation(1_000);
372        // Reservation with the smallest amount is taken.
373        assert_eq!(reservation.map(|res| res.id.amount), Some(20_000));
374
375        Ok(())
376    }
377}