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

Add POD uniqueness module #1884

Merged
merged 9 commits into from
Sep 19, 2024
Merged

Add POD uniqueness module #1884

merged 9 commits into from
Sep 19, 2024

Conversation

ax0
Copy link
Collaborator

@ax0 ax0 commented Sep 16, 2024

This PR adds a POD uniqueness module to GPCs. Summary of changes:

  • Addition of uniqueness module to gpcircuits package.
  • Minor refactoring of list membership module in gpcircuits package.
  • Changes to ProtoPODGPC circuit.
  • Addition of uniquePODs field to GPCProofConfig together with changes to the GPC compiler.
  • Corresponding tests.

This PR depends on the artifact changes in proofcarryingdata/snark-artifacts#13.

@ax0 ax0 requested a review from artwyman September 16, 2024 21:35
Copy link
Collaborator

@artwyman artwyman left a comment

Choose a reason for hiding this comment

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

Applying this in the GPCPCD filter (so it doesn't let the user choose the same POD twice) would be a cool addition, though it requires access to all the args at once, so I think the current framework doesn't allow for it.

packages/lib/gpcircuits/circuits/gpc-util.circom Outdated Show resolved Hide resolved
* Takes the first I elements of a given array and returns the array
* containing those elements.
*/
template Take(I,N) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Slice? Maybe that name implies there should be a separate start index vs. hard-coded zero.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Did the thumbs-up suggest you intended to implement this suggestion? Or does it not work because of the circom limitation discussed elsewhere?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The thumbs-up was in response to the latter comment. I suppose I could replace the take-inspired template with a slice-inspired one.

isNotMember <== MultiAND(NUM_LIST_ELEMENTS)(
Add(NUM_LIST_ELEMENTS)(
-comparisonValue,
Take(NUM_LIST_ELEMENTS, MAX_LIST_ELEMENTS)(
Copy link
Collaborator

Choose a reason for hiding this comment

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

This seems out-of-place for this as a general list-non-membership template. The need to take only the head of the list is motivated only by the uniqueness use case, so maybe Take should happen at that level, on the input array?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is how I initially did it, but Circom doesn't like that approach:

thread 'main' panicked at compiler/src/intermediate_representation/translate.rs:959:25:
internal error: entered unreachable code: On development: Circom compiler does not accept for now the assignment of arrays of unknown sizes during the execution of loops

Copy link
Collaborator

Choose a reason for hiding this comment

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

Where's the unknown size? I'd expect all the array lenghts to be known (based on template args) at the point of use. I'd have to see the specific code to comment on it, though, and I'm fully willing to believe that circom is broken.

Copy link
Collaborator Author

@ax0 ax0 Sep 17, 2024

Choose a reason for hiding this comment

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

All of the array sizes depend only on the template parameter and loop index. I assume there is a bug in the loop + anonymous component unrolling code. What I did was replace the list non-membership and uniqueness module templates with the following:

template NotEqualsAny(MAX_LIST_ELEMENTS) {
    signal input comparisonValue; 

    signal input values[MAX_LIST_ELEMENTS];

    signal output isNotEqual;

    if (MAX_LIST_ELEMENTS == 0) {
        isNotEqual <== 1;
    } else {
        isNotEqual <== MultiAND(MAX_LIST_ELEMENTS)(
            ArrayAddScalar(MAX_LIST_ELEMENTS)(
                -comparisonValue,
                values
            )
        );
    }
}

template UniquenessModule(
    NUM_LIST_ELEMENTS
) {
    signal input values[NUM_LIST_ELEMENTS];

    signal output valuesAreUnique;

    signal noDupsAfter[NUM_LIST_ELEMENTS];

    for(var i = 0; i < NUM_LIST_ELEMENTS; i++) {
        var j = i+1;
        noDupsAfter[i] <==
            NotEqualsAny(NUM_LIST_ELEMENTS - j)(
                values[i],
                Take(NUM_LIST_ELEMENTS - j, NUM_LIST_ELEMENTS)(
                    ArrayRotl(j, NUM_LIST_ELEMENTS)(values)
                )
            );
    }

    valuesAreUnique <== NOT()(
        IsZero()(
            MultiAND(NUM_LIST_ELEMENTS)(
                noDupsAfter
            )
        )
    );
}

My original version used Drop instead of Take and Rotl (which is what I believe you suggested in your other comment) but yielded the same result.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Interesting. Seems like a circom limitation keeps us from sizing arrays based on loop variables, but not if they're inside of templates. It's unintuitive to me that a loop variable can be a template argument (as demonstrated in our use of Take and Rotl) but can't be an array size. Take does return an array of a size which depends on its template argument, so it shouldn't matter if it's directly in the loop or in a template instantiated in the loop.

Would it help if you combined Take and Rotl into a single Slice template which returned array elements from [I, I+N)? Or would that make the situation worse? I don't really understand what circom's limitation is, so it's hard to know what to suggest. If there's no obvious solution I'm okay with accepting what works, given the final number of constraints is the same.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I tried doing this with a single drop-like template but ran into the same error, which is what led me to the current approach.

} else {
isMember <== IsZero()(partialProduct[MAX_LIST_ELEMENTS - 1]);
isNotMember <== MultiAND(NUM_LIST_ELEMENTS)(
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is clever, though it took me a while to verify that MultiAND really expands to just a sequence of multiplies identical to what you had before (assuming that trivial b <== a constraints get optimized out). I find the partial product easier to reason about.

I wonder if the MultiAND module comes from an older version of circom which didn't have for loops. The whole recursive divide-and-conquer algorithm feels like a lot of complexity to obfuscate the fact it's performing n-1 multiplies to calculate the product of n elements.

This is a stylistic opinion, so I don't mind if you keep the current solution.

packages/lib/gpcircuits/circuits/list-membership.circom Outdated Show resolved Hide resolved
packages/lib/gpc/src/gpcTypes.ts Outdated Show resolved Hide resolved
packages/lib/gpc/src/gpcChecks.ts Outdated Show resolved Hide resolved
packages/lib/gpc/src/gpcChecks.ts Outdated Show resolved Hide resolved
packages/lib/gpc/test/gpc.spec.ts Show resolved Hide resolved
isNotMember <== MultiAND(NUM_LIST_ELEMENTS)(
Add(NUM_LIST_ELEMENTS)(
-comparisonValue,
Take(NUM_LIST_ELEMENTS, MAX_LIST_ELEMENTS)(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Interesting. Seems like a circom limitation keeps us from sizing arrays based on loop variables, but not if they're inside of templates. It's unintuitive to me that a loop variable can be a template argument (as demonstrated in our use of Take and Rotl) but can't be an array size. Take does return an array of a size which depends on its template argument, so it shouldn't matter if it's directly in the loop or in a template instantiated in the loop.

Would it help if you combined Take and Rotl into a single Slice template which returned array elements from [I, I+N)? Or would that make the situation worse? I don't really understand what circom's limitation is, so it's hard to know what to suggest. If there's no obvious solution I'm okay with accepting what works, given the final number of constraints is the same.

// values[i] is not unique.
signal isUnique[NUM_LIST_ELEMENTS];
// element of `values` has no duplicates following it in `values`,
// i.e. noDupsAfter[i] = 0 iff this is the case.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Shouldn't this be != 0 rather than = 0? Otherwise I'd reverse the naming of the variable so it indicates what's true, not what's false.

packages/lib/gpcircuits/circuits/uniqueness.circom Outdated Show resolved Hide resolved
* Takes the first I elements of a given array and returns the array
* containing those elements.
*/
template Take(I,N) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Did the thumbs-up suggest you intended to implement this suggestion? Or does it not work because of the circom limitation discussed elsewhere?

@ax0 ax0 requested a review from artwyman September 19, 2024 07:15
signal valueDifferences[NUM_PAIRS];
for (var i = 0; i < NUM_LIST_ELEMENTS; i++) {
for(var j = i + 1; j < NUM_LIST_ELEMENTS; j++) {
var k = j + NUM_LIST_ELEMENTS*i - (i + 1)*(i + 2)\2;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh, I was oversimplifying this wasn't I. Took me a moment to realize what the last clause was for.
Had I realized the need for this, I might've just had k start at 0 and be separately incremented, but this is equivalent and cleverer.

@ax0 ax0 added this pull request to the merge queue Sep 19, 2024
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Sep 19, 2024
@ax0 ax0 added this pull request to the merge queue Sep 19, 2024
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to a conflict with the base branch Sep 19, 2024
@ax0 ax0 enabled auto-merge September 19, 2024 09:19
@ax0 ax0 added this pull request to the merge queue Sep 19, 2024
Merged via the queue into proofcarryingdata:main with commit 2971165 Sep 19, 2024
1 check passed
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