diff --git a/examples/CRISP/crates/evm_helpers/src/lib.rs b/examples/CRISP/crates/evm_helpers/src/lib.rs index d53dacffae..c8b2ef7a77 100644 --- a/examples/CRISP/crates/evm_helpers/src/lib.rs +++ b/examples/CRISP/crates/evm_helpers/src/lib.rs @@ -5,7 +5,9 @@ // or FITNESS FOR A PARTICULAR PURPOSE. use alloy::{ - network::{Ethereum, EthereumWallet}, primitives::{Address, Bytes, U256}, providers::{ + network::{Ethereum, EthereumWallet}, + primitives::{Address, Bytes, I256, U256}, + providers::{ Identity, ProviderBuilder, RootProvider, fillers::{ BlobGasFiller, ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller, WalletFiller, @@ -20,8 +22,7 @@ sol! { #[sol(rpc)] contract CRISPProgram { function setMerkleRoot(uint256 e3_id, uint256 _root) external; - function getSlotIndex(uint256 e3_id, address slot_address) external view returns (uint256); - function isSlotEmptyByAddress(uint256 e3_id, address slot_address) external view returns (bool); + function getSlotIndex(uint256 e3_id, address slot_address) external view returns (int256); function publishInput(uint256 e3_id, bytes data) external; } } @@ -128,37 +129,26 @@ impl CRISPContract { }) } - /// Get the slot index from a given slot address + /// Get the slot index from a given slot address. + /// Returns `None` when the slot is empty (contract returns -1). pub async fn get_slot_index_from_address( &self, e3_id: U256, slot_address: Address, - ) -> Result { + ) -> Result> { let contract = CRISPProgram::new(self.contract_address, self.provider.as_ref()); match contract.getSlotIndex(e3_id, slot_address).call().await { - Ok(slot_index) => Ok(slot_index), + Ok(slot_index) => { + if slot_index < I256::ZERO { + Ok(None) + } else { + Ok(Some(slot_index.as_u64())) + } + } Err(e) => Err(eyre::eyre!("Failed to get slot index: {}", e)), } } - - /// Check if a slot is empty by its address - pub async fn get_is_slot_empty_by_address( - &self, - e3_id: U256, - slot_address: Address, - ) -> Result { - let contract = CRISPProgram::new(self.contract_address, self.provider.as_ref()); - - match contract - .isSlotEmptyByAddress(e3_id, slot_address) - .call() - .await - { - Ok(is_empty) => Ok(is_empty), - Err(e) => Err(eyre::eyre!("Failed to check if slot is empty: {}", e)), - } - } } impl

CRISPContract

{ diff --git a/examples/CRISP/packages/crisp-contracts/contracts/CRISPProgram.sol b/examples/CRISP/packages/crisp-contracts/contracts/CRISPProgram.sol index 89bdc50d14..87df50b1d2 100644 --- a/examples/CRISP/packages/crisp-contracts/contracts/CRISPProgram.sol +++ b/examples/CRISP/packages/crisp-contracts/contracts/CRISPProgram.sol @@ -240,21 +240,10 @@ contract CRISPProgram is IE3Program, Ownable { /// @notice Get the slot index for a given E3 ID and slot address /// @param e3Id The E3 program ID /// @param slotAddress The slot address - /// @return The slot index - function getSlotIndex(uint256 e3Id, address slotAddress) external view returns (uint40) { + /// @return The slot index, or -1 if the slot is empty + function getSlotIndex(uint256 e3Id, address slotAddress) external view returns (int40) { uint40 storedIndexPlusOne = e3Data[e3Id].voteSlots[slotAddress]; - if (storedIndexPlusOne == 0) { - revert SlotIsEmpty(); - } - return storedIndexPlusOne - 1; - } - - /// @notice Check if a slot is empty for a given E3 ID and slot address - /// @param e3Id The E3 program ID - /// @param slotAddress The slot address - /// @return Whether the slot is empty or not - function isSlotEmptyByAddress(uint256 e3Id, address slotAddress) external view returns (bool) { - return e3Data[e3Id].voteSlots[slotAddress] == 0; + return int40(storedIndexPlusOne) - 1; } /// @inheritdoc IE3Program diff --git a/examples/CRISP/packages/crisp-sdk/README.md b/examples/CRISP/packages/crisp-sdk/README.md index 06e6011322..d03175310d 100644 --- a/examples/CRISP/packages/crisp-sdk/README.md +++ b/examples/CRISP/packages/crisp-sdk/README.md @@ -149,10 +149,10 @@ const address = await getAddressFromSignature(signature, messageHash) #### State Utilities ```typescript -import { getPreviousCiphertext, getIsSlotEmpty } from '@crisp-e3/sdk' +import { getPreviousCiphertext } from '@crisp-e3/sdk' const previousCiphertext = await getPreviousCiphertext(serverUrl, e3Id, slotAddress) -const isEmpty = await getIsSlotEmpty(serverUrl, e3Id, slotAddress) +// Returns undefined when the slot is empty (404) ``` ## API @@ -170,10 +170,8 @@ const isEmpty = await getIsSlotEmpty(serverUrl, e3Id, slotAddress) - `getRoundDetails(serverUrl: string, e3Id: number): Promise` - Get round details - `getRoundTokenDetails(serverUrl: string, e3Id: number): Promise` - Get token details for a round -- `getPreviousCiphertext(serverUrl: string, e3Id: number, address: string): Promise` - - Get previous ciphertext for a slot -- `getIsSlotEmpty(serverUrl: string, e3Id: number, address: string): Promise` - Check if a - slot is empty +- `getPreviousCiphertext(serverUrl: string, e3Id: number, address: string): Promise` - + Get previous ciphertext for a slot (undefined when slot is empty) ### Token Functions diff --git a/examples/CRISP/packages/crisp-sdk/src/constants.ts b/examples/CRISP/packages/crisp-sdk/src/constants.ts index bc60a7281a..1f7863fe59 100644 --- a/examples/CRISP/packages/crisp-sdk/src/constants.ts +++ b/examples/CRISP/packages/crisp-sdk/src/constants.ts @@ -9,7 +9,6 @@ import { hashMessage } from 'viem' export const CRISP_SERVER_TOKEN_TREE_ENDPOINT = 'state/token-holders' export const CRISP_SERVER_STATE_LITE_ENDPOINT = 'state/lite' export const CRISP_SERVER_PREVIOUS_CIPHERTEXT_ENDPOINT = 'state/previous-ciphertext' -export const CRISP_SERVER_IS_SLOT_EMPTY_ENDPOINT = 'state/is-slot-empty' export const MERKLE_TREE_MAX_DEPTH = 20 // static, hardcoded in the circuit. diff --git a/examples/CRISP/packages/crisp-sdk/src/sdk.ts b/examples/CRISP/packages/crisp-sdk/src/sdk.ts index ae7b29ff0b..3990b70650 100644 --- a/examples/CRISP/packages/crisp-sdk/src/sdk.ts +++ b/examples/CRISP/packages/crisp-sdk/src/sdk.ts @@ -4,7 +4,7 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -import { getIsSlotEmpty, getPreviousCiphertext } from './state' +import { getPreviousCiphertext } from './state' import { generateMaskVoteProof, generateVoteProof } from './vote' import type { MaskVoteProofRequest, ProofData, VoteProofRequest } from './types' @@ -33,13 +33,7 @@ export class CrispSDK { * @returns A promise that resolves to the generated proof data. */ async generateMaskVoteProof(maskProofInputs: MaskVoteProofRequest): Promise { - const isSlotEmpty = await getIsSlotEmpty(this.serverUrl, maskProofInputs.e3Id, maskProofInputs.slotAddress) - - let previousCiphertext - - if (!isSlotEmpty) { - previousCiphertext = await getPreviousCiphertext(this.serverUrl, maskProofInputs.e3Id, maskProofInputs.slotAddress) - } + const previousCiphertext = await getPreviousCiphertext(this.serverUrl, maskProofInputs.e3Id, maskProofInputs.slotAddress) return generateMaskVoteProof({ ...maskProofInputs, @@ -49,17 +43,16 @@ export class CrispSDK { /** * Generate a proof for a vote. + * + * Note: The previous ciphertext is not used in the proof computation. This method still calls + * the same server API (previous-ciphertext) as {@link generateMaskVoteProof} to prevent the + * server from inferring the vote type (mask vs normal) from the client's API usage pattern. + * * @param voteProofInputs - The inputs required to generate the vote proof. * @returns A promise that resolves to the generated proof data. */ async generateVoteProof(voteProofInputs: VoteProofRequest): Promise { - const isSlotEmpty = await getIsSlotEmpty(this.serverUrl, voteProofInputs.e3Id, voteProofInputs.slotAddress) - - let previousCiphertext - - if (!isSlotEmpty) { - previousCiphertext = await getPreviousCiphertext(this.serverUrl, voteProofInputs.e3Id, voteProofInputs.slotAddress) - } + const previousCiphertext = await getPreviousCiphertext(this.serverUrl, voteProofInputs.e3Id, voteProofInputs.slotAddress) return generateVoteProof({ ...voteProofInputs, diff --git a/examples/CRISP/packages/crisp-sdk/src/state.ts b/examples/CRISP/packages/crisp-sdk/src/state.ts index f744a6f516..eadc3d9a88 100644 --- a/examples/CRISP/packages/crisp-sdk/src/state.ts +++ b/examples/CRISP/packages/crisp-sdk/src/state.ts @@ -4,11 +4,7 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -import { - CRISP_SERVER_STATE_LITE_ENDPOINT, - CRISP_SERVER_PREVIOUS_CIPHERTEXT_ENDPOINT, - CRISP_SERVER_IS_SLOT_EMPTY_ENDPOINT, -} from './constants' +import { CRISP_SERVER_STATE_LITE_ENDPOINT, CRISP_SERVER_PREVIOUS_CIPHERTEXT_ENDPOINT } from './constants' import type { RoundDetailsResponse, RoundDetails, TokenDetails } from './types' @@ -59,13 +55,15 @@ export const getRoundTokenDetails = async (serverUrl: string, e3Id: number): Pro } /** - * Get the previous ciphertext for a slot from the CRISP server + * Get the previous ciphertext for a slot from the CRISP server. + * Returns undefined when the slot is empty (404). + * * @param serverUrl - The base URL of the CRISP server * @param e3Id - The e3Id of the round * @param address - The address of the slot - * @returns The previous ciphertext for the slot + * @returns The previous ciphertext for the slot, or undefined if the slot is empty */ -export const getPreviousCiphertext = async (serverUrl: string, e3Id: number, address: string): Promise => { +export const getPreviousCiphertext = async (serverUrl: string, e3Id: number, address: string): Promise => { const response = await fetch(`${serverUrl}/${CRISP_SERVER_PREVIOUS_CIPHERTEXT_ENDPOINT}`, { method: 'POST', headers: { @@ -74,36 +72,15 @@ export const getPreviousCiphertext = async (serverUrl: string, e3Id: number, add body: JSON.stringify({ round_id: e3Id, address }), }) - if (!response.ok) { - throw new Error(`Failed to fetch previous ciphertext: ${response.statusText}`) + if (response.status === 404) { + return undefined } - const data = await response.json() - - return new Uint8Array(data.ciphertext) -} - -/** - * Check if a slot is empty for a given E3 ID and slot address - * @param serverUrl - The base URL of the CRISP server - * @param e3Id - The e3Id of the round - * @param address - The address of the slot - * @returns Whether the slot is empty or not - */ -export const getIsSlotEmpty = async (serverUrl: string, e3Id: number, address: string): Promise => { - const response = await fetch(`${serverUrl}/${CRISP_SERVER_IS_SLOT_EMPTY_ENDPOINT}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ round_id: e3Id, address }), - }) - if (!response.ok) { - throw new Error(`Failed to check if slot is empty: ${response.statusText}`) + throw new Error(`Failed to fetch previous ciphertext: ${response.statusText}`) } const data = await response.json() - return data.is_empty as boolean + return new Uint8Array(data.ciphertext) } diff --git a/examples/CRISP/packages/crisp-sdk/tests/vote.test.ts b/examples/CRISP/packages/crisp-sdk/tests/vote.test.ts index 9fb9f0d51a..9b7c2fdb1c 100644 --- a/examples/CRISP/packages/crisp-sdk/tests/vote.test.ts +++ b/examples/CRISP/packages/crisp-sdk/tests/vote.test.ts @@ -31,14 +31,12 @@ describe('Vote', () => { const mockGetPreviousCiphertextResponse = () => ({ ok: true, + status: 200, json: async () => ({ ciphertext: previousCiphertext }), }) as Response - const mockIsSlotEmptyResponse = (isEmpty: boolean) => - ({ - ok: true, - json: async () => ({ is_empty: isEmpty }), - }) as Response + const mockPreviousCiphertextNotFoundResponse = () => + ({ ok: false, status: 404 }) as Response beforeEach(() => { vi.clearAllMocks() @@ -151,7 +149,7 @@ describe('Vote', () => { describe('generateVoteProof', () => { it('Should generate a valid vote proof', { timeout: 300000 }, async () => { - vi.spyOn(global, 'fetch').mockResolvedValueOnce(mockIsSlotEmptyResponse(true)) + vi.spyOn(global, 'fetch').mockResolvedValueOnce(mockPreviousCiphertextNotFoundResponse()) const proof = await sdk.generateVoteProof({ vote, @@ -181,7 +179,7 @@ describe('Vote', () => { describe('generateMaskVoteProof', () => { it('Should generate a valid mask vote proof when there are no votes in the slot', { timeout: 300000 }, async () => { - vi.spyOn(global, 'fetch').mockResolvedValueOnce(mockIsSlotEmptyResponse(true)) + vi.spyOn(global, 'fetch').mockResolvedValueOnce(mockPreviousCiphertextNotFoundResponse()) const proof = await sdk.generateMaskVoteProof({ balance, @@ -207,9 +205,7 @@ describe('Vote', () => { }) it('Should generate a valid mask vote proof when there is a previous vote in the slot', { timeout: 300000 }, async () => { - vi.spyOn(global, 'fetch') - .mockResolvedValueOnce(mockIsSlotEmptyResponse(false)) - .mockResolvedValueOnce(mockGetPreviousCiphertextResponse()) + vi.spyOn(global, 'fetch').mockResolvedValueOnce(mockGetPreviousCiphertextResponse()) const proof = await sdk.generateMaskVoteProof({ balance, diff --git a/examples/CRISP/server/Dockerfile b/examples/CRISP/server/Dockerfile index 219a8646d6..8c6ad43ff6 100644 --- a/examples/CRISP/server/Dockerfile +++ b/examples/CRISP/server/Dockerfile @@ -1,5 +1,5 @@ ############### stage 0: base-dev ############### -ARG RUST_VERSION=1.86.0 +ARG RUST_VERSION=1.88.0 ARG SKIP_SOLIDITY=0 FROM rust:${RUST_VERSION}-slim-bullseye AS base-dev diff --git a/examples/CRISP/server/src/server/models.rs b/examples/CRISP/server/src/server/models.rs index 26cc897420..425458a101 100644 --- a/examples/CRISP/server/src/server/models.rs +++ b/examples/CRISP/server/src/server/models.rs @@ -122,17 +122,6 @@ pub struct PreviousCiphertextResponse { pub ciphertext: Vec, } -#[derive(Debug, Deserialize, Serialize)] -pub struct IsSlotEmptyRequest { - pub round_id: u64, - pub address: String, -} - -#[derive(Serialize)] -pub struct IsSlotEmptyResponse { - pub is_empty: bool, -} - #[derive(Debug, Deserialize, Serialize)] pub struct ComputeProviderParams { pub name: String, diff --git a/examples/CRISP/server/src/server/routes/state.rs b/examples/CRISP/server/src/server/routes/state.rs index 3c91b6aebb..33adc67907 100644 --- a/examples/CRISP/server/src/server/routes/state.rs +++ b/examples/CRISP/server/src/server/routes/state.rs @@ -9,8 +9,8 @@ use std::str::FromStr; use crate::server::{ app_data::AppData, models::{ - GetRoundRequest, IsSlotEmptyRequest, IsSlotEmptyResponse, PreviousCiphertextRequest, - PreviousCiphertextResponse, RoundRequestWithRequester, WebhookPayload, + GetRoundRequest, PreviousCiphertextRequest, PreviousCiphertextResponse, + RoundRequestWithRequester, WebhookPayload, }, CONFIG, }; @@ -40,8 +40,7 @@ pub fn setup_routes(config: &mut web::ServiceConfig) { .route( "/previous-ciphertext", web::post().to(handle_get_previous_ciphertext), - ) - .route("/is-slot-empty", web::post().to(handle_is_slot_empty)), + ), ); } @@ -81,7 +80,8 @@ async fn handle_get_previous_ciphertext( .get_slot_index_from_address(U256::from(incoming.round_id), address) .await { - Ok(index) => index.to::(), + Ok(Some(index)) => index, + Ok(None) => return HttpResponse::NotFound().body("Ciphertext not found"), Err(e) => { error!("Error getting slot index from address: {:?}", e); return HttpResponse::InternalServerError() @@ -302,45 +302,6 @@ async fn get_token_holders_hashes( } } -/// Check if a slot is empty given an address -/// # Arguments -/// * `IsSlotEmptyRequest` - The request containing round_id and address -async fn handle_is_slot_empty(data: web::Json) -> impl Responder { - let incoming = data.into_inner(); - - let contract = - match CRISPContractFactory::create_read(&CONFIG.http_rpc_url, &CONFIG.e3_program_address) - .await - { - Ok(contract) => contract, - Err(e) => { - error!("Failed to create CRISP contract: {:?}", e); - return HttpResponse::InternalServerError().body("Failed to create CRISP contract"); - } - }; - - let address = match Address::from_str(incoming.address.as_str()) { - Ok(addr) => addr, - Err(e) => { - error!("Invalid address format: {:?}", e); - return HttpResponse::BadRequest().body("Invalid address format"); - } - }; - - let is_empty = match contract - .get_is_slot_empty_by_address(U256::from(incoming.round_id), address) - .await - { - Ok(empty) => empty, - Err(e) => { - error!("Error checking if slot is empty: {:?}", e); - return HttpResponse::InternalServerError().body("Failed to check if slot is empty"); - } - }; - - HttpResponse::Ok().json(IsSlotEmptyResponse { is_empty }) -} - /// Get the eligible addresses for a given round /// # Arguments /// * `GetRoundRequest` - The request data containing the round ID