Skip to main content

thruput_sdk/storage/
capacity_constraint.rs

1use super::keypath::{Keypath, LeafKey};
2use crate::exn::LibraryExn;
3use crate::re_throw::OrReThrow;
4use crate::sys_exn::nnint_internal as nnint;
5
6/// Implements a linear constraint on some set of (contract-defined) state.
7/// Specifically, this implements constraints of the form `\Sum_i a_i x_i <= b`.
8///
9/// The constraint is guaranteed to hold at the state snapshot taken at the
10/// end of each block. Contracts express not their state variables directly,
11/// but the changes in state variables that a transaction intends to make.
12///
13/// Some kinds of constraints can be expressed directly as checks within one
14/// transaction. This data type is not for those kinds of constraints.
15/// Rather, this type is how a contract can tell the Thruput runtime about
16/// constraints that must hold across transactions in a block. For example,
17/// an aggregate velocity limit on a user's token balance cannot be checked
18/// within a single transaction, but can be enforced by the Thruput runtime
19/// when packing new blocks of transactions.
20///
21/// The storage cell tracks the *remaining capacity* (`b - \Sum_i a_i x_i`).
22/// The weighted arithmetic is performed by the caller: pass
23/// [`consume_capacity`](Self::consume_capacity) the sum (for a given
24/// transaction) of `a_i * \delta_i`.
25///
26/// Callers can reset the constraint via
27/// [`set_capacity`](Self::set_capacity), which must be the first write to
28/// the cell in its transaction (or an idempotent repeat of the value
29/// already set this transaction).
30///
31/// # Writeback
32///
33/// Changes are staged on the handle and flushed to storage when it drops,
34/// *merging* with whatever earlier handles in the same transaction recorded.
35/// A handle that only read is not flushed at all: read-only access neither
36/// writes storage nor counts as a prior write for
37/// [`set_capacity`](Self::set_capacity). To catch a flush error instead of
38/// aborting in drop, flush explicitly with [`try_flush`](Self::try_flush).
39pub struct CapacityConstraint {
40    key: LeafKey,
41    capacity: Option<i128>,
42    delta: i128,
43    _live: crate::storage::live_guards::LiveGuard,
44}
45
46impl From<Keypath> for CapacityConstraint {
47    fn from(keypath: Keypath) -> Self {
48        Self {
49            key: LeafKey::from(keypath),
50            capacity: None,
51            delta: 0,
52            _live: crate::storage::live_guards::LiveGuard::new(),
53        }
54    }
55}
56
57impl CapacityConstraint {
58    /// Get the remaining capacity: the capacity at the last
59    /// [`set_capacity`](Self::set_capacity) (in this transaction, or
60    /// committed by a previous one), plus all consumption recorded this
61    /// transaction, including this handle's unflushed changes.
62    ///
63    /// If the storage location has never been written, the capacity
64    /// baseline is 0.
65    ///
66    /// Warning: this might return a negative value, if the constraint has
67    /// been violated within the current transaction. A transaction cannot
68    /// successfully commit in such a state. However, to allow for
69    /// flexibility in contract implementations (i.e. looping over
70    /// [`consume_capacity`](Self::consume_capacity) in a transaction), we
71    /// allow a contract to optionally temporarily violate its constraint,
72    /// on the knowledge that either the transaction will fail or the
73    /// contract will repair the constraint before terminating.
74    pub fn try_get_remaining_capacity(
75        &self,
76    ) -> Result<i128, crate::exn::LibraryExn> {
77        let (s, a) = match nnint::try_get_values_force(self.key.get()) {
78            Ok(values) => values,
79            Err(e) if e.is_storage_missing() => (0, 0),
80            Err(e) => return Err(e),
81        };
82        self.capacity
83            .unwrap_or(s)
84            .checked_add(a)
85            .and_then(|r| r.checked_add(self.delta))
86            .ok_or(LibraryExn::NonNegativeIntOverflow)
87    }
88
89    /// Like
90    /// [`try_get_remaining_capacity`](Self::try_get_remaining_capacity),
91    /// but aborts on failure instead of reporting it (the `try_` law; see
92    /// the [`exn`](mod@crate::exn) module docs).
93    pub fn get_remaining_capacity(&self) -> i128 {
94        self.try_get_remaining_capacity().or_re_throw()
95    }
96
97    /// Consume `amount` units of remaining capacity (without querying the
98    /// current value). A negative `amount` releases capacity back.
99    ///
100    /// The constraint is enforced at *commit*, not here: consumption is
101    /// staged on this handle, flushed to storage when it drops, and checked
102    /// by the runtime when the block is packed. Throws only on arithmetic
103    /// overflow of the staged consumption.
104    pub fn consume_capacity(&mut self, amount: i128) {
105        self.try_consume_capacity(amount).or_re_throw();
106    }
107
108    /// Like [`consume_capacity`](Self::consume_capacity), but report
109    /// overflow as `Err` instead of aborting (the `try_` law; see the
110    /// [`exn`](mod@crate::exn) module docs).
111    pub fn try_consume_capacity(
112        &mut self,
113        amount: i128,
114    ) -> Result<(), crate::exn::LibraryExn> {
115        self.delta = self.delta.checked_sub(amount).ok_or(if amount > 0 {
116            LibraryExn::NonNegativeIntUnderflow
117        } else {
118            LibraryExn::NonNegativeIntOverflow
119        })?;
120        Ok(())
121    }
122
123    /// Set the remaining capacity.
124    ///
125    /// This must be the first write to the constraint in its transaction,
126    /// or an idempotent repeat of the capacity already set this
127    /// transaction; see [`try_set_capacity`](Self::try_set_capacity).
128    /// Aborts otherwise.
129    pub fn set_capacity(&mut self, capacity: i128) {
130        self.try_set_capacity(capacity).or_re_throw();
131    }
132
133    /// Like [`set_capacity`](Self::set_capacity), but report failure as
134    /// `Err` instead of aborting (the `try_` law; see the
135    /// [`exn`](mod@crate::exn) module docs).
136    ///
137    /// Setting the capacity must be the first write to the constraint in
138    /// its transaction, with one exception: repeating the value already
139    /// set this transaction (by this handle or one already dropped) is an
140    /// idempotent no-op that preserves recorded consumption. This mirrors
141    /// the runtime's cross-transaction rule — concurrent transactions
142    /// setting the same value may share a block — so same-value sets
143    /// commute at every scope. Any other prior write reports
144    /// [`LibraryExn::CapacityConstraintAlreadyWritten`]: a different
145    /// capacity value, or consumption recorded before any set. This
146    /// prevents a capacity reset from silently discarding consumption.
147    /// The check consults the pending storage state, not just this handle,
148    /// so writes flushed by already-dropped handles are seen.
149    pub fn try_set_capacity(
150        &mut self,
151        capacity: i128,
152    ) -> Result<(), crate::exn::LibraryExn> {
153        if let Some(staged) = self.capacity {
154            return if staged == capacity {
155                Ok(())
156            } else {
157                Err(LibraryExn::CapacityConstraintAlreadyWritten)
158            };
159        }
160        let (s, a) = nnint::try_get_values(self.key.get())?;
161        if let Some(pending) = s {
162            return if pending == capacity {
163                Ok(())
164            } else {
165                Err(LibraryExn::CapacityConstraintAlreadyWritten)
166            };
167        }
168        // No set anywhere this transaction: consumption must not have been
169        // recorded either, or the reset would discard it.
170        if self.delta != 0 || a != 0 {
171            return Err(LibraryExn::CapacityConstraintAlreadyWritten);
172        }
173        self.capacity = Some(capacity);
174        Ok(())
175    }
176
177    /// Merge this handle's staged changes into the cell's pending values
178    /// now instead of waiting for drop.
179    ///
180    /// A clean handle (nothing staged since construction or the last flush)
181    /// is a no-op. A successful flush resets the staged delta to zero, so
182    /// the handle remains usable: consumption staged by later
183    /// [`consume_capacity`](Self::consume_capacity) calls is merged on top
184    /// of the flushed batch by drop (or the next flush) — consume, flush,
185    /// consume accumulates both consumptions.
186    pub fn flush(&mut self) {
187        self.try_flush().or_re_throw()
188    }
189
190    /// Like [`flush`](Self::flush), but report failure as `Err` instead of
191    /// aborting (the `try_` law; see the [`exn`](mod@crate::exn) module
192    /// docs) — in particular, the only way to catch an error the drop-time
193    /// flush would abort with. On `Err` the staged changes are retained:
194    /// drop re-attempts the flush and aborts if it still fails, so a caught
195    /// error cannot silently lose the write.
196    pub fn try_flush(&mut self) -> Result<(), crate::exn::LibraryExn> {
197        if self.capacity.is_none() && self.delta == 0 {
198            return Ok(());
199        }
200        let key = self.key.get();
201        let (s, a) = nnint::try_get_values(key)?;
202        let add_value = a
203            .checked_add(self.delta)
204            .ok_or(LibraryExn::NonNegativeIntOverflow)?;
205        nnint::try_set_add(
206            key,
207            self.capacity.or(s),
208            add_value,
209            self.key.is_exclusive(),
210        )?;
211        self.capacity = None;
212        self.delta = 0;
213        Ok(())
214    }
215}
216
217/// Lazy writeback to storage: staged changes are merged into the cell's
218/// pending values when the handle drops. An untouched handle skips the
219/// syscall entirely, so read-only access leaves no trace.
220impl Drop for CapacityConstraint {
221    fn drop(&mut self) {
222        self.try_flush().or_re_throw();
223        // live guard drops here
224    }
225}
226
227impl super::sealed::Sealed for CapacityConstraint {}
228impl super::node::StorageType for CapacityConstraint {}
229
230impl super::deletable::Deletable for CapacityConstraint {
231    fn delete_internal(&mut self, exclusive: bool) {
232        // Discard staged changes: they must not be flushed on top of the
233        // delete when this handle drops.
234        self.capacity = None;
235        self.delta = 0;
236        let exclusive = exclusive || self.key.is_exclusive();
237        crate::sys_exn::delete::delete(self.key.get(), exclusive);
238    }
239}