Skip to content

Commit

Permalink
Guard ScalarField byte representations to always be little-endian (#38
Browse files Browse the repository at this point in the history
)

fix: guard `ScalarField` to be little-endian
  • Loading branch information
jonathanpwang authored May 19, 2023
1 parent 55eebd4 commit 0948203
Show file tree
Hide file tree
Showing 8 changed files with 112 additions and 44 deletions.
8 changes: 1 addition & 7 deletions halo2-base/src/gates/flex_gate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1107,13 +1107,7 @@ impl<F: ScalarField> GateInstructions<F> for GateChip<F> {
a: AssignedValue<F>,
range_bits: usize,
) -> Vec<AssignedValue<F>> {
let a_bytes = a.value().to_repr();
let bits = a_bytes
.as_ref()
.iter()
.flat_map(|byte| (0..8u32).map(|i| (*byte as u64 >> i) & 1))
.map(|x| Witness(F::from(x)))
.take(range_bits);
let bits = a.value().to_u64_limbs(range_bits, 1).into_iter().map(|x| Witness(F::from(x)));

let mut bit_cells = Vec::with_capacity(range_bits);
let row_offset = ctx.advice.len();
Expand Down
2 changes: 1 addition & 1 deletion halo2-base/src/gates/tests/flex_gate_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ pub fn test_is_equal<F: ScalarField>(inputs: &[QuantumCell<F>]) -> F {
*a.value()
}

#[test_case((Fr::one(), 2) => vec![Fr::one(), Fr::zero()] ; "num_to_bits(): 1 -> [1, 0]")]
#[test_case((Fr::from(6u64), 3) => vec![Fr::zero(), Fr::one(), Fr::one()] ; "num_to_bits(): 6")]
pub fn test_num_to_bits<F: ScalarField>(input: (F, usize)) -> Vec<F> {
let mut builder = GateThreadBuilder::mock();
let ctx = builder.main(0);
Expand Down
15 changes: 15 additions & 0 deletions halo2-base/src/gates/tests/pos_prop_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,21 @@ proptest! {
prop_assert_eq!(result, ground_truth);
}

#[test]
fn prop_test_num_to_bits(num in any::<u64>()) {
let mut tmp = num;
let mut bits = vec![];
if num == 0 {
bits.push(0);
}
while tmp > 0 {
bits.push(tmp & 1);
tmp /= 2;
}
let result = flex_gate_tests::test_num_to_bits((Fr::from(num), bits.len()));
prop_assert_eq!(bits.into_iter().map(Fr::from).collect::<Vec<_>>(), result);
}

/*
#[test]
fn prop_test_lagrange_eval(inputs in vec(rand_fr(), 3)) {
Expand Down
1 change: 1 addition & 0 deletions halo2-base/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ pub mod utils;
/// Constant representing whether the Layouter calls `synthesize` once just to get region shape.
#[cfg(feature = "halo2-axiom")]
pub const SKIP_FIRST_PASS: bool = false;
/// Constant representing whether the Layouter calls `synthesize` once just to get region shape.
#[cfg(feature = "halo2-pse")]
pub const SKIP_FIRST_PASS: bool = true;

Expand Down
116 changes: 87 additions & 29 deletions halo2-base/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ pub trait BigPrimeField: ScalarField {
#[cfg(feature = "halo2-axiom")]
impl<F> BigPrimeField for F
where
F: FieldExt + Hash + Into<[u64; 4]> + From<[u64; 4]>,
F: ScalarField + From<[u64; 4]>, // Assume [u64; 4] is little-endian. We only implement ScalarField when this is true.
{
#[inline(always)]
fn from_u64_digits(val: &[u64]) -> Self {
Expand All @@ -30,38 +30,36 @@ where
}
}

/// Helper trait to convert to and from a [ScalarField] by decomposing its an field element into [u64] limbs.
/// Helper trait to represent a field element that can be converted into [u64] limbs.
///
/// Note: Since the number of bits necessary to represent a field element is larger than the number of bits in a u64, we decompose the bit representation of the field element into multiple [u64] values e.g. `limbs`.
#[cfg(feature = "halo2-axiom")]
/// Note: Since the number of bits necessary to represent a field element is larger than the number of bits in a u64, we decompose the integer representation of the field element into multiple [u64] values e.g. `limbs`.
pub trait ScalarField: FieldExt + Hash {
/// Returns the base `2<sup>bit_len</sup>` little endian representation of the [ScalarField] element up to `num_limbs` number of limbs (truncates any extra limbs).
///
/// Assumes `bit_len < 64`.
/// * `num_limbs`: number of limbs to return
/// * `bit_len`: number of bits in each limb
fn to_u64_limbs(self, num_limbs: usize, bit_len: usize) -> Vec<u64>;
}
#[cfg(feature = "halo2-axiom")]
impl<F> ScalarField for F
where
F: FieldExt + Hash + Into<[u64; 4]>,
{
#[inline(always)]
fn to_u64_limbs(self, num_limbs: usize, bit_len: usize) -> Vec<u64> {
// Basically same as `to_repr` but does not go further into bytes
let tmp: [u64; 4] = self.into();
decompose_u64_digits_to_limbs(tmp, num_limbs, bit_len)

/// Returns the little endian byte representation of the element.
fn to_bytes_le(&self) -> Vec<u8>;

/// Creates a field element from a little endian byte representation.
///
/// The default implementation assumes that `PrimeField::from_repr` is implemented for little-endian.
/// It should be overriden if this is not the case.
fn from_bytes_le(bytes: &[u8]) -> Self {
let mut repr = Self::Repr::default();
repr.as_mut()[..bytes.len()].copy_from_slice(bytes);
Self::from_repr(repr).unwrap()
}
}
// See below for implementations

// Later: will need to separate BigPrimeField from ScalarField when Goldilocks is introduced

#[cfg(feature = "halo2-pse")]
pub trait BigPrimeField = FieldExt<Repr = [u8; 32]> + Hash;

#[cfg(feature = "halo2-pse")]
pub trait ScalarField = FieldExt + Hash;
pub trait BigPrimeField = FieldExt<Repr = [u8; 32]> + ScalarField;

/// Converts an [Iterator] of u64 digits into `number_of_limbs` limbs of `bit_len` bits returned as a [Vec].
///
Expand Down Expand Up @@ -149,10 +147,8 @@ pub fn biguint_to_fe<F: BigPrimeField>(e: &BigUint) -> F {

#[cfg(feature = "halo2-pse")]
{
let mut repr = F::Repr::default();
let bytes = e.to_bytes_le();
repr.as_mut()[..bytes.len()].copy_from_slice(&bytes);
F::from_repr(repr).unwrap()
F::from_bytes_le(&bytes)
}
}

Expand All @@ -171,9 +167,7 @@ pub fn bigint_to_fe<F: BigPrimeField>(e: &BigInt) -> F {
#[cfg(feature = "halo2-pse")]
{
let (sign, bytes) = e.to_bytes_le();
let mut repr = F::Repr::default();
repr.as_mut()[..bytes.len()].copy_from_slice(&bytes);
let f_abs = F::from_repr(repr).unwrap();
let f_abs = F::from_bytes_le(&bytes);
if sign == Sign::Minus {
-f_abs
} else {
Expand All @@ -184,12 +178,14 @@ pub fn bigint_to_fe<F: BigPrimeField>(e: &BigInt) -> F {

/// Converts an immutable reference to an PrimeField element into a [BigUint] element.
/// * `fe`: immutable reference to PrimeField element to convert
pub fn fe_to_biguint<F: ff::PrimeField>(fe: &F) -> BigUint {
BigUint::from_bytes_le(fe.to_repr().as_ref())
pub fn fe_to_biguint<F: ScalarField>(fe: &F) -> BigUint {
BigUint::from_bytes_le(fe.to_bytes_le().as_ref())
}

/// Converts an immutable reference to a [BigPrimeField] element into a [BigInt] element.
/// * `fe`: immutable reference to [BigPrimeField] element to convert
/// Converts a [BigPrimeField] element into a [BigInt] element by sending `fe` in `[0, F::modulus())` to
/// ```
/// fe < F::modulus() / 2 ? fe : fe - F::modulus()
/// ```
pub fn fe_to_bigint<F: BigPrimeField>(fe: &F) -> BigInt {
// TODO: `F` should just have modulus as lazy_static or something
let modulus = modulus::<F>();
Expand Down Expand Up @@ -332,6 +328,7 @@ pub fn compose(input: Vec<BigUint>, bit_len: usize) -> BigUint {
#[cfg(feature = "halo2-axiom")]
pub use halo2_proofs_axiom::halo2curves::CurveAffineExt;

/// Helper trait
#[cfg(feature = "halo2-pse")]
pub trait CurveAffineExt: CurveAffine {
/// Unlike the `Coordinates` trait, this just returns the raw affine (X, Y) coordinantes without checking `is_on_curve`
Expand All @@ -343,6 +340,67 @@ pub trait CurveAffineExt: CurveAffine {
#[cfg(feature = "halo2-pse")]
impl<C: CurveAffine> CurveAffineExt for C {}

mod scalar_field_impls {
use super::{decompose_u64_digits_to_limbs, ScalarField};
use crate::halo2_proofs::halo2curves::{
bn256::{Fq as bn254Fq, Fr as bn254Fr},
secp256k1::{Fp as secpFp, Fq as secpFq},
};
#[cfg(feature = "halo2-pse")]
use ff::PrimeField;

/// To ensure `ScalarField` is only implemented for `ff:Field` where `Repr` is little endian, we use the following macro
/// to implement the trait for each field.
#[cfg(feature = "halo2-axiom")]
#[macro_export]
macro_rules! impl_scalar_field {
($field:ident) => {
impl ScalarField for $field {
#[inline(always)]
fn to_u64_limbs(self, num_limbs: usize, bit_len: usize) -> Vec<u64> {
// Basically same as `to_repr` but does not go further into bytes
let tmp: [u64; 4] = self.into();
decompose_u64_digits_to_limbs(tmp, num_limbs, bit_len)
}

#[inline(always)]
fn to_bytes_le(&self) -> Vec<u8> {
let tmp: [u64; 4] = (*self).into();
tmp.iter().flat_map(|x| x.to_le_bytes()).collect()
}
}
};
}

/// To ensure `ScalarField` is only implemented for `ff:Field` where `Repr` is little endian, we use the following macro
/// to implement the trait for each field.
#[cfg(feature = "halo2-pse")]
#[macro_export]
macro_rules! impl_scalar_field {
($field:ident) => {
impl ScalarField for $field {
#[inline(always)]
fn to_u64_limbs(self, num_limbs: usize, bit_len: usize) -> Vec<u64> {
let bytes = self.to_repr();
let digits = (0..4)
.map(|i| u64::from_le_bytes(bytes[i * 8..(i + 1) * 8].try_into().unwrap()));
decompose_u64_digits_to_limbs(digits, num_limbs, bit_len)
}

#[inline(always)]
fn to_bytes_le(&self) -> Vec<u8> {
self.to_repr().to_vec()
}
}
};
}

impl_scalar_field!(bn254Fr);
impl_scalar_field!(bn254Fq);
impl_scalar_field!(secpFp);
impl_scalar_field!(secpFq);
}

/// Module for reading parameters for Halo2 proving system from the file system.
pub mod fs {
use std::{
Expand Down
2 changes: 1 addition & 1 deletion hashes/zkevm-keccak/src/keccak_packed_multi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1941,7 +1941,7 @@ pub fn keccak_phase0<F: Field>(
.take(4)
.map(|a| {
pack_with_base::<F>(&unpack(a[0]), 2)
.to_repr()
.to_bytes_le()
.into_iter()
.take(8)
.collect::<Vec<_>>()
Expand Down
8 changes: 4 additions & 4 deletions hashes/zkevm-keccak/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ pub fn pack_part(bits: &[u8], info: &PartInfo) -> u64 {
/// Unpack a sparse keccak word into bits in the range [0,BIT_SIZE[
pub fn unpack<F: Field>(packed: F) -> [u8; NUM_BITS_PER_WORD] {
let mut bits = [0; NUM_BITS_PER_WORD];
let packed = Word::from_little_endian(packed.to_repr().as_ref());
let packed = Word::from_little_endian(packed.to_bytes_le().as_ref());
let mask = Word::from(BIT_SIZE - 1);
for (idx, bit) in bits.iter_mut().enumerate() {
*bit = ((packed >> (idx * BIT_COUNT)) & mask).as_u32() as u8;
Expand All @@ -200,10 +200,10 @@ pub fn pack_u64<F: Field>(value: u64) -> F {
/// Calculates a ^ b with a and b field elements
pub fn field_xor<F: Field>(a: F, b: F) -> F {
let mut bytes = [0u8; 32];
for (idx, (a, b)) in a.to_repr().as_ref().iter().zip(b.to_repr().as_ref().iter()).enumerate() {
bytes[idx] = *a ^ *b;
for (idx, (a, b)) in a.to_bytes_le().into_iter().zip(b.to_bytes_le()).enumerate() {
bytes[idx] = a ^ b;
}
F::from_repr(bytes).unwrap()
F::from_bytes_le(&bytes)
}

/// Returns the size (in bits) of each part size when splitting up a keccak word
Expand Down
4 changes: 2 additions & 2 deletions hashes/zkevm-keccak/src/util/eth_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ impl<F: Field> ToScalar<F> for U256 {
fn to_scalar(&self) -> Option<F> {
let mut bytes = [0u8; 32];
self.to_little_endian(&mut bytes);
F::from_repr(bytes).into()
Some(F::from_bytes_le(&bytes))
}
}

Expand Down Expand Up @@ -113,7 +113,7 @@ impl<F: Field> ToScalar<F> for Address {
let mut bytes = [0u8; 32];
bytes[32 - Self::len_bytes()..].copy_from_slice(self.as_bytes());
bytes.reverse();
F::from_repr(bytes).into()
Some(F::from_bytes_le(&bytes))
}
}

Expand Down

0 comments on commit 0948203

Please sign in to comment.