-
Notifications
You must be signed in to change notification settings - Fork 107
/
voting.ts
303 lines (259 loc) · 9.22 KB
/
voting.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
/*
* 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,
});
}
}