From 2eb9a87c14006c1a261bb8720b3972e38c98b7f2 Mon Sep 17 00:00:00 2001 From: Tim Ruffing Date: Thu, 17 Oct 2024 15:45:06 +0200 Subject: [PATCH] tests: Test also with blame=True --- python/chilldkg_ref/encpedpop.py | 45 +++++++--- python/chilldkg_ref/simplpedpop.py | 127 +++++++++++++++++------------ python/example.py | 5 +- python/tests.py | 26 +++--- 4 files changed, 122 insertions(+), 81 deletions(-) diff --git a/python/chilldkg_ref/encpedpop.py b/python/chilldkg_ref/encpedpop.py index d375710..a510ebc 100644 --- a/python/chilldkg_ref/encpedpop.py +++ b/python/chilldkg_ref/encpedpop.py @@ -1,4 +1,4 @@ -from typing import Tuple, List, NamedTuple, Optional +from typing import Tuple, List, NamedTuple, Optional, cast from secp256k1proto.secp256k1 import Scalar, GE from secp256k1proto.ecdh import ecdh_libsecp256k1 @@ -209,7 +209,7 @@ def participant_step1( # case someone derives secnonce differently. simpl_seed = derive_simpl_seed(seed, pubnonce, enc_context) - simpl_state, simpl_pmsg, shares, _ = simplpedpop.participant_step1( + simpl_state, simpl_pmsg, shares = simplpedpop.participant_step1( simpl_seed, t, n, idx ) assert len(shares) == n @@ -261,9 +261,19 @@ def participant_step2( else: simpl_blame_rec = None - dkg_output, eq_input = simplpedpop.participant_step2( - simpl_state, simpl_cmsg, secshare, simpl_blame_rec - ) + try: + dkg_output, eq_input = simplpedpop.participant_step2( + simpl_state, simpl_cmsg, secshare, simpl_blame_rec + ) + except simplpedpop.InconsistentSecsharesError 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. + assert Scalar.sum(*enc_partial_secshares) != enc_secshare + raise FaultyCoordinatorError( + "Sum of encrypted partial secshares not equal to encrypted secshare" + ) from e eq_input += b"".join(enckeys) + b"".join(pubnonces) return dkg_output, eq_input @@ -288,8 +298,8 @@ 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_cmsg, dkg_output, eq_input, all_partial_pubshares = ( + simplpedpop.coordinator_step([pmsg.simpl_pmsg for pmsg in pmsgs], t, n, blame) ) pubnonces = [pmsg.pubnonce for pmsg in pmsgs] for i in range(n): @@ -300,17 +310,28 @@ def coordinator_step( enc_secshares = [ Scalar.sum(*([pmsg.enc_shares[i] for pmsg in pmsgs])) for i in range(n) ] + # enc_secshares[0] += Scalar(n) blame_recs: List[Optional[BlameRecord]] if blame: - enc_partial_secshares = [ + # We called simplpedpop.coordinator_step(..., blame=True), which is + # supposed to return proper partial_pubshares. + assert all( + [ + all_partial_pubshares[i][j] is not None + for i in range(n) + for j in range(n) + ] + ) + all_enc_partial_secshares = [ [pmsg.enc_shares[i] for pmsg in pmsgs] for i in range(n) ] - partial_pubshares = [ - [pmsg.simpl_pmsg.com.pubshare(i) for pmsg in pmsgs] for i in range(n) - ] + # for i in range(n): + # all_enc_partial_secshares[0][i] += Scalar(1) blame_recs = [ - BlameRecord(enc_partial_secshares[i], partial_pubshares[i]) + BlameRecord( + all_enc_partial_secshares[i], cast(List[GE], all_partial_pubshares[i]) + ) for i in range(n) ] else: diff --git a/python/chilldkg_ref/simplpedpop.py b/python/chilldkg_ref/simplpedpop.py index 28ef5ec..654fd72 100644 --- a/python/chilldkg_ref/simplpedpop.py +++ b/python/chilldkg_ref/simplpedpop.py @@ -1,5 +1,5 @@ from secrets import token_bytes as random_bytes -from typing import List, NamedTuple, NewType, Tuple, Optional, cast +from typing import List, NamedTuple, NewType, Tuple, Optional, NoReturn, cast from secp256k1proto.bip340 import schnorr_sign, schnorr_verify from secp256k1proto.secp256k1 import GE, Scalar @@ -13,6 +13,15 @@ from .vss import VSS, VSSCommitment +### +### Exceptions +### + + +class InconsistentSecsharesError(ValueError): + pass + + ### ### Proofs of possession (pops) ### @@ -131,18 +140,15 @@ def participant_step1( msg = ParticipantMsg(com, pop) state = ParticipantState(t, n, idx, com_to_secret) - partial_pubshares_from_me: List[Optional[GE]] - if blame: - partial_pubshares_from_me = [com.pubshare(i) for i in range(n)] - else: - partial_pubshares_from_me = [None for i in range(n)] - - return state, msg, partial_secshares_from_me, partial_pubshares_from_me + return state, msg, partial_secshares_from_me # Helper function to prepare the secret side inputs for participant idx's -# participant_step2() from the partial_secshares returned by all participants' -# participant_step1(). +# participant_step2() from +# - the list of all partial_secshares[idx] values from participants' +# participant_step1(), and # FIXME terms are wrong here +# - the partial_pubshares list from the coordinator's coordinator_step() +# (if not blaming, this is a list containing n times None). # # This computation cannot be done entirely by the SimplPedPop coordinator # because it involves secret shares. In a pure run of SimplPedPop where secret @@ -153,10 +159,21 @@ def participant_step1( # take care of this preparation by exploiting the homomorphic property of the # encryption. def participant_step2_prepare_secret_side_inputs( - partial_secshares: List[Scalar], -) -> Scalar: + partial_secshares: List[Scalar], partial_pubshares: List[Optional[GE]] +) -> Tuple[Scalar, Optional[BlameRecord]]: + ## FIXME take n from state, amend other commit + n = len(partial_secshares) secshare = Scalar.sum(*partial_secshares) - return secshare + if not len(partial_secshares) == len(partial_pubshares) == n: + raise ValueError + # blame_rec: Optional[BlameRecord] + if partial_pubshares[0] is not None: + if not all([p is not None for p in partial_pubshares]): + raise ValueError + blame_rec = BlameRecord(partial_secshares, cast(List[GE], partial_pubshares)) + else: + blame_rec = None + return secshare, blame_rec def participant_step2( @@ -196,44 +213,12 @@ def participant_step2( pubshares = [sum_coms.pubshare(i) for i in range(n)] if not VSSCommitment.verify_secshare(secshare, pubshares[idx]): - if blame_rec is None: - raise FaultyParticipantError(None, "Received invalid secshare") + if blame_rec is not None: + _participant_step2_blame(secshare, pubshares, idx, blame_rec) else: - # TODO Extract function - partial_secshares, partial_pubshares = blame_rec - - # TODO Shoud we include these checks? They're superficial but diligent: - # Alternatively, solve the redudancy by sending only n-1 - # FIXME Reconsider this check. That's not a faulty coordinator, the - # coordinator is involved the secshares in simplpedpop. Perhaps - # we can move this check to encpedpop (and perform it on the - # encrypted shares). Or additional raise something else here. - # How can this even fail in a run of pure simplpedpop? I think only - # if the prepare function was wrong. - if Scalar.sum(*partial_secshares) != secshare: - raise FaultyCoordinatorError( - "Sum of partial secshares not equal to secshare" - ) - # FIXME Similar, but this can fail if either the coordinator was - # wrong, or the prepare function. - if GE.sum(*partial_pubshares) != pubshares[idx]: - raise FaultyCoordinatorError( - "Sum of partial pubshares not equal to pubshare" - ) - 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 it must be the coordinator. - raise FaultyCoordinatorError( - "Would blame myself" # TODO better message - ) - assert False + raise FaultyParticipantError( + None, "Received invalid secshare, consider rerunning in blame mode" + ) dkg_output = DKGOutput( secshare.to_bytes(), @@ -244,14 +229,44 @@ def participant_step2( return dkg_output, eq_input +def _participant_step2_blame( + secshare: Scalar, pubshares: List[GE], idx: int, blame_rec: BlameRecord +) -> NoReturn: + partial_secshares, partial_pubshares = blame_rec + n = len(pubshares) + if Scalar.sum(*partial_secshares) != secshare: + raise InconsistentSecsharesError + # The following check can safely be omitted, because we trust the + # coordinator for computing the partial_pubshares correctly anyway. Or, in + # other words, the coordinator can anyway make us blame some innocent + # participant. We keep it because it may help debugging benign failures. + if GE.sum(*partial_pubshares) != pubshares[idx]: + raise FaultyCoordinatorError("Sum of partial pubshares not equal to pubshare") + 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 it must be the coordinator. + raise FaultyCoordinatorError( + "Coordinator fiddled with the share from me to myself" + ) + assert False, "unreachable" + + ### ### Coordinator ### +## TODO document the last return value def coordinator_step( - pmsgs: List[ParticipantMsg], t: int, n: int -) -> Tuple[CoordinatorMsg, DKGOutput, bytes]: + pmsgs: List[ParticipantMsg], t: int, n: int, blame: bool = True +) -> Tuple[CoordinatorMsg, DKGOutput, bytes, List[List[Optional[GE]]]]: # Sum the commitments to the i-th coefficients for i > 0 # FIXME # # This procedure is introduced by Pedersen in Section 5.1 of @@ -273,10 +288,16 @@ def coordinator_step( threshold_pubkey = sum_coms.commitment_to_secret() pubshares = [sum_coms.pubshare(i) for i in range(n)] + partial_pubshares: List[List[Optional[GE]]] + if blame: + partial_pubshares = [[pmsg.com.pubshare(i) for pmsg in pmsgs] for i in range(n)] + else: + partial_pubshares = [[None for pmsg in pmsgs] 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 + return cmsg, dkg_output, eq_input, partial_pubshares diff --git a/python/example.py b/python/example.py index b3640e2..ed3726a 100755 --- a/python/example.py +++ b/python/example.py @@ -123,8 +123,9 @@ async def coordinator( # -def simulate_chilldkg_full(hostseckeys, t) -> List[Tuple[DKGOutput, RecoveryData]]: - blame = True # TODO Test also blame=False +def simulate_chilldkg_full( + hostseckeys, t, blame=True +) -> List[Tuple[DKGOutput, RecoveryData]]: # Generate common inputs for all participants and coordinator n = len(hostseckeys) hostpubkeys = [] diff --git a/python/tests.py b/python/tests.py index 07a5677..c171eb0 100755 --- a/python/tests.py +++ b/python/tests.py @@ -35,22 +35,21 @@ def rand_polynomial(t): ) -def simulate_simplpedpop(seeds, t) -> List[Tuple[simplpedpop.DKGOutput, bytes]]: - blame = True # TODO Test also blame=False +def simulate_simplpedpop(seeds, t, blame) -> List[Tuple[simplpedpop.DKGOutput, bytes]]: n = len(seeds) prets = [] for i in range(n): prets += [simplpedpop.participant_step1(seeds[i], t, n, i, blame)] pmsgs = [ret[1] for ret in prets] - cmsg, cout, ceq = simplpedpop.coordinator_step(pmsgs, t, n) + cmsg, cout, ceq, all_partial_pubshares = simplpedpop.coordinator_step(pmsgs, t, n) pre_finalize_rets = [(cout, ceq)] for i in range(n): partial_secshares = [pret[2][i] for pret in prets] + partial_pubshares = all_partial_pubshares[i] # TODO Test that the protocol fails when wrong shares are sent. # if i == n - 1: # partial_secshares[-1] += Scalar(17) - partial_pubshares = [pret[3][i] for pret in prets] secshare, blame_rec = simplpedpop.participant_step2_prepare_secret_side_inputs( partial_secshares, partial_pubshares ) @@ -66,8 +65,7 @@ def encpedpop_keys(seed: bytes) -> Tuple[bytes, bytes]: return deckey, enckey -def simulate_encpedpop(seeds, t) -> List[Tuple[simplpedpop.DKGOutput, bytes]]: - blame = True # TODO Test also blame=False +def simulate_encpedpop(seeds, t, blame) -> List[Tuple[simplpedpop.DKGOutput, bytes]]: n = len(seeds) enc_prets0 = [] enc_prets1 = [] @@ -100,9 +98,8 @@ def simulate_encpedpop(seeds, t) -> List[Tuple[simplpedpop.DKGOutput, bytes]]: def simulate_chilldkg( - hostseckeys, t + hostseckeys, t, blame ) -> List[Tuple[chilldkg.DKGOutput, chilldkg.RecoveryData]]: - blame = True # TODO Test also blame=False n = len(hostseckeys) hostpubkeys = [] @@ -201,12 +198,12 @@ def test_correctness_dkg_output(t, n, dkg_outputs: List[simplpedpop.DKGOutput]): assert recovered * G == GE.from_bytes_compressed(threshold_pubkey) -def test_correctness(t, n, simulate_dkg, recovery=False): +def test_correctness(t, n, simulate_dkg, recovery=False, blame=True): seeds = [None] + [random_bytes(32) for _ in range(n)] # rets[0] are the return values from the coordinator # rets[1 : n + 1] are from the participants - rets = simulate_dkg(seeds[1:], t) + rets = simulate_dkg(seeds[1:], t, blame=blame) assert len(rets) == n + 1 dkg_outputs = [ret[0] for ret in rets] @@ -229,7 +226,8 @@ def test_correctness(t, n, simulate_dkg, recovery=False): test_vss_correctness() test_recover_secret() for t, n in [(1, 1), (1, 2), (2, 2), (2, 3), (2, 5)]: - test_correctness(t, n, simulate_simplpedpop) - test_correctness(t, n, simulate_encpedpop) - test_correctness(t, n, simulate_chilldkg, recovery=True) - test_correctness(t, n, simulate_chilldkg_full, recovery=True) + for blame in [False, True]: + test_correctness(t, n, simulate_simplpedpop, blame=blame) + test_correctness(t, n, simulate_encpedpop, blame=blame) + test_correctness(t, n, simulate_chilldkg, blame=blame, recovery=True) + test_correctness(t, n, simulate_chilldkg_full, blame=blame, recovery=True)