Tweak multiexp to compile on core

On `core`, it'll use a serial implementation of no benefit other than the fact
that when `alloc` _is_ enabled, it'll use the multi-scalar multiplication
algorithms.

`schnorr-signatures` was prior tweaked to include a shim for
`SchnorrSignature::verify` which didn't use `multiexp_vartime` yet this same
premise. Now, instead of callers writing these shims, it's within `multiexp`.
This commit is contained in:
Luke Parker
2025-09-15 22:37:59 -04:00
parent d6d96fe8ff
commit be68e27551
10 changed files with 161 additions and 189 deletions

2
Cargo.lock generated
View File

@@ -6334,8 +6334,6 @@ dependencies = [
"group", "group",
"k256", "k256",
"rand_core 0.6.4", "rand_core 0.6.4",
"rustversion",
"std-shims",
"zeroize", "zeroize",
] ]

View File

@@ -17,11 +17,7 @@ rustdoc-args = ["--cfg", "docsrs"]
workspace = true workspace = true
[dependencies] [dependencies]
rustversion = "1" zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
std-shims = { path = "../../common/std-shims", version = "0.1.1", default-features = false, features = ["alloc"] }
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive", "alloc"] }
ff = { version = "0.13", default-features = false, features = ["bits"] } ff = { version = "0.13", default-features = false, features = ["bits"] }
group = { version = "0.13", default-features = false } group = { version = "0.13", default-features = false }
@@ -35,8 +31,9 @@ k256 = { version = "^0.13.1", default-features = false, features = ["arithmetic"
dalek-ff-group = { path = "../dalek-ff-group" } dalek-ff-group = { path = "../dalek-ff-group" }
[features] [features]
std = ["std-shims/std", "zeroize/std", "ff/std", "rand_core?/std"] alloc = ["zeroize/alloc"]
std = ["alloc", "zeroize/std", "ff/std", "rand_core?/std"]
batch = ["rand_core"] batch = ["alloc", "rand_core"]
default = ["std"] default = ["std"]

View File

@@ -12,5 +12,6 @@ culminating in commit
[669d2dbffc1dafb82a09d9419ea182667115df06](https://github.com/serai-dex/serai/tree/669d2dbffc1dafb82a09d9419ea182667115df06). [669d2dbffc1dafb82a09d9419ea182667115df06](https://github.com/serai-dex/serai/tree/669d2dbffc1dafb82a09d9419ea182667115df06).
Any subsequent changes have not undergone auditing. Any subsequent changes have not undergone auditing.
This library is usable under no_std, via alloc, when the default features are This library is usable under no-`std` and no-`alloc`. With the `alloc` feature,
disabled. the library is fully functional. Without the `alloc` feature, the `multiexp`
function is shimmed with a serial implementation.

View File

@@ -1,4 +1,4 @@
use std_shims::vec::Vec; use alloc::vec::Vec;
use rand_core::{RngCore, CryptoRng}; use rand_core::{RngCore, CryptoRng};

View File

@@ -2,41 +2,40 @@
#![doc = include_str!("../README.md")] #![doc = include_str!("../README.md")]
#![cfg_attr(not(feature = "std"), no_std)] #![cfg_attr(not(feature = "std"), no_std)]
#[cfg(not(feature = "std"))] #[cfg(feature = "alloc")]
#[macro_use]
extern crate alloc; extern crate alloc;
#[allow(unused_imports)]
use std_shims::prelude::*;
use std_shims::vec::Vec;
use zeroize::Zeroize; use zeroize::Zeroize;
use ff::PrimeFieldBits; use ff::PrimeFieldBits;
use group::Group; use group::Group;
#[cfg(feature = "alloc")]
mod straus; mod straus;
use straus::*; #[cfg(feature = "alloc")]
mod pippenger; mod pippenger;
use pippenger::*;
#[cfg(feature = "batch")] #[cfg(feature = "batch")]
mod batch; mod batch;
#[cfg(feature = "batch")]
pub use batch::BatchVerifier;
#[cfg(test)] #[cfg(all(test, feature = "alloc"))]
mod tests; mod tests;
// Use black_box when possible #[cfg(feature = "alloc")]
#[rustversion::since(1.66)] mod underlying {
use core::hint::black_box; use super::*;
#[rustversion::before(1.66)]
fn black_box<T>(val: T) -> T {
val
}
fn u8_from_bool(bit_ref: &mut bool) -> u8 { use core::hint::black_box;
use alloc::{vec, vec::Vec};
pub(crate) use straus::*;
pub(crate) use pippenger::*;
#[cfg(feature = "batch")]
pub use batch::BatchVerifier;
fn u8_from_bool(bit_ref: &mut bool) -> u8 {
let bit_ref = black_box(bit_ref); let bit_ref = black_box(bit_ref);
let mut bit = black_box(*bit_ref); let mut bit = black_box(*bit_ref);
@@ -47,14 +46,14 @@ fn u8_from_bool(bit_ref: &mut bool) -> u8 {
bit_ref.zeroize(); bit_ref.zeroize();
res res
} }
// Convert scalars to `window`-sized bit groups, as needed to index a table // Convert scalars to `window`-sized bit groups, as needed to index a table
// This algorithm works for `window <= 8` // This algorithm works for `window <= 8`
pub(crate) fn prep_bits<G: Group<Scalar: PrimeFieldBits>>( pub(crate) fn prep_bits<G: Group<Scalar: PrimeFieldBits>>(
pairs: &[(G::Scalar, G)], pairs: &[(G::Scalar, G)],
window: u8, window: u8,
) -> Vec<Vec<u8>> { ) -> Vec<Vec<u8>> {
let w_usize = usize::from(window); let w_usize = usize::from(window);
let mut groupings = vec![]; let mut groupings = vec![];
@@ -71,62 +70,18 @@ pub(crate) fn prep_bits<G: Group<Scalar: PrimeFieldBits>>(
} }
groupings groupings
} }
#[derive(Clone, Copy, PartialEq, Eq, Debug)] #[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Algorithm { enum Algorithm {
Null, Null,
Single, Single,
Straus(u8), Straus(u8),
Pippenger(u8), Pippenger(u8),
} }
/* // These are 'rule of thumb's obtained via benchmarking `k256` and `curve25519-dalek`
Release (with runs 20, so all of these are off by 20x): fn algorithm(len: usize) -> Algorithm {
k256
Straus 3 is more efficient at 5 with 678µs per
Straus 4 is more efficient at 10 with 530µs per
Straus 5 is more efficient at 35 with 467µs per
Pippenger 5 is more efficient at 125 with 431µs per
Pippenger 6 is more efficient at 275 with 349µs per
Pippenger 7 is more efficient at 375 with 360µs per
dalek
Straus 3 is more efficient at 5 with 519µs per
Straus 4 is more efficient at 10 with 376µs per
Straus 5 is more efficient at 170 with 330µs per
Pippenger 5 is more efficient at 125 with 305µs per
Pippenger 6 is more efficient at 275 with 250µs per
Pippenger 7 is more efficient at 450 with 205µs per
Pippenger 8 is more efficient at 800 with 213µs per
Debug (with runs 5, so...):
k256
Straus 3 is more efficient at 5 with 2532µs per
Straus 4 is more efficient at 10 with 1930µs per
Straus 5 is more efficient at 80 with 1632µs per
Pippenger 5 is more efficient at 150 with 1441µs per
Pippenger 6 is more efficient at 300 with 1235µs per
Pippenger 7 is more efficient at 475 with 1182µs per
Pippenger 8 is more efficient at 625 with 1170µs per
dalek:
Straus 3 is more efficient at 5 with 971µs per
Straus 4 is more efficient at 10 with 782µs per
Straus 5 is more efficient at 75 with 778µs per
Straus 6 is more efficient at 165 with 867µs per
Pippenger 5 is more efficient at 125 with 677µs per
Pippenger 6 is more efficient at 250 with 655µs per
Pippenger 7 is more efficient at 475 with 500µs per
Pippenger 8 is more efficient at 875 with 499µs per
*/
fn algorithm(len: usize) -> Algorithm {
#[cfg(not(debug_assertions))] #[cfg(not(debug_assertions))]
if len == 0 { if len == 0 {
Algorithm::Null Algorithm::Null
@@ -173,13 +128,13 @@ fn algorithm(len: usize) -> Algorithm {
} else { } else {
Algorithm::Pippenger(8) Algorithm::Pippenger(8)
} }
} }
/// Performs a multiexponentiation, automatically selecting the optimal algorithm based on the /// Performs a multiexponentiation, automatically selecting the optimal algorithm based on the
/// amount of pairs. /// amount of pairs.
pub fn multiexp<G: Zeroize + Group<Scalar: Zeroize + PrimeFieldBits>>( pub fn multiexp<G: Zeroize + Group<Scalar: Zeroize + PrimeFieldBits>>(
pairs: &[(G::Scalar, G)], pairs: &[(G::Scalar, G)],
) -> G { ) -> G {
match algorithm(pairs.len()) { match algorithm(pairs.len()) {
Algorithm::Null => Group::identity(), Algorithm::Null => Group::identity(),
Algorithm::Single => pairs[0].1 * pairs[0].0, Algorithm::Single => pairs[0].1 * pairs[0].0,
@@ -187,15 +142,37 @@ pub fn multiexp<G: Zeroize + Group<Scalar: Zeroize + PrimeFieldBits>>(
Algorithm::Straus(window) => straus(pairs, window), Algorithm::Straus(window) => straus(pairs, window),
Algorithm::Pippenger(window) => pippenger(pairs, window), Algorithm::Pippenger(window) => pippenger(pairs, window),
} }
} }
/// Performs a multiexponentiation in variable time, automatically selecting the optimal algorithm /// Performs a multiexponentiation in variable time, automatically selecting the optimal algorithm
/// based on the amount of pairs. /// based on the amount of pairs.
pub fn multiexp_vartime<G: Group<Scalar: PrimeFieldBits>>(pairs: &[(G::Scalar, G)]) -> G { pub fn multiexp_vartime<G: Group<Scalar: PrimeFieldBits>>(pairs: &[(G::Scalar, G)]) -> G {
match algorithm(pairs.len()) { match algorithm(pairs.len()) {
Algorithm::Null => Group::identity(), Algorithm::Null => Group::identity(),
Algorithm::Single => pairs[0].1 * pairs[0].0, Algorithm::Single => pairs[0].1 * pairs[0].0,
Algorithm::Straus(window) => straus_vartime(pairs, window), Algorithm::Straus(window) => straus_vartime(pairs, window),
Algorithm::Pippenger(window) => pippenger_vartime(pairs, window), Algorithm::Pippenger(window) => pippenger_vartime(pairs, window),
} }
}
} }
#[cfg(not(feature = "alloc"))]
mod underlying {
use super::*;
/// Performs a multiexponentiation, automatically selecting the optimal algorithm based on the
/// amount of pairs.
pub fn multiexp<G: Zeroize + Group<Scalar: Zeroize + PrimeFieldBits>>(
pairs: &[(G::Scalar, G)],
) -> G {
pairs.iter().map(|(scalar, point)| *point * scalar).sum()
}
/// Performs a multiexponentiation in variable time, automatically selecting the optimal algorithm
/// based on the amount of pairs.
pub fn multiexp_vartime<G: Group<Scalar: PrimeFieldBits>>(pairs: &[(G::Scalar, G)]) -> G {
pairs.iter().map(|(scalar, point)| *point * scalar).sum()
}
}
pub use underlying::*;

View File

@@ -1,3 +1,5 @@
use alloc::vec;
use zeroize::Zeroize; use zeroize::Zeroize;
use ff::PrimeFieldBits; use ff::PrimeFieldBits;

View File

@@ -1,4 +1,4 @@
use std_shims::vec::Vec; use alloc::{vec, vec::Vec};
use zeroize::Zeroize; use zeroize::Zeroize;

View File

@@ -27,7 +27,7 @@ digest = { version = "0.11.0-rc.1", default-features = false, features = ["block
transcript = { package = "flexible-transcript", path = "../transcript", version = "^0.3.2", default-features = false, optional = true } transcript = { package = "flexible-transcript", path = "../transcript", version = "^0.3.2", default-features = false, optional = true }
ciphersuite = { path = "../ciphersuite", version = "^0.4.1", default-features = false } ciphersuite = { path = "../ciphersuite", version = "^0.4.1", default-features = false }
multiexp = { path = "../multiexp", version = "0.4", default-features = false, features = ["batch"], optional = true } multiexp = { path = "../multiexp", version = "0.4", default-features = false }
[dev-dependencies] [dev-dependencies]
hex = "0.4" hex = "0.4"
@@ -40,7 +40,7 @@ dalek-ff-group = { path = "../dalek-ff-group" }
ciphersuite = { path = "../ciphersuite" } ciphersuite = { path = "../ciphersuite" }
[features] [features]
alloc = ["zeroize/alloc", "digest/alloc", "ciphersuite/alloc", "multiexp"] alloc = ["zeroize/alloc", "digest/alloc", "ciphersuite/alloc", "multiexp/alloc", "multiexp/batch"]
aggregate = ["alloc", "transcript"] aggregate = ["alloc", "transcript"]
std = ["alloc", "std-shims/std", "rand_core/std", "zeroize/std", "transcript?/std", "ciphersuite/std", "multiexp/std"] std = ["alloc", "std-shims/std", "rand_core/std", "zeroize/std", "transcript?/std", "ciphersuite/std", "multiexp/std"]
default = ["std"] default = ["std"]

View File

@@ -23,8 +23,9 @@ use ciphersuite::{
}, },
GroupIo, GroupIo,
}; };
use multiexp::multiexp_vartime;
#[cfg(feature = "alloc")] #[cfg(feature = "alloc")]
use multiexp::{multiexp_vartime, BatchVerifier}; use multiexp::BatchVerifier;
/// Half-aggregation from <https://eprint.iacr.org/2021/350>. /// Half-aggregation from <https://eprint.iacr.org/2021/350>.
#[cfg(feature = "aggregate")] #[cfg(feature = "aggregate")]
@@ -109,12 +110,7 @@ impl<C: GroupIo> SchnorrSignature<C> {
/// different keys/messages. /// different keys/messages.
#[must_use] #[must_use]
pub fn verify(&self, public_key: C::G, challenge: C::F) -> bool { pub fn verify(&self, public_key: C::G, challenge: C::F) -> bool {
let statements = self.batch_statements(public_key, challenge); multiexp_vartime(&self.batch_statements(public_key, challenge)).is_identity().into()
#[cfg(feature = "alloc")]
let res = multiexp_vartime(&statements);
#[cfg(not(feature = "alloc"))]
let res = statements.into_iter().map(|(scalar, point)| point * scalar).sum::<C::G>();
res.is_identity().into()
} }
/// Queue a signature for batch verification. /// Queue a signature for batch verification.

View File

@@ -21,7 +21,7 @@ std-shims = { path = "../../common/std-shims", default-features = false }
flexible-transcript = { path = "../../crypto/transcript", default-features = false, features = ["recommended", "merlin"] } flexible-transcript = { path = "../../crypto/transcript", default-features = false, features = ["recommended", "merlin"] }
multiexp = { path = "../../crypto/multiexp", default-features = false, features = ["batch"], optional = true } multiexp = { path = "../../crypto/multiexp", default-features = false }
dalek-ff-group = { path = "../../crypto/dalek-ff-group", default-features = false } dalek-ff-group = { path = "../../crypto/dalek-ff-group", default-features = false }
minimal-ed448 = { path = "../../crypto/ed448", default-features = false } minimal-ed448 = { path = "../../crypto/ed448", default-features = false }
@@ -46,7 +46,8 @@ bitcoin-serai = { path = "../../networks/bitcoin", default-features = false, fea
alloc = [ alloc = [
"std-shims/alloc", "std-shims/alloc",
"multiexp", "multiexp/alloc",
"multiexp/batch",
"dalek-ff-group/alloc", "dalek-ff-group/alloc",
"minimal-ed448/alloc", "minimal-ed448/alloc",