Skip to content
Closed
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
50 changes: 42 additions & 8 deletions src/wnaf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,23 +150,42 @@ pub(crate) fn wnaf_form<S: AsRef<[u8]>>(wnaf: &mut Vec<i64>, c: S, window: usize
///
/// This function must be provided a `table` and `wnaf` that were constructed with
/// the same window size; otherwise, it may panic or produce invalid results.
#[inline]
pub(crate) fn wnaf_exp<G: Group>(table: &[G], wnaf: &[i64]) -> G {
let mut result = G::identity();
wnaf_multi_exp(&[table], &[wnaf])
}

/// Performs w-NAF multi-exponentiation using the interleaved window method, also known as
/// Straus' method.
///
/// The key insight is that when computing this sum by means of additions and doublings, the
/// doublings can be shared by performing the additions within an inner loop.
///
/// This function must be provided with `tables` and `wnafs` that were constructed with
/// the same window size; otherwise, it may panic or produce invalid results.
pub(crate) fn wnaf_multi_exp<G: Group>(tables: &[&[G]], wnafs: &[&[i64]]) -> G {

@TalDerei TalDerei May 30, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ACK – traced the impl; extends wNAF to MSM via Straus' method: (N * (doublings + adds)) to (doublings + (N * adds)), where N is # LC terms. doubling savings from interleaving windows (and it stacks on existing addition savings that use signed digits). fine for small MSMs, but obviously for larger N a hand rolled Pippenger impl downstream.

generated some of my own test vectors locally, but suggesting multiscalar coverage for wnaf_multi_exp / multiscalar_mul in the diff.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The wNAF implementations in both curve25519-dalek and libsecp256k1 provide both Straus and Pippenger and select between them at runtime, which would probably be ideal here as well, but that first needs a generic implementation of Pippenger

debug_assert_eq!(tables.len(), wnafs.len());

@TalDerei TalDerei May 30, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

promote to release assertion for mismatched lengths.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

At least for now it's not possible to trip this assertion as the equality is enforced at the type system level where WnafBase and WnafScalar have a const generic WINDOW_SIZE that's ensured equal by bounds.

I can promote it, but really it's belt-and-suspenders.

let window_size = wnafs.iter().map(|w| w.len()).max().unwrap_or(0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit; window_size is a misnomer here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

What would you suggest instead?


let mut result = G::identity();
let mut found_one = false;

for n in wnaf.iter().rev() {
for i in (0..window_size).rev() {
// Only double once per iteration of the loop
if found_one {
result = result.double();
}

if *n != 0 {
found_one = true;
for (&table, &wnaf) in tables.iter().zip(wnafs.iter()) {
let n = wnaf.get(i).copied().unwrap_or(0);
if n != 0 {
found_one = true;

if *n > 0 {
result += &table[(n / 2) as usize];
} else {
result -= &table[((-n) / 2) as usize];
if n > 0 {
result += &table[(n / 2) as usize];
} else {
result -= &table[((-n) / 2) as usize];
}
}
}
}
Expand Down Expand Up @@ -499,6 +518,21 @@ impl<G: Group, const WINDOW_SIZE: usize> WnafBase<G, WINDOW_SIZE> {

WnafBase { table }
}

/// Perform a multiscalar multiplication.
pub fn multiscalar_mul<'a, I, J>(scalars: I, bases: J) -> G
Comment on lines +522 to +523

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

referencing dalek (VartimeMultiscalarMul trait and corresponding impl in straus.rs), there's explicit timing language there. suggest a timing note that this runs in variable time and not be used with secret scalars (dalek frames it as operating on "public scalars"). maybe also rename to vartime_multiscalar_mul?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We tend to put vartime at the end of the method names BTW.

@TalDerei TalDerei May 30, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

gotcha, suggestion was matching dalek's naming convention.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

FWIW we also use *_vartime as a suffix in @RustCrypto projects. When looking at something like rustdoc it makes it easier to discover operations have constant time and variable time versions

where
I: Iterator<Item = &'a WnafScalar<G::Scalar, WINDOW_SIZE>>,
J: Iterator<Item = &'a Self>,
Comment on lines +524 to +526

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit; dalek uses IntoIterator.

{
let wnafs = scalars
.map(|scalar| scalar.wnaf.as_slice())
.collect::<Vec<_>>();

let tables = bases.map(|base| base.table.as_slice()).collect::<Vec<_>>();

wnaf_multi_exp(tables.as_slice(), wnafs.as_slice())
}
}

impl<G: Group, const WINDOW_SIZE: usize> Mul<&WnafScalar<G::Scalar, WINDOW_SIZE>>
Expand Down
Loading