From d5b4f053dc0b113c5e81d34cec892a1e57b9ac6d Mon Sep 17 00:00:00 2001 From: Ahmad Date: Thu, 12 Sep 2024 22:40:49 +1000 Subject: [PATCH 01/11] Add uniqueness module --- packages/lib/gpc/src/gpcChecks.ts | 25 +++++++++ packages/lib/gpc/src/gpcCompile.ts | 25 +++++++-- packages/lib/gpc/src/gpcTypes.ts | 6 ++ packages/lib/gpc/src/gpcUtil.ts | 12 ++++ packages/lib/gpc/test/gpcChecks.spec.ts | 53 +++++++++++++++++- packages/lib/gpc/test/gpcCompile.spec.ts | 16 ++++++ packages/lib/gpc/test/gpcUtil.spec.ts | 15 +++++ packages/lib/gpcircuits/circuits.json | 6 ++ .../lib/gpcircuits/circuits/gpc-util.circom | 38 +++++++++++++ .../circuits/list-membership.circom | 54 +++++++++++++----- .../gpcircuits/circuits/proto-pod-gpc.circom | 13 +++++ .../lib/gpcircuits/circuits/uniqueness.circom | 45 +++++++++++++++ .../lib/gpcircuits/src/circuitParameters.json | 4 +- packages/lib/gpcircuits/src/index.ts | 1 + packages/lib/gpcircuits/src/proto-pod-gpc.ts | 10 ++++ packages/lib/gpcircuits/src/uniqueness.ts | 15 +++++ packages/lib/gpcircuits/src/util.ts | 47 ++++++++++++++++ .../lib/gpcircuits/test/proto-pod-gpc.spec.ts | 55 +++++++++++++----- .../lib/gpcircuits/test/uniqueness.spec.ts | 56 +++++++++++++++++++ 19 files changed, 461 insertions(+), 35 deletions(-) create mode 100644 packages/lib/gpcircuits/circuits/uniqueness.circom create mode 100644 packages/lib/gpcircuits/src/uniqueness.ts create mode 100644 packages/lib/gpcircuits/test/uniqueness.spec.ts diff --git a/packages/lib/gpc/src/gpcChecks.ts b/packages/lib/gpc/src/gpcChecks.ts index 6f8486b21f..7717c4f5d3 100644 --- a/packages/lib/gpc/src/gpcChecks.ts +++ b/packages/lib/gpc/src/gpcChecks.ts @@ -131,6 +131,10 @@ export function checkProofConfig(proofConfig: GPCProofConfig): GPCRequirements { includeOwnerV4 ||= hasOwnerV4; } + if (proofConfig.uniquePODs !== undefined) { + requireType("uniquePODs", proofConfig.uniquePODs, "boolean"); + } + if (proofConfig.tuples !== undefined) { checkProofTupleConfig(proofConfig); } @@ -618,9 +622,30 @@ export function checkProofInputsForConfig( throw new Error("Nullifier requires an entry containing owner ID."); } + checkProofPODUniquenessInputsForConfig(proofConfig, proofInputs); + checkProofListMembershipInputsForConfig(proofConfig, proofInputs); } +export function checkProofPODUniquenessInputsForConfig( + proofConfig: { uniquePODs?: boolean }, + proofInputs: { pods: Record } +) { + if (proofConfig.uniquePODs) { + const contentIDs = Object.values(proofInputs.pods).map( + (pod) => pod.contentID + ); + const uniqueContentIDs = _.uniq(contentIDs); + const podsAreUnique = _.isEqual(contentIDs, uniqueContentIDs); + + if (!podsAreUnique) { + throw new Error( + "Proof configuration specifies that the PODs should be unique, but they aren't." + ); + } + } +} + export function checkProofBoundsCheckInputsForConfig( entryName: PODEntryIdentifier, entryConfig: GPCProofEntryConfig, diff --git a/packages/lib/gpc/src/gpcCompile.ts b/packages/lib/gpc/src/gpcCompile.ts index 28687b7918..d8183e2a69 100644 --- a/packages/lib/gpc/src/gpcCompile.ts +++ b/packages/lib/gpc/src/gpcCompile.ts @@ -8,6 +8,7 @@ import { ProtoPODGPCPublicInputs, array2Bits, computeTupleIndices, + dummyObjectSignals, extendedSignalArray, hashTuple, padArray, @@ -32,6 +33,7 @@ import { import _ from "lodash"; import { GPCBoundConfig, + GPCProofConfig, GPCProofEntryConfig, GPCProofInputs, GPCProofObjectConfig, @@ -377,6 +379,9 @@ export function compileProofConfig( circuitDesc.maxListElements ); + // Create subset of inputs for POD uniqueness module. + const circuitUniquenessInputs = compileProofPODUniqueness(proofConfig); + // Create other global inputs. const circuitGlobalInputs = compileProofGlobal(proofInputs); @@ -402,6 +407,7 @@ export function compileProofConfig( ...circuitNumericValueInputs, ...circuitMultiTupleInputs, ...circuitListMembershipInputs, + ...circuitUniquenessInputs, ...circuitGlobalInputs }; } @@ -673,10 +679,9 @@ function combineProofObjects( objectSignatureS: CircuitSignal /*MAX_OBJECTS*/[]; } { // Object modules don't have an explicit disabled state, so spare object - // slots get filled in with copies of Object 0. - for (let objIndex = allObjInputs.length; objIndex < maxObjects; objIndex++) { - allObjInputs.push({ ...allObjInputs[0] }); - } + // slots get filled with dummy objects with distinct content IDs. + const objPadding = dummyObjectSignals(maxObjects - allObjInputs.length); + allObjInputs.push(...objPadding); return { objectContentID: allObjInputs.map((o) => o.contentID), @@ -721,6 +726,14 @@ function compileProofEntry( }; } +export function compileProofPODUniqueness(proofConfig: { + uniquePODs?: boolean; +}): { + uniquenessModuleIsEnabled: CircuitSignal; +} { + return { uniquenessModuleIsEnabled: BigInt(proofConfig.uniquePODs ?? false) }; +} + function compileProofVirtualEntry< ObjInput extends POD | GPCRevealedObjectClaims >( @@ -1111,6 +1124,9 @@ export function compileVerifyConfig( circuitDesc.maxListElements ); + // Create subset of inputs for POD uniqueness module. + const circuitUniquenessInputs = compileProofPODUniqueness(verifyConfig); + // Create other global inputs. Logic shared with compileProofConfig, // since all the signals involved are public. const circuitGlobalInputs = compileProofGlobal(verifyRevealed); @@ -1144,6 +1160,7 @@ export function compileVerifyConfig( ...circuitNumericValueInputs, ...circuitMultiTupleInputs, ...circuitListMembershipInputs, + ...circuitUniquenessInputs, ...circuitGlobalInputs }, circuitOutputs: { diff --git a/packages/lib/gpc/src/gpcTypes.ts b/packages/lib/gpc/src/gpcTypes.ts index 874e367daa..aba352c06a 100644 --- a/packages/lib/gpc/src/gpcTypes.ts +++ b/packages/lib/gpc/src/gpcTypes.ts @@ -320,6 +320,12 @@ export type GPCProofConfig = { */ pods: Record; + /** + * Indicates whether the configured PODs are unique. If this is true, it + * enables the POD uniqueness module on the circuit level. + */ + uniquePODs?: boolean; + /** * Defines named tuples of POD entries. The tuples' names lie in a separate * namespace and are internally prefixed with '$tuple.'. These tuples must be diff --git a/packages/lib/gpc/src/gpcUtil.ts b/packages/lib/gpc/src/gpcUtil.ts index 5683dd54b0..c6bd2f26dd 100644 --- a/packages/lib/gpc/src/gpcUtil.ts +++ b/packages/lib/gpc/src/gpcUtil.ts @@ -67,6 +67,11 @@ export function canonicalizeConfig( canonicalPODs[objName] = canonicalizeObjectConfig(objectConfig); } + // Omit POD uniqueness field if not `true`. + const canonicalizedPODUniquenessConfig = canonicalizePODUniquenessConfig( + proofConfig.uniquePODs + ); + // Force tuples and their membership lists to be sorted by name const tupleRecord = canonicalizeTupleConfig(proofConfig.tuples ?? {}); @@ -75,6 +80,7 @@ export function canonicalizeConfig( return { circuitIdentifier: circuitIdentifier, pods: canonicalPODs, + ...canonicalizedPODUniquenessConfig, ...(proofConfig.tuples !== undefined ? { tuples: tupleRecord } : {}) }; } @@ -118,6 +124,12 @@ function canonicalizeObjectConfig( }; } +export function canonicalizePODUniquenessConfig( + podUniquenessConfig: boolean | undefined +): { uniquePODs?: boolean } { + return podUniquenessConfig ? { uniquePODs: true } : {}; +} + export function canonicalizeVirtualEntryConfig( virtualEntryConfig: GPCProofEntryConfigCommon, defaultIsRevealed: boolean diff --git a/packages/lib/gpc/test/gpcChecks.spec.ts b/packages/lib/gpc/test/gpcChecks.spec.ts index 7dc880b077..abc14451dd 100644 --- a/packages/lib/gpc/test/gpcChecks.spec.ts +++ b/packages/lib/gpc/test/gpcChecks.spec.ts @@ -1,5 +1,7 @@ import { + POD, PODEdDSAPublicKeyValue, + PODName, PODValue, POD_INT_MAX, POD_INT_MIN @@ -10,8 +12,10 @@ import { GPCProofEntryBoundsCheckConfig, GPCProofEntryConfig } from "../src"; import { checkProofBoundsCheckInputsForConfig, checkProofEntryBoundsCheckConfig, - checkProofEntryConfig + checkProofEntryConfig, + checkProofPODUniquenessInputsForConfig } from "../src/gpcChecks"; +import { privateKey, sampleEntries, sampleEntries2 } from "./common"; describe("Proof entry config check should work", () => { it("should pass for a minimal entry configuration", () => { @@ -244,4 +248,51 @@ describe("Proof config check against input for bounds checks should work", () => } }); }); + +describe("Proof config check against input for POD uniqueness should work", () => { + const pod1 = POD.sign(sampleEntries, privateKey); + const pod2 = POD.sign(sampleEntries2, privateKey); + const pod3 = POD.sign({ A: sampleEntries.A }, privateKey); + const pod4 = POD.sign({ E: sampleEntries.E }, privateKey); + + const uniquePODInputs = [ + { pod1 }, + { pod1, pod2 }, + { pod1, pod2, pod3 }, + { pod1, pod2, pod3, pod4 } + ] as Record[]; + + const nonuniquePODInputs = [ + { pod1, pod2: pod1 }, + { pod1, pod2, pod3: pod1 }, + { pod2, pod1, pod3: pod1 }, + { pod1, pod2, pod3, pod4: pod1 }, + { pod2, pod3, pod1, pod4: pod1 } + ] as Record[]; + + it("should pass if disabled", () => { + for (const pods of uniquePODInputs.concat(nonuniquePODInputs)) { + for (const config of [{}, { uniquePODs: false }]) { + expect(() => checkProofPODUniquenessInputsForConfig(config, { pods })) + .to.not.throw; + } + } + }); + + it("should pass for unique POD inputs", () => { + for (const pods of uniquePODInputs) { + expect(() => + checkProofPODUniquenessInputsForConfig({ uniquePODs: true }, { pods }) + ).to.not.throw; + } + }); + + it("should throw for non-unique POD inputs if enabled", () => { + for (const pods of nonuniquePODInputs) { + expect(() => + checkProofPODUniquenessInputsForConfig({ uniquePODs: true }, { pods }) + ).to.throw; + } + }); +}); // TODO(POD-P3): More tests diff --git a/packages/lib/gpc/test/gpcCompile.spec.ts b/packages/lib/gpc/test/gpcCompile.spec.ts index 766cccba55..d5f6f9e1e3 100644 --- a/packages/lib/gpc/test/gpcCompile.spec.ts +++ b/packages/lib/gpc/test/gpcCompile.spec.ts @@ -10,6 +10,7 @@ import { poseidon2 } from "poseidon-lite/poseidon2"; import { compileProofOwnerV3, compileProofOwnerV4, + compileProofPODUniqueness, compileVerifyOwnerV3, compileVerifyOwnerV4 } from "../src/gpcCompile"; @@ -233,4 +234,19 @@ describe("Semaphore V4 owner module compilation for verification should work", ( } }); }); + +describe("POD uniqueness module compilation for proving and verification should work", () => { + it("should work as expected for a proof configuration with POD uniqueness enabled", () => { + expect(compileProofPODUniqueness({ uniquePODs: true })).to.deep.equal({ + uniquenessModuleIsEnabled: 1n + }); + }); + it("should work as expected for a proof configuration with POD uniqueness disabled", () => { + for (const config of [{}, { uniquePODs: false }]) { + expect(compileProofPODUniqueness(config)).to.deep.equal({ + uniquenessModuleIsEnabled: 0n + }); + } + }); +}); // TODO(POD-P3): More tests diff --git a/packages/lib/gpc/test/gpcUtil.spec.ts b/packages/lib/gpc/test/gpcUtil.spec.ts index a6f37f3297..5c045d58de 100644 --- a/packages/lib/gpc/test/gpcUtil.spec.ts +++ b/packages/lib/gpc/test/gpcUtil.spec.ts @@ -11,6 +11,7 @@ import { boundsCheckConfigFromProofConfig, canonicalizeBoundsCheckConfig, canonicalizeEntryConfig, + canonicalizePODUniquenessConfig, canonicalizeVirtualEntryConfig } from "../src/gpcUtil"; @@ -414,4 +415,18 @@ describe("Bounds check configuration derivation works as expected", () => { }); }); }); + +describe("POD uniqueness config canonicalization should work", () => { + it("should work as expected if omitted", () => { + expect(canonicalizePODUniquenessConfig(undefined)).to.deep.equal({}); + }); + it("should work as expected if enabled", () => { + expect(canonicalizePODUniquenessConfig(true)).to.deep.equal({ + uniquePODs: true + }); + }); + it("should work as expected if explicitly disabled", () => { + expect(canonicalizePODUniquenessConfig(false)).to.deep.equal({}); + }); +}); // TODO(POD-P3): More tests diff --git a/packages/lib/gpcircuits/circuits.json b/packages/lib/gpcircuits/circuits.json index 4858392674..ee8e1116d6 100644 --- a/packages/lib/gpcircuits/circuits.json +++ b/packages/lib/gpcircuits/circuits.json @@ -34,6 +34,7 @@ "listComparisonValueIndex", "listContainsComparisonValue", "listValidValues", + "uniquenessModuleIsEnabled", "globalWatermark" ] }, @@ -72,6 +73,7 @@ "listComparisonValueIndex", "listContainsComparisonValue", "listValidValues", + "uniquenessModuleIsEnabled", "globalWatermark" ] }, @@ -110,6 +112,7 @@ "listComparisonValueIndex", "listContainsComparisonValue", "listValidValues", + "uniquenessModuleIsEnabled", "globalWatermark" ] }, @@ -148,6 +151,7 @@ "listComparisonValueIndex", "listContainsComparisonValue", "listValidValues", + "uniquenessModuleIsEnabled", "globalWatermark" ] }, @@ -186,6 +190,7 @@ "listComparisonValueIndex", "listContainsComparisonValue", "listValidValues", + "uniquenessModuleIsEnabled", "globalWatermark" ] }, @@ -224,6 +229,7 @@ "listComparisonValueIndex", "listContainsComparisonValue", "listValidValues", + "uniquenessModuleIsEnabled", "globalWatermark" ] } diff --git a/packages/lib/gpcircuits/circuits/gpc-util.circom b/packages/lib/gpcircuits/circuits/gpc-util.circom index a57a1de1c7..4bc01e1e10 100644 --- a/packages/lib/gpcircuits/circuits/gpc-util.circom +++ b/packages/lib/gpcircuits/circuits/gpc-util.circom @@ -82,3 +82,41 @@ template MaybeInputSelector (N) { // `selectedIndex` must have been in [0,...,N-1] or -1. (1 - success)*(selectedIndex + 1) === 0; } + +/** + * Left-rotates elements of a given array by I positions. + */ +template Rotl(I,N) { + signal input in[N]; + signal output out[N]; + + for(var i = 0; i < N; i++) { + out[i] <== in[(i + I)%N]; + } +} + +/** + * Takes the first I elements of a given array and returns the array + * containing those elements. + */ +template Take(I,N) { + signal input in[N]; + signal output out[I]; + + for (var i = 0; i < I; i++) { + out[i] <== in[i]; + } +} + +/** + * Adds a field element to all elements of a given array. + */ +template Add(N) { + signal input element; + signal input in[N]; + signal output out[N]; + + for(var i = 0; i < N; i++) { + out[i] <== in[i] + element; + } +} diff --git a/packages/lib/gpcircuits/circuits/list-membership.circom b/packages/lib/gpcircuits/circuits/list-membership.circom index ff566983c0..cafa344d6d 100644 --- a/packages/lib/gpcircuits/circuits/list-membership.circom +++ b/packages/lib/gpcircuits/circuits/list-membership.circom @@ -1,6 +1,8 @@ pragma circom 2.1.8; include "circomlib/circuits/comparators.circom"; +include "circomlib/circuits/gates.circom"; +include "gpc-util.circom"; /** * Module for checking whether a value is a member of a given list. @@ -14,25 +16,51 @@ template ListMembershipModule( // Value to be checked. signal input comparisonValue; - // List of admissible value hashes. Assumed to have repetitions if the actual list length is smaller. + // List of admissible value hashes. Assumed to have repetitions if + // the actual list length is smaller. signal input validValues[MAX_LIST_ELEMENTS]; - // Boolean indicating whether `comparisonValue` lies in `validValues`. + // Boolean indicating whether `comparisonValue` lies in + // `validValues`. signal output isMember; - signal partialProduct[MAX_LIST_ELEMENTS]; + isMember <== IsZero()( + UnnormalisedListNonMembership(MAX_LIST_ELEMENTS, MAX_LIST_ELEMENTS)( + comparisonValue, + validValues + ) + ); +} - for (var i = 0; i < MAX_LIST_ELEMENTS; i++) { - if (i == 0) { - partialProduct[i] <== comparisonValue - validValues[i]; - } else { - partialProduct[i] <== partialProduct[i-1] * (comparisonValue - validValues[i]); - } - } +/** + * Helper template returning a non-zero field element if the given + * value is not an element of the given list restricted to the first + * `NUM_LIST_ELEMENTS` elements and zero otherwise. This is done by + * subtracting the value from all elements of the list and folding it + * by means of field multiplication. + */ +template UnnormalisedListNonMembership(NUM_LIST_ELEMENTS, MAX_LIST_ELEMENTS) { + // Value to be checked. + signal input comparisonValue; + + // List of admissible values. + signal input validValues[MAX_LIST_ELEMENTS]; + + // Indicator of whether the value is not an element of the list of + // admissible values, viz. a non-zero field element iff the value + // is a non-member. + signal output isNotMember; - if (MAX_LIST_ELEMENTS == 0) { - isMember <== 0; + if (NUM_LIST_ELEMENTS == 0) { + isNotMember <== 1; } else { - isMember <== IsZero()(partialProduct[MAX_LIST_ELEMENTS - 1]); + isNotMember <== MultiAND(NUM_LIST_ELEMENTS)( + Add(NUM_LIST_ELEMENTS)( + -comparisonValue, + Take(NUM_LIST_ELEMENTS, MAX_LIST_ELEMENTS)( + validValues + ) + ) + ); } } diff --git a/packages/lib/gpcircuits/circuits/proto-pod-gpc.circom b/packages/lib/gpcircuits/circuits/proto-pod-gpc.circom index fad3f7263f..505b9d02d6 100644 --- a/packages/lib/gpcircuits/circuits/proto-pod-gpc.circom +++ b/packages/lib/gpcircuits/circuits/proto-pod-gpc.circom @@ -11,6 +11,7 @@ include "numeric-value.circom"; include "object.circom"; include "ownerV3.circom"; include "ownerV4.circom"; +include "uniqueness.circom"; include "virtual-entry.circom"; /** @@ -365,6 +366,18 @@ template ProtoPODGPC ( listContainsComparisonValueBits[i] === membershipCheckResult[i]; } + + /* + * 1 UniquenessModule with its inputs & outputs. Currently only + * used for uniqueness of PODs via their content IDs. + */ + + // Boolean indicating whether the uniqueness module is enabled. + signal input uniquenessModuleIsEnabled; + signal podsAreUnique <== UniquenessModule(MAX_OBJECTS)( + objectContentID + ); + uniquenessModuleIsEnabled * (1 - podsAreUnique) === 0; /* * 1 GlobalModule with its inputs & outputs. diff --git a/packages/lib/gpcircuits/circuits/uniqueness.circom b/packages/lib/gpcircuits/circuits/uniqueness.circom new file mode 100644 index 0000000000..254de8c0b7 --- /dev/null +++ b/packages/lib/gpcircuits/circuits/uniqueness.circom @@ -0,0 +1,45 @@ +pragma circom 2.1.8; + +include "circomlib/circuits/gates.circom"; +include "list-membership.circom"; + +/** + * Module for checking whether the values forming a list are all + * unique. + */ +template UniquenessModule( + // Number of list elements + NUM_LIST_ELEMENTS +) { + // List to be checked + signal input values[NUM_LIST_ELEMENTS]; + + // Boolean indicating whether the elements of `values` are all + // unique. + signal output valuesAreUnique; + + // Array of field elements indicating whether the corresponding + // element of `values` is unique, i.e. isUnique[i] = 0 iff + // values[i] is not unique. + signal isUnique[NUM_LIST_ELEMENTS]; + + // Loop through and check whether the ith element of `values` is + // not an element of `values` with that element deleted. + for(var i = 0; i < NUM_LIST_ELEMENTS; i++) { + var j = i+1; + isUnique[i] <== + UnnormalisedListNonMembership(NUM_LIST_ELEMENTS - j, NUM_LIST_ELEMENTS)( + values[i], + Rotl(j, NUM_LIST_ELEMENTS)(values) + ); + } + + // All values are unique iff all elements of `isUnique` are non-zero. + valuesAreUnique <== NOT()( + IsZero()( + MultiAND(NUM_LIST_ELEMENTS)( + isUnique + ) + ) + ); +} diff --git a/packages/lib/gpcircuits/src/circuitParameters.json b/packages/lib/gpcircuits/src/circuitParameters.json index 1392d2ab5c..400d66e447 100644 --- a/packages/lib/gpcircuits/src/circuitParameters.json +++ b/packages/lib/gpcircuits/src/circuitParameters.json @@ -72,7 +72,7 @@ "includeOwnerV3": false, "includeOwnerV4": true }, - 38088 + 38093 ], [ { @@ -87,6 +87,6 @@ "includeOwnerV3": true, "includeOwnerV4": true }, - 40397 + 40402 ] ] \ No newline at end of file diff --git a/packages/lib/gpcircuits/src/index.ts b/packages/lib/gpcircuits/src/index.ts index 2213aaa946..a6461e24f1 100644 --- a/packages/lib/gpcircuits/src/index.ts +++ b/packages/lib/gpcircuits/src/index.ts @@ -11,5 +11,6 @@ export * from "./ownerV4"; export * from "./proto-pod-gpc"; export * from "./tuple"; export * from "./types"; +export * from "./uniqueness"; export * from "./util"; export * from "./virtual-entry"; diff --git a/packages/lib/gpcircuits/src/proto-pod-gpc.ts b/packages/lib/gpcircuits/src/proto-pod-gpc.ts index 4e7c7c7972..0c0356d507 100644 --- a/packages/lib/gpcircuits/src/proto-pod-gpc.ts +++ b/packages/lib/gpcircuits/src/proto-pod-gpc.ts @@ -67,6 +67,9 @@ export type ProtoPODGPCInputs = { /*PUB*/ listContainsComparisonValue: CircuitSignal; /*PUB*/ listValidValues: CircuitSignal /*MAX_LISTS*/[] /*MAX_LIST_ENTRIES*/[]; + // POD uniqueness module (1) + /*PUB*/ uniquenessModuleIsEnabled: CircuitSignal; + // Global module (1) /*PUB*/ globalWatermark: CircuitSignal; }; @@ -107,6 +110,7 @@ export type ProtoPODGPCInputNamesType = [ "listComparisonValueIndex", "listContainsComparisonValue", "listValidValues", + "uniquenessModuleIsEnabled", "globalWatermark" ]; @@ -152,6 +156,9 @@ export type ProtoPODGPCPublicInputs = { /*PUB*/ listContainsComparisonValue: CircuitSignal; /*PUB*/ listValidValues: CircuitSignal /*MAX_LISTS*/[] /*MAX_LIST_ENTRIES*/[]; + // POD uniqueness module (1) + /*PUB*/ uniquenessModuleIsEnabled: CircuitSignal; + // Global module (1) /*PUB*/ globalWatermark: CircuitSignal; }; @@ -179,6 +186,7 @@ export const PROTO_POD_GPC_PUBLIC_INPUT_NAMES = [ "listComparisonValueIndex", "listContainsComparisonValue", "listValidValues", + "uniquenessModuleIsEnabled", "globalWatermark" ]; @@ -447,6 +455,7 @@ export class ProtoPODGPC { listComparisonValueIndex: allInputs.listComparisonValueIndex, listContainsComparisonValue: allInputs.listContainsComparisonValue, listValidValues: allInputs.listValidValues, + uniquenessModuleIsEnabled: allInputs.uniquenessModuleIsEnabled, globalWatermark: allInputs.globalWatermark }; } @@ -520,6 +529,7 @@ export class ProtoPODGPC { ...inputs.listComparisonValueIndex, inputs.listContainsComparisonValue, ...inputs.listValidValues.flat(), + inputs.uniquenessModuleIsEnabled, inputs.globalWatermark ].map(BigInt); } diff --git a/packages/lib/gpcircuits/src/uniqueness.ts b/packages/lib/gpcircuits/src/uniqueness.ts new file mode 100644 index 0000000000..cdcf40058b --- /dev/null +++ b/packages/lib/gpcircuits/src/uniqueness.ts @@ -0,0 +1,15 @@ +import { CircuitSignal } from "./types"; + +export type UniquenessModuleInputs = { + values: CircuitSignal[]; +}; + +export type UniquenessModuleInputNamesType = [ + "values" +]; + +export type UniquenessModuleOutputs = { + valuesAreUnique: CircuitSignal; +}; + +export type UniquenessModuleOutputNamesType = ["valuesAreUnique"]; diff --git a/packages/lib/gpcircuits/src/util.ts b/packages/lib/gpcircuits/src/util.ts index c40608829f..93261ef9ab 100644 --- a/packages/lib/gpcircuits/src/util.ts +++ b/packages/lib/gpcircuits/src/util.ts @@ -1,6 +1,13 @@ +import { + decodePublicKey, + decodeSignature, + podStringHash, + signPODRoot +} from "@pcd/pod"; import { CircomkitConfig } from "circomkit"; import { PathLike } from "fs"; import path from "path"; +import { ObjectModuleInputs } from "./object"; import { CircuitSignal } from "./types"; /** @@ -193,3 +200,43 @@ export function zeroResidueMod(x: CircuitSignal, n: bigint): bigint { return (n + (xAsBigint % n)) % n; } + +/** + * Creates dummy signals for unused object slots in ProtoPODGPC via dummy + * content IDs in the form of POD string hashes of the message `unused POD ${n}` + * as well as corresponding signatures. + */ +export function dummyObjectSignals( + numObjects: number +): ObjectModuleInputs /*numObjects*/[] { + // Dummy private key. + const privateKey = + "0000000000000000000000000000000000000000000000000000000000000000"; + + const messageHashes = Array(numObjects) + .fill(undefined) + .map((_, i) => `unused POD ${i}`) + .map(podStringHash); + + const encodedSignatures = messageHashes.map((msgHash) => + signPODRoot(msgHash, privateKey) + ); + + const publicKeys = encodedSignatures.map((encSig) => + decodePublicKey(encSig.publicKey) + ); + const signatures = encodedSignatures.map((encSig) => + decodeSignature(encSig.signature) + ); + + return messageHashes.map((msgHash, i) => { + return { + contentID: msgHash, + signerPubkeyAx: publicKeys[i][0], + signerPubkeyAy: publicKeys[i][1], + signatureR8x: signatures[i].R8[0], + signatureR8y: signatures[i].R8[1], + signatureS: signatures[i].S + }; + }); +} diff --git a/packages/lib/gpcircuits/test/proto-pod-gpc.spec.ts b/packages/lib/gpcircuits/test/proto-pod-gpc.spec.ts index fca8d2209b..e442de4249 100644 --- a/packages/lib/gpcircuits/test/proto-pod-gpc.spec.ts +++ b/packages/lib/gpcircuits/test/proto-pod-gpc.spec.ts @@ -27,6 +27,7 @@ import { ProtoPODGPCOutputNamesType, ProtoPODGPCOutputs, array2Bits, + dummyObjectSignals, extendedSignalArray, gpcArtifactPaths, maxTupleArity, @@ -406,6 +407,9 @@ const sampleInput: ProtoPODGPCInputs = { ] ], + // POD uniqueness module (1) + /*PUB*/ uniquenessModuleIsEnabled: 1n, + // Global module (1) /*PUB*/ globalWatermark: 1337n }; @@ -533,25 +537,43 @@ function makeTestSignals( } // Fill in ObjectModule inputs. - const sigObjectContentID: bigint[] = []; + const sigObjectContentID: CircuitSignal[] = []; const sigObjectSignerPubkeyAx: CircuitSignal[] = []; const sigObjectSignerPubkeyAy: CircuitSignal[] = []; - const sigObjectSignatureR8x = []; - const sigObjectSignatureR8y = []; - const sigObjectSignatureS = []; - for (let objectIndex = 0; objectIndex < params.maxObjects; objectIndex++) { - // Unused objects get filled in with the same info as object 0. - const isObjectEnabled = objectIndex < testObjectsWithKeys.length; - const i = isObjectEnabled ? objectIndex : 0; - - sigObjectContentID.push(pods[i].contentID); - sigObjectSignerPubkeyAx.push(publicKeys[i][0]); - sigObjectSignerPubkeyAy.push(publicKeys[i][1]); - sigObjectSignatureR8x.push(signatures[i].R8[0]); - sigObjectSignatureR8y.push(signatures[i].R8[1]); - sigObjectSignatureS.push(signatures[i].S); + const sigObjectSignatureR8x: CircuitSignal[] = []; + const sigObjectSignatureR8y: CircuitSignal[] = []; + const sigObjectSignatureS: CircuitSignal[] = []; + for ( + let objectIndex = 0; + objectIndex < Math.min(params.maxObjects, testObjectsWithKeys.length); + objectIndex++ + ) { + sigObjectContentID.push(pods[objectIndex].contentID); + sigObjectSignerPubkeyAx.push(publicKeys[objectIndex][0]); + sigObjectSignerPubkeyAy.push(publicKeys[objectIndex][1]); + sigObjectSignatureR8x.push(signatures[objectIndex].R8[0]); + sigObjectSignatureR8y.push(signatures[objectIndex].R8[1]); + sigObjectSignatureS.push(signatures[objectIndex].S); } + // Unused objects get filled in with dummy object signals with valid + // signatures and distinct content IDs. + const numDummyObjects = Math.max( + 0, + params.maxObjects - testObjectsWithKeys.length + ); + const sigObjectPadding = dummyObjectSignals(numDummyObjects); + sigObjectContentID.push(...sigObjectPadding.map((o) => o.contentID)); + sigObjectSignerPubkeyAx.push( + ...sigObjectPadding.map((o) => o.signerPubkeyAx) + ); + sigObjectSignerPubkeyAy.push( + ...sigObjectPadding.map((o) => o.signerPubkeyAy) + ); + sigObjectSignatureR8x.push(...sigObjectPadding.map((o) => o.signatureR8x)); + sigObjectSignatureR8y.push(...sigObjectPadding.map((o) => o.signatureR8y)); + sigObjectSignatureS.push(...sigObjectPadding.map((o) => o.signatureS)); + // Fill in entry module inputs. const sigEntryObjectIndex = []; const sigEntryNameHash = []; @@ -745,6 +767,8 @@ function makeTestSignals( extendedSignalArray(isMember.map(BigInt), params.maxLists, 1n) ); + const uniquenessModuleIsEnabled = 1n; + return { inputs: { objectContentID: sigObjectContentID, @@ -801,6 +825,7 @@ function makeTestSignals( listComparisonValueIndex, listContainsComparisonValue, listValidValues, + uniquenessModuleIsEnabled, globalWatermark: 1337n }, outputs: { diff --git a/packages/lib/gpcircuits/test/uniqueness.spec.ts b/packages/lib/gpcircuits/test/uniqueness.spec.ts new file mode 100644 index 0000000000..276ce2eef0 --- /dev/null +++ b/packages/lib/gpcircuits/test/uniqueness.spec.ts @@ -0,0 +1,56 @@ +import { POD_INT_MAX, POD_INT_MIN, podValueHash } from "@pcd/pod"; +import { WitnessTester } from "circomkit"; +import "mocha"; +import { + UniquenessModuleInputNamesType, + UniquenessModuleOutputNamesType +} from "../src"; +import { circomkit } from "./common"; + +const circuit = async ( + numElements: number +): Promise< + WitnessTester +> => + circomkit.WitnessTester("UniquenessModule", { + file: "uniqueness", + template: "UniquenessModule", + params: [numElements] + }); + +describe("uniqueness.UniquenessModule should work", async function () { + it("should return 1 for unique list elements", async () => { + const lists = [ + [1n], + [1n, 2n], + [47n, 27n, 11n], + [898n, 8283n, 16n], + [1923n, 2736n, 192n, 837n] + ]; + + for (const list of lists) { + await circuit(list.length).then((c) => + c.expectPass({ values: list }, { valuesAreUnique: 1n }) + ); + } + }); + + it("should return 0 for unique list elements", async () => { + const lists = [ + [1n, 1n], + [47n, 47n, 11n], + [47n, 11n, 47n], + [11n, 47n, 47n], + [1923n, 1923n, 192n, 837n], + [192n, 837n, 1923n, 1923n], + [1923n, 837n, 1923n, 192n], + [837n, 1923n, 192n, 1923n] + ]; + + for (const list of lists) { + await circuit(list.length).then((c) => + c.expectPass({ values: list }, { valuesAreUnique: 0n }) + ); + } + }); +}); From 7ddf10f07b94969ebd3a980a5adb36e24956c6f5 Mon Sep 17 00:00:00 2001 From: Ahmad Date: Thu, 12 Sep 2024 22:40:49 +1000 Subject: [PATCH 02/11] Add uniqueness module --- packages/lib/gpc/src/gpcChecks.ts | 25 +++++++++ packages/lib/gpc/src/gpcCompile.ts | 25 +++++++-- packages/lib/gpc/src/gpcTypes.ts | 6 ++ packages/lib/gpc/src/gpcUtil.ts | 12 ++++ packages/lib/gpc/test/gpcChecks.spec.ts | 53 +++++++++++++++++- packages/lib/gpc/test/gpcCompile.spec.ts | 16 ++++++ packages/lib/gpc/test/gpcUtil.spec.ts | 15 +++++ packages/lib/gpcircuits/circuits.json | 6 ++ .../lib/gpcircuits/circuits/gpc-util.circom | 38 +++++++++++++ .../circuits/list-membership.circom | 54 +++++++++++++----- .../gpcircuits/circuits/proto-pod-gpc.circom | 13 +++++ .../lib/gpcircuits/circuits/uniqueness.circom | 45 +++++++++++++++ .../lib/gpcircuits/src/circuitParameters.json | 4 +- packages/lib/gpcircuits/src/index.ts | 1 + packages/lib/gpcircuits/src/proto-pod-gpc.ts | 10 ++++ packages/lib/gpcircuits/src/uniqueness.ts | 15 +++++ packages/lib/gpcircuits/src/util.ts | 47 ++++++++++++++++ .../lib/gpcircuits/test/proto-pod-gpc.spec.ts | 55 +++++++++++++----- .../lib/gpcircuits/test/uniqueness.spec.ts | 56 +++++++++++++++++++ 19 files changed, 461 insertions(+), 35 deletions(-) create mode 100644 packages/lib/gpcircuits/circuits/uniqueness.circom create mode 100644 packages/lib/gpcircuits/src/uniqueness.ts create mode 100644 packages/lib/gpcircuits/test/uniqueness.spec.ts diff --git a/packages/lib/gpc/src/gpcChecks.ts b/packages/lib/gpc/src/gpcChecks.ts index 6f8486b21f..7717c4f5d3 100644 --- a/packages/lib/gpc/src/gpcChecks.ts +++ b/packages/lib/gpc/src/gpcChecks.ts @@ -131,6 +131,10 @@ export function checkProofConfig(proofConfig: GPCProofConfig): GPCRequirements { includeOwnerV4 ||= hasOwnerV4; } + if (proofConfig.uniquePODs !== undefined) { + requireType("uniquePODs", proofConfig.uniquePODs, "boolean"); + } + if (proofConfig.tuples !== undefined) { checkProofTupleConfig(proofConfig); } @@ -618,9 +622,30 @@ export function checkProofInputsForConfig( throw new Error("Nullifier requires an entry containing owner ID."); } + checkProofPODUniquenessInputsForConfig(proofConfig, proofInputs); + checkProofListMembershipInputsForConfig(proofConfig, proofInputs); } +export function checkProofPODUniquenessInputsForConfig( + proofConfig: { uniquePODs?: boolean }, + proofInputs: { pods: Record } +) { + if (proofConfig.uniquePODs) { + const contentIDs = Object.values(proofInputs.pods).map( + (pod) => pod.contentID + ); + const uniqueContentIDs = _.uniq(contentIDs); + const podsAreUnique = _.isEqual(contentIDs, uniqueContentIDs); + + if (!podsAreUnique) { + throw new Error( + "Proof configuration specifies that the PODs should be unique, but they aren't." + ); + } + } +} + export function checkProofBoundsCheckInputsForConfig( entryName: PODEntryIdentifier, entryConfig: GPCProofEntryConfig, diff --git a/packages/lib/gpc/src/gpcCompile.ts b/packages/lib/gpc/src/gpcCompile.ts index 28687b7918..d8183e2a69 100644 --- a/packages/lib/gpc/src/gpcCompile.ts +++ b/packages/lib/gpc/src/gpcCompile.ts @@ -8,6 +8,7 @@ import { ProtoPODGPCPublicInputs, array2Bits, computeTupleIndices, + dummyObjectSignals, extendedSignalArray, hashTuple, padArray, @@ -32,6 +33,7 @@ import { import _ from "lodash"; import { GPCBoundConfig, + GPCProofConfig, GPCProofEntryConfig, GPCProofInputs, GPCProofObjectConfig, @@ -377,6 +379,9 @@ export function compileProofConfig( circuitDesc.maxListElements ); + // Create subset of inputs for POD uniqueness module. + const circuitUniquenessInputs = compileProofPODUniqueness(proofConfig); + // Create other global inputs. const circuitGlobalInputs = compileProofGlobal(proofInputs); @@ -402,6 +407,7 @@ export function compileProofConfig( ...circuitNumericValueInputs, ...circuitMultiTupleInputs, ...circuitListMembershipInputs, + ...circuitUniquenessInputs, ...circuitGlobalInputs }; } @@ -673,10 +679,9 @@ function combineProofObjects( objectSignatureS: CircuitSignal /*MAX_OBJECTS*/[]; } { // Object modules don't have an explicit disabled state, so spare object - // slots get filled in with copies of Object 0. - for (let objIndex = allObjInputs.length; objIndex < maxObjects; objIndex++) { - allObjInputs.push({ ...allObjInputs[0] }); - } + // slots get filled with dummy objects with distinct content IDs. + const objPadding = dummyObjectSignals(maxObjects - allObjInputs.length); + allObjInputs.push(...objPadding); return { objectContentID: allObjInputs.map((o) => o.contentID), @@ -721,6 +726,14 @@ function compileProofEntry( }; } +export function compileProofPODUniqueness(proofConfig: { + uniquePODs?: boolean; +}): { + uniquenessModuleIsEnabled: CircuitSignal; +} { + return { uniquenessModuleIsEnabled: BigInt(proofConfig.uniquePODs ?? false) }; +} + function compileProofVirtualEntry< ObjInput extends POD | GPCRevealedObjectClaims >( @@ -1111,6 +1124,9 @@ export function compileVerifyConfig( circuitDesc.maxListElements ); + // Create subset of inputs for POD uniqueness module. + const circuitUniquenessInputs = compileProofPODUniqueness(verifyConfig); + // Create other global inputs. Logic shared with compileProofConfig, // since all the signals involved are public. const circuitGlobalInputs = compileProofGlobal(verifyRevealed); @@ -1144,6 +1160,7 @@ export function compileVerifyConfig( ...circuitNumericValueInputs, ...circuitMultiTupleInputs, ...circuitListMembershipInputs, + ...circuitUniquenessInputs, ...circuitGlobalInputs }, circuitOutputs: { diff --git a/packages/lib/gpc/src/gpcTypes.ts b/packages/lib/gpc/src/gpcTypes.ts index 874e367daa..aba352c06a 100644 --- a/packages/lib/gpc/src/gpcTypes.ts +++ b/packages/lib/gpc/src/gpcTypes.ts @@ -320,6 +320,12 @@ export type GPCProofConfig = { */ pods: Record; + /** + * Indicates whether the configured PODs are unique. If this is true, it + * enables the POD uniqueness module on the circuit level. + */ + uniquePODs?: boolean; + /** * Defines named tuples of POD entries. The tuples' names lie in a separate * namespace and are internally prefixed with '$tuple.'. These tuples must be diff --git a/packages/lib/gpc/src/gpcUtil.ts b/packages/lib/gpc/src/gpcUtil.ts index 5683dd54b0..c6bd2f26dd 100644 --- a/packages/lib/gpc/src/gpcUtil.ts +++ b/packages/lib/gpc/src/gpcUtil.ts @@ -67,6 +67,11 @@ export function canonicalizeConfig( canonicalPODs[objName] = canonicalizeObjectConfig(objectConfig); } + // Omit POD uniqueness field if not `true`. + const canonicalizedPODUniquenessConfig = canonicalizePODUniquenessConfig( + proofConfig.uniquePODs + ); + // Force tuples and their membership lists to be sorted by name const tupleRecord = canonicalizeTupleConfig(proofConfig.tuples ?? {}); @@ -75,6 +80,7 @@ export function canonicalizeConfig( return { circuitIdentifier: circuitIdentifier, pods: canonicalPODs, + ...canonicalizedPODUniquenessConfig, ...(proofConfig.tuples !== undefined ? { tuples: tupleRecord } : {}) }; } @@ -118,6 +124,12 @@ function canonicalizeObjectConfig( }; } +export function canonicalizePODUniquenessConfig( + podUniquenessConfig: boolean | undefined +): { uniquePODs?: boolean } { + return podUniquenessConfig ? { uniquePODs: true } : {}; +} + export function canonicalizeVirtualEntryConfig( virtualEntryConfig: GPCProofEntryConfigCommon, defaultIsRevealed: boolean diff --git a/packages/lib/gpc/test/gpcChecks.spec.ts b/packages/lib/gpc/test/gpcChecks.spec.ts index 7dc880b077..abc14451dd 100644 --- a/packages/lib/gpc/test/gpcChecks.spec.ts +++ b/packages/lib/gpc/test/gpcChecks.spec.ts @@ -1,5 +1,7 @@ import { + POD, PODEdDSAPublicKeyValue, + PODName, PODValue, POD_INT_MAX, POD_INT_MIN @@ -10,8 +12,10 @@ import { GPCProofEntryBoundsCheckConfig, GPCProofEntryConfig } from "../src"; import { checkProofBoundsCheckInputsForConfig, checkProofEntryBoundsCheckConfig, - checkProofEntryConfig + checkProofEntryConfig, + checkProofPODUniquenessInputsForConfig } from "../src/gpcChecks"; +import { privateKey, sampleEntries, sampleEntries2 } from "./common"; describe("Proof entry config check should work", () => { it("should pass for a minimal entry configuration", () => { @@ -244,4 +248,51 @@ describe("Proof config check against input for bounds checks should work", () => } }); }); + +describe("Proof config check against input for POD uniqueness should work", () => { + const pod1 = POD.sign(sampleEntries, privateKey); + const pod2 = POD.sign(sampleEntries2, privateKey); + const pod3 = POD.sign({ A: sampleEntries.A }, privateKey); + const pod4 = POD.sign({ E: sampleEntries.E }, privateKey); + + const uniquePODInputs = [ + { pod1 }, + { pod1, pod2 }, + { pod1, pod2, pod3 }, + { pod1, pod2, pod3, pod4 } + ] as Record[]; + + const nonuniquePODInputs = [ + { pod1, pod2: pod1 }, + { pod1, pod2, pod3: pod1 }, + { pod2, pod1, pod3: pod1 }, + { pod1, pod2, pod3, pod4: pod1 }, + { pod2, pod3, pod1, pod4: pod1 } + ] as Record[]; + + it("should pass if disabled", () => { + for (const pods of uniquePODInputs.concat(nonuniquePODInputs)) { + for (const config of [{}, { uniquePODs: false }]) { + expect(() => checkProofPODUniquenessInputsForConfig(config, { pods })) + .to.not.throw; + } + } + }); + + it("should pass for unique POD inputs", () => { + for (const pods of uniquePODInputs) { + expect(() => + checkProofPODUniquenessInputsForConfig({ uniquePODs: true }, { pods }) + ).to.not.throw; + } + }); + + it("should throw for non-unique POD inputs if enabled", () => { + for (const pods of nonuniquePODInputs) { + expect(() => + checkProofPODUniquenessInputsForConfig({ uniquePODs: true }, { pods }) + ).to.throw; + } + }); +}); // TODO(POD-P3): More tests diff --git a/packages/lib/gpc/test/gpcCompile.spec.ts b/packages/lib/gpc/test/gpcCompile.spec.ts index 766cccba55..d5f6f9e1e3 100644 --- a/packages/lib/gpc/test/gpcCompile.spec.ts +++ b/packages/lib/gpc/test/gpcCompile.spec.ts @@ -10,6 +10,7 @@ import { poseidon2 } from "poseidon-lite/poseidon2"; import { compileProofOwnerV3, compileProofOwnerV4, + compileProofPODUniqueness, compileVerifyOwnerV3, compileVerifyOwnerV4 } from "../src/gpcCompile"; @@ -233,4 +234,19 @@ describe("Semaphore V4 owner module compilation for verification should work", ( } }); }); + +describe("POD uniqueness module compilation for proving and verification should work", () => { + it("should work as expected for a proof configuration with POD uniqueness enabled", () => { + expect(compileProofPODUniqueness({ uniquePODs: true })).to.deep.equal({ + uniquenessModuleIsEnabled: 1n + }); + }); + it("should work as expected for a proof configuration with POD uniqueness disabled", () => { + for (const config of [{}, { uniquePODs: false }]) { + expect(compileProofPODUniqueness(config)).to.deep.equal({ + uniquenessModuleIsEnabled: 0n + }); + } + }); +}); // TODO(POD-P3): More tests diff --git a/packages/lib/gpc/test/gpcUtil.spec.ts b/packages/lib/gpc/test/gpcUtil.spec.ts index a6f37f3297..5c045d58de 100644 --- a/packages/lib/gpc/test/gpcUtil.spec.ts +++ b/packages/lib/gpc/test/gpcUtil.spec.ts @@ -11,6 +11,7 @@ import { boundsCheckConfigFromProofConfig, canonicalizeBoundsCheckConfig, canonicalizeEntryConfig, + canonicalizePODUniquenessConfig, canonicalizeVirtualEntryConfig } from "../src/gpcUtil"; @@ -414,4 +415,18 @@ describe("Bounds check configuration derivation works as expected", () => { }); }); }); + +describe("POD uniqueness config canonicalization should work", () => { + it("should work as expected if omitted", () => { + expect(canonicalizePODUniquenessConfig(undefined)).to.deep.equal({}); + }); + it("should work as expected if enabled", () => { + expect(canonicalizePODUniquenessConfig(true)).to.deep.equal({ + uniquePODs: true + }); + }); + it("should work as expected if explicitly disabled", () => { + expect(canonicalizePODUniquenessConfig(false)).to.deep.equal({}); + }); +}); // TODO(POD-P3): More tests diff --git a/packages/lib/gpcircuits/circuits.json b/packages/lib/gpcircuits/circuits.json index 4858392674..ee8e1116d6 100644 --- a/packages/lib/gpcircuits/circuits.json +++ b/packages/lib/gpcircuits/circuits.json @@ -34,6 +34,7 @@ "listComparisonValueIndex", "listContainsComparisonValue", "listValidValues", + "uniquenessModuleIsEnabled", "globalWatermark" ] }, @@ -72,6 +73,7 @@ "listComparisonValueIndex", "listContainsComparisonValue", "listValidValues", + "uniquenessModuleIsEnabled", "globalWatermark" ] }, @@ -110,6 +112,7 @@ "listComparisonValueIndex", "listContainsComparisonValue", "listValidValues", + "uniquenessModuleIsEnabled", "globalWatermark" ] }, @@ -148,6 +151,7 @@ "listComparisonValueIndex", "listContainsComparisonValue", "listValidValues", + "uniquenessModuleIsEnabled", "globalWatermark" ] }, @@ -186,6 +190,7 @@ "listComparisonValueIndex", "listContainsComparisonValue", "listValidValues", + "uniquenessModuleIsEnabled", "globalWatermark" ] }, @@ -224,6 +229,7 @@ "listComparisonValueIndex", "listContainsComparisonValue", "listValidValues", + "uniquenessModuleIsEnabled", "globalWatermark" ] } diff --git a/packages/lib/gpcircuits/circuits/gpc-util.circom b/packages/lib/gpcircuits/circuits/gpc-util.circom index a57a1de1c7..4bc01e1e10 100644 --- a/packages/lib/gpcircuits/circuits/gpc-util.circom +++ b/packages/lib/gpcircuits/circuits/gpc-util.circom @@ -82,3 +82,41 @@ template MaybeInputSelector (N) { // `selectedIndex` must have been in [0,...,N-1] or -1. (1 - success)*(selectedIndex + 1) === 0; } + +/** + * Left-rotates elements of a given array by I positions. + */ +template Rotl(I,N) { + signal input in[N]; + signal output out[N]; + + for(var i = 0; i < N; i++) { + out[i] <== in[(i + I)%N]; + } +} + +/** + * Takes the first I elements of a given array and returns the array + * containing those elements. + */ +template Take(I,N) { + signal input in[N]; + signal output out[I]; + + for (var i = 0; i < I; i++) { + out[i] <== in[i]; + } +} + +/** + * Adds a field element to all elements of a given array. + */ +template Add(N) { + signal input element; + signal input in[N]; + signal output out[N]; + + for(var i = 0; i < N; i++) { + out[i] <== in[i] + element; + } +} diff --git a/packages/lib/gpcircuits/circuits/list-membership.circom b/packages/lib/gpcircuits/circuits/list-membership.circom index ff566983c0..cafa344d6d 100644 --- a/packages/lib/gpcircuits/circuits/list-membership.circom +++ b/packages/lib/gpcircuits/circuits/list-membership.circom @@ -1,6 +1,8 @@ pragma circom 2.1.8; include "circomlib/circuits/comparators.circom"; +include "circomlib/circuits/gates.circom"; +include "gpc-util.circom"; /** * Module for checking whether a value is a member of a given list. @@ -14,25 +16,51 @@ template ListMembershipModule( // Value to be checked. signal input comparisonValue; - // List of admissible value hashes. Assumed to have repetitions if the actual list length is smaller. + // List of admissible value hashes. Assumed to have repetitions if + // the actual list length is smaller. signal input validValues[MAX_LIST_ELEMENTS]; - // Boolean indicating whether `comparisonValue` lies in `validValues`. + // Boolean indicating whether `comparisonValue` lies in + // `validValues`. signal output isMember; - signal partialProduct[MAX_LIST_ELEMENTS]; + isMember <== IsZero()( + UnnormalisedListNonMembership(MAX_LIST_ELEMENTS, MAX_LIST_ELEMENTS)( + comparisonValue, + validValues + ) + ); +} - for (var i = 0; i < MAX_LIST_ELEMENTS; i++) { - if (i == 0) { - partialProduct[i] <== comparisonValue - validValues[i]; - } else { - partialProduct[i] <== partialProduct[i-1] * (comparisonValue - validValues[i]); - } - } +/** + * Helper template returning a non-zero field element if the given + * value is not an element of the given list restricted to the first + * `NUM_LIST_ELEMENTS` elements and zero otherwise. This is done by + * subtracting the value from all elements of the list and folding it + * by means of field multiplication. + */ +template UnnormalisedListNonMembership(NUM_LIST_ELEMENTS, MAX_LIST_ELEMENTS) { + // Value to be checked. + signal input comparisonValue; + + // List of admissible values. + signal input validValues[MAX_LIST_ELEMENTS]; + + // Indicator of whether the value is not an element of the list of + // admissible values, viz. a non-zero field element iff the value + // is a non-member. + signal output isNotMember; - if (MAX_LIST_ELEMENTS == 0) { - isMember <== 0; + if (NUM_LIST_ELEMENTS == 0) { + isNotMember <== 1; } else { - isMember <== IsZero()(partialProduct[MAX_LIST_ELEMENTS - 1]); + isNotMember <== MultiAND(NUM_LIST_ELEMENTS)( + Add(NUM_LIST_ELEMENTS)( + -comparisonValue, + Take(NUM_LIST_ELEMENTS, MAX_LIST_ELEMENTS)( + validValues + ) + ) + ); } } diff --git a/packages/lib/gpcircuits/circuits/proto-pod-gpc.circom b/packages/lib/gpcircuits/circuits/proto-pod-gpc.circom index fad3f7263f..505b9d02d6 100644 --- a/packages/lib/gpcircuits/circuits/proto-pod-gpc.circom +++ b/packages/lib/gpcircuits/circuits/proto-pod-gpc.circom @@ -11,6 +11,7 @@ include "numeric-value.circom"; include "object.circom"; include "ownerV3.circom"; include "ownerV4.circom"; +include "uniqueness.circom"; include "virtual-entry.circom"; /** @@ -365,6 +366,18 @@ template ProtoPODGPC ( listContainsComparisonValueBits[i] === membershipCheckResult[i]; } + + /* + * 1 UniquenessModule with its inputs & outputs. Currently only + * used for uniqueness of PODs via their content IDs. + */ + + // Boolean indicating whether the uniqueness module is enabled. + signal input uniquenessModuleIsEnabled; + signal podsAreUnique <== UniquenessModule(MAX_OBJECTS)( + objectContentID + ); + uniquenessModuleIsEnabled * (1 - podsAreUnique) === 0; /* * 1 GlobalModule with its inputs & outputs. diff --git a/packages/lib/gpcircuits/circuits/uniqueness.circom b/packages/lib/gpcircuits/circuits/uniqueness.circom new file mode 100644 index 0000000000..9d85c1b858 --- /dev/null +++ b/packages/lib/gpcircuits/circuits/uniqueness.circom @@ -0,0 +1,45 @@ +pragma circom 2.1.8; + +include "circomlib/circuits/gates.circom"; +include "list-membership.circom"; + +/** + * Module for checking whether the values forming a list are all + * unique. + */ +template UniquenessModule( + // Number of list elements + NUM_LIST_ELEMENTS +) { + // List to be checked + signal input values[NUM_LIST_ELEMENTS]; + + // Boolean indicating whether the elements of `values` are all + // unique. + signal output valuesAreUnique; + + // Array of field elements indicating whether the corresponding + // element of `values` is unique, i.e. isUnique[i] = 0 iff + // values[i] is not unique. + signal isUnique[NUM_LIST_ELEMENTS]; + + // Loop through and check whether the ith element of `values` is + // not an element of `values` with the first i+1 elements removed. + for(var i = 0; i < NUM_LIST_ELEMENTS; i++) { + var j = i+1; + isUnique[i] <== + UnnormalisedListNonMembership(NUM_LIST_ELEMENTS - j, NUM_LIST_ELEMENTS)( + values[i], + Rotl(j, NUM_LIST_ELEMENTS)(values) + ); + } + + // All values are unique iff all elements of `isUnique` are non-zero. + valuesAreUnique <== NOT()( + IsZero()( + MultiAND(NUM_LIST_ELEMENTS)( + isUnique + ) + ) + ); +} diff --git a/packages/lib/gpcircuits/src/circuitParameters.json b/packages/lib/gpcircuits/src/circuitParameters.json index 1392d2ab5c..400d66e447 100644 --- a/packages/lib/gpcircuits/src/circuitParameters.json +++ b/packages/lib/gpcircuits/src/circuitParameters.json @@ -72,7 +72,7 @@ "includeOwnerV3": false, "includeOwnerV4": true }, - 38088 + 38093 ], [ { @@ -87,6 +87,6 @@ "includeOwnerV3": true, "includeOwnerV4": true }, - 40397 + 40402 ] ] \ No newline at end of file diff --git a/packages/lib/gpcircuits/src/index.ts b/packages/lib/gpcircuits/src/index.ts index 2213aaa946..a6461e24f1 100644 --- a/packages/lib/gpcircuits/src/index.ts +++ b/packages/lib/gpcircuits/src/index.ts @@ -11,5 +11,6 @@ export * from "./ownerV4"; export * from "./proto-pod-gpc"; export * from "./tuple"; export * from "./types"; +export * from "./uniqueness"; export * from "./util"; export * from "./virtual-entry"; diff --git a/packages/lib/gpcircuits/src/proto-pod-gpc.ts b/packages/lib/gpcircuits/src/proto-pod-gpc.ts index 4e7c7c7972..0c0356d507 100644 --- a/packages/lib/gpcircuits/src/proto-pod-gpc.ts +++ b/packages/lib/gpcircuits/src/proto-pod-gpc.ts @@ -67,6 +67,9 @@ export type ProtoPODGPCInputs = { /*PUB*/ listContainsComparisonValue: CircuitSignal; /*PUB*/ listValidValues: CircuitSignal /*MAX_LISTS*/[] /*MAX_LIST_ENTRIES*/[]; + // POD uniqueness module (1) + /*PUB*/ uniquenessModuleIsEnabled: CircuitSignal; + // Global module (1) /*PUB*/ globalWatermark: CircuitSignal; }; @@ -107,6 +110,7 @@ export type ProtoPODGPCInputNamesType = [ "listComparisonValueIndex", "listContainsComparisonValue", "listValidValues", + "uniquenessModuleIsEnabled", "globalWatermark" ]; @@ -152,6 +156,9 @@ export type ProtoPODGPCPublicInputs = { /*PUB*/ listContainsComparisonValue: CircuitSignal; /*PUB*/ listValidValues: CircuitSignal /*MAX_LISTS*/[] /*MAX_LIST_ENTRIES*/[]; + // POD uniqueness module (1) + /*PUB*/ uniquenessModuleIsEnabled: CircuitSignal; + // Global module (1) /*PUB*/ globalWatermark: CircuitSignal; }; @@ -179,6 +186,7 @@ export const PROTO_POD_GPC_PUBLIC_INPUT_NAMES = [ "listComparisonValueIndex", "listContainsComparisonValue", "listValidValues", + "uniquenessModuleIsEnabled", "globalWatermark" ]; @@ -447,6 +455,7 @@ export class ProtoPODGPC { listComparisonValueIndex: allInputs.listComparisonValueIndex, listContainsComparisonValue: allInputs.listContainsComparisonValue, listValidValues: allInputs.listValidValues, + uniquenessModuleIsEnabled: allInputs.uniquenessModuleIsEnabled, globalWatermark: allInputs.globalWatermark }; } @@ -520,6 +529,7 @@ export class ProtoPODGPC { ...inputs.listComparisonValueIndex, inputs.listContainsComparisonValue, ...inputs.listValidValues.flat(), + inputs.uniquenessModuleIsEnabled, inputs.globalWatermark ].map(BigInt); } diff --git a/packages/lib/gpcircuits/src/uniqueness.ts b/packages/lib/gpcircuits/src/uniqueness.ts new file mode 100644 index 0000000000..cdcf40058b --- /dev/null +++ b/packages/lib/gpcircuits/src/uniqueness.ts @@ -0,0 +1,15 @@ +import { CircuitSignal } from "./types"; + +export type UniquenessModuleInputs = { + values: CircuitSignal[]; +}; + +export type UniquenessModuleInputNamesType = [ + "values" +]; + +export type UniquenessModuleOutputs = { + valuesAreUnique: CircuitSignal; +}; + +export type UniquenessModuleOutputNamesType = ["valuesAreUnique"]; diff --git a/packages/lib/gpcircuits/src/util.ts b/packages/lib/gpcircuits/src/util.ts index c40608829f..93261ef9ab 100644 --- a/packages/lib/gpcircuits/src/util.ts +++ b/packages/lib/gpcircuits/src/util.ts @@ -1,6 +1,13 @@ +import { + decodePublicKey, + decodeSignature, + podStringHash, + signPODRoot +} from "@pcd/pod"; import { CircomkitConfig } from "circomkit"; import { PathLike } from "fs"; import path from "path"; +import { ObjectModuleInputs } from "./object"; import { CircuitSignal } from "./types"; /** @@ -193,3 +200,43 @@ export function zeroResidueMod(x: CircuitSignal, n: bigint): bigint { return (n + (xAsBigint % n)) % n; } + +/** + * Creates dummy signals for unused object slots in ProtoPODGPC via dummy + * content IDs in the form of POD string hashes of the message `unused POD ${n}` + * as well as corresponding signatures. + */ +export function dummyObjectSignals( + numObjects: number +): ObjectModuleInputs /*numObjects*/[] { + // Dummy private key. + const privateKey = + "0000000000000000000000000000000000000000000000000000000000000000"; + + const messageHashes = Array(numObjects) + .fill(undefined) + .map((_, i) => `unused POD ${i}`) + .map(podStringHash); + + const encodedSignatures = messageHashes.map((msgHash) => + signPODRoot(msgHash, privateKey) + ); + + const publicKeys = encodedSignatures.map((encSig) => + decodePublicKey(encSig.publicKey) + ); + const signatures = encodedSignatures.map((encSig) => + decodeSignature(encSig.signature) + ); + + return messageHashes.map((msgHash, i) => { + return { + contentID: msgHash, + signerPubkeyAx: publicKeys[i][0], + signerPubkeyAy: publicKeys[i][1], + signatureR8x: signatures[i].R8[0], + signatureR8y: signatures[i].R8[1], + signatureS: signatures[i].S + }; + }); +} diff --git a/packages/lib/gpcircuits/test/proto-pod-gpc.spec.ts b/packages/lib/gpcircuits/test/proto-pod-gpc.spec.ts index fca8d2209b..e442de4249 100644 --- a/packages/lib/gpcircuits/test/proto-pod-gpc.spec.ts +++ b/packages/lib/gpcircuits/test/proto-pod-gpc.spec.ts @@ -27,6 +27,7 @@ import { ProtoPODGPCOutputNamesType, ProtoPODGPCOutputs, array2Bits, + dummyObjectSignals, extendedSignalArray, gpcArtifactPaths, maxTupleArity, @@ -406,6 +407,9 @@ const sampleInput: ProtoPODGPCInputs = { ] ], + // POD uniqueness module (1) + /*PUB*/ uniquenessModuleIsEnabled: 1n, + // Global module (1) /*PUB*/ globalWatermark: 1337n }; @@ -533,25 +537,43 @@ function makeTestSignals( } // Fill in ObjectModule inputs. - const sigObjectContentID: bigint[] = []; + const sigObjectContentID: CircuitSignal[] = []; const sigObjectSignerPubkeyAx: CircuitSignal[] = []; const sigObjectSignerPubkeyAy: CircuitSignal[] = []; - const sigObjectSignatureR8x = []; - const sigObjectSignatureR8y = []; - const sigObjectSignatureS = []; - for (let objectIndex = 0; objectIndex < params.maxObjects; objectIndex++) { - // Unused objects get filled in with the same info as object 0. - const isObjectEnabled = objectIndex < testObjectsWithKeys.length; - const i = isObjectEnabled ? objectIndex : 0; - - sigObjectContentID.push(pods[i].contentID); - sigObjectSignerPubkeyAx.push(publicKeys[i][0]); - sigObjectSignerPubkeyAy.push(publicKeys[i][1]); - sigObjectSignatureR8x.push(signatures[i].R8[0]); - sigObjectSignatureR8y.push(signatures[i].R8[1]); - sigObjectSignatureS.push(signatures[i].S); + const sigObjectSignatureR8x: CircuitSignal[] = []; + const sigObjectSignatureR8y: CircuitSignal[] = []; + const sigObjectSignatureS: CircuitSignal[] = []; + for ( + let objectIndex = 0; + objectIndex < Math.min(params.maxObjects, testObjectsWithKeys.length); + objectIndex++ + ) { + sigObjectContentID.push(pods[objectIndex].contentID); + sigObjectSignerPubkeyAx.push(publicKeys[objectIndex][0]); + sigObjectSignerPubkeyAy.push(publicKeys[objectIndex][1]); + sigObjectSignatureR8x.push(signatures[objectIndex].R8[0]); + sigObjectSignatureR8y.push(signatures[objectIndex].R8[1]); + sigObjectSignatureS.push(signatures[objectIndex].S); } + // Unused objects get filled in with dummy object signals with valid + // signatures and distinct content IDs. + const numDummyObjects = Math.max( + 0, + params.maxObjects - testObjectsWithKeys.length + ); + const sigObjectPadding = dummyObjectSignals(numDummyObjects); + sigObjectContentID.push(...sigObjectPadding.map((o) => o.contentID)); + sigObjectSignerPubkeyAx.push( + ...sigObjectPadding.map((o) => o.signerPubkeyAx) + ); + sigObjectSignerPubkeyAy.push( + ...sigObjectPadding.map((o) => o.signerPubkeyAy) + ); + sigObjectSignatureR8x.push(...sigObjectPadding.map((o) => o.signatureR8x)); + sigObjectSignatureR8y.push(...sigObjectPadding.map((o) => o.signatureR8y)); + sigObjectSignatureS.push(...sigObjectPadding.map((o) => o.signatureS)); + // Fill in entry module inputs. const sigEntryObjectIndex = []; const sigEntryNameHash = []; @@ -745,6 +767,8 @@ function makeTestSignals( extendedSignalArray(isMember.map(BigInt), params.maxLists, 1n) ); + const uniquenessModuleIsEnabled = 1n; + return { inputs: { objectContentID: sigObjectContentID, @@ -801,6 +825,7 @@ function makeTestSignals( listComparisonValueIndex, listContainsComparisonValue, listValidValues, + uniquenessModuleIsEnabled, globalWatermark: 1337n }, outputs: { diff --git a/packages/lib/gpcircuits/test/uniqueness.spec.ts b/packages/lib/gpcircuits/test/uniqueness.spec.ts new file mode 100644 index 0000000000..276ce2eef0 --- /dev/null +++ b/packages/lib/gpcircuits/test/uniqueness.spec.ts @@ -0,0 +1,56 @@ +import { POD_INT_MAX, POD_INT_MIN, podValueHash } from "@pcd/pod"; +import { WitnessTester } from "circomkit"; +import "mocha"; +import { + UniquenessModuleInputNamesType, + UniquenessModuleOutputNamesType +} from "../src"; +import { circomkit } from "./common"; + +const circuit = async ( + numElements: number +): Promise< + WitnessTester +> => + circomkit.WitnessTester("UniquenessModule", { + file: "uniqueness", + template: "UniquenessModule", + params: [numElements] + }); + +describe("uniqueness.UniquenessModule should work", async function () { + it("should return 1 for unique list elements", async () => { + const lists = [ + [1n], + [1n, 2n], + [47n, 27n, 11n], + [898n, 8283n, 16n], + [1923n, 2736n, 192n, 837n] + ]; + + for (const list of lists) { + await circuit(list.length).then((c) => + c.expectPass({ values: list }, { valuesAreUnique: 1n }) + ); + } + }); + + it("should return 0 for unique list elements", async () => { + const lists = [ + [1n, 1n], + [47n, 47n, 11n], + [47n, 11n, 47n], + [11n, 47n, 47n], + [1923n, 1923n, 192n, 837n], + [192n, 837n, 1923n, 1923n], + [1923n, 837n, 1923n, 192n], + [837n, 1923n, 192n, 1923n] + ]; + + for (const list of lists) { + await circuit(list.length).then((c) => + c.expectPass({ values: list }, { valuesAreUnique: 0n }) + ); + } + }); +}); From b3722e599d774939b8217393f947ba3f3cb26b39 Mon Sep 17 00:00:00 2001 From: Ahmad Date: Mon, 16 Sep 2024 14:17:54 +0200 Subject: [PATCH 03/11] Test --- packages/lib/gpc/test/gpc.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/lib/gpc/test/gpc.spec.ts b/packages/lib/gpc/test/gpc.spec.ts index 0b1832bc89..26214d5f61 100644 --- a/packages/lib/gpc/test/gpc.spec.ts +++ b/packages/lib/gpc/test/gpc.spec.ts @@ -694,7 +694,8 @@ describe("gpc library (Compiled test artifacts) should work", async function () entries: ["pod1.$signerPublicKey", "pod2.$signerPublicKey"], isMemberOf: "admissiblePubKeyPairs" } - } + }, + uniquePODs: true }; const proofInputs: GPCProofInputs = { pods: { pod1, pod2 }, From 24c37dace44fdcc5f9c7b696290f347a3d586e80 Mon Sep 17 00:00:00 2001 From: Ahmad Date: Mon, 16 Sep 2024 14:23:17 +0200 Subject: [PATCH 04/11] Lint --- packages/lib/gpc/src/gpcChecks.ts | 2 +- packages/lib/gpc/src/gpcCompile.ts | 1 - packages/lib/gpcircuits/src/proto-pod-gpc.ts | 4 ++-- packages/lib/gpcircuits/src/uniqueness.ts | 4 +--- packages/lib/gpcircuits/test/uniqueness.spec.ts | 1 - 5 files changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/lib/gpc/src/gpcChecks.ts b/packages/lib/gpc/src/gpcChecks.ts index 7717c4f5d3..f48b08384a 100644 --- a/packages/lib/gpc/src/gpcChecks.ts +++ b/packages/lib/gpc/src/gpcChecks.ts @@ -630,7 +630,7 @@ export function checkProofInputsForConfig( export function checkProofPODUniquenessInputsForConfig( proofConfig: { uniquePODs?: boolean }, proofInputs: { pods: Record } -) { +): void { if (proofConfig.uniquePODs) { const contentIDs = Object.values(proofInputs.pods).map( (pod) => pod.contentID diff --git a/packages/lib/gpc/src/gpcCompile.ts b/packages/lib/gpc/src/gpcCompile.ts index d8183e2a69..9604c2c3d6 100644 --- a/packages/lib/gpc/src/gpcCompile.ts +++ b/packages/lib/gpc/src/gpcCompile.ts @@ -33,7 +33,6 @@ import { import _ from "lodash"; import { GPCBoundConfig, - GPCProofConfig, GPCProofEntryConfig, GPCProofInputs, GPCProofObjectConfig, diff --git a/packages/lib/gpcircuits/src/proto-pod-gpc.ts b/packages/lib/gpcircuits/src/proto-pod-gpc.ts index 0c0356d507..a868d92484 100644 --- a/packages/lib/gpcircuits/src/proto-pod-gpc.ts +++ b/packages/lib/gpcircuits/src/proto-pod-gpc.ts @@ -69,7 +69,7 @@ export type ProtoPODGPCInputs = { // POD uniqueness module (1) /*PUB*/ uniquenessModuleIsEnabled: CircuitSignal; - + // Global module (1) /*PUB*/ globalWatermark: CircuitSignal; }; @@ -158,7 +158,7 @@ export type ProtoPODGPCPublicInputs = { // POD uniqueness module (1) /*PUB*/ uniquenessModuleIsEnabled: CircuitSignal; - + // Global module (1) /*PUB*/ globalWatermark: CircuitSignal; }; diff --git a/packages/lib/gpcircuits/src/uniqueness.ts b/packages/lib/gpcircuits/src/uniqueness.ts index cdcf40058b..8c37eaa760 100644 --- a/packages/lib/gpcircuits/src/uniqueness.ts +++ b/packages/lib/gpcircuits/src/uniqueness.ts @@ -4,9 +4,7 @@ export type UniquenessModuleInputs = { values: CircuitSignal[]; }; -export type UniquenessModuleInputNamesType = [ - "values" -]; +export type UniquenessModuleInputNamesType = ["values"]; export type UniquenessModuleOutputs = { valuesAreUnique: CircuitSignal; diff --git a/packages/lib/gpcircuits/test/uniqueness.spec.ts b/packages/lib/gpcircuits/test/uniqueness.spec.ts index 276ce2eef0..90cb5cfb10 100644 --- a/packages/lib/gpcircuits/test/uniqueness.spec.ts +++ b/packages/lib/gpcircuits/test/uniqueness.spec.ts @@ -1,4 +1,3 @@ -import { POD_INT_MAX, POD_INT_MIN, podValueHash } from "@pcd/pod"; import { WitnessTester } from "circomkit"; import "mocha"; import { From 50e4ffe312468e5c2ad9106c096cc3011c6901fc Mon Sep 17 00:00:00 2001 From: Ahmad Date: Mon, 16 Sep 2024 23:21:12 +0200 Subject: [PATCH 05/11] Tests --- packages/lib/gpc/test/gpc.spec.ts | 61 +++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/packages/lib/gpc/test/gpc.spec.ts b/packages/lib/gpc/test/gpc.spec.ts index 26214d5f61..7602a8f060 100644 --- a/packages/lib/gpc/test/gpc.spec.ts +++ b/packages/lib/gpc/test/gpc.spec.ts @@ -1089,6 +1089,34 @@ describe("gpc library (Compiled test artifacts) should work", async function () "TypeError", "Membership list list1 in input contains an invalid tuple. Tuples must have arity at least 2." ); + + await expectAsyncError( + async () => { + await gpcProve( + { + ...proofConfig, + pods: { + ...proofConfig.pods, + someOtherPodName: { + entries: { ticketID: { isRevealed: false } }, + signerPublicKey: { isRevealed: false } + } + }, + uniquePODs: true + }, + { + ...proofInputs, + pods: { + ...proofInputs.pods, + someOtherPodName: proofInputs.pods.somePodName + } + }, + GPC_TEST_ARTIFACTS_PATH + ); + }, + "Error", + "Proof configuration specifies that the PODs should be unique, but they aren't." + ); }); it("verifying should throw on illegal inputs", async function () { @@ -1322,6 +1350,30 @@ describe("gpc library (Compiled test artifacts) should work", async function () revealedClaims: revealedClaims2 } = await gpcProve(proofConfig2, proofInputs2, GPC_TEST_ARTIFACTS_PATH); + // Proof data with a duplicate POD + const proofConfig3 = { + ...proofConfig, + pods: { + ...proofConfig.pods, + someOtherPodName: { + entries: { ticketID: { isRevealed: false } }, + signerPublicKey: { isRevealed: false } + } + } + }; + const proofInputs3 = { + ...proofInputs, + pods: { + ...proofInputs.pods, + someOtherPodName: proofInputs.pods.somePodName + } + }; + const { + proof: proof3, + boundConfig: boundConfig3, + revealedClaims: revealedClaims3 + } = await gpcProve(proofConfig3, proofInputs3, GPC_TEST_ARTIFACTS_PATH); + // Tamper with proof let isVerified = await gpcVerify( { ...proof, pi_a: [proof.pi_a[0] + 1, proof.pi_a[1]] }, @@ -1420,6 +1472,15 @@ describe("gpc library (Compiled test artifacts) should work", async function () GPC_TEST_ARTIFACTS_PATH ); expect(isVerified).to.be.false; + + // Tamper with POD uniqueness flag + isVerified = await gpcVerify( + proof3, + { ...boundConfig3, uniquePODs: true }, + revealedClaims3, + GPC_TEST_ARTIFACTS_PATH + ); + expect(isVerified).to.be.false; }); }); From cfa1f33e7a147ec03a3aa9011f62a4e187ebfee2 Mon Sep 17 00:00:00 2001 From: Ahmad Date: Tue, 17 Sep 2024 05:12:17 +0200 Subject: [PATCH 06/11] Code review --- packages/lib/gpc/src/gpcChecks.ts | 15 +++++----- packages/lib/gpc/src/gpcCompile.ts | 4 +-- packages/lib/gpc/src/gpcTypes.ts | 4 +-- packages/lib/gpc/test/gpc.spec.ts | 2 +- packages/lib/gpc/test/gpcCompile.spec.ts | 4 +-- packages/lib/gpcircuits/circuits.json | 12 ++++---- .../lib/gpcircuits/circuits/gpc-util.circom | 4 +-- .../circuits/list-membership.circom | 26 ++++++++--------- .../gpcircuits/circuits/proto-pod-gpc.circom | 4 +-- .../lib/gpcircuits/circuits/uniqueness.circom | 16 +++++----- packages/lib/gpcircuits/src/proto-pod-gpc.ts | 12 ++++---- packages/lib/gpcircuits/src/uniqueness.ts | 2 +- packages/lib/gpcircuits/src/util.ts | 5 ++++ .../lib/gpcircuits/test/proto-pod-gpc.spec.ts | 15 +++++----- .../lib/gpcircuits/test/uniqueness.spec.ts | 29 +++++++++++++++++-- 15 files changed, 93 insertions(+), 61 deletions(-) diff --git a/packages/lib/gpc/src/gpcChecks.ts b/packages/lib/gpc/src/gpcChecks.ts index f48b08384a..3819166a37 100644 --- a/packages/lib/gpc/src/gpcChecks.ts +++ b/packages/lib/gpc/src/gpcChecks.ts @@ -23,7 +23,8 @@ import { import { Identity as IdentityV4 } from "@semaphore-protocol/core"; import { Identity } from "@semaphore-protocol/identity"; import JSONBig from "json-bigint"; -import _ from "lodash"; +import isEqual from "lodash/isEqual"; +import uniq from "lodash/uniq"; import { GPCBoundConfig, GPCIdentifier, @@ -635,12 +636,12 @@ export function checkProofPODUniquenessInputsForConfig( const contentIDs = Object.values(proofInputs.pods).map( (pod) => pod.contentID ); - const uniqueContentIDs = _.uniq(contentIDs); - const podsAreUnique = _.isEqual(contentIDs, uniqueContentIDs); + const uniqueContentIDs = uniq(contentIDs); + const podsAreUnique = isEqual(contentIDs, uniqueContentIDs); if (!podsAreUnique) { throw new Error( - "Proof configuration specifies that the PODs should be unique, but they aren't." + "Proof configuration specifies that the PODs should have unique content IDs, but they don't." ); } } @@ -727,7 +728,7 @@ export function checkProofListMembershipInputsForConfig( for (const element of inputList) { const elementWidth = widthOfEntryOrTuple(element); - if (!_.isEqual(elementWidth, comparisonWidth)) { + if (!isEqual(elementWidth, comparisonWidth)) { throw new TypeError( `Membership list ${listIdentifier} in input contains element of width ${elementWidth} while comparison value with identifier ${JSON.stringify( comparisonId @@ -740,7 +741,7 @@ export function checkProofListMembershipInputsForConfig( // hashes as this reflects how the values will be treated in the // circuit. const isComparisonValueInList = inputList.find((element) => - _.isEqual( + isEqual( applyOrMap(podValueHash, element), applyOrMap(podValueHash, comparisonValue) ) @@ -785,7 +786,7 @@ export function checkInputListNamesForConfig( ); const inputListNames = new Set(listNames); - if (!_.isEqual(configListNames, inputListNames)) { + if (!isEqual(configListNames, inputListNames)) { throw new Error( `Config and input list mismatch.` + ` Configuration expects lists ${JSON.stringify( diff --git a/packages/lib/gpc/src/gpcCompile.ts b/packages/lib/gpc/src/gpcCompile.ts index 9604c2c3d6..547aa08c49 100644 --- a/packages/lib/gpc/src/gpcCompile.ts +++ b/packages/lib/gpc/src/gpcCompile.ts @@ -728,9 +728,9 @@ function compileProofEntry( export function compileProofPODUniqueness(proofConfig: { uniquePODs?: boolean; }): { - uniquenessModuleIsEnabled: CircuitSignal; + requireUniqueContentIDs: CircuitSignal; } { - return { uniquenessModuleIsEnabled: BigInt(proofConfig.uniquePODs ?? false) }; + return { requireUniqueContentIDs: BigInt(proofConfig.uniquePODs ?? false) }; } function compileProofVirtualEntry< diff --git a/packages/lib/gpc/src/gpcTypes.ts b/packages/lib/gpc/src/gpcTypes.ts index aba352c06a..0744b11238 100644 --- a/packages/lib/gpc/src/gpcTypes.ts +++ b/packages/lib/gpc/src/gpcTypes.ts @@ -321,8 +321,8 @@ export type GPCProofConfig = { pods: Record; /** - * Indicates whether the configured PODs are unique. If this is true, it - * enables the POD uniqueness module on the circuit level. + * Indicates whether the configured PODs should have unique content IDs. + * If this is true, it enables the POD uniqueness module on the circuit level. */ uniquePODs?: boolean; diff --git a/packages/lib/gpc/test/gpc.spec.ts b/packages/lib/gpc/test/gpc.spec.ts index 7602a8f060..8562a61cff 100644 --- a/packages/lib/gpc/test/gpc.spec.ts +++ b/packages/lib/gpc/test/gpc.spec.ts @@ -1115,7 +1115,7 @@ describe("gpc library (Compiled test artifacts) should work", async function () ); }, "Error", - "Proof configuration specifies that the PODs should be unique, but they aren't." + "Proof configuration specifies that the PODs should have unique content IDs, but they don't." ); }); diff --git a/packages/lib/gpc/test/gpcCompile.spec.ts b/packages/lib/gpc/test/gpcCompile.spec.ts index d5f6f9e1e3..729bb7c82d 100644 --- a/packages/lib/gpc/test/gpcCompile.spec.ts +++ b/packages/lib/gpc/test/gpcCompile.spec.ts @@ -238,13 +238,13 @@ describe("Semaphore V4 owner module compilation for verification should work", ( describe("POD uniqueness module compilation for proving and verification should work", () => { it("should work as expected for a proof configuration with POD uniqueness enabled", () => { expect(compileProofPODUniqueness({ uniquePODs: true })).to.deep.equal({ - uniquenessModuleIsEnabled: 1n + requireUniqueContentIDs: 1n }); }); it("should work as expected for a proof configuration with POD uniqueness disabled", () => { for (const config of [{}, { uniquePODs: false }]) { expect(compileProofPODUniqueness(config)).to.deep.equal({ - uniquenessModuleIsEnabled: 0n + requireUniqueContentIDs: 0n }); } }); diff --git a/packages/lib/gpcircuits/circuits.json b/packages/lib/gpcircuits/circuits.json index ee8e1116d6..75feabd3e0 100644 --- a/packages/lib/gpcircuits/circuits.json +++ b/packages/lib/gpcircuits/circuits.json @@ -34,7 +34,7 @@ "listComparisonValueIndex", "listContainsComparisonValue", "listValidValues", - "uniquenessModuleIsEnabled", + "requireUniqueContentIDs", "globalWatermark" ] }, @@ -73,7 +73,7 @@ "listComparisonValueIndex", "listContainsComparisonValue", "listValidValues", - "uniquenessModuleIsEnabled", + "requireUniqueContentIDs", "globalWatermark" ] }, @@ -112,7 +112,7 @@ "listComparisonValueIndex", "listContainsComparisonValue", "listValidValues", - "uniquenessModuleIsEnabled", + "requireUniqueContentIDs", "globalWatermark" ] }, @@ -151,7 +151,7 @@ "listComparisonValueIndex", "listContainsComparisonValue", "listValidValues", - "uniquenessModuleIsEnabled", + "requireUniqueContentIDs", "globalWatermark" ] }, @@ -190,7 +190,7 @@ "listComparisonValueIndex", "listContainsComparisonValue", "listValidValues", - "uniquenessModuleIsEnabled", + "requireUniqueContentIDs", "globalWatermark" ] }, @@ -229,7 +229,7 @@ "listComparisonValueIndex", "listContainsComparisonValue", "listValidValues", - "uniquenessModuleIsEnabled", + "requireUniqueContentIDs", "globalWatermark" ] } diff --git a/packages/lib/gpcircuits/circuits/gpc-util.circom b/packages/lib/gpcircuits/circuits/gpc-util.circom index 4bc01e1e10..c3a5f8c486 100644 --- a/packages/lib/gpcircuits/circuits/gpc-util.circom +++ b/packages/lib/gpcircuits/circuits/gpc-util.circom @@ -86,7 +86,7 @@ template MaybeInputSelector (N) { /** * Left-rotates elements of a given array by I positions. */ -template Rotl(I,N) { +template ArrayRotl(I,N) { signal input in[N]; signal output out[N]; @@ -111,7 +111,7 @@ template Take(I,N) { /** * Adds a field element to all elements of a given array. */ -template Add(N) { +template ArrayAddScalar(N) { signal input element; signal input in[N]; signal output out[N]; diff --git a/packages/lib/gpcircuits/circuits/list-membership.circom b/packages/lib/gpcircuits/circuits/list-membership.circom index cafa344d6d..bd8a73edb6 100644 --- a/packages/lib/gpcircuits/circuits/list-membership.circom +++ b/packages/lib/gpcircuits/circuits/list-membership.circom @@ -25,7 +25,7 @@ template ListMembershipModule( signal output isMember; isMember <== IsZero()( - UnnormalisedListNonMembership(MAX_LIST_ELEMENTS, MAX_LIST_ELEMENTS)( + NotEqualsAny(MAX_LIST_ELEMENTS, MAX_LIST_ELEMENTS)( comparisonValue, validValues ) @@ -34,31 +34,31 @@ template ListMembershipModule( /** * Helper template returning a non-zero field element if the given - * value is not an element of the given list restricted to the first - * `NUM_LIST_ELEMENTS` elements and zero otherwise. This is done by - * subtracting the value from all elements of the list and folding it - * by means of field multiplication. + * value is not equal to any element of the given list restricted to + * the first `NUM_LIST_ELEMENTS` elements and zero otherwise. This is + * done by subtracting the value from all elements of the list and + * folding it by means of field multiplication. */ -template UnnormalisedListNonMembership(NUM_LIST_ELEMENTS, MAX_LIST_ELEMENTS) { +template NotEqualsAny(NUM_LIST_ELEMENTS, MAX_LIST_ELEMENTS) { // Value to be checked. signal input comparisonValue; - // List of admissible values. - signal input validValues[MAX_LIST_ELEMENTS]; + // List of values to check against for equality. + signal input values[MAX_LIST_ELEMENTS]; // Indicator of whether the value is not an element of the list of // admissible values, viz. a non-zero field element iff the value // is a non-member. - signal output isNotMember; + signal output isNotEqual; if (NUM_LIST_ELEMENTS == 0) { - isNotMember <== 1; + isNotEqual <== 1; } else { - isNotMember <== MultiAND(NUM_LIST_ELEMENTS)( - Add(NUM_LIST_ELEMENTS)( + isNotEqual <== MultiAND(NUM_LIST_ELEMENTS)( + ArrayAddScalar(NUM_LIST_ELEMENTS)( -comparisonValue, Take(NUM_LIST_ELEMENTS, MAX_LIST_ELEMENTS)( - validValues + values ) ) ); diff --git a/packages/lib/gpcircuits/circuits/proto-pod-gpc.circom b/packages/lib/gpcircuits/circuits/proto-pod-gpc.circom index 505b9d02d6..7ef0d283d3 100644 --- a/packages/lib/gpcircuits/circuits/proto-pod-gpc.circom +++ b/packages/lib/gpcircuits/circuits/proto-pod-gpc.circom @@ -373,11 +373,11 @@ template ProtoPODGPC ( */ // Boolean indicating whether the uniqueness module is enabled. - signal input uniquenessModuleIsEnabled; + signal input requireUniqueContentIDs; signal podsAreUnique <== UniquenessModule(MAX_OBJECTS)( objectContentID ); - uniquenessModuleIsEnabled * (1 - podsAreUnique) === 0; + requireUniqueContentIDs * (1 - podsAreUnique) === 0; /* * 1 GlobalModule with its inputs & outputs. diff --git a/packages/lib/gpcircuits/circuits/uniqueness.circom b/packages/lib/gpcircuits/circuits/uniqueness.circom index 9d85c1b858..d546b743fd 100644 --- a/packages/lib/gpcircuits/circuits/uniqueness.circom +++ b/packages/lib/gpcircuits/circuits/uniqueness.circom @@ -19,26 +19,26 @@ template UniquenessModule( signal output valuesAreUnique; // Array of field elements indicating whether the corresponding - // element of `values` is unique, i.e. isUnique[i] = 0 iff - // 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. + signal noDupsAfter[NUM_LIST_ELEMENTS]; // Loop through and check whether the ith element of `values` is // not an element of `values` with the first i+1 elements removed. for(var i = 0; i < NUM_LIST_ELEMENTS; i++) { var j = i+1; - isUnique[i] <== - UnnormalisedListNonMembership(NUM_LIST_ELEMENTS - j, NUM_LIST_ELEMENTS)( + noDupsAfter[i] <== + NotEqualsAny(NUM_LIST_ELEMENTS - j, NUM_LIST_ELEMENTS)( values[i], - Rotl(j, NUM_LIST_ELEMENTS)(values) + ArrayRotl(j, NUM_LIST_ELEMENTS)(values) ); } - // All values are unique iff all elements of `isUnique` are non-zero. + // All values are unique iff all elements of `noDupsAfter` are non-zero. valuesAreUnique <== NOT()( IsZero()( MultiAND(NUM_LIST_ELEMENTS)( - isUnique + noDupsAfter ) ) ); diff --git a/packages/lib/gpcircuits/src/proto-pod-gpc.ts b/packages/lib/gpcircuits/src/proto-pod-gpc.ts index a868d92484..af4d265cd2 100644 --- a/packages/lib/gpcircuits/src/proto-pod-gpc.ts +++ b/packages/lib/gpcircuits/src/proto-pod-gpc.ts @@ -68,7 +68,7 @@ export type ProtoPODGPCInputs = { /*PUB*/ listValidValues: CircuitSignal /*MAX_LISTS*/[] /*MAX_LIST_ENTRIES*/[]; // POD uniqueness module (1) - /*PUB*/ uniquenessModuleIsEnabled: CircuitSignal; + /*PUB*/ requireUniqueContentIDs: CircuitSignal; // Global module (1) /*PUB*/ globalWatermark: CircuitSignal; @@ -110,7 +110,7 @@ export type ProtoPODGPCInputNamesType = [ "listComparisonValueIndex", "listContainsComparisonValue", "listValidValues", - "uniquenessModuleIsEnabled", + "requireUniqueContentIDs", "globalWatermark" ]; @@ -157,7 +157,7 @@ export type ProtoPODGPCPublicInputs = { /*PUB*/ listValidValues: CircuitSignal /*MAX_LISTS*/[] /*MAX_LIST_ENTRIES*/[]; // POD uniqueness module (1) - /*PUB*/ uniquenessModuleIsEnabled: CircuitSignal; + /*PUB*/ requireUniqueContentIDs: CircuitSignal; // Global module (1) /*PUB*/ globalWatermark: CircuitSignal; @@ -186,7 +186,7 @@ export const PROTO_POD_GPC_PUBLIC_INPUT_NAMES = [ "listComparisonValueIndex", "listContainsComparisonValue", "listValidValues", - "uniquenessModuleIsEnabled", + "requireUniqueContentIDs", "globalWatermark" ]; @@ -455,7 +455,7 @@ export class ProtoPODGPC { listComparisonValueIndex: allInputs.listComparisonValueIndex, listContainsComparisonValue: allInputs.listContainsComparisonValue, listValidValues: allInputs.listValidValues, - uniquenessModuleIsEnabled: allInputs.uniquenessModuleIsEnabled, + requireUniqueContentIDs: allInputs.requireUniqueContentIDs, globalWatermark: allInputs.globalWatermark }; } @@ -529,7 +529,7 @@ export class ProtoPODGPC { ...inputs.listComparisonValueIndex, inputs.listContainsComparisonValue, ...inputs.listValidValues.flat(), - inputs.uniquenessModuleIsEnabled, + inputs.requireUniqueContentIDs, inputs.globalWatermark ].map(BigInt); } diff --git a/packages/lib/gpcircuits/src/uniqueness.ts b/packages/lib/gpcircuits/src/uniqueness.ts index 8c37eaa760..822fba66d4 100644 --- a/packages/lib/gpcircuits/src/uniqueness.ts +++ b/packages/lib/gpcircuits/src/uniqueness.ts @@ -1,7 +1,7 @@ import { CircuitSignal } from "./types"; export type UniquenessModuleInputs = { - values: CircuitSignal[]; + values: CircuitSignal /*NUM_LIST_ELEMENTS*/[]; }; export type UniquenessModuleInputNamesType = ["values"]; diff --git a/packages/lib/gpcircuits/src/util.ts b/packages/lib/gpcircuits/src/util.ts index 93261ef9ab..85a6c9f769 100644 --- a/packages/lib/gpcircuits/src/util.ts +++ b/packages/lib/gpcircuits/src/util.ts @@ -205,6 +205,11 @@ export function zeroResidueMod(x: CircuitSignal, n: bigint): bigint { * Creates dummy signals for unused object slots in ProtoPODGPC via dummy * content IDs in the form of POD string hashes of the message `unused POD ${n}` * as well as corresponding signatures. + * + * Note that these do not arise from actual PODs but they present valid content + * IDs since they will pass the necessary signature checks in the + * ProtoPODGPC. This is sufficient because they do not have any entries + * associated with them. */ export function dummyObjectSignals( numObjects: number diff --git a/packages/lib/gpcircuits/test/proto-pod-gpc.spec.ts b/packages/lib/gpcircuits/test/proto-pod-gpc.spec.ts index e442de4249..14353bbf07 100644 --- a/packages/lib/gpcircuits/test/proto-pod-gpc.spec.ts +++ b/packages/lib/gpcircuits/test/proto-pod-gpc.spec.ts @@ -408,7 +408,7 @@ const sampleInput: ProtoPODGPCInputs = { ], // POD uniqueness module (1) - /*PUB*/ uniquenessModuleIsEnabled: 1n, + /*PUB*/ requireUniqueContentIDs: 0n, // Global module (1) /*PUB*/ globalWatermark: 1337n @@ -459,7 +459,8 @@ const sampleOutput: ProtoPODGPCOutputs = { function makeTestSignals( params: ProtoPODGPCCircuitParams, isNullifierHashRevealed: boolean, - isV4NullifierHashRevealed: boolean + isV4NullifierHashRevealed: boolean, + requireUniqueContentIDs: boolean = true ): { inputs: ProtoPODGPCInputs; outputs: ProtoPODGPCOutputs } { // Test data is selected to exercise a lot of features at once, at full // size. Test data always includes a max of 2 real objects and 6 entries. @@ -767,8 +768,6 @@ function makeTestSignals( extendedSignalArray(isMember.map(BigInt), params.maxLists, 1n) ); - const uniquenessModuleIsEnabled = 1n; - return { inputs: { objectContentID: sigObjectContentID, @@ -825,7 +824,7 @@ function makeTestSignals( listComparisonValueIndex, listContainsComparisonValue, listValidValues, - uniquenessModuleIsEnabled, + requireUniqueContentIDs: BigInt(requireUniqueContentIDs), globalWatermark: 1337n }, outputs: { @@ -868,7 +867,8 @@ describe("proto-pod-gpc.ProtoPODGPC (WitnessTester) should work", function () { let { inputs, outputs } = makeTestSignals( GPC_PARAMS, true /*isNullifierHashRevealed*/, - true /*isV4NullifierHashRevealed*/ + true /*isV4NullifierHashRevealed*/, + false /*requireUniqueContentIDs*/ ); expect(inputs).to.deep.eq(sampleInput); expect(outputs).to.deep.eq(sampleOutput); @@ -976,7 +976,8 @@ describe("proto-pod-gpc.ProtoPODGPC (Compiled test artifacts) should work", func let { inputs, outputs } = makeTestSignals( GPC_PARAMS, true /*isNullifierHashRevealed*/, - true /*isV4NullifierHashRevealed*/ + true /*isV4NullifierHashRevealed*/, + false /*requireUniqueContentIDs*/ ); expect(inputs).to.deep.eq(sampleInput); expect(outputs).to.deep.eq(sampleOutput); diff --git a/packages/lib/gpcircuits/test/uniqueness.spec.ts b/packages/lib/gpcircuits/test/uniqueness.spec.ts index 90cb5cfb10..7abb80cc5c 100644 --- a/packages/lib/gpcircuits/test/uniqueness.spec.ts +++ b/packages/lib/gpcircuits/test/uniqueness.spec.ts @@ -1,3 +1,4 @@ +import { BABY_JUB_NEGATIVE_ONE } from "@pcd/util"; import { WitnessTester } from "circomkit"; import "mocha"; import { @@ -34,7 +35,7 @@ describe("uniqueness.UniquenessModule should work", async function () { } }); - it("should return 0 for unique list elements", async () => { + it("should return 0 for non-unique list elements", async () => { const lists = [ [1n, 1n], [47n, 47n, 11n], @@ -43,7 +44,31 @@ describe("uniqueness.UniquenessModule should work", async function () { [1923n, 1923n, 192n, 837n], [192n, 837n, 1923n, 1923n], [1923n, 837n, 1923n, 192n], - [837n, 1923n, 192n, 1923n] + [837n, 1923n, 192n, 1923n], + [ + 1n << 250n, + (1n << 251n) + 5n, + BABY_JUB_NEGATIVE_ONE, + 12348712934821734981n, + BABY_JUB_NEGATIVE_ONE - 1n, + BABY_JUB_NEGATIVE_ONE - 7n, + 987123948273498234729384273498273n, + 6473467364736473647348923847239487n, + 1233439487878787n, + 1n << 250n + ], + [ + 1n << 250n, + (1n << 251n) + 5n, + BABY_JUB_NEGATIVE_ONE, + 12348712934821734981n, + BABY_JUB_NEGATIVE_ONE - 1n, + BABY_JUB_NEGATIVE_ONE - 7n, + BABY_JUB_NEGATIVE_ONE, + 6473467364736473647348923847239487n, + 1233439487878787n, + 48738473658934759238472938n + ] ]; for (const list of lists) { From 70216d0b3536db25b02dd54338d03a07955a5149 Mon Sep 17 00:00:00 2001 From: Ahmad Date: Thu, 19 Sep 2024 05:16:19 +0200 Subject: [PATCH 07/11] GPC layer work --- packages/lib/gpc/src/gpcChecks.ts | 141 +++++++++++++++--- packages/lib/gpc/src/gpcTypes.ts | 31 +++- packages/lib/gpcircuits/circuits.json | 36 ++++- .../lib/gpcircuits/circuits/constants.circom | 12 ++ .../circuits/entry-inequality.circom | 28 ++++ .../gpcircuits/circuits/numeric-value.circom | 17 +-- .../gpcircuits/circuits/proto-pod-gpc.circom | 42 ++++++ .../scripts/gen-circuit-parameters.ts | 6 + .../lib/gpcircuits/src/circuitParameters.json | 18 ++- .../lib/gpcircuits/src/entry-inequality.ts | 15 ++ packages/lib/gpcircuits/src/index.ts | 1 + packages/lib/gpcircuits/src/proto-pod-gpc.ts | 43 +++++- .../gpcircuits/test/entry-inequality.spec.ts | 50 +++++++ .../lib/gpcircuits/test/proto-pod-gpc.spec.ts | 67 +++++++-- 14 files changed, 449 insertions(+), 58 deletions(-) create mode 100644 packages/lib/gpcircuits/circuits/constants.circom create mode 100644 packages/lib/gpcircuits/circuits/entry-inequality.circom create mode 100644 packages/lib/gpcircuits/src/entry-inequality.ts create mode 100644 packages/lib/gpcircuits/test/entry-inequality.spec.ts diff --git a/packages/lib/gpc/src/gpcChecks.ts b/packages/lib/gpc/src/gpcChecks.ts index 3819166a37..801cffec19 100644 --- a/packages/lib/gpc/src/gpcChecks.ts +++ b/packages/lib/gpc/src/gpcChecks.ts @@ -31,6 +31,7 @@ import { GPCProofConfig, GPCProofEntryBoundsCheckConfig, GPCProofEntryConfig, + GPCProofEntryInequalityConfig, GPCProofInputs, GPCProofObjectConfig, GPCRevealedClaims, @@ -114,24 +115,50 @@ export function checkProofConfig(proofConfig: GPCProofConfig): GPCRequirements { let totalObjects = 0; let totalEntries = 0; let requiredMerkleDepth = 0; - let totalNumericValues = 0; + const boundsChecks: Record = {}; + const entryInequalityChecks: Record< + PODEntryIdentifier, + Record + > = {}; let includeOwnerV3 = false; let includeOwnerV4 = false; for (const [objName, objConfig] of Object.entries(proofConfig.pods)) { checkPODName(objName); - const { nEntries, nBoundsChecks, hasOwnerV3, hasOwnerV4 } = - checkProofObjConfig(objName, objConfig); + const { + nEntries, + nBoundsChecks, + inequalityChecks, + hasOwnerV3, + hasOwnerV4 + } = checkProofObjConfig(objName, objConfig); totalObjects++; totalEntries += nEntries; requiredMerkleDepth = Math.max( requiredMerkleDepth, calcMinMerkleDepthForEntries(nEntries) ); - totalNumericValues += nBoundsChecks; + (Object.keys(nBoundsChecks) as PODEntryIdentifier[]).forEach( + (entryIdentifier: PODEntryIdentifier) => { + boundsChecks[entryIdentifier] = nBoundsChecks[entryIdentifier]; + } + ); + (Object.keys(inequalityChecks) as PODEntryIdentifier[]).forEach( + (entryIdentifier: PODEntryIdentifier) => { + entryInequalityChecks[entryIdentifier] = + inequalityChecks[entryIdentifier]; + } + ); includeOwnerV3 ||= hasOwnerV3; includeOwnerV4 ||= hasOwnerV4; } + // A range check should also be carried out on all entries involved in entry + // inequalities to ensure the validity of the entry inequality circuit. + checkProofBoundsCheckConfigForEntryInequalityConfig( + boundsChecks, + entryInequalityChecks + ); + if (proofConfig.uniquePODs !== undefined) { requireType("uniquePODs", proofConfig.uniquePODs, "boolean"); } @@ -140,6 +167,14 @@ export function checkProofConfig(proofConfig: GPCProofConfig): GPCRequirements { checkProofTupleConfig(proofConfig); } + const nBoundsChecks: number = Object.values(boundsChecks).reduce( + (x, y) => x + y + ); + + const nEntryInequalities = Object.values(entryInequalityChecks) + .map((inequalityChecks) => Object.keys(inequalityChecks).length) + .reduce((x, y) => x + y); + const listConfig: GPCProofMembershipListConfig = listConfigFromProofConfig(proofConfig); @@ -158,7 +193,7 @@ export function checkProofConfig(proofConfig: GPCProofConfig): GPCRequirements { totalObjects, totalEntries, requiredMerkleDepth, - totalNumericValues, + nBoundsChecks, numLists, maxListSize, tupleArities, @@ -172,7 +207,11 @@ function checkProofObjConfig( objConfig: GPCProofObjectConfig ): { nEntries: number; - nBoundsChecks: number; + nBoundsChecks: Record; + inequalityChecks: Record< + PODEntryIdentifier, + Record + >; hasOwnerV3: boolean; hasOwnerV4: boolean; } { @@ -183,21 +222,29 @@ function checkProofObjConfig( } let nEntries = 0; - let nBoundsChecks = 0; + const nBoundsChecks: Record = {}; + const inequalityChecks: Record< + PODEntryIdentifier, + Record + > = {}; let hasOwnerV3 = false; let hasOwnerV4 = false; for (const [entryName, entryConfig] of Object.entries(objConfig.entries)) { checkPODEntryName(entryName, true); + const podEntryIdentifier: PODEntryIdentifier = `${nameForErrorMessages}.${entryName}`; const { nBoundsChecks: nEntryBoundsChecks, hasOwnerV3Check, - hasOwnerV4Check - } = checkProofEntryConfig( - `${nameForErrorMessages}.${entryName}`, - entryConfig - ); + hasOwnerV4Check, + inequalityChecks: inequalityChecksForEntry + } = checkProofEntryConfig(podEntryIdentifier, entryConfig); nEntries++; - nBoundsChecks += nEntryBoundsChecks; + if (nEntryBoundsChecks > 0) { + nBoundsChecks[podEntryIdentifier] = nEntryBoundsChecks; + } + if (Object.keys(inequalityChecksForEntry).length > 0) { + inequalityChecks[podEntryIdentifier] = inequalityChecksForEntry; + } hasOwnerV3 ||= hasOwnerV3Check; hasOwnerV4 ||= hasOwnerV4Check; } @@ -213,16 +260,17 @@ function checkProofObjConfig( objConfig.signerPublicKey ); } - return { nEntries, nBoundsChecks, hasOwnerV3, hasOwnerV4 }; + return { nEntries, nBoundsChecks, inequalityChecks, hasOwnerV3, hasOwnerV4 }; } export function checkProofEntryConfig( - nameForErrorMessages: string, + nameForErrorMessages: PODEntryIdentifier, entryConfig: GPCProofEntryConfig ): { hasOwnerV3Check: boolean; hasOwnerV4Check: boolean; nBoundsChecks: number; + inequalityChecks: Record; } { requireType( `${nameForErrorMessages}.isValueRevealed`, @@ -277,14 +325,24 @@ export function checkProofEntryConfig( entryConfig ); + const inequalityChecks = checkProofEntryInequalityConfig( + nameForErrorMessages, + entryConfig + ); + const hasOwnerV3Check = entryConfig.isOwnerID === SEMAPHORE_V3; const hasOwnerV4Check = entryConfig.isOwnerID === SEMAPHORE_V4; - return { hasOwnerV3Check, hasOwnerV4Check, nBoundsChecks }; + return { + hasOwnerV3Check, + hasOwnerV4Check, + nBoundsChecks, + inequalityChecks + }; } export function checkProofEntryBoundsCheckConfig( - nameForErrorMessages: string, + nameForErrorMessages: PODEntryIdentifier, entryConfig: GPCProofEntryBoundsCheckConfig ): number { // Canonicalize to simplify in cases where this is necessary. @@ -333,6 +391,55 @@ export function checkProofEntryBoundsCheckConfig( return nBoundsChecks; } +export function checkProofEntryInequalityConfig( + nameForErrorMessages: PODEntryIdentifier, + entryConfig: GPCProofEntryInequalityConfig +): Record { + return Object.fromEntries( + ["lessThan", "lessThanEq", "greaterThan", "greaterThanEq"].flatMap( + (ineqCheck: string): [string, PODEntryIdentifier][] => { + const otherEntryIdentifier = + entryConfig[ineqCheck as keyof typeof entryConfig]; + + if (otherEntryIdentifier !== undefined) { + // The other entry identifier should be valid. + checkPODEntryIdentifier( + `${nameForErrorMessages}.${ineqCheck}`, + otherEntryIdentifier + ); + + return [[ineqCheck, otherEntryIdentifier]]; + } else { + return []; + } + } + ) + ); +} + +export function checkProofBoundsCheckConfigForEntryInequalityConfig( + boundsChecks: Record, + entryInequalityChecks: Record< + PODEntryIdentifier, + Record + > +) { + const inequalityCheckedEntries = uniq( + Object.keys(entryInequalityChecks).concat( + Object.values(entryInequalityChecks) + .map((inequalityChecks) => Object.values(inequalityChecks)) + .flat() + ) + ) as PODEntryIdentifier[]; + for (const entryIdentifier of inequalityCheckedEntries) { + if (boundsChecks[entryIdentifier] === undefined) { + throw new Error( + `Entry ${entryIdentifier} requires a bounds check to be used in an entry inequality.` + ); + } + } +} + export function checkProofTupleConfig(proofConfig: GPCProofConfig): void { for (const [tupleName, tupleConfig] of Object.entries( proofConfig.tuples ?? {} diff --git a/packages/lib/gpc/src/gpcTypes.ts b/packages/lib/gpc/src/gpcTypes.ts index 0744b11238..86e6cdb2d5 100644 --- a/packages/lib/gpc/src/gpcTypes.ts +++ b/packages/lib/gpc/src/gpcTypes.ts @@ -184,12 +184,41 @@ export type GPCProofEntryBoundsCheckConfig = { notInRange?: ClosedInterval; }; +/** + * Entry inequality configuration for an individual entry. This specifies + * inequalities that should be satisfied with respect to other entries. All such + * entries must be bounds-checked to lie in the appropriate range ([POD_INT_MIN, + * POD_INT_MAX]), lest the resulting circuit be underconstrained. + */ +export type GPCProofEntryInequalityConfig = { + /** + * Indicates an entry that should be greater than this one. + */ + lessThan?: PODEntryIdentifier; + + /** + * Indicates an entry that should be greater than or equal to this one. + */ + lessThanEq?: PODEntryIdentifier; + + /** + * Indicates an entry that should be less than this one. + */ + greaterThan?: PODEntryIdentifier; + + /** + * Indicates an entry that should be less than or equal to this one. + */ + greaterThanEq?: PODEntryIdentifier; +}; + /** * GPCProofConfig for a single non-virtual POD entry, specifying which features * and constraints should be enabled for that entry. */ export type GPCProofEntryConfig = GPCProofEntryConfigCommon & - GPCProofEntryBoundsCheckConfig & { + GPCProofEntryBoundsCheckConfig & + GPCProofEntryInequalityConfig & { /** * Indicates that this entry must match the public ID of the owner identity * given in {@link GPCProofInputs}. For Semaphore V3 this is the owner's diff --git a/packages/lib/gpcircuits/circuits.json b/packages/lib/gpcircuits/circuits.json index 75feabd3e0..89db2d233b 100644 --- a/packages/lib/gpcircuits/circuits.json +++ b/packages/lib/gpcircuits/circuits.json @@ -1,5 +1,5 @@ { - "proto-pod-gpc_1o-1e-5md-0nv-0x0l-0x0t-1ov3-0ov4": { + "proto-pod-gpc_1o-1e-5md-0nv-0ei-0x0l-0x0t-1ov3-0ov4": { "file": "proto-pod-gpc", "template": "ProtoPODGPC", "params": [ @@ -11,6 +11,7 @@ 0, 0, 0, + 0, 1, 0 ], @@ -30,6 +31,9 @@ "numericValueInRange", "numericMinValues", "numericMaxValues", + "entryInequalityValueIndex", + "entryInequalityOtherValueIndex", + "entryInequalityIsLessThan", "tupleIndices", "listComparisonValueIndex", "listContainsComparisonValue", @@ -38,7 +42,7 @@ "globalWatermark" ] }, - "proto-pod-gpc_1o-5e-6md-2nv-0x0l-0x0t-1ov3-1ov4": { + "proto-pod-gpc_1o-5e-6md-2nv-0ei-0x0l-0x0t-1ov3-1ov4": { "file": "proto-pod-gpc", "template": "ProtoPODGPC", "params": [ @@ -50,6 +54,7 @@ 0, 0, 0, + 0, 1, 1 ], @@ -69,6 +74,9 @@ "numericValueInRange", "numericMinValues", "numericMaxValues", + "entryInequalityValueIndex", + "entryInequalityOtherValueIndex", + "entryInequalityIsLessThan", "tupleIndices", "listComparisonValueIndex", "listContainsComparisonValue", @@ -77,7 +85,7 @@ "globalWatermark" ] }, - "proto-pod-gpc_1o-5e-5md-0nv-1x200l-1x3t-0ov3-1ov4": { + "proto-pod-gpc_1o-5e-5md-0nv-0ei-1x200l-1x3t-0ov3-1ov4": { "file": "proto-pod-gpc", "template": "ProtoPODGPC", "params": [ @@ -85,6 +93,7 @@ 5, 5, 0, + 0, 1, 200, 1, @@ -108,6 +117,9 @@ "numericValueInRange", "numericMinValues", "numericMaxValues", + "entryInequalityValueIndex", + "entryInequalityOtherValueIndex", + "entryInequalityIsLessThan", "tupleIndices", "listComparisonValueIndex", "listContainsComparisonValue", @@ -116,7 +128,7 @@ "globalWatermark" ] }, - "proto-pod-gpc_1o-11e-5md-0nv-1x200l-1x3t-0ov3-1ov4": { + "proto-pod-gpc_1o-11e-5md-0nv-0ei-1x200l-1x3t-0ov3-1ov4": { "file": "proto-pod-gpc", "template": "ProtoPODGPC", "params": [ @@ -124,6 +136,7 @@ 11, 5, 0, + 0, 1, 200, 1, @@ -147,6 +160,9 @@ "numericValueInRange", "numericMinValues", "numericMaxValues", + "entryInequalityValueIndex", + "entryInequalityOtherValueIndex", + "entryInequalityIsLessThan", "tupleIndices", "listComparisonValueIndex", "listContainsComparisonValue", @@ -155,7 +171,7 @@ "globalWatermark" ] }, - "proto-pod-gpc_3o-10e-8md-4nv-2x20l-2x2t-0ov3-1ov4": { + "proto-pod-gpc_3o-10e-8md-4nv-2ei-2x20l-2x2t-0ov3-1ov4": { "file": "proto-pod-gpc", "template": "ProtoPODGPC", "params": [ @@ -164,6 +180,7 @@ 8, 4, 2, + 2, 20, 2, 2, @@ -186,6 +203,9 @@ "numericValueInRange", "numericMinValues", "numericMaxValues", + "entryInequalityValueIndex", + "entryInequalityOtherValueIndex", + "entryInequalityIsLessThan", "tupleIndices", "listComparisonValueIndex", "listContainsComparisonValue", @@ -194,7 +214,7 @@ "globalWatermark" ] }, - "proto-pod-gpc_3o-10e-8md-4nv-4x20l-5x3t-1ov3-1ov4": { + "proto-pod-gpc_3o-10e-8md-4nv-2ei-4x20l-5x3t-1ov3-1ov4": { "file": "proto-pod-gpc", "template": "ProtoPODGPC", "params": [ @@ -202,6 +222,7 @@ 10, 8, 4, + 2, 4, 20, 5, @@ -225,6 +246,9 @@ "numericValueInRange", "numericMinValues", "numericMaxValues", + "entryInequalityValueIndex", + "entryInequalityOtherValueIndex", + "entryInequalityIsLessThan", "tupleIndices", "listComparisonValueIndex", "listContainsComparisonValue", diff --git a/packages/lib/gpcircuits/circuits/constants.circom b/packages/lib/gpcircuits/circuits/constants.circom new file mode 100644 index 0000000000..77b8a5060f --- /dev/null +++ b/packages/lib/gpcircuits/circuits/constants.circom @@ -0,0 +1,12 @@ +// Absolute value of minimum value of a 64-bit signed integer. This +// will be added to all values fed into the bounds check and entry +// inequality modules to convert them to 64-bit unsigned integers +// while preserving order. +function ABS_POD_INT_MIN() { + return 1 << (POD_INT_BITS() - 1); +} + +// Maximum number of bits in a POD int value. +function POD_INT_BITS() { + return 64; +} diff --git a/packages/lib/gpcircuits/circuits/entry-inequality.circom b/packages/lib/gpcircuits/circuits/entry-inequality.circom new file mode 100644 index 0000000000..18948255bb --- /dev/null +++ b/packages/lib/gpcircuits/circuits/entry-inequality.circom @@ -0,0 +1,28 @@ +pragma circom 2.1.8; + +include "circomlib/circuits/comparators.circom"; +include "constants.circom"; + +/** + * Module constraining an entry value to be (not) less than another + * entry value. This module should be combined with the numeric value + * module with which its values are shared. Moreover, the numeric + * value module will ensure the validity of these values as well as + * the inequality check circuit. + * + * The module has no explicit enable flag. To disable it, the input + * signals should be equal to each other, whence the output should be + * 0. + */ +template EntryInequalityModule( + // Number of bits required to represent the inputs. Must be less + // than 252, cf. {@link LessThan}. + NUM_BITS +) { + // The values to use in the inequality value < otherValue. + signal input value; + signal input otherValue; + + // Boolean indicating whether value < otherValue. + signal output out <== LessThan(NUM_BITS)([value, otherValue]); +} diff --git a/packages/lib/gpcircuits/circuits/numeric-value.circom b/packages/lib/gpcircuits/circuits/numeric-value.circom index 949449bee5..c3aef33cc8 100644 --- a/packages/lib/gpcircuits/circuits/numeric-value.circom +++ b/packages/lib/gpcircuits/circuits/numeric-value.circom @@ -2,6 +2,7 @@ pragma circom 2.1.8; include "circomlib/circuits/poseidon.circom"; include "bounds.circom"; +include "constants.circom"; /** * Module constraining a single entry value of POD object. It proves @@ -32,17 +33,11 @@ template NumericValueModule() { signal input minValue; signal input maxValue; - // Absolute value of minimum value of a 64-bit signed integer. - // This will be added to all values fed into the bounds check - // module to convert them to 64-bit unsigned integers while - // preserving order. - var ABS_POD_INT_MIN = 1 << 63; - // Check that minValue <= numericValue <= maxValue and return the // result. - signal output isInBounds <== BoundsCheckModule(64)( - numericValue + ABS_POD_INT_MIN, - minValue + ABS_POD_INT_MIN, - maxValue + ABS_POD_INT_MIN - ); + signal output isInBounds <== BoundsCheckModule(POD_INT_BITS())( + numericValue + ABS_POD_INT_MIN(), + minValue + ABS_POD_INT_MIN(), + maxValue + ABS_POD_INT_MIN() + ); } diff --git a/packages/lib/gpcircuits/circuits/proto-pod-gpc.circom b/packages/lib/gpcircuits/circuits/proto-pod-gpc.circom index 7ef0d283d3..0480fb8908 100644 --- a/packages/lib/gpcircuits/circuits/proto-pod-gpc.circom +++ b/packages/lib/gpcircuits/circuits/proto-pod-gpc.circom @@ -2,7 +2,9 @@ pragma circom 2.1.8; include "circomlib/circuits/gates.circom"; include "circomlib/circuits/poseidon.circom"; +include "constants.circom"; include "entry.circom"; +include "entry-inequality.circom"; include "global.circom"; include "gpc-util.circom"; include "list-membership.circom"; @@ -42,6 +44,9 @@ template ProtoPODGPC ( // Max number of numeric values. MAX_NUMERIC_VALUES, + + // Max number of entry inequalities. + MAX_ENTRY_INEQUALITIES, // Indicates the number of ListMembership modules included in this GPC, // setting the largest number of distinct membership lists for (tuples of) @@ -318,6 +323,43 @@ template ProtoPODGPC ( numericValueInRangeBits[i] === numericValueBoundsCheck[i]; } + + /* + * (MAX_ENTRY_INEQUALITIES) EntryInequalityModules with their + * inputs & outputs. + */ + + // Values to be compared are referenced by their indices in + // `numericValues`. These should be padded with 0s if necessary. + signal input entryInequalityValueIndex[MAX_ENTRY_INEQUALITIES]; + signal input entryInequalityOtherValueIndex[MAX_ENTRY_INEQUALITIES]; + + // Inequalities are of the form 'value < otherValue' or 'value >= + // otherValue', the former corresponding to entry inequality + // module output value 1 and the latter to 0. This motivates the + // following bit-packed indicators. These should be padded with + // 0s if necessary. + signal input entryInequalityIsLessThan; + signal entryInequalityIsLessThanBits[MAX_ENTRY_INEQUALITIES] + <== Num2Bits(MAX_ENTRY_INEQUALITIES)(entryInequalityIsLessThan); + + // Output of entry inequality check. + signal entryInequalityCheck[MAX_ENTRY_INEQUALITIES]; + + for (var i = 0; i < MAX_ENTRY_INEQUALITIES; i++) { + entryInequalityCheck[i] + <== EntryInequalityModule(POD_INT_BITS())( + InputSelector(MAX_NUMERIC_VALUES)( + numericValues, + entryInequalityValueIndex[i] + ), + InputSelector(MAX_NUMERIC_VALUES)( + numericValues, + entryInequalityOtherValueIndex[i] + ) + ); + entryInequalityIsLessThanBits[i] === entryInequalityCheck[i]; + } /* * 1 MultiTupleModule with its inputs & outputs. diff --git a/packages/lib/gpcircuits/scripts/gen-circuit-parameters.ts b/packages/lib/gpcircuits/scripts/gen-circuit-parameters.ts index e878e62d5c..e9c107ff2d 100644 --- a/packages/lib/gpcircuits/scripts/gen-circuit-parameters.ts +++ b/packages/lib/gpcircuits/scripts/gen-circuit-parameters.ts @@ -22,6 +22,7 @@ const CIRCUIT_PARAMETERS = [ maxEntries: 1, merkleMaxDepth: 5, maxNumericValues: 0, + maxEntryInequalities: 0, maxLists: 0, maxListElements: 0, maxTuples: 0, @@ -34,6 +35,7 @@ const CIRCUIT_PARAMETERS = [ maxEntries: 5, merkleMaxDepth: 6, maxNumericValues: 2, + maxEntryInequalities: 0, maxLists: 0, maxListElements: 0, maxTuples: 0, @@ -46,6 +48,7 @@ const CIRCUIT_PARAMETERS = [ maxEntries: 5, merkleMaxDepth: 5, maxNumericValues: 0, + maxEntryInequalities: 0, maxLists: 1, maxListElements: 200, maxTuples: 1, @@ -58,6 +61,7 @@ const CIRCUIT_PARAMETERS = [ maxEntries: 11, merkleMaxDepth: 5, maxNumericValues: 0, + maxEntryInequalities: 0, maxLists: 1, maxListElements: 200, maxTuples: 1, @@ -70,6 +74,7 @@ const CIRCUIT_PARAMETERS = [ maxEntries: 10, merkleMaxDepth: 8, maxNumericValues: 4, + maxEntryInequalities: 2, maxLists: 2, maxListElements: 20, maxTuples: 2, @@ -82,6 +87,7 @@ const CIRCUIT_PARAMETERS = [ maxEntries: 10, merkleMaxDepth: 8, maxNumericValues: 4, + maxEntryInequalities: 2, maxLists: 4, maxListElements: 20, maxTuples: 5, diff --git a/packages/lib/gpcircuits/src/circuitParameters.json b/packages/lib/gpcircuits/src/circuitParameters.json index 400d66e447..66ecbb3d0c 100644 --- a/packages/lib/gpcircuits/src/circuitParameters.json +++ b/packages/lib/gpcircuits/src/circuitParameters.json @@ -5,6 +5,7 @@ "maxEntries": 1, "merkleMaxDepth": 5, "maxNumericValues": 0, + "maxEntryInequalities": 0, "maxLists": 0, "maxListElements": 0, "maxTuples": 0, @@ -12,7 +13,7 @@ "includeOwnerV3": true, "includeOwnerV4": false }, - 6842 + 6843 ], [ { @@ -20,6 +21,7 @@ "maxEntries": 5, "merkleMaxDepth": 6, "maxNumericValues": 2, + "maxEntryInequalities": 0, "maxLists": 0, "maxListElements": 0, "maxTuples": 0, @@ -27,7 +29,7 @@ "includeOwnerV3": true, "includeOwnerV4": true }, - 15360 + 15361 ], [ { @@ -35,6 +37,7 @@ "maxEntries": 5, "merkleMaxDepth": 5, "maxNumericValues": 0, + "maxEntryInequalities": 0, "maxLists": 1, "maxListElements": 200, "maxTuples": 1, @@ -42,7 +45,7 @@ "includeOwnerV3": false, "includeOwnerV4": true }, - 13011 + 13012 ], [ { @@ -50,6 +53,7 @@ "maxEntries": 11, "merkleMaxDepth": 5, "maxNumericValues": 0, + "maxEntryInequalities": 0, "maxLists": 1, "maxListElements": 200, "maxTuples": 1, @@ -57,7 +61,7 @@ "includeOwnerV3": false, "includeOwnerV4": true }, - 20745 + 20746 ], [ { @@ -65,6 +69,7 @@ "maxEntries": 10, "merkleMaxDepth": 8, "maxNumericValues": 4, + "maxEntryInequalities": 2, "maxLists": 2, "maxListElements": 20, "maxTuples": 2, @@ -72,7 +77,7 @@ "includeOwnerV3": false, "includeOwnerV4": true }, - 38093 + 38257 ], [ { @@ -80,6 +85,7 @@ "maxEntries": 10, "merkleMaxDepth": 8, "maxNumericValues": 4, + "maxEntryInequalities": 2, "maxLists": 4, "maxListElements": 20, "maxTuples": 5, @@ -87,6 +93,6 @@ "includeOwnerV3": true, "includeOwnerV4": true }, - 40402 + 40566 ] ] \ No newline at end of file diff --git a/packages/lib/gpcircuits/src/entry-inequality.ts b/packages/lib/gpcircuits/src/entry-inequality.ts new file mode 100644 index 0000000000..be437f7185 --- /dev/null +++ b/packages/lib/gpcircuits/src/entry-inequality.ts @@ -0,0 +1,15 @@ +import { CircuitSignal } from "./types"; + +export type EntryInequalityModuleInputs = { + value: CircuitSignal; + otherValue: CircuitSignal; +}; + +export type EntryInequalityModuleInputNamesType = [ + "value", + "otherValue" +]; + +export type EntryInequalityModuleOutputs = { out: CircuitSignal }; + +export type EntryInequalityModuleOutputNamesType = ["out"]; diff --git a/packages/lib/gpcircuits/src/index.ts b/packages/lib/gpcircuits/src/index.ts index a6461e24f1..2f0f570c6d 100644 --- a/packages/lib/gpcircuits/src/index.ts +++ b/packages/lib/gpcircuits/src/index.ts @@ -1,6 +1,7 @@ export * from "./artifacts"; export * from "./bounds"; export * from "./entry"; +export * from "./entry-inequality"; export * from "./global"; export * from "./list-membership"; export * from "./multituple"; diff --git a/packages/lib/gpcircuits/src/proto-pod-gpc.ts b/packages/lib/gpcircuits/src/proto-pod-gpc.ts index af4d265cd2..36439826fa 100644 --- a/packages/lib/gpcircuits/src/proto-pod-gpc.ts +++ b/packages/lib/gpcircuits/src/proto-pod-gpc.ts @@ -59,6 +59,11 @@ export type ProtoPODGPCInputs = { /*PUB*/ numericMinValues: CircuitSignal /*MAX_NUMERIC_VALUES*/[]; /*PUB*/ numericMaxValues: CircuitSignal /*MAX_NUMERIC_VALUES*/[]; + // Entry inequality modules [MAX_ENTRY_INEQUALITIES]. + /*PUB*/ entryInequalityValueIndex: CircuitSignal /*MAX_ENTRY_INEQUALITIES*/[]; + /*PUB*/ entryInequalityOtherValueIndex: CircuitSignal /*MAX_ENTRY_INEQUALITIES*/[]; + /*PUB*/ entryInequalityIsLessThan: CircuitSignal /*MAX_ENTRY_INEQUALITIES packed bits*/; + // MultiTuple module (1) /*PUB*/ tupleIndices: CircuitSignal /*MAX_TUPLES*/[] /*TUPLE_ARITY*/[]; @@ -106,6 +111,9 @@ export type ProtoPODGPCInputNamesType = [ "numericValueInRange", "numericMinValues", "numericMaxValues", + "entryInequalityValueIndex", + "entryInequalityOtherValueIndex", + "entryInequalityIsLessThan", "tupleIndices", "listComparisonValueIndex", "listContainsComparisonValue", @@ -142,12 +150,17 @@ export type ProtoPODGPCPublicInputs = { /*PUB*/ ownerV4EntryIndex: CircuitSignal /*INCLUDE_OWNERV4*/[]; /*PUB*/ ownerV4IsNullifierHashRevealed: CircuitSignal /*INCLUDE_OWNERV4*/[]; - // Bounds check module (1) + // Numeric value modules [MAX_NUMERIC_VALUES] /*PUB*/ numericValueEntryIndices: CircuitSignal /*MAX_NUMERIC_VALUES*/[]; /*PUB*/ numericValueInRange: CircuitSignal /*MAX_NUMERIC_VALUES packed bits*/; /*PUB*/ numericMinValues: CircuitSignal /*MAX_NUMERIC_VALUES*/[]; /*PUB*/ numericMaxValues: CircuitSignal /*MAX_NUMERIC_VALUES*/[]; + // Entry inequality modules [MAX_ENTRY_INEQUALITIES]. + /*PUB*/ entryInequalityValueIndex: CircuitSignal /*MAX_ENTRY_INEQUALITIES*/[]; + /*PUB*/ entryInequalityOtherValueIndex: CircuitSignal /*MAX_ENTRY_INEQUALITIES*/[]; + /*PUB*/ entryInequalityIsLessThan: CircuitSignal /*MAX_ENTRY_INEQUALITIES packed bits*/; + // Tuple module (1) /*PUB*/ tupleIndices: CircuitSignal /*MAX_TUPLES*/[] /*TUPLE_ARITY*/[]; @@ -182,6 +195,9 @@ export const PROTO_POD_GPC_PUBLIC_INPUT_NAMES = [ "numericValueInRange", "numericMinValues", "numericMaxValues", + "entryInequalityValueIndex", + "entryInequalityOtherValueIndex", + "entryInequalityIsLessThan", "tupleIndices", "listComparisonValueIndex", "listContainsComparisonValue", @@ -236,6 +252,11 @@ export type ProtoPODGPCCircuitParams = { */ maxNumericValues: number; + /** + * Number of entry inequalities. + */ + maxEntryInequalities: number; + /** * Number of membership lists. */ @@ -276,6 +297,7 @@ export function ProtoPODGPCCircuitParams( maxEntries: number, merkleMaxDepth: number, maxNumericValues: number, + maxEntryInequalities: number, maxLists: number, maxListElements: number, maxTuples: number, @@ -288,6 +310,7 @@ export function ProtoPODGPCCircuitParams( maxEntries, merkleMaxDepth, maxNumericValues, + maxEntryInequalities, maxLists, maxListElements, maxTuples, @@ -310,6 +333,7 @@ export function protoPODGPCCircuitParamArray( params.maxEntries, params.merkleMaxDepth, params.maxNumericValues, + params.maxEntryInequalities, params.maxLists, params.maxListElements, params.maxTuples, @@ -336,8 +360,9 @@ export function arrayToProtoPODGPCCircuitParam( params[5], params[6], params[7], - !!params[8], - !!params[9] + params[8], + !!params[9], + !!params[10] ); } @@ -451,6 +476,9 @@ export class ProtoPODGPC { numericValueInRange: allInputs.numericValueInRange, numericMinValues: allInputs.numericMinValues, numericMaxValues: allInputs.numericMaxValues, + entryInequalityValueIndex: allInputs.entryInequalityValueIndex, + entryInequalityOtherValueIndex: allInputs.entryInequalityOtherValueIndex, + entryInequalityIsLessThan: allInputs.entryInequalityIsLessThan, tupleIndices: allInputs.tupleIndices, listComparisonValueIndex: allInputs.listComparisonValueIndex, listContainsComparisonValue: allInputs.listContainsComparisonValue, @@ -525,6 +553,9 @@ export class ProtoPODGPC { ...inputs.numericMaxValues.map((value) => zeroResidueMod(value, BABY_JUB_PRIME) ), + ...inputs.entryInequalityValueIndex, + ...inputs.entryInequalityOtherValueIndex, + inputs.entryInequalityIsLessThan, ...inputs.tupleIndices.flat(), ...inputs.listComparisonValueIndex, inputs.listContainsComparisonValue, @@ -623,9 +654,9 @@ export class ProtoPODGPC { public static circuitNameForParams(params: ProtoPODGPCCircuitParams): string { return `${params.maxObjects}o-${params.maxEntries}e-${ params.merkleMaxDepth - }md-${params.maxNumericValues}nv-${params.maxLists}x${ - params.maxListElements - }l-${params.maxTuples}x${ + }md-${params.maxNumericValues}nv-${params.maxEntryInequalities}ei-${ + params.maxLists + }x${params.maxListElements}l-${params.maxTuples}x${ params.tupleArity }t-${+params.includeOwnerV3}ov3-${+params.includeOwnerV4}ov4`; } diff --git a/packages/lib/gpcircuits/test/entry-inequality.spec.ts b/packages/lib/gpcircuits/test/entry-inequality.spec.ts new file mode 100644 index 0000000000..e3e4f1d931 --- /dev/null +++ b/packages/lib/gpcircuits/test/entry-inequality.spec.ts @@ -0,0 +1,50 @@ +import { BABY_JUB_PRIME as p } from "@pcd/util"; +import { WitnessTester } from "circomkit"; +import "mocha"; +import { + EntryInequalityModuleInputNamesType, + EntryInequalityModuleOutputNamesType +} from "../src"; +import { circomkit } from "./common"; + +for (const nBits of [32, 64, 96, 128, 252]) { + describe( + "entry-inequality.EntryInequalityModule should work as expected", async function () { + let circuit: WitnessTester< + EntryInequalityModuleInputNamesType, + EntryInequalityModuleOutputNamesType + >; + + this.beforeAll(async () => { + circuit = await circomkit.WitnessTester("EntryInequalityModule", { + file: "entry-inequality", + template: "EntryInequalityModule", + params: [nBits] + }); + }); + + it("should work for " + + nBits + + "-bit unsigned integer values", + async () => { + const twoToTheNBits = 1n << BigInt(nBits); + const sampleValues: [bigint, bigint][] = [ + [0n, 0n], + [2n,3n], + [3n,2n], + [3n, twoToTheNBits / 5n], + [twoToTheNBits / 5n - 1n, (2n * twoToTheNBits) / 5n], + [(2n * twoToTheNBits) / 5n - 5n, (3n * twoToTheNBits) / 5n], + [0n, twoToTheNBits - 1n], + [twoToTheNBits - 2n, twoToTheNBits - 1n], + [twoToTheNBits - 1n, twoToTheNBits - 2n], + [twoToTheNBits - 1n, twoToTheNBits - 1n] + ]; + + for(const [value,otherValue] of sampleValues) { + await circuit.expectPass({ value, otherValue }, {out: BigInt(value < otherValue)}); + } + }); + } + ); +} diff --git a/packages/lib/gpcircuits/test/proto-pod-gpc.spec.ts b/packages/lib/gpcircuits/test/proto-pod-gpc.spec.ts index 14353bbf07..4515bff8c9 100644 --- a/packages/lib/gpcircuits/test/proto-pod-gpc.spec.ts +++ b/packages/lib/gpcircuits/test/proto-pod-gpc.spec.ts @@ -52,6 +52,7 @@ const MAX_OBJECTS = 3; const MAX_ENTRIES = 10; const MERKLE_MAX_DEPTH = 8; const MAX_NUMERIC_VALUES = 4; +const MAX_ENTRY_INEQUALITIES = 2; const MAX_LISTS = 4; const MAX_LIST_ENTRIES = 20; const MAX_TUPLES = 5; @@ -64,6 +65,7 @@ const GPC_PARAMS = ProtoPODGPCCircuitParams( MAX_ENTRIES, MERKLE_MAX_DEPTH, MAX_NUMERIC_VALUES, + MAX_ENTRY_INEQUALITIES, MAX_LISTS, MAX_LIST_ENTRIES, MAX_TUPLES, @@ -288,16 +290,21 @@ const sampleInput: ProtoPODGPCInputs = { /*PUB*/ ownerV4IsNullifierHashRevealed: [1n], // Numeric value module (MAX_NUMERIC_VALUES) - numericValues: [-5n, 0n, 0n, 0n], + numericValues: [123n, -5n, 0n, 0n], /*PUB*/ numericValueEntryIndices: [ + 0n, 4n, 21888242871839275222246405745257275088548364400416034343698204186575808495616n, - 21888242871839275222246405745257275088548364400416034343698204186575808495616n, 21888242871839275222246405745257275088548364400416034343698204186575808495616n ], /*PUB*/ numericValueInRange: 15n, - /*PUB*/ numericMinValues: [-10n, 0n, 0n, 0n], - /*PUB*/ numericMaxValues: [132n, 0n, 0n, 0n], + /*PUB*/ numericMinValues: [0n, -10n, 0n, 0n], + /*PUB*/ numericMaxValues: [1000n, 132n, 0n, 0n], + + // Entry inequality module (MAX_ENTRY_INEQUALITIES). + /*PUB*/ entryInequalityValueIndex: [1n, 0n], + /*PUB*/ entryInequalityOtherValueIndex: [0n, 1n], + /*PUB*/ entryInequalityIsLessThan: 1n, // Tuple module (1) /*PUB*/ tupleIndices: [ @@ -674,7 +681,8 @@ function makeTestSignals( const sigVirtualEntryIsEqualToOtherEntry = sigVirtualEntryEqualToOtherEntryByIndex.map((_) => 1n); - // Constrain entry 4 (sampleEntries.K) to lie in the interval [-10n, 132n] + // Constrain entry 0 (sampleEntries.A) to lie in the interval [0n, 1000n] and + // entry 4 (sampleEntries.K) to lie in the interval [-10n, 132n] const [ numericValues, numericValueEntryIndices, @@ -682,14 +690,48 @@ function makeTestSignals( numericMinValues, numericMaxValues ] = - params.maxNumericValues === 0 || params.maxEntries < 5 + params.maxNumericValues === 0 ? [[], [], 0n, [], []] - : [ - padArray([sigEntryValue[4]], params.maxNumericValues, 0n), - padArray([4n], params.maxNumericValues, BABY_JUB_NEGATIVE_ONE), + : params.maxEntries < 5 + ? [ + padArray([sigEntryValue[0]], params.maxNumericValues, 0n), + padArray([0n], params.maxNumericValues, BABY_JUB_NEGATIVE_ONE), array2Bits(padArray([1n], params.maxNumericValues, 1n)), - padArray([-10n], params.maxNumericValues, 0n), - padArray([132n], params.maxNumericValues, 0n) + padArray([0n], params.maxNumericValues, 0n), + padArray([1000n], params.maxNumericValues, 0n) + ] + : [ + padArray( + [sigEntryValue[0], sigEntryValue[4]], + params.maxNumericValues, + 0n + ), + padArray([0n, 4n], params.maxNumericValues, BABY_JUB_NEGATIVE_ONE), + array2Bits(padArray([1n, 1n], params.maxNumericValues, 1n)), + padArray([0n, -10n], params.maxNumericValues, 0n), + padArray([1000n, 132n], params.maxNumericValues, 0n) + ]; + + // Constrain entry 4 (numeric value 1) to be less than entry 0 (numeric value + // 0) and entry 0 to be greater than or + // equal to entry 4. + const [ + entryInequalityValueIndex, + entryInequalityOtherValueIndex, + entryInequalityIsLessThan + ] = + params.maxEntryInequalities === 0 + ? [[], [], 0n] + : params.maxEntries < 5 || params.maxEntryInequalities < 2 + ? [ + padArray([], params.maxEntryInequalities, 0n), + padArray([], params.maxEntryInequalities, 0n), + 0n + ] + : [ + padArray([1n, 0n], params.maxEntryInequalities, 0n), + padArray([0n, 1n], params.maxEntryInequalities, 0n), + array2Bits(padArray([1n, 0n], params.maxEntryInequalities, 0n)) ]; // A list of pairs of indices and values. @@ -820,6 +862,9 @@ function makeTestSignals( numericValueInRange, numericMinValues, numericMaxValues, + entryInequalityValueIndex, + entryInequalityOtherValueIndex, + entryInequalityIsLessThan, tupleIndices, listComparisonValueIndex, listContainsComparisonValue, From 8a0c337c19cb6454afdc4d08ee6d751c71da9250 Mon Sep 17 00:00:00 2001 From: Ahmad Date: Fri, 20 Sep 2024 10:24:03 +0200 Subject: [PATCH 08/11] More GPC layer work --- packages/lib/gpc/src/gpcChecks.ts | 72 +++++++++++++++++++- packages/lib/gpc/src/gpcCompile.ts | 90 +++++++++++++++++++++++-- packages/lib/gpc/src/gpcUtil.ts | 75 +++++++++++++++------ packages/lib/gpc/test/gpcChecks.spec.ts | 4 ++ 4 files changed, 212 insertions(+), 29 deletions(-) diff --git a/packages/lib/gpc/src/gpcChecks.ts b/packages/lib/gpc/src/gpcChecks.ts index 801cffec19..00ef23c82e 100644 --- a/packages/lib/gpc/src/gpcChecks.ts +++ b/packages/lib/gpc/src/gpcChecks.ts @@ -6,6 +6,7 @@ import { import { EDDSA_PUBKEY_TYPE_STRING, POD, + PODIntValue, PODName, PODValue, PODValueTuple, @@ -57,6 +58,7 @@ import { resolvePODEntryIdentifier, resolvePODEntryOrTupleIdentifier, splitCircuitIdentifier, + splitPODEntryIdentifier, widthOfEntryOrTuple } from "./gpcUtil"; // TODO(POD-P2): Split out the parts of this which should be public from @@ -168,12 +170,13 @@ export function checkProofConfig(proofConfig: GPCProofConfig): GPCRequirements { } const nBoundsChecks: number = Object.values(boundsChecks).reduce( - (x, y) => x + y + (x, y) => x + y, + 0 ); const nEntryInequalities = Object.values(entryInequalityChecks) .map((inequalityChecks) => Object.keys(inequalityChecks).length) - .reduce((x, y) => x + y); + .reduce((x, y) => x + y, 0); const listConfig: GPCProofMembershipListConfig = listConfigFromProofConfig(proofConfig); @@ -194,6 +197,7 @@ export function checkProofConfig(proofConfig: GPCProofConfig): GPCRequirements { totalEntries, requiredMerkleDepth, nBoundsChecks, + nEntryInequalities, numLists, maxListSize, tupleArities, @@ -423,7 +427,7 @@ export function checkProofBoundsCheckConfigForEntryInequalityConfig( PODEntryIdentifier, Record > -) { +): void { const inequalityCheckedEntries = uniq( Object.keys(entryInequalityChecks).concat( Object.values(entryInequalityChecks) @@ -575,6 +579,9 @@ export function checkProofInputs(proofInputs: GPCProofInputs): GPCRequirements { // Numeric values (bounds checks) are handled solely in the proof config, // hence we return 0 here. 0, + // Entry inequalities are handled solely in the proof config, hence we + // return 0 here. + 0, // The number of required lists cannot be properly deduced here, so we // return 0. 0, @@ -722,6 +729,14 @@ export function checkProofInputsForConfig( entryConfig, podValue ); + + // Check entry inequalities for entry + checkProofEntryInequalityInputsForConfig( + `${objName}.${entryName}`, + entryConfig, + podValue, + proofInputs.pods + ); } } // Check that nullifier isn't requested if it's not linked to anything. @@ -793,6 +808,54 @@ export function checkProofBoundsCheckInputsForConfig( } } +export function checkProofEntryInequalityInputsForConfig( + entryName: PODEntryIdentifier, + entryConfig: GPCProofEntryConfig, + entryValue: PODValue, + pods: Record +): void { + type B = boolean; + type N = bigint; + const inequalityCheckTriples: [ + string, + PODEntryIdentifier | undefined, + (x: N, y: N) => B + ][] = [ + ["less than", entryConfig.lessThan, (x: N, y: N): B => x < y], + [ + "less than or equal to", + entryConfig.lessThanEq, + (x: N, y: N): B => x <= y + ], + ["greater than", entryConfig.greaterThan, (x: N, y: N): B => x > y], + [ + "greater than or equal to", + entryConfig.greaterThanEq, + (x: N, y: N): B => x >= y + ] + ]; + + for (const [checkType, otherEntry, cmp] of inequalityCheckTriples) { + if (otherEntry !== undefined) { + const { objName, entryName: otherEntryName } = + splitPODEntryIdentifier(otherEntry); + + // Both `entryName` and `otherEntryName` are amongst the bounds-checked + // entries and therefore exist in the input as PODIntValues. + const pod = pods[objName] as POD; + const otherEntryValue = resolvePODEntry( + otherEntryName, + pod + ) as PODIntValue; + if (!cmp(entryValue.value as bigint, otherEntryValue.value)) { + throw new Error( + `Input entry ${entryName} should be ${checkType} entry ${otherEntry}, but it is not.` + ); + } + } + } +} + export function checkProofListMembershipInputsForConfig( proofConfig: GPCProofConfig, proofInputs: GPCProofInputs @@ -1011,6 +1074,7 @@ export function checkRevealedClaims( requiredMerkleDepth, 0, 0, + 0, maxListSize, {} ); @@ -1266,6 +1330,7 @@ export function circuitDescMeetsRequirements( circuitDesc.maxEntries >= circuitReq.nEntries && circuitDesc.merkleMaxDepth >= circuitReq.merkleMaxDepth && circuitDesc.maxNumericValues >= circuitReq.nNumericValues && + circuitDesc.maxEntryInequalities >= circuitReq.nEntryInequalities && circuitDesc.maxLists >= circuitReq.nLists && // The circuit description should be able to contain the largest of the lists. circuitDesc.maxListElements >= circuitReq.maxListSize && @@ -1309,6 +1374,7 @@ export function mergeRequirements( Math.max(rs1.nEntries, rs2.nEntries), Math.max(rs1.merkleMaxDepth, rs2.merkleMaxDepth), Math.max(rs1.nNumericValues, rs2.nNumericValues), + Math.max(rs1.nEntryInequalities, rs2.nEntryInequalities), Math.max(rs1.nLists, rs2.nLists), Math.max(rs1.maxListSize, rs2.maxListSize), tupleArities, diff --git a/packages/lib/gpc/src/gpcCompile.ts b/packages/lib/gpc/src/gpcCompile.ts index 547aa08c49..d452d6c7ef 100644 --- a/packages/lib/gpc/src/gpcCompile.ts +++ b/packages/lib/gpc/src/gpcCompile.ts @@ -56,8 +56,7 @@ import { isVirtualEntryIdentifier, isVirtualEntryName, listConfigFromProofConfig, - makeWatermarkSignal, - podEntryIdentifierCompare + makeWatermarkSignal } from "./gpcUtil"; /** @@ -359,6 +358,14 @@ export function compileProofConfig( circuitDesc.maxNumericValues ); + // Create entry inequality inputs. + const circuitEntryInequalityInputs = compileCommonEntryInequalities( + boundsCheckConfig, + entryMap, + circuitDesc.maxNumericValues, + circuitDesc.maxEntryInequalities + ); + // Create subset of inputs for multituple module padded to max size. const circuitMultiTupleInputs = compileProofMultiTuples( tupleMap, @@ -404,6 +411,7 @@ export function compileProofConfig( ...circuitOwnerV3Inputs, ...circuitOwnerV4Inputs, ...circuitNumericValueInputs, + ...circuitEntryInequalityInputs, ...circuitMultiTupleInputs, ...circuitListMembershipInputs, ...circuitUniquenessInputs, @@ -488,6 +496,67 @@ function compileProofNumericValues( }; } +export function compileCommonEntryInequalities< + ObjInput extends POD | GPCRevealedObjectClaims +>( + boundsCheckConfig: GPCProofBoundsCheckConfig, + entryMap: Map>, + paramNumericValues: number, + paramEntryInequalities: number +): { + entryInequalityValueIndex: CircuitSignal[]; + entryInequalityOtherValueIndex: CircuitSignal[]; + entryInequalityIsLessThan: CircuitSignal; +} { + const numericValueIndex: Record = + Object.fromEntries( + Object.keys(boundsCheckConfig).map((entryIdentifier, i) => [ + entryIdentifier, + BigInt(i) + ]) + ); + const signalTriples = ( + Object.entries(numericValueIndex) as [PODEntryIdentifier, bigint][] + ).flatMap(([entryIdentifier, entryIndex]): bigint[][] => { + const entryConfig = entryMap.get(entryIdentifier) + ?.entryConfig as GPCProofEntryConfig; + return [ + entryConfig.lessThan + ? [[entryIndex, numericValueIndex[entryConfig.lessThan], 1n]] + : [], + entryConfig.lessThanEq + ? [[numericValueIndex[entryConfig.lessThanEq], entryIndex, 0n]] + : [], + entryConfig.greaterThan + ? [[numericValueIndex[entryConfig.greaterThan], entryIndex, 1n]] + : [], + entryConfig.greaterThanEq + ? [[entryIndex, numericValueIndex[entryConfig.greaterThanEq], 0n]] + : [] + ].flat(); + }); + + return { + entryInequalityValueIndex: extendedSignalArray( + signalTriples.map((x) => x[0]), + paramEntryInequalities, + 0n + ), + entryInequalityOtherValueIndex: extendedSignalArray( + signalTriples.map((x) => x[1]), + paramEntryInequalities, + 0n + ), + entryInequalityIsLessThan: array2Bits( + extendedSignalArray( + signalTriples.map((x) => x[2]), + paramNumericValues, + 0n + ) + ) + }; +} + function compileCommonNumericValues< ObjInput extends POD | GPCRevealedObjectClaims >( @@ -501,10 +570,10 @@ function compileCommonNumericValues< numericMinValues: CircuitSignal[]; numericMaxValues: CircuitSignal[]; } { - // Arrange POD entry identifiers according to {@link podEntryIdentifierCompare}. - const numericValueIdOrder = ( - Object.keys(boundsCheckConfig) as PODEntryIdentifier[] - ).sort(podEntryIdentifierCompare); + // POD entry identifiers arranged according to {@link podEntryIdentifierCompare}. + const numericValueIdOrder = Object.keys( + boundsCheckConfig + ) as PODEntryIdentifier[]; // Compile signals const unpaddedNumericValueSignals = numericValueIdOrder.flatMap((entryId) => { @@ -1104,6 +1173,14 @@ export function compileVerifyConfig( circuitDesc.maxNumericValues ); + // Create entry inequality inputs. + const circuitEntryInequalityInputs = compileCommonEntryInequalities( + boundsCheckConfig, + entryMap, + circuitDesc.maxNumericValues, + circuitDesc.maxEntryInequalities + ); + // Create subset of inputs for multituple module padded to max size. const circuitMultiTupleInputs = compileProofMultiTuples( tupleMap, @@ -1157,6 +1234,7 @@ export function compileVerifyConfig( ...circuitOwnerV3Inputs, ...circuitOwnerV4Inputs, ...circuitNumericValueInputs, + ...circuitEntryInequalityInputs, ...circuitMultiTupleInputs, ...circuitListMembershipInputs, ...circuitUniquenessInputs, diff --git a/packages/lib/gpc/src/gpcUtil.ts b/packages/lib/gpc/src/gpcUtil.ts index c6bd2f26dd..4ad4630d70 100644 --- a/packages/lib/gpc/src/gpcUtil.ts +++ b/packages/lib/gpc/src/gpcUtil.ts @@ -20,6 +20,7 @@ import { GPCProofEntryBoundsCheckConfig, GPCProofEntryConfig, GPCProofEntryConfigCommon, + GPCProofEntryInequalityConfig, GPCProofObjectConfig, GPCProofTupleConfig, PODEntryIdentifier, @@ -180,6 +181,8 @@ export function canonicalizeEntryConfig( proofEntryConfig.inRange, proofEntryConfig.notInRange ); + const canonicalizedEntryInequalityConfig = + canonicalizeEntryInequalityConfig(proofEntryConfig); // Set optional fields only when they have non-default values. return { isRevealed: proofEntryConfig.isRevealed, @@ -194,6 +197,7 @@ export function canonicalizeEntryConfig( ? { notEqualsEntry: proofEntryConfig.notEqualsEntry } : {}), ...canonicalizedBoundsCheckConfig, + ...canonicalizedEntryInequalityConfig, ...(proofEntryConfig.isMemberOf !== undefined ? { isMemberOf: proofEntryConfig.isMemberOf @@ -260,6 +264,25 @@ export function canonicalizeBoundsCheckConfig( }; } +export function canonicalizeEntryInequalityConfig( + proofEntryConfig: GPCProofEntryInequalityConfig +): GPCProofEntryInequalityConfig { + return { + ...(proofEntryConfig.lessThan + ? { lessThan: proofEntryConfig.lessThan } + : {}), + ...(proofEntryConfig.lessThanEq + ? { lessThanEq: proofEntryConfig.lessThanEq } + : {}), + ...(proofEntryConfig.greaterThan + ? { greaterThan: proofEntryConfig.greaterThan } + : {}), + ...(proofEntryConfig.greaterThanEq + ? { greaterThanEq: proofEntryConfig.greaterThanEq } + : {}) + }; +} + function canonicalizeTupleConfig( tupleRecord: Record ): Record { @@ -618,6 +641,11 @@ export type GPCRequirements = { */ nNumericValues: number; + /** + * Number of entry inequalities. + */ + nEntryInequalities: number; + /** * Number of lists to be included in proof. */ @@ -652,6 +680,7 @@ export function GPCRequirements( nEntries: number, merkleMaxDepth: number, nNumericValues: number = 0, + nEntryInequalities: number = 0, nLists: number = 0, maxListSize: number = 0, tupleArities: Record = {}, @@ -663,6 +692,7 @@ export function GPCRequirements( nEntries, merkleMaxDepth, nNumericValues, + nEntryInequalities, nLists, maxListSize, tupleArities, @@ -720,7 +750,8 @@ export type ListConfig = { }; /** - * Determines the bounds check configuration from the proof configuration. + * Determines the bounds check configuration from the proof configuration sorted + * in lexicographic order. * * Bounds checks are indicated in each entry field via the optional property * `inRange`, which specifies (public) constant upper and lower bounds. This @@ -735,25 +766,29 @@ export function boundsCheckConfigFromProofConfig( proofConfig: GPCProofConfig ): GPCProofBoundsCheckConfig { return Object.fromEntries( - Object.entries(proofConfig.pods).flatMap(([podName, podConfig]) => - Object.entries(podConfig.entries).flatMap(([entryName, entryConfig]) => { - return !(entryConfig.inRange || entryConfig.notInRange) - ? [] - : [ - [ - `${podName}.${entryName}`, - { - ...(entryConfig.inRange - ? { inRange: entryConfig.inRange } - : {}), - ...(entryConfig.notInRange - ? { notInRange: entryConfig.notInRange } - : {}) - } - ] - ]; - }) - ) + ( + Object.entries(proofConfig.pods).flatMap(([podName, podConfig]) => + Object.entries(podConfig.entries).flatMap( + ([entryName, entryConfig]) => { + return !(entryConfig.inRange || entryConfig.notInRange) + ? [] + : [ + [ + `${podName}.${entryName}`, + { + ...(entryConfig.inRange + ? { inRange: entryConfig.inRange } + : {}), + ...(entryConfig.notInRange + ? { notInRange: entryConfig.notInRange } + : {}) + } + ] + ]; + } + ) + ) as [PODEntryIdentifier, GPCProofEntryBoundsCheckConfig][] + ).sort((x, y) => podEntryIdentifierCompare(x[0], y[0])) ); } diff --git a/packages/lib/gpc/test/gpcChecks.spec.ts b/packages/lib/gpc/test/gpcChecks.spec.ts index abc14451dd..55fa9d7993 100644 --- a/packages/lib/gpc/test/gpcChecks.spec.ts +++ b/packages/lib/gpc/test/gpcChecks.spec.ts @@ -24,6 +24,7 @@ describe("Proof entry config check should work", () => { expect(checkProofEntryConfig(entryName, entryConfig)).to.deep.equal({ hasOwnerV3Check: false, hasOwnerV4Check: false, + inequalityChecks: {}, nBoundsChecks: 0 }); }); @@ -40,6 +41,7 @@ describe("Proof entry config check should work", () => { expect(checkProofEntryConfig(entryName, entryConfig)).to.deep.equal({ hasOwnerV3Check: false, hasOwnerV4Check: false, + inequalityChecks: {}, nBoundsChecks: 2 }); }); @@ -53,6 +55,7 @@ describe("Proof entry config check should work", () => { expect(checkProofEntryConfig(entryName, entryConfig)).to.deep.equal({ hasOwnerV3Check: true, hasOwnerV4Check: false, + inequalityChecks: {}, nBoundsChecks: 0 }); }); @@ -66,6 +69,7 @@ describe("Proof entry config check should work", () => { expect(checkProofEntryConfig(entryName, entryConfig)).to.deep.equal({ hasOwnerV3Check: false, hasOwnerV4Check: true, + inequalityChecks: {}, nBoundsChecks: 0 }); }); From 4c49ef83cdeaf8f9ef7841022f3800f53b97e1c5 Mon Sep 17 00:00:00 2001 From: Ahmad Date: Fri, 20 Sep 2024 15:52:29 +0200 Subject: [PATCH 09/11] Tests --- packages/lib/gpc/src/gpcChecks.ts | 12 +- packages/lib/gpc/src/gpcCompile.ts | 9 +- packages/lib/gpc/test/gpc.spec.ts | 122 +++++++++- packages/lib/gpc/test/gpcChecks.spec.ts | 190 ++++++++++++++- packages/lib/gpc/test/gpcCompile.spec.ts | 221 +++++++++++++++++- packages/lib/gpc/test/gpcUtil.spec.ts | 42 +++- .../circuits/entry-inequality.circom | 11 +- .../lib/gpcircuits/circuits/gpc-util.circom | 38 --- 8 files changed, 582 insertions(+), 63 deletions(-) diff --git a/packages/lib/gpc/src/gpcChecks.ts b/packages/lib/gpc/src/gpcChecks.ts index 00ef23c82e..dacec6165c 100644 --- a/packages/lib/gpc/src/gpcChecks.ts +++ b/packages/lib/gpc/src/gpcChecks.ts @@ -404,14 +404,12 @@ export function checkProofEntryInequalityConfig( (ineqCheck: string): [string, PODEntryIdentifier][] => { const otherEntryIdentifier = entryConfig[ineqCheck as keyof typeof entryConfig]; - if (otherEntryIdentifier !== undefined) { // The other entry identifier should be valid. checkPODEntryIdentifier( `${nameForErrorMessages}.${ineqCheck}`, otherEntryIdentifier ); - return [[ineqCheck, otherEntryIdentifier]]; } else { return []; @@ -425,14 +423,14 @@ export function checkProofBoundsCheckConfigForEntryInequalityConfig( boundsChecks: Record, entryInequalityChecks: Record< PODEntryIdentifier, - Record + GPCProofEntryInequalityConfig > ): void { const inequalityCheckedEntries = uniq( Object.keys(entryInequalityChecks).concat( - Object.values(entryInequalityChecks) - .map((inequalityChecks) => Object.values(inequalityChecks)) - .flat() + Object.values(entryInequalityChecks).flatMap((inequalityChecks) => + Object.values(inequalityChecks) + ) ) ) as PODEntryIdentifier[]; for (const entryIdentifier of inequalityCheckedEntries) { @@ -810,7 +808,7 @@ export function checkProofBoundsCheckInputsForConfig( export function checkProofEntryInequalityInputsForConfig( entryName: PODEntryIdentifier, - entryConfig: GPCProofEntryConfig, + entryConfig: GPCProofEntryInequalityConfig, entryValue: PODValue, pods: Record ): void { diff --git a/packages/lib/gpc/src/gpcCompile.ts b/packages/lib/gpc/src/gpcCompile.ts index d452d6c7ef..818db533df 100644 --- a/packages/lib/gpc/src/gpcCompile.ts +++ b/packages/lib/gpc/src/gpcCompile.ts @@ -362,7 +362,6 @@ export function compileProofConfig( const circuitEntryInequalityInputs = compileCommonEntryInequalities( boundsCheckConfig, entryMap, - circuitDesc.maxNumericValues, circuitDesc.maxEntryInequalities ); @@ -497,11 +496,10 @@ function compileProofNumericValues( } export function compileCommonEntryInequalities< - ObjInput extends POD | GPCRevealedObjectClaims + CompilerEntryInfo extends { entryConfig: GPCProofEntryConfig } >( boundsCheckConfig: GPCProofBoundsCheckConfig, - entryMap: Map>, - paramNumericValues: number, + entryMap: Map, paramEntryInequalities: number ): { entryInequalityValueIndex: CircuitSignal[]; @@ -550,7 +548,7 @@ export function compileCommonEntryInequalities< entryInequalityIsLessThan: array2Bits( extendedSignalArray( signalTriples.map((x) => x[2]), - paramNumericValues, + paramEntryInequalities, 0n ) ) @@ -1177,7 +1175,6 @@ export function compileVerifyConfig( const circuitEntryInequalityInputs = compileCommonEntryInequalities( boundsCheckConfig, entryMap, - circuitDesc.maxNumericValues, circuitDesc.maxEntryInequalities ); diff --git a/packages/lib/gpc/test/gpc.spec.ts b/packages/lib/gpc/test/gpc.spec.ts index 8562a61cff..fd23e61ad4 100644 --- a/packages/lib/gpc/test/gpc.spec.ts +++ b/packages/lib/gpc/test/gpc.spec.ts @@ -5,6 +5,7 @@ import { PODEdDSAPublicKeyValue, PODValue, PODValueTuple, + POD_INT_MAX, POD_INT_MIN } from "@pcd/pod"; import { expect } from "chai"; @@ -633,6 +634,36 @@ describe("gpc library (Compiled test artifacts) should work", async function () expect(isVerified).to.be.true; }); + it("should prove and verify entry inequality checks", async function () { + const { isVerified } = await gpcProofTest( + { + pods: { + pod1: { + ...typicalProofConfig.pods.pod1, + entries: { + ...typicalProofConfig.pods.pod1.entries, + A: { + isRevealed: true, + inRange: { min: POD_INT_MIN, max: POD_INT_MAX } + }, + G: { + isRevealed: false, + notInRange: { + min: 0n, + max: 6n + }, + lessThan: "pod1.A" + } + } + } + } + }, + typicalProofInputs, + expectedRevealedClaimsForTypicalCase + ); + expect(isVerified).to.be.true; + }); + it("should prove and verify a complex case", async function () { const pod1 = POD.sign(sampleEntries, privateKey); const pod2 = POD.sign(sampleEntries2, privateKey2); @@ -664,9 +695,14 @@ describe("gpc library (Compiled test artifacts) should work", async function () A: { isRevealed: false, inRange: { min: 100n, max: 132n }, - notInRange: { min: 105n, max: 110n } + notInRange: { min: 105n, max: 110n }, + greaterThanEq: "pod1.G" + }, + G: { + isRevealed: true, + notInRange: { min: POD_INT_MIN, max: 0n }, + greaterThan: "pod1.K" }, - G: { isRevealed: true, notInRange: { min: POD_INT_MIN, max: 0n } }, K: { isRevealed: false, inRange: { min: -10n, max: 0n } }, otherTicketID: { isRevealed: false }, owner: { isRevealed: false, isOwnerID: SEMAPHORE_V3 } @@ -811,6 +847,36 @@ describe("gpc library (Compiled test artifacts) should work", async function () "Must prove at least one entry in object" ); + await expectAsyncError( + async () => { + const pod1 = POD.sign(sampleEntries, privateKey); + await gpcProve( + { + pods: { + pod1: { + entries: { + A: { + isRevealed: false, + inRange: { min: POD_INT_MIN, max: POD_INT_MAX }, + greaterThan: "pod2.H" + } + } + }, + pod2: { + entries: { + H: { isRevealed: false } + } + } + } + }, + { pods: { pod1, pod2: pod1 } }, + GPC_TEST_ARTIFACTS_PATH + ); + }, + "Error", + "Entry pod2.H requires a bounds check to be used in an entry inequality." + ); + await expectAsyncError( async () => { await gpcProve( @@ -1374,6 +1440,35 @@ describe("gpc library (Compiled test artifacts) should work", async function () revealedClaims: revealedClaims3 } = await gpcProve(proofConfig3, proofInputs3, GPC_TEST_ARTIFACTS_PATH); + // Proof data with bounds checks + const proofConfig4: GPCProofConfig = { + pods: { + pod1: { + entries: { + A: { + isRevealed: false, + inRange: { min: POD_INT_MIN, max: POD_INT_MAX }, + greaterThan: "pod2.H" + } + } + }, + pod2: { + entries: { + H: { + isRevealed: false, + inRange: { min: POD_INT_MIN, max: POD_INT_MAX } + } + } + } + } + }; + const proofInputs4 = { pods: { pod1, pod2: pod1 } }; + const { + proof: proof4, + boundConfig: boundConfig4, + revealedClaims: revealedClaims4 + } = await gpcProve(proofConfig4, proofInputs4, GPC_TEST_ARTIFACTS_PATH); + // Tamper with proof let isVerified = await gpcVerify( { ...proof, pi_a: [proof.pi_a[0] + 1, proof.pi_a[1]] }, @@ -1473,6 +1568,29 @@ describe("gpc library (Compiled test artifacts) should work", async function () ); expect(isVerified).to.be.false; + // Tamper with entry inequality + isVerified = await gpcVerify( + proof4, + { + ...boundConfig4, + pods: { + ...boundConfig4.pods, + pod1: { + entries: { + A: { + isRevealed: false, + inRange: { min: POD_INT_MIN, max: POD_INT_MAX }, + lessThan: "pod2.H" + } + } + } + } + }, + revealedClaims4, + GPC_TEST_ARTIFACTS_PATH + ); + expect(isVerified).to.be.false; + // Tamper with POD uniqueness flag isVerified = await gpcVerify( proof3, diff --git a/packages/lib/gpc/test/gpcChecks.spec.ts b/packages/lib/gpc/test/gpcChecks.spec.ts index 55fa9d7993..ef59a24c99 100644 --- a/packages/lib/gpc/test/gpcChecks.spec.ts +++ b/packages/lib/gpc/test/gpcChecks.spec.ts @@ -8,11 +8,18 @@ import { } from "@pcd/pod"; import { expect } from "chai"; import "mocha"; -import { GPCProofEntryBoundsCheckConfig, GPCProofEntryConfig } from "../src"; import { + GPCProofEntryBoundsCheckConfig, + GPCProofEntryConfig, + GPCProofEntryInequalityConfig, + PODEntryIdentifier +} from "../src"; +import { + checkProofBoundsCheckConfigForEntryInequalityConfig, checkProofBoundsCheckInputsForConfig, checkProofEntryBoundsCheckConfig, checkProofEntryConfig, + checkProofEntryInequalityInputsForConfig, checkProofPODUniquenessInputsForConfig } from "../src/gpcChecks"; import { privateKey, sampleEntries, sampleEntries2 } from "./common"; @@ -36,12 +43,13 @@ describe("Proof entry config check should work", () => { isMemberOf: "someList", inRange: { min: 0n, max: 100n }, notInRange: { min: 10n, max: 30n }, - equalsEntry: "someOtherPOD.someOtherEntry" + equalsEntry: "someOtherPOD.someOtherEntry", + lessThan: "someOtherPOD.anotherEntry" }; expect(checkProofEntryConfig(entryName, entryConfig)).to.deep.equal({ hasOwnerV3Check: false, hasOwnerV4Check: false, - inequalityChecks: {}, + inequalityChecks: { lessThan: "someOtherPOD.anotherEntry" }, nBoundsChecks: 2 }); }); @@ -299,4 +307,180 @@ describe("Proof config check against input for POD uniqueness should work", () = } }); }); + +describe("Proof entry inequality config check against bounds check config should work", () => { + const boundsChecks = { + "pod1.a": 1, + "pod2.someEntry": 2, + "pod2.someOtherEntry": 1, + "pod3.entry": 1, + "pod4.a": 2, + "pod4.b": 1 + }; + it("should pass for no entry inequalities", () => { + // Without bounds checks + expect(() => checkProofBoundsCheckConfigForEntryInequalityConfig({}, {})).to + .not.throw; + // With bounds checks + expect(() => + checkProofBoundsCheckConfigForEntryInequalityConfig(boundsChecks, {}) + ).to.not.throw; + }); + it("should pass for entry inequalities with corresponding bounds checks", () => { + const entryInequalityCheckCombos: Record< + PODEntryIdentifier, + GPCProofEntryInequalityConfig + >[] = [ + { "pod1.a": { greaterThan: "pod3.entry" } }, + { + "pod1.a": { greaterThan: "pod3.entry" }, + "pod3.entry": { lessThanEq: "pod4.b" } + }, + { + "pod1.a": { greaterThan: "pod2.someEntry" }, + "pod2.someEntry": { lessThanEq: "pod1.a" }, + "pod2.someOtherEntry": { greaterThanEq: "pod3.entry" }, + "pod3.entry": { lessThanEq: "pod2.someEntry" } + }, + { + "pod1.a": { lessThan: "pod4.a", greaterThan: "pod2.someEntry" }, + "pod2.someEntry": { lessThanEq: "pod1.a" }, + "pod2.someOtherEntry": { + lessThan: "pod4.b", + greaterThan: "pod4.a", + greaterThanEq: "pod3.entry" + }, + "pod3.entry": { + lessThan: "pod4.b", + greaterThan: "pod4.a", + greaterThanEq: "pod1.a", + lessThanEq: "pod2.someEntry" + } + } + ]; + for (const entryInequalityChecks of entryInequalityCheckCombos) { + expect(() => + checkProofBoundsCheckConfigForEntryInequalityConfig( + boundsChecks, + entryInequalityChecks + ) + ).to.not.throw; + } + }); + it("should throw for entry inequalities without corresponding bounds checks", () => { + const entryInequalityCheckCombos: Record< + PODEntryIdentifier, + GPCProofEntryInequalityConfig + >[] = [ + { "pod1.a": { greaterThan: "pod3.entre" } }, + { + "pod1.a": { greaterThan: "pod3.entry" }, + "pod3.entry": { lessThanEq: "pod4.bee" } + }, + { + "pod1.a": { greaterThan: "pod2.someEntry" }, + "pod2.someEntry": { lessThanEq: "pod1.ay" }, + "pod2.someOtherEntry": { greaterThanEq: "pod3.entry" }, + "pod3.entry": { lessThanEq: "pod2.someEntry" } + }, + { + "pod1.a": { lessThan: "pod4.a", greaterThan: "pod2.someEntry" }, + "pod2.someEntry": { lessThanEq: "pod1.a" }, + "pod2.someOtherEntry": { + lessThan: "pod4.b", + greaterThan: "pod4.a", + greaterThanEq: "pod3.entry" + }, + "pod3.entry": { + lessThan: "pod4.b", + greaterThan: "pod4.ay", + greaterThanEq: "pod1.a", + lessThanEq: "pod2.someEntre" + } + } + ]; + for (const entryInequalityChecks of entryInequalityCheckCombos) { + expect(() => + checkProofBoundsCheckConfigForEntryInequalityConfig( + boundsChecks, + entryInequalityChecks + ) + ).to.throw; + } + }); +}); + +describe("Proof entry inequality config check against inputs should work", () => { + const pod1 = POD.sign(sampleEntries, privateKey); + const pod2 = pod1; + const pods = { pod1, pod2 }; + it("should pass for no entry inequality check", () => { + expect(() => + checkProofEntryInequalityInputsForConfig( + "pod1.A", + {}, + { type: "int", value: 123n }, + pods + ) + ).to.not.throw; + }); + it("should pass for simple inequality checks with valid input", () => { + const entryInequalityConfigs: GPCProofEntryInequalityConfig[] = [ + { lessThan: "pod1.B" }, + { greaterThan: "pod1.G" }, + { lessThanEq: "pod2.A" }, + { greaterThanEq: "pod1.H" } + ]; + for (const entryInequalityConfig of entryInequalityConfigs) { + expect(() => + checkProofEntryInequalityInputsForConfig( + "pod2.A", + entryInequalityConfig, + { type: "int", value: 123n }, + pods + ) + ).to.not.throw; + } + }); + it("should pass for more complex inequality checks with valid input", () => { + const entryInequalityConfigs: GPCProofEntryInequalityConfig[] = [ + { lessThan: "pod1.B", greaterThan: "pod1.G" }, + { lessThan: "pod1.B", greaterThan: "pod1.G", lessThanEq: "pod2.A" }, + { + lessThan: "pod1.B", + greaterThan: "pod1.G", + lessThanEq: "pod2.A", + greaterThanEq: "pod1.H" + } + ]; + for (const entryInequalityConfig of entryInequalityConfigs) { + expect(() => + checkProofEntryInequalityInputsForConfig( + "pod2.A", + entryInequalityConfig, + { type: "int", value: 123n }, + pods + ) + ).to.not.throw; + } + }); + it("should throw for inequality checks with invalid input", () => { + const entryInequalityConfigs: GPCProofEntryInequalityConfig[] = [ + { lessThan: "pod1.G" }, + { lessThan: "pod1.B", greaterThan: "pod2.B" }, + { greaterThan: "pod1.H", lessThanEq: "pod2.K" }, + { greaterThan: "pod1.H", greaterThanEq: "pod1.B" } + ]; + for (const entryInequalityConfig of entryInequalityConfigs) { + expect(() => + checkProofEntryInequalityInputsForConfig( + "pod2.A", + entryInequalityConfig, + { type: "int", value: 123n }, + pods + ) + ).to.throw; + } + }); +}); // TODO(POD-P3): More tests diff --git a/packages/lib/gpc/test/gpcCompile.spec.ts b/packages/lib/gpc/test/gpcCompile.spec.ts index 729bb7c82d..b6725daad1 100644 --- a/packages/lib/gpc/test/gpcCompile.spec.ts +++ b/packages/lib/gpc/test/gpcCompile.spec.ts @@ -1,5 +1,9 @@ -import { CircuitSignal } from "@pcd/gpcircuits"; -import { PODValue } from "@pcd/pod"; +import { + CircuitSignal, + array2Bits, + extendedSignalArray +} from "@pcd/gpcircuits"; +import { PODValue, POD_INT_MAX, POD_INT_MIN } from "@pcd/pod"; import { BABY_JUB_NEGATIVE_ONE, BABY_JUB_SUBGROUP_ORDER_MINUS_ONE @@ -8,13 +12,19 @@ import { expect } from "chai"; import "mocha"; import { poseidon2 } from "poseidon-lite/poseidon2"; import { + GPCProofEntryBoundsCheckConfig, + GPCProofEntryConfig, + PODEntryIdentifier +} from "../src"; +import { + compileCommonEntryInequalities, compileProofOwnerV3, compileProofOwnerV4, compileProofPODUniqueness, compileVerifyOwnerV3, compileVerifyOwnerV4 } from "../src/gpcCompile"; -import { makeWatermarkSignal } from "../src/gpcUtil"; +import { GPCProofBoundsCheckConfig, makeWatermarkSignal } from "../src/gpcUtil"; import { ownerIdentity, ownerIdentityV4 } from "./common"; describe("Semaphore V3 owner module compilation for proving should work", () => { @@ -249,4 +259,209 @@ describe("POD uniqueness module compilation for proving and verification should } }); }); + +describe("POD entry inequality module compilation for proving and verification should work", () => { + const typicalEntryIdentifiers: PODEntryIdentifier[] = [ + "pod1.a", + "pod2.someEntry", + "pod2.someOtherEntry", + "pod3.entry", + "pod4.a", + "pod4.b" + ]; + const typicalBoundsCheckConfigPairs = typicalEntryIdentifiers.map( + (entryId) => [entryId, { min: POD_INT_MIN, max: POD_INT_MAX }] + ) as [PODEntryIdentifier, GPCProofEntryBoundsCheckConfig][]; + const typicalEntryIdConfigPairs: [ + PODEntryIdentifier, + { entryConfig: GPCProofEntryConfig } + ][] = typicalEntryIdentifiers.map((entryId) => [ + entryId, + { entryConfig: { isRevealed: false } } + ]); + it("should work as expected for no entry inequality checks", () => { + for (const [paramNumericValues, paramEntryInequalities] of [ + [0, 0], + [2, 0], + [3, 3] + ]) { + const boundsCheckConfig = {}; + const entryMap = new Map(typicalEntryIdConfigPairs); + const entryIneqSignals = compileCommonEntryInequalities( + boundsCheckConfig, + entryMap, + paramEntryInequalities + ); + expect(entryIneqSignals).to.deep.eq({ + entryInequalityValueIndex: extendedSignalArray( + [], + paramEntryInequalities, + 0n + ), + entryInequalityOtherValueIndex: extendedSignalArray( + [], + paramEntryInequalities, + 0n + ), + entryInequalityIsLessThan: 0n + }); + } + }); + it("should work as expected with <=1 entry inequality check per entry", () => { + const entryIdConfigPairsWithIneq: [ + PODEntryIdentifier, + { entryConfig: GPCProofEntryConfig } + ][] = [ + [ + "pod1.a", + { entryConfig: { isRevealed: false, greaterThan: "pod2.someEntry" } } + ], + [ + "pod2.someEntry", + { entryConfig: { isRevealed: false, lessThanEq: "pod1.a" } } + ], + [ + "pod2.someOtherEntry", + { + entryConfig: { isRevealed: false, greaterThanEq: "pod3.entry" } + } + ], + [ + "pod3.entry", + { + entryConfig: { isRevealed: false, lessThanEq: "pod2.someEntry" } + } + ] + ]; + for (const paramEntryInequalities of [1, 2, 3, 4, 6]) { + const boundsCheckConfig: GPCProofBoundsCheckConfig = Object.fromEntries( + typicalBoundsCheckConfigPairs + ); + const entryMap = new Map( + typicalEntryIdConfigPairs.concat( + entryIdConfigPairsWithIneq.slice(0, paramEntryInequalities) + ) + ); + const entryIneqSignals = compileCommonEntryInequalities( + boundsCheckConfig, + entryMap, + paramEntryInequalities + ); + expect(entryIneqSignals).to.deep.eq({ + entryInequalityValueIndex: extendedSignalArray( + [1n, 0n, 2n, 1n].slice(0, paramEntryInequalities), + paramEntryInequalities, + 0n + ), + entryInequalityOtherValueIndex: extendedSignalArray( + [0n, 1n, 3n, 3n].slice(0, paramEntryInequalities), + paramEntryInequalities, + 0n + ), + entryInequalityIsLessThan: array2Bits( + extendedSignalArray( + [1n, 0n, 0n, 0n].slice(0, paramEntryInequalities), + paramEntryInequalities, + 0n + ) + ) + }); + } + }); + it("should work as expected with more complex entry inequality checks", () => { + const entryIdConfigPairsWithIneq: [ + PODEntryIdentifier, + { entryConfig: GPCProofEntryConfig } + ][] = [ + [ + "pod1.a", + { + entryConfig: { + isRevealed: false, + lessThan: "pod4.a", + greaterThan: "pod2.someEntry" + } + } + ], + [ + "pod2.someEntry", + { entryConfig: { isRevealed: false, lessThanEq: "pod1.a" } } + ], + [ + "pod2.someOtherEntry", + { + entryConfig: { + isRevealed: false, + lessThan: "pod4.b", + greaterThan: "pod4.a", + greaterThanEq: "pod3.entry" + } + } + ], + [ + "pod3.entry", + { + entryConfig: { + isRevealed: false, + lessThan: "pod4.b", + greaterThan: "pod4.a", + greaterThanEq: "pod1.a", + lessThanEq: "pod2.someEntry" + } + } + ] + ]; + for (const paramEntryInequalities of [2, 3, 6, 10, 12]) { + const numEntriesWithChecks = + paramEntryInequalities < 3 + ? 1 + : paramEntryInequalities < 6 + ? 2 + : paramEntryInequalities < 10 + ? 3 + : 4; + const boundsCheckConfig: GPCProofBoundsCheckConfig = Object.fromEntries( + typicalBoundsCheckConfigPairs + ); + const entryMap = new Map( + typicalEntryIdConfigPairs.concat( + entryIdConfigPairsWithIneq.slice(0, numEntriesWithChecks) + ) + ); + const entryIneqSignals = compileCommonEntryInequalities( + boundsCheckConfig, + entryMap, + paramEntryInequalities + ); + expect(entryIneqSignals).to.deep.eq({ + entryInequalityValueIndex: extendedSignalArray( + [0n, 1n, 0n, 2n, 4n, 2n, 3n, 1n, 4n, 3n].slice( + 0, + paramEntryInequalities + ), + paramEntryInequalities, + 0n + ), + entryInequalityOtherValueIndex: extendedSignalArray( + [4n, 0n, 1n, 5n, 2n, 3n, 5n, 3n, 3n, 0n].slice( + 0, + paramEntryInequalities + ), + paramEntryInequalities, + 0n + ), + entryInequalityIsLessThan: array2Bits( + extendedSignalArray( + [1n, 1n, 0n, 1n, 1n, 0n, 1n, 0n, 1n, 0n].slice( + 0, + paramEntryInequalities + ), + paramEntryInequalities, + 0n + ) + ) + }); + } + }); +}); // TODO(POD-P3): More tests diff --git a/packages/lib/gpc/test/gpcUtil.spec.ts b/packages/lib/gpc/test/gpcUtil.spec.ts index 5c045d58de..730ca545b3 100644 --- a/packages/lib/gpc/test/gpcUtil.spec.ts +++ b/packages/lib/gpc/test/gpcUtil.spec.ts @@ -5,12 +5,14 @@ import { GPCProofConfig, GPCProofEntryBoundsCheckConfig, GPCProofEntryConfig, - GPCProofEntryConfigCommon + GPCProofEntryConfigCommon, + GPCProofEntryInequalityConfig } from "../src"; import { boundsCheckConfigFromProofConfig, canonicalizeBoundsCheckConfig, canonicalizeEntryConfig, + canonicalizeEntryInequalityConfig, canonicalizePODUniquenessConfig, canonicalizeVirtualEntryConfig } from "../src/gpcUtil"; @@ -87,7 +89,22 @@ describe("Object entry configuration canonicalization should work", () => { const canonicalizedConfig = canonicalizeEntryConfig(config); - expect(canonicalizedConfig).to.deep.eq(canonicalizedConfig); + expect(canonicalizedConfig).to.deep.eq(config); + }); + it("should work as expected on a POD entry configuration with inequality checks", () => { + const config: GPCProofEntryConfig = { + isRevealed: false, + inRange: { min: -512n, max: 25n }, + notInRange: { min: -256n, max: -5n }, + lessThan: "somePOD.a", + greaterThan: "somePOD.c", + greaterThanEq: "somePOD.d", + lessThanEq: "somePOD.b" + }; + + const canonicalizedConfig = canonicalizeEntryConfig(config); + + expect(canonicalizedConfig).to.deep.eq(config); }); }); @@ -294,6 +311,27 @@ describe("Object entry bounds check canonicalization should work", () => { }); }); +describe("Object entry inequality check canonicalization should work", () => { + const configs: GPCProofEntryInequalityConfig[] = [ + { lessThan: "somePOD.a" }, + { greaterThan: "somePOD.a" }, + { lessThanEq: "somePOD.a" }, + { greaterThanEq: "somePOD.a" }, + { greaterThan: "somePOD.a", lessThan: "someOtherPOD.b" }, + { lessThanEq: "somePOD.a", lessThan: "someOtherPOD.b" }, + { + lessThanEq: "somePOD.a", + greaterThan: "somePOD.c", + lessThan: "someOtherPOD.b", + greaterThanEq: "somePOD.d" + } + ]; + const canonicalizedConfigs: GPCProofEntryInequalityConfig[] = configs.map( + canonicalizeEntryInequalityConfig + ); + expect(canonicalizedConfigs).to.deep.equal(configs); +}); + describe("Bounds check configuration derivation works as expected", () => { it("should work as expected on a proof configuration without bounds checks", () => { const proofConfig: GPCProofConfig = { diff --git a/packages/lib/gpcircuits/circuits/entry-inequality.circom b/packages/lib/gpcircuits/circuits/entry-inequality.circom index 18948255bb..e5a1ec132f 100644 --- a/packages/lib/gpcircuits/circuits/entry-inequality.circom +++ b/packages/lib/gpcircuits/circuits/entry-inequality.circom @@ -23,6 +23,13 @@ template EntryInequalityModule( signal input value; signal input otherValue; - // Boolean indicating whether value < otherValue. - signal output out <== LessThan(NUM_BITS)([value, otherValue]); + // Boolean indicating whether value < otherValue. Values are + // shifted to allow for signed POD int values, cf. {@link + // NumericValueModule}. + signal output out <== LessThan(NUM_BITS)( + [ + value + ABS_POD_INT_MIN(), + otherValue + ABS_POD_INT_MIN() + ] + ); } diff --git a/packages/lib/gpcircuits/circuits/gpc-util.circom b/packages/lib/gpcircuits/circuits/gpc-util.circom index c3a5f8c486..a57a1de1c7 100644 --- a/packages/lib/gpcircuits/circuits/gpc-util.circom +++ b/packages/lib/gpcircuits/circuits/gpc-util.circom @@ -82,41 +82,3 @@ template MaybeInputSelector (N) { // `selectedIndex` must have been in [0,...,N-1] or -1. (1 - success)*(selectedIndex + 1) === 0; } - -/** - * Left-rotates elements of a given array by I positions. - */ -template ArrayRotl(I,N) { - signal input in[N]; - signal output out[N]; - - for(var i = 0; i < N; i++) { - out[i] <== in[(i + I)%N]; - } -} - -/** - * Takes the first I elements of a given array and returns the array - * containing those elements. - */ -template Take(I,N) { - signal input in[N]; - signal output out[I]; - - for (var i = 0; i < I; i++) { - out[i] <== in[i]; - } -} - -/** - * Adds a field element to all elements of a given array. - */ -template ArrayAddScalar(N) { - signal input element; - signal input in[N]; - signal output out[N]; - - for(var i = 0; i < N; i++) { - out[i] <== in[i] + element; - } -} From be6eabe2973b804986d33b797b633d717707f25b Mon Sep 17 00:00:00 2001 From: Ahmad Date: Fri, 20 Sep 2024 17:12:13 +0200 Subject: [PATCH 10/11] Lint --- packages/lib/gpc/test/gpcCompile.spec.ts | 6 +- .../lib/gpcircuits/src/entry-inequality.ts | 5 +- .../gpcircuits/test/entry-inequality.spec.ts | 70 +++++++++---------- 3 files changed, 37 insertions(+), 44 deletions(-) diff --git a/packages/lib/gpc/test/gpcCompile.spec.ts b/packages/lib/gpc/test/gpcCompile.spec.ts index b6725daad1..bae86dce5e 100644 --- a/packages/lib/gpc/test/gpcCompile.spec.ts +++ b/packages/lib/gpc/test/gpcCompile.spec.ts @@ -280,11 +280,7 @@ describe("POD entry inequality module compilation for proving and verification s { entryConfig: { isRevealed: false } } ]); it("should work as expected for no entry inequality checks", () => { - for (const [paramNumericValues, paramEntryInequalities] of [ - [0, 0], - [2, 0], - [3, 3] - ]) { + for (const paramEntryInequalities of [0, 2, 6]) { const boundsCheckConfig = {}; const entryMap = new Map(typicalEntryIdConfigPairs); const entryIneqSignals = compileCommonEntryInequalities( diff --git a/packages/lib/gpcircuits/src/entry-inequality.ts b/packages/lib/gpcircuits/src/entry-inequality.ts index be437f7185..c62c6ec7ad 100644 --- a/packages/lib/gpcircuits/src/entry-inequality.ts +++ b/packages/lib/gpcircuits/src/entry-inequality.ts @@ -5,10 +5,7 @@ export type EntryInequalityModuleInputs = { otherValue: CircuitSignal; }; -export type EntryInequalityModuleInputNamesType = [ - "value", - "otherValue" -]; +export type EntryInequalityModuleInputNamesType = ["value", "otherValue"]; export type EntryInequalityModuleOutputs = { out: CircuitSignal }; diff --git a/packages/lib/gpcircuits/test/entry-inequality.spec.ts b/packages/lib/gpcircuits/test/entry-inequality.spec.ts index e3e4f1d931..63d9ff6d34 100644 --- a/packages/lib/gpcircuits/test/entry-inequality.spec.ts +++ b/packages/lib/gpcircuits/test/entry-inequality.spec.ts @@ -1,4 +1,3 @@ -import { BABY_JUB_PRIME as p } from "@pcd/util"; import { WitnessTester } from "circomkit"; import "mocha"; import { @@ -8,43 +7,44 @@ import { import { circomkit } from "./common"; for (const nBits of [32, 64, 96, 128, 252]) { - describe( - "entry-inequality.EntryInequalityModule should work as expected", async function () { - let circuit: WitnessTester< - EntryInequalityModuleInputNamesType, + describe("entry-inequality.EntryInequalityModule should work as expected", async function () { + let circuit: WitnessTester< + EntryInequalityModuleInputNamesType, EntryInequalityModuleOutputNamesType - >; + >; - this.beforeAll(async () => { - circuit = await circomkit.WitnessTester("EntryInequalityModule", { - file: "entry-inequality", - template: "EntryInequalityModule", - params: [nBits] - }); + this.beforeAll(async () => { + circuit = await circomkit.WitnessTester("EntryInequalityModule", { + file: "entry-inequality", + template: "EntryInequalityModule", + params: [nBits] }); + }); - it("should work for " + - nBits + - "-bit unsigned integer values", - async () => { - const twoToTheNBits = 1n << BigInt(nBits); - const sampleValues: [bigint, bigint][] = [ - [0n, 0n], - [2n,3n], - [3n,2n], - [3n, twoToTheNBits / 5n], - [twoToTheNBits / 5n - 1n, (2n * twoToTheNBits) / 5n], - [(2n * twoToTheNBits) / 5n - 5n, (3n * twoToTheNBits) / 5n], - [0n, twoToTheNBits - 1n], - [twoToTheNBits - 2n, twoToTheNBits - 1n], - [twoToTheNBits - 1n, twoToTheNBits - 2n], - [twoToTheNBits - 1n, twoToTheNBits - 1n] - ]; + it( + "should work for " + nBits + "-bit unsigned integer values", + async () => { + const twoToTheNBits = 1n << BigInt(nBits); + const sampleValues: [bigint, bigint][] = [ + [0n, 0n], + [2n, 3n], + [3n, 2n], + [3n, twoToTheNBits / 5n], + [twoToTheNBits / 5n - 1n, (2n * twoToTheNBits) / 5n], + [(2n * twoToTheNBits) / 5n - 5n, (3n * twoToTheNBits) / 5n], + [0n, twoToTheNBits - 1n], + [twoToTheNBits - 2n, twoToTheNBits - 1n], + [twoToTheNBits - 1n, twoToTheNBits - 2n], + [twoToTheNBits - 1n, twoToTheNBits - 1n] + ]; - for(const [value,otherValue] of sampleValues) { - await circuit.expectPass({ value, otherValue }, {out: BigInt(value < otherValue)}); - } - }); - } - ); + for (const [value, otherValue] of sampleValues) { + await circuit.expectPass( + { value, otherValue }, + { out: BigInt(value < otherValue) } + ); + } + } + ); + }); } From f52ee13a9b5a8ba16805063cb75dc4a8063d48a1 Mon Sep 17 00:00:00 2001 From: Ahmad Date: Sat, 21 Sep 2024 02:46:42 +0200 Subject: [PATCH 11/11] Code review --- packages/lib/gpc/src/gpcChecks.ts | 28 ++-- packages/lib/gpc/src/gpcCompile.ts | 97 +++++++----- packages/lib/gpc/src/gpcTypes.ts | 4 +- packages/lib/gpc/src/gpcUtil.ts | 49 +++--- packages/lib/gpc/test/gpcCompile.spec.ts | 43 +++--- packages/lib/gpc/test/gpcUtil.spec.ts | 142 ++++++++++++------ .../lib/gpcircuits/circuits/constants.circom | 14 -- .../circuits/entry-inequality.circom | 19 +-- .../gpcircuits/circuits/numeric-value.circom | 19 ++- .../gpcircuits/circuits/proto-pod-gpc.circom | 11 +- .../lib/gpcircuits/src/entry-inequality.ts | 4 +- .../gpcircuits/test/entry-inequality.spec.ts | 68 +++++---- .../lib/gpcircuits/test/numeric-value.spec.ts | 2 +- 13 files changed, 298 insertions(+), 202 deletions(-) delete mode 100644 packages/lib/gpcircuits/circuits/constants.circom diff --git a/packages/lib/gpc/src/gpcChecks.ts b/packages/lib/gpc/src/gpcChecks.ts index dacec6165c..7d7f97a63d 100644 --- a/packages/lib/gpc/src/gpcChecks.ts +++ b/packages/lib/gpc/src/gpcChecks.ts @@ -207,7 +207,7 @@ export function checkProofConfig(proofConfig: GPCProofConfig): GPCRequirements { } function checkProofObjConfig( - nameForErrorMessages: string, + objName: string, objConfig: GPCProofObjectConfig ): { nEntries: number; @@ -221,7 +221,7 @@ function checkProofObjConfig( } { if (Object.keys(objConfig.entries).length === 0) { throw new TypeError( - `Must prove at least one entry in object "${nameForErrorMessages}".` + `Must prove at least one entry in object "${objName}".` ); } @@ -235,7 +235,7 @@ function checkProofObjConfig( let hasOwnerV4 = false; for (const [entryName, entryConfig] of Object.entries(objConfig.entries)) { checkPODEntryName(entryName, true); - const podEntryIdentifier: PODEntryIdentifier = `${nameForErrorMessages}.${entryName}`; + const podEntryIdentifier: PODEntryIdentifier = `${objName}.${entryName}`; const { nBoundsChecks: nEntryBoundsChecks, hasOwnerV3Check, @@ -253,14 +253,11 @@ function checkProofObjConfig( hasOwnerV4 ||= hasOwnerV4Check; } if (objConfig.contentID !== undefined) { - checkProofEntryConfig( - `${nameForErrorMessages}.$contentID`, - objConfig.contentID - ); + checkProofEntryConfig(`${objName}.$contentID`, objConfig.contentID); } if (objConfig.signerPublicKey !== undefined) { checkProofEntryConfig( - `${nameForErrorMessages}.$signerPublicKey`, + `${objName}.$signerPublicKey`, objConfig.signerPublicKey ); } @@ -396,7 +393,7 @@ export function checkProofEntryBoundsCheckConfig( } export function checkProofEntryInequalityConfig( - nameForErrorMessages: PODEntryIdentifier, + entryIdentifier: PODEntryIdentifier, entryConfig: GPCProofEntryInequalityConfig ): Record { return Object.fromEntries( @@ -407,7 +404,7 @@ export function checkProofEntryInequalityConfig( if (otherEntryIdentifier !== undefined) { // The other entry identifier should be valid. checkPODEntryIdentifier( - `${nameForErrorMessages}.${ineqCheck}`, + `${entryIdentifier}.${ineqCheck}`, otherEntryIdentifier ); return [[ineqCheck, otherEntryIdentifier]]; @@ -835,15 +832,16 @@ export function checkProofEntryInequalityInputsForConfig( for (const [checkType, otherEntry, cmp] of inequalityCheckTriples) { if (otherEntry !== undefined) { - const { objName, entryName: otherEntryName } = + const { objName: otherObjName, entryName: otherEntryName } = splitPODEntryIdentifier(otherEntry); - // Both `entryName` and `otherEntryName` are amongst the bounds-checked - // entries and therefore exist in the input as PODIntValues. - const pod = pods[objName] as POD; + // Since {@link checkProofBoundsCheckInputsForConfig} has passed, both + // `entryName` and `otherEntryName` are amongst the bounds-checked entries + // and therefore exist in the input as PODIntValues. + const otherPOD = pods[otherObjName] as POD; const otherEntryValue = resolvePODEntry( otherEntryName, - pod + otherPOD ) as PODIntValue; if (!cmp(entryValue.value as bigint, otherEntryValue.value)) { throw new Error( diff --git a/packages/lib/gpc/src/gpcCompile.ts b/packages/lib/gpc/src/gpcCompile.ts index 818db533df..8d6e4d01e3 100644 --- a/packages/lib/gpc/src/gpcCompile.ts +++ b/packages/lib/gpc/src/gpcCompile.ts @@ -48,15 +48,16 @@ import { TupleIdentifier } from "./gpcTypes"; import { - GPCProofBoundsCheckConfig, + GPCProofEntryNumericValueConfig, GPCProofMembershipListConfig, + GPCProofNumericValueConfig, LIST_MEMBERSHIP, - boundsCheckConfigFromProofConfig, isTupleIdentifier, isVirtualEntryIdentifier, isVirtualEntryName, listConfigFromProofConfig, - makeWatermarkSignal + makeWatermarkSignal, + numericValueConfigFromProofConfig } from "./gpcUtil"; /** @@ -351,16 +352,16 @@ export function compileProofConfig( ); // Create numeric value module inputs. - const boundsCheckConfig = boundsCheckConfigFromProofConfig(proofConfig); + const numericValueConfig = numericValueConfigFromProofConfig(proofConfig); const circuitNumericValueInputs = compileProofNumericValues( - boundsCheckConfig, + numericValueConfig, entryMap, circuitDesc.maxNumericValues ); // Create entry inequality inputs. const circuitEntryInequalityInputs = compileCommonEntryInequalities( - boundsCheckConfig, + numericValueConfig, entryMap, circuitDesc.maxEntryInequalities ); @@ -437,7 +438,7 @@ function compileProofObject(objInfo: CompilerObjInfo): ObjectModuleInputs { } function compileProofNumericValues( - boundsCheckConfig: GPCProofBoundsCheckConfig, + numericValueConfig: GPCProofNumericValueConfig, entryMap: Map>, paramNumericValues: number ): { @@ -454,7 +455,7 @@ function compileProofNumericValues( numericMinValues, numericMaxValues } = compileCommonNumericValues( - boundsCheckConfig, + numericValueConfig, entryMap, paramNumericValues ); @@ -479,9 +480,9 @@ function compileProofNumericValues( // Duplicate value if both `inRange` and `notInRange` are present for the // entry. - return Object.keys(boundsCheckConfig[entryId]).map( - (_) => entryValue.value - ); + return Object.keys( + numericValueConfig.get(entryId)?.boundsCheckConfig ?? {} + ).map((_) => entryValue.value); } ); @@ -498,7 +499,7 @@ function compileProofNumericValues( export function compileCommonEntryInequalities< CompilerEntryInfo extends { entryConfig: GPCProofEntryConfig } >( - boundsCheckConfig: GPCProofBoundsCheckConfig, + numericValueConfig: GPCProofNumericValueConfig, entryMap: Map, paramEntryInequalities: number ): { @@ -506,32 +507,59 @@ export function compileCommonEntryInequalities< entryInequalityOtherValueIndex: CircuitSignal[]; entryInequalityIsLessThan: CircuitSignal; } { - const numericValueIndex: Record = - Object.fromEntries( - Object.keys(boundsCheckConfig).map((entryIdentifier, i) => [ - entryIdentifier, - BigInt(i) - ]) - ); + // For each numeric value with an entry inequality check, arrange the + // inequality check to one of the form 'a < b', returning a list of triples of + // the form [index of a as numeric value, index of b as numeric value, + // BigInt(a { + Array.from(numericValueConfig.entries()) as [ + PODEntryIdentifier, + GPCProofEntryNumericValueConfig + ][] + ).flatMap(([entryIdentifier, numericValueEntryConfig]): bigint[][] => { const entryConfig = entryMap.get(entryIdentifier) ?.entryConfig as GPCProofEntryConfig; + const entryIndex = numericValueEntryConfig.index; return [ entryConfig.lessThan - ? [[entryIndex, numericValueIndex[entryConfig.lessThan], 1n]] + ? [ + [ + entryIndex, + numericValueConfig.get(entryConfig.lessThan)?.index, + 1n + ] + ] : [], entryConfig.lessThanEq - ? [[numericValueIndex[entryConfig.lessThanEq], entryIndex, 0n]] + ? [ + [ + numericValueConfig.get(entryConfig.lessThanEq)?.index, + entryIndex, + 0n + ] + ] : [], entryConfig.greaterThan - ? [[numericValueIndex[entryConfig.greaterThan], entryIndex, 1n]] + ? [ + [ + numericValueConfig.get(entryConfig.greaterThan)?.index, + entryIndex, + 1n + ] + ] : [], entryConfig.greaterThanEq - ? [[entryIndex, numericValueIndex[entryConfig.greaterThanEq], 0n]] + ? [ + [ + entryIndex, + numericValueConfig.get(entryConfig.greaterThanEq)?.index, + 0n + ] + ] : [] - ].flat(); + ].flat() as [bigint, bigint, bigint][]; }); return { @@ -558,7 +586,7 @@ export function compileCommonEntryInequalities< function compileCommonNumericValues< ObjInput extends POD | GPCRevealedObjectClaims >( - boundsCheckConfig: GPCProofBoundsCheckConfig, + numericValueConfig: GPCProofNumericValueConfig, entryMap: Map>, paramNumericValues: number ): { @@ -569,9 +597,7 @@ function compileCommonNumericValues< numericMaxValues: CircuitSignal[]; } { // POD entry identifiers arranged according to {@link podEntryIdentifierCompare}. - const numericValueIdOrder = Object.keys( - boundsCheckConfig - ) as PODEntryIdentifier[]; + const numericValueIdOrder = Array.from(numericValueConfig.keys()); // Compile signals const unpaddedNumericValueSignals = numericValueIdOrder.flatMap((entryId) => { @@ -582,8 +608,9 @@ function compileCommonNumericValues< throw new ReferenceError(`Missing input for identifier ${entryId}.`); } - const inRange = boundsCheckConfig[entryId].inRange; - const notInRange = boundsCheckConfig[entryId].notInRange; + const inRange = numericValueConfig.get(entryId)?.boundsCheckConfig.inRange; + const notInRange = + numericValueConfig.get(entryId)?.boundsCheckConfig.notInRange; return [ ...(inRange ? [[BigInt(idx), 1n, inRange.min, inRange.max]] : []), @@ -1163,17 +1190,17 @@ export function compileVerifyConfig( ); // Create numeric value module inputs - const boundsCheckConfig = boundsCheckConfigFromProofConfig(verifyConfig); + const numericValueConfig = numericValueConfigFromProofConfig(verifyConfig); const { numericValueIdOrder: _, ...circuitNumericValueInputs } = compileCommonNumericValues( - boundsCheckConfig, + numericValueConfig, entryMap, circuitDesc.maxNumericValues ); // Create entry inequality inputs. const circuitEntryInequalityInputs = compileCommonEntryInequalities( - boundsCheckConfig, + numericValueConfig, entryMap, circuitDesc.maxEntryInequalities ); diff --git a/packages/lib/gpc/src/gpcTypes.ts b/packages/lib/gpc/src/gpcTypes.ts index 86e6cdb2d5..9fa11c79c3 100644 --- a/packages/lib/gpc/src/gpcTypes.ts +++ b/packages/lib/gpc/src/gpcTypes.ts @@ -187,8 +187,8 @@ export type GPCProofEntryBoundsCheckConfig = { /** * Entry inequality configuration for an individual entry. This specifies * inequalities that should be satisfied with respect to other entries. All such - * entries must be bounds-checked to lie in the appropriate range ([POD_INT_MIN, - * POD_INT_MAX]), lest the resulting circuit be underconstrained. + * entries must be bounds-checked to lie in (a subset of) the appropriate range + * ([POD_INT_MIN, POD_INT_MAX]), lest the resulting circuit be underconstrained. */ export type GPCProofEntryInequalityConfig = { /** diff --git a/packages/lib/gpc/src/gpcUtil.ts b/packages/lib/gpc/src/gpcUtil.ts index 4ad4630d70..cb3efe271a 100644 --- a/packages/lib/gpc/src/gpcUtil.ts +++ b/packages/lib/gpc/src/gpcUtil.ts @@ -711,17 +711,26 @@ export const LIST_MEMBERSHIP = "membership"; export const LIST_NONMEMBERSHIP = "non-membership"; /** - * Configuration for bounds checks arranged by entry identifier requiring a - * bounds check. + * Configuration for numeric values containing bounds check configurations and + * indices according to entry identifier arranged in lexicographic order. * * This is deduced from the proof configuration in - * {@link boundsCheckConfigFromProofConfig}. + * {@link numericValueConfigFromProofConfig}. */ -export type GPCProofBoundsCheckConfig = Record< +export type GPCProofNumericValueConfig = Map< PODEntryIdentifier, - GPCProofEntryBoundsCheckConfig + GPCProofEntryNumericValueConfig >; +/** + * Configuration for a single numeric value entry containing a bounds check + * configuration and index, cf. {@link GPCProofNumericValueConfig}. + */ +export type GPCProofEntryNumericValueConfig = { + boundsCheckConfig: GPCProofEntryBoundsCheckConfig; + index: bigint; +}; + /** * Configuration for named lists arranged by identifier requiring a list * (non-)membership check. @@ -750,22 +759,23 @@ export type ListConfig = { }; /** - * Determines the bounds check configuration from the proof configuration sorted - * in lexicographic order. + * Determines the numeric value module configuration from the proof + * configuration sorted by POD entry identifier in lexicographic order. * - * Bounds checks are indicated in each entry field via the optional property - * `inRange`, which specifies (public) constant upper and lower bounds. This - * procedure singles out and arranges these bounds check configurations by entry - * identifier. + * Bounds checks are indicated in each entry field via the optional properties + * `inRange` and `notInRange`, which specify (public) constant upper and lower + * bounds. This procedure singles out these bounds check configurations and + * keeps track of the indices of these numeric values assuming lexicographical + * order with respect to their entry identifiers. * * @param proofConfig the proof configuration - * @returns a record mapping entry identifiers to their bounds check - * configurations + * @returns a map taking an entry identifier to a record containing its bounds + * check configuration and numeric value index. */ -export function boundsCheckConfigFromProofConfig( +export function numericValueConfigFromProofConfig( proofConfig: GPCProofConfig -): GPCProofBoundsCheckConfig { - return Object.fromEntries( +): GPCProofNumericValueConfig { + return new Map( ( Object.entries(proofConfig.pods).flatMap(([podName, podConfig]) => Object.entries(podConfig.entries).flatMap( @@ -788,7 +798,12 @@ export function boundsCheckConfigFromProofConfig( } ) ) as [PODEntryIdentifier, GPCProofEntryBoundsCheckConfig][] - ).sort((x, y) => podEntryIdentifierCompare(x[0], y[0])) + ) + .sort((x, y) => podEntryIdentifierCompare(x[0], y[0])) + .map(([entryIdentifier, boundsCheckConfig], i) => [ + entryIdentifier, + { boundsCheckConfig, index: BigInt(i) } + ]) ); } diff --git a/packages/lib/gpc/test/gpcCompile.spec.ts b/packages/lib/gpc/test/gpcCompile.spec.ts index bae86dce5e..22bc399a17 100644 --- a/packages/lib/gpc/test/gpcCompile.spec.ts +++ b/packages/lib/gpc/test/gpcCompile.spec.ts @@ -11,11 +11,7 @@ import { import { expect } from "chai"; import "mocha"; import { poseidon2 } from "poseidon-lite/poseidon2"; -import { - GPCProofEntryBoundsCheckConfig, - GPCProofEntryConfig, - PODEntryIdentifier -} from "../src"; +import { GPCProofEntryConfig, PODEntryIdentifier } from "../src"; import { compileCommonEntryInequalities, compileProofOwnerV3, @@ -24,7 +20,11 @@ import { compileVerifyOwnerV3, compileVerifyOwnerV4 } from "../src/gpcCompile"; -import { GPCProofBoundsCheckConfig, makeWatermarkSignal } from "../src/gpcUtil"; +import { + GPCProofEntryNumericValueConfig, + GPCProofNumericValueConfig, + makeWatermarkSignal +} from "../src/gpcUtil"; import { ownerIdentity, ownerIdentityV4 } from "./common"; describe("Semaphore V3 owner module compilation for proving should work", () => { @@ -269,9 +269,18 @@ describe("POD entry inequality module compilation for proving and verification s "pod4.a", "pod4.b" ]; - const typicalBoundsCheckConfigPairs = typicalEntryIdentifiers.map( - (entryId) => [entryId, { min: POD_INT_MIN, max: POD_INT_MAX }] - ) as [PODEntryIdentifier, GPCProofEntryBoundsCheckConfig][]; + const typicalNumericValueConfigTriples = typicalEntryIdentifiers.map( + (entryId, i) => [ + entryId, + { + boundsCheckConfig: { min: POD_INT_MIN, max: POD_INT_MAX }, + index: BigInt(i) + } + ] + ) as [PODEntryIdentifier, GPCProofEntryNumericValueConfig][]; + const typicalNumericValueConfig: GPCProofNumericValueConfig = new Map( + typicalNumericValueConfigTriples + ); const typicalEntryIdConfigPairs: [ PODEntryIdentifier, { entryConfig: GPCProofEntryConfig } @@ -281,14 +290,14 @@ describe("POD entry inequality module compilation for proving and verification s ]); it("should work as expected for no entry inequality checks", () => { for (const paramEntryInequalities of [0, 2, 6]) { - const boundsCheckConfig = {}; + const numericValueConfig: GPCProofNumericValueConfig = new Map([]); const entryMap = new Map(typicalEntryIdConfigPairs); const entryIneqSignals = compileCommonEntryInequalities( - boundsCheckConfig, + numericValueConfig, entryMap, paramEntryInequalities ); - expect(entryIneqSignals).to.deep.eq({ + expect(entryIneqSignals).to.deep.equal({ entryInequalityValueIndex: extendedSignalArray( [], paramEntryInequalities, @@ -330,16 +339,13 @@ describe("POD entry inequality module compilation for proving and verification s ] ]; for (const paramEntryInequalities of [1, 2, 3, 4, 6]) { - const boundsCheckConfig: GPCProofBoundsCheckConfig = Object.fromEntries( - typicalBoundsCheckConfigPairs - ); const entryMap = new Map( typicalEntryIdConfigPairs.concat( entryIdConfigPairsWithIneq.slice(0, paramEntryInequalities) ) ); const entryIneqSignals = compileCommonEntryInequalities( - boundsCheckConfig, + typicalNumericValueConfig, entryMap, paramEntryInequalities ); @@ -416,16 +422,13 @@ describe("POD entry inequality module compilation for proving and verification s : paramEntryInequalities < 10 ? 3 : 4; - const boundsCheckConfig: GPCProofBoundsCheckConfig = Object.fromEntries( - typicalBoundsCheckConfigPairs - ); const entryMap = new Map( typicalEntryIdConfigPairs.concat( entryIdConfigPairsWithIneq.slice(0, numEntriesWithChecks) ) ); const entryIneqSignals = compileCommonEntryInequalities( - boundsCheckConfig, + typicalNumericValueConfig, entryMap, paramEntryInequalities ); diff --git a/packages/lib/gpc/test/gpcUtil.spec.ts b/packages/lib/gpc/test/gpcUtil.spec.ts index 730ca545b3..953b7845cf 100644 --- a/packages/lib/gpc/test/gpcUtil.spec.ts +++ b/packages/lib/gpc/test/gpcUtil.spec.ts @@ -9,12 +9,12 @@ import { GPCProofEntryInequalityConfig } from "../src"; import { - boundsCheckConfigFromProofConfig, canonicalizeBoundsCheckConfig, canonicalizeEntryConfig, canonicalizeEntryInequalityConfig, canonicalizePODUniquenessConfig, - canonicalizeVirtualEntryConfig + canonicalizeVirtualEntryConfig, + numericValueConfigFromProofConfig } from "../src/gpcUtil"; describe("Object entry configuration canonicalization should work", () => { @@ -332,7 +332,7 @@ describe("Object entry inequality check canonicalization should work", () => { expect(canonicalizedConfigs).to.deep.equal(configs); }); -describe("Bounds check configuration derivation works as expected", () => { +describe("Numeric value configuration derivation works as expected", () => { it("should work as expected on a proof configuration without bounds checks", () => { const proofConfig: GPCProofConfig = { pods: { @@ -345,8 +345,8 @@ describe("Bounds check configuration derivation works as expected", () => { } } }; - const boundsCheckConfig = boundsCheckConfigFromProofConfig(proofConfig); - expect(boundsCheckConfig).to.deep.eq({}); + const numericValueConfig = numericValueConfigFromProofConfig(proofConfig); + expect(numericValueConfig).to.deep.eq(new Map([])); }); it("should work as expected on a proof configuration with simple bounds checks", () => { const proofConfig: GPCProofConfig = { @@ -377,27 +377,47 @@ describe("Bounds check configuration derivation works as expected", () => { } } }; - const boundsCheckConfig = boundsCheckConfigFromProofConfig(proofConfig); - expect(boundsCheckConfig).to.deep.eq({ - "somePod.A": { - inRange: { - min: 0n, - max: POD_INT_MAX - } - }, - "somePod.B": { - notInRange: { - min: POD_INT_MIN, - max: 87n - } - }, - "someOtherPod.D": { - inRange: { - min: 5n, - max: 25n - } - } - }); + const numericValueConfig = numericValueConfigFromProofConfig(proofConfig); + expect(numericValueConfig).to.deep.eq( + new Map([ + [ + "someOtherPod.D", + { + boundsCheckConfig: { + inRange: { + min: 5n, + max: 25n + } + }, + index: 0n + } + ], + [ + "somePod.A", + { + boundsCheckConfig: { + inRange: { + min: 0n, + max: POD_INT_MAX + } + }, + index: 1n + } + ], + [ + "somePod.B", + { + boundsCheckConfig: { + notInRange: { + min: POD_INT_MIN, + max: 87n + } + }, + index: 2n + } + ] + ]) + ); }); it("should work as expected on a proof configuration with more complex bounds checks", () => { const proofConfig: GPCProofConfig = { @@ -429,28 +449,54 @@ describe("Bounds check configuration derivation works as expected", () => { } } }; - const boundsCheckConfig = boundsCheckConfigFromProofConfig(proofConfig); - expect(boundsCheckConfig).to.deep.eq({ - "somePod.A": { - inRange: { - min: 0n, - max: 24n - } - }, - "someOtherPod.D": { - inRange: { - min: 5n, - max: 30n - } - }, - "someOtherPod.E": { - inRange: { min: -1000n, max: 20n }, - notInRange: { min: -5n, max: 4n } - }, - "someOtherPod.F": { - notInRange: { min: 100n, max: 200n } - } - }); + const numericValueConfig = numericValueConfigFromProofConfig(proofConfig); + expect(numericValueConfig).to.deep.eq( + new Map([ + [ + "someOtherPod.D", + { + boundsCheckConfig: { + inRange: { + min: 5n, + max: 30n + } + }, + index: 0n + } + ], + [ + "someOtherPod.E", + { + boundsCheckConfig: { + inRange: { min: -1000n, max: 20n }, + notInRange: { min: -5n, max: 4n } + }, + index: 1n + } + ], + [ + "someOtherPod.F", + { + boundsCheckConfig: { + notInRange: { min: 100n, max: 200n } + }, + index: 2n + } + ], + [ + "somePod.A", + { + boundsCheckConfig: { + inRange: { + min: 0n, + max: 24n + } + }, + index: 3n + } + ] + ]) + ); }); }); diff --git a/packages/lib/gpcircuits/circuits/constants.circom b/packages/lib/gpcircuits/circuits/constants.circom deleted file mode 100644 index 4e31e3fed8..0000000000 --- a/packages/lib/gpcircuits/circuits/constants.circom +++ /dev/null @@ -1,14 +0,0 @@ -pragma circom 2.1.8; - -// Absolute value of minimum value of a 64-bit signed integer. This -// will be added to all values fed into the bounds check and entry -// inequality modules to convert them to 64-bit unsigned integers -// while preserving order. -function ABS_POD_INT_MIN() { - return 1 << (POD_INT_BITS() - 1); -} - -// Maximum number of bits in a POD int value. -function POD_INT_BITS() { - return 64; -} diff --git a/packages/lib/gpcircuits/circuits/entry-inequality.circom b/packages/lib/gpcircuits/circuits/entry-inequality.circom index e5a1ec132f..11d8ac08a0 100644 --- a/packages/lib/gpcircuits/circuits/entry-inequality.circom +++ b/packages/lib/gpcircuits/circuits/entry-inequality.circom @@ -1,14 +1,13 @@ pragma circom 2.1.8; include "circomlib/circuits/comparators.circom"; -include "constants.circom"; /** - * Module constraining an entry value to be (not) less than another - * entry value. This module should be combined with the numeric value - * module with which its values are shared. Moreover, the numeric - * value module will ensure the validity of these values as well as - * the inequality check circuit. + * Module outputting a boolean indicating whether an entry value to be + * (not) less than another entry value. The output is only valid if + * the values are constrained to be signed `NUM_BITS`-bit integers, + * which is accomplished by the numeric value module with which this + * module's values are shared. * * The module has no explicit enable flag. To disable it, the input * signals should be equal to each other, whence the output should be @@ -23,13 +22,15 @@ template EntryInequalityModule( signal input value; signal input otherValue; + var ABS_POD_INT_MIN = 1 << (NUM_BITS - 1); + // Boolean indicating whether value < otherValue. Values are // shifted to allow for signed POD int values, cf. {@link // NumericValueModule}. - signal output out <== LessThan(NUM_BITS)( + signal output isLessThan <== LessThan(NUM_BITS)( [ - value + ABS_POD_INT_MIN(), - otherValue + ABS_POD_INT_MIN() + value + ABS_POD_INT_MIN, + otherValue + ABS_POD_INT_MIN ] ); } diff --git a/packages/lib/gpcircuits/circuits/numeric-value.circom b/packages/lib/gpcircuits/circuits/numeric-value.circom index c3aef33cc8..0ff7d5b90b 100644 --- a/packages/lib/gpcircuits/circuits/numeric-value.circom +++ b/packages/lib/gpcircuits/circuits/numeric-value.circom @@ -2,7 +2,6 @@ pragma circom 2.1.8; include "circomlib/circuits/poseidon.circom"; include "bounds.circom"; -include "constants.circom"; /** * Module constraining a single entry value of POD object. It proves @@ -12,7 +11,11 @@ include "constants.circom"; * This module has an explicit enable flag. If it is disabled, the bounds * check parameters should take on their default values, viz. 0 and 0. */ -template NumericValueModule() { +template NumericValueModule( + // Number of bits required to represent the inputs. Must be less + // than 252, cf. {@link BoundsCheckModule}. + NUM_BITS +) { // Boolean flag for the value check. Booleanness will be deduced // from the entry index passed to the ProtoPODGPC circuit and thus // checked externally. @@ -33,11 +36,13 @@ template NumericValueModule() { signal input minValue; signal input maxValue; + var ABS_POD_INT_MIN = 1 << (NUM_BITS - 1); + // Check that minValue <= numericValue <= maxValue and return the - // result. - signal output isInBounds <== BoundsCheckModule(POD_INT_BITS())( - numericValue + ABS_POD_INT_MIN(), - minValue + ABS_POD_INT_MIN(), - maxValue + ABS_POD_INT_MIN() + // result. Values are shifted to allow for signed POD int values. + signal output isInBounds <== BoundsCheckModule(NUM_BITS)( + numericValue + ABS_POD_INT_MIN, + minValue + ABS_POD_INT_MIN, + maxValue + ABS_POD_INT_MIN ); } diff --git a/packages/lib/gpcircuits/circuits/proto-pod-gpc.circom b/packages/lib/gpcircuits/circuits/proto-pod-gpc.circom index 0480fb8908..2bbfd1f25b 100644 --- a/packages/lib/gpcircuits/circuits/proto-pod-gpc.circom +++ b/packages/lib/gpcircuits/circuits/proto-pod-gpc.circom @@ -2,7 +2,6 @@ pragma circom 2.1.8; include "circomlib/circuits/gates.circom"; include "circomlib/circuits/poseidon.circom"; -include "constants.circom"; include "entry.circom"; include "entry-inequality.circom"; include "global.circom"; @@ -72,6 +71,12 @@ template ProtoPODGPC ( // enabled. Should be 0 or 1. INCLUDE_OWNERV4 ) { + /** + * Maximum number of bits in a POD int value. Used in the bounds + * check, numeric value and entry inequality modules. + */ + var POD_INT_BITS = 64; + /* * 1+ ObjectModules. Each array corresponds to one input/output for each object module. */ @@ -308,7 +313,7 @@ template ProtoPODGPC ( for (var i = 0; i < MAX_NUMERIC_VALUES; i++) { numericValueBoundsCheck[i] - <== NumericValueModule()( + <== NumericValueModule(POD_INT_BITS)( // Disable value hash check if index is -1. NOT()( IsZero()(numericValueEntryIndices[i] + 1) @@ -348,7 +353,7 @@ template ProtoPODGPC ( for (var i = 0; i < MAX_ENTRY_INEQUALITIES; i++) { entryInequalityCheck[i] - <== EntryInequalityModule(POD_INT_BITS())( + <== EntryInequalityModule(POD_INT_BITS)( InputSelector(MAX_NUMERIC_VALUES)( numericValues, entryInequalityValueIndex[i] diff --git a/packages/lib/gpcircuits/src/entry-inequality.ts b/packages/lib/gpcircuits/src/entry-inequality.ts index c62c6ec7ad..e3e70b0d44 100644 --- a/packages/lib/gpcircuits/src/entry-inequality.ts +++ b/packages/lib/gpcircuits/src/entry-inequality.ts @@ -7,6 +7,6 @@ export type EntryInequalityModuleInputs = { export type EntryInequalityModuleInputNamesType = ["value", "otherValue"]; -export type EntryInequalityModuleOutputs = { out: CircuitSignal }; +export type EntryInequalityModuleOutputs = { isLessThan: CircuitSignal }; -export type EntryInequalityModuleOutputNamesType = ["out"]; +export type EntryInequalityModuleOutputNamesType = ["isLessThan"]; diff --git a/packages/lib/gpcircuits/test/entry-inequality.spec.ts b/packages/lib/gpcircuits/test/entry-inequality.spec.ts index 63d9ff6d34..70ce6bd7d5 100644 --- a/packages/lib/gpcircuits/test/entry-inequality.spec.ts +++ b/packages/lib/gpcircuits/test/entry-inequality.spec.ts @@ -6,13 +6,12 @@ import { } from "../src"; import { circomkit } from "./common"; -for (const nBits of [32, 64, 96, 128, 252]) { - describe("entry-inequality.EntryInequalityModule should work as expected", async function () { - let circuit: WitnessTester< - EntryInequalityModuleInputNamesType, - EntryInequalityModuleOutputNamesType - >; - +describe("entry-inequality.EntryInequalityModule should work as expected", async function () { + let circuit: WitnessTester< + EntryInequalityModuleInputNamesType, + EntryInequalityModuleOutputNamesType + >; + for (const nBits of [32, 64, 96, 128, 252]) { this.beforeAll(async () => { circuit = await circomkit.WitnessTester("EntryInequalityModule", { file: "entry-inequality", @@ -21,30 +20,41 @@ for (const nBits of [32, 64, 96, 128, 252]) { }); }); - it( - "should work for " + nBits + "-bit unsigned integer values", - async () => { - const twoToTheNBits = 1n << BigInt(nBits); - const sampleValues: [bigint, bigint][] = [ + it("should work for " + nBits + "-bit signed integer values", async () => { + const twoToTheNBitsMinusOne = 1n << BigInt(nBits - 1); + const sampleValues: [bigint, bigint][] = [ + [-twoToTheNBitsMinusOne, twoToTheNBitsMinusOne - 1n], + [-twoToTheNBitsMinusOne, -twoToTheNBitsMinusOne + 1n] + ].concat( + [ [0n, 0n], [2n, 3n], [3n, 2n], - [3n, twoToTheNBits / 5n], - [twoToTheNBits / 5n - 1n, (2n * twoToTheNBits) / 5n], - [(2n * twoToTheNBits) / 5n - 5n, (3n * twoToTheNBits) / 5n], - [0n, twoToTheNBits - 1n], - [twoToTheNBits - 2n, twoToTheNBits - 1n], - [twoToTheNBits - 1n, twoToTheNBits - 2n], - [twoToTheNBits - 1n, twoToTheNBits - 1n] - ]; + [3n, twoToTheNBitsMinusOne / 5n], + [3n, -twoToTheNBitsMinusOne / 5n], + [twoToTheNBitsMinusOne / 5n - 1n, (2n * twoToTheNBitsMinusOne) / 5n], + [ + (2n * twoToTheNBitsMinusOne) / 5n - 5n, + (3n * twoToTheNBitsMinusOne) / 5n + ], + [0n, twoToTheNBitsMinusOne - 1n], + [twoToTheNBitsMinusOne - 2n, twoToTheNBitsMinusOne - 1n], + [twoToTheNBitsMinusOne - 1n, twoToTheNBitsMinusOne - 2n], + [twoToTheNBitsMinusOne - 1n, twoToTheNBitsMinusOne - 1n] + ].flatMap(([a, b]) => [ + [a, b], + [-a, b], + [a, -b], + [-a, -b] + ]) + ) as [bigint, bigint][]; - for (const [value, otherValue] of sampleValues) { - await circuit.expectPass( - { value, otherValue }, - { out: BigInt(value < otherValue) } - ); - } + for (const [value, otherValue] of sampleValues) { + await circuit.expectPass( + { value, otherValue }, + { isLessThan: BigInt(value < otherValue) } + ); } - ); - }); -} + }); + } +}); diff --git a/packages/lib/gpcircuits/test/numeric-value.spec.ts b/packages/lib/gpcircuits/test/numeric-value.spec.ts index 58d729a2f6..d92909b3f5 100644 --- a/packages/lib/gpcircuits/test/numeric-value.spec.ts +++ b/packages/lib/gpcircuits/test/numeric-value.spec.ts @@ -17,7 +17,7 @@ describe("numeric-value.NumericValueModule should work", async function () { circuit = await circomkit.WitnessTester("NumericValueModule", { file: "numeric-value", template: "NumericValueModule", - params: [] + params: [64] }); });