Skip to content

Commit

Permalink
feat: Introduce field operations with a new transcript module
Browse files Browse the repository at this point in the history
- Created `src/transcript.rs` with concepts and traits for field operations.
- Defined a new `Limbable` trait for encoding translation between fields.
- Implemented `FieldWriter` and `FieldWritable` traits for respectively writing and producing field elements.
- Presented `FieldWritableExt` extension trait to accommodate different target field types.
- Added `FieldEncodingWriter` structure, a translating field writer.
- Integrated `smallvec` library with `const_generics` feature to implement it efficiently.
- Added tests for the `src/transcript.rs` module and its functionalities.
  • Loading branch information
huitseeker committed Feb 22, 2024
1 parent 346b9cf commit 0e57337
Show file tree
Hide file tree
Showing 3 changed files with 209 additions and 0 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ ref-cast = "1.0.20" # allocation-less conversion in multilinear polys
derive_more = "0.99.17" # lightens impl macros for pasta
static_assertions = "1.1.0"
rayon-scan = "0.1.0"
smallvec = { version = "1.13.1", features = ["const_generics"] }

[target.'cfg(any(target_arch = "x86_64", target_arch = "aarch64"))'.dependencies]
# grumpkin-msm has been patched to support MSMs for the pasta curve cycle
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ mod bellpepper;
mod circuit;
mod digest;
mod nifs;
mod transcript;

// public modules
pub mod constants;
Expand Down
207 changes: 207 additions & 0 deletions src/transcript.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
use ff::PrimeField;
use smallvec::{Array, SmallVec};
use std::{io, marker::PhantomData};

/// This encodes the translation between field elements through limbing:
/// `Self` can be represented as an array of limbs, where each limb is a `F`.
// The API uses SmallVec to cheapen the allocation w.r.t a full Vec.
pub trait Limbable<F: PrimeField> {
type A: Array<Item = F>;

fn limb(self) -> SmallVec<Self::A>;
}

/// There is a blanket reflexive implementation for `Limbable<F, 1>`
impl<F: PrimeField> Limbable<F> for F {
type A = [F; 1];

fn limb(self) -> SmallVec<Self::A> {
SmallVec::from_elem(self, 1)
}
}

/// This is a simple proxy of std::io::Write that exposes writing field elements, rather than bytes.
// The claim is there should be one and only one `FieldWriter<F>` for each algebraic transcript, using its
// native field.
pub trait FieldWriter {
type NativeField: PrimeField;
fn write(&mut self, field_elts: &[Self::NativeField]) -> Result<(), io::Error>;
fn flush(&mut self) -> Result<(), io::Error>;
}

/// This expresses that `Self` is writable as a sequence of field elements given a `FieldWriter` with a compatible (equal) native field type.
pub trait FieldWritable {
/// The natural field type for this `FieldWritable`.
/// The associated type forces Self to pick a *single* `<Self as FieldWritable>::NativeField`,
type NativeField: PrimeField;

/// Write the representation of `Self` as `NativeField` elements to a compatible sink
fn write_natively<W: Sized + FieldWriter<NativeField = Self::NativeField>>(
&self,
field_sink: &mut W,
) -> Result<(), io::Error>;
}

// We can create a translating FieldWriter using this generic struct
pub struct FieldEncodingWriter<'a, F: PrimeField, W: FieldWriter> {
writer: &'a mut W,
_phantom: PhantomData<F>,
}

impl<'a, F: PrimeField, W: FieldWriter> FieldEncodingWriter<'a, F, W> {
pub fn new(writer: &'a mut W) -> FieldEncodingWriter<'a, F, W> {
FieldEncodingWriter {
writer,
_phantom: PhantomData,
}
}
}

// It's a valid FieldWriter, for the field F instead of W's native field
impl<'a, F, W> FieldWriter for FieldEncodingWriter<'a, F, W>
where
// F is a field limbable in the native field of W
F: PrimeField + Limbable<<W as FieldWriter>::NativeField>,
W: FieldWriter,
{
type NativeField = F;

fn write(&mut self, field_elts: &[F]) -> Result<(), io::Error> {
for origin_elt in field_elts.iter() {
self.writer.write(&origin_elt.limb())?;
}
Ok(())
}

fn flush(&mut self) -> Result<(), io::Error> {
self.writer.flush()
}
}

/// This is the functionality we want: we can write `Self` as a sequence of field elements, even if the chosen target type does
/// not match the native field type of `Self`. We don't want to let folks chose how to implement this, so we'll make this an extension trait.
pub trait FieldWritableExt: FieldWritable {
/// this indicates how to write `Self` as a sequence of `TargetField` field elements,
/// as long as the NativeField of `Self`'s `FieldWritable` is Limbable into `TargetField`.
fn write<W>(&self, field_sink: &mut W) -> Result<(), io::Error>
where
W: FieldWriter,
<Self as FieldWritable>::NativeField: Limbable<<W as FieldWriter>::NativeField> {
self.write_natively(&mut FieldEncodingWriter::new(field_sink))
}
}

/// This is a blanket implementation: the only requirements are having a `FieldWritable` for the
/// native field of `Self`, and that this native field is Limbable into `TargetField`.
impl<T: FieldWritable> FieldWritableExt for T {}

#[cfg(test)]
mod tests {

use ff::PrimeField;
use num_bigint::BigUint;
use num_traits::Num;

pub use halo2curves::bn256::{Fq as Base, Fr as Scalar};

use super::{FieldWritable, FieldWritableExt, FieldWriter, Limbable};
use crate::{
provider::{Bn256EngineKZG, GrumpkinEngine},
traits::{commitment::CommitmentTrait, Dual},
Commitment,
};

#[test]
fn sanity_check() {
let bmod = BigUint::from_str_radix(&Base::MODULUS[2..], 16).unwrap();
let smod = BigUint::from_str_radix(&Scalar::MODULUS[2..], 16).unwrap();

assert!(smod < bmod);
}

// *** For demo purposes only ***
// TODO: move out of this test module, since any competing implementaiton in the main code will make
// compilation under test fail for duplicate implementations of the same trait
impl Limbable<Scalar> for Base {
type A = [Scalar; 2];

fn limb(self) -> smallvec::SmallVec<Self::A> {
let mut bytes = self.to_repr();
let (bytes_low, bytes_high) = bytes.split_at_mut(16);
let arr_low: [u8; 16] = bytes_low.try_into().unwrap();
let arr_high: [u8; 16] = bytes_high.try_into().unwrap();
let f1 = Scalar::from_u128(u128::from_le_bytes(arr_low));
let f2 = Scalar::from_u128(u128::from_le_bytes(arr_high));
smallvec::smallvec![f1, f2]
}
}

// We don't need to define Limbable<Base> for Base, since it's part of the blanket implementation

// Commitments from an Engine are natively writeable as E::Base elements
// but if we attempt to write a generic `impl<E: Engine> FieldWritable for Commitment<E>``
// (with NativeField = E::Base), the compiler will complain that the type parameter `E` is not constrained.
//
// This is because the commitment type Commitment<E> could be involved in another engine (and in fact is, if
// you consider e.g. Bn256EngineZM vs. Bn256EngineKZG, which share the same <E as Engine>::CE). Two instances
// would apply for the same concrete type:
// - impl FieldWritable for Commitment<Bn256EngineZM>
// - impl FieldWritable for Commitment<Bn256EngineKZG>
//
// But we can still define the monomorphized instances!
impl FieldWritable for Commitment<Bn256EngineKZG> {
type NativeField = Base;

// *** For demo purposes only ***
fn write_natively<W: Sized + FieldWriter<NativeField = Base>>(
&self,
field_sink: &mut W,
) -> Result<(), std::io::Error> {
let (x, y, _) = self.to_coordinates();
field_sink.write(&[x, y])
}
}
impl FieldWritable for Commitment<GrumpkinEngine> {
type NativeField = Scalar;

// *** For demo purposes only ***
fn write_natively<W: Sized + FieldWriter<NativeField = Scalar>>(
&self,
field_sink: &mut W,
) -> Result<(), std::io::Error> {
let (x, y, _) = self.to_coordinates();
field_sink.write(&[x, y])
}
}

// Let's build a trivial MockWriter
struct MockWriter {
written: Vec<Scalar>,
}

impl FieldWriter for MockWriter {
type NativeField = Scalar;

fn write(&mut self, field_elts: &[Self::NativeField]) -> Result<(), std::io::Error> {
self.written.extend(field_elts.iter().cloned());
Ok(())
}

fn flush(&mut self) -> Result<(), std::io::Error> {
Ok(())
}
}

#[test]
fn it_works() {
let commitment_native_base = Commitment::<Bn256EngineKZG>::default(); // identity element with coordinates in bn256::Base,
let commitment_dual_base = Commitment::<Dual<Bn256EngineKZG>>::default(); // identity element with coordinates in GrumpkinEngine::Base,
let mut writer = MockWriter {
written: Vec::new(),
}; // note: this works natively only on bn256::Scalar

commitment_native_base.write(&mut writer).unwrap(); // the instance above fires, this writes 2 x 2 = 4 elements
commitment_dual_base.write(&mut writer).unwrap(); // the blanket reflexive instance of Limbable fires, this writes 2 elements
assert_eq!(writer.written.len(), 6);
}
}

0 comments on commit 0e57337

Please sign in to comment.