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}