diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 298a1ca6..a1fbae44 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,7 +40,6 @@ jobs: working-directory: "hashes/zkevm" run: | cargo test packed_multi_keccak_prover::k_14 - cargo test bit_sha256_prover::k_10 cargo t test_vanilla_keccak_kat_vectors lint: diff --git a/.gitignore b/.gitignore index 17afea38..eb915932 100644 --- a/.gitignore +++ b/.gitignore @@ -17,8 +17,5 @@ Cargo.lock *.png -**/params/* -**/params/ -**/proptest-regressions/ -**/results/ -**/*.DS_Store +/halo2_ecc/src/bn254/data/ +/halo2_ecc/src/secp256k1/data/ diff --git a/halo2-base/Cargo.toml b/halo2-base/Cargo.toml index 82a0ddc6..7b1a3843 100644 --- a/halo2-base/Cargo.toml +++ b/halo2-base/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "halo2-base" -version = "0.4.0" +version = "0.4.1" authors = ["Intrinsic Technologies"] license = "MIT" edition = "2021" @@ -16,7 +16,7 @@ num-integer = "0.1" num-traits = "0.2" rand_chacha = "0.3" rustc-hash = "1.1" -rayon = "1.7" +rayon = "1.8" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" log = "0.4" @@ -24,7 +24,7 @@ getset = "0.1.2" ark-std = { version = "0.3.0", features = ["print-trace"], optional = true } # Use Axiom's custom halo2 monorepo for faster proving when feature = "halo2-axiom" is on -halo2_proofs_axiom = { version = "0.3", package = "halo2-axiom", optional = true } +halo2_proofs_axiom = { version = "0.4", package = "halo2-axiom", optional = true } # Use PSE halo2 and halo2curves for compatibility when feature = "halo2-pse" is on halo2_proofs = { git = "https://github.com/privacy-scaling-explorations/halo2.git", rev = "7a21656", optional = true } diff --git a/halo2-base/src/gates/circuit/builder.rs b/halo2-base/src/gates/circuit/builder.rs index 03dd5f92..dabd50f1 100644 --- a/halo2-base/src/gates/circuit/builder.rs +++ b/halo2-base/src/gates/circuit/builder.rs @@ -372,3 +372,15 @@ pub struct RangeStatistics { /// Total special advice cells that need to be looked up, per phase pub total_lookup_advice_per_phase: Vec, } + +impl AsRef> for BaseCircuitBuilder { + fn as_ref(&self) -> &BaseCircuitBuilder { + self + } +} + +impl AsMut> for BaseCircuitBuilder { + fn as_mut(&mut self) -> &mut BaseCircuitBuilder { + self + } +} diff --git a/halo2-base/src/gates/circuit/mod.rs b/halo2-base/src/gates/circuit/mod.rs index dc0ece12..8b3e6f60 100644 --- a/halo2-base/src/gates/circuit/mod.rs +++ b/halo2-base/src/gates/circuit/mod.rs @@ -19,7 +19,7 @@ pub mod builder; /// A struct defining the configuration parameters for a halo2-base circuit /// - this is used to configure [BaseConfig]. -#[derive(Clone, Default, Debug, Serialize, Deserialize)] +#[derive(Clone, Default, Debug, Hash, Serialize, Deserialize)] pub struct BaseCircuitParams { // Keeping FlexGateConfigParams expanded for backwards compatibility /// Specifies the number of rows in the circuit to be 2k @@ -215,3 +215,15 @@ impl CircuitBuilderStage { matches!(self, CircuitBuilderStage::Prover) } } + +impl AsRef> for BaseConfig { + fn as_ref(&self) -> &BaseConfig { + self + } +} + +impl AsMut> for BaseConfig { + fn as_mut(&mut self) -> &mut BaseConfig { + self + } +} diff --git a/halo2-base/src/gates/flex_gate/mod.rs b/halo2-base/src/gates/flex_gate/mod.rs index 92e59338..5dea4228 100644 --- a/halo2-base/src/gates/flex_gate/mod.rs +++ b/halo2-base/src/gates/flex_gate/mod.rs @@ -303,9 +303,9 @@ pub trait GateInstructions { ctx.assign_region([Constant(F::ZERO), Existing(x), Existing(x), Existing(x)], [0]); } - /// Constrains and returns a / b = 0. + /// Constrains and returns a / b = out. /// - /// Defines a vertical gate of form | 0 | b^1 * a | b | a |, where b^1 * a = out. + /// Defines a vertical gate of form | 0 | a / b | b | a |, where a / b = out. /// /// Assumes `b != 0`. /// * `ctx`: [Context] to add the constraints to @@ -823,7 +823,7 @@ pub trait GateInstructions { /// Constrains and returns little-endian bit vector representation of `a`. /// - /// Assumes `range_bits <= number of bits in a`. + /// Assumes `range_bits >= bit_length(a)`. /// * `a`: [QuantumCell] of the value to convert /// * `range_bits`: range of bits needed to represent `a` fn num_to_bits( diff --git a/halo2-base/src/gates/flex_gate/threads/single_phase.rs b/halo2-base/src/gates/flex_gate/threads/single_phase.rs index f9359814..a554d727 100644 --- a/halo2-base/src/gates/flex_gate/threads/single_phase.rs +++ b/halo2-base/src/gates/flex_gate/threads/single_phase.rs @@ -219,9 +219,15 @@ pub fn assign_with_constraints( .assign_advice(|| "", column, row_offset, || value.map(|v| *v)) .unwrap() .cell(); - copy_manager + if let Some(old_cell) = copy_manager .assigned_advices - .insert(ContextCell::new(ctx.type_id, ctx.context_id, i), cell); + .insert(ContextCell::new(ctx.type_id, ctx.context_id, i), cell) + { + assert!( + old_cell.row_offset == cell.row_offset && old_cell.column == cell.column, + "Trying to overwrite virtual cell with a different raw cell" + ); + } // If selector enabled and row_offset is valid add break point, account for break point overlap, and enforce equality constraint for gate outputs. // ⚠️ This assumes overlap is of form: gate enabled at `i - delta` and `i`, where `delta = ROTATIONS - 1`. We currently do not support `delta < ROTATIONS - 1`. diff --git a/halo2-base/src/poseidon/hasher/mod.rs b/halo2-base/src/poseidon/hasher/mod.rs index 10a03034..68cf64c6 100644 --- a/halo2-base/src/poseidon/hasher/mod.rs +++ b/halo2-base/src/poseidon/hasher/mod.rs @@ -8,7 +8,7 @@ use crate::{ ScalarField, }; -use getset::Getters; +use getset::{CopyGetters, Getters}; use num_bigint::BigUint; use std::{cell::OnceCell, mem}; @@ -23,8 +23,10 @@ pub mod spec; pub mod state; /// Stateless Poseidon hasher. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Getters)] pub struct PoseidonHasher { + /// Spec, contains round constants and optimized matrices. + #[getset(get = "pub")] spec: OptimizedPoseidonSpec, consts: OnceCell>, } @@ -51,13 +53,16 @@ impl PoseidonHasherConsts { - // Right padded inputs. No constrains on paddings. + /// Right padded inputs. No constrains on paddings. + #[getset(get = "pub")] inputs: [AssignedValue; RATE], - // is_final = 1 triggers squeeze. + /// is_final = 1 triggers squeeze. + #[getset(get_copy = "pub")] is_final: SafeBool, - // Length of `inputs`. + /// Length of `inputs`. + #[getset(get_copy = "pub")] len: AssignedValue, } @@ -87,11 +92,13 @@ impl PoseidonCompactInput { } /// A compact chunk input for Poseidon hasher. The end of a logical input could only be at the boundary of a chunk. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Getters, CopyGetters)] pub struct PoseidonCompactChunkInput { - // Inputs of a chunk. All witnesses will be absorbed. + /// Inputs of a chunk. All witnesses will be absorbed. + #[getset(get = "pub")] inputs: Vec<[AssignedValue; RATE]>, - // is_final = 1 triggers squeeze. + /// is_final = 1 triggers squeeze. + #[getset(get_copy = "pub")] is_final: SafeBool, } @@ -103,13 +110,13 @@ impl PoseidonCompactChunkInput { } /// 1 logical row of compact output for Poseidon hasher. -#[derive(Copy, Clone, Debug, Getters)] +#[derive(Copy, Clone, Debug, CopyGetters)] pub struct PoseidonCompactOutput { /// hash of 1 logical input. - #[getset(get = "pub")] + #[getset(get_copy = "pub")] hash: AssignedValue, /// is_final = 1 ==> this is the end of a logical input. - #[getset(get = "pub")] + #[getset(get_copy = "pub")] is_final: SafeBool, } diff --git a/halo2-base/src/poseidon/hasher/tests/hasher.rs b/halo2-base/src/poseidon/hasher/tests/hasher.rs index fba101cc..7b55c3c4 100644 --- a/halo2-base/src/poseidon/hasher/tests/hasher.rs +++ b/halo2-base/src/poseidon/hasher/tests/hasher.rs @@ -160,7 +160,7 @@ fn hasher_compact_chunk_inputs_compatiblity_verification< for (compact_output, chunk_input) in compact_outputs.iter().zip(chunk_inputs) { // into() doesn't work if ! is in the beginning in the bool expression... let is_final_input = chunk_input.is_final.as_ref().value(); - let is_final_output = compact_output.is_final().as_ref().value(); + let is_final_output = compact_output.is_final.as_ref().value(); assert_eq!(is_final_input, is_final_output); if is_final_output == &Fr::ONE { assert_eq!(native_results[output_offset], *compact_output.hash().value()); diff --git a/halo2-base/src/utils/halo2.rs b/halo2-base/src/utils/halo2.rs index 510f7d25..6e77fcb9 100644 --- a/halo2-base/src/utils/halo2.rs +++ b/halo2-base/src/utils/halo2.rs @@ -1,8 +1,12 @@ +use std::collections::hash_map::Entry; + use crate::ff::Field; use crate::halo2_proofs::{ circuit::{AssignedCell, Cell, Region, Value}, - plonk::{Advice, Assigned, Column, Fixed}, + plonk::{Advice, Assigned, Circuit, Column, Fixed}, }; +use crate::virtual_region::copy_constraints::{CopyConstraintManager, EXTERNAL_CELL_TYPE_ID}; +use crate::AssignedValue; /// Raw (physical) assigned cell in Plonkish arithmetization. #[cfg(feature = "halo2-axiom")] @@ -71,3 +75,111 @@ pub fn raw_constrain_equal(region: &mut Region, left: Cell, right: #[cfg(not(feature = "halo2-axiom"))] region.constrain_equal(left, right).unwrap(); } + +/// Constrains that `virtual_cell` is equal to `external_cell`. The `virtual_cell` must have +/// already been raw assigned with the raw assigned cell stored in `copy_manager` +/// **unless** it is marked an external-only cell with type id [EXTERNAL_CELL_TYPE_ID]. +/// * When the virtual cell has already been assigned, the assigned cell is constrained to be equal to the external cell. +/// * When the virtual cell has not been assigned **and** it is marked as an external cell, it is assigned to `external_cell` and the mapping is stored in `copy_manager`. +/// +/// This should only be called when `witness_gen_only` is false, otherwise it will panic. +/// +/// ## Panics +/// If witness generation only mode is true. +pub fn constrain_virtual_equals_external( + region: &mut Region, + virtual_cell: AssignedValue, + external_cell: Cell, + copy_manager: &mut CopyConstraintManager, +) { + let ctx_cell = virtual_cell.cell.unwrap(); + match copy_manager.assigned_advices.entry(ctx_cell) { + Entry::Occupied(acell) => { + // The virtual cell has already been assigned, so we can constrain it to equal the external cell. + region.constrain_equal(*acell.get(), external_cell); + } + Entry::Vacant(assigned) => { + // The virtual cell **must** be an external cell + assert_eq!(ctx_cell.type_id, EXTERNAL_CELL_TYPE_ID); + // We map the virtual cell to point to the raw external cell in `copy_manager` + assigned.insert(external_cell); + } + } +} + +/// This trait should be implemented on the minimal circuit configuration data necessary to +/// completely determine a circuit (independent of circuit inputs). +/// This is used to generate a _dummy_ instantiation of a concrete `Circuit` type for the purposes of key generation. +/// This dummy instantiation just needs to have the correct arithmetization format, but the witnesses do not need to +/// satisfy constraints. +pub trait KeygenCircuitIntent { + /// Concrete circuit type + type ConcreteCircuit: Circuit; + /// Additional data that "pins" down the circuit. These can always to deterministically rederived from `Self`, but + /// storing the `Pinning` saves recomputations in future proof generations. + type Pinning; + + /// The intent must include the log_2 domain size of the circuit. + /// This is used to get the correct trusted setup file. + fn get_k(&self) -> u32; + + /// Builds a _dummy_ instantiation of `Self::ConcreteCircuit` for the purposes of key generation. + /// This dummy instantiation just needs to have the correct arithmetization format, but the witnesses do not need to + /// satisfy constraints. + fn build_keygen_circuit(self) -> Self::ConcreteCircuit; + + /// Pinning is only fully computed after `synthesize` has been run during keygen + fn get_pinning_after_keygen( + self, + kzg_params: &ParamsKZG, + circuit: &Self::ConcreteCircuit, + ) -> Self::Pinning; +} + +use halo2_proofs_axiom::halo2curves::bn256::Bn256; +use halo2_proofs_axiom::poly::kzg::commitment::ParamsKZG; +pub use keygen::ProvingKeyGenerator; + +mod keygen { + use halo2_proofs_axiom::poly::commitment::Params; + + use crate::halo2_proofs::{ + halo2curves::bn256::{Bn256, Fr, G1Affine}, + plonk::{self, ProvingKey}, + poly::kzg::commitment::ParamsKZG, + }; + + use super::KeygenCircuitIntent; + + /// Trait for creating a proving key and a pinning for a circuit from minimal circuit configuration data. + pub trait ProvingKeyGenerator { + /// Create proving key and pinning. + fn create_pk_and_pinning( + self, + kzg_params: &ParamsKZG, + ) -> (ProvingKey, serde_json::Value); + } + + impl ProvingKeyGenerator for CI + where + CI: KeygenCircuitIntent + Clone, + CI::Pinning: serde::Serialize, + { + fn create_pk_and_pinning( + self, + kzg_params: &ParamsKZG, + ) -> (ProvingKey, serde_json::Value) { + assert_eq!(kzg_params.k(), self.get_k()); + let circuit = self.clone().build_keygen_circuit(); + #[cfg(feature = "halo2-axiom")] + let pk = plonk::keygen_pk2(kzg_params, &circuit, false).unwrap(); + #[cfg(not(feature = "halo2-axiom"))] + let pk = { + let vk = plonk::keygen_vk_custom(kzg_params, &circuit, false).unwrap(); + plonk::keygen_pk(kzg_params, vk, &circuit).unwrap() + }; + let pinning = self.get_pinning_after_keygen(kzg_params, &circuit); + (pk, serde_json::to_value(pinning).unwrap()) + } + } +} diff --git a/halo2-base/src/virtual_region/copy_constraints.rs b/halo2-base/src/virtual_region/copy_constraints.rs index 5ab80d3b..11a77944 100644 --- a/halo2-base/src/virtual_region/copy_constraints.rs +++ b/halo2-base/src/virtual_region/copy_constraints.rs @@ -15,6 +15,9 @@ use crate::{ff::Field, ContextCell}; use super::manager::VirtualRegionManager; +/// Type ID to distinguish external raw Halo2 cells. **This Type ID must be unique.** +pub const EXTERNAL_CELL_TYPE_ID: &str = "halo2-base:External Raw Halo2 Cell"; + /// Thread-safe shared global manager for all copy constraints. pub type SharedCopyConstraintManager = Arc>>; @@ -86,11 +89,15 @@ impl CopyConstraintManager { } fn load_external_cell_impl(&mut self, cell: Option) -> ContextCell { - let context_cell = - ContextCell::new("halo2-base:External Raw Halo2 Cell", 0, self.external_cell_count); + let context_cell = ContextCell::new(EXTERNAL_CELL_TYPE_ID, 0, self.external_cell_count); self.external_cell_count += 1; if let Some(cell) = cell { - self.assigned_advices.insert(context_cell, cell); + if let Some(old_cell) = self.assigned_advices.insert(context_cell, cell) { + assert!( + old_cell.row_offset == cell.row_offset && old_cell.column == cell.column, + "External cell already assigned" + ) + } } context_cell } diff --git a/halo2-base/src/virtual_region/lookups.rs b/halo2-base/src/virtual_region/lookups.rs index 817a200b..7823a573 100644 --- a/halo2-base/src/virtual_region/lookups.rs +++ b/halo2-base/src/virtual_region/lookups.rs @@ -8,12 +8,15 @@ use crate::halo2_proofs::{ circuit::{Region, Value}, plonk::{Advice, Column}, }; -use crate::utils::halo2::raw_assign_advice; +use crate::utils::halo2::{constrain_virtual_equals_external, raw_assign_advice}; use crate::{AssignedValue, ContextTag}; use super::copy_constraints::SharedCopyConstraintManager; use super::manager::VirtualRegionManager; +/// Basic dynamic lookup table gadget. +pub mod basic; + /// A manager that can be used for any lookup argument. This manager automates /// the process of copying cells to designed advice columns with lookup enabled. /// It also manages how many such advice columns are necessary. @@ -122,6 +125,8 @@ impl VirtualRegionManager type Config = Vec<[Column; ADVICE_COLS]>; fn assign_raw(&self, config: &Self::Config, region: &mut Region) { + let mut copy_manager = + (!self.witness_gen_only).then(|| self.copy_manager().lock().unwrap()); let cells_to_lookup = self.cells_to_lookup.lock().unwrap(); // Copy the cells to the config columns, going left to right, then top to bottom. // Will panic if out of rows @@ -135,12 +140,8 @@ impl VirtualRegionManager for (advice, &column) in advices.iter().zip(config[lookup_col].iter()) { let bcell = raw_assign_advice(region, column, lookup_offset, Value::known(advice.value)); - if !self.witness_gen_only { - let ctx_cell = advice.cell.unwrap(); - let copy_manager = self.copy_manager.lock().unwrap(); - let acell = - copy_manager.assigned_advices.get(&ctx_cell).expect("cell not assigned"); - region.constrain_equal(*acell, bcell.cell()); + if let Some(copy_manager) = copy_manager.as_mut() { + constrain_virtual_equals_external(region, *advice, bcell.cell(), copy_manager); } } diff --git a/halo2-base/src/virtual_region/lookups/basic.rs b/halo2-base/src/virtual_region/lookups/basic.rs new file mode 100644 index 00000000..f5299c38 --- /dev/null +++ b/halo2-base/src/virtual_region/lookups/basic.rs @@ -0,0 +1,209 @@ +use std::iter::zip; + +use crate::{ + halo2_proofs::{ + circuit::{Layouter, Region, Value}, + halo2curves::ff::Field, + plonk::{Advice, Column, ConstraintSystem, Fixed, Phase}, + poly::Rotation, + }, + utils::{ + halo2::{constrain_virtual_equals_external, raw_assign_advice, raw_assign_fixed}, + ScalarField, + }, + virtual_region::copy_constraints::SharedCopyConstraintManager, + AssignedValue, +}; + +/// A simple dynamic lookup table for when you want to verify some length `KEY_COL` key +/// is in a provided (dynamic) table of the same format. +/// +/// Note that you can also use this to look up (key, out) pairs, where you consider the whole +/// pair as the new key. +/// +/// We can have multiple sets of dedicated columns to be looked up: these can be specified +/// when calling `new`, but typically we just need 1 set. +/// +/// The `table` consists of advice columns. Since this table may have poisoned rows (blinding factors), +/// we use a fixed column `table_selector` which is default 0 and only 1 on enabled rows of the table. +/// The dynamic lookup will check that for `(key, key_is_enabled)` in `to_lookup` we have `key` matches one of +/// the rows in `table` where `table_selector == key_is_enabled`. +/// Reminder: the Halo2 lookup argument will ignore the poisoned rows in `to_lookup` +/// (see [https://zcash.github.io/halo2/design/proving-system/lookup.html#zero-knowledge-adjustment]), but it will +/// not ignore the poisoned rows in `table`. +/// +/// Part of this design consideration is to allow a key of `[F::ZERO; KEY_COL]` to still be used as a valid key +/// in the lookup argument. By default, unfilled rows in `to_lookup` will be all zeros; we require +/// at least one row in `table` where `table_is_enabled = 0` and the rest of the row in `table` are also 0s. +#[derive(Clone, Debug)] +pub struct BasicDynLookupConfig { + /// Columns for cells to be looked up. Consists of `(key, key_is_enabled)`. + pub to_lookup: Vec<([Column; KEY_COL], Column)>, + /// Table to look up against. + pub table: [Column; KEY_COL], + /// Selector to enable a row in `table` to actually be part of the lookup table. This is to prevent + /// blinding factors in `table` advice columns from being used in the lookup. + pub table_is_enabled: Column, +} + +impl BasicDynLookupConfig { + /// Assumes all columns are in the same phase `P` to make life easier. + /// We enable equality on all columns because we envision both the columns to lookup + /// and the table will need to talk to halo2-lib. + pub fn new( + meta: &mut ConstraintSystem, + phase: impl Fn() -> P, + num_lu_sets: usize, + ) -> Self { + let mut make_columns = || { + let advices = [(); KEY_COL].map(|_| { + let advice = meta.advice_column_in(phase()); + meta.enable_equality(advice); + advice + }); + let is_enabled = meta.fixed_column(); + (advices, is_enabled) + }; + let (table, table_is_enabled) = make_columns(); + let to_lookup: Vec<_> = (0..num_lu_sets).map(|_| make_columns()).collect(); + + for (key, key_is_enabled) in &to_lookup { + meta.lookup_any("dynamic lookup table", |meta| { + let table = table.map(|c| meta.query_advice(c, Rotation::cur())); + let table_is_enabled = meta.query_fixed(table_is_enabled, Rotation::cur()); + let key = key.map(|c| meta.query_advice(c, Rotation::cur())); + let key_is_enabled = meta.query_fixed(*key_is_enabled, Rotation::cur()); + zip(key, table).chain([(key_is_enabled, table_is_enabled)]).collect() + }); + } + + Self { table_is_enabled, table, to_lookup } + } + + /// Assign managed lookups. The `keys` must have already been raw assigned beforehand. + /// + /// `copy_manager` **must** be provided unless you are only doing witness generation + /// without constraints. + pub fn assign_virtual_to_lookup_to_raw( + &self, + mut layouter: impl Layouter, + keys: impl IntoIterator; KEY_COL]>, + copy_manager: Option<&SharedCopyConstraintManager>, + ) { + #[cfg(not(feature = "halo2-axiom"))] + let keys = keys.into_iter().collect::>(); + layouter + .assign_region( + || "[BasicDynLookupConfig] Advice cells to lookup", + |mut region| { + self.assign_virtual_to_lookup_to_raw_from_offset( + &mut region, + #[cfg(feature = "halo2-axiom")] + keys, + #[cfg(not(feature = "halo2-axiom"))] + keys.clone(), + 0, + copy_manager, + ); + Ok(()) + }, + ) + .unwrap(); + } + + /// Assign managed lookups. The `keys` must have already been raw assigned beforehand. + /// + /// `copy_manager` **must** be provided unless you are only doing witness generation + /// without constraints. + pub fn assign_virtual_to_lookup_to_raw_from_offset( + &self, + region: &mut Region, + keys: impl IntoIterator; KEY_COL]>, + mut offset: usize, + copy_manager: Option<&SharedCopyConstraintManager>, + ) { + let mut copy_manager = copy_manager.map(|c| c.lock().unwrap()); + // Copied from `LookupAnyManager::assign_raw` but modified to set `key_is_enabled` to 1. + // Copy the cells to the config columns, going left to right, then top to bottom. + // Will panic if out of rows + let mut lookup_col = 0; + for key in keys { + if lookup_col >= self.to_lookup.len() { + lookup_col = 0; + offset += 1; + } + let (key_col, key_is_enabled_col) = self.to_lookup[lookup_col]; + // set key_is_enabled to 1 + raw_assign_fixed(region, key_is_enabled_col, offset, F::ONE); + for (advice, column) in zip(key, key_col) { + let bcell = raw_assign_advice(region, column, offset, Value::known(advice.value)); + if let Some(copy_manager) = copy_manager.as_mut() { + constrain_virtual_equals_external(region, advice, bcell.cell(), copy_manager); + } + } + + lookup_col += 1; + } + } + + /// Assign virtual table to raw. The `rows` must have already been raw assigned beforehand. + /// + /// `copy_manager` **must** be provided unless you are only doing witness generation + /// without constraints. + pub fn assign_virtual_table_to_raw( + &self, + mut layouter: impl Layouter, + rows: impl IntoIterator; KEY_COL]>, + copy_manager: Option<&SharedCopyConstraintManager>, + ) { + #[cfg(not(feature = "halo2-axiom"))] + let rows = rows.into_iter().collect::>(); + layouter + .assign_region( + || "[BasicDynLookupConfig] Dynamic Lookup Table", + |mut region| { + self.assign_virtual_table_to_raw_from_offset( + &mut region, + #[cfg(feature = "halo2-axiom")] + rows, + #[cfg(not(feature = "halo2-axiom"))] + rows.clone(), + 0, + copy_manager, + ); + Ok(()) + }, + ) + .unwrap(); + } + + /// Assign virtual table to raw. The `rows` must have already been raw assigned beforehand. + /// + /// `copy_manager` **must** be provided unless you are only doing witness generation + /// without constraints. + pub fn assign_virtual_table_to_raw_from_offset( + &self, + region: &mut Region, + rows: impl IntoIterator; KEY_COL]>, + mut offset: usize, + copy_manager: Option<&SharedCopyConstraintManager>, + ) { + let mut copy_manager = copy_manager.map(|c| c.lock().unwrap()); + for row in rows { + // Enable this row in the table + raw_assign_fixed(region, self.table_is_enabled, offset, F::ONE); + for (advice, column) in zip(row, self.table) { + let bcell = raw_assign_advice(region, column, offset, Value::known(advice.value)); + if let Some(copy_manager) = copy_manager.as_mut() { + constrain_virtual_equals_external(region, advice, bcell.cell(), copy_manager); + } + } + offset += 1; + } + // always assign one disabled row with all 0s, so disabled to_lookup works for sure + raw_assign_fixed(region, self.table_is_enabled, offset, F::ZERO); + for col in self.table { + raw_assign_advice(region, col, offset, Value::known(F::ZERO)); + } + } +} diff --git a/halo2-base/src/virtual_region/tests/lookups/memory.rs b/halo2-base/src/virtual_region/tests/lookups/memory.rs index 66df4085..8b94a6e5 100644 --- a/halo2-base/src/virtual_region/tests/lookups/memory.rs +++ b/halo2-base/src/virtual_region/tests/lookups/memory.rs @@ -1,11 +1,17 @@ -use crate::halo2_proofs::{ - arithmetic::Field, - circuit::{Layouter, SimpleFloorPlanner, Value}, - dev::MockProver, - halo2curves::bn256::Fr, - plonk::{keygen_pk, keygen_vk, Advice, Circuit, Column, ConstraintSystem, Error}, - poly::Rotation, +use crate::{ + halo2_proofs::{ + arithmetic::Field, + circuit::{Layouter, SimpleFloorPlanner}, + dev::MockProver, + halo2curves::bn256::Fr, + plonk::{keygen_pk, keygen_vk, Assigned, Circuit, ConstraintSystem, Error}, + }, + virtual_region::{ + copy_constraints::EXTERNAL_CELL_TYPE_ID, lookups::basic::BasicDynLookupConfig, + }, + AssignedValue, ContextCell, }; +use halo2_proofs_axiom::plonk::FirstPhase; use rand::{rngs::StdRng, Rng, SeedableRng}; use test_log::test; @@ -16,25 +22,22 @@ use crate::{ }, utils::{ fs::gen_srs, - halo2::raw_assign_advice, testing::{check_proof, gen_proof}, ScalarField, }, - virtual_region::{lookups::LookupAnyManager, manager::VirtualRegionManager}, + virtual_region::manager::VirtualRegionManager, }; #[derive(Clone, Debug)] struct RAMConfig { cpu: FlexGateConfig, - copy: Vec<[Column; 2]>, - // dynamic lookup table - memory: [Column; 2], + memory: BasicDynLookupConfig<2>, } #[derive(Clone, Default)] struct RAMConfigParams { cpu: FlexGateConfigParams, - copy_columns: usize, + num_lu_sets: usize, } struct RAMCircuit { @@ -44,7 +47,7 @@ struct RAMCircuit { ptrs: [usize; CYCLES], cpu: SinglePhaseCoreManager, - ram: LookupAnyManager, + mem_access: Vec<[AssignedValue; 2]>, params: RAMConfigParams, } @@ -57,8 +60,8 @@ impl RAMCircuit { witness_gen_only: bool, ) -> Self { let cpu = SinglePhaseCoreManager::new(witness_gen_only, Default::default()); - let ram = LookupAnyManager::new(witness_gen_only, cpu.copy_manager.clone()); - Self { memory, ptrs, cpu, ram, params } + let mem_access = vec![]; + Self { memory, ptrs, cpu, mem_access, params } } fn compute(&mut self) { @@ -67,9 +70,9 @@ impl RAMCircuit { let mut sum = ctx.load_constant(F::ZERO); for &ptr in &self.ptrs { let value = self.memory[ptr]; - let ptr = ctx.load_witness(F::from(ptr as u64 + 1)); + let ptr = ctx.load_witness(F::from(ptr as u64)); let value = ctx.load_witness(value); - self.ram.add_lookup((ctx.type_id(), ctx.id()), [ptr, value]); + self.mem_access.push([ptr, value]); sum = gate.add(ctx, sum, value); } } @@ -89,30 +92,12 @@ impl Circuit for RAMCircuit { } fn configure_with_params(meta: &mut ConstraintSystem, params: Self::Params) -> Self::Config { - let k = params.cpu.k; - let mut cpu = FlexGateConfig::configure(meta, params.cpu); - let copy: Vec<_> = (0..params.copy_columns) - .map(|_| { - [(); 2].map(|_| { - let advice = meta.advice_column(); - meta.enable_equality(advice); - advice - }) - }) - .collect(); - let mem = [meta.advice_column(), meta.advice_column()]; - - for copy in © { - meta.lookup_any("dynamic memory lookup table", |meta| { - let mem = mem.map(|c| meta.query_advice(c, Rotation::cur())); - let copy = copy.map(|c| meta.query_advice(c, Rotation::cur())); - vec![(copy[0].clone(), mem[0].clone()), (copy[1].clone(), mem[1].clone())] - }); - } + let memory = BasicDynLookupConfig::new(meta, || FirstPhase, params.num_lu_sets); + let cpu = FlexGateConfig::configure(meta, params.cpu); + log::info!("Poisoned rows: {}", meta.minimum_rows()); - cpu.max_rows = (1 << k) - meta.minimum_rows(); - RAMConfig { cpu, copy, memory: mem } + RAMConfig { cpu, memory } } fn configure(_: &mut ConstraintSystem) -> Self::Config { @@ -125,20 +110,46 @@ impl Circuit for RAMCircuit { mut layouter: impl Layouter, ) -> Result<(), Error> { layouter.assign_region( - || "RAM Circuit", + || "cpu", |mut region| { - // Raw assign the private memory inputs - for (i, &value) in self.memory.iter().enumerate() { - // I think there will always be (0, 0) in the table so we index starting from 1 - let idx = Value::known(F::from(i as u64 + 1)); - raw_assign_advice(&mut region, config.memory[0], i, idx); - raw_assign_advice(&mut region, config.memory[1], i, Value::known(value)); - } self.cpu.assign_raw( &(config.cpu.basic_gates[0].clone(), config.cpu.max_rows), &mut region, ); - self.ram.assign_raw(&config.copy, &mut region); + Ok(()) + }, + )?; + + let copy_manager = (!self.cpu.witness_gen_only()).then_some(&self.cpu.copy_manager); + + // Make purely virtual cells so we can raw assign them + let memory = self.memory.iter().enumerate().map(|(i, value)| { + let idx = Assigned::Trivial(F::from(i as u64)); + let idx = AssignedValue { + value: idx, + cell: Some(ContextCell::new(EXTERNAL_CELL_TYPE_ID, 0, i)), + }; + let value = Assigned::Trivial(*value); + let value = + AssignedValue { value, cell: Some(ContextCell::new(EXTERNAL_CELL_TYPE_ID, 1, i)) }; + [idx, value] + }); + + config.memory.assign_virtual_table_to_raw( + layouter.namespace(|| "memory"), + memory, + copy_manager, + ); + + config.memory.assign_virtual_to_lookup_to_raw( + layouter.namespace(|| "memory accesses"), + self.mem_access.clone(), + copy_manager, + ); + // copy constraints at the very end for safety: + layouter.assign_region( + || "copy constraints", + |mut region| { self.cpu.copy_manager.assign_raw(&config.cpu.constants, &mut region); Ok(()) }, @@ -155,7 +166,6 @@ fn test_ram_mock() { let memory: Vec<_> = (0..mem_len).map(|_| Fr::random(&mut rng)).collect(); let ptrs = [(); CYCLES].map(|_| rng.gen_range(0..memory.len())); let usable_rows = 2usize.pow(k) - 11; // guess - let copy_columns = CYCLES / usable_rows + 1; let params = RAMConfigParams::default(); let mut circuit = RAMCircuit::new(memory, ptrs, params, false); circuit.compute(); @@ -166,10 +176,43 @@ fn test_ram_mock() { num_advice_per_phase: vec![num_advice], num_fixed: 1, }; - circuit.params.copy_columns = copy_columns; + circuit.params.num_lu_sets = CYCLES / usable_rows + 1; MockProver::run(k, &circuit, vec![]).unwrap().assert_satisfied(); } +#[test] +#[should_panic = "called `Result::unwrap()` on an `Err` value: [Lookup dynamic lookup table(index: 2) is not satisfied in Region 2 ('[BasicDynLookupConfig] Advice cells to lookup') at offset 16]"] +fn test_ram_mock_failed_access() { + let k = 5u32; + const CYCLES: usize = 50; + let mut rng = StdRng::seed_from_u64(0); + let mem_len = 16usize; + let memory: Vec<_> = (0..mem_len).map(|_| Fr::random(&mut rng)).collect(); + let ptrs = [(); CYCLES].map(|_| rng.gen_range(0..memory.len())); + let usable_rows = 2usize.pow(k) - 11; // guess + let params = RAMConfigParams::default(); + let mut circuit = RAMCircuit::new(memory, ptrs, params, false); + circuit.compute(); + + // === PRANK === + // Try to claim memory[0] = 0 + let ctx = circuit.cpu.main(); + let ptr = ctx.load_witness(Fr::ZERO); + let value = ctx.load_witness(Fr::ZERO); + circuit.mem_access.push([ptr, value]); + // === end prank === + + // auto-configuration stuff + let num_advice = circuit.cpu.total_advice() / usable_rows + 1; + circuit.params.cpu = FlexGateConfigParams { + k: k as usize, + num_advice_per_phase: vec![num_advice], + num_fixed: 1, + }; + circuit.params.num_lu_sets = CYCLES / usable_rows + 1; + MockProver::run(k, &circuit, vec![]).unwrap().verify().unwrap(); +} + #[test] fn test_ram_prover() { let k = 10u32; @@ -182,7 +225,6 @@ fn test_ram_prover() { let ptrs = [0; CYCLES]; let usable_rows = 2usize.pow(k) - 11; // guess - let copy_columns = CYCLES / usable_rows + 1; let params = RAMConfigParams::default(); let mut circuit = RAMCircuit::new(memory, ptrs, params, false); circuit.compute(); @@ -192,7 +234,7 @@ fn test_ram_prover() { num_advice_per_phase: vec![num_advice], num_fixed: 1, }; - circuit.params.copy_columns = copy_columns; + circuit.params.num_lu_sets = CYCLES / usable_rows + 1; let params = gen_srs(k); let vk = keygen_vk(¶ms, &circuit).unwrap(); diff --git a/halo2-ecc/Cargo.toml b/halo2-ecc/Cargo.toml index f792f4fb..8776eb9b 100644 --- a/halo2-ecc/Cargo.toml +++ b/halo2-ecc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "halo2-ecc" -version = "0.4.0" +version = "0.4.1" authors = ["Intrinsic Technologies"] license = "MIT" edition = "2021" @@ -10,7 +10,7 @@ description = "In-circuit elliptic curve library for halo2." rust-version = "1.73.0" [dependencies] -itertools = "0.10" +itertools = "0.11" num-bigint = { version = "0.4", features = ["rand"] } num-integer = "0.1" num-traits = "0.2" @@ -19,10 +19,10 @@ rand = "0.8" rand_chacha = "0.3.1" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -rayon = "1.6.1" +rayon = "1.8" test-case = "3.1.0" -halo2-base = { version = "=0.4.0", path = "../halo2-base", default-features = false } +halo2-base = { version = "=0.4.1", path = "../halo2-base", default-features = false } # plotting circuit layout plotters = { version = "0.3.0", optional = true } diff --git a/hashes/zkevm/Cargo.toml b/hashes/zkevm/Cargo.toml index 57b19787..e12cd5fd 100644 --- a/hashes/zkevm/Cargo.toml +++ b/hashes/zkevm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zkevm-hashes" -version = "0.2.0" +version = "0.2.1" authors = ["Privacy Scaling Explorations Team", "Taiko Labs", "Intrinsic Technologies"] license = "MIT OR Apache-2.0" edition = "2021" @@ -17,13 +17,15 @@ itertools = "0.11" lazy_static = "1.4" log = "0.4" num-bigint = { version = "0.4" } -halo2-base = { version = "=0.4.0", path = "../../halo2-base", default-features = false, features = ["test-utils"] } -rayon = "1.7" +halo2-base = { version = "=0.4.1", path = "../../halo2-base", default-features = false, features = ["test-utils"] } +serde = { version = "1.0", features = ["derive"] } +rayon = "1.8" sha3 = "0.10.8" -# always included but without features to use Native poseidon -snark-verifier = { version = "=0.1.6", default-features = false } -# snark-verifier = { git = "https://github.com/axiom-crypto/snark-verifier.git", branch = "release-0.1.6-rc0", default-features = false } +# always included but without features to use Native poseidon and get CircuitExt trait +# snark-verifier = { version = "=0.1.7", default-features = false } +snark-verifier-sdk = { git = "https://github.com/axiom-crypto/snark-verifier.git", branch = "release-0.1.7-rc", default-features = false } getset = "0.1.2" +type-map = "0.5.0" [dev-dependencies] ethers-signers = "2.0.8" @@ -34,13 +36,12 @@ rand_core = "0.6.4" rand_xorshift = "0.3" env_logger = "0.10" test-case = "3.1.0" -sha2 = "0.10.7" [features] default = ["halo2-axiom", "display"] -display = ["halo2-base/display", "snark-verifier/display"] -#halo2-pse = ["halo2-base/halo2-pse", "snark-verifier/halo2-pse"] -halo2-axiom = ["halo2-base/halo2-axiom", "snark-verifier/halo2-axiom"] +display = ["snark-verifier-sdk/display"] +halo2-pse = ["halo2-base/halo2-pse"] +halo2-axiom = ["halo2-base/halo2-axiom"] jemallocator = ["halo2-base/jemallocator"] mimalloc = ["halo2-base/mimalloc"] asm = ["halo2-base/asm"] diff --git a/hashes/zkevm/README.md b/hashes/zkevm/README.md deleted file mode 100644 index d0d89e88..00000000 --- a/hashes/zkevm/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# zkEVM Hashes - -## Keccak - -See [readme](./src/keccak/README.md). - -## SHA-256 - -See [readme](./src/sha256/README.md). diff --git a/hashes/zkevm/src/keccak/component/circuit/shard.rs b/hashes/zkevm/src/keccak/component/circuit/shard.rs index 745464ff..604da477 100644 --- a/hashes/zkevm/src/keccak/component/circuit/shard.rs +++ b/hashes/zkevm/src/keccak/component/circuit/shard.rs @@ -7,7 +7,11 @@ use crate::{ get_words_to_witness_multipliers, num_poseidon_absorb_per_keccak_f, num_word_per_witness, }, - output::{dummy_circuit_output, KeccakCircuitOutput}, + get_poseidon_spec, + output::{ + calculate_circuit_outputs_commit, dummy_circuit_output, + multi_inputs_to_circuit_outputs, KeccakCircuitOutput, + }, param::*, }, vanilla::{ @@ -17,7 +21,7 @@ use crate::{ }, util::eth_types::Field, }; -use getset::{CopyGetters, Getters}; +use getset::{CopyGetters, Getters, MutGetters}; use halo2_base::{ gates::{ circuit::{builder::BaseCircuitBuilder, BaseCircuitParams, BaseConfig}, @@ -28,32 +32,38 @@ use halo2_base::{ circuit::{Layouter, SimpleFloorPlanner}, plonk::{Circuit, ConstraintSystem, Error}, }, - poseidon::hasher::{ - spec::OptimizedPoseidonSpec, PoseidonCompactChunkInput, PoseidonCompactOutput, - PoseidonHasher, - }, + poseidon::hasher::{PoseidonCompactChunkInput, PoseidonCompactOutput, PoseidonHasher}, safe_types::{SafeBool, SafeTypeChip}, + virtual_region::copy_constraints::SharedCopyConstraintManager, AssignedValue, Context, QuantumCell::Constant, }; use itertools::Itertools; +use serde::{Deserialize, Serialize}; +use snark_verifier_sdk::CircuitExt; /// Keccak Component Shard Circuit -#[derive(Getters)] +#[derive(Getters, MutGetters)] pub struct KeccakComponentShardCircuit { - inputs: Vec>, + /// The multiple inputs to be hashed. + #[getset(get = "pub")] + inputs: RefCell>>, /// Parameters of this circuit. The same parameters always construct the same circuit. - #[getset(get = "pub")] + #[getset(get_mut = "pub")] params: KeccakComponentShardCircuitParams, - + #[getset(get = "pub")] base_circuit_builder: RefCell>, + /// Poseidon hasher. Stateless once initialized. + #[getset(get = "pub")] hasher: RefCell>, + /// Stateless gate chip + #[getset(get = "pub")] gate_chip: GateChip, } /// Parameters of KeccakComponentCircuit. -#[derive(Default, Clone, CopyGetters)] +#[derive(Default, Clone, CopyGetters, Serialize, Deserialize)] pub struct KeccakComponentShardCircuitParams { /// This circuit has 2^k rows. #[getset(get_copy = "pub")] @@ -84,7 +94,7 @@ impl KeccakComponentShardCircuitParams { ) -> 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::native_circuit::keccak_packed_multi::get_keccak_capacity]. + // Derived from [crate::keccak::vanilla::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 }; @@ -157,7 +167,7 @@ impl Circuit for KeccakComponentShardCircuit { || "keccak circuit", |mut region| { let (keccak_rows, _) = multi_keccak::( - &self.inputs, + &self.inputs.borrow(), Some(self.params.capacity), self.params.keccak_circuit_params, ); @@ -220,11 +230,11 @@ impl KeccakComponentShardCircuit { witness_gen_only: bool, ) -> Self { let input_size = inputs.iter().map(|input| get_num_keccak_f(input.len())).sum::(); - assert!(input_size < params.capacity, "Input size exceeds capacity"); + assert!(input_size <= params.capacity, "Input size exceeds capacity"); let mut base_circuit_builder = BaseCircuitBuilder::new(witness_gen_only); base_circuit_builder.set_params(params.base_circuit_params.clone()); Self { - inputs, + inputs: RefCell::new(inputs), params, base_circuit_builder: RefCell::new(base_circuit_builder), hasher: RefCell::new(create_hasher()), @@ -291,31 +301,11 @@ impl KeccakComponentShardCircuit { ) -> 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(); - assigned_rows - .into_iter() - .step_by(rows_per_round) - // Skip the first round which is dummy. - .skip(1) - .chunks(NUM_ROUNDS + 1) - .into_iter() - .map(|rounds| { - let mut rounds = rounds.collect_vec(); - assert_eq!(rounds.len(), NUM_ROUNDS + 1); - let bytes_left = copy_manager.load_external_assigned(rounds[0].bytes_left.clone()); - let output_row = rounds.pop().unwrap(); - let word_values = core::array::from_fn(|i| { - let assigned_row = &rounds[i]; - copy_manager.load_external_assigned(assigned_row.word_value.clone()) - }); - let is_final = SafeTypeChip::unsafe_to_bool( - copy_manager.load_external_assigned(output_row.is_final), - ); - let hash_lo = copy_manager.load_external_assigned(output_row.hash_lo); - let hash_hi = copy_manager.load_external_assigned(output_row.hash_hi); - LoadedKeccakF { bytes_left, word_values, is_final, hash_lo, hash_hi } - }) - .collect() + transmute_keccak_assigned_to_virtual( + &base_circuit_builder.core().copy_manager, + assigned_rows, + rows_per_round, + ) } /// Generate witnesses of the base circuit. @@ -362,7 +352,7 @@ impl KeccakComponentShardCircuit { lookup_key_per_keccak_f.iter().zip_eq(loaded_keccak_fs) { let is_final = AssignedValue::from(loaded_keccak_f.is_final); - let key = gate.select(ctx, *compact_output.hash(), dummy_key_witness, is_final); + let key = gate.select(ctx, compact_output.hash(), dummy_key_witness, is_final); let hash_lo = gate.select(ctx, loaded_keccak_f.hash_lo, dummy_keccak_lo_witness, is_final); let hash_hi = @@ -413,23 +403,19 @@ impl KeccakComponentShardCircuit { pub(crate) fn create_hasher() -> PoseidonHasher { // Construct in-circuit Poseidon hasher. - let spec = OptimizedPoseidonSpec::::new::< - POSEIDON_R_F, - POSEIDON_R_P, - POSEIDON_SECURE_MDS, - >(); + let spec = get_poseidon_spec(); PoseidonHasher::::new(spec) } -/// Encode raw inputs from Keccak circuit witnesses into lookup keys. +/// Packs raw inputs from Keccak circuit witnesses into fewer field elements for the purpose of creating lookup keys. +/// The packed field elements can be either random linearly combined (RLC'd) or Poseidon-hashed 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 fn encode_inputs_from_keccak_fs( +pub fn pack_inputs_from_keccak_fs( ctx: &mut Context, gate: &impl GateInstructions, - initialized_hasher: &PoseidonHasher, loaded_keccak_fs: &[LoadedKeccakF], -) -> Vec> { +) -> 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::(); @@ -445,6 +431,7 @@ pub fn encode_inputs_from_keccak_fs( let mut compact_chunk_inputs = Vec::with_capacity(loaded_keccak_fs.len()); let mut last_is_final = one_const; + // TODO: this could be parallelized 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; @@ -474,6 +461,96 @@ pub fn encode_inputs_from_keccak_fs( compact_chunk_inputs.push(PoseidonCompactChunkInput::new(compact_inputs, is_final)); last_is_final = is_final.into(); } + compact_chunk_inputs +} +/// 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 fn encode_inputs_from_keccak_fs( + ctx: &mut Context, + gate: &impl GateInstructions, + initialized_hasher: &PoseidonHasher, + loaded_keccak_fs: &[LoadedKeccakF], +) -> Vec> { + let compact_chunk_inputs = pack_inputs_from_keccak_fs(ctx, gate, loaded_keccak_fs); initialized_hasher.hash_compact_chunk_inputs(ctx, gate, &compact_chunk_inputs) } + +/// Converts the pertinent raw assigned cells from a keccak_f permutation into virtual `halo2-lib` cells so they can be used +/// by [halo2_base]. This function doesn't create any new witnesses/constraints. +/// +/// This function is made public for external libraries to use for compatibility. It is the responsibility of the developer +/// to ensure that `rows_per_round` **must** match the configuration of the vanilla zkEVM Keccak circuit itself. +/// +/// ## Assumptions +/// - `rows_per_round` **must** match the configuration of the vanilla zkEVM Keccak circuit itself. +/// - `assigned_rows` **must** start from the 0-th row of the keccak circuit. This is because the first `rows_per_round` rows are dummy rows. +pub fn transmute_keccak_assigned_to_virtual( + copy_manager: &SharedCopyConstraintManager, + assigned_rows: Vec>, + rows_per_round: usize, +) -> Vec> { + let mut copy_manager = copy_manager.lock().unwrap(); + assigned_rows + .into_iter() + .step_by(rows_per_round) + // Skip the first round which is dummy. + .skip(1) + .chunks(NUM_ROUNDS + 1) + .into_iter() + .map(|rounds| { + let mut rounds = rounds.collect_vec(); + assert_eq!(rounds.len(), NUM_ROUNDS + 1); + let bytes_left = copy_manager.load_external_assigned(rounds[0].bytes_left.clone()); + let output_row = rounds.pop().unwrap(); + let word_values = core::array::from_fn(|i| { + let assigned_row = &rounds[i]; + copy_manager.load_external_assigned(assigned_row.word_value.clone()) + }); + let is_final = SafeTypeChip::unsafe_to_bool( + copy_manager.load_external_assigned(output_row.is_final), + ); + let hash_lo = copy_manager.load_external_assigned(output_row.hash_lo); + let hash_hi = copy_manager.load_external_assigned(output_row.hash_hi); + LoadedKeccakF { bytes_left, word_values, is_final, hash_lo, hash_hi } + }) + .collect() +} + +impl CircuitExt for KeccakComponentShardCircuit { + fn instances(&self) -> Vec> { + let circuit_outputs = + multi_inputs_to_circuit_outputs(&self.inputs.borrow(), self.params.capacity); + if self.params.publish_raw_outputs { + vec![ + circuit_outputs.iter().map(|o| o.key).collect(), + circuit_outputs.iter().map(|o| o.hash_lo).collect(), + circuit_outputs.iter().map(|o| o.hash_hi).collect(), + ] + } else { + vec![vec![calculate_circuit_outputs_commit(&circuit_outputs)]] + } + } + + fn num_instance(&self) -> Vec { + if self.params.publish_raw_outputs { + vec![self.params.capacity; OUTPUT_NUM_COL_RAW] + } else { + vec![1; OUTPUT_NUM_COL_COMMIT] + } + } + + fn accumulator_indices() -> Option> { + None + } + + fn selectors(config: &Self::Config) -> Vec { + // the vanilla keccak circuit does not use selectors + // this is from the BaseCircuitBuilder + config.base_circuit_config.gate().basic_gates[0] + .iter() + .map(|basic| basic.q_enable) + .collect() + } +} diff --git a/hashes/zkevm/src/keccak/component/encode.rs b/hashes/zkevm/src/keccak/component/encode.rs index 907dbaf2..1bed38be 100644 --- a/hashes/zkevm/src/keccak/component/encode.rs +++ b/hashes/zkevm/src/keccak/component/encode.rs @@ -8,14 +8,13 @@ use halo2_base::{ }; use itertools::Itertools; use num_bigint::BigUint; -use snark_verifier::loader::native::NativeLoader; use crate::{ keccak::vanilla::{keccak_packed_multi::get_num_keccak_f, param::*}, util::eth_types::Field, }; -use super::param::*; +use super::{create_native_poseidon_sponge, param::*}; // TODO: Abstract this module into a trait for all component circuits. @@ -24,6 +23,22 @@ use super::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 { + let witnesses_per_keccak_f = pack_native_input(bytes); + // Absorb witnesses keccak_f by keccak_f. + let mut native_poseidon_sponge = create_native_poseidon_sponge(); + for witnesses in witnesses_per_keccak_f { + for absorbing in witnesses.chunks(POSEIDON_RATE) { + // To avoid absorbing witnesses crossing keccak_fs together, pad 0s to make sure absorb.len() == RATE. + let mut padded_absorb = [F::ZERO; POSEIDON_RATE]; + padded_absorb[..absorbing.len()].copy_from_slice(absorbing); + native_poseidon_sponge.update(&padded_absorb); + } + } + native_poseidon_sponge.squeeze() +} + +/// Pack native input bytes into num_word_per_witness field elements which are more poseidon friendly. +pub fn pack_native_input(bytes: &[u8]) -> Vec> { assert!(NUM_BITS_PER_WORD <= u128::BITS as usize); let multipliers: Vec = get_words_to_witness_multipliers::(); let num_word_per_witness = num_word_per_witness::(); @@ -68,22 +83,7 @@ pub fn encode_native_input(bytes: &[u8]) -> F { .collect_vec() }) .collect_vec(); - // Absorb witnesses keccak_f by keccak_f. - let mut native_poseidon_sponge = - snark_verifier::util::hash::Poseidon::::new::< - POSEIDON_R_F, - POSEIDON_R_P, - POSEIDON_SECURE_MDS, - >(&NativeLoader); - for witnesses in witnesses_per_keccak_f { - for absorbing in witnesses.chunks(POSEIDON_RATE) { - // To avoid absorbing witnesses crossing keccak_fs together, pad 0s to make sure absorb.len() == RATE. - let mut padded_absorb = [F::ZERO; POSEIDON_RATE]; - padded_absorb[..absorbing.len()].copy_from_slice(absorbing); - native_poseidon_sponge.update(&padded_absorb); - } - } - native_poseidon_sponge.squeeze() + witnesses_per_keccak_f } /// Encode a VarLenBytesVec into its corresponding lookup key. @@ -117,7 +117,7 @@ pub fn encode_var_len_bytes_vec( initialized_hasher.hash_compact_chunk_inputs(ctx, range_chip.gate(), &chunk_inputs); range_chip.gate().select_by_indicator( ctx, - compact_outputs.into_iter().map(|o| *o.hash()), + compact_outputs.into_iter().map(|o| o.hash()), f_indicator, ) } @@ -197,7 +197,7 @@ pub(crate) fn get_bytes_to_words_multipliers() -> Vec { multipliers } -fn format_input( +pub fn format_input( ctx: &mut Context, gate: &impl GateInstructions, bytes: &[SafeByte], diff --git a/hashes/zkevm/src/keccak/component/ingestion.rs b/hashes/zkevm/src/keccak/component/ingestion.rs index cc0b2c3f..c65ebc0c 100644 --- a/hashes/zkevm/src/keccak/component/ingestion.rs +++ b/hashes/zkevm/src/keccak/component/ingestion.rs @@ -42,12 +42,12 @@ impl KeccakIngestionFormat { /// Very similar to [crate::keccak::component::encode::encode_native_input] except we do not do the /// encoding part (that will be done in circuit, not natively). /// -/// Returns `Err(true_capacity)` if `true_capacity > capacity`, where `true_capacity` is the number of keccak_f needed -/// to compute all requests. +/// Returns `(ingestions, true_capacity)`, where `ingestions` is resized to `capacity` length +/// and `true_capacity` is the number of keccak_f needed to compute all requests. pub fn format_requests_for_ingestion( requests: impl IntoIterator)>, capacity: usize, -) -> Result, usize> +) -> (Vec, usize) where B: AsRef<[u8]>, { @@ -77,10 +77,7 @@ where last_mut.hash_lo = u128::from_be_bytes(hash[16..].try_into().unwrap()); } log::info!("Actual number of keccak_f used = {}", ingestions.len()); - if ingestions.len() > capacity { - Err(ingestions.len()) - } else { - ingestions.resize_with(capacity, Default::default); - Ok(ingestions) - } + let true_capacity = ingestions.len(); + ingestions.resize_with(capacity, Default::default); + (ingestions, true_capacity) } diff --git a/hashes/zkevm/src/keccak/component/mod.rs b/hashes/zkevm/src/keccak/component/mod.rs index 13bbd303..d1a6477e 100644 --- a/hashes/zkevm/src/keccak/component/mod.rs +++ b/hashes/zkevm/src/keccak/component/mod.rs @@ -1,3 +1,14 @@ +use std::sync::RwLock; + +use halo2_base::poseidon::hasher::spec::OptimizedPoseidonSpec; +use lazy_static::lazy_static; +use snark_verifier_sdk::{snark_verifier, NativeLoader}; +use type_map::concurrent::TypeMap; + +use crate::util::eth_types::Field; + +use self::param::{POSEIDON_RATE, POSEIDON_R_F, POSEIDON_R_P, POSEIDON_SECURE_MDS, POSEIDON_T}; + /// Module of Keccak component circuit(s). pub mod circuit; /// Module of encoding raw inputs to component circuit lookup keys. @@ -10,3 +21,37 @@ pub mod output; pub mod param; #[cfg(test)] mod tests; + +lazy_static! { + static ref POSEIDON_SPEC_CACHE: RwLock = Default::default(); +} + +pub(crate) fn get_poseidon_spec() -> OptimizedPoseidonSpec { + let spec = POSEIDON_SPEC_CACHE + .read() + .unwrap_or_else(|e| e.into_inner()) + .get::>() + .cloned(); + if let Some(spec) = spec { + return spec; + } + let spec = { + let mut to_write = POSEIDON_SPEC_CACHE.write().unwrap_or_else(|e| e.into_inner()); + let spec = OptimizedPoseidonSpec::::new::< + POSEIDON_R_F, + POSEIDON_R_P, + POSEIDON_SECURE_MDS, + >(); + to_write.insert(spec.clone()); + spec + }; + spec +} + +pub(crate) fn create_native_poseidon_sponge( +) -> snark_verifier::util::hash::Poseidon { + snark_verifier::util::hash::Poseidon::::from_spec( + &NativeLoader, + get_poseidon_spec(), + ) +} diff --git a/hashes/zkevm/src/keccak/component/output.rs b/hashes/zkevm/src/keccak/component/output.rs index fa010bbe..22688b5f 100644 --- a/hashes/zkevm/src/keccak/component/output.rs +++ b/hashes/zkevm/src/keccak/component/output.rs @@ -1,8 +1,12 @@ -use super::{encode::encode_native_input, param::*}; +use std::sync::RwLock; + +use super::{create_native_poseidon_sponge, encode::encode_native_input}; use crate::{keccak::vanilla::keccak_packed_multi::get_num_keccak_f, util::eth_types::Field}; use itertools::Itertools; +use lazy_static::lazy_static; +use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use sha3::{Digest, Keccak256}; -use snark_verifier::loader::native::NativeLoader; +use type_map::concurrent::TypeMap; /// Witnesses to be exposed as circuit outputs. #[derive(Clone, Copy, PartialEq, Debug)] @@ -21,8 +25,8 @@ pub fn multi_inputs_to_circuit_outputs( capacity: usize, ) -> Vec> { assert!(u128::BITS <= F::CAPACITY); - let mut outputs = - inputs.iter().flat_map(|input| input_to_circuit_outputs::(input)).collect_vec(); + let mut outputs: Vec<_> = + inputs.par_iter().flat_map(|input| input_to_circuit_outputs::(input)).collect(); assert!(outputs.len() <= capacity); outputs.resize(capacity, dummy_circuit_output()); outputs @@ -48,8 +52,30 @@ pub fn input_to_circuit_outputs(bytes: &[u8]) -> Vec = Default::default(); +} + /// Return the dummy circuit output for padding. pub fn dummy_circuit_output() -> KeccakCircuitOutput { + let output = DUMMY_CIRCUIT_OUTPUT_CACHE + .read() + .unwrap_or_else(|e| e.into_inner()) + .get::>() + .cloned(); + if let Some(output) = output { + return output; + } + let output = { + let mut to_write = DUMMY_CIRCUIT_OUTPUT_CACHE.write().unwrap_or_else(|e| e.into_inner()); + let output = dummy_circuit_output_impl(); + to_write.insert(output); + output + }; + output +} + +fn dummy_circuit_output_impl() -> KeccakCircuitOutput { assert!(u128::BITS <= F::CAPACITY); let key = encode_native_input(&[]); // Output of Keccak256::digest is big endian. @@ -61,12 +87,7 @@ pub fn dummy_circuit_output() -> KeccakCircuitOutput { /// Calculate the commitment of circuit outputs. pub fn calculate_circuit_outputs_commit(outputs: &[KeccakCircuitOutput]) -> F { - let mut native_poseidon_sponge = - snark_verifier::util::hash::Poseidon::::new::< - POSEIDON_R_F, - POSEIDON_R_P, - POSEIDON_SECURE_MDS, - >(&NativeLoader); + let mut native_poseidon_sponge = create_native_poseidon_sponge(); native_poseidon_sponge.update( &outputs .iter() diff --git a/hashes/zkevm/src/keccak/mod.rs b/hashes/zkevm/src/keccak/mod.rs index 9aa101bb..dd9a660b 100644 --- a/hashes/zkevm/src/keccak/mod.rs +++ b/hashes/zkevm/src/keccak/mod.rs @@ -1,10 +1,3 @@ -//! The zkEVM keccak circuit implementation, with some modifications. -//! Credit goes to https://github.com/privacy-scaling-explorations/zkevm-circuits/tree/main/zkevm-circuits/src/keccak_circuit -//! -//! This is a lookup table based implementation, where bytes are packed into big field elements as efficiently as possible. -//! The circuits can be configured to use different numbers of columns, by specifying the number of rows per internal -//! round of the keccak_f permutation. - /// Module for component circuits. pub mod component; /// Module for Keccak circuits in vanilla halo2. diff --git a/hashes/zkevm/src/keccak/vanilla/mod.rs b/hashes/zkevm/src/keccak/vanilla/mod.rs index 8018142f..c049ed89 100644 --- a/hashes/zkevm/src/keccak/vanilla/mod.rs +++ b/hashes/zkevm/src/keccak/vanilla/mod.rs @@ -15,8 +15,9 @@ use crate::{ }; use halo2_base::utils::halo2::{raw_assign_advice, raw_assign_fixed}; use itertools::Itertools; -use log::{debug, info}; +use log::info; use rayon::prelude::{IntoParallelRefIterator, ParallelIterator}; +use serde::{Deserialize, Serialize}; use std::marker::PhantomData; pub mod cell_manager; @@ -30,7 +31,7 @@ pub mod util; pub mod witness; /// Configuration parameters to define [`KeccakCircuitConfig`] -#[derive(Copy, Clone, Debug, Default)] +#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize)] pub struct KeccakConfigParams { /// The circuit degree, i.e., circuit has 2k rows pub k: u32, @@ -635,7 +636,7 @@ impl KeccakCircuitConfig { }); // Logically here we want !q_input[cur] && !start_new_hash(cur) ==> bytes_left[cur + num_rows_per_round] == bytes_left[cur] // In practice, in order to save a degree we use !(q_input[cur] ^ start_new_hash(cur)) ==> bytes_left[cur + num_rows_per_round] == bytes_left[cur] - // When q_input[cur] is true, the above constraint q_input[cur] ==> bytes_left[cur + num_rows_per_round] + word_len == bytes_left[cur] has + // When q_input[cur] is true, the above constraint q_input[cur] ==> bytes_left[cur + num_rows_per_round] + word_len == bytes_left[cur] has // already been enabled. Even is_final in start_new_hash(cur) is true, it's just over-constrainted. // Note: At the first row of any round except the last round, is_final could be either true or false. cb.condition(not::expr(q(q_input, meta) + start_new_hash(meta, Rotation::cur())), |cb| { diff --git a/hashes/zkevm/src/keccak/vanilla/witness.rs b/hashes/zkevm/src/keccak/vanilla/witness.rs index bba2f05a..cea22f23 100644 --- a/hashes/zkevm/src/keccak/vanilla/witness.rs +++ b/hashes/zkevm/src/keccak/vanilla/witness.rs @@ -411,7 +411,7 @@ fn keccak( .collect::>() }) .collect::>(); - debug!("hash: {:x?}", &(hash_bytes[0..4].concat())); + log::debug!("hash: {:x?}", &(hash_bytes[0..4].concat())); assert_eq!(length, bytes.len()); } } diff --git a/hashes/zkevm/src/lib.rs b/hashes/zkevm/src/lib.rs index c5b4cf4e..e17f02a9 100644 --- a/hashes/zkevm/src/lib.rs +++ b/hashes/zkevm/src/lib.rs @@ -1,6 +1,9 @@ +//! The zkEVM keccak circuit implementation, with some minor modifications +//! Credit goes to + use halo2_base::halo2_proofs; +/// Keccak packed multi pub mod keccak; -pub mod sha256; /// Util pub mod util; diff --git a/hashes/zkevm/src/sha256/README.md b/hashes/zkevm/src/sha256/README.md deleted file mode 100644 index 058cb371..00000000 --- a/hashes/zkevm/src/sha256/README.md +++ /dev/null @@ -1,72 +0,0 @@ -# ZKEVM SHA-256 - -## Vanilla - -SHA-256 circuit in vanilla halo2. This implementation is largely based on [Brechtpd](https://github.com/Brechtpd)'s [PR](https://github.com/privacy-scaling-explorations/zkevm-circuits/pull/756) to the PSE `zkevm-circuits`. His implementation of SHA-256 is in turn based on his implementation of Keccak using the "Bits" approach: one can read more about it [here](https://hackmd.io/NaTuIvmaQCybaOYgd-DG1Q?view#Bit-implementation). - -The major differences is that this version directly represent raw inputs and SHA-256 digests as witnesses, while the original version only has RLCs (random linear combination) of raw inputs and outputs. Because this version doesn't need RLCs, it doesn't have the 2nd phase or use challenge APIs. - -### Logical Input/Output - -Logically the circuit takes a variable length array of variable length bytes as inputs and SHA-256 digests of these bytes as outputs. -While these logical inputs are variable, what is fixed in a given circuit is max number of _total number of SHA-256 input blocks_ that can be processed (see below). We refer to this as the capacity of the circuit. - -`sha256::vanilla::witness::generate_witnesses_multi_sha256` generates the witnesses of the ciruit for a given input. - -### Background Knowledge - -- Given a variable length byte array, one first pads as follows (taken from [Wikipedia](https://en.wikipedia.org/wiki/SHA-2#Pseudocode)): - -``` -begin with the original message of length L bits -append a single '1' bit -append K '0' bits, where K is the minimum number >= 0 such that (L + 1 + K + 64) is a multiple of 512 -append L as a 64-bit big-endian integer, making the total post-processed length a multiple of 512 bits -such that the bits in the message are: 1 , (the number of bits will be a multiple of 512) -``` - -- The SHA-256 algorithm processes padded input data in _blocks_ of 512 bits or 64 bytes. -- The hashing process comprises a series of `NUM_ROUNDS` (64) rounds. -- The algorithm can be organized so that the 64 bytes are divided into `NUM_WORDS_TO_ABSORB` (16) _words_ of 32 bits each, and one new word is ingested in each of the first `NUM_WORDS_TO_ABSORB` rounds. - -### Circuit Overview - -- The circuit operates on one 512 bit input block at a time. -- For each block, `SHA256_NUM_ROWS` (72) are used. This consists of `NUM_START_ROWS` (4) + `NUM_ROUNDS` (64) + `NUM_END_ROWS` (4) rows. - - As described above, the input is "absorbed" in 32 bit words, one in each row of rows `NUM_START_ROWS..NUM_START_ROWS + NUM_WORDS_TO_ABSORB`. These are the rows in which a selector `q_input` is turned on. -- We store inputs and outputs for external use in columns inside the `ShaTable` struct. These are: - - `is_enabled`: a boolean indicating if it is the last row of the block and also this is the last input block of a full input (i.e., this is the block with the finalized digest). - - `length`: the running length in bytes of input data "absorbed" so far, including the current block, excluding padding. This is only constrained when `q_input` is true. One recovers the length of the unpadded input by reading this value on the last "absorb" row in a block with `is_enabled` true. - - `word_value`: 32 bits of the input, as described above. We use the following slightly funny conversion: we consider the 4 byte chunk of the input, replace the padding with 0s, and then convert to a 32-bit integer by considering the 4 bytes _in little endian_. This choice was chosen for consistency with the Keccak circuit, but is arbitrary. - - Only constrained when `q_input` is true. - - `output` (2): the hash digest the SHA-256 algorithm on the input bytes (32 bytes). We represent this as two field elements in hi-lo form - we split 32 bytes into two 16 byte chunks, and convert them to `u128` as _big endian_. - - Only constrained when the last row of a block. Should only be considered meaningful when `is_enabled` is true. -- We convenient store the relevant cells for the above data, per input block, in the struct `AssignedSha256Block`. -- This circuit has a hard constraint that the input array has length up to `2^32 - 1` bits, whereas the official SHA-256 spec supports up to `2^64 - 1`. (In practice it is likely impossible to create a circuit that can handle `2^32 - 1` bit inputs.) -- Details are provided in inline comments. - -### Example - -To illustrate, let's consider `inputs = [[], [0x00, 0x01, ..., 0x37]]`. The corresponding table will look like (input idx is not a real column, provided for viewing convenience): - -| row | input idx | word_value | length | is_enabled | hash_lo | hash_hi | -| --- | --------- | ------------ | ------ | ---------- | ------- | ------- | -| 0 | 0 | - | ... | false | | -| ... | 0 | ... | ... | ... | | -| 4 | 0 | `0` | 0 | false | | -| ... | 0 | `0` | 0 | false | | -| 71 | 0 | - | 0 | true | RESULT | RESULT | -| 72 | 1 | - | ... | ... | | -| ... | 1 | ... | ... | false | | -| 76 | 1 | `0x03020100` | 4 | false | | -| ... | 1 | ... | ... | false | | -| 91 | 1 | `0x0` | 56 | false | | -| 143 | 1 | - | - | false | | | -| 144 | 1 | - | ... | ... | | -| ... | 1 | ... | ... | false | | -| 148 | 1 | `0x0` | 56 | false | | -| ... | 1 | ... | ... | false | | -| 163 | 1 | `0x0` | 56 | false | | -| 215 | 1 | - | - | true | RESULT | RESULT | - -Here the second input has length 56 (in bytes) and requires two blocks due to padding: `56 * 8 + 1 + 64 > 512`. diff --git a/hashes/zkevm/src/sha256/mod.rs b/hashes/zkevm/src/sha256/mod.rs deleted file mode 100644 index 01439206..00000000 --- a/hashes/zkevm/src/sha256/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! Brecht's SHA-256 circuit implementation, which he modified from the Keccak bit implementation. -//! Note this circuit does **not** use lookup tables, only custom gates. -//! The number of columns are fixed (~130). Unlike keccak, it is not configurable. -//! -//! More details here: https://github.com/privacy-scaling-explorations/zkevm-circuits/pull/756 -//! -//! Note: this circuit only supports SHA256 of a bit array of length up to 2^32 - 1, unlike the spec which supports up -//! to 2^64 - 1. - -pub mod vanilla; diff --git a/hashes/zkevm/src/sha256/vanilla/columns.rs b/hashes/zkevm/src/sha256/vanilla/columns.rs deleted file mode 100644 index 844beecd..00000000 --- a/hashes/zkevm/src/sha256/vanilla/columns.rs +++ /dev/null @@ -1,73 +0,0 @@ -//! The columns of the Sha256 circuit -use std::marker::PhantomData; - -use halo2_base::halo2_proofs::plonk::{Advice, Column, ConstraintSystem, Fixed}; - -use crate::util::eth_types::Field; - -use super::param::*; - -/// ShaTable, copied from KeccakTable. However note that `NUM_BYTES_PER_WORD` is different for SHA256 -#[derive(Clone, Debug)] -pub struct ShaTable { - /// Selector always turned on except in blinding rows. - pub(super) q_enable: Column, - /// Single shared column containing different IO data depending on the `offset` within - /// a SHA256 input block ([SHA256_NUM_ROWS] = 72 rows): If offset is in - /// Encoded input: - /// - [NUM_START_ROWS]..[NUM_START_ROWS] + [NUM_WORDS_TO_ABSORB]: Raw SHA256 word([NUM_BYTES_PER_WORD] bytes) of inputs - /// SHA256 hash of input in hi-lo format: - /// - [SHA256_NUM_ROWS] - 2: output.hi() - /// - [SHA256_NUM_ROWS] - 1: output.lo() - pub io: Column, - /// Length in bytes of the input processed so far. Does not include padding. - pub length: Column, - /// Advice to represent if this input block is the last one for a variable length input. - /// The advice value should only be used in the last row of each [SHA256_NUM_ROWS] block. - pub(super) is_final: Column, -} - -impl ShaTable { - /// Construct a new ShaTable - pub fn construct(meta: &mut ConstraintSystem) -> Self { - let q_enable = meta.fixed_column(); - let io = meta.advice_column(); - let length = meta.advice_column(); - let hash_lo = meta.advice_column(); - let hash_hi = meta.advice_column(); - meta.enable_equality(io); - meta.enable_equality(length); - meta.enable_equality(hash_lo); - meta.enable_equality(hash_hi); - let is_final = meta.advice_column(); - Self { q_enable, io, length, is_final } - } -} - -/// Columns for the Sha256 circuit -#[derive(Clone, Debug)] -pub struct Sha256CircuitConfig { - pub(super) q_first: Column, - pub(super) q_extend: Column, - pub(super) q_start: Column, - pub(super) q_compression: Column, - pub(super) q_end: Column, - // Bool. True on rows NUM_START_ROWS..NUM_START_ROWS + NUM_WORDS_TO_ABSORB per input block. - // These are the rounds when input might be absorbed. - // It "might" contain inputs because it's possible that a round only have paddings. - pub(super) q_input: Column, - // Bool. True on row NUM_START_ROWS + NUM_WORDS_TO_ABSORB - 1 for each input block. - // This is the last round when input is absorbed. - pub(super) q_input_last: Column, - pub(super) q_squeeze: Column, - pub(super) word_w: [Column; NUM_BITS_PER_WORD_W], - pub(super) word_a: [Column; NUM_BITS_PER_WORD_EXT], - pub(super) word_e: [Column; NUM_BITS_PER_WORD_EXT], - pub(super) is_paddings: [Column; ABSORB_WIDTH_PER_ROW_BYTES], - pub(super) round_cst: Column, - pub(super) h_a: Column, - pub(super) h_e: Column, - /// The columns for other circuits to lookup hash results - pub hash_table: ShaTable, - pub(super) _marker: PhantomData, -} diff --git a/hashes/zkevm/src/sha256/vanilla/constraints.rs b/hashes/zkevm/src/sha256/vanilla/constraints.rs deleted file mode 100644 index aced6d8b..00000000 --- a/hashes/zkevm/src/sha256/vanilla/constraints.rs +++ /dev/null @@ -1,518 +0,0 @@ -//! The constraints of the Sha256 circuit - -use std::marker::PhantomData; - -use halo2_base::halo2_proofs::{ - plonk::{Advice, Column, ConstraintSystem, Expression, VirtualCells}, - poly::Rotation, -}; -use itertools::Itertools; -use log::info; - -use crate::{ - sha256::vanilla::{ - columns::ShaTable, - util::{decode, rotate, shift, to_be_bytes}, - }, - util::{ - constraint_builder::BaseConstraintBuilder, - eth_types::Field, - expression::{and, from_bytes, not, select, sum, xor, Expr}, - word::{self, WordExpr}, - }, -}; - -use super::columns::Sha256CircuitConfig; -use super::param::*; - -impl Sha256CircuitConfig { - pub fn new(meta: &mut ConstraintSystem) -> Self { - let q_first = meta.fixed_column(); - let q_extend = meta.fixed_column(); - let q_start = meta.fixed_column(); - let q_compression = meta.fixed_column(); - let q_end = meta.fixed_column(); - let q_input = meta.fixed_column(); - let q_input_last = meta.fixed_column(); - let q_squeeze = meta.fixed_column(); - let word_w = array_init::array_init(|_| meta.advice_column()); - let word_a = array_init::array_init(|_| meta.advice_column()); - let word_e = array_init::array_init(|_| meta.advice_column()); - let is_paddings = array_init::array_init(|_| meta.advice_column()); - let round_cst = meta.fixed_column(); - let h_a = meta.fixed_column(); - let h_e = meta.fixed_column(); - let hash_table = ShaTable::construct(meta); - let length = hash_table.length; - let q_enable = hash_table.q_enable; - let is_final = hash_table.is_final; - - // State bits - let mut w_ext = vec![0u64.expr(); NUM_BITS_PER_WORD_W]; - let mut w_2 = vec![0u64.expr(); NUM_BITS_PER_WORD]; - let mut w_7 = vec![0u64.expr(); NUM_BITS_PER_WORD]; - let mut w_15 = vec![0u64.expr(); NUM_BITS_PER_WORD]; - let mut w_16 = vec![0u64.expr(); NUM_BITS_PER_WORD]; - let mut a = vec![0u64.expr(); NUM_BITS_PER_WORD]; - let mut b = vec![0u64.expr(); NUM_BITS_PER_WORD]; - let mut c = vec![0u64.expr(); NUM_BITS_PER_WORD]; - let mut d = vec![0u64.expr(); NUM_BITS_PER_WORD]; - let mut e = vec![0u64.expr(); NUM_BITS_PER_WORD]; - let mut f = vec![0u64.expr(); NUM_BITS_PER_WORD]; - let mut g = vec![0u64.expr(); NUM_BITS_PER_WORD]; - let mut h = vec![0u64.expr(); NUM_BITS_PER_WORD]; - let mut d_68 = vec![0u64.expr(); NUM_BITS_PER_WORD]; - let mut h_68 = vec![0u64.expr(); NUM_BITS_PER_WORD]; - let mut new_a_ext = vec![0u64.expr(); NUM_BITS_PER_WORD_EXT]; - let mut new_e_ext = vec![0u64.expr(); NUM_BITS_PER_WORD_EXT]; - meta.create_gate("Query state bits", |meta| { - for k in 0..NUM_BITS_PER_WORD_W { - w_ext[k] = meta.query_advice(word_w[k], Rotation::cur()); - } - for i in 0..NUM_BITS_PER_WORD { - let k = i + NUM_BITS_PER_WORD_W - NUM_BITS_PER_WORD; - w_2[i] = meta.query_advice(word_w[k], Rotation(-2)); - w_7[i] = meta.query_advice(word_w[k], Rotation(-7)); - w_15[i] = meta.query_advice(word_w[k], Rotation(-15)); - w_16[i] = meta.query_advice(word_w[k], Rotation(-16)); - let k = i + NUM_BITS_PER_WORD_EXT - NUM_BITS_PER_WORD; - a[i] = meta.query_advice(word_a[k], Rotation(-1)); - b[i] = meta.query_advice(word_a[k], Rotation(-2)); - c[i] = meta.query_advice(word_a[k], Rotation(-3)); - d[i] = meta.query_advice(word_a[k], Rotation(-4)); - e[i] = meta.query_advice(word_e[k], Rotation(-1)); - f[i] = meta.query_advice(word_e[k], Rotation(-2)); - g[i] = meta.query_advice(word_e[k], Rotation(-3)); - h[i] = meta.query_advice(word_e[k], Rotation(-4)); - d_68[i] = meta.query_advice(word_a[k], Rotation(-((NUM_ROUNDS + 4) as i32))); - h_68[i] = meta.query_advice(word_e[k], Rotation(-((NUM_ROUNDS + 4) as i32))); - } - for k in 0..NUM_BITS_PER_WORD_EXT { - new_a_ext[k] = meta.query_advice(word_a[k], Rotation(0)); - new_e_ext[k] = meta.query_advice(word_e[k], Rotation(0)); - } - vec![0u64.expr()] - }); - let w = &w_ext[NUM_BITS_PER_WORD_W - NUM_BITS_PER_WORD..NUM_BITS_PER_WORD_W]; - let new_a = &new_a_ext[NUM_BITS_PER_WORD_EXT - NUM_BITS_PER_WORD..NUM_BITS_PER_WORD_EXT]; - let new_e = &new_e_ext[NUM_BITS_PER_WORD_EXT - NUM_BITS_PER_WORD..NUM_BITS_PER_WORD_EXT]; - - let xor = |a: &[Expression], b: &[Expression]| { - debug_assert_eq!(a.len(), b.len(), "invalid length"); - let mut c = vec![0.expr(); a.len()]; - for (idx, (a, b)) in a.iter().zip(b.iter()).enumerate() { - c[idx] = xor::expr(a, b); - } - c - }; - - let select = - |c: &[Expression], when_true: &[Expression], when_false: &[Expression]| { - debug_assert_eq!(c.len(), when_true.len(), "invalid length"); - debug_assert_eq!(c.len(), when_false.len(), "invalid length"); - let mut r = vec![0.expr(); c.len()]; - for (idx, (c, (when_true, when_false))) in - c.iter().zip(when_true.iter().zip(when_false.iter())).enumerate() - { - r[idx] = select::expr(c.clone(), when_true.clone(), when_false.clone()); - } - r - }; - - meta.create_gate("input checks", |meta| { - let mut cb = BaseConstraintBuilder::new(MAX_DEGREE); - for w in w_ext.iter() { - cb.require_boolean("w bit boolean", w.clone()); - } - for a in new_a_ext.iter() { - cb.require_boolean("a bit boolean", a.clone()); - } - for e in new_e_ext.iter() { - cb.require_boolean("e bit boolean", e.clone()); - } - cb.gate(meta.query_fixed(q_enable, Rotation::cur())) - }); - - meta.create_gate("w extend", |meta| { - let mut cb = BaseConstraintBuilder::new(MAX_DEGREE); - let s0 = xor( - &rotate::expr(&w_15, 7), - &xor(&rotate::expr(&w_15, 18), &shift::expr(&w_15, 3)), - ); - let s1 = - xor(&rotate::expr(&w_2, 17), &xor(&rotate::expr(&w_2, 19), &shift::expr(&w_2, 10))); - let new_w = - decode::expr(&w_16) + decode::expr(&s0) + decode::expr(&w_7) + decode::expr(&s1); - cb.require_equal("w", new_w, decode::expr(&w_ext)); - cb.gate(meta.query_fixed(q_extend, Rotation::cur())) - }); - - meta.create_gate("compression", |meta| { - let mut cb = BaseConstraintBuilder::new(MAX_DEGREE); - let s1 = xor(&rotate::expr(&e, 6), &xor(&rotate::expr(&e, 11), &rotate::expr(&e, 25))); - let ch = select(&e, &f, &g); - let temp1 = decode::expr(&h) - + decode::expr(&s1) - + decode::expr(&ch) - + meta.query_fixed(round_cst, Rotation::cur()) - + decode::expr(w); - - let s0 = xor(&rotate::expr(&a, 2), &xor(&rotate::expr(&a, 13), &rotate::expr(&a, 22))); - let maj = select(&xor(&b, &c), &a, &b); - let temp2 = decode::expr(&s0) + decode::expr(&maj); - cb.require_equal("compress a", decode::expr(&new_a_ext), temp1.clone() + temp2); - cb.require_equal("compress e", decode::expr(&new_e_ext), decode::expr(&d) + temp1); - cb.gate(meta.query_fixed(q_compression, Rotation::cur())) - }); - - meta.create_gate("start", |meta| { - let is_final = meta.query_advice(is_final, Rotation::cur()); - let h_a = meta.query_fixed(h_a, Rotation::cur()); - let h_e = meta.query_fixed(h_e, Rotation::cur()); - let mut cb = BaseConstraintBuilder::new(MAX_DEGREE); - cb.require_equal( - "start a", - decode::expr(&new_a_ext), - select::expr(is_final.expr(), h_a, decode::expr(&d)), - ); - cb.require_equal( - "start e", - decode::expr(&new_e_ext), - select::expr(is_final.expr(), h_e, decode::expr(&h)), - ); - cb.gate(meta.query_fixed(q_start, Rotation::cur())) - }); - - meta.create_gate("end", |meta| { - let mut cb = BaseConstraintBuilder::new(MAX_DEGREE); - cb.require_equal( - "end a", - decode::expr(&new_a_ext), - decode::expr(&d) + decode::expr(&d_68), - ); - cb.require_equal( - "end e", - decode::expr(&new_e_ext), - decode::expr(&h) + decode::expr(&h_68), - ); - cb.gate(meta.query_fixed(q_end, Rotation::cur())) - }); - - // Enforce logic for when this block is the last block for a hash - meta.create_gate("is final", |meta| { - let mut cb = BaseConstraintBuilder::new(MAX_DEGREE); - let is_padding = meta.query_advice( - *is_paddings.last().unwrap(), - Rotation(-((NUM_END_ROWS + NUM_ROUNDS - NUM_WORDS_TO_ABSORB) as i32) - 2), - ); - let is_final_prev = meta.query_advice(is_final, Rotation::prev()); - let is_final = meta.query_advice(is_final, Rotation::cur()); - // On the first row is_final needs to be enabled - cb.condition(meta.query_fixed(q_first, Rotation::cur()), |cb| { - cb.require_equal("is_final needs to remain the same", is_final.expr(), 1.expr()); - }); - // Get the correct is_final state from the padding selector - cb.condition(meta.query_fixed(q_squeeze, Rotation::cur()), |cb| { - cb.require_equal( - "is_final needs to match the padding selector", - is_final.expr(), - is_padding, - ); - }); - // Copy the is_final state to the q_start rows - cb.condition( - meta.query_fixed(q_start, Rotation::cur()) - - meta.query_fixed(q_first, Rotation::cur()), - |cb| { - cb.require_equal( - "is_final needs to remain the same", - is_final.expr(), - is_final_prev, - ); - }, - ); - cb.gate(1.expr()) - }); - - let start_new_hash = |meta: &mut VirtualCells| { - // A new hash is started when the previous hash is done or on the first row - meta.query_advice(is_final, Rotation::cur()) - }; - - // Create bytes from input bits - let input_bytes = to_be_bytes::expr(w); - - // Padding - meta.create_gate("padding", |meta| { - let mut cb = BaseConstraintBuilder::new(MAX_DEGREE); - let prev_is_padding = meta.query_advice(*is_paddings.last().unwrap(), Rotation::prev()); - let q_input = meta.query_fixed(q_input, Rotation::cur()); - let q_input_last = meta.query_fixed(q_input_last, Rotation::cur()); - let length = meta.query_advice(length, Rotation::cur()); - let is_final_padding_row = - meta.query_advice(*is_paddings.last().unwrap(), Rotation(-2)); - // All padding selectors need to be boolean - let is_paddings_expr = is_paddings.iter().map(|is_padding| meta.query_advice(*is_padding, Rotation::cur())).collect::>(); - for is_padding in is_paddings_expr.iter() { - cb.condition(meta.query_fixed(q_enable, Rotation::cur()), |cb| { - cb.require_boolean("is_padding boolean", is_padding.clone()); - }); - } - // Now for each padding selector - for idx in 0..is_paddings.len() { - // Previous padding selector can be on the previous row - let is_padding_prev = if idx == 0 { - prev_is_padding.expr() - } else { - is_paddings_expr[idx - 1].clone() - }; - let is_padding = is_paddings_expr[idx].clone(); - let is_first_padding = is_padding.clone() - is_padding_prev.clone(); - // Check padding transition 0 -> 1 done only once - cb.condition(q_input.expr(), |cb| { - cb.require_boolean("padding step boolean", is_first_padding.clone()); - }); - // Padding start/intermediate byte, all padding rows except the last one - cb.condition( - and::expr([(q_input.expr() - q_input_last.expr()), is_padding.expr()]), - |cb| { - // Input bytes need to be zero, or 128 if this is the first padding byte - cb.require_equal( - "padding start/intermediate byte, all padding rows except the last one", - input_bytes[idx].clone(), - is_first_padding.expr() * 128.expr(), - ); - }, - ); - // Padding start/intermediate byte, last padding row but not in the final block - cb.condition( - and::expr([ - q_input_last.expr(), - is_padding.expr(), - not::expr(is_final_padding_row.expr()), - ]), - |cb| { - // Input bytes need to be zero, or 128 if this is the first padding byte - cb.require_equal( - "padding start/intermediate byte, last padding row but not in the final block", - input_bytes[idx].clone(), - is_first_padding.expr() * 128.expr(), - ); - }, - ); - } - // The padding spec: begin with the original message of length L bits - // append a single '1' bit - // append K '0' bits, where K is the minimum number >= 0 such that (L + 1 + K + 64) is a multiple of 512 - // append L as a 64-bit big-endian integer, making the total post-processed length a multiple of 512 bits - // such that the bits in the message are: 1 , (the number of bits will be a multiple of 512) - // - // The last row containing input/padding data in the final block needs to - // contain the length in bits (Only input lengths up to 2**32 - 1 - // bits are supported, which is lower than the spec of 2**64 - 1 bits) - cb.condition(and::expr([q_input_last.expr(), is_final_padding_row.expr()]), |cb| { - cb.require_equal("padding length", decode::expr(w), length.expr() * 8.expr()); - }); - cb.gate(1.expr()) - }); - - // Each round gets access to up to 32 bits of input data. - // We store that as a little-endian word. - meta.create_gate("word_value", |meta| { - let mut cb = BaseConstraintBuilder::new(MAX_DEGREE); - let masked_input_bytes = input_bytes - .iter() - .zip_eq(is_paddings) - .map(|(input_byte, is_padding)| { - input_byte.clone() * not::expr(meta.query_advice(is_padding, Rotation::cur())) - }) - .collect_vec(); - // Convert to u32 as little-endian bytes. Choice of LE is arbitrary, but consistent with Keccak impl. - let input_word = from_bytes::expr(&masked_input_bytes); - // hash_table.io = word_value when q_input is true - cb.require_equal( - "word value", - input_word, - meta.query_advice(hash_table.io, Rotation::cur()), - ); - cb.gate(meta.query_fixed(q_input, Rotation::cur())) - }); - // Update the length on rows where we absorb data - meta.create_gate("length", |meta| { - let mut cb = BaseConstraintBuilder::new(MAX_DEGREE); - let length_prev = meta.query_advice(length, Rotation::prev()); - let length = meta.query_advice(length, Rotation::cur()); - // Length increases by the number of bytes that aren't padding - // In a new block we have to start from 0 if the previous block was the final one - cb.require_equal( - "update length", - length.clone(), - length_prev.clone() - + sum::expr(is_paddings.map(|is_padding| { - not::expr(meta.query_advice(is_padding, Rotation::cur())) - })), - ); - cb.gate(meta.query_fixed(q_input, Rotation::cur())) - }); - - // Make sure data is consistent between blocks - meta.create_gate("cross block data consistency", |meta| { - let mut cb = BaseConstraintBuilder::new(MAX_DEGREE); - let start_new_hash = start_new_hash(meta); - let mut add = |_name: &'static str, column: Column| { - let last_rot = - Rotation(-((NUM_END_ROWS + NUM_ROUNDS - NUM_WORDS_TO_ABSORB) as i32)); - let value_to_copy = meta.query_advice(column, last_rot); - let prev_value = meta.query_advice(column, Rotation::prev()); - let cur_value = meta.query_advice(column, Rotation::cur()); - // On squeeze rows fetch the last used value - cb.condition(meta.query_fixed(q_squeeze, Rotation::cur()), |cb| { - cb.require_equal("copy check", cur_value.expr(), value_to_copy.expr()); - }); - // On first rows keep the length the same, or reset the length when starting a - // new hash - cb.condition( - meta.query_fixed(q_start, Rotation::cur()) - - meta.query_fixed(q_first, Rotation::cur()), - |cb| { - cb.require_equal( - "equality check", - cur_value.expr(), - prev_value.expr() * not::expr(start_new_hash.expr()), - ); - }, - ); - // Set the value to zero on the first row - cb.condition(meta.query_fixed(q_first, Rotation::cur()), |cb| { - cb.require_equal("initialized to 0", cur_value.clone(), 0.expr()); - }); - }; - add("length", length); - add("last padding", *is_paddings.last().unwrap()); - cb.gate(1.expr()) - }); - - // Squeeze - meta.create_gate("squeeze", |meta| { - let mut cb = BaseConstraintBuilder::new(MAX_DEGREE); - // Squeeze out the hash - // Last 4 rows assigned in weird order; this translates to hs[0], hs[1], ..., hs[7] - let hash_parts = [new_a, &a, &b, &c, new_e, &e, &f, &g]; - let hash_bytes_be = hash_parts.iter().flat_map(|part| to_be_bytes::expr(part)); - let hash_bytes_le = hash_bytes_be.rev().collect::>(); - cb.condition(start_new_hash(meta), |cb| { - // hash_table.io is [output_hi, output_lo] at rotations [-1, 0] when q_squeeze is true - cb.require_equal_word( - "hash check", - word::Word32::new(hash_bytes_le.try_into().expect("32 bytes")).to_word(), - word::Word::new( - [Rotation::cur(), Rotation::prev()] - .map(|at| meta.query_advice(hash_table.io, at)), - ), - ); - }); - cb.gate(meta.query_fixed(q_squeeze, Rotation::cur())) - }); - - // Constrain all unused cells to be 0 for safety. - #[allow(clippy::needless_range_loop)] - meta.create_gate("unused is zero", |meta| { - let to_const = - |value: &String| -> &'static str { Box::leak(value.clone().into_boxed_str()) }; - let mut cb = BaseConstraintBuilder::new(MAX_DEGREE); - - let q_start = meta.query_fixed(q_start, Rotation::cur()); - let q_compression = meta.query_fixed(q_compression, Rotation::cur()); - let q_input = meta.query_fixed(q_input, Rotation::cur()); - let q_extend = meta.query_fixed(q_extend, Rotation::cur()); - let q_end = meta.query_fixed(q_end, Rotation::cur()); - // if second to last row: - let q_squeeze_next = meta.query_fixed(q_squeeze, Rotation::next()); - let q_squeeze = meta.query_fixed(q_squeeze, Rotation::cur()); - - let is_final = meta.query_advice(is_final, Rotation::cur()); - let is_paddings = is_paddings.map(|c| meta.query_advice(c, Rotation::cur())); - let io = meta.query_advice(hash_table.io, Rotation::cur()); - let length = meta.query_advice(hash_table.length, Rotation::cur()); - - // column w.b0-w.b1 at offsets [0-19, 68-71] - cb.condition(not::expr(q_extend.clone()), |cb| { - cb.require_zero("if not(q_extend) w.b0 = 0", w_ext[0].clone()); - cb.require_zero("if not(q_extend) w.b1 = 0", w_ext[1].clone()); - }); - // column w.b2-w.b33 at offsets [0-3, 68-71] - cb.condition(q_start.clone() + q_end.clone(), |cb| { - for k in 2..=33 { - cb.require_zero( - to_const(&format!("if q_start and q_end w.b{k} = 0")), - w_ext[k].clone(), - ); - } - }); - // column is_final at offsets [4, 20-70] - cb.condition(q_compression.clone() + q_end.clone() - q_squeeze.clone(), |cb| { - cb.require_zero( - "if q_compression or (q_end and not(q_squeeze)) is_final = 0", - is_final, - ); - }); - // column pad0-pad2 at offsets [0-3, 20-71] - cb.condition(not::expr(q_input.clone()), |cb| { - for k in 0..=2 { - cb.require_zero( - to_const(&format!("if not(q_input) is_paddings[{k}] = 0")), - is_paddings[k].clone(), - ); - } - }); - // column pad3 at offsets [20-70] - cb.condition(q_extend.clone() + q_end.clone() - q_squeeze.clone(), |cb| { - cb.require_zero( - "if q_extend or (q_end and not(q_squeeze)) is_paddings[3] = 0", - is_paddings[3].clone(), - ); - }); - // column io at offsets [0-3, 20-69] - cb.condition( - not::expr(q_input.clone() + q_squeeze_next.clone() + q_squeeze.clone()), - |cb| { - cb.require_zero( - "if not(q_input or q_squeeze_prev or q_squeeze) hash_table.io = 0", - io.clone(), - ); - }, - ); - // column len at offsets [20-70] - cb.condition(q_extend.clone() + q_end.clone() - q_squeeze.clone(), |cb| { - cb.require_zero( - "if q_extend or (q_end and not(q_squeeze)) hash_table.lenght = 0", - length.clone(), - ); - }); - - cb.gate(meta.query_fixed(q_enable, Rotation::cur())) - }); - - info!("degree: {}", meta.degree()); - - Sha256CircuitConfig { - q_first, - q_extend, - q_start, - q_compression, - q_end, - q_input, - q_input_last, - q_squeeze, - hash_table, - word_w, - word_a, - word_e, - is_paddings, - round_cst, - h_a, - h_e, - _marker: PhantomData, - } - } -} diff --git a/hashes/zkevm/src/sha256/vanilla/mod.rs b/hashes/zkevm/src/sha256/vanilla/mod.rs deleted file mode 100644 index 3db81a38..00000000 --- a/hashes/zkevm/src/sha256/vanilla/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! Brecht's SHA-256 circuit implementation, which he modified from the Keccak bit implementation. -//! Note this circuit does **not** use lookup tables, only custom gates. -//! The number of columns are fixed (~130). Unlike keccak, it is not configurable. -//! -//! More details here: https://github.com/privacy-scaling-explorations/zkevm-circuits/pull/756 - -pub mod columns; -pub mod constraints; -pub mod param; -#[cfg(test)] -mod tests; -pub mod util; -pub mod witness; diff --git a/hashes/zkevm/src/sha256/vanilla/param.rs b/hashes/zkevm/src/sha256/vanilla/param.rs deleted file mode 100644 index 23a31687..00000000 --- a/hashes/zkevm/src/sha256/vanilla/param.rs +++ /dev/null @@ -1,36 +0,0 @@ -pub const NUM_BITS_PER_BYTE: usize = 8; -pub const NUM_BYTES_PER_WORD: usize = 4; -pub const NUM_BITS_PER_WORD: usize = NUM_BYTES_PER_WORD * NUM_BITS_PER_BYTE; -pub const NUM_BITS_PER_WORD_W: usize = NUM_BITS_PER_WORD + 2; -pub const NUM_BITS_PER_WORD_EXT: usize = NUM_BITS_PER_WORD + 3; -pub const NUM_ROUNDS: usize = 64; -pub const RATE: usize = 16 * NUM_BYTES_PER_WORD; -pub const RATE_IN_BITS: usize = RATE * NUM_BITS_PER_BYTE; -pub const NUM_WORDS_TO_ABSORB: usize = 16; -pub const NUM_WORDS_TO_SQUEEZE: usize = 8; -pub const NUM_BYTES_TO_SQUEEZE: usize = NUM_WORDS_TO_SQUEEZE * NUM_BYTES_PER_WORD; -pub const ABSORB_WIDTH_PER_ROW_BYTES: usize = 4; -pub const NUM_BITS_PADDING_LENGTH: usize = NUM_BYTES_PADDING_LENGTH * NUM_BITS_PER_BYTE; -pub const NUM_BYTES_PADDING_LENGTH: usize = 8; -pub const NUM_START_ROWS: usize = 4; -pub const NUM_END_ROWS: usize = 4; -/// Total number of rows per 512-bit chunk of SHA-256 circuit. -/// Currently this is a fixed constant. -pub const SHA256_NUM_ROWS: usize = NUM_ROUNDS + NUM_START_ROWS + NUM_END_ROWS; - -pub(super) const MAX_DEGREE: usize = 4; - -pub const ROUND_CST: [u32; NUM_ROUNDS] = [ - 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, - 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, - 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, - 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, - 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, - 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, - 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, - 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, -]; - -pub const H: [u32; 8] = [ - 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19, -]; diff --git a/hashes/zkevm/src/sha256/vanilla/tests.rs b/hashes/zkevm/src/sha256/vanilla/tests.rs deleted file mode 100644 index dd1bc408..00000000 --- a/hashes/zkevm/src/sha256/vanilla/tests.rs +++ /dev/null @@ -1,211 +0,0 @@ -use std::marker::PhantomData; - -use rand::{rngs::StdRng, Rng}; -use rand_core::SeedableRng; -use sha2::{Digest, Sha256}; - -use super::{ - columns::Sha256CircuitConfig, - param::SHA256_NUM_ROWS, - util::{get_num_sha2_blocks, get_sha2_capacity}, - witness::AssignedSha256Block, -}; -use crate::halo2_proofs::{ - circuit::SimpleFloorPlanner, - dev::MockProver, - halo2curves::bn256::Fr, - plonk::Circuit, - plonk::{keygen_pk, keygen_vk}, -}; -use halo2_base::{ - halo2_proofs::{ - circuit::Layouter, - plonk::{Assigned, ConstraintSystem, Error}, - }, - utils::{ - fs::gen_srs, - halo2::Halo2AssignedCell, - testing::{check_proof, gen_proof}, - value_to_option, - }, -}; -use test_case::test_case; - -use crate::util::eth_types::Field; - -/// Sha256BitCircuit -#[derive(Default)] -pub struct Sha256BitCircuit { - inputs: Vec>, - num_rows: Option, - verify_output: bool, - _marker: PhantomData, -} - -impl Circuit for Sha256BitCircuit { - type Config = Sha256CircuitConfig; - type FloorPlanner = SimpleFloorPlanner; - type Params = (); - - fn without_witnesses(&self) -> Self { - unimplemented!() - } - - fn configure(meta: &mut ConstraintSystem) -> Self::Config { - Sha256CircuitConfig::new(meta) - } - - fn synthesize( - &self, - config: Self::Config, - mut layouter: impl Layouter, - ) -> Result<(), Error> { - layouter.assign_region( - || "SHA256 Bit Circuit", - |mut region| { - let start = std::time::Instant::now(); - let blocks = config.multi_sha256( - &mut region, - self.inputs.clone(), - self.num_rows.map(get_sha2_capacity), - ); - println!("Witness generation time: {:?}", start.elapsed()); - - if self.verify_output { - self.verify_output_witness(&blocks); - } - Ok(()) - }, - ) - } -} - -impl Sha256BitCircuit { - /// Creates a new circuit instance - pub fn new(num_rows: Option, inputs: Vec>, verify_output: bool) -> Self { - Sha256BitCircuit { num_rows, inputs, verify_output, _marker: PhantomData } - } - - fn verify_output_witness(&self, assigned_blocks: &[AssignedSha256Block]) { - let mut input_offset = 0; - let mut input = vec![]; - let extract_value = |a: Halo2AssignedCell| { - let value = *value_to_option(a.value()).unwrap(); - #[cfg(feature = "halo2-axiom")] - let value = *value; - #[cfg(not(feature = "halo2-axiom"))] - let value = value.clone(); - match value { - Assigned::Trivial(v) => v, - Assigned::Zero => F::ZERO, - Assigned::Rational(a, b) => a * b.invert().unwrap(), - } - }; - for input_block in assigned_blocks { - let AssignedSha256Block { is_final, output, word_values, length, .. } = - input_block.clone(); - let [is_final, output_lo, output_hi, length] = - [is_final, output.lo(), output.hi(), length].map(extract_value); - let word_values = word_values.iter().cloned().map(extract_value).collect::>(); - for word in word_values { - let word = word.get_lower_32().to_le_bytes(); - input.extend_from_slice(&word); - } - let is_final = is_final == F::ONE; - if is_final { - let empty = vec![]; - let true_input = self.inputs.get(input_offset).unwrap_or(&empty); - let true_length = true_input.len(); - assert_eq!(length.get_lower_64(), true_length as u64, "Length does not match"); - // clear global input and make it local - let mut input = std::mem::take(&mut input); - input.truncate(true_length); - assert_eq!(&input, true_input, "Inputs do not match"); - let output_lo = output_lo.to_repr(); // u128 as 32 byte LE - let output_hi = output_hi.to_repr(); - let mut output = [&output_lo[..16], &output_hi[..16]].concat(); - output.reverse(); // = [output_hi_be, output_lo_be].concat() - - let mut hasher = Sha256::new(); - hasher.update(true_input); - assert_eq!(output, hasher.finalize().to_vec(), "Outputs do not match"); - - input_offset += 1; - } - } - } -} - -fn verify(k: u32, inputs: Vec>, success: bool) { - let circuit = Sha256BitCircuit::new(Some(2usize.pow(k) - 109usize), inputs, success); - - let prover = MockProver::::run(k, &circuit, vec![]).unwrap(); - if success { - prover.assert_satisfied(); - } else { - assert!(prover.verify().is_err()); - } -} - -#[test_case(10; "k: 10")] -fn bit_sha256_simple(k: u32) { - let _ = env_logger::builder().is_test(true).try_init(); - let inputs = vec![ - vec![], - (0u8..1).collect::>(), - (0u8..54).collect::>(), - (0u8..55).collect::>(), // with padding 55 + 1 + 8 = 64 bytes, still fits in 1 block - (0u8..56).collect::>(), // needs 2 blocks, due to padding - (0u8..200).collect::>(), - ]; - verify::(k, inputs, true); -} - -#[test_case(18; "k: 18")] -fn bit_sha256_mock_random(k: u32) { - let _ = env_logger::builder().is_test(true).try_init(); - - let mut rng = StdRng::seed_from_u64(0); - let mut rows = 0; - let mut inputs = vec![]; - let max_rows = 2usize.pow(k) - 109usize; - while rows < max_rows { - let num_bytes = rng.gen_range(0..1000); - let input = (0..num_bytes).map(|_| rng.gen()).collect::>(); - rows += get_num_sha2_blocks(num_bytes) * SHA256_NUM_ROWS; - if rows > max_rows { - break; - } - inputs.push(input); - } - verify::(k, inputs, true); -} - -#[test_case(10; "k: 10")] -#[test_case(20; "k: 20")] -fn bit_sha256_prover(k: u32) { - let _ = env_logger::builder().is_test(true).try_init(); - - let params = gen_srs(k); - - let dummy_circuit = Sha256BitCircuit::new(Some(2usize.pow(k) - 100), vec![], false); - let vk = keygen_vk(¶ms, &dummy_circuit).unwrap(); - let pk = keygen_pk(¶ms, vk, &dummy_circuit).unwrap(); - - let inputs = vec![ - (0u8..200).collect::>(), - vec![], - (0u8..1).collect::>(), - (0u8..54).collect::>(), - (0u8..55).collect::>(), // with padding 55 + 1 + 8 = 64 bytes, still fits in 1 block - (0u8..56).collect::>(), // needs 2 blocks, due to padding - ]; - let circuit = Sha256BitCircuit::new(Some(2usize.pow(k) - 100), inputs, false); - let capacity = get_sha2_capacity(circuit.num_rows.unwrap()); - - let start = std::time::Instant::now(); - let proof = gen_proof(¶ms, &pk, circuit); - println!("Proving time for {} SHA256 blocks: {:?}", capacity, start.elapsed()); - - check_proof(¶ms, pk.get_vk(), &proof, true); -} diff --git a/hashes/zkevm/src/sha256/vanilla/util.rs b/hashes/zkevm/src/sha256/vanilla/util.rs deleted file mode 100644 index a40649ab..00000000 --- a/hashes/zkevm/src/sha256/vanilla/util.rs +++ /dev/null @@ -1,99 +0,0 @@ -use halo2_base::halo2_proofs::plonk::Expression; - -use crate::util::eth_types::Field; - -use super::param::*; - -/// The number of 512-bit blocks of SHA-256 necessary to hash an _unpadded_ byte array of `byte_length`, -/// where the number of blocks does account for padding. -pub const fn get_num_sha2_blocks(byte_length: usize) -> usize { - // ceil( (byte_length + 1 + NUM_BYTES_PADDING_LENGTH) / RATE) - (byte_length + NUM_BYTES_PADDING_LENGTH) / RATE + 1 -} - -/// The number of 512-bit blocks of SHA-256 that can be done in a circuit -/// with `num_rows` usable rows. Usable rows means rows without blinding factors. -pub const fn get_sha2_capacity(num_rows: usize) -> usize { - num_rows / SHA256_NUM_ROWS -} - -/// Decodes be bits -pub mod decode { - use super::{Expression, Field}; - use crate::util::expression::Expr; - - pub(crate) fn expr(bits: &[Expression]) -> Expression { - let mut value = 0.expr(); - let mut multiplier = F::ONE; - for bit in bits.iter().rev() { - value = value + bit.expr() * multiplier; - multiplier *= F::from(2); - } - value - } - - pub(crate) fn value(bits: &[u8]) -> u64 { - let mut value = 0u64; - for (idx, &bit) in bits.iter().rev().enumerate() { - value += (bit as u64) << idx; - } - value - } -} - -/// Rotates bits to the right -pub mod rotate { - use super::{Expression, Field}; - - pub(crate) fn expr(bits: &[Expression], count: usize) -> Vec> { - let mut rotated = bits.to_vec(); - rotated.rotate_right(count); - rotated - } - - pub(crate) fn value(value: u64, count: u32) -> u64 { - ((value as u32).rotate_right(count)) as u64 - } -} - -/// Shifts bits to the right -pub mod shift { - use super::NUM_BITS_PER_WORD; - use super::{Expression, Field}; - use crate::util::expression::Expr; - - pub(crate) fn expr(bits: &[Expression], count: usize) -> Vec> { - let mut res = vec![0.expr(); count]; - res.extend_from_slice(&bits[0..NUM_BITS_PER_WORD - count]); - res - } - - pub(crate) fn value(value: u64, count: u32) -> u64 { - ((value as u32) >> count) as u64 - } -} - -/// Convert big-endian bits to big-endian bytes -pub mod to_be_bytes { - use crate::util::to_bytes; - - use super::{Expression, Field}; - - pub(crate) fn expr(bits: &[Expression]) -> Vec> { - to_bytes::expr(&bits.iter().rev().cloned().collect::>()) - .into_iter() - .rev() - .collect::>() - } -} - -/// Converts bytes into bits -pub(super) fn into_be_bits(bytes: &[u8]) -> Vec { - let mut bits: Vec = vec![0; bytes.len() * 8]; - for (byte_idx, byte) in bytes.iter().enumerate() { - for idx in 0u64..8 { - bits[byte_idx * 8 + (idx as usize)] = (*byte >> (7 - idx)) & 1; - } - } - bits -} diff --git a/hashes/zkevm/src/sha256/vanilla/witness.rs b/hashes/zkevm/src/sha256/vanilla/witness.rs deleted file mode 100644 index db95d9e6..00000000 --- a/hashes/zkevm/src/sha256/vanilla/witness.rs +++ /dev/null @@ -1,448 +0,0 @@ -use std::marker::PhantomData; - -use getset::Getters; -use halo2_base::{ - halo2_proofs::circuit::{Region, Value}, - utils::halo2::{raw_assign_advice, raw_assign_fixed, Halo2AssignedCell}, -}; -use itertools::Itertools; -use log::debug; -use rayon::prelude::*; - -use crate::{ - sha256::vanilla::util::{decode, into_be_bits, rotate, shift}, - util::{eth_types::Field, word::Word}, -}; - -use super::{columns::Sha256CircuitConfig, param::*, util::get_num_sha2_blocks}; - -/// The values of a row _to be assigned_ in the SHA-256 circuit. -#[derive(Clone, Copy, Debug, PartialEq)] -pub struct VirtualShaRow { - w: [bool; NUM_BITS_PER_WORD_W], - a: [bool; NUM_BITS_PER_WORD_EXT], - e: [bool; NUM_BITS_PER_WORD_EXT], - pub(crate) is_paddings: [bool; ABSORB_WIDTH_PER_ROW_BYTES], - pub is_final: bool, - pub length: usize, - /// A SHA-256 word (32 bytes) of the input, in little endian, when `q_input` is true. - /// Ignored when `q_input` is false. Assigned to `hash_table.io` column. - pub word_value: u32, - /// A u128 limb of the hash digest (32 bytes). Will be assigned to `hash_table.io` column, only in the last two rows of a block. - pub hash_limb: u128, -} - -/// The assigned cells of [VirtualShaRow] that belong in [ShaTable]. We only keep the [ShaTable] parts since -/// those may be used externally. -#[derive(Clone, Debug)] -struct AssignedShaTableRow<'v, F: Field> { - /// Should only be used to represent whether this is the final block of an input - /// if this row has q_squeeze = true. - /// Is 0 unless `q_enable` true. - is_final: Halo2AssignedCell<'v, F>, - /// This cell contains different IO data depending on the `offset` of the row within - /// a SHA256 input block ([SHA256_NUM_ROWS] = 72 rows): - /// - When `q_input` is true (offset in [NUM_START_ROWS]..[NUM_START_ROWS] + [NUM_WORDS_TO_ABSORB]): Raw SHA256 word([NUM_BYTES_PER_WORD] bytes) of inputs. u32 input word, little-endian. - /// SHA256 hash of input in hi-lo format: - /// - When offset is [SHA256_NUM_ROWS] - 2: output.hi() - /// - When `q_squeeze` (offset equals [SHA256_NUM_ROWS] - 1): output.lo() - io: Halo2AssignedCell<'v, F>, - /// Length in bytes of the input processed so far. Does not include padding. - /// Is 0 unless `q_input` is true. - length: Halo2AssignedCell<'v, F>, - _marker: PhantomData<&'v F>, -} - -/// The assigned cells from a chunk of `SHA256_NUM_ROWS` rows corresponding to a 512-bit SHA-256 input block. -/// We get the relevant cells from the correct rows, so the user doesn't need to think about circuit internal logic. -#[derive(Clone, Debug, Getters)] -pub struct AssignedSha256Block<'v, F: Field> { - /// This input block is the last one for a variable length input. - #[getset(get = "pub")] - pub(crate) is_final: Halo2AssignedCell<'v, F>, - /// Hash digest (32 bytes) in hi-lo form. Should **not** be used if `is_final` is false. - #[getset(get = "pub")] - pub(crate) output: Word>, - /// Input words (u32) of this block, each u32 consists of the input bytes **in little-endian** - #[getset(get = "pub")] - pub(crate) word_values: [Halo2AssignedCell<'v, F>; NUM_WORDS_TO_ABSORB], - /// Length in bytes of the input processed so far. Does not include padding. - /// This should only be used if `is_final` is true. - #[getset(get = "pub")] - pub(crate) length: Halo2AssignedCell<'v, F>, - _marker: PhantomData<&'v F>, -} - -// Functions for assigning witnesses to Halo2AssignedCells. -// Skip below this block to see the witness generation logic functions themselves. -impl Sha256CircuitConfig { - /// Computes witnesses for computing SHA-256 for each bytearray in `bytes` - /// and assigns the witnesses to Halo2 cells, starting from a blank region. - pub fn multi_sha256<'v>( - &self, - region: &mut Region<'_, F>, - bytes: Vec>, - capacity: Option, - ) -> Vec> { - self.multi_sha256_shifted(region, bytes, capacity, 0) - } - - /// Computes witnesses for computing SHA-256 for each bytearray in `bytes` - /// and assigns the witnesses to Halo2 cells, starting from row offset `start_offset`. - /// - /// **Warning:** Low level call. User needs to supply `start_offset` correctly. - pub fn multi_sha256_shifted<'v>( - &self, - region: &mut Region<'_, F>, - bytes: Vec>, - capacity: Option, - start_offset: usize, - ) -> Vec> { - let virtual_rows = generate_witnesses_multi_sha256(bytes, capacity); - let assigned_rows: Vec<_> = virtual_rows - .into_iter() - .enumerate() - .map(|(offset, row)| self.set_row(region, start_offset + offset, row)) - .collect(); - debug_assert_eq!(assigned_rows.len() % SHA256_NUM_ROWS, 0); - assigned_rows - .chunks_exact(SHA256_NUM_ROWS) - .map(|rows| { - let last_row = rows.last(); - let is_final = last_row.unwrap().is_final.clone(); - let output_lo = last_row.unwrap().io.clone(); - let output_hi = rows[SHA256_NUM_ROWS - 2].io.clone(); - let input_rows = &rows[NUM_START_ROWS..NUM_START_ROWS + NUM_WORDS_TO_ABSORB]; - let word_values: [_; NUM_WORDS_TO_ABSORB] = input_rows - .iter() - .map(|row| row.io.clone()) - .collect::>() - .try_into() - .unwrap(); - let length = input_rows.last().unwrap().length.clone(); - AssignedSha256Block { - is_final, - output: Word::new([output_lo, output_hi]), - word_values, - length, - _marker: PhantomData, - } - }) - .collect() - } - - /// Phase 0 (= FirstPhase) assignment of row to Halo2 assigned cells. - /// Output is `length` at that row - fn set_row<'v>( - &self, - region: &mut Region<'_, F>, - offset: usize, - row: VirtualShaRow, - ) -> AssignedShaTableRow<'v, F> { - let round = offset % SHA256_NUM_ROWS; - let q_squeeze = round == SHA256_NUM_ROWS - 1; - let q_input = (NUM_START_ROWS..NUM_START_ROWS + NUM_WORDS_TO_ABSORB).contains(&round); - - // Fixed values - for (_name, column, value) in &[ - ("q_enable", self.hash_table.q_enable, F::from(true)), - ("q_first", self.q_first, F::from(offset == 0)), - ( - "q_extend", - self.q_extend, - F::from( - (NUM_START_ROWS + NUM_WORDS_TO_ABSORB..NUM_START_ROWS + NUM_ROUNDS) - .contains(&round), - ), - ), - ("q_start", self.q_start, F::from(round < NUM_START_ROWS)), - ( - "q_compression", - self.q_compression, - F::from((NUM_START_ROWS..NUM_ROUNDS + NUM_START_ROWS).contains(&round)), - ), - ("q_end", self.q_end, F::from(round >= NUM_ROUNDS + NUM_START_ROWS)), - ("q_input", self.q_input, F::from(q_input)), - ( - "q_input_last", - self.q_input_last, - F::from(round == NUM_START_ROWS + NUM_WORDS_TO_ABSORB - 1), - ), - ("q_squeeze", self.q_squeeze, F::from(q_squeeze)), - ( - "round_cst", - self.round_cst, - F::from(if (NUM_START_ROWS..NUM_START_ROWS + NUM_ROUNDS).contains(&round) { - ROUND_CST[round - NUM_START_ROWS] as u64 - } else { - 0 - }), - ), - ("Ha", self.h_a, F::from(if round < NUM_START_ROWS { H[3 - round] as u64 } else { 0 })), - ("He", self.h_e, F::from(if round < NUM_START_ROWS { H[7 - round] as u64 } else { 0 })), - ] { - raw_assign_fixed(region, *column, offset, *value); - } - - // Advice values - for (_name, columns, values) in [ - ("w bits", self.word_w.as_slice(), row.w.as_slice()), - ("a bits", self.word_a.as_slice(), row.a.as_slice()), - ("e bits", self.word_e.as_slice(), row.e.as_slice()), - ("padding selectors", self.is_paddings.as_slice(), row.is_paddings.as_slice()), - ] { - for (value, column) in values.iter().zip_eq(columns.iter()) { - raw_assign_advice(region, *column, offset, Value::known(F::from(*value))); - } - } - - let io_value = if q_input { - F::from(row.word_value as u64) - } else if round >= SHA256_NUM_ROWS - 2 { - F::from_u128(row.hash_limb) - } else { - F::ZERO - }; - let [is_final, io, length] = [ - (self.hash_table.is_final, F::from(row.is_final)), - (self.hash_table.io, io_value), - (self.hash_table.length, F::from(row.length as u64)), - ] - .map(|(column, value)| raw_assign_advice(region, column, offset, Value::known(value))); - - AssignedShaTableRow { is_final, io, length, _marker: PhantomData } - } -} - -/// Generates virtual rows of witnesses necessary for computing SHA256(input_bytes) -/// and appends them to `rows`. -/// -/// Not generally recommended to call this function directly. -pub fn generate_witnesses_sha256(rows: &mut Vec, input_bytes: &[u8]) { - let mut bits = into_be_bits(input_bytes); - - // Padding - let length = bits.len(); - let mut length_in_bits = into_be_bits(&(length as u64).to_be_bytes()); - assert_eq!(length_in_bits.len(), NUM_BITS_PADDING_LENGTH); - bits.push(1); - while (bits.len() + NUM_BITS_PADDING_LENGTH) % RATE_IN_BITS != 0 { - bits.push(0); - } - bits.append(&mut length_in_bits); - assert_eq!(bits.len() % RATE_IN_BITS, 0); - - // Set the initial state - let mut hs: [u64; 8] = H.iter().map(|v| *v as u64).collect::>().try_into().unwrap(); - let mut length = 0usize; - let mut in_padding = false; - - let zero_hash = [0; NUM_BYTES_TO_SQUEEZE]; - let mut hash_bytes = zero_hash; - // Process each block - let chunks = bits.chunks(RATE_IN_BITS); - let num_chunks = chunks.len(); - for (idx, chunk) in chunks.enumerate() { - // Adds a row - let mut add_row = |w: u64, - a: u64, - e: u64, - is_final, - length, - is_paddings, - hash_limb: u128, - is_input: bool| { - let word_to_bits = |value: u64, num_bits: usize| { - into_be_bits(&value.to_be_bytes())[64 - num_bits..64] - .iter() - .map(|b| *b != 0) - .collect::>() - }; - let word_value = if is_input { - let mut word_bytes_be = (w as u32).to_be_bytes(); - for (byte, is_padding) in word_bytes_be.iter_mut().zip(is_paddings) { - *byte = if is_padding { 0 } else { *byte }; - } - u32::from_le_bytes(word_bytes_be) - } else { - 0 - }; - rows.push(VirtualShaRow { - w: word_to_bits(w, NUM_BITS_PER_WORD_W).try_into().unwrap(), - a: word_to_bits(a, NUM_BITS_PER_WORD_EXT).try_into().unwrap(), - e: word_to_bits(e, NUM_BITS_PER_WORD_EXT).try_into().unwrap(), - is_final, - length, - is_paddings, - word_value, - hash_limb, - }); - }; - - // Last block for this hash - let is_final_block = idx == num_chunks - 1; - - // Set the state - let (mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut h) = - (hs[0], hs[1], hs[2], hs[3], hs[4], hs[5], hs[6], hs[7]); - - // Add start rows - let mut add_row_start = |a: u64, e: u64, is_final| { - add_row(0, a, e, is_final, length, [false, false, false, in_padding], 0u128, false) - }; - add_row_start(d, h, idx == 0); - add_row_start(c, g, idx == 0); - add_row_start(b, f, idx == 0); - add_row_start(a, e, idx == 0); - - let mut ws = Vec::new(); - for (round, round_cst) in ROUND_CST.iter().enumerate() { - // Padding/Length - let mut is_paddings = [false; ABSORB_WIDTH_PER_ROW_BYTES]; - if round < NUM_WORDS_TO_ABSORB { - // padding/length - for is_padding in is_paddings.iter_mut() { - *is_padding = if length == input_bytes.len() { - true - } else { - length += 1; - false - }; - } - in_padding = *is_paddings.last().unwrap(); - } - // w - let w_ext = if round < NUM_WORDS_TO_ABSORB { - decode::value(&chunk[round * 32..(round + 1) * 32]) - } else { - let get_w = |offset: usize| ws[ws.len() - offset] & 0xFFFFFFFF; - let s0 = rotate::value(get_w(15), 7) - ^ rotate::value(get_w(15), 18) - ^ shift::value(get_w(15), 3); - let s1 = rotate::value(get_w(2), 17) - ^ rotate::value(get_w(2), 19) - ^ shift::value(get_w(2), 10); - get_w(16) + s0 + get_w(7) + s1 - }; - // Masking to ensure word is 32 bits - let w = w_ext & 0xFFFFFFFF; - ws.push(w); - - // compression - let s1 = rotate::value(e, 6) ^ rotate::value(e, 11) ^ rotate::value(e, 25); - let ch = (e & f) ^ (!e & g); - let temp1 = h + s1 + ch + (*round_cst as u64) + w; - let s0 = rotate::value(a, 2) ^ rotate::value(a, 13) ^ rotate::value(a, 22); - let maj = (a & b) ^ (a & c) ^ (b & c); - let temp2 = s0 + maj; - - h = g; - g = f; - f = e; - e = d + temp1; - d = c; - c = b; - b = a; - a = temp1 + temp2; - - // Add the row - add_row( - w_ext, - a, - e, - false, - if round < NUM_WORDS_TO_ABSORB { length } else { 0 }, - is_paddings, - 0u128, - round < NUM_WORDS_TO_ABSORB, - ); - - // Truncate the newly calculated values - a &= 0xFFFFFFFF; - e &= 0xFFFFFFFF; - } - - // Accumulate - hs[0] += a; - hs[1] += b; - hs[2] += c; - hs[3] += d; - hs[4] += e; - hs[5] += f; - hs[6] += g; - hs[7] += h; - - // Squeeze - hash_bytes = if is_final_block { - hs.iter() - .flat_map(|h| (*h as u32).to_be_bytes()) - .collect::>() - .try_into() - .unwrap() - } else { - zero_hash - }; - let hash_lo = u128::from_be_bytes(hash_bytes[16..].try_into().unwrap()); - let hash_hi = u128::from_be_bytes(hash_bytes[..16].try_into().unwrap()); - - // Add end rows - let mut add_row_end = |a: u64, e: u64, hash_limb: u128| { - add_row(0, a, e, false, 0, [false; ABSORB_WIDTH_PER_ROW_BYTES], hash_limb, false) - }; - add_row_end(hs[3], hs[7], 0u128); - add_row_end(hs[2], hs[6], 0u128); - add_row_end(hs[1], hs[5], hash_hi); - add_row( - 0, - hs[0], - hs[4], - is_final_block, - length, - [false, false, false, in_padding], - hash_lo, - false, - ); - - // Now truncate the results - for h in hs.iter_mut() { - *h &= 0xFFFFFFFF; - } - } - - debug!("hash: {:x?}", hash_bytes); -} - -/// Does multi-threaded witness generation by calling [sha256] on each input in `multi_input_bytes` in parallel. -/// Returns `rows` needs to be assigned using `set_row` inside a circuit. -/// The order of `rows` is the same as `multi_input_bytes` (hence it is deterministic). -/// -/// If `capacity` is specified, then extra dummy inputs of empty bytearray ("") are added until -/// the total number of SHA-256 blocks "absorbed" is equal to `capacity`. -pub fn generate_witnesses_multi_sha256( - multi_input_bytes: Vec>, - capacity: Option, -) -> Vec { - // Actual SHA-256, FirstPhase - let rows: Vec<_> = multi_input_bytes - .par_iter() - .map(|input_bytes| { - let num_chunks = get_num_sha2_blocks(input_bytes.len()); - let mut rows = Vec::with_capacity(num_chunks * SHA256_NUM_ROWS); - generate_witnesses_sha256(&mut rows, input_bytes); - rows - }) - .collect(); - let mut rows = rows.concat(); - - if let Some(capacity) = capacity { - // Pad with no data hashes to the expected capacity - while rows.len() < capacity * SHA256_NUM_ROWS { - generate_witnesses_sha256(&mut rows, &[]); - } - // Check that we are not over capacity - if rows.len() > capacity * SHA256_NUM_ROWS { - panic!("SHA-256 Circuit Over Capacity"); - } - } - rows -} diff --git a/hashes/zkevm/src/util/mod.rs b/hashes/zkevm/src/util/mod.rs index 07f5a589..e5f9463e 100644 --- a/hashes/zkevm/src/util/mod.rs +++ b/hashes/zkevm/src/util/mod.rs @@ -1,44 +1,4 @@ pub mod constraint_builder; pub mod eth_types; pub mod expression; - -/// Packs bits into bytes -pub mod to_bytes { - use std::iter::successors; - - use crate::util::eth_types::Field; - use crate::util::expression::Expr; - use halo2_base::halo2_proofs::plonk::Expression; - - pub fn expr(bits: &[Expression]) -> Vec> { - debug_assert!(bits.len() % 8 == 0, "bits not a multiple of 8"); - let two = F::from(2); - let multipliers = - successors(Some(F::ONE), |prev| Some(two * prev)).take(8).collect::>(); - - let mut bytes = Vec::with_capacity(bits.len() / 8); - for byte_bits in bits.chunks_exact(8) { - let mut value = 0.expr(); - for (byte, &multiplier) in byte_bits.iter().zip(multipliers.iter()) { - value = value + byte.expr() * multiplier; - } - bytes.push(value); - } - bytes - } - - pub fn value(bits: &[u8]) -> Vec { - debug_assert!(bits.len() % 8 == 0, "bits not a multiple of 8"); - let mut bytes = Vec::new(); - for byte_bits in bits.chunks(8) { - let mut value = 0u8; - for (idx, bit) in byte_bits.iter().enumerate() { - value += *bit << idx; - } - bytes.push(value); - } - bytes - } -} - pub mod word; diff --git a/hashes/zkevm/src/util/word.rs b/hashes/zkevm/src/util/word.rs index 9d91f5ee..1d417fbb 100644 --- a/hashes/zkevm/src/util/word.rs +++ b/hashes/zkevm/src/util/word.rs @@ -20,7 +20,7 @@ const N_BYTES_HALF_WORD: usize = 16; /// The EVM word for witness #[derive(Clone, Debug, Copy)] pub struct WordLimbs { - /// The limbs of this word. Little-endian. + /// The limbs of this word. pub limbs: [T; N], } @@ -85,7 +85,7 @@ pub trait WordExpr { pub struct Word(Word2); impl Word { - /// Construct the word from 2 limbs [lo, hi] + /// Construct the word from 2 limbs pub fn new(limbs: [T; 2]) -> Self { Self(WordLimbs::::new(limbs)) }