Random Seeds

Random values are used widely in Secure Multiparty Computation to mask secret information, and Cicada is no exception. For example, the shares of an additively shared secret are random numbers carefully chosen so that they sum to the secret value. Thus, it should come as no surprise that random seeds play a critical role in the tradeoff between privacy and repeatability for debugging.

For example, when you create an instance of AdditiveProtocolSuite, the seed used to generate secret shares defaults to a random value, and is guaranteed to be different even on forked processes. This ensures that, by default, every player’s shares are unique, every time a Cicada program is run. In the following example, we run the same function twice in a row, but note that every player has different shares for each run, even though the secret shared value 42 is the same.

[1]:
import logging

import numpy

from cicada.additive import AdditiveProtocolSuite
from cicada.communicator import SocketCommunicator
from cicada.logging import Logger

logging.basicConfig(level=logging.INFO)

def main(communicator):
    log = Logger(logging.getLogger(), communicator)
    protocol = AdditiveProtocolSuite(communicator)

    log.info("*" * 80, src=0)
    value = numpy.array(42) if communicator.rank == 0 else None
    value_share = protocol.share(src=0, secret=value, shape=())
    log.info(f"Player {communicator.rank} share: {value_share}")

SocketCommunicator.run(world_size=3, fn=main);
SocketCommunicator.run(world_size=3, fn=main);
INFO:root:********************************************************************************
INFO:root:Player 0 share: cicada.additive.AdditiveArrayShare(storage=16755233681110755624)
INFO:root:Player 1 share: cicada.additive.AdditiveArrayShare(storage=8296785773883102793)
INFO:root:Player 2 share: cicada.additive.AdditiveArrayShare(storage=11841468692427997209)
INFO:root:********************************************************************************
INFO:root:Player 0 share: cicada.additive.AdditiveArrayShare(storage=12750284787561461432)
INFO:root:Player 1 share: cicada.additive.AdditiveArrayShare(storage=2621599907541443234)
INFO:root:Player 2 share: cicada.additive.AdditiveArrayShare(storage=3074859378609399403)

This is ideal for privacy, but can make debugging programs very difficult, especially when contributing to Cicada itself. However, we can specify an explicit random seed when we create the AdditiveProtocolSuite object:

[2]:
def main(communicator):
    log = Logger(logging.getLogger(), communicator)
    protocol = AdditiveProtocolSuite(communicator, seed=123)

    log.info("*" * 80, src=0)
    value = numpy.array(42) if communicator.rank == 0 else None
    value_share = protocol.share(src=0, secret=value, shape=())
    log.info(f"Player {communicator.rank} share: {value_share}")

SocketCommunicator.run(world_size=3, fn=main);
SocketCommunicator.run(world_size=3, fn=main);
INFO:root:********************************************************************************
INFO:root:Player 0 share: cicada.additive.AdditiveArrayShare(storage=15267930134754468249)
INFO:root:Player 1 share: cicada.additive.AdditiveArrayShare(storage=7969444847926663136)
INFO:root:Player 2 share: cicada.additive.AdditiveArrayShare(storage=13656113164740724241)
INFO:root:********************************************************************************
INFO:root:Player 0 share: cicada.additive.AdditiveArrayShare(storage=15267930134754468249)
INFO:root:Player 1 share: cicada.additive.AdditiveArrayShare(storage=7969444847926663136)
INFO:root:Player 2 share: cicada.additive.AdditiveArrayShare(storage=13656113164740724241)

Now, each player still produces unique shares, but the shares are repeated on subsequent runs, which is extremely helpful for debugging.

However, for the toughest problems, even this may not be enough - if you are verifying your code by hand, the large random field values used as shares are still a distraction. In this case, you can further modify the random behavior of the protocol by specifying an explicit seed_offset during construction:

[3]:
def main(communicator):
    log = Logger(logging.getLogger(), communicator)
    protocol = AdditiveProtocolSuite(communicator, seed=123, seed_offset=0)

    log.info("*" * 80, src=0)
    value = numpy.array(2) if communicator.rank == 0 else None
    value_share = protocol.share(src=0, secret=value, shape=())
    log.info(f"Player {communicator.rank} share: {value_share}")

SocketCommunicator.run(world_size=3, fn=main);
SocketCommunicator.run(world_size=3, fn=main);
INFO:root:********************************************************************************
INFO:root:Player 0 share: cicada.additive.AdditiveArrayShare(storage=131072)
INFO:root:Player 1 share: cicada.additive.AdditiveArrayShare(storage=0)
INFO:root:Player 2 share: cicada.additive.AdditiveArrayShare(storage=0)
INFO:root:********************************************************************************
INFO:root:Player 0 share: cicada.additive.AdditiveArrayShare(storage=131072)
INFO:root:Player 1 share: cicada.additive.AdditiveArrayShare(storage=0)
INFO:root:Player 2 share: cicada.additive.AdditiveArrayShare(storage=0)

… now, every player’s share of a secret value is zero, except for the player supplying the secret, where the share is the secret itself; this greatly simplifies debugging. Of course, all privacy has been eliminated, so you should be careful not do this in production!