diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b156653..6607146 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ ubuntu-latest, windows-latest, macOS-latest ] + os: [ubuntu-latest, windows-latest, macOS-latest] steps: - uses: actions/checkout@v4 - name: Run tests @@ -24,7 +24,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ ubuntu-latest, windows-latest, macOS-latest ] + os: [ubuntu-latest, windows-latest, macOS-latest] steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable @@ -37,43 +37,42 @@ jobs: - name: Verify working directory is clean (excluding lockfile) run: git diff --exit-code ':!Cargo.lock' - # The scripts embedded in this job are having trouble parsing our branch names - # build-nodefault: - # name: Build target ${{ matrix.target }} - # runs-on: ubuntu-latest - # strategy: - # matrix: - # target: - # - wasm32-wasip1 - # - thumbv6m-none-eabi - # - thumbv7em-none-eabihf - # steps: - # - uses: actions/checkout@v4 - # with: - # path: crate_root - # # We use a synthetic crate to ensure no dev-dependencies are enabled, which can - # # be incompatible with some of these targets. - # - name: Create synthetic crate for testing - # run: cargo init --edition 2021 --lib ci-build - # - name: Copy Rust version into synthetic crate - # run: cp crate_root/rust-toolchain.toml ci-build/ - # - name: Copy patch directives into synthetic crate - # run: | - # echo "[patch.crates-io]" >> ./ci-build/Cargo.toml - # cat ./crate_root/Cargo.toml | sed "0,/.\+\(patch.crates.\+\)/d" >> ./ci-build/Cargo.toml - # - name: Add no_std pragma to lib.rs - # run: | - # echo "#![no_std]" > ./ci-build/src/lib.rs - # - name: Add group as a dependency of the synthetic crate - # working-directory: ./ci-build - # # run: cargo add --no-default-features --path ../crate_root - # run: sed -i 's;\[dependencies\];\[dependencies\]\nrustcrypto-group = { path = "../crate_root", default-features = false };g' ./Cargo.toml - # - name: Add target - # working-directory: ./ci-build - # run: rustup target add ${{ matrix.target }} - # - name: Build for target - # working-directory: ./ci-build - # run: cargo build --verbose --target ${{ matrix.target }} + build-nodefault: + name: Build target ${{ matrix.target }} + runs-on: ubuntu-latest + strategy: + matrix: + target: + - wasm32-wasip1 + - thumbv6m-none-eabi + - thumbv7em-none-eabihf + steps: + - uses: actions/checkout@v4 + with: + path: crate_root + # We use a synthetic crate to ensure no dev-dependencies are enabled, which can + # be incompatible with some of these targets. + - name: Create synthetic crate for testing + run: cargo init --edition 2021 --lib ci-build + - name: Copy Rust version into synthetic crate + run: cp crate_root/rust-toolchain.toml ci-build/ + - name: Copy patch directives into synthetic crate + run: | + echo "[patch.crates-io]" >> ./ci-build/Cargo.toml + cat ./crate_root/Cargo.toml | sed "0,/.\+\(patch.crates.\+\)/d" >> ./ci-build/Cargo.toml + - name: Add no_std pragma to lib.rs + run: | + echo "#![no_std]" > ./ci-build/src/lib.rs + - name: Add group as a dependency of the synthetic crate + working-directory: ./ci-build + # run: cargo add --no-default-features --path ../crate_root + run: sed -i 's;\[dependencies\];\[dependencies\]\ngroup = { path = "../crate_root", default-features = false };g' ./Cargo.toml + - name: Add target + working-directory: ./ci-build + run: rustup target add ${{ matrix.target }} + - name: Build for target + working-directory: ./ci-build + run: cargo build --verbose --target ${{ matrix.target }} doc-links: name: Intra-doc links @@ -82,7 +81,6 @@ jobs: - uses: actions/checkout@v4 - run: cargo fetch # Requires #![deny(rustdoc::broken_intra_doc_links)] in crates. - - run: sudo apt-get -y install libfontconfig1-dev - name: Check intra-doc links run: cargo doc --all-features --document-private-items diff --git a/CHANGELOG.md b/CHANGELOG.md index d7434ec..7cf6177 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,14 +6,38 @@ and this library adheres to Rust's notion of [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + +## [0.14.0] - 2026-06-01 +### Added +- `group::CurveAffine`, an affine-representation trait that the curve-specific + affine traits are now built on top of. +- `group::Group::mul_by_generator`, with a default implementation. Implementors + can override it to take advantage of precomputed tables. +- `group::Group::try_random(rng: &mut R) -> Result`, + a new trait method that must be implemented by downstreams. It samples a + non-identity group element using a fallible RNG and propagates the RNG's error. + ### Changed -- MSRV is now 1.63.0. -- Migrated to `ff 0.14`, `rand_core 0.9`. +- MSRV is now 1.85.0. +- Bumped dependencies to `ff 0.14`, `rand_core 0.10`. - `group::Group::random(rng: impl RngCore) -> Self` has been changed to - `Group::random(rng: &mut R) -> Self`, to enable passing a - trait object as the RNG. -- `group::Group::try_from_rng` is a new trait method that must be implemented by - downstreams. `Group::random` now has a default implementation that calls it. + `Group::random(rng: &mut R) -> Self`, to enable passing a + trait object as the RNG. It now has a default implementation in terms of + `Group::try_random`. +- The curve-related traits have been refactored around the new `CurveAffine` + trait: + - `group::Curve::AffineRepr` has been renamed to `Curve::Affine`. + - All of the trait methods and associated types on the following traits have + been removed (use `group::Curve::Affine` or the `group::CurveAffine` trait + instead; trait implementors must implement `group::CurveAffine` instead + using the same logic): + - `group::cofactor::CofactorCurve` + - `group::cofactor::CofactorCurveAffine` + - `group::prime::PrimeCurve` + - `group::prime::PrimeCurveAffine` + - `group::cofactor::CofactorCurveAffine` and `group::prime::PrimeCurveAffine` + now have blanket implementations for all types `C: group::CurveAffine` where + `C::Curve` implements `CofactorCurve` or `PrimeCurve` respectively. ## [0.13.0] - 2022-12-06 ### Changed diff --git a/src/wnaf.rs b/src/wnaf.rs index 5adaf66..8946141 100644 --- a/src/wnaf.rs +++ b/src/wnaf.rs @@ -150,23 +150,45 @@ pub(crate) fn wnaf_form>(wnaf: &mut Vec, 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(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, W: AsRef<[i64]>>( + tables: &[T], + wnafs: &[W], +) -> G { + debug_assert_eq!(tables.len(), wnafs.len()); + let window_size = wnafs.iter().map(|w| w.as_ref().len()).max().unwrap_or(0); + 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.as_ref().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.as_ref()[(n / 2) as usize]; + } else { + result -= table.as_ref()[((-n) / 2) as usize]; + } } } } @@ -499,6 +521,20 @@ impl WnafBase { WnafBase { table } } + + /// Perform a multiscalar multiplication. + /// + /// Computes a sum-of-products `aA + bB + ...` in variable time with w-NAF multi-exponentiation + /// using the interleaved window method, also known as Straus' method. + pub fn multiscalar_mul(scalars: I, bases: J) -> G + where + I: IntoIterator>, + J: IntoIterator, + { + let wnafs = scalars.into_iter().map(|s| s.wnaf).collect::>(); + let tables = bases.into_iter().map(|b| b.table).collect::>(); + wnaf_multi_exp(tables.as_slice(), wnafs.as_slice()) + } } impl Mul<&WnafScalar>