🎉 Up to counting elections
This commit is contained in:
parent
81807ca7f8
commit
6af132bb5f
@ -89,10 +89,10 @@ class ListField(Field):
|
||||
self.element_field = element_field
|
||||
|
||||
def serialise(self, value, for_hash=False, should_protect=False):
|
||||
return [self.element_field.serialise(x, for_hash, should_protect) for x in value]
|
||||
return [self.element_field.serialise(x, for_hash, should_protect) for x in (value.impl if isinstance(value, EosList) else value)]
|
||||
|
||||
def deserialise(self, value):
|
||||
return [self.element_field.deserialise(x) for x in value]
|
||||
return EosList([self.element_field.deserialise(x) for x in value])
|
||||
|
||||
class EmbeddedObjectListField(Field):
|
||||
def __init__(self, object_type=None, *args, **kwargs):
|
||||
@ -100,9 +100,8 @@ class EmbeddedObjectListField(Field):
|
||||
self.object_type = object_type
|
||||
|
||||
def serialise(self, value, for_hash=False, should_protect=False):
|
||||
#return [EosObject.serialise_and_wrap(x, self.object_type, for_hash, should_protect) for x in value]
|
||||
# TNYI: Doesn't know how to deal with iterators like this
|
||||
return [EosObject.serialise_and_wrap(x, self.object_type, for_hash, should_protect) for x in value.impl]
|
||||
# TNYI: Doesn't know how to deal with iterators like EosList
|
||||
return [EosObject.serialise_and_wrap(x, self.object_type, for_hash, should_protect) for x in (value.impl if isinstance(value, EosList) else value)]
|
||||
|
||||
def deserialise(self, value):
|
||||
return EosList([EosObject.deserialise_and_unwrap(x, self.object_type) for x in value])
|
||||
@ -192,9 +191,14 @@ class EosList(EosObject):
|
||||
def post_init(self):
|
||||
for i in range(len(self.impl)):
|
||||
val = self.impl[i]
|
||||
val._instance = (self, i)
|
||||
if not val._inited:
|
||||
val.post_init()
|
||||
# Check if object has writeable attributes
|
||||
if is_python and hasattr(val, '__dict__'):
|
||||
val._instance = (self, i)
|
||||
if not val._inited:
|
||||
val.post_init()
|
||||
|
||||
def __repr__(self):
|
||||
return '<EosList {}>'.format(repr(self.impl))
|
||||
|
||||
# Lists in JS are implemented as native Arrays, so no cheating here :(
|
||||
def __len__(self):
|
||||
@ -209,6 +213,16 @@ class EosList(EosObject):
|
||||
def __contains__(self, val):
|
||||
return val in self.impl
|
||||
|
||||
# For sorting, etc.
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, EosList):
|
||||
other = other.impl
|
||||
return self.impl == other
|
||||
def __lt__(self, other):
|
||||
if isinstance(other, EosList):
|
||||
other = other.impl
|
||||
return self.impl < other
|
||||
|
||||
def append(self, value):
|
||||
if isinstance(value, EosObject):
|
||||
value._instance = (self, len(self))
|
||||
|
@ -21,6 +21,8 @@ from eos.base.election import *
|
||||
from eos.psr.bitstream import *
|
||||
from eos.psr.crypto import *
|
||||
|
||||
from eos.core.objects import __pragma__
|
||||
|
||||
class BlockEncryptedAnswer(EncryptedAnswer):
|
||||
blocks = EmbeddedObjectListField()
|
||||
|
||||
@ -55,14 +57,10 @@ class MixChallengeResponse(EmbeddedObject):
|
||||
rand = EmbeddedObjectField(BigInt)
|
||||
|
||||
class MixingTrustee(Trustee):
|
||||
mixed_questions = ListField(EmbeddedObjectListField(BlockEncryptedAnswer))
|
||||
commitments = ListField(EmbeddedObjectListField(BigInt))
|
||||
challenge = EmbeddedObjectListField(BigInt)
|
||||
response = ListField(EmbeddedObjectListField(MixChallengeResponse))
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.mixnets = [] # TODO: Remove this stuff
|
||||
mixed_questions = ListField(EmbeddedObjectListField(BlockEncryptedAnswer), is_hashed=False)
|
||||
commitments = ListField(EmbeddedObjectListField(BigInt), is_hashed=False)
|
||||
challenge = EmbeddedObjectListField(BigInt, is_hashed=False)
|
||||
response = ListField(EmbeddedObjectListField(MixChallengeResponse), is_hashed=False)
|
||||
|
||||
def compute_challenge(self, question_num):
|
||||
if self._instance[1] % 2 == 1:
|
||||
@ -102,12 +100,14 @@ class MixingTrustee(Trustee):
|
||||
challenge_bs = InfiniteHashBitStream(challenge)
|
||||
|
||||
# Check each challenge response
|
||||
responses_iter = iter(self.response[question_num])
|
||||
for k in range(len(self.mixed_questions[question_num])):
|
||||
response = self.response[question_num][k]
|
||||
|
||||
challenge_bit = challenge_bs.read(1)
|
||||
should_reveal = ((self._instance[1] % 2) == (challenge_bit % 2))
|
||||
if should_reveal:
|
||||
response = next(responses_iter)
|
||||
if response is None:
|
||||
raise Exception('Response inconsistent with challenge')
|
||||
|
||||
# Check the commitment matches
|
||||
if self.commitments[question_num][k] != SHA256().update_obj(response).hash_as_bigint():
|
||||
@ -132,11 +132,17 @@ class MixingTrustee(Trustee):
|
||||
raise Exception('Reencryption not consistent with challenge response')
|
||||
if claimed_blocks[k].delta != reencrypted_block.delta:
|
||||
raise Exception('Reencryption not consistent with challenge response')
|
||||
else:
|
||||
if response is not None:
|
||||
raise Exception('Response inconsistent with challenge')
|
||||
|
||||
# Check the responses are consistent with a permutation
|
||||
challenge_indexes = []
|
||||
response_indexes = []
|
||||
for response in self.response[question_num]:
|
||||
if response is None:
|
||||
continue
|
||||
|
||||
if response.challenge_index in challenge_indexes:
|
||||
raise Exception('Response not consistent with a permutation')
|
||||
if response.response_index in response_indexes:
|
||||
@ -152,6 +158,59 @@ class MixingTrustee(Trustee):
|
||||
if block in blocks:
|
||||
raise Exception('Duplicate ciphertexts in output')
|
||||
blocks.append(block)
|
||||
|
||||
def mix_votes(self, question=0):
|
||||
return False
|
||||
def prove_mixes(self, question=0):
|
||||
return False
|
||||
|
||||
class InternalMixingTrustee(MixingTrustee):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.mixnets = []
|
||||
|
||||
def mix_votes(self, question=0):
|
||||
__pragma__('skip')
|
||||
from eos.psr.mixnet import RPCMixnet
|
||||
__pragma__('noskip')
|
||||
|
||||
election = self.recurse_parents('eos.base.election.Election')
|
||||
index = self._instance[1]
|
||||
|
||||
self.mixnets.append(RPCMixnet(index))
|
||||
if index > 0:
|
||||
orig_answers = election.mixing_trustees[index - 1].mixed_questions[question]
|
||||
else:
|
||||
orig_answers = []
|
||||
for voter in election.voters:
|
||||
if len(voter.votes) > 0:
|
||||
ballot = voter.votes[-1].ballot
|
||||
orig_answers.append(ballot.encrypted_answers[question])
|
||||
shuffled_answers, commitments = self.mixnets[question].shuffle(orig_answers)
|
||||
self.mixed_questions.append(EosList(shuffled_answers))
|
||||
self.commitments.append(EosList(commitments))
|
||||
|
||||
return True
|
||||
|
||||
def prove_mixes(self, question=0):
|
||||
election = self.recurse_parents('eos.base.election.Election')
|
||||
index = self._instance[1]
|
||||
|
||||
self.challenge.append(self.compute_challenge(question))
|
||||
challenge_bs = InfiniteHashBitStream(self.challenge[question])
|
||||
|
||||
self.response.append(EosList())
|
||||
|
||||
for k in range(len(self.mixed_questions[question])):
|
||||
challenge_bit = challenge_bs.read(1)
|
||||
should_reveal = ((index % 2) == (challenge_bit % 2))
|
||||
if should_reveal:
|
||||
response = self.mixnets[question].challenge(k)
|
||||
self.response[question].append(response)
|
||||
else:
|
||||
self.response[question].append(None)
|
||||
|
||||
return True
|
||||
|
||||
class PSRElection(Election):
|
||||
_db_name = Election._name
|
||||
@ -159,4 +218,4 @@ class PSRElection(Election):
|
||||
sk = EmbeddedObjectField(SEGPrivateKey, is_protected=True) # TODO: Threshold
|
||||
|
||||
public_key = EmbeddedObjectField(SEGPublicKey)
|
||||
mixing_trustees = EmbeddedObjectListField(MixingTrustee)
|
||||
mixing_trustees = EmbeddedObjectListField()
|
||||
|
@ -235,7 +235,7 @@ class ElectionTestCase(EosTestCase):
|
||||
election.voters.append(voter)
|
||||
|
||||
for i in range(3):
|
||||
mixing_trustee = MixingTrustee()
|
||||
mixing_trustee = InternalMixingTrustee()
|
||||
election.mixing_trustees.append(mixing_trustee)
|
||||
|
||||
election.sk = EGPrivateKey.generate()
|
||||
@ -275,60 +275,24 @@ class ElectionTestCase(EosTestCase):
|
||||
election.save()
|
||||
|
||||
# Mix votes
|
||||
election.workflow.get_task('eos.psr.workflow.TaskMixVotes').enter()
|
||||
election.save()
|
||||
|
||||
# Do the mix
|
||||
for i in range(len(election.questions)):
|
||||
for j in range(len(election.mixing_trustees)):
|
||||
# Wouldn't happen server-side IRL
|
||||
election.mixing_trustees[j].mixnets.append(RPCMixnet(j))
|
||||
if j > 0:
|
||||
orig_answers = election.mixing_trustees[j - 1].mixed_questions[i]
|
||||
else:
|
||||
orig_answers = []
|
||||
for voter in election.voters:
|
||||
ballot = voter.votes[-1].ballot
|
||||
orig_answers.append(ballot.encrypted_answers[i])
|
||||
shuffled_answers, commitments = election.mixing_trustees[j].mixnets[i].shuffle(orig_answers)
|
||||
election.mixing_trustees[j].mixed_questions.append(EosList(shuffled_answers))
|
||||
election.mixing_trustees[j].commitments.append(EosList(commitments))
|
||||
|
||||
election.workflow.get_task('eos.psr.workflow.TaskMixVotes').exit()
|
||||
self.do_task_assert(election, 'eos.psr.workflow.TaskMixVotes', 'eos.psr.workflow.TaskProveMixes')
|
||||
election.save()
|
||||
|
||||
# Prove mixes
|
||||
election.workflow.get_task('eos.psr.workflow.TaskProveMixes').enter()
|
||||
self.do_task_assert(election, 'eos.psr.workflow.TaskProveMixes', 'eos.base.workflow.TaskDecryptVotes')
|
||||
election.save()
|
||||
|
||||
# Record challenge responses
|
||||
# Verify mixes
|
||||
for i in range(len(election.questions)):
|
||||
for j in range(len(election.mixing_trustees)):
|
||||
trustee = election.mixing_trustees[j]
|
||||
trustee.challenge.append(trustee.compute_challenge(i))
|
||||
challenge_bs = InfiniteHashBitStream(trustee.challenge[i])
|
||||
|
||||
trustee.response.append(EosList())
|
||||
|
||||
for k in range(len(trustee.mixed_questions[i])):
|
||||
challenge_bit = challenge_bs.read(1)
|
||||
should_reveal = ((j % 2) == (challenge_bit % 2))
|
||||
if should_reveal:
|
||||
response = trustee.mixnets[i].challenge(k)
|
||||
trustee.response[i].append(response)
|
||||
|
||||
# Verify challenge response
|
||||
trustee.verify(i)
|
||||
|
||||
election.workflow.get_task('eos.psr.workflow.TaskProveMixes').exit()
|
||||
election.save()
|
||||
election.mixing_trustees[j].verify(i)
|
||||
|
||||
# Decrypt votes, for realsies
|
||||
self.do_task_assert(election, 'eos.base.workflow.TaskDecryptVotes', 'eos.base.workflow.TaskReleaseResults')
|
||||
election.save()
|
||||
|
||||
# Check result
|
||||
RESULTS = [[voter[i] for voter in VOTES] for i in range(len(election.questions))]
|
||||
RESULTS = [[EosList(voter[i]) for voter in VOTES] for i in range(len(election.questions))]
|
||||
for i in range(len(RESULTS)):
|
||||
votes1 = RESULTS[i]
|
||||
votes2 = [x.choices for x in election.results[i].answers]
|
||||
|
@ -25,15 +25,37 @@ class TaskMixVotes(WorkflowTask):
|
||||
depends_on = ['eos.base.workflow.TaskCloseVoting']
|
||||
|
||||
def on_enter(self):
|
||||
# Do not automatically exit this task
|
||||
pass
|
||||
election = self.recurse_parents('eos.base.election.Election')
|
||||
|
||||
should_exit = True
|
||||
|
||||
for i in range(len(election.questions)):
|
||||
for j in range(len(election.mixing_trustees)):
|
||||
success = election.mixing_trustees[j].mix_votes(i)
|
||||
if not success:
|
||||
should_exit = False
|
||||
break # out of inner loop - further mixing required by hand for this question
|
||||
|
||||
if should_exit:
|
||||
self.exit()
|
||||
|
||||
class TaskProveMixes(WorkflowTask):
|
||||
depends_on = ['eos.psr.workflow.TaskMixVotes']
|
||||
|
||||
def on_enter(self):
|
||||
# Do not automatically exit this task
|
||||
pass
|
||||
election = self.recurse_parents('eos.base.election.Election')
|
||||
|
||||
should_exit = True
|
||||
|
||||
for i in range(len(election.questions)):
|
||||
for j in range(len(election.mixing_trustees)):
|
||||
success = election.mixing_trustees[j].prove_mixes(i)
|
||||
if not success:
|
||||
should_exit = False
|
||||
break # out of inner loop - further mixing required by hand for this question
|
||||
|
||||
if should_exit:
|
||||
self.exit()
|
||||
|
||||
class TaskDecryptVotes(eos.base.workflow.TaskDecryptVotes):
|
||||
depends_on = ['eos.psr.workflow.TaskProveMixes']
|
||||
|
@ -58,7 +58,8 @@ def setup_test_election():
|
||||
election.voters.append(Voter(name='Bob'))
|
||||
election.voters.append(Voter(name='Charlie'))
|
||||
|
||||
election.mixing_trustees.append(MixingTrustee(name='Eos Voting'))
|
||||
election.mixing_trustees.append(InternalMixingTrustee(name='Eos Voting'))
|
||||
election.mixing_trustees.append(InternalMixingTrustee(name='Eos Voting'))
|
||||
|
||||
election.sk = EGPrivateKey.generate()
|
||||
election.public_key = election.sk.public_key
|
||||
@ -79,6 +80,28 @@ def setup_test_election():
|
||||
|
||||
election.save()
|
||||
|
||||
@app.cli.command('close_test_election')
|
||||
def close_test_election():
|
||||
election = Election.get_all()[0]
|
||||
election.workflow.get_task('eos.base.workflow.TaskCloseVoting').enter()
|
||||
|
||||
election.save()
|
||||
|
||||
@app.cli.command('count_test_election')
|
||||
def count_test_election():
|
||||
election = Election.get_all()[0]
|
||||
|
||||
# Mix votes
|
||||
election.workflow.get_task('eos.psr.workflow.TaskMixVotes').enter()
|
||||
# Prove mixes
|
||||
election.workflow.get_task('eos.psr.workflow.TaskProveMixes').enter()
|
||||
# Decrypt votes, for realsies
|
||||
election.workflow.get_task('eos.psr.workflow.TaskDecryptVotes').enter()
|
||||
# Release result
|
||||
election.workflow.get_task('eos.base.workflow.TaskReleaseResults').enter()
|
||||
|
||||
election.save()
|
||||
|
||||
@app.context_processor
|
||||
def inject_globals():
|
||||
return {'eos': eos, 'eosweb': eosweb, 'SHA256': eos.core.hashing.SHA256}
|
||||
@ -129,6 +152,10 @@ def election_view_trustees(election):
|
||||
@app.route('/election/<election_id>/cast_ballot', methods=['POST'])
|
||||
@using_election
|
||||
def election_api_cast_vote(election):
|
||||
if election.workflow.get_task('eos.base.workflow.TaskOpenVoting').status >= WorkflowTask.Status.EXITED or election.workflow.get_task('eos.base.workflow.TaskCloseVoting').status <= WorkflowTask.Status.READY:
|
||||
# Voting is not yet open or has closed
|
||||
return flask.Response('Voting is not yet open or has closed', 405)
|
||||
|
||||
data = json.loads(flask.request.data)
|
||||
|
||||
voter = None
|
||||
|
@ -19,16 +19,15 @@ from eos.psr.election import *
|
||||
|
||||
model_view_map = {
|
||||
ApprovalQuestion: {
|
||||
'view': 'question/approval/view.html'
|
||||
'view': 'question/approval/view.html',
|
||||
'result_raw': 'question/approval/result_raw.html',
|
||||
'selections_make': 'question/approval/selections_make.html',
|
||||
'selections_review': 'question/approval/selections_review.html'
|
||||
},
|
||||
Election: {
|
||||
'tabs': 'election/core/tabs.html'
|
||||
},
|
||||
PSRElection: {
|
||||
'tabs': 'election/psr/tabs.html'
|
||||
},
|
||||
ApprovalQuestion: {
|
||||
'selections_make': 'question/approval/selections_make.html',
|
||||
'selections_review': 'question/approval/selections_review.html'
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,9 @@
|
||||
{% if election.workflow.get_task('eos.base.workflow.TaskConfigureElection').status == Status.EXITED %}
|
||||
{% if election.workflow.get_task('eos.base.workflow.TaskOpenVoting').status == Status.EXITED %}
|
||||
{% if election.workflow.get_task('eos.base.workflow.TaskCloseVoting').status == Status.EXITED %}
|
||||
<p><button class="ui huge button">Voting in this election has closed</button></p>
|
||||
{% if election.workflow.get_task('eos.base.workflow.TaskReleaseResults').status < Status.EXITED %}
|
||||
<p><button class="ui huge button">Voting in this election has closed</button></p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p><a href="{{ url_for('election_booth', election_id=election._id) }}" class="ui huge primary button">Click here to vote in this election</a></p>
|
||||
{% endif %}
|
||||
@ -52,4 +54,13 @@
|
||||
|
||||
<p>The administrator of this election has not yet finished setting the election parameters. The details of the election may change at any time.</p>
|
||||
{% endif %}
|
||||
|
||||
{% if election.workflow.get_task('eos.base.workflow.TaskReleaseResults').status == Status.EXITED %}
|
||||
<h2>Results</h2>
|
||||
|
||||
{% for question in election.questions %}
|
||||
<h3>{{ loop.index }}. {{ question.prompt }}</h2>
|
||||
{% include eosweb.core.main.model_view_map[question.__class__]['result_raw'] %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
27
eosweb/core/templates/question/approval/result_raw.html
Normal file
27
eosweb/core/templates/question/approval/result_raw.html
Normal file
@ -0,0 +1,27 @@
|
||||
{#
|
||||
Eos - Verifiable elections
|
||||
Copyright © 2017 RunasSudo (Yingtong Li)
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#}
|
||||
|
||||
<ul class="ui list">
|
||||
{% for answer in election.results[loop.index0].answers %}
|
||||
<li>
|
||||
{% for choice in answer.choices %}
|
||||
{{ question.choices[choice] }},
|
||||
{% endfor %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
Loading…
Reference in New Issue
Block a user