Skip to content

Commit

Permalink
Merge pull request #3 from robknight/robknight/gpc-proofs
Browse files Browse the repository at this point in the history
GPC proofs in test client
  • Loading branch information
robknight authored Sep 14, 2024
2 parents 82daf9b + 6b261f4 commit eb9f823
Show file tree
Hide file tree
Showing 31 changed files with 561 additions and 194 deletions.
1 change: 1 addition & 0 deletions apps/client-web/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ dist-ssr
*.njsproj
*.sln
*.sw?
public/artifacts/*
2 changes: 1 addition & 1 deletion apps/client-web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PARCNET Client</title>
</head>
<body class="bg-black h-screen w-screen overflow-hidden">
<body class="bg-black h-screen w-screen overflow-scroll">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
Expand Down
5 changes: 2 additions & 3 deletions apps/client-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@
"eventemitter3": "^5.0.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"vite-plugin-node-polyfills": "^0.22.0",
"zod": "^3.23.8"
"vite-plugin-node-polyfills": "^0.22.0"
},
"devDependencies": {
"@parcnet/eslint-config": "workspace:*",
Expand All @@ -33,6 +32,6 @@
"postcss": "^8.4.41",
"tailwindcss": "^3.4.10",
"typescript": "^5.5",
"vite": "^5.4.1"
"vite": "^5.4.4"
}
}
278 changes: 256 additions & 22 deletions apps/client-web/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import { listen } from "@parcnet/client-helpers/connection/iframe";
import { Zapp } from "@parcnet/client-rpc";
import { Dispatch, ReactNode, useEffect, useReducer } from "react";
import { EntriesSchema, PODSchema, proofRequest } from "@parcnet/podspec";
import { gpcProve } from "@pcd/gpc";
import { POD, POD_INT_MAX, POD_INT_MIN } from "@pcd/pod";
import {
Dispatch,
Fragment,
ReactNode,
useEffect,
useReducer,
useState
} from "react";
import { ParcnetClientProcessor } from "./client/client";
import { PODCollection } from "./client/pod_collection";
import { loadPODsFromStorage, savePODsToStorage } from "./client/utils";
Expand Down Expand Up @@ -67,44 +77,268 @@ function App() {
state.authorized &&
state.zapp &&
state.proofInProgress && (
<Prove proveOperation={state.proofInProgress} />
<Prove proveOperation={state.proofInProgress} dispatch={dispatch} />
)}
</div>
</main>
);
}

function Reveal({ children }: { children: ReactNode }) {
const [isRevealed, setIsRevealed] = useState(false);
return (
<>
<button onClick={() => setIsRevealed(!isRevealed)}>
{isRevealed ? "Hide" : "Reveal"}
</button>
{isRevealed && <div className="my-1">{children}</div>}
</>
);
}

function ProvePODInfo({
name,
schema,
pods,
selectedPOD,
onChange
}: {
name: string;
schema: PODSchema<EntriesSchema>;
pods: POD[];
selectedPOD: POD | undefined;
onChange: (pod: POD | undefined) => void;
}): ReactNode {
const revealedEntries = Object.entries(schema.entries)
.map(([name, entry]) => {
if (entry.type === "optional") {
entry = entry.innerType;
}
return [name, entry] as const;
})
.filter(([_, entry]) => entry.isRevealed);

const selectedPODEntries = selectedPOD?.content.asEntries();

const entriesWithConstraints = Object.entries(schema.entries)
.map(([name, entry]) => {
if (entry.type === "optional") {
entry = entry.innerType;
}
return [name, entry] as const;
})
.filter(
([_, entry]) =>
!!entry.isMemberOf ||
!!entry.isNotMemberOf ||
!!(entry.type === "int" && entry.inRange)
);

return (
<div className="py-2">
<div className="flex items-center gap-2">
<div className="font-semibold">{name}</div>
<select
className="block w-full rounded-md bg-gray-800 border-transparent focus:border-gray-500 focus:ring-0 p-2 text-sm"
value={selectedPOD?.signature ?? ""}
onChange={(ev) => {
onChange(pods.find((pod) => pod.signature === ev.target.value));
}}
>
<option value="" disabled>
-- None selected --
</option>
{pods.map((pod) => {
return (
<option key={pod.signature} value={pod.signature}>
{pod.signature.substring(0, 16)}
</option>
);
})}
</select>
</div>
<div className="mt-2 font-semibold">
{revealedEntries.length > 0 && "Revealed entries:"}
</div>
<div className="grid grid-cols-2 gap-2">
{revealedEntries.map(([entryName, _]) => {
return (
<Fragment key={`${name}-${entryName}`}>
<div>{entryName}</div>
<div>
{selectedPODEntries?.[entryName].value.toString() ?? "-"}
</div>
</Fragment>
);
})}
</div>
{entriesWithConstraints.length > 0 && (
<div className="mt-2">
<div className="font-semibold">Proven constraints:</div>
{entriesWithConstraints.map(([entryName, entry]) => {
return (
<div key={`${name}-${entryName}-constraints`} className="my-1">
{entry.isMemberOf && (
<div>
<span className="font-semibold">{entryName}</span> is member
of list:{" "}
<Reveal>
<div className="p-1 rounded bg-gray-800">
{entry.isMemberOf
.map((v) => v.value.toString())
.join(", ")}
</div>
</Reveal>
</div>
)}
{entry.isNotMemberOf && (
<div>
<span className="font-semibold">{entryName}</span> is not
member of list:{" "}
<Reveal>
<div className="p-1 rounded bg-gray-800">
{entry.isNotMemberOf
.map((v) => v.value.toString())
.join(", ")}
</div>
</Reveal>
</div>
)}
{entry.type === "int" && entry.inRange && (
<div className="flex gap-1 items-center">
<span className="font-semibold">{entryName}</span> is
<div className="p-1 rounded bg-gray-800">
{entry.inRange.min === POD_INT_MIN &&
entry.inRange.max === POD_INT_MAX &&
"any number"}
{entry.inRange.min !== POD_INT_MIN &&
entry.inRange.max === POD_INT_MAX &&
`greater than ${entry.inRange.min}`}
{entry.inRange.min === POD_INT_MIN &&
entry.inRange.max !== POD_INT_MAX &&
`less than ${entry.inRange.max}`}
{entry.inRange.min !== POD_INT_MIN &&
entry.inRange.max !== POD_INT_MAX &&
`between ${entry.inRange.min} and ${entry.inRange.max}`}
</div>
</div>
)}
</div>
);
})}
</div>
)}
</div>
);
}

function Prove({
proveOperation
proveOperation,
dispatch
}: {
proveOperation: NonNullable<ClientState["proofInProgress"]>;
dispatch: Dispatch<ClientAction>;
}): ReactNode {
const canProve =
Object.keys(proveOperation.selectedPods).length ===
Object.keys(proveOperation.proofRequest.pods).length &&
Object.values(proveOperation.selectedPods).every((maybePod) => !!maybePod);

/**
* show exactly which fields are revealed and which are not, literally show
* the whole POD and which entries are revealed
* could be complicated for tuples?
*/
return (
<div>
<ul>
{Object.entries(proveOperation.pods).map(([key, pods]) => {
return (
<div key={key}>
<p>{key}</p>
<div className="text-sm">
{pods.map((pod) => {
return (
<div className="text-sm" key={pod.signature}>
{pod.contentID.toString()}
{pod.serialize()}
</div>
);
})}
</div>
</div>
);
})}
</ul>
<div className="text-sm">
<p>This proof will reveal the following data from your PODs:</p>
{Object.entries(proveOperation.proofRequest.pods).map(
([name, schema]) => {
return (
<ProvePODInfo
key={name}
name={name}
schema={schema}
pods={proveOperation.pods[name]}
selectedPOD={proveOperation.selectedPods[name]}
onChange={(pod) => {
dispatch({
type: "set-proof-in-progress",
...proveOperation,
selectedPods: {
...proveOperation.selectedPods,
...{ [name]: pod }
}
});
}}
/>
);
}
)}
<div className="mt-4">
<button
disabled={!canProve}
className="border-2 text-center font-semibold cursor-pointer border-white py-1 px-2 uppercase active:translate-x-[2px] active:translate-y-[2px] disabled:border-gray-500 disabled:text-gray-500"
onClick={() => {
dispatch({
type: "set-proof-in-progress",
...proveOperation,
proving: true
});
const prs = proofRequest(
proveOperation.proofRequest
).getProofRequest();
gpcProve(
prs.proofConfig,
{
pods: proveOperation.selectedPods as Record<string, POD>,
membershipLists: prs.membershipLists,
watermark: prs.watermark
},
new URL("/artifacts", window.location.origin).toString()
)
.then((proof) => {
proveOperation.resolve?.({
success: true,
proof: proof.proof,
boundConfig: proof.boundConfig,
revealedClaims: proof.revealedClaims
});
dispatch({
type: "clear-proof-in-progress"
});
})
.catch((error) => console.error(error));
}}
>
{proveOperation.proving ? (
<svg
className="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
) : (
"Prove"
)}
</button>
</div>
</div>
</div>
);
}
Expand Down
9 changes: 1 addition & 8 deletions apps/client-web/src/client/gpc.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
import { ConnectorAdvice } from "@parcnet/client-helpers";
import {
ParcnetGPCRPC,
ParcnetRPCSchema,
ProveResult
} from "@parcnet/client-rpc";
import { ParcnetGPCRPC, ProveResult } from "@parcnet/client-rpc";
import { PodspecProofRequest, proofRequest } from "@parcnet/podspec";
import { Dispatch } from "react";
import { ClientAction } from "../state";
import { PODCollection } from "./pod_collection";
import { validateInput } from "./utils";

export class ParcnetGPCProcessor implements ParcnetGPCRPC {
public constructor(
Expand All @@ -17,7 +12,6 @@ export class ParcnetGPCProcessor implements ParcnetGPCRPC {
private readonly advice: ConnectorAdvice
) {}

@validateInput(ParcnetRPCSchema.shape.gpc.shape.canProve)
public async canProve(request: PodspecProofRequest): Promise<boolean> {
const prs = proofRequest(request);

Expand All @@ -31,7 +25,6 @@ export class ParcnetGPCProcessor implements ParcnetGPCRPC {
return true;
}

@validateInput(ParcnetRPCSchema.shape.gpc.shape.prove)
public async prove(request: PodspecProofRequest): Promise<ProveResult> {
const prs = proofRequest(request);

Expand Down
Loading

0 comments on commit eb9f823

Please sign in to comment.