#[cfg(all(feature = "alloc", not(feature = "std")))]
use alloc::{vec, vec::Vec};
#[cfg(feature = "std")]
use std::{vec, vec::Vec};
use elliptic_curve::{
Group, PrimeField,
subtle::{Choice, ConditionallySelectable, ConstantTimeEq},
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Algorithm {
Null,
Single,
Straus(u8),
Pippenger(u8),
}
const STRAUS_CT_WINDOW: u8 = 5;
const MIN_WINDOW: u8 = 4;
const MAX_STRAUS_WINDOW: u8 = 5;
fn vartime_algorithm(len: usize) -> Algorithm {
const FINAL_ALGORITHM: Algorithm = Algorithm::Pippenger(8);
const THRESHOLDS: &[(usize, Algorithm)] = &[
(128, Algorithm::Straus(5)),
(400, Algorithm::Pippenger(6)),
(900, Algorithm::Pippenger(7)),
];
if len == 0 {
return Algorithm::Null;
}
if len == 1 {
return Algorithm::Single;
}
for (limit, algorithm) in THRESHOLDS {
if len < *limit {
return *algorithm;
}
}
FINAL_ALGORITHM
}
fn scalar_bits<G: Group>() -> usize {
<<G as Group>::Scalar as PrimeField>::Repr::default()
.as_ref()
.len()
* 8
}
fn signed_digit_count<G: Group>(window: u8) -> usize {
scalar_bits::<G>().div_ceil(usize::from(window)) + 1
}
fn signed_table_len(window: u8) -> usize {
(1_usize << (window - 1)) + 1
}
fn little_endian_repr<F: PrimeField>() -> bool {
let repr = F::ONE.to_repr();
repr.as_ref().first().copied() == Some(1)
}
fn recode_signed<F: PrimeField>(
scalars: impl IntoIterator<Item = F>,
window: u8,
digits: &mut [u8],
) {
let w = usize::from(window);
let windows = (F::Repr::default().as_ref().len() * 8).div_ceil(w);
let count = windows + 1;
let mask = (1_u32 << w) - 1;
let half = 1_i32 << (w - 1);
let base = 1_i32 << w;
let little_endian = little_endian_repr::<F>();
for (scalar_index, scalar) in scalars.into_iter().enumerate() {
let repr = scalar.to_repr();
let bytes: &[u8] = repr.as_ref();
let len = bytes.len();
let offset = scalar_index * count;
let mut accumulator = 0_u32;
let mut available_bits = 0_usize;
let mut produced = 0_usize;
let mut carry = 0_i32;
let recode = |unsigned: i32, carry: &mut i32| -> u8 {
let t = unsigned + *carry; let ge = ((half - 1 - t) >> 31) & 1; let signed = t - ge * base; *carry = ge;
(signed as i8) as u8
};
for index in 0..len {
let byte = if little_endian {
bytes[index]
} else {
bytes[len - 1 - index]
};
accumulator |= u32::from(byte) << available_bits;
available_bits += 8;
while available_bits >= w && produced < windows {
let unsigned = (accumulator & mask) as i32;
accumulator >>= w;
available_bits -= w;
digits[offset + produced] = recode(unsigned, &mut carry);
produced += 1;
}
}
while produced < windows {
let unsigned = (accumulator & mask) as i32;
accumulator >>= w;
digits[offset + produced] = recode(unsigned, &mut carry);
produced += 1;
}
digits[offset + windows] = (carry as i8) as u8;
}
}
fn fill_signed_tables<G: Group>(points: impl IntoIterator<Item = G>, window: u8, values: &mut [G]) {
let len = signed_table_len(window);
for (index, point) in points.into_iter().enumerate() {
let offset = index * len;
values[offset] = G::identity();
let mut accum = G::identity();
for entry in values[offset + 1..offset + len].iter_mut() {
accum += point;
*entry = accum;
}
}
}
fn signed_select_ct<G>(table: &[G], digit: i8) -> G
where
G: ConditionallySelectable + Group,
{
let magnitude = digit.unsigned_abs();
let sign = (digit as u8) >> 7;
let mut selected = G::identity();
for (index, candidate) in table.iter().enumerate() {
selected = G::conditional_select(&selected, candidate, (index as u8).ct_eq(&magnitude));
}
let negated = -selected;
G::conditional_select(&selected, &negated, Choice::from(sign))
}
fn straus_accumulate_ct<G>(window: u8, count: usize, len: usize, digits: &[u8], tables: &[G]) -> G
where
G: ConditionallySelectable + Group,
{
let mut result = G::identity();
for digit_index in (0..count).rev() {
if digit_index != count - 1 {
for _ in 0..window {
result = result.double();
}
}
for (scalar, table) in tables.chunks_exact(len).enumerate() {
let digit = digits[scalar * count + digit_index] as i8;
result += signed_select_ct(table, digit);
}
}
result
}
fn straus_accumulate_vartime<G>(
window: u8,
count: usize,
len: usize,
digits: &[u8],
tables: &[G],
) -> G
where
G: Group,
{
let mut result = G::identity();
for digit_index in (0..count).rev() {
if digit_index != count - 1 {
for _ in 0..window {
result = result.double();
}
}
for (scalar, table) in tables.chunks_exact(len).enumerate() {
let digit = digits[scalar * count + digit_index] as i8;
let magnitude = usize::from(digit.unsigned_abs());
if magnitude != 0 {
let point = table[magnitude];
if digit < 0 {
result += -point;
} else {
result += point;
}
}
}
}
result
}
fn straus_with<G>(pairs: &[(G::Scalar, G)], window: u8, digits: &mut [u8], tables: &mut [G]) -> G
where
G: ConditionallySelectable + Group,
{
recode_signed(pairs.iter().map(|(scalar, _)| *scalar), window, digits);
fill_signed_tables(pairs.iter().map(|(_, point)| *point), window, tables);
straus_accumulate_ct(
window,
signed_digit_count::<G>(window),
signed_table_len(window),
digits,
tables,
)
}
fn straus_vartime_with<G>(
pairs: &[(G::Scalar, G)],
window: u8,
digits: &mut [u8],
tables: &mut [G],
) -> G
where
G: Group,
{
recode_signed(pairs.iter().map(|(scalar, _)| *scalar), window, digits);
fill_signed_tables(pairs.iter().map(|(_, point)| *point), window, tables);
straus_accumulate_vartime(
window,
signed_digit_count::<G>(window),
signed_table_len(window),
digits,
tables,
)
}
fn pippenger_vartime_with<G>(
pairs: &[(G::Scalar, G)],
window: u8,
digits: &mut [u8],
buckets: &mut [G],
) -> G
where
G: Group,
{
recode_signed(pairs.iter().map(|(scalar, _)| *scalar), window, digits);
let count = signed_digit_count::<G>(window);
let identity = G::identity();
let mut result = identity;
for digit_index in (0..count).rev() {
if digit_index != count - 1 {
for _ in 0..window {
result = result.double();
}
}
buckets.fill(identity);
for (scalar, (_, point)) in pairs.iter().enumerate() {
let digit = digits[scalar * count + digit_index] as i8;
let magnitude = usize::from(digit.unsigned_abs());
if magnitude != 0 {
if digit < 0 {
buckets[magnitude] += -*point;
} else {
buckets[magnitude] += point;
}
}
}
let mut intermediate_sum = identity;
for bucket in buckets.iter().skip(1).rev() {
intermediate_sum += bucket;
result += intermediate_sum;
}
}
result
}
fn straus<G>(pairs: &[(G::Scalar, G)], window: u8) -> G
where
G: ConditionallySelectable + Group,
{
let n = pairs.len();
let mut digits = vec![0u8; n * signed_digit_count::<G>(window)];
let mut tables = vec![G::identity(); n * signed_table_len(window)];
straus_with(pairs, window, &mut digits, &mut tables)
}
fn straus_vartime<G>(pairs: &[(G::Scalar, G)], window: u8) -> G
where
G: Group,
{
let n = pairs.len();
let mut digits = vec![0u8; n * signed_digit_count::<G>(window)];
let mut tables = vec![G::identity(); n * signed_table_len(window)];
straus_vartime_with(pairs, window, &mut digits, &mut tables)
}
fn pippenger_vartime<G>(pairs: &[(G::Scalar, G)], window: u8) -> G
where
G: Group,
{
let n = pairs.len();
let mut digits = vec![0u8; n * signed_digit_count::<G>(window)];
let mut buckets = vec![G::identity(); signed_table_len(window)];
pippenger_vartime_with(pairs, window, &mut digits, &mut buckets)
}
pub(crate) fn multiexp<G>(pairs: &[(G::Scalar, G)]) -> G
where
G: ConditionallySelectable + Group,
{
match pairs.len() {
0 => G::identity(),
1 => pairs[0].1 * pairs[0].0,
_ => straus(pairs, STRAUS_CT_WINDOW),
}
}
pub(crate) fn multiexp_iter<G, I>(pairs: I) -> G
where
G: ConditionallySelectable + Group,
I: IntoIterator<Item = (G::Scalar, G)>,
I::IntoIter: ExactSizeIterator,
{
let mut iter = pairs.into_iter();
let n = iter.len();
if n == 0 {
return G::identity();
}
if n == 1 {
if let Some((scalar, point)) = iter.next() {
return point * scalar;
}
return G::identity();
}
let window = STRAUS_CT_WINDOW;
let count = signed_digit_count::<G>(window);
let len = signed_table_len(window);
let mut digits = vec![0u8; n * count];
let mut tables = vec![G::identity(); n * len];
for (index, (scalar, point)) in iter.take(n).enumerate() {
recode_signed(
core::iter::once(scalar),
window,
&mut digits[index * count..index * count + count],
);
fill_signed_tables(
core::iter::once(point),
window,
&mut tables[index * len..index * len + len],
);
}
straus_accumulate_ct(window, count, len, &digits, &tables)
}
pub(crate) fn multiexp_vartime<G>(pairs: &[(G::Scalar, G)]) -> G
where
G: Group,
{
match vartime_algorithm(pairs.len()) {
Algorithm::Null => G::identity(),
Algorithm::Single => pairs[0].1 * pairs[0].0,
Algorithm::Straus(window) => straus_vartime(pairs, window),
Algorithm::Pippenger(window) => pippenger_vartime(pairs, window),
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ScratchBuffer {
Digits,
Points,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct InsufficientScratch {
pub buffer: ScratchBuffer,
pub provided: usize,
pub required: usize,
}
impl core::fmt::Display for InsufficientScratch {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let buffer = match self.buffer {
ScratchBuffer::Digits => "digit",
ScratchBuffer::Points => "point",
};
write!(
f,
"insufficient {buffer} scratch: provided {}, required {}",
self.provided, self.required
)
}
}
impl core::error::Error for InsufficientScratch {}
pub struct Scratch<G> {
digits: Vec<u8>,
points: Vec<G>,
}
impl<G: Group> Scratch<G> {
pub fn new(capacity: usize) -> Self {
Self {
digits: vec![0u8; capacity.saturating_mul(signed_digit_count::<G>(MIN_WINDOW))],
points: vec![
G::identity();
capacity.saturating_mul(signed_table_len(MAX_STRAUS_WINDOW))
],
}
}
fn check(&self, digits: usize, points: usize) -> Result<(), InsufficientScratch> {
if self.digits.len() < digits {
return Err(InsufficientScratch {
buffer: ScratchBuffer::Digits,
provided: self.digits.len(),
required: digits,
});
}
if self.points.len() < points {
return Err(InsufficientScratch {
buffer: ScratchBuffer::Points,
provided: self.points.len(),
required: points,
});
}
Ok(())
}
}
pub(crate) fn multiexp_inplace<G>(
pairs: &[(G::Scalar, G)],
scratch: &mut Scratch<G>,
) -> Result<G, InsufficientScratch>
where
G: ConditionallySelectable + Group,
{
let n = pairs.len();
match n {
0 => return Ok(G::identity()),
1 => return Ok(pairs[0].1 * pairs[0].0),
_ => {}
}
let window = STRAUS_CT_WINDOW;
let digits = n * signed_digit_count::<G>(window);
let points = n * signed_table_len(window);
scratch.check(digits, points)?;
Ok(straus_with(
pairs,
window,
&mut scratch.digits[..digits],
&mut scratch.points[..points],
))
}
pub(crate) fn multiexp_vartime_inplace<G>(
pairs: &[(G::Scalar, G)],
scratch: &mut Scratch<G>,
) -> Result<G, InsufficientScratch>
where
G: Group,
{
let n = pairs.len();
match vartime_algorithm(n) {
Algorithm::Null => Ok(G::identity()),
Algorithm::Single => Ok(pairs[0].1 * pairs[0].0),
Algorithm::Straus(window) => {
let digits = n * signed_digit_count::<G>(window);
let points = n * signed_table_len(window);
scratch.check(digits, points)?;
Ok(straus_vartime_with(
pairs,
window,
&mut scratch.digits[..digits],
&mut scratch.points[..points],
))
}
Algorithm::Pippenger(window) => {
let digits = n * signed_digit_count::<G>(window);
let points = signed_table_len(window);
scratch.check(digits, points)?;
Ok(pippenger_vartime_with(
pairs,
window,
&mut scratch.digits[..digits],
&mut scratch.points[..points],
))
}
}
}
const PRECOMPUTE_WINDOW: u8 = 5;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct LengthMismatch {
pub points: usize,
pub scalars: usize,
}
impl core::fmt::Display for LengthMismatch {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"length mismatch: {} basis points, {} scalars",
self.points, self.scalars
)
}
}
impl core::error::Error for LengthMismatch {}
pub struct Precomputed<G> {
tables: Vec<G>,
window: u8,
len: usize,
}
impl<G: Group> Precomputed<G> {
pub fn new(points: &[G]) -> Self {
Self::with_window(points, PRECOMPUTE_WINDOW)
}
pub fn with_window(points: &[G], window: u8) -> Self {
let window = window.clamp(2, 8);
let len = points.len();
let mut tables = vec![G::identity(); len * signed_table_len(window)];
fill_signed_tables(points.iter().copied(), window, &mut tables);
Self {
tables,
window,
len,
}
}
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn sum_of_products(&self, scalars: &[G::Scalar]) -> Result<G, LengthMismatch>
where
G: ConditionallySelectable,
{
self.sum_of_products_iter(scalars.iter().copied())
}
pub fn sum_of_products_iter<I>(&self, scalars: I) -> Result<G, LengthMismatch>
where
G: ConditionallySelectable,
I: IntoIterator<Item = G::Scalar>,
I::IntoIter: ExactSizeIterator,
{
let (count, digits) = self.recode_or_mismatch(scalars)?;
Ok(straus_accumulate_ct(
self.window,
count,
signed_table_len(self.window),
&digits,
&self.tables,
))
}
pub fn sum_of_products_vartime(&self, scalars: &[G::Scalar]) -> Result<G, LengthMismatch> {
self.sum_of_products_vartime_iter(scalars.iter().copied())
}
pub fn sum_of_products_vartime_iter<I>(&self, scalars: I) -> Result<G, LengthMismatch>
where
I: IntoIterator<Item = G::Scalar>,
I::IntoIter: ExactSizeIterator,
{
let (count, digits) = self.recode_or_mismatch(scalars)?;
Ok(straus_accumulate_vartime(
self.window,
count,
signed_table_len(self.window),
&digits,
&self.tables,
))
}
fn recode_or_mismatch<I>(&self, scalars: I) -> Result<(usize, Vec<u8>), LengthMismatch>
where
I: IntoIterator<Item = G::Scalar>,
I::IntoIter: ExactSizeIterator,
{
let scalars = scalars.into_iter();
if scalars.len() != self.len {
return Err(LengthMismatch {
points: self.len,
scalars: scalars.len(),
});
}
let count = signed_digit_count::<G>(self.window);
let mut digits = vec![0u8; self.len * count];
recode_signed(scalars, self.window, &mut digits);
Ok((count, digits))
}
}