diff --git a/apps/passport-client/package.json b/apps/passport-client/package.json index c3a4f6b210..ba91fe3d52 100644 --- a/apps/passport-client/package.json +++ b/apps/passport-client/package.json @@ -108,7 +108,7 @@ "@esbuild-plugins/node-globals-polyfill": "^0.2.3", "@esbuild-plugins/node-modules-polyfill": "^0.2.2", "@pcd/eslint-config-custom": "0.11.4", - "@pcd/proto-pod-gpc-artifacts": "0.9.0", + "@pcd/proto-pod-gpc-artifacts": "0.10.0", "@types/chai": "^4.3.11", "@types/email-validator": "^1.0.6", "@types/express": "^4.17.17", diff --git a/examples/pod-gpc-example/package.json b/examples/pod-gpc-example/package.json index 9d35c679e6..938b8e56ec 100644 --- a/examples/pod-gpc-example/package.json +++ b/examples/pod-gpc-example/package.json @@ -39,7 +39,7 @@ }, "devDependencies": { "@pcd/eslint-config-custom": "0.11.4", - "@pcd/proto-pod-gpc-artifacts": "0.9.0", + "@pcd/proto-pod-gpc-artifacts": "0.10.0", "@pcd/tsconfig": "0.11.4", "@types/chai": "^4.3.5", "@types/mocha": "^10.0.1", diff --git a/packages/lib/gpc/src/gpcChecks.ts b/packages/lib/gpc/src/gpcChecks.ts index 6f8486b21f..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, @@ -131,6 +132,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 +623,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 } +): void { + 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 have unique content IDs, but they don't." + ); + } + } +} + export function checkProofBoundsCheckInputsForConfig( entryName: PODEntryIdentifier, entryConfig: GPCProofEntryConfig, @@ -702,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 @@ -715,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) ) @@ -760,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 28687b7918..547aa08c49 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, @@ -377,6 +378,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 +406,7 @@ export function compileProofConfig( ...circuitNumericValueInputs, ...circuitMultiTupleInputs, ...circuitListMembershipInputs, + ...circuitUniquenessInputs, ...circuitGlobalInputs }; } @@ -673,10 +678,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 +725,14 @@ function compileProofEntry( }; } +export function compileProofPODUniqueness(proofConfig: { + uniquePODs?: boolean; +}): { + requireUniqueContentIDs: CircuitSignal; +} { + return { requireUniqueContentIDs: BigInt(proofConfig.uniquePODs ?? false) }; +} + function compileProofVirtualEntry< ObjInput extends POD | GPCRevealedObjectClaims >( @@ -1111,6 +1123,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 +1159,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..0744b11238 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 should have unique content IDs. + * 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/gpc.spec.ts b/packages/lib/gpc/test/gpc.spec.ts index 0b1832bc89..8562a61cff 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 }, @@ -1088,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 have unique content IDs, but they don't." + ); }); it("verifying should throw on illegal inputs", async function () { @@ -1321,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]] }, @@ -1419,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; }); }); 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..729bb7c82d 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({ + 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({ + requireUniqueContentIDs: 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..75feabd3e0 100644 --- a/packages/lib/gpcircuits/circuits.json +++ b/packages/lib/gpcircuits/circuits.json @@ -34,6 +34,7 @@ "listComparisonValueIndex", "listContainsComparisonValue", "listValidValues", + "requireUniqueContentIDs", "globalWatermark" ] }, @@ -72,6 +73,7 @@ "listComparisonValueIndex", "listContainsComparisonValue", "listValidValues", + "requireUniqueContentIDs", "globalWatermark" ] }, @@ -110,6 +112,7 @@ "listComparisonValueIndex", "listContainsComparisonValue", "listValidValues", + "requireUniqueContentIDs", "globalWatermark" ] }, @@ -148,6 +151,7 @@ "listComparisonValueIndex", "listContainsComparisonValue", "listValidValues", + "requireUniqueContentIDs", "globalWatermark" ] }, @@ -186,6 +190,7 @@ "listComparisonValueIndex", "listContainsComparisonValue", "listValidValues", + "requireUniqueContentIDs", "globalWatermark" ] }, @@ -224,6 +229,7 @@ "listComparisonValueIndex", "listContainsComparisonValue", "listValidValues", + "requireUniqueContentIDs", "globalWatermark" ] } diff --git a/packages/lib/gpcircuits/circuits/list-membership.circom b/packages/lib/gpcircuits/circuits/list-membership.circom index ff566983c0..1b64dec56c 100644 --- a/packages/lib/gpcircuits/circuits/list-membership.circom +++ b/packages/lib/gpcircuits/circuits/list-membership.circom @@ -1,6 +1,7 @@ pragma circom 2.1.8; include "circomlib/circuits/comparators.circom"; +include "circomlib/circuits/gates.circom"; /** * Module for checking whether a value is a member of a given list. @@ -14,25 +15,30 @@ 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]; - + // Shift the values by `comparisonValue`. + signal shiftedValues[MAX_LIST_ELEMENTS]; 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]); - } + shiftedValues[i] <== validValues[i] - comparisonValue; } if (MAX_LIST_ELEMENTS == 0) { isMember <== 0; } else { - isMember <== IsZero()(partialProduct[MAX_LIST_ELEMENTS - 1]); + // `comparisonValue` lies in `validValues` iff + // `shiftedValues[i]` is 0 for some i, which is equivalent to + // the product of all elements of `shiftedValues` being 0. + isMember <== IsZero()( + MultiAND(MAX_LIST_ELEMENTS)( + shiftedValues + ) + ); } } diff --git a/packages/lib/gpcircuits/circuits/proto-pod-gpc.circom b/packages/lib/gpcircuits/circuits/proto-pod-gpc.circom index fad3f7263f..7ef0d283d3 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 requireUniqueContentIDs; + signal podsAreUnique <== UniquenessModule(MAX_OBJECTS)( + objectContentID + ); + 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 new file mode 100644 index 0000000000..c99b7e13fb --- /dev/null +++ b/packages/lib/gpcircuits/circuits/uniqueness.circom @@ -0,0 +1,49 @@ +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; + + // Number of pairs of distinct indices in [0, NUM_LIST_ELEMENTS[ + // (modulo order). + var NUM_PAIRS = NUM_LIST_ELEMENTS*(NUM_LIST_ELEMENTS - 1)\2; + + // Matrix of differences of the form M[i,j] = values[i] - + // values[j] for i < j arranged in column-major order. + signal valueDifferences[NUM_PAIRS]; + for (var i = 0; i < NUM_LIST_ELEMENTS; i++) { + for(var j = i + 1; j < NUM_LIST_ELEMENTS; j++) { + var k = j + NUM_LIST_ELEMENTS*i - (i + 1)*(i + 2)\2; + valueDifferences[k] <== values[i] - values[j]; + } + } + + if (NUM_PAIRS == 0) { + valuesAreUnique <== 1; + } else { + // All values are unique iff all elements of + // `valueDifferences` are nonzero, which is the case iff the + // product of all elements of `valueDifferences` is nonzero. + valuesAreUnique <== NOT()( + IsZero()( + MultiAND(NUM_PAIRS)( + valueDifferences + ) + ) + ); + } +} 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..d86afe92f5 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*/ requireUniqueContentIDs: CircuitSignal; + // Global module (1) /*PUB*/ globalWatermark: CircuitSignal; }; @@ -107,6 +110,7 @@ export type ProtoPODGPCInputNamesType = [ "listComparisonValueIndex", "listContainsComparisonValue", "listValidValues", + "requireUniqueContentIDs", "globalWatermark" ]; @@ -152,6 +156,9 @@ export type ProtoPODGPCPublicInputs = { /*PUB*/ listContainsComparisonValue: CircuitSignal; /*PUB*/ listValidValues: CircuitSignal /*MAX_LISTS*/[] /*MAX_LIST_ENTRIES*/[]; + // POD uniqueness module (1) + /*PUB*/ requireUniqueContentIDs: CircuitSignal; + // Global module (1) /*PUB*/ globalWatermark: CircuitSignal; }; @@ -179,6 +186,7 @@ export const PROTO_POD_GPC_PUBLIC_INPUT_NAMES = [ "listComparisonValueIndex", "listContainsComparisonValue", "listValidValues", + "requireUniqueContentIDs", "globalWatermark" ]; @@ -447,6 +455,7 @@ export class ProtoPODGPC { listComparisonValueIndex: allInputs.listComparisonValueIndex, listContainsComparisonValue: allInputs.listContainsComparisonValue, listValidValues: allInputs.listValidValues, + requireUniqueContentIDs: allInputs.requireUniqueContentIDs, globalWatermark: allInputs.globalWatermark }; } @@ -520,6 +529,7 @@ export class ProtoPODGPC { ...inputs.listComparisonValueIndex, inputs.listContainsComparisonValue, ...inputs.listValidValues.flat(), + inputs.requireUniqueContentIDs, inputs.globalWatermark ].map(BigInt); } @@ -662,5 +672,5 @@ export class ProtoPODGPC { * Version of the published artifacts on NPM which are compatible with this * version of the GPC circuits. */ - public static ARTIFACTS_NPM_VERSION = "0.9.0"; + public static ARTIFACTS_NPM_VERSION = "0.10.0"; } diff --git a/packages/lib/gpcircuits/src/uniqueness.ts b/packages/lib/gpcircuits/src/uniqueness.ts new file mode 100644 index 0000000000..822fba66d4 --- /dev/null +++ b/packages/lib/gpcircuits/src/uniqueness.ts @@ -0,0 +1,13 @@ +import { CircuitSignal } from "./types"; + +export type UniquenessModuleInputs = { + values: CircuitSignal /*NUM_LIST_ELEMENTS*/[]; +}; + +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..85a6c9f769 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,48 @@ 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. + * + * 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 +): 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..14353bbf07 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*/ requireUniqueContentIDs: 0n, + // Global module (1) /*PUB*/ globalWatermark: 1337n }; @@ -455,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. @@ -533,25 +538,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 = []; @@ -801,6 +824,7 @@ function makeTestSignals( listComparisonValueIndex, listContainsComparisonValue, listValidValues, + requireUniqueContentIDs: BigInt(requireUniqueContentIDs), globalWatermark: 1337n }, outputs: { @@ -843,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); @@ -951,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 new file mode 100644 index 0000000000..7abb80cc5c --- /dev/null +++ b/packages/lib/gpcircuits/test/uniqueness.spec.ts @@ -0,0 +1,80 @@ +import { BABY_JUB_NEGATIVE_ONE } from "@pcd/util"; +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 non-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], + [ + 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) { + await circuit(list.length).then((c) => + c.expectPass({ values: list }, { valuesAreUnique: 0n }) + ); + } + }); +}); diff --git a/packages/pcd/gpc-pcd/package.json b/packages/pcd/gpc-pcd/package.json index 1d59e15edf..273697b640 100644 --- a/packages/pcd/gpc-pcd/package.json +++ b/packages/pcd/gpc-pcd/package.json @@ -41,8 +41,8 @@ }, "devDependencies": { "@pcd/eslint-config-custom": "0.11.4", + "@pcd/proto-pod-gpc-artifacts": "0.10.0", "@pcd/pod": "0.1.7", - "@pcd/proto-pod-gpc-artifacts": "0.9.0", "@pcd/tsconfig": "0.11.4", "@semaphore-protocol/identity": "^3.15.2", "@types/chai": "^4.3.5", diff --git a/yarn.lock b/yarn.lock index 4afbf96f7e..a544c8f139 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5101,10 +5101,10 @@ "@pcd/gpc" "0.0.8" "@pcd/pod" "0.1.7" -"@pcd/proto-pod-gpc-artifacts@0.9.0": - version "0.9.0" - resolved "https://registry.yarnpkg.com/@pcd/proto-pod-gpc-artifacts/-/proto-pod-gpc-artifacts-0.9.0.tgz#784ce57cf7e8efee155f9d6b290f0ff797917fb9" - integrity sha512-DipHMqcwpwCGy0/4hpENYjhKPcxVDEM4Koe13L1QpDYlOVVlUfsV9Y5ObpjtZELH4Q8jLWEW2tnMW9vR+c388w== +"@pcd/proto-pod-gpc-artifacts@0.10.0": + version "0.10.0" + resolved "https://registry.yarnpkg.com/@pcd/proto-pod-gpc-artifacts/-/proto-pod-gpc-artifacts-0.10.0.tgz#bea63e671f770d2f98a28fa8af4da9f37aee6be5" + integrity sha512-Gusvv631cZgjbR2jox0Zj8jDn5xaVDBnKT8g76fH/ficiwORD+IKXdwILiatZJZfCASEVOGhS5bFfj510ylBUQ== "@peculiar/asn1-android@^2.3.3": version "2.3.6"