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