Semi-Trusted Party (STP) Distributed Key Generation (DKG)

This document specifies a proposal for a robust DKG that can work for
small deployments with a small number of parties and infrequent DKG
executions. Robust means that the protocol even succeeds if some
parties cheat and this is detected, but no party aborts. If someone
aborts then the protocol needs to run again, possibly after kicking
out misbehaving parties. This protocol does support maximum 127
peers. This is probably already too much for a non-robust protocol,
but it might work in very special circumstances.

Broadcast is implemented by the semi-trusted party (STP) opening a
channel to each peer secured by the peers long-term encryption
key. Every message - both broadcast and private - are routed through
the STP.

Peer long-term encryption keys can be either TLS-based, or
Noise_XK-based (https://noiseexplorer.com/patterns/XK/). In the latter
case the long-term public keys must be known and validated in advance
by the STP.

Peer long-term signature keys must be known by all participating
parties.

The basis for this protocol is based on the FT-Joint-DL-VSS (fig 7.)
protocol from [GRR98].

------<=[ Rationale                                     ]=>-----------

Traditionally DKGs are used in setting where all parties are equal and
are using the distributed key together, without having any one party
having a different role in the protocol utilizing the shared key. This
does not translate entirely to threshold OPRFs (tOPRF) and protocols
based on these.

In an OPRF there is normally two parties, one holding the key, and
another one holding the input and learning the output. In a tOPRF the
party holding the key is a group of peers that hold shares of the key
in a threshold setting. In a special case, that of updatable threshold
OPRFs the updating might be done by a semi-trusted 3rd party. In that
case the semi-trusted 3rd party is merely honest-but-curious, but
unable to learn anything about the input nor really the output of the
OPRF, while being able to update the key of the OPRF. This can be
handy for automated, regular key-updates. For updating the key, the
participants must generate a new key, and this can be orchestrated by
a STP acting as the broadcast and general communication medium between
the parties.

------<=[ Difference to the [GRR98] paper               ]=>-----------

In the original paper fig. 7 describing the FT-Joint-DL-VSS protocol,
does not explicitly check whether the 𝓗(α_i,ρ_i) commitments match
those broadcast. However the algorithm New-VSS from fig. 1 does
so. And since FT-Joint-DL-VSS is supposedly based on New-VSS we do
check the commitment, broadcast and check complaints and dealers of
verified complaints are disqualified.

P_i after generating C_ij and share s_ij,r_ij, broadcasts a hash of all
(concatenated) values C_i1,...,C_in.

After broadcasting C_ij and sharing s_ij,r_ij, before checking VSPS
all participants check that the hash value broadcast in Round 1 by
P_i, for i=1,...,n, corresponds to the C_ij values broadcast by P_i in
Round 2.  If this is not the case, P_j aborts.

------<=[ Prototocol Phases                             ]=>-----------

The protocol has the following phases:

  1. Initialization and introduction: announcement of parameters n, t,
     long-term signing public keys of all participants, their
     ordering, and encryption public keys, Establishment of a jointly
     generated session id.
  2. AKE - Setup secure P2P channels: to establish protected channels
     between all peers.
  3. core DKG
  4. Complaint analysis: In case of invalid commitments establishment
     of identity of cheaters.
  5. Final verification of transcript and completion of
     protocol

------<=[ Simplified API                                ]=>-----------

Since the protocol consists of many steps, the API is abstracted to
the following schema:

0. Initialize
While not done and not error:
  1. Allocate input buffer(s)
  2. input = receive()
  3. allocate output buffer
  4. run next step of protocol
  5. if there is output: send(output)
6. Post-processing

This simple schema simplifies the load of an implementer using this
protocol, while at the same time reducing opportunities for errors and
provides strict security. It also allows full abstraction of the
underlying communication media.

The reference implementation in stp-dkg.c follows this schema for both
the STP and the peers.

------<=[ Protocol transcript                           ]=>-----------

Transcript - all broadcast messages are accumulated into a transcript
by each peer and the semi-trusted party, at the end of the protocol
all parties except for the STP publish their signed transcripts and
only if all signatures are correct and the transcripts match, is the
protocol successful. The STP uses its own transcript to learn the
of the parties agreement.

The transcript is a hash, which is initialized with the string:
   "stp vss dkg session transcript"

in pseudo-code:

   transcript_state = hash_init("stp vss dkg session transcript")

Updating the transcript first updates the hash with the canonical
32bit size of the message to be added to the transcript, then the
message itself is added to the hash.

    transcript_state = hash_update(transcript_state, I2OSP(len(msg))
    transcript_state = hash_update(transcript_state, msg)

The signature of each message is similarly added to the transcript.

A function `update_ts` can be used as a high-level interface to
updating the transcript with messages and their signatures:

```
update_ts(state,msg,sig)
    state = hash_update(state, I2OSP(len(msg))
    state = hash_update(state, msg)
    state = hash_update(state, I2OSP(len(sig))
    state = hash_update(state, sig)
    return state
```

------<=[ Session id                                    ]=>-----------

Every execution of the protocol starts by the participants
establishing a unique and fresh session id, this is to ensure that no
messages can be replayed. The session id is a 256 bit (32B) random
value of cryptographic quality entropy.

Each peer P_i chooses a 256 bit nonce, signs it, broadcast it. Then
everyone (including the STP) verifies the signatures, aborts if any
signatures fail. And then everyone uses the hash of the concatenation
of all nonces as the session identifier.

The session_id is established as early as possible in the first
(initialization) phase of protocol. The STP learns (and starts using)
it in step 2, and the peers verify if it is correct and start using it
in step 3. Every message sent after step 3 MUST contain a valid
session_id.

```
nonce_i = random_bytes(32)
signed_nonce_i = sign(i | nonce, ltsigkey_i)
broadcast(signed_nonce_i)

acc = ""
for i in 1..n:
   signed_nonce_i = recv(i)
   i', nonce_i = verify(signed_nonce_i, ltsigpub_i) or abort()
   assert i == i'
   acc = acc | nonce_i

sessionid = h(acc)
```

------<=[ Message header                                ]=>-----------

All messages have a message header:

  uint8  signature[32]
  uint0  sign_here[0]
  uint8  type = 0x80
  uint8  version = 0
  uint8  messageno
  uint32 len
  uint8  from
  uint8  to
  uint64 timestamp
  uint8  sessionid[32]

The header begins with the signature of the sender over the rest of
the header and all of the data.

The field sign_here is a zero-bit field, only used for addressing the
start of the data to be signed or verified.

The next field is the protocol type identifier. STP-DKG has an
identifier value of 128 (0x80). And a version number of 0 for the current
version.

The following field in the header is really a state identifier. A
recipient MUST verify that the messageno is matching with the expected
number related to the state of the protocol.

The len field MUST be equal to the size of the packet received on the
network including the packet header.

The `from` field is simply the index of the peer, since peers are
indexed starting from 1, the value 0 is used for the semi-trusted
party. Any value greater than 128 is invalid. The state defines from
whom to receive messages, and thus the from field MUST be validated
against these expectations.

The `to` field is similar to the `from` field, with the difference
that the value 0xff is reserved for broadcast messages. The peer (or
STP) MUST validate that it is indeed the recipient of a given message.

The timestamp field is just a 64bit timestamp as seconds elapsed since
1970/01/01, for peers that have no accurate clock themselves but do
have an RTC, the first initiating message from the STP SHOULD be used
as a reference for synchronizing during the protocol.

------<=[ Message signatures                            ]=>-----------

Every message MUST be signed using the sender peers long-term signing
key. The signature is made over the complete message including the
header, excluding the signature field itself, starting from the
zero-width sign_here field.

------<=[ Verifying messages                            ]=>-----------

Whenever a message is received by any participant, they first MUST
check the correctness of the signature:

```
   msg = recv()
   sign_pk = sign_keys[expected_sender_id]
   assert(verify(sign_pk, msg.sign_her, msg.signature))
```

The recipient MUST also assert the correctness of all the other header
fields:

```
   assert(msg.type == 0x80)
   assert(msg.version == 0)
   assert(msg.messageno == expected_messageno)
   assert(msg.from == expected_sender_id)
   assert(msg.to == (own_peer_id or 0xff))
   assert(ref_ts <= msg.ts < ref_ts + timeout))
   ref_ts = msg.ts
```

The value `timeout` should be configurable and be set to the smallest
value that doesn't cause protocol aborts due to slow responses.

If at any step of the protocol any participant receives one or more
messages that fail these checks, the participant MUST abort the
protocol and log all violations and if possible alert the user.

------<=[ Message transmission                          ]=>-----------

A higher level message transmission interface can be provided, for
sending:

```
msg = send_msg(msgno, from, to, sign_sk, session_id, data)
    ts = timestamp()
    msg = type: 0x80, version: 0, messageno: msgno, len: len(header) + len(data) + len(sig), from: from, to: to, ts: ts, data
    msg.sig = sign(sign_sk, msg.sign_here)
    return msg
```

And for validating incoming messages:

```

data = recv_msg(msgno, from, to, ref_ts, sign_pk, session_id, msg)
   assert(verify(sign_pk, msg.sign_here, msg.sig)
   assert(msg.type == 0x80)
   assert(msg.version == 0)
   assert(msg.messageno == msgno)
   assert(msg.len == len(msg|sig))
   assert(msg.from == from)
   assert(msg.to == to)
   assert(ref_ts < msg.ts < ref_ts + timeout))

   if msg.to == 0xff:
       update_ts(state,msg,sig)
```

The parameters `msgno`, `from`, `to`, `session_id` should be the
values expected according to the current protocol state.

------<=[ Cheater detection                             ]=>-----------

The STP and the peers MUST report to the user all errors that can
identify cheating peers in any given step. For each detected cheating
peer the STP MUST record the following information:

 - the current protocol step,
 - the violating peer,
 - the other peer involved, and
 - the type of violation

In order to detect other misbehaving peers in the current step,
processing for the rest of the SHOULD peers continue until the end of
the current step. Any further violations should be recorded as above.

Before the next message to the peers is sent, the STP MUST check if
there are no noted hard violations, if so the STP aborts and reports
all violators with their parameters to the user. Soft violations -
corruptions robustly handled by the protocol - only need to be
reported, they do not cause an abort.

Abort conditions include any errors detected by recv_msg(), failure to
verify the hash of the commitments, or when the number of complaints
is more than t for one peer, or more than t^2 in total. Soft
violations are failures of dealers VSPS checks and shares not matching
their commitments.

Participants should log all broadcast interactions, so that any
post-mortem investigation can identify cheaters.

------<=[ Second generator point                        ]=>-----------

For the homomorphic commitment this protocol requires a second
generator on the group. We generate it in the following manner:

  h = voprf_hash_to_group(blake2b("nothing up my sleeve number"))

Where voprf_hash_to_groups is according to [RFC9497].

------<=[ The protocol                                  ]=>-----------

------<=[ 0. Precondition                               ]=>-----------

Peers use TLS or STP knows long-term encryption keys for all peers.

STP and peers MUST know long-term signing keys of all peers.

------<=[ 1. DKG Announcement - STP(peers, t, proto_name) ]=>----------

The protocol starts by asking the semi-trusted party (STP) to initiate
a new run of the DKG protocol by providing it with:

  - a list of the peers,
  - a threshold value, and
  - protocol instance name used as a domain separation token.

The STP then sanity checks these parameters:

```
n = len(peers)
assert(1<t<n)
assert(len(proto_name)>0)
```

The STP then generates a hash of the DST.

The STP then creates a broadcast message containing the hash (so that
the message is always of fixed size) of the DST, the values n and t
and its own public signing key:

```
dst_str = "STP VSS DKG for protocol %s" % proto_name
dst = hash(I2OSP(len(dst_str)) | dst_str | n | t)
sessionid = random_bytes(32)
data = {stp_sign_pk, dst, n, t}
msg_0 = send_msg(0, 0, 0xff, stp_sign_sk, sessionid, data)
broadcast(msg_0)
```

Note that the STP also generates a temporary session id, which is used
until the parties agree on a joint session id.

The STPs copy of the transcript is initialized by the STP, and updated
with the value of the 1st broadcast message:

```
state = hash_init("stp vss dkg session transcript")
state = update_ts(state, msg, sig)
```

Since the order of the peers is random, and important for the protocol
a custom message is created for each peer by the STP and sent
individually notifying each peer of their index in this protocol
run. The msg.to field conveys the index of the peer. Additionally the
hashes of the long-term signing public keys of the other peers are
also sent along so that each of the peers can load the corresponding
long-term signing and noise public keys.

```
# sending each peer its index
pkhashes = ""
for i in 1..n:
  pkhashes = pkhashes | hash(ltsigpk[i])

for i in 1..n:
  msg_1 = send_msg(1, 0, i, stp_sign_sk, session_id, {pkhashes})
  send(i, msg_1)
```

------<=[ 2. each peer(msg_0)                          ]=>------------

In this step each peer receives the initial parameter broadcast,
verifies it, initializes the transcript and adds the initial
message. Then receives the message assigning its index.

```
msg_0 = recv()
assert(recv_msg(0, 0, 0xff, ref_ts, msg.data.stp_sign_pk, msg.sessionid, msg_0))
sessionid = msg.sessionid
```

If the peer has no accurate internal clock but has at least an RTC, it
SHOULD set the ref_ts to the message timestamp:

```
ref_ts = msg_0.ts
```

Furthermore the peer MUST also verify that the n & t parameters are
sane, and if possible the peer SHOULD also check if the temporary
STP-assigned session id is fresh (if it is not possible, isfresh() MAY
always return true.

```
assert(1 < n <= 128)
assert(2 <= msg_0.t < n)
assert(isfresh(msg_0,sessionid))
```

The transcript MUST be initialized by the peer, and updated with the
value of the 1st broadcast message:

```
state = hash_init("stp vss dkg session transcript")
state = update_ts(state, msg, sig)
```

After processing the broadcast message from the STP, the peers also
have to process the second message from the STP in which they are
assigned their index.

```
msg1 = recv()
peerids = recv_msg(1, 0, msg1.to, ref_ts, stp_sign_pk, session_id, msg_1)
assert(msg1.to <= n and msg1.to > 0)
peerid = msg.to
peers_noise_pks = []
peers_sign_pks = []
for i in 1..n
   peers_sign_pks[i], peers_noise_pks[i] = keyloader(peerids[i])
```

------<=[ 3. peers broadcast fresh session nonce        ]=>-------------

All peers broadcast generate a fresh session nonce for use in the
session_id to all peers via the STP.

```
nonce_i = random_bytes(32)

msg_2 = send_msg(2, peerid, 0xff, peer_sign_sk, session_id, nonce_i)
broadcast(msg_2)
```

------<=[ 4. STP collects and broadcasts messages     ]=>-------------

Then the STP acts as a broadcast medium on the messages.

This is a recurring pattern where the STP acts in its broadcasting
intermediary role:

  1. receives the messages from each peer
  2. validates the message using recv_msg()
  3. extracts all nonces (or other information depending on the
     current step) for usage by the STP in the rest of the protocol
  4. concatenates all received messages into a new message
  5. signs the message of messages
  6. adds this the message of messages and its signature to the transcript
  7. sends it to all peers

In this case the STP calculates the session id, which is the hash of
the concatenation of all nonces in order of their sending peers index
(with the STP always having index 0). The STP already uses the joint
session_id to wrap all the peers messages into its broadcast envelope.

```
peer_sig_pks = []
msgs = []
nonces = nonce_stp
for i in 1..N
   msg_2 = recv()
   nonce_i, = recv_msg(2, i, 0xff, ref_ts, msg_2.data.peer_sign_pk, nonce_i, msg_2)
   msgs = msgs | msg_2
   nonces = nonces | nonce_i

session_id = hash(nonces)
msg_3 = send_msg(3, 0, 0xff, stp_sign_sk, session_id, msgs)

state = update_ts(state, msg_3)

broadcast(msg_3)
```

------<=[ 5. finish init phase, start AKE phase         ]=>-------

In this step all peers process the broadcast nonces received from all
peers, finishing the initial phase.

Every peer also verifies if the joint session_id matches the one in
the STP broadcast envelope. From now on, every participant has this
joint session id and uses it for all further messages.

This step also marks the start of the next phase. In this AKE phase
each peer initiates a noise_xk handshake with all other peers
(including themselves for simplicity and thus security).

```
msg_3 = recv()
msgs = recv_msg(3, 0, 0xff, ref_ts, stp_sign_pk, msg_3.session_id, msg_3)

state = update_ts(state, msg_3)

nonces = []
for i in 1..N
   msg, sig = msgs[i]
   nonce_i, = recv_msg(2, i, 0xff, ref_ts, peer_sign_pks[i], session_id, msg, sig)
   nonces = nonces | nonce_i

session_id = hash(nonces)
assert(msg_3.session_id == session_id)
send_session = []
for i in 1..N
   send_session[i], handshake1 = noisexk_initiator_session(peer_noise_sk, peers_noise_pks[i])
   msg, sig = send_msg(4,peerid,i,peer_sign_sk, session_id, handshake1)
   send(msg | sig)
```

------<=[ 6. STP routes handshakes from each peer to each peer ]=>-------

The STP receives all 1st handshake messages from all peers and routes
them correctly to their destination. These messages are not broadcast,
each of them is an encrypted P2P message. The benefit of the STP
forming a star topology here is, that the peers can be on very
different physical networks (wifi, lora, uart, nfc, bluetooth, usb,
etc) and only the STP needs to be able to connect to all of them.

```
for i in 1..N
   handshakes = recv(i)
   for j in 1..N
       send(j, handshakes[j])
```

------<=[ 7. each peer responds to each handshake each peer ]=>-------

Peer receives noise handshake1 from each peer and responds with
handshake2 answer to each peer.

```
for i in 1..N
   msg, sig = recv()
   handshake1 = recv_msg(4, i, peerid, ref_ts, peers_sign_pks[i], session_id, msg, sig)
   receive_session[i], handshake2 = noisexk_responder_session(peer_noise_sk, handshake1)
   msg, sig = send_msg(5, peerid, i, peer_sign_sk, session_id, handshake2)
   send(msg | sig)
```

------<=[ 8. STP routes handshakes from each peer to each peer ]=>-------

STP just routes all P2P messages from all peers to the correct
recipients of the messages.

```
for i in 1..N
   handshakes = recv(i)
   for j in 1..N
       send(j, handshakes[j])
```

------<=[ 9. end of AKE phase, start of core DKG phase  ]=>-------

Peers complete the noise handshake.

```
for i in 1..N
   msg, sig = recv()
   handshake3 = recv_msg(5, i, peerid, ref_ts, peers_sign_pks[i], session_id, msg, sig)
   send_session[i] = noisexk_initiator_session_complete(send_session[i], handshake3)
```

Each peer has a confidential connection with every peer (including
self, for simplicity)

The one time this channel is used, is when distributing the dealer
shares. The sender uses the initiator interface of the noise session,
and the receiver uses the responder interface.

This step starts the core FT-Joint-DL-VSS protocol as per [GRR98]
fig. 7:

Player P_i chooses a random value r_i and shares it using the DL-VSS
protocol, Denote by α_i,j , ρ_i,j the share player P_i gives to player
P_j, and the value 𝓐_i,j = g^(α_i,j)*h^(ρ_i,j) is the dealers
commitment to share α_i,j , ρ_i,j.

The Noise handshake is finalized by encrypting one last empty message,
which sets the final shared secret. This is needed in case later
someone accuses this peer with their shares not matching their
commitment, in which case the peer can reveal this final encryption
key and prove everyone that the accuser was lying or not.

The shares α_i,j ,ρ_i,j are encrypted using the final Noise key for
the recipient, and a key-committing HMAC is also calculated over the
encrypted shares, since the Noise implementation we use does use
Poly1305 which is not key-committing and thus would allow the dealer
to cheat.

The encrypted shares and their commitments 𝓐_i,j are stored by the
peer for later distribution.

The HMAC for each encrypted share is broadcast together with
the hash of the concatenation of all A_i,j commitments:

      C_i = hash(A_i0 | A_i1 | .. | A_in)

```
encrypted_shares = []
hmacs = []
for i in 1..N
   encrypted_shares[i] = noise_send(send_session[i], share[i])
   hmacs[i] = hmac(send_session[i].key, encrypted_shares[i])

C_i = hash(commitments)
msg_6 = send_msg(6, peerid, 0xff, peer_sign_sk, session_id, {C_i, hmacs})
```

------<=[ 10. STP collects and broadcasts all C_i commitments ]=>-------

This is another broadcast pattern instance:
receive-verify-collect-sign-transcript-broadcast. The STP keeps a copy
of all commitment-hashes and share-HMACs being broadcast.

The STP keeps a local copy of all commitment hashes and HMACs for
later verification.

```
C_hashes = []
hmacs = []
msgs = []
for i in 1..N
   msg_6 = recv(i)
   C_hashes[i], hmacs[i] = recv_msg(6, i, 0xff, ref_ts, peer_sign_pks[i], session_id, msg_6)
   msgs = msgs | msg_6

msg_7 = send_msg(7, 0, 0xff, stp_sign_sk, session_id, msgs)

state = update_ts(state, msg_7)

broadcast(msg_7)
```

------<=[ 11. Peers receive commitment hashes & HMACs   ]=>-------

The peers receive all C_i commitment hashes and share-HMACs and
broadcast their A commitment vectors:

```
msg_7 = recv()
msgs = recv_msg(7, 0, 0xff, ref_ts, stp_sign_pk, session_id, msg_7)
state = update_ts(state, msg_7)

C_hashes = []
share_macs = []
for i in 1..N
   msg_6 = msgs[i]
   C_hashes[i], share_macs[i] = recv_msg(6, i, 0xff, ref_ts, peer_sign_pks[i], session_id, msg_6)
   msgs = msgs | msg_6

msg_8 = send_msg(8, peerid, 0xff, peer_sign_sk, session_id, A)
```

------<=[ 12. STP broadcasts all commitments            ]=>-------

This is a classical STP broadcast step. Besides keeping a copy of all
commitments, the STP does also verify the commitment hashes and does
an FT-VSPS check on the commitments.

The STP verifies the VSPS property of the sum of the shared secrets by
running VSPS-Check on 𝓐_i,..,𝓐_n where

           𝓐_j = Π 𝓐_i,j
                 i

If this check fails the STP runs VSPS-Checks on each individual
sharing. These checks are informational, and should guide the operator
in detecting and deterring cheaters.

```
commitments[][]
msgs = []
for i in 1..N
   msg_8 = recv(i)
   commitments[i] = recv_msg(8, i, 0xff, ref_ts, peer_sign_pks[i], session_id, msg_8)
   msgs = msgs | msg_6
   if C_hashes != hash(commitments[i])
      report(i)

C = []
for i in 1..n
   C[i] = sum(commitments[j][i] for j in 1..n)

if vsps(C) fails:
   for i..n
      if vsps(commitments[i]) fails report(i)

msg_9 = send_msg(9, 0, 0xff, stp_sign_sk, session_id, msgs)

state = update_ts(state, msg_9)

broadcast(msg_9)
```

------<=[ 13. Peers receive commitments, send shares    ]=>-------

The peers receive the broadcast commitments of all dealers, they check
the commitment hashes and abort if they don't match, otherwise they
store the commitments for the next step.

Peers privately send the shares to each recipient.

```
msg_9 = recv()
msgs = recv_msg(9, 0, 0xff, ref_ts, stp_sign_pk, session_id, msg_9)
state = update_ts(state, msg_9)

commitments = [][]
for i in 1..N
   msg_8 = msgs[i]
   commitments[i] = recv_msg(8, i, 0xff, ref_ts, peer_sign_pks[i], session_id, msg_8)
   assert C_hashes[i] == hash(commitments[i])

msg_10s = []
for i in 1..N
   msg, sig = send_msg(9, peerid, i, peer_sign_sk, session_id, encrypted_shares[i])
   send(msg,sig)

```

------<=[ 14. STP routes shares to recipients           ]=>-------

STP just routes all P2P messages from all peers to the correct
recipients of the messages.

```
for i in 1..N
   msgs = recv(i)
   for j in 1..N
       send(j, msgs[j])
```

------<=[ 15. each peer receives shares & check commitments   ]=>-------

The peers receive the private messages containing their shares. The
peers verify the shares against the previously broadcast commitment
vectors. For each
    𝓐_i,j == g^(α_i,j) * h^(ρ_i,j)
pair that fails, a complaint against the peer producing the
conflicting commitment and share is logged in an array, which is
broadcast to everyone.

```
s = []
for i in 1..N
   msg = recv()
   pkt = recv_msg(9, i, peerid, ref_ts, peer_sign_pks[i], session_id, msg)
   encrypted_share, final_noise_handshake = pkt
   recv_session[i] = noise_session_decrypt(recv_session[i], final_noise_handshake)
   α[i,peerid],ρ[i,peerid] = noise_recv(receive_session[i], pkt)
complaints = []
for i in 1..N
   if commitment[i,peerid] != g^(α[i,peerid])*h^(ρ[i,peerid])
      complaints = complaints | i

msg, sig = send_msg(10, peerid, 0xff, peer_sign_sk, session_id, len(complaints) | complaints)
send(msg | sig)

```

------<=[ 16. STP collects complaints                         ]=>-------

Another receive-verify-collect-sign-transcribe-broadcast
instantiation. The STP keeps a copy of all complaints.

If any peer complaints about more than t peers, that complaining peer
is a cheater, and must be disqualified. Furthermore if there are in
total more than t^2 complaints there are multiple cheaters and the
protocol must be aborted and new peers must be chosen in case a rerun
is initiated.

```
complaints = []
msgs = []
for i in 1..N
   msg_10 = recv(i)
   complaints_i = recv_msg(10, i, 0xff, ref_ts, peer_sign_pks[i], session_id, msg_10)
   assert(len(complaints_i) < t)
   complaints = complaints | complaints_i
   msgs = msgs | msg_10

assert(len(complaints) < t^2)

msg_11 = send_msg(11, 0, 0xff, stp_sign_sk, session_id, msgs)

state = update_ts(state, msg_11)

broadcast(msg_11)
```

The next phase of the protocol depends on the number of complaints
received, if none then the next phase is finishing, otherwise the next
phase is complaint analysis.

If the next STP phase is complaint analysis (there are complaints) the
next input buffer size depends on the number of complaints against
each peer.

Each complaint is answered by the encrypted shares and the symmetric
encryption key used to encrypt these shares of the accused belonging to
the complainer. Each accused packs all answers into one message.

------<=[ 17. Each peer receives all complaints               ]=>-------

All complaint messages broadcast are received by each peer. If peer_i
is being complained about by peer_j, peer_i broadcasts the encrypted
shares and the symmetric encryption key that was used to encrypt them.

If there are no complaints at all the peers skip over to the final
phase step 20., otherwise they engage in the complaint analysis phase.

```
msg_11 = recv()
msgs = recv_msg(11, 0, 0xff, ref_ts, stp_sign_pk, session_id, msg_11)
state = update_ts(state, msg_11)
defenses = []

for i in 1..N
   msg = msgs[i]
   complaints_len, complaints = recv_msg(10, i, 0xff, ref_ts, peers_sign_pks[i], session_id, msg)

   for k in 0..complaints_len
      if complaints[k] == peerid
          # complaint about current peer, publish key used to encrypt α_i,j , ρ_i,j
          defenses = defenses | {i, send_session[i].key, encrypted_shares[i]}

if len(keys) > 0
   msg_12 = send_msg(12, peer, 0xff, peer_sign_sk, session_id, keys)
   send(msg_12)
```

------<=[ 18. STP collects all defenses, verifies&broadcasts them ]=>-------

STP checks if all complaints lodged earlier are answered by the
correct encrypted shares and their keys, by first checking if the
previously recorded MAC successfully verifies the encrypted share with
the disclosed key, and then decrypts the share with this key, and
checks if this satisfies the previously recorded commitment for this
share. If it does, the accuser is reported as a cheater, if the
commitment doesn't match the share, then the accused dealer is
disqualified from the protocol and its shares will not contribute to
the final shared secret.

```
msgs = []
for i in 1..N
    if len(complaints[i]) < 1
        continue

    msg = recv(i)
    defenses = recv_msg(12, i, 0xff, ref_ts, peers_sign_pks[i], session_id, msg)
    msgs = msgs | msg
    assert(len(defenses) == len(complaints[i]))
    for j, key, encrypted_share in defenses
       assert j==i
       if hmacs[i][peerid] == hmac(key, encrypted_share)
           report(i)
       s,r=decrypt(key, encrypted_share]) or report(i)
       if commitments[i][peerid] != g^s * h^r
           report(i)

msg_13 = send_msg(13, 0, 0xff, stp_sign_sk, session_id, msgs)
state = update_ts(state, msg_13)
broadcast(msg_13)
```

------<=[ 19. Peers receive and check all defenses            ]=>-------

Peers receive the encrypted shares, and their encryption keys, and
then run essentially the same step as the STP in the previous step,
then they directly skip to the final phase in the next step.

------<=[ 20. Peers VSPS check, calculate shares and finish   ]=>-------

Players verify the VSPS property of the sum of the shared secrets by
running VSPS-Check on 𝓐_i,..,𝓐_n where

           𝓐_j = Π 𝓐_i,j
                 i

If this check fails the players run VSPS-Check on each individual
sharing. Any player that fails this check is disqualified. The number
of all qualified peers (from this step, and the complaint analysis) is
checked that is greater than 1 and then number of disqualified peers
is less than t. If this fails the protocol aborts.

The shares dealt by the qualified peers are summed, creating the final
share. The commitment for this final share is calculated.

Each peer calculates the final transcripts and broadcasts this
together with the final commitments to all parties.

```
C = []
for i in 1..n
   C[i] = sum(commitments[j][i] for j in 1..n if peer[i] is qualified)

if vsps(C) fails:
   for i in 1..n
      if vsps(commitments[i]) fails disqualify(i)

s = 0, r = 0
for i in 1..n
   if i is disqualfied: continue
   s += shares[i]_s
   r += shares[i]_r

C = g^s * h^r

transcript = final_ts(state)
msg_20 = send_msg(20, peerid, 0, peer_sign_sk, session_id, {transcript, C})
send(msg_20)
```

------<=[ 20. STP receives all and verifies transcripts        ]=>-------

STP receives all transcripts, and asserts that they all match its own
transcript, it reports if any transcript mismatch is detected. It also
does a final VSPS check on the commitments seen.

```
transcript = final_ts(state)

msgs = []
commitments = []
for i in 1..N
    msg, sig = recv(i)
    ts, c[i] = recv_msg(20, i, 0xff, ref_ts, peers_sign_pks[i], session_id, msg, sig)
    if ts != transcript
       report transcript mismatch
    msgs = msgs | {msg, sig}

if vsps(commitments) fails:
    report failure

------<=[ 21. END, peers set their share                ]=>-------

All peers receive the broadcasts transcripts and commitments, they run
the same check as the STP in the previous step and abort if any of
these fails. Otherwise the protocol completes successfully, and the
peers can store the final shares and commitment together with the list
of long-term public signing keys of all peers.

------<=[ References                                    ]=>-------

[GRR98] R. Gennaro, M. O. Rabin, and T. Rabin. "Simplified VSS and
fact-track multiparty computations with applications to threshold
cryptography" In B. A. Coan and Y. Afek, editors, 17th ACM PODC, pages
101–111. ACM, June / July 1998

[RFC9497] RFC 9497: Oblivious Pseudorandom Functions (OPRFs) Using
Prime-Order Groups
