Transcripts

In this section we extend Cicada’s basic Logging functionality to support advanced use cases including debugging, network traffic analysis, consistency verification, and zero-knowledge proofs. We refer to these more detailed logs as transcripts, and generate them using the cicada.transcript module. Transcripts supplement Cicada’s normal logging with an optional recording mechanism that generates new categories of log messages, and a set of logging components to filter and format those messages in straightforward ways.

Tip

You’ll need a good working knowledge of the Python logging module to fully understand the following examples!

For the rest of this article, we’ll be working with the following Cicada program, which adds two numbers using additive secret sharing:

[1]:
import numpy

from cicada.additive import AdditiveProtocolSuite
from cicada.communicator import SocketCommunicator
from cicada.encoding import FixedPoint

def main(communicator):
    encoding = FixedPoint(precision=2)
    protocol = AdditiveProtocolSuite(communicator, order=127, encoding=encoding)
    a_share = protocol.share(src=0, secret=numpy.array(2), shape=())
    b_share = protocol.share(src=1, secret=numpy.array(3), shape=())
    c_share = protocol.add(a_share, b_share)
    c = protocol.reveal(c_share)

Note that we’re using a very low-precision encoding (precision=2) and a very small field (order=127) to make the values in the logged output easier to read.

Network Traffic

To see how Cicada transcripts work, let’s generate a transcript of the network messages generated by our program:

[2]:
import logging

from cicada import transcript

with transcript.record():
    handler = transcript.net_handler()
    transcript.set_handler(logging.getLogger(), handler)
    SocketCommunicator.run(fn=main, world_size=3);
Player 0: --> 1 PRZS 2113258192961202993
Player 2: --> 0 PRZS 4989078122295468691
Player 1: --> 2 PRZS 1641890102022822472
Player 0: <-- 2 PRZS 4989078122295468691
Player 2: <-- 1 PRZS 1641890102022822472
Player 1: <-- 0 PRZS 2113258192961202993
Player 2: --> 0 GATHER 93
Player 0: --> 0 GATHER 15
Player 1: --> 0 GATHER 39
Player 2: --> 1 GATHER 93
Player 0: <-- 2 GATHER 93
Player 1: --> 1 GATHER 39
Player 2: --> 2 GATHER 93
Player 1: <-- 2 GATHER 93
Player 0: <-- 0 GATHER 15
Player 2: <-- 2 GATHER 93
Player 0: <-- 1 GATHER 39
Player 1: <-- 1 GATHER 39
Player 0: --> 1 GATHER 15
Player 0: --> 2 GATHER 15
Player 1: <-- 0 GATHER 15
Player 2: <-- 0 GATHER 15
Player 1: --> 2 GATHER 39
Player 2: <-- 1 GATHER 39

We begin by calling transcript.record before running our program, to enable generation of the additional log messages supported by transcription.

Then, we use transcript.net_handler to create a StreamHandler, configured to display networking messages to the console using a default human-readable representation where each line of output represents one message that is sent or received. For example, in the above output:

Player 1: –> 2 GATHER X

means that player 1 has sent a GATHER message to player 2, with a payload of “X” (if you run these examples yourself, the payloads will be different due to random seeding).

Note that interpreting message traffic can be extremely difficult without the context of the program generating them. For this reason, transcript output can include arbitrary “context” messages that you insert from your application yourself:

[3]:
def main(communicator):
    transcript.log("Let's setup additive sharing!")
    encoding = FixedPoint(precision=2)
    protocol = AdditiveProtocolSuite(communicator, order=127, encoding=encoding)
    transcript.log("Let's share some secrets!")
    a_share = protocol.share(src=0, secret=numpy.array(2), shape=())
    b_share = protocol.share(src=1, secret=numpy.array(3), shape=())
    transcript.log("Let's add some secrets!")
    c_share = protocol.add(a_share, b_share)
    transcript.log("Let's reveal the results!")
    c = protocol.reveal(c_share)

with transcript.record():
    handler = transcript.net_handler()
    transcript.set_handler(logging.getLogger(), handler)
    SocketCommunicator.run(fn=main, world_size=3);
Player 0: Let's setup additive sharing!
Player 2: Let's setup additive sharing!
Player 1: Let's setup additive sharing!
Player 0: --> 1 PRZS 4612915327354008253
Player 1: --> 2 PRZS 7414186504898461884
Player 2: --> 0 PRZS 1882202436348525527
Player 1: <-- 0 PRZS 4612915327354008253
Player 2: <-- 1 PRZS 7414186504898461884
Player 0: <-- 2 PRZS 1882202436348525527
Player 1: Let's share some secrets!
Player 2: Let's share some secrets!
Player 0: Let's share some secrets!
Player 1: Let's add some secrets!
Player 2: Let's add some secrets!
Player 0: Let's add some secrets!
Player 1: Let's reveal the results!
Player 2: Let's reveal the results!
Player 0: Let's reveal the results!
Player 1: --> 0 GATHER 6
Player 2: --> 0 GATHER 75
Player 0: --> 0 GATHER 66
Player 1: --> 1 GATHER 6
Player 2: --> 1 GATHER 75
Player 0: <-- 1 GATHER 6
Player 2: --> 2 GATHER 75
Player 1: <-- 1 GATHER 6
Player 0: <-- 2 GATHER 75
Player 2: <-- 2 GATHER 75
Player 1: <-- 2 GATHER 75
Player 0: <-- 0 GATHER 66
Player 0: --> 1 GATHER 66
Player 0: --> 2 GATHER 66
Player 1: <-- 0 GATHER 66
Player 2: <-- 0 GATHER 66
Player 1: --> 2 GATHER 6
Player 2: <-- 1 GATHER 6

Now, we see the context messages originating directly from our script, interleaved with the transcribed network traffic. Context messages logged by the application should be chosen to provide information about what the program is doing at a high level, which will make it much easier to understand the rest of the transcript output.

By the way …

If you examine the message transcript closely, you may be surprised to see that our program doesn’t send any messages while generating secret shares. Does that surprise you? It’s because Cicada uses a cool trick - pseudorandom zero sharing - to generate additive secret shares without communication. Similarly, you can see that communication isn’t needed to add secrets, when using additive secret sharing.

If you want to customize the transcript, you can override the network message format string when configuring the handler, using a rich set of fields including net.arrow (message direction, relative to the local rank), net.comm.name (communicator name), net.dir (message direction, relative to the local rank), net.dst (message destination), net.other (the other player sending or receiving with the local rank), net.payload (message payload), net.comm.rank (local player), net.src (message source), net.tag (message type), and net.verb (“sent” or “received”, depending on whether the local player is sending or receiving).

Note in the above example that the rank of the player logging the event is always on the left, whether they are the sender or the receiver. With the arrows indicating which direction the message is travelling, this is intuitive for a person to understand, but it can complicate programmatically parsing the transcript. As an alternative, you could use some of the alternative fields to unconditionally put the sender and recipient in fixed positions, regardless of which player is logging the event. As an example, let’s try formatting our network messages as CSV for easier ingestion in some hypothetical analysis workflow (note that we also override the context message formatting so the context messages are valid CSV, too):

[4]:
with transcript.record():
    fmt = "# {processName}: {msg}"
    netfmt = "{processName},{net.verb},{net.src},{net.dst},{net.tag},{net.payload}"
    handler = transcript.net_handler(fmt=fmt, netfmt=netfmt)
    transcript.set_handler(logging.getLogger(), handler)
    SocketCommunicator.run(fn=main, world_size=3);
# Player 0: Let's setup additive sharing!
# Player 1: Let's setup additive sharing!
# Player 2: Let's setup additive sharing!
Player 0,sent,0,1,PRZS,6501041567845331950
Player 2,sent,2,0,PRZS,6064374732292475082
Player 1,sent,1,2,PRZS,3210179197484905169
Player 1,received,0,1,PRZS,6501041567845331950
Player 0,received,2,0,PRZS,6064374732292475082
Player 2,received,1,2,PRZS,3210179197484905169
# Player 1: Let's share some secrets!
# Player 0: Let's share some secrets!
# Player 2: Let's share some secrets!
# Player 1: Let's add some secrets!
# Player 2: Let's add some secrets!
# Player 0: Let's add some secrets!
# Player 1: Let's reveal the results!
# Player 2: Let's reveal the results!
# Player 0: Let's reveal the results!
Player 1,sent,1,0,GATHER,95
Player 2,sent,2,0,GATHER,12
Player 0,sent,0,0,GATHER,40
Player 1,sent,1,1,GATHER,95
Player 2,sent,2,1,GATHER,12
Player 0,received,1,0,GATHER,95
Player 1,received,1,1,GATHER,95
Player 0,received,2,0,GATHER,12
Player 2,sent,2,2,GATHER,12
Player 1,received,2,1,GATHER,12
Player 2,received,2,2,GATHER,12
Player 0,received,0,0,GATHER,40
Player 0,sent,0,1,GATHER,40
Player 0,sent,0,2,GATHER,40
Player 1,received,0,1,GATHER,40
Player 2,received,0,2,GATHER,40
Player 1,sent,1,2,GATHER,95
Player 2,received,1,2,GATHER,95

If you look carefully, you can see that this produces two nearly identical events for each message (once when the message is sent, and once when the message is received). If you wish to eliminate the duplication, e.g. by only logging messages when they’re sent, you can specify that too:

[5]:
with transcript.record():
    fmt = "# {processName}: {msg}"
    netfmt = "{processName},{net.verb},{net.src},{net.dst},{net.tag},{net.payload}"
    handler = transcript.net_handler(received=False, fmt=fmt, netfmt=netfmt)
    transcript.set_handler(logging.getLogger(), handler)
    SocketCommunicator.run(fn=main, world_size=3);
# Player 0: Let's setup additive sharing!
# Player 1: Let's setup additive sharing!
# Player 2: Let's setup additive sharing!
Player 0,sent,0,1,PRZS,4923441390019161378
Player 2,sent,2,0,PRZS,3792132382029304949
Player 1,sent,1,2,PRZS,8009281797746407522
# Player 0: Let's share some secrets!
# Player 1: Let's share some secrets!
# Player 2: Let's share some secrets!
# Player 0: Let's add some secrets!
# Player 2: Let's add some secrets!
# Player 1: Let's add some secrets!
# Player 0: Let's reveal the results!
# Player 2: Let's reveal the results!
# Player 1: Let's reveal the results!
Player 0,sent,0,0,GATHER,99
Player 2,sent,2,0,GATHER,64
Player 1,sent,1,0,GATHER,111
Player 2,sent,2,1,GATHER,64
Player 1,sent,1,1,GATHER,111
Player 0,sent,0,1,GATHER,99
Player 2,sent,2,2,GATHER,64
Player 0,sent,0,2,GATHER,99
Player 1,sent,1,2,GATHER,111

Now the transcript only contains sent messages. Of course, you could use sent=False instead, if you prefer to only see messages when they’re received.

Consistency Verification

In addition to logging application context and network message events, Cicada’s transcription framework can generate a set of Python statements that summarize the computation being performed in a way that supports consistency verification, along with forms of zero-knowledge proof such as MPC-in-the-head [1].

To get started, let’s switch to a much simpler computation, one that only requires a single player and no communication:

[6]:
from cicada.arithmetic import Field

with transcript.record():
    handler = transcript.code_handler()
    transcript.set_handler(logging.getLogger(), handler)

    f = Field(order=127)
    a = f.ones(3)
    b = f.uniform(size=3, generator=numpy.random.default_rng())
    f.inplace_add(a, b)
cicada.transcript.assert_equal(cicada.arithmetic.Field(order=127).ones(shape=3), numpy.array([1, 1, 1], dtype='object'))

bg = numpy.random.PCG64()
bg.state = {'bit_generator': 'PCG64', 'state': {'state': 310581584614395710612860853258973098112, 'inc': 87454387631014545490430812487103061493}, 'has_uint32': 0, 'uinteger': 0}
cicada.transcript.assert_equal(cicada.arithmetic.Field(order=127).uniform(size=3, generator=numpy.random.Generator(bg)), numpy.array([24, 47, 119], dtype='object'))

lhs = numpy.array([1, 1, 1], dtype='object')
cicada.arithmetic.Field(order=127).inplace_add(lhs=lhs, rhs=numpy.array([24, 47, 119], dtype='object'))
cicada.transcript.assert_equal(lhs, numpy.array([25, 48, 120], dtype='object'))

Note that we’ve switched our logging setup from net_handler to code_handler, to make the code output visible.

It’s important to understand that the consistency verification output isn’t simply a copy of the code being executed; rather, it’s a set of derived statements that can be executed to verify that the computation that was performed is logically consistent. In particular, there is not a one-to-one relationship between lines of original source code and lines of verification code. For example, the first line of the computation:

f = Field(order=127)

in the original source doesn’t generate any verification output at all, because the verification code is designed to be (relatively) stateless. Instead, the next line:

a = f.ones(3)

generates a single verification statement:

cicada.transcript.assert_equal(cicada.arithmetic.Field(order=127).ones(shape=3), numpy.array([1, 1, 1], dtype=object))

… which uses cicada.transcript.assert_equal() to test that the left- and right-hand expressions are equal. Note that the expressions have been expanded and “flattened” so that variable assignments are replaced with temporary objects, and all arguments are explicitly specified. If you used the Python interpreter to exec() this entire expression, it would return without error, since the expression:

cicada.arithmetic.Field(order=127).ones(shape=3)

does, in-fact, produce an array of three ones:

numpy.array([1, 1, 1], dtype=object)

Let’s look at the next line in the program:

b = f.uniform(size=3, generator=numpy.random.default_rng())

This code is slightly tricky, since we’re creating a random number generator using numpy’s default random initialization, which will produce different results every time it is run. In this case, we get three lines of verification code (if you run this code yourself, the output will be different due to the random initialization):

bg = numpy.random.PCG64()
bg.state = {'bit_generator': 'PCG64', 'state': {'state': ..., 'inc': ...}, 'has_uint32': 0, 'uinteger': 0}
cicada.transcript.assert_equal(cicada.arithmetic.Field(order=127).uniform(size=3, generator=numpy.random.Generator(bg)), numpy.array([...], dtype=object))

The first two lines of code are used to carefully initialize a random number generator with state identical to that when the original code was run, so that it will produce the same output. Again, if you ran these three lines using exec(), they would return without error, because the the expression:

cicada.arithmetic.Field(order=127).uniform(size=3, generator=numpy.random.Generator(bg))

initialized with the correct state, should produce a numpy array with the given values as output.

Finally, the original statement:

f.inplace_add(a, b)

also produces three lines of consistency ouput:

lhs = numpy.array([1, 1, 1], dtype=object)
cicada.arithmetic.Field(order=127).inplace_add(lhs=lhs, rhs=numpy.array([59, 60, 55], dtype=object))
cicada.transcript.assert_equal(lhs, numpy.array([...], dtype=object))

… where the additional lines are necessary to work around the fact that inplace_add() doesn’t have a return value, since it modifies its input in-place.

Let’s confirm that we can execute the consistency verification code without error. We’ll buffer the transcript to a string instead of writing it to the console, so we can execute it. To do this, you can supply your own choice of handler to code_handler() or net_handler() to replace the default:

[7]:
import io

with transcript.record():
    buffer = io.StringIO()
    handler = transcript.code_handler(logging.StreamHandler(buffer))
    transcript.set_handler(logging.getLogger(), handler)

    f = Field(order=127)
    a = f.ones(3)
    b = f.uniform(size=3, generator=numpy.random.default_rng())
    f.inplace_add(a, b)

Let’s inspect the buffered transcript, to ensure that it contains what we expect:

Danger

Executing code from other parties (even Cicada) is a serious security risk! Always carefully review a transcript before executing it!

[8]:
buffer.seek(0)
for line in buffer:
    print(line.strip())
cicada.transcript.assert_equal(cicada.arithmetic.Field(order=127).ones(shape=3), numpy.array([1, 1, 1], dtype='object'))

bg = numpy.random.PCG64()
bg.state = {'bit_generator': 'PCG64', 'state': {'state': 294111206801531245155334061750484527776, 'inc': 44482354329879700285959243340315682281}, 'has_uint32': 0, 'uinteger': 0}
cicada.transcript.assert_equal(cicada.arithmetic.Field(order=127).uniform(size=3, generator=numpy.random.Generator(bg)), numpy.array([42, 25, 0], dtype='object'))

lhs = numpy.array([1, 1, 1], dtype='object')
cicada.arithmetic.Field(order=127).inplace_add(lhs=lhs, rhs=numpy.array([42, 25, 0], dtype='object'))
cicada.transcript.assert_equal(lhs, numpy.array([43, 26, 1], dtype='object'))

Manual inspection shows that the transcript contains the expected output. Now, we can execute the buffered transcript, one-line-at-a-time, and confirm that none of the assertions raise exceptions:

[9]:
import cicada.transcript

try:
    buffer.seek(0)
    for line in buffer:
        exec(line)
except Exception as e:
    print(f"Consistency verification failed: {e}")
else:
    print("Consistency verification succeeded.")
Consistency verification succeeded.

Let’s go back to our original example. Because it requires three players, we’ll setup Cicada to write a separate transcript for each player:

[10]:
def main(comm):
    handler = transcript.code_handler(logging.FileHandler(f"player-{comm.rank}.py", "w"))
    transcript.set_handler(logging.getLogger(), handler)

    transcript.log("Let's setup additive sharing!")
    encoding = FixedPoint(precision=2)
    protocol = AdditiveProtocolSuite(comm, order=127, encoding=encoding)
    transcript.log("Let's share some secrets!")
    a_share = protocol.share(src=0, secret=numpy.array(2), shape=())
    b_share = protocol.share(src=1, secret=numpy.array(3), shape=())
    transcript.log("Let's add some secrets!")
    c_share = protocol.add(a_share, b_share)
    transcript.log("Let's reveal the results!")
    c = protocol.reveal(c_share)

with transcript.record():
    SocketCommunicator.run(fn=main, world_size=3);

Note that we’ve moved our transcript configuration code inside main, which allows us to generate a separate filename for each transcript based on player rank. Also note that we’ve left our context messages in-place. Let’s see what the results look like for one of the players:

[11]:
with open("player-0.py") as stream:
    for line in stream:
        print(line.strip())
# Let's setup additive sharing!
# Let's share some secrets!
# cicada.additive.AdditiveProtocolSuite().share(src=0, secret=numpy.array(2, dtype='int64'), shape=(), encoding=None)

bg = numpy.random.PCG64()
bg.state = {'bit_generator': 'PCG64', 'state': {'state': 275934374581578202693231533907928662192, 'inc': 74303973817490204264452453842712767295}, 'has_uint32': 0, 'uinteger': 0}
cicada.transcript.assert_equal(cicada.arithmetic.Field(order=127).uniform(size=(), generator=numpy.random.Generator(bg)), numpy.array(62, dtype='object'))

bg = numpy.random.PCG64()
bg.state = {'bit_generator': 'PCG64', 'state': {'state': 45413300204548699694289800305975195387, 'inc': 122458647806018768901565485422780565793}, 'has_uint32': 0, 'uinteger': 0}
cicada.transcript.assert_equal(cicada.arithmetic.Field(order=127).uniform(size=(), generator=numpy.random.Generator(bg)), numpy.array(117, dtype='object'))

lhs = numpy.array(62, dtype='object')
cicada.arithmetic.Field(order=127).inplace_subtract(lhs=lhs, rhs=numpy.array(117, dtype='object'))
cicada.transcript.assert_equal(lhs, numpy.array(72, dtype='object'))

cicada.transcript.assert_equal(cicada.arithmetic.Field(order=127).__call__(object=numpy.array(8, dtype='object')), numpy.array(8, dtype='object'))

cicada.transcript.assert_equal(cicada.encoding.FixedPoint(precision=2).encode(array=numpy.array(2, dtype='int64'), field=cicada.arithmetic.Field(order=127)), numpy.array(8, dtype='object'))

lhs = numpy.array(72, dtype='object')
cicada.arithmetic.Field(order=127).inplace_add(lhs=lhs, rhs=numpy.array(8, dtype='object'))
cicada.transcript.assert_equal(lhs, numpy.array(80, dtype='object'))

# cicada.additive.AdditiveProtocolSuite().share(src=1, secret=numpy.array(3, dtype='int64'), shape=(), encoding=None)

bg = numpy.random.PCG64()
bg.state = {'bit_generator': 'PCG64', 'state': {'state': 286236090826420737711002355196760144559, 'inc': 74303973817490204264452453842712767295}, 'has_uint32': 1, 'uinteger': 1242373202}
cicada.transcript.assert_equal(cicada.arithmetic.Field(order=127).uniform(size=(), generator=numpy.random.Generator(bg)), numpy.array(82, dtype='object'))

bg = numpy.random.PCG64()
bg.state = {'bit_generator': 'PCG64', 'state': {'state': 66772109718043022979323393862998920392, 'inc': 122458647806018768901565485422780565793}, 'has_uint32': 1, 'uinteger': 123532106}
cicada.transcript.assert_equal(cicada.arithmetic.Field(order=127).uniform(size=(), generator=numpy.random.Generator(bg)), numpy.array(74, dtype='object'))

lhs = numpy.array(82, dtype='object')
cicada.arithmetic.Field(order=127).inplace_subtract(lhs=lhs, rhs=numpy.array(74, dtype='object'))
cicada.transcript.assert_equal(lhs, numpy.array(8, dtype='object'))

# Let's add some secrets!
# cicada.additive.AdditiveProtocolSuite().add(lhs=cicada.additive.AdditiveArrayShare(storage=80), rhs=cicada.additive.AdditiveArrayShare(storage=8), encoding=None)

# cicada.additive.AdditiveProtocolSuite().field_add(lhs=cicada.additive.AdditiveArrayShare(storage=80), rhs=cicada.additive.AdditiveArrayShare(storage=8))

cicada.transcript.assert_equal(cicada.arithmetic.Field(order=127).add(lhs=numpy.array(80, dtype='object'), rhs=numpy.array(8, dtype='object')), numpy.array(88, dtype='object'))

# Let's reveal the results!
# cicada.additive.AdditiveProtocolSuite().reveal(share=cicada.additive.AdditiveArrayShare(storage=88), dst=None, encoding=None)

lhs = numpy.array(88, dtype='object')
cicada.arithmetic.Field(order=127).inplace_add(lhs=lhs, rhs=numpy.array(0, dtype='object'))
cicada.transcript.assert_equal(lhs, numpy.array(88, dtype='object'))

lhs = numpy.array(88, dtype='object')
cicada.arithmetic.Field(order=127).inplace_add(lhs=lhs, rhs=numpy.array(59, dtype='object'))
cicada.transcript.assert_equal(lhs, numpy.array(20, dtype='object'))

cicada.transcript.assert_equal(cicada.encoding.FixedPoint(precision=2).decode(array=numpy.array(20, dtype='object'), field=cicada.arithmetic.Field(order=127)), numpy.array(5.0, dtype='float64'))

As you can see, the context messages are incorporated into the transcript as comments. Because we’re transcribing a full-fledged Cicada program, there are additional nuances to be aware of. First, you will notice that there are comments besides those that we explicitly generate from our code, including:

# cicada.additive.AdditiveProtocolSuite().share(src=0, secret=numpy.array(2, dtype='int64'), shape=(), encoding=None)

and:

# cicada.additive.AdditiveProtocolSuite().reveal(share=cicada.additive.AdditiveArrayShare(storage=91), dst=None, encoding=None)

You might wonder why these high-level statements are converted to comments instead of executable code. This is because consistency verification checks for these operations would require a full-fledged multi-player environment with networking, communicators, and-so-on. Instead, the transcript contains all of the low-level field arithmetic operations that underlie these operations, while the commented code provides context for why those particular field operations are being performed.

Note

We expect the consistency verification code output from Cicada to grow and evolve over time as we continue to develop this new capability. In particular, the current set of output is defined using whitelists that will likely change with experience. There are no guarantees that the generated code will not change from one version of the library to the next, only that it should continue to run without raising exceptions.

Now, let’s run all three transcripts to verify their consistency:

[12]:
import traceback

for rank in range(3):
    try:
        with open(f"player-{rank}.py") as stream:
            for line in stream:
                exec(line)
    except Exception as e:
        print(f"Player {rank} consistency verification failed: {e}")
        print(traceback.format_exc())
    else:
        print(f"Player {rank} consistency verification succeeded.")
Player 0 consistency verification succeeded.
Player 1 consistency verification succeeded.
Player 2 consistency verification succeeded.

So, that worked perfectly. Let’s consider some other options you can use when working with code outputs. First, you can choose to have network messages interleaved with the code, using sent=True and received=True, which behave similarly to what we saw earlier with net_handler(). Let’s try turning them on. We’ll also go back to sending the transcript directly to the screen, but this time we’ll use a NullHandler to disable the output for every player except player 0:

[13]:
def main(comm):
    handler = logging.StreamHandler() if comm.rank == 0 else logging.NullHandler()
    handler = transcript.code_handler(handler, sent=True, received=True)
    transcript.set_handler(logging.getLogger(), handler)

    transcript.log("Let's setup additive sharing!")
    encoding = FixedPoint(precision=2)
    protocol = AdditiveProtocolSuite(comm, order=127, encoding=encoding)
    transcript.log("Let's share some secrets!")
    a_share = protocol.share(src=0, secret=numpy.array(2), shape=())
    b_share = protocol.share(src=1, secret=numpy.array(3), shape=())
    transcript.log("Let's add some secrets!")
    c_share = protocol.add(a_share, b_share)
    transcript.log("Let's reveal the results!")
    c = protocol.reveal(c_share)

with transcript.record():
    SocketCommunicator.run(fn=main, world_size=3);
# Let's setup additive sharing!
# 0 <-- 2 PRZS 2484391363636162676
# 0 --> 1 PRZS 3705543425192515578
# 0 <-- 2 GATHER 11
# Let's share some secrets!
# cicada.additive.AdditiveProtocolSuite().share(src=0, secret=numpy.array(2, dtype='int64'), shape=(), encoding=None)

# 0 <-- 1 GATHER 95
bg = numpy.random.PCG64()
bg.state = {'bit_generator': 'PCG64', 'state': {'state': 70824561701714392247314131268118952641, 'inc': 248539783406888609260376891470122436953}, 'has_uint32': 0, 'uinteger': 0}
cicada.transcript.assert_equal(cicada.arithmetic.Field(order=127).uniform(size=(), generator=numpy.random.Generator(bg)), numpy.array(31, dtype='object'))

bg = numpy.random.PCG64()
bg.state = {'bit_generator': 'PCG64', 'state': {'state': 327741436583401028878067909547269207792, 'inc': 66951744883520492504918500539108410445}, 'has_uint32': 0, 'uinteger': 0}
cicada.transcript.assert_equal(cicada.arithmetic.Field(order=127).uniform(size=(), generator=numpy.random.Generator(bg)), numpy.array(43, dtype='object'))

lhs = numpy.array(31, dtype='object')
cicada.arithmetic.Field(order=127).inplace_subtract(lhs=lhs, rhs=numpy.array(43, dtype='object'))
cicada.transcript.assert_equal(lhs, numpy.array(115, dtype='object'))

cicada.transcript.assert_equal(cicada.arithmetic.Field(order=127).__call__(object=numpy.array(8, dtype='object')), numpy.array(8, dtype='object'))

cicada.transcript.assert_equal(cicada.encoding.FixedPoint(precision=2).encode(array=numpy.array(2, dtype='int64'), field=cicada.arithmetic.Field(order=127)), numpy.array(8, dtype='object'))

lhs = numpy.array(115, dtype='object')
cicada.arithmetic.Field(order=127).inplace_add(lhs=lhs, rhs=numpy.array(8, dtype='object'))
cicada.transcript.assert_equal(lhs, numpy.array(123, dtype='object'))

# cicada.additive.AdditiveProtocolSuite().share(src=1, secret=numpy.array(3, dtype='int64'), shape=(), encoding=None)

bg = numpy.random.PCG64()
bg.state = {'bit_generator': 'PCG64', 'state': {'state': 33710239833072880162431526170503648606, 'inc': 248539783406888609260376891470122436953}, 'has_uint32': 1, 'uinteger': 1396278564}
cicada.transcript.assert_equal(cicada.arithmetic.Field(order=127).uniform(size=(), generator=numpy.random.Generator(bg)), numpy.array(36, dtype='object'))

bg = numpy.random.PCG64()
bg.state = {'bit_generator': 'PCG64', 'state': {'state': 93867515633347282979830946528566031101, 'inc': 66951744883520492504918500539108410445}, 'has_uint32': 1, 'uinteger': 1422560118}
cicada.transcript.assert_equal(cicada.arithmetic.Field(order=127).uniform(size=(), generator=numpy.random.Generator(bg)), numpy.array(118, dtype='object'))

lhs = numpy.array(36, dtype='object')
cicada.arithmetic.Field(order=127).inplace_subtract(lhs=lhs, rhs=numpy.array(118, dtype='object'))
cicada.transcript.assert_equal(lhs, numpy.array(45, dtype='object'))

# Let's add some secrets!
# cicada.additive.AdditiveProtocolSuite().add(lhs=cicada.additive.AdditiveArrayShare(storage=123), rhs=cicada.additive.AdditiveArrayShare(storage=45), encoding=None)

# cicada.additive.AdditiveProtocolSuite().field_add(lhs=cicada.additive.AdditiveArrayShare(storage=123), rhs=cicada.additive.AdditiveArrayShare(storage=45))

cicada.transcript.assert_equal(cicada.arithmetic.Field(order=127).add(lhs=numpy.array(123, dtype='object'), rhs=numpy.array(45, dtype='object')), numpy.array(41, dtype='object'))

# Let's reveal the results!
# cicada.additive.AdditiveProtocolSuite().reveal(share=cicada.additive.AdditiveArrayShare(storage=41), dst=None, encoding=None)

# 0 --> 0 GATHER 41
# 0 <-- 0 GATHER 41
lhs = numpy.array(41, dtype='object')
cicada.arithmetic.Field(order=127).inplace_add(lhs=lhs, rhs=numpy.array(95, dtype='object'))
cicada.transcript.assert_equal(lhs, numpy.array(9, dtype='object'))

lhs = numpy.array(9, dtype='object')
cicada.arithmetic.Field(order=127).inplace_add(lhs=lhs, rhs=numpy.array(11, dtype='object'))
cicada.transcript.assert_equal(lhs, numpy.array(20, dtype='object'))

# 0 --> 1 GATHER 41
# 0 --> 2 GATHER 41
cicada.transcript.assert_equal(cicada.encoding.FixedPoint(precision=2).decode(array=numpy.array(20, dtype='object'), field=cicada.arithmetic.Field(order=127)), numpy.array(5.0, dtype='float64'))

Now we can see the network messages, formatted as Python comments just like context messsages, so they won’t interfere with executing the consistency verification code.

Important

You should understand that messages from other players arrive when they arrive, without regard to what the local player is doing at the moment. It is not at all unusual to see messages associated with a given operation arriving from another player long before the current player has reached that point in the code. This happens when the other player is “running ahead” of the current player due to quirks of process sceduling and network timing, and is completely normal. Welcome to distributed computation!

You can configure the context and network message formatting using the same fmt and netfmt parameters covered earlier; if you do, it’ll be up to you to include the “#” in your format strings, to maintain the executability of the consistency verification output.

See also

Logging

Footnotes