Skip to content
Merged
Show file tree
Hide file tree
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
63 changes: 63 additions & 0 deletions w3f-plonk-common/src/cond_select.rs
Original file line number Diff line number Diff line change
@@ -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.

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.

Why not use https://github.com/dalek-cryptography/subtle here?

Arkworks doesn't have constant time below us, but nothing wrong in having our stuff be constant time, and hoping their improves. Also, one could write arkworks curve wrappers over curves that claim constant time, although whether this claims hold is another matter.

@davxy davxy Aug 10, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah subtle introduction is a nice follow-up. Perhaps I'll open a PR to introduce it.
But let's first ship these crates as downstream we need them for production asap

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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


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<F: 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<F: Field>(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<F: Field>: Sized {
/// `mask` must be 0 or 1.
fn select(mask: F, if_true: &Self, if_false: &Self) -> Self;
}

impl<C: TECurveConfig> PointSelect<C::BaseField> for TeProjective<C> {
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<C: SWCurveConfig> PointSelect<C::BaseField> for SwProjective<C> {
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::<Fq>(false), Fq::from(0));
assert_eq!(bit_to_field::<Fq>(true), Fq::from(1));
}
}
6 changes: 2 additions & 4 deletions w3f-plonk-common/src/gadgets/booleanity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -16,10 +17,7 @@ pub struct BitColumn<F: FftField> {

impl<F: FftField> BitColumn<F> {
pub fn init(bits: Vec<bool>, domain: &Domain<F>) -> 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();

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.

I'd probably have passed an iterator here, but whatever.

let col = domain.column(bits_as_field_elements);
Self { bits, col }
}
Expand Down
74 changes: 69 additions & 5 deletions w3f-plonk-common/src/gadgets/ec/mod.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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<F>,
points: AffineColumn<F, P>,
seed: P,
domain: &Domain<F>,
) -> Self {
) -> Self
where
P::Group: PointSelect<F>,
{
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();
Expand All @@ -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();
Expand Down Expand Up @@ -145,3 +152,60 @@ pub struct CondAddValues<F: Field, P: AffineRepr<BaseField = F>> {
pub acc: (F, F),
pub _phantom: PhantomData<P>,
}

#[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<F, P>()
where
F: FftField,
P: AffineRepr<BaseField = F>,
P::Group: PointSelect<F>,
{
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::<P, _>(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<P> = 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>();
}
}
1 change: 1 addition & 0 deletions w3f-plonk-common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
13 changes: 10 additions & 3 deletions w3f-ring-proof/src/piop/prover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -46,7 +47,10 @@ impl<F: PrimeField, G: AffineRepr<BaseField = F>> PiopProver<F, G> {
fixed_columns: FixedColumns<F, G>,
prover_index_in_keys: usize,
secret: G::ScalarField,
) -> Self {
) -> Self
where
G::Group: PointSelect<F>,
{
let domain = params.domain.clone();
let FixedColumns {
points,
Expand Down Expand Up @@ -81,8 +85,11 @@ impl<F: PrimeField, G: AffineRepr<BaseField = F>> PiopProver<F, G> {
index_in_keys: usize,
secret: G::ScalarField,
) -> BitColumn<F> {
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<bool> = (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);
Expand Down
9 changes: 6 additions & 3 deletions w3f-ring-vrf-snark/src/piop/params.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -71,8 +71,11 @@ impl<F: PrimeField, Curve: TECurveConfig<BaseField = F>> PiopParams<F, Curve> {
/// Represents `index` as a binary column.
pub fn pk_index_col(&self, index: usize) -> BitColumn<F> {
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)
}

Expand Down
Loading