-
Notifications
You must be signed in to change notification settings - Fork 56
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
LEM eval completed #617
LEM eval completed #617
Conversation
let diff = |_| pack - num.get_variable(); | ||
let premise_lc = |_| premise.lc(CS::one(), F::ONE); | ||
let zero = |lc| lc; | ||
|
||
cs.enforce( | ||
|| "pack", | ||
|_| v_lc, | ||
|lc| lc + CS::one(), | ||
|lc| lc + num.get_variable(), | ||
); | ||
cs.enforce(|| "pack", diff, premise_lc, zero); |
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.
Because this logic is tricky and we already abstract it elsewhere for that purpose, I think it would be better to isolate implementation there. Concretely, I think you can use enforce_implication_lc_zero(diff)
here.
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 will also eventually want an allocating version of this (allocating a bit) so we can do erroring type checks. It doesn't have to happen now, but in the interest of thinking about how we want to normalize decomposition of these gadgets, I would suggest that this has an is_pack
(maybe there is a better name) part, which is a predicate that returns a linear combination.
Then from that we can create an allocated predicated, an 'implies' function, and an 'enforce' function. Then we could just define our predicates and derive the rest with a proc macro (or more manually, but in a transparent procedural way).
src/circuit/gadgets/constraints.rs
Outdated
let alloc_num = AllocatedNum::alloc(&mut cs.namespace(|| "num"), || { | ||
Ok(Fr::from_str_vartime("42").unwrap()) | ||
}) | ||
.unwrap(); |
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.
Even if not a full-on proptest, I would love to see this test made at least a little more convincing. For example, can we call it on 0, 1, and the largest U64?
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.
proptest!{
#[test]
fn test_implies_u64(f in any::<FWrap<Fr>>()) {
let mut cs = TestConstraintSystem::<Fr>::new();
let num = AllocatedNum::alloc(cs.namespace(|| "num"), || Ok(f.0)).unwrap();
let t = Boolean::Constant(true);
implies_u64(&mut cs.namespace(|| "enforce u64"), &t, &num).unwrap();
let f_u64_roundtrip: Fr = f.0.to_u64_unchecked().into();
let was_u64 = f_u64_roundtrip == f.0;
prop_assert_eq!(was_u64, cs.is_satisfied());
}
}
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.
thanks, just added it
src/circuit/gadgets/constraints.rs
Outdated
let mut cs = TestConstraintSystem::<Fr>::new(); | ||
|
||
let alloc_num = AllocatedNum::alloc(&mut cs.namespace(|| "num"), || { | ||
Ok(Fr::from_str_vartime("18446744073709551616").unwrap()) |
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.
Can we also at least test this on 2^64?
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.
added a comment to make this edge case clear
src/lem/circuit.rs
Outdated
let b = u64::wrapping_sub(u64::wrapping_pow(2, *n), 1); | ||
let a = bound_allocations.get(a)?; | ||
let a_bits = a | ||
.hash() | ||
.to_bits_le_strict(&mut cs.namespace(|| "bitwise_and"))?; | ||
let mut trunc_bits = Vec::with_capacity(64); | ||
let mut b_rest = b; | ||
for a_bit in a_bits { | ||
let b_bit = b_rest & 1; | ||
if b_bit == 1 { | ||
trunc_bits.push(a_bit.clone()); | ||
} else { | ||
trunc_bits.push(Boolean::Constant(false)) | ||
} | ||
b_rest >>= 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.
Considering the goal is truncation, and the reason to switch to that (from bitwise-and) was simplicity and less room for error in providing unneeded generality, I don't think using a bitwise-and operation under the hood as the implementation technique is ideal.
I think instead you should just do the obvious simple thing, which is just take the least significant n
bits from a_bits
. I think both b
and any concept of bitwise-and just add noise and don't need to appear in the source at all.
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.
If you need to have this be a constant sized/shaped circuit, regardless of n
(which I understand), you can still pad with Boolean::Constant(false)
.
src/lem/circuit.rs
Outdated
.hash() | ||
.get_value() | ||
.map(|a| F::from_u64(a.to_u64_unchecked() & b)) | ||
.unwrap(); |
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 think this unwrap()
is going to cause a problem when you're trying to synthesize the shape and don't have any value for a
— but still need to construct constraints that will work for any a
that may be supplied when generating a witness.
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.
General impression: I'd love more testing, both on the gadgets, and on the larger lem invariants.
@@ -141,6 +141,18 @@ pub trait LurkField: PrimeField + PrimeFieldBits { | |||
Some(u64::from_le_bytes(byte_array)) | |||
} | |||
|
|||
/// Attempts to convert the field element to a u64 | |||
fn to_u128(&self) -> Option<u128> { |
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.
Just impl TryFrom<LurkField> for u128
should be more idiomatic.
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 is following the pattern in the rest of the code. If anything, we can create an issue to rewrite all the functions at once
src/circuit/gadgets/constraints.rs
Outdated
@@ -1282,4 +1319,32 @@ mod tests { | |||
|
|||
assert!(cs.is_satisfied()); | |||
} | |||
|
|||
#[test] | |||
fn test_enforce_u64() { |
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 should be renamed to match what it's testing.
src/circuit/gadgets/constraints.rs
Outdated
let alloc_num = AllocatedNum::alloc(&mut cs.namespace(|| "num"), || { | ||
Ok(Fr::from_str_vartime("42").unwrap()) | ||
}) | ||
.unwrap(); |
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.
proptest!{
#[test]
fn test_implies_u64(f in any::<FWrap<Fr>>()) {
let mut cs = TestConstraintSystem::<Fr>::new();
let num = AllocatedNum::alloc(cs.namespace(|| "num"), || Ok(f.0)).unwrap();
let t = Boolean::Constant(true);
implies_u64(&mut cs.namespace(|| "enforce u64"), &t, &num).unwrap();
let f_u64_roundtrip: Fr = f.0.to_u64_unchecked().into();
let was_u64 = f_u64_roundtrip == f.0;
prop_assert_eq!(was_u64, cs.is_satisfied());
}
}
let t = Symbol("t"); | ||
let zero = Num(0); | ||
let size_u64 = Num(18446744073709551616); |
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.
the "at compile time" meaning may be overloaded, but inline_const is due to be stabilized soon™, which may help write u64::pow(2, 64)
here.
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.
Approved based on general characteristics of the language defined and circuit generated. Pending @huitseeker' approval.
This is not a complete review of either LEM or the circuit, but it's enough to merge this work.
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.
Please add a docstring on implies_u64 such that reading it allows one to understand the difference with {implies, enforce}_pack.
src/lem/circuit.rs
Outdated
implies_u64(cs.namespace(|| "rem_u64"), not_dummy, &rem)?; | ||
implies_u64(cs.namespace(|| "diff_u64"), not_dummy, &diff)?; | ||
|
||
linear(cs, || "linear", b, &div, &rem, a); |
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 see that rename
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 is great! thanks, all!
This PR implements the rest of the Lurk step function in LEM