Repository: stellar/rs-soroban-env
Pull request #791 (2023-05-01 22:41:16)

Add a library for the fee computation to the host.
### What

Add a library for the fee computation to the host.

While this is not strictly a part of the host, it is shared between the host users, hence it makes sense to distribute these together.

### Why

This will be used both in Stellar core and in Soroban RPC for the fee computations.

### Known limitations

N/A



Patch:
diff --git a/soroban-env-host/src/fees.rs b/soroban-env-host/src/fees.rs
new file mode 100644
index 00000000..04dd8c62
--- /dev/null
+++ b/soroban-env-host/src/fees.rs
@@ -0,0 +1,128 @@
+/// This module defines the fee computation protocol for Soroban.
+///
+/// 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).
+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 {
+    /// Number of CPU instructions.
+    pub instructions: u32,
+    /// Number of ledger entries the transaction reads.
+    pub read_entries: u32,
+    /// Number of ledger entries the transaction writes (these are also counted
+    /// as entries that are being read for the sake of the respective fees).
+    pub write_entries: u32,
+    /// Number of bytes read from ledger.
+    pub read_bytes: u32,
+    /// Number of bytes written to ledger.
+    pub write_bytes: u32,
+    /// Size of the metadata that transaction emits. Consists of the size of
+    /// the events XDR, the size of writeable entries XDR before the transaction
+    /// is applied, the size of writeable entries XDR after the transaction is
+    /// applied.
+    pub metadata_size_bytes: u32,
+    /// Size of the transaction XDR.
+    pub transaction_size_bytes: u32,
+}
+
+/// Fee-related network configuration.
+///
+/// This should be normally loaded from the ledger.
+pub struct FeeConfiguration {
+    /// Fee per `INSTRUCTIONS_INCREMENT=10000` instructions.
+    pub fee_per_instruction_increment: i64,
+    /// Fee per 1 entry read from ledger.
+    pub fee_per_read_entry: i64,
+    /// Fee per 1 entry written to ledger.
+    pub fee_per_write_entry: i64,
+    /// Fee per 1KB read from ledger.
+    pub fee_per_read_1kb: i64,
+    /// Fee per 1KB written to ledger.
+    pub fee_per_write_1kb: i64,
+    /// Fee per 1KB written to history (the history write size is based on
+    /// transaction size and `TX_BASE_RESULT_SIZE`).
+    pub fee_per_historical_1kb: i64,
+    /// Fee per 1KB of metadata written.
+    pub fee_per_metadata_1kb: i64,
+    /// Fee per 1KB propagate to the network (the propagated size is equal to
+    /// the transaction size).
+    pub fee_per_propagate_1kb: i64,
+}
+
+/// Computes the resource fee for a transaction based on the resource
+/// consumption and the fee-related network configuration.
+///
+/// Returns a pair of `(fee, refundable_fee)`, where refundable fee is also
+/// included into the final fee.
+pub fn compute_transaction_resource_fee(
+    tx_resources: &TransactionResources,
+    fee_config: &FeeConfiguration,
+) -> (i64, i64) {
+    let compute_fee = compute_fee_per_increment(
+        tx_resources.instructions,
+        fee_config.fee_per_instruction_increment,
+        INSTRUCTIONS_INCREMENT,
+    );
+    let ledger_read_entry_fee: i64 = fee_config.fee_per_read_entry.saturating_mul(
+        tx_resources
+            .read_entries
+            .saturating_add(tx_resources.write_entries)
+            .into(),
+    );
+    let ledger_write_entry_fee = fee_config
+        .fee_per_write_entry
+        .saturating_mul(tx_resources.write_entries.into());
+    let ledger_read_bytes_fee = compute_fee_per_increment(
+        tx_resources.read_bytes,
+        fee_config.fee_per_read_1kb,
+        DATA_SIZE_1KB_INCREMENT,
+    );
+    let ledger_write_bytes_fee = compute_fee_per_increment(
+        tx_resources.write_bytes,
+        fee_config.fee_per_write_1kb,
+        DATA_SIZE_1KB_INCREMENT,
+    );
+
+    let historical_fee = compute_fee_per_increment(
+        tx_resources
+            .transaction_size_bytes
+            .saturating_add(TX_BASE_RESULT_SIZE),
+        fee_config.fee_per_historical_1kb,
+        DATA_SIZE_1KB_INCREMENT,
+    );
+
+    let meta_data_fee = compute_fee_per_increment(
+        tx_resources.metadata_size_bytes,
+        fee_config.fee_per_metadata_1kb,
+        DATA_SIZE_1KB_INCREMENT,
+    );
+
+    let bandwidth_fee = compute_fee_per_increment(
+        tx_resources.transaction_size_bytes,
+        fee_config.fee_per_propagate_1kb,
+        DATA_SIZE_1KB_INCREMENT,
+    );
+
+    let refundable_fee = meta_data_fee;
+    let non_refundable_fee = compute_fee
+        .saturating_add(ledger_read_entry_fee)
+        .saturating_add(ledger_write_entry_fee)
+        .saturating_add(ledger_read_bytes_fee)
+        .saturating_add(ledger_write_bytes_fee)
+        .saturating_add(historical_fee)
+        .saturating_add(bandwidth_fee);
+    let res = (
+        refundable_fee.saturating_add(non_refundable_fee),
+        refundable_fee,
+    );
+    res
+}
+
+fn compute_fee_per_increment(resource_value: u32, fee_rate: i64, increment: i64) -> i64 {
+    let resource_val: i64 = resource_value.into();
+    num_integer::div_ceil(resource_val.saturating_mul(fee_rate), increment)
+}
diff --git a/soroban-env-host/src/lib.rs b/soroban-env-host/src/lib.rs
index 8204cca6..d9e15ee5 100644
--- a/soroban-env-host/src/lib.rs
+++ b/soroban-env-host/src/lib.rs
@@ -56,3 +56,5 @@ pub use host::{
     LedgerInfo,
 };
 pub use soroban_env_common::*;
+
+pub mod fees;


Test patch:
diff --git a/soroban-env-host/tests/fees.rs b/soroban-env-host/tests/fees.rs
new file mode 100644
index 00000000..5af160f1
--- /dev/null
+++ b/soroban-env-host/tests/fees.rs
@@ -0,0 +1,117 @@
+use soroban_env_host::fees::{
+    compute_transaction_resource_fee, FeeConfiguration, TransactionResources,
+};
+
+#[test]
+fn resource_fee_computation() {
+    // No resources
+    assert_eq!(
+        compute_transaction_resource_fee(
+            &TransactionResources {
+                instructions: 0,
+                read_entries: 0,
+                write_entries: 0,
+                read_bytes: 0,
+                write_bytes: 0,
+                metadata_size_bytes: 0,
+                transaction_size_bytes: 0,
+            },
+            &FeeConfiguration {
+                fee_per_instruction_increment: 100,
+                fee_per_read_entry: 100,
+                fee_per_write_entry: 100,
+                fee_per_read_1kb: 100,
+                fee_per_write_1kb: 100,
+                fee_per_historical_1kb: 100,
+                fee_per_metadata_1kb: 100,
+                fee_per_propagate_1kb: 100,
+            },
+        ),
+        // 30 comes from TX_BASE_RESULT_SIZE
+        (30, 0)
+    );
+
+    // Minimal resources
+    assert_eq!(
+        compute_transaction_resource_fee(
+            &TransactionResources {
+                instructions: 1,
+                read_entries: 1,
+                write_entries: 1,
+                read_bytes: 1,
+                write_bytes: 1,
+                metadata_size_bytes: 1,
+                transaction_size_bytes: 1,
+            },
+            &FeeConfiguration {
+                fee_per_instruction_increment: 100,
+                fee_per_read_entry: 100,
+                fee_per_write_entry: 100,
+                fee_per_read_1kb: 100,
+                fee_per_write_1kb: 100,
+                fee_per_historical_1kb: 100,
+                fee_per_metadata_1kb: 100,
+                fee_per_propagate_1kb: 100,
+            },
+        ),
+        // 2 entry read + 1 write + 30 from TX_BASE_RESULT_SIZE + 1 for
+        // everything else
+        (335, 1)
+    );
+
+    // Different resource/fee values
+    assert_eq!(
+        compute_transaction_resource_fee(
+            &TransactionResources {
+                instructions: 10_123_456,
+                read_entries: 30,
+                write_entries: 10,
+                read_bytes: 25_600,
+                write_bytes: 10_340,
+                metadata_size_bytes: 321_654,
+                transaction_size_bytes: 35_721,
+            },
+            &FeeConfiguration {
+                fee_per_instruction_increment: 1000,
+                fee_per_read_entry: 2000,
+                fee_per_write_entry: 4000,
+                fee_per_read_1kb: 1500,
+                fee_per_write_1kb: 3000,
+                fee_per_historical_1kb: 300,
+                fee_per_metadata_1kb: 200,
+                fee_per_propagate_1kb: 900,
+            },
+        ),
+        (1304913, 62824)
+    );
+
+    // Integer limits
+    assert_eq!(
+        compute_transaction_resource_fee(
+            &TransactionResources {
+                instructions: u32::MAX,
+                read_entries: u32::MAX,
+                write_entries: u32::MAX,
+                read_bytes: u32::MAX,
+                write_bytes: u32::MAX,
+                metadata_size_bytes: u32::MAX,
+                transaction_size_bytes: u32::MAX,
+            },
+            &FeeConfiguration {
+                fee_per_instruction_increment: i64::MAX,
+                fee_per_read_entry: i64::MAX,
+                fee_per_write_entry: i64::MAX,
+                fee_per_read_1kb: i64::MAX,
+                fee_per_write_1kb: i64::MAX,
+                fee_per_historical_1kb: i64::MAX,
+                fee_per_metadata_1kb: i64::MAX,
+                fee_per_propagate_1kb: i64::MAX,
+            },
+        ),
+        // The refundable (metadata) fee is not i64::MAX because we do division
+        // after multiplication and hence it's i64::MAX / 1024.
+        // Hitting the integer size limits shouldn't be an issue in practice;
+        // we need to just make sure there are no overflows.
+        (i64::MAX, 9007199254740992)
+    );
+}