Skip to content
Merged
Show file tree
Hide file tree
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
9 changes: 9 additions & 0 deletions changelog.d/fee-flow-meter-decay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Fixed

- **Fee estimates count the full arrival rate.** The flow meter behind
`estimatesmartfee`, Electrum `blockchain.estimatefee` and Esplora
`/fee-estimates` decayed the bucket it updated twice on each admit, so a
steady stream counted near half its rate (10,000 WU/s read as 5,012) and
near targets could quote low. A fee refresh between admits also changed
the next sample: the admit's weight was divided by the time since the
refresh instead of since the last admit.
10 changes: 8 additions & 2 deletions crates/rbitcoin-mempool/src/fee_flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ pub struct FeeFlowMeter {
pub admit_wu_s: Vec<f64>,
/// Decayed admitted weight (WU). Not the per-bucket time-gap alpha.
pub admitted_wu: f64,
/// Decay clock. Reads move it too.
pub last: Instant,
/// Previous admit. The sample is weight over the gap since this one.
last_admit: Instant,
}

impl Default for FeeFlowMeter {
Expand All @@ -34,6 +37,7 @@ impl FeeFlowMeter {
admit_wu_s: vec![0.0; n],
admitted_wu: 0.0,
last: now,
last_admit: now,
}
}

Expand All @@ -57,13 +61,15 @@ impl FeeFlowMeter {
}

pub fn note_admit(&mut self, weight_wu: u64, rate_sat_per_kvb: u64, now: Instant) {
let dt = now.duration_since(self.last).as_secs_f64().max(1e-3);
let dt = now.duration_since(self.last_admit).as_secs_f64().max(1e-3);
// Every bucket has decayed by `1 - alpha` since `last_admit`, reads included.
self.decay_to(now);
let i = bucket_index(rate_sat_per_kvb).min(self.admit_wu_s.len().saturating_sub(1));
let sample = weight_wu as f64 / dt;
let alpha = 1.0 - (-std::f64::consts::LN_2 * dt / ADMIT_HALF_LIFE_SECS).exp();
self.admit_wu_s[i] = self.admit_wu_s[i] * (1.0 - alpha) + sample * alpha;
self.admit_wu_s[i] += sample * alpha;
self.admitted_wu += weight_wu as f64;
self.last_admit = now;
}

#[doc(hidden)]
Expand Down
39 changes: 38 additions & 1 deletion crates/rbitcoin-mempool/tests/fee_flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ fn two_admits_pin_the_bucket_ema() {
m.note_admit(10_000, 1_000, t0 + Duration::from_secs(4));
let rates = m.admit_rates_wu_s(t0 + Duration::from_secs(4));
let bi = bucket_index(1_000);
assert_eq!(rates[bi], 91, "{rates:?}");
assert_eq!(rates[bi], 92, "{rates:?}");
}

#[test]
Expand Down Expand Up @@ -81,3 +81,40 @@ fn idle_decays() {
let bi = bucket_index(500);
assert!(cold[bi] < hot[bi], "hot={} cold={}", hot[bi], cold[bi]);
}

/// A steady stream settles at the stream's own rate: 10_000 WU every second
/// for 20 half-lives reads 10_000 WU/s. Decaying the admitted bucket twice
/// settled near half that.
#[test]
fn steady_stream_reads_its_own_rate() {
let t0 = Instant::now();
let mut m = FeeFlowMeter::new(t0);
for i in 1..=3_000 {
m.note_admit(10_000, 1_000, t0 + Duration::from_secs(i));
}
let rates = m.admit_rates_wu_s(t0 + Duration::from_secs(3_000));
assert_eq!(rates[bucket_index(1_000)], 10_000);
}

/// A snapshot between admits leaves the next sample alone. The read used to
/// move the gap clock, so the admit at 400 s divided its weight by the 1 s
/// since the read instead of the 300 s since the last admit.
#[test]
fn snapshot_read_does_not_change_the_next_sample() {
let t0 = Instant::now();
let mut quiet = FeeFlowMeter::new(t0);
let mut read = FeeFlowMeter::new(t0);
for m in [&mut quiet, &mut read] {
m.note_admit(1_000_000, 1_000, t0 + Duration::from_secs(100));
}
read.admit_rates_wu_s(t0 + Duration::from_secs(399));
for m in [&mut quiet, &mut read] {
m.note_admit(1_000_000, 1_000, t0 + Duration::from_secs(400));
}
let at = t0 + Duration::from_secs(400);
let bi = bucket_index(1_000);
assert_eq!(
read.admit_rates_wu_s(at)[bi],
quiet.admit_rates_wu_s(at)[bi]
);
}
Loading