Skip to content
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
25 changes: 25 additions & 0 deletions examples/CRISP/circuits/src/ecdsa.nr
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
15 changes: 6 additions & 9 deletions examples/CRISP/circuits/src/utils.nr
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,9 @@
use polynomial::Polynomial;

// Check that all valid coefficients are either 0 or 1
pub fn check_coefficient_values<let D: u32>(
k1: Polynomial<D>,
q_mod_t: Field,
) {
// This value would allow to fit
// 268435456 for yes and 268435456 for no
pub fn check_coefficient_values<let D: u32>(k1: Polynomial<D>, 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;

Expand All @@ -24,14 +21,14 @@ pub fn check_coefficient_values<let D: u32>(
// 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]
Expand Down
9 changes: 8 additions & 1 deletion examples/CRISP/client/src/hooks/voting/useVoteCasting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -21,6 +23,7 @@ export const useVoteCasting = () => {
setTxUrl,
} = useVoteManagementContext();

const { signMessageAsync } = useSignMessage();
const { showToast } = useNotificationAlertContext();
const navigate = useNavigate();
const [isLoading, setIsLoading] = useState<boolean>(false);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -114,7 +120,8 @@ export const useVoteCasting = () => {
setTxUrl,
showToast,
navigate,
handleVoteEncryption
handleVoteEncryption,
signMessageAsync,
]);

return { castVoteWithProof, isLoading };
Expand Down
19 changes: 0 additions & 19 deletions examples/CRISP/client/src/utils/vote.ts

This file was deleted.

12 changes: 11 additions & 1 deletion examples/CRISP/packages/crisp-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
45 changes: 45 additions & 0 deletions examples/CRISP/packages/crisp-sdk/src/signature.ts
Original file line number Diff line number Diff line change
@@ -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<NoirSignatureInputs> => {
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,
}
}
22 changes: 22 additions & 0 deletions examples/CRISP/packages/crisp-sdk/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
25 changes: 25 additions & 0 deletions examples/CRISP/packages/crisp-sdk/src/vote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<CRISPCircuitInputs> => {
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()),
}
}
5 changes: 5 additions & 0 deletions examples/CRISP/packages/crisp-sdk/tests/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
23 changes: 23 additions & 0 deletions examples/CRISP/packages/crisp-sdk/tests/signature.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
})
54 changes: 41 additions & 13 deletions examples/CRISP/packages/crisp-sdk/tests/vote.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,39 @@
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', () => {
const params: BFVParams = {
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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Comment thread
ctrlc03 marked this conversation as resolved.

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