Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 167 additions & 0 deletions src/int.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
//! Stack-allocated big signed integers.

use num_traits::Zero;
use subtle::{Choice, ConditionallySelectable, ConstantTimeEq};

use crate::{Limb, Uint};

mod add;
mod cmp;
mod div;
mod mul;
mod neg;
mod sub;

#[derive(Copy, Clone, Hash, Debug, PartialEq)]
pub struct Int<const LIMBS: usize> {
// TODO(erik): replace with Choice once I've figured how to const-construct those.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no problem here, worst case scenario, just use u8, CtChoice is more or less that.

is_negative: bool,
Comment on lines +17 to +18
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bool anywhere in constant-time code is worrisome because it's an invitation for e.g. LLVM's optimizer to insert branches based on the value, even in cases where the original code deliberately avoids them

magnitude: Uint<LIMBS>,
}

impl<const LIMBS: usize> Int<LIMBS> {
/// The value `0`.
pub const ZERO: Self = Self {
is_negative: false,
magnitude: Uint::ZERO,
};

/// The value `1`.
pub const ONE: Self = Self {
is_negative: false,
magnitude: Uint::ONE,
};

/// The value `-1`.
pub const MINUS_ONE: Self = Self {
is_negative: true,
magnitude: Uint::ONE,
};

/// Maximum value this [`Uint`] can express.
pub const MAX: Self = Self {
is_negative: false,
magnitude: Uint::MAX,
};

/// Smallest value this [`Uint`] can express.
// Note: keep in mind that when `LIMBS = 0`, the minimal value we can express is zero.
pub const MIN: Self = Self {
is_negative: LIMBS != 0,
magnitude: Uint::MAX,
};

/// The number of limbs used on this platform.
pub const LIMBS: usize = LIMBS;

/// Const-friendly [`Int`] constructor.
pub const fn new(is_negative: bool, limbs: [Limb; LIMBS]) -> Self {
Self {
is_negative,
magnitude: Uint::new(limbs),
}
}

/// Const-friendly [`Int`] constructor, using a `Uint`.
pub const fn new_from_uint(is_negative: bool, magnitude: Uint<LIMBS>) -> Self {
Self {
is_negative,
magnitude,
}
}

/// Whether this [`Int`] is negative
pub fn is_negative(&self) -> Choice {
Choice::from(u8::from(self.is_negative))
}

/// Whether this [`Int`] is zero
pub fn is_zero(&self) -> bool {
self.magnitude.is_zero()
}
}

impl<const LIMBS: usize> ConditionallySelectable for Int<LIMBS> {
fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self {
let magnitude = Uint::conditional_select(&a.magnitude, &b.magnitude, choice);
let is_negative = Choice::conditional_select(&a.is_negative(), &b.is_negative(), choice);

Self {
is_negative: is_negative.unwrap_u8() == 1,
magnitude,
}
}
}

impl<const LIMBS: usize> Default for Int<LIMBS> {
fn default() -> Self {
Self::ZERO
}
}

impl<const LIMBS: usize> Zero for Int<LIMBS> {
fn zero() -> Self {
Self::ZERO
}

fn is_zero(&self) -> bool {
self.ct_eq(&Self::ZERO).into()
}
}

#[cfg(target_pointer_width = "64")]
#[allow(dead_code)]
type I128 = Int<2>;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is where it's tricky, if we want to relate this to builtin i128 type, this is actually closer to I129 because of using sign and magnitude instead of two's complements.


#[cfg(target_pointer_width = "32")]
#[allow(dead_code)]
type I128 = Int<4>;

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use crate::{
int::{Int, I128},
Uint, U128,
};

#[test]
fn zero() {
let n = I128::ZERO;
assert!(!n.is_negative);
assert_eq!(n.magnitude, U128::ZERO);
}

#[test]
fn one() {
let n = I128::ONE;
assert!(!n.is_negative);
assert_eq!(n.magnitude, U128::ONE);
}

#[test]
fn minus_one() {
let n = I128::MINUS_ONE;
assert!(n.is_negative);
assert_eq!(n.magnitude, U128::ONE);
}

#[test]
fn min() {
let n = I128::MIN;
assert!(n.is_negative);
assert_eq!(n.magnitude, U128::MAX);

// Deal with case that LIMBS = 0
let n = Int::<0>::MIN;
assert!(!n.is_negative);
assert_eq!(n.magnitude, Uint::<0>::MAX); // dirty trick; MAX is zero, while ZERO panics.
}

#[test]
fn max() {
let n = I128::MAX;
assert!(!n.is_negative);
assert_eq!(n.magnitude, U128::MAX);
}
}
186 changes: 186 additions & 0 deletions src/int/add.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
//! [`Int`] addition operations.

use core::ops::{Add, AddAssign};

use subtle::{Choice, ConstantTimeEq, CtOption};

use crate::int::Int;
use crate::{Checked, CheckedAdd, CheckedSub, ConstantTimeSelect};

impl<const LIMBS: usize> Int<LIMBS> {
/// Add two [`Int`]s, checking for overflow.
fn checked_adc(&self, rhs: &Self) -> CtOption<Self> {
Copy link

@lleoha lleoha Oct 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is unnecessary complex.
it's much easier to do it that way:

  • positive and negative never overflows
  • two positives (or two negatives) overflow if MSB of the result is different that MSB of operands (the result and operands of magnitude I mean)
    no need for min/max, sub etc.

// Step 1. Select the element with the largest magnitude to be the lhs.
let (lhs, rhs) = Int::abs_max_min(self, rhs);

// Step 2. Add/subtract the magnitudes of the two sides to/from each other
let magnitude_add = lhs.magnitude.checked_add(&rhs.magnitude);
let magnitude_sub = lhs.magnitude.checked_sub(&rhs.magnitude);

// Step 3. Select the correct magnitude.
// magnitude_sub is used when the signs of the two elements are not the same.
let different_signs = lhs.is_negative().ct_ne(&rhs.is_negative());
let magnitude = CtOption::ct_select(&magnitude_add, &magnitude_sub, different_signs);

// Step 4. Determine the sign of the result.
// This is always the same as the sign of the self (since it assumed to have the
// larger magnitude), except when the sum is zero.
let sum_is_zero = different_signs & lhs.magnitude.ct_eq(&rhs.magnitude);
let is_negative = lhs.is_negative() & !sum_is_zero;

magnitude.and_then(|magnitude| {
CtOption::new(
Self {
is_negative: is_negative.unwrap_u8() == 1,
magnitude,
},
Choice::from(1),
)
})
}
}

impl<const LIMBS: usize> Add for Int<LIMBS> {
type Output = Self;

fn add(self, rhs: Self) -> Self {
self.add(&rhs)
}
}

impl<const LIMBS: usize> Add<&Int<LIMBS>> for Int<LIMBS> {
type Output = Self;

fn add(self, rhs: &Self) -> Self {
self.checked_add(rhs)
.expect("attempted to add with overflow")
}
}

impl<const LIMBS: usize> AddAssign for Int<LIMBS> {
fn add_assign(&mut self, other: Self) {
*self += &other;
}
}

impl<const LIMBS: usize> AddAssign<&Int<LIMBS>> for Int<LIMBS> {
fn add_assign(&mut self, other: &Self) {
*self = *self + other;
}
}

impl<const LIMBS: usize> AddAssign for Checked<Int<LIMBS>> {
fn add_assign(&mut self, other: Self) {
*self = *self + other;
}
}

impl<const LIMBS: usize> AddAssign<&Checked<Int<LIMBS>>> for Checked<Int<LIMBS>> {
fn add_assign(&mut self, other: &Self) {
*self = *self + other;
}
}

impl<const LIMBS: usize> CheckedAdd for Int<LIMBS> {
/// Add two [`Int`]s, checking for overflow.
fn checked_add(&self, rhs: &Self) -> CtOption<Self> {
self.checked_adc(rhs)
}
}

#[cfg(test)]
mod tests {
use crate::int::I128;
use crate::{CheckedAdd, U128};

#[test]
fn checked_add() {
let max_minus_one = U128::MAX.wrapping_sub(&U128::ONE);
let two = U128::from(2u32);

// lhs = MIN

let result = I128::MIN.checked_add(&I128::MIN);
assert!(bool::from(result.is_none()));

let result = I128::MIN.checked_add(&I128::MINUS_ONE);
assert!(bool::from(result.is_none()));

let result = I128::MIN.checked_add(&I128::ZERO);
assert_eq!(result.unwrap(), I128::MIN);

let result = I128::MIN.checked_add(&I128::ONE);
assert_eq!(result.unwrap(), I128::new_from_uint(true, max_minus_one));

let result = I128::MIN.checked_add(&I128::MAX);
assert_eq!(result.unwrap(), I128::ZERO);

// lhs = -1

let result = I128::MINUS_ONE.checked_add(&I128::MIN);
assert!(bool::from(result.is_none()));

let result = I128::MINUS_ONE.checked_add(&I128::MINUS_ONE);
assert_eq!(result.unwrap(), I128::new_from_uint(true, two));

let result = I128::MINUS_ONE.checked_add(&I128::ZERO);
assert_eq!(result.unwrap(), I128::MINUS_ONE);

let result = I128::MINUS_ONE.checked_add(&I128::ONE);
assert_eq!(result.unwrap(), I128::ZERO);

let result = I128::MINUS_ONE.checked_add(&I128::MAX);
assert_eq!(result.unwrap(), I128::new_from_uint(false, max_minus_one));

// lhs = 0

let result = I128::ZERO.checked_add(&I128::MIN);
assert_eq!(result.unwrap(), I128::MIN);

let result = I128::ZERO.checked_add(&I128::MINUS_ONE);
assert_eq!(result.unwrap(), I128::MINUS_ONE);

let result = I128::ZERO.checked_add(&I128::ZERO);
assert_eq!(result.unwrap(), I128::ZERO);

let result = I128::ZERO.checked_add(&I128::ONE);
assert_eq!(result.unwrap(), I128::ONE);

let result = I128::ZERO.checked_add(&I128::MAX);
assert_eq!(result.unwrap(), I128::MAX);

// lhs = 1

let result = I128::ONE.checked_add(&I128::MIN);
assert_eq!(result.unwrap(), I128::new_from_uint(true, max_minus_one));

let result = I128::ONE.checked_add(&I128::MINUS_ONE);
assert_eq!(result.unwrap(), I128::ZERO);

let result = I128::ONE.checked_add(&I128::ZERO);
assert_eq!(result.unwrap(), I128::ONE);

let result = I128::ONE.checked_add(&I128::ONE);
assert_eq!(result.unwrap(), I128::new_from_uint(false, two));

let result = I128::ONE.checked_add(&I128::MAX);
assert!(bool::from(result.is_none()));

// lhs = MAX

let result = I128::MAX.checked_add(&I128::MIN);
assert_eq!(result.unwrap(), I128::ZERO);

let result = I128::MAX.checked_add(&I128::MINUS_ONE);
assert_eq!(result.unwrap(), I128::new_from_uint(false, max_minus_one));

let result = I128::MAX.checked_add(&I128::ZERO);
assert_eq!(result.unwrap(), I128::MAX);

let result = I128::MAX.checked_add(&I128::ONE);
assert!(bool::from(result.is_none()));

let result = I128::MAX.checked_add(&I128::MAX);
assert!(bool::from(result.is_none()));
}
}
35 changes: 35 additions & 0 deletions src/int/cmp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//! [`Int`] comparisons.
//!
//! By default, these are all constant-time and use the `subtle` crate.

use subtle::{Choice, ConditionallySelectable, ConstantTimeEq, ConstantTimeLess};

use crate::int::Int;
use crate::Uint;

impl<const LIMBS: usize> Int<LIMBS> {
/// Given two [`Int`]s, return a tuple with the larger magnitude first.
///
/// When both have the same magnitude, they are returned in the same order
/// as they were provided.
pub(crate) fn abs_max_min(&self, rhs: &Self) -> (Self, Self) {
let swap = self.magnitude.ct_lt(&rhs.magnitude);
let greatest = Int::conditional_select(self, rhs, swap);
let smallest = Int::conditional_select(rhs, self, swap);
(greatest, smallest)
}

#[inline]
/// TODO: make this a const function?
pub(crate) fn eq(lhs: &Self, rhs: &Self) -> Choice {
Choice::from(Uint::eq(&lhs.magnitude, &rhs.magnitude))
& Choice::ct_eq(&lhs.is_negative(), &rhs.is_negative())
}
}

impl<const LIMBS: usize> ConstantTimeEq for Int<LIMBS> {
#[inline]
fn ct_eq(&self, other: &Self) -> Choice {
Int::eq(self, other)
}
}
Loading