1use core::{ops::Deref, fmt::Debug};
2use std_shims::{
3 sync::{Arc, Mutex},
4 io::{self, Read, Write},
5 collections::HashMap,
6};
7
8use rand_core::{RngCore, CryptoRng, SeedableRng};
9use rand_chacha::ChaCha20Rng;
10
11use zeroize::{Zeroize, Zeroizing};
12
13use curve25519_dalek::{scalar::Scalar, edwards::EdwardsPoint};
14
15use group::{
16 ff::{Field, PrimeField},
17 Group, GroupEncoding,
18};
19
20use transcript::{Transcript, RecommendedTranscript};
21use dalek_ff_group as dfg;
22use frost::{
23 curve::Ed25519,
24 Participant, FrostError, ThresholdKeys, ThresholdView,
25 algorithm::{WriteAddendum, Algorithm},
26};
27
28use monero_generators::biased_hash_to_point;
29
30use crate::{ClsagContext, Clsag};
31
32impl ClsagContext {
33 fn transcript<T: Transcript>(&self, transcript: &mut T) {
34 transcript.append_message(b"signer_index", [self.decoys.signer_index()]);
38
39 for (i, pair) in self.decoys.ring().iter().enumerate() {
41 transcript.append_message(b"member", [u8::try_from(i).expect("ring size exceeded 255")]);
45 transcript.append_message(b"key", pair[0].compress().to_bytes());
47 transcript.append_message(b"commitment", pair[1].compress().to_bytes())
48 }
49
50 }
54}
55
56#[derive(Debug)]
60pub struct ClsagMultisigMaskSender {
61 buf: Arc<Mutex<Option<Scalar>>>,
62}
63#[derive(Debug)]
64struct ClsagMultisigMaskReceiver {
65 buf: Arc<Mutex<Option<Scalar>>>,
66}
67impl ClsagMultisigMaskSender {
68 fn new() -> (ClsagMultisigMaskSender, ClsagMultisigMaskReceiver) {
69 let buf = Arc::new(Mutex::new(None));
70 (ClsagMultisigMaskSender { buf: buf.clone() }, ClsagMultisigMaskReceiver { buf })
71 }
72
73 pub fn send(self, mask: Scalar) {
75 *self.buf.lock() = Some(mask);
78 }
79}
80impl ClsagMultisigMaskReceiver {
81 fn recv(self) -> Option<Scalar> {
82 *self.buf.lock()
83 }
84}
85
86#[derive(Clone, PartialEq, Eq, Zeroize, Debug)]
88pub struct ClsagAddendum {
89 key_image_share: dfg::EdwardsPoint,
90}
91
92impl ClsagAddendum {
93 pub fn key_image_share(&self) -> dfg::EdwardsPoint {
95 self.key_image_share
96 }
97}
98
99impl WriteAddendum for ClsagAddendum {
100 fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
101 writer.write_all(self.key_image_share.compress().to_bytes().as_ref())
102 }
103}
104
105#[allow(non_snake_case)]
106#[derive(Clone, PartialEq, Eq, Debug)]
107struct Interim {
108 p: Scalar,
109 c: Scalar,
110
111 clsag: Clsag,
112 pseudo_out: EdwardsPoint,
113}
114
115#[allow(non_snake_case)]
125#[derive(Debug)]
126pub struct ClsagMultisig {
127 transcript: RecommendedTranscript,
128
129 key_image_generator: EdwardsPoint,
130 key_image_shares: HashMap<[u8; 32], dfg::EdwardsPoint>,
131 image: dfg::EdwardsPoint,
132
133 context: ClsagContext,
134
135 mask_recv: Option<ClsagMultisigMaskReceiver>,
136 mask: Option<Scalar>,
137
138 msg_hash: Option<[u8; 32]>,
139 interim: Option<Interim>,
140}
141
142impl ClsagMultisig {
143 pub fn new(
145 transcript: RecommendedTranscript,
146 context: ClsagContext,
147 ) -> (ClsagMultisig, ClsagMultisigMaskSender) {
148 let (mask_send, mask_recv) = ClsagMultisigMaskSender::new();
149 (
150 ClsagMultisig {
151 transcript,
152
153 key_image_generator: biased_hash_to_point(
154 context.decoys.signer_ring_members()[0].compress().0,
155 ),
156 key_image_shares: HashMap::new(),
157 image: dfg::EdwardsPoint::identity(),
158
159 context,
160
161 mask_recv: Some(mask_recv),
162 mask: None,
163
164 msg_hash: None,
165 interim: None,
166 },
167 mask_send,
168 )
169 }
170
171 pub fn key_image_generator(&self) -> EdwardsPoint {
173 self.key_image_generator
174 }
175}
176
177impl Algorithm<Ed25519> for ClsagMultisig {
178 type Transcript = RecommendedTranscript;
179 type Addendum = ClsagAddendum;
180 type Signature = (Clsag, EdwardsPoint);
182
183 fn nonces(&self) -> Vec<Vec<dfg::EdwardsPoint>> {
185 vec![vec![dfg::EdwardsPoint::generator(), dfg::EdwardsPoint(self.key_image_generator)]]
186 }
187
188 fn preprocess_addendum<R: RngCore + CryptoRng>(
190 &mut self,
191 _rng: &mut R,
192 keys: &ThresholdKeys<Ed25519>,
193 ) -> ClsagAddendum {
194 ClsagAddendum {
195 key_image_share: dfg::EdwardsPoint(self.key_image_generator) *
196 keys.original_secret_share().deref(),
197 }
198 }
199
200 fn read_addendum<R: Read>(&self, reader: &mut R) -> io::Result<ClsagAddendum> {
201 let mut bytes = [0; 32];
202 reader.read_exact(&mut bytes)?;
203 let xH = Option::<dfg::EdwardsPoint>::from(dfg::EdwardsPoint::from_bytes(&bytes))
205 .ok_or_else(|| io::Error::other("invalid key image"))?;
206 if xH.to_bytes() != bytes {
208 Err(io::Error::other("non-canonical key image"))?;
209 }
210
211 Ok(ClsagAddendum { key_image_share: xH })
212 }
213
214 fn process_addendum(
215 &mut self,
216 view: &ThresholdView<Ed25519>,
217 l: Participant,
218 addendum: ClsagAddendum,
219 ) -> Result<(), FrostError> {
220 if let Some(mask_recv) = self.mask_recv.take() {
221 self.transcript.domain_separate(b"CLSAG");
222 self.context.transcript(&mut self.transcript);
224 let mask = mask_recv
228 .recv()
229 .ok_or(FrostError::InternalError("CLSAG mask was not provided before process_addendum"))?;
230 self.mask = Some(mask);
231 self.transcript.append_message(b"mask", mask.to_bytes());
233 }
234
235 self.transcript.append_message(b"participant", l.to_bytes());
237 self
238 .transcript
239 .append_message(b"key_image_share", addendum.key_image_share.compress().to_bytes());
240
241 let interpolated_key_image_share = addendum.key_image_share *
243 view
244 .interpolation_factor(l)
245 .ok_or(FrostError::InternalError("processing addendum for non-participant"))?;
246 self.image += interpolated_key_image_share;
247
248 self
249 .key_image_shares
250 .insert(view.verification_share(l).to_bytes(), interpolated_key_image_share);
251
252 Ok(())
253 }
254
255 fn transcript(&mut self) -> &mut Self::Transcript {
256 &mut self.transcript
257 }
258
259 fn sign_share(
260 &mut self,
261 view: &ThresholdView<Ed25519>,
262 nonce_sums: &[Vec<dfg::EdwardsPoint>],
263 nonces: Vec<Zeroizing<dfg::Scalar>>,
264 msg_hash: &[u8],
265 ) -> dfg::Scalar {
266 self.image =
267 (self.image * view.scalar()) + (dfg::EdwardsPoint(self.key_image_generator) * view.offset());
268
269 let mut rng = ChaCha20Rng::from_seed(self.transcript.rng_seed(b"decoy_responses"));
277
278 let msg_hash = msg_hash.try_into().expect("CLSAG message hash should be 32-bytes");
279 self.msg_hash = Some(msg_hash);
280
281 let sign_core = Clsag::sign_core(
282 &mut rng,
283 &self.image,
284 &self.context,
285 self.mask.expect("mask wasn't set within process_addendum"),
286 &msg_hash,
287 nonce_sums[0][0].0,
288 nonce_sums[0][1].0,
289 );
290 self.interim = Some(Interim {
291 p: sign_core.key_challenge,
292 c: sign_core.challenged_mask,
293 clsag: sign_core.incomplete_clsag,
294 pseudo_out: sign_core.pseudo_out,
295 });
296
297 *nonces[0] - dfg::Scalar(sign_core.key_challenge) * view.secret_share().deref()
299 }
300
301 fn verify(
302 &self,
303 _: dfg::EdwardsPoint,
304 _: &[Vec<dfg::EdwardsPoint>],
305 sum: dfg::Scalar,
306 ) -> Option<Self::Signature> {
307 let interim = self.interim.as_ref().expect("verify called before sign_share");
308 let mut clsag = interim.clsag.clone();
309 clsag.s[usize::from(self.context.decoys.signer_index())] = sum.0 - interim.c;
312 if clsag
313 .verify(
314 self.context.decoys.ring(),
315 &self.image.0,
316 &interim.pseudo_out,
317 self.msg_hash.as_ref().expect("verify called before sign_share"),
318 )
319 .is_ok()
320 {
321 return Some((clsag, interim.pseudo_out));
322 }
323 None
324 }
325
326 fn verify_share(
327 &self,
328 verification_share: dfg::EdwardsPoint,
329 nonces: &[Vec<dfg::EdwardsPoint>],
330 share: dfg::Scalar,
331 ) -> Result<Vec<(dfg::Scalar, dfg::EdwardsPoint)>, ()> {
332 let interim = self.interim.as_ref().expect("verify_share called before sign_share");
333
334 let key_image_share = self.key_image_shares[&verification_share.to_bytes()];
357
358 let mut weight_transcript =
360 RecommendedTranscript::new(b"monero-oxide v0.1 ClsagMultisig::verify_share");
361 weight_transcript.append_message(b"G", dfg::EdwardsPoint::generator().to_bytes());
362 weight_transcript.append_message(b"H", self.key_image_generator.to_bytes());
363 weight_transcript.append_message(b"xG", verification_share.to_bytes());
364 weight_transcript.append_message(b"xH", key_image_share.to_bytes());
365 weight_transcript.append_message(b"rG", nonces[0][0].to_bytes());
366 weight_transcript.append_message(b"rH", nonces[0][1].to_bytes());
367 weight_transcript.append_message(b"c", dfg::Scalar(interim.p).to_repr());
368 weight_transcript.append_message(b"s", share.to_repr());
369 let weight = weight_transcript.challenge(b"weight");
370 let weight = dfg::Scalar(Scalar::from_bytes_mod_order_wide(&weight.into()));
371
372 let part_one = vec![
373 (share, dfg::EdwardsPoint::generator()),
374 (-dfg::Scalar::ONE, nonces[0][0]),
376 (dfg::Scalar(interim.p), verification_share),
377 ];
378
379 let mut part_two = vec![
380 (weight * share, dfg::EdwardsPoint(self.key_image_generator)),
381 (-weight, nonces[0][1]),
383 (weight * dfg::Scalar(interim.p), key_image_share),
384 ];
385
386 let mut all = part_one;
387 all.append(&mut part_two);
388 Ok(all)
389 }
390}