|
| 1 | +from secrets import randbelow |
| 2 | + |
| 3 | +from buidl.ecc import N, G, S256Point, SchnorrSignature |
| 4 | +from buidl.helper import ( |
| 5 | + big_endian_to_int, |
| 6 | + encode_varint, |
| 7 | + int_to_big_endian, |
| 8 | +) |
| 9 | +from buidl.hash import hash_challenge |
| 10 | +from buidl.phash import tagged_hash |
| 11 | + |
| 12 | + |
| 13 | +def hash_frost_keygen(m): |
| 14 | + """Hash used for cooperative key generation. This should be a tagged hash""" |
| 15 | + return tagged_hash(b"FROST/keygen", m) |
| 16 | + |
| 17 | + |
| 18 | +def hash_frost_commitment(m): |
| 19 | + """Hash used for message commitment in signing. This should be a tagged hash""" |
| 20 | + return tagged_hash(b"FROST/commitment", m) |
| 21 | + |
| 22 | + |
| 23 | +class FrostParticipant: |
| 24 | + """Represents a participant in a t-of-n FROST""" |
| 25 | + |
| 26 | + def __init__(self, t, n, index): |
| 27 | + # t-of-n FROST with this one being at index in [1, n] |
| 28 | + self.t = t |
| 29 | + self.n = n |
| 30 | + self.index = index |
| 31 | + self.keygen_coefficients = None |
| 32 | + self.coefficient_commitments = [[] for _ in range(self.n)] |
| 33 | + self.shares_from = [None for _ in range(self.n)] |
| 34 | + |
| 35 | + def key_generation_round_1(self, name): |
| 36 | + if self.keygen_coefficients is not None: |
| 37 | + raise ValueError("secrets have already been defined") |
| 38 | + # generate t random numbers for a Shamir polynomial |
| 39 | + self.keygen_coefficients = [randbelow(N) for _ in range(self.t)] |
| 40 | + my_commitments = [coef * G for coef in self.keygen_coefficients] |
| 41 | + self.coefficient_commitments[self.index] = my_commitments |
| 42 | + k = randbelow(N) # TODO: change this to use the k generation from bip340 |
| 43 | + r = k * G |
| 44 | + c = hash_frost_keygen( |
| 45 | + encode_varint(self.index) + name + my_commitments[0].xonly() + r.xonly() |
| 46 | + ) |
| 47 | + # proof proves that we know the first coefficient |
| 48 | + proof = (k + self.keygen_coefficients[0] * big_endian_to_int(c)) % N |
| 49 | + return (my_commitments, r, proof) |
| 50 | + |
| 51 | + def poly_value(self, x): |
| 52 | + """return the polynomial value f(x) for the polynomial defined by the secrets""" |
| 53 | + result = 0 |
| 54 | + for coef_index in range(self.t): |
| 55 | + result += self.keygen_coefficients[coef_index] * x**coef_index % N |
| 56 | + return result % N |
| 57 | + |
| 58 | + def verify_round_1(self, name, participant_index, commitments, r, proof): |
| 59 | + """check that the commitment at index 0, r and proof are valid""" |
| 60 | + if participant_index == self.index: |
| 61 | + return |
| 62 | + c = hash_frost_keygen( |
| 63 | + encode_varint(participant_index) + name + commitments[0].xonly() + r.xonly() |
| 64 | + ) |
| 65 | + if r != -big_endian_to_int(c) * commitments[0] + proof: |
| 66 | + raise RuntimeError("commitment does not correspond to proof") |
| 67 | + self.coefficient_commitments[participant_index] = commitments |
| 68 | + |
| 69 | + def key_generation_round_2(self): |
| 70 | + """Deal out shares to each participant corresponding to their index + 1""" |
| 71 | + shares = [] |
| 72 | + for participant_index in range(self.n): |
| 73 | + shares.append(self.poly_value(participant_index + 1)) |
| 74 | + self.shares_from[self.index] = shares[self.index] |
| 75 | + shares[self.index] = None |
| 76 | + return shares |
| 77 | + |
| 78 | + def verify_round_2(self, participant_index, share): |
| 79 | + """Check that we have a valid point in the committed Shamir polynomial |
| 80 | + from this participant""" |
| 81 | + if participant_index == self.index: |
| 82 | + return |
| 83 | + commitments = self.coefficient_commitments[participant_index] |
| 84 | + x = self.index + 1 |
| 85 | + target = share * G |
| 86 | + points = [] |
| 87 | + for coef_index in range(self.t): |
| 88 | + coef = x**coef_index % N |
| 89 | + points.append(coef * commitments[coef_index]) |
| 90 | + if S256Point.combine(points) != target: |
| 91 | + raise RuntimeError("share does not correspond to the commitment") |
| 92 | + self.shares_from[participant_index] = share |
| 93 | + |
| 94 | + def compute_keys(self): |
| 95 | + """Now compute the pubkeys for each participant and the secret share for |
| 96 | + our pubkey""" |
| 97 | + self.pubkeys = [] |
| 98 | + for _ in range(self.n): |
| 99 | + points = [] |
| 100 | + for participant_index in range(self.n): |
| 101 | + for coef_index in range(self.t): |
| 102 | + coef = (self.index + 1) ** coef_index % N |
| 103 | + points.append( |
| 104 | + coef |
| 105 | + * self.coefficient_commitments[participant_index][coef_index] |
| 106 | + ) |
| 107 | + self.pubkeys.append(S256Point.combine(points)) |
| 108 | + # the constant term of the combined polynomial is the pubkey |
| 109 | + self.group_pubkey = S256Point.combine( |
| 110 | + [ |
| 111 | + self.coefficient_commitments[participant_index][0] |
| 112 | + for participant_index in range(self.n) |
| 113 | + ] |
| 114 | + ) |
| 115 | + # the secret shares that were dealt to us, we now combine for the secret |
| 116 | + self.secret = sum(self.shares_from) % N |
| 117 | + # sanity check against the public key we computed |
| 118 | + self.pubkey = self.pubkeys[self.index] |
| 119 | + if self.secret * G != self.pubkey: |
| 120 | + raise RuntimeError("something wrong with the secret") |
| 121 | + # if we have an odd group key, negate everything |
| 122 | + if self.group_pubkey.parity: |
| 123 | + # negate the pubkeys, the group pubkey and our secret |
| 124 | + self.pubkeys = [-1 * p for p in self.pubkeys] |
| 125 | + self.group_pubkey = -1 * self.group_pubkey |
| 126 | + self.secret = N - self.secret |
| 127 | + self.pubkey = self.pubkeys[self.index] |
| 128 | + return self.group_pubkey |
| 129 | + |
| 130 | + def generate_nonce_pairs(self, num=200): |
| 131 | + """We now deal to everyone the nonces we will be using for signing. |
| 132 | + Each signing requires a pair of nonces and we return the nonce commitments""" |
| 133 | + # create two nonces for use in the signing |
| 134 | + self.nonces = {} |
| 135 | + self.nonce_pubs = [] |
| 136 | + for _ in range(num): |
| 137 | + # this should probably involve some deterministic process involving |
| 138 | + # the private key |
| 139 | + nonce_1, nonce_2 = randbelow(N), randbelow(N) |
| 140 | + nonce_pub_1 = nonce_1 * G |
| 141 | + nonce_pub_2 = nonce_2 * G |
| 142 | + self.nonces[nonce_pub_1] = (nonce_1, nonce_2) |
| 143 | + self.nonce_pubs.append((nonce_pub_1, nonce_pub_2)) |
| 144 | + return self.nonce_pubs |
| 145 | + |
| 146 | + def register_nonce_pubs(self, nonce_pubs_list): |
| 147 | + """When we receive the nonce commitments, we store them""" |
| 148 | + self.nonces_available = [] |
| 149 | + for nonce_pubs in nonce_pubs_list: |
| 150 | + nonce_lookup = {} |
| 151 | + for nonce_pub_1, nonce_pub_2 in nonce_pubs: |
| 152 | + nonce_lookup[(nonce_pub_1, nonce_pub_2)] = True |
| 153 | + self.nonces_available.append(nonce_lookup) |
| 154 | + |
| 155 | + def compute_group_r(self, msg, nonces_to_use): |
| 156 | + """The R that we use for signing can be computed based on the nonces |
| 157 | + we are using and the message that we're signing""" |
| 158 | + # add up the first nonces as normal |
| 159 | + ds = [] |
| 160 | + for key in sorted(nonces_to_use.keys()): |
| 161 | + value = nonces_to_use[key] |
| 162 | + ds.append(value[0]) |
| 163 | + result = [S256Point.combine(ds)] |
| 164 | + # the second nonces need to be multiplied by the commitment |
| 165 | + for key in sorted(nonces_to_use.keys()): |
| 166 | + value = nonces_to_use[key] |
| 167 | + commitment = ( |
| 168 | + big_endian_to_int( |
| 169 | + hash_frost_commitment( |
| 170 | + msg + encode_varint(key) + value[0].xonly() + value[1].xonly() |
| 171 | + ) |
| 172 | + ) |
| 173 | + % N |
| 174 | + ) |
| 175 | + result.append(commitment * value[1]) |
| 176 | + return S256Point.combine(result) |
| 177 | + |
| 178 | + def sign(self, msg, nonces_to_use): |
| 179 | + """Sign using our secret share given the nonces we are supposed to use""" |
| 180 | + group_r = self.compute_group_r(msg, nonces_to_use) |
| 181 | + # compute the lagrange coefficient based on the participants |
| 182 | + lagrange = 1 |
| 183 | + for key in sorted(nonces_to_use.keys()): |
| 184 | + value = nonces_to_use[key] |
| 185 | + if not self.nonces_available[key][value]: |
| 186 | + raise ValueError("Using an unknown or already used nonce") |
| 187 | + if key == self.index: |
| 188 | + my_commitment = ( |
| 189 | + big_endian_to_int( |
| 190 | + hash_frost_commitment( |
| 191 | + msg |
| 192 | + + encode_varint(key) |
| 193 | + + value[0].xonly() |
| 194 | + + value[1].xonly() |
| 195 | + ) |
| 196 | + ) |
| 197 | + % N |
| 198 | + ) |
| 199 | + else: |
| 200 | + lagrange *= (key + 1) * pow(key - self.index, -1, N) % N |
| 201 | + # the group challenge is the normal Schnorr Signature challenge from BIP340 |
| 202 | + challenge = big_endian_to_int( |
| 203 | + hash_challenge(group_r.xonly() + self.group_pubkey.xonly() + msg) |
| 204 | + ) |
| 205 | + # use the two nonces to compute the k we will use |
| 206 | + my_d, my_e = self.nonces[nonces_to_use[self.index][0]] |
| 207 | + my_k = my_d + my_e * my_commitment |
| 208 | + d_pub, e_pub = my_d * G, my_e * G |
| 209 | + my_r = S256Point.combine([d_pub, my_commitment * e_pub]) |
| 210 | + # if the group r is odd, we negate everything |
| 211 | + if group_r.parity: |
| 212 | + group_r = -1 * group_r |
| 213 | + my_k = N - my_k |
| 214 | + my_r = -1 * my_r |
| 215 | + sig_share = (my_k + lagrange * self.secret * challenge) % N |
| 216 | + # sanity check the s we generated |
| 217 | + second = (challenge * lagrange % N) * self.pubkey |
| 218 | + if -1 * second + sig_share != my_r: |
| 219 | + raise RuntimeError("signature didn't do what we expected") |
| 220 | + # delete nonce used |
| 221 | + for key in sorted(nonces_to_use.keys()): |
| 222 | + value = nonces_to_use[key] |
| 223 | + del self.nonces_available[key][value] |
| 224 | + return sig_share |
| 225 | + |
| 226 | + def combine_shares(self, shares, msg, nonces_to_use): |
| 227 | + """Convenience method to return a Schnorr Signature once |
| 228 | + the participants have returned their shares""" |
| 229 | + r = self.compute_group_r(msg, nonces_to_use) |
| 230 | + s = sum(shares) % N |
| 231 | + return SchnorrSignature.parse(r.xonly() + int_to_big_endian(s, 32)) |
0 commit comments