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

LEM eval completed #617

Merged
merged 47 commits into from
Aug 19, 2023
Merged

LEM eval completed #617

merged 47 commits into from
Aug 19, 2023

Conversation

gabriel-barrett
Copy link
Member

This PR implements the rest of the Lurk step function in LEM

@gabriel-barrett gabriel-barrett requested review from a team as code owners August 16, 2023 22:44
Comment on lines +198 to +202
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);
Copy link
Collaborator

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.

Copy link
Collaborator

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).

Comment on lines 1327 to 1330
let alloc_num = AllocatedNum::alloc(&mut cs.namespace(|| "num"), || {
Ok(Fr::from_str_vartime("42").unwrap())
})
.unwrap();
Copy link
Collaborator

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?

Copy link
Member

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());
        }
    }

Copy link
Contributor

Choose a reason for hiding this comment

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

thanks, just added it

let mut cs = TestConstraintSystem::<Fr>::new();

let alloc_num = AllocatedNum::alloc(&mut cs.namespace(|| "num"), || {
Ok(Fr::from_str_vartime("18446744073709551616").unwrap())
Copy link
Collaborator

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?

Copy link
Contributor

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

Comment on lines 695 to 710
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;
}
Copy link
Collaborator

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.

Copy link
Collaborator

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).

.hash()
.get_value()
.map(|a| F::from_u64(a.to_u64_unchecked() & b))
.unwrap();
Copy link
Collaborator

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.

Copy link
Member

@huitseeker huitseeker left a 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> {
Copy link
Member

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.

Copy link
Member

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 Show resolved Hide resolved
@@ -1282,4 +1319,32 @@ mod tests {

assert!(cs.is_satisfied());
}

#[test]
fn test_enforce_u64() {
Copy link
Member

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.

Comment on lines 1327 to 1330
let alloc_num = AllocatedNum::alloc(&mut cs.namespace(|| "num"), || {
Ok(Fr::from_str_vartime("42").unwrap())
})
.unwrap();
Copy link
Member

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);
Copy link
Member

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.

src/lem/interpreter.rs Outdated Show resolved Hide resolved
src/lem/mod.rs Show resolved Hide resolved
porcuquine
porcuquine previously approved these changes Aug 18, 2023
Copy link
Collaborator

@porcuquine porcuquine left a 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.

Copy link
Member

@huitseeker huitseeker left a 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/circuit/gadgets/constraints.rs Show resolved Hide resolved
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);
Copy link
Member

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

@porcuquine porcuquine added this pull request to the merge queue Aug 19, 2023
Copy link
Member

@huitseeker huitseeker left a 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!

Merged via the queue into master with commit 58105dc Aug 19, 2023
6 checks passed
@porcuquine porcuquine deleted the lem_div branch August 19, 2023 02:31
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.

5 participants