barretenberg
Loading...
Searching...
No Matches
multisig.hpp
1#pragma once
2
3#include <algorithm>
4#include <cstdint>
5#include <numeric>
6#include <optional>
7#include <utility>
8#include <vector>
9
10#include "barretenberg/serialize/msgpack.hpp"
11
12#include "proof_of_possession.hpp"
13#include "schnorr.hpp"
14
15namespace crypto::schnorr {
16
28template <typename G1, typename HashRegNon, typename HashSig = Blake2sHasher> class multisig {
29
30 // ensure that a different hash function is used for signature and proof of possession/nonce.
31 // we can apply domain separation for HashRegNon but not for HashSig, so this ensures all hash functions
32 // are modeled as different random oracles.
33 static_assert(!std::is_same_v<HashRegNon, HashSig>);
34
35 public:
36 using Fq = typename G1::coordinate_field;
37 using Fr = typename G1::subgroup_field;
38 using affine_element = typename G1::affine_element;
39 using element = typename G1::element;
41
52 typedef uint8_t const* in_buf;
53 typedef uint8_t const* vec_in_buf;
54 typedef uint8_t* out_buf;
55 typedef uint8_t** vec_out_buf;
56
57 affine_element public_key = G1::affine_point_at_infinity;
58 // proof of knowledge of the secret_key for public_key
59 ProofOfPossession<G1, HashRegNon> proof_of_possession;
60
61 // For serialization, update with any new fields
62 MSGPACK_FIELDS(public_key, proof_of_possession);
63 // restore default constructor to enable deserialization
64 MultiSigPublicKey() = default;
65 // create a MultiSigPublicKey with a proof of possession associated with public_key of account
66 MultiSigPublicKey(const key_pair& account)
67 : public_key(account.public_key)
68 , proof_of_possession(account)
69 {}
70 // Needed to appease MSGPACK_FIELDS
71 MultiSigPublicKey(const affine_element& public_key,
72 const ProofOfPossession<G1, HashRegNon>& proof_of_possession)
73 : public_key(public_key)
74 , proof_of_possession(proof_of_possession)
75 {}
76 };
77
79 typedef uint8_t const* in_buf;
80 typedef uint8_t* out_buf;
81
82 Fr r;
83 Fr s;
84 // For serialization, update with any new fields
85 MSGPACK_FIELDS(r, s);
86 };
87
89 typedef uint8_t const* in_buf;
90 typedef uint8_t const* vec_in_buf;
91 typedef uint8_t* out_buf;
92 typedef uint8_t** vec_out_buf;
93
94 // R = r⋅G
95 affine_element R;
96 // S = s⋅G
97 affine_element S;
98 // For serialization, update with any new fields
99 MSGPACK_FIELDS(R, S);
100 // for std::sort
101 bool operator<(const RoundOnePublicOutput& other) const
102 {
103 return ((R < other.R) || ((R == other.R) && S < (other.S)));
104 }
105
106 bool operator==(const RoundOnePublicOutput& other) const { return (R == other.R) && (S == other.S); }
107 };
108 // corresponds to z = r + as - ex,
109 using RoundTwoPublicOutput = Fr;
110
111 private:
119 static bool valid_round1_nonces(const std::vector<RoundOnePublicOutput>& round1_public_outputs)
120 {
121 for (size_t i = 0; i < round1_public_outputs.size(); ++i) {
122 auto& [R_user, S_user] = round1_public_outputs[i];
123 if (!R_user.on_curve() || R_user.is_point_at_infinity()) {
124 info("Round 1 commitments contains invalid R at index ", i);
125 return false;
126 }
127 if (!S_user.on_curve() || S_user.is_point_at_infinity()) {
128 info("Round 1 commitments contains invalid S at index ", i);
129 return false;
130 }
131 }
132 if (auto duplicated = duplicated_indices(round1_public_outputs); duplicated.size() > 0) {
133 info("Round 1 commitments contains duplicate values at indices ", duplicated);
134 return false;
135 }
136 return true;
137 }
138
154 static Fr generate_nonce_challenge(const std::string& message,
155 const affine_element& aggregate_pubkey,
156 const std::vector<RoundOnePublicOutput>& round_1_nonces)
157 {
158 // Domain separation for H_non
159 const std::string domain_separator_nonce("h_nonce");
160
161 // compute nonce challenge
162 // H('domain_separator_nonce', G, X, "m_start", m.size(), m, "m_end", {(R1, S1), ..., (Rn, Sn)})
163 std::vector<uint8_t> nonce_challenge_buffer;
164 // write domain separator
165 std::copy(
166 domain_separator_nonce.begin(), domain_separator_nonce.end(), std::back_inserter(nonce_challenge_buffer));
167
168 // write the group generator
169 serialize::write(nonce_challenge_buffer, G1::affine_one);
170
171 // write X
172 serialize::write(nonce_challenge_buffer, aggregate_pubkey);
173
174 // we slightly deviate from the protocol when including 'm', since the length of 'm' is variable
175 // by writing a prefix and a suffix, we prevent the message from being interpreted as coming from a different
176 // session.
177
178 // write "m_start"
179 const std::string m_start = "m_start";
180 std::copy(m_start.begin(), m_start.end(), std::back_inserter(nonce_challenge_buffer));
181 // write m.size()
182 write(nonce_challenge_buffer, static_cast<uint32_t>(message.size()));
183 // write message
184 std::copy(message.begin(), message.end(), std::back_inserter(nonce_challenge_buffer));
185 // write "m_end"
186 const std::string m_end = "m_end";
187 std::copy(m_end.begin(), m_end.end(), std::back_inserter(nonce_challenge_buffer));
188
189 // write {(R1, S1), ..., (Rn, Sn)}
190 for (const auto& nonce : round_1_nonces) {
191 serialize::write(nonce_challenge_buffer, nonce.R);
192 serialize::write(nonce_challenge_buffer, nonce.S);
193 }
194
195 // uses the different hash function for proper domain separation
196 auto nonce_challenge_raw = HashRegNon::hash(nonce_challenge_buffer);
197 // this results in a slight bias
198 Fr nonce_challenge = Fr::serialize_from_buffer(&nonce_challenge_raw[0]);
199 return nonce_challenge;
200 }
201
210 static affine_element construct_multisig_nonce(const Fr& a, const std::vector<RoundOnePublicOutput>& round_1_nonces)
211 {
212 element R_sum = round_1_nonces[0].R;
213 element S_sum = round_1_nonces[0].S;
214 for (size_t i = 1; i < round_1_nonces.size(); ++i) {
215 const auto& [R, S] = round_1_nonces[i];
216 R_sum += R;
217 S_sum += S;
218 }
219 affine_element R(R_sum + S_sum * a);
220 return R;
221 }
222
232 template <typename T> static std::vector<size_t> duplicated_indices(const std::vector<T>& input)
233 {
234 const size_t num_inputs = input.size();
235 // indices = [0,1,..., num_inputs-1]
236 std::vector<size_t> indices(num_inputs);
237 std::iota(indices.begin(), indices.end(), 0);
238
239 // sort indices according to input.
240 // input[indices[i-1]] <= input[indices[i]]
241 std::sort(indices.begin(), indices.end(), [&](size_t a, size_t b) { return input[a] < input[b]; });
242
243 // This loop will include multiple copies of the same index if an element appears more than twice.
244 std::vector<size_t> duplicates;
245 for (size_t i = 1; i < num_inputs; ++i) {
246 const size_t idx1 = indices[i - 1];
247 const size_t idx2 = indices[i];
248 if (input[idx1] == input[idx2]) {
249 duplicates.push_back(idx1);
250 duplicates.push_back(idx2);
251 }
252 }
253 return duplicates;
254 }
255
256 public:
265 static std::optional<affine_element> validate_and_combine_signer_pubkeys(
266 const std::vector<MultiSigPublicKey>& signer_pubkeys)
267 {
268 std::vector<affine_element> points;
269 for (const auto& [public_key, proof_of_possession] : signer_pubkeys) {
270 points.push_back(public_key);
271 }
272
273 if (auto duplicated = duplicated_indices(points); duplicated.size() > 0) {
274 info("Duplicated public keys at indices ", duplicated);
275 return std::nullopt;
276 }
277
278 element aggregate_pubkey_jac = G1::point_at_infinity;
279 for (size_t i = 0; i < signer_pubkeys.size(); ++i) {
280 const auto& [public_key, proof_of_possession] = signer_pubkeys[i];
281 if (!public_key.on_curve() || public_key.is_point_at_infinity()) {
282 info("Multisig signer pubkey not a valid point at index ", i);
283 return std::nullopt;
284 }
285 if (!proof_of_possession.verify(public_key)) {
286 info("Multisig proof of posession invalid at index ", i);
287 return std::nullopt;
288 }
289 aggregate_pubkey_jac += public_key;
290 }
291
292 // This would prevent accidentally creating an aggregate key for the point at inifinity,
293 // with the trivial secret key.
294 // While it shouldn't happen, it is a cheap check.
295 affine_element aggregate_pubkey(aggregate_pubkey_jac);
296 if (aggregate_pubkey.is_point_at_infinity()) {
297 info("Multisig aggregate public key is invalid");
298 return std::nullopt;
299 }
300 return aggregate_pubkey;
301 }
302
311 static std::pair<RoundOnePublicOutput, RoundOnePrivateOutput> construct_signature_round_1()
312 {
313 // r_user ← 𝔽
314 // TODO: securely erase `r_user`
315 Fr r_user = Fr::random_element();
316 // R_user ← r_user⋅G
317 affine_element R_user = G1::one * r_user;
318
319 // s_user ← 𝔽
320 // TODO: securely erase `s_user`
321 Fr s_user = Fr::random_element();
322 // S_user ← s_user⋅G
323 affine_element S_user = G1::one * s_user;
324
325 RoundOnePublicOutput pubOut{ R_user, S_user };
326 RoundOnePrivateOutput privOut{ r_user, s_user };
327 return { pubOut, privOut };
328 }
329
342 static std::optional<RoundTwoPublicOutput> construct_signature_round_2(
343 const std::string& message,
344 const key_pair& signer,
345 const RoundOnePrivateOutput& signer_round_1_private_output,
346 const std::vector<MultiSigPublicKey>& signer_pubkeys,
347 const std::vector<RoundOnePublicOutput>& round_1_nonces)
348 {
349 const size_t num_signers = signer_pubkeys.size();
350 if (round_1_nonces.size() != num_signers) {
351 info("Multisig mismatch round_1_nonces and signers");
352 return std::nullopt;
353 }
354
355 // check that round_1_nonces does not contain duplicates and that all points are valid
356 if (!valid_round1_nonces(round_1_nonces)) {
357 return std::nullopt;
358 }
359
360 // compute aggregate key X = X_1 + ... + X_n
361 auto aggregate_pubkey = validate_and_combine_signer_pubkeys(signer_pubkeys);
362 if (!aggregate_pubkey.has_value()) {
363 // previous call has failed
364 return std::nullopt;
365 }
366
367 // compute nonce challenge H_non(G, X, "m_start", m, "m_end", {(R1, S1), ..., (Rn, Sn)})
368 Fr a = generate_nonce_challenge(message, *aggregate_pubkey, round_1_nonces);
369
370 // compute aggregate nonce R = R1 + ... + Rn + S1 * a + ... + Sn * a
371 affine_element R = construct_multisig_nonce(a, round_1_nonces);
372
373 // Now we have the multisig nonce, compute schnorr challenge e (termed `c` in the speedyMuSig paper)
374 auto e_buf = generate_schnorr_challenge<HashSig, G1>(message, *aggregate_pubkey, R);
375 Fr e = Fr::serialize_from_buffer(&e_buf[0]);
376
377 // output of round 2 is z
378 Fr z = signer_round_1_private_output.r + signer_round_1_private_output.s * a - signer.private_key * e;
379 return z;
380 }
381
394 static std::optional<signature> combine_signatures(
395 const std::string& message,
396 const std::vector<MultiSigPublicKey>& signer_pubkeys,
397 const std::vector<RoundOnePublicOutput>& round_1_nonces,
398 const std::vector<RoundTwoPublicOutput>& round_2_signature_shares)
399 {
400 const size_t num_signers = signer_pubkeys.size();
401 if (round_1_nonces.size() != num_signers) {
402 info("Invalid number of round1 messages");
403 return std::nullopt;
404 }
405 if (round_2_signature_shares.size() != num_signers) {
406 info("Invalid number of round2 messages");
407 return std::nullopt;
408 }
409 if (!valid_round1_nonces(round_1_nonces)) {
410 return std::nullopt;
411 }
412
413 // compute aggregate key X = X_1 + ... + X_n
414 auto aggregate_pubkey = validate_and_combine_signer_pubkeys(signer_pubkeys);
415 if (!aggregate_pubkey.has_value()) {
416 // previous call has failed
417 return std::nullopt;
418 }
419
420 // compute nonce challenge H(X, m, {(R1, S1), ..., (Rn, Sn)})
421 Fr a = generate_nonce_challenge(message, *aggregate_pubkey, round_1_nonces);
422
423 // compute aggregate nonce R = R1 + ... + Rn + S1 * a + ... + Sn * a
424 affine_element R = construct_multisig_nonce(a, round_1_nonces);
425
426 auto e_buf = generate_schnorr_challenge<HashSig, G1>(message, *aggregate_pubkey, R);
427
428 signature sig;
429 // copy e as its raw bit representation (without modular reduction)
430 std::copy(e_buf.begin(), e_buf.end(), sig.e.begin());
431
432 Fr s = 0;
433 for (auto& z : round_2_signature_shares) {
434 s += z;
435 }
436 // write s, which will always produce an integer < r
437 Fr::serialize_to_buffer(s, &sig.s[0]);
438
439 // verify the final signature before returning
440 if (!verify_signature<HashSig, Fq, Fr, G1>(message, *aggregate_pubkey, sig)) {
441 return std::nullopt;
442 }
443
444 return sig;
445 }
446};
447} // namespace crypto::schnorr
Definition: affine_element.hpp:11
Implements the SpeedyMuSig protocol; a secure 2-round interactive multisignature scheme whose signatu...
Definition: multisig.hpp:28
static std::optional< RoundTwoPublicOutput > construct_signature_round_2(const std::string &message, const key_pair &signer, const RoundOnePrivateOutput &signer_round_1_private_output, const std::vector< MultiSigPublicKey > &signer_pubkeys, const std::vector< RoundOnePublicOutput > &round_1_nonces)
Second round of SpeedyMuSig. Given the signer pubkeys and the output of round 1, round 2 has each sig...
Definition: multisig.hpp:342
static std::optional< affine_element > validate_and_combine_signer_pubkeys(const std::vector< MultiSigPublicKey > &signer_pubkeys)
Computes the sum of all signer pubkeys. Output is the public key of the public-facing schnorr multisi...
Definition: multisig.hpp:265
static std::pair< RoundOnePublicOutput, RoundOnePrivateOutput > construct_signature_round_1()
First round of SpeedyMuSig. Signers generate random nonce keypairs R = {r, [R]}, S = {s,...
Definition: multisig.hpp:311
static std::optional< signature > combine_signatures(const std::string &message, const std::vector< MultiSigPublicKey > &signer_pubkeys, const std::vector< RoundOnePublicOutput > &round_1_nonces, const std::vector< RoundTwoPublicOutput > &round_2_signature_shares)
the final step in the SpeedyMuSig multisig scheme. Can be computed by an untrusted 3rd party....
Definition: multisig.hpp:394
A proof of possession is a Schnorr proof of knowledge of a secret key corresponding to a given public...
Definition: proof_of_possession.hpp:18
Definition: schnorr.hpp:17
MultiSigPublicKey wraps a signer's public key g1::affine_element along with a proof of posession: a s...
Definition: multisig.hpp:51
Definition: schnorr.hpp:25