diff --git a/README.md b/README.md
index 516cdd7..fadccf1 100644
--- a/README.md
+++ b/README.md
@@ -21,6 +21,8 @@ Eos aims to be implementation-agnostic with respect to cryptographic details. Th
* SCHNORR, Claus Peter and Markus JAKOBSSON. ‘Security of Signed ElGamal Encryption’. In: T. OKAMOTO, ed. *Advances in Cryptology – ASIACRYPT 2000*. Berlin: Springer-Verlag, 2000. pp. 73–89. Lecture Notes in Computer Science, vol. 1976. ISBN 978-3-540-44448-0. Available from: https://doi.org/10.1007/3-540-44448-3_7
* **R**andomised partial checking (RPC) due to Jakobsson, Juels and Rivest (2002)
* JAKOBSSON, Markus, Ari JUELS and Ronald L. RIVEST. ‘Making Mix Nets Robust For Electronic Voting By Randomized Partial Checking’. In: *Proceedings of the 11th USENIX Security Symposium*. pp. 339–353. Berkeley: USENIX Association, 2002. Available from: https://www.usenix.org/event/sec02/full_papers/jakobsson/jakobsson.pdf
+ * Taking note of points raised by Khazaei and Wikström (2013)
+ * KHAZAEI, Shahram and Douglas WIKSTRÖM. ‘Randomized Partial Checking Revisited’. In: E. DAWSON, ed. *Topics in Cryptology – CT-RSA 2013*. Berlin: Springer-Verlag, 2013. pp. 115–128. Lecture Notes in Computer Science, vol. 7779. ISBN 978-3-642-36095-4. Available from: https://doi.org/10.1007/978-3-642-36095-4_8
## Mother of all disclaimers
diff --git a/eos/base/election.py b/eos/base/election.py
index af8213c..882f97e 100644
--- a/eos/base/election.py
+++ b/eos/base/election.py
@@ -45,23 +45,12 @@ class Result(EmbeddedObject):
class ApprovalQuestion(Question):
choices = ListField(StringField())
-
- def compute_result(self):
- result = ApprovalResult(choices=([0] * len(self.choices)))
- for voter in self.recurse_parents(Election).voters:
- for ballot in voter.ballots:
- # TODO: Separate decryption phase
- encrypted_answer = ballot.encrypted_answers[self._instance[1]] # _instance[1] is the question number
- answer = encrypted_answer.decrypt()
- for choice in answer.choices:
- result.choices[choice] += 1
- return result
class ApprovalAnswer(Answer):
choices = ListField(IntField())
-class ApprovalResult(Result):
- choices = ListField(IntField())
+class RawResult(Result):
+ answers = EmbeddedObjectListField()
class Election(TopLevelObject):
_id = UUIDField()
diff --git a/eos/base/tests.py b/eos/base/tests.py
index 1055e6e..143df2c 100644
--- a/eos/base/tests.py
+++ b/eos/base/tests.py
@@ -23,19 +23,16 @@ from eos.core.objects import *
class ElectionTestCase(EosTestCase):
@classmethod
def setUpClass(cls):
- if is_python:
- client.drop_database('test')
+ client.drop_database('test')
- def exit_task_assert(self, election, task, next_task):
+ def do_task_assert(self, election, task, next_task):
self.assertEqual(election.workflow.get_task(task).status, WorkflowTask.Status.READY)
- self.assertEqual(election.workflow.get_task(next_task).status, WorkflowTask.Status.NOT_READY)
- election.workflow.get_task(task).exit()
+ if next_task is not None:
+ self.assertEqual(election.workflow.get_task(next_task).status, WorkflowTask.Status.NOT_READY)
+ election.workflow.get_task(task).enter()
self.assertEqual(election.workflow.get_task(task).status, WorkflowTask.Status.EXITED)
- self.assertEqual(election.workflow.get_task(next_task).status, WorkflowTask.Status.READY)
-
- def save_if_python(self, obj):
- if is_python:
- obj.save()
+ if next_task is not None:
+ self.assertEqual(election.workflow.get_task(next_task).status, WorkflowTask.Status.READY)
@py_only
def test_run_election(self):
@@ -65,18 +62,17 @@ class ElectionTestCase(EosTestCase):
question = ApprovalQuestion(prompt='Chairman', choices=['John Doe', 'Andrew Citizen'])
election.questions.append(question)
- self.save_if_python(election)
+ election.save()
# Check that it saved
- if is_python:
- self.assertEqual(db[Election._name].find_one()['value'], election.serialise())
- self.assertEqual(EosObject.deserialise_and_unwrap(db[Election._name].find_one()).serialise(), election.serialise())
+ self.assertEqual(db[Election._db_name].find_one()['value'], election.serialise())
+ self.assertEqual(EosObject.deserialise_and_unwrap(db[Election._db_name].find_one()).serialise(), election.serialise())
self.assertEqualJSON(EosObject.deserialise_and_unwrap(EosObject.serialise_and_wrap(election)).serialise(), election.serialise())
# Freeze election
- self.exit_task_assert(election, 'eos.base.workflow.TaskConfigureElection', 'eos.base.workflow.TaskOpenVoting')
- self.save_if_python(election)
+ self.do_task_assert(election, 'eos.base.workflow.TaskConfigureElection', 'eos.base.workflow.TaskOpenVoting')
+ election.save()
# Try to freeze it again
try:
@@ -85,6 +81,10 @@ class ElectionTestCase(EosTestCase):
except Exception:
pass
+ # Open voting
+ self.do_task_assert(election, 'eos.base.workflow.TaskOpenVoting', 'eos.base.workflow.TaskCloseVoting')
+ election.save()
+
# Cast ballots
VOTES = [[[0], [0]], [[0, 1], [1]], [[2], [0]]]
@@ -96,19 +96,23 @@ class ElectionTestCase(EosTestCase):
ballot.encrypted_answers.append(encrypted_answer)
election.voters[i].ballots.append(ballot)
- self.save_if_python(election)
+ election.save()
# Close voting
- self.exit_task_assert(election, 'eos.base.workflow.TaskOpenVoting', 'eos.base.workflow.TaskCloseVoting')
- self.save_if_python(election)
+ self.do_task_assert(election, 'eos.base.workflow.TaskCloseVoting', 'eos.base.workflow.TaskDecryptVotes')
+ election.save()
- # Compute result
- election.results = [None, None]
- for i in range(2):
- result = election.questions[i].compute_result()
- election.results[i] = result
+ # "Decrypt" votes
+ self.do_task_assert(election, 'eos.base.workflow.TaskDecryptVotes', 'eos.base.workflow.TaskReleaseResults')
+ election.save()
- self.save_if_python(election)
+ # Check result
+ RESULTS = [[[0], [0, 1], [2]], [[0], [1], [0]]]
+ for i in range(len(RESULTS)):
+ votes1 = RESULTS[i]
+ votes2 = [x.choices for x in election.results[i].answers]
+ self.assertEqual(sorted(votes1), sorted(votes2))
- self.assertEqual(election.results[0].choices, [2, 1, 1])
- self.assertEqual(election.results[1].choices, [2, 1])
+ # Release result
+ self.do_task_assert(election, 'eos.base.workflow.TaskReleaseResults', None)
+ election.save()
diff --git a/eos/base/workflow.py b/eos/base/workflow.py
index db1369c..6651086 100644
--- a/eos/base/workflow.py
+++ b/eos/base/workflow.py
@@ -20,7 +20,7 @@ class WorkflowTask(EmbeddedObject):
class Status:
NOT_READY = 10
READY = 20
- #ENTERED = 30
+ ENTERED = 30
#COMPLETE = 40
EXITED = 50
@@ -40,6 +40,7 @@ class WorkflowTask(EmbeddedObject):
self.status = WorkflowTask.Status.READY if self.are_dependencies_met() else WorkflowTask.Status.NOT_READY
self.listeners = {
+ 'enter': [],
'exit': []
}
@@ -62,16 +63,31 @@ class WorkflowTask(EmbeddedObject):
def satisfies(cls, descriptor):
return cls._name == descriptor or descriptor in cls.provides
+ def on_enter(self):
+ self.exit()
+
+ def enter(self):
+ if self.status is not WorkflowTask.Status.READY:
+ raise Exception('Attempted to enter a task when not ready')
+
+ self.status = WorkflowTask.Status.ENTERED
+ self.fire_event('enter')
+ self.on_enter()
+
def fire_event(self, event):
for listener in self.listeners[event]:
listener()
+ def on_exit(self):
+ pass
+
def exit(self):
- if self.status is not WorkflowTask.Status.READY:
- raise Exception('Attempted to exit a task when not ready')
+ if self.status is not WorkflowTask.Status.ENTERED:
+ raise Exception('Attempted to exit a task when not entered')
self.status = WorkflowTask.Status.EXITED
self.fire_event('exit')
+ self.on_exit()
class Workflow(EmbeddedObject):
tasks = EmbeddedObjectListField()
@@ -109,6 +125,26 @@ class TaskOpenVoting(WorkflowTask):
class TaskCloseVoting(WorkflowTask):
depends_on = ['eos.base.workflow.TaskOpenVoting']
+class TaskDecryptVotes(WorkflowTask):
+ depends_on = ['eos.base.workflow.TaskCloseVoting']
+
+ def on_enter(self):
+ election = self.recurse_parents('eos.base.election.Election')
+
+ for _ in range(len(election.questions)):
+ election.results.append(EosObject.objects['eos.base.election.RawResult']())
+
+ for voter in election.voters:
+ for ballot in voter.ballots:
+ for i in range(len(ballot.encrypted_answers)):
+ answer = ballot.encrypted_answers[i].decrypt()
+ election.results[i].answers.append(answer)
+
+ self.exit()
+
+class TaskReleaseResults(WorkflowTask):
+ depends_on = ['eos.base.workflow.TaskDecryptVotes']
+
# Concrete workflows
# ==================
@@ -119,3 +155,5 @@ class WorkflowBase(Workflow):
self.tasks.append(TaskConfigureElection())
self.tasks.append(TaskOpenVoting())
self.tasks.append(TaskCloseVoting())
+ self.tasks.append(TaskDecryptVotes())
+ self.tasks.append(TaskReleaseResults())
diff --git a/eos/core/objects/__init__.py b/eos/core/objects/__init__.py
index c6fa8b7..4b52bb6 100644
--- a/eos/core/objects/__init__.py
+++ b/eos/core/objects/__init__.py
@@ -129,6 +129,9 @@ class EosObjectType(type):
cls._name = ((cls.__module__ if is_python else meta.__next_class_module__) + '.' + cls.__name__).replace('.js.', '.').replace('.python.', '.') #TNYI: module and qualname
if name != 'EosObject':
EosObject.objects[cls._name] = cls
+ if '_db_name' not in attrs:
+ # Don't inherit _db_name, use only if explicitly given
+ cls._db_name = cls._name
return cls
class EosObject(metaclass=EosObjectType):
@@ -142,6 +145,9 @@ class EosObject(metaclass=EosObjectType):
self._inited = True
def recurse_parents(self, cls):
+ if not isinstance(cls, type):
+ cls = EosObject.objects[cls]
+
if isinstance(self, cls):
return self
if self._instance[0]:
@@ -207,6 +213,9 @@ class EosList(EosObject):
return self.impl[idx]
def __setitem__(self, idx, val):
self.impl[idx] = val
+ val._instance = (self, idx)
+ if not val._inited:
+ val.post_init()
def __contains__(self, val):
return val in self.impl
@@ -311,7 +320,7 @@ class DocumentObject(EosObject, metaclass=DocumentObjectType):
class TopLevelObject(DocumentObject):
def save(self):
#res = db[self._name].replace_one({'_id': self.serialise()['_id']}, self.serialise(), upsert=True)
- res = db[self._name].replace_one({'_id': self._fields['_id'].serialise(self._id)}, EosObject.serialise_and_wrap(self), upsert=True)
+ res = db[self._db_name].replace_one({'_id': self._fields['_id'].serialise(self._id)}, EosObject.serialise_and_wrap(self), upsert=True)
class EmbeddedObject(DocumentObject):
pass
diff --git a/eos/psr/crypto.py b/eos/psr/crypto.py
index 02dcbf0..e401d84 100644
--- a/eos/psr/crypto.py
+++ b/eos/psr/crypto.py
@@ -139,73 +139,3 @@ class SEGCiphertext(EGCiphertext):
_, c = EosObject.to_sha256(str(gs), str(self.gamma), str(self.delta))
return self.c == c
-
-class BlockEncryptedAnswer(EncryptedAnswer):
- blocks = EmbeddedObjectListField()
-
- def decrypt(self):
- # TODO
- raise Exception('NYI')
-
-class RPCMixnet:
- def __init__(self):
- self.params = []
-
- def random_permutation(self, n):
- permutation = list(range(n))
- # Fisher-Yates shuffle
- i = n
- while i != 0:
- rnd = BigInt.crypto_random(0, i - 1)
- rnd = rnd.__int__()
- i -= 1
- permutation[rnd], permutation[i] = permutation[i], permutation[rnd]
- return permutation
-
- def shuffle(self, encrypted_answers):
- shuffled_answers = [None] * len(encrypted_answers)
- permutations = self.random_permutation(len(encrypted_answers))
-
- permutations_and_reenc = []
-
- for i in range(len(encrypted_answers)):
- encrypted_answer = encrypted_answers[i]
-
- # Reencrypt the answer
- shuffled_blocks = []
- block_reencryptions = []
- for block in encrypted_answer.blocks:
- block2, reenc = block.reencrypt()
- shuffled_blocks.append(block2)
- block_reencryptions.append(reenc)
- # And shuffle it to the new position
- shuffled_answers[permutations[i]] = BlockEncryptedAnswer(blocks=shuffled_blocks)
- # Record the parameters
- permutations_and_reenc.append([permutations[i], block_reencryptions, block.public_key.group.random_element(), block.public_key.group.random_element()])
-
- commitments_left = []
- for i in range(len(permutations_and_reenc)):
- val = permutations_and_reenc[i]
- val_json = [val[0], [str(x) for x in val[1]], str(val[2])]
- commitments_left.append(EosObject.to_sha256(EosObject.to_json(val_json))[0])
-
- commitments_right = []
- for i in range(len(permutations_and_reenc)):
- # Find the answer that went to 'i'
- idx = next(idx for idx in range(len(permutations_and_reenc)) if permutations_and_reenc[idx][0] == i)
- val = permutations_and_reenc[idx]
-
- val_json = [idx, [str(x) for x in val[1]], str(val[3])]
- commitments_right.append(EosObject.to_sha256(EosObject.to_json(val_json))[0])
-
- self.params = permutations_and_reenc
- return shuffled_answers, commitments_left, commitments_right
-
- def challenge(self, i, is_left):
- if is_left:
- val = self.params[i]
- return [val[0], val[1], val[2]]
- else:
- idx = next(idx for idx in range(len(self.params)) if self.params[idx][0] == i)
- val = self.params[idx]
- return [idx, val[1], val[3]]
diff --git a/eos/psr/election.py b/eos/psr/election.py
new file mode 100644
index 0000000..412331a
--- /dev/null
+++ b/eos/psr/election.py
@@ -0,0 +1,57 @@
+# 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 .
+
+from eos.core.objects import *
+from eos.base.election import *
+from eos.psr.bitstream import *
+from eos.psr.crypto import *
+
+class BlockEncryptedAnswer(EncryptedAnswer):
+ blocks = EmbeddedObjectListField()
+
+ @classmethod
+ def encrypt(cls, pk, obj):
+ pt = EosObject.to_json(EosObject.serialise_and_wrap(obj))
+ bs = BitStream()
+ bs.write_string(pt)
+ bs.multiple_of(pk.group.p.nbits() - 1, True)
+ ct = bs.map(pk.encrypt, pk.group.p.nbits() - 1)
+
+ return cls(blocks=ct)
+
+ def decrypt(self, sk=None):
+ if sk is None:
+ sk = self.recurse_parents(PSRElection).sk
+
+ bs = BitStream.unmap(self.blocks, sk.decrypt, sk.public_key.group.p.nbits() - 1)
+ m = bs.read_string()
+ obj = EosObject.deserialise_and_unwrap(EosObject.from_json(m))
+
+ return obj
+
+class Trustee(EmbeddedObject):
+ name = StringField()
+ email = StringField()
+
+class MixingTrustee(Trustee):
+ mix_order = IntField()
+ mixed_questions = EmbeddedObjectListField()
+
+class PSRElection(Election):
+ _db_name = Election._name
+
+ sk = EmbeddedObjectField(SEGPrivateKey) # TODO: Threshold
+ mixing_trustees = EmbeddedObjectListField(MixingTrustee)
diff --git a/eos/psr/mixnet.py b/eos/psr/mixnet.py
new file mode 100644
index 0000000..8568e45
--- /dev/null
+++ b/eos/psr/mixnet.py
@@ -0,0 +1,82 @@
+# 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 .
+
+from eos.core.bigint import *
+from eos.core.objects import *
+from eos.psr.election import *
+
+class RPCMixnet:
+ def __init__(self):
+ self.params = []
+
+ def random_permutation(self, n):
+ permutation = list(range(n))
+ # Fisher-Yates shuffle
+ i = n
+ while i != 0:
+ rnd = BigInt.crypto_random(0, i - 1)
+ rnd = rnd.__int__()
+ i -= 1
+ permutation[rnd], permutation[i] = permutation[i], permutation[rnd]
+ return permutation
+
+ def shuffle(self, encrypted_answers):
+ shuffled_answers = [None] * len(encrypted_answers)
+ permutations = self.random_permutation(len(encrypted_answers))
+
+ permutations_and_reenc = []
+
+ for i in range(len(encrypted_answers)):
+ encrypted_answer = encrypted_answers[i]
+
+ # Reencrypt the answer
+ shuffled_blocks = []
+ block_reencryptions = []
+ for block in encrypted_answer.blocks:
+ block2, reenc = block.reencrypt()
+ shuffled_blocks.append(block2)
+ block_reencryptions.append(reenc)
+ # And shuffle it to the new position
+ shuffled_answers[permutations[i]] = BlockEncryptedAnswer(blocks=shuffled_blocks)
+ # Record the parameters
+ permutations_and_reenc.append([permutations[i], block_reencryptions, block.public_key.group.random_element(), block.public_key.group.random_element()])
+
+ commitments_left = []
+ for i in range(len(permutations_and_reenc)):
+ val = permutations_and_reenc[i]
+ val_json = [val[0], [str(x) for x in val[1]], str(val[2])]
+ commitments_left.append(EosObject.to_sha256(EosObject.to_json(val_json))[0])
+
+ commitments_right = []
+ for i in range(len(permutations_and_reenc)):
+ # Find the answer that went to 'i'
+ idx = next(idx for idx in range(len(permutations_and_reenc)) if permutations_and_reenc[idx][0] == i)
+ val = permutations_and_reenc[idx]
+
+ val_json = [idx, [str(x) for x in val[1]], str(val[3])]
+ commitments_right.append(EosObject.to_sha256(EosObject.to_json(val_json))[0])
+
+ self.params = permutations_and_reenc
+ return shuffled_answers, commitments_left, commitments_right
+
+ def challenge(self, i, is_left):
+ if is_left:
+ val = self.params[i]
+ return [val[0], val[1], val[2]]
+ else:
+ idx = next(idx for idx in range(len(self.params)) if self.params[idx][0] == i)
+ val = self.params[idx]
+ return [idx, val[1], val[3]]
diff --git a/eos/psr/tests.py b/eos/psr/tests.py
index ceadbcd..3e2ca86 100644
--- a/eos/psr/tests.py
+++ b/eos/psr/tests.py
@@ -19,6 +19,9 @@ from eos.core.tests import *
from eos.core.bigint import *
from eos.psr.bitstream import *
from eos.psr.crypto import *
+from eos.psr.election import *
+from eos.psr.mixnet import *
+from eos.psr.workflow import *
class EGTestCase(EosTestCase):
def test_eg(self):
@@ -102,15 +105,11 @@ class BlockEGTestCase(EosTestCase):
def test_object(self):
obj = self.Person(name='John Smith')
- pt = EosObject.to_json(EosObject.serialise_and_wrap(obj))
- bs = BitStream()
- bs.write_string(pt)
- bs.multiple_of(self.test_group.p.nbits() - 1, True)
- ct = bs.map(self.sk.public_key.encrypt, self.test_group.p.nbits() - 1)
- bs2 = BitStream.unmap(ct, self.sk.decrypt, self.test_group.p.nbits() - 1)
- m = bs2.read_string()
- obj2 = EosObject.deserialise_and_unwrap(EosObject.from_json(m))
- self.assertEqualJSON(obj, obj2)
+
+ ct = BlockEncryptedAnswer.encrypt(self.sk.public_key, obj)
+ m = ct.decrypt(self.sk)
+
+ self.assertEqualJSON(obj, m)
class MixnetTestCase(EosTestCase):
@py_only
@@ -167,3 +166,79 @@ class MixnetTestCase(EosTestCase):
val_json = [perm, [str(x) for x in reencs], str(rand)]
self.assertEqual(commitments_right[i], EosObject.to_sha256(EosObject.to_json(val_json))[0])
verify_shuffle(perm, i, reencs)
+
+class ElectionTestCase(EosTestCase):
+ def do_task_assert(self, election, task, next_task):
+ self.assertEqual(election.workflow.get_task(task).status, WorkflowTask.Status.READY)
+ if next_task is not None:
+ self.assertEqual(election.workflow.get_task(next_task).status, WorkflowTask.Status.NOT_READY)
+ election.workflow.get_task(task).enter()
+ self.assertEqual(election.workflow.get_task(task).status, WorkflowTask.Status.EXITED)
+ if next_task is not None:
+ self.assertEqual(election.workflow.get_task(next_task).status, WorkflowTask.Status.READY)
+
+ @py_only
+ def test_run_election(self):
+ # Set up election
+ election = PSRElection()
+ election.workflow = PSRWorkflow()
+
+ # Set election details
+ election.name = 'Test Election'
+
+ for i in range(3):
+ voter = Voter()
+ election.voters.append(voter)
+
+ for i in range(3):
+ mixing_trustee = MixingTrustee(mix_order=i)
+ election.mixing_trustees.append(mixing_trustee)
+
+ election.sk = EGPrivateKey.generate()
+
+ question = ApprovalQuestion(prompt='President', choices=['John Smith', 'Joe Bloggs', 'John Q. Public'])
+ election.questions.append(question)
+
+ question = ApprovalQuestion(prompt='Chairman', choices=['John Doe', 'Andrew Citizen'])
+ election.questions.append(question)
+
+ election.save()
+
+ # Freeze election
+ self.do_task_assert(election, 'eos.base.workflow.TaskConfigureElection', 'eos.base.workflow.TaskOpenVoting')
+
+ # Open voting
+ self.do_task_assert(election, 'eos.base.workflow.TaskOpenVoting', 'eos.base.workflow.TaskCloseVoting')
+ election.save()
+
+ # Cast ballots
+ VOTES = [[[0], [0]], [[0, 1], [1]], [[2], [0]]]
+
+ for i in range(3):
+ ballot = Ballot()
+ for j in range(2):
+ answer = ApprovalAnswer(choices=VOTES[i][j])
+ encrypted_answer = BlockEncryptedAnswer.encrypt(election.sk.public_key, answer)
+ ballot.encrypted_answers.append(encrypted_answer)
+ election.voters[i].ballots.append(ballot)
+
+ election.save()
+
+ # Close voting
+ self.do_task_assert(election, 'eos.base.workflow.TaskCloseVoting', 'eos.base.workflow.TaskDecryptVotes')
+ election.save()
+
+ # Decrypt votes, for realsies
+ self.do_task_assert(election, 'eos.base.workflow.TaskDecryptVotes', 'eos.base.workflow.TaskReleaseResults')
+ election.save()
+
+ # Check result
+ RESULTS = [[[0], [0, 1], [2]], [[0], [1], [0]]]
+ for i in range(len(RESULTS)):
+ votes1 = RESULTS[i]
+ votes2 = [x.choices for x in election.results[i].answers]
+ self.assertEqual(sorted(votes1), sorted(votes2))
+
+ # Release result
+ self.do_task_assert(election, 'eos.base.workflow.TaskReleaseResults', None)
+ election.save()
diff --git a/eos/psr/workflow.py b/eos/psr/workflow.py
new file mode 100644
index 0000000..f6b9087
--- /dev/null
+++ b/eos/psr/workflow.py
@@ -0,0 +1,34 @@
+# 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 .
+
+from eos.core.objects import *
+from eos.base.workflow import *
+
+# Concrete tasks
+# ==============
+
+# Concrete workflows
+# ==================
+
+class PSRWorkflow(Workflow):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.tasks.append(TaskConfigureElection())
+ self.tasks.append(TaskOpenVoting())
+ self.tasks.append(TaskCloseVoting())
+ self.tasks.append(TaskDecryptVotes())
+ self.tasks.append(TaskReleaseResults())
diff --git a/eos/tests.py b/eos/tests.py
index 04ce0bd..496481b 100644
--- a/eos/tests.py
+++ b/eos/tests.py
@@ -79,14 +79,14 @@ for dirpath, dirnames, filenames in os.walk('eos'):
method_val = getattr(impl, method)
if isinstance(method_val, types.MethodType) and not hasattr(cls_py, method):
# Python
- if not getattr(method_val, '_js_only', False):
+ if not (len(sys.argv) > 2 and sys.argv[2] == 'js') and not getattr(method_val, '_js_only', False):
cls_py.add_method(method)
if method.startswith('test_'):
test_case = cls_py(method)
test_suite.addTest(test_case)
# Javascript
- if not getattr(method_val, '_py_only', False):
+ if not (len(sys.argv) > 2 and sys.argv[2] == 'py') and not getattr(method_val, '_py_only', False):
if method.startswith('test_'):
cls_js.add_method(method)
test_case = cls_js(method)