soroban_env_host/
fees.rs

1/// This module defines the fee computation protocol for Soroban.
2///
3/// This is technically not part of the Soroban host and is provided here for
4/// the sake of sharing between the systems that run Soroban host (such as
5/// Stellar core or Soroban RPC service).
6///
7/// Rough estimate of the base size of any transaction result in the archives
8/// (independent of the transaction envelope size).
9pub const TX_BASE_RESULT_SIZE: u32 = 300;
10/// Estimate for any `TtlEntry` ledger entry
11pub const TTL_ENTRY_SIZE: u32 = 48;
12
13pub const INSTRUCTIONS_INCREMENT: i64 = 10000;
14pub const DATA_SIZE_1KB_INCREMENT: i64 = 1024;
15
16// minimum effective write fee per 1KB
17pub const MINIMUM_WRITE_FEE_PER_1KB: i64 = 1000;
18
19/// These are the resource upper bounds specified by the Soroban transaction.
20pub struct TransactionResources {
21    /// Number of CPU instructions.
22    pub instructions: u32,
23    /// Number of ledger entries the transaction reads.
24    pub read_entries: u32,
25    /// Number of ledger entries the transaction writes (these are also counted
26    /// as entries that are being read for the sake of the respective fees).
27    pub write_entries: u32,
28    /// Number of bytes read from ledger.
29    pub read_bytes: u32,
30    /// Number of bytes written to ledger.
31    pub write_bytes: u32,
32    /// Size of the contract events XDR.
33    pub contract_events_size_bytes: u32,
34    /// Size of the transaction XDR.
35    pub transaction_size_bytes: u32,
36}
37
38/// Fee-related network configuration.
39///
40/// This should be normally loaded from the ledger, with exception of the
41/// `fee_per_write_1kb`, that has to be computed via `compute_write_fee_per_1kb`
42/// function.
43
44#[derive(Debug, Default, PartialEq, Eq)]
45pub struct FeeConfiguration {
46    /// Fee per `INSTRUCTIONS_INCREMENT=10000` instructions.
47    pub fee_per_instruction_increment: i64,
48    /// Fee per 1 entry read from ledger.
49    pub fee_per_read_entry: i64,
50    /// Fee per 1 entry written to ledger.
51    pub fee_per_write_entry: i64,
52    /// Fee per 1KB read from ledger.
53    pub fee_per_read_1kb: i64,
54    /// Fee per 1KB written to ledger. This has to be computed via
55    /// `compute_write_fee_per_1kb`.
56    pub fee_per_write_1kb: i64,
57    /// Fee per 1KB written to history (the history write size is based on
58    /// transaction size and `TX_BASE_RESULT_SIZE`).
59    pub fee_per_historical_1kb: i64,
60    /// Fee per 1KB of contract events written.
61    pub fee_per_contract_event_1kb: i64,
62    /// Fee per 1KB of transaction size.
63    pub fee_per_transaction_size_1kb: i64,
64}
65
66/// Network configuration used to determine the ledger write fee.
67///
68/// This should be normally loaded from the ledger.
69#[derive(Debug, Default, PartialEq, Eq)]
70pub struct WriteFeeConfiguration {
71    // Write fee grows linearly until bucket list reaches this size.
72    pub bucket_list_target_size_bytes: i64,
73    // Fee per 1KB write when the bucket list is empty.
74    pub write_fee_1kb_bucket_list_low: i64,
75    // Fee per 1KB write when the bucket list has reached
76    // `bucket_list_target_size_bytes`.
77    pub write_fee_1kb_bucket_list_high: i64,
78    // Write fee multiplier for any additional data past the first
79    // `bucket_list_target_size_bytes`.
80    pub bucket_list_write_fee_growth_factor: u32,
81}
82
83/// Change in a single ledger entry with parameters relevant for rent fee
84/// computations.
85///
86/// This represents the entry state before and after transaction has been
87/// applied.
88pub struct LedgerEntryRentChange {
89    /// Whether this is persistent or temporary entry.
90    pub is_persistent: bool,
91    /// Size of the entry in bytes before it has been modified, including the
92    /// key.
93    /// `0` for newly-created entires.
94    pub old_size_bytes: u32,
95    /// Size of the entry in bytes after it has been modified, including the
96    /// key.
97    pub new_size_bytes: u32,
98    /// Live until ledger of the entry before it has been modified.
99    /// Should be less than the current ledger for newly-created entires.
100    pub old_live_until_ledger: u32,
101    /// Live until ledger of the entry after it has been modified.
102    pub new_live_until_ledger: u32,
103}
104
105/// Rent fee-related network configuration.
106///
107/// This should be normally loaded from the ledger, with exception of the
108/// `fee_per_write_1kb`, that has to be computed via `compute_write_fee_per_1kb`
109/// function.
110
111#[derive(Debug, Default, PartialEq, Eq)]
112pub struct RentFeeConfiguration {
113    /// Fee per 1KB written to ledger.
114    /// This is the same field as in `FeeConfiguration` and it has to be
115    /// computed via `compute_write_fee_per_1kb`.
116    pub fee_per_write_1kb: i64,
117    /// Fee per 1 entry written to ledger.
118    /// This is the same field as in `FeeConfiguration`.
119    pub fee_per_write_entry: i64,
120    /// Denominator for the total rent fee for persistent storage.
121    ///
122    /// This can be thought of as the number of ledgers of rent that costs as
123    /// much, as writing the entry for the first time (i.e. if the value is
124    /// `1000`, then we would charge the entry write fee for every 1000 ledgers
125    /// of rent).
126    pub persistent_rent_rate_denominator: i64,
127    /// Denominator for the total rent fee for temporary storage.
128    ///
129    /// This has the same semantics as `persistent_rent_rate_denominator`.
130    pub temporary_rent_rate_denominator: i64,
131}
132
133/// Computes the resource fee for a transaction based on the resource
134/// consumption and the fee-related network configuration.
135///
136/// This can handle unsantized user inputs for `tx_resources`, but expects
137/// sane configuration.
138///
139/// Returns a pair of `(non_refundable_fee, refundable_fee)` that represent
140/// non-refundable and refundable resource fee components respectively.
141pub fn compute_transaction_resource_fee(
142    tx_resources: &TransactionResources,
143    fee_config: &FeeConfiguration,
144) -> (i64, i64) {
145    let compute_fee = compute_fee_per_increment(
146        tx_resources.instructions,
147        fee_config.fee_per_instruction_increment,
148        INSTRUCTIONS_INCREMENT,
149    );
150    let ledger_read_entry_fee: i64 = fee_config.fee_per_read_entry.saturating_mul(
151        tx_resources
152            .read_entries
153            .saturating_add(tx_resources.write_entries)
154            .into(),
155    );
156    let ledger_write_entry_fee = fee_config
157        .fee_per_write_entry
158        .saturating_mul(tx_resources.write_entries.into());
159    let ledger_read_bytes_fee = compute_fee_per_increment(
160        tx_resources.read_bytes,
161        fee_config.fee_per_read_1kb,
162        DATA_SIZE_1KB_INCREMENT,
163    );
164    let ledger_write_bytes_fee = compute_fee_per_increment(
165        tx_resources.write_bytes,
166        fee_config.fee_per_write_1kb,
167        DATA_SIZE_1KB_INCREMENT,
168    );
169
170    let historical_fee = compute_fee_per_increment(
171        tx_resources
172            .transaction_size_bytes
173            .saturating_add(TX_BASE_RESULT_SIZE),
174        fee_config.fee_per_historical_1kb,
175        DATA_SIZE_1KB_INCREMENT,
176    );
177
178    let events_fee = compute_fee_per_increment(
179        tx_resources.contract_events_size_bytes,
180        fee_config.fee_per_contract_event_1kb,
181        DATA_SIZE_1KB_INCREMENT,
182    );
183
184    let bandwidth_fee = compute_fee_per_increment(
185        tx_resources.transaction_size_bytes,
186        fee_config.fee_per_transaction_size_1kb,
187        DATA_SIZE_1KB_INCREMENT,
188    );
189
190    let refundable_fee = events_fee;
191    let non_refundable_fee = compute_fee
192        .saturating_add(ledger_read_entry_fee)
193        .saturating_add(ledger_write_entry_fee)
194        .saturating_add(ledger_read_bytes_fee)
195        .saturating_add(ledger_write_bytes_fee)
196        .saturating_add(historical_fee)
197        .saturating_add(bandwidth_fee);
198
199    (non_refundable_fee, refundable_fee)
200}
201
202// Helper for clamping values to the range of positive i64, with
203// invalid cases mapped to i64::MAX.
204trait ClampFee {
205    fn clamp_fee(self) -> i64;
206}
207
208impl ClampFee for i64 {
209    fn clamp_fee(self) -> i64 {
210        if self < 0 {
211            // Negatives shouldn't be possible -- they're banned in the logic
212            // that sets most of the configs, and we're only using i64 for XDR
213            // sake, ultimately I think compatibility with java which only has
214            // signed types -- anyway we're assuming i64::MAX is more likely the
215            // safest in-band default-value for erroneous cses, since it's more
216            // likely to fail a tx, than to open a "0 cost tx" DoS vector.
217            i64::MAX
218        } else {
219            self
220        }
221    }
222}
223
224impl ClampFee for i128 {
225    fn clamp_fee(self) -> i64 {
226        if self < 0 {
227            i64::MAX
228        } else {
229            i64::try_from(self).unwrap_or(i64::MAX)
230        }
231    }
232}
233
234/// Computes the effective write fee per 1 KB of data written to ledger.
235///
236/// The computed fee should be used in fee configuration for
237/// `compute_transaction_resource_fee` function.
238///
239/// This depends only on the current ledger (more specifically, bucket list)
240/// size.
241pub fn compute_write_fee_per_1kb(
242    bucket_list_size_bytes: i64,
243    fee_config: &WriteFeeConfiguration,
244) -> i64 {
245    let fee_rate_multiplier = fee_config
246        .write_fee_1kb_bucket_list_high
247        .saturating_sub(fee_config.write_fee_1kb_bucket_list_low)
248        .clamp_fee();
249    let mut write_fee_per_1kb: i64;
250    if bucket_list_size_bytes < fee_config.bucket_list_target_size_bytes {
251        // Convert multipliers to i128 to make sure we can handle large bucket list
252        // sizes.
253        write_fee_per_1kb = num_integer::div_ceil(
254            (fee_rate_multiplier as i128).saturating_mul(bucket_list_size_bytes as i128),
255            (fee_config.bucket_list_target_size_bytes as i128).max(1),
256        )
257        .clamp_fee();
258        // no clamp_fee here
259        write_fee_per_1kb =
260            write_fee_per_1kb.saturating_add(fee_config.write_fee_1kb_bucket_list_low);
261    } else {
262        write_fee_per_1kb = fee_config.write_fee_1kb_bucket_list_high;
263        let bucket_list_size_after_reaching_target =
264            bucket_list_size_bytes.saturating_sub(fee_config.bucket_list_target_size_bytes);
265        let post_target_fee = num_integer::div_ceil(
266            (fee_rate_multiplier as i128)
267                .saturating_mul(bucket_list_size_after_reaching_target as i128)
268                .saturating_mul(fee_config.bucket_list_write_fee_growth_factor as i128),
269            (fee_config.bucket_list_target_size_bytes as i128).max(1),
270        )
271        .clamp_fee();
272        write_fee_per_1kb = write_fee_per_1kb.saturating_add(post_target_fee);
273    }
274
275    write_fee_per_1kb.max(MINIMUM_WRITE_FEE_PER_1KB)
276}
277
278/// Computes the total rent-related fee for the provided ledger entry changes.
279///
280/// The rent-related fees consist of the fees for TTL extensions and fees for
281/// increasing the entry size (with or without TTL extensions).
282///
283/// This cannot handle unsantized inputs and relies on sane configuration and
284/// ledger changes. This is due to the fact that rent is managed automatically
285/// wihtout user-provided inputs.
286pub fn compute_rent_fee(
287    changed_entries: &[LedgerEntryRentChange],
288    fee_config: &RentFeeConfiguration,
289    current_ledger_seq: u32,
290) -> i64 {
291    let mut fee: i64 = 0;
292    let mut extended_entries: i64 = 0;
293    let mut extended_entry_key_size_bytes: u32 = 0;
294    for e in changed_entries {
295        fee = fee.saturating_add(rent_fee_per_entry_change(e, fee_config, current_ledger_seq));
296        if e.old_live_until_ledger < e.new_live_until_ledger {
297            extended_entries = extended_entries.saturating_add(1);
298            extended_entry_key_size_bytes =
299                extended_entry_key_size_bytes.saturating_add(TTL_ENTRY_SIZE);
300        }
301    }
302    // The TTL extensions need to be written to the ledger. As they have
303    // constant size, we can charge for writing them independently of the actual
304    // entry size.
305    fee = fee.saturating_add(
306        fee_config
307            .fee_per_write_entry
308            .saturating_mul(extended_entries),
309    );
310    fee = fee.saturating_add(compute_fee_per_increment(
311        extended_entry_key_size_bytes,
312        fee_config.fee_per_write_1kb,
313        DATA_SIZE_1KB_INCREMENT,
314    ));
315
316    fee
317}
318
319// Size of half-open range (lo, hi], or None if lo>hi
320fn exclusive_ledger_diff(lo: u32, hi: u32) -> Option<u32> {
321    hi.checked_sub(lo)
322}
323
324// Size of closed range [lo, hi] or None if lo>hi
325fn inclusive_ledger_diff(lo: u32, hi: u32) -> Option<u32> {
326    exclusive_ledger_diff(lo, hi).map(|diff| diff.saturating_add(1))
327}
328
329impl LedgerEntryRentChange {
330    fn entry_is_new(&self) -> bool {
331        self.old_size_bytes == 0 && self.old_live_until_ledger == 0
332    }
333
334    fn extension_ledgers(&self, current_ledger: u32) -> Option<u32> {
335        let ledger_before_extension = if self.entry_is_new() {
336            current_ledger.saturating_sub(1)
337        } else {
338            self.old_live_until_ledger
339        };
340        exclusive_ledger_diff(ledger_before_extension, self.new_live_until_ledger)
341    }
342
343    fn prepaid_ledgers(&self, current_ledger: u32) -> Option<u32> {
344        if self.entry_is_new() {
345            None
346        } else {
347            inclusive_ledger_diff(current_ledger, self.old_live_until_ledger)
348        }
349    }
350
351    fn size_increase(&self) -> Option<u32> {
352        self.new_size_bytes.checked_sub(self.old_size_bytes)
353    }
354}
355
356fn rent_fee_per_entry_change(
357    entry_change: &LedgerEntryRentChange,
358    fee_config: &RentFeeConfiguration,
359    current_ledger: u32,
360) -> i64 {
361    let mut fee: i64 = 0;
362    // If there was a difference-in-expiration, pay for the new ledger range
363    // at the new size.
364    if let Some(rent_ledgers) = entry_change.extension_ledgers(current_ledger) {
365        fee = fee.saturating_add(rent_fee_for_size_and_ledgers(
366            entry_change.is_persistent,
367            entry_change.new_size_bytes,
368            rent_ledgers,
369            fee_config,
370        ));
371    }
372
373    // If there were some ledgers already paid for at an old size, and the size
374    // of the entry increased, those pre-paid ledgers need to pay top-up fees to
375    // account for the change in size.
376    if let (Some(rent_ledgers), Some(entry_size)) = (
377        entry_change.prepaid_ledgers(current_ledger),
378        entry_change.size_increase(),
379    ) {
380        fee = fee.saturating_add(rent_fee_for_size_and_ledgers(
381            entry_change.is_persistent,
382            entry_size,
383            rent_ledgers,
384            fee_config,
385        ));
386    }
387    fee
388}
389
390fn rent_fee_for_size_and_ledgers(
391    is_persistent: bool,
392    entry_size: u32,
393    rent_ledgers: u32,
394    fee_config: &RentFeeConfiguration,
395) -> i64 {
396    // Multiplication can overflow here - unlike fee computation this can rely
397    // on sane input parameters as rent fee computation does not depend on any
398    // user inputs.
399    let num = (entry_size as i64)
400        .saturating_mul(fee_config.fee_per_write_1kb)
401        .saturating_mul(rent_ledgers as i64);
402    let storage_coef = if is_persistent {
403        fee_config.persistent_rent_rate_denominator
404    } else {
405        fee_config.temporary_rent_rate_denominator
406    };
407    let denom = DATA_SIZE_1KB_INCREMENT.saturating_mul(storage_coef);
408    num_integer::div_ceil(num, denom.max(1))
409}
410
411fn compute_fee_per_increment(resource_value: u32, fee_rate: i64, increment: i64) -> i64 {
412    let resource_val: i64 = resource_value.into();
413    num_integer::div_ceil(resource_val.saturating_mul(fee_rate), increment.max(1))
414}