From 42dfd702e1a4db4b462f5b508d1b8496968f87fb Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Tue, 3 Sep 2024 15:08:03 +0200 Subject: [PATCH] Add ZK proof generation --- .../frontend/package.json | 6 +- .../frontend/src/App.tsx | 5 +- .../src/components/ZkProofSubmission.tsx | 84 ++++++++++++++----- .../frontend/src/constants.ts | 10 +++ .../frontend/src/utils.ts | 54 +++++++++++- .../frontend/yarn.lock | 37 ++++++++ 6 files changed, 172 insertions(+), 24 deletions(-) diff --git a/compliant-reward-distribution/frontend/package.json b/compliant-reward-distribution/frontend/package.json index 075e6fcc..83719471 100644 --- a/compliant-reward-distribution/frontend/package.json +++ b/compliant-reward-distribution/frontend/package.json @@ -16,6 +16,7 @@ "@concordium/react-components": "^0.4.0", "@concordium/wallet-connectors": "^0.4.0", "@concordium/web-sdk": "^7.3.2", + "@types/sha256": "^0.2.2", "bootstrap": "^5.3.3", "json-bigint": "^1.0.0", "moment": "^2.30.1", @@ -27,17 +28,18 @@ "react-select": "^5.8.0", "react-switch": "^7.0.0", "rollup-plugin-node-polyfills": "^0.2.1", + "sha256": "^0.2.0", "vite-plugin-node-polyfills": "^0.21.0" }, "devDependencies": { "@concordium/ccd-js-gen": "^1.2.0", "@swc-jotai/react-refresh": "^0.1.0", + "@types/json-bigint": "^1.0.4", "@types/react": "^18.2.55", "@types/react-dom": "^18.2.19", + "@types/react-router-dom": "^5.3.3", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.20.0", - "@types/json-bigint": "^1.0.4", - "@types/react-router-dom": "^5.3.3", "@vitejs/plugin-react-swc": "^3.6.0", "dotenv": "^16.3.1", "eslint": "^8.45.0", diff --git a/compliant-reward-distribution/frontend/src/App.tsx b/compliant-reward-distribution/frontend/src/App.tsx index 9dae2022..866a7eef 100644 --- a/compliant-reward-distribution/frontend/src/App.tsx +++ b/compliant-reward-distribution/frontend/src/App.tsx @@ -7,11 +7,14 @@ import { ConnectWallet } from './components/ConnectWallet'; import { ZkProofSubmission } from './components/ZkProofSubmission'; import { version } from '../package.json'; import './styles.scss'; +import { TESTNET, useGrpcClient } from '@concordium/react-components'; export const App = () => { const [provider, setProvider] = useState(); const [account, setAccount] = useState(); + const grpcClient = useGrpcClient(TESTNET); + useEffect(() => { if (provider !== undefined) { return () => { @@ -69,7 +72,7 @@ export const App = () => { /> } + element={} /> {/* ({ mode: 'all' }); + const [error, setError] = useState(undefined); + const [zkStatement, setZkStatement] = useState(undefined); - const [zkStatement] = useWatch({ - control: control, - name: ['zkStatement'], - }); + useEffect(() => { + const fetchStatement = async () => { + const statement: CredentialStatement = await getStatement(); + setZkStatement(statement); + }; - const [error, setError] = useState(undefined); + fetchStatement(); + }, []); + + interface FormType { } + const { handleSubmit } = useForm({ mode: 'all' }); async function onSubmit() { setError(undefined); + if (grpcClient === undefined) { + setError(`'grpcClient' is undefined`); + throw Error(`'grpcClient' is undefined`); + } + + const bestBlockHeight = (await grpcClient.client.getConsensusInfo(''))?.response.bestBlockHeight; + + if (bestBlockHeight === undefined) { + setError(`Couldn't get 'bestBlockHeight' from chain`); + throw Error(`Couldn't get 'bestBlockHeight' from chain`); + } + + const recentBlockHeight = bestBlockHeight.value - 10n; + + const recentBlockHash = ( + await grpcClient.client.getBlocksAtHeight({ + // TODO: Type in web-sdk needs to be fixed to do this ergonomically. + blocksAtHeight: { + oneofKind: 'absolute', + absolute: { + height: { value: recentBlockHeight }, + }, + }, + }) + )?.response.blocks[0].value; + + if (recentBlockHash === undefined) { + setError(`'recentBlockHash' is undefined`); + throw Error(`'recentBlockHash' is undefined`); + } + + const hashDigest = [recentBlockHash, CONTEXT_STRING]; + const challenge = sha256(hashDigest.flatMap((item) => Array.from(item))); + if (zkStatement === undefined) { - setError(`'statement' input field is undefined`); - throw Error(`'statement' input field is undefined`); + setError(`'zkStatement' is undefined`); + throw Error(`'zkStatement' is undefined`); } if (provider === undefined) { setError(`'provider' is undefined`); throw Error(`'provider' is undefined`); } + + const presentation = await provider.requestVerifiablePresentation(challenge, [zkStatement]); + + try { + await submitZkProof(presentation, recentBlockHeight); + } catch (error) { + setError((error as Error).message); + } } return ( @@ -44,12 +94,6 @@ export function ZkProofSubmission(props: Props) {

Submit ZK Proof


- {/* - ZK statement - - {formState.errors.zkStatement && ZK statement is required } - - */} diff --git a/compliant-reward-distribution/frontend/src/constants.ts b/compliant-reward-distribution/frontend/src/constants.ts index 1466ae73..447f9de7 100644 --- a/compliant-reward-distribution/frontend/src/constants.ts +++ b/compliant-reward-distribution/frontend/src/constants.ts @@ -18,6 +18,16 @@ export const CONTRACT_ADDRESS = ContractAddress.fromSerializable(CONFIG.contract export const NETWORK = CONFIG.network === 'mainnet' ? MAINNET : TESTNET; export const CCD_SCAN_URL = NETWORK === MAINNET ? 'https://ccdscan.io' : 'https://testnet.ccdscan.io'; +export const BACKEDN_BASE_URL = 'http://localhost:8080/'; + +// The string "CONCORDIUM_COMPLIANT_REWARD_DISTRIBUTION_DAPP" in bytes is used +// as context for signing messages and generating ZK proofs. The same account +// can be used in different Concordium services without the risk of re-playing +// signatures/zk-proofs across the different services due to this context string. +export const CONTEXT_STRING = new Uint8Array([ + 67, 79, 78, 67, 79, 82, 68, 73, 85, 77, 95, 67, 79, 77, 80, 76, 73, 65, 78, 84, 95, 82, 69, 87, 65, 82, 68, 95, 68, + 73, 83, 84, 82, 73, 66, 85, 84, 73, 79, 78, 95, 68, 65, 80, 80, +]); // Before submitting a transaction we simulate/dry-run the transaction to get an // estimate of the energy needed for executing the transaction. In addition, we diff --git a/compliant-reward-distribution/frontend/src/utils.ts b/compliant-reward-distribution/frontend/src/utils.ts index 30851070..477c4b48 100644 --- a/compliant-reward-distribution/frontend/src/utils.ts +++ b/compliant-reward-distribution/frontend/src/utils.ts @@ -1,4 +1,56 @@ -import { AccountAddress } from '@concordium/web-sdk'; +import { AccountAddress, AtomicStatementV2, CredentialStatement, VerifiablePresentation } from '@concordium/web-sdk'; +import { BACKEDN_BASE_URL } from './constants'; + +/** + * Fetch the statement to prove from the backend + */ +export async function getStatement(): Promise { + const response = await fetch(`${BACKEDN_BASE_URL}api/getZKProofStatements`, { method: 'get' }); + + if (!response.ok) { + const error = (await response.json()) as Error; + throw new Error(`Unable to get the ZK statement from the backend: ${JSON.stringify(error)}`); + } + + const body = (await response.json()).data as AtomicStatementV2[]; + + if (body) { + const credentialStatement: CredentialStatement = { + idQualifier: { + type: 'cred', + // We allow all identity providers on mainnet and on testnet. + // This list is longer than necessary to include all current/future + // identity providers on mainnet and testnet. + // This list should be updated to only include the identity providers that you trust. + issuers: [0, 1, 2, 3, 4, 5, 6, 7], + }, + statement: body, + }; + + return credentialStatement; + } else { + throw new Error(`Unable to get the ZK statement from the backend`); + } +} + +/** + * Submit ZK proof to the backend + */ +export async function submitZkProof(presentation: VerifiablePresentation, recentBlockHeight: bigint) { + const response = await fetch(`${BACKEDN_BASE_URL}api/postZKProof`, { + method: 'POST', + headers: new Headers({ 'content-type': 'application/json' }), + body: JSON.stringify({ + blockHeight: Number(recentBlockHeight), + presentation: presentation, + }), + }); + + if (!response.ok) { + const error = (await response.json()) as Error; + throw new Error(`Unable to submit ZK proof to the backend: ${JSON.stringify(error)}`); + } +} /** * This function validates if a string represents a valid accountAddress in base58 encoding. diff --git a/compliant-reward-distribution/frontend/yarn.lock b/compliant-reward-distribution/frontend/yarn.lock index 38a85fb1..cf0f6202 100644 --- a/compliant-reward-distribution/frontend/yarn.lock +++ b/compliant-reward-distribution/frontend/yarn.lock @@ -1037,6 +1037,13 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== +"@types/node@*": + version "22.5.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.5.2.tgz#e42344429702e69e28c839a7e16a8262a8086793" + integrity sha512-acJsPTEqYqulZS/Yp/S3GgeE6GZ0qYODUR8aVr/DkhHQ8l9nd4j5x1/ZJy9/gHrRlFMqkO6i0I3E27Alu4jjPg== + dependencies: + undici-types "~6.19.2" + "@types/node@>=13.7.0": version "20.12.3" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.3.tgz#d6658c2c7776c1cad93534bb45428195ed840c65" @@ -1098,6 +1105,13 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ== +"@types/sha256@^0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@types/sha256/-/sha256-0.2.2.tgz#633bf20405e90cab0c4df1054b657f206ef15bf5" + integrity sha512-uKMaDzyzfcDYGEwTgLh+hmgDMxXWyIVodY8T+qt7A+NYvikW0lmGLMGbQ7BipCB8dzXHa55C9g+Ii/3Lgt1KmA== + dependencies: + "@types/node" "*" + "@types/warning@^3.0.0": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/warning/-/warning-3.0.3.tgz#d1884c8cc4a426d1ac117ca2611bf333834c6798" @@ -2028,11 +2042,21 @@ constants-browserify@^1.0.0: resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" integrity sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ== +convert-hex@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/convert-hex/-/convert-hex-0.1.0.tgz#08c04568922c27776b8a2e81a95d393362ea0b65" + integrity sha512-w20BOb1PiR/sEJdS6wNrUjF5CSfscZFUp7R9NSlXH8h2wynzXVEPFPJECAnkNylZ+cvf3p7TyRUHggDmrwXT9A== + convert-source-map@^1.5.0: version "1.9.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== +convert-string@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/convert-string/-/convert-string-0.1.0.tgz#79ce41a9bb0d03bcf72cdc6a8f3c56fbbc64410a" + integrity sha512-1KX9ESmtl8xpT2LN2tFnKSbV4NiarbVi8DVb39ZriijvtTklyrT+4dT1wsGMHKD3CJUjXgvJzstm9qL9ICojGA== + cookie-es@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/cookie-es/-/cookie-es-1.1.0.tgz#68f8d9f48aeb5a534f3896f80e792760d3d20def" @@ -4648,6 +4672,14 @@ sha.js@^2.4.0, sha.js@^2.4.8: inherits "^2.0.1" safe-buffer "^5.0.1" +sha256@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/sha256/-/sha256-0.2.0.tgz#73a0b418daab7035bff86e8491e363412fc2ab05" + integrity sha512-kTWMJUaez5iiT9CcMv8jSq6kMhw3ST0uRdcIWl3D77s6AsLXNXRp3heeqqfu5+Dyfu4hwpQnMzhqHh8iNQxw0w== + dependencies: + convert-hex "~0.1.0" + convert-string "~0.1.0" + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -5075,6 +5107,11 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== +undici-types@~6.19.2: + version "6.19.8" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" + integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== + unenv@^1.9.0: version "1.9.0" resolved "https://registry.yarnpkg.com/unenv/-/unenv-1.9.0.tgz#469502ae85be1bd3a6aa60f810972b1a904ca312"