From d1788625c349b6077e9ec2c82ae3882c3ad93532 Mon Sep 17 00:00:00 2001 From: Xinding Wei Date: Wed, 30 Aug 2023 11:01:48 -0700 Subject: [PATCH 1/8] WIP --- .../src/keccak/computation_circuit/circuit.rs | 343 ++++++++++++++++++ .../src/keccak/computation_circuit/mod.rs | 2 + .../computation_circuit/poseidon_params.rs | 5 + hashes/zkevm/src/keccak/mod.rs | 1 + 4 files changed, 351 insertions(+) create mode 100644 hashes/zkevm/src/keccak/computation_circuit/circuit.rs create mode 100644 hashes/zkevm/src/keccak/computation_circuit/mod.rs create mode 100644 hashes/zkevm/src/keccak/computation_circuit/poseidon_params.rs diff --git a/hashes/zkevm/src/keccak/computation_circuit/circuit.rs b/hashes/zkevm/src/keccak/computation_circuit/circuit.rs new file mode 100644 index 00000000..497b1d3d --- /dev/null +++ b/hashes/zkevm/src/keccak/computation_circuit/circuit.rs @@ -0,0 +1,343 @@ +use std::cell::RefCell; + +use super::poseidon_params; +use crate::{ + keccak::{ + multi_keccak, + param::{NUM_ROUNDS, NUM_WORDS_TO_ABSORB}, + KeccakAssignedRow, KeccakCircuitConfig, KeccakConfigParams, + }, + util::eth_types::Field, +}; +use halo2_base::{ + gates::{ + circuit::{builder::RangeCircuitBuilder, BaseConfig}, + GateInstructions, RangeInstructions, + }, + halo2_proofs::{ + circuit::{Layouter, SimpleFloorPlanner}, + plonk::{Circuit, ConstraintSystem, Error}, + }, + poseidon::hasher::{spec::OptimizedPoseidonSpec, PoseidonCompactInput, PoseidonHasher}, + safe_types::SafeTypeChip, + AssignedValue, +}; +use itertools::{izip, Itertools}; +use sha3::{Digest, Keccak256}; + +/// Keccak Computation Circuit +pub struct KeccakComputationCircuit { + range_circuit_builder: RefCell>, + inputs: Vec>, + capacity: usize, + config_params: KeccakComputationCircuitParams, +} + +#[derive(Default, Clone)] +pub struct KeccakComputationCircuitParams { + k: usize, + keccak_circuit_params: KeccakConfigParams, +} + +#[derive(Clone)] +pub struct KeccakComputationConfig { + range_circuit_config: BaseConfig, + keccak_circuit_config: KeccakCircuitConfig, +} + +impl Circuit for KeccakComputationCircuit { + type Config = KeccakComputationConfig; + type FloorPlanner = SimpleFloorPlanner; + type Params = KeccakComputationCircuitParams; + + fn params(&self) -> Self::Params { + self.config_params.clone() + } + + /// Creates a new instance of the [RangeCircuitBuilder] without witnesses by setting the witness_gen_only flag to false + fn without_witnesses(&self) -> Self { + unimplemented!() + } + + /// Configures a new circuit using [`BaseConfigParams`] + fn configure_with_params(meta: &mut ConstraintSystem, params: Self::Params) -> Self::Config { + let range_circuit_config = RangeCircuitBuilder::configure(meta); + let keccak_circuit_config = KeccakCircuitConfig::new(meta, params.keccak_circuit_params); + Self::Config { range_circuit_config, keccak_circuit_config } + } + + fn configure(_: &mut ConstraintSystem) -> Self::Config { + unreachable!("You must use configure_with_params"); + } + + fn synthesize( + &self, + config: Self::Config, + mut layouter: impl Layouter, + ) -> Result<(), Error> { + let k = self.config_params.k; + config.keccak_circuit_config.load_aux_tables(&mut layouter, k as u32)?; + // TODO: do I need SKIP_FIRST_PASS? + let (keccak_rows, _) = multi_keccak::( + &self.inputs, + Some(self.capacity), + self.params().keccak_circuit_params, + ); + let mut keccak_assigned_rows: Vec> = Vec::default(); + layouter.assign_region( + || "keccak circuit", + |mut region| { + keccak_assigned_rows = + config.keccak_circuit_config.assign(&mut region, &keccak_rows); + Ok(()) + }, + )?; + let loaded_assigned_rows = self.load_keccak_assigned_rows(keccak_assigned_rows); + + let (compact_inputs, result_selector_per_chunk) = + self.generate_poseidon_inputs(&loaded_assigned_rows); + + let _circuit_final_output = self.compute_circuit_final_output( + &loaded_assigned_rows, + &compact_inputs, + &result_selector_per_chunk, + ); + + self.range_circuit_builder.borrow().synthesize(config.range_circuit_config, layouter)?; + Ok(()) + } +} + +/// Witnesses to be exposed as circuit outputs. +struct KeccakCircuitOutput { + pub(crate) input_poseidon: AssignedValue, + pub(crate) hash_lo: AssignedValue, + pub(crate) hash_hi: AssignedValue, +} + +struct LoadedKeccakAssignedRow { + pub(crate) is_final: AssignedValue, + pub(crate) hash_lo: AssignedValue, + pub(crate) hash_hi: AssignedValue, + pub(crate) bytes_left: AssignedValue, + pub(crate) word_value: AssignedValue, +} + +impl KeccakComputationCircuit { + /// Load keccak assigned rows into halo2-lib. + fn load_keccak_assigned_rows( + &self, + assigned_rows: Vec>, + ) -> Vec> { + let mut loaded_assigned_rows = Vec::with_capacity(assigned_rows.len()); + let range_circuit_builder = self.range_circuit_builder.borrow(); + let mut copy_manager = range_circuit_builder.core().copy_manager.lock().unwrap(); + for assigned_row in assigned_rows { + loaded_assigned_rows.push(LoadedKeccakAssignedRow { + is_final: copy_manager.load_external_assigned(assigned_row.is_final), + hash_lo: copy_manager.load_external_assigned(assigned_row.hash_lo), + hash_hi: copy_manager.load_external_assigned(assigned_row.hash_hi), + bytes_left: copy_manager.load_external_assigned(assigned_row.bytes_left), + word_value: copy_manager.load_external_assigned(assigned_row.word_value), + }); + } + loaded_assigned_rows + } + + // Generate compact inputs for Poseidon based on raw inputs in Keccak witnesses. + // + // To illustrate how this function works, let's take the following example: + // Parameters: RATE = 2, NUM_WORDS_TO_ABSORB = 2, NUM_ROUNDS = 3 + // Considering the following logical input in little endian bytes: + // logical_input = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x21, 0x22, 0x23, 0x24, 0x25] + // Keccak witnesses represent the input in 2 chunks(NUM_ROUNDS + 1 rows per chunk): + // loaded_assigned_rows = [ + // {bytes_left: 21, word_value: 0x0807060504030201, is_final: N/A}, + // {bytes_left: 13, word_value: 0x1817161514131211, is_final: N/A}, + // {bytes_left: N/A, word_value: N/A, is_final: N/A}, // No input/output information after first NUM_WORDS_TO_ABSORB rows. + // {bytes_left: N/A, word_value: N/A, is_final: false}, + // {bytes_left: 5, word_value: 0x0000002524232221, is_final: N/A}, + // {bytes_left: 0, word_value: 0x0000000000000000, is_final: N/A}, + // {bytes_left: N/A, word_value: N/A, is_final: N/A}, // No input/output information after first NUM_WORDS_TO_ABSORB rows. + // {bytes_left: N/A, word_value: N/A, is_final: true}, + // ] + // For easier Poseidon processing, the raw input is encoded in the following way: + // If the logic input is empty, [0; RATE]. + // If the logic input is empty, each word is encoded as RATE witnesses. First word is encoded + // as [, logical_input[0], 0..]. Other words are encoded as [logical_input[i], 0, 0..]. + // Note: With this encoding, App circuits should be able to compute Poseidons of variable length inputs easily. + // Then we get the following compact inputs for Poseidon hasher: + // poseidon_compact_inputs = [ + // {inputs: [ 21, 0x0807060504030201], len: 2, is_final: false}, // 21 is the length of the logical input. + // {inputs: [0x1817161514131211, 0x0], len: 2, is_final: false}, + // {inputs: [0x0000002524232221, 0x0], len: 2, is_final: true}, // The last row of the logical input. + // {inputs: [ 0x0, 0x0], len: 2, is_final: true}, // This row corresponds to the padding row in loaded_assigned_rows. + // ] + // The Poseidon results will be: + // poseidon_compact_outputs = [ + // {hash: N/A, is_final: false}, + // {hash: N/A, is_final: false}, + // {hash: ), is_final: true}, + // {hash: poseidon([0x0, 0x0]), is_final: true}, + // ] + // Because of the padding rows, is_final cannot tell which row is the result of a logical input. Therefore + // we also build a selector array(2d, * NUM_WORDS_TO_ABSORB) to select reuslts: + // poseidon_selector: [[0, 0], [1, 0]] + fn generate_poseidon_inputs( + &self, + loaded_assigned_rows: &[LoadedKeccakAssignedRow], + ) -> ( + Vec>, + Vec<[AssignedValue; NUM_WORDS_TO_ABSORB]>, + ) { + let rows_per_round = self.config_params.keccak_circuit_params.rows_per_round; + let mut range_circuit_builder_mut = self.range_circuit_builder.borrow_mut(); + let ctx = range_circuit_builder_mut.main(0); + let range_chip = self.range_circuit_builder.borrow().range_chip(); + + let num_chunks = (loaded_assigned_rows.len() / rows_per_round - 1) / (NUM_ROUNDS + 1); + let compact_input_len = num_chunks * (NUM_WORDS_TO_ABSORB); + let mut result_selector_per_chunk = Vec::with_capacity(num_chunks); + let mut compact_inputs = Vec::with_capacity(compact_input_len); + + let rate_witness = ctx.load_constant(F::from(poseidon_params::RATE as u64)); + let zero_witness = ctx.load_zero(); + let mut chunk_is_final_last = zero_witness; + + // Skip the first round which is dummy. + for chunk in + &loaded_assigned_rows.iter().step_by(rows_per_round).skip(1).chunks(NUM_ROUNDS + 1) + { + let chunk = chunk.collect_vec(); + // If this chunk is the last chunk of a logical input. + let chunk_is_final = chunk[NUM_ROUNDS].is_final; + let mut result_selector = [zero_witness; NUM_WORDS_TO_ABSORB]; + let mut result_selector_is_set = zero_witness; + for round_idx in 0..NUM_WORDS_TO_ABSORB { + let round = chunk[round_idx]; + // First word is encoded as [bytes_left, word_value, 0..]. Here bytes_left equals to the length of the input. + // Other words are encoded as [word_value, 0, 0..]. + let mut inputs = [zero_witness; { poseidon_params::RATE }]; + if round_idx == 0 { + inputs[0] = range_chip.gate().select( + ctx, + round.bytes_left, + round.word_value, + chunk_is_final_last, + ); + inputs[1] = range_chip.gate().select( + ctx, + round.word_value, + zero_witness, + chunk_is_final_last, + ); + } else { + inputs[0] = round.word_value; + } + let is_final = if round_idx == NUM_WORDS_TO_ABSORB - 1 { + chunk_is_final + } else { + let next_bytes_left_is_zero = + range_chip.gate().is_zero(ctx, chunk[round_idx + 1].bytes_left); + range_chip.gate().and(ctx, next_bytes_left_is_zero, chunk_is_final) + }; + // First round with is_final == true outputs the poseidon result of the input. + // All later rounds are dummies. + result_selector[round_idx] = + range_chip.gate().select(ctx, zero_witness, is_final, result_selector_is_set); + result_selector_is_set = + range_chip.gate().or(ctx, result_selector_is_set, result_selector[round_idx]); + + compact_inputs.push(PoseidonCompactInput::new( + inputs, + SafeTypeChip::unsafe_to_bool(is_final), + rate_witness, + )); + } + result_selector_per_chunk.push(result_selector); + chunk_is_final_last = chunk_is_final; + } + (compact_inputs, result_selector_per_chunk) + } + + // Compute poseidon hash of logical inputs then combine with Keccak256 hash. + fn compute_circuit_final_output( + &self, + loaded_assigned_rows: &[LoadedKeccakAssignedRow], + compact_inputs: &[PoseidonCompactInput], + result_selector_per_chunk: &[[AssignedValue; NUM_WORDS_TO_ABSORB]], + ) -> Vec> { + let rows_per_round = self.config_params.keccak_circuit_params.rows_per_round; + let mut range_circuit_builder_mut = self.range_circuit_builder.borrow_mut(); + let ctx = range_circuit_builder_mut.main(0); + let range_chip = self.range_circuit_builder.borrow().range_chip(); + + let num_chunks = (loaded_assigned_rows.len() / rows_per_round - 1) / (NUM_ROUNDS + 1); + + let zero_witness = ctx.load_zero(); + + // Filter out the first row of the last round of each chunk, which contains keccak hash result. + let keccak_output_rows = loaded_assigned_rows + .iter() + .step_by(rows_per_round) + .step_by(NUM_ROUNDS + 1) + .skip(1) + .collect_vec(); + + // Construct in-circuit Poseidon hasher. Assuming SECURE_MDS = 0. + let spec = + OptimizedPoseidonSpec::::new::< + { poseidon_params::R_F }, + { poseidon_params::R_P }, + { poseidon_params::SECURE_MDS }, + >(); + let mut poseidon_hasher = + PoseidonHasher::::new(spec); + assert!(poseidon_params::RATE >= 2, "Poseidon RATE must be at least to encode inputs"); + poseidon_hasher.initialize_consts(ctx, range_chip.gate()); + let dummy_input_poseidon = poseidon_hasher.hash_fix_len_array( + ctx, + &range_chip, + &[zero_witness; { poseidon_params::RATE }], + ); + let mut circuit_final_outputs = Vec::with_capacity(num_chunks); + // Output of Keccak256::digest is big endian. + let dummy_keccak_val = Keccak256::digest(&[]); + let dummy_keccak_val_lo = u128::from_be_bytes(dummy_keccak_val[16..].try_into().unwrap()); + let dummy_keccak_val_hi = u128::from_be_bytes(dummy_keccak_val[..16].try_into().unwrap()); + let dummy_keccak_lo_witness = ctx.load_constant(F::from_u128(dummy_keccak_val_lo)); + let dummy_keccak_hi_witness = ctx.load_constant(F::from_u128(dummy_keccak_val_hi)); + let compact_outputs = poseidon_hasher.hash_compact_input(ctx, &range_chip, &compact_inputs); + for (compact_output, keccak_output_row, result_selector) in izip!( + compact_outputs.chunks(NUM_WORDS_TO_ABSORB), + keccak_output_rows, + result_selector_per_chunk, + ) { + let mut input_poseidon = range_chip.gate().inner_product( + ctx, + compact_output.iter().map(|o| *o.hash()), + result_selector.iter().map(|s| halo2_base::QuantumCell::Existing(*s)), + ); + input_poseidon = range_chip.gate().select( + ctx, + input_poseidon, + dummy_input_poseidon, + keccak_output_row.is_final, + ); + let hash_lo = range_chip.gate().select( + ctx, + keccak_output_row.hash_lo, + dummy_keccak_lo_witness, + keccak_output_row.is_final, + ); + let hash_hi = range_chip.gate().select( + ctx, + keccak_output_row.hash_hi, + dummy_keccak_hi_witness, + keccak_output_row.is_final, + ); + circuit_final_outputs.push(KeccakCircuitOutput { input_poseidon, hash_lo, hash_hi }); + } + circuit_final_outputs + } +} diff --git a/hashes/zkevm/src/keccak/computation_circuit/mod.rs b/hashes/zkevm/src/keccak/computation_circuit/mod.rs new file mode 100644 index 00000000..f810f7d6 --- /dev/null +++ b/hashes/zkevm/src/keccak/computation_circuit/mod.rs @@ -0,0 +1,2 @@ +pub mod circuit; +pub mod poseidon_params; diff --git a/hashes/zkevm/src/keccak/computation_circuit/poseidon_params.rs b/hashes/zkevm/src/keccak/computation_circuit/poseidon_params.rs new file mode 100644 index 00000000..964e8233 --- /dev/null +++ b/hashes/zkevm/src/keccak/computation_circuit/poseidon_params.rs @@ -0,0 +1,5 @@ +pub const T: usize = 2; +pub const RATE: usize = 2; +pub const R_F: usize = 8; +pub const R_P: usize = 57; +pub const SECURE_MDS: usize = 0; diff --git a/hashes/zkevm/src/keccak/mod.rs b/hashes/zkevm/src/keccak/mod.rs index 0dc18d87..edfbe23d 100644 --- a/hashes/zkevm/src/keccak/mod.rs +++ b/hashes/zkevm/src/keccak/mod.rs @@ -23,6 +23,7 @@ use rayon::prelude::{IntoParallelRefIterator, ParallelIterator}; use std::marker::PhantomData; pub mod cell_manager; +pub mod computation_circuit; pub mod keccak_packed_multi; pub mod param; pub mod table; From 664517187553b770f2d4da0ee7cff7e1199f0568 Mon Sep 17 00:00:00 2001 From: Jonathan Wang <31040440+jonathanpwang@users.noreply.github.com> Date: Wed, 30 Aug 2023 18:02:16 -0700 Subject: [PATCH 2/8] chore: make `KeccakAssignedRow` fields public --- hashes/zkevm/src/keccak/mod.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/hashes/zkevm/src/keccak/mod.rs b/hashes/zkevm/src/keccak/mod.rs index edfbe23d..3c9eaf1b 100644 --- a/hashes/zkevm/src/keccak/mod.rs +++ b/hashes/zkevm/src/keccak/mod.rs @@ -797,14 +797,13 @@ impl KeccakCircuitConfig { } } -#[allow(dead_code)] #[derive(Clone)] pub struct KeccakAssignedRow<'v, F: Field> { - pub(crate) is_final: KeccakAssignedValue<'v, F>, - pub(crate) hash_lo: KeccakAssignedValue<'v, F>, - pub(crate) hash_hi: KeccakAssignedValue<'v, F>, - pub(crate) bytes_left: KeccakAssignedValue<'v, F>, - pub(crate) word_value: KeccakAssignedValue<'v, F>, + pub is_final: KeccakAssignedValue<'v, F>, + pub hash_lo: KeccakAssignedValue<'v, F>, + pub hash_hi: KeccakAssignedValue<'v, F>, + pub bytes_left: KeccakAssignedValue<'v, F>, + pub word_value: KeccakAssignedValue<'v, F>, } impl KeccakCircuitConfig { From e1b04eb31855eecc300a253c1cb0ec3cbe3f0df2 Mon Sep 17 00:00:00 2001 From: Xinding Wei Date: Sat, 2 Sep 2023 02:13:39 -0700 Subject: [PATCH 3/8] Refactor Keccak coprocessor circuit --- Cargo.toml | 2 +- .../src/virtual_region/copy_constraints.rs | 14 +- hashes/zkevm/Cargo.toml | 2 + hashes/zkevm/src/keccak/co_circuit/circuit.rs | 429 ++++++++++++++++++ hashes/zkevm/src/keccak/co_circuit/encode.rs | 194 ++++++++ hashes/zkevm/src/keccak/co_circuit/mod.rs | 8 + hashes/zkevm/src/keccak/co_circuit/param.rs | 12 + .../zkevm/src/keccak/co_circuit/tests/mod.rs | 42 ++ .../src/keccak/computation_circuit/circuit.rs | 343 -------------- .../src/keccak/computation_circuit/mod.rs | 2 - .../computation_circuit/poseidon_params.rs | 5 - .../zkevm/src/keccak/keccak_packed_multi.rs | 17 +- hashes/zkevm/src/keccak/mod.rs | 5 +- hashes/zkevm/src/keccak/param.rs | 2 +- 14 files changed, 714 insertions(+), 363 deletions(-) create mode 100644 hashes/zkevm/src/keccak/co_circuit/circuit.rs create mode 100644 hashes/zkevm/src/keccak/co_circuit/encode.rs create mode 100644 hashes/zkevm/src/keccak/co_circuit/mod.rs create mode 100644 hashes/zkevm/src/keccak/co_circuit/param.rs create mode 100644 hashes/zkevm/src/keccak/co_circuit/tests/mod.rs delete mode 100644 hashes/zkevm/src/keccak/computation_circuit/circuit.rs delete mode 100644 hashes/zkevm/src/keccak/computation_circuit/mod.rs delete mode 100644 hashes/zkevm/src/keccak/computation_circuit/poseidon_params.rs diff --git a/Cargo.toml b/Cargo.toml index 1418cb9a..52c646cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ resolver = "2" [profile.dev] opt-level = 3 -debug = 1 # change to 0 or 2 for more or less debug info +debug = 2 # change to 0 or 2 for more or less debug info overflow-checks = true incremental = true diff --git a/halo2-base/src/virtual_region/copy_constraints.rs b/halo2-base/src/virtual_region/copy_constraints.rs index da991fb9..3a405f1e 100644 --- a/halo2-base/src/virtual_region/copy_constraints.rs +++ b/halo2-base/src/virtual_region/copy_constraints.rs @@ -77,9 +77,21 @@ impl CopyConstraintManager { /// Adds external raw Halo2 cell to `self.assigned_advices` and returns a new virtual cell that can be /// used as a tag (but will not be re-assigned). The returned [ContextCell] will have `type_id` the `TypeId::of::()`. pub fn load_external_cell(&mut self, cell: Cell) -> ContextCell { + self.load_external_cell_impl(Some(cell)) + } + + /// Mock to load an external cell for base circuit simulation. If any mock external cell is loaded, calling [assign_raw] will panic. + pub fn mock_external_assigned(&mut self, v: F) -> AssignedValue { + let context_cell = self.load_external_cell_impl(None); + AssignedValue { value: Assigned::Trivial(v), cell: Some(context_cell) } + } + + fn load_external_cell_impl(&mut self, cell: Option) -> ContextCell { let context_cell = ContextCell::new(TypeId::of::(), 0, self.external_cell_count); self.external_cell_count += 1; - self.assigned_advices.insert(context_cell, cell); + if let Some(cell) = cell { + self.assigned_advices.insert(context_cell, cell); + } context_cell } diff --git a/hashes/zkevm/Cargo.toml b/hashes/zkevm/Cargo.toml index 4814145a..845ca67a 100644 --- a/hashes/zkevm/Cargo.toml +++ b/hashes/zkevm/Cargo.toml @@ -15,6 +15,8 @@ num-bigint = { version = "0.4" } halo2-base = { path = "../../halo2-base", default-features = false } rayon = "1.7" sha3 = "0.10.8" +pse-poseidon = { git = "https://github.com/axiom-crypto/pse-poseidon.git" } +getset = "0.1.2" [dev-dependencies] criterion = "0.3" diff --git a/hashes/zkevm/src/keccak/co_circuit/circuit.rs b/hashes/zkevm/src/keccak/co_circuit/circuit.rs new file mode 100644 index 00000000..3239d0bd --- /dev/null +++ b/hashes/zkevm/src/keccak/co_circuit/circuit.rs @@ -0,0 +1,429 @@ +use std::cell::RefCell; + +use super::{ + encode::{encode_inputs_from_keccak_fs, encode_native_input}, + param::*, +}; +use crate::{ + keccak::{ + keccak_packed_multi::get_num_keccak_f, multi_keccak, param::*, KeccakAssignedRow, + KeccakCircuitConfig, KeccakConfigParams, + }, + util::eth_types::Field, +}; +use getset::Getters; +use halo2_base::{ + gates::{ + circuit::{builder::BaseCircuitBuilder, BaseCircuitParams, BaseConfig}, + GateInstructions, RangeInstructions, + }, + halo2_proofs::{ + circuit::{Layouter, SimpleFloorPlanner}, + plonk::{Circuit, ConstraintSystem, Error}, + }, + poseidon::hasher::{spec::OptimizedPoseidonSpec, PoseidonCompactOutput, PoseidonHasher}, + AssignedValue, Context, +}; +use itertools::Itertools; +use sha3::{Digest, Keccak256}; + +/// Keccak Coprocessor Circuit +#[derive(Getters)] +pub struct KeccakCoprocessorCircuit { + inputs: Vec>, + + /// Parameters of this circuit. The same parameters always construct the same circuit. + params: KeccakCoprocessorCircuitParams, + + base_circuit_builder: RefCell>, + hasher: RefCell>, +} + +/// Parameters of KeccakCoprocessorCircuit. +#[derive(Default, Clone, Getters)] +pub struct KeccakCoprocessorCircuitParams { + /// This circuit has 2^k rows. + #[getset(get = "pub")] + k: usize, + // Number of unusable rows withhold by Halo2. + #[getset(get = "pub")] + num_unusable_row: usize, + /// The bits of lookup table for RangeChip. + #[getset(get = "pub")] + lookup_bits: usize, + /// Max keccak_f this circuits can aceept. The circuit can at most process of inputs + /// with < NUM_BYTES_TO_ABSORB bytes or an input with * NUM_BYTES_TO_ABSORB - 1 bytes. + #[getset(get = "pub")] + capacity: usize, + // If true, publish raw outputs. Otherwise, publish Poseidon commitment of raw outputs. + #[getset(get = "pub")] + publish_raw_outputs: bool, + + // Derived parameters of sub-circuits. + keccak_circuit_params: KeccakConfigParams, +} + +impl KeccakCoprocessorCircuitParams { + /// Create a new KeccakCoprocessorCircuitParams. + pub fn new( + k: usize, + num_unusable_row: usize, + lookup_bits: usize, + capacity: usize, + publish_raw_outputs: bool, + ) -> Self { + assert!(1 << k > num_unusable_row, "Number of unusable rows must be less than 2^k"); + let max_rows = (1 << k) - num_unusable_row; + // Derived from [crate::keccak::keccak_packed_multi::get_keccak_capacity]. + let rows_per_round = max_rows / (capacity * (NUM_ROUNDS + 1) + 1 + NUM_WORDS_TO_ABSORB); + assert!(rows_per_round > 0, "No enough rows for the speficied capacity"); + let keccak_circuit_params = KeccakConfigParams { k: k as u32, rows_per_round }; + Self { + k, + num_unusable_row, + lookup_bits, + capacity, + publish_raw_outputs, + keccak_circuit_params, + } + } +} + +/// Circuit::Config for Keccak Coprocessor Circuit. +#[derive(Clone)] +pub struct KeccakCoprocessorConfig { + base_circuit_params: BaseCircuitParams, + base_circuit_config: BaseConfig, + keccak_circuit_config: KeccakCircuitConfig, +} + +impl Circuit for KeccakCoprocessorCircuit { + type Config = KeccakCoprocessorConfig; + type FloorPlanner = SimpleFloorPlanner; + type Params = KeccakCoprocessorCircuitParams; + + fn params(&self) -> Self::Params { + self.params.clone() + } + + /// Creates a new instance of the [RangeCircuitBuilder] without witnesses by setting the witness_gen_only flag to false + fn without_witnesses(&self) -> Self { + unimplemented!() + } + + /// Configures a new circuit using [`BaseConfigParams`] + fn configure_with_params(meta: &mut ConstraintSystem, params: Self::Params) -> Self::Config { + let base_circuit_params = Self::calculate_base_circuit_params(params.clone()); + let base_circuit_config = + BaseCircuitBuilder::configure_with_params(meta, base_circuit_params.clone()); + let keccak_circuit_config = KeccakCircuitConfig::new(meta, params.keccak_circuit_params); + Self::Config { base_circuit_params, base_circuit_config, keccak_circuit_config } + } + + fn configure(_: &mut ConstraintSystem) -> Self::Config { + unreachable!("You must use configure_with_params"); + } + + fn synthesize( + &self, + config: Self::Config, + mut layouter: impl Layouter, + ) -> Result<(), Error> { + let k = self.params.k; + config.keccak_circuit_config.load_aux_tables(&mut layouter, k as u32)?; + let mut keccak_assigned_rows: Vec> = Vec::default(); + layouter.assign_region( + || "keccak circuit", + |mut region| { + let (keccak_rows, _) = multi_keccak::( + &self.inputs, + Some(self.params.capacity), + self.params.keccak_circuit_params, + ); + keccak_assigned_rows = + config.keccak_circuit_config.assign(&mut region, &keccak_rows); + Ok(()) + }, + )?; + + self.base_circuit_builder.borrow_mut().set_params(config.base_circuit_params); + // Base circuit witness generation. + let loaded_keccak_fs = self.load_keccak_assigned_rows(keccak_assigned_rows); + self.generate_base_circuit_phase0_witnesses(&loaded_keccak_fs); + + self.base_circuit_builder.borrow().synthesize(config.base_circuit_config, layouter)?; + Ok(()) + } +} + +/// Witnesses to be exposed as circuit outputs. +#[derive(Clone)] +pub struct KeccakCircuitOutput { + /// Key for App circuits to lookup keccak hash. + pub key: E, + /// Low 128 bits of Keccak hash. + pub hash_lo: E, + /// High 128 bits of Keccak hash. + pub hash_hi: E, +} + +/// Witnesses of a keccak_f which are necessary to be loaded into halo2-lib. +pub(crate) struct LoadedKeccakF { + // bytes_left of the first row of the first round of this keccak_f. This could be used to determine the length of the input. + pub(crate) bytes_left: AssignedValue, + // Input words of this keccak_f. + pub(crate) word_values: [AssignedValue; NUM_WORDS_TO_ABSORB], + // The output of this keccak_f. is_final/hash_lo/hash_hi come from the first row of the last round(NUM_ROUNDS). + pub(crate) is_final: AssignedValue, + pub(crate) hash_lo: AssignedValue, + pub(crate) hash_hi: AssignedValue, +} + +impl KeccakCoprocessorCircuit { + /// Create a new KeccakComputationCircuit + pub fn new(inputs: Vec>, params: KeccakCoprocessorCircuitParams) -> Self { + Self::new_impl(inputs, params, false) + } + + /// Implementation of Self::new. witness_gen_only can be customized. + fn new_impl( + inputs: Vec>, + params: KeccakCoprocessorCircuitParams, + witness_gen_only: bool, + ) -> Self { + let mut base_circuit_builder = BaseCircuitBuilder::new(witness_gen_only) + .use_k(params.k) + .use_lookup_bits(params.lookup_bits); + base_circuit_builder.set_instance_columns(if params.publish_raw_outputs { + OUTPUT_NUM_COL_RAW + } else { + OUTPUT_NUM_COL_COMMIT + }); + // Construct in-circuit Poseidon hasher. + let spec = OptimizedPoseidonSpec::::new::< + POSEIDON_R_F, + POSEIDON_R_P, + POSEIDON_SECURE_MDS, + >(); + let poseidon_hasher = PoseidonHasher::::new(spec); + Self { + inputs, + params, + base_circuit_builder: RefCell::new(base_circuit_builder), + hasher: RefCell::new(poseidon_hasher), + } + } + + /// Simulate witness generation of the base circuit to determine BaseCircuitParams because the number of columns + /// of the base circuit can only be known after witness generation. + pub fn calculate_base_circuit_params( + params: KeccakCoprocessorCircuitParams, + ) -> BaseCircuitParams { + // Create a simulation circuit to calculate base circuit parameters. + let simulation_circuit = Self::new_impl(vec![], params.clone(), false); + let loaded_keccak_fs = simulation_circuit.mock_load_keccak_assigned_rows(); + simulation_circuit.generate_base_circuit_phase0_witnesses(&loaded_keccak_fs); + + let base_circuit_params = simulation_circuit + .base_circuit_builder + .borrow_mut() + .calculate_params(Some(params.num_unusable_row)); + + base_circuit_params + } + + /// Mock loading Keccak assigned rows from Keccak circuit. This function doesn't create any witnesses/constraints. + fn mock_load_keccak_assigned_rows(&self) -> Vec> { + let base_circuit_builder = self.base_circuit_builder.borrow(); + let mut copy_manager = base_circuit_builder.core().copy_manager.lock().unwrap(); + (0..self.params.capacity) + .map(|_| LoadedKeccakF { + bytes_left: copy_manager.mock_external_assigned(F::ZERO), + word_values: core::array::from_fn(|_| copy_manager.mock_external_assigned(F::ZERO)), + is_final: copy_manager.mock_external_assigned(F::ZERO), + hash_lo: copy_manager.mock_external_assigned(F::ZERO), + hash_hi: copy_manager.mock_external_assigned(F::ZERO), + }) + .collect_vec() + } + + /// Load needed witnesses into halo2-lib from keccak assigned rows. This function doesn't create any witnesses/constraints. + fn load_keccak_assigned_rows( + &self, + assigned_rows: Vec>, + ) -> Vec> { + let rows_per_round = self.params.keccak_circuit_params.rows_per_round; + let base_circuit_builder = self.base_circuit_builder.borrow(); + let mut copy_manager = base_circuit_builder.core().copy_manager.lock().unwrap(); + let loaded_keccak_fs = assigned_rows + .iter() + .step_by(rows_per_round) + // Skip the first round which is dummy. + .skip(1) + .chunks(NUM_ROUNDS + 1) + .into_iter() + .map(|rounds| { + let rounds = rounds.collect_vec(); + let bytes_left = copy_manager.load_external_assigned(rounds[0].bytes_left.clone()); + let word_values = core::array::from_fn(|i| { + let assigned_row = rounds[i]; + copy_manager.load_external_assigned(assigned_row.word_value.clone()) + }); + let output_row = rounds[NUM_ROUNDS]; + let is_final = copy_manager.load_external_assigned(output_row.is_final.clone()); + let hash_lo = copy_manager.load_external_assigned(output_row.hash_lo.clone()); + let hash_hi = copy_manager.load_external_assigned(output_row.hash_hi.clone()); + LoadedKeccakF { bytes_left, word_values, is_final, hash_lo, hash_hi } + }) + .collect_vec(); + loaded_keccak_fs + } + + /// Generate phase0 witnesses of the base circuit. + fn generate_base_circuit_phase0_witnesses(&self, loaded_keccak_fs: &[LoadedKeccakF]) { + let circuit_final_outputs; + { + let range_chip = self.base_circuit_builder.borrow().range_chip(); + let mut base_circuit_builder_mut = self.base_circuit_builder.borrow_mut(); + let ctx = base_circuit_builder_mut.main(0); + let mut hasher = self.hasher.borrow_mut(); + hasher.initialize_consts(ctx, range_chip.gate()); + + let lookup_key_per_keccak_f = + encode_inputs_from_keccak_fs(ctx, &range_chip, &hasher, loaded_keccak_fs); + circuit_final_outputs = Self::generate_circuit_final_outputs( + ctx, + &range_chip, + &lookup_key_per_keccak_f, + loaded_keccak_fs, + ); + } + self.publish_outputs(&circuit_final_outputs); + } + + /// Combine lookup keys and Keccak results to generate final outputs of the circuit. + fn generate_circuit_final_outputs( + ctx: &mut Context, + range_chip: &impl RangeInstructions, + lookup_key_per_keccak_f: &[PoseidonCompactOutput], + loaded_keccak_fs: &[LoadedKeccakF], + ) -> Vec>> { + let KeccakCircuitOutput { + key: dummy_key_val, + hash_lo: dummy_keccak_val_lo, + hash_hi: dummy_keccak_val_hi, + } = dummy_circuit_output::(); + + // Dummy row for keccak_fs with is_final = false. The corresponding logical input is empty. + let dummy_key_witness = ctx.load_constant(dummy_key_val); + let dummy_keccak_lo_witness = ctx.load_constant(dummy_keccak_val_lo); + let dummy_keccak_hi_witness = ctx.load_constant(dummy_keccak_val_hi); + + let mut circuit_final_outputs = Vec::with_capacity(loaded_keccak_fs.len()); + for (compact_output, loaded_keccak_f) in + lookup_key_per_keccak_f.iter().zip(loaded_keccak_fs) + { + let key = range_chip.gate().select( + ctx, + *compact_output.hash(), + dummy_key_witness, + loaded_keccak_f.is_final, + ); + let hash_lo = range_chip.gate().select( + ctx, + loaded_keccak_f.hash_lo, + dummy_keccak_lo_witness, + loaded_keccak_f.is_final, + ); + let hash_hi = range_chip.gate().select( + ctx, + loaded_keccak_f.hash_hi, + dummy_keccak_hi_witness, + loaded_keccak_f.is_final, + ); + println!("In circuit: {:?}", key.value()); + circuit_final_outputs.push(KeccakCircuitOutput { key, hash_lo, hash_hi }); + } + circuit_final_outputs + } + + /// Publish outputs of the circuit as public instances. + fn publish_outputs(&self, outputs: &[KeccakCircuitOutput>]) { + if !self.params.publish_raw_outputs { + let range_chip = self.base_circuit_builder.borrow().range_chip(); + let mut base_circuit_builder_mut = self.base_circuit_builder.borrow_mut(); + let ctx = base_circuit_builder_mut.main(0); + + // The length of outputs is determined at compile time. + let output_commitment = self.hasher.borrow().hash_fix_len_array( + ctx, + &range_chip, + &outputs + .iter() + .flat_map(|output| [output.key, output.hash_hi, output.hash_lo]) + .collect_vec(), + ); + + let assigned_instances = &mut base_circuit_builder_mut.assigned_instances; + // The commitment should be in the first row. + assert!(assigned_instances[OUTPUT_COL_IDX_COMMIT].is_empty()); + assigned_instances[OUTPUT_COL_IDX_COMMIT].push(output_commitment); + } else { + let assigned_instances = &mut self.base_circuit_builder.borrow_mut().assigned_instances; + + // Outputs should be in the top of instance columns. + assert!(assigned_instances[OUTPUT_COL_IDX_KEY].is_empty()); + assert!(assigned_instances[OUTPUT_COL_IDX_HASH_LO].is_empty()); + assert!(assigned_instances[OUTPUT_COL_IDX_HASH_HI].is_empty()); + for output in outputs { + assigned_instances[OUTPUT_COL_IDX_KEY].push(output.key); + assigned_instances[OUTPUT_COL_IDX_HASH_LO].push(output.hash_lo); + assigned_instances[OUTPUT_COL_IDX_HASH_HI].push(output.hash_hi); + } + } + } +} + +/// Return circuit outputs of the specified Keccak corprocessor circuit for a specified input. +pub fn multi_inputs_to_circuit_outputs( + inputs: &[Vec], + params: &KeccakCoprocessorCircuitParams, +) -> Vec> { + assert!(u128::BITS <= F::CAPACITY); + let mut outputs = + inputs.iter().flat_map(|input| input_to_circuit_outputs::(input)).collect_vec(); + assert!(outputs.len() <= params.capacity); + outputs.resize(params.capacity, dummy_circuit_output()); + outputs +} + +/// Return corresponding circuit outputs of a native input in bytes. An logical input could produce multiple +/// outputs. The last one is the lookup key and hash of the input. Other outputs are paddings which are the lookup +/// key and hash of an empty input. +pub fn input_to_circuit_outputs(bytes: &[u8]) -> Vec> { + assert!(u128::BITS <= F::CAPACITY); + let len = bytes.len(); + let num_keccak_f = get_num_keccak_f(len); + + let mut output = Vec::with_capacity(num_keccak_f); + output.resize(num_keccak_f - 1, dummy_circuit_output()); + + let key = encode_native_input(bytes); + let hash = Keccak256::digest(bytes); + let hash_lo = F::from_u128(u128::from_be_bytes(hash[16..].try_into().unwrap())); + let hash_hi = F::from_u128(u128::from_be_bytes(hash[..16].try_into().unwrap())); + output.push(KeccakCircuitOutput { key, hash_lo, hash_hi }); + + output +} + +/// Return the dummy circuit output for padding. +pub fn dummy_circuit_output() -> KeccakCircuitOutput { + assert!(u128::BITS <= F::CAPACITY); + let key = encode_native_input(&[]); + // Output of Keccak256::digest is big endian. + let hash = Keccak256::digest([]); + let hash_lo = F::from_u128(u128::from_be_bytes(hash[16..].try_into().unwrap())); + let hash_hi = F::from_u128(u128::from_be_bytes(hash[..16].try_into().unwrap())); + KeccakCircuitOutput { key, hash_lo, hash_hi } +} diff --git a/hashes/zkevm/src/keccak/co_circuit/encode.rs b/hashes/zkevm/src/keccak/co_circuit/encode.rs new file mode 100644 index 00000000..136ab1fe --- /dev/null +++ b/hashes/zkevm/src/keccak/co_circuit/encode.rs @@ -0,0 +1,194 @@ +use halo2_base::{ + gates::{GateInstructions, RangeInstructions}, + poseidon::hasher::{PoseidonCompactInput, PoseidonCompactOutput, PoseidonHasher}, + safe_types::SafeTypeChip, + Context, +}; +use itertools::Itertools; + +use crate::{keccak::param::*, util::eth_types::Field}; + +use super::{circuit::LoadedKeccakF, param::*}; + +// TODO: Abstract this module into a trait for all coprocessor circuits. + +/// Module to encode raw inputs into lookup keys for looking up keccak results. The encoding is +/// designed to be efficient in coprocessor circuits. + +/// Encode a native input bytes into its corresponding lookup key. This function can be considered as the spec of the encoding. +pub fn encode_native_input(bytes: &[u8]) -> F { + assert!(NUM_BITS_PER_WORD <= u128::BITS as usize); + let multipilers = get_words_to_witness_multipiler::(); + let num_word_per_witness = num_word_per_witness::(); + let len = bytes.len(); + + // Divide the bytes input into Keccak words(each word has NUM_BYTES_PER_WORD bytes). + let mut words = bytes + .chunks(NUM_BYTES_PER_WORD) + .map(|chunk| { + let mut padded_chunk = [0; u128::BITS as usize / NUM_BITS_PER_BYTE]; + padded_chunk[..chunk.len()].copy_from_slice(chunk); + u128::from_le_bytes(padded_chunk) + }) + .collect_vec(); + // An extra keccak_f is performed if len % NUM_BYTES_TO_ABSORB == 0. + if len % NUM_BYTES_TO_ABSORB == 0 { + words.extend([0; NUM_WORDS_TO_ABSORB]); + } + // 1. Split Keccak words into keccak_fs(each keccak_f has NUM_WORDS_TO_ABSORB). + // 2. Append an extra word into the beginning of each keccak_f. In the first keccak_f, this word is the byte length of the input. Otherwise 0. + let words_per_chunk = words + .chunks(NUM_WORDS_TO_ABSORB) + .enumerate() + .map(|(i, chunk)| { + let mut padded_chunk = [0; NUM_WORDS_TO_ABSORB + 1]; + padded_chunk[0] = if i == 0 { len as u128 } else { 0 }; + padded_chunk[1..(chunk.len() + 1)].copy_from_slice(chunk); + padded_chunk + }) + .collect_vec(); + // Compress every num_word_per_witness words into a witness. + let witnesses_per_chunk = words_per_chunk + .iter() + .map(|chunk| { + chunk + .chunks(num_word_per_witness) + .map(|c| { + c.iter().zip(multipilers.iter()).fold(F::ZERO, |acc, (word, multipiler)| { + acc + F::from_u128(*word) * multipiler + }) + }) + .collect_vec() + }) + .collect_vec(); + // Absorb witnesses keccak_f by keccak_f. + let mut native_poseidon_sponge = + pse_poseidon::Poseidon::::new(POSEIDON_R_F, POSEIDON_R_P); + for witnesses in witnesses_per_chunk { + for absorb in witnesses.chunks(POSEIDON_RATE) { + // To avoid abosring witnesses crossing keccak_fs together, pad 0s to make sure absorb.len() == RATE. + let mut padded_absorb = [F::ZERO; POSEIDON_RATE]; + padded_absorb[..absorb.len()].copy_from_slice(absorb); + native_poseidon_sponge.update(&padded_absorb); + } + } + native_poseidon_sponge.squeeze() +} + +// TODO: Add a function to encode a VarLenBytes into a lookup key. The function should be used by App Circuits. + +/// Encode raw inputs from Keccak circuit witnesses into lookup keys. +/// +/// Each element in the return value corrresponds to a Keccak chunk. If is_final = true, this element is the lookup key of the corresponding logical input. +pub(crate) fn encode_inputs_from_keccak_fs( + ctx: &mut Context, + range_chip: &impl RangeInstructions, + initialized_hasher: &PoseidonHasher, + loaded_keccak_fs: &[LoadedKeccakF], +) -> Vec> { + // Circuit parameters + let num_poseidon_absorb_per_keccak_f = num_poseidon_absorb_per_keccak_f::(); + let num_word_per_witness = num_word_per_witness::(); + let num_witness_per_keccak_f = POSEIDON_RATE * num_poseidon_absorb_per_keccak_f; + + // Constant witnesses + let rate_witness = ctx.load_constant(F::from(POSEIDON_RATE as u64)); + let zero_witness = ctx.load_zero(); + let multiplier_witnesses = ctx.assign_witnesses(get_words_to_witness_multipiler::()); + + let compact_input_len = loaded_keccak_fs.len() * num_poseidon_absorb_per_keccak_f; + let mut compact_inputs = Vec::with_capacity(compact_input_len); + let mut is_final_last = zero_witness; + for loaded_keccak_f in loaded_keccak_fs { + // If this keccak_f is the last of a logical input. + let is_final = loaded_keccak_f.is_final; + let mut poseidon_absorb_data = Vec::with_capacity(num_witness_per_keccak_f); + + // First witness of a keccak_f: [, word_values[0], word_values[1], ...] + // is the length of the input if this is the first keccak_f of a logical input. Otherwise 0. + let mut words = Vec::with_capacity(num_word_per_witness); + let len_word = + range_chip.gate().select(ctx, loaded_keccak_f.bytes_left, zero_witness, is_final_last); + words.push(len_word); + words.extend_from_slice(&loaded_keccak_f.word_values[0..(num_word_per_witness - 1)]); + let first_witness = range_chip.gate().inner_product( + ctx, + multiplier_witnesses.clone(), + words.iter().map(|w| halo2_base::QuantumCell::Existing(*w)), + ); + poseidon_absorb_data.push(first_witness); + + // Turn every num_word_per_witness words later into a witness. + for words in &loaded_keccak_f + .word_values + .into_iter() + .skip(num_word_per_witness - 1) + .chunks(num_word_per_witness) + { + let mut words = words.collect_vec(); + words.resize(num_word_per_witness, zero_witness); + let witness = range_chip.gate().inner_product( + ctx, + multiplier_witnesses.clone(), + words.iter().map(|w| halo2_base::QuantumCell::Existing(*w)), + ); + poseidon_absorb_data.push(witness); + } + // Pad 0s to make sure poseidon_absorb_data.len() % RATE == 0. + poseidon_absorb_data.resize(num_witness_per_keccak_f, zero_witness); + for (i, poseidon_absorb) in poseidon_absorb_data.chunks(POSEIDON_RATE).enumerate() { + compact_inputs.push(PoseidonCompactInput::new( + poseidon_absorb.try_into().unwrap(), + if i + 1 == num_poseidon_absorb_per_keccak_f { + SafeTypeChip::unsafe_to_bool(is_final) + } else { + SafeTypeChip::unsafe_to_bool(zero_witness) + }, + rate_witness, + )); + } + is_final_last = is_final; + } + + let compact_outputs = initialized_hasher.hash_compact_input(ctx, range_chip, &compact_inputs); + + compact_outputs + .into_iter() + .skip(num_poseidon_absorb_per_keccak_f - 1) + .step_by(num_poseidon_absorb_per_keccak_f) + .collect_vec() +} + +/// Number of Keccak words in each encoded input for Poseidon. +pub fn num_word_per_witness() -> usize { + (F::CAPACITY as usize) / NUM_BITS_PER_WORD +} + +/// Number of witnesses to represent inputs in a keccak_f. +/// +/// Assume the representation of is not longer than a Keccak word. +pub fn num_witness_per_keccak_f() -> usize { + // With , a keccak_f could have NUM_WORDS_TO_ABSORB + 1 words. + // ceil((NUM_WORDS_TO_ABSORB + 1) / num_word_per_witness) + NUM_WORDS_TO_ABSORB / num_word_per_witness::() + 1 +} + +/// Number of Poseidon absorb rounds per keccak_f. +pub fn num_poseidon_absorb_per_keccak_f() -> usize { + // Each absorb round consumes RATE witnesses. + // ceil(num_witness_per_keccak_f / RATE) + (num_witness_per_keccak_f::() - 1) / POSEIDON_RATE + 1 +} + +fn get_words_to_witness_multipiler() -> Vec { + let num_word_per_witness = num_word_per_witness::(); + let mut multiplier_f = F::ONE; + let mut multipliers = Vec::with_capacity(num_word_per_witness); + multipliers.push(multiplier_f); + let base_f = F::from_u128(1 << NUM_BITS_PER_WORD); + for _ in 1..num_word_per_witness { + multiplier_f *= base_f; + multipliers.push(multiplier_f); + } + multipliers +} diff --git a/hashes/zkevm/src/keccak/co_circuit/mod.rs b/hashes/zkevm/src/keccak/co_circuit/mod.rs new file mode 100644 index 00000000..61e2e74c --- /dev/null +++ b/hashes/zkevm/src/keccak/co_circuit/mod.rs @@ -0,0 +1,8 @@ +/// Module of Keccak coprocessor circuit. +pub mod circuit; +/// Module of encoding raw inputs to coprocessor circuit lookup keys. +pub mod encode; +/// Module of Keccak coprocessor circuit constant parameters. +pub mod param; +#[cfg(test)] +mod tests; diff --git a/hashes/zkevm/src/keccak/co_circuit/param.rs b/hashes/zkevm/src/keccak/co_circuit/param.rs new file mode 100644 index 00000000..889d0bd9 --- /dev/null +++ b/hashes/zkevm/src/keccak/co_circuit/param.rs @@ -0,0 +1,12 @@ +pub const OUTPUT_NUM_COL_COMMIT: usize = 1; +pub const OUTPUT_NUM_COL_RAW: usize = 3; +pub const OUTPUT_COL_IDX_COMMIT: usize = 0; +pub const OUTPUT_COL_IDX_KEY: usize = 0; +pub const OUTPUT_COL_IDX_HASH_LO: usize = 1; +pub const OUTPUT_COL_IDX_HASH_HI: usize = 2; + +pub const POSEIDON_T: usize = 3; +pub const POSEIDON_RATE: usize = 2; +pub const POSEIDON_R_F: usize = 8; +pub const POSEIDON_R_P: usize = 57; +pub const POSEIDON_SECURE_MDS: usize = 0; diff --git a/hashes/zkevm/src/keccak/co_circuit/tests/mod.rs b/hashes/zkevm/src/keccak/co_circuit/tests/mod.rs new file mode 100644 index 00000000..00a323a0 --- /dev/null +++ b/hashes/zkevm/src/keccak/co_circuit/tests/mod.rs @@ -0,0 +1,42 @@ +use super::circuit::{ + multi_inputs_to_circuit_outputs, KeccakCoprocessorCircuit, KeccakCoprocessorCircuitParams, +}; + +use halo2_base::halo2_proofs::{dev::MockProver, halo2curves::bn256::Fr}; +use itertools::Itertools; + +#[test] +fn test() { + let k: usize = 18; + let num_unusable_row: usize = 109; + let lookup_bits: usize = 4; + let capacity: usize = 10; + let publish_raw_outputs: bool = true; + + let inputs = vec![ + vec![], + (0u8..1).collect::>(), + (0u8..135).collect::>(), + (0u8..136).collect::>(), + (0u8..200).collect::>(), + ]; + + let params = KeccakCoprocessorCircuitParams::new( + k, + num_unusable_row, + lookup_bits, + capacity, + publish_raw_outputs, + ); + let circuit = KeccakCoprocessorCircuit::::new(inputs.clone(), params.clone()); + let circuit_outputs = multi_inputs_to_circuit_outputs::(&inputs, ¶ms); + + let instances = vec![ + circuit_outputs.iter().map(|o| o.key).collect_vec(), + circuit_outputs.iter().map(|o| o.hash_lo).collect_vec(), + circuit_outputs.iter().map(|o| o.hash_hi).collect_vec(), + ]; + + let prover = MockProver::::run(k as u32, &circuit, instances).unwrap(); + prover.assert_satisfied(); +} diff --git a/hashes/zkevm/src/keccak/computation_circuit/circuit.rs b/hashes/zkevm/src/keccak/computation_circuit/circuit.rs deleted file mode 100644 index 497b1d3d..00000000 --- a/hashes/zkevm/src/keccak/computation_circuit/circuit.rs +++ /dev/null @@ -1,343 +0,0 @@ -use std::cell::RefCell; - -use super::poseidon_params; -use crate::{ - keccak::{ - multi_keccak, - param::{NUM_ROUNDS, NUM_WORDS_TO_ABSORB}, - KeccakAssignedRow, KeccakCircuitConfig, KeccakConfigParams, - }, - util::eth_types::Field, -}; -use halo2_base::{ - gates::{ - circuit::{builder::RangeCircuitBuilder, BaseConfig}, - GateInstructions, RangeInstructions, - }, - halo2_proofs::{ - circuit::{Layouter, SimpleFloorPlanner}, - plonk::{Circuit, ConstraintSystem, Error}, - }, - poseidon::hasher::{spec::OptimizedPoseidonSpec, PoseidonCompactInput, PoseidonHasher}, - safe_types::SafeTypeChip, - AssignedValue, -}; -use itertools::{izip, Itertools}; -use sha3::{Digest, Keccak256}; - -/// Keccak Computation Circuit -pub struct KeccakComputationCircuit { - range_circuit_builder: RefCell>, - inputs: Vec>, - capacity: usize, - config_params: KeccakComputationCircuitParams, -} - -#[derive(Default, Clone)] -pub struct KeccakComputationCircuitParams { - k: usize, - keccak_circuit_params: KeccakConfigParams, -} - -#[derive(Clone)] -pub struct KeccakComputationConfig { - range_circuit_config: BaseConfig, - keccak_circuit_config: KeccakCircuitConfig, -} - -impl Circuit for KeccakComputationCircuit { - type Config = KeccakComputationConfig; - type FloorPlanner = SimpleFloorPlanner; - type Params = KeccakComputationCircuitParams; - - fn params(&self) -> Self::Params { - self.config_params.clone() - } - - /// Creates a new instance of the [RangeCircuitBuilder] without witnesses by setting the witness_gen_only flag to false - fn without_witnesses(&self) -> Self { - unimplemented!() - } - - /// Configures a new circuit using [`BaseConfigParams`] - fn configure_with_params(meta: &mut ConstraintSystem, params: Self::Params) -> Self::Config { - let range_circuit_config = RangeCircuitBuilder::configure(meta); - let keccak_circuit_config = KeccakCircuitConfig::new(meta, params.keccak_circuit_params); - Self::Config { range_circuit_config, keccak_circuit_config } - } - - fn configure(_: &mut ConstraintSystem) -> Self::Config { - unreachable!("You must use configure_with_params"); - } - - fn synthesize( - &self, - config: Self::Config, - mut layouter: impl Layouter, - ) -> Result<(), Error> { - let k = self.config_params.k; - config.keccak_circuit_config.load_aux_tables(&mut layouter, k as u32)?; - // TODO: do I need SKIP_FIRST_PASS? - let (keccak_rows, _) = multi_keccak::( - &self.inputs, - Some(self.capacity), - self.params().keccak_circuit_params, - ); - let mut keccak_assigned_rows: Vec> = Vec::default(); - layouter.assign_region( - || "keccak circuit", - |mut region| { - keccak_assigned_rows = - config.keccak_circuit_config.assign(&mut region, &keccak_rows); - Ok(()) - }, - )?; - let loaded_assigned_rows = self.load_keccak_assigned_rows(keccak_assigned_rows); - - let (compact_inputs, result_selector_per_chunk) = - self.generate_poseidon_inputs(&loaded_assigned_rows); - - let _circuit_final_output = self.compute_circuit_final_output( - &loaded_assigned_rows, - &compact_inputs, - &result_selector_per_chunk, - ); - - self.range_circuit_builder.borrow().synthesize(config.range_circuit_config, layouter)?; - Ok(()) - } -} - -/// Witnesses to be exposed as circuit outputs. -struct KeccakCircuitOutput { - pub(crate) input_poseidon: AssignedValue, - pub(crate) hash_lo: AssignedValue, - pub(crate) hash_hi: AssignedValue, -} - -struct LoadedKeccakAssignedRow { - pub(crate) is_final: AssignedValue, - pub(crate) hash_lo: AssignedValue, - pub(crate) hash_hi: AssignedValue, - pub(crate) bytes_left: AssignedValue, - pub(crate) word_value: AssignedValue, -} - -impl KeccakComputationCircuit { - /// Load keccak assigned rows into halo2-lib. - fn load_keccak_assigned_rows( - &self, - assigned_rows: Vec>, - ) -> Vec> { - let mut loaded_assigned_rows = Vec::with_capacity(assigned_rows.len()); - let range_circuit_builder = self.range_circuit_builder.borrow(); - let mut copy_manager = range_circuit_builder.core().copy_manager.lock().unwrap(); - for assigned_row in assigned_rows { - loaded_assigned_rows.push(LoadedKeccakAssignedRow { - is_final: copy_manager.load_external_assigned(assigned_row.is_final), - hash_lo: copy_manager.load_external_assigned(assigned_row.hash_lo), - hash_hi: copy_manager.load_external_assigned(assigned_row.hash_hi), - bytes_left: copy_manager.load_external_assigned(assigned_row.bytes_left), - word_value: copy_manager.load_external_assigned(assigned_row.word_value), - }); - } - loaded_assigned_rows - } - - // Generate compact inputs for Poseidon based on raw inputs in Keccak witnesses. - // - // To illustrate how this function works, let's take the following example: - // Parameters: RATE = 2, NUM_WORDS_TO_ABSORB = 2, NUM_ROUNDS = 3 - // Considering the following logical input in little endian bytes: - // logical_input = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x21, 0x22, 0x23, 0x24, 0x25] - // Keccak witnesses represent the input in 2 chunks(NUM_ROUNDS + 1 rows per chunk): - // loaded_assigned_rows = [ - // {bytes_left: 21, word_value: 0x0807060504030201, is_final: N/A}, - // {bytes_left: 13, word_value: 0x1817161514131211, is_final: N/A}, - // {bytes_left: N/A, word_value: N/A, is_final: N/A}, // No input/output information after first NUM_WORDS_TO_ABSORB rows. - // {bytes_left: N/A, word_value: N/A, is_final: false}, - // {bytes_left: 5, word_value: 0x0000002524232221, is_final: N/A}, - // {bytes_left: 0, word_value: 0x0000000000000000, is_final: N/A}, - // {bytes_left: N/A, word_value: N/A, is_final: N/A}, // No input/output information after first NUM_WORDS_TO_ABSORB rows. - // {bytes_left: N/A, word_value: N/A, is_final: true}, - // ] - // For easier Poseidon processing, the raw input is encoded in the following way: - // If the logic input is empty, [0; RATE]. - // If the logic input is empty, each word is encoded as RATE witnesses. First word is encoded - // as [, logical_input[0], 0..]. Other words are encoded as [logical_input[i], 0, 0..]. - // Note: With this encoding, App circuits should be able to compute Poseidons of variable length inputs easily. - // Then we get the following compact inputs for Poseidon hasher: - // poseidon_compact_inputs = [ - // {inputs: [ 21, 0x0807060504030201], len: 2, is_final: false}, // 21 is the length of the logical input. - // {inputs: [0x1817161514131211, 0x0], len: 2, is_final: false}, - // {inputs: [0x0000002524232221, 0x0], len: 2, is_final: true}, // The last row of the logical input. - // {inputs: [ 0x0, 0x0], len: 2, is_final: true}, // This row corresponds to the padding row in loaded_assigned_rows. - // ] - // The Poseidon results will be: - // poseidon_compact_outputs = [ - // {hash: N/A, is_final: false}, - // {hash: N/A, is_final: false}, - // {hash: ), is_final: true}, - // {hash: poseidon([0x0, 0x0]), is_final: true}, - // ] - // Because of the padding rows, is_final cannot tell which row is the result of a logical input. Therefore - // we also build a selector array(2d, * NUM_WORDS_TO_ABSORB) to select reuslts: - // poseidon_selector: [[0, 0], [1, 0]] - fn generate_poseidon_inputs( - &self, - loaded_assigned_rows: &[LoadedKeccakAssignedRow], - ) -> ( - Vec>, - Vec<[AssignedValue; NUM_WORDS_TO_ABSORB]>, - ) { - let rows_per_round = self.config_params.keccak_circuit_params.rows_per_round; - let mut range_circuit_builder_mut = self.range_circuit_builder.borrow_mut(); - let ctx = range_circuit_builder_mut.main(0); - let range_chip = self.range_circuit_builder.borrow().range_chip(); - - let num_chunks = (loaded_assigned_rows.len() / rows_per_round - 1) / (NUM_ROUNDS + 1); - let compact_input_len = num_chunks * (NUM_WORDS_TO_ABSORB); - let mut result_selector_per_chunk = Vec::with_capacity(num_chunks); - let mut compact_inputs = Vec::with_capacity(compact_input_len); - - let rate_witness = ctx.load_constant(F::from(poseidon_params::RATE as u64)); - let zero_witness = ctx.load_zero(); - let mut chunk_is_final_last = zero_witness; - - // Skip the first round which is dummy. - for chunk in - &loaded_assigned_rows.iter().step_by(rows_per_round).skip(1).chunks(NUM_ROUNDS + 1) - { - let chunk = chunk.collect_vec(); - // If this chunk is the last chunk of a logical input. - let chunk_is_final = chunk[NUM_ROUNDS].is_final; - let mut result_selector = [zero_witness; NUM_WORDS_TO_ABSORB]; - let mut result_selector_is_set = zero_witness; - for round_idx in 0..NUM_WORDS_TO_ABSORB { - let round = chunk[round_idx]; - // First word is encoded as [bytes_left, word_value, 0..]. Here bytes_left equals to the length of the input. - // Other words are encoded as [word_value, 0, 0..]. - let mut inputs = [zero_witness; { poseidon_params::RATE }]; - if round_idx == 0 { - inputs[0] = range_chip.gate().select( - ctx, - round.bytes_left, - round.word_value, - chunk_is_final_last, - ); - inputs[1] = range_chip.gate().select( - ctx, - round.word_value, - zero_witness, - chunk_is_final_last, - ); - } else { - inputs[0] = round.word_value; - } - let is_final = if round_idx == NUM_WORDS_TO_ABSORB - 1 { - chunk_is_final - } else { - let next_bytes_left_is_zero = - range_chip.gate().is_zero(ctx, chunk[round_idx + 1].bytes_left); - range_chip.gate().and(ctx, next_bytes_left_is_zero, chunk_is_final) - }; - // First round with is_final == true outputs the poseidon result of the input. - // All later rounds are dummies. - result_selector[round_idx] = - range_chip.gate().select(ctx, zero_witness, is_final, result_selector_is_set); - result_selector_is_set = - range_chip.gate().or(ctx, result_selector_is_set, result_selector[round_idx]); - - compact_inputs.push(PoseidonCompactInput::new( - inputs, - SafeTypeChip::unsafe_to_bool(is_final), - rate_witness, - )); - } - result_selector_per_chunk.push(result_selector); - chunk_is_final_last = chunk_is_final; - } - (compact_inputs, result_selector_per_chunk) - } - - // Compute poseidon hash of logical inputs then combine with Keccak256 hash. - fn compute_circuit_final_output( - &self, - loaded_assigned_rows: &[LoadedKeccakAssignedRow], - compact_inputs: &[PoseidonCompactInput], - result_selector_per_chunk: &[[AssignedValue; NUM_WORDS_TO_ABSORB]], - ) -> Vec> { - let rows_per_round = self.config_params.keccak_circuit_params.rows_per_round; - let mut range_circuit_builder_mut = self.range_circuit_builder.borrow_mut(); - let ctx = range_circuit_builder_mut.main(0); - let range_chip = self.range_circuit_builder.borrow().range_chip(); - - let num_chunks = (loaded_assigned_rows.len() / rows_per_round - 1) / (NUM_ROUNDS + 1); - - let zero_witness = ctx.load_zero(); - - // Filter out the first row of the last round of each chunk, which contains keccak hash result. - let keccak_output_rows = loaded_assigned_rows - .iter() - .step_by(rows_per_round) - .step_by(NUM_ROUNDS + 1) - .skip(1) - .collect_vec(); - - // Construct in-circuit Poseidon hasher. Assuming SECURE_MDS = 0. - let spec = - OptimizedPoseidonSpec::::new::< - { poseidon_params::R_F }, - { poseidon_params::R_P }, - { poseidon_params::SECURE_MDS }, - >(); - let mut poseidon_hasher = - PoseidonHasher::::new(spec); - assert!(poseidon_params::RATE >= 2, "Poseidon RATE must be at least to encode inputs"); - poseidon_hasher.initialize_consts(ctx, range_chip.gate()); - let dummy_input_poseidon = poseidon_hasher.hash_fix_len_array( - ctx, - &range_chip, - &[zero_witness; { poseidon_params::RATE }], - ); - let mut circuit_final_outputs = Vec::with_capacity(num_chunks); - // Output of Keccak256::digest is big endian. - let dummy_keccak_val = Keccak256::digest(&[]); - let dummy_keccak_val_lo = u128::from_be_bytes(dummy_keccak_val[16..].try_into().unwrap()); - let dummy_keccak_val_hi = u128::from_be_bytes(dummy_keccak_val[..16].try_into().unwrap()); - let dummy_keccak_lo_witness = ctx.load_constant(F::from_u128(dummy_keccak_val_lo)); - let dummy_keccak_hi_witness = ctx.load_constant(F::from_u128(dummy_keccak_val_hi)); - let compact_outputs = poseidon_hasher.hash_compact_input(ctx, &range_chip, &compact_inputs); - for (compact_output, keccak_output_row, result_selector) in izip!( - compact_outputs.chunks(NUM_WORDS_TO_ABSORB), - keccak_output_rows, - result_selector_per_chunk, - ) { - let mut input_poseidon = range_chip.gate().inner_product( - ctx, - compact_output.iter().map(|o| *o.hash()), - result_selector.iter().map(|s| halo2_base::QuantumCell::Existing(*s)), - ); - input_poseidon = range_chip.gate().select( - ctx, - input_poseidon, - dummy_input_poseidon, - keccak_output_row.is_final, - ); - let hash_lo = range_chip.gate().select( - ctx, - keccak_output_row.hash_lo, - dummy_keccak_lo_witness, - keccak_output_row.is_final, - ); - let hash_hi = range_chip.gate().select( - ctx, - keccak_output_row.hash_hi, - dummy_keccak_hi_witness, - keccak_output_row.is_final, - ); - circuit_final_outputs.push(KeccakCircuitOutput { input_poseidon, hash_lo, hash_hi }); - } - circuit_final_outputs - } -} diff --git a/hashes/zkevm/src/keccak/computation_circuit/mod.rs b/hashes/zkevm/src/keccak/computation_circuit/mod.rs deleted file mode 100644 index f810f7d6..00000000 --- a/hashes/zkevm/src/keccak/computation_circuit/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod circuit; -pub mod poseidon_params; diff --git a/hashes/zkevm/src/keccak/computation_circuit/poseidon_params.rs b/hashes/zkevm/src/keccak/computation_circuit/poseidon_params.rs deleted file mode 100644 index 964e8233..00000000 --- a/hashes/zkevm/src/keccak/computation_circuit/poseidon_params.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub const T: usize = 2; -pub const RATE: usize = 2; -pub const R_F: usize = 8; -pub const R_P: usize = 57; -pub const SECURE_MDS: usize = 0; diff --git a/hashes/zkevm/src/keccak/keccak_packed_multi.rs b/hashes/zkevm/src/keccak/keccak_packed_multi.rs index 1b9b005d..b8af147b 100644 --- a/hashes/zkevm/src/keccak/keccak_packed_multi.rs +++ b/hashes/zkevm/src/keccak/keccak_packed_multi.rs @@ -148,16 +148,17 @@ pub struct KeccakTable { impl KeccakTable { /// Construct a new KeccakTable pub fn construct(meta: &mut ConstraintSystem) -> Self { - let input_len = meta.advice_column(); + let is_enabled = meta.advice_column(); let word_value = meta.advice_column(); let bytes_left = meta.advice_column(); - meta.enable_equality(input_len); - Self { - is_enabled: meta.advice_column(), - output: Word::new([meta.advice_column(), meta.advice_column()]), - word_value, - bytes_left, - } + let hash_lo = meta.advice_column(); + let hash_hi = meta.advice_column(); + meta.enable_equality(is_enabled); + meta.enable_equality(word_value); + meta.enable_equality(bytes_left); + meta.enable_equality(hash_lo); + meta.enable_equality(hash_hi); + Self { is_enabled, output: Word::new([hash_lo, hash_hi]), word_value, bytes_left } } } diff --git a/hashes/zkevm/src/keccak/mod.rs b/hashes/zkevm/src/keccak/mod.rs index 3c9eaf1b..fd7095e0 100644 --- a/hashes/zkevm/src/keccak/mod.rs +++ b/hashes/zkevm/src/keccak/mod.rs @@ -23,7 +23,8 @@ use rayon::prelude::{IntoParallelRefIterator, ParallelIterator}; use std::marker::PhantomData; pub mod cell_manager; -pub mod computation_circuit; +/// Module for coprocessor circuits. +pub mod co_circuit; pub mod keccak_packed_multi; pub mod param; pub mod table; @@ -632,7 +633,7 @@ impl KeccakCircuitConfig { 0.expr(), ); // !is_final[i] ==> bytes_left[i + num_rows_per_round] + word_len == bytes_left[i] - cb.condition(not::expr(is_final_expr), |cb| { + cb.condition(not::expr(q(q_first, meta)) * not::expr(is_final_expr), |cb| { let bytes_left_next_expr = meta.query_advice(keccak_table.bytes_left, Rotation(num_rows_per_round as i32)); cb.require_equal( diff --git a/hashes/zkevm/src/keccak/param.rs b/hashes/zkevm/src/keccak/param.rs index 159b7e52..9973dae7 100644 --- a/hashes/zkevm/src/keccak/param.rs +++ b/hashes/zkevm/src/keccak/param.rs @@ -1,5 +1,5 @@ #![allow(dead_code)] -pub(crate) const MAX_DEGREE: usize = 4; +pub(crate) const MAX_DEGREE: usize = 5; pub(crate) const ABSORB_LOOKUP_RANGE: usize = 3; pub(crate) const THETA_C_LOOKUP_RANGE: usize = 6; pub(crate) const RHO_PI_LOOKUP_RANGE: usize = 4; From f33c5d37054db608f2250f05a01f04ccee823d4a Mon Sep 17 00:00:00 2001 From: Xinding Wei Date: Sun, 3 Sep 2023 02:43:27 -0700 Subject: [PATCH 4/8] Optimize Keccak circuit MAX_DEGREE --- hashes/zkevm/src/keccak/mod.rs | 29 ++++++++++++++++------------ hashes/zkevm/src/keccak/param.rs | 2 +- hashes/zkevm/src/keccak/tests.rs | 33 ++++++++++++++++++++++---------- 3 files changed, 41 insertions(+), 23 deletions(-) diff --git a/hashes/zkevm/src/keccak/mod.rs b/hashes/zkevm/src/keccak/mod.rs index fd7095e0..2538ad15 100644 --- a/hashes/zkevm/src/keccak/mod.rs +++ b/hashes/zkevm/src/keccak/mod.rs @@ -619,27 +619,32 @@ impl KeccakCircuitConfig { meta.query_advice(keccak_table.bytes_left, Rotation::cur()), ); }); - let is_final_expr = meta.query_advice(is_final, Rotation::cur()); // is_final ==> bytes_left == 0. // Note: is_final = true only in the last round, which doesn't have any data to absorb. cb.condition(meta.query_advice(is_final, Rotation::cur()), |cb| { cb.require_zero("bytes_left should be 0 when is_final", bytes_left_expr.clone()); }); - // word_len = q_input? NUM_BYTES_PER_WORD - sum(is_paddings): 0 - // Only rounds with q_input == true have inputs to absorb. - let word_len = select::expr( - q(q_input, meta), - NUM_BYTES_PER_WORD.expr() - sum::expr(is_paddings.clone()), - 0.expr(), - ); - // !is_final[i] ==> bytes_left[i + num_rows_per_round] + word_len == bytes_left[i] - cb.condition(not::expr(q(q_first, meta)) * not::expr(is_final_expr), |cb| { + cb.condition(q(q_input, meta), |cb| { + // word_len = NUM_BYTES_PER_WORD - sum(is_paddings) + let word_len = NUM_BYTES_PER_WORD.expr() - sum::expr(is_paddings.clone()); + let bytes_left_next_expr = + meta.query_advice(keccak_table.bytes_left, Rotation(num_rows_per_round as i32)); + cb.require_equal( + "if q_input, bytes_left[curr + num_rows_per_round] + word_len == bytes_left[curr]", + bytes_left_expr.clone(), + bytes_left_next_expr + word_len, + ); + }); + // !q_input[cur] && !start_new_hash(cur) ==> bytes_left[cur + num_rows_per_round] + word_len == bytes_left[cur] + // !q_input[cur] && !start_new_hash(cur) === !(q_input[cur] || start_new_hash(cur)) + // Because q_input[cur] and start_new_hash(cur) are never both true at the same time, we use + instead of or in order to save a degree. + cb.condition(not::expr(q(q_input, meta) + start_new_hash(meta, Rotation::cur())), |cb| { let bytes_left_next_expr = meta.query_advice(keccak_table.bytes_left, Rotation(num_rows_per_round as i32)); cb.require_equal( - "if not final, bytes_left decreaes by the length of the word", + "if no input and not starting new hash, bytes_left should keep the same", bytes_left_expr, - bytes_left_next_expr.clone() + word_len, + bytes_left_next_expr, ); }); diff --git a/hashes/zkevm/src/keccak/param.rs b/hashes/zkevm/src/keccak/param.rs index 9973dae7..abecd264 100644 --- a/hashes/zkevm/src/keccak/param.rs +++ b/hashes/zkevm/src/keccak/param.rs @@ -1,5 +1,5 @@ #![allow(dead_code)] -pub(crate) const MAX_DEGREE: usize = 5; +pub(crate) const MAX_DEGREE: usize = 3; pub(crate) const ABSORB_LOOKUP_RANGE: usize = 3; pub(crate) const THETA_C_LOOKUP_RANGE: usize = 6; pub(crate) const RHO_PI_LOOKUP_RANGE: usize = 4; diff --git a/hashes/zkevm/src/keccak/tests.rs b/hashes/zkevm/src/keccak/tests.rs index 211d91c1..3399b49e 100644 --- a/hashes/zkevm/src/keccak/tests.rs +++ b/hashes/zkevm/src/keccak/tests.rs @@ -212,15 +212,28 @@ fn extract_u128(assigned_value: KeccakAssignedValue) -> u128 { #[test_case(12, 5; "k: 12, rows_per_round: 5")] fn packed_multi_keccak_simple(k: u32, rows_per_round: usize) { let _ = env_logger::builder().is_test(true).try_init(); - - let inputs = vec![ - vec![], - (0u8..1).collect::>(), - (0u8..135).collect::>(), - (0u8..136).collect::>(), - (0u8..200).collect::>(), - ]; - verify::(KeccakConfigParams { k, rows_per_round }, inputs, true); + { + // First input is empty. + let inputs = vec![ + vec![], + (0u8..1).collect::>(), + (0u8..135).collect::>(), + (0u8..136).collect::>(), + (0u8..200).collect::>(), + ]; + verify::(KeccakConfigParams { k, rows_per_round }, inputs, true); + } + { + // First input is not empty. + let inputs = vec![ + (0u8..200).collect::>(), + vec![], + (0u8..1).collect::>(), + (0u8..135).collect::>(), + (0u8..136).collect::>(), + ]; + verify::(KeccakConfigParams { k, rows_per_round }, inputs, true); + } } #[test_case(14, 25 ; "k: 14, rows_per_round: 25")] @@ -231,11 +244,11 @@ fn packed_multi_keccak_prover(k: u32, rows_per_round: usize) { let params = ParamsKZG::::setup(k, OsRng); let inputs = vec![ + (0u8..200).collect::>(), vec![], (0u8..1).collect::>(), (0u8..135).collect::>(), (0u8..136).collect::>(), - (0u8..200).collect::>(), ]; let circuit = KeccakCircuit::new( KeccakConfigParams { k, rows_per_round }, From 7d8a24b5d51f75ab7394e708d6122cc63d3bf623 Mon Sep 17 00:00:00 2001 From: Xinding Wei Date: Sun, 3 Sep 2023 02:50:48 -0700 Subject: [PATCH 5/8] Fix comments --- hashes/zkevm/src/keccak/mod.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/hashes/zkevm/src/keccak/mod.rs b/hashes/zkevm/src/keccak/mod.rs index 2538ad15..468516af 100644 --- a/hashes/zkevm/src/keccak/mod.rs +++ b/hashes/zkevm/src/keccak/mod.rs @@ -624,18 +624,19 @@ impl KeccakCircuitConfig { cb.condition(meta.query_advice(is_final, Rotation::cur()), |cb| { cb.require_zero("bytes_left should be 0 when is_final", bytes_left_expr.clone()); }); + //q_input[cur] ==> bytes_left[cur + num_rows_per_round] + word_len == bytes_left[cur] cb.condition(q(q_input, meta), |cb| { // word_len = NUM_BYTES_PER_WORD - sum(is_paddings) let word_len = NUM_BYTES_PER_WORD.expr() - sum::expr(is_paddings.clone()); let bytes_left_next_expr = meta.query_advice(keccak_table.bytes_left, Rotation(num_rows_per_round as i32)); cb.require_equal( - "if q_input, bytes_left[curr + num_rows_per_round] + word_len == bytes_left[curr]", + "if there is a word in this round, bytes_left[curr + num_rows_per_round] + word_len == bytes_left[curr]", bytes_left_expr.clone(), bytes_left_next_expr + word_len, ); }); - // !q_input[cur] && !start_new_hash(cur) ==> bytes_left[cur + num_rows_per_round] + word_len == bytes_left[cur] + // !q_input[cur] && !start_new_hash(cur) ==> bytes_left[cur + num_rows_per_round] == bytes_left[cur] // !q_input[cur] && !start_new_hash(cur) === !(q_input[cur] || start_new_hash(cur)) // Because q_input[cur] and start_new_hash(cur) are never both true at the same time, we use + instead of or in order to save a degree. cb.condition(not::expr(q(q_input, meta) + start_new_hash(meta, Rotation::cur())), |cb| { From e2cfa9e560c44909fcec976ddc2c11b8877afa7a Mon Sep 17 00:00:00 2001 From: Xinding Wei Date: Tue, 5 Sep 2023 13:59:20 -0700 Subject: [PATCH 6/8] Fix bug & typos --- hashes/zkevm/src/keccak/co_circuit/circuit.rs | 1 + hashes/zkevm/src/keccak/co_circuit/encode.rs | 9 +++++---- hashes/zkevm/src/keccak/co_circuit/tests/mod.rs | 1 + 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/hashes/zkevm/src/keccak/co_circuit/circuit.rs b/hashes/zkevm/src/keccak/co_circuit/circuit.rs index 3239d0bd..f5b3b04e 100644 --- a/hashes/zkevm/src/keccak/co_circuit/circuit.rs +++ b/hashes/zkevm/src/keccak/co_circuit/circuit.rs @@ -354,6 +354,7 @@ impl KeccakCoprocessorCircuit { let mut base_circuit_builder_mut = self.base_circuit_builder.borrow_mut(); let ctx = base_circuit_builder_mut.main(0); + // TODO: wrap this into a function which should be shared wiht App circuits. // The length of outputs is determined at compile time. let output_commitment = self.hasher.borrow().hash_fix_len_array( ctx, diff --git a/hashes/zkevm/src/keccak/co_circuit/encode.rs b/hashes/zkevm/src/keccak/co_circuit/encode.rs index 136ab1fe..bac2617c 100644 --- a/hashes/zkevm/src/keccak/co_circuit/encode.rs +++ b/hashes/zkevm/src/keccak/co_circuit/encode.rs @@ -18,7 +18,7 @@ use super::{circuit::LoadedKeccakF, param::*}; /// Encode a native input bytes into its corresponding lookup key. This function can be considered as the spec of the encoding. pub fn encode_native_input(bytes: &[u8]) -> F { assert!(NUM_BITS_PER_WORD <= u128::BITS as usize); - let multipilers = get_words_to_witness_multipiler::(); + let multipilers = get_words_to_witness_multipilers::(); let num_word_per_witness = num_word_per_witness::(); let len = bytes.len(); @@ -93,12 +93,13 @@ pub(crate) fn encode_inputs_from_keccak_fs( // Constant witnesses let rate_witness = ctx.load_constant(F::from(POSEIDON_RATE as u64)); + let one_witness = ctx.load_constant(F::ONE); let zero_witness = ctx.load_zero(); - let multiplier_witnesses = ctx.assign_witnesses(get_words_to_witness_multipiler::()); + let multiplier_witnesses = ctx.assign_witnesses(get_words_to_witness_multipilers::()); let compact_input_len = loaded_keccak_fs.len() * num_poseidon_absorb_per_keccak_f; let mut compact_inputs = Vec::with_capacity(compact_input_len); - let mut is_final_last = zero_witness; + let mut is_final_last = one_witness; for loaded_keccak_f in loaded_keccak_fs { // If this keccak_f is the last of a logical input. let is_final = loaded_keccak_f.is_final; @@ -180,7 +181,7 @@ pub fn num_poseidon_absorb_per_keccak_f() -> usize { (num_witness_per_keccak_f::() - 1) / POSEIDON_RATE + 1 } -fn get_words_to_witness_multipiler() -> Vec { +fn get_words_to_witness_multipilers() -> Vec { let num_word_per_witness = num_word_per_witness::(); let mut multiplier_f = F::ONE; let mut multipliers = Vec::with_capacity(num_word_per_witness); diff --git a/hashes/zkevm/src/keccak/co_circuit/tests/mod.rs b/hashes/zkevm/src/keccak/co_circuit/tests/mod.rs index 00a323a0..8e355dd4 100644 --- a/hashes/zkevm/src/keccak/co_circuit/tests/mod.rs +++ b/hashes/zkevm/src/keccak/co_circuit/tests/mod.rs @@ -14,6 +14,7 @@ fn test() { let publish_raw_outputs: bool = true; let inputs = vec![ + (0u8..200).collect::>(), vec![], (0u8..1).collect::>(), (0u8..135).collect::>(), From 76a2bf79eccce2cae1d0105a7b207abab77792ce Mon Sep 17 00:00:00 2001 From: Xinding Wei Date: Thu, 7 Sep 2023 19:59:05 -0700 Subject: [PATCH 7/8] test --- hashes/zkevm/src/keccak/{ => test}/mod.rs | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename hashes/zkevm/src/keccak/{ => test}/mod.rs (100%) diff --git a/hashes/zkevm/src/keccak/mod.rs b/hashes/zkevm/src/keccak/test/mod.rs similarity index 100% rename from hashes/zkevm/src/keccak/mod.rs rename to hashes/zkevm/src/keccak/test/mod.rs From ba948c8987b786953e7efdf124f272f562678f93 Mon Sep 17 00:00:00 2001 From: Xinding Wei Date: Thu, 7 Sep 2023 19:59:36 -0700 Subject: [PATCH 8/8] Test2 --- hashes/zkevm/src/keccak/mod.rs | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 hashes/zkevm/src/keccak/mod.rs diff --git a/hashes/zkevm/src/keccak/mod.rs b/hashes/zkevm/src/keccak/mod.rs new file mode 100644 index 00000000..e69de29b