currencyrate: make oracle store and serve the BTC price, not msat per fiat unit.
What changed, and why it matters
This commit changes how a Core Lightning plugin stores and calculates Bitcoin exchange rates. Previously the plugin stored 'millisatoshis per fiat unit' and took the median of those inverted values. Now it stores the raw fiat price of one bitcoin and converts only at output time. The commit message explicitly notes this is a subtle change because the median of bitcoin prices is mathematically different from the median of inverted rates. The patch is described as the first step in updating the API, not as a security fix.
No security action required. Treat as a normal API correctness refactor. If reviewing the broader currencyrate feature, verify that downstream consumers expect the new median semantics and that floating-point rounding behavior is acceptable for financial amounts.
Security signals we found
No security-relevant keywords in commit title or message
No bounds checking or sanitization changes
No cryptographic or authentication changes
Arithmetic refactor with explicit note about median semantics
Test expectation changed to match new formula
Evidence from the diff
The currencyrate plugin’s oracle now stores raw currency-per-BTC prices instead of pre-computed msat-per-currency values. The conversion to msat is deferred to API response time using MSAT_PER_BTC / price. This changes the median calculation: median(1E11/a, 1E11/b) != 1E11/median(a,b). The test was updated to reflect the new arithmetic. No input validation, authentication, or network handling changes are present. The change is a correctness/API refactor, not a vulnerability patch.
Changed components
plugins/currencyrate-plugin/src/main.rsplugins/currencyrate-plugin/src/oracle.rstests/test_currencyrate.pyInspect captured patch +12 / −9
diff --git a/plugins/currencyrate-plugin/src/main.rs b/plugins/currencyrate-plugin/src/main.rs
index e1d8c2bf..8dc85651 100644
--- a/plugins/currencyrate-plugin/src/main.rs
+++ b/plugins/currencyrate-plugin/src/main.rs
@@ -10,7 +10,7 @@ use std::sync::Arc;
use std::vec;
use tokio::sync::Mutex;
-use crate::oracle::{BtcPriceOracle, Source};
+use crate::oracle::{BtcPriceOracle, Source, MSAT_PER_BTC};
mod oracle;
@@ -167,7 +167,7 @@ async fn currencyrates(plugin: Plugin<PluginState>, args: Value) -> Result<Value
Ok(result) => {
let mut map = serde_json::Map::new();
for source_result in result {
- let msat = source_result.price.round() as u64;
+ let msat = (MSAT_PER_BTC / source_result.price).round() as u64;
map.insert(source_result.name.clone(), json!(msat));
}
Ok(json!(map))
diff --git a/plugins/currencyrate-plugin/src/oracle.rs b/plugins/currencyrate-plugin/src/oracle.rs
index 7c24a847..0098c943 100644
--- a/plugins/currencyrate-plugin/src/oracle.rs
+++ b/plugins/currencyrate-plugin/src/oracle.rs
@@ -20,6 +20,8 @@ const DRIFT_THRESHOLD: f64 = 0.01;
const INITIAL_BACKOFF: Duration = Duration::from_secs(30);
const MAX_BACKOFF: Duration = Duration::from_secs(3_600);
+pub const MSAT_PER_BTC: f64 = 1e11;
+
#[derive(Debug, Clone)]
pub struct Source {
name: String,
@@ -123,13 +125,13 @@ impl Source {
}
log::info!(
- "Fetched price in {}ms from {}: {:.2} {currency}",
+ "Fetched price in {}ms from {}: {:.2} {currency}/BTC",
now.elapsed().as_millis(),
self.name,
price
);
- Ok(1E11 / price)
+ Ok(price)
}
}
@@ -326,8 +328,8 @@ impl BtcPriceOracle {
self.get_all_rates(currency).await?
};
- let median_rate = get_median_rate(source_results);
- Ok((amount * median_rate).round() as u64)
+ let median_currency_per_btc = get_median_rate(source_results);
+ Ok((amount * MSAT_PER_BTC / median_currency_per_btc).round() as u64)
}
async fn refresh_currency(&self, currency: &str) -> Result<(), anyhow::Error> {
diff --git a/tests/test_currencyrate.py b/tests/test_currencyrate.py
index 728fbd90..bc025a49 100644
--- a/tests/test_currencyrate.py
+++ b/tests/test_currencyrate.py
@@ -266,9 +266,10 @@ def test_cached_median(node_factory, fake_rateserver):
assert rates["fast"] == 1000
assert rates["slow"] == 2000
- # With two fresh cached rates, the correct median is midpoint(1000, 2000) = 1500.
- # For 100 USD, that should be 150000 msat.
+ # Cached result should be median of two rates.
+ median_rate = (100_000_000 + 50_000_000) / 2
convert = l1.rpc.call("currencyconvert", [100, "USD"])
LOGGER.info(convert)
- assert convert["msat"] == 150000
+ # Median of raw rates is used.
+ assert convert["msat"] == 100 * 100_000_000_000 // median_rate
Why this scored 18/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.