diff --git a/eos/core/objects/__init__.py b/eos/core/objects/__init__.py index bdba02d..ad5f57a 100644 --- a/eos/core/objects/__init__.py +++ b/eos/core/objects/__init__.py @@ -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 ''.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)) diff --git a/eos/psr/election.py b/eos/psr/election.py index bda3dbf..87fd432 100644 --- a/eos/psr/election.py +++ b/eos/psr/election.py @@ -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() diff --git a/eos/psr/tests.py b/eos/psr/tests.py index 7a47986..418806b 100644 --- a/eos/psr/tests.py +++ b/eos/psr/tests.py @@ -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] diff --git a/eos/psr/workflow.py b/eos/psr/workflow.py index 2d6bd37..aaae517 100644 --- a/eos/psr/workflow.py +++ b/eos/psr/workflow.py @@ -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'] diff --git a/eosweb/core/main.py b/eosweb/core/main.py index def8809..1132f57 100644 --- a/eosweb/core/main.py +++ b/eosweb/core/main.py @@ -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//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 diff --git a/eosweb/core/modelview.py b/eosweb/core/modelview.py index c88e0e5..d33d6f6 100644 --- a/eosweb/core/modelview.py +++ b/eosweb/core/modelview.py @@ -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' } } diff --git a/eosweb/core/templates/election/view.html b/eosweb/core/templates/election/view.html index 6ba0122..c31e45c 100644 --- a/eosweb/core/templates/election/view.html +++ b/eosweb/core/templates/election/view.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 %} -

+ {% if election.workflow.get_task('eos.base.workflow.TaskReleaseResults').status < Status.EXITED %} +

+ {% endif %} {% else %}

Click here to vote in this election

{% endif %} @@ -52,4 +54,13 @@

The administrator of this election has not yet finished setting the election parameters. The details of the election may change at any time.

{% endif %} + + {% if election.workflow.get_task('eos.base.workflow.TaskReleaseResults').status == Status.EXITED %} +

Results

+ + {% for question in election.questions %} +

{{ loop.index }}. {{ question.prompt }}

+ {% include eosweb.core.main.model_view_map[question.__class__]['result_raw'] %} + {% endfor %} + {% endif %} {% endblock %} diff --git a/eosweb/core/templates/question/approval/result_raw.html b/eosweb/core/templates/question/approval/result_raw.html new file mode 100644 index 0000000..8db40f3 --- /dev/null +++ b/eosweb/core/templates/question/approval/result_raw.html @@ -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 . +#} + +
    + {% for answer in election.results[loop.index0].answers %} +
  • + {% for choice in answer.choices %} + {{ question.choices[choice] }}, + {% endfor %} +
  • + {% endfor %} +