Merge branch '14-panic-occurred-attempt-to-multiply-with-overflow' into 'master'
Resolve "Panic occurred: attempt to multiply with overflow" Closes #14 See merge request riftenlabs/riftenlabs-indexer!40
This commit is contained in:
commit
e999c2e32d
6 changed files with 1087 additions and 480 deletions
|
|
@ -401,7 +401,7 @@ pub fn get_new_headers(
|
|||
let mut blockhash = *new_tip;
|
||||
|
||||
while blockhash != null_hash {
|
||||
if new_headers.len() % 1000 == 0 {
|
||||
if new_headers.len().is_multiple_of(1000) {
|
||||
info!(
|
||||
"Downloading headers progress: {} fetched... ",
|
||||
new_headers.len()
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -23,8 +23,8 @@ mod tests {
|
|||
};
|
||||
use crate::db::cauldron::tokenlist::list_tokens_volume::db_list_tokens_by_volume;
|
||||
use crate::db::cauldron::tokenlist::token_utils::{
|
||||
apy_30d_bp_for_token, compute_score, pct_change_bp_dec, pow10_dec, price_from_tvl,
|
||||
resolve_decimals, resolve_display_labels,
|
||||
apy_30d_bp_for_token, compute_score, overflow_f64_fallback, pct_change_bp_dec, pow10_dec,
|
||||
price_from_tvl, resolve_decimals, resolve_display_labels,
|
||||
};
|
||||
use crate::db::cauldron::tx::{insert_block_tx, insert_mempool_tx};
|
||||
use crate::db::cauldron::utxo_funding::insert_utxo_funding;
|
||||
|
|
@ -150,18 +150,15 @@ mod tests {
|
|||
fn test_helper_math() {
|
||||
// pow10
|
||||
assert_eq!(pow10_dec(0), Decimal::ONE);
|
||||
assert_eq!(pow10_dec(2), Decimal::from_i32(100).unwrap());
|
||||
assert_eq!(pow10_dec(2), dec!(100));
|
||||
|
||||
// price_from_tvl
|
||||
assert_eq!(
|
||||
price_from_tvl(100, 10).unwrap(),
|
||||
Decimal::from_i32(10).unwrap()
|
||||
);
|
||||
assert_eq!(price_from_tvl(100, 10).unwrap(), dec!(10));
|
||||
assert!(price_from_tvl(100, 0).is_none());
|
||||
|
||||
// pct_change (Decimal)
|
||||
let a = Decimal::from_i32(110).unwrap();
|
||||
let b = Decimal::from_i32(100).unwrap();
|
||||
let a = dec!(110);
|
||||
let b = dec!(100);
|
||||
assert_eq!(pct_change_bp_dec(a, b), 1000);
|
||||
|
||||
// compute_score
|
||||
|
|
@ -1003,4 +1000,257 @@ mod tests {
|
|||
.unwrap();
|
||||
assert!(cached_ts.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fast_updater_handles_extreme_decimals_without_panicking() {
|
||||
// Arrange: fresh DBs/tables
|
||||
let mock = mock_db_pool(|conn| setup_basic_schemas(conn));
|
||||
let cw = mock.cauldron_w.get().expect("cauldron_w");
|
||||
let bcmr_w = mock.bcmr_w.get().expect("bcmr_w");
|
||||
let bcmr_r = mock.bcmr_r.get().expect("bcmr_r");
|
||||
let crc_r = mock.crc20_r.get().expect("crc20_r");
|
||||
let orc_r = mock.oracle_r.get().expect("oracle_r");
|
||||
|
||||
// Seed BCMR with absurd decimals (should trigger overflow pre-patch)
|
||||
let token = TokenID::from_inner([0xDE; 32]);
|
||||
let token_hex = token.to_hex();
|
||||
let utxo = OutPointHash::from_inner([0xEE; 32]);
|
||||
let txid = Txid::from_inner([0xEF; 32]);
|
||||
|
||||
let row = BCMRRow {
|
||||
name: "OverflowCoin".into(),
|
||||
description: "trigger pow10 overflow".into(),
|
||||
token: BCMRToken {
|
||||
category: "cat".into(),
|
||||
symbol: "OF".into(),
|
||||
decimals: 30, // <-- extreme
|
||||
},
|
||||
uris: Uris {
|
||||
icon: None,
|
||||
web: None,
|
||||
},
|
||||
filemeta: FileMeta {
|
||||
expected_hash: Some("x".into()),
|
||||
actual_hash: Some("y".into()),
|
||||
source: "test".into(),
|
||||
},
|
||||
};
|
||||
insert_bcmr_data(&bcmr_w, &utxo, &row).unwrap();
|
||||
insert_authheader(
|
||||
&bcmr_w,
|
||||
&utxo,
|
||||
&BlockHash::all_zeros(),
|
||||
&txid,
|
||||
&token,
|
||||
100,
|
||||
Some(Vec::from("bcmr".as_bytes())),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Sanity: ensure resolve_decimals sees 30 via the same code path the updater uses
|
||||
let dec = resolve_decimals(&bcmr_r, &crc_r, &token_hex);
|
||||
assert_eq!(
|
||||
dec, 30,
|
||||
"resolve_decimals must read the extreme value for this test"
|
||||
);
|
||||
|
||||
// Ensure TVL so the updater scales price by 10^decimals
|
||||
let now = crate::timeutil::time_now();
|
||||
super::tests::seed_minimal_token_history(
|
||||
&cw,
|
||||
token,
|
||||
now - 120,
|
||||
now - 60,
|
||||
1_000,
|
||||
500,
|
||||
2_000,
|
||||
1_000,
|
||||
);
|
||||
|
||||
update_tvl_and_price_now(&cw, &bcmr_r, &crc_r, &orc_r)
|
||||
.expect("fast updater should not panic on extreme decimals");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pow10_dec_clamps_and_does_not_overflow() {
|
||||
use rust_decimal_macros::dec;
|
||||
|
||||
// Exact 10^28 (1 followed by 28 zeros)
|
||||
let e28 = crate::db::cauldron::tokenlist::token_utils::pow10_dec(28);
|
||||
let e28_expected = dec!(10000000000000000000000000000); // ← 29 digits total
|
||||
assert_eq!(e28, e28_expected);
|
||||
|
||||
// Asking for 30 should clamp to 28 and not panic
|
||||
let e30 = crate::db::cauldron::tokenlist::token_utils::pow10_dec(30);
|
||||
assert_eq!(e30, e28_expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn core_overflow_paths_set_prices_null_not_panic() {
|
||||
let mock = mock_db_pool(|conn| setup_basic_schemas(conn));
|
||||
let cw = mock.cauldron_w.get().unwrap();
|
||||
let bcmr_w = mock.bcmr_w.get().unwrap();
|
||||
let bcmr_r = mock.bcmr_r.get().unwrap();
|
||||
let crc_r = mock.crc20_r.get().unwrap();
|
||||
let orc_r = mock.oracle_r.get().unwrap();
|
||||
|
||||
let token = TokenID::from_inner([0xEE; 32]);
|
||||
let utxo = OutPointHash::from_inner([0xCD; 32]);
|
||||
let txid = Txid::from_inner([0xAB; 32]);
|
||||
|
||||
// decimals = 28 OK, but we’ll make price huge by tiny tokens
|
||||
insert_bcmr_data(
|
||||
&bcmr_w,
|
||||
&utxo,
|
||||
&BCMRRow {
|
||||
name: "HugePrice".into(),
|
||||
description: "".into(),
|
||||
token: BCMRToken {
|
||||
category: "c".into(),
|
||||
symbol: "HP".into(),
|
||||
decimals: 28,
|
||||
},
|
||||
uris: Uris {
|
||||
icon: None,
|
||||
web: None,
|
||||
},
|
||||
filemeta: FileMeta {
|
||||
expected_hash: Some("x".into()),
|
||||
actual_hash: Some("y".into()),
|
||||
source: "test".into(),
|
||||
},
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
insert_authheader(
|
||||
&bcmr_w,
|
||||
&utxo,
|
||||
&BlockHash::all_zeros(),
|
||||
&txid,
|
||||
&token,
|
||||
100,
|
||||
Some(b"bcmr".to_vec()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// seed: huge sats, tiny tokens (1) to push price_now_dec very large
|
||||
let now = crate::timeutil::time_now();
|
||||
super::tests::seed_minimal_token_history(
|
||||
&cw,
|
||||
token,
|
||||
now - 120,
|
||||
now - 60,
|
||||
9_000_000_000_000_000,
|
||||
1,
|
||||
9_000_000_000_000_000,
|
||||
1,
|
||||
);
|
||||
|
||||
// Create presence in cache
|
||||
cw.execute("INSERT INTO cached_token_metrics(token_id, trade_volume, tvl_sats, tvl_tokens, score, updated_at)
|
||||
VALUES(?1,0,1,1,0,strftime('%s','now'))
|
||||
ON CONFLICT(token_id) DO NOTHING", rusqlite::params![token.to_hex()]).unwrap();
|
||||
|
||||
// Should not panic
|
||||
update_changes_score_volume_and_ranking(&cw, &bcmr_r, &crc_r, &orc_r).unwrap();
|
||||
|
||||
// Prices likely NULL after overflow guard
|
||||
let (p_now_usd, p_24h, p_7d): (Option<f64>, Option<f64>, Option<f64>) =
|
||||
cw.query_row("SELECT price_now_usd, price_24h, price_7d FROM cached_token_metrics WHERE token_id=?1",
|
||||
rusqlite::params![token.to_hex()],
|
||||
|r| Ok((r.get(0)?, r.get(1)?, r.get(2)?))).unwrap();
|
||||
assert!(p_now_usd.is_none() || p_now_usd.unwrap().is_finite());
|
||||
// allow None here; the point is: no panic
|
||||
let _ = (p_24h, p_7d);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pct_change_bp_handles_huge_ratio_without_panic() {
|
||||
use crate::db::cauldron::tokenlist::token_utils::{pct_change_bp_dec, pow10_dec};
|
||||
use rust_decimal::Decimal;
|
||||
|
||||
// Arrange: absurd values that would overflow in current code
|
||||
let current: Decimal = pow10_dec(28); // ~1e28
|
||||
let past: Decimal = Decimal::ONE / pow10_dec(18); // ~1e-18
|
||||
|
||||
// Act: call pct_change_bp_dec
|
||||
let result = std::panic::catch_unwind(|| pct_change_bp_dec(current, past));
|
||||
|
||||
// Assert: should NOT panic once patched
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"pct_change_bp_dec panicked on huge ratio (needs overflow guard)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overflow_fallback_uses_f64_and_does_not_panic() {
|
||||
// simulate absurdly high decimals that would overflow Decimal multiplication
|
||||
let base = Decimal::new(123456789, 0); // 123456789
|
||||
let decimals: u32 = 1000;
|
||||
|
||||
let price_now_human = overflow_f64_fallback(base, decimals);
|
||||
|
||||
assert!(price_now_human.is_finite() || price_now_human.is_infinite());
|
||||
}
|
||||
#[test]
|
||||
fn price_from_tvl_handles_u64_extremes_without_overflow() {
|
||||
use std::u64;
|
||||
|
||||
// Max sats, 1 token → result should be exactly MAX as Decimal
|
||||
let p = price_from_tvl(u64::MAX, 1).expect("not None");
|
||||
assert_eq!(p, Decimal::from_u128(u64::MAX as u128).unwrap());
|
||||
|
||||
// Symmetric max → 1
|
||||
let p = price_from_tvl(u64::MAX, u64::MAX).expect("not None");
|
||||
assert_eq!(p, Decimal::ONE);
|
||||
|
||||
// Max sats, 2 tokens → roughly MAX/2
|
||||
let p = price_from_tvl(u64::MAX, 2).expect("not None");
|
||||
// Use a tolerant comparison because Decimal division can normalize scale
|
||||
let half = Decimal::from_u128((u64::MAX as u128) / 2).unwrap();
|
||||
// Allow a difference of 1 ulp at integer scale if needed
|
||||
assert!(p >= half && p <= half + Decimal::ONE);
|
||||
|
||||
// Tiny sats, huge tokens → small decimal > 0
|
||||
let p = price_from_tvl(1, u64::MAX).expect("not None");
|
||||
assert!(p > Decimal::ZERO && p < Decimal::ONE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn price_from_tvl_zero_tokens_is_none() {
|
||||
assert!(price_from_tvl(0, 0).is_none());
|
||||
assert!(price_from_tvl(100, 0).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn price_from_tvl_no_u64_intermediate_multiply() {
|
||||
// This test is about behavior: if an accidental u64 multiply had crept in,
|
||||
// some extreme combos would panic or wrap. We assert it doesn't.
|
||||
let cases = [
|
||||
(std::u64::MAX, 1u64),
|
||||
(std::u64::MAX, std::u64::MAX),
|
||||
(1u64, std::u64::MAX),
|
||||
(9_000_000_000_000_000u64, 1u64),
|
||||
];
|
||||
for (sats, toks) in cases {
|
||||
let _ = price_from_tvl(sats, toks); // should not panic
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn scaling_price_never_requires_integer_multiply() {
|
||||
// Simulate a very large ratio, then scale by decimals via Decimal first
|
||||
let p = price_from_tvl(std::u64::MAX, 1).unwrap(); // huge base
|
||||
// Use your clamped pow10_dec; should not panic
|
||||
let factor = crate::db::cauldron::tokenlist::token_utils::pow10_dec(28);
|
||||
let scaled = p.checked_mul(factor).unwrap_or_else(|| Decimal::ZERO);
|
||||
// Convert to f64 bounded; must be finite or clamped per your helper
|
||||
let f = crate::db::cauldron::tokenlist::token_utils::dec_to_f64_bounded(scaled);
|
||||
assert!(f.is_finite() || f.abs() == f64::MAX);
|
||||
}
|
||||
#[test]
|
||||
fn compute_score_never_overflows() {
|
||||
let s = compute_score(u64::MAX, u64::MAX);
|
||||
assert!(s >= 0); // and, importantly, no panic occurred
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
// This software is licensed under the GNU Affero General Public License (AGPL), version 3.0 or later.
|
||||
// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/agpl-3.0.html
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::Result;
|
||||
use log::warn;
|
||||
use rusqlite::Connection;
|
||||
|
||||
|
|
@ -11,9 +11,45 @@ use crate::db::cauldron::pool::get_pool_period_snapshot;
|
|||
use crate::db::oracle::get_closest;
|
||||
use crate::rpc::apy::apyaggregator::APYAggregator;
|
||||
use crate::rpc::apy::poolperiod::PoolPeriod;
|
||||
use malachite::base::num::arithmetic::traits::FloorSqrt;
|
||||
use malachite::Integer;
|
||||
|
||||
use rust_decimal::prelude::*;
|
||||
use rust_decimal::Decimal;
|
||||
use rust_decimal_macros::dec;
|
||||
|
||||
/// Fallback path when Decimal scaling overflows: convert to f64 with clamped exponent.
|
||||
pub fn overflow_f64_fallback(base: Decimal, decimals: u32) -> f64 {
|
||||
let base_f = dec_to_f64_bounded(base);
|
||||
let exp = (decimals as i32).clamp(0, 308); // max safe for f64::powi
|
||||
let factor_f = 10f64.powi(exp);
|
||||
let p = base_f * factor_f;
|
||||
if p.is_finite() {
|
||||
p
|
||||
} else {
|
||||
f64::MAX.copysign(p)
|
||||
}
|
||||
}
|
||||
|
||||
/// Round to a reasonable scale before conversion (tunable).
|
||||
pub fn dec_round(d: Decimal, scale: u32) -> Decimal {
|
||||
d.round_dp_with_strategy(scale, rust_decimal::RoundingStrategy::MidpointNearestEven)
|
||||
}
|
||||
|
||||
/// Convert Decimal -> f64, guaranteed finite (never NaN/∞). Clamps if needed.
|
||||
/// Optional: enforce a floor so tiny non-zero values don't collapse to 0.
|
||||
pub fn dec_to_f64_bounded(d: Decimal) -> f64 {
|
||||
if let Some(v) = d.to_f64() {
|
||||
if v.is_finite() {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
if d.is_sign_negative() {
|
||||
-f64::MAX
|
||||
} else {
|
||||
f64::MAX
|
||||
}
|
||||
}
|
||||
|
||||
pub const SATS_PER_BCH: i64 = 100_000_000;
|
||||
pub const ORACLE_SCALE: i64 = 1_000_000; // match the delphi scale
|
||||
|
|
@ -31,31 +67,62 @@ pub fn usd_per_bch_at_or_before(conn: &Connection, ts: i64) -> Decimal {
|
|||
#[inline]
|
||||
pub fn pct_change_bp_dec(current: Decimal, past: Decimal) -> i64 {
|
||||
if past.is_zero() {
|
||||
0
|
||||
} else {
|
||||
((current - past) / past * Decimal::from_i32(10_000).unwrap())
|
||||
.round()
|
||||
.to_i64()
|
||||
.unwrap_or(0)
|
||||
return 0;
|
||||
}
|
||||
|
||||
let tenk = dec!(10000);
|
||||
|
||||
// Compute: ((current / past) - 1) * 10_000 in a safer order:
|
||||
// => (current * 10_000 / past) - 10_000
|
||||
let scaled_current = match current.checked_mul(tenk) {
|
||||
Some(v) => v,
|
||||
None => return if current >= past { i64::MAX } else { i64::MIN },
|
||||
};
|
||||
|
||||
let ratio_bp = match scaled_current.checked_div(past) {
|
||||
Some(v) => v,
|
||||
None => return 0, // should not happen (past==0 handled), but be defensive
|
||||
};
|
||||
|
||||
let delta_bp = match ratio_bp.checked_sub(tenk) {
|
||||
Some(v) => v,
|
||||
None => return if current >= past { i64::MAX } else { i64::MIN },
|
||||
};
|
||||
|
||||
// Round and convert; clamp if it wouldn't fit in i64
|
||||
match delta_bp.round().to_i64() {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
if delta_bp.is_sign_negative() {
|
||||
i64::MIN
|
||||
} else {
|
||||
i64::MAX
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn compute_score(tvl_sats: u64, volume: u64) -> i64 {
|
||||
if tvl_sats == 0 || volume == 0 {
|
||||
pub fn compute_score(tvl_sats: u64, vol_30d: u64) -> i64 {
|
||||
if tvl_sats == 0 || vol_30d == 0 {
|
||||
return 0;
|
||||
}
|
||||
let s = (volume as f64) * (tvl_sats as f64).sqrt(); // same as BigNumber sqrt + pow(2) pattern
|
||||
if !s.is_finite() {
|
||||
return 0;
|
||||
}
|
||||
if s >= i64::MAX as f64 {
|
||||
i64::MAX
|
||||
} else {
|
||||
s.round() as i64
|
||||
} // ROUND_HALF_UP for positives
|
||||
}
|
||||
|
||||
// score = vol_30d * floor_sqrt(tvl_sats), all in big-int to avoid overflow
|
||||
let sqrt_tvl: Integer = Integer::from(tvl_sats).floor_sqrt();
|
||||
let score_big: Integer = Integer::from(vol_30d) * sqrt_tvl;
|
||||
|
||||
// Clamp to i64 range (non-negative by construction)
|
||||
if score_big > i64::MAX {
|
||||
return i64::MAX;
|
||||
}
|
||||
|
||||
// Convert safely; fall back to MAX on unexpected parse failure
|
||||
match score_big.to_string().parse::<i64>() {
|
||||
Ok(v) => v,
|
||||
Err(_) => i64::MAX,
|
||||
}
|
||||
}
|
||||
pub fn resolve_decimals(bcmr_conn: &Connection, crc20_conn: &Connection, token_id: &str) -> u32 {
|
||||
// On-chain BCMR (latest by height)
|
||||
if let Ok(Some(v)) = bcmr_conn.query_row(
|
||||
|
|
@ -105,10 +172,25 @@ pub fn resolve_decimals(bcmr_conn: &Connection, crc20_conn: &Connection, token_i
|
|||
0
|
||||
}
|
||||
|
||||
/// Max safe exponent for 10^d that fits in `rust_decimal` (1e28 fits; 1e29 does not).
|
||||
pub const MAX_DECIMALS_FOR_SCALING: u32 = 28;
|
||||
|
||||
/// Clamp a metadata decimals value to something rust_decimal can represent.
|
||||
#[inline]
|
||||
pub fn clamp_decimals(decimals: u32) -> u32 {
|
||||
if decimals > MAX_DECIMALS_FOR_SCALING {
|
||||
warn!(
|
||||
"Clamping decimals {} -> {} to avoid overflow in pow10_dec()",
|
||||
decimals, MAX_DECIMALS_FOR_SCALING
|
||||
);
|
||||
}
|
||||
decimals.min(MAX_DECIMALS_FOR_SCALING)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn pow10_dec(decimals: u32) -> Decimal {
|
||||
// rust_decimal supports integer powers
|
||||
Decimal::from_i32(10).unwrap().powu(decimals as u64)
|
||||
// After clamping, powu is safe and exact.
|
||||
dec!(10).powu(clamp_decimals(decimals) as u64)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
|
@ -151,11 +233,22 @@ pub fn apy_30d_bp_for_token(conn: &Connection, token_id: &str, now: i64) -> Resu
|
|||
e
|
||||
})?;
|
||||
|
||||
// Percent → basis points (100 bp = 1%). If it won’t fit into i64, surface an error.
|
||||
let bp_dec = (apy_dec * Decimal::from_i32(100).unwrap()).round();
|
||||
let apy_bp = bp_dec
|
||||
.to_i64()
|
||||
.ok_or_else(|| anyhow!("APY bp overflow for {}", token_id))?;
|
||||
// Percent → basis points (100 bp = 1%)
|
||||
let hundred = dec!(100);
|
||||
|
||||
let bp_dec = match apy_dec.checked_mul(hundred) {
|
||||
Some(v) => v.round(),
|
||||
None => {
|
||||
// clamp on overflow instead of panicking
|
||||
return Ok(i64::MAX);
|
||||
}
|
||||
};
|
||||
|
||||
let apy_bp = match bp_dec.to_i64() {
|
||||
Some(v) => v,
|
||||
None if bp_dec.is_sign_negative() => i64::MIN,
|
||||
None => i64::MAX,
|
||||
};
|
||||
Ok(apy_bp)
|
||||
}
|
||||
|
||||
|
|
|
|||
43
src/main.rs
43
src/main.rs
|
|
@ -15,7 +15,7 @@ use rocket::{launch, routes};
|
|||
use rocket_cors::{AllowedHeaders, AllowedOrigins};
|
||||
use rpc::ResponseCache;
|
||||
use rusqlite::OpenFlags;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::{
|
||||
backtrace::Backtrace,
|
||||
collections::HashMap,
|
||||
|
|
@ -23,7 +23,7 @@ use std::{
|
|||
path::Path,
|
||||
process,
|
||||
sync::{Arc, Mutex},
|
||||
thread::{self, sleep},
|
||||
thread::sleep,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use stderrlog::LogLevelNum;
|
||||
|
|
@ -136,7 +136,6 @@ fn start_program(
|
|||
) -> Result<(DB, BCMRDownloader, WellKnownDownloader, CRC20Fetcher)> {
|
||||
let create_db_pool = |db_path| -> (bool, DBPool, DBPool) {
|
||||
let db_exists = Path::new(db_path).exists();
|
||||
|
||||
info!("Initializing connection to {db_path}");
|
||||
|
||||
let write_manager = r2d2_sqlite::SqliteConnectionManager::file(db_path)
|
||||
|
|
@ -258,12 +257,12 @@ fn start_program(
|
|||
|
||||
let indexing_in_progress_clone = indexing_in_progress.clone();
|
||||
|
||||
thread::spawn(move || {
|
||||
std::thread::spawn(move || {
|
||||
let db = db_cpy;
|
||||
|
||||
// Initial full index
|
||||
let mut tip: BlockHash = loop {
|
||||
indexing_in_progress_clone.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
indexing_in_progress_clone.store(true, Ordering::Relaxed);
|
||||
break match index_blocks(chain.clone(), db.clone(), client.clone(), true) {
|
||||
Ok(tip) => tip,
|
||||
Err(e) => {
|
||||
|
|
@ -275,22 +274,21 @@ fn start_program(
|
|||
}
|
||||
};
|
||||
};
|
||||
indexing_in_progress_clone.store(false, Ordering::Relaxed);
|
||||
|
||||
indexing_in_progress_clone.store(false, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
// Follow chain
|
||||
loop {
|
||||
let new_tip = match electrum_get_tip(&client.lock().unwrap()) {
|
||||
Ok(t) => t.0.block_hash(),
|
||||
Err(e) => {
|
||||
warn!("Failed to get block chain tip from electrum: {e}");
|
||||
thread::sleep(Duration::from_secs(5));
|
||||
warn!("Failed to get chain tip from electrum: {e}");
|
||||
std::thread::sleep(Duration::from_secs(5));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if new_tip != tip {
|
||||
indexing_in_progress_clone.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
indexing_in_progress_clone.store(true, Ordering::Relaxed);
|
||||
tip = match index_blocks(chain.clone(), db.clone(), client.clone(), true) {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
|
|
@ -298,15 +296,18 @@ fn start_program(
|
|||
tip
|
||||
}
|
||||
};
|
||||
indexing_in_progress_clone.store(false, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
indexing_in_progress_clone.store(false, std::sync::atomic::Ordering::Relaxed);
|
||||
// Avoid overlapping writer while indexer is on
|
||||
if !indexing_in_progress_clone.load(Ordering::Relaxed) {
|
||||
if let Err(e) =
|
||||
update_mempool(db.cauldron_w.clone(), db.oracle_w.clone(), client.clone())
|
||||
{
|
||||
error!("Failed to update mempool: {e}");
|
||||
}
|
||||
}
|
||||
if let Err(e) =
|
||||
update_mempool(db.cauldron_w.clone(), db.oracle_w.clone(), client.clone())
|
||||
{
|
||||
error!("Failed to update mempool: {e}");
|
||||
}
|
||||
thread::sleep(Duration::from_secs(5));
|
||||
std::thread::sleep(Duration::from_secs(5));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -316,6 +317,8 @@ fn start_program(
|
|||
let mut wellknowndownloader = WellKnownDownloader::new(db.bcmr_w.clone());
|
||||
wellknowndownloader.start()?;
|
||||
|
||||
spawn_token_metrics_updater(db.clone(), indexing_in_progress.clone());
|
||||
|
||||
Ok((db, bcmrdownloader, wellknowndownloader, crc20fetcher))
|
||||
}
|
||||
|
||||
|
|
@ -364,8 +367,6 @@ fn launch() -> _ {
|
|||
create_cached_token_metrics_table(&conn).expect("ensure cached_token_metrics exists");
|
||||
}
|
||||
|
||||
spawn_token_metrics_updater(dbpool.clone());
|
||||
|
||||
rocket::build()
|
||||
.manage(dbpool)
|
||||
.manage(response_cache)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@
|
|||
// This software is licensed under the GNU Affero General Public License (AGPL), version 3.0 or later.
|
||||
// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/agpl-3.0.html
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use malachite::base::num::arithmetic::traits::FloorSqrt;
|
||||
use malachite::Integer;
|
||||
use rust_decimal::MathematicalOps;
|
||||
use rust_decimal::{prelude::FromPrimitive, Decimal};
|
||||
use rust_decimal_macros::dec;
|
||||
|
|
@ -18,8 +20,8 @@ pub struct PoolPeriod {
|
|||
pub start: PoolSnapshot,
|
||||
pub end: PoolSnapshot,
|
||||
|
||||
start_k: Decimal,
|
||||
end_k: Decimal,
|
||||
start_k: Integer,
|
||||
end_k: Integer,
|
||||
}
|
||||
|
||||
impl PoolPeriod {
|
||||
|
|
@ -29,14 +31,12 @@ impl PoolPeriod {
|
|||
"start timestamp ({}) > end timestamp ({})",
|
||||
start.timestamp,
|
||||
end.timestamp
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
let start_k = Decimal::from_u64(start.sats * start.token_amount)
|
||||
.context("failed to convert inital_k to decimal")?;
|
||||
|
||||
let end_k = Decimal::from_u64(end.sats * end.token_amount)
|
||||
.context("failed to convert final_k to decimal")?;
|
||||
// Multiply with big-int to avoid u64 overflow, then downcast to Decimal.
|
||||
let start_k = Integer::from(start.sats) * Integer::from(start.token_amount);
|
||||
let end_k = Integer::from(end.sats) * Integer::from(end.token_amount);
|
||||
|
||||
Ok(Self {
|
||||
start,
|
||||
|
|
@ -72,40 +72,79 @@ impl PoolPeriod {
|
|||
}
|
||||
|
||||
pub fn days_over_year(&self, starting: &Option<u64>) -> Result<Decimal> {
|
||||
// If duration_days is 0, checked_div returns None → default to 0
|
||||
Ok(DAYS_IN_YEAR
|
||||
.checked_div(self.duration_days(starting)?)
|
||||
.unwrap_or_default())
|
||||
}
|
||||
|
||||
pub fn end_k_sqrt(&self) -> Result<Decimal> {
|
||||
self.end_k.sqrt().context("failed to sqrt end")
|
||||
fn sqrt_integer_as_decimal(k: &Integer) -> anyhow::Result<Decimal> {
|
||||
use std::str::FromStr;
|
||||
let s: Integer = k.clone().floor_sqrt();
|
||||
if s == 0u8 {
|
||||
return Ok(Decimal::ZERO);
|
||||
}
|
||||
let r: Integer = k - &(&s * &s); // r = k - s^2 (exact)
|
||||
let s_dec = Decimal::from_str(&s.to_string())?;
|
||||
let r_dec = Decimal::from_str(&r.to_string())?;
|
||||
// sqrt(k) ≈ s + r/(2s) (first-order correction)
|
||||
let corr = r_dec
|
||||
.checked_div(s_dec * dec!(2))
|
||||
.ok_or_else(|| anyhow::anyhow!("div"))?;
|
||||
s_dec
|
||||
.checked_add(corr)
|
||||
.ok_or_else(|| anyhow::anyhow!("add"))
|
||||
}
|
||||
|
||||
pub fn pool_yield(&self) -> Result<Decimal> {
|
||||
let start_k_sr = self.start_k.sqrt().context("failed to sqrt start")?;
|
||||
let end_k_sr = self.end_k.sqrt().context("failed to sqrt end")?;
|
||||
pub fn end_k_sqrt(&self) -> anyhow::Result<Decimal> {
|
||||
Self::sqrt_integer_as_decimal(&self.end_k)
|
||||
}
|
||||
|
||||
Ok(((end_k_sr - start_k_sr) / start_k_sr) * Decimal::ONE_HUNDRED)
|
||||
pub fn pool_yield(&self) -> anyhow::Result<Decimal> {
|
||||
let start_k_sr = Self::sqrt_integer_as_decimal(&self.start_k)?;
|
||||
let end_k_sr = Self::sqrt_integer_as_decimal(&self.end_k)?;
|
||||
if start_k_sr.is_zero() {
|
||||
anyhow::bail!("start sqrt is zero; division by zero");
|
||||
}
|
||||
let num = end_k_sr
|
||||
.checked_sub(start_k_sr)
|
||||
.ok_or_else(|| anyhow::anyhow!("sub"))?;
|
||||
let ratio = num
|
||||
.checked_div(start_k_sr)
|
||||
.ok_or_else(|| anyhow::anyhow!("div"))?;
|
||||
ratio
|
||||
.checked_mul(Decimal::ONE_HUNDRED)
|
||||
.ok_or_else(|| anyhow::anyhow!("mul"))
|
||||
}
|
||||
|
||||
pub fn yield_and_apy(&self, starting: &Option<u64>) -> Result<(Decimal, Decimal)> {
|
||||
let pool_yield = self.pool_yield()?;
|
||||
let years_elapsed = self.days_over_year(starting)?; // > 0 means at least some duration
|
||||
|
||||
let years_elapsed = self.days_over_year(starting)?;
|
||||
|
||||
// to avoid powd overflow; don't calculate for pools < 6 hour old
|
||||
if self.duration(starting) < 3600 * 6 {
|
||||
// avoid powd blowups for very short periods (< 6h)
|
||||
if self.duration(starting) < 6 * 3600 {
|
||||
return Ok((pool_yield, Decimal::ZERO));
|
||||
}
|
||||
|
||||
let apy = if years_elapsed.is_zero() {
|
||||
Decimal::ZERO
|
||||
} else {
|
||||
(((pool_yield / Decimal::ONE_HUNDRED) + Decimal::ONE)
|
||||
// APY = ((1 + y)^years - 1) * 100, with overflow guards
|
||||
let one_plus = (pool_yield
|
||||
.checked_div(Decimal::ONE_HUNDRED)
|
||||
.ok_or_else(|| anyhow!("divide by 100 overflow"))?)
|
||||
.checked_add(Decimal::ONE)
|
||||
.ok_or_else(|| anyhow!("1 + yield overflow"))?;
|
||||
|
||||
let powd = one_plus
|
||||
.checked_powd(years_elapsed)
|
||||
.context("powd overflow")?
|
||||
- Decimal::ONE)
|
||||
* Decimal::ONE_HUNDRED
|
||||
.context("powd overflow")?;
|
||||
let minus_one = powd
|
||||
.checked_sub(Decimal::ONE)
|
||||
.ok_or_else(|| anyhow!("pow - 1 underflow"))?;
|
||||
minus_one
|
||||
.checked_mul(Decimal::ONE_HUNDRED)
|
||||
.ok_or_else(|| anyhow!("apy * 100 overflow"))?
|
||||
};
|
||||
|
||||
Ok((pool_yield, apy))
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue