Repository: stellar/rs-soroban-env
Pull request #1010 (2023-08-21 22:49:51)

Charge write fees for expiration entry bumps.
### What

Charge write fees for expiration entry bumps.

We charge these as a part of the rent fee, so it's effectively an additional flat component of the final rent cost. It's not going to be explicitly exposed or limited.

### Why

Rent bumps are not completely free, so fee model needs to reflect that. Also this discourages doing extremely short-termed bumps and thus should reduce the ledger load a bit.

### Known limitations

N/A



Patch:
diff --git a/soroban-env-host/src/fees.rs b/soroban-env-host/src/fees.rs
index 5ebcd41e..88366d13 100644
--- a/soroban-env-host/src/fees.rs
+++ b/soroban-env-host/src/fees.rs
@@ -3,9 +3,16 @@
 /// This is technically not part of the Soroban host and is provided here for
 /// the sake of sharing between the systems that run Soroban host (such as
 /// Stellar core or Soroban RPC service).
+
+/// Rough estimate of the base size of any transaction result in the archives
+/// (independent of the transaction envelope size).
+pub const TX_BASE_RESULT_SIZE: u32 = 300;
+/// Estimate for any `ExpirationEntry` ledger entry, consisting of a 32-byte
+/// key and entry with key and 4 bytes for expiration ledger.
+pub const EXPIRATION_ENTRY_SIZE: u32 = 2 * 32 + 4;
+
 const INSTRUCTIONS_INCREMENT: i64 = 10000;
 const DATA_SIZE_1KB_INCREMENT: i64 = 1024;
-const TX_BASE_RESULT_SIZE: u32 = 300;
 
 /// These are the resource upper bounds specified by the Soroban transaction.
 pub struct TransactionResources {
@@ -76,10 +83,12 @@ pub struct WriteFeeConfiguration {
 pub struct LedgerEntryRentChange {
     /// Whether this is persistent or temporary entry.
     pub is_persistent: bool,
-    /// Size of the entry in bytes before it has been modified.
+    /// Size of the entry in bytes before it has been modified, including the
+    /// key.
     /// `0` for newly-created entires.
     pub old_size_bytes: u32,
-    /// Size of the entry in bytes after it has been modified.
+    /// Size of the entry in bytes after it has been modified, including the
+    /// key.
     pub new_size_bytes: u32,
     /// Expiration ledger of the entry before it has been modified.
     /// Should be less than the current ledger for newly-created entires.
@@ -98,6 +107,9 @@ pub struct RentFeeConfiguration {
     /// This is the same field as in `FeeConfiguration` and it has to be
     /// computed via `compute_write_fee_per_1kb`.
     pub fee_per_write_1kb: i64,
+    /// Fee per 1 entry written to ledger.
+    /// This is the same field as in `FeeConfiguration`.
+    pub fee_per_write_entry: i64,
     /// Denominator for the total rent fee for persistent storage.
     ///
     /// This can be thought of as the number of ledgers of rent that costs as
@@ -233,10 +245,31 @@ pub fn compute_rent_fee(
     fee_config: &RentFeeConfiguration,
     current_ledger_seq: u32,
 ) -> i64 {
-    let mut fee = 0;
+    let mut fee: i64 = 0;
+    let mut bumped_entries: i64 = 0;
+    let mut bumped_entry_key_size_bytes: u32 = 0;
     for e in changed_entries {
-        fee += rent_fee_per_entry_change(e, fee_config, current_ledger_seq);
+        fee = fee.saturating_add(rent_fee_per_entry_change(e, fee_config, current_ledger_seq));
+        if e.old_expiration_ledger < e.new_expiration_ledger {
+            bumped_entries = bumped_entries.saturating_add(1);
+            bumped_entry_key_size_bytes =
+                bumped_entry_key_size_bytes.saturating_add(EXPIRATION_ENTRY_SIZE);
+        }
     }
+    // The expiration bumps need to be written to the ledger. As they have
+    // constant size, we can charge for writing them independently of the actual
+    // entry size.
+    fee = fee.saturating_add(
+        fee_config
+            .fee_per_write_entry
+            .saturating_mul(bumped_entries),
+    );
+    fee = fee.saturating_add(compute_fee_per_increment(
+        bumped_entry_key_size_bytes,
+        fee_config.fee_per_write_1kb,
+        DATA_SIZE_1KB_INCREMENT,
+    ));
+
     fee
 }
 
@@ -245,10 +278,10 @@ fn rent_fee_per_entry_change(
     fee_config: &RentFeeConfiguration,
     current_ledger: u32,
 ) -> i64 {
-    let mut fee = 0;
+    let mut fee: i64 = 0;
     // Pay for the rent extension (if any).
     if entry_change.new_expiration_ledger > entry_change.old_expiration_ledger {
-        fee += rent_fee_for_size_and_ledgers(
+        fee = fee.saturating_add(rent_fee_for_size_and_ledgers(
             entry_change.is_persistent,
             // New portion of rent is payed for the new size of the entry.
             entry_change.new_size_bytes,
@@ -258,12 +291,12 @@ fn rent_fee_per_entry_change(
             entry_change.new_expiration_ledger
                 - entry_change.old_expiration_ledger.max(current_ledger - 1),
             fee_config,
-        );
+        ));
     }
     // Pay for the entry size increase (if any).
     if entry_change.new_size_bytes > entry_change.old_size_bytes && entry_change.old_size_bytes > 0
     {
-        fee += rent_fee_for_size_and_ledgers(
+        fee = fee.saturating_add(rent_fee_for_size_and_ledgers(
             entry_change.is_persistent,
             // Pay only for the size increase.
             entry_change.new_size_bytes - entry_change.old_size_bytes,
@@ -271,7 +304,7 @@ fn rent_fee_per_entry_change(
             // covered above for the whole new size.
             entry_change.old_expiration_ledger - current_ledger + 1,
             fee_config,
-        );
+        ));
     }
 
     fee
@@ -286,13 +319,15 @@ fn rent_fee_for_size_and_ledgers(
     // Multiplication can overflow here - unlike fee computation this can rely
     // on sane input parameters as rent fee computation does not depend on any
     // user inputs.
-    let num = entry_size as i64 * fee_config.fee_per_write_1kb * rent_ledgers as i64;
+    let num = (entry_size as i64)
+        .saturating_mul(fee_config.fee_per_write_1kb)
+        .saturating_mul(rent_ledgers as i64);
     let storage_coef = if is_persistent {
         fee_config.persistent_rent_rate_denominator
     } else {
         fee_config.temporary_rent_rate_denominator
     };
-    let denom = DATA_SIZE_1KB_INCREMENT * storage_coef;
+    let denom = DATA_SIZE_1KB_INCREMENT.saturating_mul(storage_coef);
     num_integer::div_ceil(num, denom)
 }
 


Test patch:
diff --git a/soroban-env-host/tests/fees.rs b/soroban-env-host/tests/fees.rs
index 3c544955..11c59a30 100644
--- a/soroban-env-host/tests/fees.rs
+++ b/soroban-env-host/tests/fees.rs
@@ -121,6 +121,7 @@ fn resource_fee_computation() {
 #[test]
 fn test_rent_bump_fees_with_only_bump() {
     let fee_config = RentFeeConfiguration {
+        fee_per_write_entry: 10,
         fee_per_write_1kb: 1000,
         persistent_rent_rate_denominator: 10_000,
         temporary_rent_rate_denominator: 100_000,
@@ -139,8 +140,10 @@ fn test_rent_bump_fees_with_only_bump() {
             &fee_config,
             50_000,
         ),
-        // 1 * 1000 * 200_000 / (10_000 * 1024)
-        20
+        // Rent: ceil(1 * 1000 * 200_000 / (10_000 * 1024)) (=20) +
+        // Expiration entry write bytes: ceil(1000 * 68 / 1024) (=67) +
+        // Expiration entry write: 10
+        20 + 67 + 10
     );
 
     // Minimal ledgers
@@ -156,8 +159,9 @@ fn test_rent_bump_fees_with_only_bump() {
             &fee_config,
             50_000,
         ),
-        // ceil(10 * 1024 * 1000 * 1 / (10_000 * 1024))
-        1
+        // Rent: ceil(10 * 1024 * 1000 * 1 / (10_000 * 1024)) (=1) +
+        // Expiration entry write entry/bytes: 77
+        1 + 77
     );
 
     // Minimal ledgers & size
@@ -173,8 +177,9 @@ fn test_rent_bump_fees_with_only_bump() {
             &fee_config,
             50_000,
         ),
-        // ceil(1 * 1000 * 1 / (10_000 * 1024))
-        1
+        // Rent: ceil(1 * 1000 * 1 / (10_000 * 1024))
+        // Expiration entry write entry/bytes: 77
+        1 + 77
     );
 
     // No size change
@@ -190,8 +195,9 @@ fn test_rent_bump_fees_with_only_bump() {
             &fee_config,
             50_000,
         ),
-        // 10 * 1024 * 1000 * 200_000 / (10_000 * 1024)
-        200_000
+        // Rent: ceil(10 * 1024 * 1000 * 200_000 / (10_000 * 1024)) (=200_000)
+        // Expiration entry write entry/bytes: 77
+        200_000 + 77
     );
 
     // Size decrease
@@ -207,8 +213,9 @@ fn test_rent_bump_fees_with_only_bump() {
             &fee_config,
             50_000,
         ),
-        // 5 * 1024 * 1000 * 200_000 / (10_000 * 1024)
-        100_000
+        // Rent: ceil(5 * 1024 * 1000 * 200_000 / (10_000 * 1024)) (=100_000) +
+        // Expiration entry write entry/bytes: 77
+        100_000 + 77
     );
 
     // Temp storage rate
@@ -224,8 +231,9 @@ fn test_rent_bump_fees_with_only_bump() {
             &fee_config,
             50_000,
         ),
-        // 10 * 1024 * 1000 * 200_000 / (100_000 * 1024)
-        20_000
+        // Rent: ceil(10 * 1024 * 1000 * 200_000 / (100_000 * 1024)) (=20_000) +
+        // Expiration entry write entry/bytes: 77
+        20_000 + 77
     );
 
     // Multiple entries
@@ -259,19 +267,36 @@ fn test_rent_bump_fees_with_only_bump() {
                     new_size_bytes: 1,
                     old_expiration_ledger: 100_000,
                     new_expiration_ledger: 300_000,
+                },
+                LedgerEntryRentChange {
+                    is_persistent: true,
+                    old_size_bytes: 10 * 1024,
+                    new_size_bytes: 10 * 1024,
+                    old_expiration_ledger: 100_000,
+                    new_expiration_ledger: 300_000,
+                },
+                LedgerEntryRentChange {
+                    is_persistent: false,
+                    old_size_bytes: 10 * 1024,
+                    new_size_bytes: 10 * 1024,
+                    old_expiration_ledger: 100_000,
+                    new_expiration_ledger: 300_000,
                 }
             ],
             &fee_config,
             50_000,
         ),
-        // 20_000 + 200_000 + 1 + 20
-        220_021
+        // Rent: 20_000 + 200_000 + 1 + 20 + 200_000 + 20_000 (=440_021) +
+        // Expiration entry write bytes: ceil(6 * 1000 * 68 / 1024) (=399) +
+        // Expiration entry write: 10 * 6
+        440_021 + 399 + 60
     );
 }
 
 #[test]
 fn test_rent_bump_fees_with_only_size_change() {
     let fee_config = RentFeeConfiguration {
+        fee_per_write_entry: 100,
         fee_per_write_1kb: 1000,
         persistent_rent_rate_denominator: 10_000,
         temporary_rent_rate_denominator: 100_000,
@@ -375,6 +400,7 @@ fn test_rent_bump_fees_with_only_size_change() {
 #[test]
 fn test_rent_bump_with_size_change_and_bump() {
     let fee_config = RentFeeConfiguration {
+        fee_per_write_entry: 10,
         fee_per_write_1kb: 1000,
         persistent_rent_rate_denominator: 10_000,
         temporary_rent_rate_denominator: 100_000,
@@ -393,9 +419,10 @@ fn test_rent_bump_with_size_change_and_bump() {
             &fee_config,
             25_000,
         ),
-        // 100_000 * 1000 * 200_000 / (10_000 * 1024) +
+        // Rent: 100_000 * 1000 * 200_000 / (10_000 * 1024) +
         // 99_999 * 1000 * (100_000 - 25_000 + 1) / (10_000 * 1024)
-        2_685_550
+        // Expiration entry write entry/bytes: 77
+        2_685_550 + 77
     );
 
     // Temp entry
@@ -411,9 +438,10 @@ fn test_rent_bump_with_size_change_and_bump() {
             &fee_config,
             25_000,
         ),
-        // 100_000 * 1000 * 200_000 / (10_000 * 1024) +
+        // Rent: 100_000 * 1000 * 200_000 / (10_000 * 1024) +
         // 99_999 * 1000 * (100_000 - 25_000 + 1) / (10_000 * 1024)
-        268_556
+        // Expiration entry write entry/bytes: 77
+        268_556 + 77
     );
 
     // Multiple entries
@@ -438,8 +466,10 @@ fn test_rent_bump_with_size_change_and_bump() {
             &fee_config,
             25_000,
         ),
-        // 2_685_550 + 268_556
-        2_954_106
+        // Rent: 2_685_550 + 268_556
+        // Expiration entry write bytes: ceil(2 * 1000 * 68 / 1024) (=133) +
+        // Expiration entry write: 10 * 2
+        2_954_106 + 133 + 20
     );
 
     // Small increments
@@ -455,15 +485,17 @@ fn test_rent_bump_with_size_change_and_bump() {
             &fee_config,
             99_999,
         ),
-        // ceil(2 * 1000 * 1 / (10_000 * 1024)) +
-        // ceil(1 * 1000 * (100_000 - 99_999 + 1) / (10_000 * 1024))
-        2
+        // Rent: ceil(2 * 1000 * 1 / (10_000 * 1024)) +
+        //       ceil(1 * 1000 * (100_000 - 99_999 + 1) / (10_000 * 1024)) (=2)
+        // Expiration entry write entry/bytes: 77
+        2 + 77
     );
 }
 
 #[test]
 fn test_rent_bump_without_old_entry() {
     let fee_config = RentFeeConfiguration {
+        fee_per_write_entry: 10,
         fee_per_write_1kb: 1000,
         persistent_rent_rate_denominator: 10_000,
         temporary_rent_rate_denominator: 100_000,
@@ -482,8 +514,9 @@ fn test_rent_bump_without_old_entry() {
             &fee_config,
             25_000,
         ),
-        // 100_000 * 1000 * (100_000 - 25_000) / (10_000 * 1024)
-        732_432
+        // Rent: 100_000 * 1000 * (100_000 - 25_000) / (10_000 * 1024)
+        // Expiration entry write entry/bytes: 77
+        732_432 + 77
     );
 
     // Temp storage
@@ -499,8 +532,9 @@ fn test_rent_bump_without_old_entry() {
             &fee_config,
             25_000,
         ),
-        // 100_000 * 1000 * (100_000 - 25_000) / (10_000 * 1024)
-        73_244
+        // Rent: 100_000 * 1000 * (100_000 - 25_000) / (10_000 * 1024)
+        // Expiration entry write entry/bytes: 77
+        73_244 + 77
     );
 }