Source code for tinybid.tinybid

"""
Minimal pure-Python library that implements a basic single-item
first-price auction via a secure multi-party computation (MPC)
protocol.
"""
from __future__ import annotations
from typing import Dict, List, Set, Tuple, Sequence, Iterable
import doctest
import secrets
from modulo import modulo
from bitlist import bitlist
import tinynmc

[docs]class node: """ Data structure for maintaining the information associated with a node and performing node operations. Suppose that a workflow is supported by three nodes (parties performing a decentralized auction). 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 bids that a workflow requires must be known, and it is assumed that all permitted bid prices are integers greater than or equal to ``0`` and strictly less than a fixed maximum value. The number of bids and the number of distinct prices must be supplied to the :obj:`preprocess` function. >>> preprocess(nodes, bids=4, prices=16) Each bidder must then submit a request for the opportunity to submit a bid. The bidders can create :obj:`request` instances for this purpose. In the example below, each of the four bidders 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 bidder 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 bidder. >>> 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 bidder can then generate locally a :obj:`bid` instance (*i.e.*, a masked bid price). >>> bid_zero = bid(masks_zero, 7) >>> bid_one = bid(masks_one, 11) >>> bid_two = bid(masks_two, 2) >>> bid_three = bid(masks_three, 11) Every bidder can broadcast its masked bid to all the nodes. Each node can locally assemble these as they arrive. Once a node has received all masked bids, it can determine its shares of the overall outcome of the auction using the :obj:`outcome` method. >>> shares = [ ... node.outcome([bid_zero, bid_one, bid_two, bid_three]) ... for node in nodes ... ] The overall outcome can be reconstructed from the shares by the auction operator using the :obj:`reveal` function. The outcome is represented as a :obj:`set` containing the :obj:`int` identifiers of the winning bidders. >>> list(sorted(reveal(shares))) [1, 3] """
[docs] def masks( # pylint: disable=arguments-renamed,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 bidder. """ return [ # pylint: disable=no-member tinynmc.node.masks(self._nodes[i], request) for i in range(self._prices) # pylint: disable=no-member ]
[docs] def outcome(self: node, bids: Sequence[bid]) -> List[modulo]: """ Perform computation to determine a share of the auction outcome. :param bids: Sequence of masked bids. """ prices = len(bids[0]) return [ # pylint: disable=no-member self._nodes[i].compute( getattr(self, '_signature'), [bid[i] for bid in bids] ) for i in range(prices) ]
[docs]class request(List[Tuple[int, int]]): """ Data structure for representing a request to submit a bid. A request can be submitted to each node to obtain corresponding masks for a bid. :param identifier: Integer identifying the requesting bidder. 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) -> request: self.append((0, identifier))
[docs]class bid(List[Dict[Tuple[int, int], modulo]]): """ Data structure for representing a bid that can be broadcast to nodes. :param masks: Collection of masks to be applied to the bid price. :param price: Non-negative integer representing the bid price. Suppose masks have already been obtained from the nodes via the steps below. >>> nodes = [node(), node(), node()] >>> preprocess(nodes, bids=4, prices=16) >>> identifier = 2 >>> price = 7 >>> masks = [node.masks(request(identifier)) for node in nodes] This method can be used to mask the bid price (in preparation for broadcasting it to the nodes). >>> isinstance(bid(masks, price), bid) True """ def __init__( self: bid, masks: List[List[Dict[Tuple[int, int], modulo]]], price: int ): """ Create a masked bid price that can be broadcast to nodes. """ modulus = list(masks[0][0].values())[0].modulus prices = len(masks[0]) for i in range(prices): masks_i = [mask[i] for mask in masks] key = list(masks_i[0].keys())[0] identifier = key[1] coordinate_to_value = {} coordinate_to_value[key] = ( 2**(2**(identifier + 1)) if i == price else ( 1 if i > price else 1 + secrets.randbelow(modulus - 1) ) ) self.append(tinynmc.masked_factors(coordinate_to_value, masks_i))
[docs]def preprocess(nodes: Sequence[node], bids: int, prices: int): """ Simulate a preprocessing workflow among the supplied nodes for a workflow that supports the specified number of bids and distinct prices (where prices are assumed to be integers greater than or equal to ``0`` and strictly less than the value ``prices``). :param nodes: Collection of nodes involved in the workflow. :param bids: Number of bids. :param prices: Number of distinct prices (from ``0`` to ``prices``). The example below performs a preprocessing workflow involving three nodes. >>> nodes = [node(), node(), node()] >>> preprocess(nodes, bids=4, prices=16) """ signature = [bids] for node_ in nodes: setattr(node_, '_signature', signature) setattr(node_, '_prices', prices) setattr(node_, '_bids', []) setattr(node_, '_nodes', [ tinynmc.node() for _ in range(prices) ]) for i in range(prices): # pylint: disable=protected-access tinynmc.preprocess( signature, [node_._nodes[i] for node_ in nodes] )
[docs]def reveal(shares: List[List[modulo]]) -> Set[int]: """ Reconstruct the auction outcome from the shares obtained from each node. :param shares: Outcome shares (where each share is a list of components, with one component per permitted price). Suppose the shares below are returned from the three nodes in a workflow. >>> 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 be reconstructing the individual components and decoding the identifiers of the winning bidders. >>> reveal(shares) {1} """ prices = len(shares[0]) for i in reversed(range(prices)): shares_i = [share[i] for share in shares] bits = bitlist(int(sum(shares_i)).bit_length() - 1, length=5)[:-1] outcome = { identifier for (identifier, bit) in enumerate(reversed(bits)) if bit == 1 } if len(outcome) > 0: return outcome return set() # pragma: no cover
if __name__ == '__main__': doctest.testmod() # pragma: no cover