feat(AVM): Fail on incomplete toRadix decompositions#17233
feat(AVM): Fail on incomplete toRadix decompositions#17233sirasistant merged 14 commits intomerge-train/avmfrom
Conversation
| auto value = fr::random_element(); | ||
| uint32_t num_limbs = static_cast<uint32_t>(state.range(0)); | ||
| uint32_t radix = static_cast<uint32_t>(state.range(1)); | ||
| uint256_t value = 0; |
There was a problem hiding this comment.
could you add a comment about what you are doing here?
| bool invalid_bitwise_radix = is_output_bits && (radix != 2); | ||
| // Error handling - if num_limbs is zero, value needs to be zero | ||
| bool invalid_num_limbs = (num_limbs == 0) && value.is_zero(); | ||
| bool invalid_num_limbs = (num_limbs == 0) && (!value.is_zero()); |
| throw ToRadixException("Invalid parameters for ToRadix"); | ||
| } | ||
|
|
||
| if (is_output_bits) { |
There was a problem hiding this comment.
I preferred using this form because the bits version is likely to be far faster than the non-bits version.
Also, it would be great to not reimplement here and be able to call the other ones?
I understand you reimplemented because you needed to know if it truncated... maybe consider returning std::pair<vector<>, /*truncated*/bool> from the lower level methods?
There was a problem hiding this comment.
Making the low level one return a truncated boolean would help cleaning up the constrained one too
| return limbs; | ||
| } | ||
|
|
||
| std::vector<bool> PureToRadix::to_le_bits(const FF& value, uint32_t num_limbs) |
There was a problem hiding this comment.
Just a note that I don't think this one is being exercised in bulk, depending on your confidence you might want to add a test file (until I sort out how to best share the tests)
[or add it to the bulk test via the opcode, if you make the opcode use this]
| @@ -44,11 +44,14 @@ void VM_pure_to_radix_memory(State& state) | |||
| for (auto _ : state) { | |||
There was a problem hiding this comment.
jeanmon
left a comment
There was a problem hiding this comment.
Good work overall. Please consider the comments before merging. You might consider the suggested comparison (value and radix to the power num_limbs) to check if a truncation appeared in the simulation code.
| pol commit sel_should_decompose; | ||
| sel_should_decompose * (1 - sel_should_decompose) = 0; | ||
|
|
||
| // On the start row, we define sel_should_decompose as !validation_error && !num_limbs_is_zero |
There was a problem hiding this comment.
| // On the start row, we define sel_should_decompose as !validation_error && !num_limbs_is_zero | |
| // On the start row, we define sel_should_decompose as !input_validation_error && !num_limbs_is_zero |
| * (1 - sel_radix_gt_256_err) * (1 - sel_invalid_bitwise_radix) | ||
| * (1 - sel_invalid_num_limbs_err); | ||
|
|
||
| pol commit output_limb_value; |
There was a problem hiding this comment.
A comment here about what is output_limb_value and value_found would be welcome.
There was a problem hiding this comment.
Maybe a rename to limb_value would be less confusing IMHO.
| pol commit sel_truncation_error; | ||
| sel_truncation_error * (1 - sel_truncation_error) = 0; | ||
| // A truncation error happens if in the start row, we look up the to_radix gadget and value_found is off. | ||
| // The to radix gadget is little endian, so the first row that we lookup is the last limb. If it's not found in the last limb, |
There was a problem hiding this comment.
| // The to radix gadget is little endian, so the first row that we lookup is the last limb. If it's not found in the last limb, | |
| // The to_radix gadget is little endian, so the first row that we lookup is the last limb. If it's not found in the last limb, |
|
|
||
| // Negative test: disable decomposition after the start row: | ||
| trace.set(Column::to_radix_mem_sel_should_decompose, 2, 0); | ||
| EXPECT_THROW_WITH_MESSAGE((check_relation<to_radix_mem>(trace, to_radix_mem::SR_SEL_SHOULD_WRITE_MEM_CONTINUITY)), |
There was a problem hiding this comment.
I think you want to test another relation or you need to "reset the trace", otherwise it is clear that the relation will fail again.
There was a problem hiding this comment.
Ah it's a typo. I meant to check the decompose selector continuity. will fix
| check_interaction<ToRadixTraceBuilder, lookup_to_radix_mem_check_radix_lt_2_settings>(trace); | ||
| } | ||
|
|
||
| TEST(ToRadixMemoryConstrainingTest, TruncationError) |
There was a problem hiding this comment.
Can we craft a negative test like setting the truncation error while everything is fine and/or the othe way around the error is not toggled when it should?
| { | ||
| FF value = FF(0); | ||
| for (const auto& limb : limbs) { | ||
| value = value * radix + limb.as_ff(); |
There was a problem hiding this comment.
Maybe an explicit cast for radix into FF would not be bad.
| memory.set(dst_addr + i, be_output_limbs[i]); | ||
| } | ||
| // We need to reconstruct the value to check for truncation. The alternative would be to change the interface | ||
| // to return a truncated flag. |
There was a problem hiding this comment.
We can also raise the radix to the power of num_limbs and compare to the value.
In other words, if value < radix^num_limbs, we know for sure that it is not truncated.
There was a problem hiding this comment.
We need to be careful with overflowing the field (or uint256_t) while raising radix to the power of num_limbs though.
| }, | ||
| ); | ||
|
|
||
| it('Should throw an error for a number that cant be decomposed in the given number of limbs', async () => { |
There was a problem hiding this comment.
| it('Should throw an error for a number that cant be decomposed in the given number of limbs', async () => { | |
| it('Should throw an error for a number that cannot be decomposed in the given number of limbs', async () => { |
fcarreiro
left a comment
There was a problem hiding this comment.
LGTM with the small changes that are left
| */ | ||
|
|
||
| std::vector<uint8_t> limbs = to_le_radix(value, num_limbs, 2); | ||
| const auto& [limbs, truncated] = to_le_radix(value, num_limbs, 2); |
There was a problem hiding this comment.
danger danger, you want const auto without & here ;)
this only doesn't blow up because of https://stackoverflow.com/questions/39718268/why-do-const-references-extend-the-lifetime-of-rvalues
| bool invalid_bitwise_radix = is_output_bits && (radix != 2); | ||
| // Error handling - if num_limbs is zero, value needs to be zero | ||
| bool invalid_num_limbs = (num_limbs == 0) && value.is_zero(); | ||
| bool invalid_num_limbs = (num_limbs == 0) && (!value.is_zero()); |
| if (truncated) { | ||
| throw ToRadixException("Truncation error"); | ||
| } | ||
| std::reverse(limbs.begin(), limbs.end()); |
There was a problem hiding this comment.
std::ranges::reverse() to be consistent with code below.
There was a problem hiding this comment.
it doesn't work on vectors of bits
BEGIN_COMMIT_OVERRIDE feat(AVM): Fail on incomplete toRadix decompositions (#17233) END_COMMIT_OVERRIDE
Resolves #17144