From b24513a4e38b67c662c8f9da28d9b21e9aeb2616 Mon Sep 17 00:00:00 2001 From: Scott Arciszewski Date: Thu, 19 Mar 2026 11:36:25 -0400 Subject: [PATCH] Fix wNAF scalar multiplication for big-endian scalar repr Two bugs prevented wNAF from working with NIST/SEC1 curves: 1. The group crate's wnaf_form() assumes Scalar::to_repr() returns little-endian bytes, but ff::PrimeField documents repr endianness as implementation-specific. All SEC1 curves use big-endian. Fix: add WnafGroup::scalar_repr_to_le_bytes() with a default LE-passthrough; primeorder overrides it to reverse bytes. 2. wnaf_form() drops the final carry when the scalar fills all bit_len bits. This was masked on BLS12-381 (255-bit modulus in 256 bits) but fails on p256 (256-bit modulus in 256 bits). Fix: emit remaining carry after the loop (in group crate). Also implements recommended_wnaf_for_num_scalars (was todo!()). Co-Authored-By: Claude Opus 4.6 (1M context) --- p256/tests/projective.proptest-regressions | 1 + primeorder/src/projective.rs | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/p256/tests/projective.proptest-regressions b/p256/tests/projective.proptest-regressions index b332dce68..dc9f6ab50 100644 --- a/p256/tests/projective.proptest-regressions +++ b/p256/tests/projective.proptest-regressions @@ -5,3 +5,4 @@ # It is recommended to check this file in to source control so that # everyone who runs the test benefits from these saved cases. cc e19ee42c127b7289fbe7e42df47abf141eb644afcbd13ac141e39b9960362174 # shrinks to point = ProjectivePoint { x: FieldElement(0x823CD15F6DD3C71933565064513A6B2BD183E554C6A08622F713EBBBFACE98BE), y: FieldElement(0x55DF5D5850F47BAD82149139979369FE498A9022A412B5E0BEDD2CFC21C3ED91), z: FieldElement(0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5) }, scalar = Scalar(0x0000000000000000000000000000000000000000000000000000000000000001) +cc 67d76546dee30db7f75f666ed335f84d90da4ce8775d612dcdb88a3058ef7071 # shrinks to point = ProjectivePoint { x: FieldElement(0x823CD15F6DD3C71933565064513A6B2BD183E554C6A08622F713EBBBFACE98BE), y: FieldElement(0x55DF5D5850F47BAD82149139979369FE498A9022A412B5E0BEDD2CFC21C3ED91), z: FieldElement(0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5) }, scalar = Scalar(0xAE74000000000000000000000000000000000000000000000000000000000000) diff --git a/primeorder/src/projective.rs b/primeorder/src/projective.rs index 5a621dd92..17c304810 100644 --- a/primeorder/src/projective.rs +++ b/primeorder/src/projective.rs @@ -605,7 +605,25 @@ where FieldBytes: Copy, { fn recommended_wnaf_for_num_scalars(num_scalars: usize) -> usize { - todo!() + // Empirical heuristic from the zcash/bellman implementation. + if num_scalars >= 32 { + 3 + } else if num_scalars >= 1 { + 2 + } else { + 4 + } + } + + fn scalar_repr_to_le_bytes( + repr: & as PrimeField>::Repr, + ) -> Vec { + // SEC1/NIST curves use big-endian scalar representations; + // reverse to get little-endian for wNAF decomposition. + let mut le: Vec = + AsRef::<[u8]>::as_ref(repr).to_vec(); + le.reverse(); + le } }