Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove extra SHL/SHR CTL. #1270

Merged
merged 4 commits into from
Oct 5, 2023

Conversation

LindaGuiga
Copy link
Contributor

@LindaGuiga LindaGuiga commented Oct 2, 2023

This PR changes the way SHL and SHR are generated so that all the CPU CTLs looking into ArithmeticStark have the same form, and we can save one CTL.

Here, shift operations are generated on the arithmetic side rather on the CPU side. This leads to specific constraints for the shift operations, since now, the first input is shift rather than 1 << shift.
The three input registers are used in the Arithmetic STARK: the first is shift, the second is the input, and the third is 1 << shift.
This way, we can have the same CTL shape for all arithmetic operations, and it removes the special shift CTL.

@unzvfu Would mind looking at this if you have the time?

@unzvfu unzvfu self-requested a review October 3, 2023 06:24
Copy link
Contributor

@unzvfu unzvfu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall this is a nice improvement, thanks! There are several things that will need fixing or tidying up though; see my comments.

Comment on lines 43 to 48
let shifted_input1 = if input1.bits() <= 32 {
U256::one() << input1
} else {
U256::zero()
};
input0.overflowing_mul(shifted_input1).0
Copy link
Contributor

@unzvfu unzvfu Oct 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, are you sure this is right? Maybe I'm missing something, but I would have expected this to be

Suggested change
let shifted_input1 = if input1.bits() <= 32 {
U256::one() << input1
} else {
U256::zero()
};
input0.overflowing_mul(shifted_input1).0
if input0 < U256::from(256usize) {
input1 << input0
} else {
U256::zero()
}

as in

fn run_shl(&mut self) {

If your implementation is correct, could you please document (i) why the argument order is reversed (cf. https://www.evm.codes/#1b?fork=shanghai) and (ii) why you limit input1 to 2^32 rather than 256? Thanks!

On the other hand, if my suggestion is correct, then we should probably expand the test suite since it would be a bit concerning that this wasn't caught!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The argument order for the generation on the Arithmetic side was indeed reversed: I fixed this.
I limited input1 to 2^32 bits (mistakenly) basing my code on this piece of code in operation.rs:

if input0.bits() <= 32 {
        let (_, read) = mem_read_gp_with_log_and_fill(LOOKUP_CHANNEL, lookup_addr, state, &mut row);
        state.traces.push_memory(read);
    } else {
        // The shift constraints still expect the address to be set, even though no read will occur.
        let channel = &mut row.mem_channels[LOOKUP_CHANNEL];
        channel.addr_context = F::from_canonical_usize(lookup_addr.context);
        channel.addr_segment = F::from_canonical_usize(lookup_addr.segment);
        channel.addr_virtual = F::from_canonical_usize(lookup_addr.virt);
    }

where we carry out a memory read if the number of bits is less than 32. I changed the limitation to 16 bits in the new version, but I was wondering why there was a difference between the memory reads and the limit in shift?

Comment on lines 61 to 70
let shifted_input1 = if input1.bits() <= 32 {
U256::one() << input1
} else {
U256::zero()
};
if shifted_input1.is_zero() {
U256::zero()
} else {
input0 / shifted_input1
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar concern to BinaryOperator::Shl above. I would have expected this to be

Suggested change
let shifted_input1 = if input1.bits() <= 32 {
U256::one() << input1
} else {
U256::zero()
};
if shifted_input1.is_zero() {
U256::zero()
} else {
input0 / shifted_input1
}
input1 >> input0

as in

fn run_shr(&mut self) {

Note that, in any case, we can just use the U256 arithmetic operations here rather than mimicking the way the values are verified later (e.g. shifted_input1 is not used elsewhere).

evm/src/arithmetic/mod.rs Show resolved Hide resolved
// The second register holds the input which needs shifting.
u256_to_array(&mut lv[INPUT_REGISTER_1], input);
u256_to_array(&mut lv[OUTPUT_REGISTER], result);
// If `shift > 2^32`, the shifted displacement is set to 0.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// If `shift > 2^32`, the shifted displacement is set to 0.
// If `shift >= 256`, the shifted displacement is set to 0.

evm/src/arithmetic/shift.rs Show resolved Hide resolved
evm/src/arithmetic/shift.rs Outdated Show resolved Hide resolved
lv[IS_SHR] = F::ZERO;

for _i in 0..N_RND_TESTS {
let shift = U256::from(rng.gen::<usize>());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If shift is a random usize-sized value, then the probability that it is less than 256 is about 2^-56. :)

Suggested change
let shift = U256::from(rng.gen::<usize>());
let shift = U256::from(rng.gen::<u8>());

Probably better to have a separate test for big shifts where the expected result is zero.

evm/src/arithmetic/shift.rs Show resolved Hide resolved
U256::from(lv[ai].to_canonical_u64()) + full_input * U256::from(1 << 16);
}

let output = full_input * shifted;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that making the suggested change of usize to u8 above triggers an overflow exception on this line. Probably easiest to just do

Suggested change
let output = full_input * shifted;
let output = full_input << shift;

@LindaGuiga
Copy link
Contributor Author

LindaGuiga commented Oct 4, 2023

@unzvfu Thank you for your review!

As suggested in one of the comments, I added separate tests for shift > 256, and I realized a mistake had been introduced (I think in PR #1166, which introduced IS_SHR and IS_SHL) for that case: the modulus was not set to 0 but to 1 in that case (and some constraints involved only IS_DIV when they should have included both IS_DIV and IS_SHR).
I fixed this issue in the latest commit, where I also introduced the tests for shift > 256. If you think this should go into another PR, I can always revert the latest commit!

@LindaGuiga
Copy link
Contributor Author

LindaGuiga commented Oct 4, 2023

Just a side note on my latest commit and comment: the shift issue on main was causing a couple of tests to fail in evm-tests (shr_-1_256 / shr_2^255_256 / shr_2^255_257/expPower256Of256 for instance) and the tests pass with the fix.

Copy link
Contributor

@unzvfu unzvfu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great, thanks Linda! Just a few little nits about function visibility that will be easy to incorporate (not a big deal, but I think its a good habit to limit the visibility of helper functions).

And nice work catching the bug with big shifts!


/// Verify that num = quo * den + rem and 0 <= rem < den.
fn eval_packed_divmod_helper<P: PackedField>(
pub fn eval_packed_divmod_helper<P: PackedField>(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit:

Suggested change
pub fn eval_packed_divmod_helper<P: PackedField>(
pub(crate) fn eval_packed_divmod_helper<P: PackedField>(

let input1 = read_value_i64_limbs(lv, INPUT_REGISTER_1);

/// Given the two limbs of `left_in` and `right_in`, computes `left_in * right_in`.
pub fn generate_mul<F: PrimeField64>(lv: &mut [F], left_in: [i64; 16], right_in: [i64; 16]) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit:

Suggested change
pub fn generate_mul<F: PrimeField64>(lv: &mut [F], left_in: [i64; 16], right_in: [i64; 16]) {
pub(crate) fn generate_mul<F: PrimeField64>(lv: &mut [F], left_in: [i64; 16], right_in: [i64; 16]) {

eval_packed_generic_mul(lv, is_mul, input0_limbs, input1_limbs, yield_constr);
}

pub fn eval_ext_mul_circuit<F: RichField + Extendable<D>, const D: usize>(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit:

Suggested change
pub fn eval_ext_mul_circuit<F: RichField + Extendable<D>, const D: usize>(
pub(crate) fn eval_ext_mul_circuit<F: RichField + Extendable<D>, const D: usize>(

generate_mul(lv, input0, input1);
}

pub fn eval_packed_generic_mul<P: PackedField>(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit:

Suggested change
pub fn eval_packed_generic_mul<P: PackedField>(
pub(crate) fn eval_packed_generic_mul<P: PackedField>(

/// The logic is the same as the one for MUL. The only difference is that
/// the inputs are in `INPUT_REGISTER_1` and `INPUT_REGISTER_2` instead of
/// `INPUT_REGISTER_0` and `INPUT_REGISTER_1`.
pub fn eval_packed_shl<P: PackedField>(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit:

Suggested change
pub fn eval_packed_shl<P: PackedField>(
fn eval_packed_shl<P: PackedField>(

eval_packed_shr(lv, nv, yield_constr);
}

pub fn eval_ext_circuit_shl<F: RichField + Extendable<D>, const D: usize>(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit:

Suggested change
pub fn eval_ext_circuit_shl<F: RichField + Extendable<D>, const D: usize>(
fn eval_ext_circuit_shl<F: RichField + Extendable<D>, const D: usize>(

);
}

pub fn eval_ext_circuit_shr<F: RichField + Extendable<D>, const D: usize>(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit:

Suggested change
pub fn eval_ext_circuit_shr<F: RichField + Extendable<D>, const D: usize>(
fn eval_ext_circuit_shr<F: RichField + Extendable<D>, const D: usize>(

@LindaGuiga LindaGuiga merged commit 0de6f94 into 0xPolygonZero:main Oct 5, 2023
2 checks passed
@Nashtare Nashtare deleted the remove-shift-ctl branch January 9, 2024 13:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants