diff --git a/examples/CRISP/circuits/src/ecdsa.nr b/examples/CRISP/circuits/src/ecdsa.nr index f7f293c537..7da7b37dd6 100644 --- a/examples/CRISP/circuits/src/ecdsa.nr +++ b/examples/CRISP/circuits/src/ecdsa.nr @@ -92,6 +92,31 @@ fn test_verify_signature() { verify_signature(hashed_message, pub_key_x, pub_key_y, signature); } +#[test] +fn test_verify_signature_sdk_input() { + let hashed_message = [ + 200, 232, 98, 162, 80, 131, 242, 57, 252, 76, 226, 45, 127, 206, 207, 39, 206, 44, 211, 171, + 113, 67, 121, 68, 78, 253, 202, 79, 29, 128, 130, 76, + ]; + + let pub_key_x = [ + 131, 24, 83, 91, 84, 16, 93, 74, 122, 174, 96, 192, 143, 196, 95, 150, 135, 24, 27, 79, 223, + 198, 37, 189, 26, 117, 63, 167, 57, 127, 237, 117, + ]; + let pub_key_y = [ + 53, 71, 241, 28, 168, 105, 102, 70, 242, 243, 172, 176, 142, 49, 1, 106, 250, 194, 62, 99, + 12, 93, 17, 245, 159, 97, 254, 245, 123, 13, 42, 165, + ]; + let signature = [ + 22, 65, 67, 29, 14, 211, 253, 134, 129, 79, 2, 109, 166, 46, 17, 67, 75, 83, 198, 168, 81, + 98, 254, 167, 249, 146, 24, 191, 60, 48, 125, 236, 127, 54, 28, 35, 95, 7, 182, 88, 120, 10, + 253, 145, 165, 201, 214, 141, 106, 75, 20, 213, 235, 5, 17, 246, 104, 141, 62, 145, 20, 14, + 236, 18, + ]; + + verify_signature(hashed_message, pub_key_x, pub_key_y, signature); +} + #[test] fn test_fail_verify_signature() { let hashed_message = [ diff --git a/examples/CRISP/circuits/src/utils.nr b/examples/CRISP/circuits/src/utils.nr index 0aafd08e08..d9b33940fd 100644 --- a/examples/CRISP/circuits/src/utils.nr +++ b/examples/CRISP/circuits/src/utils.nr @@ -7,12 +7,9 @@ use polynomial::Polynomial; // Check that all valid coefficients are either 0 or 1 -pub fn check_coefficient_values( - k1: Polynomial, - q_mod_t: Field, -) { - // This value would allow to fit - // 268435456 for yes and 268435456 for no +pub fn check_coefficient_values(k1: Polynomial, q_mod_t: Field) { + // This value would allow to fit + // 268435456 for yes and 268435456 for no // which would fit the supply of most tokens really (imagining one user just holds all tokens) let HALF_LARGEST_MINIMUM_DEGREE = 28; @@ -24,14 +21,14 @@ pub fn check_coefficient_values( // yes part for i in START_INDEX_Y..HALF_D { let coeff = k1.coefficients[i]; - assert(0 == coeff * (q_mod_t - coeff)); + assert(0 == coeff * (q_mod_t - coeff)); } // no part for i in START_INDEX_N..D { let coeff = k1.coefficients[i]; - assert(0 == coeff * (q_mod_t - coeff)); - } + assert(0 == coeff * (q_mod_t - coeff)); + } } #[test] diff --git a/examples/CRISP/client/src/hooks/voting/useVoteCasting.ts b/examples/CRISP/client/src/hooks/voting/useVoteCasting.ts index 9d3db15744..c9e0a31b76 100644 --- a/examples/CRISP/client/src/hooks/voting/useVoteCasting.ts +++ b/examples/CRISP/client/src/hooks/voting/useVoteCasting.ts @@ -6,6 +6,8 @@ import { useState, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; +import { useSignMessage } from 'wagmi'; + import { useVoteManagementContext } from '@/context/voteManagement'; import { useNotificationAlertContext } from '@/context/NotificationAlert/NotificationAlert.context.tsx'; import { Poll } from '@/model/poll.model'; @@ -21,6 +23,7 @@ export const useVoteCasting = () => { setTxUrl, } = useVoteManagementContext(); + const { signMessageAsync } = useSignMessage(); const { showToast } = useNotificationAlertContext(); const navigate = useNavigate(); const [isLoading, setIsLoading] = useState(false); @@ -48,6 +51,9 @@ export const useVoteCasting = () => { setIsLoading(true); console.log("Processing vote..."); + // For now just sign and do not do nothing with the signature + // await signMessageAsync({ message: `Vote for round ${roundState.id}` }); + try { const voteEncrypted = await handleVoteEncryption(pollSelected); if (!voteEncrypted) { @@ -114,7 +120,8 @@ export const useVoteCasting = () => { setTxUrl, showToast, navigate, - handleVoteEncryption + handleVoteEncryption, + signMessageAsync, ]); return { castVoteWithProof, isLoading }; diff --git a/examples/CRISP/client/src/utils/vote.ts b/examples/CRISP/client/src/utils/vote.ts deleted file mode 100644 index 5a79c33fbc..0000000000 --- a/examples/CRISP/client/src/utils/vote.ts +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// -// This file is provided WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY -// or FITNESS FOR A PARTICULAR PURPOSE. - -import { VotingConfigRequest } from '@/model/vote.model' -import { Chain } from '@/utils/network' - -//"0x51Ec8aB3e53146134052444693Ab3Ec53663a12B" e.g votingAddress -export const generateCrispRound = (votingAddress: string): VotingConfigRequest => { - return { - round_id: 0, // We can get this from the server - chain_id: Chain.SEPOLIA, - voting_address: votingAddress, - ciphernode_count: 2, // We can hard code this so they don't have to choose - voter_count: 0, // The server will replace this with a timestamp for how long they have to vote - } -} \ No newline at end of file diff --git a/examples/CRISP/packages/crisp-sdk/src/index.ts b/examples/CRISP/packages/crisp-sdk/src/index.ts index b6d537375b..90506ac327 100644 --- a/examples/CRISP/packages/crisp-sdk/src/index.ts +++ b/examples/CRISP/packages/crisp-sdk/src/index.ts @@ -8,6 +8,16 @@ export * from './token' export * from './state' export * from './constants' export * from './utils' +export * from './vote' +export * from './signature' export { VotingMode } from './types' -export type { IRoundDetails, IRoundDetailsResponse, ITokenDetails, IMerkleProof, IVote, CRISPCircuitInputs } from './types' +export type { + IRoundDetails, + IRoundDetailsResponse, + ITokenDetails, + IMerkleProof, + IVote, + CRISPCircuitInputs, + NoirSignatureInputs, +} from './types' diff --git a/examples/CRISP/packages/crisp-sdk/src/signature.ts b/examples/CRISP/packages/crisp-sdk/src/signature.ts new file mode 100644 index 0000000000..d0351af1b3 --- /dev/null +++ b/examples/CRISP/packages/crisp-sdk/src/signature.ts @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +import type { NoirSignatureInputs } from './types' + +import { hashMessage, hexToBytes, recoverPublicKey } from 'viem' + +/** + * Given a message and its signed version, extract the signature components + * @param message The original message + * @param signedMessage The signed message (signature) + * @returns The extracted signature components + */ +export const extractSignature = async (message: string, signedMessage: `0x${string}`): Promise => { + const messageHash = hashMessage(message) + const messageBytes = hexToBytes(messageHash) + + const publicKey = await recoverPublicKey({ + hash: messageHash, + signature: signedMessage, + }) + + const publicKeyBytes = hexToBytes(publicKey) + const publicKeyX = publicKeyBytes.slice(1, 33) + const publicKeyY = publicKeyBytes.slice(33, 65) + + // Extract r and s from signature (remove v) + const sigBytes = hexToBytes(signedMessage) + const r = sigBytes.slice(0, 32) // First 32 bytes + const s = sigBytes.slice(32, 64) // Next 32 bytes + + const signatureBytes = new Uint8Array(64) + signatureBytes.set(r, 0) + signatureBytes.set(s, 32) + + return { + hashed_message: messageBytes, + pub_key_x: publicKeyX, + pub_key_y: publicKeyY, + signature: signatureBytes, + } +} diff --git a/examples/CRISP/packages/crisp-sdk/src/types.ts b/examples/CRISP/packages/crisp-sdk/src/types.ts index e57f5f31ee..6e9e2e547a 100644 --- a/examples/CRISP/packages/crisp-sdk/src/types.ts +++ b/examples/CRISP/packages/crisp-sdk/src/types.ts @@ -176,3 +176,25 @@ export interface BFVParams { plaintextModulus: bigint moduli: BigInt64Array } + +/** + * Interface representing the inputs for Noir signature verification + */ +export interface NoirSignatureInputs { + /** + * X coordinate of the public key + */ + pub_key_x: Uint8Array + /** + * Y coordinate of the public key + */ + pub_key_y: Uint8Array + /** + * The signature to verify + */ + signature: Uint8Array + /** + * The hashed message that was signed + */ + hashed_message: Uint8Array +} diff --git a/examples/CRISP/packages/crisp-sdk/src/vote.ts b/examples/CRISP/packages/crisp-sdk/src/vote.ts index c1e1e56e70..18135b59d2 100644 --- a/examples/CRISP/packages/crisp-sdk/src/vote.ts +++ b/examples/CRISP/packages/crisp-sdk/src/vote.ts @@ -8,6 +8,7 @@ import { ZKInputsGenerator } from '@enclave/crisp-zk-inputs' import { BFVParams, type CRISPCircuitInputs, type IVote, VotingMode } from './types' import { toBinary } from './utils' import { MAXIMUM_VOTE_VALUE, DEFAULT_BFV_PARAMS } from './constants' +import { extractSignature } from './signature' /** * This utility function calculates the first valid index for vote options @@ -180,3 +181,27 @@ export const encryptVoteAndGenerateCRISPInputs = async ( merkle_proof_siblings: [], } } + +/** + * Generate the CRISP circuit inputs by extracting signature components and adding them to the partial inputs + * @todo Add the merkle tree inputs too + * @param partialInputs The partial CRISP circuit inputs + * @param signature The voter's signature + * @param message The signed message + * @returns The complete CRISP circuit inputs + */ +export const generateCRISPInputs = async ( + partialInputs: CRISPCircuitInputs, + signature: `0x${string}`, + message: string, +): Promise => { + const { hashed_message, pub_key_x, pub_key_y, signature: extractedSignature } = await extractSignature(message, signature) + + return { + ...partialInputs, + hashed_message: Array.from(hashed_message).map((b) => b.toString()), + public_key_x: Array.from(pub_key_x).map((b) => b.toString()), + public_key_y: Array.from(pub_key_y).map((b) => b.toString()), + signature: Array.from(extractedSignature).map((b) => b.toString()), + } +} diff --git a/examples/CRISP/packages/crisp-sdk/tests/constants.ts b/examples/CRISP/packages/crisp-sdk/tests/constants.ts index ce3a8c6784..2261a03a66 100644 --- a/examples/CRISP/packages/crisp-sdk/tests/constants.ts +++ b/examples/CRISP/packages/crisp-sdk/tests/constants.ts @@ -5,3 +5,8 @@ // or FITNESS FOR A PARTICULAR PURPOSE. export const CRISP_SERVER_URL = 'http://localhost:4000' + +export const MESSAGE = 'Vote for round 0' +export const SIGNATURE = + '0x1641431d0ed3fd86814f026da62e11434b53c6a85162fea7f99218bf3c307dec7f361c235f07b658780afd91a5c9d68d6a4b14d5eb0511f6688d3e91140eec121b' +export const VOTE = { yes: 10n, no: 0n } diff --git a/examples/CRISP/packages/crisp-sdk/tests/signature.test.ts b/examples/CRISP/packages/crisp-sdk/tests/signature.test.ts new file mode 100644 index 0000000000..c76da6a120 --- /dev/null +++ b/examples/CRISP/packages/crisp-sdk/tests/signature.test.ts @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +import { describe, it, expect } from 'vitest' + +import { extractSignature } from '../src/signature' +import { MESSAGE, SIGNATURE } from './constants' + +describe('Signature', () => { + describe('extractSignature', () => { + it('should extract signature components correctly', async () => { + const { hashed_message, pub_key_x, pub_key_y, signature: extractedSignature } = await extractSignature(MESSAGE, SIGNATURE) + + expect(hashed_message).toBeInstanceOf(Uint8Array) + expect(pub_key_x).toBeInstanceOf(Uint8Array) + expect(pub_key_y).toBeInstanceOf(Uint8Array) + expect(extractedSignature).toBeInstanceOf(Uint8Array) + }) + }) +}) diff --git a/examples/CRISP/packages/crisp-sdk/tests/vote.test.ts b/examples/CRISP/packages/crisp-sdk/tests/vote.test.ts index 133ef74629..11e3c3998d 100644 --- a/examples/CRISP/packages/crisp-sdk/tests/vote.test.ts +++ b/examples/CRISP/packages/crisp-sdk/tests/vote.test.ts @@ -7,16 +7,29 @@ import { describe, it, expect } from 'vitest' import { ZKInputsGenerator } from '@enclave/crisp-zk-inputs' -import { calculateValidIndicesForPlaintext, decodeTally, encodeVote, encryptVoteAndGenerateCRISPInputs, validateVote } from '../src/vote' +import { + calculateValidIndicesForPlaintext, + decodeTally, + encodeVote, + encryptVoteAndGenerateCRISPInputs, + generateCRISPInputs, + validateVote, +} from '../src/vote' import { BFVParams, VotingMode } from '../src/types' import { DEFAULT_BFV_PARAMS, MAXIMUM_VOTE_VALUE } from '../src' +import { MESSAGE, SIGNATURE, VOTE } from './constants' + describe('Vote', () => { const votingPower = 10n + + let zkInputsGenerator = ZKInputsGenerator.withDefaults() + let publicKey = zkInputsGenerator.generatePublicKey() + const previousCiphertext = zkInputsGenerator.encryptVote(publicKey, new BigInt64Array([0n])) + describe('encodeVote', () => { - const vote = { yes: 10n, no: 0n } it('should work for valid votes', () => { - const encoded = encodeVote(vote, VotingMode.GOVERNANCE, votingPower) + const encoded = encodeVote(VOTE, VotingMode.GOVERNANCE, votingPower) expect(encoded.length).toBe(DEFAULT_BFV_PARAMS.degree) }) it('should work with small moduli', () => { @@ -24,9 +37,9 @@ describe('Vote', () => { degree: 10, // Irrelevant for this test. plaintextModulus: 0n, - moduli: [0n], + moduli: new BigInt64Array([0n]), } - const encoded = encodeVote(vote, VotingMode.GOVERNANCE, votingPower, params) + const encoded = encodeVote(VOTE, VotingMode.GOVERNANCE, votingPower, params) expect(encoded.length).toBe(params.degree) // 01010 = 10 @@ -113,15 +126,8 @@ describe('Vote', () => { }) describe('encryptVoteAndGenerateCRISPInputs', () => { - const vote = { yes: 10n, no: 0n } - const votingPower = 10n - - let zkInputsGenerator = ZKInputsGenerator.withDefaults() - let publicKey = zkInputsGenerator.generatePublicKey() - const previousCiphertext = zkInputsGenerator.encryptVote(publicKey, new BigInt64Array([0n])) - it('should encrypt a vote and generate the circuit inputs', async () => { - const encodedVote = encodeVote(vote, VotingMode.GOVERNANCE, votingPower) + const encodedVote = encodeVote(VOTE, VotingMode.GOVERNANCE, votingPower) const crispInputs = await encryptVoteAndGenerateCRISPInputs(encodedVote, publicKey, previousCiphertext) expect(crispInputs.ct_add).toBeInstanceOf(Object) @@ -135,4 +141,26 @@ describe('Vote', () => { expect(crispInputs.p1is).toBeInstanceOf(Array) }) }) + + describe('generateCRISPInputs', () => { + it('should add the remaining inputs to the CRISP inputs object', async () => { + const encodedVote = encodeVote(VOTE, VotingMode.GOVERNANCE, votingPower) + const partialInputs = await encryptVoteAndGenerateCRISPInputs(encodedVote, publicKey, previousCiphertext) + const crispInputs = await generateCRISPInputs(partialInputs, SIGNATURE, MESSAGE) + + expect(crispInputs.ct_add).toBeInstanceOf(Object) + expect(crispInputs.params).toBeInstanceOf(Object) + expect(crispInputs.ct0is).toBeInstanceOf(Array) + expect(crispInputs.ct1is).toBeInstanceOf(Array) + expect(crispInputs.pk0is).toBeInstanceOf(Array) + expect(crispInputs.pk1is).toBeInstanceOf(Array) + expect(crispInputs.r1is).toBeInstanceOf(Array) + expect(crispInputs.r2is).toBeInstanceOf(Array) + expect(crispInputs.p1is).toBeInstanceOf(Array) + expect(crispInputs.hashed_message).toBeInstanceOf(Array) + expect(crispInputs.public_key_x).toBeInstanceOf(Array) + expect(crispInputs.public_key_y).toBeInstanceOf(Array) + expect(crispInputs.signature).toBeInstanceOf(Array) + }) + }) })