GHZ Based Anonymous Entanglement

In this example, we will demonstrate how to simulate an instance of GHZ-based quantum anonymous entanglement. The goal of the protocol is to hide the creation of an entangled pair between two parties. This protocol implementation demonstrates the simplicity of translating a protocol from a high-level mathematical syntax into simulation using QuNetSim. The protocol involves establishing GHZ states amongst \(n\) parties as well as broadcasting measurement outcomes.

First we develop the the behaviour of the GHZ distributor, which in this example is node \(A\). QuNetSim provides a function for distributing GHZ states so the function distribute simply takes the distributing host as the first parameter and the list of receiving nodes as the second. One notices that the flag distribute has been set to true in the send_ghz method call. This tells the sending host that it should not keep part of the GHZ state, rather, it should generate a GHZ state amongst the list given and send it to the parties in the node list, keeping no part of the state for itself.

1
2
3
def distribute(host, nodes):
    # distribute=True => sender doesn't keep part of the GHZ
    host.send_ghz(nodes, distribute=True)

The next type of behaviour we would like to simulate is that of a node in the group that is not attempting to establish an EPR pair. In the anonymous entanglement protocol, the behaviour of such a node is to simply receive a piece of a GHZ state, perform a Hadamard operation on the received qubit, measure it, and broadcast to the remaining participating parties the outcome of a measurement in the computational basis. Below we see how to accomplish this. In the node function below, the first parameter is, as always, the host that is performing the protocol. The second is the ID of the node distribution the GHZ state, which in this example, is host \(A\). The host fetches its GHZ state where, if it is not available at the time, they will wait ten seconds for it, accomplished by setting the wait parameter. If they have not received part of the GHZ state, then the protocol has failed, otherwise they simply perform the Hadamard operation on the received qubit, measures it, and broadcasts the message to the network. QuNetSim includes the task of broadcasting as a built-in function and therefore the task of sending classical messages to the whole network is not only simplified, but the host performing the network does not need to know the entire network to send a broadcast.

1
2
3
4
5
6
7
8
def node(host, distributor):
    q = host.get_ghz(distributor, wait=10)
    if q is None:
        print('failed')
        return
    q.H()
    m = q.measure()
    host.send_broadcast(str(m))

We next implement the behaviour of the party in the protocol acting as one end of the EPR link that we label the sender. Here we take three parameters (other than the host running the protocol), the ID of the distributor, the receiver that is the holder of the other half of the EPR pair, and an agreed upon ID for the EPR pair that will be generated. In QuNetSim, qubits have IDs for easier synchronization between parties. For EPR pairs and GHZ states, qubits share and ID, that is, the collection of qubits would all have the same ID. This is done so that when parties share many EPR pairs, they can easily synchronize their joint operations. The sender protocol is the following: first they receive part of a GHZ state, they select a random bit and then broadcast the message so that they appear as just any other node. They then manipulate their part of their part of the GHZ state according to what the random bit was. If the bit was \(1\), then a \(Z\) gate is applied. The sending party can then add the qubit as an EPR pair shared with the receiver. This EPR pair can then be used as if the sender and receiver established an EPR directly.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def sender(host, distributor, r, epr_id):
    q = host.get_ghz(distributor, wait=10)
    b = random.choice(['0', '1'])
    host.send_broadcast(b)
    if b == '1':
        q.Z()

    host.add_epr(r, q, q_id=epr_id)
    sending_qubit = Qubit(host)
    sending_qubit.X()
    print('Sending %s' % sending_qubit.id)
    # Generate EPR if none shouldn't change anything, but if there is
    # no shared entanglement between s and r, then there should
    # be a mistake in the protocol
    host.send_teleport(r, sending_qubit, generate_epr_if_none=False, await_ack=False)
    host.empty_classical()

Finally, we establish the behaviour of the receiver. The receiver here behaves as follows: First, in order to mask their behaviour they randomly choose a bit and broadcast it to the network. Once complete, they await the remainder of the broadcast messages. In QuNetSim, classical messages are stored as a list in the classical field. Since there are 3 other parties, other than the receiver themself, they await the other three messages. Once they arrive, the receiver computes a global parity operation by taking the \(XOR\) of all received bits along with their own random choice. With this, the receiver can apply a controlled \(Z\) gate which establishes the EPR pair with the correct sender. They simply add the EPR pair and complete the protocol.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def receiver(host, distributor, s, epr_id):
    q = host.get_ghz(distributor, wait=10)
    b = random.choice(['0', '1'])
    host.send_broadcast(b)

    messages = []
    # Await broadcast messages from all parties
    while len(messages) < 3:
        messages = host.classical
        messages = [x for x in messages if x.content != 'ACK']
        time.sleep(0.5)

    print([m.content for m in messages])
    parity = int(b)
    for m in messages:
        if m.sender != s:
            parity = parity ^ int(m.content)

    if parity == 1:
        q.Z()

    print('established secret EPR')
    host.add_epr(s, q, q_id=epr_id)
    q = host.get_qubit(s, wait=10)
    host.empty_classical()
    print('Received qubit %s in the %d state' % (q.id, q.measure()))

Full example:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
import time

from qunetsim.components import Host
from qunetsim.components import Network
import random

from qunetsim.objects import Qubit


def distribute(host, nodes):
    # distribute=True => sender doesn't keep part of the GHZ
    host.send_ghz(nodes, distribute=True)


def sender(host, distributor, r, epr_id):
    q = host.get_ghz(distributor, wait=10)
    b = random.choice(['0', '1'])
    host.send_broadcast(b)
    if b == '1':
        q.Z()

    host.add_epr(r, q, q_id=epr_id)
    sending_qubit = Qubit(host)
    sending_qubit.X()
    print('Sending %s' % sending_qubit.id)
    # Generate EPR if none shouldn't change anything, but if there is
    # no shared entanglement between s and r, then there should
    # be a mistake in the protocol
    host.send_teleport(r, sending_qubit, generate_epr_if_none=False, await_ack=False)
    host.empty_classical()


def receiver(host, distributor, s, epr_id):
    q = host.get_ghz(distributor, wait=10)
    b = random.choice(['0', '1'])
    host.send_broadcast(b)

    messages = []
    # Await broadcast messages from all parties
    while len(messages) < 3:
        messages = host.classical
        messages = [x for x in messages if x.content != 'ACK']
        time.sleep(0.5)

    print([m.content for m in messages])
    parity = int(b)
    for m in messages:
        if m.sender != s:
            parity = parity ^ int(m.content)

    if parity == 1:
        q.Z()

    print('established secret EPR')
    host.add_epr(s, q, q_id=epr_id)
    q = host.get_qubit(s, wait=10)
    host.empty_classical()
    print('Received qubit %s in the %d state' % (q.id, q.measure()))


def node(host, distributor):
    q = host.get_ghz(distributor, wait=10)
    if q is None:
        print('failed')
        return
    q.H()
    m = q.measure()
    host.send_broadcast(str(m))


def main():
    network = Network.get_instance()
    nodes = ['A', 'B', 'C', 'D', 'E']
    network.start(nodes)

    host_A = Host('A')
    host_A.add_connections(['B', 'C', 'D', 'E'])
    host_A.start()
    host_B = Host('B')
    host_B.add_c_connections(['C', 'D', 'E'])
    host_B.start()
    host_C = Host('C')
    host_C.add_c_connections(['B', 'D', 'E'])
    host_C.start()
    host_D = Host('D')
    host_D.add_c_connections(['B', 'C', 'E'])
    host_D.start()
    host_E = Host('E')
    host_E.add_c_connections(['B', 'C', 'D'])
    host_E.start()

    network.add_hosts([host_A, host_B, host_C, host_D, host_E])

    # Run the protocol 10 times
    for i in range(10):
        # The ID of the generated secret EPR pair has to be agreed upon in advance
        epr_id = '123'
        host_A.run_protocol(distribute, ([host_B.host_id, host_C.host_id, host_D.host_id, host_E.host_id],))
        host_B.run_protocol(node, (host_A.host_id,))
        host_C.run_protocol(node, (host_A.host_id,))
        host_D.run_protocol(sender, (host_A.host_id, host_E.host_id, epr_id))
        host_E.run_protocol(receiver, (host_A.host_id, host_D.host_id, epr_id), blocking=True)
        time.sleep(0.5)
    network.stop(True)


if __name__ == '__main__':
    main()