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}