Skip to content

Commit

Permalink
New anonymous flow
Browse files Browse the repository at this point in the history
  • Loading branch information
marcvelmer committed Nov 28, 2023
1 parent 28b0295 commit a2fe1c0
Show file tree
Hide file tree
Showing 9 changed files with 227 additions and 88 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.6.0] - 2023-11-28

### Added

- New election service functions `nextElectionId` and `getElectionSalt`.

### Changed

- [**BREAKING**] Refactored options for `isInCensus`, `hasAlreadyVoted`, `isAbleToVote` and `votesLeftCount`.
- [**BREAKING**] New options for `AnonymousVote` which enable to add the user's signature.
- [**BREAKING**] New internal anonymous flow when signature is given by the consumer.

## [0.5.3] - 2023-11-28

### Added
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@vocdoni/sdk",
"author": "Vocdoni",
"version": "0.5.3",
"version": "0.6.0",
"description": "⚒️An SDK for building applications on top of Vocdoni API",
"repository": "https://github.com/vocdoni/vocdoni-sdk.git",
"license": "AGPL-3.0-or-later",
Expand Down
129 changes: 71 additions & 58 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,17 @@ import {
CspVote,
ElectionStatus,
ElectionStatusReady,
HasAlreadyVotedOptions,
InvalidElection,
IsAbleToVoteOptions,
IsInCensusOptions,
PlainCensus,
PublishedElection,
SendTokensOptions,
TokenCensus,
UnpublishedElection,
Vote,
VotesLeftCountOptions,
WeightedCensus,
} from './types';
import { API_URL, CENSUS_CHUNK_SIZE, EXPLORER_URL, FAUCET_URL, TX_WAIT_OPTIONS } from './util/constants';
Expand Down Expand Up @@ -212,14 +216,14 @@ export class VocdoniSDKClient {

private setAccountSIK(
electionId: string,
sik: string,
signature: string,
password: string,
censusProof: CensusProof,
wallet: Wallet | Signer
): Promise<void> {
return wallet
.getAddress()
.then((address) => AnonymousService.calcSik(address, sik, password))
.then((address) => AnonymousService.calcSik(address, signature, password))
.then((calculatedSIK) => {
const registerSIKTx = AccountCore.generateRegisterSIKTransaction(electionId, calculatedSIK, censusProof);
return this.accountService.signTransaction(registerSIKTx.tx, registerSIKTx.message, wallet);
Expand All @@ -233,30 +237,31 @@ export class VocdoniSDKClient {
*
* @param election
* @param wallet
* @param signature
* @param password
* @returns {Promise<ZkProof>}
*/
private async calcZKProofForWallet(
election: PublishedElection,
wallet: Wallet | Signer,
signature: string,
password: string = '0'
): Promise<ZkProof> {
const [address, sik, censusProof] = await Promise.all([
const [address, censusProof] = await Promise.all([
wallet.getAddress(),
this.anonymousService.signSIKPayload(wallet),
this.fetchProofForWallet(election.census.censusId, wallet),
]);

return this.anonymousService
.fetchAccountSIK(address)
.catch(() => this.setAccountSIK(election.id, sik, password, censusProof, wallet))
.catch(() => this.setAccountSIK(election.id, signature, password, censusProof, wallet))
.then(() => this.anonymousService.fetchZKProof(address))
.then((zkProof) =>
AnonymousService.prepareCircuitInputs(
election.id,
address,
password,
sik,
signature,
censusProof.value,
censusProof.value,
zkProof.censusRoot,
Expand Down Expand Up @@ -621,97 +626,99 @@ export class VocdoniSDKClient {
/**
* Checks if the user is in census.
*
* @param {string} electionId The id of the election
* @param {Object} key The key in the census to check
* @param {HasAlreadyVotedOptions} options Options for is in census
* @returns {Promise<boolean>}
*/
async isInCensus(electionId?: string, key?: string): Promise<boolean> {
if (!this.electionId && !electionId) {
throw Error('No election set');
}
if (!this.wallet && !key) {
throw Error('No key given or Wallet not found');
}

const election = await this.fetchElection(electionId ?? this.electionId);
let proofPromise;

if (key) {
proofPromise = this.censusService.fetchProof(election.census.censusId, key);
} else if (election) {
proofPromise = this.fetchProofForWallet(election.census.censusId, this.wallet);
} else {
proofPromise = Promise.reject();
}
async isInCensus(options?: IsInCensusOptions): Promise<boolean> {
const settings = {
wallet: options?.wallet ?? this.wallet,
electionId: options?.electionId ?? this.electionId,
...options,
};
invariant(settings.wallet, 'No wallet or signer set or given');
invariant(settings.electionId, 'No election identifier set or given');

return proofPromise.then(() => true).catch(() => false);
return this.fetchElection(settings.electionId)
.then((election) => this.fetchProofForWallet(election.census.censusId, settings.wallet))
.then(() => true)
.catch(() => false);
}

/**
* Checks if the user has already voted
*
* @param {string} electionId The id of the election
* @param {HasAlreadyVotedOptions} options Options for has already voted
* @returns {Promise<string>} The id of the vote
*/
async hasAlreadyVoted(electionId?: string): Promise<string> {
if (!this.electionId && !electionId) {
throw Error('No election set');
}
if (!this.wallet) {
throw Error('No wallet found');
}
async hasAlreadyVoted(options?: HasAlreadyVotedOptions): Promise<string> {
const settings = {
wallet: options?.wallet ?? this.wallet,
electionId: options?.electionId ?? this.electionId,
...options,
};
invariant(settings.wallet, 'No wallet or signer set or given');
invariant(settings.electionId, 'No election identifier set or given');

const election = await this.fetchElection(electionId ?? this.electionId);
const election = await this.fetchElection(settings.electionId);

if (election.electionType.anonymous) {
throw Error('This function cannot be used with an anonymous election');
if (election.electionType.anonymous && !settings?.voteId) {
throw Error('This function cannot be used without a vote identifier for an anonymous election');
}

return this.wallet
return settings.wallet
.getAddress()
.then((address) => this.voteService.info(address.toLowerCase(), election.id))
.then((address) =>
this.voteService.info(
election.electionType.anonymous ? settings.voteId : keccak256(address.toLowerCase() + election.id)
)
)
.then((voteInfo) => voteInfo.voteID)
.catch(() => null);
}

/**
* Checks if the user is able to vote
*
* @param {string} electionId The id of the election
* @param {IsAbleToVoteOptions} options Options for is able to vote
* @returns {Promise<boolean>}
*/
isAbleToVote(electionId?: string): Promise<boolean> {
return this.votesLeftCount(electionId).then((votesLeftCount) => votesLeftCount > 0);
isAbleToVote(options?: IsAbleToVoteOptions): Promise<boolean> {
return this.votesLeftCount(options).then((votesLeftCount) => votesLeftCount > 0);
}

/**
* Checks how many times a user can submit their vote
*
* @param {string} electionId The id of the election
* @param {VotesLeftCountOptions} options Options for votes left count
* @returns {Promise<number>}
*/
async votesLeftCount(electionId?: string): Promise<number> {
if (!this.electionId && !electionId) {
throw Error('No election set');
}
if (!this.wallet) {
throw Error('No wallet found');
}
async votesLeftCount(options?: VotesLeftCountOptions): Promise<number> {
const settings = {
wallet: options?.wallet ?? this.wallet,
electionId: options?.electionId ?? this.electionId,
...options,
};
invariant(settings.wallet, 'No wallet or signer set or given');
invariant(settings.electionId, 'No election identifier set or given');

const election = await this.fetchElection(electionId ?? this.electionId);
const election = await this.fetchElection(settings.electionId);

if (election.electionType.anonymous) {
throw Error('This function cannot be used with an anonymous election');
if (election.electionType.anonymous && !settings?.voteId) {
throw Error('This function cannot be used without a vote identifier for an anonymous election');
}

const isInCensus = await this.isInCensus(election.id);
const isInCensus = await this.isInCensus({ electionId: election.id });
if (!isInCensus) {
return Promise.resolve(0);
}

return this.wallet
.getAddress()
.then((address) => this.voteService.info(address.toLowerCase(), election.id))
.then((address) =>
this.voteService.info(
election.electionType.anonymous ? settings.voteId : keccak256(address.toLowerCase() + election.id)
)
)
.then((voteInfo) => election.voteType.maxVoteOverwrites - voteInfo.overwriteCount)
.catch(() => election.voteType.maxVoteOverwrites + 1);
}
Expand All @@ -737,10 +744,16 @@ export class VocdoniSDKClient {
if (election.census.type == CensusType.WEIGHTED) {
censusProof = await this.fetchProofForWallet(election.census.censusId, this.wallet);
} else if (election.census.type == CensusType.ANONYMOUS) {
let signature: string;
if (vote instanceof AnonymousVote) {
signature = vote.signature ?? (await this.anonymousService.signSIKPayload(this.wallet));
} else {
signature = await this.anonymousService.signSIKPayload(this.wallet);
}
if (vote instanceof AnonymousVote) {
censusProof = await this.calcZKProofForWallet(election, this.wallet, vote.password);
censusProof = await this.calcZKProofForWallet(election, this.wallet, signature, vote.password);
} else {
censusProof = await this.calcZKProofForWallet(election, this.wallet);
censusProof = await this.calcZKProofForWallet(election, this.wallet, signature);
}
} else if (election.census.type == CensusType.CSP && vote instanceof CspVote) {
censusProof = {
Expand Down
41 changes: 26 additions & 15 deletions src/services/anonymous.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,35 +203,46 @@ export class AnonymousService extends Service implements AnonymousServicePropert
censusRoot: string,
censusSiblings: string[]
): Promise<CircuitInputs> {
signature = AnonymousService.signatureToVocdoniSikSignature(strip0x(signature));

const arboElectionId = await AnonymousService.arbo_utils.toHash(electionId);
const ffsignature = AnonymousService.ff_utils.hexToFFBigInt(strip0x(signature)).toString();
const ffpassword = AnonymousService.ff_utils.hexToFFBigInt(hexlify(toUtf8Bytes(password))).toString();

return Promise.all([
AnonymousService.calcNullifier(ffsignature, ffpassword, arboElectionId),
AnonymousService.calcCircuitInputs(signature, password, electionId),
AnonymousService.arbo_utils.toHash(AnonymousService.hex_utils.fromBigInt(BigInt(ensure0x(availableWeight)))),
]).then((data) => ({
electionId: arboElectionId,
nullifier: data[0].toString(),
]).then(([circuitInputs, voteHash]) => ({
electionId: circuitInputs.arboElectionId,
nullifier: circuitInputs.nullifier.toString(),
availableWeight: AnonymousService.arbo_utils.toBigInt(availableWeight).toString(),
voteHash: data[1],
voteHash,
sikRoot: AnonymousService.arbo_utils.toBigInt(sikRoot).toString(),
censusRoot: AnonymousService.arbo_utils.toBigInt(censusRoot).toString(),
address: AnonymousService.arbo_utils.toBigInt(strip0x(address)).toString(),
password: ffpassword,
signature: ffsignature,
password: circuitInputs.ffpassword,
signature: circuitInputs.ffsignature,
voteWeight: AnonymousService.arbo_utils.toBigInt(voteWeight).toString(),
sikSiblings,
censusSiblings,
}));
}

static async calcNullifier(ffsignature: string, ffpassword: string, arboElectionId: string[]): Promise<bigint> {
static async calcCircuitInputs(signature: string, password: string, electionId: string) {
signature = AnonymousService.signatureToVocdoniSikSignature(strip0x(signature));
const arboElectionId = await AnonymousService.arbo_utils.toHash(electionId);
const ffsignature = AnonymousService.ff_utils.hexToFFBigInt(strip0x(signature)).toString();
const ffpassword = AnonymousService.ff_utils.hexToFFBigInt(hexlify(toUtf8Bytes(password))).toString();

const poseidon = await buildPoseidon();
const hash = poseidon([ffsignature, ffpassword, arboElectionId[0], arboElectionId[1]]);
return poseidon.F.toObject(hash);
const nullifier = poseidon.F.toObject(hash);

return { nullifier, arboElectionId, ffsignature, ffpassword };
}

static async calcNullifier(signature: string, password: string, electionId: string): Promise<bigint> {
return this.calcCircuitInputs(signature, password, electionId).then((circuitInputs) => circuitInputs.nullifier);
}

static async calcVoteId(signature: string, password: string, electionId: string): Promise<string> {
return this.calcNullifier(signature, password ?? '0', electionId).then((nullifier) =>
nullifier.toString().length === 76 ? nullifier.toString() : nullifier.toString() + '0'
);
}

static async calcSik(address: string, personal_sign: string, password: string = '0'): Promise<string> {
Expand Down
40 changes: 38 additions & 2 deletions src/services/election.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { Service, ServiceProperties } from './service';
import { Census, InvalidElection, PublishedCensus, PublishedElection, UnpublishedElection } from '../types';
import {
ArchivedElection,
Census,
InvalidElection,
PublishedCensus,
PublishedElection,
UnpublishedElection,
} from '../types';
import { AccountAPI, ElectionAPI, IElectionCreateResponse, IElectionKeysResponse } from '../api';
import { CensusService } from './census';
import { allSettled } from '../util/promise';
Expand All @@ -9,7 +16,8 @@ import { ChainService } from './chain';
import { Wallet } from '@ethersproject/wallet';
import { Signer } from '@ethersproject/abstract-signer';
import { ArchivedCensus } from '../types/census/archived';
import { ArchivedElection } from '../types/election/archived';
import { keccak256 } from '@ethersproject/keccak256';
import { Buffer } from 'buffer';

interface ElectionServiceProperties {
censusService: CensusService;
Expand Down Expand Up @@ -217,6 +225,34 @@ export class ElectionService extends Service implements ElectionServicePropertie
}).then((response) => response.electionID);
}

/**
* Returns an election salt for address
*
* @param {string} address The address of the account
* @param {number} electionCount The election count
* @returns {Promise<string>} The election salt
*/
getElectionSalt(address: string, electionCount: number): Promise<string> {
invariant(this.url, 'No URL set');
invariant(this.chainService, 'No chain service set');
return this.chainService.fetchChainData().then((chainData) => {
return keccak256(Buffer.from(address + chainData.chainId + electionCount.toString()));
});
}

/**
* Returns a numeric election identifier
*
* @param {string} electionId The identifier of the election
* @returns {number} The numeric identifier
*/
getNumericElectionId(electionId: string): number {
const arr = electionId.substring(electionId.length - 8, electionId.length).match(/.{1,2}/g);
const uint32Array = new Uint8Array(arr.map((byte) => parseInt(byte, 16)));
const dataView = new DataView(uint32Array.buffer);
return dataView.getUint32(0);
}

/**
* Fetches the encryption keys from the specified process.
*
Expand Down
Loading

0 comments on commit a2fe1c0

Please sign in to comment.