From 9076a8327a73e1fb706c3dae156f8d010b38359a Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Fri, 14 Aug 2026 09:53:05 +0200 Subject: [PATCH] Select secret witness bits via subtle --- Cargo.toml | 3 +- w3f-plonk-common/Cargo.toml | 3 +- w3f-plonk-common/src/cond_select.rs | 118 +++++++++++++++------ w3f-plonk-common/src/gadgets/booleanity.rs | 7 +- w3f-plonk-common/src/gadgets/ec/mod.rs | 10 +- w3f-plonk-common/src/gadgets/inner_prod.rs | 65 +++++++++++- w3f-ring-proof/Cargo.toml | 2 +- w3f-ring-proof/src/lib.rs | 1 + w3f-ring-proof/src/piop/prover.rs | 12 ++- w3f-ring-proof/src/ring_prover.rs | 3 +- w3f-ring-vrf-snark/src/piop/params.rs | 6 +- w3f-ring-vrf-snark/src/piop/prover.rs | 6 +- w3f-ring-vrf-snark/src/ring_vrf_prover.rs | 3 +- 13 files changed, 188 insertions(+), 51 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0e56502a..79d7b10c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ ark-ec = { version = "0.6", default-features = false } ark-poly = { version = "0.6", default-features = false } ark-serialize = { version = "0.6", default-features = false, features = ["derive"] } w3f-pcs = { version = "0.0.7", default-features = false } -w3f-plonk-common = { version = "0.0.9", default-features = false } +w3f-plonk-common = { version = "0.0.10", path = "w3f-plonk-common", default-features = false } rayon = { version = "1", default-features = false } ark-transcript = { version = "0.0.6", default-features = false } blake2 = { version = "0.10", default-features = false } @@ -24,3 +24,4 @@ ark-ed-on-bls12-381-bandersnatch = { version = "0.6", default-features = false } criterion = { version = "0.7", features = ["html_reports"] } getrandom_or_panic = { version = "0.0.3", default-features = false } rand_core = "0.6" +subtle = { version = "2.6", default-features = false, features = ["const-generics"] } diff --git a/w3f-plonk-common/Cargo.toml b/w3f-plonk-common/Cargo.toml index 0f575c86..a1cc62fe 100644 --- a/w3f-plonk-common/Cargo.toml +++ b/w3f-plonk-common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "w3f-plonk-common" -version = "0.0.9" +version = "0.0.10" edition = "2021" authors = ["Sergey Vasilyev "] license = "MIT/Apache-2.0" @@ -18,6 +18,7 @@ w3f-pcs.workspace = true rayon = { workspace = true, optional = true } getrandom_or_panic.workspace = true rand_core.workspace = true +subtle.workspace = true [dev-dependencies] ark-ed-on-bls12-381-bandersnatch.workspace = true diff --git a/w3f-plonk-common/src/cond_select.rs b/w3f-plonk-common/src/cond_select.rs index 200e08a9..29da8d6b 100644 --- a/w3f-plonk-common/src/cond_select.rs +++ b/w3f-plonk-common/src/cond_select.rs @@ -1,51 +1,85 @@ -//! 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. +//! Constant-time handling of secret witness bits (the prover's ring +//! position and the blinding scalar bits). The secret bit is wrapped +//! into a `subtle::Choice` (an optimization barrier) on entry and +//! selection is limb-wise on the raw Montgomery representation, so no +//! field arithmetic -- and none of arkworks' data-dependent conditional +//! reductions -- ever touches the bits. Defense in depth, not a complete +//! countermeasure: the unconditional point additions run variable-time +//! arkworks field arithmetic on secret-derived accumulator values, and +//! the column FFTs and polynomial commitment MSMs downstream 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; +use ark_ff::{BigInt, Field, Fp, FpConfig}; +use ark_std::marker::PhantomData; +use subtle::ConditionallySelectable; + +pub use subtle::Choice; + +/// Conversion into [`Choice`]. The `bool` impl applies subtle's +/// optimization barrier, preventing the compiler from branching on the +/// bit later. (`From for Choice` does not exist upstream, hence +/// this local trait.) +pub trait IntoChoice { + fn into_choice(self) -> Choice; +} + +impl IntoChoice for Choice { + fn into_choice(self) -> Choice { + self + } +} + +impl IntoChoice for bool { + fn into_choice(self) -> Choice { + Choice::from(self as u8) + } +} /// 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() +pub fn bit_to_field(bit: bool) -> F { + F::select(bit, &F::one(), &F::zero()) } -/// 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) +/// Constant-time two-way select. +pub trait CondSelect: Sized { + fn select(bit: impl IntoChoice, if_true: &Self, if_false: &Self) -> Self; } -/// 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, const N: usize> CondSelect for Fp { + fn select(bit: impl IntoChoice, if_true: &Self, if_false: &Self) -> Self { + let limbs = + <[u64; N]>::conditional_select(&if_false.0 .0, &if_true.0 .0, bit.into_choice()); + Fp(BigInt(limbs), PhantomData) + } } -impl PointSelect for TeProjective { - fn select(mask: C::BaseField, if_true: &Self, if_false: &Self) -> Self { +impl CondSelect for TeProjective +where + C::BaseField: CondSelect, +{ + fn select(bit: impl IntoChoice, if_true: &Self, if_false: &Self) -> Self { + let choice = bit.into_choice(); 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), + CondSelect::select(choice, &if_true.x, &if_false.x), + CondSelect::select(choice, &if_true.y, &if_false.y), + CondSelect::select(choice, &if_true.t, &if_false.t), + CondSelect::select(choice, &if_true.z, &if_false.z), ) } } -impl PointSelect for SwProjective { - fn select(mask: C::BaseField, if_true: &Self, if_false: &Self) -> Self { +impl CondSelect for SwProjective +where + C::BaseField: CondSelect, +{ + fn select(bit: impl IntoChoice, if_true: &Self, if_false: &Self) -> Self { + let choice = bit.into_choice(); 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), + CondSelect::select(choice, &if_true.x, &if_false.x), + CondSelect::select(choice, &if_true.y, &if_false.y), + CondSelect::select(choice, &if_true.z, &if_false.z), ) } } @@ -53,11 +87,31 @@ impl PointSelect for SwProjective { #[cfg(test)] mod tests { use super::*; - use ark_ed_on_bls12_381_bandersnatch::Fq; + use ark_ed_on_bls12_381_bandersnatch::{EdwardsProjective, Fq}; + use ark_std::{test_rng, UniformRand}; #[test] fn bit_lift_is_exact() { assert_eq!(bit_to_field::(false), Fq::from(0)); assert_eq!(bit_to_field::(true), Fq::from(1)); } + + // The selected values feed committed columns, so they are + // consensus-critical: the select must return the operand bit for bit, + // not merely an equivalent representation. + #[test] + fn select_returns_exact_operand() { + let rng = &mut test_rng(); + let a = Fq::rand(rng); + let b = Fq::rand(rng); + assert_eq!(Fq::select(true, &a, &b), a); + assert_eq!(Fq::select(false, &a, &b), b); + + let p = EdwardsProjective::rand(rng); + let q = EdwardsProjective::rand(rng); + let s = EdwardsProjective::select(true, &p, &q); + assert_eq!((s.x, s.y, s.t, s.z), (p.x, p.y, p.t, p.z)); + let s = EdwardsProjective::select(false, &p, &q); + assert_eq!((s.x, s.y, s.t, s.z), (q.x, q.y, q.t, q.z)); + } } diff --git a/w3f-plonk-common/src/gadgets/booleanity.rs b/w3f-plonk-common/src/gadgets/booleanity.rs index a2e693de..08b9363e 100644 --- a/w3f-plonk-common/src/gadgets/booleanity.rs +++ b/w3f-plonk-common/src/gadgets/booleanity.rs @@ -4,7 +4,7 @@ use ark_poly::{Evaluations, GeneralEvaluationDomain, Polynomial}; use ark_std::{vec, vec::Vec}; -use crate::cond_select::bit_to_field; +use crate::cond_select::{bit_to_field, CondSelect}; use crate::domain::Domain; use crate::gadgets::VerifierGadget; use crate::{const_evals, Column, FieldColumn}; @@ -16,7 +16,10 @@ pub struct BitColumn { } impl BitColumn { - pub fn init(bits: Vec, domain: &Domain) -> Self { + pub fn init(bits: Vec, domain: &Domain) -> Self + where + F: CondSelect, + { 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 c5c872ad..c62d9e05 100644 --- a/w3f-plonk-common/src/gadgets/ec/mod.rs +++ b/w3f-plonk-common/src/gadgets/ec/mod.rs @@ -1,4 +1,4 @@ -use crate::cond_select::{bit_to_field, PointSelect}; +use crate::cond_select::CondSelect; use crate::domain::Domain; use crate::gadgets::booleanity::BitColumn; use crate::{Column, FieldColumn}; @@ -90,7 +90,7 @@ where domain: &Domain, ) -> Self where - P::Group: PointSelect, + P::Group: CondSelect, { debug_assert_eq!(bitmask.payload_len(), domain.capacity - 1); debug_assert_eq!(points.payload_len(), domain.capacity - 1); @@ -103,7 +103,7 @@ where .map(|(&bit, point)| { let mut sum = projective_acc; sum += point; - projective_acc = P::Group::select(bit_to_field(bit), &sum, &projective_acc); + projective_acc = P::Group::select(bit, &sum, &projective_acc); projective_acc }) .collect(); @@ -166,9 +166,9 @@ mod tests { // before the hardening were built from. fn acc_matches_naive_accumulation() where - F: FftField, + F: FftField + CondSelect, P: AffineRepr, - P::Group: PointSelect, + P::Group: CondSelect, { let rng = &mut test_rng(); let domain = Domain::test_domain(256, true); diff --git a/w3f-plonk-common/src/gadgets/inner_prod.rs b/w3f-plonk-common/src/gadgets/inner_prod.rs index cb460a9d..0f855ff3 100644 --- a/w3f-plonk-common/src/gadgets/inner_prod.rs +++ b/w3f-plonk-common/src/gadgets/inner_prod.rs @@ -4,7 +4,9 @@ use ark_poly::{Evaluations, GeneralEvaluationDomain}; use ark_std::{vec, vec::Vec}; +use crate::cond_select::CondSelect; use crate::domain::Domain; +use crate::gadgets::booleanity::BitColumn; use crate::gadgets::{ProverGadget, VerifierGadget}; use crate::{Column, FieldColumn}; @@ -39,6 +41,27 @@ impl InnerProd { } } + /// Same as `init` with `b` the field representation of `bits`, but the + /// products are computed with a constant-time select, so the + /// accumulation does not branch on the bits. + pub fn init_bits(a: FieldColumn, bits: &BitColumn, domain: &Domain) -> Self + where + F: CondSelect, + { + assert_eq!(a.payload_len(), domain.capacity - 1); + assert_eq!(bits.payload_len(), domain.capacity - 1); + let inner_prods = Self::partial_bit_prods(a.payload(), &bits.bits); + let mut acc = vec![F::zero()]; + acc.extend(inner_prods); + let acc = domain.column(acc); + Self { + a, + b: bits.col.clone(), + not_last: domain.not_last_row.clone(), + acc, + } + } + /// Returns a[0]b[0], a[0]b[0] + a[1]b[1], ..., a[0]b[0] + a[1]b[1] + ... + a[n-1]b[n-1] fn partial_inner_prods(a: &[F], b: &[F]) -> Vec { assert_eq!(a.len(), b.len()); @@ -50,6 +73,21 @@ impl InnerProd { }) .collect() } + + fn partial_bit_prods(a: &[F], bits: &[bool]) -> Vec + where + F: CondSelect, + { + assert_eq!(a.len(), bits.len()); + a.iter() + .zip(bits) + .scan(F::zero(), |state, (&a, &bit)| { + let sum = *state + a; + *state = F::select(bit, &sum, state); + Some(*state) + }) + .collect() + } } impl ProverGadget for InnerProd { @@ -92,7 +130,7 @@ mod tests { use ark_std::test_rng; use crate::domain::Domain; - use crate::test_helpers::random_vec; + use crate::test_helpers::{random_bitvec, random_vec}; use super::*; @@ -131,4 +169,29 @@ mod tests { _test_inner_prod_gadget(false); _test_inner_prod_gadget(true); } + + // The acc column is committed, so its values are consensus-critical: + // the branch-free bit accumulation must produce exactly the column the + // naive product accumulation produces. Only the payload rows are + // compared: the trailing zk rows are random per column instance. + #[test] + fn bit_acc_matches_naive_accumulation() { + let rng = &mut test_rng(); + let domain = Domain::test_domain(256, true); + let a: Vec = random_vec(domain.capacity - 1, rng); + let bits = random_bitvec(domain.capacity - 1, 0.5, rng); + + let naive = InnerProd::init( + domain.column(a.clone()), + domain.column(bits.iter().map(|&bit| Fq::from(bit)).collect()), + &domain, + ); + let bit_col = BitColumn::init(bits, &domain); + let gadget = InnerProd::init_bits(domain.column(a), &bit_col, &domain); + + assert_eq!( + gadget.acc.evals.evals[..domain.capacity], + naive.acc.evals.evals[..domain.capacity] + ); + } } diff --git a/w3f-ring-proof/Cargo.toml b/w3f-ring-proof/Cargo.toml index c27233b0..0837ed0b 100644 --- a/w3f-ring-proof/Cargo.toml +++ b/w3f-ring-proof/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "w3f-ring-proof" -version = "0.0.9" +version = "0.0.10" edition = "2021" authors = ["Sergey Vasilyev "] license = "MIT/Apache-2.0" diff --git a/w3f-ring-proof/src/lib.rs b/w3f-ring-proof/src/lib.rs index 72865c88..46bc54f9 100644 --- a/w3f-ring-proof/src/lib.rs +++ b/w3f-ring-proof/src/lib.rs @@ -5,6 +5,7 @@ use ark_std::rand::RngCore; use w3f_pcs::pcs::PCS; pub use piop::index; +pub use w3f_plonk_common::cond_select::CondSelect; pub use w3f_plonk_common::domain::Domain; use w3f_plonk_common::Proof; diff --git a/w3f-ring-proof/src/piop/prover.rs b/w3f-ring-proof/src/piop/prover.rs index 8dcaa6aa..eed0bf3b 100644 --- a/w3f-ring-proof/src/piop/prover.rs +++ b/w3f-ring-proof/src/piop/prover.rs @@ -12,7 +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::cond_select::CondSelect; use w3f_plonk_common::domain::Domain; use w3f_plonk_common::gadgets::booleanity::{BitColumn, Booleanity}; use w3f_plonk_common::gadgets::ec::AffineColumn; @@ -49,7 +49,8 @@ impl> PiopProver { secret: G::ScalarField, ) -> Self where - G::Group: PointSelect, + F: CondSelect, + G::Group: CondSelect, { let domain = params.domain.clone(); let FixedColumns { @@ -58,7 +59,7 @@ impl> PiopProver { } = fixed_columns; let bits = Self::bits_column(¶ms, prover_index_in_keys, secret); let booleanity = Booleanity::init(bits.clone()); - let inner_prod = InnerProd::init(ring_selector.clone(), bits.col.clone(), &domain); + let inner_prod = InnerProd::init_bits(ring_selector.clone(), &bits, &domain); let inner_prod_acc = FixedCells::init(inner_prod.acc.clone(), &domain, F::zero(), F::one()); let cond_add = CondAdd::init(bits.clone(), points.clone(), params.seed, &domain); let (seed_x, seed_y) = params.seed.xy().unwrap(); @@ -84,7 +85,10 @@ impl> PiopProver { params: &PiopParams, index_in_keys: usize, secret: G::ScalarField, - ) -> BitColumn { + ) -> BitColumn + where + F: CondSelect, + { // 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) diff --git a/w3f-ring-proof/src/ring_prover.rs b/w3f-ring-proof/src/ring_prover.rs index 19862666..09312596 100644 --- a/w3f-ring-proof/src/ring_prover.rs +++ b/w3f-ring-proof/src/ring_prover.rs @@ -1,6 +1,7 @@ use ark_ec::twisted_edwards::{Affine, TECurveConfig}; use ark_ff::PrimeField; use w3f_pcs::pcs::PCS; +use w3f_plonk_common::cond_select::CondSelect; use w3f_plonk_common::piop::ProverPiop; use w3f_plonk_common::prover::PlonkProver; use w3f_plonk_common::transcript::PlonkTranscript; @@ -27,7 +28,7 @@ where impl RingProver where - F: PrimeField, + F: PrimeField + CondSelect, CS: PCS, Curve: TECurveConfig, T: PlonkTranscript, diff --git a/w3f-ring-vrf-snark/src/piop/params.rs b/w3f-ring-vrf-snark/src/piop/params.rs index 7fae509c..d2b55f8d 100644 --- a/w3f-ring-vrf-snark/src/piop/params.rs +++ b/w3f-ring-vrf-snark/src/piop/params.rs @@ -4,6 +4,7 @@ use ark_ff::{BigInteger, PrimeField}; use ark_std::vec::Vec; use crate::piop::FixedColumns; +use w3f_plonk_common::cond_select::CondSelect; use w3f_plonk_common::domain::Domain; use w3f_plonk_common::gadgets::booleanity::BitColumn; use w3f_plonk_common::gadgets::ec::te_doubling::Doubling; @@ -69,7 +70,10 @@ impl> PiopParams { } /// Represents `index` as a binary column. - pub fn pk_index_col(&self, index: usize) -> BitColumn { + pub fn pk_index_col(&self, index: usize) -> BitColumn + where + F: CondSelect, + { assert!(index < self.max_keys()); // The index is the prover's ring position: an equality scan avoids a // secret-index memory write. diff --git a/w3f-ring-vrf-snark/src/piop/prover.rs b/w3f-ring-vrf-snark/src/piop/prover.rs index cc73f17c..ec0e6942 100644 --- a/w3f-ring-vrf-snark/src/piop/prover.rs +++ b/w3f-ring-vrf-snark/src/piop/prover.rs @@ -9,6 +9,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::CondSelect; use w3f_plonk_common::domain::Domain; use w3f_plonk_common::gadgets::booleanity::{BitColumn, Booleanity}; use w3f_plonk_common::gadgets::ec::AffineColumn; @@ -91,7 +92,10 @@ impl> PiopProver { pk_index: usize, sk: Curve::ScalarField, vrf_in: Affine, - ) -> Self { + ) -> Self + where + F: CondSelect, + { let domain = params.domain.clone(); let FixedColumns { diff --git a/w3f-ring-vrf-snark/src/ring_vrf_prover.rs b/w3f-ring-vrf-snark/src/ring_vrf_prover.rs index fe14cfbe..5ef30406 100644 --- a/w3f-ring-vrf-snark/src/ring_vrf_prover.rs +++ b/w3f-ring-vrf-snark/src/ring_vrf_prover.rs @@ -1,6 +1,7 @@ use ark_ec::twisted_edwards::{Affine, TECurveConfig}; use ark_ff::PrimeField; use w3f_pcs::pcs::PCS; +use w3f_plonk_common::cond_select::CondSelect; use w3f_plonk_common::piop::ProverPiop; use w3f_plonk_common::prover::PlonkProver; use w3f_plonk_common::transcript::PlonkTranscript; @@ -25,7 +26,7 @@ where impl RingVrfProver where - F: PrimeField, + F: PrimeField + CondSelect, CS: PCS, Curve: TECurveConfig, T: PlonkTranscript,