o1js
Version:
TypeScript framework for zk-SNARKs and zkApps
345 lines (294 loc) • 9.56 kB
text/typescript
/*
* used to do a dry run, without tests
* ./run ./src/examples/zkapps/voting/demo.ts
*
* Warning: The reducer API in o1js is currently not safe to use in production applications. The `reduce()`
* method breaks if more than the hard-coded number (default: 32) of actions are pending. Work is actively
* in progress to mitigate this limitation.
*/
import { Mina, AccountUpdate, PrivateKey, UInt64, Reducer, Bool } from 'o1js';
import { VotingApp, VotingAppParams } from './factory.js';
import { Member, MyMerkleWitness } from './member.js';
import { OffchainStorage } from './off-chain-storage.js';
import {
ParticipantPreconditions,
ElectionPreconditions,
} from './preconditions.js';
let Local = await Mina.LocalBlockchain({
proofsEnabled: false,
enforceTransactionLimits: false,
});
Mina.setActiveInstance(Local);
let [feePayer] = Local.testAccounts;
let tx;
// B62qra25W4URGXxZYqYjfkXBa6SfwwrSjX2ZFJ24x12sSy8khGRcRH1
let voterKey = PrivateKey.fromBase58(
'EKEgiGWBmGG77ERKU7ihArYbUTfroEr466Gs1RKUph8bgpvF5BSD'
);
// B62qohqUFi8iy5mA4roZDNEuHdj1bWtyriYZouybC33wb8Q6AiUc7D7
let candidateKey = PrivateKey.fromBase58(
'EKELdqBuWoNa4KFibyumCJNCr1SzMFJi5mV3pCASXfNH3geh6ezG'
);
// B62qq2s61y9gzALPWSAFitucxq1PhLEjQLGwb65gQ7UgsVFNTtjrzRj
let votingKey = PrivateKey.fromBase58(
'EKFHGpCJTuQk1xHTkQH3q3xXJCHMQLPwhy5iTJk3L2bK4FG9iVnv'
);
let params: VotingAppParams = {
candidatePreconditions: new ParticipantPreconditions(
UInt64.from(100),
UInt64.from(1000)
),
voterPreconditions: new ParticipantPreconditions(
UInt64.from(10),
UInt64.from(200)
),
electionPreconditions: ElectionPreconditions.default,
voterKey,
candidateKey,
votingKey,
doProofs: true,
};
params.electionPreconditions.enforce = Bool(true);
let contracts = await VotingApp(params);
let voterStore = new OffchainStorage<Member>(3);
let candidateStore = new OffchainStorage<Member>(3);
let votesStore = new OffchainStorage<Member>(3);
let initialRoot = voterStore.getRoot();
tx = await Mina.transaction(feePayer, async () => {
AccountUpdate.fundNewAccount(feePayer, 3);
await contracts.voting.deploy();
contracts.voting.committedVotes.set(votesStore.getRoot());
contracts.voting.accumulatedVotes.set(Reducer.initialActionState);
await contracts.candidateContract.deploy();
contracts.candidateContract.committedMembers.set(candidateStore.getRoot());
contracts.candidateContract.accumulatedMembers.set(
Reducer.initialActionState
);
await contracts.voterContract.deploy();
contracts.voterContract.committedMembers.set(voterStore.getRoot());
contracts.voterContract.accumulatedMembers.set(Reducer.initialActionState);
});
await tx.sign([feePayer.key, votingKey, candidateKey, voterKey]).send();
let m: Member = Member.empty();
// lets register three voters
tx = await Mina.transaction(feePayer, async () => {
// creating and registering a new voter
m = registerMember(
/*
NOTE: it isn't wise to use an incremented integer as an
identifier for real world applications for your entries,
but instead a public key
*/
0n,
Member.from(PrivateKey.random().toPublicKey(), UInt64.from(150)),
voterStore
);
contracts.voting.voterRegistration(m);
});
await tx.prove();
await tx.sign([feePayer.key]).send();
// lets register three voters
tx = await Mina.transaction(feePayer, async () => {
// creating and registering a new voter
m = registerMember(
/*
NOTE: it isn't wise to use an incremented integer as an
identifier for real world applications for your entries,
but instead a public key
*/
1n,
Member.from(PrivateKey.random().toPublicKey(), UInt64.from(160)),
voterStore
);
contracts.voting.voterRegistration(m);
});
await tx.prove();
await tx.sign([feePayer.key]).send();
// lets register three voters
tx = await Mina.transaction(feePayer, async () => {
// creating and registering a new voter
m = registerMember(
/*
NOTE: it isn't wise to use an incremented integer as an
identifier for real world applications for your entries,
but instead a public key
*/
2n,
Member.from(PrivateKey.random().toPublicKey(), UInt64.from(170)),
voterStore
);
contracts.voting.voterRegistration(m);
});
await tx.prove();
await tx.sign([feePayer.key]).send();
/*
since the voting contract calls the voter membership contract via invoking voterRegister,
the membership contract will then emit one event per new member
we should have emitted three new members
*/
console.log(
'3 events?? ',
(await contracts.voterContract.reducer.fetchActions()).length === 3
);
/*
Lets register two candidates
*/
tx = await Mina.transaction(feePayer, async () => {
// creating and registering 1 new candidate
let m = registerMember(
/*
NOTE: it isn't wise to use an incremented integer as an
identifier for real world applications for your entries,
but instead a public key
*/
0n,
Member.from(PrivateKey.random().toPublicKey(), UInt64.from(250)),
candidateStore
);
contracts.voting.candidateRegistration(m);
});
await tx.prove();
await tx.sign([feePayer.key]).send();
tx = await Mina.transaction(feePayer, async () => {
// creating and registering 1 new candidate
let m = registerMember(
/*
NOTE: it isn't wise to use an incremented integer as an
identifier for real world applications for your entries,
but instead a public key
*/
1n,
Member.from(PrivateKey.random().toPublicKey(), UInt64.from(400)),
candidateStore
);
contracts.voting.candidateRegistration(m);
});
await tx.prove();
await tx.sign([feePayer.key]).send();
/*
since the voting contact calls the candidate membership contract via invoking candidateRegister,
the membership contract will then emit one event per new member
we should have emitted 2 new members, because we registered 2 new candidates
*/
console.log(
'2 events?? ',
(await contracts.candidateContract.reducer.fetchActions()).length === 2
);
/*
we only emitted sequence events,
so the merkel roots of both membership contract should still be the initial ones
because the committed state should only change after publish has been invoked
*/
console.log(
'still initial root? ',
contracts.candidateContract.committedMembers
.get()
.equals(initialRoot)
.toBoolean()
);
console.log(
'still initial root? ',
contracts.voterContract.committedMembers.get().equals(initialRoot).toBoolean()
);
/*
if we now call approveVoters, which invokes publish on both membership contracts,
we will also update the committed members!
and since we keep track of voters and candidates in our off-chain storage,
both the on-chain committedMembers variable and the off-chain merkle tree root need to be equal
*/
tx = await Mina.transaction(feePayer, async () => {
contracts.voting.approveRegistrations();
});
await tx.prove();
await tx.sign([feePayer.key]).send();
for (let a of candidateStore.values()) {
console.log(a.publicKey.toBase58());
}
console.log(
'candidate root? ',
contracts.candidateContract.committedMembers
.get()
.equals(candidateStore.getRoot())
.toBoolean()
);
console.log(
'voter root? ',
contracts.voterContract.committedMembers
.get()
.equals(voterStore.getRoot())
.toBoolean()
);
/*
lets vote for the one candidate we have
*/
// we have to up the slot so we are within our election period
Local.incrementGlobalSlot(5);
tx = await Mina.transaction(feePayer, async () => {
let c = candidateStore.get(0n)!;
c.witness = new MyMerkleWitness(candidateStore.getWitness(0n));
c.votesWitness = new MyMerkleWitness(votesStore.getWitness(0n));
// we are voting for candidate c, 0n, with voter 2n
contracts.voting.vote(c, voterStore.get(2n)!);
});
await tx.prove();
await tx.sign([feePayer.key]).send();
// after the transaction went through, we have to update our off chain store as well
vote(0n);
// vote dispatches a new sequence events, so we should have one
console.log(
'1 vote sequence event? ',
(await contracts.voting.reducer.fetchActions()).length === 1
);
/*
counting the votes
*/
tx = await Mina.transaction(feePayer, async () => {
contracts.voting.countVotes();
});
await tx.prove();
await tx.sign([feePayer.key]).send();
// vote dispatches a new sequence events, so we should have one
console.log(
'votes roots equal? ',
votesStore.getRoot().equals(contracts.voting.committedVotes.get()).toBoolean()
);
printResult();
function registerMember(
i: bigint,
m: Member,
store: OffchainStorage<Member>
): Member {
Local.addAccount(m.publicKey, m.balance.toString());
// we will also have to keep track of new voters and candidates within our off-chain merkle tree
store.set(i, m); // setting voter 0n
// setting the merkle witness
m.witness = new MyMerkleWitness(store.getWitness(i));
return m;
}
function vote(i: bigint) {
let c_ = votesStore.get(i)!;
if (!c_) {
votesStore.set(i, candidateStore.get(i)!);
c_ = votesStore.get(i)!;
}
c_ = c_.addVote();
votesStore.set(i, c_);
return c_;
}
function printResult() {
if (
!contracts.voting.committedVotes
.get()
.equals(votesStore.getRoot())
.toBoolean()
) {
throw new Error('On-chain root is not up to date with the off-chain tree');
}
let result: any = [];
votesStore.forEach((m, i) => {
result.push({
[ ]: m.votes.toString(),
});
});
console.log(result);
}