"""
Minimal pure-Python library that demonstrates a basic encrypted
voting workflow by leveraging a secure multi-party computation (MPC)
`protocol <https://eprint.iacr.org/2023/1740>`__.
"""
from __future__ import annotations
from typing import Dict, List, Tuple, Sequence, Iterable
import doctest
from modulo import modulo
import tinynmc
[docs]class node:
"""
Data structure for maintaining the information associated with a node
and performing node operations.
Suppose that a secure decentralized voting workflow is supported by three
parties. The :obj:`node` objects would be instantiated locally by each of
these three parties.
>>> nodes = [node(), node(), node()]
The preprocessing workflow that the nodes must execute can be simulated
using the :obj:`preprocess` function. The number of voters that the
workflow supports must be known, and it is assumed that all permitted
choices are integers greater than or equal to ``0`` and strictly less
than a fixed maximum value. The number of voters and the number of
distinct choices must be supplied to the :obj:`preprocess` function.
>>> preprocess(nodes, votes=4, choices=2)
Each voter must then submit a request for the opportunity to submit their
vote. The voters can create :obj:`request` instances for this purpose. In
the example below, each of the four voters creates such a request.
>>> request_zero = request(identifier=0)
>>> request_one = request(identifier=1)
>>> request_two = request(identifier=2)
>>> request_three = request(identifier=3)
Each voter can deliver their request to each node, and each node can then
locally use its :obj:`masks` method to generate masks that can be returned
to the requesting voter.
>>> masks_zero = [node.masks(request_zero) for node in nodes]
>>> masks_one = [node.masks(request_one) for node in nodes]
>>> masks_two = [node.masks(request_two) for node in nodes]
>>> masks_three = [node.masks(request_three) for node in nodes]
Each voter can then generate locally a :obj:`vote` instance (*i.e.*, a
masked vote choice).
>>> vote_zero = vote(masks_zero, 0)
>>> vote_one = vote(masks_one, 1)
>>> vote_two = vote(masks_two, 1)
>>> vote_three = vote(masks_three, 1)
Every voter can broadcast its masked vote choice to all the nodes. Each
node can locally assemble these as they arrive. Once a node has received
all masked votes, it can determine its shares of the overall tally of the
votes using the :obj:`outcome` method.
>>> shares = [
... node.outcome([vote_zero, vote_one, vote_two, vote_three])
... for node in nodes
... ]
The overall outcome can be reconstructed from the shares by the voting
workflow operator using the :obj:`reveal` function. The outcome is
represented as a :obj:`list` in which each entry contains the tally for
the choice corresponding to the entry's index.
>>> reveal(shares)
[1, 3]
"""
def __init__(self: node):
"""
Create a node instance and instantiate its private attributes.
"""
self._signature: List[int] = None
self._choices: int = None
self._nodes: List[tinynmc.node] = None
[docs] def masks( # pylint: disable=redefined-outer-name
self: node,
request: Iterable[Tuple[int, int]]
) -> List[Dict[Tuple[int, int], modulo]]:
"""
Return masks for a given request.
:param request: Request from voter.
"""
return [ # pylint: disable=unsubscriptable-object
tinynmc.node.masks(self._nodes[i], request)
for i in range(self._choices)
]
[docs] def outcome(self: node, votes: Sequence[vote]) -> List[modulo]:
"""
Perform computation to determine a share of the overall outcome.
:param votes: Sequence of masked votes.
"""
choices: int = len(votes[0])
return [ # pylint: disable=unsubscriptable-object
self._nodes[i].compute(self._signature, [vote_[i] for vote_ in votes])
for i in range(choices)
]
[docs]class request(List[Tuple[int, int]]):
"""
Data structure for representing a request to submit a vote. A request can be
submitted to each node to obtain corresponding masks for a vote.
:param identifier: Integer identifying the requesting voter.
The example below demonstrates how requests can be created.
>>> request(identifier=1),
([(0, 1)],)
>>> request(identifier=3),
([(0, 3)],)
"""
def __init__(self: request, identifier: int):
self.append((0, identifier))
[docs]class vote(List[Dict[Tuple[int, int], modulo]]):
"""
Data structure for representing a vote that can be broadcast to nodes.
:param masks: Collection of masks to be applied to the vote choice.
:param choice: Non-negative integer representing the vote choice.
Suppose masks have already been obtained from the nodes via the steps
below.
>>> nodes = [node(), node(), node()]
>>> preprocess(nodes, votes=4, choices=3)
>>> identifier = 2
>>> choice = 2
>>> masks = [node.masks(request(identifier)) for node in nodes]
This method can be used to mask the vote choice (in preparation for
broadcasting it to the nodes).
>>> isinstance(vote(masks, choice), vote)
True
"""
def __init__(
self: vote,
masks: List[List[Dict[Tuple[int, int], modulo]]],
choice: int
):
"""
Create a masked vote choice that can be broadcast to nodes.
"""
choices: int = len(masks[0])
for i in range(choices):
masks_i = [mask[i] for mask in masks]
key = list(masks_i[0].keys())[0]
coordinate_to_value = {}
coordinate_to_value[key] = 2 if i == choice else 1
self.append(tinynmc.masked_factors(coordinate_to_value, masks_i))
[docs]def preprocess(nodes: Sequence[node], votes: int, choices: int):
"""
Simulate a preprocessing workflow among the supplied nodes for a workflow
that supports the specified number of votes and distinct choices (where
choices are assumed to be integers greater than or equal to ``0`` and
strictly less than the value ``choices``).
:param nodes: Collection of nodes involved in the workflow.
:param votes: Number of votes.
:param choices: Number of distinct choices (from ``0`` to ``choices - 1``).
The example below performs a preprocessing workflow involving three nodes.
>>> nodes = [node(), node(), node()]
>>> preprocess(nodes, votes=4, choices=3)
"""
# pylint: disable=protected-access
signature: List[int] = [votes]
for node_ in nodes:
node_._signature = signature
node_._choices = choices
node_._nodes = [tinynmc.node() for _ in range(choices)]
for i in range(choices):
tinynmc.preprocess(signature, [node_._nodes[i] for node_ in nodes])
[docs]def reveal(shares: List[List[modulo]]) -> List[int]:
"""
Reconstruct the overall tally of votes from the shares obtained from each
node.
:param shares: Shares of overall outcome tally (where each share is a list
of components, with one component per permitted choice).
Suppose the shares below are returned from the three nodes in a workflow.
>>> from modulo import modulo
>>> p = 4215209819
>>> shares = [
... [modulo(3, p), modulo(5, p), modulo(4, p)],
... [modulo(1, p), modulo(2, p), modulo(9, p)],
... [modulo(8, p), modulo(0, p), modulo(8, p)]
... ]
This method combines such shares into an overall outcome by reconstructing
the individual components and returning a list representing a tally of the
total number of votes for each choice.
>>> reveal(shares)
[3, 2, 4]
"""
choices: int = len(shares[0])
return [
int(sum(share[i] for share in shares)).bit_length() - 1
for i in range(choices)
]
if __name__ == '__main__':
doctest.testmod() # pragma: no cover