Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
multisig.hpp
Go to the documentation of this file.
1// === AUDIT STATUS ===
2// internal: { status: not started, auditors: [], date: YYYY-MM-DD }
3// external_1: { status: not started, auditors: [], date: YYYY-MM-DD }
4// external_2: { status: not started, auditors: [], date: YYYY-MM-DD }
5// =====================
6
7#pragma once
8
9#include <algorithm>
10#include <cstdint>
11#include <numeric>
12#include <optional>
13#include <utility>
14#include <vector>
15
17
19#include "schnorr.hpp"
20
21namespace bb::crypto {
22
34template <typename G1, typename HashRegNon, typename HashSig = Blake2sHasher> class schnorr_multisig {
35
36 // ensure that a different hash function is used for signature and proof of possession/nonce.
37 // we can apply domain separation for HashRegNon but not for HashSig, so this ensures all hash functions
38 // are modeled as different random oracles.
40
41 public:
42 using Fq = typename G1::Fq;
43 using Fr = typename G1::Fr;
44 using affine_element = typename G1::affine_element;
45 using element = typename G1::element;
47
58 typedef uint8_t const* in_buf;
59 typedef uint8_t const* vec_in_buf;
60 typedef uint8_t* out_buf;
61 typedef uint8_t** vec_out_buf;
62
63 affine_element public_key = G1::affine_point_at_infinity;
64 // proof of knowledge of the secret_key for public_key
66
67 // For serialization, update with any new fields
69 // restore default constructor to enable deserialization
70 MultiSigPublicKey() = default;
71 // create a MultiSigPublicKey with a proof of possession associated with public_key of account
73 : public_key(account.public_key)
74 , proof_of_possession(account)
75 {}
76 // Needed to appease MSGPACK_FIELDS
82 };
83
85 using in_buf = const uint8_t*;
86 using out_buf = uint8_t*;
87
90 // For serialization, update with any new fields
92 };
93
95 using in_buf = const uint8_t*;
96 using vec_in_buf = const uint8_t*;
97 using out_buf = uint8_t*;
98 using vec_out_buf = uint8_t**;
99
100 // R = r⋅G
102 // S = s⋅G
104 // For serialization, update with any new fields
106 // for std::sort
107 bool operator<(const RoundOnePublicOutput& other) const
108 {
109 return ((R < other.R) || ((R == other.R) && S < (other.S)));
110 }
111
112 bool operator==(const RoundOnePublicOutput& other) const { return (R == other.R) && (S == other.S); }
113 };
114 // corresponds to z = r + as - ex,
116
117 private:
125 static bool valid_round1_nonces(const std::vector<RoundOnePublicOutput>& round1_public_outputs)
126 {
127 for (size_t i = 0; i < round1_public_outputs.size(); ++i) {
128 auto& [R_user, S_user] = round1_public_outputs[i];
129 if (!R_user.on_curve() || R_user.is_point_at_infinity()) {
130 info("Round 1 commitments contains invalid R at index ", i);
131 return false;
132 }
133 if (!S_user.on_curve() || S_user.is_point_at_infinity()) {
134 info("Round 1 commitments contains invalid S at index ", i);
135 return false;
136 }
137 }
138 if (auto duplicated = duplicated_indices(round1_public_outputs); duplicated.size() > 0) {
139 info("Round 1 commitments contains duplicate values at indices ", duplicated);
140 return false;
141 }
142 return true;
143 }
144
160 static Fr generate_nonce_challenge(const std::string& message,
161 const affine_element& aggregate_pubkey,
162 const std::vector<RoundOnePublicOutput>& round_1_nonces)
163 {
164 // Domain separation for H_non
165 const std::string domain_separator_nonce("h_nonce");
166
167 // compute nonce challenge
168 // H('domain_separator_nonce', G, X, "m_start", m.size(), m, "m_end", {(R1, S1), ..., (Rn, Sn)})
169 std::vector<uint8_t> nonce_challenge_buffer;
170 // write domain separator
171 std::copy(
172 domain_separator_nonce.begin(), domain_separator_nonce.end(), std::back_inserter(nonce_challenge_buffer));
173
174 // write the group generator
175 write(nonce_challenge_buffer, G1::affine_one);
176
177 // write X
178 write(nonce_challenge_buffer, aggregate_pubkey);
179
180 // we slightly deviate from the protocol when including 'm', since the length of 'm' is variable
181 // by writing a prefix and a suffix, we prevent the message from being interpreted as coming from a different
182 // session.
183
184 // write "m_start"
185 const std::string m_start = "m_start";
186 std::copy(m_start.begin(), m_start.end(), std::back_inserter(nonce_challenge_buffer));
187 // write m.size()
188 write(nonce_challenge_buffer, static_cast<uint32_t>(message.size()));
189 // write message
190 std::copy(message.begin(), message.end(), std::back_inserter(nonce_challenge_buffer));
191 // write "m_end"
192 const std::string m_end = "m_end";
193 std::copy(m_end.begin(), m_end.end(), std::back_inserter(nonce_challenge_buffer));
194
195 // write {(R1, S1), ..., (Rn, Sn)}
196 for (const auto& nonce : round_1_nonces) {
197 write(nonce_challenge_buffer, nonce.R);
198 write(nonce_challenge_buffer, nonce.S);
199 }
200
201 // uses the different hash function for proper domain separation
202 auto nonce_challenge_raw = HashRegNon::hash(nonce_challenge_buffer);
203 // this results in a slight bias
204 Fr nonce_challenge = Fr::serialize_from_buffer(&nonce_challenge_raw[0]);
205 return nonce_challenge;
206 }
207
217 {
218 element R_sum = round_1_nonces[0].R;
219 element S_sum = round_1_nonces[0].S;
220 for (size_t i = 1; i < round_1_nonces.size(); ++i) {
221 const auto& [R, S] = round_1_nonces[i];
222 R_sum += R;
223 S_sum += S;
224 }
225 affine_element R(R_sum + S_sum * a);
226 return R;
227 }
228
238 template <typename T> static std::vector<size_t> duplicated_indices(const std::vector<T>& input)
239 {
240 const size_t num_inputs = input.size();
241 // indices = [0,1,..., num_inputs-1]
242 std::vector<size_t> indices(num_inputs);
243 std::iota(indices.begin(), indices.end(), 0);
244
245 // sort indices according to input.
246 // input[indices[i-1]] <= input[indices[i]]
247 std::sort(indices.begin(), indices.end(), [&](size_t a, size_t b) { return input[a] < input[b]; });
248
249 // This loop will include multiple copies of the same index if an element appears more than twice.
250 std::vector<size_t> duplicates;
251 for (size_t i = 1; i < num_inputs; ++i) {
252 const size_t idx1 = indices[i - 1];
253 const size_t idx2 = indices[i];
254 if (input[idx1] == input[idx2]) {
255 duplicates.push_back(idx1);
256 duplicates.push_back(idx2);
257 }
258 }
259 return duplicates;
260 }
261
262 public:
272 const std::vector<MultiSigPublicKey>& signer_pubkeys)
273 {
275 for (const auto& [public_key, proof_of_possession] : signer_pubkeys) {
276 points.push_back(public_key);
277 }
278
279 if (auto duplicated = duplicated_indices(points); duplicated.size() > 0) {
280 info("Duplicated public keys at indices ", duplicated);
281 return std::nullopt;
282 }
283
284 element aggregate_pubkey_jac = G1::point_at_infinity;
285 for (size_t i = 0; i < signer_pubkeys.size(); ++i) {
286 const auto& [public_key, proof_of_possession] = signer_pubkeys[i];
287 if (!public_key.on_curve() || public_key.is_point_at_infinity()) {
288 info("Multisig signer pubkey not a valid point at index ", i);
289 return std::nullopt;
290 }
291 if (!proof_of_possession.verify(public_key)) {
292 info("Multisig proof of posession invalid at index ", i);
293 return std::nullopt;
294 }
295 aggregate_pubkey_jac += public_key;
296 }
297
298 // This would prevent accidentally creating an aggregate key for the point at inifinity,
299 // with the trivial secret key.
300 // While it shouldn't happen, it is a cheap check.
301 affine_element aggregate_pubkey(aggregate_pubkey_jac);
302 if (aggregate_pubkey.is_point_at_infinity()) {
303 info("Multisig aggregate public key is invalid");
304 return std::nullopt;
305 }
306 return aggregate_pubkey;
307 }
308
318 {
319 // r_user ← 𝔽
320 // TODO: securely erase `r_user`
321 Fr r_user = Fr::random_element();
322 // R_user ← r_user⋅G
323 affine_element R_user = G1::one * r_user;
324
325 // s_user ← 𝔽
326 // TODO: securely erase `s_user`
327 Fr s_user = Fr::random_element();
328 // S_user ← s_user⋅G
329 affine_element S_user = G1::one * s_user;
330
331 RoundOnePublicOutput pubOut{ R_user, S_user };
332 RoundOnePrivateOutput privOut{ r_user, s_user };
333 return { pubOut, privOut };
334 }
335
349 const std::string& message,
350 const key_pair& signer,
351 const RoundOnePrivateOutput& signer_round_1_private_output,
352 const std::vector<MultiSigPublicKey>& signer_pubkeys,
353 const std::vector<RoundOnePublicOutput>& round_1_nonces)
354 {
355 const size_t num_signers = signer_pubkeys.size();
356 if (round_1_nonces.size() != num_signers) {
357 info("Multisig mismatch round_1_nonces and signers");
358 return std::nullopt;
359 }
360
361 // check that round_1_nonces does not contain duplicates and that all points are valid
362 if (!valid_round1_nonces(round_1_nonces)) {
363 return std::nullopt;
364 }
365
366 // compute aggregate key X = X_1 + ... + X_n
367 auto aggregate_pubkey = validate_and_combine_signer_pubkeys(signer_pubkeys);
368 if (!aggregate_pubkey.has_value()) {
369 // previous call has failed
370 return std::nullopt;
371 }
372
373 // compute nonce challenge H_non(G, X, "m_start", m, "m_end", {(R1, S1), ..., (Rn, Sn)})
374 Fr a = generate_nonce_challenge(message, *aggregate_pubkey, round_1_nonces);
375
376 // compute aggregate nonce R = R1 + ... + Rn + S1 * a + ... + Sn * a
377 affine_element R = construct_multisig_nonce(a, round_1_nonces);
378
379 // Now we have the multisig nonce, compute schnorr challenge e (termed `c` in the speedyMuSig paper)
380 auto e_buf = schnorr_generate_challenge<HashSig, G1>(message, *aggregate_pubkey, R);
381 Fr e = Fr::serialize_from_buffer(&e_buf[0]);
382
383 // output of round 2 is z
384 Fr z = signer_round_1_private_output.r + signer_round_1_private_output.s * a - signer.private_key * e;
385 return z;
386 }
387
401 const std::string& message,
402 const std::vector<MultiSigPublicKey>& signer_pubkeys,
403 const std::vector<RoundOnePublicOutput>& round_1_nonces,
404 const std::vector<RoundTwoPublicOutput>& round_2_signature_shares)
405 {
406 const size_t num_signers = signer_pubkeys.size();
407 if (round_1_nonces.size() != num_signers) {
408 info("Invalid number of round1 messages");
409 return std::nullopt;
410 }
411 if (round_2_signature_shares.size() != num_signers) {
412 info("Invalid number of round2 messages");
413 return std::nullopt;
414 }
415 if (!valid_round1_nonces(round_1_nonces)) {
416 return std::nullopt;
417 }
418
419 // compute aggregate key X = X_1 + ... + X_n
420 auto aggregate_pubkey = validate_and_combine_signer_pubkeys(signer_pubkeys);
421 if (!aggregate_pubkey.has_value()) {
422 // previous call has failed
423 return std::nullopt;
424 }
425
426 // compute nonce challenge H(X, m, {(R1, S1), ..., (Rn, Sn)})
427 Fr a = generate_nonce_challenge(message, *aggregate_pubkey, round_1_nonces);
428
429 // compute aggregate nonce R = R1 + ... + Rn + S1 * a + ... + Sn * a
430 affine_element R = construct_multisig_nonce(a, round_1_nonces);
431
432 auto e_buf = schnorr_generate_challenge<HashSig, G1>(message, *aggregate_pubkey, R);
433
435 // copy e as its raw bit representation (without modular reduction)
436 std::copy(e_buf.begin(), e_buf.end(), sig.e.begin());
437
438 Fr s = 0;
439 for (auto& z : round_2_signature_shares) {
440 s += z;
441 }
442 // write s, which will always produce an integer < r
443 Fr::serialize_to_buffer(s, &sig.s[0]);
444
445 // verify the final signature before returning
446 if (!schnorr_verify_signature<HashSig, Fq, Fr, G1>(message, *aggregate_pubkey, sig)) {
447 return std::nullopt;
448 }
449
450 return sig;
451 }
452};
453} // namespace bb::crypto
Implements the SpeedyMuSig protocol; a secure 2-round interactive multisignature scheme whose signatu...
Definition multisig.hpp:34
static std::vector< size_t > duplicated_indices(const std::vector< T > &input)
Returns a vector of indices of elements in input that are included more than once.
Definition multisig.hpp:238
static std::optional< schnorr_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:400
typename G1::element element
Definition multisig.hpp:45
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:317
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:348
static Fr generate_nonce_challenge(const std::string &message, const affine_element &aggregate_pubkey, const std::vector< RoundOnePublicOutput > &round_1_nonces)
Generates the Fiat-Shamir challenge a that is used to create a Schnorr signature nonce group element ...
Definition multisig.hpp:160
static bool valid_round1_nonces(const std::vector< RoundOnePublicOutput > &round1_public_outputs)
given a list of commitments to nonces produced in round 1, we check that all points are valid and tha...
Definition multisig.hpp:125
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:271
typename G1::affine_element affine_element
Definition multisig.hpp:44
static affine_element construct_multisig_nonce(const Fr &a, const std::vector< RoundOnePublicOutput > &round_1_nonces)
Compute the Schnorr signature scheme's nonce group element [R], given each signer's public nonces [R_...
Definition multisig.hpp:216
void info(Args... args)
Definition log.hpp:70
FF a
FF b
void write(B &buf, SchnorrProofOfPossession< G1, Hash > const &proof_of_possession)
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
Definition tuple.hpp:13
A proof of possession is a Schnorr proof of knowledge of a secret key corresponding to a given public...
MultiSigPublicKey wraps a signer's public key g1::affine_element along with a proof of posession: a s...
Definition multisig.hpp:57
SchnorrProofOfPossession< G1, HashRegNon > proof_of_possession
Definition multisig.hpp:65
MSGPACK_FIELDS(public_key, proof_of_possession)
MultiSigPublicKey(const affine_element &public_key, const SchnorrProofOfPossession< G1, HashRegNon > &proof_of_possession)
Definition multisig.hpp:77
bool operator==(const RoundOnePublicOutput &other) const
Definition multisig.hpp:112
bool operator<(const RoundOnePublicOutput &other) const
Definition multisig.hpp:107
std::array< uint8_t, 32 > s
Definition schnorr.hpp:36
std::array< uint8_t, 32 > e
Definition schnorr.hpp:39
static field random_element(numeric::RNG *engine=nullptr) noexcept
static field serialize_from_buffer(const uint8_t *buffer)
static void serialize_to_buffer(const field &value, uint8_t *buffer)