From 3d911bb33ceb83d9669ccbfa65791be8e6c7f749 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Thu, 4 Nov 2021 08:25:48 -0700 Subject: [PATCH 01/33] Add method for getting twap --- examples/get_accounts.rs | 21 ++++++++++++++++----- src/lib.rs | 21 +++++++++++++++++---- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/examples/get_accounts.rs b/examples/get_accounts.rs index 4e57442..f9e3976 100644 --- a/examples/get_accounts.rs +++ b/examples/get_accounts.rs @@ -108,17 +108,18 @@ fn main() { let pd = clnt.get_account_data( &px_pkey ).unwrap(); let pa = cast::( &pd ); - let maybe_price = pa.get_current_price(); assert_eq!( pa.magic, MAGIC, "not a valid pyth account" ); assert_eq!( pa.atype, AccountType::Price as u32, "not a valid pyth price account" ); assert_eq!( pa.ver, VERSION_2, "unexpected pyth price account version" ); println!( " price_account .. {:?}", px_pkey ); + + let maybe_price = pa.get_current_price(); match maybe_price { - Some((price, confidence, _)) => { - println!(" price ........ {}", price); - println!(" conf ......... {}", confidence); + Some((price, confidence, expo)) => { + println!(" price ........ {} x 10^{}", price, expo); + println!(" conf ......... {} x 10^{}", confidence, expo); } None => { println!(" price ........ unavailable"); @@ -134,7 +135,17 @@ fn main() { println!( " num_qt ....... {}", pa.num_qt ); println!( " valid_slot ... {}", pa.valid_slot ); println!( " publish_slot . {}", pa.agg.pub_slot ); - println!( " twap ......... {}", pa.twap.val ); + + let maybe_twap = pa.get_twap(); + match maybe_twap { + Some((twap, expo)) => { + println!( " twap ......... {} x 10^{}", twap, expo ); + } + None => { + println!( " twap ......... unavailable"); + } + } + println!( " twac ......... {}", pa.twac.val ); // go to next price account in list diff --git a/src/lib.rs b/src/lib.rs index c81c968..4ce5cbd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -133,10 +133,9 @@ pub struct Price impl Price { /** - * Get the current price and confidence interval as fixed-point numbers. Returns a triple of - * the current price, confidence interval, and the exponent for both numbers (i.e., the number - * of decimal places.) - * For example: + * Get the current price and confidence interval as fixed-point numbers of the form a * 10^e. + * Returns a triple of the current price, confidence interval, and the exponent for both + * numbers. For example: * * get_current_price() -> Some((12345, 267, -2)) // represents 123.45 +- 2.67 * get_current_price() -> Some((123, 1, 2)) // represents 12300 +- 100 @@ -150,6 +149,20 @@ impl Price { Some((self.agg.price, self.agg.conf, self.expo)) } } + + /** + * Get the time-weighted average price (TWAP) as a fixed point number of the form a * 10^e. + * Returns a tuple of the current twap and its exponent. For example: + * + * get_twap() -> Some((123, -2)) // represents 1.23 + * get_twap() -> Some((45, 3)) // represents 45000 + * + * Returns None if the twap is currently unavailable. + */ + pub fn get_twap(&self) -> Option<(i64, i32)> { + // This method currently cannot return None, but may do so in the future. + Some((self.twap.val, self.expo)) + } } struct AccKeyU64 From a0c226171fe3eef406c1b03f0054af2b51b4bd80 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Wed, 15 Dec 2021 14:09:43 -0800 Subject: [PATCH 02/33] initial implementation, seems to work --- src/lib.rs | 191 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 4ce5cbd..6fface4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,11 @@ pub const PROD_ACCT_SIZE : usize = 512; pub const PROD_HDR_SIZE : usize = 48; pub const PROD_ATTR_SIZE : usize = PROD_ACCT_SIZE - PROD_HDR_SIZE; +const PD_EXPO: i32 = -9; +const PD_SCALE: u64 = 1000000000; +const MAX_PD_V_I64: i64 = (1 << 28) - 1; +const MAX_PD_V_U64: u64 = (1 << 28) - 1; + // each account has its own type #[repr(C)] pub enum AccountType @@ -86,6 +91,16 @@ pub struct PriceInfo pub pub_slot : u64 } +impl PriceInfo { + pub fn get_checked(&self) -> Option<(i64, u64)> { + if !matches!(self.status, PriceStatus::Trading) { + None + } else { + Some((self.price, self.conf)) + } + } +} + // latest component price and price used in aggregate snapshot #[repr(C)] pub struct PriceComp @@ -182,3 +197,179 @@ impl AccKey return k8.val[0]!=0 || k8.val[1]!=0 || k8.val[2]!=0 || k8.val[3]!=0; } } + +/** + * Given the price accounts for the products X/Z and Y/Z, return the current price for X/Y. + * The value returned by this method has the same semantics as Price::get_current_price above. + * + * `result_expo` determines the exponent of the result, i.e., the number of digits of precision in + * the price. For any given base/quote pair, the minimum possible exponent is + * `-9 + base.exponent - quote.exponent`. + */ +pub fn get_base_in_quote(base: Price, quote: Price, result_expo: i32) -> Option<(i64, u64, i32)> { + return rebase_price_info(base.agg, base.expo, quote.agg, quote.expo, result_expo); +} + +// Helper fn for rebase that is extracted for testing purposes. +fn rebase_price_info( + base_info: PriceInfo, + base_expo: i32, + quote_info: PriceInfo, + quote_expo: i32, + result_expo: i32, +) -> Option<(i64, u64, i32)> { + return match base_info.get_checked() { + Some((base_price, base_confidence)) => + match quote_info.get_checked() { + Some((quote_price, quote_confidence)) => { + // Note that this assertion implies that the prices can be cast to u64. + // We need prices as u64 in order to divide, as solana doesn't implement signed division. + // It's also extremely unclear what this method should do if one of the prices is negative, + // so assuming positive prices throughout seems fine. + assert!(base_price >= 0 && base_price <= MAX_PD_V_I64); + assert!(quote_price > 0 && quote_price <= MAX_PD_V_I64); + let base_price = base_price as u64; + let quote_price = quote_price as u64; + + assert!(base_confidence <= MAX_PD_V_U64); + assert!(quote_confidence <= MAX_PD_V_U64); + + println!("base ({} +- {}) * 10^{}", base_price, base_confidence, base_expo); + println!("quote ({} +- {}) * 10^{}", quote_price, quote_confidence, quote_expo); + + // Compute the midprice, base in terms of quote. + let midprice = (base_price * PD_SCALE) / quote_price; + let midprice_expo = PD_EXPO + base_expo - quote_expo; + println!("mean {} * 10^{}", midprice, midprice_expo); + assert!(result_expo >= midprice_expo); + + // Compute the confidence interval. + // This code uses the 1-norm instead of the 2-norm for computational reasons. + // The correct formula is midprice * sqrt(c_1^2 + c_2^2), where c_1 and c_2 are the + // confidence intervals in price-percentage terms of the base and quote. This quantity + // is difficult to compute due to the sqrt, and overflow/underflow considerations. + // Instead, this code uses midprice * (c_1 + c_2). + // This quantity is at most a factor of sqrt(2) greater than the correct result, which + // shouldn't matter considering that confidence intervals are typically ~0.1% of the price. + + // The exponent is PD_EXPO for both of these. + let base_confidence_pct = (base_confidence * PD_SCALE) / base_price; + let quote_confidence_pct = (quote_confidence * PD_SCALE) / quote_price; + + // Need to rescale the numbers to prevent the multiplication from overflowing + let (rescaled_z, rescaled_z_expo) = rescale_num(base_confidence_pct + quote_confidence_pct, PD_EXPO); + println!("rescaled_z {} * 10^{}", rescaled_z, rescaled_z_expo); + let (rescaled_mid, rescaled_mid_expo) = rescale_num(midprice, midprice_expo); + println!("rescaled_mean {} * 10^{}", rescaled_mid, rescaled_mid_expo); + let conf = (rescaled_z * rescaled_mid); + let conf_expo = rescaled_z_expo + rescaled_mid_expo; + println!("conf {} * 10^{}", conf, conf_expo); + + // Scale results to the target exponent. + let midprice_in_result_expo = scale_to_exponent(midprice, midprice_expo, result_expo); + let conf_in_result_expo = scale_to_exponent(conf, conf_expo, result_expo); + let midprice_i64 = midprice_in_result_expo as i64; + assert!(midprice_i64 >= 0); + + Some((midprice_i64, conf_in_result_expo, result_expo)) + }, + None => None, + } + None => None, + } +} + +/** Scale num and its exponent such that it is < MAX_PD_V_U64 + * (which guarantees that multiplication doesn't overflow). + */ +pub fn rescale_num( + num: u64, + expo: i32, +) -> (u64, i32) { + let mut p: u64 = num; + let mut c: i32 = 0; + + while p > MAX_PD_V_U64 { + p = p / 10; + c += 1; + } + + println!("c: {}", c); + + return (p, expo + c); +} + +/** Scale num so that its exponent is target_expo. + * This method can only reduce precision, i.e., target_expo must be > current_expo. + */ +pub fn scale_to_exponent( + num: u64, + current_expo: i32, + target_expo: i32, +) -> u64 { + let mut delta = target_expo - current_expo; + let mut res = num; + assert!(delta >= 0); + + while delta > 0 { + res /= 10; + delta -= 1; + } + + return res; +} + +#[cfg(test)] +mod test { + use crate::{Price, PriceStatus, PriceInfo, PriceType, AccountType, MAGIC, VERSION, CorpAction, rebase_price_info, MAX_PD_V_I64, MAX_PD_V_U64}; + + fn mock_price_info(price: i64, conf: u64, status: PriceStatus) -> PriceInfo { + return PriceInfo { + price, + conf, + status, + corp_act: CorpAction::NoCorpAct, + pub_slot: 0, + } + } + + #[test] + fn test_rebase() { + fn run_test( + price1: (i64, u64, i32), + price2: (i64, u64, i32), + result_expo: i32, + expected: (i64, u64), + ) { + let pinfo1 = mock_price_info(price1.0, price1.1, PriceStatus::Trading); + let pinfo2 = mock_price_info(price2.0, price2.1, PriceStatus::Trading); + let result = rebase_price_info(pinfo1, price1.2, pinfo2, price2.2, result_expo); + assert_eq!(result, Some((expected.0, expected.1, result_expo))); + } + + run_test((1, 1, 0), (1, 1, 0), 0, (1, 2)); + run_test((10, 1, 0), (1, 1, 0), 0, (10, 11)); + run_test((1, 1, 1), (1, 1, 0), 0, (10, 20)); + run_test((1, 1, 0), (5, 1, 0), 0, (0, 0)); + run_test((1, 1, 0), (5, 1, 0), -2, (20, 24)); + + // The maximum price / confidence value that can appear in on-chain decimals. + run_test((MAX_PD_V_I64, MAX_PD_V_U64, 0), (MAX_PD_V_I64, MAX_PD_V_U64, 0), 0, (1, 2)); + run_test((MAX_PD_V_I64, MAX_PD_V_U64, 0), (1, 1, 0), 0, (MAX_PD_V_I64, 2 * MAX_PD_V_U64)); + run_test((1, MAX_PD_V_U64, 0), (1, MAX_PD_V_U64, 0), 0, (1, 2 * MAX_PD_V_U64)); + + // Test error cases at + + // TODO: need tests at the edges of the capacity of PD + + + + // run_test((100, 1, -2), (1, 1, 0), 1, (2, 1)); + + + // run_test(100, 5, 0, 10, 1, 0, 10, 2, 1); + + // run_test(1000, 1, 0, 100, 1, 0, 10, 1, 0); + // run_test(1000, 1, 0, 100, 1, 0, 100, 1, 0); + } +} \ No newline at end of file From 3563003a3b893fca0f40e65fd5488643748d18b4 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Fri, 17 Dec 2021 06:16:59 -0800 Subject: [PATCH 03/33] minor --- src/lib.rs | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6fface4..3f4d237 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -321,7 +321,7 @@ pub fn scale_to_exponent( #[cfg(test)] mod test { - use crate::{Price, PriceStatus, PriceInfo, PriceType, AccountType, MAGIC, VERSION, CorpAction, rebase_price_info, MAX_PD_V_I64, MAX_PD_V_U64}; + use crate::{AccountType, CorpAction, MAGIC, MAX_PD_V_I64, MAX_PD_V_U64, Price, PriceInfo, PriceStatus, PriceType, rebase_price_info, VERSION}; fn mock_price_info(price: i64, conf: u64, status: PriceStatus) -> PriceInfo { return PriceInfo { @@ -353,23 +353,15 @@ mod test { run_test((1, 1, 0), (5, 1, 0), 0, (0, 0)); run_test((1, 1, 0), (5, 1, 0), -2, (20, 24)); - // The maximum price / confidence value that can appear in on-chain decimals. + // Test with end range of possible inputs to check for overflow. run_test((MAX_PD_V_I64, MAX_PD_V_U64, 0), (MAX_PD_V_I64, MAX_PD_V_U64, 0), 0, (1, 2)); run_test((MAX_PD_V_I64, MAX_PD_V_U64, 0), (1, 1, 0), 0, (MAX_PD_V_I64, 2 * MAX_PD_V_U64)); run_test((1, MAX_PD_V_U64, 0), (1, MAX_PD_V_U64, 0), 0, (1, 2 * MAX_PD_V_U64)); - // Test error cases at - // TODO: need tests at the edges of the capacity of PD + // TODO: Test non-trading cases - - // run_test((100, 1, -2), (1, 1, 0), 1, (2, 1)); - - - // run_test(100, 5, 0, 10, 1, 0, 10, 2, 1); - - // run_test(1000, 1, 0, 100, 1, 0, 10, 1, 0); - // run_test(1000, 1, 0, 100, 1, 0, 100, 1, 0); + // TODO: test cases where the exponents are dramatically different } } \ No newline at end of file From 5a3b99384bc2373fcbe5adf41eee680cff96ca67 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Fri, 17 Dec 2021 06:40:23 -0800 Subject: [PATCH 04/33] minor --- src/lib.rs | 299 +++++++++++++++++++++++++++-------------------------- 1 file changed, 152 insertions(+), 147 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 3f4d237..9715081 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -157,11 +157,39 @@ impl Price { * * Returns None if price information is currently unavailable. */ - pub fn get_current_price(&self) -> Option<(i64, u64, i32)> { + pub fn get_current_price(&self) -> Option { if !matches!(self.agg.status, PriceStatus::Trading) { None } else { - Some((self.agg.price, self.agg.conf, self.expo)) + Some(PriceConf { + price: self.agg.price, + conf: self.agg.conf, + expo: self.expo + }) + } + } + + /** + * Get the current price of this account in a different quote currency. If this account + * represents the price of the product X/Z, and `quote` represents the price of the product Y/Z, + * this method returns the price of X/Y. Use this method to get the price of e.g., mSOL/SOL from + * the mSOL/USD and SOL/USD accounts. + * + * The value returned by this method has the same semantics as Price::get_current_price above. + * + * `result_expo` determines the exponent of the result, i.e., the number of digits of precision in + * the price. For any given base/quote pair, the minimum possible exponent is + * `-9 + base.exponent - quote.exponent`. + */ + pub fn get_price_in_quote(&self, quote: Price, result_expo: i32) -> Option { + return match self.get_current_price() { + Some(base_price_conf) => + match quote.get_current_price() { + Some(quote_price_conf) => + Some(base_price_conf.div(quote_price_conf, result_expo)); + None => None + } + None => None } } @@ -180,6 +208,116 @@ impl Price { } } +pub struct PriceConf { + pub price: i64, + pub conf: u64, + pub expo: i32, +} + +impl PriceConf { + /** + * Divides this price and confidence interval by y. + * + * The result is returned with result_exp + */ + pub fn div(&self, quote: PriceConf, result_expo: i32) -> PriceConf { + // Note that this assertion implies that the prices can be cast to u64. + // We need prices as u64 in order to divide, as solana doesn't implement signed division. + // It's also extremely unclear what this method should do if one of the prices is negative, + // so assuming positive prices throughout seems fine. + assert!(self.price >= 0 && self.price <= MAX_PD_V_I64); + assert!(quote.price > 0 && quote.price <= MAX_PD_V_I64); + let base_price = self.price as u64; + let quote_price = quote.price as u64; + + assert!(self.conf <= MAX_PD_V_U64); + assert!(quote.conf <= MAX_PD_V_U64); + + println!("base ({} +- {}) * 10^{}", base_price, self.conf, self.expo); + println!("quote ({} +- {}) * 10^{}", quote_price, quote.conf, quote.expo); + + // Compute the midprice, base in terms of quote. + let midprice = (base_price * PD_SCALE) / quote_price; + let midprice_expo = PD_EXPO + self.expo - quote.expo; + println!("mean {} * 10^{}", midprice, midprice_expo); + assert!(result_expo >= midprice_expo); + + // Compute the confidence interval. + // This code uses the 1-norm instead of the 2-norm for computational reasons. + // The correct formula is midprice * sqrt(c_1^2 + c_2^2), where c_1 and c_2 are the + // confidence intervals in price-percentage terms of the base and quote. This quantity + // is difficult to compute due to the sqrt, and overflow/underflow considerations. + // Instead, this code uses midprice * (c_1 + c_2). + // This quantity is at most a factor of sqrt(2) greater than the correct result, which + // shouldn't matter considering that confidence intervals are typically ~0.1% of the price. + + // The exponent is PD_EXPO for both of these. + let base_confidence_pct = (self.conf * PD_SCALE) / base_price; + let quote_confidence_pct = (quote.conf * PD_SCALE) / quote_price; + + // Need to rescale the numbers to prevent the multiplication from overflowing + let (rescaled_z, rescaled_z_expo) = rescale_num(base_confidence_pct + quote_confidence_pct, PD_EXPO); + println!("rescaled_z {} * 10^{}", rescaled_z, rescaled_z_expo); + let (rescaled_mid, rescaled_mid_expo) = rescale_num(midprice, midprice_expo); + println!("rescaled_mean {} * 10^{}", rescaled_mid, rescaled_mid_expo); + let conf = (rescaled_z * rescaled_mid); + let conf_expo = rescaled_z_expo + rescaled_mid_expo; + println!("conf {} * 10^{}", conf, conf_expo); + + // Scale results to the target exponent. + let midprice_in_result_expo = scale_to_exponent(midprice, midprice_expo, result_expo); + let conf_in_result_expo = scale_to_exponent(conf, conf_expo, result_expo); + let midprice_i64 = midprice_in_result_expo as i64; + assert!(midprice_i64 >= 0); + + PriceConf { + price: midprice_i64, + conf: conf_in_result_expo, + expo: result_expo + } + } + + /** Scale num and its exponent such that it is < MAX_PD_V_U64 + * (which guarantees that multiplication doesn't overflow). + */ + fn rescale_num( + num: u64, + expo: i32, + ) -> (u64, i32) { + let mut p: u64 = num; + let mut c: i32 = 0; + + while p > MAX_PD_V_U64 { + p = p / 10; + c += 1; + } + + println!("c: {}", c); + + return (p, expo + c); + } + + /** Scale num so that its exponent is target_expo. + * This method can only reduce precision, i.e., target_expo must be > current_expo. + */ + fn scale_to_exponent( + num: u64, + current_expo: i32, + target_expo: i32, + ) -> u64 { + let mut delta = target_expo - current_expo; + let mut res = num; + assert!(delta >= 0); + + while delta > 0 { + res /= 10; + delta -= 1; + } + + return res; + } +} + struct AccKeyU64 { pub val: [u64;4] @@ -198,165 +336,32 @@ impl AccKey } } -/** - * Given the price accounts for the products X/Z and Y/Z, return the current price for X/Y. - * The value returned by this method has the same semantics as Price::get_current_price above. - * - * `result_expo` determines the exponent of the result, i.e., the number of digits of precision in - * the price. For any given base/quote pair, the minimum possible exponent is - * `-9 + base.exponent - quote.exponent`. - */ -pub fn get_base_in_quote(base: Price, quote: Price, result_expo: i32) -> Option<(i64, u64, i32)> { - return rebase_price_info(base.agg, base.expo, quote.agg, quote.expo, result_expo); -} - -// Helper fn for rebase that is extracted for testing purposes. -fn rebase_price_info( - base_info: PriceInfo, - base_expo: i32, - quote_info: PriceInfo, - quote_expo: i32, - result_expo: i32, -) -> Option<(i64, u64, i32)> { - return match base_info.get_checked() { - Some((base_price, base_confidence)) => - match quote_info.get_checked() { - Some((quote_price, quote_confidence)) => { - // Note that this assertion implies that the prices can be cast to u64. - // We need prices as u64 in order to divide, as solana doesn't implement signed division. - // It's also extremely unclear what this method should do if one of the prices is negative, - // so assuming positive prices throughout seems fine. - assert!(base_price >= 0 && base_price <= MAX_PD_V_I64); - assert!(quote_price > 0 && quote_price <= MAX_PD_V_I64); - let base_price = base_price as u64; - let quote_price = quote_price as u64; - - assert!(base_confidence <= MAX_PD_V_U64); - assert!(quote_confidence <= MAX_PD_V_U64); - - println!("base ({} +- {}) * 10^{}", base_price, base_confidence, base_expo); - println!("quote ({} +- {}) * 10^{}", quote_price, quote_confidence, quote_expo); - - // Compute the midprice, base in terms of quote. - let midprice = (base_price * PD_SCALE) / quote_price; - let midprice_expo = PD_EXPO + base_expo - quote_expo; - println!("mean {} * 10^{}", midprice, midprice_expo); - assert!(result_expo >= midprice_expo); - - // Compute the confidence interval. - // This code uses the 1-norm instead of the 2-norm for computational reasons. - // The correct formula is midprice * sqrt(c_1^2 + c_2^2), where c_1 and c_2 are the - // confidence intervals in price-percentage terms of the base and quote. This quantity - // is difficult to compute due to the sqrt, and overflow/underflow considerations. - // Instead, this code uses midprice * (c_1 + c_2). - // This quantity is at most a factor of sqrt(2) greater than the correct result, which - // shouldn't matter considering that confidence intervals are typically ~0.1% of the price. - - // The exponent is PD_EXPO for both of these. - let base_confidence_pct = (base_confidence * PD_SCALE) / base_price; - let quote_confidence_pct = (quote_confidence * PD_SCALE) / quote_price; - - // Need to rescale the numbers to prevent the multiplication from overflowing - let (rescaled_z, rescaled_z_expo) = rescale_num(base_confidence_pct + quote_confidence_pct, PD_EXPO); - println!("rescaled_z {} * 10^{}", rescaled_z, rescaled_z_expo); - let (rescaled_mid, rescaled_mid_expo) = rescale_num(midprice, midprice_expo); - println!("rescaled_mean {} * 10^{}", rescaled_mid, rescaled_mid_expo); - let conf = (rescaled_z * rescaled_mid); - let conf_expo = rescaled_z_expo + rescaled_mid_expo; - println!("conf {} * 10^{}", conf, conf_expo); - - // Scale results to the target exponent. - let midprice_in_result_expo = scale_to_exponent(midprice, midprice_expo, result_expo); - let conf_in_result_expo = scale_to_exponent(conf, conf_expo, result_expo); - let midprice_i64 = midprice_in_result_expo as i64; - assert!(midprice_i64 >= 0); - - Some((midprice_i64, conf_in_result_expo, result_expo)) - }, - None => None, - } - None => None, - } -} - -/** Scale num and its exponent such that it is < MAX_PD_V_U64 - * (which guarantees that multiplication doesn't overflow). - */ -pub fn rescale_num( - num: u64, - expo: i32, -) -> (u64, i32) { - let mut p: u64 = num; - let mut c: i32 = 0; - - while p > MAX_PD_V_U64 { - p = p / 10; - c += 1; - } - - println!("c: {}", c); - - return (p, expo + c); -} - -/** Scale num so that its exponent is target_expo. - * This method can only reduce precision, i.e., target_expo must be > current_expo. - */ -pub fn scale_to_exponent( - num: u64, - current_expo: i32, - target_expo: i32, -) -> u64 { - let mut delta = target_expo - current_expo; - let mut res = num; - assert!(delta >= 0); - - while delta > 0 { - res /= 10; - delta -= 1; - } - - return res; -} - #[cfg(test)] mod test { - use crate::{AccountType, CorpAction, MAGIC, MAX_PD_V_I64, MAX_PD_V_U64, Price, PriceInfo, PriceStatus, PriceType, rebase_price_info, VERSION}; - - fn mock_price_info(price: i64, conf: u64, status: PriceStatus) -> PriceInfo { - return PriceInfo { - price, - conf, - status, - corp_act: CorpAction::NoCorpAct, - pub_slot: 0, - } - } + use crate::{AccountType, CorpAction, MAGIC, MAX_PD_V_I64, MAX_PD_V_U64, Price, PriceInfo, PriceStatus, PriceType, rebase_price_info, VERSION, PriceConf}; #[test] fn test_rebase() { fn run_test( - price1: (i64, u64, i32), - price2: (i64, u64, i32), + price1: PriceConf, + price2: PriceConf, result_expo: i32, expected: (i64, u64), ) { - let pinfo1 = mock_price_info(price1.0, price1.1, PriceStatus::Trading); - let pinfo2 = mock_price_info(price2.0, price2.1, PriceStatus::Trading); - let result = rebase_price_info(pinfo1, price1.2, pinfo2, price2.2, result_expo); + let result = pinfo1.div(pinfo2, result_expo); assert_eq!(result, Some((expected.0, expected.1, result_expo))); } - run_test((1, 1, 0), (1, 1, 0), 0, (1, 2)); - run_test((10, 1, 0), (1, 1, 0), 0, (10, 11)); - run_test((1, 1, 1), (1, 1, 0), 0, (10, 20)); - run_test((1, 1, 0), (5, 1, 0), 0, (0, 0)); - run_test((1, 1, 0), (5, 1, 0), -2, (20, 24)); + run_test(PriceConf(1, 1, 0), PriceConf(1, 1, 0), 0, (1, 2)); + run_test(PriceConf(10, 1, 0), PriceConf(1, 1, 0), 0, (10, 11)); + run_test(PriceConf(1, 1, 1), PriceConf(1, 1, 0), 0, (10, 20)); + run_test(PriceConf(1, 1, 0), PriceConf(5, 1, 0), 0, (0, 0)); + run_test(PriceConf(1, 1, 0), PriceConf(5, 1, 0), -2, (20, 24)); // Test with end range of possible inputs to check for overflow. - run_test((MAX_PD_V_I64, MAX_PD_V_U64, 0), (MAX_PD_V_I64, MAX_PD_V_U64, 0), 0, (1, 2)); - run_test((MAX_PD_V_I64, MAX_PD_V_U64, 0), (1, 1, 0), 0, (MAX_PD_V_I64, 2 * MAX_PD_V_U64)); - run_test((1, MAX_PD_V_U64, 0), (1, MAX_PD_V_U64, 0), 0, (1, 2 * MAX_PD_V_U64)); + run_test(PriceConf(MAX_PD_V_I64, MAX_PD_V_U64, 0), PriceConf(MAX_PD_V_I64, MAX_PD_V_U64, 0), 0, (1, 2)); + run_test(PriceConf(MAX_PD_V_I64, MAX_PD_V_U64, 0), PriceConf(1, 1, 0), 0, (MAX_PD_V_I64, 2 * MAX_PD_V_U64)); + run_test(PriceConf(1, MAX_PD_V_U64, 0), PriceConf(1, MAX_PD_V_U64, 0), 0, (1, 2 * MAX_PD_V_U64)); // TODO: need tests at the edges of the capacity of PD From 1896da171d533040b7f960fe61c3f8e4983ae926 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Fri, 17 Dec 2021 06:47:56 -0800 Subject: [PATCH 05/33] refactor --- examples/get_accounts.rs | 6 ++--- src/lib.rs | 48 ++++++++++++++++++++++++---------------- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/examples/get_accounts.rs b/examples/get_accounts.rs index f9e3976..d26a0dd 100644 --- a/examples/get_accounts.rs +++ b/examples/get_accounts.rs @@ -117,9 +117,9 @@ fn main() { let maybe_price = pa.get_current_price(); match maybe_price { - Some((price, confidence, expo)) => { - println!(" price ........ {} x 10^{}", price, expo); - println!(" conf ......... {} x 10^{}", confidence, expo); + Some(p) => { + println!(" price ........ {} x 10^{}", p.price, p.expo); + println!(" conf ......... {} x 10^{}", p.conf, p.expo); } None => { println!(" price ........ unavailable"); diff --git a/src/lib.rs b/src/lib.rs index 9715081..186f36b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -186,10 +186,10 @@ impl Price { Some(base_price_conf) => match quote.get_current_price() { Some(quote_price_conf) => - Some(base_price_conf.div(quote_price_conf, result_expo)); - None => None + Some(base_price_conf.div(quote_price_conf, result_expo)), + None => None, } - None => None + None => None, } } @@ -208,6 +208,8 @@ impl Price { } } +#[derive(PartialEq)] +#[derive(Debug)] pub struct PriceConf { pub price: i64, pub conf: u64, @@ -256,17 +258,17 @@ impl PriceConf { let quote_confidence_pct = (quote.conf * PD_SCALE) / quote_price; // Need to rescale the numbers to prevent the multiplication from overflowing - let (rescaled_z, rescaled_z_expo) = rescale_num(base_confidence_pct + quote_confidence_pct, PD_EXPO); + let (rescaled_z, rescaled_z_expo) = PriceConf::rescale_num(base_confidence_pct + quote_confidence_pct, PD_EXPO); println!("rescaled_z {} * 10^{}", rescaled_z, rescaled_z_expo); - let (rescaled_mid, rescaled_mid_expo) = rescale_num(midprice, midprice_expo); + let (rescaled_mid, rescaled_mid_expo) = PriceConf::rescale_num(midprice, midprice_expo); println!("rescaled_mean {} * 10^{}", rescaled_mid, rescaled_mid_expo); - let conf = (rescaled_z * rescaled_mid); + let conf = rescaled_z * rescaled_mid; let conf_expo = rescaled_z_expo + rescaled_mid_expo; println!("conf {} * 10^{}", conf, conf_expo); // Scale results to the target exponent. - let midprice_in_result_expo = scale_to_exponent(midprice, midprice_expo, result_expo); - let conf_in_result_expo = scale_to_exponent(conf, conf_expo, result_expo); + let midprice_in_result_expo = PriceConf::scale_to_exponent(midprice, midprice_expo, result_expo); + let conf_in_result_expo = PriceConf::scale_to_exponent(conf, conf_expo, result_expo); let midprice_i64 = midprice_in_result_expo as i64; assert!(midprice_i64 >= 0); @@ -338,7 +340,15 @@ impl AccKey #[cfg(test)] mod test { - use crate::{AccountType, CorpAction, MAGIC, MAX_PD_V_I64, MAX_PD_V_U64, Price, PriceInfo, PriceStatus, PriceType, rebase_price_info, VERSION, PriceConf}; + use crate::{MAX_PD_V_I64, MAX_PD_V_U64, PriceConf}; + + fn pc(price: i64, conf: u64, expo: i32) -> PriceConf { + PriceConf { + price: price, + conf: conf, + expo: expo, + } + } #[test] fn test_rebase() { @@ -348,20 +358,20 @@ mod test { result_expo: i32, expected: (i64, u64), ) { - let result = pinfo1.div(pinfo2, result_expo); - assert_eq!(result, Some((expected.0, expected.1, result_expo))); + let result = price1.div(price2, result_expo); + assert_eq!(result, pc(expected.0, expected.1, result_expo)); } - run_test(PriceConf(1, 1, 0), PriceConf(1, 1, 0), 0, (1, 2)); - run_test(PriceConf(10, 1, 0), PriceConf(1, 1, 0), 0, (10, 11)); - run_test(PriceConf(1, 1, 1), PriceConf(1, 1, 0), 0, (10, 20)); - run_test(PriceConf(1, 1, 0), PriceConf(5, 1, 0), 0, (0, 0)); - run_test(PriceConf(1, 1, 0), PriceConf(5, 1, 0), -2, (20, 24)); + run_test(pc(1, 1, 0), pc(1, 1, 0), 0, (1, 2)); + run_test(pc(10, 1, 0), pc(1, 1, 0), 0, (10, 11)); + run_test(pc(1, 1, 1), pc(1, 1, 0), 0, (10, 20)); + run_test(pc(1, 1, 0), pc(5, 1, 0), 0, (0, 0)); + run_test(pc(1, 1, 0), pc(5, 1, 0), -2, (20, 24)); // Test with end range of possible inputs to check for overflow. - run_test(PriceConf(MAX_PD_V_I64, MAX_PD_V_U64, 0), PriceConf(MAX_PD_V_I64, MAX_PD_V_U64, 0), 0, (1, 2)); - run_test(PriceConf(MAX_PD_V_I64, MAX_PD_V_U64, 0), PriceConf(1, 1, 0), 0, (MAX_PD_V_I64, 2 * MAX_PD_V_U64)); - run_test(PriceConf(1, MAX_PD_V_U64, 0), PriceConf(1, MAX_PD_V_U64, 0), 0, (1, 2 * MAX_PD_V_U64)); + run_test(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), 0, (1, 2)); + run_test(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(1, 1, 0), 0, (MAX_PD_V_I64, 2 * MAX_PD_V_U64)); + run_test(pc(1, MAX_PD_V_U64, 0), pc(1, MAX_PD_V_U64, 0), 0, (1, 2 * MAX_PD_V_U64)); // TODO: need tests at the edges of the capacity of PD From b735842f2c61032f9fd926002980287fbf824153 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Fri, 17 Dec 2021 08:29:14 -0800 Subject: [PATCH 06/33] found bad case --- examples/get_accounts.rs | 8 ++-- src/lib.rs | 100 ++++++++++++++++++++++----------------- 2 files changed, 60 insertions(+), 48 deletions(-) diff --git a/examples/get_accounts.rs b/examples/get_accounts.rs index d26a0dd..759cfa8 100644 --- a/examples/get_accounts.rs +++ b/examples/get_accounts.rs @@ -138,16 +138,16 @@ fn main() { let maybe_twap = pa.get_twap(); match maybe_twap { - Some((twap, expo)) => { - println!( " twap ......... {} x 10^{}", twap, expo ); + Some(twap) => { + println!( " twap ......... {} x 10^{}", twap.price, twap.expo ); + println!( " twac ......... {} x 10^{}", twap.conf, twap.expo ); } None => { println!( " twap ......... unavailable"); + println!( " twac ......... unavailable"); } } - println!( " twac ......... {}", pa.twac.val ); - // go to next price account in list if pa.next.is_valid() { px_pkey = Pubkey::new( &pa.next.val ); diff --git a/src/lib.rs b/src/lib.rs index 186f36b..3421362 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ pub const PROD_ACCT_SIZE : usize = 512; pub const PROD_HDR_SIZE : usize = 48; pub const PROD_ATTR_SIZE : usize = PROD_ACCT_SIZE - PROD_HDR_SIZE; +// Constants for working with pyth's number representation const PD_EXPO: i32 = -9; const PD_SCALE: u64 = 1000000000; const MAX_PD_V_I64: i64 = (1 << 28) - 1; @@ -91,16 +92,6 @@ pub struct PriceInfo pub pub_slot : u64 } -impl PriceInfo { - pub fn get_checked(&self) -> Option<(i64, u64)> { - if !matches!(self.status, PriceStatus::Trading) { - None - } else { - Some((self.price, self.conf)) - } - } -} - // latest component price and price used in aggregate snapshot #[repr(C)] pub struct PriceComp @@ -149,13 +140,8 @@ pub struct Price impl Price { /** * Get the current price and confidence interval as fixed-point numbers of the form a * 10^e. - * Returns a triple of the current price, confidence interval, and the exponent for both - * numbers. For example: - * - * get_current_price() -> Some((12345, 267, -2)) // represents 123.45 +- 2.67 - * get_current_price() -> Some((123, 1, 2)) // represents 12300 +- 100 - * - * Returns None if price information is currently unavailable. + * Returns a struct containing the current price, confidence interval, and the exponent for both + * numbers. Returns None if price information is currently unavailable. */ pub fn get_current_price(&self) -> Option { if !matches!(self.agg.status, PriceStatus::Trading) { @@ -175,11 +161,10 @@ impl Price { * this method returns the price of X/Y. Use this method to get the price of e.g., mSOL/SOL from * the mSOL/USD and SOL/USD accounts. * - * The value returned by this method has the same semantics as Price::get_current_price above. - * * `result_expo` determines the exponent of the result, i.e., the number of digits of precision in * the price. For any given base/quote pair, the minimum possible exponent is - * `-9 + base.exponent - quote.exponent`. + * `-9 + self.exponent - quote.exponent`. (This minimum exponent reflects the maximum possible + * precision for the result given the precision of the two inputs and the numeric representation.) */ pub fn get_price_in_quote(&self, quote: Price, result_expo: i32) -> Option { return match self.get_current_price() { @@ -194,20 +179,32 @@ impl Price { } /** - * Get the time-weighted average price (TWAP) as a fixed point number of the form a * 10^e. - * Returns a tuple of the current twap and its exponent. For example: - * - * get_twap() -> Some((123, -2)) // represents 1.23 - * get_twap() -> Some((45, 3)) // represents 45000 - * + * Get the time-weighted average price (TWAP) and a confidence interval on the result. * Returns None if the twap is currently unavailable. + * + * At the moment, the confidence interval returned by this method is computed in + * a somewhat questionable way, so we do not recommend using it for high-value applications. */ - pub fn get_twap(&self) -> Option<(i64, i32)> { + pub fn get_twap(&self) -> Option { // This method currently cannot return None, but may do so in the future. - Some((self.twap.val, self.expo)) + // Note that the twac is a positive number in i64, so safe to cast to u64. + Some(PriceConf { price: self.twap.val, conf: self.twac.val as u64, expo: self.expo }) } } + +/** + * A price with a degree of uncertainty, represented as a price +- a confidence interval. + * The confidence interval roughly corresponds to the standard error of a normal distribution. + * Both the price and confidence are stored in a fixed-point numeric representation, `x * 10^expo`, + * where `expo` is the exponent. For example: + * + * ``` + * use pyth_client::PriceConf; + * PriceConf { price: 12345, conf: 267, expo: -2 }; // represents 123.45 +- 2.67 + * PriceConf { price: 123, conf: 1, expo: 2 }; // represents 12300 +- 100 + * ``` + */ #[derive(PartialEq)] #[derive(Debug)] pub struct PriceConf { @@ -218,36 +215,40 @@ pub struct PriceConf { impl PriceConf { /** - * Divides this price and confidence interval by y. + * Divide this price by `other` while propagating the uncertainty in both prices into the result. + * The uncertainty propagation algorithm is an approximation (due to computational limitations) + * and may slightly overestimate the resulting uncertainty (by at most a factor of sqrt(2)). * - * The result is returned with result_exp + * `result_expo` determines the exponent of the result. The minimum possible exponent is + * `-9 + self.exponent - other.exponent`. (This minimum exponent reflects the maximum possible + * precision for the result given the precision of the two inputs and the numeric representation.) */ - pub fn div(&self, quote: PriceConf, result_expo: i32) -> PriceConf { + pub fn div(&self, other: PriceConf, result_expo: i32) -> PriceConf { // Note that this assertion implies that the prices can be cast to u64. // We need prices as u64 in order to divide, as solana doesn't implement signed division. // It's also extremely unclear what this method should do if one of the prices is negative, // so assuming positive prices throughout seems fine. - assert!(self.price >= 0 && self.price <= MAX_PD_V_I64); - assert!(quote.price > 0 && quote.price <= MAX_PD_V_I64); - let base_price = self.price as u64; - let quote_price = quote.price as u64; + assert!(self.price > 0); + assert!(other.price > 0); + let (base_price, base_expo) = PriceConf::rescale_num(self.price as u64, self.expo); + let (other_price, other_expo) = PriceConf::rescale_num(other.price as u64, other.expo); assert!(self.conf <= MAX_PD_V_U64); - assert!(quote.conf <= MAX_PD_V_U64); + assert!(other.conf <= MAX_PD_V_U64); - println!("base ({} +- {}) * 10^{}", base_price, self.conf, self.expo); - println!("quote ({} +- {}) * 10^{}", quote_price, quote.conf, quote.expo); + println!("base ({} +- {}) * 10^{}", base_price, self.conf, base_expo); + println!("other ({} +- {}) * 10^{}", other_price, other.conf, other_expo); - // Compute the midprice, base in terms of quote. - let midprice = (base_price * PD_SCALE) / quote_price; - let midprice_expo = PD_EXPO + self.expo - quote.expo; + // Compute the midprice, base in terms of other. + let midprice = (base_price * PD_SCALE) / other_price; + let midprice_expo = PD_EXPO + base_expo - other_expo; println!("mean {} * 10^{}", midprice, midprice_expo); assert!(result_expo >= midprice_expo); // Compute the confidence interval. // This code uses the 1-norm instead of the 2-norm for computational reasons. // The correct formula is midprice * sqrt(c_1^2 + c_2^2), where c_1 and c_2 are the - // confidence intervals in price-percentage terms of the base and quote. This quantity + // confidence intervals in price-percentage terms of the base and other. This quantity // is difficult to compute due to the sqrt, and overflow/underflow considerations. // Instead, this code uses midprice * (c_1 + c_2). // This quantity is at most a factor of sqrt(2) greater than the correct result, which @@ -255,10 +256,10 @@ impl PriceConf { // The exponent is PD_EXPO for both of these. let base_confidence_pct = (self.conf * PD_SCALE) / base_price; - let quote_confidence_pct = (quote.conf * PD_SCALE) / quote_price; + let other_confidence_pct = (other.conf * PD_SCALE) / other_price; // Need to rescale the numbers to prevent the multiplication from overflowing - let (rescaled_z, rescaled_z_expo) = PriceConf::rescale_num(base_confidence_pct + quote_confidence_pct, PD_EXPO); + let (rescaled_z, rescaled_z_expo) = PriceConf::rescale_num(base_confidence_pct + other_confidence_pct, PD_EXPO); println!("rescaled_z {} * 10^{}", rescaled_z, rescaled_z_expo); let (rescaled_mid, rescaled_mid_expo) = PriceConf::rescale_num(midprice, midprice_expo); println!("rescaled_mean {} * 10^{}", rescaled_mid, rescaled_mid_expo); @@ -365,8 +366,18 @@ mod test { run_test(pc(1, 1, 0), pc(1, 1, 0), 0, (1, 2)); run_test(pc(10, 1, 0), pc(1, 1, 0), 0, (10, 11)); run_test(pc(1, 1, 1), pc(1, 1, 0), 0, (10, 20)); + run_test(pc(1, 1, -8), pc(1, 1, -8), -8, (100000000, 200000000)); run_test(pc(1, 1, 0), pc(5, 1, 0), 0, (0, 0)); + run_test(pc(1, 1, 0), pc(5, 1, 0), -1, (2, 2)); run_test(pc(1, 1, 0), pc(5, 1, 0), -2, (20, 24)); + run_test(pc(1, 1, 0), pc(5, 1, 0), -9, (200000000, 240000000)); + + // More realistic inputs (get BTC price in ETH) + let ten_e7: i64 = 10000000; + let uten_e7: u64 = 10000000; + run_test(pc(520010 * ten_e7, 310 * uten_e7, -8), + pc(38591 * ten_e7, 18 * uten_e7, -8), + -8, (1347490347, 1431806)); // Test with end range of possible inputs to check for overflow. run_test(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), 0, (1, 2)); @@ -375,6 +386,7 @@ mod test { // TODO: need tests at the edges of the capacity of PD + // TODO: Test non-trading cases // TODO: test cases where the exponents are dramatically different From 422145eccbc2f2831a63461ba647cfb2454e3ab8 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Mon, 20 Dec 2021 14:04:36 -0600 Subject: [PATCH 07/33] use u128 --- src/lib.rs | 45 ++++++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 3421362..4e73899 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,7 @@ pub const PROD_ATTR_SIZE : usize = PROD_ACCT_SIZE - PROD_HDR_SIZE; // Constants for working with pyth's number representation const PD_EXPO: i32 = -9; -const PD_SCALE: u64 = 1000000000; +const PD_SCALE: u128 = 1000000000; const MAX_PD_V_I64: i64 = (1 << 28) - 1; const MAX_PD_V_U64: u64 = (1 << 28) - 1; @@ -230,18 +230,20 @@ impl PriceConf { // so assuming positive prices throughout seems fine. assert!(self.price > 0); assert!(other.price > 0); - let (base_price, base_expo) = PriceConf::rescale_num(self.price as u64, self.expo); - let (other_price, other_expo) = PriceConf::rescale_num(other.price as u64, other.expo); + // let (base_price, base_expo) = PriceConf::rescale_num(self.price as u64, self.expo); + // let (other_price, other_expo) = PriceConf::rescale_num(other.price as u64, other.expo); + let base_price = self.price as u128; + let other_price = other.price as u128; - assert!(self.conf <= MAX_PD_V_U64); - assert!(other.conf <= MAX_PD_V_U64); + // assert!(self.conf <= MAX_PD_V_U64); + // assert!(other.conf <= MAX_PD_V_U64); - println!("base ({} +- {}) * 10^{}", base_price, self.conf, base_expo); - println!("other ({} +- {}) * 10^{}", other_price, other.conf, other_expo); + println!("base ({} +- {}) * 10^{}", base_price, self.conf, self.expo); + println!("other ({} +- {}) * 10^{}", other_price, other.conf, other.expo); // Compute the midprice, base in terms of other. - let midprice = (base_price * PD_SCALE) / other_price; - let midprice_expo = PD_EXPO + base_expo - other_expo; + let midprice = base_price * PD_SCALE / other_price; + let midprice_expo = PD_EXPO + self.expo - other.expo; println!("mean {} * 10^{}", midprice, midprice_expo); assert!(result_expo >= midprice_expo); @@ -254,9 +256,11 @@ impl PriceConf { // This quantity is at most a factor of sqrt(2) greater than the correct result, which // shouldn't matter considering that confidence intervals are typically ~0.1% of the price. + // (p/q) * (c_1/p + c_2/q) = (c_1 / q) + c_2 p / q^2 + // The exponent is PD_EXPO for both of these. - let base_confidence_pct = (self.conf * PD_SCALE) / base_price; - let other_confidence_pct = (other.conf * PD_SCALE) / other_price; + let base_confidence_pct: u128 = ((self.conf as u128) * PD_SCALE) / base_price; + let other_confidence_pct: u128 = ((other.conf as u128) * PD_SCALE) / other_price; // Need to rescale the numbers to prevent the multiplication from overflowing let (rescaled_z, rescaled_z_expo) = PriceConf::rescale_num(base_confidence_pct + other_confidence_pct, PD_EXPO); @@ -269,7 +273,7 @@ impl PriceConf { // Scale results to the target exponent. let midprice_in_result_expo = PriceConf::scale_to_exponent(midprice, midprice_expo, result_expo); - let conf_in_result_expo = PriceConf::scale_to_exponent(conf, conf_expo, result_expo); + let conf_in_result_expo = PriceConf::scale_to_exponent(conf as u128, conf_expo, result_expo); let midprice_i64 = midprice_in_result_expo as i64; assert!(midprice_i64 >= 0); @@ -284,27 +288,25 @@ impl PriceConf { * (which guarantees that multiplication doesn't overflow). */ fn rescale_num( - num: u64, + num: u128, expo: i32, ) -> (u64, i32) { - let mut p: u64 = num; + let mut p: u128 = num; let mut c: i32 = 0; - while p > MAX_PD_V_U64 { + while p > (MAX_PD_V_U64 as u128) { p = p / 10; c += 1; } - println!("c: {}", c); - - return (p, expo + c); + return (p as u64, expo + c); } /** Scale num so that its exponent is target_expo. * This method can only reduce precision, i.e., target_expo must be > current_expo. */ fn scale_to_exponent( - num: u64, + num: u128, current_expo: i32, target_expo: i32, ) -> u64 { @@ -317,7 +319,8 @@ impl PriceConf { delta -= 1; } - return res; + // FIXME: check that this cast panics if res > max_u64 + return res as u64; } } @@ -377,7 +380,7 @@ mod test { let uten_e7: u64 = 10000000; run_test(pc(520010 * ten_e7, 310 * uten_e7, -8), pc(38591 * ten_e7, 18 * uten_e7, -8), - -8, (1347490347, 1431806)); + -8, (1347490347, 1431804)); // Test with end range of possible inputs to check for overflow. run_test(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), 0, (1, 2)); From c49ce56f8fbb58d8bb41e9dc38fd61517bb596fd Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Tue, 21 Dec 2021 09:17:34 -0600 Subject: [PATCH 08/33] working on it --- src/lib.rs | 129 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 89 insertions(+), 40 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 4e73899..6777d20 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,7 @@ pub const PROD_ATTR_SIZE : usize = PROD_ACCT_SIZE - PROD_HDR_SIZE; // Constants for working with pyth's number representation const PD_EXPO: i32 = -9; -const PD_SCALE: u128 = 1000000000; +const PD_SCALE: u64 = 1_000_000_000; const MAX_PD_V_I64: i64 = (1 << 28) - 1; const MAX_PD_V_U64: u64 = (1 << 28) - 1; @@ -216,12 +216,18 @@ pub struct PriceConf { impl PriceConf { /** * Divide this price by `other` while propagating the uncertainty in both prices into the result. - * The uncertainty propagation algorithm is an approximation (due to computational limitations) - * and may slightly overestimate the resulting uncertainty (by at most a factor of sqrt(2)). + * The uncertainty propagation algorithm is an approximation due to computational limitations + * that may slightly overestimate the resulting uncertainty (by at most a factor of sqrt(2)). * * `result_expo` determines the exponent of the result. The minimum possible exponent is * `-9 + self.exponent - other.exponent`. (This minimum exponent reflects the maximum possible * precision for the result given the precision of the two inputs and the numeric representation.) + * + * This function may panic unless the following conditions are satisfied: + * 1. The prices of self and other are > 0. + * 2. The result price can be represented using a 64-bit number with exponent `result_expo`. + * (To satisfy this condition, simply don't choose a very negative `result_expo`) + * 3. The confidence interval is */ pub fn div(&self, other: PriceConf, result_expo: i32) -> PriceConf { // Note that this assertion implies that the prices can be cast to u64. @@ -230,22 +236,26 @@ impl PriceConf { // so assuming positive prices throughout seems fine. assert!(self.price > 0); assert!(other.price > 0); - // let (base_price, base_expo) = PriceConf::rescale_num(self.price as u64, self.expo); - // let (other_price, other_expo) = PriceConf::rescale_num(other.price as u64, other.expo); - let base_price = self.price as u128; - let other_price = other.price as u128; - // assert!(self.conf <= MAX_PD_V_U64); - // assert!(other.conf <= MAX_PD_V_U64); + // PriceConf is not guaranteed to store its price/confidence in normalized form. + // Normalize them here to bound the range of price/conf, which is required to perform + // arithmetic operations. + let base = self.normalized(); + let other = other.normalized(); + + // These use at most 27 bits each + let base_price = base.price as u64; + let other_price = other.price as u64; - println!("base ({} +- {}) * 10^{}", base_price, self.conf, self.expo); + println!("----"); + println!("base ({} +- {}) * 10^{}", base_price, base.conf, base.expo); println!("other ({} +- {}) * 10^{}", other_price, other.conf, other.expo); // Compute the midprice, base in terms of other. + // Uses at most 57 bits let midprice = base_price * PD_SCALE / other_price; - let midprice_expo = PD_EXPO + self.expo - other.expo; + let midprice_expo = PD_EXPO + base.expo - other.expo; println!("mean {} * 10^{}", midprice, midprice_expo); - assert!(result_expo >= midprice_expo); // Compute the confidence interval. // This code uses the 1-norm instead of the 2-norm for computational reasons. @@ -256,24 +266,21 @@ impl PriceConf { // This quantity is at most a factor of sqrt(2) greater than the correct result, which // shouldn't matter considering that confidence intervals are typically ~0.1% of the price. - // (p/q) * (c_1/p + c_2/q) = (c_1 / q) + c_2 p / q^2 + // The exponent is PD_EXPO for both of these. Each of these uses 57 bits. + let base_confidence_pct: u64 = (base.conf * PD_SCALE) / base_price; + let other_confidence_pct: u64 = (other.conf * PD_SCALE) / other_price; - // The exponent is PD_EXPO for both of these. - let base_confidence_pct: u128 = ((self.conf as u128) * PD_SCALE) / base_price; - let other_confidence_pct: u128 = ((other.conf as u128) * PD_SCALE) / other_price; - - // Need to rescale the numbers to prevent the multiplication from overflowing - let (rescaled_z, rescaled_z_expo) = PriceConf::rescale_num(base_confidence_pct + other_confidence_pct, PD_EXPO); - println!("rescaled_z {} * 10^{}", rescaled_z, rescaled_z_expo); - let (rescaled_mid, rescaled_mid_expo) = PriceConf::rescale_num(midprice, midprice_expo); - println!("rescaled_mean {} * 10^{}", rescaled_mid, rescaled_mid_expo); - let conf = rescaled_z * rescaled_mid; - let conf_expo = rescaled_z_expo + rescaled_mid_expo; + // at most 58 bits + let confidence_pct = base_confidence_pct + other_confidence_pct; + println!("rescaled_z {} * 10^{}", confidence_pct, PD_EXPO); + // at most 115 bits + let conf = (confidence_pct as u128) * (midprice as u128); + let conf_expo = PD_EXPO + midprice_expo; println!("conf {} * 10^{}", conf, conf_expo); // Scale results to the target exponent. - let midprice_in_result_expo = PriceConf::scale_to_exponent(midprice, midprice_expo, result_expo); - let conf_in_result_expo = PriceConf::scale_to_exponent(conf as u128, conf_expo, result_expo); + let midprice_in_result_expo = PriceConf::scale_to_exponent(midprice as u128, midprice_expo, result_expo); + let conf_in_result_expo = PriceConf::scale_to_exponent(conf, conf_expo, result_expo); let midprice_i64 = midprice_in_result_expo as i64; assert!(midprice_i64 >= 0); @@ -284,6 +291,28 @@ impl PriceConf { } } + pub fn normalized(&self) -> PriceConf { + assert!(self.price > 0); + let mut p: u64 = self.price as u64; + let mut c: u64 = self.conf; + let mut e: i32 = self.expo; + + while p > MAX_PD_V_U64 || c > MAX_PD_V_U64 { + p = p / 10; + c = c / 10; + e += 1; + } + + // Can get p == 0 if confidence is >> price. + assert!(p > 0); + + PriceConf { + price: p as i64, + conf: c, + expo: e, + } + } + /** Scale num and its exponent such that it is < MAX_PD_V_U64 * (which guarantees that multiplication doesn't overflow). */ @@ -344,7 +373,7 @@ impl AccKey #[cfg(test)] mod test { - use crate::{MAX_PD_V_I64, MAX_PD_V_U64, PriceConf}; + use crate::{MAX_PD_V_I64, MAX_PD_V_U64, PriceConf, PD_SCALE}; fn pc(price: i64, conf: u64, expo: i32) -> PriceConf { PriceConf { @@ -369,29 +398,49 @@ mod test { run_test(pc(1, 1, 0), pc(1, 1, 0), 0, (1, 2)); run_test(pc(10, 1, 0), pc(1, 1, 0), 0, (10, 11)); run_test(pc(1, 1, 1), pc(1, 1, 0), 0, (10, 20)); - run_test(pc(1, 1, -8), pc(1, 1, -8), -8, (100000000, 200000000)); + run_test(pc(1, 1, -8), pc(1, 1, -8), -8, (100_000_000, 200_000_000)); run_test(pc(1, 1, 0), pc(5, 1, 0), 0, (0, 0)); run_test(pc(1, 1, 0), pc(5, 1, 0), -1, (2, 2)); run_test(pc(1, 1, 0), pc(5, 1, 0), -2, (20, 24)); - run_test(pc(1, 1, 0), pc(5, 1, 0), -9, (200000000, 240000000)); + run_test(pc(1, 1, 0), pc(5, 1, 0), -9, (200_000_000, 240_000_000)); + + // Different exponents in the two inputs + run_test(pc(100, 10, -8), pc(2, 1, -7), -8, (500_000_000, 300_000_000)); + run_test(pc(100, 10, -4), pc(2, 1, 0), -8, (500_000, 300_000)); + run_test(pc(100, 10, -4), pc(2, 1, 0), -4, (50, 30)); + + // Test with end range of possible inputs where the output should not lose precision. + run_test(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), 0, (1, 2)); + run_test(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(1, 1, 0), 0, (MAX_PD_V_I64, 2 * MAX_PD_V_U64)); + run_test(pc(1, MAX_PD_V_U64, 0), pc(1, MAX_PD_V_U64, 0), 0, (1, 2 * MAX_PD_V_U64)); + run_test(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(1, MAX_PD_V_U64, 0), 0, (MAX_PD_V_I64, MAX_PD_V_U64 * MAX_PD_V_U64 + (MAX_PD_V_I64 as u64))); // More realistic inputs (get BTC price in ETH) + // Note these inputs are not normalized let ten_e7: i64 = 10000000; let uten_e7: u64 = 10000000; run_test(pc(520010 * ten_e7, 310 * uten_e7, -8), pc(38591 * ten_e7, 18 * uten_e7, -8), -8, (1347490347, 1431804)); - // Test with end range of possible inputs to check for overflow. - run_test(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), 0, (1, 2)); - run_test(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(1, 1, 0), 0, (MAX_PD_V_I64, 2 * MAX_PD_V_U64)); - run_test(pc(1, MAX_PD_V_U64, 0), pc(1, MAX_PD_V_U64, 0), 0, (1, 2 * MAX_PD_V_U64)); - - // TODO: need tests at the edges of the capacity of PD - - - // TODO: Test non-trading cases - - // TODO: test cases where the exponents are dramatically different + // Test with end range of possible inputs to identify overflow + // These inputs will lose precision due to the initial normalization. + // Get the rounded versions of these inputs in order to compute the expected results. + let normed = pc(i64::MAX, u64::MAX, 0).normalized(); + let max_i64 = normed.price * (10_i64.pow(normed.expo as u32)); + let max_u64 = normed.conf * (10_u64.pow(normed.expo as u32)); + + let mi = normed.price; + let mu = normed.conf; + + run_test(pc(i64::MAX, u64::MAX, 0), pc(i64::MAX, u64::MAX, 0), 0, (1, 4)); + run_test(pc(i64::MAX, u64::MAX, 0), pc(1, 1, 0), 7, (max_i64 / ten_e7, 3 * ((max_i64 as u64) / uten_e7))); + // FIXME: the price being 0 here is not good + // Could make a precondition that conf < price * something + /* + run_test(pc(1, u64::MAX, 0), pc(1, u64::MAX, 0), 7, (0, 2 * (max_u64 / uten_e7))); + run_test(pc(i64::MAX, u64::MAX, 0), pc(1, u64::MAX, 0), normed.expo, + (mi, mu * mu + (mi as u64))); + */ } } \ No newline at end of file From 8b96d3b9ac51ccdf544bd7464d569e88c6b448af Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Tue, 21 Dec 2021 10:56:24 -0600 Subject: [PATCH 09/33] clarify --- src/lib.rs | 266 ++++++++++++++++++++++++++++------------------------- 1 file changed, 140 insertions(+), 126 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6777d20..fbbbfd1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -167,14 +167,10 @@ impl Price { * precision for the result given the precision of the two inputs and the numeric representation.) */ pub fn get_price_in_quote(&self, quote: Price, result_expo: i32) -> Option { - return match self.get_current_price() { - Some(base_price_conf) => - match quote.get_current_price() { - Some(quote_price_conf) => - Some(base_price_conf.div(quote_price_conf, result_expo)), - None => None, - } - None => None, + return match (self.get_current_price(), quote.get_current_price()) { + (Some(base_price_conf), Some(quote_price_conf)) => + base_price_conf.div(quote_price_conf, result_expo), + (_, _) => None, } } @@ -223,93 +219,104 @@ impl PriceConf { * `-9 + self.exponent - other.exponent`. (This minimum exponent reflects the maximum possible * precision for the result given the precision of the two inputs and the numeric representation.) * - * This function may panic unless the following conditions are satisfied: + * This function will return `None` unless all of the following conditions are satisfied: * 1. The prices of self and other are > 0. - * 2. The result price can be represented using a 64-bit number with exponent `result_expo`. - * (To satisfy this condition, simply don't choose a very negative `result_expo`) - * 3. The confidence interval is + * 2. The resulting price and confidence can be represented using a 64-bit number with + * exponent `result_expo`. + * 3. The confidence interval of self / other are << MAX_PD_V_U64 times their the respective price. + * (This condition should essentially always be satisfied in the real world.) */ - pub fn div(&self, other: PriceConf, result_expo: i32) -> PriceConf { - // Note that this assertion implies that the prices can be cast to u64. - // We need prices as u64 in order to divide, as solana doesn't implement signed division. - // It's also extremely unclear what this method should do if one of the prices is negative, - // so assuming positive prices throughout seems fine. - assert!(self.price > 0); - assert!(other.price > 0); - + pub fn div(&self, other: PriceConf, result_expo: i32) -> Option { // PriceConf is not guaranteed to store its price/confidence in normalized form. // Normalize them here to bound the range of price/conf, which is required to perform // arithmetic operations. - let base = self.normalized(); - let other = other.normalized(); - - // These use at most 27 bits each - let base_price = base.price as u64; - let other_price = other.price as u64; - - println!("----"); - println!("base ({} +- {}) * 10^{}", base_price, base.conf, base.expo); - println!("other ({} +- {}) * 10^{}", other_price, other.conf, other.expo); - - // Compute the midprice, base in terms of other. - // Uses at most 57 bits - let midprice = base_price * PD_SCALE / other_price; - let midprice_expo = PD_EXPO + base.expo - other.expo; - println!("mean {} * 10^{}", midprice, midprice_expo); - - // Compute the confidence interval. - // This code uses the 1-norm instead of the 2-norm for computational reasons. - // The correct formula is midprice * sqrt(c_1^2 + c_2^2), where c_1 and c_2 are the - // confidence intervals in price-percentage terms of the base and other. This quantity - // is difficult to compute due to the sqrt, and overflow/underflow considerations. - // Instead, this code uses midprice * (c_1 + c_2). - // This quantity is at most a factor of sqrt(2) greater than the correct result, which - // shouldn't matter considering that confidence intervals are typically ~0.1% of the price. - - // The exponent is PD_EXPO for both of these. Each of these uses 57 bits. - let base_confidence_pct: u64 = (base.conf * PD_SCALE) / base_price; - let other_confidence_pct: u64 = (other.conf * PD_SCALE) / other_price; - - // at most 58 bits - let confidence_pct = base_confidence_pct + other_confidence_pct; - println!("rescaled_z {} * 10^{}", confidence_pct, PD_EXPO); - // at most 115 bits - let conf = (confidence_pct as u128) * (midprice as u128); - let conf_expo = PD_EXPO + midprice_expo; - println!("conf {} * 10^{}", conf, conf_expo); - - // Scale results to the target exponent. - let midprice_in_result_expo = PriceConf::scale_to_exponent(midprice as u128, midprice_expo, result_expo); - let conf_in_result_expo = PriceConf::scale_to_exponent(conf, conf_expo, result_expo); - let midprice_i64 = midprice_in_result_expo as i64; - assert!(midprice_i64 >= 0); - - PriceConf { - price: midprice_i64, - conf: conf_in_result_expo, - expo: result_expo + match (self.normalized(), other.normalized()) { + (Some(base), Some(other)) => { + // Note that normalization implies that the prices can be cast to u64. + // We need prices as u64 in order to divide, as solana doesn't implement signed division. + // It's also extremely unclear what this method should do if one of the prices is negative, + // so assuming positive prices throughout seems fine. + + // These use at most 27 bits each + let base_price = base.price as u64; + let other_price = other.price as u64; + + println!("----"); + println!("base ({} +- {}) * 10^{}", base_price, base.conf, base.expo); + println!("other ({} +- {}) * 10^{}", other_price, other.conf, other.expo); + + // Compute the midprice, base in terms of other. + // Uses at most 57 bits + let midprice = base_price * PD_SCALE / other_price; + let midprice_expo = PD_EXPO + base.expo - other.expo; + println!("mean {} * 10^{}", midprice, midprice_expo); + + // Compute the confidence interval. + // This code uses the 1-norm instead of the 2-norm for computational reasons. + // The correct formula is midprice * sqrt(c_1^2 + c_2^2), where c_1 and c_2 are the + // confidence intervals in price-percentage terms of the base and other. This quantity + // is difficult to compute due to the sqrt, and overflow/underflow considerations. + // Instead, this code uses midprice * (c_1 + c_2). + // This quantity is at most a factor of sqrt(2) greater than the correct result, which + // shouldn't matter considering that confidence intervals are typically ~0.1% of the price. + + // The exponent is PD_EXPO for both of these. Each of these uses 57 bits. + let base_confidence_pct: u64 = (base.conf * PD_SCALE) / base_price; + let other_confidence_pct: u64 = (other.conf * PD_SCALE) / other_price; + + // at most 58 bits + let confidence_pct = base_confidence_pct + other_confidence_pct; + println!("rescaled_z {} * 10^{}", confidence_pct, PD_EXPO); + // at most 115 bits + let conf = (confidence_pct as u128) * (midprice as u128); + let conf_expo = PD_EXPO + midprice_expo; + println!("conf {} * 10^{}", conf, conf_expo); + + // Scale results to the target exponent. + let midprice_in_result_expo = PriceConf::scale_to_exponent(midprice as u128, midprice_expo, result_expo); + let conf_in_result_expo = PriceConf::scale_to_exponent(conf, conf_expo, result_expo); + match (midprice_in_result_expo, conf_in_result_expo) { + (Some(m), Some(c)) => { + let m_i64 = m as i64; + assert!(m_i64 >= 0); + + Some(PriceConf { + price: m_i64, + conf: c, + expo: result_expo + }) + } + (_, _) => None + } + } + (_, _) => None } } - pub fn normalized(&self) -> PriceConf { - assert!(self.price > 0); - let mut p: u64 = self.price as u64; - let mut c: u64 = self.conf; - let mut e: i32 = self.expo; - - while p > MAX_PD_V_U64 || c > MAX_PD_V_U64 { - p = p / 10; - c = c / 10; - e += 1; - } - - // Can get p == 0 if confidence is >> price. - assert!(p > 0); - - PriceConf { - price: p as i64, - conf: c, - expo: e, + pub fn normalized(&self) -> Option { + if self.price > 0 { + let mut p: u64 = self.price as u64; + let mut c: u64 = self.conf; + let mut e: i32 = self.expo; + + while p > MAX_PD_V_U64 || c > MAX_PD_V_U64 { + p = p / 10; + c = c / 10; + e += 1; + } + + // Can get p == 0 if confidence is >> price. + if p > 0 { + Some(PriceConf { + price: p as i64, + conf: c, + expo: e, + }) + } else { + None + } + } else { + None } } @@ -338,18 +345,20 @@ impl PriceConf { num: u128, current_expo: i32, target_expo: i32, - ) -> u64 { + ) -> Option { let mut delta = target_expo - current_expo; let mut res = num; - assert!(delta >= 0); - - while delta > 0 { - res /= 10; - delta -= 1; + if (delta >= 0) { + while delta > 0 { + res /= 10; + delta -= 1; + } + + // FIXME: check that this cast panics if res > max_u64 + return Some(res as u64); + } else { + None } - - // FIXME: check that this cast panics if res > max_u64 - return res as u64; } } @@ -385,62 +394,67 @@ mod test { #[test] fn test_rebase() { - fn run_test( + fn test_succeeds( price1: PriceConf, price2: PriceConf, result_expo: i32, expected: (i64, u64), ) { let result = price1.div(price2, result_expo); - assert_eq!(result, pc(expected.0, expected.1, result_expo)); + assert_eq!(result, Some(pc(expected.0, expected.1, result_expo))); + } + + fn test_fails( + price1: PriceConf, + price2: PriceConf, + result_expo: i32, + ) { + let result = price1.div(price2, result_expo); + assert_eq!(result, None); } - run_test(pc(1, 1, 0), pc(1, 1, 0), 0, (1, 2)); - run_test(pc(10, 1, 0), pc(1, 1, 0), 0, (10, 11)); - run_test(pc(1, 1, 1), pc(1, 1, 0), 0, (10, 20)); - run_test(pc(1, 1, -8), pc(1, 1, -8), -8, (100_000_000, 200_000_000)); - run_test(pc(1, 1, 0), pc(5, 1, 0), 0, (0, 0)); - run_test(pc(1, 1, 0), pc(5, 1, 0), -1, (2, 2)); - run_test(pc(1, 1, 0), pc(5, 1, 0), -2, (20, 24)); - run_test(pc(1, 1, 0), pc(5, 1, 0), -9, (200_000_000, 240_000_000)); + test_succeeds(pc(1, 1, 0), pc(1, 1, 0), 0, (1, 2)); + test_succeeds(pc(10, 1, 0), pc(1, 1, 0), 0, (10, 11)); + test_succeeds(pc(1, 1, 1), pc(1, 1, 0), 0, (10, 20)); + test_succeeds(pc(1, 1, -8), pc(1, 1, -8), -8, (100_000_000, 200_000_000)); + test_succeeds(pc(1, 1, 0), pc(5, 1, 0), 0, (0, 0)); + test_succeeds(pc(1, 1, 0), pc(5, 1, 0), -1, (2, 2)); + test_succeeds(pc(1, 1, 0), pc(5, 1, 0), -2, (20, 24)); + test_succeeds(pc(1, 1, 0), pc(5, 1, 0), -9, (200_000_000, 240_000_000)); // Different exponents in the two inputs - run_test(pc(100, 10, -8), pc(2, 1, -7), -8, (500_000_000, 300_000_000)); - run_test(pc(100, 10, -4), pc(2, 1, 0), -8, (500_000, 300_000)); - run_test(pc(100, 10, -4), pc(2, 1, 0), -4, (50, 30)); + test_succeeds(pc(100, 10, -8), pc(2, 1, -7), -8, (500_000_000, 300_000_000)); + test_succeeds(pc(100, 10, -4), pc(2, 1, 0), -8, (500_000, 300_000)); + test_succeeds(pc(100, 10, -4), pc(2, 1, 0), -4, (50, 30)); // Test with end range of possible inputs where the output should not lose precision. - run_test(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), 0, (1, 2)); - run_test(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(1, 1, 0), 0, (MAX_PD_V_I64, 2 * MAX_PD_V_U64)); - run_test(pc(1, MAX_PD_V_U64, 0), pc(1, MAX_PD_V_U64, 0), 0, (1, 2 * MAX_PD_V_U64)); - run_test(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(1, MAX_PD_V_U64, 0), 0, (MAX_PD_V_I64, MAX_PD_V_U64 * MAX_PD_V_U64 + (MAX_PD_V_I64 as u64))); + test_succeeds(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), 0, (1, 2)); + test_succeeds(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(1, 1, 0), 0, (MAX_PD_V_I64, 2 * MAX_PD_V_U64)); + test_succeeds(pc(1, MAX_PD_V_U64, 0), pc(1, MAX_PD_V_U64, 0), 0, (1, 2 * MAX_PD_V_U64)); + test_succeeds(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(1, MAX_PD_V_U64, 0), 0, (MAX_PD_V_I64, MAX_PD_V_U64 * MAX_PD_V_U64 + (MAX_PD_V_I64 as u64))); // More realistic inputs (get BTC price in ETH) // Note these inputs are not normalized let ten_e7: i64 = 10000000; let uten_e7: u64 = 10000000; - run_test(pc(520010 * ten_e7, 310 * uten_e7, -8), - pc(38591 * ten_e7, 18 * uten_e7, -8), - -8, (1347490347, 1431804)); + test_succeeds(pc(520010 * ten_e7, 310 * uten_e7, -8), + pc(38591 * ten_e7, 18 * uten_e7, -8), + -8, (1347490347, 1431804)); // Test with end range of possible inputs to identify overflow // These inputs will lose precision due to the initial normalization. // Get the rounded versions of these inputs in order to compute the expected results. - let normed = pc(i64::MAX, u64::MAX, 0).normalized(); + let normed = pc(i64::MAX, u64::MAX, 0).normalized().unwrap(); let max_i64 = normed.price * (10_i64.pow(normed.expo as u32)); let max_u64 = normed.conf * (10_u64.pow(normed.expo as u32)); let mi = normed.price; let mu = normed.conf; - run_test(pc(i64::MAX, u64::MAX, 0), pc(i64::MAX, u64::MAX, 0), 0, (1, 4)); - run_test(pc(i64::MAX, u64::MAX, 0), pc(1, 1, 0), 7, (max_i64 / ten_e7, 3 * ((max_i64 as u64) / uten_e7))); - // FIXME: the price being 0 here is not good - // Could make a precondition that conf < price * something - /* - run_test(pc(1, u64::MAX, 0), pc(1, u64::MAX, 0), 7, (0, 2 * (max_u64 / uten_e7))); - run_test(pc(i64::MAX, u64::MAX, 0), pc(1, u64::MAX, 0), normed.expo, - (mi, mu * mu + (mi as u64))); - */ + test_succeeds(pc(i64::MAX, u64::MAX, 0), pc(i64::MAX, u64::MAX, 0), 0, (1, 4)); + test_succeeds(pc(i64::MAX, u64::MAX, 0), pc(1, 1, 0), 7, (max_i64 / ten_e7, 3 * ((max_i64 as u64) / uten_e7))); + + // Can't normalize the input when the confidence is >> price. + test_fails(pc(1, u64::MAX, 0), pc(1, u64::MAX, 0), 7); } } \ No newline at end of file From 82135978ad38e3ae9622b6943544b3c780f3ee15 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Tue, 21 Dec 2021 11:34:16 -0600 Subject: [PATCH 10/33] cleanup --- src/lib.rs | 36 +++++++++++++++--------------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index fbbbfd1..a8aad49 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -218,6 +218,7 @@ impl PriceConf { * `result_expo` determines the exponent of the result. The minimum possible exponent is * `-9 + self.exponent - other.exponent`. (This minimum exponent reflects the maximum possible * precision for the result given the precision of the two inputs and the numeric representation.) + * TODO: possibly allow smaller exponents * * This function will return `None` unless all of the following conditions are satisfied: * 1. The prices of self and other are > 0. @@ -278,6 +279,7 @@ impl PriceConf { match (midprice_in_result_expo, conf_in_result_expo) { (Some(m), Some(c)) => { let m_i64 = m as i64; + // This should be guaranteed to succeed because midprice uses <= 57 bits assert!(m_i64 >= 0); Some(PriceConf { @@ -293,8 +295,14 @@ impl PriceConf { } } + /** + * Get a copy of this struct where the price and confidence + * have been normalized to be less than `MAX_PD_V_U64`. + * Returns `None` if `price == 0` before or after normalization. + */ pub fn normalized(&self) -> Option { if self.price > 0 { + // BPF only supports unsigned division let mut p: u64 = self.price as u64; let mut c: u64 = self.conf; let mut e: i32 = self.expo; @@ -320,25 +328,8 @@ impl PriceConf { } } - /** Scale num and its exponent such that it is < MAX_PD_V_U64 - * (which guarantees that multiplication doesn't overflow). - */ - fn rescale_num( - num: u128, - expo: i32, - ) -> (u64, i32) { - let mut p: u128 = num; - let mut c: i32 = 0; - - while p > (MAX_PD_V_U64 as u128) { - p = p / 10; - c += 1; - } - - return (p as u64, expo + c); - } - - /** Scale num so that its exponent is target_expo. + /** + * Scale num so that its exponent is target_expo. * This method can only reduce precision, i.e., target_expo must be > current_expo. */ fn scale_to_exponent( @@ -354,8 +345,11 @@ impl PriceConf { delta -= 1; } - // FIXME: check that this cast panics if res > max_u64 - return Some(res as u64); + if res <= (u64::MAX_VALUE as u128) { + Some(res as u64) + } else { + None + } } else { None } From 45a8ebcf49ff4198ee603a5381bad68c25b4e572 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Tue, 21 Dec 2021 11:44:04 -0600 Subject: [PATCH 11/33] more cleanup --- src/lib.rs | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index a8aad49..bed9896 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -339,13 +339,13 @@ impl PriceConf { ) -> Option { let mut delta = target_expo - current_expo; let mut res = num; - if (delta >= 0) { + if delta >= 0 { while delta > 0 { res /= 10; delta -= 1; } - if res <= (u64::MAX_VALUE as u128) { + if res <= (u64::MAX as u128) { Some(res as u64) } else { None @@ -376,7 +376,7 @@ impl AccKey #[cfg(test)] mod test { - use crate::{MAX_PD_V_I64, MAX_PD_V_U64, PriceConf, PD_SCALE}; + use crate::{MAX_PD_V_I64, MAX_PD_V_U64, PriceConf, PD_SCALE, PD_EXPO}; fn pc(price: i64, conf: u64, expo: i32) -> PriceConf { PriceConf { @@ -442,13 +442,22 @@ mod test { let max_i64 = normed.price * (10_i64.pow(normed.expo as u32)); let max_u64 = normed.conf * (10_u64.pow(normed.expo as u32)); - let mi = normed.price; - let mu = normed.conf; - test_succeeds(pc(i64::MAX, u64::MAX, 0), pc(i64::MAX, u64::MAX, 0), 0, (1, 4)); test_succeeds(pc(i64::MAX, u64::MAX, 0), pc(1, 1, 0), 7, (max_i64 / ten_e7, 3 * ((max_i64 as u64) / uten_e7))); + // Price is zero pre-normalization + test_fails(pc(0, 1, 0), pc(1, 1, 0), PD_EXPO - 1); + test_fails(pc(1, 1, 0), pc(0, 1, 0), PD_EXPO - 1); + // Can't normalize the input when the confidence is >> price. - test_fails(pc(1, u64::MAX, 0), pc(1, u64::MAX, 0), 7); + test_fails(pc(1, 1, 0), pc(1, u64::MAX, 0), 7); + test_fails(pc(1, u64::MAX, 0), pc(1, 1, 0), 7); + + // Result exponent too small + test_succeeds(pc(1, 1, 0), pc(1, 1, 0), PD_EXPO, (1 * (PD_SCALE as i64), 2 * PD_SCALE)); + test_fails(pc(1, 1, 0), pc(1, 1, 0), PD_EXPO - 1); + + // TODO: handle the case where the result exponent is too large more gracefully. + test_succeeds(pc(1, 1, 0), pc(1, 1, 0), 1, (0, 0)); } } \ No newline at end of file From dcb505390efcb5800c231c05017be40289f1d4e2 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Tue, 21 Dec 2021 12:34:29 -0600 Subject: [PATCH 12/33] pretty sure i need this --- Cargo.toml | 13 +++++++++++-- Xargo.toml | 2 ++ 2 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 Xargo.toml diff --git a/Cargo.toml b/Cargo.toml index 69b5b60..43dc019 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,10 +10,19 @@ description = "pyth price oracle data structures and example usage" keywords = [ "pyth", "solana", "oracle" ] readme = "README.md" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +test-bpf = [] + +[dependencies] +solana-program = "1.6.7" [dev-dependencies] solana-client = "1.6.7" solana-sdk = "1.6.7" -solana-program = "1.6.7" + +[lib] +crate-type = ["cdylib", "lib"] + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] diff --git a/Xargo.toml b/Xargo.toml new file mode 100644 index 0000000..475fb71 --- /dev/null +++ b/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] From 3fc33c7f1b122c130d30847ea105d01799910d39 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Tue, 21 Dec 2021 12:39:26 -0600 Subject: [PATCH 13/33] better --- src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index bed9896..1bb7473 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,6 @@ pub const PROD_ATTR_SIZE : usize = PROD_ACCT_SIZE - PROD_HDR_SIZE; // Constants for working with pyth's number representation const PD_EXPO: i32 = -9; const PD_SCALE: u64 = 1_000_000_000; -const MAX_PD_V_I64: i64 = (1 << 28) - 1; const MAX_PD_V_U64: u64 = (1 << 28) - 1; // each account has its own type @@ -376,7 +375,9 @@ impl AccKey #[cfg(test)] mod test { - use crate::{MAX_PD_V_I64, MAX_PD_V_U64, PriceConf, PD_SCALE, PD_EXPO}; + use crate::{MAX_PD_V_U64, PriceConf, PD_SCALE, PD_EXPO}; + + const MAX_PD_V_I64: i64 = (1 << 28) - 1; fn pc(price: i64, conf: u64, expo: i32) -> PriceConf { PriceConf { @@ -440,7 +441,6 @@ mod test { // Get the rounded versions of these inputs in order to compute the expected results. let normed = pc(i64::MAX, u64::MAX, 0).normalized().unwrap(); let max_i64 = normed.price * (10_i64.pow(normed.expo as u32)); - let max_u64 = normed.conf * (10_u64.pow(normed.expo as u32)); test_succeeds(pc(i64::MAX, u64::MAX, 0), pc(i64::MAX, u64::MAX, 0), 0, (1, 4)); test_succeeds(pc(i64::MAX, u64::MAX, 0), pc(1, 1, 0), 7, (max_i64 / ten_e7, 3 * ((max_i64 as u64) / uten_e7))); From 0b8f7dd7e39687f45fb3318a75488bff1a6c77b2 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Tue, 21 Dec 2021 12:56:38 -0600 Subject: [PATCH 14/33] bad merge --- README.md | 12 ++++++++++-- src/lib.rs | 14 -------------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 09df993..04182fa 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # pyth-client-rs -A rust API for desribing on-chain pyth account structures. A primer on pyth accounts can be found at https://github.com/pyth-network/pyth-client/blob/main/doc/aggregate_price.md +A rust API for describing on-chain pyth account structures. A primer on pyth accounts can be found at https://github.com/pyth-network/pyth-client/blob/main/doc/aggregate_price.md Contains a library for use in on-chain program development and an off-chain example program for loading and printing product reference data and aggregate prices from all devnet pyth accounts. @@ -37,4 +37,12 @@ product_account .. 6MEwdxe4g1NeAF9u6KDG14anJpFsVEa2cvr5H6iriFZ8 publish_slot . 91340925 twap ......... 7426390900 twac ......... 2259870 -``` \ No newline at end of file +``` + +### Development + +``` +cargo test-bpf +``` + + diff --git a/src/lib.rs b/src/lib.rs index 7cb086d..1bb7473 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -353,20 +353,6 @@ impl PriceConf { None } } - - /** - * Get the time-weighted average price (TWAP) as a fixed point number of the form a * 10^e. - * Returns a tuple of the current twap and its exponent. For example: - * - * get_twap() -> Some((123, -2)) // represents 1.23 - * get_twap() -> Some((45, 3)) // represents 45000 - * - * Returns None if the twap is currently unavailable. - */ - pub fn get_twap(&self) -> Option<(i64, i32)> { - // This method currently cannot return None, but may do so in the future. - Some((self.twap.val, self.expo)) - } } struct AccKeyU64 From 564dd9c60a20b30145954e7a5d4e060e3f7ca385 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Tue, 21 Dec 2021 12:57:11 -0600 Subject: [PATCH 15/33] no println --- src/lib.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 1bb7473..ed5d34a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -241,15 +241,10 @@ impl PriceConf { let base_price = base.price as u64; let other_price = other.price as u64; - println!("----"); - println!("base ({} +- {}) * 10^{}", base_price, base.conf, base.expo); - println!("other ({} +- {}) * 10^{}", other_price, other.conf, other.expo); - // Compute the midprice, base in terms of other. // Uses at most 57 bits let midprice = base_price * PD_SCALE / other_price; let midprice_expo = PD_EXPO + base.expo - other.expo; - println!("mean {} * 10^{}", midprice, midprice_expo); // Compute the confidence interval. // This code uses the 1-norm instead of the 2-norm for computational reasons. @@ -266,11 +261,9 @@ impl PriceConf { // at most 58 bits let confidence_pct = base_confidence_pct + other_confidence_pct; - println!("rescaled_z {} * 10^{}", confidence_pct, PD_EXPO); // at most 115 bits let conf = (confidence_pct as u128) * (midprice as u128); let conf_expo = PD_EXPO + midprice_expo; - println!("conf {} * 10^{}", conf, conf_expo); // Scale results to the target exponent. let midprice_in_result_expo = PriceConf::scale_to_exponent(midprice as u128, midprice_expo, result_expo); From 8e1a5ca7875b65dcf9474b4f99e90f714de92d86 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Tue, 21 Dec 2021 15:12:10 -0600 Subject: [PATCH 16/33] adding solana tx stuff --- Cargo.toml | 8 +++++--- src/entrypoint.rs | 16 ++++++++++++++++ src/lib.rs | 3 +++ src/processor.rs | 36 ++++++++++++++++++++++++++++++++++++ tests/integration.rs | 32 ++++++++++++++++++++++++++++++++ 5 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 src/entrypoint.rs create mode 100644 src/processor.rs create mode 100644 tests/integration.rs diff --git a/Cargo.toml b/Cargo.toml index 43dc019..d1114f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,13 +12,15 @@ readme = "README.md" [features] test-bpf = [] +no-entrypoint = [] [dependencies] -solana-program = "1.6.7" +solana-program = "1.8.1" [dev-dependencies] -solana-client = "1.6.7" -solana-sdk = "1.6.7" +solana-program-test = "1.8.1" +solana-client = "1.8.1" +solana-sdk = "1.8.1" [lib] crate-type = ["cdylib", "lib"] diff --git a/src/entrypoint.rs b/src/entrypoint.rs new file mode 100644 index 0000000..c0c253a --- /dev/null +++ b/src/entrypoint.rs @@ -0,0 +1,16 @@ +//! Program entrypoint + +#![cfg(not(feature = "no-entrypoint"))] + +use solana_program::{ + account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, pubkey::Pubkey, +}; + +entrypoint!(process_instruction); +fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + crate::processor::process_instruction(program_id, accounts, instruction_data) +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index ed5d34a..43d7d14 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,6 @@ +mod entrypoint; +pub mod processor; + pub const MAGIC : u32 = 0xa1b2c3d4; pub const VERSION_2 : u32 = 2; pub const VERSION : u32 = VERSION_2; diff --git a/src/processor.rs b/src/processor.rs new file mode 100644 index 0000000..07712e0 --- /dev/null +++ b/src/processor.rs @@ -0,0 +1,36 @@ +//! Program instruction processor + +use solana_program::{ + account_info::AccountInfo, + entrypoint::ProgramResult, + log::{sol_log_compute_units, sol_log_params, sol_log_slice}, + msg, + pubkey::Pubkey, +}; + +/// Instruction processor +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + // Log a string + msg!("static string"); + + // Log a slice + sol_log_slice(instruction_data); + + // Log a formatted message, use with caution can be expensive + msg!("formatted {}: {:?}", "message", instruction_data); + + // Log a public key + program_id.log(); + + // Log all the program's input parameters + sol_log_params(accounts, instruction_data); + + // Log the number of compute units remaining that the program can consume. + sol_log_compute_units(); + + Ok(()) +} \ No newline at end of file diff --git a/tests/integration.rs b/tests/integration.rs new file mode 100644 index 0000000..20718d6 --- /dev/null +++ b/tests/integration.rs @@ -0,0 +1,32 @@ +use { + solana_program::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + }, + solana_program_test::*, + solana_sdk::{signature::Signer, transaction::Transaction}, + pyth_client::processor::process_instruction, + std::str::FromStr, +}; + +#[tokio::test] +async fn test_logging() { + let program_id = Pubkey::from_str("Logging111111111111111111111111111111111111").unwrap(); + let (mut banks_client, payer, recent_blockhash) = ProgramTest::new( + "pyth_client", + program_id, + processor!(process_instruction), + ) + .start() + .await; + let mut transaction = Transaction::new_with_payer( + &[Instruction::new_with_bincode( + program_id, + &[10_u8, 11, 12, 13, 14], + vec![AccountMeta::new(Pubkey::new_unique(), false)], + )], + Some(&payer.pubkey()), + ); + transaction.sign(&[&payer], recent_blockhash); + banks_client.process_transaction(transaction).await.unwrap(); +} \ No newline at end of file From f076bb448f86efc1cf7ba8dc387e0cda80e6f7db Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Wed, 22 Dec 2021 12:34:17 -0600 Subject: [PATCH 17/33] change approach a bit --- README.md | 8 -- src/lib.rs | 225 +++++++++++++++++++++++++++++++++-------------------- 2 files changed, 139 insertions(+), 94 deletions(-) diff --git a/README.md b/README.md index 04182fa..272606f 100644 --- a/README.md +++ b/README.md @@ -38,11 +38,3 @@ product_account .. 6MEwdxe4g1NeAF9u6KDG14anJpFsVEa2cvr5H6iriFZ8 twap ......... 7426390900 twac ......... 2259870 ``` - -### Development - -``` -cargo test-bpf -``` - - diff --git a/src/lib.rs b/src/lib.rs index ed5d34a..6b83232 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -160,15 +160,14 @@ impl Price { * this method returns the price of X/Y. Use this method to get the price of e.g., mSOL/SOL from * the mSOL/USD and SOL/USD accounts. * - * `result_expo` determines the exponent of the result, i.e., the number of digits of precision in - * the price. For any given base/quote pair, the minimum possible exponent is - * `-9 + self.exponent - quote.exponent`. (This minimum exponent reflects the maximum possible - * precision for the result given the precision of the two inputs and the numeric representation.) + * `result_expo` determines the exponent of the result, i.e., the number of digits below the decimal + * point. This method returns `None` if either the price or confidence are too large to be + * represented with the requested exponent. */ - pub fn get_price_in_quote(&self, quote: Price, result_expo: i32) -> Option { + pub fn get_price_in_quote(&self, quote: &Price, result_expo: i32) -> Option { return match (self.get_current_price(), quote.get_current_price()) { (Some(base_price_conf), Some(quote_price_conf)) => - base_price_conf.div(quote_price_conf, result_expo), + base_price_conf.div("e_price_conf)?.scale_to_exponent(result_expo), (_, _) => None, } } @@ -214,23 +213,24 @@ impl PriceConf { * The uncertainty propagation algorithm is an approximation due to computational limitations * that may slightly overestimate the resulting uncertainty (by at most a factor of sqrt(2)). * - * `result_expo` determines the exponent of the result. The minimum possible exponent is - * `-9 + self.exponent - other.exponent`. (This minimum exponent reflects the maximum possible - * precision for the result given the precision of the two inputs and the numeric representation.) - * TODO: possibly allow smaller exponents + * This method will automatically select a reasonable exponent for the result. If both + * `self` and `other` are normalized, the exponent is `self.expo + PD_EXPO - other.expo` (i.e., + * the fraction has `PD_EXPO` digits of additional precision). If they are not normalized, + * this method will normalize them, resulting in an unpredictable result exponent. + * If the result is used in a context that requires a specific exponent, please call + * `scale_to_exponent` on it. * * This function will return `None` unless all of the following conditions are satisfied: * 1. The prices of self and other are > 0. - * 2. The resulting price and confidence can be represented using a 64-bit number with - * exponent `result_expo`. - * 3. The confidence interval of self / other are << MAX_PD_V_U64 times their the respective price. - * (This condition should essentially always be satisfied in the real world.) + * 2. The confidence of the result can be represented using a 64-bit number in the computed + * exponent. This condition will fail if the confidence is >> the price of either input, + * (which should almost never occur in the real world) */ - pub fn div(&self, other: PriceConf, result_expo: i32) -> Option { + pub fn div(&self, other: &PriceConf) -> Option { // PriceConf is not guaranteed to store its price/confidence in normalized form. // Normalize them here to bound the range of price/conf, which is required to perform // arithmetic operations. - match (self.normalized(), other.normalized()) { + match (self.normalize(), other.normalize()) { (Some(base), Some(other)) => { // Note that normalization implies that the prices can be cast to u64. // We need prices as u64 in order to divide, as solana doesn't implement signed division. @@ -261,38 +261,53 @@ impl PriceConf { // at most 58 bits let confidence_pct = base_confidence_pct + other_confidence_pct; - // at most 115 bits - let conf = (confidence_pct as u128) * (midprice as u128); - let conf_expo = PD_EXPO + midprice_expo; - - // Scale results to the target exponent. - let midprice_in_result_expo = PriceConf::scale_to_exponent(midprice as u128, midprice_expo, result_expo); - let conf_in_result_expo = PriceConf::scale_to_exponent(conf, conf_expo, result_expo); - match (midprice_in_result_expo, conf_in_result_expo) { - (Some(m), Some(c)) => { - let m_i64 = m as i64; - // This should be guaranteed to succeed because midprice uses <= 57 bits - assert!(m_i64 >= 0); - - Some(PriceConf { - price: m_i64, - conf: c, - expo: result_expo - }) - } - (_, _) => None + // at most 57 + 58 - 29 = 86 bits, with the same exponent as the midprice. + // FIXME: round this up. There's a div_ceil method but it's unstable (?) + let conf = ((confidence_pct as u128) * (midprice as u128)) / (PD_SCALE as u128); + + // Note that this check only fails if an argument's confidence interval was >> its price, + // in which case None is a reasonable result, as we have essentially 0 information about the price. + if conf < (u64::MAX as u128) { + let m_i64 = midprice as i64; + // This should be guaranteed to succeed because midprice uses <= 57 bits + assert!(m_i64 >= 0); + Some(PriceConf { + price: m_i64, + conf: conf as u64, + expo: midprice_expo, + }) + } else { + None } } (_, _) => None } } + // FIXME Implement these functions + // The idea is that you should be able to get the price of a mixture of tokens (e.g., for LP tokens) + // using something like: + // price1.scale_to_exponent(result_expo).cmul(qty1, 0).add( + // price2.scale_to_exponent(result_expo).cmul(qty2, 0) + // ) + // + // Add two PriceConfs assuming the expos are == + pub fn add(&self, other: PriceConf) -> Option { + panic!() + } + + // multiply by a constant + pub fn cmul(&self, c: u64, e: i32) -> Option { + panic!() + } + /** * Get a copy of this struct where the price and confidence * have been normalized to be less than `MAX_PD_V_U64`. * Returns `None` if `price == 0` before or after normalization. + * FIXME: tests */ - pub fn normalized(&self) -> Option { + pub fn normalize(&self) -> Option { if self.price > 0 { // BPF only supports unsigned division let mut p: u64 = self.price as u64; @@ -322,28 +337,46 @@ impl PriceConf { /** * Scale num so that its exponent is target_expo. - * This method can only reduce precision, i.e., target_expo must be > current_expo. + * FIXME: tests */ - fn scale_to_exponent( - num: u128, - current_expo: i32, + pub fn scale_to_exponent( + &self, target_expo: i32, - ) -> Option { - let mut delta = target_expo - current_expo; - let mut res = num; + ) -> Option { + let mut delta = target_expo - self.expo; if delta >= 0 { + let mut p = self.price; + let mut c = self.conf; while delta > 0 { - res /= 10; + p /= 10; + c /= 10; delta -= 1; } + // FIXME: check for 0s here and handle this case more gracefully. (0, 0) is a bad answer that will cause bugs + Some(PriceConf { + price: p, + conf: c, + expo: target_expo + }) + } else { + let mut p = Some(self.price); + let mut c = Some(self.conf); - if res <= (u64::MAX as u128) { - Some(res as u64) - } else { - None + while delta < 0 { + p = p?.checked_mul(10); + c = c?.checked_mul(10); + delta += 1; + } + + match (p, c) { + (Some(price), Some(conf)) => + Some(PriceConf { + price, + conf, + expo: target_expo + }), + (_, _) => None, } - } else { - None } } } @@ -380,77 +413,97 @@ mod test { } } + fn pc_scaled(price: i64, conf: u64, cur_expo: i32, expo: i32) -> PriceConf { + PriceConf { + price: price, + conf: conf, + expo: cur_expo + }.scale_to_exponent(expo).unwrap() + } + #[test] fn test_rebase() { fn test_succeeds( price1: PriceConf, price2: PriceConf, - result_expo: i32, - expected: (i64, u64), + expected: PriceConf, ) { - let result = price1.div(price2, result_expo); - assert_eq!(result, Some(pc(expected.0, expected.1, result_expo))); + assert_eq!(price1.div(&price2).unwrap(), expected); } fn test_fails( price1: PriceConf, price2: PriceConf, - result_expo: i32, ) { - let result = price1.div(price2, result_expo); + let result = price1.div(&price2); assert_eq!(result, None); } - test_succeeds(pc(1, 1, 0), pc(1, 1, 0), 0, (1, 2)); - test_succeeds(pc(10, 1, 0), pc(1, 1, 0), 0, (10, 11)); - test_succeeds(pc(1, 1, 1), pc(1, 1, 0), 0, (10, 20)); - test_succeeds(pc(1, 1, -8), pc(1, 1, -8), -8, (100_000_000, 200_000_000)); - test_succeeds(pc(1, 1, 0), pc(5, 1, 0), 0, (0, 0)); - test_succeeds(pc(1, 1, 0), pc(5, 1, 0), -1, (2, 2)); - test_succeeds(pc(1, 1, 0), pc(5, 1, 0), -2, (20, 24)); - test_succeeds(pc(1, 1, 0), pc(5, 1, 0), -9, (200_000_000, 240_000_000)); + test_succeeds(pc(1, 1, 0), pc(1, 1, 0), pc_scaled(1, 2, 0, PD_EXPO)); + test_succeeds(pc(1, 1, -8), pc(1, 1, -8), pc_scaled(1, 2, 0, PD_EXPO)); + test_succeeds(pc(10, 1, 0), pc(1, 1, 0), pc_scaled(10, 11, 0, PD_EXPO)); + test_succeeds(pc(1, 1, 1), pc(1, 1, 0), pc_scaled(10, 20, 0, PD_EXPO + 1)); + test_succeeds(pc(1, 1, 0), pc(5, 1, 0), pc_scaled(20, 24, -2, PD_EXPO)); // Different exponents in the two inputs - test_succeeds(pc(100, 10, -8), pc(2, 1, -7), -8, (500_000_000, 300_000_000)); - test_succeeds(pc(100, 10, -4), pc(2, 1, 0), -8, (500_000, 300_000)); - test_succeeds(pc(100, 10, -4), pc(2, 1, 0), -4, (50, 30)); + test_succeeds(pc(100, 10, -8), pc(2, 1, -7), pc_scaled(500_000_000, 300_000_000, -8, PD_EXPO - 1)); + test_succeeds(pc(100, 10, -4), pc(2, 1, 0), pc_scaled(500_000, 300_000, -8, PD_EXPO + -4)); // Test with end range of possible inputs where the output should not lose precision. - test_succeeds(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), 0, (1, 2)); - test_succeeds(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(1, 1, 0), 0, (MAX_PD_V_I64, 2 * MAX_PD_V_U64)); - test_succeeds(pc(1, MAX_PD_V_U64, 0), pc(1, MAX_PD_V_U64, 0), 0, (1, 2 * MAX_PD_V_U64)); - test_succeeds(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(1, MAX_PD_V_U64, 0), 0, (MAX_PD_V_I64, MAX_PD_V_U64 * MAX_PD_V_U64 + (MAX_PD_V_I64 as u64))); + test_succeeds(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc_scaled(1, 2, 0, PD_EXPO)); + test_succeeds(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(1, 1, 0), pc_scaled(MAX_PD_V_I64, 2 * MAX_PD_V_U64, 0, PD_EXPO)); + test_succeeds(pc(1, 1, 0), + pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), + pc((PD_SCALE as i64) / MAX_PD_V_I64, 2 * (PD_SCALE / MAX_PD_V_U64), PD_EXPO)); + + test_succeeds(pc(1, MAX_PD_V_U64, 0), pc(1, MAX_PD_V_U64, 0), pc_scaled(1, 2 * MAX_PD_V_U64, 0, PD_EXPO)); + // This fails because the confidence interval is too large to be represented in PD_EXPO + test_fails(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(1, MAX_PD_V_U64, 0)); + + // Unnormalized tests below here // More realistic inputs (get BTC price in ETH) - // Note these inputs are not normalized let ten_e7: i64 = 10000000; let uten_e7: u64 = 10000000; test_succeeds(pc(520010 * ten_e7, 310 * uten_e7, -8), pc(38591 * ten_e7, 18 * uten_e7, -8), - -8, (1347490347, 1431804)); + pc(1347490347, 1431804, -8)); // Test with end range of possible inputs to identify overflow // These inputs will lose precision due to the initial normalization. // Get the rounded versions of these inputs in order to compute the expected results. - let normed = pc(i64::MAX, u64::MAX, 0).normalized().unwrap(); - let max_i64 = normed.price * (10_i64.pow(normed.expo as u32)); - - test_succeeds(pc(i64::MAX, u64::MAX, 0), pc(i64::MAX, u64::MAX, 0), 0, (1, 4)); - test_succeeds(pc(i64::MAX, u64::MAX, 0), pc(1, 1, 0), 7, (max_i64 / ten_e7, 3 * ((max_i64 as u64) / uten_e7))); + let normed = pc(i64::MAX, u64::MAX, 0).normalize().unwrap(); + + test_succeeds(pc(i64::MAX, u64::MAX, 0), pc(i64::MAX, u64::MAX, 0), pc_scaled(1, 4, 0, PD_EXPO)); + test_succeeds(pc(i64::MAX, u64::MAX, 0), + pc(1, 1, 0), + pc_scaled(normed.price, 3 * (normed.price as u64), normed.expo, normed.expo + PD_EXPO)); + test_succeeds(pc(1, 1, 0), + pc(i64::MAX, u64::MAX, 0), + pc((PD_SCALE as i64) / normed.price, 3 * (PD_SCALE / (normed.price as u64)), PD_EXPO - normed.expo)); + + // FIXME: rounding the confidence to 0 may not be ideal here. Probably should guarantee this rounds up. + test_succeeds(pc(i64::MAX, 1, 0), pc(i64::MAX, 1, 0), pc_scaled(1, 0, 0, PD_EXPO)); + test_succeeds(pc(i64::MAX, 1, 0), + pc(1, 1, 0), + pc_scaled(normed.price, normed.price as u64, normed.expo, normed.expo + PD_EXPO)); + test_succeeds(pc(1, 1, 0), + pc(i64::MAX, 1, 0), + pc((PD_SCALE as i64) / normed.price, PD_SCALE / (normed.price as u64), PD_EXPO - normed.expo)); // Price is zero pre-normalization - test_fails(pc(0, 1, 0), pc(1, 1, 0), PD_EXPO - 1); - test_fails(pc(1, 1, 0), pc(0, 1, 0), PD_EXPO - 1); + test_fails(pc(0, 1, 0), pc(1, 1, 0)); + test_fails(pc(1, 1, 0), pc(0, 1, 0)); // Can't normalize the input when the confidence is >> price. - test_fails(pc(1, 1, 0), pc(1, u64::MAX, 0), 7); - test_fails(pc(1, u64::MAX, 0), pc(1, 1, 0), 7); + test_fails(pc(1, 1, 0), pc(1, u64::MAX, 0)); + test_fails(pc(1, u64::MAX, 0), pc(1, 1, 0)); + // FIXME: move to scaling tests // Result exponent too small + /* test_succeeds(pc(1, 1, 0), pc(1, 1, 0), PD_EXPO, (1 * (PD_SCALE as i64), 2 * PD_SCALE)); test_fails(pc(1, 1, 0), pc(1, 1, 0), PD_EXPO - 1); - - // TODO: handle the case where the result exponent is too large more gracefully. - test_succeeds(pc(1, 1, 0), pc(1, 1, 0), 1, (0, 0)); + */ } } \ No newline at end of file From 44254dbf6e85b4c821f4db12ad05ca8f620688e8 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Wed, 22 Dec 2021 13:40:24 -0600 Subject: [PATCH 18/33] this seems to work --- Cargo.toml | 2 + src/instruction.rs | 116 +++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 11 +++- src/processor.rs | 49 +++++++++--------- tests/integration.rs | 35 +++++++++---- 5 files changed, 178 insertions(+), 35 deletions(-) create mode 100644 src/instruction.rs diff --git a/Cargo.toml b/Cargo.toml index d1114f1..17c6e6e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,8 @@ no-entrypoint = [] [dependencies] solana-program = "1.8.1" +borsh = "0.9" +borsh-derive = "0.9.0" [dev-dependencies] solana-program-test = "1.8.1" diff --git a/src/instruction.rs b/src/instruction.rs new file mode 100644 index 0000000..0574b23 --- /dev/null +++ b/src/instruction.rs @@ -0,0 +1,116 @@ +//! Program instructions, used for end-to-end testing and instruction counts + +use { + crate::id, + borsh::{BorshDeserialize, BorshSerialize}, + solana_program::instruction::Instruction, + crate::PriceConf, +}; + +/// Instructions supported by the pyth-client program, used for testing and +/// instruction counts +#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, PartialEq)] +pub enum PythClientInstruction { + Divide { + numerator: PriceConf, + denominator: PriceConf, + }, + /// Don't do anything for comparison + /// + /// No accounts required for this instruction + Noop, +} + +pub fn divide(numerator: PriceConf, denominator: PriceConf) -> Instruction { + Instruction { + program_id: id(), + accounts: vec![], + data: PythClientInstruction::Divide { numerator, denominator } + .try_to_vec() + .unwrap(), + } +} + +/* +/// Create SquareRoot instruction +pub fn sqrt_u64(radicand: u64) -> Instruction { + Instruction { + program_id: id(), + accounts: vec![], + data: PythClientInstruction::SquareRootU64 { radicand } + .try_to_vec() + .unwrap(), + } +} + +/// Create SquareRoot instruction +pub fn sqrt_u128(radicand: u128) -> Instruction { + Instruction { + program_id: id(), + accounts: vec![], + data: PythClientInstruction::SquareRootU128 { radicand } + .try_to_vec() + .unwrap(), + } +} + +/// Create PreciseSquareRoot instruction +pub fn u64_multiply(multiplicand: u64, multiplier: u64) -> Instruction { + Instruction { + program_id: id(), + accounts: vec![], + data: PythClientInstruction::U64Multiply { + multiplicand, + multiplier, + } + .try_to_vec() + .unwrap(), + } +} + +/// Create PreciseSquareRoot instruction +pub fn u64_divide(dividend: u64, divisor: u64) -> Instruction { + Instruction { + program_id: id(), + accounts: vec![], + data: PythClientInstruction::U64Divide { dividend, divisor } + .try_to_vec() + .unwrap(), + } +} + +/// Create PreciseSquareRoot instruction +pub fn f32_multiply(multiplicand: f32, multiplier: f32) -> Instruction { + Instruction { + program_id: id(), + accounts: vec![], + data: PythClientInstruction::F32Multiply { + multiplicand, + multiplier, + } + .try_to_vec() + .unwrap(), + } +} + +/// Create PreciseSquareRoot instruction +pub fn f32_divide(dividend: f32, divisor: f32) -> Instruction { + Instruction { + program_id: id(), + accounts: vec![], + data: PythClientInstruction::F32Divide { dividend, divisor } + .try_to_vec() + .unwrap(), + } +} + + */ + +/// Create PreciseSquareRoot instruction +pub fn noop() -> Instruction { + Instruction { + program_id: id(), + accounts: vec![], + data: PythClientInstruction::Noop.try_to_vec().unwrap(), + } +} diff --git a/src/lib.rs b/src/lib.rs index c949ea8..5c1dbb5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,13 @@ +use { + borsh::{BorshDeserialize, BorshSerialize}, +}; + mod entrypoint; pub mod processor; +pub mod instruction; + +// FIXME +solana_program::declare_id!("PythC11111111111111111111111111111111111111"); pub const MAGIC : u32 = 0xa1b2c3d4; pub const VERSION_2 : u32 = 2; @@ -202,8 +210,7 @@ impl Price { * PriceConf { price: 123, conf: 1, expo: 2 }; // represents 12300 +- 100 * ``` */ -#[derive(PartialEq)] -#[derive(Debug)] +#[derive(PartialEq, Debug, BorshSerialize, BorshDeserialize, Clone)] pub struct PriceConf { pub price: i64, pub conf: u64, diff --git a/src/processor.rs b/src/processor.rs index 07712e0..4e7657a 100644 --- a/src/processor.rs +++ b/src/processor.rs @@ -7,30 +7,31 @@ use solana_program::{ msg, pubkey::Pubkey, }; +use crate::{ + instruction::PythClientInstruction, + PriceConf, +}; +use borsh::BorshDeserialize; -/// Instruction processor pub fn process_instruction( - program_id: &Pubkey, - accounts: &[AccountInfo], - instruction_data: &[u8], + _program_id: &Pubkey, + _accounts: &[AccountInfo], + input: &[u8], ) -> ProgramResult { - // Log a string - msg!("static string"); - - // Log a slice - sol_log_slice(instruction_data); - - // Log a formatted message, use with caution can be expensive - msg!("formatted {}: {:?}", "message", instruction_data); - - // Log a public key - program_id.log(); - - // Log all the program's input parameters - sol_log_params(accounts, instruction_data); - - // Log the number of compute units remaining that the program can consume. - sol_log_compute_units(); - - Ok(()) -} \ No newline at end of file + let instruction = PythClientInstruction::try_from_slice(input).unwrap(); + match instruction { + PythClientInstruction::Divide { numerator, denominator } => { + msg!("Calculating numerator.div(denominator)"); + sol_log_compute_units(); + let result = numerator.div(&denominator); + sol_log_compute_units(); + msg!("{:?}", result); + Ok(()) + } + PythClientInstruction::Noop => { + msg!("Do nothing"); + msg!("{}", 0_u64); + Ok(()) + } + } +} diff --git a/tests/integration.rs b/tests/integration.rs index 20718d6..f74d948 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -7,26 +7,43 @@ use { solana_sdk::{signature::Signer, transaction::Transaction}, pyth_client::processor::process_instruction, std::str::FromStr, + borsh::BorshDeserialize, + pyth_client::{id, instruction, PriceConf}, }; -#[tokio::test] -async fn test_logging() { - let program_id = Pubkey::from_str("Logging111111111111111111111111111111111111").unwrap(); +async fn test_instr(instr: Instruction) { let (mut banks_client, payer, recent_blockhash) = ProgramTest::new( "pyth_client", - program_id, + id(), processor!(process_instruction), ) .start() .await; let mut transaction = Transaction::new_with_payer( - &[Instruction::new_with_bincode( - program_id, - &[10_u8, 11, 12, 13, 14], - vec![AccountMeta::new(Pubkey::new_unique(), false)], - )], + &[instr], Some(&payer.pubkey()), ); transaction.sign(&[&payer], recent_blockhash); banks_client.process_transaction(transaction).await.unwrap(); +} + +#[tokio::test] +async fn test_noop() { + test_instr(instruction::noop()).await; +} + +#[tokio::test] +async fn test_div() { + test_instr(instruction::divide( + PriceConf { + price: 1, + conf: 1, + expo: 0 + }, + PriceConf { + price: 1, + conf: 1, + expo: 0 + } + )).await; } \ No newline at end of file From acb2f231aec4bd1a94674ab569ac2df3b48575fd Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Thu, 23 Dec 2021 08:53:45 -0600 Subject: [PATCH 19/33] comment --- src/lib.rs | 2 +- src/processor.rs | 2 +- tests/integration.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 5c1dbb5..0d2ddbb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -319,7 +319,7 @@ impl PriceConf { */ pub fn normalize(&self) -> Option { if self.price > 0 { - // BPF only supports unsigned division + // FIXME: support negative numbers. let mut p: u64 = self.price as u64; let mut c: u64 = self.conf; let mut e: i32 = self.expo; diff --git a/src/processor.rs b/src/processor.rs index 4e7657a..fc4f28f 100644 --- a/src/processor.rs +++ b/src/processor.rs @@ -25,7 +25,7 @@ pub fn process_instruction( sol_log_compute_units(); let result = numerator.div(&denominator); sol_log_compute_units(); - msg!("{:?}", result); + msg!("result: {:?}", result); Ok(()) } PythClientInstruction::Noop => { diff --git a/tests/integration.rs b/tests/integration.rs index f74d948..7be0beb 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -36,7 +36,7 @@ async fn test_noop() { async fn test_div() { test_instr(instruction::divide( PriceConf { - price: 1, + price: i64::MAX, conf: 1, expo: 0 }, From 4536f0c901fa3302f1635f90164b970422990389 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Thu, 23 Dec 2021 08:56:23 -0600 Subject: [PATCH 20/33] cleanup --- src/instruction.rs | 77 +--------------------------------------------- src/lib.rs | 1 - 2 files changed, 1 insertion(+), 77 deletions(-) diff --git a/src/instruction.rs b/src/instruction.rs index 0574b23..62dee61 100644 --- a/src/instruction.rs +++ b/src/instruction.rs @@ -31,82 +31,7 @@ pub fn divide(numerator: PriceConf, denominator: PriceConf) -> Instruction { } } -/* -/// Create SquareRoot instruction -pub fn sqrt_u64(radicand: u64) -> Instruction { - Instruction { - program_id: id(), - accounts: vec![], - data: PythClientInstruction::SquareRootU64 { radicand } - .try_to_vec() - .unwrap(), - } -} - -/// Create SquareRoot instruction -pub fn sqrt_u128(radicand: u128) -> Instruction { - Instruction { - program_id: id(), - accounts: vec![], - data: PythClientInstruction::SquareRootU128 { radicand } - .try_to_vec() - .unwrap(), - } -} - -/// Create PreciseSquareRoot instruction -pub fn u64_multiply(multiplicand: u64, multiplier: u64) -> Instruction { - Instruction { - program_id: id(), - accounts: vec![], - data: PythClientInstruction::U64Multiply { - multiplicand, - multiplier, - } - .try_to_vec() - .unwrap(), - } -} - -/// Create PreciseSquareRoot instruction -pub fn u64_divide(dividend: u64, divisor: u64) -> Instruction { - Instruction { - program_id: id(), - accounts: vec![], - data: PythClientInstruction::U64Divide { dividend, divisor } - .try_to_vec() - .unwrap(), - } -} - -/// Create PreciseSquareRoot instruction -pub fn f32_multiply(multiplicand: f32, multiplier: f32) -> Instruction { - Instruction { - program_id: id(), - accounts: vec![], - data: PythClientInstruction::F32Multiply { - multiplicand, - multiplier, - } - .try_to_vec() - .unwrap(), - } -} - -/// Create PreciseSquareRoot instruction -pub fn f32_divide(dividend: f32, divisor: f32) -> Instruction { - Instruction { - program_id: id(), - accounts: vec![], - data: PythClientInstruction::F32Divide { dividend, divisor } - .try_to_vec() - .unwrap(), - } -} - - */ - -/// Create PreciseSquareRoot instruction +/// Noop instruction for comparison purposes pub fn noop() -> Instruction { Instruction { program_id: id(), diff --git a/src/lib.rs b/src/lib.rs index 0d2ddbb..7e77681 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,7 +6,6 @@ mod entrypoint; pub mod processor; pub mod instruction; -// FIXME solana_program::declare_id!("PythC11111111111111111111111111111111111111"); pub const MAGIC : u32 = 0xa1b2c3d4; From 74faf191e17f6b8faaec87f8ebe0a637839d905c Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Thu, 23 Dec 2021 09:08:40 -0600 Subject: [PATCH 21/33] refactor --- src/lib.rs | 311 +--------------------------------------------- src/price_conf.rs | 310 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 313 insertions(+), 308 deletions(-) create mode 100644 src/price_conf.rs diff --git a/src/lib.rs b/src/lib.rs index 7e77681..ac21b24 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,9 @@ mod entrypoint; pub mod processor; pub mod instruction; +mod price_conf; +pub use self::price_conf::PriceConf; + solana_program::declare_id!("PythC11111111111111111111111111111111111111"); pub const MAGIC : u32 = 0xa1b2c3d4; @@ -16,11 +19,6 @@ pub const PROD_ACCT_SIZE : usize = 512; pub const PROD_HDR_SIZE : usize = 48; pub const PROD_ATTR_SIZE : usize = PROD_ACCT_SIZE - PROD_HDR_SIZE; -// Constants for working with pyth's number representation -const PD_EXPO: i32 = -9; -const PD_SCALE: u64 = 1_000_000_000; -const MAX_PD_V_U64: u64 = (1 << 28) - 1; - // each account has its own type #[repr(C)] pub enum AccountType @@ -196,200 +194,6 @@ impl Price { } } - -/** - * A price with a degree of uncertainty, represented as a price +- a confidence interval. - * The confidence interval roughly corresponds to the standard error of a normal distribution. - * Both the price and confidence are stored in a fixed-point numeric representation, `x * 10^expo`, - * where `expo` is the exponent. For example: - * - * ``` - * use pyth_client::PriceConf; - * PriceConf { price: 12345, conf: 267, expo: -2 }; // represents 123.45 +- 2.67 - * PriceConf { price: 123, conf: 1, expo: 2 }; // represents 12300 +- 100 - * ``` - */ -#[derive(PartialEq, Debug, BorshSerialize, BorshDeserialize, Clone)] -pub struct PriceConf { - pub price: i64, - pub conf: u64, - pub expo: i32, -} - -impl PriceConf { - /** - * Divide this price by `other` while propagating the uncertainty in both prices into the result. - * The uncertainty propagation algorithm is an approximation due to computational limitations - * that may slightly overestimate the resulting uncertainty (by at most a factor of sqrt(2)). - * - * This method will automatically select a reasonable exponent for the result. If both - * `self` and `other` are normalized, the exponent is `self.expo + PD_EXPO - other.expo` (i.e., - * the fraction has `PD_EXPO` digits of additional precision). If they are not normalized, - * this method will normalize them, resulting in an unpredictable result exponent. - * If the result is used in a context that requires a specific exponent, please call - * `scale_to_exponent` on it. - * - * This function will return `None` unless all of the following conditions are satisfied: - * 1. The prices of self and other are > 0. - * 2. The confidence of the result can be represented using a 64-bit number in the computed - * exponent. This condition will fail if the confidence is >> the price of either input, - * (which should almost never occur in the real world) - */ - pub fn div(&self, other: &PriceConf) -> Option { - // PriceConf is not guaranteed to store its price/confidence in normalized form. - // Normalize them here to bound the range of price/conf, which is required to perform - // arithmetic operations. - match (self.normalize(), other.normalize()) { - (Some(base), Some(other)) => { - // Note that normalization implies that the prices can be cast to u64. - // We need prices as u64 in order to divide, as solana doesn't implement signed division. - // It's also extremely unclear what this method should do if one of the prices is negative, - // so assuming positive prices throughout seems fine. - - // These use at most 27 bits each - let base_price = base.price as u64; - let other_price = other.price as u64; - - // Compute the midprice, base in terms of other. - // Uses at most 57 bits - let midprice = base_price * PD_SCALE / other_price; - let midprice_expo = PD_EXPO + base.expo - other.expo; - - // Compute the confidence interval. - // This code uses the 1-norm instead of the 2-norm for computational reasons. - // The correct formula is midprice * sqrt(c_1^2 + c_2^2), where c_1 and c_2 are the - // confidence intervals in price-percentage terms of the base and other. This quantity - // is difficult to compute due to the sqrt, and overflow/underflow considerations. - // Instead, this code uses midprice * (c_1 + c_2). - // This quantity is at most a factor of sqrt(2) greater than the correct result, which - // shouldn't matter considering that confidence intervals are typically ~0.1% of the price. - - // The exponent is PD_EXPO for both of these. Each of these uses 57 bits. - let base_confidence_pct: u64 = (base.conf * PD_SCALE) / base_price; - let other_confidence_pct: u64 = (other.conf * PD_SCALE) / other_price; - - // at most 58 bits - let confidence_pct = base_confidence_pct + other_confidence_pct; - // at most 57 + 58 - 29 = 86 bits, with the same exponent as the midprice. - // FIXME: round this up. There's a div_ceil method but it's unstable (?) - let conf = ((confidence_pct as u128) * (midprice as u128)) / (PD_SCALE as u128); - - // Note that this check only fails if an argument's confidence interval was >> its price, - // in which case None is a reasonable result, as we have essentially 0 information about the price. - if conf < (u64::MAX as u128) { - let m_i64 = midprice as i64; - // This should be guaranteed to succeed because midprice uses <= 57 bits - assert!(m_i64 >= 0); - Some(PriceConf { - price: m_i64, - conf: conf as u64, - expo: midprice_expo, - }) - } else { - None - } - } - (_, _) => None - } - } - - // FIXME Implement these functions - // The idea is that you should be able to get the price of a mixture of tokens (e.g., for LP tokens) - // using something like: - // price1.scale_to_exponent(result_expo).cmul(qty1, 0).add( - // price2.scale_to_exponent(result_expo).cmul(qty2, 0) - // ) - // - // Add two PriceConfs assuming the expos are == - pub fn add(&self, other: PriceConf) -> Option { - panic!() - } - - // multiply by a constant - pub fn cmul(&self, c: u64, e: i32) -> Option { - panic!() - } - - /** - * Get a copy of this struct where the price and confidence - * have been normalized to be less than `MAX_PD_V_U64`. - * Returns `None` if `price == 0` before or after normalization. - * FIXME: tests - */ - pub fn normalize(&self) -> Option { - if self.price > 0 { - // FIXME: support negative numbers. - let mut p: u64 = self.price as u64; - let mut c: u64 = self.conf; - let mut e: i32 = self.expo; - - while p > MAX_PD_V_U64 || c > MAX_PD_V_U64 { - p = p / 10; - c = c / 10; - e += 1; - } - - // Can get p == 0 if confidence is >> price. - if p > 0 { - Some(PriceConf { - price: p as i64, - conf: c, - expo: e, - }) - } else { - None - } - } else { - None - } - } - - /** - * Scale num so that its exponent is target_expo. - * FIXME: tests - */ - pub fn scale_to_exponent( - &self, - target_expo: i32, - ) -> Option { - let mut delta = target_expo - self.expo; - if delta >= 0 { - let mut p = self.price; - let mut c = self.conf; - while delta > 0 { - p /= 10; - c /= 10; - delta -= 1; - } - // FIXME: check for 0s here and handle this case more gracefully. (0, 0) is a bad answer that will cause bugs - Some(PriceConf { - price: p, - conf: c, - expo: target_expo - }) - } else { - let mut p = Some(self.price); - let mut c = Some(self.conf); - - while delta < 0 { - p = p?.checked_mul(10); - c = c?.checked_mul(10); - delta += 1; - } - - match (p, c) { - (Some(price), Some(conf)) => - Some(PriceConf { - price, - conf, - expo: target_expo - }), - (_, _) => None, - } - } - } -} - struct AccKeyU64 { pub val: [u64;4] @@ -407,112 +211,3 @@ impl AccKey return k8.val[0]!=0 || k8.val[1]!=0 || k8.val[2]!=0 || k8.val[3]!=0; } } - -#[cfg(test)] -mod test { - use crate::{MAX_PD_V_U64, PriceConf, PD_SCALE, PD_EXPO}; - - const MAX_PD_V_I64: i64 = (1 << 28) - 1; - - fn pc(price: i64, conf: u64, expo: i32) -> PriceConf { - PriceConf { - price: price, - conf: conf, - expo: expo, - } - } - - fn pc_scaled(price: i64, conf: u64, cur_expo: i32, expo: i32) -> PriceConf { - PriceConf { - price: price, - conf: conf, - expo: cur_expo - }.scale_to_exponent(expo).unwrap() - } - - #[test] - fn test_rebase() { - fn test_succeeds( - price1: PriceConf, - price2: PriceConf, - expected: PriceConf, - ) { - assert_eq!(price1.div(&price2).unwrap(), expected); - } - - fn test_fails( - price1: PriceConf, - price2: PriceConf, - ) { - let result = price1.div(&price2); - assert_eq!(result, None); - } - - test_succeeds(pc(1, 1, 0), pc(1, 1, 0), pc_scaled(1, 2, 0, PD_EXPO)); - test_succeeds(pc(1, 1, -8), pc(1, 1, -8), pc_scaled(1, 2, 0, PD_EXPO)); - test_succeeds(pc(10, 1, 0), pc(1, 1, 0), pc_scaled(10, 11, 0, PD_EXPO)); - test_succeeds(pc(1, 1, 1), pc(1, 1, 0), pc_scaled(10, 20, 0, PD_EXPO + 1)); - test_succeeds(pc(1, 1, 0), pc(5, 1, 0), pc_scaled(20, 24, -2, PD_EXPO)); - - // Different exponents in the two inputs - test_succeeds(pc(100, 10, -8), pc(2, 1, -7), pc_scaled(500_000_000, 300_000_000, -8, PD_EXPO - 1)); - test_succeeds(pc(100, 10, -4), pc(2, 1, 0), pc_scaled(500_000, 300_000, -8, PD_EXPO + -4)); - - // Test with end range of possible inputs where the output should not lose precision. - test_succeeds(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc_scaled(1, 2, 0, PD_EXPO)); - test_succeeds(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(1, 1, 0), pc_scaled(MAX_PD_V_I64, 2 * MAX_PD_V_U64, 0, PD_EXPO)); - test_succeeds(pc(1, 1, 0), - pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), - pc((PD_SCALE as i64) / MAX_PD_V_I64, 2 * (PD_SCALE / MAX_PD_V_U64), PD_EXPO)); - - test_succeeds(pc(1, MAX_PD_V_U64, 0), pc(1, MAX_PD_V_U64, 0), pc_scaled(1, 2 * MAX_PD_V_U64, 0, PD_EXPO)); - // This fails because the confidence interval is too large to be represented in PD_EXPO - test_fails(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(1, MAX_PD_V_U64, 0)); - - // Unnormalized tests below here - - // More realistic inputs (get BTC price in ETH) - let ten_e7: i64 = 10000000; - let uten_e7: u64 = 10000000; - test_succeeds(pc(520010 * ten_e7, 310 * uten_e7, -8), - pc(38591 * ten_e7, 18 * uten_e7, -8), - pc(1347490347, 1431804, -8)); - - // Test with end range of possible inputs to identify overflow - // These inputs will lose precision due to the initial normalization. - // Get the rounded versions of these inputs in order to compute the expected results. - let normed = pc(i64::MAX, u64::MAX, 0).normalize().unwrap(); - - test_succeeds(pc(i64::MAX, u64::MAX, 0), pc(i64::MAX, u64::MAX, 0), pc_scaled(1, 4, 0, PD_EXPO)); - test_succeeds(pc(i64::MAX, u64::MAX, 0), - pc(1, 1, 0), - pc_scaled(normed.price, 3 * (normed.price as u64), normed.expo, normed.expo + PD_EXPO)); - test_succeeds(pc(1, 1, 0), - pc(i64::MAX, u64::MAX, 0), - pc((PD_SCALE as i64) / normed.price, 3 * (PD_SCALE / (normed.price as u64)), PD_EXPO - normed.expo)); - - // FIXME: rounding the confidence to 0 may not be ideal here. Probably should guarantee this rounds up. - test_succeeds(pc(i64::MAX, 1, 0), pc(i64::MAX, 1, 0), pc_scaled(1, 0, 0, PD_EXPO)); - test_succeeds(pc(i64::MAX, 1, 0), - pc(1, 1, 0), - pc_scaled(normed.price, normed.price as u64, normed.expo, normed.expo + PD_EXPO)); - test_succeeds(pc(1, 1, 0), - pc(i64::MAX, 1, 0), - pc((PD_SCALE as i64) / normed.price, PD_SCALE / (normed.price as u64), PD_EXPO - normed.expo)); - - // Price is zero pre-normalization - test_fails(pc(0, 1, 0), pc(1, 1, 0)); - test_fails(pc(1, 1, 0), pc(0, 1, 0)); - - // Can't normalize the input when the confidence is >> price. - test_fails(pc(1, 1, 0), pc(1, u64::MAX, 0)); - test_fails(pc(1, u64::MAX, 0), pc(1, 1, 0)); - - // FIXME: move to scaling tests - // Result exponent too small - /* - test_succeeds(pc(1, 1, 0), pc(1, 1, 0), PD_EXPO, (1 * (PD_SCALE as i64), 2 * PD_SCALE)); - test_fails(pc(1, 1, 0), pc(1, 1, 0), PD_EXPO - 1); - */ - } -} \ No newline at end of file diff --git a/src/price_conf.rs b/src/price_conf.rs new file mode 100644 index 0000000..a9358ef --- /dev/null +++ b/src/price_conf.rs @@ -0,0 +1,310 @@ +use { + borsh::{BorshDeserialize, BorshSerialize}, +}; + +// Constants for working with pyth's number representation +const PD_EXPO: i32 = -9; +const PD_SCALE: u64 = 1_000_000_000; +const MAX_PD_V_U64: u64 = (1 << 28) - 1; + +/** + * A price with a degree of uncertainty, represented as a price +- a confidence interval. + * The confidence interval roughly corresponds to the standard error of a normal distribution. + * Both the price and confidence are stored in a fixed-point numeric representation, `x * 10^expo`, + * where `expo` is the exponent. For example: + * + * ``` + * use pyth_client::PriceConf; + * PriceConf { price: 12345, conf: 267, expo: -2 }; // represents 123.45 +- 2.67 + * PriceConf { price: 123, conf: 1, expo: 2 }; // represents 12300 +- 100 + * ``` + */ +#[derive(PartialEq, Debug, BorshSerialize, BorshDeserialize, Clone)] +pub struct PriceConf { + pub price: i64, + pub conf: u64, + pub expo: i32, +} + +impl PriceConf { + /** + * Divide this price by `other` while propagating the uncertainty in both prices into the result. + * The uncertainty propagation algorithm is an approximation due to computational limitations + * that may slightly overestimate the resulting uncertainty (by at most a factor of sqrt(2)). + * + * This method will automatically select a reasonable exponent for the result. If both + * `self` and `other` are normalized, the exponent is `self.expo + PD_EXPO - other.expo` (i.e., + * the fraction has `PD_EXPO` digits of additional precision). If they are not normalized, + * this method will normalize them, resulting in an unpredictable result exponent. + * If the result is used in a context that requires a specific exponent, please call + * `scale_to_exponent` on it. + * + * This function will return `None` unless all of the following conditions are satisfied: + * 1. The prices of self and other are > 0. + * 2. The confidence of the result can be represented using a 64-bit number in the computed + * exponent. This condition will fail if the confidence is >> the price of either input, + * (which should almost never occur in the real world) + */ + pub fn div(&self, other: &PriceConf) -> Option { + // PriceConf is not guaranteed to store its price/confidence in normalized form. + // Normalize them here to bound the range of price/conf, which is required to perform + // arithmetic operations. + match (self.normalize(), other.normalize()) { + (Some(base), Some(other)) => { + // Note that normalization implies that the prices can be cast to u64. + // We need prices as u64 in order to divide, as solana doesn't implement signed division. + // It's also extremely unclear what this method should do if one of the prices is negative, + // so assuming positive prices throughout seems fine. + + // These use at most 27 bits each + let base_price = base.price as u64; + let other_price = other.price as u64; + + // Compute the midprice, base in terms of other. + // Uses at most 57 bits + let midprice = base_price * PD_SCALE / other_price; + let midprice_expo = PD_EXPO + base.expo - other.expo; + + // Compute the confidence interval. + // This code uses the 1-norm instead of the 2-norm for computational reasons. + // The correct formula is midprice * sqrt(c_1^2 + c_2^2), where c_1 and c_2 are the + // confidence intervals in price-percentage terms of the base and other. This quantity + // is difficult to compute due to the sqrt, and overflow/underflow considerations. + // Instead, this code uses midprice * (c_1 + c_2). + // This quantity is at most a factor of sqrt(2) greater than the correct result, which + // shouldn't matter considering that confidence intervals are typically ~0.1% of the price. + + // The exponent is PD_EXPO for both of these. Each of these uses 57 bits. + let base_confidence_pct: u64 = (base.conf * PD_SCALE) / base_price; + let other_confidence_pct: u64 = (other.conf * PD_SCALE) / other_price; + + // at most 58 bits + let confidence_pct = base_confidence_pct + other_confidence_pct; + // at most 57 + 58 - 29 = 86 bits, with the same exponent as the midprice. + // FIXME: round this up. There's a div_ceil method but it's unstable (?) + let conf = ((confidence_pct as u128) * (midprice as u128)) / (PD_SCALE as u128); + + // Note that this check only fails if an argument's confidence interval was >> its price, + // in which case None is a reasonable result, as we have essentially 0 information about the price. + if conf < (u64::MAX as u128) { + let m_i64 = midprice as i64; + // This should be guaranteed to succeed because midprice uses <= 57 bits + assert!(m_i64 >= 0); + Some(PriceConf { + price: m_i64, + conf: conf as u64, + expo: midprice_expo, + }) + } else { + None + } + } + (_, _) => None + } + } + + // FIXME Implement these functions + // The idea is that you should be able to get the price of a mixture of tokens (e.g., for LP tokens) + // using something like: + // price1.scale_to_exponent(result_expo).cmul(qty1, 0).add( + // price2.scale_to_exponent(result_expo).cmul(qty2, 0) + // ) + // + // Add two PriceConfs assuming the expos are == + pub fn add(&self, other: PriceConf) -> Option { + panic!() + } + + // multiply by a constant + pub fn cmul(&self, c: u64, e: i32) -> Option { + panic!() + } + + /** + * Get a copy of this struct where the price and confidence + * have been normalized to be less than `MAX_PD_V_U64`. + * Returns `None` if `price == 0` before or after normalization. + * FIXME: tests + */ + pub fn normalize(&self) -> Option { + if self.price > 0 { + // FIXME: support negative numbers. + let mut p: u64 = self.price as u64; + let mut c: u64 = self.conf; + let mut e: i32 = self.expo; + + while p > MAX_PD_V_U64 || c > MAX_PD_V_U64 { + p = p / 10; + c = c / 10; + e += 1; + } + + // Can get p == 0 if confidence is >> price. + if p > 0 { + Some(PriceConf { + price: p as i64, + conf: c, + expo: e, + }) + } else { + None + } + } else { + None + } + } + + /** + * Scale num so that its exponent is target_expo. + * FIXME: tests + */ + pub fn scale_to_exponent( + &self, + target_expo: i32, + ) -> Option { + let mut delta = target_expo - self.expo; + if delta >= 0 { + let mut p = self.price; + let mut c = self.conf; + while delta > 0 { + p /= 10; + c /= 10; + delta -= 1; + } + // FIXME: check for 0s here and handle this case more gracefully. (0, 0) is a bad answer that will cause bugs + Some(PriceConf { + price: p, + conf: c, + expo: target_expo, + }) + } else { + let mut p = Some(self.price); + let mut c = Some(self.conf); + + while delta < 0 { + p = p?.checked_mul(10); + c = c?.checked_mul(10); + delta += 1; + } + + match (p, c) { + (Some(price), Some(conf)) => + Some(PriceConf { + price, + conf, + expo: target_expo, + }), + (_, _) => None, + } + } + } +} + +#[cfg(test)] +mod test { + use crate::price_conf::{MAX_PD_V_U64, PD_EXPO, PD_SCALE, PriceConf}; + + const MAX_PD_V_I64: i64 = (1 << 28) - 1; + + fn pc(price: i64, conf: u64, expo: i32) -> PriceConf { + PriceConf { + price: price, + conf: conf, + expo: expo, + } + } + + fn pc_scaled(price: i64, conf: u64, cur_expo: i32, expo: i32) -> PriceConf { + PriceConf { + price: price, + conf: conf, + expo: cur_expo, + }.scale_to_exponent(expo).unwrap() + } + + #[test] + fn test_rebase() { + fn test_succeeds( + price1: PriceConf, + price2: PriceConf, + expected: PriceConf, + ) { + assert_eq!(price1.div(&price2).unwrap(), expected); + } + + fn test_fails( + price1: PriceConf, + price2: PriceConf, + ) { + let result = price1.div(&price2); + assert_eq!(result, None); + } + + test_succeeds(pc(1, 1, 0), pc(1, 1, 0), pc_scaled(1, 2, 0, PD_EXPO)); + test_succeeds(pc(1, 1, -8), pc(1, 1, -8), pc_scaled(1, 2, 0, PD_EXPO)); + test_succeeds(pc(10, 1, 0), pc(1, 1, 0), pc_scaled(10, 11, 0, PD_EXPO)); + test_succeeds(pc(1, 1, 1), pc(1, 1, 0), pc_scaled(10, 20, 0, PD_EXPO + 1)); + test_succeeds(pc(1, 1, 0), pc(5, 1, 0), pc_scaled(20, 24, -2, PD_EXPO)); + + // Different exponents in the two inputs + test_succeeds(pc(100, 10, -8), pc(2, 1, -7), pc_scaled(500_000_000, 300_000_000, -8, PD_EXPO - 1)); + test_succeeds(pc(100, 10, -4), pc(2, 1, 0), pc_scaled(500_000, 300_000, -8, PD_EXPO + -4)); + + // Test with end range of possible inputs where the output should not lose precision. + test_succeeds(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc_scaled(1, 2, 0, PD_EXPO)); + test_succeeds(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(1, 1, 0), pc_scaled(MAX_PD_V_I64, 2 * MAX_PD_V_U64, 0, PD_EXPO)); + test_succeeds(pc(1, 1, 0), + pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), + pc((PD_SCALE as i64) / MAX_PD_V_I64, 2 * (PD_SCALE / MAX_PD_V_U64), PD_EXPO)); + + test_succeeds(pc(1, MAX_PD_V_U64, 0), pc(1, MAX_PD_V_U64, 0), pc_scaled(1, 2 * MAX_PD_V_U64, 0, PD_EXPO)); + // This fails because the confidence interval is too large to be represented in PD_EXPO + test_fails(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(1, MAX_PD_V_U64, 0)); + + // Unnormalized tests below here + + // More realistic inputs (get BTC price in ETH) + let ten_e7: i64 = 10000000; + let uten_e7: u64 = 10000000; + test_succeeds(pc(520010 * ten_e7, 310 * uten_e7, -8), + pc(38591 * ten_e7, 18 * uten_e7, -8), + pc(1347490347, 1431804, -8)); + + // Test with end range of possible inputs to identify overflow + // These inputs will lose precision due to the initial normalization. + // Get the rounded versions of these inputs in order to compute the expected results. + let normed = pc(i64::MAX, u64::MAX, 0).normalize().unwrap(); + + test_succeeds(pc(i64::MAX, u64::MAX, 0), pc(i64::MAX, u64::MAX, 0), pc_scaled(1, 4, 0, PD_EXPO)); + test_succeeds(pc(i64::MAX, u64::MAX, 0), + pc(1, 1, 0), + pc_scaled(normed.price, 3 * (normed.price as u64), normed.expo, normed.expo + PD_EXPO)); + test_succeeds(pc(1, 1, 0), + pc(i64::MAX, u64::MAX, 0), + pc((PD_SCALE as i64) / normed.price, 3 * (PD_SCALE / (normed.price as u64)), PD_EXPO - normed.expo)); + + // FIXME: rounding the confidence to 0 may not be ideal here. Probably should guarantee this rounds up. + test_succeeds(pc(i64::MAX, 1, 0), pc(i64::MAX, 1, 0), pc_scaled(1, 0, 0, PD_EXPO)); + test_succeeds(pc(i64::MAX, 1, 0), + pc(1, 1, 0), + pc_scaled(normed.price, normed.price as u64, normed.expo, normed.expo + PD_EXPO)); + test_succeeds(pc(1, 1, 0), + pc(i64::MAX, 1, 0), + pc((PD_SCALE as i64) / normed.price, PD_SCALE / (normed.price as u64), PD_EXPO - normed.expo)); + + // Price is zero pre-normalization + test_fails(pc(0, 1, 0), pc(1, 1, 0)); + test_fails(pc(1, 1, 0), pc(0, 1, 0)); + + // Can't normalize the input when the confidence is >> price. + test_fails(pc(1, 1, 0), pc(1, u64::MAX, 0)); + test_fails(pc(1, u64::MAX, 0), pc(1, 1, 0)); + + // FIXME: move to scaling tests + // Result exponent too small + /* + test_succeeds(pc(1, 1, 0), pc(1, 1, 0), PD_EXPO, (1 * (PD_SCALE as i64), 2 * PD_SCALE)); + test_fails(pc(1, 1, 0), pc(1, 1, 0), PD_EXPO - 1); + */ + } +} From 50bd93b832bf5cfeec473112f5025776c3c4c7a5 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Thu, 23 Dec 2021 09:22:41 -0600 Subject: [PATCH 22/33] refactor --- src/price_conf.rs | 209 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 169 insertions(+), 40 deletions(-) diff --git a/src/price_conf.rs b/src/price_conf.rs index a9358ef..60ae03e 100644 --- a/src/price_conf.rs +++ b/src/price_conf.rs @@ -116,8 +116,50 @@ impl PriceConf { } // multiply by a constant - pub fn cmul(&self, c: u64, e: i32) -> Option { - panic!() + pub fn cmul(&self, c: i64, e: i32) -> Option { + self.mul(&PriceConf { price: c, conf: 0, expo: e}) + } + + pub fn mul(&self, other: &PriceConf) -> Option { + // PriceConf is not guaranteed to store its price/confidence in normalized form. + // Normalize them here to bound the range of price/conf, which is required to perform + // arithmetic operations. + match (self.normalize(), other.normalize()) { + (Some(base), Some(other)) => { + // TODO: think about negative numbers + // FIXME: bitcounts + + // These use at most 27 bits each + let base_price = base.price; + let other_price = other.price; + + // Compute the midprice, base in terms of other. + // Uses at most 27*2 bits + let midprice = base_price * other_price; + let midprice_expo = base.expo + other.expo; + + // Compute the confidence interval. + // This code uses the 1-norm instead of the 2-norm for computational reasons. + // The correct formula is midprice * sqrt(c_1^2 + c_2^2), where c_1 and c_2 are the + // confidence intervals in price-percentage terms of the base and other. This quantity + // is difficult to compute due to the sqrt, and overflow/underflow considerations. + // Instead, this code uses midprice * (c_1 + c_2). + // This quantity is at most a factor of sqrt(2) greater than the correct result, which + // shouldn't matter considering that confidence intervals are typically ~0.1% of the price. + // + // Note that this simplifies to + // pq * (a/p + b/q) = qa + bp + // 27*2 + 1 bits + let conf = base.conf * other.price + other.conf * base.price; + + Some(PriceConf { + price: midprice, + conf, + expo: midprice_expo, + }) + } + (_, _) => None + } } /** @@ -223,8 +265,8 @@ mod test { } #[test] - fn test_rebase() { - fn test_succeeds( + fn test_div() { + fn succeeds( price1: PriceConf, price2: PriceConf, expected: PriceConf, @@ -232,7 +274,7 @@ mod test { assert_eq!(price1.div(&price2).unwrap(), expected); } - fn test_fails( + fn fails( price1: PriceConf, price2: PriceConf, ) { @@ -240,65 +282,152 @@ mod test { assert_eq!(result, None); } - test_succeeds(pc(1, 1, 0), pc(1, 1, 0), pc_scaled(1, 2, 0, PD_EXPO)); - test_succeeds(pc(1, 1, -8), pc(1, 1, -8), pc_scaled(1, 2, 0, PD_EXPO)); - test_succeeds(pc(10, 1, 0), pc(1, 1, 0), pc_scaled(10, 11, 0, PD_EXPO)); - test_succeeds(pc(1, 1, 1), pc(1, 1, 0), pc_scaled(10, 20, 0, PD_EXPO + 1)); - test_succeeds(pc(1, 1, 0), pc(5, 1, 0), pc_scaled(20, 24, -2, PD_EXPO)); + succeeds(pc(1, 1, 0), pc(1, 1, 0), pc_scaled(1, 2, 0, PD_EXPO)); + succeeds(pc(1, 1, -8), pc(1, 1, -8), pc_scaled(1, 2, 0, PD_EXPO)); + succeeds(pc(10, 1, 0), pc(1, 1, 0), pc_scaled(10, 11, 0, PD_EXPO)); + succeeds(pc(1, 1, 1), pc(1, 1, 0), pc_scaled(10, 20, 0, PD_EXPO + 1)); + succeeds(pc(1, 1, 0), pc(5, 1, 0), pc_scaled(20, 24, -2, PD_EXPO)); + + // Different exponents in the two inputs + succeeds(pc(100, 10, -8), pc(2, 1, -7), pc_scaled(500_000_000, 300_000_000, -8, PD_EXPO - 1)); + succeeds(pc(100, 10, -4), pc(2, 1, 0), pc_scaled(500_000, 300_000, -8, PD_EXPO + -4)); + + // Test with end range of possible inputs where the output should not lose precision. + succeeds(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc_scaled(1, 2, 0, PD_EXPO)); + succeeds(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(1, 1, 0), pc_scaled(MAX_PD_V_I64, 2 * MAX_PD_V_U64, 0, PD_EXPO)); + succeeds(pc(1, 1, 0), + pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), + pc((PD_SCALE as i64) / MAX_PD_V_I64, 2 * (PD_SCALE / MAX_PD_V_U64), PD_EXPO)); + + succeeds(pc(1, MAX_PD_V_U64, 0), pc(1, MAX_PD_V_U64, 0), pc_scaled(1, 2 * MAX_PD_V_U64, 0, PD_EXPO)); + // This fails because the confidence interval is too large to be represented in PD_EXPO + fails(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(1, MAX_PD_V_U64, 0)); + + // Unnormalized tests below here + + // More realistic inputs (get BTC price in ETH) + let ten_e7: i64 = 10000000; + let uten_e7: u64 = 10000000; + succeeds(pc(520010 * ten_e7, 310 * uten_e7, -8), + pc(38591 * ten_e7, 18 * uten_e7, -8), + pc(1347490347, 1431804, -8)); + + // Test with end range of possible inputs to identify overflow + // These inputs will lose precision due to the initial normalization. + // Get the rounded versions of these inputs in order to compute the expected results. + let normed = pc(i64::MAX, u64::MAX, 0).normalize().unwrap(); + + succeeds(pc(i64::MAX, u64::MAX, 0), pc(i64::MAX, u64::MAX, 0), pc_scaled(1, 4, 0, PD_EXPO)); + succeeds(pc(i64::MAX, u64::MAX, 0), + pc(1, 1, 0), + pc_scaled(normed.price, 3 * (normed.price as u64), normed.expo, normed.expo + PD_EXPO)); + succeeds(pc(1, 1, 0), + pc(i64::MAX, u64::MAX, 0), + pc((PD_SCALE as i64) / normed.price, 3 * (PD_SCALE / (normed.price as u64)), PD_EXPO - normed.expo)); + + // FIXME: rounding the confidence to 0 may not be ideal here. Probably should guarantee this rounds up. + succeeds(pc(i64::MAX, 1, 0), pc(i64::MAX, 1, 0), pc_scaled(1, 0, 0, PD_EXPO)); + succeeds(pc(i64::MAX, 1, 0), + pc(1, 1, 0), + pc_scaled(normed.price, normed.price as u64, normed.expo, normed.expo + PD_EXPO)); + succeeds(pc(1, 1, 0), + pc(i64::MAX, 1, 0), + pc((PD_SCALE as i64) / normed.price, PD_SCALE / (normed.price as u64), PD_EXPO - normed.expo)); + + // Price is zero pre-normalization + fails(pc(0, 1, 0), pc(1, 1, 0)); + fails(pc(1, 1, 0), pc(0, 1, 0)); + + // Can't normalize the input when the confidence is >> price. + fails(pc(1, 1, 0), pc(1, u64::MAX, 0)); + fails(pc(1, u64::MAX, 0), pc(1, 1, 0)); + + // FIXME: move to scaling tests + // Result exponent too small + /* + test_succeeds(pc(1, 1, 0), pc(1, 1, 0), PD_EXPO, (1 * (PD_SCALE as i64), 2 * PD_SCALE)); + test_fails(pc(1, 1, 0), pc(1, 1, 0), PD_EXPO - 1); + */ + } + + #[test] + fn test_mul() { + fn succeeds( + price1: PriceConf, + price2: PriceConf, + expected: PriceConf, + ) { + assert_eq!(price1.mul(&price2).unwrap(), expected); + } + + fn fails( + price1: PriceConf, + price2: PriceConf, + ) { + let result = price1.mul(&price2); + assert_eq!(result, None); + } + + succeeds(pc(1, 1, 0), pc(1, 1, 0), pc(1, 2, 0)); + succeeds(pc(1, 1, -8), pc(1, 1, -8), pc(1, 2, -16)); + succeeds(pc(10, 1, 0), pc(1, 1, 0), pc(10, 11, 0)); + succeeds(pc(1, 1, 1), pc(1, 1, 0), pc(1, 2, 1)); + succeeds(pc(1, 1, 0), pc(5, 1, 0), pc(5, 6, 0)); // Different exponents in the two inputs - test_succeeds(pc(100, 10, -8), pc(2, 1, -7), pc_scaled(500_000_000, 300_000_000, -8, PD_EXPO - 1)); - test_succeeds(pc(100, 10, -4), pc(2, 1, 0), pc_scaled(500_000, 300_000, -8, PD_EXPO + -4)); + /* + succeeds(pc(100, 10, -8), pc(2, 1, -7), pc_scaled(500_000_000, 300_000_000, -8, PD_EXPO - 1)); + succeeds(pc(100, 10, -4), pc(2, 1, 0), pc_scaled(500_000, 300_000, -8, PD_EXPO + -4)); // Test with end range of possible inputs where the output should not lose precision. - test_succeeds(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc_scaled(1, 2, 0, PD_EXPO)); - test_succeeds(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(1, 1, 0), pc_scaled(MAX_PD_V_I64, 2 * MAX_PD_V_U64, 0, PD_EXPO)); - test_succeeds(pc(1, 1, 0), - pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), - pc((PD_SCALE as i64) / MAX_PD_V_I64, 2 * (PD_SCALE / MAX_PD_V_U64), PD_EXPO)); + succeeds(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc_scaled(1, 2, 0, PD_EXPO)); + succeeds(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(1, 1, 0), pc_scaled(MAX_PD_V_I64, 2 * MAX_PD_V_U64, 0, PD_EXPO)); + succeeds(pc(1, 1, 0), + pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), + pc((PD_SCALE as i64) / MAX_PD_V_I64, 2 * (PD_SCALE / MAX_PD_V_U64), PD_EXPO)); - test_succeeds(pc(1, MAX_PD_V_U64, 0), pc(1, MAX_PD_V_U64, 0), pc_scaled(1, 2 * MAX_PD_V_U64, 0, PD_EXPO)); + succeeds(pc(1, MAX_PD_V_U64, 0), pc(1, MAX_PD_V_U64, 0), pc_scaled(1, 2 * MAX_PD_V_U64, 0, PD_EXPO)); // This fails because the confidence interval is too large to be represented in PD_EXPO - test_fails(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(1, MAX_PD_V_U64, 0)); + fails(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(1, MAX_PD_V_U64, 0)); // Unnormalized tests below here // More realistic inputs (get BTC price in ETH) let ten_e7: i64 = 10000000; let uten_e7: u64 = 10000000; - test_succeeds(pc(520010 * ten_e7, 310 * uten_e7, -8), - pc(38591 * ten_e7, 18 * uten_e7, -8), - pc(1347490347, 1431804, -8)); + succeeds(pc(520010 * ten_e7, 310 * uten_e7, -8), + pc(38591 * ten_e7, 18 * uten_e7, -8), + pc(1347490347, 1431804, -8)); // Test with end range of possible inputs to identify overflow // These inputs will lose precision due to the initial normalization. // Get the rounded versions of these inputs in order to compute the expected results. let normed = pc(i64::MAX, u64::MAX, 0).normalize().unwrap(); - test_succeeds(pc(i64::MAX, u64::MAX, 0), pc(i64::MAX, u64::MAX, 0), pc_scaled(1, 4, 0, PD_EXPO)); - test_succeeds(pc(i64::MAX, u64::MAX, 0), - pc(1, 1, 0), - pc_scaled(normed.price, 3 * (normed.price as u64), normed.expo, normed.expo + PD_EXPO)); - test_succeeds(pc(1, 1, 0), - pc(i64::MAX, u64::MAX, 0), - pc((PD_SCALE as i64) / normed.price, 3 * (PD_SCALE / (normed.price as u64)), PD_EXPO - normed.expo)); + succeeds(pc(i64::MAX, u64::MAX, 0), pc(i64::MAX, u64::MAX, 0), pc_scaled(1, 4, 0, PD_EXPO)); + succeeds(pc(i64::MAX, u64::MAX, 0), + pc(1, 1, 0), + pc_scaled(normed.price, 3 * (normed.price as u64), normed.expo, normed.expo + PD_EXPO)); + succeeds(pc(1, 1, 0), + pc(i64::MAX, u64::MAX, 0), + pc((PD_SCALE as i64) / normed.price, 3 * (PD_SCALE / (normed.price as u64)), PD_EXPO - normed.expo)); // FIXME: rounding the confidence to 0 may not be ideal here. Probably should guarantee this rounds up. - test_succeeds(pc(i64::MAX, 1, 0), pc(i64::MAX, 1, 0), pc_scaled(1, 0, 0, PD_EXPO)); - test_succeeds(pc(i64::MAX, 1, 0), - pc(1, 1, 0), - pc_scaled(normed.price, normed.price as u64, normed.expo, normed.expo + PD_EXPO)); - test_succeeds(pc(1, 1, 0), - pc(i64::MAX, 1, 0), - pc((PD_SCALE as i64) / normed.price, PD_SCALE / (normed.price as u64), PD_EXPO - normed.expo)); + succeeds(pc(i64::MAX, 1, 0), pc(i64::MAX, 1, 0), pc_scaled(1, 0, 0, PD_EXPO)); + succeeds(pc(i64::MAX, 1, 0), + pc(1, 1, 0), + pc_scaled(normed.price, normed.price as u64, normed.expo, normed.expo + PD_EXPO)); + succeeds(pc(1, 1, 0), + pc(i64::MAX, 1, 0), + pc((PD_SCALE as i64) / normed.price, PD_SCALE / (normed.price as u64), PD_EXPO - normed.expo)); // Price is zero pre-normalization - test_fails(pc(0, 1, 0), pc(1, 1, 0)); - test_fails(pc(1, 1, 0), pc(0, 1, 0)); + fails(pc(0, 1, 0), pc(1, 1, 0)); + fails(pc(1, 1, 0), pc(0, 1, 0)); // Can't normalize the input when the confidence is >> price. - test_fails(pc(1, 1, 0), pc(1, u64::MAX, 0)); - test_fails(pc(1, u64::MAX, 0), pc(1, 1, 0)); + fails(pc(1, 1, 0), pc(1, u64::MAX, 0)); + fails(pc(1, u64::MAX, 0), pc(1, 1, 0)); // FIXME: move to scaling tests // Result exponent too small From e0f8a43d497b6575bcf87e0ff2a38ded5d0566ac Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Thu, 23 Dec 2021 10:03:10 -0600 Subject: [PATCH 23/33] initial implementation of mul --- src/price_conf.rs | 79 ++++++++++++++++++++++++++++------------------- 1 file changed, 48 insertions(+), 31 deletions(-) diff --git a/src/price_conf.rs b/src/price_conf.rs index 60ae03e..ddcd1f1 100644 --- a/src/price_conf.rs +++ b/src/price_conf.rs @@ -63,6 +63,7 @@ impl PriceConf { // Compute the midprice, base in terms of other. // Uses at most 57 bits let midprice = base_price * PD_SCALE / other_price; + // FIXME: Exponent computations can overflow let midprice_expo = PD_EXPO + base.expo - other.expo; // Compute the confidence interval. @@ -135,6 +136,7 @@ impl PriceConf { // Compute the midprice, base in terms of other. // Uses at most 27*2 bits + // FIXME: Exponent computations can overflow let midprice = base_price * other_price; let midprice_expo = base.expo + other.expo; @@ -150,7 +152,8 @@ impl PriceConf { // Note that this simplifies to // pq * (a/p + b/q) = qa + bp // 27*2 + 1 bits - let conf = base.conf * other.price + other.conf * base.price; + // FIXME: the u64s are hacks :( + let conf = base.conf * (other.price as u64) + other.conf * (base.price as u64); Some(PriceConf { price: midprice, @@ -375,52 +378,67 @@ mod test { succeeds(pc(1, 1, 0), pc(5, 1, 0), pc(5, 6, 0)); // Different exponents in the two inputs - /* - succeeds(pc(100, 10, -8), pc(2, 1, -7), pc_scaled(500_000_000, 300_000_000, -8, PD_EXPO - 1)); - succeeds(pc(100, 10, -4), pc(2, 1, 0), pc_scaled(500_000, 300_000, -8, PD_EXPO + -4)); + succeeds(pc(100, 10, -8), pc(2, 1, -7), pc(200, 120, -15)); + succeeds(pc(100, 10, -4), pc(2, 1, 0), pc(200, 120, -4)); - // Test with end range of possible inputs where the output should not lose precision. - succeeds(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc_scaled(1, 2, 0, PD_EXPO)); - succeeds(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(1, 1, 0), pc_scaled(MAX_PD_V_I64, 2 * MAX_PD_V_U64, 0, PD_EXPO)); - succeeds(pc(1, 1, 0), - pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), - pc((PD_SCALE as i64) / MAX_PD_V_I64, 2 * (PD_SCALE / MAX_PD_V_U64), PD_EXPO)); + // Zero... + // FIXME: fails because normalization doesn't deal with signs at the moment. + // succeeds(pc(0, 10, -4), pc(2, 1, 0), pc(0, 20, -4)); - succeeds(pc(1, MAX_PD_V_U64, 0), pc(1, MAX_PD_V_U64, 0), pc_scaled(1, 2 * MAX_PD_V_U64, 0, PD_EXPO)); - // This fails because the confidence interval is too large to be represented in PD_EXPO - fails(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(1, MAX_PD_V_U64, 0)); + // Test with end range of possible inputs where the output should not lose precision. + succeeds( + pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), + pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), + pc(MAX_PD_V_I64 * MAX_PD_V_I64, 2 * MAX_PD_V_U64 * MAX_PD_V_U64, 0) + ); + succeeds(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(1, 1, 0), pc(MAX_PD_V_I64, 2 * MAX_PD_V_U64, 0)); + succeeds( + pc(1, MAX_PD_V_U64, 0), + pc(3, 1, 0), + pc(3, 1 + 3 * MAX_PD_V_U64, 0) + ); + + succeeds(pc(1, MAX_PD_V_U64, 0), pc(1, MAX_PD_V_U64, 0), pc(1, 2 * MAX_PD_V_U64, 0)); + succeeds( + pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), + pc(1, MAX_PD_V_U64, 0), + pc(MAX_PD_V_I64, MAX_PD_V_U64 + MAX_PD_V_U64 * MAX_PD_V_U64, 0) + ); // Unnormalized tests below here - - // More realistic inputs (get BTC price in ETH) let ten_e7: i64 = 10000000; let uten_e7: u64 = 10000000; - succeeds(pc(520010 * ten_e7, 310 * uten_e7, -8), - pc(38591 * ten_e7, 18 * uten_e7, -8), - pc(1347490347, 1431804, -8)); + succeeds( + pc(3 * (PD_SCALE as i64), 3 * PD_SCALE, PD_EXPO), + pc(2 * (PD_SCALE as i64), 4 * PD_SCALE, PD_EXPO), + pc(6 * ten_e7 * ten_e7, 18 * uten_e7 * uten_e7, -14) + ); // Test with end range of possible inputs to identify overflow // These inputs will lose precision due to the initial normalization. // Get the rounded versions of these inputs in order to compute the expected results. let normed = pc(i64::MAX, u64::MAX, 0).normalize().unwrap(); - succeeds(pc(i64::MAX, u64::MAX, 0), pc(i64::MAX, u64::MAX, 0), pc_scaled(1, 4, 0, PD_EXPO)); + succeeds( + pc(i64::MAX, u64::MAX, 0), + pc(i64::MAX, u64::MAX, 0), + pc(normed.price * normed.price, 4 * ((normed.price * normed.price) as u64), normed.expo * 2) + ); succeeds(pc(i64::MAX, u64::MAX, 0), pc(1, 1, 0), - pc_scaled(normed.price, 3 * (normed.price as u64), normed.expo, normed.expo + PD_EXPO)); - succeeds(pc(1, 1, 0), - pc(i64::MAX, u64::MAX, 0), - pc((PD_SCALE as i64) / normed.price, 3 * (PD_SCALE / (normed.price as u64)), PD_EXPO - normed.expo)); - - // FIXME: rounding the confidence to 0 may not be ideal here. Probably should guarantee this rounds up. - succeeds(pc(i64::MAX, 1, 0), pc(i64::MAX, 1, 0), pc_scaled(1, 0, 0, PD_EXPO)); + pc(normed.price, 3 * (normed.price as u64), normed.expo)); + + // FIXME: rounding the confidence to 0 is not be ideal here. Probably should guarantee this rounds up. + succeeds( + pc(i64::MAX, 1, 0), + pc(i64::MAX, 1, 0), + pc(normed.price * normed.price, 0, normed.expo * 2) + ); succeeds(pc(i64::MAX, 1, 0), pc(1, 1, 0), - pc_scaled(normed.price, normed.price as u64, normed.expo, normed.expo + PD_EXPO)); - succeeds(pc(1, 1, 0), - pc(i64::MAX, 1, 0), - pc((PD_SCALE as i64) / normed.price, PD_SCALE / (normed.price as u64), PD_EXPO - normed.expo)); + pc(normed.price, normed.price as u64, normed.expo)); + /* // Price is zero pre-normalization fails(pc(0, 1, 0), pc(1, 1, 0)); fails(pc(1, 1, 0), pc(0, 1, 0)); @@ -431,7 +449,6 @@ mod test { // FIXME: move to scaling tests // Result exponent too small - /* test_succeeds(pc(1, 1, 0), pc(1, 1, 0), PD_EXPO, (1 * (PD_SCALE as i64), 2 * PD_SCALE)); test_fails(pc(1, 1, 0), pc(1, 1, 0), PD_EXPO - 1); */ From be58b99bb96ff1c2036dde11b44fa2c782892509 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Thu, 23 Dec 2021 10:29:54 -0600 Subject: [PATCH 24/33] exponent --- src/price_conf.rs | 194 +++++++++++++++++++++++++--------------------- 1 file changed, 104 insertions(+), 90 deletions(-) diff --git a/src/price_conf.rs b/src/price_conf.rs index ddcd1f1..4c8dd62 100644 --- a/src/price_conf.rs +++ b/src/price_conf.rs @@ -49,58 +49,57 @@ impl PriceConf { // PriceConf is not guaranteed to store its price/confidence in normalized form. // Normalize them here to bound the range of price/conf, which is required to perform // arithmetic operations. - match (self.normalize(), other.normalize()) { - (Some(base), Some(other)) => { - // Note that normalization implies that the prices can be cast to u64. - // We need prices as u64 in order to divide, as solana doesn't implement signed division. - // It's also extremely unclear what this method should do if one of the prices is negative, - // so assuming positive prices throughout seems fine. - - // These use at most 27 bits each - let base_price = base.price as u64; - let other_price = other.price as u64; - - // Compute the midprice, base in terms of other. - // Uses at most 57 bits - let midprice = base_price * PD_SCALE / other_price; - // FIXME: Exponent computations can overflow - let midprice_expo = PD_EXPO + base.expo - other.expo; - - // Compute the confidence interval. - // This code uses the 1-norm instead of the 2-norm for computational reasons. - // The correct formula is midprice * sqrt(c_1^2 + c_2^2), where c_1 and c_2 are the - // confidence intervals in price-percentage terms of the base and other. This quantity - // is difficult to compute due to the sqrt, and overflow/underflow considerations. - // Instead, this code uses midprice * (c_1 + c_2). - // This quantity is at most a factor of sqrt(2) greater than the correct result, which - // shouldn't matter considering that confidence intervals are typically ~0.1% of the price. - - // The exponent is PD_EXPO for both of these. Each of these uses 57 bits. - let base_confidence_pct: u64 = (base.conf * PD_SCALE) / base_price; - let other_confidence_pct: u64 = (other.conf * PD_SCALE) / other_price; - - // at most 58 bits - let confidence_pct = base_confidence_pct + other_confidence_pct; - // at most 57 + 58 - 29 = 86 bits, with the same exponent as the midprice. - // FIXME: round this up. There's a div_ceil method but it's unstable (?) - let conf = ((confidence_pct as u128) * (midprice as u128)) / (PD_SCALE as u128); - - // Note that this check only fails if an argument's confidence interval was >> its price, - // in which case None is a reasonable result, as we have essentially 0 information about the price. - if conf < (u64::MAX as u128) { - let m_i64 = midprice as i64; - // This should be guaranteed to succeed because midprice uses <= 57 bits - assert!(m_i64 >= 0); - Some(PriceConf { - price: m_i64, - conf: conf as u64, - expo: midprice_expo, - }) - } else { - None - } - } - (_, _) => None + let base = self.normalize()?; + let other = other.normalize()?; + + // FIXME: negative numbers + // Note that normalization implies that the prices can be cast to u64. + // We need prices as u64 in order to divide, as solana doesn't implement signed division. + // It's also extremely unclear what this method should do if one of the prices is negative, + // so assuming positive prices throughout seems fine. + + // These use at most 27 bits each + let base_price = base.price as u64; + let other_price = other.price as u64; + + // Compute the midprice, base in terms of other. + // Uses at most 57 bits + let midprice = base_price * PD_SCALE / other_price; + + let midprice_expo = base.expo.checked_sub(other.expo)?.checked_add(PD_EXPO)?; + + // Compute the confidence interval. + // This code uses the 1-norm instead of the 2-norm for computational reasons. + // The correct formula is midprice * sqrt(c_1^2 + c_2^2), where c_1 and c_2 are the + // confidence intervals in price-percentage terms of the base and other. This quantity + // is difficult to compute due to the sqrt, and overflow/underflow considerations. + // Instead, this code uses midprice * (c_1 + c_2). + // This quantity is at most a factor of sqrt(2) greater than the correct result, which + // shouldn't matter considering that confidence intervals are typically ~0.1% of the price. + + // The exponent is PD_EXPO for both of these. Each of these uses 57 bits. + let base_confidence_pct: u64 = (base.conf * PD_SCALE) / base_price; + let other_confidence_pct: u64 = (other.conf * PD_SCALE) / other_price; + + // at most 58 bits + let confidence_pct = base_confidence_pct + other_confidence_pct; + // at most 57 + 58 - 29 = 86 bits, with the same exponent as the midprice. + // FIXME: round this up. There's a div_ceil method but it's unstable (?) + let conf = ((confidence_pct as u128) * (midprice as u128)) / (PD_SCALE as u128); + + // Note that this check only fails if an argument's confidence interval was >> its price, + // in which case None is a reasonable result, as we have essentially 0 information about the price. + if conf < (u64::MAX as u128) { + let m_i64 = midprice as i64; + // This should be guaranteed to succeed because midprice uses <= 57 bits + assert!(m_i64 >= 0); + Some(PriceConf { + price: m_i64, + conf: conf as u64, + expo: midprice_expo, + }) + } else { + None } } @@ -125,44 +124,41 @@ impl PriceConf { // PriceConf is not guaranteed to store its price/confidence in normalized form. // Normalize them here to bound the range of price/conf, which is required to perform // arithmetic operations. - match (self.normalize(), other.normalize()) { - (Some(base), Some(other)) => { - // TODO: think about negative numbers - // FIXME: bitcounts - - // These use at most 27 bits each - let base_price = base.price; - let other_price = other.price; - - // Compute the midprice, base in terms of other. - // Uses at most 27*2 bits - // FIXME: Exponent computations can overflow - let midprice = base_price * other_price; - let midprice_expo = base.expo + other.expo; - - // Compute the confidence interval. - // This code uses the 1-norm instead of the 2-norm for computational reasons. - // The correct formula is midprice * sqrt(c_1^2 + c_2^2), where c_1 and c_2 are the - // confidence intervals in price-percentage terms of the base and other. This quantity - // is difficult to compute due to the sqrt, and overflow/underflow considerations. - // Instead, this code uses midprice * (c_1 + c_2). - // This quantity is at most a factor of sqrt(2) greater than the correct result, which - // shouldn't matter considering that confidence intervals are typically ~0.1% of the price. - // - // Note that this simplifies to - // pq * (a/p + b/q) = qa + bp - // 27*2 + 1 bits - // FIXME: the u64s are hacks :( - let conf = base.conf * (other.price as u64) + other.conf * (base.price as u64); - - Some(PriceConf { - price: midprice, - conf, - expo: midprice_expo, - }) - } - (_, _) => None - } + let base = self.normalize()?; + let other = other.normalize()?; + + // TODO: think about negative numbers + // FIXME: bitcounts + + // These use at most 27 bits each + let base_price = base.price; + let other_price = other.price; + + // Compute the midprice, base in terms of other. + // Uses at most 27*2 bits + let midprice = base_price * other_price; + let midprice_expo = base.expo.checked_add(other.expo)?; + + // Compute the confidence interval. + // This code uses the 1-norm instead of the 2-norm for computational reasons. + // The correct formula is midprice * sqrt(c_1^2 + c_2^2), where c_1 and c_2 are the + // confidence intervals in price-percentage terms of the base and other. This quantity + // is difficult to compute due to the sqrt, and overflow/underflow considerations. + // Instead, this code uses midprice * (c_1 + c_2). + // This quantity is at most a factor of sqrt(2) greater than the correct result, which + // shouldn't matter considering that confidence intervals are typically ~0.1% of the price. + // + // Note that this simplifies to + // pq * (a/p + b/q) = qa + bp + // 27*2 + 1 bits + // FIXME: the u64s are hacks :( + let conf = base.conf * (other.price as u64) + other.conf * (base.price as u64); + + Some(PriceConf { + price: midprice, + conf, + expo: midprice_expo, + }) } /** @@ -345,6 +341,14 @@ mod test { fails(pc(1, 1, 0), pc(1, u64::MAX, 0)); fails(pc(1, u64::MAX, 0), pc(1, 1, 0)); + // Exponent under/overflow. + succeeds(pc(1, 1, i32::MAX), pc(1, 1, 0), pc(PD_SCALE as i64, 2 * PD_SCALE, i32::MAX + PD_EXPO)); + fails(pc(1, 1, i32::MAX), pc(1, 1, -1)); + + succeeds(pc(1, 1, i32::MIN - PD_EXPO), pc(1, 1, 0), pc(PD_SCALE as i64, 2 * PD_SCALE, i32::MIN)); + succeeds(pc(1, 1, i32::MIN), pc(1, 1, PD_EXPO), pc(PD_SCALE as i64, 2 * PD_SCALE, i32::MIN)); + fails(pc(1, 1, i32::MIN - PD_EXPO), pc(1, 1, 1)); + // FIXME: move to scaling tests // Result exponent too small /* @@ -438,6 +442,16 @@ mod test { pc(1, 1, 0), pc(normed.price, normed.price as u64, normed.expo)); + // Exponent under/overflow. + succeeds(pc(1, 1, i32::MAX), pc(1, 1, 0), pc(1, 2, i32::MAX)); + succeeds(pc(1, 1, i32::MAX), pc(1, 1, -1), pc(1, 2, i32::MAX - 1)); + fails(pc(1, 1, i32::MAX), pc(1, 1, 1)); + + succeeds(pc(1, 1, i32::MIN), pc(1, 1, 0), pc(1, 2, i32::MIN)); + succeeds(pc(1, 1, i32::MIN), pc(1, 1, 1), pc(1, 2, i32::MIN + 1)); + fails(pc(1, 1, i32::MIN), pc(1, 1, -1)); + + /* // Price is zero pre-normalization fails(pc(0, 1, 0), pc(1, 1, 0)); From d41524ba4d7ddccaa914c50a16ec9d35265efe26 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Thu, 23 Dec 2021 11:23:09 -0600 Subject: [PATCH 25/33] tests for normalize --- Cargo.toml | 1 + src/price_conf.rs | 83 +++++++++++++++++++++++++++++++---------------- 2 files changed, 56 insertions(+), 28 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 17c6e6e..7733d2d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ no-entrypoint = [] solana-program = "1.8.1" borsh = "0.9" borsh-derive = "0.9.0" +num-integer = "0.1.44" [dev-dependencies] solana-program-test = "1.8.1" diff --git a/src/price_conf.rs b/src/price_conf.rs index 4c8dd62..0e69528 100644 --- a/src/price_conf.rs +++ b/src/price_conf.rs @@ -6,6 +6,8 @@ use { const PD_EXPO: i32 = -9; const PD_SCALE: u64 = 1_000_000_000; const MAX_PD_V_U64: u64 = (1 << 28) - 1; +const MAX_PD_V_I64: i64 = MAX_PD_V_U64 as i64; +const MIN_PD_V_I64: i64 = -MAX_PD_V_I64; /** * A price with a degree of uncertainty, represented as a price +- a confidence interval. @@ -117,7 +119,7 @@ impl PriceConf { // multiply by a constant pub fn cmul(&self, c: i64, e: i32) -> Option { - self.mul(&PriceConf { price: c, conf: 0, expo: e}) + self.mul(&PriceConf { price: c, conf: 0, expo: e }) } pub fn mul(&self, other: &PriceConf) -> Option { @@ -163,36 +165,24 @@ impl PriceConf { /** * Get a copy of this struct where the price and confidence - * have been normalized to be less than `MAX_PD_V_U64`. - * Returns `None` if `price == 0` before or after normalization. - * FIXME: tests + * have been normalized to be between MIN_PD_V_I64 and MAX_PD_V_I64. */ pub fn normalize(&self) -> Option { - if self.price > 0 { - // FIXME: support negative numbers. - let mut p: u64 = self.price as u64; - let mut c: u64 = self.conf; - let mut e: i32 = self.expo; - - while p > MAX_PD_V_U64 || c > MAX_PD_V_U64 { - p = p / 10; - c = c / 10; - e += 1; - } - - // Can get p == 0 if confidence is >> price. - if p > 0 { - Some(PriceConf { - price: p as i64, - conf: c, - expo: e, - }) - } else { - None - } - } else { - None + let mut p = self.price; + let mut c = self.conf; + let mut e = self.expo; + + while p > MAX_PD_V_I64 || p < MIN_PD_V_I64 || c > MAX_PD_V_U64 { + p = p / 10; + c = c / 10; + e = e.checked_add(1)?; } + + Some(PriceConf { + price: p, + conf: c, + expo: e, + }) } /** @@ -263,6 +253,43 @@ mod test { }.scale_to_exponent(expo).unwrap() } + #[test] + fn test_normalize() { + fn succeeds( + price1: PriceConf, + expected: PriceConf, + ) { + assert_eq!(price1.normalize().unwrap(), expected); + } + + fn fails( + price1: PriceConf, + ) { + assert_eq!(price1.normalize(), None); + } + + succeeds( + pc((PD_SCALE as i64) * 2, PD_SCALE * 3, 0), + pc(2 * (PD_SCALE as i64) / 100, 3 * PD_SCALE / 100, 2) + ); + + // the max values are a factor of 10^11 larger than MAX_PD_V + let expo = -(PD_EXPO - 2); + let scale_i64 = (PD_SCALE as i64) * 100; + let scale_u64 = scale_i64 as u64; + succeeds(pc(i64::MAX, 1, 0), pc(i64::MAX / scale_i64, 0, expo)); + succeeds(pc(i64::MIN, 1, 0), pc(i64::MIN / scale_i64, 0, expo)); + succeeds(pc(1, u64::MAX, 0), pc(0, u64::MAX / scale_u64, expo)); + + // exponent overflows + succeeds(pc(i64::MAX, 1, i32::MAX - expo), pc(i64::MAX / scale_i64, 0, i32::MAX)); + fails(pc(i64::MAX, 1, i32::MAX - expo + 1)); + succeeds(pc(i64::MAX, 1, i32::MIN), pc(i64::MAX / scale_i64, 0, i32::MIN + expo)); + + succeeds(pc(1, u64::MAX, i32::MAX - expo), pc(0, u64::MAX / scale_u64, i32::MAX)); + fails(pc(1, u64::MAX, i32::MAX - expo + 1)); + } + #[test] fn test_div() { fn succeeds( From 6a6f1853d89ddd4b618f69258ea48e89f46df467 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Thu, 23 Dec 2021 11:23:40 -0600 Subject: [PATCH 26/33] tests for normalize --- src/price_conf.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/price_conf.rs b/src/price_conf.rs index 0e69528..a407cde 100644 --- a/src/price_conf.rs +++ b/src/price_conf.rs @@ -269,10 +269,15 @@ mod test { } succeeds( - pc((PD_SCALE as i64) * 2, PD_SCALE * 3, 0), + pc(2 * (PD_SCALE as i64), 3 * PD_SCALE, 0), pc(2 * (PD_SCALE as i64) / 100, 3 * PD_SCALE / 100, 2) ); - + + succeeds( + pc(-2 * (PD_SCALE as i64), 3 * PD_SCALE, 0), + pc(-2 * (PD_SCALE as i64) / 100, 3 * PD_SCALE / 100, 2) + ); + // the max values are a factor of 10^11 larger than MAX_PD_V let expo = -(PD_EXPO - 2); let scale_i64 = (PD_SCALE as i64) * 100; From 830a7da19104008b2a50197f82672b3aa34b0f82 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Thu, 23 Dec 2021 11:56:45 -0600 Subject: [PATCH 27/33] negative numbers in div --- src/price_conf.rs | 65 ++++++++++++++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 23 deletions(-) diff --git a/src/price_conf.rs b/src/price_conf.rs index a407cde..30ec61f 100644 --- a/src/price_conf.rs +++ b/src/price_conf.rs @@ -54,20 +54,17 @@ impl PriceConf { let base = self.normalize()?; let other = other.normalize()?; - // FIXME: negative numbers - // Note that normalization implies that the prices can be cast to u64. - // We need prices as u64 in order to divide, as solana doesn't implement signed division. - // It's also extremely unclear what this method should do if one of the prices is negative, - // so assuming positive prices throughout seems fine. + if other.price == 0 { + return None; + } // These use at most 27 bits each - let base_price = base.price as u64; - let other_price = other.price as u64; + let (base_price, base_sign) = PriceConf::to_unsigned(base.price); + let (other_price, other_sign) = PriceConf::to_unsigned(other.price); // Compute the midprice, base in terms of other. // Uses at most 57 bits let midprice = base_price * PD_SCALE / other_price; - let midprice_expo = base.expo.checked_sub(other.expo)?.checked_add(PD_EXPO)?; // Compute the confidence interval. @@ -80,23 +77,22 @@ impl PriceConf { // shouldn't matter considering that confidence intervals are typically ~0.1% of the price. // The exponent is PD_EXPO for both of these. Each of these uses 57 bits. - let base_confidence_pct: u64 = (base.conf * PD_SCALE) / base_price; + // let base_confidence_pct: u64 = (base.conf * PD_SCALE) / base_price; let other_confidence_pct: u64 = (other.conf * PD_SCALE) / other_price; // at most 58 bits - let confidence_pct = base_confidence_pct + other_confidence_pct; + // let confidence_pct = base_confidence_pct + other_confidence_pct; // at most 57 + 58 - 29 = 86 bits, with the same exponent as the midprice. // FIXME: round this up. There's a div_ceil method but it's unstable (?) - let conf = ((confidence_pct as u128) * (midprice as u128)) / (PD_SCALE as u128); + // let conf = ((confidence_pct as u128) * (midprice as u128)) / (PD_SCALE as u128); + + let conf = (((base.conf * PD_SCALE) / other_price) as u128) + ((other_confidence_pct as u128) * (midprice as u128)) / (PD_SCALE as u128); // Note that this check only fails if an argument's confidence interval was >> its price, // in which case None is a reasonable result, as we have essentially 0 information about the price. if conf < (u64::MAX as u128) { - let m_i64 = midprice as i64; - // This should be guaranteed to succeed because midprice uses <= 57 bits - assert!(m_i64 >= 0); Some(PriceConf { - price: m_i64, + price: (midprice as i64) * base_sign * other_sign, conf: conf as u64, expo: midprice_expo, }) @@ -229,13 +225,20 @@ impl PriceConf { } } } + + fn to_unsigned(x: i64) -> (u64, i64) { + assert!(x <= MAX_PD_V_I64 && x >= MIN_PD_V_I64); + if (x < 0) { + (-x as u64, -1) + } else { + (x as u64, 1) + } + } } #[cfg(test)] mod test { - use crate::price_conf::{MAX_PD_V_U64, PD_EXPO, PD_SCALE, PriceConf}; - - const MAX_PD_V_I64: i64 = (1 << 28) - 1; + use crate::price_conf::{MAX_PD_V_U64, MAX_PD_V_I64, MIN_PD_V_I64, PD_EXPO, PD_SCALE, PriceConf}; fn pc(price: i64, conf: u64, expo: i32) -> PriceConf { PriceConf { @@ -319,6 +322,11 @@ mod test { succeeds(pc(1, 1, 1), pc(1, 1, 0), pc_scaled(10, 20, 0, PD_EXPO + 1)); succeeds(pc(1, 1, 0), pc(5, 1, 0), pc_scaled(20, 24, -2, PD_EXPO)); + // Negative numbers + succeeds(pc(-1, 1, 0), pc(1, 1, 0), pc_scaled(-1, 2, 0, PD_EXPO)); + succeeds(pc(1, 1, 0), pc(-1, 1, 0), pc_scaled(-1, 2, 0, PD_EXPO)); + succeeds(pc(-1, 1, 0), pc(-1, 1, 0), pc_scaled(1, 2, 0, PD_EXPO)); + // Different exponents in the two inputs succeeds(pc(100, 10, -8), pc(2, 1, -7), pc_scaled(500_000_000, 300_000_000, -8, PD_EXPO - 1)); succeeds(pc(100, 10, -4), pc(2, 1, 0), pc_scaled(500_000, 300_000, -8, PD_EXPO + -4)); @@ -330,6 +338,12 @@ mod test { pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc((PD_SCALE as i64) / MAX_PD_V_I64, 2 * (PD_SCALE / MAX_PD_V_U64), PD_EXPO)); + succeeds(pc(MIN_PD_V_I64, MAX_PD_V_U64, 0), pc(MIN_PD_V_I64, MAX_PD_V_U64, 0), pc_scaled(1, 2, 0, PD_EXPO)); + succeeds(pc(MIN_PD_V_I64, MAX_PD_V_U64, 0), pc(1, 1, 0), pc_scaled(MIN_PD_V_I64, 2 * MAX_PD_V_U64, 0, PD_EXPO)); + succeeds(pc(1, 1, 0), + pc(MIN_PD_V_I64, MAX_PD_V_U64, 0), + pc((PD_SCALE as i64) / MIN_PD_V_I64, 2 * (PD_SCALE / MAX_PD_V_U64), PD_EXPO)); + succeeds(pc(1, MAX_PD_V_U64, 0), pc(1, MAX_PD_V_U64, 0), pc_scaled(1, 2 * MAX_PD_V_U64, 0, PD_EXPO)); // This fails because the confidence interval is too large to be represented in PD_EXPO fails(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(1, MAX_PD_V_U64, 0)); @@ -356,7 +370,6 @@ mod test { pc(i64::MAX, u64::MAX, 0), pc((PD_SCALE as i64) / normed.price, 3 * (PD_SCALE / (normed.price as u64)), PD_EXPO - normed.expo)); - // FIXME: rounding the confidence to 0 may not be ideal here. Probably should guarantee this rounds up. succeeds(pc(i64::MAX, 1, 0), pc(i64::MAX, 1, 0), pc_scaled(1, 0, 0, PD_EXPO)); succeeds(pc(i64::MAX, 1, 0), pc(1, 1, 0), @@ -365,13 +378,20 @@ mod test { pc(i64::MAX, 1, 0), pc((PD_SCALE as i64) / normed.price, PD_SCALE / (normed.price as u64), PD_EXPO - normed.expo)); + // TODO: negative number tests around i64::MIN + // Price is zero pre-normalization - fails(pc(0, 1, 0), pc(1, 1, 0)); + succeeds(pc(0, 1, 0), pc(1, 1, 0), pc_scaled(0, 1, 0, PD_EXPO)); + succeeds(pc(0, 1, 0), pc(100, 1, 0), pc_scaled(0, 1, -2, PD_EXPO)); fails(pc(1, 1, 0), pc(0, 1, 0)); - // Can't normalize the input when the confidence is >> price. + // Normalizing the input when the confidence is >> price produces a price of 0. fails(pc(1, 1, 0), pc(1, u64::MAX, 0)); - fails(pc(1, u64::MAX, 0), pc(1, 1, 0)); + succeeds( + pc(1, u64::MAX, 0), + pc(1, 1, 0), + pc_scaled(0, normed.conf, normed.expo, normed.expo + PD_EXPO) + ); // Exponent under/overflow. succeeds(pc(1, 1, i32::MAX), pc(1, 1, 0), pc(PD_SCALE as i64, 2 * PD_SCALE, i32::MAX + PD_EXPO)); @@ -464,7 +484,6 @@ mod test { pc(1, 1, 0), pc(normed.price, 3 * (normed.price as u64), normed.expo)); - // FIXME: rounding the confidence to 0 is not be ideal here. Probably should guarantee this rounds up. succeeds( pc(i64::MAX, 1, 0), pc(i64::MAX, 1, 0), From 87ac025016c5b3b7e4ab75cfba268a5c6c7b826e Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Thu, 23 Dec 2021 12:00:06 -0600 Subject: [PATCH 28/33] handle negative numbers --- src/price_conf.rs | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/price_conf.rs b/src/price_conf.rs index 30ec61f..d0e6efe 100644 --- a/src/price_conf.rs +++ b/src/price_conf.rs @@ -125,12 +125,9 @@ impl PriceConf { let base = self.normalize()?; let other = other.normalize()?; - // TODO: think about negative numbers - // FIXME: bitcounts - // These use at most 27 bits each - let base_price = base.price; - let other_price = other.price; + let (base_price, base_sign) = PriceConf::to_unsigned(base.price); + let (other_price, other_sign) = PriceConf::to_unsigned(other.price); // Compute the midprice, base in terms of other. // Uses at most 27*2 bits @@ -149,11 +146,10 @@ impl PriceConf { // Note that this simplifies to // pq * (a/p + b/q) = qa + bp // 27*2 + 1 bits - // FIXME: the u64s are hacks :( - let conf = base.conf * (other.price as u64) + other.conf * (base.price as u64); + let conf = base.conf * other_price + other.conf * base_price; Some(PriceConf { - price: midprice, + price: (midprice as i64) * base_sign * other_sign, conf, expo: midprice_expo, }) @@ -427,6 +423,8 @@ mod test { assert_eq!(result, None); } + // TODO: test negative numbers + succeeds(pc(1, 1, 0), pc(1, 1, 0), pc(1, 2, 0)); succeeds(pc(1, 1, -8), pc(1, 1, -8), pc(1, 2, -16)); succeeds(pc(10, 1, 0), pc(1, 1, 0), pc(10, 11, 0)); @@ -437,9 +435,9 @@ mod test { succeeds(pc(100, 10, -8), pc(2, 1, -7), pc(200, 120, -15)); succeeds(pc(100, 10, -4), pc(2, 1, 0), pc(200, 120, -4)); - // Zero... - // FIXME: fails because normalization doesn't deal with signs at the moment. - // succeeds(pc(0, 10, -4), pc(2, 1, 0), pc(0, 20, -4)); + // Zero + succeeds(pc(0, 10, -4), pc(2, 1, 0), pc(0, 20, -4)); + succeeds(pc(2, 1, 0), pc(0, 10, -4), pc(0, 20, -4)); // Test with end range of possible inputs where the output should not lose precision. succeeds( From 670392bda1d985a41ea8bed5ee87169666713701 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Thu, 23 Dec 2021 12:04:56 -0600 Subject: [PATCH 29/33] comments --- src/price_conf.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/price_conf.rs b/src/price_conf.rs index d0e6efe..ac09cce 100644 --- a/src/price_conf.rs +++ b/src/price_conf.rs @@ -180,6 +180,8 @@ impl PriceConf { /** * Scale num so that its exponent is target_expo. * FIXME: tests + * TODO: exponent overflow + * TODO: should confidences always be ceiled when divided (??) */ pub fn scale_to_exponent( &self, From a640066f2200a8cea75b7b667fa8513e3318f5aa Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Mon, 27 Dec 2021 10:14:12 -0800 Subject: [PATCH 30/33] stuff --- src/entrypoint.rs | 2 +- src/lib.rs | 34 ++++++++++++++----- src/price_conf.rs | 83 +++++++++++++++++++++++++++++++++++------------ 3 files changed, 89 insertions(+), 30 deletions(-) diff --git a/src/entrypoint.rs b/src/entrypoint.rs index c0c253a..82641a4 100644 --- a/src/entrypoint.rs +++ b/src/entrypoint.rs @@ -13,4 +13,4 @@ fn process_instruction( instruction_data: &[u8], ) -> ProgramResult { crate::processor::process_instruction(program_id, accounts, instruction_data) -} \ No newline at end of file +} diff --git a/src/lib.rs b/src/lib.rs index ac21b24..b19302d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -162,6 +162,19 @@ impl Price { } } + /** + * Get the time-weighted average price (TWAP) and a confidence interval on the result. + * Returns None if the twap is currently unavailable. + * + * At the moment, the confidence interval returned by this method is computed in + * a somewhat questionable way, so we do not recommend using it for high-value applications. + */ + pub fn get_twap(&self) -> Option { + // This method currently cannot return None, but may do so in the future. + // Note that the twac is a positive number in i64, so safe to cast to u64. + Some(PriceConf { price: self.twap.val, conf: self.twac.val as u64, expo: self.expo }) + } + /** * Get the current price of this account in a different quote currency. If this account * represents the price of the product X/Z, and `quote` represents the price of the product Y/Z, @@ -181,16 +194,21 @@ impl Price { } /** - * Get the time-weighted average price (TWAP) and a confidence interval on the result. - * Returns None if the twap is currently unavailable. + * Get the price of a basket of currencies. Each entry in `amounts` is of the form + * `(price, qty, qty_expo)`, and the result is the sum of `price * qty * 10^qty_expo`. + * The result is returned with exponent `result_expo`. * - * At the moment, the confidence interval returned by this method is computed in - * a somewhat questionable way, so we do not recommend using it for high-value applications. + * An example use case for this function is to get the value of an LP token. */ - pub fn get_twap(&self) -> Option { - // This method currently cannot return None, but may do so in the future. - // Note that the twac is a positive number in i64, so safe to cast to u64. - Some(PriceConf { price: self.twap.val, conf: self.twac.val as u64, expo: self.expo }) + pub fn price_basket(amounts: &[(Price, i64, i32)], result_expo: i32) -> Option { + assert!(amounts.len() > 0); + let mut res = PriceConf { price: 0, conf: 0, expo: result_expo }; + for i in 0..amounts.len() { + res = res.add( + &amounts[i].0.get_current_price()?.cmul(amounts[i].1, amounts[i].2)?.scale_to_exponent(result_expo)? + )? + } + Some(res) } } diff --git a/src/price_conf.rs b/src/price_conf.rs index ac09cce..9488147 100644 --- a/src/price_conf.rs +++ b/src/price_conf.rs @@ -83,9 +83,6 @@ impl PriceConf { // at most 58 bits // let confidence_pct = base_confidence_pct + other_confidence_pct; // at most 57 + 58 - 29 = 86 bits, with the same exponent as the midprice. - // FIXME: round this up. There's a div_ceil method but it's unstable (?) - // let conf = ((confidence_pct as u128) * (midprice as u128)) / (PD_SCALE as u128); - let conf = (((base.conf * PD_SCALE) / other_price) as u128) + ((other_confidence_pct as u128) * (midprice as u128)) / (PD_SCALE as u128); // Note that this check only fails if an argument's confidence interval was >> its price, @@ -101,23 +98,30 @@ impl PriceConf { } } - // FIXME Implement these functions - // The idea is that you should be able to get the price of a mixture of tokens (e.g., for LP tokens) - // using something like: - // price1.scale_to_exponent(result_expo).cmul(qty1, 0).add( - // price2.scale_to_exponent(result_expo).cmul(qty2, 0) - // ) - // - // Add two PriceConfs assuming the expos are == - pub fn add(&self, other: PriceConf) -> Option { - panic!() + /** + * Add `other` to this, propagating uncertainty in both prices. Requires both + * `PriceConf`s to have the same exponent -- use `scale_to_exponent` on the arguments + * if necessary. + */ + pub fn add(&self, other: &PriceConf) -> Option { + assert_eq!(self.expo, other.expo); + + let price = self.price.checked_add(other.price)?; + // The conf should technically be sqrt(a^2 + b^2), but that's harder to compute. + let conf = self.conf.checked_add(other.conf)?; + Some(PriceConf { + price, + conf, + expo: self.expo, + }) } - // multiply by a constant + /** Multiply this `PriceConf` by a constant `c * 10^e`. */ pub fn cmul(&self, c: i64, e: i32) -> Option { self.mul(&PriceConf { price: c, conf: 0, expo: e }) } + /** Multiply this `PriceConf` by `other`, propagating any uncertainty. */ pub fn mul(&self, other: &PriceConf) -> Option { // PriceConf is not guaranteed to store its price/confidence in normalized form. // Normalize them here to bound the range of price/conf, which is required to perform @@ -157,7 +161,7 @@ impl PriceConf { /** * Get a copy of this struct where the price and confidence - * have been normalized to be between MIN_PD_V_I64 and MAX_PD_V_I64. + * have been normalized to be between `MIN_PD_V_I64` and `MAX_PD_V_I64`. */ pub fn normalize(&self) -> Option { let mut p = self.price; @@ -178,16 +182,18 @@ impl PriceConf { } /** - * Scale num so that its exponent is target_expo. - * FIXME: tests - * TODO: exponent overflow - * TODO: should confidences always be ceiled when divided (??) + * Scale this price/confidence so that its exponent is `target_expo`. Return `None` if + * this number is outside the range of numbers representable in `target_expo`, which will + * happen if `target_expo` is too small. + * + * Warning: if `target_expo` is significantly larger than the current exponent, this function + * will return 0 +- 0. */ pub fn scale_to_exponent( &self, target_expo: i32, ) -> Option { - let mut delta = target_expo - self.expo; + let mut delta = target_expo.checked_sub(self.expo)?; if delta >= 0 { let mut p = self.price; let mut c = self.conf; @@ -196,7 +202,7 @@ impl PriceConf { c /= 10; delta -= 1; } - // FIXME: check for 0s here and handle this case more gracefully. (0, 0) is a bad answer that will cause bugs + Some(PriceConf { price: p, conf: c, @@ -296,6 +302,41 @@ mod test { fails(pc(1, u64::MAX, i32::MAX - expo + 1)); } + #[test] + fn test_scale_to_exponent() { + fn succeeds( + price1: PriceConf, + target: i32, + expected: PriceConf, + ) { + assert_eq!(price1.scale_to_exponent(target).unwrap(), expected); + } + + fn fails( + price1: PriceConf, + target: i32, + ) { + assert_eq!(price1.scale_to_exponent(target), None); + } + + succeeds(pc(1234, 1234, 0), 0, pc(1234, 1234, 0)); + succeeds(pc(1234, 1234, 0), 1, pc(123, 123, 1)); + succeeds(pc(1234, 1234, 0), 2, pc(12, 12, 2)); + succeeds(pc(-1234, 1234, 0), 2, pc(-12, 12, 2)); + // TODO: confidence rounding question + succeeds(pc(1234, 1234, 0), 4, pc(0, 0, 4)); + succeeds(pc(1234, 1234, 0), -1, pc(12340, 12340, -1)); + succeeds(pc(1234, 1234, 0), -2, pc(123400, 123400, -2)); + succeeds(pc(1234, 1234, 0), -8, pc(123400000000, 123400000000, -8)); + // insufficient precision to represent the result in this exponent + fails(pc(1234, 1234, 0), -20); + fails(pc(1234, 0, 0), -20); + fails(pc(0, 1234, 0), -20); + + // fails because exponent delta overflows + fails(pc(1, 1, i32::MIN), i32::MAX); + } + #[test] fn test_div() { fn succeeds( From 7e06597befcfa34ec4aeccd51214e51a328c3dad Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Mon, 27 Dec 2021 10:51:43 -0800 Subject: [PATCH 31/33] cleanup --- src/entrypoint.rs | 10 ++-- src/instruction.rs | 62 +++++++++++-------- src/lib.rs | 6 +- src/price_conf.rs | 140 ++++++++++++++++++++++++++----------------- src/processor.rs | 60 +++++++++++-------- tests/integration.rs | 22 ++++++- 6 files changed, 182 insertions(+), 118 deletions(-) diff --git a/src/entrypoint.rs b/src/entrypoint.rs index 82641a4..bac23e4 100644 --- a/src/entrypoint.rs +++ b/src/entrypoint.rs @@ -3,14 +3,14 @@ #![cfg(not(feature = "no-entrypoint"))] use solana_program::{ - account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, pubkey::Pubkey, + account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, pubkey::Pubkey, }; entrypoint!(process_instruction); fn process_instruction( - program_id: &Pubkey, - accounts: &[AccountInfo], - instruction_data: &[u8], + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], ) -> ProgramResult { - crate::processor::process_instruction(program_id, accounts, instruction_data) + crate::processor::process_instruction(program_id, accounts, instruction_data) } diff --git a/src/instruction.rs b/src/instruction.rs index 62dee61..d04df30 100644 --- a/src/instruction.rs +++ b/src/instruction.rs @@ -1,41 +1,55 @@ //! Program instructions, used for end-to-end testing and instruction counts use { - crate::id, - borsh::{BorshDeserialize, BorshSerialize}, - solana_program::instruction::Instruction, - crate::PriceConf, + crate::id, + borsh::{BorshDeserialize, BorshSerialize}, + solana_program::instruction::Instruction, + crate::PriceConf, }; /// Instructions supported by the pyth-client program, used for testing and /// instruction counts #[derive(Clone, Debug, BorshSerialize, BorshDeserialize, PartialEq)] pub enum PythClientInstruction { - Divide { - numerator: PriceConf, - denominator: PriceConf, - }, - /// Don't do anything for comparison - /// - /// No accounts required for this instruction - Noop, + Divide { + numerator: PriceConf, + denominator: PriceConf, + }, + Multiply { + x: PriceConf, + y: PriceConf, + }, + /// Don't do anything for comparison + /// + /// No accounts required for this instruction + Noop, } pub fn divide(numerator: PriceConf, denominator: PriceConf) -> Instruction { - Instruction { - program_id: id(), - accounts: vec![], - data: PythClientInstruction::Divide { numerator, denominator } - .try_to_vec() - .unwrap(), - } + Instruction { + program_id: id(), + accounts: vec![], + data: PythClientInstruction::Divide { numerator, denominator } + .try_to_vec() + .unwrap(), + } +} + +pub fn multiply(x: PriceConf, y: PriceConf) -> Instruction { + Instruction { + program_id: id(), + accounts: vec![], + data: PythClientInstruction::Multiply { x, y } + .try_to_vec() + .unwrap(), + } } /// Noop instruction for comparison purposes pub fn noop() -> Instruction { - Instruction { - program_id: id(), - accounts: vec![], - data: PythClientInstruction::Noop.try_to_vec().unwrap(), - } + Instruction { + program_id: id(), + accounts: vec![], + data: PythClientInstruction::Noop.try_to_vec().unwrap(), + } } diff --git a/src/lib.rs b/src/lib.rs index b19302d..9dbebb8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,10 @@ -use { - borsh::{BorshDeserialize, BorshSerialize}, -}; +pub use self::price_conf::PriceConf; mod entrypoint; pub mod processor; pub mod instruction; mod price_conf; -pub use self::price_conf::PriceConf; - solana_program::declare_id!("PythC11111111111111111111111111111111111111"); pub const MAGIC : u32 = 0xa1b2c3d4; diff --git a/src/price_conf.rs b/src/price_conf.rs index 9488147..a98e461 100644 --- a/src/price_conf.rs +++ b/src/price_conf.rs @@ -20,6 +20,14 @@ const MIN_PD_V_I64: i64 = -MAX_PD_V_I64; * PriceConf { price: 12345, conf: 267, expo: -2 }; // represents 123.45 +- 2.67 * PriceConf { price: 123, conf: 1, expo: 2 }; // represents 12300 +- 100 * ``` + * + * `PriceConf` supports a limited set of mathematical operations. All of these operations will + * propagate any uncertainty in the arguments into the result. However, the uncertainty in the + * result may overestimate the true uncertainty (by at most a factor of `sqrt(2)`) due to + * computational limitations. Furthermore, all of these operations may return `None` if their + * result cannot be represented within the numeric representation (e.g., the exponent is so + * small that the price does not fit into an i64). Users of these methods should (1) select + * their exponents to avoid this problem, and (2) handle the `None` case gracefully. */ #[derive(PartialEq, Debug, BorshSerialize, BorshDeserialize, Clone)] pub struct PriceConf { @@ -31,8 +39,6 @@ pub struct PriceConf { impl PriceConf { /** * Divide this price by `other` while propagating the uncertainty in both prices into the result. - * The uncertainty propagation algorithm is an approximation due to computational limitations - * that may slightly overestimate the resulting uncertainty (by at most a factor of sqrt(2)). * * This method will automatically select a reasonable exponent for the result. If both * `self` and `other` are normalized, the exponent is `self.expo + PD_EXPO - other.expo` (i.e., @@ -40,12 +46,6 @@ impl PriceConf { * this method will normalize them, resulting in an unpredictable result exponent. * If the result is used in a context that requires a specific exponent, please call * `scale_to_exponent` on it. - * - * This function will return `None` unless all of the following conditions are satisfied: - * 1. The prices of self and other are > 0. - * 2. The confidence of the result can be represented using a 64-bit number in the computed - * exponent. This condition will fail if the confidence is >> the price of either input, - * (which should almost never occur in the real world) */ pub fn div(&self, other: &PriceConf) -> Option { // PriceConf is not guaranteed to store its price/confidence in normalized form. @@ -69,20 +69,18 @@ impl PriceConf { // Compute the confidence interval. // This code uses the 1-norm instead of the 2-norm for computational reasons. - // The correct formula is midprice * sqrt(c_1^2 + c_2^2), where c_1 and c_2 are the - // confidence intervals in price-percentage terms of the base and other. This quantity - // is difficult to compute due to the sqrt, and overflow/underflow considerations. - // Instead, this code uses midprice * (c_1 + c_2). + // Let p +- a and q +- b be the two arguments to this method. The correct + // formula is p/q * sqrt( (a/p)^2 + (b/q)^2 ). This quantity + // is difficult to compute due to the sqrt and overflow/underflow considerations. + // + // This code instead computes p/q * (a/p + b/q) = a/q + pb/q^2 . // This quantity is at most a factor of sqrt(2) greater than the correct result, which // shouldn't matter considering that confidence intervals are typically ~0.1% of the price. - // The exponent is PD_EXPO for both of these. Each of these uses 57 bits. - // let base_confidence_pct: u64 = (base.conf * PD_SCALE) / base_price; + // This uses 57 bits and has an exponent of PD_EXPO. let other_confidence_pct: u64 = (other.conf * PD_SCALE) / other_price; - // at most 58 bits - // let confidence_pct = base_confidence_pct + other_confidence_pct; - // at most 57 + 58 - 29 = 86 bits, with the same exponent as the midprice. + // first term is 57 bits, second term is 57 + 58 - 29 = 86 bits. Same exponent as the midprice. let conf = (((base.conf * PD_SCALE) / other_price) as u128) + ((other_confidence_pct as u128) * (midprice as u128)) / (PD_SCALE as u128); // Note that this check only fails if an argument's confidence interval was >> its price, @@ -102,6 +100,8 @@ impl PriceConf { * Add `other` to this, propagating uncertainty in both prices. Requires both * `PriceConf`s to have the same exponent -- use `scale_to_exponent` on the arguments * if necessary. + * + * TODO: could generalize this method to support different exponents. */ pub fn add(&self, other: &PriceConf) -> Option { assert_eq!(self.expo, other.expo); @@ -133,22 +133,13 @@ impl PriceConf { let (base_price, base_sign) = PriceConf::to_unsigned(base.price); let (other_price, other_sign) = PriceConf::to_unsigned(other.price); - // Compute the midprice, base in terms of other. - // Uses at most 27*2 bits + // Uses at most 27*2 = 54 bits let midprice = base_price * other_price; let midprice_expo = base.expo.checked_add(other.expo)?; // Compute the confidence interval. // This code uses the 1-norm instead of the 2-norm for computational reasons. - // The correct formula is midprice * sqrt(c_1^2 + c_2^2), where c_1 and c_2 are the - // confidence intervals in price-percentage terms of the base and other. This quantity - // is difficult to compute due to the sqrt, and overflow/underflow considerations. - // Instead, this code uses midprice * (c_1 + c_2). - // This quantity is at most a factor of sqrt(2) greater than the correct result, which - // shouldn't matter considering that confidence intervals are typically ~0.1% of the price. - // - // Note that this simplifies to - // pq * (a/p + b/q) = qa + bp + // Note that this simplifies: pq * (a/p + b/q) = qa + pb // 27*2 + 1 bits let conf = base.conf * other_price + other.conf * base_price; @@ -230,9 +221,16 @@ impl PriceConf { } } + /** + * Helper function to convert signed integers to unsigned and a sign bit, which simplifies + * some of the computations above. + */ fn to_unsigned(x: i64) -> (u64, i64) { + // this check is stricter than necessary. it technically only needs to guard against + // i64::MIN, which can't be negated. However, this method should only be used in the context + // of normalized numbers. assert!(x <= MAX_PD_V_I64 && x >= MIN_PD_V_I64); - if (x < 0) { + if x < 0 { (-x as u64, -1) } else { (x as u64, 1) @@ -285,7 +283,7 @@ mod test { pc(-2 * (PD_SCALE as i64) / 100, 3 * PD_SCALE / 100, 2) ); - // the max values are a factor of 10^11 larger than MAX_PD_V + // the i64 / u64 max values are a factor of 10^11 larger than MAX_PD_V let expo = -(PD_EXPO - 2); let scale_i64 = (PD_SCALE as i64) * 100; let scale_u64 = scale_i64 as u64; @@ -323,7 +321,6 @@ mod test { succeeds(pc(1234, 1234, 0), 1, pc(123, 123, 1)); succeeds(pc(1234, 1234, 0), 2, pc(12, 12, 2)); succeeds(pc(-1234, 1234, 0), 2, pc(-12, 12, 2)); - // TODO: confidence rounding question succeeds(pc(1234, 1234, 0), 4, pc(0, 0, 4)); succeeds(pc(1234, 1234, 0), -1, pc(12340, 12340, -1)); succeeds(pc(1234, 1234, 0), -2, pc(123400, 123400, -2)); @@ -417,7 +414,25 @@ mod test { pc(i64::MAX, 1, 0), pc((PD_SCALE as i64) / normed.price, PD_SCALE / (normed.price as u64), PD_EXPO - normed.expo)); - // TODO: negative number tests around i64::MIN + let normed = pc(i64::MIN, u64::MAX, 0).normalize().unwrap(); + let normed_c = (-normed.price) as u64; + + succeeds(pc(i64::MIN, u64::MAX, 0), pc(i64::MIN, u64::MAX, 0), pc_scaled(1, 4, 0, PD_EXPO)); + succeeds(pc(i64::MIN, u64::MAX, 0), pc(i64::MAX, u64::MAX, 0), pc_scaled(-1, 4, 0, PD_EXPO)); + succeeds(pc(i64::MIN, u64::MAX, 0), + pc(1, 1, 0), + pc_scaled(normed.price, 3 * normed_c, normed.expo, normed.expo + PD_EXPO)); + succeeds(pc(1, 1, 0), + pc(i64::MIN, u64::MAX, 0), + pc((PD_SCALE as i64) / normed.price, 3 * (PD_SCALE / normed_c), PD_EXPO - normed.expo)); + + succeeds(pc(i64::MIN, 1, 0), pc(i64::MIN, 1, 0), pc_scaled(1, 0, 0, PD_EXPO)); + succeeds(pc(i64::MIN, 1, 0), + pc(1, 1, 0), + pc_scaled(normed.price, normed_c, normed.expo, normed.expo + PD_EXPO)); + succeeds(pc(1, 1, 0), + pc(i64::MIN, 1, 0), + pc((PD_SCALE as i64) / normed.price, PD_SCALE / (normed_c), PD_EXPO - normed.expo)); // Price is zero pre-normalization succeeds(pc(0, 1, 0), pc(1, 1, 0), pc_scaled(0, 1, 0, PD_EXPO)); @@ -439,13 +454,6 @@ mod test { succeeds(pc(1, 1, i32::MIN - PD_EXPO), pc(1, 1, 0), pc(PD_SCALE as i64, 2 * PD_SCALE, i32::MIN)); succeeds(pc(1, 1, i32::MIN), pc(1, 1, PD_EXPO), pc(PD_SCALE as i64, 2 * PD_SCALE, i32::MIN)); fails(pc(1, 1, i32::MIN - PD_EXPO), pc(1, 1, 1)); - - // FIXME: move to scaling tests - // Result exponent too small - /* - test_succeeds(pc(1, 1, 0), pc(1, 1, 0), PD_EXPO, (1 * (PD_SCALE as i64), 2 * PD_SCALE)); - test_fails(pc(1, 1, 0), pc(1, 1, 0), PD_EXPO - 1); - */ } #[test] @@ -502,6 +510,23 @@ mod test { pc(MAX_PD_V_I64, MAX_PD_V_U64 + MAX_PD_V_U64 * MAX_PD_V_U64, 0) ); + succeeds( + pc(MIN_PD_V_I64, MAX_PD_V_U64, 0), + pc(MIN_PD_V_I64, MAX_PD_V_U64, 0), + pc(MIN_PD_V_I64 * MIN_PD_V_I64, 2 * MAX_PD_V_U64 * MAX_PD_V_U64, 0) + ); + succeeds( + pc(MIN_PD_V_I64, MAX_PD_V_U64, 0), + pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), + pc(MIN_PD_V_I64 * MAX_PD_V_I64, 2 * MAX_PD_V_U64 * MAX_PD_V_U64, 0) + ); + succeeds(pc(MIN_PD_V_I64, MAX_PD_V_U64, 0), pc(1, 1, 0), pc(MIN_PD_V_I64, 2 * MAX_PD_V_U64, 0)); + succeeds( + pc(MIN_PD_V_I64, MAX_PD_V_U64, 0), + pc(1, MAX_PD_V_U64, 0), + pc(MIN_PD_V_I64, MAX_PD_V_U64 + MAX_PD_V_U64 * MAX_PD_V_U64, 0) + ); + // Unnormalized tests below here let ten_e7: i64 = 10000000; let uten_e7: u64 = 10000000; @@ -534,6 +559,27 @@ mod test { pc(1, 1, 0), pc(normed.price, normed.price as u64, normed.expo)); + let normed = pc(i64::MIN, u64::MAX, 0).normalize().unwrap(); + let normed_c = (-normed.price) as u64; + + succeeds( + pc(i64::MIN, u64::MAX, 0), + pc(i64::MIN, u64::MAX, 0), + pc(normed.price * normed.price, 4 * (normed_c * normed_c), normed.expo * 2) + ); + succeeds(pc(i64::MIN, u64::MAX, 0), + pc(1, 1, 0), + pc(normed.price, 3 * normed_c, normed.expo)); + + succeeds( + pc(i64::MIN, 1, 0), + pc(i64::MIN, 1, 0), + pc(normed.price * normed.price, 0, normed.expo * 2) + ); + succeeds(pc(i64::MIN, 1, 0), + pc(1, 1, 0), + pc(normed.price, normed_c, normed.expo)); + // Exponent under/overflow. succeeds(pc(1, 1, i32::MAX), pc(1, 1, 0), pc(1, 2, i32::MAX)); succeeds(pc(1, 1, i32::MAX), pc(1, 1, -1), pc(1, 2, i32::MAX - 1)); @@ -542,21 +588,5 @@ mod test { succeeds(pc(1, 1, i32::MIN), pc(1, 1, 0), pc(1, 2, i32::MIN)); succeeds(pc(1, 1, i32::MIN), pc(1, 1, 1), pc(1, 2, i32::MIN + 1)); fails(pc(1, 1, i32::MIN), pc(1, 1, -1)); - - - /* - // Price is zero pre-normalization - fails(pc(0, 1, 0), pc(1, 1, 0)); - fails(pc(1, 1, 0), pc(0, 1, 0)); - - // Can't normalize the input when the confidence is >> price. - fails(pc(1, 1, 0), pc(1, u64::MAX, 0)); - fails(pc(1, u64::MAX, 0), pc(1, 1, 0)); - - // FIXME: move to scaling tests - // Result exponent too small - test_succeeds(pc(1, 1, 0), pc(1, 1, 0), PD_EXPO, (1 * (PD_SCALE as i64), 2 * PD_SCALE)); - test_fails(pc(1, 1, 0), pc(1, 1, 0), PD_EXPO - 1); - */ } } diff --git a/src/processor.rs b/src/processor.rs index fc4f28f..1b311e5 100644 --- a/src/processor.rs +++ b/src/processor.rs @@ -1,37 +1,45 @@ //! Program instruction processor +use borsh::BorshDeserialize; use solana_program::{ - account_info::AccountInfo, - entrypoint::ProgramResult, - log::{sol_log_compute_units, sol_log_params, sol_log_slice}, - msg, - pubkey::Pubkey, + account_info::AccountInfo, + entrypoint::ProgramResult, + log::sol_log_compute_units, + msg, + pubkey::Pubkey, }; + use crate::{ - instruction::PythClientInstruction, - PriceConf, + instruction::PythClientInstruction, }; -use borsh::BorshDeserialize; pub fn process_instruction( - _program_id: &Pubkey, - _accounts: &[AccountInfo], - input: &[u8], + _program_id: &Pubkey, + _accounts: &[AccountInfo], + input: &[u8], ) -> ProgramResult { - let instruction = PythClientInstruction::try_from_slice(input).unwrap(); - match instruction { - PythClientInstruction::Divide { numerator, denominator } => { - msg!("Calculating numerator.div(denominator)"); - sol_log_compute_units(); - let result = numerator.div(&denominator); - sol_log_compute_units(); - msg!("result: {:?}", result); - Ok(()) - } - PythClientInstruction::Noop => { - msg!("Do nothing"); - msg!("{}", 0_u64); - Ok(()) - } + let instruction = PythClientInstruction::try_from_slice(input).unwrap(); + match instruction { + PythClientInstruction::Divide { numerator, denominator } => { + msg!("Calculating numerator.div(denominator)"); + sol_log_compute_units(); + let result = numerator.div(&denominator); + sol_log_compute_units(); + msg!("result: {:?}", result); + Ok(()) + } + PythClientInstruction::Multiply { x, y } => { + msg!("Calculating numerator.div(denominator)"); + sol_log_compute_units(); + let result = x.mul(&y); + sol_log_compute_units(); + msg!("result: {:?}", result); + Ok(()) + } + PythClientInstruction::Noop => { + msg!("Do nothing"); + msg!("{}", 0_u64); + Ok(()) } + } } diff --git a/tests/integration.rs b/tests/integration.rs index 7be0beb..595431f 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1,14 +1,14 @@ use { + borsh::BorshDeserialize, + pyth_client::{id, instruction, PriceConf}, + pyth_client::processor::process_instruction, solana_program::{ instruction::{AccountMeta, Instruction}, pubkey::Pubkey, }, solana_program_test::*, solana_sdk::{signature::Signer, transaction::Transaction}, - pyth_client::processor::process_instruction, std::str::FromStr, - borsh::BorshDeserialize, - pyth_client::{id, instruction, PriceConf}, }; async fn test_instr(instr: Instruction) { @@ -46,4 +46,20 @@ async fn test_div() { expo: 0 } )).await; +} + +#[tokio::test] +async fn test_mul() { + test_instr(instruction::multiply( + PriceConf { + price: 100, + conf: 1, + expo: 2 + }, + PriceConf { + price: 123, + conf: 1, + expo: -2 + } + )).await; } \ No newline at end of file From 033001a1744ca0db7d819031892824aa06534a38 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Mon, 27 Dec 2021 10:52:58 -0800 Subject: [PATCH 32/33] unused --- Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 7733d2d..17c6e6e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,6 @@ no-entrypoint = [] solana-program = "1.8.1" borsh = "0.9" borsh-derive = "0.9.0" -num-integer = "0.1.44" [dev-dependencies] solana-program-test = "1.8.1" From 1506f81a4492721aea460c11ba22f2a5890a55b4 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Mon, 27 Dec 2021 10:54:39 -0800 Subject: [PATCH 33/33] minor --- src/price_conf.rs | 2 -- src/processor.rs | 2 +- tests/integration.rs | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/price_conf.rs b/src/price_conf.rs index a98e461..87f75ad 100644 --- a/src/price_conf.rs +++ b/src/price_conf.rs @@ -474,8 +474,6 @@ mod test { assert_eq!(result, None); } - // TODO: test negative numbers - succeeds(pc(1, 1, 0), pc(1, 1, 0), pc(1, 2, 0)); succeeds(pc(1, 1, -8), pc(1, 1, -8), pc(1, 2, -16)); succeeds(pc(10, 1, 0), pc(1, 1, 0), pc(10, 11, 0)); diff --git a/src/processor.rs b/src/processor.rs index 1b311e5..ad74d38 100644 --- a/src/processor.rs +++ b/src/processor.rs @@ -29,7 +29,7 @@ pub fn process_instruction( Ok(()) } PythClientInstruction::Multiply { x, y } => { - msg!("Calculating numerator.div(denominator)"); + msg!("Calculating numerator.mul(denominator)"); sol_log_compute_units(); let result = x.mul(&y); sol_log_compute_units(); diff --git a/tests/integration.rs b/tests/integration.rs index 595431f..d5ec2e3 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -62,4 +62,4 @@ async fn test_mul() { expo: -2 } )).await; -} \ No newline at end of file +}