From f579b6e659e7764936c390d93f472ec3f389cc9a Mon Sep 17 00:00:00 2001 From: Artem Storozhuk Date: Tue, 4 Feb 2025 19:25:18 +0200 Subject: [PATCH 1/3] example: Add example of Projected column usage --- examples/Cargo.toml | 4 + examples/acc-projected.rs | 248 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 252 insertions(+) create mode 100644 examples/acc-projected.rs diff --git a/examples/Cargo.toml b/examples/Cargo.toml index dfef66255..40afea006 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -92,6 +92,10 @@ path = "acc-shifted.rs" name = "acc-packed" path = "acc-packed.rs" +[[example]] +name = "acc-projected" +path = "acc-projected.rs" + [lints.clippy] needless_range_loop = "allow" diff --git a/examples/acc-projected.rs b/examples/acc-projected.rs new file mode 100644 index 000000000..8d4de121d --- /dev/null +++ b/examples/acc-projected.rs @@ -0,0 +1,248 @@ +use binius_circuits::{builder::ConstraintSystemBuilder, unconstrained::unconstrained}; +use binius_core::{constraint_system::validate::validate_witness, oracle::ProjectionVariant}; +use binius_field::{arch::OptimalUnderlier, BinaryField128b, BinaryField8b}; + +type U = OptimalUnderlier; +type F128 = BinaryField128b; +type F8 = BinaryField8b; + +#[derive(Clone)] +struct U8U128ProjectionInfo { + log_size: usize, + decimal: usize, + binary: Vec, + variant: ProjectionVariant, +} + +// The idea behind projection is that data from a column of some given field (F8) +// can be interpreted as a data of some or greater field (F128) and written to another column with equal or smaller length, +// which depends on LOG_SIZE and values of projection. Also two possible variants of projections are available, which +// has significant impact on input data processing. +// In the following example we have input column with bytes (u8) projected to the output column with u128 values. +fn projection( + builder: &mut ConstraintSystemBuilder, + projection_info: U8U128ProjectionInfo, + namespace: &str, +) { + builder.push_namespace(format!("projection {}", namespace)); + + let input = + unconstrained::(builder, "in", projection_info.clone().log_size).unwrap(); + + let projected = builder + .add_projected( + "projected", + input, + projection_info.clone().binary, + projection_info.clone().variant, + ) + .unwrap(); + + if let Some(witness) = builder.witness() { + let input_values = witness.get::(input).unwrap().as_slice::(); + let mut projected_witness = witness.new_column::(projected); + let projected_values = projected_witness.as_mut_slice::(); + + assert_eq!(projected_values.len(), projection_info.expected_projection_len()); + + match projection_info.variant { + ProjectionVariant::FirstVars => { + // Quite elaborated regularity, on my opinion + for idx in 0..projected_values.len() { + projected_values[idx] = F128::new( + input_values[(idx + * 2usize.pow(projection_info.clone().binary.len() as u32)) + + projection_info.clone().decimal] as u128, + ); + } + } + ProjectionVariant::LastVars => { + // decimal representation of the binary values is used as a simple offset + for idx in 0..projected_values.len() { + projected_values[idx] = + F128::new(input_values[projection_info.clone().decimal + idx] as u128); + } + } + }; + } + builder.pop_namespace(); +} + +impl U8U128ProjectionInfo { + fn new( + log_size: usize, + decimal: usize, + binary: Vec, + variant: ProjectionVariant, + ) -> U8U128ProjectionInfo { + assert!(log_size >= binary.len()); + + if variant == ProjectionVariant::LastVars { + // Pad with zeroes to LOG_SIZE len iterator. + // In this case we interpret binary values in a reverse order, meaning that the very first + // element is elder byte, so zeroes must be explicitly appended + let mut binary_clone = binary.clone(); + let mut zeroes = vec![F128::new(0u128); log_size - binary.len()]; + binary_clone.append(&mut zeroes); + + let coefficients = (0..binary_clone.len()) + .map(|degree| F128::new(2usize.pow(degree as u32) as u128)) + .collect::>(); + + let value = binary_clone + .iter() + .zip(coefficients.iter().rev()) + .fold(F128::new(0u128), |acc, (byte, coefficient)| acc + (*byte) * (*coefficient)); + + assert_eq!(decimal as u128, value.val()); + } + + U8U128ProjectionInfo { + log_size, + decimal, + binary, + variant, + } + } + + fn expected_projection_len(&self) -> usize { + 2usize.pow((self.log_size - self.binary.len()) as u32) + } +} + +fn main() { + let allocator = bumpalo::Bump::new(); + let mut builder = ConstraintSystemBuilder::::new_with_witness(&allocator); + + let projection_data = U8U128ProjectionInfo::new( + 4usize, + 9usize, + vec![ + F128::from(1u128), + F128::from(0u128), + F128::from(0u128), + F128::from(1u128), + ], + ProjectionVariant::FirstVars, + ); + projection(&mut builder, projection_data, "test_1"); + + let projection_data = U8U128ProjectionInfo::new( + 16usize, + 34816usize, + vec![ + F128::from(1u128), + F128::from(0u128), + F128::from(0u128), + F128::from(0u128), + F128::from(1u128), + ], + ProjectionVariant::LastVars, + ); + projection(&mut builder, projection_data, "test_2"); + + let projection_data = U8U128ProjectionInfo::new( + 4usize, + 15usize, + vec![ + F128::from(1u128), + F128::from(1u128), + F128::from(1u128), + F128::from(1u128), + ], + ProjectionVariant::LastVars, + ); + projection(&mut builder, projection_data, "test_3"); + + let projection_data = U8U128ProjectionInfo::new( + 6usize, + 60usize, + vec![ + F128::from(1u128), + F128::from(1u128), + F128::from(1u128), + F128::from(1u128), + ], + ProjectionVariant::LastVars, + ); + /* + With projection_data defined above we have 2^LOG_SIZE = 2^6 bytes in the input, + the size of projection is computed as follows: 2.pow(LOG_SIZE - binary.len()) = 2.pow(6 - 4) = 4. + the index of the input byte to use as projection is computed as follows (according to + a LastVars projection variant regularity): + + idx + decimal, e.g.: + + 0 + 60 + 1 + 60 + 2 + 60 + 3 + 60 + + where idx is [0..4]. + + Memory layout: + + input: [a5, a2, b1, 60, 91, ed, 5e, fb, ae, 1c, b2, 14, 92, 73, 92, c8, 56, 6d, fa, de, a8, 46, 77, 48, e1, cc, 90, 75, 78, d5, 19, be, 0c, 86, 39, 28, 0c, cc, e9, 4e, 46, d9, 84, 65, 4a, a2, b4, 64, eb, 59, 7b, fd, 3f, 0e, 2d, ea, 06, 42, a9, ea, (19), (8f), (19), (52)], len: 64 + output: [ + BinaryField128b(0x00000000000000000000000000000019), + BinaryField128b(0x0000000000000000000000000000008f), + BinaryField128b(0x00000000000000000000000000000019), + BinaryField128b(0x00000000000000000000000000000052) + ] + */ + projection(&mut builder, projection_data, "test_4"); + + let projection_data = U8U128ProjectionInfo::new( + 4usize, + 15usize, + vec![ + F128::from(1u128), + F128::from(1u128), + F128::from(1u128), + F128::from(1u128), + ], + ProjectionVariant::FirstVars, + ); + projection(&mut builder, projection_data, "test_5"); + + let projection_data = U8U128ProjectionInfo::new( + 6usize, + 13usize, + vec![ + F128::from(1u128), + F128::from(0u128), + F128::from(1u128), + F128::from(1u128), + ], + ProjectionVariant::FirstVars, + ); + /* + With projection_data defined above we have 2^LOG_SIZE = 2^6 bytes in the input, + the size of projection is computed as follows: 2.pow(LOG_SIZE - binary.len()) = 2.pow(6 - 4) = 4. + the index of the input byte to use as projection is computed as follows: + + idx * 2usize.pow(binary.len()) + decimal, e.g.: + + 0 * 2.pow(4) + 13 = 13, so input[13] + 1 * 2.pow(4) + 13 = 29, so input[29] + 2 * 2.pow(4) + 13 = 45, so input[45] + 3 * 2.pow(4) + 13 = 61, so input[61] + + where idx is [0..4] according to a FirstVars projection variant regularity. + + Memory layout: + + input: [18, d8, 58, d3, 24, f1, 8b, ec, 74, 1c, ab, 78, 13, (3e), 57, d7, 36, 15, 54, 50, 9a, cb, 98, 90, 58, cb, 79, 05, 83, (72), ea, 4d, f6, 3d, f3, 2f, af, e3, 32, 11, c9, 97, fb, ba, 24, (36), e9, 38, 7e, c7, a9, 68, bf, 31, 51, cf, 7b, 12, 20, 53, d8, (df), d7, cc], len: 64 + + output: BinaryField128b(0x0000000000000000000000000000003e) + output: BinaryField128b(0x00000000000000000000000000000072) + output: BinaryField128b(0x00000000000000000000000000000036) + output: BinaryField128b(0x000000000000000000000000000000df) + */ + projection(&mut builder, projection_data, "test_6"); + + let witness = builder.take_witness().unwrap(); + let cs = builder.build().unwrap(); + + validate_witness(&cs, &[], &witness).unwrap(); +} From 5902adce01cdd09287f325154f69ec66ce0d0023 Mon Sep 17 00:00:00 2001 From: Artem Storozhuk Date: Wed, 5 Feb 2025 15:13:26 +0200 Subject: [PATCH 2/3] example: Add example of Repeated column usage --- examples/Cargo.toml | 4 ++ examples/acc-repeated.rs | 116 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 examples/acc-repeated.rs diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 40afea006..c1a3ff4c2 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -96,6 +96,10 @@ path = "acc-packed.rs" name = "acc-projected" path = "acc-projected.rs" +[[example]] +name = "acc-repeated" +path = "acc-repeated.rs" + [lints.clippy] needless_range_loop = "allow" diff --git a/examples/acc-repeated.rs b/examples/acc-repeated.rs new file mode 100644 index 000000000..22697759a --- /dev/null +++ b/examples/acc-repeated.rs @@ -0,0 +1,116 @@ +use binius_circuits::{builder::ConstraintSystemBuilder, unconstrained::unconstrained}; +use binius_core::constraint_system::validate::validate_witness; +use binius_field::{ + arch::OptimalUnderlier, packed::set_packed_slice, BinaryField128b, BinaryField1b, + BinaryField8b, PackedBinaryField128x1b, +}; + +type U = OptimalUnderlier; +type F128 = BinaryField128b; +type F8 = BinaryField8b; +type F1 = BinaryField1b; + +// FIXME: Following gadgets are unconstrained. Only for demonstrative purpose, don't use in production + +const LOG_SIZE: usize = 8; + +// The idea of 'Repeated' column is that one can just copy data from initial column multiple times, +// so new column is X times bigger than original one. The following gadget operates over bytes, e.g. +// it creates column with some input bytes written and then creates one more 'Repeated' column +// where the same bytes are copied multiple times. +fn bytes_repeat_gadget(builder: &mut ConstraintSystemBuilder) { + let bytes = unconstrained::(builder, "input", LOG_SIZE).unwrap(); + + let repeat_times_log = 4usize; + let repeating = builder + .add_repeating("repeating", bytes, repeat_times_log) + .unwrap(); + + if let Some(witness) = builder.witness() { + let input_values = witness.get::(bytes).unwrap().as_slice::(); + + let mut repeating_witness = witness.new_column::(repeating); + let repeating_values = repeating_witness.as_mut_slice::(); + + let repeat_times = 2usize.pow(repeat_times_log as u32); + assert_eq!(2usize.pow(LOG_SIZE as u32), input_values.len()); + assert_eq!(input_values.len() * repeat_times, repeating_values.len()); + + for idx in 0..repeat_times { + let start = idx * input_values.len(); + let end = start + input_values.len(); + repeating_values[start..end].copy_from_slice(input_values); + } + } +} + +// Bit-oriented repeating is more elaborated due to a specifics of memory layout in Binius. +// In the following example, we use LOG_SIZE=8, which gives 2.pow(8) = 32 bytes written in the memory +// layout. This gives 32 * 8 = 256 bits of input information. Having that Repeated' column +// is instantiated with 'repeat_times_log = 2', this means that we have to repeat our bytes +// 2.pow(repeat_times_log) = 4 times ultimately. For setting bit values we use PackedBinaryField128x1b, +// so for 32 bytes (256 bits) of input data we use 2 PackedBinaryField128x1b elements. Considering 4 +// repetitions Binius creates column with 8 PackedBinaryField128x1b elements totally. +// Proper writing bits requires separate iterating over PackedBinaryField128x1b elements and input bytes +// with extracting particular bit values from the input and setting appropriate bit in a given PackedBinaryField128x1b. +fn bits_repeat_gadget(builder: &mut ConstraintSystemBuilder) { + let bits = unconstrained::(builder, "input", LOG_SIZE).unwrap(); + let repeat_times_log = 2usize; + + // Binius will create column with appropriate height for us + let repeating = builder + .add_repeating("repeating", bits, repeat_times_log) + .unwrap(); + + if let Some(witness) = builder.witness() { + let input_values = witness.get::(bits).unwrap().as_slice::(); + let mut repeating_witness = witness.new_column::(repeating); + let output_values = repeating_witness.packed(); + + // this performs writing input bits exactly 1 time. Depending on number of repetitions we + // need to call this multiple times, providing offset for output values (PackedBinaryField128x1b elements) + fn write_input( + input_values: &[u8], + output_values: &mut [PackedBinaryField128x1b], + output_packed_offset: usize, + ) { + let mut output_index = output_packed_offset; + for (input_index, _) in (0..input_values.len()).enumerate() { + let byte = input_values[input_index]; + + set_packed_slice(output_values, output_index, F1::from(byte)); + set_packed_slice(output_values, output_index + 1, F1::from((byte >> 1) & 0x01)); + set_packed_slice(output_values, output_index + 2, F1::from((byte >> 2) & 0x01)); + set_packed_slice(output_values, output_index + 3, F1::from((byte >> 3) & 0x01)); + set_packed_slice(output_values, output_index + 4, F1::from((byte >> 4) & 0x01)); + set_packed_slice(output_values, output_index + 5, F1::from((byte >> 5) & 0x01)); + set_packed_slice(output_values, output_index + 6, F1::from((byte >> 6) & 0x01)); + set_packed_slice(output_values, output_index + 7, F1::from((byte >> 7) & 0x01)); + + output_index += 8; + } + } + + let repeat_times = 2u32.pow(repeat_times_log as u32); + + let mut offset = 0; + for _ in 0..repeat_times { + write_input(input_values, output_values, offset); + offset += input_values.len() * 8; + } + } +} + +fn main() { + let allocator = bumpalo::Bump::new(); + + let mut builder = ConstraintSystemBuilder::::new_with_witness(&allocator); + + bytes_repeat_gadget(&mut builder); + bits_repeat_gadget(&mut builder); + + let witness = builder.take_witness().unwrap(); + let cs = builder.build().unwrap(); + + validate_witness(&cs, &[], &witness).unwrap(); +} From c50195ca4c25995858e2c979db9b3a8559678176 Mon Sep 17 00:00:00 2001 From: Artem Storozhuk Date: Thu, 6 Feb 2025 16:05:32 +0200 Subject: [PATCH 3/3] example: Add example of ZeroPadded column usage --- examples/Cargo.toml | 4 ++++ examples/acc-repeated.rs | 8 +++++++ examples/acc-zeropadded.rs | 45 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+) create mode 100644 examples/acc-zeropadded.rs diff --git a/examples/Cargo.toml b/examples/Cargo.toml index c1a3ff4c2..3b309cb93 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -100,6 +100,10 @@ path = "acc-projected.rs" name = "acc-repeated" path = "acc-repeated.rs" +[[example]] +name = "acc-zeropadded" +path = "acc-zeropadded.rs" + [lints.clippy] needless_range_loop = "allow" diff --git a/examples/acc-repeated.rs b/examples/acc-repeated.rs index 22697759a..749bb6fee 100644 --- a/examples/acc-repeated.rs +++ b/examples/acc-repeated.rs @@ -19,6 +19,8 @@ const LOG_SIZE: usize = 8; // it creates column with some input bytes written and then creates one more 'Repeated' column // where the same bytes are copied multiple times. fn bytes_repeat_gadget(builder: &mut ConstraintSystemBuilder) { + builder.push_namespace("bytes_repeat_gadget"); + let bytes = unconstrained::(builder, "input", LOG_SIZE).unwrap(); let repeat_times_log = 4usize; @@ -42,6 +44,8 @@ fn bytes_repeat_gadget(builder: &mut ConstraintSystemBuilder) { repeating_values[start..end].copy_from_slice(input_values); } } + + builder.pop_namespace(); } // Bit-oriented repeating is more elaborated due to a specifics of memory layout in Binius. @@ -54,6 +58,8 @@ fn bytes_repeat_gadget(builder: &mut ConstraintSystemBuilder) { // Proper writing bits requires separate iterating over PackedBinaryField128x1b elements and input bytes // with extracting particular bit values from the input and setting appropriate bit in a given PackedBinaryField128x1b. fn bits_repeat_gadget(builder: &mut ConstraintSystemBuilder) { + builder.push_namespace("bits_repeat_gadget"); + let bits = unconstrained::(builder, "input", LOG_SIZE).unwrap(); let repeat_times_log = 2usize; @@ -99,6 +105,8 @@ fn bits_repeat_gadget(builder: &mut ConstraintSystemBuilder) { offset += input_values.len() * 8; } } + + builder.pop_namespace(); } fn main() { diff --git a/examples/acc-zeropadded.rs b/examples/acc-zeropadded.rs new file mode 100644 index 000000000..2cbb954ca --- /dev/null +++ b/examples/acc-zeropadded.rs @@ -0,0 +1,45 @@ +use binius_circuits::{builder::ConstraintSystemBuilder, unconstrained::unconstrained}; +use binius_core::constraint_system::validate::validate_witness; +use binius_field::{arch::OptimalUnderlier, BinaryField128b, BinaryField8b}; + +type U = OptimalUnderlier; +type F128 = BinaryField128b; +type F8 = BinaryField8b; + +const LOG_SIZE: usize = 4; + +fn main() { + let allocator = bumpalo::Bump::new(); + + let mut builder = ConstraintSystemBuilder::::new_with_witness(&allocator); + + let bytes = unconstrained::(&mut builder, "bytes", LOG_SIZE).unwrap(); + + // Height of ZeroPadded column can't be smaller than input one. + // If n_vars equals to LOG_SIZE, then no padding is required, + // the ZeroPadded column will have same length as input one, so we use bigger number + let n_vars = 5usize; + let zeropadded = builder + .add_zero_padded("zeropadded", bytes, n_vars) + .unwrap(); + + if let Some(witness) = builder.witness() { + let input_values = witness.get::(bytes).unwrap().as_slice::(); + + let mut zeropadded_witness = witness.new_column::(zeropadded); + let zeropadded_values = zeropadded_witness.as_mut_slice::(); + + // padding naturally happens in the end, so we just copy input data to ZeroPadded column + zeropadded_values[..input_values.len()].copy_from_slice(input_values); + + assert_eq!(zeropadded_values.len(), 2usize.pow(n_vars as u32)); + assert!(n_vars >= LOG_SIZE); + let zeroes_to_pad = 2usize.pow(n_vars as u32) - 2usize.pow(LOG_SIZE as u32); + assert_eq!(zeroes_to_pad, zeropadded_values.len() - input_values.len()); + } + + let witness = builder.take_witness().unwrap(); + let cs = builder.build().unwrap(); + + validate_witness(&cs, &[], &witness).unwrap(); +}