Bit Decomposition

Here we demonstrate Cicada’s ability to convert a secret shared value into a set of secret shared bits that comprise that value. Note that this operation is costly in terms of both communication and time.

The result will have one more dimension than the input secret: for example, a scalar (zero-dimensional) input will yield a vector (one-dimensional) output, a vector input will return a matrix output, and-so-on. The size of the additional dimension will equal the number of bits used to store the encoded secret, which for AdditiveProtocolSuite defaults to 64. The returned bit values are stored using the additional dimension, in big-endian order so that the most-significant bit is located at index 0 and-so-on.

In the following example we initialize AdditiveProtocolSuite with a custom field order (251) and an encoding with fewer bits of precision (4) so that the encoded representation requires just 8 bits, for readability. Note too that we use the Bits encoding when we reveal the results, since the bits are represented by the field values \(0\) and \(1\), and the default FixedPoint encoding would produce unexpected results.

[1]:
import logging

import numpy

from cicada.additive import AdditiveProtocolSuite
from cicada.communicator import SocketCommunicator
from cicada.encoding import FixedPoint, Bits
from cicada.logging import Logger

logging.basicConfig(level=logging.INFO)

def main(communicator):
    log = Logger(logging.getLogger(), communicator)
    protocol = AdditiveProtocolSuite(communicator, order=251, encoding=FixedPoint(precision=4))

    secret = numpy.array(3) if communicator.rank == 0 else None
    log.info(f"Player {communicator.rank} secret: {secret}", src=0)

    secret_share = protocol.share(src=0, secret=secret, shape=())

    decomposition_share = protocol.bit_decompose(secret_share)
    decomposition = protocol.reveal(decomposition_share, encoding=Bits())
    log.info(f"Player {communicator.rank} decomposition shape: {decomposition.shape}", src=1)
    log.info(f"Player {communicator.rank} decomposition: {decomposition}", src=2)

SocketCommunicator.run(world_size=3, fn=main);
INFO:root:Player 0 secret: 3
INFO:root:Player 1 decomposition shape: (8,)
INFO:root:Player 2 decomposition: [0 0 1 1 0 0 0 0]

Note that the decomposition contains 8 bits with the integer value “3” stored in the first four as \(0 0 1 1\), and the fractional value “.0” stored in the last four as \(0 0 0 0\).

Let’s re-run the computation with the fractional value \(3.5\):

[2]:
def main(communicator):
    log = Logger(logging.getLogger(), communicator)
    protocol = AdditiveProtocolSuite(communicator, order=251, encoding=FixedPoint(precision=4))

    secret = numpy.array(3.5) if communicator.rank == 0 else None
    log.info(f"Player {communicator.rank} secret: {secret}", src=0)

    secret_share = protocol.share(src=0, secret=secret, shape=())

    decomposition_share = protocol.bit_decompose(secret_share)
    decomposition = protocol.reveal(decomposition_share, encoding=Bits())
    log.info(f"Player {communicator.rank} decomposition shape: {decomposition.shape}", src=1)
    log.info(f"Player {communicator.rank} decomposition: {decomposition}", src=2)

SocketCommunicator.run(world_size=3, fn=main);
INFO:root:Player 0 secret: 3.5
INFO:root:Player 1 decomposition shape: (8,)
INFO:root:Player 2 decomposition: [0 0 1 1 1 0 0 0]

Now we see that the integer portion of our value is still stored in the first four bits as \(0011\) while the fractional portion “0.5” is stored in the last four as \(1000\).