Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add POD uniqueness module #1884

Merged
merged 9 commits into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/passport-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion examples/pod-gpc-example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
34 changes: 30 additions & 4 deletions packages/lib/gpc/src/gpcChecks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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<PODName, POD> }
): 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,
Expand Down Expand Up @@ -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
Expand All @@ -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)
)
Expand Down Expand Up @@ -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(
Expand Down
24 changes: 20 additions & 4 deletions packages/lib/gpc/src/gpcCompile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
ProtoPODGPCPublicInputs,
array2Bits,
computeTupleIndices,
dummyObjectSignals,
extendedSignalArray,
hashTuple,
padArray,
Expand Down Expand Up @@ -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);

Expand All @@ -402,6 +406,7 @@ export function compileProofConfig(
...circuitNumericValueInputs,
...circuitMultiTupleInputs,
...circuitListMembershipInputs,
...circuitUniquenessInputs,
...circuitGlobalInputs
};
}
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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
>(
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -1144,6 +1159,7 @@ export function compileVerifyConfig(
...circuitNumericValueInputs,
...circuitMultiTupleInputs,
...circuitListMembershipInputs,
...circuitUniquenessInputs,
...circuitGlobalInputs
},
circuitOutputs: {
Expand Down
6 changes: 6 additions & 0 deletions packages/lib/gpc/src/gpcTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,12 @@ export type GPCProofConfig = {
*/
pods: Record<PODName, GPCProofObjectConfig>;

/**
* 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
Expand Down
12 changes: 12 additions & 0 deletions packages/lib/gpc/src/gpcUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? {});

Expand All @@ -75,6 +80,7 @@ export function canonicalizeConfig(
return {
circuitIdentifier: circuitIdentifier,
pods: canonicalPODs,
...canonicalizedPODUniquenessConfig,
...(proofConfig.tuples !== undefined ? { tuples: tupleRecord } : {})
};
}
Expand Down Expand Up @@ -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
Expand Down
64 changes: 63 additions & 1 deletion packages/lib/gpc/test/gpc.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -694,7 +694,8 @@ describe("gpc library (Compiled test artifacts) should work", async function ()
entries: ["pod1.$signerPublicKey", "pod2.$signerPublicKey"],
isMemberOf: "admissiblePubKeyPairs"
}
}
},
uniquePODs: true
ax0 marked this conversation as resolved.
Show resolved Hide resolved
};
const proofInputs: GPCProofInputs = {
pods: { pod1, pod2 },
Expand Down Expand Up @@ -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 () {
Expand Down Expand Up @@ -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]] },
Expand Down Expand Up @@ -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;
});
});

Expand Down
53 changes: 52 additions & 1 deletion packages/lib/gpc/test/gpcChecks.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {
POD,
PODEdDSAPublicKeyValue,
PODName,
PODValue,
POD_INT_MAX,
POD_INT_MIN
Expand All @@ -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", () => {
Expand Down Expand Up @@ -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<PODName, POD>[];

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<PODName, POD>[];

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
Loading
Loading