pp-phragmen 0.1.0__py3-none-any.whl

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.
@@ -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,15 @@
1
+ protocol/__init__.py,sha256=bM6PzNTHCypxU6yI6Uc8d5AGce3foao77t0yDBpqWYo,53
2
+ protocol/algorithm1_permutation.py,sha256=VD_3Ex9DlkgSA2Bbd-okOdyNUwMpBCCr_CDMm1zv844,5501
3
+ protocol/algorithm2_validation.py,sha256=ojsJQe-GukJ9vUkk8ZgLDWgIUeqLJdMJURmrogMCsNk,3756
4
+ protocol/algorithm3_initialization.py,sha256=uyfIRaSWMBL8iPz2H0xIVVIDEGVKS-38FdVMz9NxJXc,1861
5
+ protocol/algorithm4_score.py,sha256=iBqSF8U1tu723cbbbesR5G3t6ZnIj91gmz7QBsSgc0w,4442
6
+ protocol/algorithm5_find_min.py,sha256=YVJTnLhdTp0KYQmcb9ZJrGx314PTUA5qUIk96zVLFoo,3789
7
+ protocol/algorithm6_one_hot.py,sha256=trCZmnNjIWCFCdJojA8-tKQf7X6Y3uo5QgBgI4gLejU,5509
8
+ protocol/algorithm7_wallet_update.py,sha256=omWdPS5lUY7WbRqUnqW3-55zihkMTzZ1rQekPWq0d3g,5991
9
+ protocol/algorithm8_reconstruct_winner.py,sha256=_VebyU6ClNEvFuDoZtV-nNSpJ7CyViBJEJ5FuVxnHk0,3285
10
+ protocol/state_manager.py,sha256=Wx9K_68Ix31IW_xPGrP-up8KTC57MVJSB2v6htzXkRg,4831
11
+ protocol/types.py,sha256=wVANfsSPTGgpHtZK7SZJBHlX_vqwOA_58qoGeRDE6to,527
12
+ pp_phragmen-0.1.0.dist-info/METADATA,sha256=lltZBU52UnAlst2QD6k-oax2I3Cu2SD7k6a8sdL-Y2M,340
13
+ pp_phragmen-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
14
+ pp_phragmen-0.1.0.dist-info/top_level.txt,sha256=uy1jQLBxIt2TPgpqghfyarfAM3CLAYNky5SOdYnPSCg,9
15
+ pp_phragmen-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ protocol
protocol/__init__.py ADDED
@@ -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
@@ -0,0 +1,135 @@
1
+ """Algorithm 6 — One-Hot Winner Identification with Permuted Tie-Break.
2
+
3
+ Reference: Algorithm 6 in "Fairness Without Exposure: Privacy-Preserving
4
+ Phragmén Voting", Section 5.6 (pages 12–13). Run every election period.
5
+
6
+ Produces a one-hot shared indicator {[[χ_m]]}_{m ∈ [M]} with χ_{m*} = 1 for
7
+ the unique elected candidate m* and χ_m = 0 for all others.
8
+
9
+ Two sequential passes:
10
+
11
+ Pass 1 (Lines 1–4, O(1) rounds — all M triples are independent and batched):
12
+ For each candidate m, test whether Score_m equals the winning score Score*
13
+ via cross-multiplication followed by SecureIsZero:
14
+ α_m = Score_m^num · Score*^den
15
+ β_m = Score*^num · Score_m^den
16
+ e_m = SecureIsZero(α_m − β_m) ← 1 iff Score_m = Score*
17
+
18
+ Pass 2 (Lines 5–11, O(M) rounds — sequential prefix scan):
19
+ Walk the candidates in the public permutation order π. The running prefix
20
+ accumulator [[s]] starts at 0; the first π-ordered candidate m with e_m = 1
21
+ gets χ_m = 1 and sets s = 1, suppressing all subsequent matches via
22
+ χ_m = SecureMult(e_m, [[1]] − [[s]])
23
+
24
+ The permutation π (from Algorithm 1) breaks ties uniformly at random while
25
+ keeping the winner's identity secret until Algorithm 8.
26
+
27
+ Round complexity: O(M) dominated by the prefix scan (paper Remark 6.1).
28
+ Communication: 2M + (M−1) SecureMult and M SecureIsZero per period.
29
+ """
30
+
31
+ from typing import List
32
+
33
+ from mpc_secret_shares import (
34
+ secure_mult,
35
+ is_zero,
36
+ shares_add,
37
+ shares_one,
38
+ shares_sub,
39
+ )
40
+ from protocol.types import ScorePair, Shares
41
+
42
+
43
+ def algorithm_6_one_hot_winner(
44
+ scores: List[ScorePair],
45
+ winning_score: ScorePair,
46
+ pi: List[int],
47
+ n: int,
48
+ t: int,
49
+ p: int,
50
+ ) -> List[Shares]:
51
+ """Compute a one-hot shared indicator for the winning candidate.
52
+
53
+ Algorithm 6 of "Fairness Without Exposure: Privacy-Preserving Phragmén
54
+ Voting", Section 5.6.
55
+
56
+ Args:
57
+ scores: List of M ScorePairs ``(Score_m^num, Score_m^den)`` from
58
+ Algorithm 4. ``scores[m]`` corresponds to candidate m (0-indexed).
59
+ winning_score: The minimum score ``(Score*^num, Score*^den)`` from
60
+ Algorithm 5.
61
+ pi: Public permutation of ``[0, M)`` from Algorithm 1 (0-indexed).
62
+ Used to break ties obliviously; ``pi[0]`` is the highest-priority
63
+ candidate in case of a tie.
64
+ n: Number of MPC parties.
65
+ t: Reconstruction threshold.
66
+ p: Prime field modulus. Cross-products Score_m^num · Score*^den must
67
+ not exceed p (paper Section 7).
68
+
69
+ Returns:
70
+ ``chi`` — a list of M (t,n)-sharings where ``chi[m] = [[χ_m]]``.
71
+ Exactly one entry encodes the secret 1; all others encode 0.
72
+
73
+ Raises:
74
+ ValueError: If ``scores`` is empty or ``pi`` has wrong length.
75
+ """
76
+ m_count = len(scores)
77
+ if m_count == 0:
78
+ raise ValueError("scores must be non-empty: at least one candidate required")
79
+ if len(pi) != m_count:
80
+ raise ValueError(
81
+ f"pi has length {len(pi)}, expected {m_count} (one entry per candidate)"
82
+ )
83
+
84
+ star_num: Shares = winning_score[0]
85
+ star_den: Shares = winning_score[1]
86
+ one: Shares = shares_one(n)
87
+
88
+ # ------------------------------------------------------------------
89
+ # Lines 1-4: compute e_m = SecureIsZero(α_m − β_m) for each candidate.
90
+ # All M triples (SecureMult, SecureMult, SecureIsZero) are mutually
91
+ # independent → they can be batched in O(1) rounds.
92
+ # ------------------------------------------------------------------
93
+ e: List[Shares] = []
94
+ for m in range(m_count): # Line 1
95
+ score_num, score_den = scores[m]
96
+
97
+ # Line 2: α_m = SecureMult([[Score_m^num]], [[Score*^den]])
98
+ alpha_m: Shares = secure_mult(
99
+ score_num, star_den, n, t, p
100
+ )
101
+
102
+ # Line 3: β_m = SecureMult([[Score*^num]], [[Score_m^den]])
103
+ beta_m: Shares = secure_mult(
104
+ star_num, score_den, n, t, p
105
+ )
106
+
107
+ # Line 4: e_m = SecureIsZero([[α_m]] − [[β_m]])
108
+ # α_m − β_m = 0 iff Score_m = Score* (same fraction)
109
+ diff: Shares = shares_sub(alpha_m, beta_m, p)
110
+ e.append(is_zero(diff, n, t, p))
111
+
112
+ # ------------------------------------------------------------------
113
+ # Lines 5-11: prefix scan in permutation order π.
114
+ # chi is pre-allocated and filled by candidate index (not π-order).
115
+ # ------------------------------------------------------------------
116
+ chi: List[Shares] = [None] * m_count # type: ignore[list-item]
117
+
118
+ # Lines 5-7: initialise with the first candidate in π-order.
119
+ m1: int = pi[0] # Line 5
120
+ chi[m1] = e[m1] # Line 6
121
+ s: Shares = e[m1] # Line 7
122
+
123
+ # Lines 8-11: suppress all but the first π-ordered tie-winner.
124
+ for k in range(1, m_count): # Line 8
125
+ m = pi[k] # Line 9
126
+
127
+ # Line 10: [[χ_m]] = SecureMult([[e_m]], [[1]] − [[s]])
128
+ # [[1]] − [[s]] is local (affine subtraction of public 1).
129
+ one_minus_s: Shares = shares_sub(one, s, p)
130
+ chi[m] = secure_mult(e[m], one_minus_s, n, t, p) # Line 10
131
+
132
+ # Line 11: [[s]] ← [[s]] + [[χ_m]] (local)
133
+ s = shares_add(s, chi[m], p) # Line 11
134
+
135
+ return chi
@@ -0,0 +1,149 @@
1
+ """Algorithm 7 — Wallet Update (Global Denominator Scaling).
2
+
3
+ Reference: Algorithm 7 in "Fairness Without Exposure: Privacy-Preserving
4
+ Phragmén Voting", Section 5.7 (pages 13–14). Run every election period.
5
+
6
+ Updates every voter's scaled wallet W̃_i and the global denominator Δ after
7
+ the winner m* is chosen, maintaining the central invariant (paper Eq. 6):
8
+
9
+ w_n = W̃_n / Δ for all n ∈ [N_valid], all periods.
10
+
11
+ The update rule (paper Eq. 2) for a voter V_n is:
12
+
13
+ w_n ← 0 if n ∈ A(m*) (supporter: reset)
14
+ w_n ← w_n + Score* if n ∉ A(m*) (non-supporter: gain)
15
+
16
+ where Score* = (Δ − T̃*) / (|A(m*)| · Δ).
17
+
18
+ To avoid any field division the algorithm scales all wallet values by |A(m*)|
19
+ and increments the global denominator by the same factor:
20
+
21
+ New W̃_i (supporter) = 0
22
+ New W̃_i (non-supporter) = W̃_i · |A(m*)| + κ* where κ* = Δ − T̃*
23
+ New Δ = Δ · |A(m*)|
24
+
25
+ Invariant verification:
26
+ New w_n = New W̃_n / New Δ
27
+ For supporters: 0 / (Δ · |A(m*)|) = 0 ✓
28
+ For non-supporters: (W̃_n · |A(m*)| + κ*) / (Δ · |A(m*)|)
29
+ = W̃_n/Δ + κ*/(Δ · |A(m*)|)
30
+ = w_n + Score* ✓
31
+ """
32
+
33
+ from typing import List, Tuple
34
+
35
+ from mpc_secret_shares import (
36
+ secure_mult,
37
+ shares_add,
38
+ shares_one,
39
+ shares_sub,
40
+ shares_zero,
41
+ )
42
+ from protocol.types import BallotMatrix, Shares
43
+
44
+
45
+ def algorithm_7_wallet_update(
46
+ B_hat: BallotMatrix,
47
+ chi: List[Shares],
48
+ wallet_shares: List[Shares],
49
+ delta_shares: Shares,
50
+ A_shares: List[Shares],
51
+ T_tilde_shares: List[Shares],
52
+ n_valid: int,
53
+ num_candidates: int,
54
+ n: int,
55
+ t: int,
56
+ p: int,
57
+ ) -> Tuple[List[Shares], Shares]:
58
+ """Update voter wallets and the global denominator after electing a winner.
59
+
60
+ Algorithm 7 of "Fairness Without Exposure: Privacy-Preserving Phragmén
61
+ Voting", Section 5.7.
62
+
63
+ Precondition: at least one voter supports the elected candidate
64
+ (paper Section 5, page 8 — guaranteed by the protocol assumptions).
65
+
66
+ Args:
67
+ B_hat: N_valid × M ballot matrix of (t,n)-sharings.
68
+ chi: List of M (t,n)-sharings — the one-hot winner indicator from
69
+ Algorithm 6. Exactly one entry encodes 1.
70
+ wallet_shares: List of N_valid (t,n)-sharings of the current W̃_i.
71
+ delta_shares: (t,n)-sharing of the current global denominator Δ.
72
+ A_shares: List of M (t,n)-sharings of |A(m)| from Algorithm 4.
73
+ T_tilde_shares: List of M (t,n)-sharings of T̃_m from Algorithm 4.
74
+ n_valid: Number of honest voters N_valid.
75
+ num_candidates: Number of candidates M.
76
+ n: Number of MPC parties.
77
+ t: Reconstruction threshold.
78
+ p: Prime field modulus.
79
+
80
+ Returns:
81
+ A tuple ``(new_wallet_shares, new_delta_shares)`` where:
82
+
83
+ * ``new_wallet_shares[i]`` is the updated (t,n)-sharing of W̃_i.
84
+ * ``new_delta_shares`` is the updated (t,n)-sharing of Δ.
85
+
86
+ The invariant w_n = W̃_n / Δ is preserved for every honest voter.
87
+ """
88
+ one: Shares = shares_one(n)
89
+
90
+ # ------------------------------------------------------------------
91
+ # Lines 1-5: extract the winner's supporter count and load sum.
92
+ # The χ-weighted sums isolate the winner's row from Algorithm 4 output.
93
+ # ------------------------------------------------------------------
94
+ A_star: Shares = shares_zero(n) # [[|A(m*)|]] Line 1
95
+ T_star: Shares = shares_zero(n) # [[T̃*]] Line 2
96
+
97
+ for m in range(num_candidates): # Line 3
98
+ # Line 4: [[|A(m*)|]] ← [[|A(m*)|]] + SecureMult([[χ_m]], [[|A(m)|]])
99
+ A_star = shares_add(
100
+ A_star,
101
+ secure_mult(chi[m], A_shares[m], n, t, p),
102
+ p,
103
+ )
104
+ # Line 5: [[T̃*]] ← [[T̃*]] + SecureMult([[χ_m]], [[T̃_m]])
105
+ T_star = shares_add(
106
+ T_star,
107
+ secure_mult(chi[m], T_tilde_shares[m], n, t, p),
108
+ p,
109
+ )
110
+
111
+ # Line 6: [[κ*]] ← [[Δ]] − [[T̃*]] (= Score*^num, local)
112
+ kappa_star: Shares = shares_sub(delta_shares, T_star, p)
113
+
114
+ # ------------------------------------------------------------------
115
+ # Lines 7-13: per-voter wallet update.
116
+ # γ_i = 1 iff voter i supports the winner; 0 otherwise.
117
+ # Supporters are reset (× 0); non-supporters are incremented (× 1).
118
+ # ------------------------------------------------------------------
119
+ new_wallets: List[Shares] = []
120
+
121
+ for i in range(n_valid): # Line 7
122
+ # Lines 8-10: [[γ_i]] = Σ_m SecureMult([[B̂_{i,m}]], [[χ_m]])
123
+ gamma_i: Shares = shares_zero(n) # Line 8
124
+ for m in range(num_candidates): # Line 9
125
+ gamma_i = shares_add(
126
+ gamma_i,
127
+ secure_mult(B_hat[i][m], chi[m], n, t, p),
128
+ p,
129
+ ) # Line 10
130
+
131
+ # Line 11: [[μ_i]] ← SecureMult([[W̃_i]], [[|A(m*)|]])
132
+ mu_i: Shares = secure_mult(
133
+ wallet_shares[i], A_star, n, t, p
134
+ )
135
+
136
+ # Line 12: [[ν_i]] ← [[μ_i]] + [[κ*]] (local)
137
+ nu_i: Shares = shares_add(mu_i, kappa_star, p)
138
+
139
+ # Line 13: [[W̃_i]] ← SecureMult([[1]] − [[γ_i]], [[ν_i]])
140
+ # [[1]] − [[γ_i]] is local. Supporters (γ_i=1) → 0; others → ν_i.
141
+ one_minus_gamma: Shares = shares_sub(one, gamma_i, p)
142
+ new_wallets.append(
143
+ secure_mult(one_minus_gamma, nu_i, n, t, p)
144
+ ) # Line 13
145
+
146
+ # Line 14: [[Δ]] ← SecureMult([[Δ]], [[|A(m*)|]])
147
+ new_delta: Shares = secure_mult(delta_shares, A_star, n, t, p)
148
+
149
+ return new_wallets, new_delta # Line 14
@@ -0,0 +1,79 @@
1
+ """Algorithm 8 — Winner Reconstruction.
2
+
3
+ Reference: Algorithm 8 in "Fairness Without Exposure: Privacy-Preserving
4
+ Phragmén Voting", Section 5.8 (page 14). Run every election period as the
5
+ final step, after Algorithm 7 has updated the wallets.
6
+
7
+ Reconstructs the one-hot indicator {[[χ_m]]}_{m∈[M]} produced by Algorithm 6
8
+ into a publicly revealed winner index m*. This is the only step in the
9
+ per-period loop that reveals information: the identity of the elected
10
+ candidate. All ballots, wallets, and intermediate scores remain secret.
11
+
12
+ The two consistency checks (Lines 3–6) guard against upstream protocol
13
+ failures; they abort rather than silently return a wrong winner.
14
+
15
+ No MPC secure operations are performed — Reconstruct is local Lagrange
16
+ interpolation on already-held shares.
17
+ """
18
+
19
+ from typing import List
20
+
21
+ from mpc_secret_shares import reconstruct
22
+ from protocol.types import Shares
23
+
24
+
25
+ def algorithm_8_reconstruct_winner(
26
+ chi: List[Shares],
27
+ t: int,
28
+ p: int,
29
+ ) -> int:
30
+ """Reconstruct and reveal the winning candidate index from the one-hot indicator.
31
+
32
+ Algorithm 8 of "Fairness Without Exposure: Privacy-Preserving Phragmén
33
+ Voting", Section 5.8.
34
+
35
+ Each sharing [[χ_m]] is reconstructed publicly using t shares. The
36
+ results must form a valid one-hot vector: every entry in {0, 1} and
37
+ exactly one entry equal to 1. Any deviation indicates an upstream
38
+ protocol failure and causes an immediate abort.
39
+
40
+ Args:
41
+ chi: List of M (t,n)-sharings of χ_m ∈ {0, 1}, produced by
42
+ Algorithm 6. ``chi[m]`` corresponds to candidate m (0-indexed).
43
+ t: Reconstruction threshold — the number of shares used for
44
+ Lagrange interpolation.
45
+ p: Prime field modulus.
46
+
47
+ Returns:
48
+ The 0-indexed winner m* — the unique m ∈ [0, M) with χ_m = 1.
49
+
50
+ Raises:
51
+ ValueError: If any χ_m ∉ {0, 1} (Lines 3–4 of the paper).
52
+ ValueError: If Σ χ_m ≠ 1 — either no winner or multiple winners
53
+ (Lines 5–6 of the paper).
54
+ ValueError: If ``chi`` is empty.
55
+ """
56
+ if not chi:
57
+ raise ValueError("chi must be non-empty: at least one candidate required")
58
+
59
+ # Lines 1-4: reconstruct every χ_m publicly and validate each value.
60
+ chi_plain: List[int] = []
61
+ for m, shares in enumerate(chi): # Line 1
62
+ val: int = reconstruct(shares[:t], p) # Line 2
63
+ if val not in (0, 1): # Line 3
64
+ raise ValueError(
65
+ f"χ_{m} = {val} is not in {{0, 1}}: "
66
+ "one-hot vector is malformed (upstream protocol failure)"
67
+ ) # Line 4
68
+ chi_plain.append(val)
69
+
70
+ # Lines 5-6: verify exactly one candidate received χ = 1.
71
+ total = sum(chi_plain)
72
+ if total != 1: # Line 5
73
+ raise ValueError(
74
+ f"Σ χ_m = {total} ≠ 1: expected exactly one winner "
75
+ f"(got {total})"
76
+ ) # Line 6
77
+
78
+ # Lines 7-8: return the unique winner index.
79
+ return chi_plain.index(1) # Lines 7-8
@@ -0,0 +1,139 @@
1
+ """State persistence between election periods for tallying parties.
2
+
3
+ Reference: "Fairness Without Exposure: Privacy-Preserving Phragmén Voting",
4
+ Section 5 — the protocol operates *periodically*; talliers retain the shared
5
+ wallet state from one period to the next.
6
+
7
+ Architecture note
8
+ -----------------
9
+ The entities that hold persistent state are the **talliers** P_1 … P_D, not
10
+ voters and not candidates. Each tallier P_d stores only its own share index
11
+ (the d-th component) from every (t,n)-sharing:
12
+
13
+ delta_share_d = delta_shares[d-1] — one Tuple[int, int]
14
+ wallet_d[i] = wallet_shares[i][d-1] — one Tuple[int, int] per voter
15
+
16
+ Voters submit **fresh ballots** each period and may vote for different
17
+ candidates. Only the scaled-wallet state carries over.
18
+
19
+ Workflow
20
+ --------
21
+ End of period r:
22
+ for d in 1..n:
23
+ save_tallier_state(d, delta_r, wallets_r, path_d)
24
+
25
+ Start of period r+1:
26
+ states = [load_tallier_state(d, path_d) for d in 1..n]
27
+ delta_r, wallets_r = combine_tallier_states(states)
28
+ # then run Algorithm 2 on the *new* ballots, Algorithms 4-8 as usual
29
+ """
30
+
31
+ import json
32
+ from typing import List, Tuple
33
+
34
+ from protocol.types import Shares
35
+
36
+ # One party's contribution to a single sharing: (party_id, share_value).
37
+ SingleShare = Tuple[int, int]
38
+
39
+
40
+ def save_tallier_state(
41
+ tallier_id: int,
42
+ delta_shares: Shares,
43
+ wallet_shares: List[Shares],
44
+ path: str,
45
+ ) -> None:
46
+ """Persist tallier d's individual shares to a JSON file.
47
+
48
+ Extracts the d-th entry from each (t,n)-sharing and writes it to disk.
49
+ No other party's data is included; the file contains only what P_d
50
+ would hold in a real distributed deployment.
51
+
52
+ Args:
53
+ tallier_id: 1-based party index d ∈ {1, …, n}.
54
+ delta_shares: Full (t,n)-sharing of the global denominator Δ.
55
+ wallet_shares: List of N_valid full (t,n)-sharings of W̃_i.
56
+ path: Destination file path. Parent directory must exist.
57
+ """
58
+ idx = tallier_id - 1 # 0-based index into the Shares list
59
+
60
+ state = {
61
+ "tallier_id": tallier_id,
62
+ "n_valid": len(wallet_shares),
63
+ "delta_share": list(delta_shares[idx]),
64
+ "wallet_shares": [list(ws[idx]) for ws in wallet_shares],
65
+ }
66
+
67
+ with open(path, "w", encoding="utf-8") as fh:
68
+ json.dump(state, fh)
69
+
70
+
71
+ def load_tallier_state(
72
+ tallier_id: int,
73
+ path: str,
74
+ ) -> Tuple[SingleShare, List[SingleShare]]:
75
+ """Load tallier d's individual shares from a JSON file.
76
+
77
+ Args:
78
+ tallier_id: 1-based party index d — must match the value stored in
79
+ the file, otherwise a ValueError is raised.
80
+ path: Source file path.
81
+
82
+ Returns:
83
+ A tuple ``(delta_share_d, wallet_shares_d)`` where:
84
+
85
+ * ``delta_share_d`` is ``(d, Δ_d)`` — tallier d's share of Δ.
86
+ * ``wallet_shares_d`` is ``[(d, W̃_i_d) for i in range(n_valid)]``
87
+ — tallier d's share of each voter's wallet.
88
+
89
+ Raises:
90
+ ValueError: If the stored tallier_id does not match the argument.
91
+ FileNotFoundError: If the path does not exist.
92
+ """
93
+ with open(path, "r", encoding="utf-8") as fh:
94
+ state = json.load(fh)
95
+
96
+ stored_id = state["tallier_id"]
97
+ if stored_id != tallier_id:
98
+ raise ValueError(
99
+ f"File {path!r} contains state for tallier {stored_id}, "
100
+ f"but tallier_id={tallier_id} was requested"
101
+ )
102
+
103
+ delta_share: SingleShare = tuple(state["delta_share"]) # type: ignore[assignment]
104
+ wallet_shares_d: List[SingleShare] = [
105
+ tuple(ws) for ws in state["wallet_shares"] # type: ignore[misc]
106
+ ]
107
+ return delta_share, wallet_shares_d
108
+
109
+
110
+ def combine_tallier_states(
111
+ tallier_states: List[Tuple[SingleShare, List[SingleShare]]],
112
+ ) -> Tuple[Shares, List[Shares]]:
113
+ """Reconstruct full (t,n)-Shares objects from all talliers' individual shares.
114
+
115
+ In a real deployment each tallier sends its share to an agreed
116
+ reconstruction point; in the simulation all shares are available locally.
117
+
118
+ Args:
119
+ tallier_states: List of ``(delta_share_d, wallet_shares_d)`` tuples,
120
+ one per tallier, ordered by tallier index 1 … n
121
+ (``tallier_states[0]`` = P_1's state, etc.).
122
+
123
+ Returns:
124
+ ``(delta_shares, wallet_shares)`` — full (t,n)-sharings ready for
125
+ use by Algorithms 4–8 in the next period.
126
+ """
127
+ n = len(tallier_states)
128
+
129
+ # delta_shares = [(1, Δ_1), (2, Δ_2), …, (n, Δ_n)]
130
+ delta_shares: Shares = [tallier_states[d][0] for d in range(n)]
131
+
132
+ # wallet_shares[i] = [(1, W̃_i_1), …, (n, W̃_i_n)]
133
+ n_valid = len(tallier_states[0][1])
134
+ wallet_shares: List[Shares] = [
135
+ [tallier_states[d][1][i] for d in range(n)]
136
+ for i in range(n_valid)
137
+ ]
138
+
139
+ return delta_shares, wallet_shares
protocol/types.py ADDED
@@ -0,0 +1,16 @@
1
+ """Shared type aliases used across all PP-Phragmén protocol modules.
2
+
3
+ Reference: Section 5 of "Fairness Without Exposure: Privacy-Preserving
4
+ Phragmén Voting."
5
+ """
6
+
7
+ from typing import List, Tuple
8
+
9
+ Shares = List[Tuple[int, int]]
10
+ """A (t,n)-Shamir sharing: [(party_id, share_value), ...] of length n."""
11
+
12
+ BallotMatrix = List[List[Shares]]
13
+ """N_valid × M matrix; BallotMatrix[i][m] is the (t,n)-sharing of B̂_{i,m}."""
14
+
15
+ ScorePair = Tuple[Shares, Shares]
16
+ """Candidate score as (Score^num, Score^den) — both (t,n)-sharings."""