Skip to content

Commit

Permalink
python: Add blame mode (identifiable aborts)
Browse files Browse the repository at this point in the history
  • Loading branch information
real-or-random committed Oct 25, 2024
1 parent f9ba0ac commit 917e28d
Show file tree
Hide file tree
Showing 6 changed files with 244 additions and 25 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,7 @@ Perform a participant's second step of a ChillDKG session.
*Raises*:

- `SecKeyError` - If the length of `hostseckey` is not 32 bytes.
FIXME
- `FaultyParticipantError` - If `cmsg1` is invalid. This can happen if
another participant has sent an invalid message to the coordinator,
or if the coordinator has sent an invalid `cmsg1`.
Expand Down Expand Up @@ -834,6 +835,14 @@ of the success of the DKG session by presenting recovery data to us.
- `SessionNotFinalizedError` - If finalizing the DKG session was not
successful from this participant's perspective (see above).

#### participant\_blame

```python
def participant_blame(hostseckey: bytes, state1: ParticipantState1, cmsg1: CoordinatorMsg1, blame_rec: encpedpop.BlameRecord) -> Tuple[DKGOutput, RecoveryData]
```

Perform a participant's blame step of a ChillDKG session. TODO

#### coordinator\_step1

```python
Expand Down Expand Up @@ -896,6 +905,14 @@ Perform the coordinator's final step of a ChillDKG session.
received messages from other participants via a communication
channel beside the coordinator (or be malicious).

#### coordinator\_blame

```python
def coordinator_blame(pmsgs: List[ParticipantMsg1]) -> List[encpedpop.BlameRecord]
```

Perform the coordinator's blame step of a ChillDKG session. TODO

#### recover

```python
Expand Down
37 changes: 31 additions & 6 deletions python/chilldkg_ref/chilldkg.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ def params_id(params: SessionParams) -> bytes:
OverflowError: If `t >= 2^32` (so `t` cannot be serialized in 4 bytes).
"""
params_validate(params)
(hostpubkeys, t) = params
hostpubkeys, t = params

t_bytes = t.to_bytes(4, byteorder="big") # OverflowError if t >= 2**32
params_id = tagged_hash_bip_dkg(
Expand Down Expand Up @@ -448,6 +448,7 @@ def participant_step2(
Raises:
SecKeyError: If the length of `hostseckey` is not 32 bytes.
FIXME
FaultyParticipantError: If `cmsg1` is invalid. This can happen if
another participant has sent an invalid message to the coordinator,
or if the coordinator has sent an invalid `cmsg1`.
Expand All @@ -460,7 +461,7 @@ def participant_step2(
as a consequence, the caller should not conclude that the party
hinted at is malicious.
"""
(params, idx, enc_state) = state1
params, idx, enc_state = state1
enc_cmsg, enc_secshares = cmsg1

enc_dkg_output, eq_input = encpedpop.participant_step2(
Expand Down Expand Up @@ -514,11 +515,28 @@ def participant_finalize(
SessionNotFinalizedError: If finalizing the DKG session was not
successful from this participant's perspective (see above).
"""
(params, eq_input, dkg_output) = state2
params, eq_input, dkg_output = state2
certeq_verify(params.hostpubkeys, eq_input, cmsg2.cert) # SessionNotFinalizedError
return dkg_output, RecoveryData(eq_input + cmsg2.cert)


def participant_blame(
hostseckey: bytes,
state1: ParticipantState1,
cmsg1: CoordinatorMsg1,
blame_rec: encpedpop.BlameRecord,
) -> Tuple[DKGOutput, RecoveryData]:
"""Perform a participant's blame step of a ChillDKG session. TODO"""
_, idx, enc_state = state1
return encpedpop.participant_blame(
state=enc_state,
deckey=hostseckey,
cmsg=cmsg1.enc_cmsg,
enc_secshare=cmsg1.enc_secshares[idx],
blame_rec=blame_rec,
)


###
### Coordinator
###
Expand Down Expand Up @@ -553,10 +571,12 @@ def coordinator_step1(
OverflowError: If `t >= 2^32` (so `t` cannot be serialized in 4 bytes).
"""
params_validate(params)
(hostpubkeys, t) = params
hostpubkeys, t = params

enc_cmsg, enc_dkg_output, eq_input, enc_secshares = encpedpop.coordinator_step(
pmsgs=[pmsg1.enc_pmsg for pmsg1 in pmsgs1], t=t, enckeys=hostpubkeys
pmsgs=[pmsg1.enc_pmsg for pmsg1 in pmsgs1],
t=t,
enckeys=hostpubkeys,
)
eq_input += b"".join([bytes_from_int(int(share)) for share in enc_secshares])
dkg_output = DKGOutput._make(enc_dkg_output) # Convert to chilldkg.DKGOutput type
Expand Down Expand Up @@ -589,12 +609,17 @@ def coordinator_finalize(
received messages from other participants via a communication
channel beside the coordinator (or be malicious).
"""
(params, eq_input, dkg_output) = state
params, eq_input, dkg_output = state
cert = certeq_coordinator_step([pmsg2.sig for pmsg2 in pmsgs2])
certeq_verify(params.hostpubkeys, eq_input, cert) # SessionNotFinalizedError
return CoordinatorMsg2(cert), dkg_output, RecoveryData(eq_input + cert)


def coordinator_blame(pmsgs: List[ParticipantMsg1]) -> List[encpedpop.BlameRecord]:
"""Perform the coordinator's blame step of a ChillDKG session. TODO"""
return encpedpop.coordinator_blame([pmsg.enc_pmsg for pmsg in pmsgs])


###
### Recovery
###
Expand Down
70 changes: 65 additions & 5 deletions python/chilldkg_ref/encpedpop.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Tuple, List, NamedTuple
from typing import Tuple, List, NamedTuple, NoReturn

from secp256k1proto.secp256k1 import Scalar
from secp256k1proto.secp256k1 import Scalar, GE
from secp256k1proto.ecdh import ecdh_libsecp256k1
from secp256k1proto.keys import pubkey_gen_plain
from secp256k1proto.util import int_from_bytes
Expand Down Expand Up @@ -145,6 +145,11 @@ class CoordinatorMsg(NamedTuple):
pubnonces: List[bytes]


class BlameRecord(NamedTuple):
enc_partial_secshares: List[Scalar]
partial_pubshares: List[GE]


###
### Participant
###
Expand Down Expand Up @@ -221,13 +226,52 @@ def participant_step2(
secshare = decrypt_sum(
deckey, enckeys[idx], pubnonces, enc_context, idx, enc_secshare
)

dkg_output, eq_input = simplpedpop.participant_step2(
simpl_state, simpl_cmsg, secshare
)
eq_input += b"".join(enckeys) + b"".join(pubnonces)
return dkg_output, eq_input


def participant_blame(
state: ParticipantState,
deckey: bytes,
cmsg: CoordinatorMsg,
enc_secshare: Scalar,
blame_rec: BlameRecord,
) -> NoReturn:
simpl_state, _, enckeys, idx = state
_, pubnonces = cmsg
enc_partial_secshares, partial_pubshares = blame_rec

# Compute the encryption pads once and use them to decrypt both the
# enc_secshare and all enc_partial_secshares
enc_context = serialize_enc_context(simpl_state.t, enckeys)
pads = decaps_multi(deckey, enckeys[idx], pubnonces, enc_context, idx)
secshare = enc_secshare - Scalar.sum(*pads)
partial_secshares = [
enc_partial_secshare - pad
for enc_partial_secshare, pad in zip(enc_partial_secshares, pads, strict=True)
]

simpl_blame_rec = simplpedpop.BlameRecord(partial_pubshares)
try:
simplpedpop.participant_blame(
simpl_state, secshare, partial_secshares, simpl_blame_rec
)
except simplpedpop.SecshareSumError as e:
# The secshare is not equal to the sum of the partial secshares in the
# blame records. Since the encryption is additively homomorphic, this
# can only happen if the sum of the *encrypted* secshare is not equal
# to the sum of the encrypted partial sechares, which is the
# coordinator's fault.
assert Scalar.sum(*enc_partial_secshares) != enc_secshare
raise FaultyCoordinatorError(
"Sum of encrypted partial secshares not equal to encrypted secshare"
) from e


###
### Coordinator
###
Expand All @@ -241,9 +285,9 @@ def coordinator_step(
n = len(enckeys)
if n != len(pmsgs):
raise ValueError
simpl_cmsg, dkg_output, eq_input = simplpedpop.coordinator_step(
[pmsg.simpl_pmsg for pmsg in pmsgs], t, n
)

simpl_pmsgs = [pmsg.simpl_pmsg for pmsg in pmsgs]
simpl_cmsg, dkg_output, eq_input = simplpedpop.coordinator_step(simpl_pmsgs, t, n)
pubnonces = [pmsg.pubnonce for pmsg in pmsgs]
for i in range(n):
if len(pmsgs[i].enc_shares) != n:
Expand All @@ -254,6 +298,7 @@ def coordinator_step(
Scalar.sum(*([pmsg.enc_shares[i] for pmsg in pmsgs])) for i in range(n)
]
eq_input += b"".join(enckeys) + b"".join(pubnonces)

# In ChillDKG, the coordinator needs to broadcast the entire enc_secshares
# array to all participants. But in pure EncPedPop, the coordinator needs to
# send to each participant i only their entry enc_secshares[i].
Expand All @@ -269,3 +314,18 @@ def coordinator_step(
eq_input,
enc_secshares,
)


def coordinator_blame(pmsgs: List[ParticipantMsg]) -> List[BlameRecord]:
n = len(pmsgs)
simpl_pmsgs = [pmsg.simpl_pmsg for pmsg in pmsgs]

all_enc_partial_secshares = [
[pmsg.enc_shares[i] for pmsg in pmsgs] for i in range(n)
]
simpl_blame_recs = simplpedpop.coordinator_blame(simpl_pmsgs)
blame_recs = [
BlameRecord(all_enc_partial_secshares[i], simpl_blame_recs[i].partial_pubshares)
for i in range(n)
]
return blame_recs
73 changes: 68 additions & 5 deletions python/chilldkg_ref/simplpedpop.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from secrets import token_bytes as random_bytes
from typing import List, NamedTuple, NewType, Tuple, Optional
from typing import List, NamedTuple, NewType, Tuple, Optional, NoReturn

from secp256k1proto.bip340 import schnorr_sign, schnorr_verify
from secp256k1proto.secp256k1 import GE, Scalar
Expand All @@ -14,6 +14,15 @@
from .vss import VSS, VSSCommitment


###
### Exceptions
###


class SecshareSumError(ValueError):
pass


###
### Proofs of possession (pops)
###
Expand Down Expand Up @@ -63,6 +72,10 @@ def to_bytes(self) -> bytes:
) + b"".join(self.pops)


class BlameRecord(NamedTuple):
partial_pubshares: List[GE]


###
### Other common definitions
###
Expand Down Expand Up @@ -145,7 +158,7 @@ def participant_step1(
def participant_step2_prepare_secshare(
partial_secshares: List[Scalar],
) -> Scalar:
secshare = Scalar.sum(*partial_secshares)
secshare: Scalar = Scalar.sum(*partial_secshares)
return secshare


Expand Down Expand Up @@ -182,10 +195,14 @@ def participant_step2(
)
sum_coms = assemble_sum_coms(coms_to_secrets, sum_coms_to_nonconst_terms, n)
threshold_pubkey = sum_coms.commitment_to_secret()
pubshares = [sum_coms.pubshare(i) for i in range(n)]
if not VSSCommitment.verify_secshare(secshare, pubshares[idx]):
raise UnknownFaultyPartyError("Received invalid secshare")
pubshare = sum_coms.pubshare(idx)

if not VSSCommitment.verify_secshare(secshare, pubshare):
raise UnknownFaultyPartyError(
"Received invalid secshare, consider blaming to determine faulty party",
)

pubshares = [sum_coms.pubshare(i) if i != idx else pubshare for i in range(n)]
dkg_output = DKGOutput(
secshare.to_bytes(),
threshold_pubkey.to_bytes_compressed(),
Expand All @@ -195,6 +212,45 @@ def participant_step2(
return dkg_output, eq_input


def participant_blame(
state: ParticipantState,
secshare: Scalar,
partial_secshares: List[Scalar],
blame_rec: BlameRecord,
) -> NoReturn:
_, n, idx, _ = state
partial_pubshares = blame_rec.partial_pubshares

if Scalar.sum(*partial_secshares) != secshare:
raise SecshareSumError("Sum of partial secshares not equal to secshare")

for i in range(n):
if not VSSCommitment.verify_secshare(
partial_secshares[i], partial_pubshares[i]
):
if i != idx:
raise FaultyParticipantError(
i, "Participant sent invalid partial secshare"
)
else:
# We are not faulty, so the coordinator must be.
raise FaultyCoordinatorError(
"Coordinator fiddled with the share from me to myself"
)

# We now know:
# - The sum of the partial secshares is equal to the secshare.
# - Every partial secshare matches its corresponding partial pubshare.
# - The sum of the partial pubshares is not equal to the pubshare (because
# the caller shouldn't have called us otherwise).
# Therefore, the sum of the partial pubshares is not equal to the pubshare,
# and this is the coordinator's fault.
raise FaultyCoordinatorError(
"Sum of partial pubshares not equal to pubshare (or participant_blame() "
"was called even though participant_step2() was successful)"
)


###
### Coordinator
###
Expand All @@ -221,10 +277,17 @@ def coordinator_step(
sum_coms = assemble_sum_coms(coms_to_secrets, sum_coms_to_nonconst_terms, n)
threshold_pubkey = sum_coms.commitment_to_secret()
pubshares = [sum_coms.pubshare(i) for i in range(n)]

dkg_output = DKGOutput(
None,
threshold_pubkey.to_bytes_compressed(),
[pubshare.to_bytes_compressed() for pubshare in pubshares],
)
eq_input = t.to_bytes(4, byteorder="big") + sum_coms.to_bytes()
return cmsg, dkg_output, eq_input


def coordinator_blame(pmsgs: List[ParticipantMsg]) -> List[BlameRecord]:
n = len(pmsgs)
all_partial_pubshares = [[pmsg.com.pubshare(i) for pmsg in pmsgs] for i in range(n)]
return [BlameRecord(all_partial_pubshares[i]) for i in range(n)]
Loading

0 comments on commit 917e28d

Please sign in to comment.