Additional assert

In circuits/src/keccak/utils.rs, the function assigned_field_element_bytes_into_parts is implemented as follows:

/// Splits the field element represented by `bytes` into `NUM_PARTS` parts.
fn assigned_field_element_bytes_into_parts<'a, F, const NUM_PARTS: usize>(
    ctx: &mut Context<F>,
    chip: &RangeChip<F>,
    bytes: impl ExactSizeIterator<Item = &'a AssignedValue<F>>,
) -> [AssignedValue<F>; NUM_PARTS]
where
    F: EccPrimeField,
{
    let num_bytes = num_bytes::<F>();
    assert_eq!(num_bytes, bytes.len(), "Wrong number of bytes");
    let num_bytes_in_part = num_bytes / NUM_PARTS;
    let powers = byte_decomposition_powers()
        .into_iter()
        .take(num_bytes_in_part)
        .map(|power| QuantumCell::from(ctx.load_constant(power)))
        .collect_vec();
    bytes
        .into_iter()
        .chunks(num_bytes_in_part)
        .into_iter()
        .map(|chunk| {
            chip.gate.inner_product(
                ctx,
                chunk.into_iter().cloned().map(QuantumCell::from),
                powers.clone(),
            )
        })
        .collect_vec()
        .try_into()
        .expect("Conversion from vector into array is not allowed to fail")
}

This function is passed a certain number of field elements, and attempts to convert chunks of num_bytes_in_part field elements at a time to field elements, by interpreting them as the byte representation of the value in little endian.

It appears that the intention was that the length of the input iterator would be NUM_PARTS * num_bytes_in_part or phrased differently that NUM_PARTS divides num_bytes. Should this not be the case, then the result will, with the current implementation, correspond to the result with the appropriately zero extended input. The zero extension would be by appending zeros, which does not change the value represented in the case of little-endian format.

However, this behavior depends on the implementation of the halo2's flex gate, whose inner_product function is documented as follows:

/// Constrains and returns the inner product of `<a, b>`.
///
! /// Assumes 'a' and 'b' are the same length.
/// * `ctx`: [Context] to add the constraints to
/// * `a`: Iterator of [QuantumCell] values
/// * `b`: Iterator of [QuantumCell] values to take inner product of `a` by

Thus, we recommend to consider not relying on the current behavior of inner_product. If assigned_field_element_bytes_into_parts is not needed to be used for input that is not full length, then it could be asserted that NUM_PARTS divides num_bytes:

let num_bytes = num_bytes::<F>();
assert_eq!(num_bytes, bytes.len(), "Wrong number of bytes");
let num_bytes_in_part = num_bytes / NUM_PARTS;
+ assert_eq!(num_bytes_in_part * NUM_PARTS, num_bytes, "Number of bytes not divisible by number of parts");
Zellic © 2025Back to top ↑