From 65d1f2cc08da007c53de9b1a10ed89b41c4effb5 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Tue, 4 Aug 2026 14:35:55 +0200 Subject: [PATCH] Remove secret-dependent branches and memory indexing from witness generation --- w3f-plonk-common/src/cond_select.rs | 63 ++++++++++++++++++ w3f-plonk-common/src/gadgets/booleanity.rs | 6 +- w3f-plonk-common/src/gadgets/ec/mod.rs | 74 ++++++++++++++++++++-- w3f-plonk-common/src/lib.rs | 1 + w3f-ring-proof/src/piop/prover.rs | 13 +++- w3f-ring-vrf-snark/src/piop/params.rs | 9 ++- 6 files changed, 151 insertions(+), 15 deletions(-) create mode 100644 w3f-plonk-common/src/cond_select.rs diff --git a/w3f-plonk-common/src/cond_select.rs b/w3f-plonk-common/src/cond_select.rs new file mode 100644 index 00000000..200e08a9 --- /dev/null +++ b/w3f-plonk-common/src/cond_select.rs @@ -0,0 +1,63 @@ +//! Branch-free handling of secret witness bits (the prover's ring position +//! and the blinding scalar bits). These helpers keep secret-dependent +//! branches and memory indexing out of witness generation. They are defense +//! in depth, not a complete side-channel countermeasure: later pipeline +//! stages (in particular the polynomial commitment MSMs) still process the +//! witness in variable time. + +use ark_ec::short_weierstrass::{Projective as SwProjective, SWCurveConfig}; +use ark_ec::twisted_edwards::{Projective as TeProjective, TECurveConfig}; +use ark_ff::Field; + +/// Lifts a bit to a field element without branching on its value. +/// +/// `F::from(bool)` bottoms out in arkworks' `from_bigint`, which returns +/// early for zero; mapping the bit to 1 or 2 first routes both values +/// through the same Montgomery conversion. +pub fn bit_to_field(bit: bool) -> F { + F::from(bit as u64 + 1) - F::one() +} + +/// Arithmetic two-way select. `mask` must be 0 or 1. +pub fn field_select(mask: F, if_true: F, if_false: F) -> F { + if_false + mask * (if_true - if_false) +} + +/// Coordinate-wise arithmetic select between two curve points. +pub trait PointSelect: Sized { + /// `mask` must be 0 or 1. + fn select(mask: F, if_true: &Self, if_false: &Self) -> Self; +} + +impl PointSelect for TeProjective { + fn select(mask: C::BaseField, if_true: &Self, if_false: &Self) -> Self { + Self::new_unchecked( + field_select(mask, if_true.x, if_false.x), + field_select(mask, if_true.y, if_false.y), + field_select(mask, if_true.t, if_false.t), + field_select(mask, if_true.z, if_false.z), + ) + } +} + +impl PointSelect for SwProjective { + fn select(mask: C::BaseField, if_true: &Self, if_false: &Self) -> Self { + Self::new_unchecked( + field_select(mask, if_true.x, if_false.x), + field_select(mask, if_true.y, if_false.y), + field_select(mask, if_true.z, if_false.z), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use ark_ed_on_bls12_381_bandersnatch::Fq; + + #[test] + fn bit_lift_is_exact() { + assert_eq!(bit_to_field::(false), Fq::from(0)); + assert_eq!(bit_to_field::(true), Fq::from(1)); + } +} diff --git a/w3f-plonk-common/src/gadgets/booleanity.rs b/w3f-plonk-common/src/gadgets/booleanity.rs index e8fa94e5..a2e693de 100644 --- a/w3f-plonk-common/src/gadgets/booleanity.rs +++ b/w3f-plonk-common/src/gadgets/booleanity.rs @@ -4,6 +4,7 @@ use ark_poly::{Evaluations, GeneralEvaluationDomain, Polynomial}; use ark_std::{vec, vec::Vec}; +use crate::cond_select::bit_to_field; use crate::domain::Domain; use crate::gadgets::VerifierGadget; use crate::{const_evals, Column, FieldColumn}; @@ -16,10 +17,7 @@ pub struct BitColumn { impl BitColumn { pub fn init(bits: Vec, domain: &Domain) -> Self { - let bits_as_field_elements = bits - .iter() - .map(|&b| if b { F::one() } else { F::zero() }) - .collect(); + let bits_as_field_elements = bits.iter().map(|&bit| bit_to_field(bit)).collect(); let col = domain.column(bits_as_field_elements); Self { bits, col } } diff --git a/w3f-plonk-common/src/gadgets/ec/mod.rs b/w3f-plonk-common/src/gadgets/ec/mod.rs index 7c54d379..c5c872ad 100644 --- a/w3f-plonk-common/src/gadgets/ec/mod.rs +++ b/w3f-plonk-common/src/gadgets/ec/mod.rs @@ -1,3 +1,4 @@ +use crate::cond_select::{bit_to_field, PointSelect}; use crate::domain::Domain; use crate::gadgets::booleanity::BitColumn; use crate::{Column, FieldColumn}; @@ -79,12 +80,18 @@ where // Both SW and TE gadgets use non-complete formulas, so special cases have to be avoided. // If we assume the proofs of possession have been verified for the ring points, // this can be achieved by setting the seed to a point of unknown dlog from the prime order subgroup. + // The bits are secret (the prover's ring position and blinding scalar), so the + // accumulation adds unconditionally and selects the result arithmetically + // rather than branching on them. pub fn init( bitmask: BitColumn, points: AffineColumn, seed: P, domain: &Domain, - ) -> Self { + ) -> Self + where + P::Group: PointSelect, + { debug_assert_eq!(bitmask.payload_len(), domain.capacity - 1); debug_assert_eq!(points.payload_len(), domain.capacity - 1); let not_last = domain.not_last_row.clone(); @@ -93,10 +100,10 @@ where .bits .iter() .zip(points.points.iter()) - .map(|(&b, point)| { - if b { - projective_acc += point; - } + .map(|(&bit, point)| { + let mut sum = projective_acc; + sum += point; + projective_acc = P::Group::select(bit_to_field(bit), &sum, &projective_acc); projective_acc }) .collect(); @@ -145,3 +152,60 @@ pub struct CondAddValues> { pub acc: (F, F), pub _phantom: PhantomData

, } + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_helpers::{random_bitvec, random_vec}; + use ark_ed_on_bls12_381_bandersnatch::{EdwardsAffine, SWAffine}; + use ark_std::test_rng; + + // The acc column is committed, so its values are consensus-critical: + // every row produced by the branch-free accumulation must equal the naive + // conditional sum that the constraints encode and that proofs generated + // before the hardening were built from. + fn acc_matches_naive_accumulation() + where + F: FftField, + P: AffineRepr, + P::Group: PointSelect, + { + let rng = &mut test_rng(); + let domain = Domain::test_domain(256, true); + let bits = random_bitvec(domain.capacity - 1, 0.5, rng); + let points = random_vec::(domain.capacity - 1, rng); + let seed = P::generator(); + + let gadget = CondAdd::init( + BitColumn::init(bits.clone(), &domain), + AffineColumn::column(points.clone(), &domain), + seed, + &domain, + ); + + let mut acc = seed.into_group(); + let expected: Vec

= bits + .iter() + .zip(&points) + .map(|(&bit, point)| { + if bit { + acc += point; + } + acc.into_affine() + }) + .collect(); + + assert_eq!(gadget.acc.payload()[0], seed); + assert_eq!(&gadget.acc.payload()[1..], &expected[..]); + } + + #[test] + fn te_acc_matches_naive_accumulation() { + acc_matches_naive_accumulation::<_, EdwardsAffine>(); + } + + #[test] + fn sw_acc_matches_naive_accumulation() { + acc_matches_naive_accumulation::<_, SWAffine>(); + } +} diff --git a/w3f-plonk-common/src/lib.rs b/w3f-plonk-common/src/lib.rs index 7f0f89d4..18e2ee44 100644 --- a/w3f-plonk-common/src/lib.rs +++ b/w3f-plonk-common/src/lib.rs @@ -9,6 +9,7 @@ use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use ark_std::{vec, vec::Vec}; use w3f_pcs::pcs::{Commitment, PCS}; +pub mod cond_select; pub mod domain; pub mod gadgets; pub mod kzg_acc; diff --git a/w3f-ring-proof/src/piop/prover.rs b/w3f-ring-proof/src/piop/prover.rs index 8235cb44..8dcaa6aa 100644 --- a/w3f-ring-proof/src/piop/prover.rs +++ b/w3f-ring-proof/src/piop/prover.rs @@ -12,6 +12,7 @@ use w3f_pcs::pcs::Commitment; use crate::piop::params::PiopParams; use crate::piop::FixedColumns; use crate::piop::{RingCommitments, RingEvaluations}; +use w3f_plonk_common::cond_select::PointSelect; use w3f_plonk_common::domain::Domain; use w3f_plonk_common::gadgets::booleanity::{BitColumn, Booleanity}; use w3f_plonk_common::gadgets::ec::AffineColumn; @@ -46,7 +47,10 @@ impl> PiopProver { fixed_columns: FixedColumns, prover_index_in_keys: usize, secret: G::ScalarField, - ) -> Self { + ) -> Self + where + G::Group: PointSelect, + { let domain = params.domain.clone(); let FixedColumns { points, @@ -81,8 +85,11 @@ impl> PiopProver { index_in_keys: usize, secret: G::ScalarField, ) -> BitColumn { - let mut keyset_part = vec![false; params.keyset_part_size]; - keyset_part[index_in_keys] = true; + // The index is the prover's ring position: an equality scan avoids the + // secret-index memory write of `keyset_part[index_in_keys] = true`. + let keyset_part: Vec = (0..params.keyset_part_size) + .map(|position| position == index_in_keys) + .collect(); let scalar_part = params.scalar_part(secret); let bits = [keyset_part, scalar_part].concat(); assert_eq!(bits.len(), params.domain.capacity - 1); diff --git a/w3f-ring-vrf-snark/src/piop/params.rs b/w3f-ring-vrf-snark/src/piop/params.rs index c275b657..7fae509c 100644 --- a/w3f-ring-vrf-snark/src/piop/params.rs +++ b/w3f-ring-vrf-snark/src/piop/params.rs @@ -1,7 +1,7 @@ use ark_ec::twisted_edwards::{Affine, TECurveConfig}; use ark_ec::AffineRepr; use ark_ff::{BigInteger, PrimeField}; -use ark_std::{vec, vec::Vec}; +use ark_std::vec::Vec; use crate::piop::FixedColumns; use w3f_plonk_common::domain::Domain; @@ -71,8 +71,11 @@ impl> PiopParams { /// Represents `index` as a binary column. pub fn pk_index_col(&self, index: usize) -> BitColumn { assert!(index < self.max_keys()); - let mut col = vec![false; self.max_keys()]; - col[index] = true; + // The index is the prover's ring position: an equality scan avoids a + // secret-index memory write. + let col = (0..self.max_keys()) + .map(|position| position == index) + .collect(); BitColumn::init(col, &self.domain) }