-
Notifications
You must be signed in to change notification settings - Fork 301
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
feat(avm): range checks in vm2 #11433
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copied this file over but renamed some things and removed the selectors specifying which subtrace a range check originated from
9e6df37
to
834e682
Compare
sel {op1, op2, op3, op4} is sel {op1, op2, op3, op4}; No newline at end of file | ||
sel {op1, op2, op3, op4} is sel {op1, op2, op3, op4}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know what's different about our editors, but this happens all the time
TEST(AvmSimulationRangeCheckTest, AssertRange) | ||
{ | ||
EventEmitter<RangeCheckEvent> emitter; | ||
RangeCheck range_check(emitter); | ||
|
||
uint128_t value = 333; | ||
uint8_t num_bits = 100; | ||
range_check.assert_range(value, num_bits); | ||
|
||
RangeCheckEvent expect_event = { | ||
.value = value, | ||
.num_bits = num_bits, | ||
}; | ||
|
||
std::vector<RangeCheckEvent> events = { expect_event }; | ||
EXPECT_EQ(emitter.dump_events(), events); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test feels like it's doing... nothing. But I guess the range-check simulation module does just about nothing since it is really a tracegen thing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah indeed. It's such a simple gadget (simulation wise) that it doesnt even call anything else.
uint32_t row = 0; | ||
for (const auto& event : events) { | ||
uint256_t value_u256 = uint256_t::from_uint128(event.value); | ||
uint8_t num_bits = 0; | ||
|
||
std::array<uint16_t, 7> fixed_slice_registers; // u16_r0...6 | ||
// Enum representing which of is_lte_u* holds true. | ||
// Only one can be chosen/true for a given range check. | ||
// The highest X is chosen such that "value is lte X" holds true. | ||
IsLte is_lte = IsLte::IS_LTE_U16; | ||
uint16_t dynamic_slice_register = 0; // same as u16_r7 | ||
uint16_t dynamic_bits = 0; | ||
|
||
// Split the value into 16-bit chunks | ||
for (size_t i = 0; i < 8; i++) { | ||
// The most significant 16-bits have to be placed in the dynamic slice register | ||
if (event.num_bits <= 16) { | ||
dynamic_slice_register = static_cast<uint16_t>(value_u256); | ||
// Set is_lte based on on how many 16 bit chunks have already been processed | ||
// For example, if this is the 0th, then IS_LTE_U16 is set | ||
// If this is the first, then IS_LTE_U32 is set, ... | ||
// TODO(dbanks12): is this more or less intuitive then the bit magic from before? | ||
is_lte = IsLte(i); | ||
dynamic_bits = event.num_bits; | ||
break; | ||
} | ||
// We have more chunks of 16-bits to operate on, so set the ith fixed register | ||
fixed_slice_registers[i] = static_cast<uint16_t>(value_u256); | ||
num_bits -= 16; | ||
value_u256 >>= 16; | ||
} | ||
|
||
auto dynamic_diff = static_cast<uint16_t>((1 << dynamic_bits) - dynamic_slice_register - 1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See my TODO here
struct RangeCheckEvent { | ||
uint128_t value; | ||
uint8_t num_bits; | ||
|
||
// TODO(dbanks12): do we want this operator just for tests? | ||
bool operator==(const RangeCheckEvent& other) const { return value == other.value && num_bits == other.num_bits; } | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See TODO. The simple test for this event uses this operator==
to do checks. But maybe it's unnecessary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking great from a VM2 perspective!
@@ -0,0 +1,189 @@ | |||
namespace range_check(256); | |||
// Range check selector | |||
pol commit sel_rng_chk; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are now just using sel
for the subtrace selector so that it looks like range_check.sel
when the namespace is used.
sel_r5_16_bit_rng_lookup - (is_lte_u112 + is_lte_u128) = 0; | ||
sel_r6_16_bit_rng_lookup - is_lte_u128 = 0; | ||
|
||
#[LOOKUP_RNG_CHK_IS_R0_16_BIT] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These interactions were not supposed to happen :) but I was about to propose that we can start doing lookups into precomputed (and only precomputed). So let's keep them and make them work :)
} // namespace bb::avm2::constraining |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
||
TEST(AvmConstrainingTest, RangeCheckNegativeCheckRecomposition) | ||
{ | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Please remove these extra new lines
{ C::range_check_sel_r1_16_bit_rng_lookup, 0 }, // BAD! SHOULD BE 1 | ||
} }); | ||
|
||
EXPECT_THROW_WITH_MESSAGE(check_relation<range_check>(trace.as_rows()), "Relation range_check"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Negative tests should probably be more granular (non full); if sth else starts failing, this statement will pass. See other negative tests. Probably you want to check a given subrelation(s).
{ | ||
using C = Column; | ||
|
||
uint32_t row = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you use any shifts ('
) in pil? If yes you have to start from 1, otherwise it's ok.
{ C::range_check_dyn_rng_chk_bits, dynamic_bits }, | ||
{ C::range_check_dyn_rng_chk_pow_2, 1 << dynamic_bits }, | ||
{ C::range_check_dyn_diff, dynamic_diff }, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe a comment if you want to separate them, e.g.
// Lookups.
[&]() { | ||
PrecomputedTraceBuilder precomputed_builder; | ||
AVM_TRACK_TIME("tracegen/precomputed/rng_chk_8", precomputed_builder.process_sel_rng_chk_8(trace)); | ||
}, | ||
[&]() { | ||
PrecomputedTraceBuilder precomputed_builder; | ||
AVM_TRACK_TIME("tracegen/precomputed/rng_chk_16", precomputed_builder.process_sel_rng_chk_16(trace)); | ||
}, | ||
[&]() { | ||
PrecomputedTraceBuilder precomputed_builder; | ||
AVM_TRACK_TIME("tracegen/precomputed/power_of_2", precomputed_builder.process_power_of_2(trace)); | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are so cheap that I'd put the 3 of them in the same job.
@@ -122,6 +146,46 @@ TraceContainer AvmTraceGenHelper::generate_trace(EventsContainer&& events) | |||
PermutationBuilder<perm_dummy_dynamic_permutation_settings> perm_execution_execution; | |||
perm_execution_execution.process(trace); | |||
}, | |||
[&]() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
perfect 👍
|
||
namespace bb::avm2::tracegen { | ||
|
||
template <typename LookupSettings> class LookupIntoRngChk : public BaseLookupTraceBuilder<LookupSettings> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah interesting! You were able to use the same class for lookups into u8, u16 etc, because they all use CLK and only change based on the selector. Maybe add a comment?
// Range check selector | ||
pol commit sel_rng_chk; | ||
sel_rng_chk * (1 - sel_rng_chk) = 0; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add
// No relations will be checked if this identity is satisfied.
#[skippable_if]
sel = 0;
?
we can discuss skippable in a huddle
using C = Column; | ||
using range_check = bb::avm2::range_check<FF>; | ||
|
||
TEST(AvmConstrainingTest, RangeCheckPositiveIsLteMutuallyExclusive) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add a test where you
- create a single row with
clk = 1
. this is to force a row to be in the trace. I actually want a completely empty row but it doesnt matter. - check the whole relation and expect no failures
?
This is to test that all relations pass in rows fulls of 0. We probably need to add this test to every gadget.
Also sel_rng_chk_8/16 and power_of_2 precomputed tables