lschoe / mpyc

MPyC: Multiparty Computation in Python
MIT License
367 stars 76 forks source link

Most efficient way to XOR `SecureIntegerArray` #41

Closed Samuel-Maddock closed 1 year ago

Samuel-Maddock commented 1 year ago

Hi lschoe, thanks for such a great library! I have a question about the most efficient way to perform an XOR using SecureArrays.

My problem is as follows: I would like to securely generate n random numbers in [0,1) by having two participants generate n x k bits, perform a secure XOR on each k-bit array and then convert this into n secure floats to use in other computations.

For simplicity let's just assume I want to generate uniform random integers. From my understanding in #36 the only supported way to XOR SecureIntegers is to work directly with bits using mpc.to_bits() and mpc.from_bits() or in my case the np versions.

My code is the following:

from mpyc.runtime import mpc
import numpy as np

async def main():
    await mpc.start()
    n = 2 # Two random ints
    secint = mpc.SecInt()

    random_ints = np.random.random_integers(0, 2**31, size=n)
    shares = mpc.input(mpc.np_to_bits(secint.array(random_ints))) # Shape (n, k=32)

    xor_arrs = shares[0] + shares[1] - 2*(shares[0] * shares[1]) # xor
    r = mpc.np_from_bits(xor_arrs) # Shape (n,) SecInt32
    print(await mpc.output(r))

    await mpc.shutdown()
mpc.run(main())

This currently errors with:

Traceback (most recent call last):
  File "/Users/samuelmaddock/Documents/GitHub/examples/issue.py", line 17, in <module>
    mpc.run(main())
  File "/Users/samuelmaddock/Documents/GitHub/mpyc/mpyc/runtime.py", line 184, in run
    return self._loop.run_until_complete(f)
  File "/opt/homebrew/Caskroom/miniforge/base/envs/synth/lib/python3.10/asyncio/base_events.py", line 646, in run_until_complete
    return future.result()
  File "/Users/samuelmaddock/Documents/GitHub/examples/issue.py", line 10, in main
    shares = mpc.input(mpc.np_to_bits(secint.array(random_ints))) # Shape (n, k=32)
  File "/Users/samuelmaddock/Documents/GitHub/mpyc/mpyc/runtime.py", line 386, in input
    y = self._distribute(x, senders)
  File "/Users/samuelmaddock/Documents/GitHub/mpyc/mpyc/asyncoro.py", line 414, in typed_asyncoro
    coro.send(None)
  File "/Users/samuelmaddock/Documents/GitHub/mpyc/mpyc/runtime.py", line 428, in _distribute
    x = x[0].value.flat  # indexable iterator
AttributeError: 'NoneType' object has no attribute 'value'

This is because mpc.np_to_bits() is only supported for a SecureFiniteField and returns None when I pass it an ArraySecInt32.

Is there a way I can rewrite this code (or modify np_to_bits) to apply to integers? I tried working directly with SecureFiniteField but I couldn't find a way to securely cast between array types?

lschoe commented 1 year ago

Hi, maybe to start out a bit simpler, let's go back to how you've introduced your problem? If you want to generate n random numbers in [0, 1) and use these as secure floats, you could do the following. Assuming that secure fixed-point numbers are OK instead of secure floats (arithmetic with secure floats is much more expensive than with secure fixed-point numbers).

n = 5
k = 20
secfxp = mpc.SecFxp(2*k)  # secure fixed-point numbers with k fractional bits (and k-bit integer part)
r = await mpc.output([mpc.random.random(secfxp) for _ in range(n)])
print(r)

Each call mpc.random.random(secfxp) generates a uniform random fixed-point number in the range [0.0, 1.0). A sample output looks like this:

[0.5972480773925781, 0.5688104629516602, 0.4264688491821289, 0.8405065536499023, 0.32822132110595703]

Does this fit your use case?

Samuel-Maddock commented 1 year ago

Thanks for the quick response!

Fixed point numbers are definitely okay. To clarify here - how does the underlying implementation of mpc.random.random (or random_bits) generate randomness with multiple participants?

My more general use case is also to track the total amount of communication (sent/received) between parties. Is there an easy way to do this? I think my confusion stems from how your example works with 2 or 3 participants:

from mpyc.runtime import mpc

async def main():
    n = 5
    k = 20
    secfxp = mpc.SecFxp(
        2 * k
    )  # secure fixed-point numbers with k fractional bits (and k-bit integer part)
    r = await mpc.output([mpc.random.random(secfxp) for _ in range(n)])
    print(r)

mpc.run(main())

Running the above with -M2 seems to only use a single participant to generate the random numbers. With -M3 the code does not run at all.

lschoe commented 1 year ago

So, "ultimately" the randomness for these secret-shared random bits and numbers is obtained using pseudorandom secret sharing (PRSS). The use of PRSS is not essential but enhances the performance (eliminating one round of communication). You can inspect the source code to see the details.

Your code will run with -M3 if you enclose it again with await mpc.start() and await mpc.shutdown() like you did before in your first code sample.

Samuel-Maddock commented 1 year ago

Got it - think this clears everything up. Thanks!