🎉 Up to counting elections

This commit is contained in:
RunasSudo 2017-11-24 19:37:48 +11:00
parent 81807ca7f8
commit 6af132bb5f
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
8 changed files with 195 additions and 72 deletions

View File

@ -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,10 +191,15 @@ class EosList(EosObject):
def post_init(self):
for i in range(len(self.impl)):
val = self.impl[i]
# 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):
return len(self.impl)
@ -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))

View File

@ -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:
@ -153,10 +159,63 @@ class MixingTrustee(Trustee):
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
sk = EmbeddedObjectField(SEGPrivateKey, is_protected=True) # TODO: Threshold
public_key = EmbeddedObjectField(SEGPublicKey)
mixing_trustees = EmbeddedObjectListField(MixingTrustee)
mixing_trustees = EmbeddedObjectListField()

View File

@ -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]

View File

@ -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']

View File

@ -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

View File

@ -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'
}
}

View File

@ -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 %}
{% 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 %}

View 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>