o1js
Version:
TypeScript framework for zk-SNARKs and zkApps
304 lines (259 loc) • 9.44 kB
text/typescript
/*
* 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 {
Field,
SmartContract,
state,
State,
method,
Permissions,
PublicKey,
Bool,
Reducer,
provablePure,
AccountUpdate,
Provable,
} from 'o1js';
import { Member } from './member.js';
import {
ElectionPreconditions,
ParticipantPreconditions,
} from './preconditions.js';
import { Membership_ } from './membership.js';
/**
* Address to the Membership instance that keeps track of Candidates.
*/
let candidateAddress = PublicKey.empty();
/**
* Address to the Membership instance that keeps track of Voters.
*/
let voterAddress = PublicKey.empty();
/**
* Requirements in order for a Member to participate in the election as a Candidate.
*/
let candidatePreconditions = ParticipantPreconditions.default;
/**
* Requirements in order for a Member to participate in the election as a Voter.
*/
let voterPreconditions = ParticipantPreconditions.default;
/**
* Defines the preconditions of an election.
*/
let electionPreconditions = ElectionPreconditions.default;
type VotingParams = {
electionPreconditions: ElectionPreconditions;
voterPreconditions: ParticipantPreconditions;
candidatePreconditions: ParticipantPreconditions;
candidateAddress: PublicKey;
voterAddress: PublicKey;
contractAddress: PublicKey;
doProofs: boolean;
};
/**
* Returns a new contract instance that based on a set of preconditions.
* @param params {@link Voting_}
*/
export async function Voting(params: VotingParams): Promise<Voting_> {
electionPreconditions = params.electionPreconditions;
voterPreconditions = params.voterPreconditions;
candidatePreconditions = params.candidatePreconditions;
candidateAddress = params.candidateAddress;
voterAddress = params.voterAddress;
let contract = new Voting_(params.contractAddress);
params.doProofs = true;
if (params.doProofs) {
await Voting_.compile();
}
return contract;
}
export class Voting_ extends SmartContract {
/**
* Root of the merkle tree that stores all committed votes.
*/
@state(Field) committedVotes = State<Field>();
/**
* Accumulator of all emitted votes.
*/
@state(Field) accumulatedVotes = State<Field>();
reducer = Reducer({ actionType: Member });
events = {
newVoteFor: PublicKey,
newVoteState: provablePure({
accumulatedVotesRoot: Field,
committedVotesRoot: Field,
}),
};
async deploy() {
await super.deploy();
this.account.permissions.set({
...Permissions.default(),
editState: Permissions.proofOrSignature(),
editActionState: Permissions.proofOrSignature(),
incrementNonce: Permissions.proofOrSignature(),
setVerificationKey: Permissions.VerificationKey.none(),
setPermissions: Permissions.proofOrSignature(),
});
this.accumulatedVotes.set(Reducer.initialActionState);
}
/**
* Method used to register a new voter. Calls the `addEntry(member)` method of the Voter-Membership contract.
* @param member
*/
@method
async voterRegistration(member: Member) {
let currentSlot = this.network.globalSlotSinceGenesis.get();
this.network.globalSlotSinceGenesis.requireBetween(
currentSlot,
currentSlot.add(10)
);
// can only register voters before the election has started
Provable.if(
electionPreconditions.enforce,
currentSlot.lessThanOrEqual(electionPreconditions.startElection),
Bool(true)
).assertTrue('Outside of election period!');
// can only register voters if their balance is gte the minimum amount required
// this snippet pulls the account data of an address from the network
let accountUpdate = AccountUpdate.create(member.publicKey);
accountUpdate.account.balance.requireEquals(
accountUpdate.account.balance.get()
);
let balance = accountUpdate.account.balance.get();
balance.assertGreaterThanOrEqual(
voterPreconditions.minMina,
'Balance not high enough!'
);
balance.assertLessThanOrEqual(
voterPreconditions.maxMina,
'Balance too high!'
);
let VoterContract: Membership_ = new Membership_(voterAddress);
let exists = await VoterContract.addEntry(member);
// the check happens here because we want to see if the other contract returns a value
// if exists is true, that means the member already exists within the accumulated state
// if its false, its a new entry
exists.assertFalse('Member already exists!');
}
/**
* Method used to register a new candidate.
* Calls the `addEntry(member)` method of the Candidate-Membership contract.
* @param member
*/
@method
async candidateRegistration(member: Member) {
let currentSlot = this.network.globalSlotSinceGenesis.get();
this.network.globalSlotSinceGenesis.requireBetween(
currentSlot,
currentSlot.add(10)
);
// can only register candidates before the election has started
Provable.if(
electionPreconditions.enforce,
currentSlot.lessThanOrEqual(electionPreconditions.startElection),
Bool(true)
).assertTrue('Outside of election period!');
// can only register candidates if their balance is gte the minimum amount required
// and lte the maximum amount
// this snippet pulls the account data of an address from the network
let accountUpdate = AccountUpdate.create(member.publicKey);
accountUpdate.account.balance.requireEquals(
accountUpdate.account.balance.get()
);
let balance = accountUpdate.account.balance.get();
balance.assertGreaterThanOrEqual(
candidatePreconditions.minMina,
'Balance not high enough!'
);
balance.assertLessThanOrEqual(
candidatePreconditions.maxMina,
'Balance too high!'
);
let CandidateContract: Membership_ = new Membership_(candidateAddress);
let exists = await CandidateContract.addEntry(member);
// the check happens here because we want to see if the other contract returns a value
// if exists is true, that means the member already exists within the accumulated state
// if its false, its a new entry
exists.assertEquals(false);
}
/**
* Method used to register update all pending member registrations.
* Calls the `publish()` method of the Candidate-Membership and Voter-Membership contract.
*/
@method
async approveRegistrations() {
// Invokes the publish method of both Voter and Candidate Membership contracts.
let VoterContract: Membership_ = new Membership_(voterAddress);
await VoterContract.publish();
let CandidateContract: Membership_ = new Membership_(candidateAddress);
await CandidateContract.publish();
}
/**
* Method used to cast a vote to a specific candidate.
* Dispatches a new vote sequence event.
* @param candidate
* @param voter
*/
@method
async vote(candidate: Member, voter: Member) {
let currentSlot = this.network.globalSlotSinceGenesis.get();
this.network.globalSlotSinceGenesis.requireBetween(
currentSlot,
currentSlot.add(10)
);
// we can only vote in the election period time frame
Provable.if(
electionPreconditions.enforce,
currentSlot
.greaterThanOrEqual(electionPreconditions.startElection)
.and(currentSlot.lessThanOrEqual(electionPreconditions.endElection)),
Bool(true)
).assertTrue('Not in voting period!');
// verifying that both the voter and the candidate are actually part of our member set
// ideally we would also verify a signature here, but ignoring that for now
let VoterContract: Membership_ = new Membership_(voterAddress);
(await VoterContract.isMember(voter)).assertTrue('Member is not a voter!');
let CandidateContract: Membership_ = new Membership_(candidateAddress);
(await CandidateContract.isMember(candidate)).assertTrue(
'Member is not a candidate!'
);
// emits a sequence event with the information about the candidate
this.reducer.dispatch(candidate);
this.emitEvent('newVoteFor', candidate.publicKey);
}
/**
* Method used to accumulate all pending votes from sequence events
* and applies state changes to the votes merkle tree.
*/
@method
async countVotes() {
let accumulatedVotes = this.accumulatedVotes.get();
this.accumulatedVotes.requireEquals(accumulatedVotes);
let committedVotes = this.committedVotes.get();
this.committedVotes.requireEquals(committedVotes);
let actions = this.reducer.getActions({
fromActionState: accumulatedVotes,
});
let newCommittedVotes = this.reducer.reduce(
actions,
Field,
(state: Field, action: Member) => {
// apply one vote
action = action.addVote();
// this is the new root after we added one vote
return action.votesWitness.calculateRoot(action.getHash());
},
// initial state
committedVotes
);
let newAccumulatedVotes = actions.hash;
this.committedVotes.set(newCommittedVotes);
this.accumulatedVotes.set(newAccumulatedVotes);
this.emitEvent('newVoteState', {
committedVotesRoot: newCommittedVotes,
accumulatedVotesRoot: newAccumulatedVotes,
});
}
}