mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-10-05 12:28:22 +00:00
fix(pricing): reject an exchange quote that underflows the sats price
A quote of `1e-320` is finite and positive, so it passed the feed's guards and then won the `min()` — but the node prices in sats, and `1e-320 / 100_000_000` is `0.0`. A zero sats price divides by zero on every model's rate, so one malformed feed could take the node's pricing down while two healthy quotes stood beside it. Require the quote to survive the conversion it is going to be put through, and name the divisor while it has two call sites. Also drops a line from the boolean-quote test that said this coercion was the only one rejecting booleans; they all share one now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014X8RZzzbAuQCbavhFjTvJ4
This commit is contained in:
co-authored by
Claude Opus 5
parent
337be4895e
commit
027bb3da31
@@ -12,6 +12,8 @@ logger = get_logger(__name__)
|
||||
BTC_USD_PRICE: float | None = None
|
||||
SATS_USD_PRICE: float | None = None
|
||||
|
||||
SATS_PER_BTC = 100_000_000
|
||||
|
||||
|
||||
def _parse_quote(raw: object, exchange: str) -> float | None:
|
||||
"""Coerce an exchange quote to a price, or ``None`` if it is not one.
|
||||
@@ -20,10 +22,12 @@ def _parse_quote(raw: object, exchange: str) -> float | None:
|
||||
of what it collects: an unusable quote does not merely join the sample, it
|
||||
*wins* it, and the result is the rate every model and every request on the
|
||||
node is priced at. A quote is stricter than a billable rate — it must be
|
||||
positive, since a BTC price of zero is a broken feed, not free money.
|
||||
positive, and positive *after* the sats conversion the node prices in: a
|
||||
subnormal quote survives every guard here and still underflows to a zero
|
||||
sats price, which then divides by zero on every model's rate.
|
||||
"""
|
||||
price = coerce_rate(raw)
|
||||
if price is None or price <= 0:
|
||||
if price is None or price <= 0 or price / SATS_PER_BTC <= 0:
|
||||
logger.warning(
|
||||
"Unusable price quote — ignoring this exchange",
|
||||
extra={"exchange": exchange, "quote": repr(raw)},
|
||||
@@ -141,7 +145,7 @@ async def _update_prices() -> None:
|
||||
)
|
||||
return
|
||||
BTC_USD_PRICE = btc_price
|
||||
SATS_USD_PRICE = btc_price / 100_000_000
|
||||
SATS_USD_PRICE = btc_price / SATS_PER_BTC
|
||||
|
||||
|
||||
def btc_usd_price() -> float:
|
||||
|
||||
@@ -315,9 +315,7 @@ async def test_boolean_exchange_quote_does_not_set_the_node_price(
|
||||
|
||||
``float(True)`` is ``1.0``, which is finite and positive, so a payload whose
|
||||
price field turned into a boolean passes every numeric guard — and then
|
||||
*wins* the ``min()``, pricing the whole node at one dollar per bitcoin. The
|
||||
node's other coercions all reject ``bool`` before the numeric check for this
|
||||
reason; this one is the exception.
|
||||
*wins* the ``min()``, pricing the whole node at one dollar per bitcoin.
|
||||
"""
|
||||
from routstr.payment.price import btc_usd_price
|
||||
|
||||
@@ -328,6 +326,25 @@ async def test_boolean_exchange_quote_does_not_set_the_node_price(
|
||||
assert btc_usd_price() == pytest.approx(100000.0)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_an_underflowing_exchange_quote_does_not_set_the_node_price(
|
||||
refresh_price_with: Any,
|
||||
) -> None:
|
||||
"""A quote too small to survive the sats conversion is not a price.
|
||||
|
||||
``1e-320`` is positive, so it passes the guards and wins the ``min()``, but
|
||||
the node prices in sats and ``1e-320 / 100_000_000`` underflows to ``0.0``
|
||||
— a zero sats price divides by zero on every model's rate.
|
||||
"""
|
||||
from routstr.payment.price import btc_usd_price
|
||||
|
||||
await refresh_price_with(
|
||||
{"kraken": "1e-320", "coinbase": "100000.0", "binance": "100000.0"}
|
||||
)
|
||||
|
||||
assert btc_usd_price() == pytest.approx(100000.0)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_an_unreadable_exchange_response_drops_only_that_quote(
|
||||
refresh_price_with: Any,
|
||||
|
||||
Reference in New Issue
Block a user