From 4f572d461a08d95022897aaec45040ca8ee41155 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Tue, 25 Nov 2025 16:06:17 -0800 Subject: [PATCH 01/47] [ENG-87] Implement price library --- Cargo.lock | 9 ++++ Cargo.toml | 2 +- price/Cargo.toml | 14 ++++++ price/src/lib.rs | 112 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 price/Cargo.toml create mode 100644 price/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 8edb66be..69fb724f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2509,6 +2509,15 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "price" +version = "0.1.0" +dependencies = [ + "static_assertions", + "strum", + "strum_macros", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" diff --git a/Cargo.toml b/Cargo.toml index 0f2a4f4d..08f3ce35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ "interface", "grpc-stream", "program", - "transaction-parser", + "transaction-parser", "price", ] [workspace.package] diff --git a/price/Cargo.toml b/price/Cargo.toml new file mode 100644 index 00000000..aa326ad9 --- /dev/null +++ b/price/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "price" +version.workspace = true +edition.workspace = true + +[dependencies] +static_assertions.workspace = true + +[dev-dependencies] +strum.workspace = true +strum_macros.workspace = true + +[lints] +workspace = true diff --git a/price/src/lib.rs b/price/src/lib.rs new file mode 100644 index 00000000..36c33052 --- /dev/null +++ b/price/src/lib.rs @@ -0,0 +1,112 @@ +use static_assertions::const_assert; + +/// The number of significant digits in the significant; i.e., the digits represented in the price. +/// +/// For example, for a price of 12.413 and [`SIGNIFICANT_DIGITS`] == 8: +/// +/// THe significand/price would be `12_413_000`. +pub const SIGNIFICANT_DIGITS: u8 = 8; + +#[repr(C)] +pub struct Price { + pub price: u64, + pub base: u64, + pub quote: u64, +} + +#[derive(Debug)] +#[cfg_attr(test, derive(strum_macros::Display))] +pub enum PriceError { + InvalidLotExponent, + InvalidTickExponent, + LotMinusTickUnderflow, +} + +type SignificandType = u32; +type LotsType = u16; + +#[allow(clippy::absurd_extreme_comparisons)] +const _: () = { + const_assert!((LotsType::MAX as u64 * SignificandType::MAX as u64) <= u64::MAX); +}; + +pub fn to_price( + significand: u32, + lots: u16, + lot_exp: u8, + tick_exp: u8, +) -> Result { + // This is only for compile-time type checking. It ensures that the const assertion types + // reflect the types passed to this function. + let _: &SignificandType = &significand; + let _: &LotsType = &lots; + + let lots = lots as u64; + let significand = significand as u64; + + let base = lots + .checked_mul(pow10_u64!(lot_exp, PriceError::InvalidLotExponent)) + .ok_or(PriceError::InvalidLotExponent)?; + + // Note: significant * lots is always <= u64::MAX, checked with const asserts above. + let quote = (significand * lots) + .checked_mul(pow10_u64!(tick_exp, PriceError::InvalidLotExponent)) + .ok_or(PriceError::InvalidLotExponent)?; + + if lot_exp > tick_exp { + return Err(PriceError::LotMinusTickUnderflow); + } + let price_exp = pow10_u64!(tick_exp - lot_exp, PriceError::InvalidLotExponent); + let price = significand + .checked_mul(price_exp) + .ok_or(PriceError::InvalidLotExponent)?; + + Ok(Price { price, base, quote }) +} + +/// Returns `10^exp` inline using a `match` on the exponent. +/// +/// Supported exponents map directly to their corresponding power-of-ten +/// value. Any unsupported exponent causes the macro to emit an early +/// `return Err($err)` from the surrounding function. +/// +/// # Example +/// +/// ``` +/// let scale = pow10_u64!(3, MyError::InvalidExponent); +/// assert_eq!(scale, 1000); // 10^3 +/// ``` +#[macro_export] +macro_rules! pow10_u64 { + ($exp:expr, $err:expr) => {{ + match $exp { + 0 => 1u64, + 1 => 10, + 2 => 100, + 3 => 1_000, + 4 => 10_000, + 5 => 100_000, + 6 => 1_000_000, + 7 => 10_000_000, + 8 => 100_000_000, + 9 => 1_000_000_000, + 10 => 10_000_000_000, + 11 => 100_000_000_000, + 12 => 1_000_000_000_000, + 13 => 10_000_000_000_000, + 14 => 100_000_000_000_000, + 15 => 1_000_000_000_000_000, + _ => return Err($err), + } + }}; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let _result = to_price(2, 2, 3, 4).expect("Should calculate price"); + } +} From df427bb206e5144b0c31fd3e9d5ba0d74905152e Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Tue, 25 Nov 2025 16:07:32 -0800 Subject: [PATCH 02/47] Fix cargo.toml formatting --- Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 08f3ce35..d0056d87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,8 +7,9 @@ members = [ "instruction-macros/crates/test-fixtures", "interface", "grpc-stream", + "price", "program", - "transaction-parser", "price", + "transaction-parser", ] [workspace.package] From 56fc0f9c3da9119323ff8a1dae59a74440c7ff60 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Tue, 25 Nov 2025 18:15:34 -0800 Subject: [PATCH 03/47] Fix checks, remove type assertions, check significand final result range --- price/src/lib.rs | 65 ++++++++++++++++++++++++------------------------ 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/price/src/lib.rs b/price/src/lib.rs index 36c33052..7416f2ea 100644 --- a/price/src/lib.rs +++ b/price/src/lib.rs @@ -1,11 +1,8 @@ -use static_assertions::const_assert; +/// The number of significant digits in the significand; i.e., the digits represented in the price. +pub const SIGNIFICANT_DIGITS: u8 = 9; -/// The number of significant digits in the significant; i.e., the digits represented in the price. -/// -/// For example, for a price of 12.413 and [`SIGNIFICANT_DIGITS`] == 8: -/// -/// THe significand/price would be `12_413_000`. -pub const SIGNIFICANT_DIGITS: u8 = 8; +const MAX_SIGNIFICAND: u64 = 999_999_999; +const MIN_SIGNIFICAND: u64 = 100_000_000; #[repr(C)] pub struct Price { @@ -20,48 +17,49 @@ pub enum PriceError { InvalidLotExponent, InvalidTickExponent, LotMinusTickUnderflow, + ArithmeticOverflow, + InvalidSignificand, } -type SignificandType = u32; -type LotsType = u16; - -#[allow(clippy::absurd_extreme_comparisons)] -const _: () = { - const_assert!((LotsType::MAX as u64 * SignificandType::MAX as u64) <= u64::MAX); -}; - pub fn to_price( significand: u32, - lots: u16, + lots: u64, lot_exp: u8, tick_exp: u8, ) -> Result { - // This is only for compile-time type checking. It ensures that the const assertion types - // reflect the types passed to this function. - let _: &SignificandType = &significand; - let _: &LotsType = &lots; - - let lots = lots as u64; let significand = significand as u64; let base = lots .checked_mul(pow10_u64!(lot_exp, PriceError::InvalidLotExponent)) .ok_or(PriceError::InvalidLotExponent)?; - // Note: significant * lots is always <= u64::MAX, checked with const asserts above. - let quote = (significand * lots) + let significand_times_lots = significand + .checked_mul(lots) + .ok_or(PriceError::ArithmeticOverflow)?; + + let quote = significand_times_lots .checked_mul(pow10_u64!(tick_exp, PriceError::InvalidLotExponent)) - .ok_or(PriceError::InvalidLotExponent)?; + .ok_or(PriceError::ArithmeticOverflow)?; if lot_exp > tick_exp { return Err(PriceError::LotMinusTickUnderflow); } - let price_exp = pow10_u64!(tick_exp - lot_exp, PriceError::InvalidLotExponent); - let price = significand - .checked_mul(price_exp) - .ok_or(PriceError::InvalidLotExponent)?; + // Safety: the underflow condition was just checked; it returns early if underflow would occur. + let price_exp = unsafe { tick_exp.unchecked_sub(lot_exp) }; - Ok(Price { price, base, quote }) + // Safety: + // The mult operation here is always strictly <= (significand * lots) * tick_exp, since + // price_exp == tick_exp - lot_exp. + // This means that the multiplication operation here does not need to be checked, as it is + // guaranteed to not overflow since the `quote` calculation did not overflow. + let price = + unsafe { significand.unchecked_mul(pow10_u64!(price_exp, PriceError::InvalidLotExponent)) }; + + if (MIN_SIGNIFICAND..MAX_SIGNIFICAND).contains(&price) { + Ok(Price { price, base, quote }) + } else { + Err(PriceError::InvalidSignificand) + } } /// Returns `10^exp` inline using a `match` on the exponent. @@ -106,7 +104,10 @@ mod tests { use super::*; #[test] - fn it_works() { - let _result = to_price(2, 2, 3, 4).expect("Should calculate price"); + fn happy_path_simple_price() { + let price = to_price(1234, 1, 0, 0).expect("Should calculate price"); + assert_eq!(price.base, 1); + assert_eq!(price.quote, 1234); + assert_eq!(price.price, 1234); } } From 81f13bf90cfcc5d63fdbbce75cc348b68912984c Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Tue, 25 Nov 2025 18:19:06 -0800 Subject: [PATCH 04/47] Add todo/wip comment --- price/src/lib.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/price/src/lib.rs b/price/src/lib.rs index 7416f2ea..591268af 100644 --- a/price/src/lib.rs +++ b/price/src/lib.rs @@ -3,6 +3,7 @@ pub const SIGNIFICANT_DIGITS: u8 = 9; const MAX_SIGNIFICAND: u64 = 999_999_999; const MIN_SIGNIFICAND: u64 = 100_000_000; +const BIAS: u8 = 15; #[repr(C)] pub struct Price { @@ -19,6 +20,7 @@ pub enum PriceError { LotMinusTickUnderflow, ArithmeticOverflow, InvalidSignificand, + InvalidBiasedExponent, } pub fn to_price( @@ -27,6 +29,16 @@ pub fn to_price( lot_exp: u8, tick_exp: u8, ) -> Result { + // TODO: Implement bias for these values (these values represent bias factored in) + // 1: this means making sure these checks are correct (the checks below) + // 2: and factoring in negative exponents in the pow10 calculations..? + let lot_exp = lot_exp + .checked_sub(BIAS) + .ok_or(PriceError::InvalidBiasedExponent)?; + let tick_exp = tick_exp + .checked_sub(BIAS) + .ok_or(PriceError::InvalidBiasedExponent)?; + let significand = significand as u64; let base = lots From 97b6f3508c3f2ba58c0fbb9381b355faf3eb06e7 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Wed, 26 Nov 2025 19:10:24 -0800 Subject: [PATCH 05/47] Add price changes with some stuff WIP --- price/Cargo.toml | 1 + price/src/lib.rs | 236 ++++++++++++++++++++++++++++++++++++----------- 2 files changed, 185 insertions(+), 52 deletions(-) diff --git a/price/Cargo.toml b/price/Cargo.toml index aa326ad9..7742a432 100644 --- a/price/Cargo.toml +++ b/price/Cargo.toml @@ -5,6 +5,7 @@ edition.workspace = true [dependencies] static_assertions.workspace = true +pinocchio.workspace = true [dev-dependencies] strum.workspace = true diff --git a/price/src/lib.rs b/price/src/lib.rs index 591268af..d971f260 100644 --- a/price/src/lib.rs +++ b/price/src/lib.rs @@ -1,81 +1,155 @@ -/// The number of significant digits in the significand; i.e., the digits represented in the price. -pub const SIGNIFICANT_DIGITS: u8 = 9; +use pinocchio::hint; -const MAX_SIGNIFICAND: u64 = 999_999_999; -const MIN_SIGNIFICAND: u64 = 100_000_000; +const MANTISSA_DIGITS_LOWER_BOUND: u32 = 10_000_000; +const MANTISSA_DIGITS_UPPER_BOUND: u32 = 99_999_999; + +const PRICE_MANTISSA_BITS: u8 = 27; + +/// The base-10 bias of the exponents passed to price functions. +/// # Example +/// // Say you want to divide some value by const BIAS: u8 = 15; +// TODO: wip- finish the bias implementation here/above/in the pow10_* calls +const BIAS_: u8 = 1 << (32 - PRICE_MANTISSA_BITS); + +#[cfg(debug_assertions)] +mod debug_assertions { + use static_assertions::*; + + use super::*; + + const U32_BITS: usize = 32; + + /// The bitmask for the price exponent calculated from the number of bits in the price mantissa. + pub const PRICE_EXPONENT_MASK: u32 = u32::MAX << (PRICE_MANTISSA_BITS as usize); + + /// The bitmask for the price mantissa calculated from the number of bits it uses. + pub const PRICE_MANTISSA_MASK: u32 = u32::MAX >> (U32_BITS - PRICE_MANTISSA_BITS as usize); + + // The max price mantissa is its bitmask. Ensure the mantissa's upper bound doesn't exceed that. + const_assert!(MANTISSA_DIGITS_UPPER_BOUND < PRICE_MANTISSA_MASK); + + // The price exponent and mantissa bit masks xor'd should just be a u32 with all hi bits. + const_assert_eq!(PRICE_EXPONENT_MASK ^ PRICE_MANTISSA_MASK, u32::MAX); +} + +#[derive(Copy, Clone, Debug)] +/// The encoded price as a u32. +/// +/// If `N` = the number of exponent bits and `M` = the number of price bits, the u32 bit layout is: +/// +/// ```text +/// N M +/// |-----------------|--------------| +/// [ exponent_bits ] | [ price_bits ] +/// |--------------------------------| +/// 32 +/// ``` +pub struct EncodedPrice(pub u32); + +impl EncodedPrice { + #[inline(always)] + pub fn new(price_exponent: u8, price_mantissa: ValidatedPriceMantissa) -> Self { + let exponent_bits = (price_exponent as u32) << PRICE_MANTISSA_BITS; + + // No need to mask the price mantissa since it has already been range checked/validated. + // Thus it will only occupy the lower M bits where M = PRICE_MANTISSA_BITS. + Self(exponent_bits | price_mantissa.0) + } +} + +pub struct ValidatedPriceMantissa(pub u32); + +impl TryFrom for ValidatedPriceMantissa { + type Error = OrderInfoError; + + #[inline(always)] + fn try_from(price_mantissa: u32) -> Result { + if (MANTISSA_DIGITS_LOWER_BOUND..MANTISSA_DIGITS_UPPER_BOUND).contains(&price_mantissa) { + Ok(Self(price_mantissa)) + } else { + hint::cold_path(); + Err(OrderInfoError::InvalidPriceMantissa) + } + } +} #[repr(C)] -pub struct Price { - pub price: u64, - pub base: u64, - pub quote: u64, +pub struct OrderInfo { + pub price: EncodedPrice, + pub base_atoms: u64, + pub quote_atoms: u64, } +#[repr(u8)] #[derive(Debug)] #[cfg_attr(test, derive(strum_macros::Display))] -pub enum PriceError { - InvalidLotExponent, - InvalidTickExponent, - LotMinusTickUnderflow, +pub enum OrderInfoError { + InvalidBaseExponent, + InvalidQuoteExponent, + BaseMinusQuoteUnderflow, ArithmeticOverflow, - InvalidSignificand, + InvalidPriceMantissa, InvalidBiasedExponent, } -pub fn to_price( - significand: u32, - lots: u64, - lot_exp: u8, - tick_exp: u8, -) -> Result { +pub fn to_order_info( + price_mantissa: u32, + base_scalar: u64, + base_exponent_with_bias: u8, + quote_exponent_with_bias: u8, +) -> Result { + let b_biased = base_exponent_with_bias; + let q_biased = quote_exponent_with_bias; + let b_checked_bias = + checked_sub_unsigned!(b_biased, BIAS, OrderInfoError::InvalidBiasedExponent)?; + let biased_q = checked_sub_unsigned!(q_biased, BIAS, OrderInfoError::InvalidBiasedExponent)?; + + // let base = checked_mul_unsigned!(base_scalar, ) + // TODO: Implement bias for these values (these values represent bias factored in) // 1: this means making sure these checks are correct (the checks below) // 2: and factoring in negative exponents in the pow10 calculations..? - let lot_exp = lot_exp + + let lot_exp = base_exponent_with_bias .checked_sub(BIAS) - .ok_or(PriceError::InvalidBiasedExponent)?; - let tick_exp = tick_exp + .ok_or(OrderInfoError::InvalidBiasedExponent)?; + let tick_exp = quote_exponent_with_bias .checked_sub(BIAS) - .ok_or(PriceError::InvalidBiasedExponent)?; - - let significand = significand as u64; + .ok_or(OrderInfoError::InvalidBiasedExponent)?; - let base = lots - .checked_mul(pow10_u64!(lot_exp, PriceError::InvalidLotExponent)) - .ok_or(PriceError::InvalidLotExponent)?; + let base = base_scalar + .checked_mul(pow10_u64!(lot_exp, OrderInfoError::InvalidBaseExponent)) + .ok_or(OrderInfoError::InvalidBaseExponent)?; - let significand_times_lots = significand - .checked_mul(lots) - .ok_or(PriceError::ArithmeticOverflow)?; + let significand_times_lots = price_mantissa + .checked_mul(base_scalar) + .ok_or(OrderInfoError::ArithmeticOverflow)?; let quote = significand_times_lots - .checked_mul(pow10_u64!(tick_exp, PriceError::InvalidLotExponent)) - .ok_or(PriceError::ArithmeticOverflow)?; + .checked_mul(pow10_u64!(tick_exp, OrderInfoError::InvalidBaseExponent)) + .ok_or(OrderInfoError::ArithmeticOverflow)?; if lot_exp > tick_exp { - return Err(PriceError::LotMinusTickUnderflow); + hint::cold_path(); + return Err(OrderInfoError::BaseMinusQuoteUnderflow); } // Safety: the underflow condition was just checked; it returns early if underflow would occur. - let price_exp = unsafe { tick_exp.unchecked_sub(lot_exp) }; - - // Safety: - // The mult operation here is always strictly <= (significand * lots) * tick_exp, since - // price_exp == tick_exp - lot_exp. - // This means that the multiplication operation here does not need to be checked, as it is - // guaranteed to not overflow since the `quote` calculation did not overflow. - let price = - unsafe { significand.unchecked_mul(pow10_u64!(price_exp, PriceError::InvalidLotExponent)) }; - - if (MIN_SIGNIFICAND..MAX_SIGNIFICAND).contains(&price) { - Ok(Price { price, base, quote }) - } else { - Err(PriceError::InvalidSignificand) - } + let price_exponent = unsafe { tick_exp.unchecked_sub(lot_exp) }; + + let validated_mantissa = ValidatedPriceMantissa::try_from(price_mantissa)?; + + Ok(OrderInfo { + price: EncodedPrice::new(price_exponent, validated_mantissa), + base_atoms, + quote_atoms, + }) } /// Returns `10^exp` inline using a `match` on the exponent. /// +/// The `exp` +/// /// Supported exponents map directly to their corresponding power-of-ten /// value. Any unsupported exponent causes the macro to emit an early /// `return Err($err)` from the surrounding function. @@ -97,6 +171,8 @@ macro_rules! pow10_u64 { 4 => 10_000, 5 => 100_000, 6 => 1_000_000, + this needs to be 0 - 15 for negative exponents and then 15 - 31 for positive exponents + needs to factor in bias 7 => 10_000_000, 8 => 100_000_000, 9 => 1_000_000_000, @@ -111,15 +187,71 @@ macro_rules! pow10_u64 { }}; } +/// A checked subtraction with a custom error return value and the error path marked as cold. +/// +/// This is only intended for usage with **unsigned** integer types. +/// +/// # Example +/// ```rust +/// let res: Result = checked_sub!(5, 4, MyError::BadSub); +/// assert_eq!(res, Ok(1)); +/// +/// let res: Result = checked_sub!(5, 6, MyError::BadSub); +/// assert_eq!(res, Err(MyError::BadSub)); +/// ``` +#[macro_export] +macro_rules! checked_sub_unsigned { + ($lhs:expr, $rhs:expr, $err:expr $(,)?) => {{ + let lhs = $lhs; + let rhs = $rhs; + if lhs >= rhs { + Ok(lhs - rhs) + } else { + ::pinocchio::hint::cold_path(); + Err($err) + } + }}; +} + +/// A checked multiplication with a custom error return value and the error path marked as cold. +/// +/// This is only intended for usage with **unsigned** integer types. +/// +/// # Example +/// ```rust +/// let res: Result = checked_mul!(255, 1, MyError::BadMul); +/// assert_eq!(res, Ok(255)); +/// +/// let res: Result = checked_mul!(255, 2, MyError::BadMul); +/// assert_eq!(res, Err(MyError::BadMul)); +/// ``` +#[macro_export] +macro_rules! checked_mul_unsigned { + ($lhs:expr, $rhs:expr, $err:expr $(,)?) => {{ + let lhs = $lhs; + let rhs = $rhs; + match lhs.checked_mul(rhs) { + Some(val) => Ok(val), + None => { + ::pinocchio::hint::cold_path(); + Err($err) + } + } + }}; +} + #[cfg(test)] mod tests { use super::*; #[test] fn happy_path_simple_price() { - let price = to_price(1234, 1, 0, 0).expect("Should calculate price"); - assert_eq!(price.base, 1); - assert_eq!(price.quote, 1234); + let price = to_order_info(1234, 1, 0, 0).expect("Should calculate price"); + assert_eq!(price.base_atoms, 1); + assert_eq!(price.quote_atoms, 1234); assert_eq!(price.price, 1234); } + + #[test] + fn hi_encoded_price_bits() {} } From 5b87f0408581ef6f0b9da2b5ff896b2121e834f9 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Fri, 5 Dec 2025 15:39:11 -0800 Subject: [PATCH 06/47] Finish price implementation with reorganized files --- price/src/decoded_price.rs | 36 ++++ price/src/encoded_price.rs | 88 +++++++++ price/src/error.rs | 11 ++ price/src/lib.rs | 341 +++++++++++++------------------- price/src/macros.rs | 134 +++++++++++++ price/src/validated_mantissa.rs | 41 ++++ 6 files changed, 442 insertions(+), 209 deletions(-) create mode 100644 price/src/decoded_price.rs create mode 100644 price/src/encoded_price.rs create mode 100644 price/src/error.rs create mode 100644 price/src/macros.rs create mode 100644 price/src/validated_mantissa.rs diff --git a/price/src/decoded_price.rs b/price/src/decoded_price.rs new file mode 100644 index 00000000..bd072af2 --- /dev/null +++ b/price/src/decoded_price.rs @@ -0,0 +1,36 @@ +use crate::{ + EncodedPrice, + OrderInfoError, + ValidatedPriceMantissa, + BIAS, + PRICE_MANTISSA_BITS, + PRICE_MANTISSA_MASK, +}; + +#[derive(Clone)] +#[cfg_attr(test, derive(Debug))] +pub struct DecodedPrice { + pub price_exponent_biased: u8, + pub price_mantissa: ValidatedPriceMantissa, +} + +impl TryFrom for DecodedPrice { + type Error = OrderInfoError; + + fn try_from(value: EncodedPrice) -> Result { + let price_exponent_biased = (value.0 >> PRICE_MANTISSA_BITS) as u8; + let validated_mantissa = value.0 & PRICE_MANTISSA_MASK; + + Ok(Self { + price_exponent_biased, + price_mantissa: ValidatedPriceMantissa::new_unchecked(validated_mantissa), + }) + } +} + +impl From for f64 { + fn from(decoded: DecodedPrice) -> Self { + (decoded.price_mantissa.get() as f64) + * 10f64.powi(decoded.price_exponent_biased as i32 - BIAS as i32) + } +} diff --git a/price/src/encoded_price.rs b/price/src/encoded_price.rs new file mode 100644 index 00000000..67d8bf48 --- /dev/null +++ b/price/src/encoded_price.rs @@ -0,0 +1,88 @@ +use crate::{ + ValidatedPriceMantissa, + PRICE_MANTISSA_BITS, +}; + +#[derive(Copy, Clone, Debug)] +/// The encoded price as a u32. +/// +/// If `N` = the number of exponent bits and `M` = the number of price bits, the u32 bit layout is: +/// +/// ```text +/// N M +/// |-----------------|--------------| +/// [ exponent_bits ] | [ price_bits ] +/// |--------------------------------| +/// 32 +/// ``` +pub struct EncodedPrice(pub u32); + +impl EncodedPrice { + #[inline(always)] + pub fn new(price_exponent_biased: u8, price_mantissa: ValidatedPriceMantissa) -> Self { + let exponent_bits = (price_exponent_biased as u32) << PRICE_MANTISSA_BITS; + + // No need to mask the price mantissa since it has already been range checked/validated. + // Thus it's guaranteed it will only occupy the lower M bits where M = PRICE_MANTISSA_BITS. + Self(exponent_bits | price_mantissa.get()) + } + + /// The encoded price representation of a market buy/taker order with no contraints on the + /// maximum filled ask price. + #[inline(always)] + pub const fn infinity() -> Self { + Self(u32::MAX) + } + + #[inline(always)] + pub fn is_infinity(&self) -> bool { + self.0 == u32::MAX + } + + /// The encoded price representation of a market sell/taker order with no constraints on the + /// minimum filled bid price. + #[inline(always)] + pub const fn zero() -> Self { + Self(0) + } + + #[inline(always)] + pub fn is_zero(&self) -> bool { + self.0 == 0 + } +} + +#[cfg(test)] +mod tests { + use crate::{ + to_biased_exponent, + EncodedPrice, + ValidatedPriceMantissa, + BIAS, + PRICE_MANTISSA_BITS, + PRICE_MANTISSA_MASK, + }; + + #[test] + fn encoded_price_bits() { + let exponent = 0b0_1111; + let price_mantissa = 0b000_1111_0000_1111_0000_1111_0000; + let encoded_price = EncodedPrice::new( + to_biased_exponent!(exponent), + ValidatedPriceMantissa::try_from(price_mantissa).unwrap(), + ); + assert_eq!( + encoded_price.0 >> PRICE_MANTISSA_BITS, + (exponent + BIAS) as u32 + ); + assert_eq!(encoded_price.0 & PRICE_MANTISSA_MASK, price_mantissa); + } + + #[test] + fn test_infinity() { + assert_eq!(EncodedPrice::infinity().0, u32::MAX); + assert_eq!(EncodedPrice::zero().0, 0); + assert!(EncodedPrice::infinity().is_infinity()); + assert!(EncodedPrice::zero().is_zero()); + } +} diff --git a/price/src/error.rs b/price/src/error.rs new file mode 100644 index 00000000..fadaf4cf --- /dev/null +++ b/price/src/error.rs @@ -0,0 +1,11 @@ +#[repr(u8)] +#[derive(Debug)] +#[cfg_attr(test, derive(strum_macros::Display))] +pub enum OrderInfoError { + InvalidBaseExponent, + InvalidQuoteExponent, + BaseMinusQuoteUnderflow, + ArithmeticOverflow, + InvalidPriceMantissa, + InvalidBiasedExponent, +} diff --git a/price/src/lib.rs b/price/src/lib.rs index d971f260..d7691a4a 100644 --- a/price/src/lib.rs +++ b/price/src/lib.rs @@ -1,16 +1,38 @@ -use pinocchio::hint; +mod decoded_price; +mod encoded_price; +mod error; +mod macros; +mod validated_mantissa; + +pub use decoded_price::*; +pub use encoded_price::*; +pub use error::*; +pub use validated_mantissa::*; const MANTISSA_DIGITS_LOWER_BOUND: u32 = 10_000_000; const MANTISSA_DIGITS_UPPER_BOUND: u32 = 99_999_999; +const U32_BITS: u8 = 32; const PRICE_MANTISSA_BITS: u8 = 27; -/// The base-10 bias of the exponents passed to price functions. -/// # Example -/// // Say you want to divide some value by -const BIAS: u8 = 15; -// TODO: wip- finish the bias implementation here/above/in the pow10_* calls -const BIAS_: u8 = 1 << (32 - PRICE_MANTISSA_BITS); +#[allow(dead_code)] +/// The number of exponent bits is simply the remaining bits in a u32 after storing the price +/// mantissa bits. +const EXPONENT_BITS: u8 = U32_BITS - PRICE_MANTISSA_BITS; +#[allow(dead_code)] +/// The full range of valid biased exponent values. That is, 0 <= biased_exponent <= EXPONENT_RANGE. +const EXPONENT_RANGE: u8 = (1 << (EXPONENT_BITS)) - 1; + +/// [`BIAS`] is the number that satisfies: `BIAS + SMALLEST_POSSIBLE_EXPONENT == 0`. +/// That is, if the exponent range is 32 values from -16 <= n <= 15, the smallest possible exponent +/// is -16, so the BIAS must be 16. +/// Note the decision to use a larger negative range instead of a larger positive range (i.e., +/// [-16, 15] instead of [-15, 16]) is because [-16, 15] has a tighter range in terms of the +/// difference in orders of magnitude for the smallest and largest exponent values. +pub const BIAS: u8 = 16; + +/// The bitmask for the price mantissa calculated from the number of bits it uses. +pub const PRICE_MANTISSA_MASK: u32 = u32::MAX >> ((U32_BITS - PRICE_MANTISSA_BITS) as usize); #[cfg(debug_assertions)] mod debug_assertions { @@ -18,240 +40,141 @@ mod debug_assertions { use super::*; - const U32_BITS: usize = 32; + // The max price mantissa representable with `PRICE_MANTISSA_BITS` should exceed the upper bound + // used to ensure a fixed number of digits for the price mantissa. + const_assert!(MANTISSA_DIGITS_UPPER_BOUND < PRICE_MANTISSA_MASK); + #[allow(dead_code)] /// The bitmask for the price exponent calculated from the number of bits in the price mantissa. pub const PRICE_EXPONENT_MASK: u32 = u32::MAX << (PRICE_MANTISSA_BITS as usize); - /// The bitmask for the price mantissa calculated from the number of bits it uses. - pub const PRICE_MANTISSA_MASK: u32 = u32::MAX >> (U32_BITS - PRICE_MANTISSA_BITS as usize); - - // The max price mantissa is its bitmask. Ensure the mantissa's upper bound doesn't exceed that. - const_assert!(MANTISSA_DIGITS_UPPER_BOUND < PRICE_MANTISSA_MASK); - - // The price exponent and mantissa bit masks xor'd should just be a u32 with all hi bits. + // XOR'ing the price exponent and mantissa bit masks should result in a u32 with all 1 bits, + // aka u32::MAX. const_assert_eq!(PRICE_EXPONENT_MASK ^ PRICE_MANTISSA_MASK, u32::MAX); } -#[derive(Copy, Clone, Debug)] -/// The encoded price as a u32. -/// -/// If `N` = the number of exponent bits and `M` = the number of price bits, the u32 bit layout is: -/// -/// ```text -/// N M -/// |-----------------|--------------| -/// [ exponent_bits ] | [ price_bits ] -/// |--------------------------------| -/// 32 -/// ``` -pub struct EncodedPrice(pub u32); - -impl EncodedPrice { - #[inline(always)] - pub fn new(price_exponent: u8, price_mantissa: ValidatedPriceMantissa) -> Self { - let exponent_bits = (price_exponent as u32) << PRICE_MANTISSA_BITS; - - // No need to mask the price mantissa since it has already been range checked/validated. - // Thus it will only occupy the lower M bits where M = PRICE_MANTISSA_BITS. - Self(exponent_bits | price_mantissa.0) - } -} - -pub struct ValidatedPriceMantissa(pub u32); - -impl TryFrom for ValidatedPriceMantissa { - type Error = OrderInfoError; - - #[inline(always)] - fn try_from(price_mantissa: u32) -> Result { - if (MANTISSA_DIGITS_LOWER_BOUND..MANTISSA_DIGITS_UPPER_BOUND).contains(&price_mantissa) { - Ok(Self(price_mantissa)) - } else { - hint::cold_path(); - Err(OrderInfoError::InvalidPriceMantissa) - } - } -} - #[repr(C)] +#[cfg_attr(test, derive(Debug))] pub struct OrderInfo { - pub price: EncodedPrice, + pub encoded_price: EncodedPrice, pub base_atoms: u64, pub quote_atoms: u64, } -#[repr(u8)] -#[derive(Debug)] -#[cfg_attr(test, derive(strum_macros::Display))] -pub enum OrderInfoError { - InvalidBaseExponent, - InvalidQuoteExponent, - BaseMinusQuoteUnderflow, - ArithmeticOverflow, - InvalidPriceMantissa, - InvalidBiasedExponent, -} - pub fn to_order_info( price_mantissa: u32, base_scalar: u64, - base_exponent_with_bias: u8, - quote_exponent_with_bias: u8, + base_exponent_biased: u8, + quote_exponent_biased: u8, ) -> Result { - let b_biased = base_exponent_with_bias; - let q_biased = quote_exponent_with_bias; - let b_checked_bias = - checked_sub_unsigned!(b_biased, BIAS, OrderInfoError::InvalidBiasedExponent)?; - let biased_q = checked_sub_unsigned!(q_biased, BIAS, OrderInfoError::InvalidBiasedExponent)?; - - // let base = checked_mul_unsigned!(base_scalar, ) - - // TODO: Implement bias for these values (these values represent bias factored in) - // 1: this means making sure these checks are correct (the checks below) - // 2: and factoring in negative exponents in the pow10 calculations..? - - let lot_exp = base_exponent_with_bias - .checked_sub(BIAS) - .ok_or(OrderInfoError::InvalidBiasedExponent)?; - let tick_exp = quote_exponent_with_bias - .checked_sub(BIAS) - .ok_or(OrderInfoError::InvalidBiasedExponent)?; - - let base = base_scalar - .checked_mul(pow10_u64!(lot_exp, OrderInfoError::InvalidBaseExponent)) - .ok_or(OrderInfoError::InvalidBaseExponent)?; - - let significand_times_lots = price_mantissa - .checked_mul(base_scalar) - .ok_or(OrderInfoError::ArithmeticOverflow)?; - - let quote = significand_times_lots - .checked_mul(pow10_u64!(tick_exp, OrderInfoError::InvalidBaseExponent)) - .ok_or(OrderInfoError::ArithmeticOverflow)?; - - if lot_exp > tick_exp { - hint::cold_path(); - return Err(OrderInfoError::BaseMinusQuoteUnderflow); - } - // Safety: the underflow condition was just checked; it returns early if underflow would occur. - let price_exponent = unsafe { tick_exp.unchecked_sub(lot_exp) }; - let validated_mantissa = ValidatedPriceMantissa::try_from(price_mantissa)?; + let base_atoms = pow10_u64!(base_scalar, base_exponent_biased)?; + + let price_mantissa_times_base_scalar = checked_mul!( + validated_mantissa.get() as u64, + base_scalar, + OrderInfoError::ArithmeticOverflow + )?; + let quote_atoms = pow10_u64!(price_mantissa_times_base_scalar, quote_exponent_biased)?; + + // Ultimately, the price mantissa is multiplied by: + // 10 ^ (quote_exponent_biased - base_exponent_biased) + // aka 10 ^ (q - b) + // which means q - b may be negative and must be re-biased. Underflow only occurs if the + // re-biased exponent difference is negative. + let price_exponent_rebiased = checked_sub!( + quote_exponent_biased + BIAS, + base_exponent_biased, + OrderInfoError::BaseMinusQuoteUnderflow + )?; + Ok(OrderInfo { - price: EncodedPrice::new(price_exponent, validated_mantissa), + encoded_price: EncodedPrice::new(price_exponent_rebiased, validated_mantissa), base_atoms, quote_atoms, }) } -/// Returns `10^exp` inline using a `match` on the exponent. -/// -/// The `exp` -/// -/// Supported exponents map directly to their corresponding power-of-ten -/// value. Any unsupported exponent causes the macro to emit an early -/// `return Err($err)` from the surrounding function. -/// -/// # Example -/// -/// ``` -/// let scale = pow10_u64!(3, MyError::InvalidExponent); -/// assert_eq!(scale, 1000); // 10^3 -/// ``` -#[macro_export] -macro_rules! pow10_u64 { - ($exp:expr, $err:expr) => {{ - match $exp { - 0 => 1u64, - 1 => 10, - 2 => 100, - 3 => 1_000, - 4 => 10_000, - 5 => 100_000, - 6 => 1_000_000, - this needs to be 0 - 15 for negative exponents and then 15 - 31 for positive exponents - needs to factor in bias - 7 => 10_000_000, - 8 => 100_000_000, - 9 => 1_000_000_000, - 10 => 10_000_000_000, - 11 => 100_000_000_000, - 12 => 1_000_000_000_000, - 13 => 10_000_000_000_000, - 14 => 100_000_000_000_000, - 15 => 1_000_000_000_000_000, - _ => return Err($err), - } - }}; -} - -/// A checked subtraction with a custom error return value and the error path marked as cold. -/// -/// This is only intended for usage with **unsigned** integer types. -/// -/// # Example -/// ```rust -/// let res: Result = checked_sub!(5, 4, MyError::BadSub); -/// assert_eq!(res, Ok(1)); -/// -/// let res: Result = checked_sub!(5, 6, MyError::BadSub); -/// assert_eq!(res, Err(MyError::BadSub)); -/// ``` -#[macro_export] -macro_rules! checked_sub_unsigned { - ($lhs:expr, $rhs:expr, $err:expr $(,)?) => {{ - let lhs = $lhs; - let rhs = $rhs; - if lhs >= rhs { - Ok(lhs - rhs) - } else { - ::pinocchio::hint::cold_path(); - Err($err) - } - }}; -} - -/// A checked multiplication with a custom error return value and the error path marked as cold. -/// -/// This is only intended for usage with **unsigned** integer types. -/// -/// # Example -/// ```rust -/// let res: Result = checked_mul!(255, 1, MyError::BadMul); -/// assert_eq!(res, Ok(255)); -/// -/// let res: Result = checked_mul!(255, 2, MyError::BadMul); -/// assert_eq!(res, Err(MyError::BadMul)); -/// ``` -#[macro_export] -macro_rules! checked_mul_unsigned { - ($lhs:expr, $rhs:expr, $err:expr $(,)?) => {{ - let lhs = $lhs; - let rhs = $rhs; - match lhs.checked_mul(rhs) { - Some(val) => Ok(val), - None => { - ::pinocchio::hint::cold_path(); - Err($err) - } - } - }}; -} - #[cfg(test)] mod tests { + use std::ops::Mul; + + use static_assertions::*; + use super::*; #[test] fn happy_path_simple_price() { - let price = to_order_info(1234, 1, 0, 0).expect("Should calculate price"); - assert_eq!(price.base_atoms, 1); - assert_eq!(price.quote_atoms, 1234); - assert_eq!(price.price, 1234); + let base_biased_exponent = to_biased_exponent!(0); + let quote_biased_exponent = to_biased_exponent!(-4); + let order = to_order_info(12_340_000, 1, base_biased_exponent, quote_biased_exponent) + .expect("Should calculate price"); + assert_eq!(order.base_atoms, 1); + assert_eq!(order.quote_atoms, 1234); + + let decoded_price: f64 = DecodedPrice::try_from(order.encoded_price) + .expect("Should decode") + .into(); + assert_eq!(decoded_price, "1234".parse().unwrap()); + } + + #[test] + fn price_with_max_sig_digits() { + let order = to_order_info(12345678, 1, to_biased_exponent!(0), to_biased_exponent!(0)) + .expect("Should calculate price"); + assert_eq!(order.base_atoms, 1); + assert_eq!(order.quote_atoms, 12345678); + + let decoded_price: f64 = DecodedPrice::try_from(order.encoded_price) + .expect("Should decode") + .into(); + assert_eq!(decoded_price, "12345678".parse().unwrap()); } #[test] - fn hi_encoded_price_bits() {} + fn decimal_price() { + let mantissa = 12345678; + let order = to_order_info(mantissa, 1, to_biased_exponent!(8), to_biased_exponent!(0)) + .expect("Should calculate price"); + assert_eq!(order.quote_atoms, 12345678); + assert_eq!(order.base_atoms, 100000000); + + let decoded_price = DecodedPrice::try_from(order.encoded_price).expect("Should decode"); + let decoded_f64: f64 = decoded_price.clone().into(); + assert_eq!(decoded_price.price_mantissa.get(), mantissa); + assert_eq!(decoded_f64, "0.12345678".parse().unwrap()); + assert_eq!( + (decoded_price.price_mantissa.get() as f64) + .mul(10f64.powi(decoded_price.price_exponent_biased as i32 - BIAS as i32)), + decoded_f64 + ); + } + + #[test] + fn bias_ranges() { + const_assert_eq!(16, BIAS); + + let val_156_e_neg_16: u64 = 1_560_000_000_000_000_000; + let calculated = val_156_e_neg_16 / 10u64.pow(BIAS as u32); + let expected = 156; + assert_eq!( + pow10_u64!(val_156_e_neg_16, 0).expect("0 is a valid biased exponent"), + calculated, + ); + assert_eq!(calculated, expected); + + let val: u64 = 156; + let max_exponent = EXPONENT_RANGE as u32; + let calculated = val + * 10u64 + .checked_pow(max_exponent - BIAS as u32) + .expect("Shouldn't overflow"); + let expected: u64 = 156_000_000_000_000_000; + assert_eq!( + pow10_u64!(val, max_exponent).expect("Exponent should be valid"), + calculated + ); + assert_eq!(calculated, expected); + } } diff --git a/price/src/macros.rs b/price/src/macros.rs new file mode 100644 index 00000000..2aea3b07 --- /dev/null +++ b/price/src/macros.rs @@ -0,0 +1,134 @@ +/// Macro utility for calculating the value of an operation given a biased exponent, where a biased +/// exponent represents the base 10 positive or negative exponent value without using negative +/// values. +#[macro_export] +#[rustfmt::skip] +macro_rules! pow10_u64 { + ($value:expr, $biased_exponent:expr) => {{ + ::static_assertions::const_assert_eq!($crate::BIAS - 16, 0); + match $biased_exponent { + /* BIAS - 16 */ 0 => Ok($value / 10_000_000_000_000_000), + /* BIAS - 15 */ 1 => Ok($value / 1_000_000_000_000_000), + /* BIAS - 14 */ 2 => Ok($value / 100_000_000_000_000), + /* BIAS - 13 */ 3 => Ok($value / 10_000_000_000_000), + /* BIAS - 12 */ 4 => Ok($value / 1_000_000_000_000), + /* BIAS - 11 */ 5 => Ok($value / 100_000_000_000), + /* BIAS - 10 */ 6 => Ok($value / 10_000_000_000), + /* BIAS - 9 */ 7 => Ok($value / 1_000_000_000), + /* BIAS - 8 */ 8 => Ok($value / 100_000_000), + /* BIAS - 7 */ 9 => Ok($value / 10_000_000), + /* BIAS - 6 */ 10 => Ok($value / 1_000_000), + /* BIAS - 5 */ 11 => Ok($value / 100_000), + /* BIAS - 4 */ 12 => Ok($value / 10_000), + /* BIAS - 3 */ 13 => Ok($value / 1_000), + /* BIAS - 2 */ 14 => Ok($value / 100), + /* BIAS - 1 */ 15 => Ok($value / 10), + /* BIAS - 0 */ 16 => Ok($value), + /* BIAS + 1 */ 17 => checked_mul!($value, 10, OrderInfoError::ArithmeticOverflow), + /* BIAS + 2 */ 18 => checked_mul!($value, 100, OrderInfoError::ArithmeticOverflow), + /* BIAS + 3 */ 19 => checked_mul!($value, 1_000, OrderInfoError::ArithmeticOverflow), + /* BIAS + 4 */ 20 => checked_mul!($value, 10_000, OrderInfoError::ArithmeticOverflow), + /* BIAS + 5 */ 21 => checked_mul!($value, 100_000, OrderInfoError::ArithmeticOverflow), + /* BIAS + 6 */ 22 => checked_mul!($value, 1_000_000, OrderInfoError::ArithmeticOverflow), + /* BIAS + 7 */ 23 => checked_mul!($value, 10_000_000, OrderInfoError::ArithmeticOverflow), + /* BIAS + 8 */ 24 => checked_mul!($value, 100_000_000, OrderInfoError::ArithmeticOverflow), + /* BIAS + 9 */ 25 => checked_mul!($value, 1_000_000_000, OrderInfoError::ArithmeticOverflow), + /* BIAS + 10 */ 26 => checked_mul!($value, 10_000_000_000, OrderInfoError::ArithmeticOverflow), + /* BIAS + 11 */ 27 => checked_mul!($value, 100_000_000_000, OrderInfoError::ArithmeticOverflow), + /* BIAS + 12 */ 28 => checked_mul!($value, 1_000_000_000_000, OrderInfoError::ArithmeticOverflow), + /* BIAS + 13 */ 29 => checked_mul!($value, 10_000_000_000_000, OrderInfoError::ArithmeticOverflow), + /* BIAS + 14 */ 30 => checked_mul!($value, 100_000_000_000_000, OrderInfoError::ArithmeticOverflow), + /* BIAS + 15 */ 31 => checked_mul!($value, 1_000_000_000_000_000, OrderInfoError::ArithmeticOverflow), + _ => Err(OrderInfoError::InvalidBiasedExponent), + } + }}; +} + +/// A checked subtraction with a custom error return value and the error path marked as cold. +/// +/// *NOTE: This is only intended for usage with **unsigned** integer types.* +/// +/// # Example +/// ```rust +/// let res: Result = checked_sub!(5, 4, MyError::BadSub); +/// assert_eq!(res, Ok(1)); +/// +/// let res: Result = checked_sub!(5, 6, MyError::BadSub); +/// assert_eq!(res, Err(MyError::BadSub)); +/// ``` +#[macro_export] +macro_rules! checked_sub { + ($lhs:expr, $rhs:expr, $err:expr $(,)?) => {{ + let lhs = $lhs; + let rhs = $rhs; + if lhs >= rhs { + Ok(lhs - rhs) + } else { + ::pinocchio::hint::cold_path(); + Err($err) + } + }}; +} + +/// A checked multiplication with a custom error return value and the error path marked as cold. +/// +/// *NOTE: This is only intended for usage with **unsigned** integer types.* +/// +/// # Example +/// ```rust +/// let res: Result = checked_mul!(255, 1, MyError::BadMul); +/// assert_eq!(res, Ok(255)); +/// +/// let res: Result = checked_mul!(255, 2, MyError::BadMul); +/// assert_eq!(res, Err(MyError::BadMul)); +/// ``` +#[macro_export] +macro_rules! checked_mul { + ($lhs:expr, $rhs:expr, $err:expr $(,)?) => {{ + match $lhs.checked_mul($rhs) { + Some(val) => Ok(val), + None => { + ::pinocchio::hint::cold_path(); + Err($err) + } + } + }}; +} + +/// A checked divide with a custom error return value and the error path marked as cold. +/// +/// Only errors on division by zero. +/// +/// # Example +/// ```rust +/// let res: Result = checked_div!(255, 1, MyError::DivideByZero); +/// assert_eq!(res, Ok(255)); +/// +/// let res: Result = checked_div!(255, 0, MyError::DivideByZero); +/// assert_eq!(res, Err(MyError::DivideByZero)); +/// ``` +#[macro_export] +macro_rules! checked_div { + ($lhs:expr, $rhs:expr, $err:expr $(,)?) => {{ + match $rhs { + 0 => { + ::pinocchio::hint::cold_path(); + Err($err) + } + _ => $lhs / $rhs, + } + }}; +} + +/// Test utility macro for converting unbiased exponents to biased exponents. +#[cfg(test)] +#[macro_export] +macro_rules! to_biased_exponent { + ($unbiased_exponent:expr) => {{ + let unbiased_signed = $unbiased_exponent as i16; + match unbiased_signed { + -15..=16 => (unbiased_signed + $crate::BIAS as i16) as u8, + _ => panic!("Invalid unbiased exponent."), + } + }}; +} diff --git a/price/src/validated_mantissa.rs b/price/src/validated_mantissa.rs new file mode 100644 index 00000000..88aa8a7e --- /dev/null +++ b/price/src/validated_mantissa.rs @@ -0,0 +1,41 @@ +use pinocchio::hint; + +use crate::{ + OrderInfoError, + MANTISSA_DIGITS_LOWER_BOUND, + MANTISSA_DIGITS_UPPER_BOUND, +}; + +#[derive(Clone)] +#[cfg_attr(test, derive(Debug))] +pub struct ValidatedPriceMantissa(u32); + +impl TryFrom for ValidatedPriceMantissa { + type Error = OrderInfoError; + + #[inline(always)] + fn try_from(price_mantissa: u32) -> Result { + if (MANTISSA_DIGITS_LOWER_BOUND..MANTISSA_DIGITS_UPPER_BOUND).contains(&price_mantissa) { + Ok(Self(price_mantissa)) + } else { + hint::cold_path(); + Err(OrderInfoError::InvalidPriceMantissa) + } + } +} + +impl ValidatedPriceMantissa { + /// Creates a new [`ValidatedPriceMantissa`] without range checking the passed value. + /// This should only be used when the price mantissa has definitively already been validated. + #[inline(always)] + pub(crate) fn new_unchecked(price_mantissa: u32) -> Self { + Self(price_mantissa) + } +} + +impl ValidatedPriceMantissa { + #[inline(always)] + pub fn get(&self) -> u32 { + self.0 + } +} From 698041a6c723828b3956df2c0e547ec56352b864 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Fri, 5 Dec 2025 15:40:37 -0800 Subject: [PATCH 07/47] Add pinocchio to price deps so it's possible to use the `hint::` module --- Cargo.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.lock b/Cargo.lock index 69fb724f..46a4485f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2513,6 +2513,7 @@ dependencies = [ name = "price" version = "0.1.0" dependencies = [ + "pinocchio", "static_assertions", "strum", "strum_macros", From 36aeed19ead457704d67b9bf9e10d9a1b7f30f45 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Fri, 5 Dec 2025 15:49:21 -0800 Subject: [PATCH 08/47] Don't have doctests run the tests in macro docs --- price/src/macros.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/price/src/macros.rs b/price/src/macros.rs index 2aea3b07..dabbfe4e 100644 --- a/price/src/macros.rs +++ b/price/src/macros.rs @@ -49,7 +49,7 @@ macro_rules! pow10_u64 { /// *NOTE: This is only intended for usage with **unsigned** integer types.* /// /// # Example -/// ```rust +/// ```rust,ignore /// let res: Result = checked_sub!(5, 4, MyError::BadSub); /// assert_eq!(res, Ok(1)); /// @@ -75,7 +75,7 @@ macro_rules! checked_sub { /// *NOTE: This is only intended for usage with **unsigned** integer types.* /// /// # Example -/// ```rust +/// ```rust,ignore /// let res: Result = checked_mul!(255, 1, MyError::BadMul); /// assert_eq!(res, Ok(255)); /// @@ -100,7 +100,7 @@ macro_rules! checked_mul { /// Only errors on division by zero. /// /// # Example -/// ```rust +/// ```rust,ignore /// let res: Result = checked_div!(255, 1, MyError::DivideByZero); /// assert_eq!(res, Ok(255)); /// From 0560ee465b4374ee49b2cc41b0c3ca24b50378db Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Fri, 5 Dec 2025 16:11:36 -0800 Subject: [PATCH 09/47] Fix doctests instead of disabling them --- price/src/macros.rs | 49 +++++++++++++-------------------------------- 1 file changed, 14 insertions(+), 35 deletions(-) diff --git a/price/src/macros.rs b/price/src/macros.rs index dabbfe4e..a21fc08d 100644 --- a/price/src/macros.rs +++ b/price/src/macros.rs @@ -49,12 +49,14 @@ macro_rules! pow10_u64 { /// *NOTE: This is only intended for usage with **unsigned** integer types.* /// /// # Example -/// ```rust,ignore -/// let res: Result = checked_sub!(5, 4, MyError::BadSub); -/// assert_eq!(res, Ok(1)); +/// ```rust +/// enum MyError { BadSub } /// -/// let res: Result = checked_sub!(5, 6, MyError::BadSub); -/// assert_eq!(res, Err(MyError::BadSub)); +/// let res: Result = price::checked_sub!(5, 4, MyError::BadSub); +/// assert!(matches!(res, Ok(1))); +/// +/// let res: Result = price::checked_sub!(5, 6, MyError::BadSub); +/// assert!(matches!(res, Err(MyError::BadSub))); /// ``` #[macro_export] macro_rules! checked_sub { @@ -75,12 +77,14 @@ macro_rules! checked_sub { /// *NOTE: This is only intended for usage with **unsigned** integer types.* /// /// # Example -/// ```rust,ignore -/// let res: Result = checked_mul!(255, 1, MyError::BadMul); -/// assert_eq!(res, Ok(255)); +/// ```rust +/// enum MyError { BadMul } +/// +/// let res: Result = price::checked_mul!(255u8, 1, MyError::BadMul); +/// assert!(matches!(res, Ok(255))); /// -/// let res: Result = checked_mul!(255, 2, MyError::BadMul); -/// assert_eq!(res, Err(MyError::BadMul)); +/// let res: Result = price::checked_mul!(255u8, 2, MyError::BadMul); +/// assert!(matches!(res, Err(MyError::BadMul))); /// ``` #[macro_export] macro_rules! checked_mul { @@ -95,31 +99,6 @@ macro_rules! checked_mul { }}; } -/// A checked divide with a custom error return value and the error path marked as cold. -/// -/// Only errors on division by zero. -/// -/// # Example -/// ```rust,ignore -/// let res: Result = checked_div!(255, 1, MyError::DivideByZero); -/// assert_eq!(res, Ok(255)); -/// -/// let res: Result = checked_div!(255, 0, MyError::DivideByZero); -/// assert_eq!(res, Err(MyError::DivideByZero)); -/// ``` -#[macro_export] -macro_rules! checked_div { - ($lhs:expr, $rhs:expr, $err:expr $(,)?) => {{ - match $rhs { - 0 => { - ::pinocchio::hint::cold_path(); - Err($err) - } - _ => $lhs / $rhs, - } - }}; -} - /// Test utility macro for converting unbiased exponents to biased exponents. #[cfg(test)] #[macro_export] From cda4dbbf541d83ec1ad3dc740013801e501d8104 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Fri, 5 Dec 2025 16:29:04 -0800 Subject: [PATCH 10/47] Fix constraints typo --- price/src/encoded_price.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/price/src/encoded_price.rs b/price/src/encoded_price.rs index 67d8bf48..3f0096a6 100644 --- a/price/src/encoded_price.rs +++ b/price/src/encoded_price.rs @@ -27,7 +27,7 @@ impl EncodedPrice { Self(exponent_bits | price_mantissa.get()) } - /// The encoded price representation of a market buy/taker order with no contraints on the + /// The encoded price representation of a market buy/taker order with no constraints on the /// maximum filled ask price. #[inline(always)] pub const fn infinity() -> Self { From a1dd93ecef35f164f3d831c8fc66fa4092613e0b Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Fri, 5 Dec 2025 17:00:09 -0800 Subject: [PATCH 11/47] Use fallible decoded price conversions that factor in the possibility of infinity/zero --- price/src/decoded_price.rs | 68 +++++++++++++++++++++++++++++--------- price/src/encoded_price.rs | 11 +++--- price/src/error.rs | 1 + price/src/lib.rs | 28 ++++++++++------ 4 files changed, 77 insertions(+), 31 deletions(-) diff --git a/price/src/decoded_price.rs b/price/src/decoded_price.rs index bd072af2..11bd2c94 100644 --- a/price/src/decoded_price.rs +++ b/price/src/decoded_price.rs @@ -3,34 +3,70 @@ use crate::{ OrderInfoError, ValidatedPriceMantissa, BIAS, + ENCODED_PRICE_INFINITY, + ENCODED_PRICE_ZERO, PRICE_MANTISSA_BITS, PRICE_MANTISSA_MASK, }; #[derive(Clone)] #[cfg_attr(test, derive(Debug))] -pub struct DecodedPrice { - pub price_exponent_biased: u8, - pub price_mantissa: ValidatedPriceMantissa, +pub enum DecodedPrice { + Zero, + Infinity, + ExponentAndMantissa { + price_exponent_biased: u8, + price_mantissa: ValidatedPriceMantissa, + }, } -impl TryFrom for DecodedPrice { - type Error = OrderInfoError; +impl DecodedPrice { + pub fn as_exponent_and_mantissa(&self) -> Option<(&u8, &ValidatedPriceMantissa)> { + if let DecodedPrice::ExponentAndMantissa { + price_exponent_biased, + price_mantissa, + } = self + { + Some((price_exponent_biased, price_mantissa)) + } else { + None + } + } +} - fn try_from(value: EncodedPrice) -> Result { - let price_exponent_biased = (value.0 >> PRICE_MANTISSA_BITS) as u8; - let validated_mantissa = value.0 & PRICE_MANTISSA_MASK; +impl From for DecodedPrice { + fn from(encoded: EncodedPrice) -> Self { + match encoded.0 { + ENCODED_PRICE_ZERO => Self::Zero, + ENCODED_PRICE_INFINITY => Self::Infinity, + value => { + let price_exponent_biased = (value >> PRICE_MANTISSA_BITS) as u8; + let validated_mantissa = value & PRICE_MANTISSA_MASK; - Ok(Self { - price_exponent_biased, - price_mantissa: ValidatedPriceMantissa::new_unchecked(validated_mantissa), - }) + Self::ExponentAndMantissa { + price_exponent_biased, + price_mantissa: ValidatedPriceMantissa::new_unchecked(validated_mantissa), + } + } + } } } -impl From for f64 { - fn from(decoded: DecodedPrice) -> Self { - (decoded.price_mantissa.get() as f64) - * 10f64.powi(decoded.price_exponent_biased as i32 - BIAS as i32) +impl TryFrom for f64 { + type Error = OrderInfoError; + + fn try_from(decoded: DecodedPrice) -> Result { + match decoded { + DecodedPrice::Zero => Ok(0f64), + DecodedPrice::Infinity => Err(OrderInfoError::InfinityIsNotAFloat), + DecodedPrice::ExponentAndMantissa { + price_exponent_biased, + price_mantissa, + } => { + let res = (price_mantissa.get() as f64) + * 10f64.powi(price_exponent_biased as i32 - BIAS as i32); + Ok(res) + } + } } } diff --git a/price/src/encoded_price.rs b/price/src/encoded_price.rs index 3f0096a6..604d7083 100644 --- a/price/src/encoded_price.rs +++ b/price/src/encoded_price.rs @@ -17,6 +17,9 @@ use crate::{ /// ``` pub struct EncodedPrice(pub u32); +pub const ENCODED_PRICE_INFINITY: u32 = u32::MAX; +pub const ENCODED_PRICE_ZERO: u32 = 0; + impl EncodedPrice { #[inline(always)] pub fn new(price_exponent_biased: u8, price_mantissa: ValidatedPriceMantissa) -> Self { @@ -31,24 +34,24 @@ impl EncodedPrice { /// maximum filled ask price. #[inline(always)] pub const fn infinity() -> Self { - Self(u32::MAX) + Self(ENCODED_PRICE_INFINITY) } #[inline(always)] pub fn is_infinity(&self) -> bool { - self.0 == u32::MAX + self.0 == ENCODED_PRICE_INFINITY } /// The encoded price representation of a market sell/taker order with no constraints on the /// minimum filled bid price. #[inline(always)] pub const fn zero() -> Self { - Self(0) + Self(ENCODED_PRICE_ZERO) } #[inline(always)] pub fn is_zero(&self) -> bool { - self.0 == 0 + self.0 == ENCODED_PRICE_ZERO } } diff --git a/price/src/error.rs b/price/src/error.rs index fadaf4cf..edff4f42 100644 --- a/price/src/error.rs +++ b/price/src/error.rs @@ -8,4 +8,5 @@ pub enum OrderInfoError { ArithmeticOverflow, InvalidPriceMantissa, InvalidBiasedExponent, + InfinityIsNotAFloat, } diff --git a/price/src/lib.rs b/price/src/lib.rs index d7691a4a..98695ab2 100644 --- a/price/src/lib.rs +++ b/price/src/lib.rs @@ -113,9 +113,9 @@ mod tests { assert_eq!(order.base_atoms, 1); assert_eq!(order.quote_atoms, 1234); - let decoded_price: f64 = DecodedPrice::try_from(order.encoded_price) - .expect("Should decode") - .into(); + let decoded_price: f64 = DecodedPrice::from(order.encoded_price) + .try_into() + .expect("Should be a valid f64"); assert_eq!(decoded_price, "1234".parse().unwrap()); } @@ -126,9 +126,9 @@ mod tests { assert_eq!(order.base_atoms, 1); assert_eq!(order.quote_atoms, 12345678); - let decoded_price: f64 = DecodedPrice::try_from(order.encoded_price) - .expect("Should decode") - .into(); + let decoded_price: f64 = DecodedPrice::from(order.encoded_price) + .try_into() + .expect("Should be a valid f64"); assert_eq!(decoded_price, "12345678".parse().unwrap()); } @@ -140,13 +140,19 @@ mod tests { assert_eq!(order.quote_atoms, 12345678); assert_eq!(order.base_atoms, 100000000); - let decoded_price = DecodedPrice::try_from(order.encoded_price).expect("Should decode"); - let decoded_f64: f64 = decoded_price.clone().into(); - assert_eq!(decoded_price.price_mantissa.get(), mantissa); + let decoded_price = DecodedPrice::from(order.encoded_price); + + let (decoded_exponent, decoded_mantissa) = decoded_price + .as_exponent_and_mantissa() + .expect("Should be exponent + mantissa"); + let decoded_f64: f64 = decoded_price + .clone() + .try_into() + .expect("Should be a valid f64"); + assert_eq!(decoded_mantissa.get(), mantissa); assert_eq!(decoded_f64, "0.12345678".parse().unwrap()); assert_eq!( - (decoded_price.price_mantissa.get() as f64) - .mul(10f64.powi(decoded_price.price_exponent_biased as i32 - BIAS as i32)), + (decoded_mantissa.get() as f64).mul(10f64.powi(*decoded_exponent as i32 - BIAS as i32)), decoded_f64 ); } From da88fb941ae2419226d4dba504310d6541f0740b Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Fri, 5 Dec 2025 17:06:06 -0800 Subject: [PATCH 12/47] Updated mantissa range and add tests for it --- price/src/validated_mantissa.rs | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/price/src/validated_mantissa.rs b/price/src/validated_mantissa.rs index 88aa8a7e..b4da7a92 100644 --- a/price/src/validated_mantissa.rs +++ b/price/src/validated_mantissa.rs @@ -15,7 +15,7 @@ impl TryFrom for ValidatedPriceMantissa { #[inline(always)] fn try_from(price_mantissa: u32) -> Result { - if (MANTISSA_DIGITS_LOWER_BOUND..MANTISSA_DIGITS_UPPER_BOUND).contains(&price_mantissa) { + if (MANTISSA_DIGITS_LOWER_BOUND..=MANTISSA_DIGITS_UPPER_BOUND).contains(&price_mantissa) { Ok(Self(price_mantissa)) } else { hint::cold_path(); @@ -39,3 +39,28 @@ impl ValidatedPriceMantissa { self.0 } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn valid_mantissas() { + for mantissa in [ + MANTISSA_DIGITS_LOWER_BOUND, + MANTISSA_DIGITS_LOWER_BOUND + 1, + MANTISSA_DIGITS_UPPER_BOUND, + MANTISSA_DIGITS_UPPER_BOUND - 1, + ] { + let validated_mantissa = ValidatedPriceMantissa::try_from(mantissa); + assert!(validated_mantissa.is_ok()); + assert_eq!(validated_mantissa.unwrap().0, mantissa); + } + } + + #[test] + fn invalid_mantissas() { + assert!(ValidatedPriceMantissa::try_from(MANTISSA_DIGITS_LOWER_BOUND - 1).is_err()); + assert!(ValidatedPriceMantissa::try_from(MANTISSA_DIGITS_UPPER_BOUND + 1).is_err()); + } +} From 955c273c76345b40333d3c0d654fcb1f371e09f9 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Fri, 5 Dec 2025 17:15:13 -0800 Subject: [PATCH 13/47] Add checks to the unbiased macro range checks --- price/src/lib.rs | 8 ++++++++ price/src/macros.rs | 37 ++++++++++++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/price/src/lib.rs b/price/src/lib.rs index 98695ab2..f1d5e35d 100644 --- a/price/src/lib.rs +++ b/price/src/lib.rs @@ -31,6 +31,14 @@ const EXPONENT_RANGE: u8 = (1 << (EXPONENT_BITS)) - 1; /// difference in orders of magnitude for the smallest and largest exponent values. pub const BIAS: u8 = 16; +/// The minimum unbiased exponent value. +#[cfg(test)] +const UNBIASED_MIN: i16 = 0 - crate::BIAS as i16; + +/// The maximum unbiased exponent value. +#[cfg(test)] +const UNBIASED_MAX: i16 = (crate::BIAS as i16) - 1; + /// The bitmask for the price mantissa calculated from the number of bits it uses. pub const PRICE_MANTISSA_MASK: u32 = u32::MAX >> ((U32_BITS - PRICE_MANTISSA_BITS) as usize); diff --git a/price/src/macros.rs b/price/src/macros.rs index a21fc08d..cdf26c51 100644 --- a/price/src/macros.rs +++ b/price/src/macros.rs @@ -106,8 +106,43 @@ macro_rules! to_biased_exponent { ($unbiased_exponent:expr) => {{ let unbiased_signed = $unbiased_exponent as i16; match unbiased_signed { - -15..=16 => (unbiased_signed + $crate::BIAS as i16) as u8, + $crate::UNBIASED_MIN..=$crate::UNBIASED_MAX => { + (unbiased_signed + $crate::BIAS as i16) as u8 + } _ => panic!("Invalid unbiased exponent."), } }}; } + +#[cfg(test)] +mod tests { + use crate::{ + BIAS, + UNBIASED_MAX, + UNBIASED_MIN, + }; + + #[test] + fn biased_exponent_happy_paths() { + let expected_min = (UNBIASED_MIN + BIAS as i16) as u8; + assert_eq!(to_biased_exponent!(UNBIASED_MIN), expected_min); + + let expected_mid = BIAS; + assert_eq!(to_biased_exponent!(0), expected_mid); + + let expected_max = (UNBIASED_MAX + BIAS as i16) as u8; + assert_eq!(to_biased_exponent!(UNBIASED_MAX), expected_max); + } + + #[test] + #[should_panic(expected = "Invalid unbiased exponent.")] + fn below_minimum_unbiased() { + to_biased_exponent!(UNBIASED_MIN - 1); + } + + #[test] + #[should_panic(expected = "Invalid unbiased exponent.")] + fn above_maximum_unbiased() { + to_biased_exponent!(UNBIASED_MAX + 1); + } +} From 7430b72fec972aa9860d79b3fdf93cab71497e09 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Fri, 5 Dec 2025 17:22:58 -0800 Subject: [PATCH 14/47] Make inner value of encoded price private --- price/src/decoded_price.rs | 2 +- price/src/encoded_price.rs | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/price/src/decoded_price.rs b/price/src/decoded_price.rs index 11bd2c94..2ccf22b1 100644 --- a/price/src/decoded_price.rs +++ b/price/src/decoded_price.rs @@ -36,7 +36,7 @@ impl DecodedPrice { impl From for DecodedPrice { fn from(encoded: EncodedPrice) -> Self { - match encoded.0 { + match encoded.get() { ENCODED_PRICE_ZERO => Self::Zero, ENCODED_PRICE_INFINITY => Self::Infinity, value => { diff --git a/price/src/encoded_price.rs b/price/src/encoded_price.rs index 604d7083..d574cfb2 100644 --- a/price/src/encoded_price.rs +++ b/price/src/encoded_price.rs @@ -15,7 +15,7 @@ use crate::{ /// |--------------------------------| /// 32 /// ``` -pub struct EncodedPrice(pub u32); +pub struct EncodedPrice(u32); pub const ENCODED_PRICE_INFINITY: u32 = u32::MAX; pub const ENCODED_PRICE_ZERO: u32 = 0; @@ -30,6 +30,11 @@ impl EncodedPrice { Self(exponent_bits | price_mantissa.get()) } + #[inline(always)] + pub fn get(&self) -> u32 { + self.0 + } + /// The encoded price representation of a market buy/taker order with no constraints on the /// maximum filled ask price. #[inline(always)] From f3ef73fb542ab614fb15cf557a4165e37bef1699 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Fri, 5 Dec 2025 17:24:28 -0800 Subject: [PATCH 15/47] Remove unnecessary qualification on bias in UNBIASED_* consts --- price/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/price/src/lib.rs b/price/src/lib.rs index f1d5e35d..90f53117 100644 --- a/price/src/lib.rs +++ b/price/src/lib.rs @@ -33,11 +33,11 @@ pub const BIAS: u8 = 16; /// The minimum unbiased exponent value. #[cfg(test)] -const UNBIASED_MIN: i16 = 0 - crate::BIAS as i16; +const UNBIASED_MIN: i16 = 0 - BIAS as i16; /// The maximum unbiased exponent value. #[cfg(test)] -const UNBIASED_MAX: i16 = (crate::BIAS as i16) - 1; +const UNBIASED_MAX: i16 = (BIAS as i16) - 1; /// The bitmask for the price mantissa calculated from the number of bits it uses. pub const PRICE_MANTISSA_MASK: u32 = u32::MAX >> ((U32_BITS - PRICE_MANTISSA_BITS) as usize); From 18f49cf9d79841022c36671517098422c2d3175a Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Fri, 5 Dec 2025 17:28:45 -0800 Subject: [PATCH 16/47] Make exponential 10^n const values in macro easier to read/evaluate for correctness --- price/src/macros.rs | 54 ++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/price/src/macros.rs b/price/src/macros.rs index cdf26c51..023967d3 100644 --- a/price/src/macros.rs +++ b/price/src/macros.rs @@ -7,38 +7,38 @@ macro_rules! pow10_u64 { ($value:expr, $biased_exponent:expr) => {{ ::static_assertions::const_assert_eq!($crate::BIAS - 16, 0); match $biased_exponent { - /* BIAS - 16 */ 0 => Ok($value / 10_000_000_000_000_000), - /* BIAS - 15 */ 1 => Ok($value / 1_000_000_000_000_000), - /* BIAS - 14 */ 2 => Ok($value / 100_000_000_000_000), - /* BIAS - 13 */ 3 => Ok($value / 10_000_000_000_000), - /* BIAS - 12 */ 4 => Ok($value / 1_000_000_000_000), - /* BIAS - 11 */ 5 => Ok($value / 100_000_000_000), - /* BIAS - 10 */ 6 => Ok($value / 10_000_000_000), - /* BIAS - 9 */ 7 => Ok($value / 1_000_000_000), - /* BIAS - 8 */ 8 => Ok($value / 100_000_000), - /* BIAS - 7 */ 9 => Ok($value / 10_000_000), - /* BIAS - 6 */ 10 => Ok($value / 1_000_000), - /* BIAS - 5 */ 11 => Ok($value / 100_000), - /* BIAS - 4 */ 12 => Ok($value / 10_000), - /* BIAS - 3 */ 13 => Ok($value / 1_000), + /* BIAS - 16 */ 0 => Ok($value / 10000000000000000), + /* BIAS - 15 */ 1 => Ok($value / 1000000000000000), + /* BIAS - 14 */ 2 => Ok($value / 100000000000000), + /* BIAS - 13 */ 3 => Ok($value / 10000000000000), + /* BIAS - 12 */ 4 => Ok($value / 1000000000000), + /* BIAS - 11 */ 5 => Ok($value / 100000000000), + /* BIAS - 10 */ 6 => Ok($value / 10000000000), + /* BIAS - 9 */ 7 => Ok($value / 1000000000), + /* BIAS - 8 */ 8 => Ok($value / 100000000), + /* BIAS - 7 */ 9 => Ok($value / 10000000), + /* BIAS - 6 */ 10 => Ok($value / 1000000), + /* BIAS - 5 */ 11 => Ok($value / 100000), + /* BIAS - 4 */ 12 => Ok($value / 10000), + /* BIAS - 3 */ 13 => Ok($value / 1000), /* BIAS - 2 */ 14 => Ok($value / 100), /* BIAS - 1 */ 15 => Ok($value / 10), /* BIAS - 0 */ 16 => Ok($value), /* BIAS + 1 */ 17 => checked_mul!($value, 10, OrderInfoError::ArithmeticOverflow), /* BIAS + 2 */ 18 => checked_mul!($value, 100, OrderInfoError::ArithmeticOverflow), - /* BIAS + 3 */ 19 => checked_mul!($value, 1_000, OrderInfoError::ArithmeticOverflow), - /* BIAS + 4 */ 20 => checked_mul!($value, 10_000, OrderInfoError::ArithmeticOverflow), - /* BIAS + 5 */ 21 => checked_mul!($value, 100_000, OrderInfoError::ArithmeticOverflow), - /* BIAS + 6 */ 22 => checked_mul!($value, 1_000_000, OrderInfoError::ArithmeticOverflow), - /* BIAS + 7 */ 23 => checked_mul!($value, 10_000_000, OrderInfoError::ArithmeticOverflow), - /* BIAS + 8 */ 24 => checked_mul!($value, 100_000_000, OrderInfoError::ArithmeticOverflow), - /* BIAS + 9 */ 25 => checked_mul!($value, 1_000_000_000, OrderInfoError::ArithmeticOverflow), - /* BIAS + 10 */ 26 => checked_mul!($value, 10_000_000_000, OrderInfoError::ArithmeticOverflow), - /* BIAS + 11 */ 27 => checked_mul!($value, 100_000_000_000, OrderInfoError::ArithmeticOverflow), - /* BIAS + 12 */ 28 => checked_mul!($value, 1_000_000_000_000, OrderInfoError::ArithmeticOverflow), - /* BIAS + 13 */ 29 => checked_mul!($value, 10_000_000_000_000, OrderInfoError::ArithmeticOverflow), - /* BIAS + 14 */ 30 => checked_mul!($value, 100_000_000_000_000, OrderInfoError::ArithmeticOverflow), - /* BIAS + 15 */ 31 => checked_mul!($value, 1_000_000_000_000_000, OrderInfoError::ArithmeticOverflow), + /* BIAS + 3 */ 19 => checked_mul!($value, 1000, OrderInfoError::ArithmeticOverflow), + /* BIAS + 4 */ 20 => checked_mul!($value, 10000, OrderInfoError::ArithmeticOverflow), + /* BIAS + 5 */ 21 => checked_mul!($value, 100000, OrderInfoError::ArithmeticOverflow), + /* BIAS + 6 */ 22 => checked_mul!($value, 1000000, OrderInfoError::ArithmeticOverflow), + /* BIAS + 7 */ 23 => checked_mul!($value, 10000000, OrderInfoError::ArithmeticOverflow), + /* BIAS + 8 */ 24 => checked_mul!($value, 100000000, OrderInfoError::ArithmeticOverflow), + /* BIAS + 9 */ 25 => checked_mul!($value, 1000000000, OrderInfoError::ArithmeticOverflow), + /* BIAS + 10 */ 26 => checked_mul!($value, 10000000000, OrderInfoError::ArithmeticOverflow), + /* BIAS + 11 */ 27 => checked_mul!($value, 100000000000, OrderInfoError::ArithmeticOverflow), + /* BIAS + 12 */ 28 => checked_mul!($value, 1000000000000, OrderInfoError::ArithmeticOverflow), + /* BIAS + 13 */ 29 => checked_mul!($value, 10000000000000, OrderInfoError::ArithmeticOverflow), + /* BIAS + 14 */ 30 => checked_mul!($value, 100000000000000, OrderInfoError::ArithmeticOverflow), + /* BIAS + 15 */ 31 => checked_mul!($value, 1000000000000000, OrderInfoError::ArithmeticOverflow), _ => Err(OrderInfoError::InvalidBiasedExponent), } }}; From ce55456c43607b7c8d90cd64c47c1875fc2e2313 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Fri, 5 Dec 2025 17:58:01 -0800 Subject: [PATCH 17/47] Add const assertions and unit tests for the potential rebias overflow, with unsafe add on bias --- price/src/lib.rs | 10 ++++++++-- price/src/macros.rs | 22 ++++++++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/price/src/lib.rs b/price/src/lib.rs index 90f53117..d423901b 100644 --- a/price/src/lib.rs +++ b/price/src/lib.rs @@ -34,11 +34,15 @@ pub const BIAS: u8 = 16; /// The minimum unbiased exponent value. #[cfg(test)] const UNBIASED_MIN: i16 = 0 - BIAS as i16; - /// The maximum unbiased exponent value. #[cfg(test)] const UNBIASED_MAX: i16 = (BIAS as i16) - 1; +#[allow(dead_code)] +const MAX_BIASED_EXPONENT: u8 = 31; +// Ensure that adding the bias to the max biased exponent never overflows. +static_assertions::const_assert!((MAX_BIASED_EXPONENT as u16) + (BIAS as u16) < (u8::MAX as u16)); + /// The bitmask for the price mantissa calculated from the number of bits it uses. pub const PRICE_MANTISSA_MASK: u32 = u32::MAX >> ((U32_BITS - PRICE_MANTISSA_BITS) as usize); @@ -92,7 +96,9 @@ pub fn to_order_info( // which means q - b may be negative and must be re-biased. Underflow only occurs if the // re-biased exponent difference is negative. let price_exponent_rebiased = checked_sub!( - quote_exponent_biased + BIAS, + // Safety: The quote exponent must be <= MAX_BIASED_EXPONENT, and const assertions ensure + // that `MAX_BIASED_EXPONENT + BIAS` is always less than `u8::MAX`. + unsafe { quote_exponent_biased.unchecked_add(BIAS) }, base_exponent_biased, OrderInfoError::BaseMinusQuoteUnderflow )?; diff --git a/price/src/macros.rs b/price/src/macros.rs index 023967d3..df0b096d 100644 --- a/price/src/macros.rs +++ b/price/src/macros.rs @@ -7,7 +7,7 @@ macro_rules! pow10_u64 { ($value:expr, $biased_exponent:expr) => {{ ::static_assertions::const_assert_eq!($crate::BIAS - 16, 0); match $biased_exponent { - /* BIAS - 16 */ 0 => Ok($value / 10000000000000000), + /* BIAS - 16 */ 0 => Ok($value / 10000000000000000u64), /* BIAS - 15 */ 1 => Ok($value / 1000000000000000), /* BIAS - 14 */ 2 => Ok($value / 100000000000000), /* BIAS - 13 */ 3 => Ok($value / 10000000000000), @@ -117,13 +117,31 @@ macro_rules! to_biased_exponent { #[cfg(test)] mod tests { use crate::{ + OrderInfoError, BIAS, + MAX_BIASED_EXPONENT, UNBIASED_MAX, UNBIASED_MIN, }; #[test] - fn biased_exponent_happy_paths() { + fn check_max_biased_exponent() { + // The max biased exponent should be valid. + assert_eq!( + pow10_u64!(2u64, MAX_BIASED_EXPONENT).unwrap(), + 2 * 10u64 + .checked_pow(MAX_BIASED_EXPONENT as u32 - BIAS as u32) + .unwrap() + ); + // One past the max biased exponent should result in an error. + assert!(matches!( + pow10_u64!(2u64, MAX_BIASED_EXPONENT + 1), + Err(OrderInfoError::InvalidBiasedExponent) + )); + } + + #[test] + fn unbiased_exponent_happy_paths() { let expected_min = (UNBIASED_MIN + BIAS as i16) as u8; assert_eq!(to_biased_exponent!(UNBIASED_MIN), expected_min); From 2ffcb85fe8781abfc6bb527b030722f9afc71569 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Fri, 5 Dec 2025 17:59:31 -0800 Subject: [PATCH 18/47] Add `get` documentation --- price/src/encoded_price.rs | 1 + price/src/validated_mantissa.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/price/src/encoded_price.rs b/price/src/encoded_price.rs index d574cfb2..a40705e5 100644 --- a/price/src/encoded_price.rs +++ b/price/src/encoded_price.rs @@ -30,6 +30,7 @@ impl EncodedPrice { Self(exponent_bits | price_mantissa.get()) } + /// Returns the inner encoded price as a u32. #[inline(always)] pub fn get(&self) -> u32 { self.0 diff --git a/price/src/validated_mantissa.rs b/price/src/validated_mantissa.rs index b4da7a92..f230ca64 100644 --- a/price/src/validated_mantissa.rs +++ b/price/src/validated_mantissa.rs @@ -34,6 +34,7 @@ impl ValidatedPriceMantissa { } impl ValidatedPriceMantissa { + /// Returns the validated price mantissa as a u32. #[inline(always)] pub fn get(&self) -> u32 { self.0 From bf7d13dcd779a50bdac0c969081f248109c6cc6f Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Fri, 5 Dec 2025 18:04:19 -0800 Subject: [PATCH 19/47] Fix underflow error variant name --- price/src/error.rs | 2 +- price/src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/price/src/error.rs b/price/src/error.rs index edff4f42..6d52d300 100644 --- a/price/src/error.rs +++ b/price/src/error.rs @@ -4,7 +4,7 @@ pub enum OrderInfoError { InvalidBaseExponent, InvalidQuoteExponent, - BaseMinusQuoteUnderflow, + ExponentUnderflow, ArithmeticOverflow, InvalidPriceMantissa, InvalidBiasedExponent, diff --git a/price/src/lib.rs b/price/src/lib.rs index d423901b..bf2fec2d 100644 --- a/price/src/lib.rs +++ b/price/src/lib.rs @@ -100,7 +100,7 @@ pub fn to_order_info( // that `MAX_BIASED_EXPONENT + BIAS` is always less than `u8::MAX`. unsafe { quote_exponent_biased.unchecked_add(BIAS) }, base_exponent_biased, - OrderInfoError::BaseMinusQuoteUnderflow + OrderInfoError::ExponentUnderflow )?; Ok(OrderInfo { From 8465b6bfd2dda6fe336ef0908db3f8492d75c787 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Fri, 5 Dec 2025 18:08:49 -0800 Subject: [PATCH 20/47] Add documentation for `as_exponent_and_mantissa` --- price/src/decoded_price.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/price/src/decoded_price.rs b/price/src/decoded_price.rs index 2ccf22b1..2b1f0cd2 100644 --- a/price/src/decoded_price.rs +++ b/price/src/decoded_price.rs @@ -21,6 +21,8 @@ pub enum DecodedPrice { } impl DecodedPrice { + /// Return the optional tuple of exponent and mantissa from a decoded price. + /// If the decoded price is not a [`DecodedPrice::ExponentAndMantissa`], this returns `None`. pub fn as_exponent_and_mantissa(&self) -> Option<(&u8, &ValidatedPriceMantissa)> { if let DecodedPrice::ExponentAndMantissa { price_exponent_biased, From 3b47edea9b3a5156caa48272ab8a8ab11fe60088 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Fri, 5 Dec 2025 18:42:03 -0800 Subject: [PATCH 21/47] Add documentation for max biased exponent value and rename it and remove `EXPONENT_RANGE`/rename it --- price/src/encoded_price.rs | 3 +++ price/src/lib.rs | 10 +++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/price/src/encoded_price.rs b/price/src/encoded_price.rs index a40705e5..7db5055c 100644 --- a/price/src/encoded_price.rs +++ b/price/src/encoded_price.rs @@ -21,8 +21,11 @@ pub const ENCODED_PRICE_INFINITY: u32 = u32::MAX; pub const ENCODED_PRICE_ZERO: u32 = 0; impl EncodedPrice { + /// Creates a new [`EncodedPrice`] from a biased price exponent and a validated price mantissa. #[inline(always)] pub fn new(price_exponent_biased: u8, price_mantissa: ValidatedPriceMantissa) -> Self { + // The biased price exponent doesn't need to be checked, because the bit shift will always + // truncate to a value <= the max possible exponent value. let exponent_bits = (price_exponent_biased as u32) << PRICE_MANTISSA_BITS; // No need to mask the price mantissa since it has already been range checked/validated. diff --git a/price/src/lib.rs b/price/src/lib.rs index bf2fec2d..a45d0b32 100644 --- a/price/src/lib.rs +++ b/price/src/lib.rs @@ -19,9 +19,11 @@ const PRICE_MANTISSA_BITS: u8 = 27; /// The number of exponent bits is simply the remaining bits in a u32 after storing the price /// mantissa bits. const EXPONENT_BITS: u8 = U32_BITS - PRICE_MANTISSA_BITS; + #[allow(dead_code)] -/// The full range of valid biased exponent values. That is, 0 <= biased_exponent <= EXPONENT_RANGE. -const EXPONENT_RANGE: u8 = (1 << (EXPONENT_BITS)) - 1; +/// The max biased exponent. This also determines the range of valid exponents. +/// I.e., 0 <= biased_exponent <= [`MAX_BIASED_EXPONENT`]. +const MAX_BIASED_EXPONENT: u8 = (1 << (EXPONENT_BITS)) - 1; /// [`BIAS`] is the number that satisfies: `BIAS + SMALLEST_POSSIBLE_EXPONENT == 0`. /// That is, if the exponent range is 32 values from -16 <= n <= 15, the smallest possible exponent @@ -38,8 +40,6 @@ const UNBIASED_MIN: i16 = 0 - BIAS as i16; #[cfg(test)] const UNBIASED_MAX: i16 = (BIAS as i16) - 1; -#[allow(dead_code)] -const MAX_BIASED_EXPONENT: u8 = 31; // Ensure that adding the bias to the max biased exponent never overflows. static_assertions::const_assert!((MAX_BIASED_EXPONENT as u16) + (BIAS as u16) < (u8::MAX as u16)); @@ -185,7 +185,7 @@ mod tests { assert_eq!(calculated, expected); let val: u64 = 156; - let max_exponent = EXPONENT_RANGE as u32; + let max_exponent = MAX_BIASED_EXPONENT as u32; let calculated = val * 10u64 .checked_pow(max_exponent - BIAS as u32) From ebf2d85001071d1252097dd12e47d71b0631b1c3 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Fri, 5 Dec 2025 19:04:40 -0800 Subject: [PATCH 22/47] Add a test that ensures the order info construction fails if the quote exponent is too large --- price/src/error.rs | 2 -- price/src/lib.rs | 21 +++++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/price/src/error.rs b/price/src/error.rs index 6d52d300..d4a7e03a 100644 --- a/price/src/error.rs +++ b/price/src/error.rs @@ -2,8 +2,6 @@ #[derive(Debug)] #[cfg_attr(test, derive(strum_macros::Display))] pub enum OrderInfoError { - InvalidBaseExponent, - InvalidQuoteExponent, ExponentUnderflow, ArithmeticOverflow, InvalidPriceMantissa, diff --git a/price/src/lib.rs b/price/src/lib.rs index a45d0b32..951c5db2 100644 --- a/price/src/lib.rs +++ b/price/src/lib.rs @@ -88,6 +88,7 @@ pub fn to_order_info( base_scalar, OrderInfoError::ArithmeticOverflow )?; + let quote_atoms = pow10_u64!(price_mantissa_times_base_scalar, quote_exponent_biased)?; // Ultimately, the price mantissa is multiplied by: @@ -98,6 +99,7 @@ pub fn to_order_info( let price_exponent_rebiased = checked_sub!( // Safety: The quote exponent must be <= MAX_BIASED_EXPONENT, and const assertions ensure // that `MAX_BIASED_EXPONENT + BIAS` is always less than `u8::MAX`. + // Unit tests also guarantee this invariant. unsafe { quote_exponent_biased.unchecked_add(BIAS) }, base_exponent_biased, OrderInfoError::ExponentUnderflow @@ -197,4 +199,23 @@ mod tests { ); assert_eq!(calculated, expected); } + + #[test] + fn ensure_invalid_quote_exponent_fails() { + let e_base = to_biased_exponent!(0); + let e_quote = MAX_BIASED_EXPONENT + 1; + + // Ensure the base exponent is valid so that it can't be the trigger for the error. + let _one_to_the_base_exponent = pow10_u64!(1u64, e_base).unwrap(); + + let all_good = to_order_info(10_000_000, 1, e_base, e_base); + let arithmetic_overflow = to_order_info(10_000_000, 1, e_base, e_quote - 1); + let invalid_biased_exponent = to_order_info(10_000_000, 1, e_base, e_quote); + + assert!(all_good.is_ok()); + #[rustfmt::skip] + assert!(matches!(arithmetic_overflow, Err(OrderInfoError::ArithmeticOverflow))); + #[rustfmt::skip] + assert!(matches!(invalid_biased_exponent, Err(OrderInfoError::InvalidBiasedExponent))); + } } From 2a8b15d0bdc0e8d3ba76c8a3d0b465460f7bd0e6 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Mon, 8 Dec 2025 16:46:52 -0800 Subject: [PATCH 23/47] Update bitshift comment --- price/src/encoded_price.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/price/src/encoded_price.rs b/price/src/encoded_price.rs index 7db5055c..84b8992f 100644 --- a/price/src/encoded_price.rs +++ b/price/src/encoded_price.rs @@ -24,8 +24,8 @@ impl EncodedPrice { /// Creates a new [`EncodedPrice`] from a biased price exponent and a validated price mantissa. #[inline(always)] pub fn new(price_exponent_biased: u8, price_mantissa: ValidatedPriceMantissa) -> Self { - // The biased price exponent doesn't need to be checked, because the bit shift will always - // truncate to a value <= the max possible exponent value. + // The biased price exponent doesn't need to be checked because a leftwards bitshift will + // always discard irrelevant bits. let exponent_bits = (price_exponent_biased as u32) << PRICE_MANTISSA_BITS; // No need to mask the price mantissa since it has already been range checked/validated. From 5abfbcd517bfd3d1977009fa097b452ba5fbd0a6 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Mon, 8 Dec 2025 16:47:59 -0800 Subject: [PATCH 24/47] Update decoded price documentation --- price/src/decoded_price.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/price/src/decoded_price.rs b/price/src/decoded_price.rs index 2b1f0cd2..6815cbd1 100644 --- a/price/src/decoded_price.rs +++ b/price/src/decoded_price.rs @@ -9,6 +9,7 @@ use crate::{ PRICE_MANTISSA_MASK, }; +/// An enum representing a decoded `EncodedPrice`. #[derive(Clone)] #[cfg_attr(test, derive(Debug))] pub enum DecodedPrice { From 20e04febc090ff9889feb75931a3be44ea1d78be Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Mon, 8 Dec 2025 16:52:26 -0800 Subject: [PATCH 25/47] Remove `new_unchecked` --- price/src/decoded_price.rs | 14 +++++++++----- price/src/lib.rs | 8 +++++--- price/src/validated_mantissa.rs | 9 --------- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/price/src/decoded_price.rs b/price/src/decoded_price.rs index 6815cbd1..b6dc7c32 100644 --- a/price/src/decoded_price.rs +++ b/price/src/decoded_price.rs @@ -37,9 +37,11 @@ impl DecodedPrice { } } -impl From for DecodedPrice { - fn from(encoded: EncodedPrice) -> Self { - match encoded.get() { +impl TryFrom for DecodedPrice { + type Error = OrderInfoError; + + fn try_from(encoded: EncodedPrice) -> Result { + let res = match encoded.get() { ENCODED_PRICE_ZERO => Self::Zero, ENCODED_PRICE_INFINITY => Self::Infinity, value => { @@ -48,10 +50,12 @@ impl From for DecodedPrice { Self::ExponentAndMantissa { price_exponent_biased, - price_mantissa: ValidatedPriceMantissa::new_unchecked(validated_mantissa), + price_mantissa: ValidatedPriceMantissa::try_from(validated_mantissa)?, } } - } + }; + + Ok(res) } } diff --git a/price/src/lib.rs b/price/src/lib.rs index 951c5db2..cb26c212 100644 --- a/price/src/lib.rs +++ b/price/src/lib.rs @@ -129,7 +129,8 @@ mod tests { assert_eq!(order.base_atoms, 1); assert_eq!(order.quote_atoms, 1234); - let decoded_price: f64 = DecodedPrice::from(order.encoded_price) + let decoded_price: f64 = DecodedPrice::try_from(order.encoded_price) + .expect("Should decode") .try_into() .expect("Should be a valid f64"); assert_eq!(decoded_price, "1234".parse().unwrap()); @@ -142,7 +143,8 @@ mod tests { assert_eq!(order.base_atoms, 1); assert_eq!(order.quote_atoms, 12345678); - let decoded_price: f64 = DecodedPrice::from(order.encoded_price) + let decoded_price: f64 = DecodedPrice::try_from(order.encoded_price) + .expect("Should decode") .try_into() .expect("Should be a valid f64"); assert_eq!(decoded_price, "12345678".parse().unwrap()); @@ -156,7 +158,7 @@ mod tests { assert_eq!(order.quote_atoms, 12345678); assert_eq!(order.base_atoms, 100000000); - let decoded_price = DecodedPrice::from(order.encoded_price); + let decoded_price = DecodedPrice::try_from(order.encoded_price).expect("Should decode"); let (decoded_exponent, decoded_mantissa) = decoded_price .as_exponent_and_mantissa() diff --git a/price/src/validated_mantissa.rs b/price/src/validated_mantissa.rs index f230ca64..ad47ec43 100644 --- a/price/src/validated_mantissa.rs +++ b/price/src/validated_mantissa.rs @@ -24,15 +24,6 @@ impl TryFrom for ValidatedPriceMantissa { } } -impl ValidatedPriceMantissa { - /// Creates a new [`ValidatedPriceMantissa`] without range checking the passed value. - /// This should only be used when the price mantissa has definitively already been validated. - #[inline(always)] - pub(crate) fn new_unchecked(price_mantissa: u32) -> Self { - Self(price_mantissa) - } -} - impl ValidatedPriceMantissa { /// Returns the validated price mantissa as a u32. #[inline(always)] From c2a87d77f77a191712abe66d7e0d50e26563b107 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Mon, 8 Dec 2025 16:55:51 -0800 Subject: [PATCH 26/47] Make mantissa constant bounds public --- price/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/price/src/lib.rs b/price/src/lib.rs index cb26c212..7c5bd9df 100644 --- a/price/src/lib.rs +++ b/price/src/lib.rs @@ -9,8 +9,8 @@ pub use encoded_price::*; pub use error::*; pub use validated_mantissa::*; -const MANTISSA_DIGITS_LOWER_BOUND: u32 = 10_000_000; -const MANTISSA_DIGITS_UPPER_BOUND: u32 = 99_999_999; +pub const MANTISSA_DIGITS_LOWER_BOUND: u32 = 10_000_000; +pub const MANTISSA_DIGITS_UPPER_BOUND: u32 = 99_999_999; const U32_BITS: u8 = 32; const PRICE_MANTISSA_BITS: u8 = 27; From 749808d740f8919d748ab4b66c22754e87e868d3 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Mon, 8 Dec 2025 17:20:25 -0800 Subject: [PATCH 27/47] Add documentation for `pow10_u64` --- price/src/lib.rs | 4 ++-- price/src/macros.rs | 31 ++++++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/price/src/lib.rs b/price/src/lib.rs index 7c5bd9df..a3d07d5e 100644 --- a/price/src/lib.rs +++ b/price/src/lib.rs @@ -15,14 +15,14 @@ pub const MANTISSA_DIGITS_UPPER_BOUND: u32 = 99_999_999; const U32_BITS: u8 = 32; const PRICE_MANTISSA_BITS: u8 = 27; -#[allow(dead_code)] /// The number of exponent bits is simply the remaining bits in a u32 after storing the price /// mantissa bits. +#[allow(dead_code)] const EXPONENT_BITS: u8 = U32_BITS - PRICE_MANTISSA_BITS; -#[allow(dead_code)] /// The max biased exponent. This also determines the range of valid exponents. /// I.e., 0 <= biased_exponent <= [`MAX_BIASED_EXPONENT`]. +#[allow(dead_code)] const MAX_BIASED_EXPONENT: u8 = (1 << (EXPONENT_BITS)) - 1; /// [`BIAS`] is the number that satisfies: `BIAS + SMALLEST_POSSIBLE_EXPONENT == 0`. diff --git a/price/src/macros.rs b/price/src/macros.rs index df0b096d..69db68ac 100644 --- a/price/src/macros.rs +++ b/price/src/macros.rs @@ -1,11 +1,40 @@ +use static_assertions::const_assert_eq; + /// Macro utility for calculating the value of an operation given a biased exponent, where a biased /// exponent represents the base 10 positive or negative exponent value without using negative /// values. + +// Static assertions for macro invariants. +static_assertions::const_assert_eq!(crate::BIAS - 16, 0); +static_assertions::const_assert_eq!(crate::MAX_BIASED_EXPONENT, 31); + +/// Documentation for `pow10_64` relies on [`BIAS`] == 16. If that changes, this and the +/// documentation needs to be updated. +const _: () = { + const_assert_eq!(crate::BIAS, 16); +}; + +/// Performs base-10 exponentiation on a value using a biased exponent. +/// +/// # Parameters +/// - `$value`: The `u64` to be scaled by a power of 10. +/// - `$biased_exponent`: A biased exponent in the range `0..=31`. +/// +/// # Biased Exponent Concept +/// The actual exponent is: +/// +/// `exponent = $biased_exponent - BIAS` +/// +/// With the current `BIAS = 16`, this means: +/// - `0` → exponent `-16` (division by 10^16) +/// - `16` → exponent `0` (multiplication by 1 aka 10^0) +/// - `31` → exponent `+15` (multiplication by 10^15) +/// +/// Errors on an invalid biased exponent or on arithmetic overflow. #[macro_export] #[rustfmt::skip] macro_rules! pow10_u64 { ($value:expr, $biased_exponent:expr) => {{ - ::static_assertions::const_assert_eq!($crate::BIAS - 16, 0); match $biased_exponent { /* BIAS - 16 */ 0 => Ok($value / 10000000000000000u64), /* BIAS - 15 */ 1 => Ok($value / 1000000000000000), From 534d2185c6f1e7606f1cea87e54f8a25f39298c8 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Mon, 8 Dec 2025 17:26:52 -0800 Subject: [PATCH 28/47] Fix documentation being below attributes --- price/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/price/src/lib.rs b/price/src/lib.rs index a3d07d5e..bdb1a3b0 100644 --- a/price/src/lib.rs +++ b/price/src/lib.rs @@ -56,8 +56,8 @@ mod debug_assertions { // used to ensure a fixed number of digits for the price mantissa. const_assert!(MANTISSA_DIGITS_UPPER_BOUND < PRICE_MANTISSA_MASK); - #[allow(dead_code)] /// The bitmask for the price exponent calculated from the number of bits in the price mantissa. + #[allow(dead_code)] pub const PRICE_EXPONENT_MASK: u32 = u32::MAX << (PRICE_MANTISSA_BITS as usize); // XOR'ing the price exponent and mantissa bit masks should result in a u32 with all 1 bits, From fe7118f50a9f042fca08f23401c16d472252a248 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Mon, 8 Dec 2025 18:03:25 -0800 Subject: [PATCH 29/47] Add `OrderInfo` documentation --- price/src/lib.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/price/src/lib.rs b/price/src/lib.rs index bdb1a3b0..31b7a5d5 100644 --- a/price/src/lib.rs +++ b/price/src/lib.rs @@ -65,11 +65,19 @@ mod debug_assertions { const_assert_eq!(PRICE_EXPONENT_MASK ^ PRICE_MANTISSA_MASK, u32::MAX); } +/// The fixed struct layout for information about a `dropset` order. +/// +/// This struct is a C-style struct to facilitate a predictable, fixed layout for on-chain function +/// calls related to `dropset` orders. #[repr(C)] #[cfg_attr(test, derive(Debug))] pub struct OrderInfo { + /// The encoded price, containing an exponent and price mantissa. + /// See [`EncodedPrice`] for more details. pub encoded_price: EncodedPrice, + /// The indivisible units (aka atoms) of base token. pub base_atoms: u64, + /// The indivisible units (aka atoms) of quote token. pub quote_atoms: u64, } From 5261c36913796d548743b35b2dea14b6947fabe8 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Mon, 8 Dec 2025 18:08:09 -0800 Subject: [PATCH 30/47] Update const assertion to use `<=`, update documentation to reflect that, update wording on unit test condition check --- price/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/price/src/lib.rs b/price/src/lib.rs index 31b7a5d5..f02ca8c7 100644 --- a/price/src/lib.rs +++ b/price/src/lib.rs @@ -41,7 +41,7 @@ const UNBIASED_MIN: i16 = 0 - BIAS as i16; const UNBIASED_MAX: i16 = (BIAS as i16) - 1; // Ensure that adding the bias to the max biased exponent never overflows. -static_assertions::const_assert!((MAX_BIASED_EXPONENT as u16) + (BIAS as u16) < (u8::MAX as u16)); +static_assertions::const_assert!((MAX_BIASED_EXPONENT as u16) + (BIAS as u16) <= (u8::MAX as u16)); /// The bitmask for the price mantissa calculated from the number of bits it uses. pub const PRICE_MANTISSA_MASK: u32 = u32::MAX >> ((U32_BITS - PRICE_MANTISSA_BITS) as usize); @@ -106,8 +106,8 @@ pub fn to_order_info( // re-biased exponent difference is negative. let price_exponent_rebiased = checked_sub!( // Safety: The quote exponent must be <= MAX_BIASED_EXPONENT, and const assertions ensure - // that `MAX_BIASED_EXPONENT + BIAS` is always less than `u8::MAX`. - // Unit tests also guarantee this invariant. + // that `MAX_BIASED_EXPONENT + BIAS` is always <= `u8::MAX`. + // Unit tests also check this condition. unsafe { quote_exponent_biased.unchecked_add(BIAS) }, base_exponent_biased, OrderInfoError::ExponentUnderflow From 9a5680a0fdf007e33dc1d494f5cf546744f88cf4 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Mon, 8 Dec 2025 18:57:08 -0800 Subject: [PATCH 31/47] Update macro documentation to pass cargo test and be more descriptive --- price/src/macros.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/price/src/macros.rs b/price/src/macros.rs index 69db68ac..23bdd404 100644 --- a/price/src/macros.rs +++ b/price/src/macros.rs @@ -1,9 +1,5 @@ use static_assertions::const_assert_eq; -/// Macro utility for calculating the value of an operation given a biased exponent, where a biased -/// exponent represents the base 10 positive or negative exponent value without using negative -/// values. - // Static assertions for macro invariants. static_assertions::const_assert_eq!(crate::BIAS - 16, 0); static_assertions::const_assert_eq!(crate::MAX_BIASED_EXPONENT, 31); @@ -15,15 +11,18 @@ const _: () = { }; /// Performs base-10 exponentiation on a value using a biased exponent. +/// +/// This facilitates representing negative exponent values with unsigned integers by ensuring the +/// biased exponent is never negative. The unbiased exponent is therefore the real exponent value. /// /// # Parameters /// - `$value`: The `u64` to be scaled by a power of 10. /// - `$biased_exponent`: A biased exponent in the range `0..=31`. /// /// # Biased Exponent Concept -/// The actual exponent is: +/// The actual (aka unbiased) exponent is: /// -/// `exponent = $biased_exponent - BIAS` +/// `exponent = $biased_exponent - price::BIAS` /// /// With the current `BIAS = 16`, this means: /// - `0` → exponent `-16` (division by 10^16) From 2df108482ed3b2dafc17b57b41b98951e0215bba Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Mon, 8 Dec 2025 18:57:36 -0800 Subject: [PATCH 32/47] Add test for price mantissa * base scalar results in arithmetic overflow properly --- price/src/lib.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/price/src/lib.rs b/price/src/lib.rs index f02ca8c7..9961f640 100644 --- a/price/src/lib.rs +++ b/price/src/lib.rs @@ -210,6 +210,29 @@ mod tests { assert_eq!(calculated, expected); } + #[test] + fn ensure_price_mantissa_times_base_scalar_arithmetic_overflow() { + const PRICE_MANTISSA: u32 = 10_000_000; + + assert!(to_order_info( + PRICE_MANTISSA, + u64::MAX / PRICE_MANTISSA as u64, + to_biased_exponent!(0), + to_biased_exponent!(0), + ) + .is_ok()); + + assert!(matches!( + to_order_info( + PRICE_MANTISSA + 1, + u64::MAX / PRICE_MANTISSA as u64, + to_biased_exponent!(0), + to_biased_exponent!(0) + ), + Err(OrderInfoError::ArithmeticOverflow) + )); + } + #[test] fn ensure_invalid_quote_exponent_fails() { let e_base = to_biased_exponent!(0); From 118c257545282d529a84c1a6530f37d0d02cbe2d Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Mon, 8 Dec 2025 19:17:56 -0800 Subject: [PATCH 33/47] Add explicit exponent underflow unit test --- price/src/lib.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/price/src/lib.rs b/price/src/lib.rs index 9961f640..0a9fa8d3 100644 --- a/price/src/lib.rs +++ b/price/src/lib.rs @@ -102,8 +102,10 @@ pub fn to_order_info( // Ultimately, the price mantissa is multiplied by: // 10 ^ (quote_exponent_biased - base_exponent_biased) // aka 10 ^ (q - b) - // which means q - b may be negative and must be re-biased. Underflow only occurs if the - // re-biased exponent difference is negative. + // which means q - b may be negative and must be re-biased. + // + // Exponent underflow only occurs here if: + // `quote_exponent_biased + BIAS < base_exponent_biased`. let price_exponent_rebiased = checked_sub!( // Safety: The quote exponent must be <= MAX_BIASED_EXPONENT, and const assertions ensure // that `MAX_BIASED_EXPONENT + BIAS` is always <= `u8::MAX`. @@ -233,6 +235,19 @@ mod tests { )); } + #[test] + fn ensure_exponent_underflow() { + let price_mantissa = 10_000_000; + let base_scalar = 1; + + assert!(to_order_info(price_mantissa, base_scalar, BIAS, 0).is_ok()); + + assert!(matches!( + to_order_info(price_mantissa, base_scalar, BIAS + 1, 0), + Err(OrderInfoError::ExponentUnderflow) + )); + } + #[test] fn ensure_invalid_quote_exponent_fails() { let e_base = to_biased_exponent!(0); From d6c5f2295ab1bf1b8debe40ab2adaf7aa1b3a783 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Mon, 8 Dec 2025 19:18:18 -0800 Subject: [PATCH 34/47] Use `matches` with the exact error type for `invalid_mantissas` unit test, instead of `is_err()` --- price/src/validated_mantissa.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/price/src/validated_mantissa.rs b/price/src/validated_mantissa.rs index ad47ec43..c982bda6 100644 --- a/price/src/validated_mantissa.rs +++ b/price/src/validated_mantissa.rs @@ -52,7 +52,13 @@ mod tests { #[test] fn invalid_mantissas() { - assert!(ValidatedPriceMantissa::try_from(MANTISSA_DIGITS_LOWER_BOUND - 1).is_err()); - assert!(ValidatedPriceMantissa::try_from(MANTISSA_DIGITS_UPPER_BOUND + 1).is_err()); + assert!(matches!( + ValidatedPriceMantissa::try_from(MANTISSA_DIGITS_LOWER_BOUND - 1), + Err(OrderInfoError::InvalidPriceMantissa) + )); + assert!(matches!( + ValidatedPriceMantissa::try_from(MANTISSA_DIGITS_UPPER_BOUND + 1), + Err(OrderInfoError::InvalidPriceMantissa) + )); } } From 6df233668a6062b35c0eb15d181038ca8c89ef50 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Mon, 8 Dec 2025 19:29:38 -0800 Subject: [PATCH 35/47] Fix comment variable names --- price/src/macros.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/price/src/macros.rs b/price/src/macros.rs index 23bdd404..0fe193b6 100644 --- a/price/src/macros.rs +++ b/price/src/macros.rs @@ -4,7 +4,7 @@ use static_assertions::const_assert_eq; static_assertions::const_assert_eq!(crate::BIAS - 16, 0); static_assertions::const_assert_eq!(crate::MAX_BIASED_EXPONENT, 31); -/// Documentation for `pow10_64` relies on [`BIAS`] == 16. If that changes, this and the +/// Documentation for [`pow10_u64`] relies on [`crate::BIAS`] == 16. If that changes, this and the /// documentation needs to be updated. const _: () = { const_assert_eq!(crate::BIAS, 16); From 5e881c4bbee9b4f3030cad180eaccb1d7f783e5b Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Mon, 8 Dec 2025 19:32:32 -0800 Subject: [PATCH 36/47] Fix `price_bits` var naming, update to `mantissa_bits` --- price/src/encoded_price.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/price/src/encoded_price.rs b/price/src/encoded_price.rs index 84b8992f..efa9aec9 100644 --- a/price/src/encoded_price.rs +++ b/price/src/encoded_price.rs @@ -6,14 +6,15 @@ use crate::{ #[derive(Copy, Clone, Debug)] /// The encoded price as a u32. /// -/// If `N` = the number of exponent bits and `M` = the number of price bits, the u32 bit layout is: +/// If `N` = the number of exponent bits and `M` = the number of price mantissa bits, the u32 bit +/// layout is: /// /// ```text /// N M -/// |-----------------|--------------| -/// [ exponent_bits ] | [ price_bits ] -/// |--------------------------------| -/// 32 +/// |-------------------|-------------------| +/// [ exponent_bits ] | [ mantissa_bits ] +/// |---------------------------------------| +/// 32 /// ``` pub struct EncodedPrice(u32); @@ -76,7 +77,7 @@ mod tests { }; #[test] - fn encoded_price_bits() { + fn encoded_price_mantissa_bits() { let exponent = 0b0_1111; let price_mantissa = 0b000_1111_0000_1111_0000_1111_0000; let encoded_price = EncodedPrice::new( From fa3308f8138e11131739bcaedcdd8bcf01df0dd4 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Tue, 9 Dec 2025 15:28:37 -0800 Subject: [PATCH 37/47] Clarify documentation on the exponent range; make formula for `UNBIASED_MAX` clearer --- price/src/lib.rs | 12 +++++++----- price/src/macros.rs | 40 +++++++++++++++++++++++++++++++++++++--- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/price/src/lib.rs b/price/src/lib.rs index 0a9fa8d3..4dde047c 100644 --- a/price/src/lib.rs +++ b/price/src/lib.rs @@ -26,11 +26,13 @@ const EXPONENT_BITS: u8 = U32_BITS - PRICE_MANTISSA_BITS; const MAX_BIASED_EXPONENT: u8 = (1 << (EXPONENT_BITS)) - 1; /// [`BIAS`] is the number that satisfies: `BIAS + SMALLEST_POSSIBLE_EXPONENT == 0`. -/// That is, if the exponent range is 32 values from -16 <= n <= 15, the smallest possible exponent +/// It facilitates the expression of negative exponents with only unsigned integers. +/// +/// The exponent range is 32 values from -16 <= n <= 15 and the smallest possible exponent /// is -16, so the BIAS must be 16. -/// Note the decision to use a larger negative range instead of a larger positive range (i.e., -/// [-16, 15] instead of [-15, 16]) is because [-16, 15] has a tighter range in terms of the -/// difference in orders of magnitude for the smallest and largest exponent values. +/// +/// See [`pow10_u64`] for more information on the reasoning behind the exponent range. +/// ``` pub const BIAS: u8 = 16; /// The minimum unbiased exponent value. @@ -38,7 +40,7 @@ pub const BIAS: u8 = 16; const UNBIASED_MIN: i16 = 0 - BIAS as i16; /// The maximum unbiased exponent value. #[cfg(test)] -const UNBIASED_MAX: i16 = (BIAS as i16) - 1; +const UNBIASED_MAX: i16 = MAX_BIASED_EXPONENT as i16 - BIAS as i16; // Ensure that adding the bias to the max biased exponent never overflows. static_assertions::const_assert!((MAX_BIASED_EXPONENT as u16) + (BIAS as u16) <= (u8::MAX as u16)); diff --git a/price/src/macros.rs b/price/src/macros.rs index 0fe193b6..e7c502e7 100644 --- a/price/src/macros.rs +++ b/price/src/macros.rs @@ -4,8 +4,8 @@ use static_assertions::const_assert_eq; static_assertions::const_assert_eq!(crate::BIAS - 16, 0); static_assertions::const_assert_eq!(crate::MAX_BIASED_EXPONENT, 31); -/// Documentation for [`pow10_u64`] relies on [`crate::BIAS`] == 16. If that changes, this and the -/// documentation needs to be updated. +/// Documentation for [`pow10_u64`] relies on [`crate::BIAS`] == 16. If that changes, +/// [`crate::BIAS`] and the [`pow10_u64`] documentation needs to be updated. const _: () = { const_assert_eq!(crate::BIAS, 16); }; @@ -29,7 +29,41 @@ const _: () = { /// - `16` → exponent `0` (multiplication by 1 aka 10^0) /// - `31` → exponent `+15` (multiplication by 10^15) /// -/// Errors on an invalid biased exponent or on arithmetic overflow. +/// The code output from the macro will error on an invalid biased exponent or arithmetic overflow. +/// +/// # Reasoning behind exponent range +/// +/// The decision to use a larger negative range instead of a larger positive range is because +/// a larger negative range results in the price mantissa * exponent product forming in a tighter +/// range around `1`. +/// +/// For example, with `[-2, 1] vs [-1, 2]`: +/// +/// ```markdown +/// # With [-2, 1] as the smallest/largest exponents +/// | | Smallest exponent | Largest exponent | +/// | -------------------- | ----------------------------------------- | +/// | Smallest mantissa | 1.00 * 10^-2 = 0.01 | 1.00 * 10^1 = 10 | +/// | Largest mantissa | 9.99 * 10^-2 = ~0.1 | 9.99 * 10^1 = ~100 | +/// | -------------------- | ----------------------------------------- | +/// +/// Both the smallest and largest products (0.01 and 100) are 2 orders +/// of magnitude below/above `1`. +/// +/// # With [-1, 2] as the smallest/largest exponents +/// | | Smallest exponent | Largest exponent | +/// | -------------------- | ----------------------------------------- | +/// | Smallest mantissa | 1.00 * 10^-1 = 0.1 | 1.00 * 10^2 = 100 | +/// | Largest mantissa | 9.99 * 10^-1 = ~1 | 9.99 * 10^2 = ~1000 | +/// | -------------------- | ----------------------------------------- | +/// +/// The lower product (0.1) is 1 order of magnitude below `1` and the higher +/// product (1000) is 3 orders of magnitude above `1`. +/// +/// The first option is preferable because it offers a more dynamic, +/// symmetrical range in terms of orders of magnitude below/above `1`. +/// +/// Therefore, [-16, 15] is used as the exponent range instead of [-15, 16]. #[macro_export] #[rustfmt::skip] macro_rules! pow10_u64 { From 756077b9b8310199fdacfbc6f3eb1ed9cb83be68 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Tue, 9 Dec 2025 16:00:40 -0800 Subject: [PATCH 38/47] Make safety expectation on unchecked add with unit test condition check clearer in documentation for `to_order_info` --- price/src/lib.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/price/src/lib.rs b/price/src/lib.rs index 4dde047c..e74e5d25 100644 --- a/price/src/lib.rs +++ b/price/src/lib.rs @@ -83,6 +83,14 @@ pub struct OrderInfo { pub quote_atoms: u64, } +/// # Safety note: +/// +/// In the rebiased exponent calculation, there is an unchecked add that is actually safe. It's +/// safe because the prior function body ensures the quote exponent is <= MAX_BIASED_EXPONENT, and +/// const assertions ensure that `MAX_BIASED_EXPONENT + BIAS` is always <= `u8::MAX`. +/// +/// [`tests::ensure_invalid_quote_exponent_fails_early`] ensures that the function fails early if +/// the quote exponent isn't <= MAX_BIASED_EXPONENT prior to the unchecked add. pub fn to_order_info( price_mantissa: u32, base_scalar: u64, @@ -109,9 +117,7 @@ pub fn to_order_info( // Exponent underflow only occurs here if: // `quote_exponent_biased + BIAS < base_exponent_biased`. let price_exponent_rebiased = checked_sub!( - // Safety: The quote exponent must be <= MAX_BIASED_EXPONENT, and const assertions ensure - // that `MAX_BIASED_EXPONENT + BIAS` is always <= `u8::MAX`. - // Unit tests also check this condition. + // Safety: See the function documentation. unsafe { quote_exponent_biased.unchecked_add(BIAS) }, base_exponent_biased, OrderInfoError::ExponentUnderflow @@ -251,7 +257,7 @@ mod tests { } #[test] - fn ensure_invalid_quote_exponent_fails() { + pub(crate) fn ensure_invalid_quote_exponent_fails_early() { let e_base = to_biased_exponent!(0); let e_quote = MAX_BIASED_EXPONENT + 1; From 24fcbad4fb33f44e26a6146d4f71081ed12e0cb9 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Tue, 9 Dec 2025 16:06:41 -0800 Subject: [PATCH 39/47] Add `base_exponent_too_large` explicit unit test for `to_order_info` Update unit test name to max and max plus one base --- price/src/lib.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/price/src/lib.rs b/price/src/lib.rs index e74e5d25..fc0cfe5f 100644 --- a/price/src/lib.rs +++ b/price/src/lib.rs @@ -274,4 +274,22 @@ mod tests { #[rustfmt::skip] assert!(matches!(invalid_biased_exponent, Err(OrderInfoError::InvalidBiasedExponent))); } + + #[test] + fn max_and_max_plus_one_base() { + let e_base = MAX_BIASED_EXPONENT; + let e_quote = to_biased_exponent!(0); + + // Ensure the quote exponent is valid so that it can't be the trigger for the error. + let _one_to_the_quote_expoent = pow10_u64!(1u64, e_quote).unwrap(); + + let all_good = to_order_info(10_000_000, 1, e_base, e_quote); + let invalid_quote_exponent = to_order_info(10_000_000, 1, e_base + 1, e_quote); + + assert!(all_good.is_ok()); + assert!(matches!( + invalid_quote_exponent, + Err(OrderInfoError::InvalidBiasedExponent) + )); + } } From 8ec535d9b77d96b7f40443396119163e98be64bc Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Tue, 9 Dec 2025 16:11:38 -0800 Subject: [PATCH 40/47] Derive `Copy` for validated mantissa --- price/src/validated_mantissa.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/price/src/validated_mantissa.rs b/price/src/validated_mantissa.rs index c982bda6..8f6f04fa 100644 --- a/price/src/validated_mantissa.rs +++ b/price/src/validated_mantissa.rs @@ -6,7 +6,7 @@ use crate::{ MANTISSA_DIGITS_UPPER_BOUND, }; -#[derive(Clone)] +#[derive(Clone, Copy)] #[cfg_attr(test, derive(Debug))] pub struct ValidatedPriceMantissa(u32); From 44dd318fe0baa55f217649bbea53d309344e2b49 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Tue, 9 Dec 2025 16:14:23 -0800 Subject: [PATCH 41/47] Fix `pow10_u64` macro formatting --- price/src/macros.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/price/src/macros.rs b/price/src/macros.rs index e7c502e7..63ea0f3c 100644 --- a/price/src/macros.rs +++ b/price/src/macros.rs @@ -42,20 +42,20 @@ const _: () = { /// ```markdown /// # With [-2, 1] as the smallest/largest exponents /// | | Smallest exponent | Largest exponent | -/// | -------------------- | ----------------------------------------- | +/// | -------------------- | ------------------- | ------------------- | /// | Smallest mantissa | 1.00 * 10^-2 = 0.01 | 1.00 * 10^1 = 10 | /// | Largest mantissa | 9.99 * 10^-2 = ~0.1 | 9.99 * 10^1 = ~100 | -/// | -------------------- | ----------------------------------------- | +/// | -------------------- | -------------------|--------------------- | /// /// Both the smallest and largest products (0.01 and 100) are 2 orders /// of magnitude below/above `1`. /// /// # With [-1, 2] as the smallest/largest exponents -/// | | Smallest exponent | Largest exponent | -/// | -------------------- | ----------------------------------------- | +/// | | Smallest exponent | Largest exponent | +/// | -------------------- | ------------------ | -------------------- | /// | Smallest mantissa | 1.00 * 10^-1 = 0.1 | 1.00 * 10^2 = 100 | /// | Largest mantissa | 9.99 * 10^-1 = ~1 | 9.99 * 10^2 = ~1000 | -/// | -------------------- | ----------------------------------------- | +/// | -------------------- | -------------------|--------------------- | /// /// The lower product (0.1) is 1 order of magnitude below `1` and the higher /// product (1000) is 3 orders of magnitude above `1`. From 3f1e5cc99ba9ddb183c201452e1d65c37a4adbd4 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Wed, 10 Dec 2025 18:01:28 -0800 Subject: [PATCH 42/47] Make `EncodedPrice` repr transparent --- price/src/encoded_price.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/price/src/encoded_price.rs b/price/src/encoded_price.rs index efa9aec9..81abb3df 100644 --- a/price/src/encoded_price.rs +++ b/price/src/encoded_price.rs @@ -3,7 +3,6 @@ use crate::{ PRICE_MANTISSA_BITS, }; -#[derive(Copy, Clone, Debug)] /// The encoded price as a u32. /// /// If `N` = the number of exponent bits and `M` = the number of price mantissa bits, the u32 bit @@ -16,6 +15,8 @@ use crate::{ /// |---------------------------------------| /// 32 /// ``` +#[repr(transparent)] +#[derive(Copy, Clone, Debug)] pub struct EncodedPrice(u32); pub const ENCODED_PRICE_INFINITY: u32 = u32::MAX; From 35f5b29176fbccafc19115b0a6a0ebeb2b1dbc7b Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Thu, 11 Dec 2025 15:45:52 -0800 Subject: [PATCH 43/47] Remove trailing whitespace in macros.rs --- price/src/macros.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/price/src/macros.rs b/price/src/macros.rs index 63ea0f3c..43ce068b 100644 --- a/price/src/macros.rs +++ b/price/src/macros.rs @@ -11,7 +11,7 @@ const _: () = { }; /// Performs base-10 exponentiation on a value using a biased exponent. -/// +/// /// This facilitates representing negative exponent values with unsigned integers by ensuring the /// biased exponent is never negative. The unbiased exponent is therefore the real exponent value. /// @@ -28,11 +28,11 @@ const _: () = { /// - `0` → exponent `-16` (division by 10^16) /// - `16` → exponent `0` (multiplication by 1 aka 10^0) /// - `31` → exponent `+15` (multiplication by 10^15) -/// +/// /// The code output from the macro will error on an invalid biased exponent or arithmetic overflow. -/// +/// /// # Reasoning behind exponent range -/// +/// /// The decision to use a larger negative range instead of a larger positive range is because /// a larger negative range results in the price mantissa * exponent product forming in a tighter /// range around `1`. @@ -62,7 +62,7 @@ const _: () = { /// /// The first option is preferable because it offers a more dynamic, /// symmetrical range in terms of orders of magnitude below/above `1`. -/// +/// /// Therefore, [-16, 15] is used as the exponent range instead of [-15, 16]. #[macro_export] #[rustfmt::skip] From 5a9db217e57ff09ebbd2dadf1caf2d47095c3bfa Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Thu, 11 Dec 2025 15:48:38 -0800 Subject: [PATCH 44/47] Remove trailing whitespace in other files --- .../src/render/try_from_tag_macro.rs | 2 +- instruction-macros/crates/test-fixtures/src/client.rs | 6 +++--- interface/src/events/mod.rs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/instruction-macros/crates/instruction-macros-impl/src/render/try_from_tag_macro.rs b/instruction-macros/crates/instruction-macros-impl/src/render/try_from_tag_macro.rs index 04338232..ec125601 100644 --- a/instruction-macros/crates/instruction-macros-impl/src/render/try_from_tag_macro.rs +++ b/instruction-macros/crates/instruction-macros-impl/src/render/try_from_tag_macro.rs @@ -59,7 +59,7 @@ use crate::parse::{ /// // Use it to implement `TryFrom`: /// impl TryFrom for MyInstruction { /// type Error = ProgramError; -/// +/// /// #[inline(always)] /// fn try_from(tag: u8) -> Result { /// MyInstruction_try_from_tag!(tag, ProgramError::InvalidInstructionData) diff --git a/instruction-macros/crates/test-fixtures/src/client.rs b/instruction-macros/crates/test-fixtures/src/client.rs index fc5f459a..058f389d 100644 --- a/instruction-macros/crates/test-fixtures/src/client.rs +++ b/instruction-macros/crates/test-fixtures/src/client.rs @@ -19,7 +19,7 @@ mod test { #[account(9, name = "quote_token_program", desc = "The quote mint's token program.")] #[args(sector_index_hint: u32, "A hint indicating which sector the user's seat resides in.")] CloseSeat, - + #[account(0, signer, name = "user", desc = "The user depositing or registering their seat.")] #[account(1, writable, name = "market_account", desc = "The market account PDA.")] #[account(2, writable, name = "user_ata", desc = "The user's associated token account.")] @@ -29,10 +29,10 @@ mod test { #[args(amount: u64, "The amount to deposit.")] #[args(sector_index_hint: u32, "A hint indicating which sector the user's seat resides in (pass `NIL` when registering a new seat).")] Deposit, - + #[account(0, signer, name = "event_authority", desc = "Flush events.")] FlushEvents, - + Batch, } } diff --git a/interface/src/events/mod.rs b/interface/src/events/mod.rs index acd28c17..c33a9352 100644 --- a/interface/src/events/mod.rs +++ b/interface/src/events/mod.rs @@ -39,7 +39,7 @@ pub enum DropsetEventTag { #[args(seat_sector_index: u32, "The user's (possibly newly registered) market seat sector index.")] DepositEvent, #[args(amount: u64, "The amount withdrawn.")] - #[args(is_base: bool, "Which token, i.e., `true` => base token, `false` => quote token.")] + #[args(is_base: bool, "Which token, i.e., `true` => base token, `false` => quote token.")] WithdrawEvent, #[args(market: [u8; 32], "The newly registered market.")] RegisterMarketEvent, From c9eae72f68b219f06d276eaa5f0b0dd0209bc6ed Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Thu, 11 Dec 2025 16:30:17 -0800 Subject: [PATCH 45/47] Fix `exponent` typo --- price/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/price/src/lib.rs b/price/src/lib.rs index fc0cfe5f..8cb0c802 100644 --- a/price/src/lib.rs +++ b/price/src/lib.rs @@ -281,7 +281,7 @@ mod tests { let e_quote = to_biased_exponent!(0); // Ensure the quote exponent is valid so that it can't be the trigger for the error. - let _one_to_the_quote_expoent = pow10_u64!(1u64, e_quote).unwrap(); + let _one_to_the_quote_exponent = pow10_u64!(1u64, e_quote).unwrap(); let all_good = to_order_info(10_000_000, 1, e_base, e_quote); let invalid_quote_exponent = to_order_info(10_000_000, 1, e_base + 1, e_quote); From b49a7bc3bb33587d12a21f6022c408dc92b3dbfa Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Thu, 11 Dec 2025 16:53:04 -0800 Subject: [PATCH 46/47] Add unit test for overflowing quote atoms --- price/src/lib.rs | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/price/src/lib.rs b/price/src/lib.rs index 8cb0c802..fb8b41b5 100644 --- a/price/src/lib.rs +++ b/price/src/lib.rs @@ -292,4 +292,47 @@ mod tests { Err(OrderInfoError::InvalidBiasedExponent) )); } + + #[test] + fn quote_atoms_overflow() { + let mantissa: u32 = 10_000_000; + let base_scalar: u64 = 1; + + let quote_exponent = 12; + assert!((mantissa as u64).checked_mul(base_scalar).is_some()); + + // No overflow with quote exponent using core rust operations. + assert!((mantissa as u64) + .checked_mul(base_scalar) + .unwrap() + .checked_mul(10u64.checked_pow(quote_exponent as u32).unwrap()) + .is_some()); + + // Overflow with quote exponent + 1 using core rust operations. + assert!((mantissa as u64) + .checked_mul(base_scalar) + .unwrap() + .checked_mul(10u64.checked_pow((quote_exponent + 1) as u32).unwrap()) + .is_none()); + + // No overflow with quote exponent in `to_order_info`. + assert!(to_order_info( + mantissa, + base_scalar, + to_biased_exponent!(0), + to_biased_exponent!(quote_exponent) + ) + .is_ok()); + + // Overflow with quote exponent + 1 in `to_order_info`. + assert!(matches!( + to_order_info( + mantissa, + base_scalar, + to_biased_exponent!(0), + to_biased_exponent!(quote_exponent + 1) + ), + Err(OrderInfoError::ArithmeticOverflow) + )); + } } From 1c059d1c1ad1ae28976bc52cebeb3b669c3418dc Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:01:25 -0800 Subject: [PATCH 47/47] Fix unclosed code blocks --- price/src/lib.rs | 1 - price/src/macros.rs | 14 +++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/price/src/lib.rs b/price/src/lib.rs index fb8b41b5..ce09537b 100644 --- a/price/src/lib.rs +++ b/price/src/lib.rs @@ -32,7 +32,6 @@ const MAX_BIASED_EXPONENT: u8 = (1 << (EXPONENT_BITS)) - 1; /// is -16, so the BIAS must be 16. /// /// See [`pow10_u64`] for more information on the reasoning behind the exponent range. -/// ``` pub const BIAS: u8 = 16; /// The minimum unbiased exponent value. diff --git a/price/src/macros.rs b/price/src/macros.rs index 43ce068b..5503b191 100644 --- a/price/src/macros.rs +++ b/price/src/macros.rs @@ -45,25 +45,29 @@ const _: () = { /// | -------------------- | ------------------- | ------------------- | /// | Smallest mantissa | 1.00 * 10^-2 = 0.01 | 1.00 * 10^1 = 10 | /// | Largest mantissa | 9.99 * 10^-2 = ~0.1 | 9.99 * 10^1 = ~100 | -/// | -------------------- | -------------------|--------------------- | +/// | -------------------- | ------------------- | ------------------- | +/// ``` /// /// Both the smallest and largest products (0.01 and 100) are 2 orders /// of magnitude below/above `1`. /// +/// ```markdown /// # With [-1, 2] as the smallest/largest exponents /// | | Smallest exponent | Largest exponent | /// | -------------------- | ------------------ | -------------------- | /// | Smallest mantissa | 1.00 * 10^-1 = 0.1 | 1.00 * 10^2 = 100 | /// | Largest mantissa | 9.99 * 10^-1 = ~1 | 9.99 * 10^2 = ~1000 | -/// | -------------------- | -------------------|--------------------- | +/// | -------------------- | ------------------ | -------------------- | +/// ``` /// -/// The lower product (0.1) is 1 order of magnitude below `1` and the higher -/// product (1000) is 3 orders of magnitude above `1`. +/// The lower product (0.1) is 1 order of magnitude below 1 and the higher +/// product (1000) is 3 orders of magnitude above 1. /// /// The first option is preferable because it offers a more dynamic, -/// symmetrical range in terms of orders of magnitude below/above `1`. +/// symmetrical range in terms of orders of magnitude below/above 1. /// /// Therefore, [-16, 15] is used as the exponent range instead of [-15, 16]. +/// #[macro_export] #[rustfmt::skip] macro_rules! pow10_u64 {