Skip to content
This repository was archived by the owner on Aug 18, 2020. It is now read-only.

Elections #12

Open
wants to merge 68 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
ce916dd
Use element now.
abbashan03 Jan 28, 2020
748aa1f
Published and Saved Msg defined, that'll be send, once a Messages has…
abbashan03 Jan 28, 2020
374e7c0
Branches fro Read, Write, Saved and Published in update.
abbashan03 Jan 28, 2020
2eb2020
Created save_page function
abbashan03 Jan 28, 2020
5a26b87
Added utility functions: add_if_not_member and remove_duplicates
abbashan03 Jan 28, 2020
6b72e00
Use save_page in update
abbashan03 Jan 28, 2020
1cf4967
Added type alias Election.
abbashan03 Jan 28, 2020
5b3b67e
Added Messages.elm with decoder for Message type.
abbashan03 Jan 28, 2020
1d0c4b2
Moved Messages.elm to src/
CSDUMMI Jan 31, 2020
a4c17bb
Added Location of gunicorn installation to path for the execution of …
abbashan03 Feb 4, 2020
16e79e4
mypy and static testing.
abbashan03 Feb 4, 2020
6374e2f
Trying to implement a secure vote function, where the vote can be pub…
CSDUMMI Feb 6, 2020
db324e3
Implemented two step encryption in vote()
CSDUMMI Feb 6, 2020
92140e1
votes field in elections
CSDUMMI Feb 7, 2020
8575934
Encryption of Votes
CSDUMMI Feb 7, 2020
52fc6bf
count_votes rewritten.
CSDUMMI Feb 7, 2020
7aa1161
class Turn created.
CSDUMMI Feb 7, 2020
a250b54
Documentation for .el File Format
CSDUMMI Feb 8, 2020
b4873a6
Changed name from generate_elections to generate_election.
CSDUMMI Feb 8, 2020
8580baa
Added Log File mechanic
CSDUMMI Feb 8, 2020
8c77aa2
Added mechanic for log file in election_example.py
CSDUMMI Feb 8, 2020
370be14
Writing to log file perfected.
CSDUMMI Feb 8, 2020
2afda38
Stopping counting, if two options
CSDUMMI Feb 8, 2020
57e709d
Introduced variable victory threshold for election.
CSDUMMI Feb 8, 2020
9781cfb
Made threshold parameter available through count_votes()as positional
CSDUMMI Feb 8, 2020
7203639
Solved a bug about how self.__resort
CSDUMMI Feb 8, 2020
0e27741
Removed attempt at fixing bug in
CSDUMMI Feb 8, 2020
2621fc4
Removed election_example.py
CSDUMMI Feb 8, 2020
44f8556
Added test/election.py
CSDUMMI Feb 8, 2020
4c4a582
Unit Test for Server/election.py
CSDUMMI Feb 8, 2020
c20f840
Using json.dumps as a
CSDUMMI Feb 8, 2020
1142ba7
Seems like this algorithem has a
CSDUMMI Feb 9, 2020
a66857b
Removed variable threshold.
CSDUMMI Feb 9, 2020
99b5fb9
Solved a bug with the nonexistent
CSDUMMI Feb 9, 2020
dbdedb2
Removing all instances of an elimanted options.
CSDUMMI Feb 9, 2020
d734870
Trying to filter all occurences of s out if it occures in least.
CSDUMMI Feb 9, 2020
b0d84b0
If one of least is NoneOfTheOtherOptions,
abbashan03 Feb 10, 2020
656ccee
Removing any special treatment of NoneOfTheOtherOptions for now
CSDUMMI Feb 10, 2020
d327e6a
Realized: Turn.least() makes it possible that no options are left.
CSDUMMI Feb 10, 2020
5293ddc
Moved check for NoneOfTheOtherOptions into count.
CSDUMMI Feb 12, 2020
4bf9557
Rewrote Turn.count and Turn.__init__ in a more comprehensive and bug …
CSDUMMI Feb 12, 2020
d1b6fbf
Added field potential_voters to Turn.
CSDUMMI Feb 12, 2020
a85d28d
Made filtering for occurences of alternatives valid.
CSDUMMI Feb 12, 2020
ac06dbb
Removed Turn class, as it seems to be impossible to implement AV.
CSDUMMI Feb 12, 2020
f996302
Working counting function, as it seems.
CSDUMMI Feb 15, 2020
ee75ec7
It isn't yet perfect, but good enough.
CSDUMMI Feb 16, 2020
cc65db4
Testing for remie.
CSDUMMI Feb 16, 2020
789bd7e
vote -> count
CSDUMMI Feb 16, 2020
9cc1443
Using count in test function.
CSDUMMI Feb 16, 2020
dc34605
removed outdated tests
Feb 18, 2020
7f7f7cb
Wrote unit test and got an error on
Feb 18, 2020
4d5dd7a
Corrected the calculation of votes and thrown_out and solved the issu…
CSDUMMI Feb 18, 2020
fcf6a54
At last, it seems like I had overestimated my machine.
CSDUMMI Feb 18, 2020
ccad7c6
Merge branch 'master' into Elections
CSDUMMI Feb 18, 2020
bd89c62
Added template for config.yml
CSDUMMI Feb 20, 2020
f380832
Added pytest to requirements.txt and manage.py as a file to execute t…
CSDUMMI Feb 20, 2020
059d81b
manage.py is now either going to run the server or run a test.
CSDUMMI Feb 20, 2020
bd02d25
Added exiting with the returncode of pytest or start, so CircleCI kno…
CSDUMMI Feb 20, 2020
cc9525e
Changed way of executing pytest.
CSDUMMI Feb 20, 2020
a5b4eda
removed proxy in manage.py to execute pytest.
CSDUMMI Feb 20, 2020
c18b0bc
Changed invoking of pytest.
CSDUMMI Feb 20, 2020
6906be9
Removed test_patches.py
CSDUMMI Feb 20, 2020
d9d5aeb
Added stupid test.
CSDUMMI Feb 20, 2020
9565712
Made test into function.
CSDUMMI Feb 20, 2020
9b5b9f2
Merge branch 'ContinousIntegration' into Elections
CSDUMMI Feb 20, 2020
a80892d
Removed merge signatures.
CSDUMMI Feb 20, 2020
7515147
Removed SEED env variables.
CSDUMMI Feb 20, 2020
fb7beb7
Got it. fi
CSDUMMI Feb 27, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
version: 2.1

orbs:
python: circleci/[email protected]

jobs:
build-and-test:
executor: python/default
steps:
- checkout
- python/load-cache
- python/install-deps
- python/save-cache
- run:
command: python -m pytest
name: Test

workflows:
main:
jobs:
- build-and-test
2 changes: 2 additions & 0 deletions BUILD.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@
01/27/20-17:17:06-joris
01/27/20-17:18:06-joris
01/27/20-17:22:43-joris
02/04/20-16:52:22-
02/04/20-16:53:02-
1 change: 1 addition & 0 deletions Database.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ A sample Election Document looks like this:
, "closed" : <Bool: is Closed?>
, "winner" : <Winner Proposal>
, "type" : "<either machine or human>"
, "votes" : [{ "username" : "<username of voter>", "vote" : b"<encrypted vote>" }]
}
```
There can be two kinds of proposals:
Expand Down
18 changes: 0 additions & 18 deletions ROADMAP.md

This file was deleted.

47 changes: 47 additions & 0 deletions Server/.goutputstream-9HC4F0
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import functools,random
from typing import List, Dict

def vote(votes : List[List[str]], options : List[str]):
options = {key : list(filter(lambda v: v[-1] == key or (key == "NoneOfTheOtherOptions" and v == []), votes)) for key in options}
winner = False
while(winner == False):
result = _vote(options,len(votes))
if type(result) == type(""):
winner = (result,len(options[result]))
elif type(result) == type(None):
winner = (None, None)
elif type(result) == type({}):
options = result
else:
raise "VotingError: Unknow type retuned by _vote"
return winner

def _vote(options : Dict[str,List[List[str]]], votes : int):
ordered = sorted(list(options), key=lambda o: options[o], reverse=True)

if len(options[ordered[0]])/votes > 0.5:
return ordered[0]
elif len(options[ordered[0]]) == len(options[ordered[1]]) and len(options[ordered[0]]) == 0.5:
return None
else:
least = options[ordered[-1]]
print(least)
least = options.pop(ordered[-1])
options = { key : [a for a in least if a != ordered[-1]] for key in list(options)}


for vote in least:
if len(vote) > 1:
vote.pop()
options[vote[-1]].append(vote)
print(vote[-1])
elif len(vote) == 1:
options["NoneOfTheOtherOptions"].append([])
else:
raise "VotingError: NoneOfTheOtherOptions seems to be removed from options, because empty lists are supposed to be reallocated, which only appear in NoneOfTheOtherOptions"
return options

def test(n):
options = [str(x) for x in range(100)]
votes = [random.sample(options,k=random.randint(1,len(options))) for i in range(n)]
return vote(votes,options)
8 changes: 8 additions & 0 deletions Server/ELECTION.el
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# File to represent an election result

Votes:
# List of votes as python lists
Thrown:
# All the options, that were thrown out after each other
Winner:
# The Option, that won + the number of votes in support of it.
54 changes: 40 additions & 14 deletions Server/Elections.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
from Server.election import count_votes
from pymongo import MongoClient
from Crypto.Hash import SHA256

from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
from typing import List, Tuple, Mapping

"""
Utility function to get the MongoClient.demnet[<collection>]
Expand Down Expand Up @@ -93,21 +95,32 @@ def create(type,deadline,proposals):
elections.insert_one(election)
return election['hash']


"""Stores a vote byte string, that is encrypted like this:
Given Keys:
- Private Key of the User (private_key_of_user)
- Public Key of the Election Authority (public_key_of_ea)
Encryption of the vote as string:
vote_e = E(private_key_of_user, E(public_key_of_ea, vote))
If you only have the public key of the user you can only
tell, that a user has voted but not how.
And only the EA can read the vote, with their private key.
If you don't trust the EA or the EA has been compromised,
the election is invalid to you, but this could easily
be decentralised.
"""
a vote is a list of all options ranked by
how much a voter wants them to win. (see alternative vote).
**This function call cannot leave any trace of the association between
username and vote.**
"""
def vote(election_hash,vote,username):
def vote(election_hash : str, username : str, vote : List[str], private_key_of_user : RSA.RsaKey, public_key_of_ea : RSA.RsaKey) -> bool:
elections = collection("elections")
election = elections.find_one({ "hash" : election_hash })

if username in election["participants"] and not election['closed']:
cipher_ea = PKCS1_OAEP.new(public_key_of_ea)
cipher_user = PKCS1_OAEP.new(private_key_of_user)

if username in election["participants"]:
return False
else:
elections.update_one({ "hash" : election_hash }, { "$push" : { "participants" : username }})
elections.update_one({ "hash" : election_hash }, { "$push" : { "votes" : vote }})
vote = cipher_user(cipher_ea.encrypt(json.dumps(vote).encode('utf-8')))
elections.update_one({ "hash" : election_hash }, { "$push" : { "votes" : { "username" : username, "vote" : vote_e }}}
return True


Expand All @@ -118,21 +131,22 @@ def vote(election_hash,vote,username):
Publishing the votes, to make the independent control
possible.
"""
def close(election_hash):
def close(election_hash : str, private_key_ea : RSA.RsaKey):
elections = collection('elections')
election = elections.find_one({ "hash" : election_hash })

if election:
if election.get('deadline') <= time.time():
winner = count_votes(election.votes, len(election.participants), range(0,len(election.proposals)+1))["ballot"]
votes = encrypt_votes(election["votes"], private_key_ea)
winner = count_votes(election["votes"], len(election.participants), range(0,len(election.proposals)+1))["ballot"]
winner = election.proposals[winner]
elections.update_one({ "hash" : election_hash }, { "$set" : { "winner" : winner, "closed" : True } })

if election['type'] == "machine":
patches = collection('patches')
patch = patches.find_one({ "hash" : winner['patch_id'] })
Patches.close(patch['name'], patch['name'], patch['hash'], merge=True)
elif election['type'] == "human":
else:
laws = collection("laws")
# Append ammendments to laws
for ammendment in winner["ammendment"]:
Expand All @@ -150,6 +164,18 @@ def close(election_hash):
laws.remove_one({ "title" : removal })


return True
return True
else:
return False

def encrypt_votes(votes : List[bytes], private_key_ea : RSA.RsaKey) -> List[List[str]]:
users = collection("users")
cipher_ea = PKCS1_OAEP.new(private_key_ea)
for vote in votes:
user = users.find_one({ "username" : vote["username"] })
public_key_of_user = RSA.import_key(user['public_key'])
cipher_user = PKCS1_OAEP.new(public_key_of_user)
try:
vote = cipher_ea.decrypt(cipher_user.decrypt(user["vote"]))
except Exception as e:
raise
2 changes: 1 addition & 1 deletion Server/Users.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"""
from Crypto.PublicKey import RSA
from Crypto.Hash import SHA256
from Crypto.Cipher import AES
from Crypto.Cipher import PKCS1_OAEP
from pymongo import MongoClient
import datetime, sys, json

Expand Down
Binary file modified Server/__pycache__/election.cpython-37.pyc
Binary file not shown.
5 changes: 5 additions & 0 deletions Server/election.el
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Votes:
[["A", "B", "C"], ["C", "A", "B"], ["A", "B", "C"]]
Thrown:
Winner:
('C', 1.0)
94 changes: 48 additions & 46 deletions Server/election.py
Original file line number Diff line number Diff line change
@@ -1,47 +1,49 @@
import functools

def count_votes( votes, participant_count, options ):
ballot = map( lambda option : { 'option' : option, 'support' : [] }, options )
winner = distribute_votes( votes, participant_count, list(ballot) )
return { "ballot": winner, "participants" : participant_count, "options" : options }

def distribute_votes( votes, participant_count, ballot ):
if len(ballot[0]['support']) > participant_count * 0.5 or len(ballot) <= 1:
return ballot[0] # Winner!
else:
# Distribute votes
for option in ballot:
option['support'] = option['support'] + list(filter(lambda v: v[-1] == option['option'], votes))

# Eliminate another low ranking option
# Sort by Support
ballot = sorted(ballot, key = lambda option : len(option['support']), reverse=True)

looser = least_popular(ballot)
ballot.remove(looser)
looser['support'] = list(map(lambda v: v[:-1], looser['support']))
return distribute_votes(looser['support'], participant_count, ballot)

def least_popular( ballot ):
# If there is a tie, look at the option, that is more popular in the alternative votes.
if len(ballot[-1]['support']) == len(ballot[-2]['support']):
option1 = ballot[-1]
option2 = ballot[-2]
alternative_votes = list( map( lambda option: option['support'][:-1], ballot ) )
alternative_votes = functools.reduce( lambda x, y : x + y, alternative_votes )

i = -1
least_popular = False

while(not least_popular):
is_tie = alternative_votes[i].count(option1['option']) - alternative_votes.count(option2['option'])

if is_tie < 0:
least_popular = option1
elif is_tie > 0:
least_popular = option2
import random, pprint
from typing import List, Dict

def count(votes : List[List[str]], options : List[str]):
options : Dict[str, List[List[str]]] = { key : list(filter(lambda v: v[-1] == key, votes)) for key in list(options) }
votes = len(votes)
all_participants = votes
thrown_out = 0
result = { "winner" : False
, "rounds" : []
}
while result["winner"] == False:
# Does a candidate have more than 50%?
result["rounds"].append(options)
winners = list(filter(lambda o: len(options[o]) >= 0.5*votes, list(options)))
if len(winners) == 1:
result["winner"] = winners[0]
break
elif len(winners) >= 2:
result["winner"] = None
break
elif len(winners) == 0:
# Is there only one candidate left?
if len(list(options)) == 1:
result["winner"] = options(list(options)[0])
break
elif len(list(options)) == 0:
result["winner"] = None
break
else:
least_popular = False
return least_popular
else:
return ballot[-1]
# Drop worst candidate and find out who voters liked next best
sorted_options = sorted(list(options), key=lambda o: len(options[o]))
worst = sorted_options[-1]
worsts_votes = options.pop(worst)
options = { key : [list(filter(lambda a: a != worst, vote)) for vote in options[key]] for key in list(options)}
options = { key : options[key] for key in list(options) if options[key] != []}
sum_votes = sum([len(options[o]) for o in list(options)])
thrown_out += (votes - sum_votes)
votes = sum_votes
continue

result["thrown_out"] = thrown_out
if result["thrown_out"]/all_participants > 0.5:
result["winner"] = "NoneOfTheOtherOptions"
elif len(result["winner"])/all_participants == 0.5 and (result["thrown_out"]/all_participants == 0.5):
result["winner"] = None

result["winner"] = "NoneOfTheOtherOptions" if (result["thrown_out"]/all_participants) > 0.5 else result["winner"]
return result
39 changes: 0 additions & 39 deletions Server/election_example.py

This file was deleted.

7 changes: 7 additions & 0 deletions Server/sample_election.el
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Votes:
[['A', 'B', 'C'], ['C', 'B', 'A'], ['D', 'A', 'B', 'C'], ['D', 'A', 'C', 'B'], ['NoneOfTheOtherOptions']]
Thrown:
A
D
Winner:
B, 3
20 changes: 20 additions & 0 deletions Server/test_election.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from election import count
import random,os

def test_count(n=50, repeat_for=10**5,seed=None):
for i in range(repeat_for):
options = [str(i) for i in range(random.randint(4,20))]
votes = [random.sample(options,k=random.randint(1,len(options))) for i in range(n)]
result = count(votes,options)

# On AV there is one option deleted every round
# thus after len(options) rounds, AV has to stop.
assert len(result["rounds"]) <= len(options)
assert len(result["rounds"]) > 0

assert result["thrown_out"] >= 0

if result["winner"] != "NoneOfTheOtherOptions":
assert result["thrown_out"]/len(votes) < 0.5
else:
assert result["thrown_out"] > 0.5*len(votes)
Loading