diff --git a/kimchi/src/circuits/polynomials/keccak/mod.rs b/kimchi/src/circuits/polynomials/keccak/mod.rs index f2b51ef27b..3cd8bcdf30 100644 --- a/kimchi/src/circuits/polynomials/keccak/mod.rs +++ b/kimchi/src/circuits/polynomials/keccak/mod.rs @@ -6,9 +6,9 @@ pub mod witness; pub const DIM: usize = 5; pub const QUARTERS: usize = 4; pub const ROUNDS: usize = 24; -pub const RATE: usize = 136; -pub const KECCAK_COLS: usize = 2344; +pub const RATE: usize = 1088 / 8; pub const CAPACITY: usize = 512 / 8; +pub const KECCAK_COLS: usize = 2344; use crate::circuits::expr::constraints::ExprOps; use ark_ff::PrimeField; @@ -75,6 +75,11 @@ pub(crate) const RC: [u64; 24] = [ 0x8000000080008008, ]; +// Composes a vector of 4 dense quarters into the dense full u64 word +pub(crate) fn compose(quarters: &[u64]) -> u64 { + quarters[0] + (1 << 16) * quarters[1] + (1 << 32) * quarters[2] + (1 << 48) * quarters[3] +} + // Takes a dense u64 word and decomposes it into a vector of 4 dense quarters pub(crate) fn decompose(word: u64) -> Vec { vec![ @@ -98,6 +103,82 @@ pub(crate) fn expand_word>(word: u64) -> Vec { .collect::>() } +/// Pads the message with the 10*1 rule until reaching a length that is a multiple of the rate +pub(crate) fn pad(message: &[u8]) -> Vec { + let mut padded = message.to_vec(); + padded.push(0x01); + while padded.len() % 136 != 0 { + padded.push(0x00); + } + let last = padded.len() - 1; + padded[last] += 0x80; + padded +} + +/// From each quarter in sparse representation, it computes its 4 resets. +/// The resulting vector contains 4 times as many elements as the input. +/// The output is placed in the vector as [reset0, reset1, reset2, reset3] +pub(crate) fn shift(state: &[u64]) -> Vec { + let mut shifts = vec![vec![]; 4]; + let aux = expand(0xFFFF); + for term in state { + shifts[0].push(aux & term); // shift0 = reset0 + shifts[1].push(((aux << 1) & term) / 2); // shift1 = reset1/2 + shifts[2].push(((aux << 2) & term) / 4); // shift2 = reset2/4 + shifts[3].push(((aux << 3) & term) / 8); // shift3 = reset3/8 + } + shifts.iter().flatten().copied().collect() +} + +/// From a vector of shifts, resets the underlying value returning only shift0 +pub(crate) fn reset(shifts: &[u64]) -> Vec { + shifts + .iter() + .copied() + .take(shifts.len() / 4) + .collect::>() +} + +/// From a reset0 state, obtain the corresponding 16-bit dense terms +pub(crate) fn collapse(state: &[u64]) -> Vec { + let mut dense = vec![]; + for reset in state { + dense.push(u64::from_str_radix(&format!("{:x}", reset), 2).unwrap()); + } + dense +} + +/// Outputs the state into dense quarters of 16-bits each in little endian order +pub(crate) fn quarters(state: &[u8]) -> Vec { + let mut quarters = vec![]; + for pair in state.chunks(2) { + quarters.push(u16::from_le_bytes([pair[0], pair[1]]) as u64); + } + quarters +} + +/// On input a vector of 16-bit dense quarters, outputs a vector of 8-bit bytes in the right order for Keccak +pub(crate) fn bytestring(dense: &[u64]) -> Vec { + dense + .iter() + .map(|x| vec![x % 256, x / 256]) + .collect::>>() + .iter() + .flatten() + .copied() + .collect() +} + +/// On input a 200-byte vector, generates a vector of 100 expanded quarters representing the 1600-bit state +pub(crate) fn expand_state(state: &[u8]) -> Vec { + let mut expanded = vec![]; + for pair in state.chunks(2) { + let quarter = u16::from_le_bytes([pair[0], pair[1]]); + expanded.push(expand(quarter as u64)); + } + expanded +} + /// On input a length, returns the smallest multiple of RATE that is greater than the bytelength pub(crate) fn padded_length(bytelength: usize) -> usize { (bytelength / RATE + 1) * RATE diff --git a/kimchi/src/circuits/polynomials/keccak/witness.rs b/kimchi/src/circuits/polynomials/keccak/witness.rs index bd0df8463c..a22713039f 100644 --- a/kimchi/src/circuits/polynomials/keccak/witness.rs +++ b/kimchi/src/circuits/polynomials/keccak/witness.rs @@ -1,14 +1,28 @@ //! Keccak witness computation -use crate::circuits::witness::{IndexCell, WitnessCell}; +use std::array; + +use crate::circuits::polynomials::keccak::{compose, decompose, expand_state, quarters, RC}; +use crate::{ + auto_clone, + circuits::{ + polynomials::keccak::ROUNDS, + witness::{self, IndexCell, Variables, WitnessCell}, + }, + grid, variable_map, +}; use ark_ff::PrimeField; +use num_bigint::BigUint; -use super::KECCAK_COLS; +use super::{ + bytestring, collapse, expand, pad, reset, shift, CAPACITY, DIM, KECCAK_COLS, OFF, QUARTERS, + RATE, +}; -type _Layout = Vec, COLUMNS>>>; +type Layout = Vec, COLUMNS>>>; -fn _layout_round() -> _Layout { - vec![ +fn layout_round() -> [Layout; 1] { + [vec![ IndexCell::create("state_a", 0, 100), IndexCell::create("state_c", 100, 120), IndexCell::create("shifts_c", 120, 200), @@ -31,15 +45,397 @@ fn _layout_round() -> _Layout { IndexCell::create("shifts_b", 1540, 1940), IndexCell::create("shifts_sum", 1940, 2340), IndexCell::create("f00", 2340, 2344), - ] + ]] } -fn _layout_sponge() -> _Layout { - vec![ +fn layout_sponge() -> [Layout; 1] { + [vec![ IndexCell::create("old_state", 0, 100), IndexCell::create("new_state", 100, 200), - IndexCell::create("dense", 200, 300), - IndexCell::create("bytes", 300, 500), - IndexCell::create("shifts", 500, 900), - ] + IndexCell::create("bytes", 200, 400), + IndexCell::create("shifts", 400, 800), + ]] +} + +// Transforms a vector of u64 into a vector of field elements +fn field(input: &[u64]) -> Vec { + input.iter().map(|x| F::from(*x)).collect::>() +} + +// Contains the quotient, remainder, bound, dense rotated as quarters of at most 16 bits each +// Contains the expansion of the rotated word +struct Rotation { + quotient: Vec, + remainder: Vec, + bound: Vec, + dense_rot: Vec, + expand_rot: Vec, +} + +impl Rotation { + // Returns rotation of 0 bits + fn none(dense: &[u64]) -> Self { + Self { + quotient: vec![0; QUARTERS], + remainder: dense.to_vec(), + bound: vec![0xFFFF; QUARTERS], + dense_rot: dense.to_vec(), + expand_rot: dense.iter().map(|x| expand(*x)).collect(), + } + } + + // On input the dense quarters of a word, rotate the word offset bits to the left + fn new(dense: &[u64], offset: u32) -> Self { + if offset == 0 { + return Self::none(dense); + } + let word = compose(dense); + let rem = (word as u128 * 2u128.pow(offset) % 2u128.pow(64)) as u64; + let quo = word / 2u64.pow(64 - offset); + let bnd = (quo as u128) + 2u128.pow(64) - 2u128.pow(offset); + let rot = rem + quo; + assert!(rot == word.rotate_left(offset)); + + Self { + quotient: decompose(quo), + remainder: decompose(rem), + bound: decompose(bnd as u64), + dense_rot: decompose(rot), + expand_rot: decompose(rot).iter().map(|x| expand(*x)).collect(), + } + } + + // On input the dense quarters of many words, rotate the word offset bits to the left + fn many(words: &[u64], offsets: &[u32]) -> Self { + assert!(words.len() == QUARTERS * offsets.len()); + let mut quotient = vec![]; + let mut remainder = vec![]; + let mut bound = vec![]; + let mut dense_rot = vec![]; + let mut expand_rot = vec![]; + for (word, offset) in words.chunks(QUARTERS).zip(offsets.iter()) { + let mut rot = Self::new(word, *offset); + quotient.append(&mut rot.quotient); + remainder.append(&mut rot.remainder); + bound.append(&mut rot.bound); + dense_rot.append(&mut rot.dense_rot); + expand_rot.append(&mut rot.expand_rot); + } + Self { + quotient, + remainder, + bound, + dense_rot, + expand_rot, + } + } +} + +struct Theta { + state_c: Vec, + shifts_c: Vec, + dense_c: Vec, + quotient_c: Vec, + remainder_c: Vec, + bound_c: Vec, + dense_rot_c: Vec, + expand_rot_c: Vec, + state_d: Vec, + state_e: Vec, +} + +impl Theta { + fn create(state_a: &[u64]) -> Self { + let state_c = Self::compute_state_c(state_a); + let shifts_c = shift(&state_c); + let dense_c = collapse(&reset(&shifts_c)); + let rotation_c = Rotation::many(&dense_c, &[1; DIM]); + let state_d = Self::compute_state_d(&shifts_c, &rotation_c.expand_rot); + let state_e = Self::compute_state_e(state_a, &state_d); + Self { + state_c, + shifts_c, + dense_c, + quotient_c: rotation_c.quotient, + remainder_c: rotation_c.remainder, + bound_c: rotation_c.bound, + dense_rot_c: rotation_c.dense_rot, + expand_rot_c: rotation_c.expand_rot, + state_d, + state_e, + } + } + + fn compute_state_c(state_a: &[u64]) -> Vec { + let state_a = grid!(100, state_a); + let mut state_c = vec![]; + for x in 0..DIM { + for q in 0..QUARTERS { + state_c.push( + state_a(0, x, q) + + state_a(1, x, q) + + state_a(2, x, q) + + state_a(3, x, q) + + state_a(4, x, q), + ); + } + } + state_c + } + + fn compute_state_d(shifts_c: &[u64], expand_rot_c: &[u64]) -> Vec { + let shifts_c = grid!(20, shifts_c); + let expand_rot_c = grid!(20, expand_rot_c); + let mut state_d = vec![]; + for x in 0..DIM { + for q in 0..QUARTERS { + state_d.push(shifts_c((x + DIM - 1) % DIM, q) + expand_rot_c((x + 1) % DIM, q)); + } + } + state_d + } + + fn compute_state_e(state_a: &[u64], state_d: &[u64]) -> Vec { + let state_a = grid!(100, state_a); + let state_d = grid!(20, state_d); + let mut state_e = vec![]; + for y in 0..DIM { + for x in 0..DIM { + for q in 0..QUARTERS { + state_e.push(state_a(y, x, q) + state_d(x, q)); + } + } + } + state_e + } +} + +struct PiRho { + shifts_e: Vec, + dense_e: Vec, + quotient_e: Vec, + remainder_e: Vec, + bound_e: Vec, + dense_rot_e: Vec, + expand_rot_e: Vec, + state_b: Vec, +} + +impl PiRho { + fn create(state_e: &[u64]) -> Self { + let shifts_e = shift(state_e); + let dense_e = collapse(&reset(&shifts_e)); + let rotation_e = Rotation::many( + &dense_e, + &OFF.iter() + .flatten() + .map(|x| *x as u32) + .collect::>(), + ); + + let mut state_b = vec![vec![vec![0; QUARTERS]; DIM]; DIM]; + let aux = grid!(100, rotation_e.expand_rot); + for y in 0..DIM { + for x in 0..DIM { + for q in 0..QUARTERS { + state_b[(2 * x + 3 * y) % DIM][y][q] = aux(y, x, q); + } + } + } + let state_b = state_b.iter().flatten().flatten().copied().collect(); + + Self { + shifts_e, + dense_e, + quotient_e: rotation_e.quotient, + remainder_e: rotation_e.remainder, + bound_e: rotation_e.bound, + dense_rot_e: rotation_e.dense_rot, + expand_rot_e: rotation_e.expand_rot, + state_b, + } + } +} + +struct Chi { + shifts_b: Vec, + shifts_sum: Vec, + state_f: Vec, +} + +impl Chi { + fn create(state_b: &[u64]) -> Self { + let shifts_b = shift(state_b); + let shiftsb = grid!(400, shifts_b); + let mut sum = vec![]; + for y in 0..DIM { + for x in 0..DIM { + for q in 0..QUARTERS { + let not = 0x1111111111111111u64 - shiftsb(0, y, (x + 1) % DIM, q); + sum.push(not + shiftsb(0, y, (x + 2) % DIM, q)); + } + } + } + let shifts_sum = shift(&sum); + let shiftsum = grid!(400, shifts_sum); + let mut state_f = vec![]; + for y in 0..DIM { + for x in 0..DIM { + for q in 0..QUARTERS { + let and = shiftsum(1, y, x, q); + state_f.push(shiftsb(0, y, x, q) + and); + } + } + } + + Self { + shifts_b, + shifts_sum, + state_f, + } + } +} + +struct Iota { + state_g: Vec, +} + +impl Iota { + fn create(state_f: Vec, round: usize) -> Self { + let rc = decompose(RC[round]) + .iter() + .map(|x| expand(*x)) + .collect::>(); + let mut state_g = state_f.clone(); + for (i, c) in rc.iter().enumerate() { + state_g[i] = state_f[i] + *c; + } + Self { state_g } + } +} + +/// Creates a witness for the Keccak hash function +/// Input: +/// - message: the message to be hashed +/// Note: +/// Requires at least one more row after the keccak gadget so that +/// constraints can access the next row in the squeeze +pub fn extend_keccak_witness(witness: &mut [Vec; KECCAK_COLS], message: BigUint) { + let padded = pad(&message.to_bytes_be()); + let chunks = padded.chunks(RATE); + + // The number of rows that need to be added to the witness correspond to + // - Absorb phase: + // - 1 per block for the sponge row + // - 24 for the rounds + // - Squeeze phase: + // - 1 for the final sponge row + let rows: usize = chunks.len() * (ROUNDS + 1) + 1; + + let mut keccak_witness = array::from_fn(|_| vec![F::zero(); rows]); + + // Absorb phase + let mut row = 0; + let mut state = vec![0; QUARTERS * DIM * DIM]; + for chunk in chunks { + let mut block = chunk.to_vec(); + // Pad the block until reaching 200 bytes + block.append(&mut vec![0; CAPACITY]); + let dense = quarters(&block); + let new_state = expand_state(&block); + auto_clone!(new_state); + let shifts = shift(&new_state()); + let bytes = block.iter().map(|b| *b as u64).collect::>(); + + // Initialize the absorb sponge row + witness::init( + &mut keccak_witness, + row, + &layout_sponge(), + &variable_map!["old_state" => field(&state), "new_state" => field(&new_state()), "dense" => field(&dense), "bytes" => field(&bytes), "shifts" => field(&shifts)], + ); + row += 1; + + let xor_state = state + .iter() + .zip(new_state()) + .map(|(x, y)| x + y) + .collect::>(); + + let mut ini_state = xor_state.clone(); + + for round in 0..ROUNDS { + // Theta + let theta = Theta::create(&ini_state); + + // PiRho + let pirho = PiRho::create(&theta.state_e); + + // Chi + let chi = Chi::create(&pirho.state_b); + let f00 = chi + .state_f + .clone() + .into_iter() + .take(QUARTERS) + .collect::>(); + + // Iota + let iota = Iota::create(chi.state_f, round); + + // Initialize the round row + witness::init( + &mut keccak_witness, + row, + &layout_round(), + &variable_map![ + "state_a" => field(&ini_state), + "state_c" => field(&theta.state_c), + "shifts_c" => field(&theta.shifts_c), + "dense_c" => field(&theta.dense_c), + "quotient_c" => field(&theta.quotient_c), + "remainder_c" => field(&theta.remainder_c), + "bound_c" => field(&theta.bound_c), + "dense_rot_c" => field(&theta.dense_rot_c), + "expand_rot_c" => field(&theta.expand_rot_c), + "state_d" => field(&theta.state_d), + "state_e" => field(&theta.state_e), + "shifts_e" => field(&pirho.shifts_e), + "dense_e" => field(&pirho.dense_e), + "quotient_e" => field(&pirho.quotient_e), + "remainder_e" => field(&pirho.remainder_e), + "bound_e" => field(&pirho.bound_e), + "dense_rot_e" => field(&pirho.dense_rot_e), + "expand_rot_e" => field(&pirho.expand_rot_e), + "state_b" => field(&pirho.state_b), + "shifts_b" => field(&chi.shifts_b), + "shifts_sum" => field(&chi.shifts_sum), + "f00" => field(&f00) + ], + ); + row += 1; + ini_state = iota.state_g; + } + // update state after rounds + state = ini_state; + } + + // Squeeze phase + + let new_state = vec![0; QUARTERS * DIM * DIM]; + let shifts = shift(&state); + let dense = collapse(&reset(&shifts)); + let bytes = bytestring(&dense); + + // Initialize the squeeze sponge row + witness::init( + &mut keccak_witness, + row, + &layout_sponge(), + &variable_map!["old_state" => field(&state), "new_state" => field(&new_state), "bytes" => field(&bytes), "shifts" => field(&shifts)], + ); + + for col in 0..KECCAK_COLS { + witness[col].extend(keccak_witness[col].iter()); + } }