Skip to content

Commit

Permalink
decrypt and submit results (#24)
Browse files Browse the repository at this point in the history
  • Loading branch information
akorchyn authored Apr 5, 2024
1 parent 716ed6a commit be9262a
Show file tree
Hide file tree
Showing 10 changed files with 867 additions and 22 deletions.
540 changes: 536 additions & 4 deletions relayer/package-lock.json

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion relayer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@
},
"author": "akorchyn",
"dependencies": {
"@supercharge/promise-pool": "^3.2.0",
"bn.js": "^5.2.1",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"miscreant": "^0.3.2",
"secp256k1": "^5.0.0"
"p-retry": "4.6.2",
"secp256k1": "^5.0.0",
"secretjs": "^1.12.5"
},
"devDependencies": {
"@types/cors": "^2.8.17",
Expand Down
109 changes: 109 additions & 0 deletions relayer/src/api/controllers/decryption.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { Request, Response } from "express";
import { getSecretKeys } from "../utils/secret";
import { base_encode } from "near-api-js/lib/utils/serialize";
import { getVoterPublicKey, isNominee, getAllVotes, sendResultsToContract } from "../utils/near";
import { decrypt, verifySignature } from "../../cryptography";
import { VotingPackage } from "../../cryptography/types";

export const getPublicKey = async (_: Request, res: Response) => {
const secretKeys = await getSecretKeys();

if (secretKeys.error || !secretKeys.data) {
console.error("Error while getting secret keys", secretKeys.error);
return res.status(500).send({ message: "Error while getting secret keys" });
}

return res.status(200).send(base_encode(secretKeys.data.public));
}

export const postDecryption = async (req: Request, res: Response) => {
const secretKeys = await getSecretKeys();

if (secretKeys.error || !secretKeys.data) {
console.error("Error while getting secret keys", secretKeys.error);
return res.status(500).send({ message: "Error while getting secret keys" });
}

if (secretKeys.data.private === undefined) {
const endTime = new Date(secretKeys.data.end_time / 1_000_000).toUTCString()
return res.status(425).send({ message: `Secret key not available yet. Please come after ${endTime} UTC` });
}

const encryptedVotes = await getAllVotes();
if (encryptedVotes.length === 0) {
return res.status(400).send({ message: "No votes to decrypt" });
}

const decryptedVotes = new Map<string, VotingPackage>();
for (let i = 0; i < encryptedVotes.length; i++) {
const vote = encryptedVotes[i];
let result = await decrypt(vote, secretKeys.data.private);

if (result.error || !result.data) {
console.log(`Discard vote ${i}: ${result.error}`);
continue;
}

let isValid = await validateVote(result.data, i);
if (!isValid) {
continue;
}

if (decryptedVotes.has(result.data.accountId)) {
console.log(`Found new vote for ${result.data.accountId}. Replacing the old vote`);
}
decryptedVotes.set(result.data.accountId, result.data);
}

const results = new Map<string, number>();
decryptedVotes.forEach((vote) => {
vote.votes.forEach((v) => {
results.set(v.candidate, (results.get(v.candidate) ?? 0) + v.weight);
});
});

if (await sendResultsToContract(Array.from(results.entries()))) {
return res.status(200).send(decryptedVotes);
}
return res.status(500).send({ message: "Error while submitting results to the contract" });
}

const validateVote = async (vote: VotingPackage, voteNumber: number): Promise<boolean> => {
const { accountId, votes, signature } = vote;

const voterInfo = await getVoterPublicKey(accountId);
if (!voterInfo) {
console.log(`Discard vote ${voteNumber}: Voter is not registered`);
return false;
}

const data = base_encode(JSON.stringify({ accountId, votes }));
if (!verifySignature(data, voterInfo.public_key, signature)) {
console.log(`Discard vote ${voteNumber}: Invalid user signature`);
return false;
}


let totalWeightUsed = 0;
for (let i = 0; i < votes.length; i++) {
let vote = votes[i];
if (vote.weight < 0) {
console.log(`Discard vote ${voteNumber}: Invalid vote weight`);
return false;
}
totalWeightUsed += vote.weight;

let status = await isNominee(vote.candidate);
if (!status) {
console.log(`Discard vote ${voteNumber}: Invalid candidate`);
return false;
}
}

if (totalWeightUsed > voterInfo.vote_weight) {
console.log(`Discard vote ${voteNumber}: Vote weight exceeds the voter's total weight`);
return false;
}

return true;
}
1 change: 0 additions & 1 deletion relayer/src/api/controllers/submitVote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ export const postVote = async (req: Request, res: Response) => {
}
}


// Check if the user is a registered voter in the snapshot contract
const voterInfo = await getVoterPublicKey(data.accountId);
if (!voterInfo) {
Expand Down
4 changes: 4 additions & 0 deletions relayer/src/api/routes.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import express from "express";
import { postVote } from "./controllers/submitVote";
import { getPublicKey, postDecryption } from "./controllers/decryption";

const routes = express.Router();

routes.get("/encryption-public-key", getPublicKey);

routes.post("/vote", postVote)
routes.post("/decrypt", postDecryption);

export { routes };
87 changes: 80 additions & 7 deletions relayer/src/api/utils/near.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { connect, Contract, KeyPair, keyStores, ConnectConfig, Account, Near } from 'near-api-js';
import { connect, Contract, keyStores, ConnectConfig, Account, Near } from 'near-api-js';
import { AccountId, EncryptedVotingPackage } from '../../cryptography/types';
import { NETWORK_ID, RELAYER_ACCOUNT, SNAPSHOT_CONTRACT, VOTING_CONTRACT } from '../..';
import os from 'os';
import path from 'path';
import { PublicKey } from 'near-api-js/lib/utils';
import { parseNearAmount } from 'near-api-js/lib/utils/format';
import PromisePool from '@supercharge/promise-pool';
import pRetry, { FailedAttemptError } from "p-retry";


// Load credentials;
const homedir = os.homedir();
Expand All @@ -15,6 +17,7 @@ const keyStore = new keyStores.UnencryptedFileSystemKeyStore(credentialsPath);
// 300TGAS
const GAS = "300000000000000";
const DEPOSIT = parseNearAmount("0.5");
const RETRIES = 20;

let connectionConfig: ConnectConfig;
if (NETWORK_ID === "mainnet") {
Expand Down Expand Up @@ -42,10 +45,15 @@ let votingContract: VotingContract;

type SnapshotContract = Contract & {
get_voter_information: (args: { voter: AccountId }) => Promise<VoterInfo>;
is_nominee: (args: { nominee: AccountId }) => Promise<boolean>;
};

type VotingContract = Contract & {
send_encrypted_votes: (args: any) => Promise<void>;
sumbit_results: (args: any) => Promise<void>;

get_total_votes: () => Promise<number>;
get_votes: (args: { page: number, limit: number }) => Promise<any>;
};

export const initializeNear = async () => {
Expand All @@ -55,14 +63,14 @@ export const initializeNear = async () => {
relayer = await near.account(RELAYER_ACCOUNT!);

snapshotContract = new Contract(relayer, SNAPSHOT_CONTRACT!, {
viewMethods: ["get_voter_information"],
viewMethods: ["get_voter_information", "is_nominee"],
changeMethods: [],
useLocalViewExecution: false,
}) as SnapshotContract;

votingContract = new Contract(relayer, VOTING_CONTRACT!, {
viewMethods: [],
changeMethods: ['send_encrypted_votes'],
viewMethods: ['get_total_votes', 'get_votes'],
changeMethods: ['send_encrypted_votes', 'sumbit_results'],
useLocalViewExecution: false,
}) as VotingContract;
} catch (error) {
Expand All @@ -81,8 +89,7 @@ export const getVoterPublicKey = async (accountId: AccountId): Promise<VoterInfo
try {
const voterInfo: VoterInfo = await snapshotContract.get_voter_information({ voter: accountId });
return voterInfo || undefined;
} catch (error) {
console.error('Error fetching voter public key:', error);
} catch (_) {
return undefined;
}
};
Expand All @@ -104,3 +111,69 @@ export const sendVoteToContract = async (encryptedVotingPackage: EncryptedVoting
return false;
}
};

export const sendResultsToContract = async (results: [AccountId, number][]): Promise<boolean> => {
try {
await votingContract.sumbit_results({ args: { results }, gas: GAS, amount: DEPOSIT });
return true;
} catch (error) {
console.error('Error submitting results to contract:', error);
return false;
}
}

export const getAllVotes = async (): Promise<EncryptedVotingPackage[]> => {
try {
const totalVotes = await votingContract.get_total_votes();
const votes: EncryptedVotingPackage[] = [];

const PAGE_SIZE = 2;
const totalPages = Math.ceil(totalVotes / PAGE_SIZE);
const pageNumbers = Array.from({ length: totalPages }, (_, i) => i);

const onFailedAttempt = (error: FailedAttemptError) => {
console.warn(`Attempt ${error.attemptNumber} failed. There are ${error.retriesLeft} retries left.`);
};

const { results: voteResults, errors: voteErrors } = await PromisePool
.withConcurrency(10)
.for(pageNumbers)
.useCorrespondingResults()
.process(async (page) => {
return pRetry(async () => {
const pageVotes = await votingContract.get_votes({
page,
limit: PAGE_SIZE,
});

return pageVotes.map((vote: any) => ({
encryptedData: vote.vote,
publicKey: vote.pubkey,
}));
}, {
retries: RETRIES,
onFailedAttempt,
});
});

votes.push(...voteResults.flat());

if (voteErrors.length > 0) {
console.error('Errors occurred while loading votes:', voteErrors);
return [];
}

return votes;
} catch (error) {
console.error('Error loading votes from contract:', error);
return [];
}
};

export const isNominee = async (accountId: AccountId): Promise<boolean> => {
try {
return await snapshotContract.is_nominee({ nominee: accountId });
} catch (_) {
return false;
}
}
74 changes: 74 additions & 0 deletions relayer/src/api/utils/secret.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { SecretNetworkClient, Wallet } from "secretjs";
import ecdsa from "secp256k1";
import { NETWORK_ID, SECRET_CODE_HASH, SECRET_CONTRACT } from "../..";
import { Result } from "../../cryptography";


const wallet = new Wallet();

let secretjs: SecretNetworkClient;

if (NETWORK_ID === "mainnet") {
secretjs = new SecretNetworkClient({
chainId: "secret-4",
url: "https://rpc.ankr.com/http/scrt_cosmos",
wallet: wallet,
walletAddress: wallet.address,
});
}
else {
secretjs = new SecretNetworkClient({
chainId: "pulsar-3",
url: "https://lcd.pulsar-3.secretsaturn.net",
wallet: wallet,
walletAddress: wallet.address,
});
}

type SecretResponse = {
public: number[],
private: number[] | undefined,
end_time: number
}

type Response = {
public: Uint8Array,
private: Uint8Array | undefined,
end_time: number
}

type Request = {
get_keys: {}
}

export async function getSecretKeys(): Promise<Result<Response>> {
let query = await secretjs.query.compute.queryContract<Request, SecretResponse>({
contract_address: SECRET_CONTRACT!,
query: {
get_keys: {},
},
code_hash: SECRET_CODE_HASH,
});

if (query.private !== undefined && query.private.length > 0) {
const derivedPubKey = ecdsa.publicKeyCreate(Uint8Array.from(query.private), true);
const publicKey = query.public;

if (!derivedPubKey.every((value, index) => value === publicKey[index])) {
return {
error: 'Secret key is invalid',
data: undefined
}
}
}

return {
error: undefined,
data: {
public: Uint8Array.from(query.public),
private: query.private ? Uint8Array.from(query.private) : undefined,
end_time: query.end_time
}
}
};

Loading

0 comments on commit be9262a

Please sign in to comment.