pp-phragmen 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pp_phragmen-0.1.0/PKG-INFO +9 -0
- pp_phragmen-0.1.0/README.md +3 -0
- pp_phragmen-0.1.0/pp_phragmen.egg-info/PKG-INFO +9 -0
- pp_phragmen-0.1.0/pp_phragmen.egg-info/SOURCES.txt +28 -0
- pp_phragmen-0.1.0/pp_phragmen.egg-info/dependency_links.txt +1 -0
- pp_phragmen-0.1.0/pp_phragmen.egg-info/requires.txt +1 -0
- pp_phragmen-0.1.0/pp_phragmen.egg-info/top_level.txt +1 -0
- pp_phragmen-0.1.0/protocol/__init__.py +1 -0
- pp_phragmen-0.1.0/protocol/algorithm1_permutation.py +147 -0
- pp_phragmen-0.1.0/protocol/algorithm2_validation.py +95 -0
- pp_phragmen-0.1.0/protocol/algorithm3_initialization.py +56 -0
- pp_phragmen-0.1.0/protocol/algorithm4_score.py +116 -0
- pp_phragmen-0.1.0/protocol/algorithm5_find_min.py +109 -0
- pp_phragmen-0.1.0/protocol/algorithm6_one_hot.py +135 -0
- pp_phragmen-0.1.0/protocol/algorithm7_wallet_update.py +149 -0
- pp_phragmen-0.1.0/protocol/algorithm8_reconstruct_winner.py +79 -0
- pp_phragmen-0.1.0/protocol/state_manager.py +139 -0
- pp_phragmen-0.1.0/protocol/types.py +16 -0
- pp_phragmen-0.1.0/pyproject.toml +18 -0
- pp_phragmen-0.1.0/setup.cfg +4 -0
- pp_phragmen-0.1.0/tests/test_algorithm1.py +221 -0
- pp_phragmen-0.1.0/tests/test_algorithm2.py +209 -0
- pp_phragmen-0.1.0/tests/test_algorithm3.py +91 -0
- pp_phragmen-0.1.0/tests/test_algorithm4.py +291 -0
- pp_phragmen-0.1.0/tests/test_algorithm5.py +173 -0
- pp_phragmen-0.1.0/tests/test_algorithm6.py +186 -0
- pp_phragmen-0.1.0/tests/test_algorithm7.py +552 -0
- pp_phragmen-0.1.0/tests/test_algorithm8.py +138 -0
- pp_phragmen-0.1.0/tests/test_field_size.py +236 -0
- pp_phragmen-0.1.0/tests/test_integration.py +337 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pp-phragmen
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Privacy-Preserving Phragmén Voting via Secure Multi-Party Computation
|
|
5
|
+
Author-email: Liran Shem-Tov <liranst13@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/liranst13/Phragmen_Voting
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Requires-Dist: mpc-secret-shares>=0.2.0
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
# PP-Phragmén: Privacy-Preserving Phragmén Voting
|
|
2
|
+
|
|
3
|
+
PP-Phragmén is a cryptographic implementation of the Phragmén proportional representation voting method in which all ballots and intermediate tallies remain secret throughout the computation. The protocol runs as a Secure Multi-Party Computation (MPC) over Shamir secret shares: each party holds only an encrypted fragment of every vote and score, and the winning committee is determined by jointly executing a sequence of secure comparison, multiplication, and division sub-protocols without any party ever learning another party's inputs or any intermediate plaintext. This implementation follows Algorithms 1–8 of the accompanying Artificial Intelligence journal submission and is built on top of the MPC primitive library in `mpc_primitives/`.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pp-phragmen
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Privacy-Preserving Phragmén Voting via Secure Multi-Party Computation
|
|
5
|
+
Author-email: Liran Shem-Tov <liranst13@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/liranst13/Phragmen_Voting
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Requires-Dist: mpc-secret-shares>=0.2.0
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
pp_phragmen.egg-info/PKG-INFO
|
|
4
|
+
pp_phragmen.egg-info/SOURCES.txt
|
|
5
|
+
pp_phragmen.egg-info/dependency_links.txt
|
|
6
|
+
pp_phragmen.egg-info/requires.txt
|
|
7
|
+
pp_phragmen.egg-info/top_level.txt
|
|
8
|
+
protocol/__init__.py
|
|
9
|
+
protocol/algorithm1_permutation.py
|
|
10
|
+
protocol/algorithm2_validation.py
|
|
11
|
+
protocol/algorithm3_initialization.py
|
|
12
|
+
protocol/algorithm4_score.py
|
|
13
|
+
protocol/algorithm5_find_min.py
|
|
14
|
+
protocol/algorithm6_one_hot.py
|
|
15
|
+
protocol/algorithm7_wallet_update.py
|
|
16
|
+
protocol/algorithm8_reconstruct_winner.py
|
|
17
|
+
protocol/state_manager.py
|
|
18
|
+
protocol/types.py
|
|
19
|
+
tests/test_algorithm1.py
|
|
20
|
+
tests/test_algorithm2.py
|
|
21
|
+
tests/test_algorithm3.py
|
|
22
|
+
tests/test_algorithm4.py
|
|
23
|
+
tests/test_algorithm5.py
|
|
24
|
+
tests/test_algorithm6.py
|
|
25
|
+
tests/test_algorithm7.py
|
|
26
|
+
tests/test_algorithm8.py
|
|
27
|
+
tests/test_field_size.py
|
|
28
|
+
tests/test_integration.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mpc-secret-shares>=0.2.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
protocol
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# PP-Phragmén voting protocol — Algorithms 1–8.
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Algorithm 1 — Oblivious Candidate Permutation.
|
|
2
|
+
|
|
3
|
+
Reference: Algorithm 1 in "Fairness Without Exposure: Privacy-Preserving
|
|
4
|
+
Phragmén Voting", Section 5.1 (page 8). Run once at setup, before any
|
|
5
|
+
election period. No MPC secure operations are required.
|
|
6
|
+
|
|
7
|
+
Each tallying party P_d (d ∈ [D]):
|
|
8
|
+
1. Samples a secret random seed s_d ← {0,1}^λ.
|
|
9
|
+
2. Broadcasts a commitment com_d = SHA-256(s_d ‖ d).
|
|
10
|
+
3. Reveals s_d; all other parties verify the commitment.
|
|
11
|
+
|
|
12
|
+
The shared seed is s = s_1 ⊕ … ⊕ s_D, and the public permutation is
|
|
13
|
+
derived by running a Fisher-Yates shuffle seeded with s.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import hashlib
|
|
17
|
+
import os
|
|
18
|
+
import random
|
|
19
|
+
from typing import List, Optional
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
# Internal helpers (exposed for unit-testing)
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
def _commitment(seed: bytes, party_index: int) -> bytes:
|
|
27
|
+
"""Return SHA-256(seed ‖ party_index) as defined in Algorithm 1, Line 3.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
seed: The party's random seed bytes.
|
|
31
|
+
party_index: Zero-based party index, encoded as a big-endian 4-byte
|
|
32
|
+
unsigned integer before hashing.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
32-byte SHA-256 digest.
|
|
36
|
+
"""
|
|
37
|
+
return hashlib.sha256(seed + party_index.to_bytes(4, "big")).digest()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _verify_commitments(seeds: List[bytes], commitments: List[bytes]) -> None:
|
|
41
|
+
"""Verify every revealed seed against its published commitment.
|
|
42
|
+
|
|
43
|
+
Implements the verification step in Algorithm 1, Lines 6–8.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
seeds: Revealed seeds, one per party (zero-indexed).
|
|
47
|
+
commitments: Published commitments in the same order.
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
ValueError: If any seed does not reproduce its commitment.
|
|
51
|
+
"""
|
|
52
|
+
for d, (seed, com) in enumerate(zip(seeds, commitments)):
|
|
53
|
+
if _commitment(seed, d) != com:
|
|
54
|
+
raise ValueError(
|
|
55
|
+
f"commitment mismatch for party {d}: "
|
|
56
|
+
"revealed seed does not match published commitment"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _fisher_yates(combined_seed: bytes, num_candidates: int) -> List[int]:
|
|
61
|
+
"""Produce a uniformly random permutation of [0, M) via Fisher-Yates.
|
|
62
|
+
|
|
63
|
+
Implements Algorithm 1, Line 10. The combined seed is converted to an
|
|
64
|
+
integer and used to seed Python's Mersenne-Twister PRNG, which then
|
|
65
|
+
drives `random.shuffle` (a Fisher-Yates implementation).
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
combined_seed: XOR of all party seeds (Algorithm 1, Line 9).
|
|
69
|
+
num_candidates: M — size of the candidate set.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
A permutation of [0, M) as a list of length M.
|
|
73
|
+
"""
|
|
74
|
+
seed_int = int.from_bytes(combined_seed, "big")
|
|
75
|
+
rng = random.Random(seed_int)
|
|
76
|
+
pi = list(range(num_candidates))
|
|
77
|
+
rng.shuffle(pi)
|
|
78
|
+
return pi
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
# Algorithm 1
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
def algorithm_1_oblivious_candidate_permutation(
|
|
86
|
+
security_bits: int,
|
|
87
|
+
num_candidates: int,
|
|
88
|
+
num_parties: int,
|
|
89
|
+
seeds: Optional[List[bytes]] = None,
|
|
90
|
+
) -> List[int]:
|
|
91
|
+
"""Generate a public uniformly random permutation of candidates.
|
|
92
|
+
|
|
93
|
+
Algorithm 1 of "Fairness Without Exposure: Privacy-Preserving Phragmén
|
|
94
|
+
Voting", Section 5.1.
|
|
95
|
+
|
|
96
|
+
Each party commits to a random seed via SHA-256, then reveals it. The
|
|
97
|
+
combined (XOR) seed drives a Fisher-Yates shuffle that produces the
|
|
98
|
+
oblivious permutation π used by Algorithm 6 for tie-breaking.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
security_bits: λ — bit-length of each party's random seed.
|
|
102
|
+
Must be a positive multiple of 8.
|
|
103
|
+
num_candidates: M — number of candidates to permute.
|
|
104
|
+
num_parties: D (= n in MPC notation) — number of tallying parties.
|
|
105
|
+
seeds: Optional list of D byte-strings each of length
|
|
106
|
+
``security_bits // 8``. When None every party samples a fresh
|
|
107
|
+
random seed. Supply explicit seeds only in tests to obtain
|
|
108
|
+
deterministic output.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
A permutation π of [0, M) as a list of M distinct integers.
|
|
112
|
+
|
|
113
|
+
Raises:
|
|
114
|
+
ValueError: If ``seeds`` has wrong length or wrong element sizes.
|
|
115
|
+
ValueError: If any revealed seed does not match its commitment
|
|
116
|
+
(simulates an abort on cheating during the reveal phase).
|
|
117
|
+
"""
|
|
118
|
+
seed_len = security_bits // 8
|
|
119
|
+
|
|
120
|
+
# Lines 1-3: every party samples a seed and publishes a commitment.
|
|
121
|
+
if seeds is None:
|
|
122
|
+
seeds = [os.urandom(seed_len) for _ in range(num_parties)]
|
|
123
|
+
else:
|
|
124
|
+
if len(seeds) != num_parties:
|
|
125
|
+
raise ValueError(
|
|
126
|
+
f"expected {num_parties} seeds, got {len(seeds)}"
|
|
127
|
+
)
|
|
128
|
+
for d, s in enumerate(seeds):
|
|
129
|
+
if len(s) != seed_len:
|
|
130
|
+
raise ValueError(
|
|
131
|
+
f"seed for party {d} has length {len(s)}, "
|
|
132
|
+
f"expected {seed_len} (security_bits={security_bits})"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
commitments = [_commitment(s, d) for d, s in enumerate(seeds)] # Line 3
|
|
136
|
+
|
|
137
|
+
# Lines 4-8: reveal phase — every party broadcasts its seed and all
|
|
138
|
+
# others verify it against the stored commitment.
|
|
139
|
+
_verify_commitments(seeds, commitments) # Lines 6-8
|
|
140
|
+
|
|
141
|
+
# Line 9: derive the shared seed by XOR-ing all party seeds.
|
|
142
|
+
combined = seeds[0]
|
|
143
|
+
for s in seeds[1:]:
|
|
144
|
+
combined = bytes(a ^ b for a, b in zip(combined, s))
|
|
145
|
+
|
|
146
|
+
# Lines 10-11: Fisher-Yates shuffle seeded with the combined seed.
|
|
147
|
+
return _fisher_yates(combined, num_candidates)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Algorithm 2 — Input Validation, Sanitization & Compaction.
|
|
2
|
+
|
|
3
|
+
Reference: Algorithm 2 in "Fairness Without Exposure: Privacy-Preserving
|
|
4
|
+
Phragmén Voting", Section 5.2 (page 9). Run once at setup (or per period
|
|
5
|
+
if ballots are re-submitted).
|
|
6
|
+
|
|
7
|
+
For each ballot entry B_{n,m} the protocol verifies B_{n,m} ∈ {0, 1} by
|
|
8
|
+
checking that B_{n,m}·(B_{n,m} − 1) = 0 using a single SecureMult followed
|
|
9
|
+
by a public Reconstruct. Voters whose ballot fails this check are flagged as
|
|
10
|
+
cheaters and removed; the remaining honest-voter rows are compacted into a
|
|
11
|
+
fresh matrix B̂ of size N_valid × M.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from typing import List, Tuple
|
|
15
|
+
|
|
16
|
+
from mpc_secret_shares import (
|
|
17
|
+
reconstruct,
|
|
18
|
+
secure_mult,
|
|
19
|
+
shares_one,
|
|
20
|
+
shares_sub,
|
|
21
|
+
)
|
|
22
|
+
from protocol.types import BallotMatrix, Shares
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def algorithm_2_input_validation(
|
|
26
|
+
B_shares: BallotMatrix,
|
|
27
|
+
n: int,
|
|
28
|
+
t: int,
|
|
29
|
+
p: int,
|
|
30
|
+
) -> Tuple[BallotMatrix, int]:
|
|
31
|
+
"""Validate ballot entries, remove cheating voters, and compact the matrix.
|
|
32
|
+
|
|
33
|
+
Algorithm 2 of "Fairness Without Exposure: Privacy-Preserving Phragmén
|
|
34
|
+
Voting", Section 5.2.
|
|
35
|
+
|
|
36
|
+
For each entry B_{n,m} the protocol computes
|
|
37
|
+
``[[u]] = SecureMult([[B]], [[B]] − [[1]])``
|
|
38
|
+
and reconstructs u publicly. Because u = B·(B−1) equals 0 if and only
|
|
39
|
+
if B ∈ {0, 1}, a non-zero u identifies voter n as a cheater. The first
|
|
40
|
+
invalid entry short-circuits the inner loop (Lines 7–8 of the paper).
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
B_shares: An N × M matrix of (t,n)-sharings, where
|
|
44
|
+
``B_shares[voter][candidate]`` is the sharing of B_{n,m}.
|
|
45
|
+
Voters are zero-indexed; the paper uses 1-indexed notation.
|
|
46
|
+
n: Number of MPC parties (talliers).
|
|
47
|
+
t: Reconstruction threshold.
|
|
48
|
+
p: Prime field modulus.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
A tuple ``(B_hat, n_valid)`` where:
|
|
52
|
+
|
|
53
|
+
* ``B_hat`` is an N_valid × M matrix of (t,n)-sharings containing
|
|
54
|
+
only the rows of honest voters, in their original relative order
|
|
55
|
+
and re-indexed from 0.
|
|
56
|
+
* ``n_valid`` is the count of honest voters (public after this step).
|
|
57
|
+
"""
|
|
58
|
+
num_voters = len(B_shares)
|
|
59
|
+
num_candidates = len(B_shares[0]) if num_voters > 0 else 0
|
|
60
|
+
|
|
61
|
+
cheaters: set[int] = set()
|
|
62
|
+
one_shares: Shares = shares_one(n) # [[1]] — shared public constant
|
|
63
|
+
|
|
64
|
+
# Lines 2-8: for each voter, check every ballot entry until a violation
|
|
65
|
+
# is found. The inner loop breaks on the first detected cheat (Line 8).
|
|
66
|
+
for voter in range(num_voters): # Line 2
|
|
67
|
+
for cand in range(num_candidates): # Line 3
|
|
68
|
+
b_nm: Shares = B_shares[voter][cand]
|
|
69
|
+
|
|
70
|
+
# Line 4: [[u]] = SecureMult([[B]], [[B]] − [[1]])
|
|
71
|
+
# [[B]] − [[1]] is a local operation (affine subtraction).
|
|
72
|
+
b_minus_one: Shares = shares_sub(b_nm, one_shares, p)
|
|
73
|
+
u_shares: Shares = secure_mult(
|
|
74
|
+
b_nm, b_minus_one, n, t, p
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Line 5: u ← Reconstruct([[u]]) — publicly revealed.
|
|
78
|
+
u_val: int = reconstruct(u_shares[:t], p)
|
|
79
|
+
|
|
80
|
+
# Lines 6-8: non-zero u means B ∉ {0, 1} → voter is a cheater.
|
|
81
|
+
if u_val != 0: # Line 6
|
|
82
|
+
cheaters.add(voter) # Line 7
|
|
83
|
+
break # Line 8
|
|
84
|
+
|
|
85
|
+
# Line 9: N_valid = N − |Cheaters|.
|
|
86
|
+
n_valid: int = num_voters - len(cheaters)
|
|
87
|
+
|
|
88
|
+
# Lines 10-15: compact honest voters into B_hat, preserving row order.
|
|
89
|
+
B_hat: BallotMatrix = [
|
|
90
|
+
B_shares[voter]
|
|
91
|
+
for voter in range(num_voters)
|
|
92
|
+
if voter not in cheaters
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
return B_hat, n_valid # Line 16
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Algorithm 3 — Initialization.
|
|
2
|
+
|
|
3
|
+
Reference: Algorithm 3 in "Fairness Without Exposure: Privacy-Preserving
|
|
4
|
+
Phragmén Voting", Section 5.3 (page 9). Run once, prior to the first
|
|
5
|
+
election period only.
|
|
6
|
+
|
|
7
|
+
Sets the global denominator Δ to 1 and every voter's scaled wallet W̃_i to 0,
|
|
8
|
+
establishing the invariant w_n = W̃_n / Δ = 0 for all voters at the start.
|
|
9
|
+
No MPC secure operations are required — both values are public constants.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import List, Tuple
|
|
13
|
+
|
|
14
|
+
from mpc_secret_shares import (
|
|
15
|
+
shares_one,
|
|
16
|
+
shares_zero,
|
|
17
|
+
)
|
|
18
|
+
from protocol.types import Shares
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def algorithm_3_initialization(
|
|
22
|
+
n_valid: int,
|
|
23
|
+
n: int,
|
|
24
|
+
t: int,
|
|
25
|
+
p: int,
|
|
26
|
+
) -> Tuple[Shares, List[Shares]]:
|
|
27
|
+
"""Initialise the global denominator and all voter wallets to their
|
|
28
|
+
starting values.
|
|
29
|
+
|
|
30
|
+
Algorithm 3 of "Fairness Without Exposure: Privacy-Preserving Phragmén
|
|
31
|
+
Voting", Section 5.3.
|
|
32
|
+
|
|
33
|
+
At the start of the first period all rational wallet balances w_n are 0.
|
|
34
|
+
The protocol represents each balance as w_n = W̃_n / Δ, so initialising
|
|
35
|
+
Δ = 1 and W̃_n = 0 satisfies the invariant without any field division.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
n_valid: N_valid — number of honest voters after Algorithm 2.
|
|
39
|
+
n: Number of MPC parties (talliers).
|
|
40
|
+
t: Reconstruction threshold.
|
|
41
|
+
p: Prime field modulus.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
A tuple ``(delta_shares, wallet_shares)`` where:
|
|
45
|
+
|
|
46
|
+
* ``delta_shares`` is a (t,n)-sharing of Δ = 1 (Algorithm 3, Line 1).
|
|
47
|
+
* ``wallet_shares`` is a list of N_valid (t,n)-sharings, each
|
|
48
|
+
encoding W̃_i = 0 (Algorithm 3, Lines 2–3).
|
|
49
|
+
"""
|
|
50
|
+
# Line 1: [[Δ]] ← [[1]]
|
|
51
|
+
delta_shares: Shares = shares_one(n)
|
|
52
|
+
|
|
53
|
+
# Lines 2-3: [[W̃_i]] ← [[0]] for every honest voter i.
|
|
54
|
+
wallet_shares: List[Shares] = [shares_zero(n) for _ in range(n_valid)]
|
|
55
|
+
|
|
56
|
+
return delta_shares, wallet_shares
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Algorithm 4 — Oblivious Score Computation (Global Denominator).
|
|
2
|
+
|
|
3
|
+
Reference: Algorithm 4 in "Fairness Without Exposure: Privacy-Preserving
|
|
4
|
+
Phragmén Voting", Section 5.4 (pages 10–11). Run every election period.
|
|
5
|
+
|
|
6
|
+
Computes per-candidate scores in numerator/denominator form using the
|
|
7
|
+
common-denominator strategy (Section 5, page 7):
|
|
8
|
+
|
|
9
|
+
Score_m = Score_m^num / Score_m^den
|
|
10
|
+
= (Δ − T̃_m) / (|A(m)| · Δ)
|
|
11
|
+
|
|
12
|
+
where T̃_m = Σ_{i ∈ A(m)} W̃_i is the sum of scaled wallets of candidate
|
|
13
|
+
m's supporters, and Δ is the shared global denominator. This eliminates
|
|
14
|
+
per-voter cross-multiplication from the critical path (O(MN) SecureMult
|
|
15
|
+
instead of O(MN²)).
|
|
16
|
+
|
|
17
|
+
Precondition (paper Section 5, page 8): at least one candidate must have
|
|
18
|
+
|A(m)| > 0 (i.e. at least one voter approves at least one candidate).
|
|
19
|
+
The degenerate all-zero ballot matrix is explicitly excluded by the paper;
|
|
20
|
+
calling this function with n_valid=0 or all-zero ballots is outside the
|
|
21
|
+
paper's guaranteed domain and will produce Score^den = 0 for every candidate,
|
|
22
|
+
making Algorithm 5's comparison meaningless.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from typing import List, Tuple
|
|
26
|
+
|
|
27
|
+
from mpc_secret_shares import (
|
|
28
|
+
secure_mult,
|
|
29
|
+
shares_add,
|
|
30
|
+
shares_sub,
|
|
31
|
+
shares_zero,
|
|
32
|
+
)
|
|
33
|
+
from protocol.types import BallotMatrix, ScorePair, Shares
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def algorithm_4_score_computation(
|
|
37
|
+
B_hat: BallotMatrix,
|
|
38
|
+
wallet_shares: List[Shares],
|
|
39
|
+
delta_shares: Shares,
|
|
40
|
+
n_valid: int,
|
|
41
|
+
num_candidates: int,
|
|
42
|
+
n: int,
|
|
43
|
+
t: int,
|
|
44
|
+
p: int,
|
|
45
|
+
) -> Tuple[List[ScorePair], List[Shares], List[Shares]]:
|
|
46
|
+
"""Compute per-candidate scores and supporting data for the current period.
|
|
47
|
+
|
|
48
|
+
Algorithm 4 of "Fairness Without Exposure: Privacy-Preserving Phragmén
|
|
49
|
+
Voting", Section 5.4.
|
|
50
|
+
|
|
51
|
+
For each candidate m the algorithm accumulates:
|
|
52
|
+
|
|
53
|
+
* ``[[|A(m)|]]`` — supporter count (local additions of ballot bits).
|
|
54
|
+
* ``[[T̃_m]]`` — scaled wallet sum of supporters (SecureMult per voter).
|
|
55
|
+
|
|
56
|
+
Then it derives the score representation from Equation (4) of the paper:
|
|
57
|
+
|
|
58
|
+
* ``[[Score_m^num]] = [[Δ]] − [[T̃_m]]`` (local subtraction)
|
|
59
|
+
* ``[[Score_m^den]] = SecureMult([[Δ]], [[|A(m)|]])``
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
B_hat: N_valid × M matrix of (t,n)-sharings of B̂_{i,m} ∈ {0,1}.
|
|
63
|
+
wallet_shares: List of N_valid (t,n)-sharings of the scaled wallets
|
|
64
|
+
W̃_i. Index matches the row index of B_hat.
|
|
65
|
+
delta_shares: (t,n)-sharing of the global denominator Δ.
|
|
66
|
+
n_valid: Number of valid voters N_valid. Must equal len(B_hat).
|
|
67
|
+
num_candidates: Number of candidates M.
|
|
68
|
+
n: Number of MPC parties.
|
|
69
|
+
t: Reconstruction threshold.
|
|
70
|
+
p: Prime field modulus.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
A tuple ``(scores, A_shares, T_tilde_shares)`` where:
|
|
74
|
+
|
|
75
|
+
* ``scores[m]`` is ``(Score_m^num, Score_m^den)`` — a ScorePair of
|
|
76
|
+
(t,n)-sharings for candidate m (Algorithm 4, Lines 8–9).
|
|
77
|
+
* ``A_shares[m]`` is ``[[|A(m)|]]`` — supporter count for candidate m
|
|
78
|
+
(Line 5), needed by Algorithm 7.
|
|
79
|
+
* ``T_tilde_shares[m]`` is ``[[T̃_m]]`` — scaled wallet sum for
|
|
80
|
+
candidate m (Line 7), needed by Algorithm 7.
|
|
81
|
+
"""
|
|
82
|
+
scores: List[ScorePair] = []
|
|
83
|
+
A_shares: List[Shares] = []
|
|
84
|
+
T_tilde_shares: List[Shares] = []
|
|
85
|
+
|
|
86
|
+
for m in range(num_candidates): # Line 1
|
|
87
|
+
A_m: Shares = shares_zero(n) # Line 2
|
|
88
|
+
T_m: Shares = shares_zero(n) # Line 3
|
|
89
|
+
|
|
90
|
+
for i in range(n_valid): # Line 4
|
|
91
|
+
b_im: Shares = B_hat[i][m]
|
|
92
|
+
|
|
93
|
+
# Line 5: [[|A(m)|]] ← [[|A(m)|]] + [[B̂_{i,m}]] (local)
|
|
94
|
+
A_m = shares_add(A_m, b_im, p)
|
|
95
|
+
|
|
96
|
+
# Line 6: [[contrib]] ← SecureMult([[B̂_{i,m}]], [[W̃_i]])
|
|
97
|
+
contrib: Shares = secure_mult(
|
|
98
|
+
b_im, wallet_shares[i], n, t, p
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Line 7: [[T̃_m]] ← [[T̃_m]] + [[contrib]] (local)
|
|
102
|
+
T_m = shares_add(T_m, contrib, p)
|
|
103
|
+
|
|
104
|
+
# Line 8: [[Score_m^num]] ← [[Δ]] − [[T̃_m]] (local)
|
|
105
|
+
score_num: Shares = shares_sub(delta_shares, T_m, p)
|
|
106
|
+
|
|
107
|
+
# Line 9: [[Score_m^den]] ← SecureMult([[Δ]], [[|A(m)|]])
|
|
108
|
+
score_den: Shares = secure_mult(
|
|
109
|
+
delta_shares, A_m, n, t, p
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
scores.append((score_num, score_den))
|
|
113
|
+
A_shares.append(A_m)
|
|
114
|
+
T_tilde_shares.append(T_m)
|
|
115
|
+
|
|
116
|
+
return scores, A_shares, T_tilde_shares
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Algorithm 5 — Find Minimum Score.
|
|
2
|
+
|
|
3
|
+
Reference: Algorithm 5 in "Fairness Without Exposure: Privacy-Preserving
|
|
4
|
+
Phragmén Voting", Section 5.5 (page 12). Run every election period.
|
|
5
|
+
|
|
6
|
+
Finds the winning (minimum) score among all M candidates using a sequential
|
|
7
|
+
left-to-right reduction. At each step the running best is compared against
|
|
8
|
+
the next candidate via cross-multiplication to avoid division:
|
|
9
|
+
|
|
10
|
+
Score_m < Score_best iff Score_m^num · best^den < best^num · Score_m^den
|
|
11
|
+
|
|
12
|
+
Both cross-products are non-negative (Lemma 7.1), so SecureLT is valid.
|
|
13
|
+
|
|
14
|
+
Round complexity: O(M) — this sequential chain is the dominant latency
|
|
15
|
+
bottleneck of the per-period loop (paper Remark 6.1).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from typing import List, Tuple
|
|
19
|
+
|
|
20
|
+
from mpc_secret_shares import (
|
|
21
|
+
secure_mult,
|
|
22
|
+
secure_lt,
|
|
23
|
+
shares_add,
|
|
24
|
+
shares_sub,
|
|
25
|
+
)
|
|
26
|
+
from protocol.types import ScorePair, Shares
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def algorithm_5_find_minimum_score(
|
|
30
|
+
scores: List[ScorePair],
|
|
31
|
+
n: int,
|
|
32
|
+
t: int,
|
|
33
|
+
p: int,
|
|
34
|
+
) -> ScorePair:
|
|
35
|
+
"""Find the shared winning (minimum) score across all candidates.
|
|
36
|
+
|
|
37
|
+
Algorithm 5 of "Fairness Without Exposure: Privacy-Preserving Phragmén
|
|
38
|
+
Voting", Section 5.5.
|
|
39
|
+
|
|
40
|
+
Maintains a running best ``(best^num, best^den)`` initialised to the
|
|
41
|
+
first candidate's score (Lines 1–2). For each subsequent candidate m
|
|
42
|
+
(Lines 3–8) the algorithm:
|
|
43
|
+
|
|
44
|
+
1. Computes two cross-products to compare scores without division
|
|
45
|
+
(Lines 4–5).
|
|
46
|
+
2. Calls SecureLT to get a shared bit β_m = 1_{Score_m < Score_best}
|
|
47
|
+
(Line 6).
|
|
48
|
+
3. Updates the running best via a multiplexer:
|
|
49
|
+
``best ← best + β_m · (Score_m − best)``
|
|
50
|
+
which selects Score_m when β_m = 1 and leaves best unchanged when 0
|
|
51
|
+
(Lines 7–8).
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
scores: List of M ScorePairs ``(Score_m^num, Score_m^den)`` produced
|
|
55
|
+
by Algorithm 4. Must contain at least one entry.
|
|
56
|
+
n: Number of MPC parties.
|
|
57
|
+
t: Reconstruction threshold.
|
|
58
|
+
p: Prime field modulus.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
A ScorePair ``(Score*^num, Score*^den)`` — a (t,n)-sharing of the
|
|
62
|
+
minimum score among all candidates.
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
ValueError: If ``scores`` is empty (no candidates).
|
|
66
|
+
"""
|
|
67
|
+
if not scores:
|
|
68
|
+
raise ValueError("scores must be non-empty: at least one candidate required")
|
|
69
|
+
|
|
70
|
+
# Lines 1-2: initialise the running best to the first candidate's score.
|
|
71
|
+
best_num: Shares = scores[0][0]
|
|
72
|
+
best_den: Shares = scores[0][1]
|
|
73
|
+
|
|
74
|
+
# Lines 3-8: compare each subsequent candidate against the running best.
|
|
75
|
+
for m in range(1, len(scores)): # Line 3
|
|
76
|
+
score_num: Shares = scores[m][0]
|
|
77
|
+
score_den: Shares = scores[m][1]
|
|
78
|
+
|
|
79
|
+
# Line 4: cross_a = Score_m^num · best^den
|
|
80
|
+
cross_a: Shares = secure_mult(
|
|
81
|
+
score_num, best_den, n, t, p
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Line 5: cross_b = best^num · Score_m^den
|
|
85
|
+
cross_b: Shares = secure_mult(
|
|
86
|
+
best_num, score_den, n, t, p
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Line 6: β_m = SecureLT(cross_a, cross_b)
|
|
90
|
+
# β_m = 1 iff Score_m < Score_best (candidate m is a better winner)
|
|
91
|
+
beta_m: Shares = secure_lt(cross_a, cross_b, n, t, p)
|
|
92
|
+
|
|
93
|
+
# Line 7: best^num ← best^num + β_m · (Score_m^num − best^num)
|
|
94
|
+
diff_num: Shares = shares_sub(score_num, best_num, p)
|
|
95
|
+
best_num = shares_add(
|
|
96
|
+
best_num,
|
|
97
|
+
secure_mult(beta_m, diff_num, n, t, p),
|
|
98
|
+
p,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Line 8: best^den ← best^den + β_m · (Score_m^den − best^den)
|
|
102
|
+
diff_den: Shares = shares_sub(score_den, best_den, p)
|
|
103
|
+
best_den = shares_add(
|
|
104
|
+
best_den,
|
|
105
|
+
secure_mult(beta_m, diff_den, n, t, p),
|
|
106
|
+
p,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
return best_num, best_den # Lines 9-10
|