From 785fbf0e72e628ed9e9025863acf12ea82a3a75d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Garillot?= Date: Wed, 21 Feb 2024 22:27:35 -0500 Subject: [PATCH] feat: Introduce field operations with a new `transcript` module - 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. --- Cargo.toml | 1 + src/lib.rs | 1 + src/transcript.rs | 217 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 219 insertions(+) create mode 100644 src/transcript.rs diff --git a/Cargo.toml b/Cargo.toml index 78ba2ccca..0162b8a9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 diff --git a/src/lib.rs b/src/lib.rs index 3e6d02274..09ae7002f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,6 +15,7 @@ mod bellpepper; mod circuit; mod digest; mod nifs; +mod transcript; // public modules pub mod constants; diff --git a/src/transcript.rs b/src/transcript.rs new file mode 100644 index 000000000..83141a657 --- /dev/null +++ b/src/transcript.rs @@ -0,0 +1,217 @@ +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 { + type A: Array; + + fn limb(self) -> SmallVec; +} + +/// There is a blanket reflexive implementation for `Limbable` +impl Limbable for F { + type A = [F; 1]; + + fn limb(self) -> SmallVec { + 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` 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* `::NativeField`, + type NativeField: PrimeField; + + /// Write the representation of `Self` as `NativeField` elements to a compatible sink + fn write_natively>( + &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, +} + +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<::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(&self, field_sink: &mut W) -> Result<(), io::Error> + where + W: FieldWriter, + ::NativeField: Limbable<::NativeField>; +} + +/// 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 FieldWritableExt for T { + fn write(&self, field_sink: &mut W) -> Result<(), io::Error> + where + W: FieldWriter, + ::NativeField: Limbable<::NativeField>, + { + self.write_natively(&mut FieldEncodingWriter::< + '_, + ::NativeField, + W, + >::new(field_sink)) + } +} + +#[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 for Base { + type A = [Scalar; 2]; + + fn limb(self) -> smallvec::SmallVec { + 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 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 FieldWritable for Commitment`` + // (with NativeField = E::Base), the compiler will complain that the type parameter `E` is not constrained. + // + // This is because the commitment type Commitment could be involved in another engine (and in fact is, if + // you consider e.g. Bn256EngineZM vs. Bn256EngineKZG, which share the same ::CE). Two instances + // would apply for the same concrete type: + // - impl FieldWritable for Commitment + // - impl FieldWritable for Commitment + // + // But we can still define the monomorphized instances! + impl FieldWritable for Commitment { + type NativeField = Base; + + // *** For demo purposes only *** + fn write_natively>( + &self, + field_sink: &mut W, + ) -> Result<(), std::io::Error> { + let (x, y, _) = self.to_coordinates(); + field_sink.write(&[x, y]) + } + } + impl FieldWritable for Commitment { + type NativeField = Scalar; + + // *** For demo purposes only *** + fn write_natively>( + &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, + } + + 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::::default(); // identity element with coordinates in bn256::Base, + let commitment_dual_base = Commitment::>::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); + } +}