Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions packages/devkit/src/simulation/network_load.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ use rand::rngs::SmallRng;
use rand::{Rng, SeedableRng};

/// Configuration for the seeded random network load generator.
///
/// # Fields
///
/// * `min_tx` – Minimum transactions per ledger.
/// * `max_tx` – Maximum transactions per ledger.
/// * `ledger_capacity` – Maximum transactions the network can process per ledger (capacity limit).
/// * `ledger_interval_ms` – Time between ledger closes in milliseconds.
/// * `duration_secs` – Total simulation duration in seconds.
/// * `seed` – Optional RNG seed for reproducibility.
pub struct NetworkLoadConfig {
/// Minimum transactions per ledger.
pub min_tx: u64,
Expand All @@ -13,6 +22,8 @@ pub struct NetworkLoadConfig {
pub ledger_capacity: u64,
/// Time between ledger closes in milliseconds.
pub ledger_interval_ms: u64,
/// Total simulation duration in seconds.
pub duration_secs: u64,
/// Optional RNG seed for reproducibility.
pub seed: Option<u64>,
}
Expand All @@ -24,6 +35,7 @@ impl Default for NetworkLoadConfig {
max_tx: 1000,
ledger_capacity: 1000,
ledger_interval_ms: 5000,
duration_secs: 3600,
seed: None,
}
}
Expand All @@ -35,6 +47,11 @@ impl NetworkLoadConfig {
let mid = (self.min_tx + self.max_tx) as f64 / 2.0;
(mid / self.ledger_capacity as f64).clamp(0.0, 1.0)
}

/// Number of ledger events in `duration_secs` based on `ledger_interval_ms`.
pub fn ledger_count(&self) -> usize {
(self.duration_secs * 1000 / self.ledger_interval_ms) as usize
}
}

/// A single simulated ledger produced by the throughput simulator.
Expand Down Expand Up @@ -69,6 +86,8 @@ impl NetworkLoad {
}

/// Simulate `count` ledger closes, returning full `SimulatedLedger` records.
///
/// Resolves #243.
pub fn simulate(&mut self, count: usize) -> Vec<SimulatedLedger> {
self.generate(count)
.into_iter()
Expand All @@ -85,17 +104,58 @@ impl NetworkLoad {
.collect()
}

/// Simulate for the full `duration_secs` configured in `NetworkLoadConfig`.
///
/// Returns one `SimulatedLedger` per ledger close interval.
/// Resolves #243.
pub fn run(&mut self) -> Vec<SimulatedLedger> {
self.simulate(self.config.ledger_count())
}

/// Returns a fee multiplier (1.0–3.0) based on hour of day (0–23).
///
/// Models diurnal congestion: peak around hour 14 (2pm UTC), trough around hour 2 (2am UTC).
/// Resolves #245.
pub fn diurnal_multiplier(hour: u8) -> f64 {
// Simple sinusoidal: peak at hour 14, trough at hour 2
let angle = std::f64::consts::PI * (hour as f64 - 2.0) / 12.0;
1.0 + angle.sin().max(0.0) * 2.0
}

/// Apply diurnal multiplier to a base fee given the hour of day.
///
/// Resolves #245.
pub fn diurnal_fee(base_fee: u64, hour: u8) -> u64 {
(base_fee as f64 * Self::diurnal_multiplier(hour)).round() as u64
}

/// Scale a tx count by the diurnal multiplier for the given hour.
///
/// During peak hours (08:00–20:00 UTC) the multiplier is >1.0, increasing
/// the effective transaction volume.
///
/// Resolves #245.
pub fn diurnal_tx_count(tx_count: u64, hour: u8) -> u64 {
(tx_count as f64 * Self::diurnal_multiplier(hour)).round() as u64
}
}

/// Scale a fee upward as capacity pressure approaches 1.0.
///
/// The fee increases linearly from `base_fee` at pressure 0.0 to
/// `base_fee * max_multiple` at pressure 1.0.
///
/// # Examples
///
/// ```
/// use stellar_devkit::simulation::network_load::capacity_pressure_fee;
///
/// assert_eq!(capacity_pressure_fee(100, 0.0, 10), 100);
/// assert_eq!(capacity_pressure_fee(100, 1.0, 10), 1000);
/// assert_eq!(capacity_pressure_fee(100, 0.5, 10), 550);
/// ```
pub fn capacity_pressure_fee(base_fee: u64, pressure: f64, max_multiple: u64) -> u64 {
let pressure = pressure.clamp(0.0, 1.0);
let scaled = base_fee as f64 * (1.0 + pressure * (max_multiple.saturating_sub(1)) as f64);
(scaled.round() as u64).max(1)
}
Loading