1use std::borrow::Borrow;
13
14use bitcoin::{
15 Amount, FeeRate, OutPoint, ScriptBuf, Sequence, TapSighashType, Transaction, TxIn, TxOut, Txid, Witness
16};
17use bitcoin::hashes::Hash;
18use bitcoin::hex::DisplayHex;
19use bitcoin::secp256k1::{schnorr, Keypair, PublicKey};
20use bitcoin::sighash::{Prevouts, SighashCache};
21
22use bitcoin_ext::{fee, BlockDelta, BlockHeight, KeypairExt, TxOutExt, P2TR_DUST};
23
24use crate::{musig, ServerVtxo, ServerVtxoPolicy, Vtxo, VtxoId, SECP};
25use crate::connectors::construct_multi_connector_tx;
26use crate::vtxo::{Bare, Full};
27
28
29pub const OFFBOARD_TX_OFFBOARD_VOUT: usize = 0;
31pub const OFFBOARD_TX_CONNECTOR_VOUT: usize = 1;
33
34const CONNECTOR_EXPIRY_DELTA: BlockDelta = 144;
36
37
38#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
39#[error("invalid offboard request: {0}")]
40pub struct InvalidOffboardRequestError(&'static str);
41
42#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
44pub struct OffboardRequest {
45 #[serde(with = "bitcoin_ext::serde::encodable")]
47 pub script_pubkey: ScriptBuf,
48 #[serde(rename = "amount_sat", with = "bitcoin::amount::serde::as_sat")]
50 pub net_amount: Amount,
51 pub deduct_fees_from_gross_amount: bool,
54 #[serde(rename = "fee_rate_kwu")]
56 pub fee_rate: FeeRate,
57}
58
59impl OffboardRequest {
60 pub fn validate(&self) -> Result<(), InvalidOffboardRequestError> {
62 if !self.to_txout().is_standard() {
63 return Err(InvalidOffboardRequestError("non-standard output"));
64 }
65 Ok(())
66 }
67
68 pub fn to_txout(&self) -> TxOut {
70 TxOut {
71 script_pubkey: self.script_pubkey.clone(),
72 value: self.net_amount,
73 }
74 }
75}
76
77#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
78#[error("invalid offboard transaction: {0}")]
79pub struct InvalidOffboardTxError(String);
80
81impl<S: Into<String>> From<S> for InvalidOffboardTxError {
82 fn from(v: S) -> Self {
83 Self(v.into())
84 }
85}
86
87impl From<InvalidOffboardRequestError> for InvalidOffboardTxError {
88 fn from(e: InvalidOffboardRequestError) -> Self {
89 InvalidOffboardTxError(format!("invalid offboard request: {:#}", e))
90 }
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
94#[error("invalid partial signature for VTXO {vtxo}")]
95pub struct InvalidUserPartialSignatureError {
96 pub vtxo: VtxoId,
97}
98
99pub struct OffboardForfeitSignatures {
100 pub public_nonces: Vec<musig::PublicNonce>,
101 pub partial_signatures: Vec<musig::PartialSignature>,
102}
103
104pub struct OffboardForfeitResult {
105 pub forfeit_txs: Vec<Transaction>,
106 pub forfeit_vtxos: Vec<ServerVtxo>,
107 pub connector_tx: Option<Transaction>,
108 pub connector_vtxos: Vec<ServerVtxo>,
109}
110
111impl OffboardForfeitResult {
112 pub fn spend_info<'a>(
113 &'a self,
114 inputs: impl Iterator<Item = VtxoId> + 'a,
115 offboard_txid: Txid,
116 ) -> impl Iterator<Item = (VtxoId, Txid)> + 'a {
117 let vtxos_to_ff = inputs.zip(self.forfeit_txs.iter().map(|t| t.compute_txid()));
123
124 let connector = if let Some(ref conn_tx) = self.connector_tx {
125 Some((OutPoint::new(offboard_txid, 1).into(), conn_tx.compute_txid()))
126 } else {
127 None
128 };
129
130 vtxos_to_ff.chain(connector)
131 }
132}
133
134pub struct OffboardForfeitContext<'a, V> {
135 input_vtxos: &'a [V],
136 offboard_tx: &'a Transaction,
137}
138
139impl<'a, V> OffboardForfeitContext<'a, V>
140where
141 V: AsRef<Vtxo<Full>>,
142{
143 pub fn new(input_vtxos: &'a [V], offboard_tx: &'a Transaction) -> Self {
147 assert_ne!(input_vtxos.len(), 0, "no input VTXOs");
148 Self { input_vtxos, offboard_tx }
149 }
150
151 pub fn validate_offboard_tx(
153 &self,
154 req: &OffboardRequest,
155 ) -> Result<(), InvalidOffboardTxError> {
156 let offb_txout = self.offboard_tx.output.get(OFFBOARD_TX_OFFBOARD_VOUT)
157 .ok_or("missing offboard output")?;
158 let exp_txout = req.to_txout();
159
160 if exp_txout.script_pubkey != offb_txout.script_pubkey {
161 return Err(format!(
162 "offboard output scriptPubkey doesn't match: got={}, expected={}",
163 offb_txout.script_pubkey.as_bytes().as_hex(),
164 exp_txout.script_pubkey.as_bytes().as_hex(),
165 ).into());
166 }
167 if exp_txout.value != offb_txout.value {
168 return Err(format!(
169 "offboard output amount doesn't match: got={}, expected={}",
170 offb_txout.value, exp_txout.value,
171 ).into());
172 }
173
174 let conn_txout = self.offboard_tx.output.get(OFFBOARD_TX_CONNECTOR_VOUT)
176 .ok_or("missing connector output")?;
177 let required_conn_value = P2TR_DUST * self.input_vtxos.len() as u64;
178 if conn_txout.value != required_conn_value {
179 return Err(format!(
180 "insufficient connector amount: got={}, need={}",
181 conn_txout.value, required_conn_value,
182 ).into());
183 }
184
185 Ok(())
186 }
187
188 pub fn user_sign_forfeits(
195 &self,
196 keys: &[impl Borrow<Keypair>],
197 server_nonces: &[musig::PublicNonce],
198 ) -> OffboardForfeitSignatures {
199 assert_eq!(self.input_vtxos.len(), keys.len(), "wrong number of keys");
200 assert_eq!(self.input_vtxos.len(), server_nonces.len(), "wrong number of nonces");
201 assert_ne!(self.input_vtxos.len(), 0, "no inputs");
202
203 let mut pub_nonces = Vec::with_capacity(self.input_vtxos.len());
204 let mut part_sigs = Vec::with_capacity(self.input_vtxos.len());
205 let offboard_txid = self.offboard_tx.compute_txid();
206 let connector_prev = OutPoint::new(offboard_txid, OFFBOARD_TX_CONNECTOR_VOUT as u32);
207 let connector_txout = self.offboard_tx.output.get(OFFBOARD_TX_CONNECTOR_VOUT)
208 .expect("invalid offboard tx");
209
210 if self.input_vtxos.len() == 1 {
211 let (nonce, sig) = user_sign_vtxo_forfeit_input(
212 self.input_vtxos[0].as_ref(),
213 keys[0].borrow(),
214 connector_prev,
215 connector_txout,
216 &server_nonces[0],
217 );
218 pub_nonces.push(nonce);
219 part_sigs.push(sig);
220 } else {
221 let connector_tx = construct_multi_connector_tx(
225 connector_prev, self.input_vtxos.len(), &connector_txout.script_pubkey,
226 );
227 let connector_txid = connector_tx.compute_txid();
228
229 let iter = self.input_vtxos.iter().zip(keys).zip(server_nonces);
231 for (i, ((vtxo, key), server_nonce)) in iter.enumerate() {
232 let connector = OutPoint::new(connector_txid, i as u32);
233 let (nonce, sig) = user_sign_vtxo_forfeit_input(
234 vtxo.as_ref(), key.borrow(), connector, connector_txout, server_nonce,
235 );
236 pub_nonces.push(nonce);
237 part_sigs.push(sig);
238 }
239 }
240
241 OffboardForfeitSignatures {
242 public_nonces: pub_nonces,
243 partial_signatures: part_sigs,
244 }
245 }
246
247 pub fn finish(
252 &self,
253 server_key: &Keypair,
254 connector_key: &Keypair,
255 server_pub_nonces: &[musig::PublicNonce],
256 server_sec_nonces: Vec<musig::SecretNonce>,
257 user_pub_nonces: &[musig::PublicNonce],
258 user_partial_sigs: &[musig::PartialSignature],
259 ) -> Result<OffboardForfeitResult, InvalidUserPartialSignatureError> {
260 assert_eq!(self.input_vtxos.len(), server_pub_nonces.len());
261 assert_eq!(self.input_vtxos.len(), server_sec_nonces.len());
262 assert_eq!(self.input_vtxos.len(), user_pub_nonces.len());
263 assert_eq!(self.input_vtxos.len(), user_partial_sigs.len());
264 assert_ne!(self.input_vtxos.len(), 0, "no inputs");
265
266 let offboard_txid = self.offboard_tx.compute_txid();
267 let connector_prev = OutPoint::new(offboard_txid, OFFBOARD_TX_CONNECTOR_VOUT as u32);
268 let connector_txout = self.offboard_tx.output.get(OFFBOARD_TX_CONNECTOR_VOUT)
269 .expect("invalid offboard tx");
270 let tweaked_connector_key = connector_key.for_keyspend_only(&*SECP);
271
272 let mut ret = OffboardForfeitResult {
273 forfeit_txs: Vec::with_capacity(self.input_vtxos.len()),
274 forfeit_vtxos: Vec::with_capacity(self.input_vtxos.len()),
275 connector_tx: None,
276 connector_vtxos: Vec::new(),
277 };
278
279 if self.input_vtxos.len() == 1 {
280 let vtxo = self.input_vtxos[0].as_ref();
281 let tx = server_check_finalize_forfeit_tx(
282 vtxo,
283 server_key,
284 &tweaked_connector_key,
285 connector_prev,
286 connector_txout,
287 (&server_pub_nonces[0], server_sec_nonces.into_iter().next().unwrap()),
288 &user_pub_nonces[0],
289 &user_partial_sigs[0],
290 ).ok_or_else(|| InvalidUserPartialSignatureError { vtxo: vtxo.id() })?;
291 ret.forfeit_vtxos = vec![construct_forfeit_vtxo(vtxo, &tx)];
292 ret.forfeit_txs.push(tx);
293 ret.connector_vtxos = vec![construct_connector_vtxo_single(vtxo, offboard_txid)];
294 } else {
295 let connector_tx = construct_multi_connector_tx(
299 connector_prev, self.input_vtxos.len(), &connector_txout.script_pubkey,
300 );
301 let connector_txid = connector_tx.compute_txid();
302
303 ret.connector_tx = Some(connector_tx);
304 ret.connector_vtxos = Vec::with_capacity(self.input_vtxos.len() + 1);
305 ret.connector_vtxos.push(construct_connector_vtxo_fanout_root(
306 offboard_txid,
307 self.input_vtxos.iter().map(|v| v.as_ref().expiry_height()).max().unwrap(),
308 self.input_vtxos[0].as_ref().server_pubkey(), self.input_vtxos.len(),
310 ));
311
312 let iter = self.input_vtxos.iter()
314 .zip(server_pub_nonces)
315 .zip(server_sec_nonces)
316 .zip(user_pub_nonces)
317 .zip(user_partial_sigs);
318 for (i, ((((vtxo, server_pub), server_sec), user_pub), user_part)) in iter.enumerate() {
319 let vtxo = vtxo.as_ref();
320 let connector = OutPoint::new(connector_txid, i as u32);
321 let tx = server_check_finalize_forfeit_tx(
322 vtxo,
323 server_key,
324 &tweaked_connector_key,
325 connector,
326 connector_txout,
327 (server_pub, server_sec),
328 user_pub,
329 user_part,
330 ).ok_or_else(|| InvalidUserPartialSignatureError { vtxo: vtxo.as_ref().id() })?;
331
332 ret.forfeit_vtxos.push(construct_forfeit_vtxo(vtxo, &tx));
333 ret.forfeit_txs.push(tx);
334 ret.connector_vtxos.push(construct_connector_vtxo_fanout_leaf(
335 vtxo, i, offboard_txid, connector_txid,
336 ));
337 }
338 }
339
340 Ok(ret)
341 }
342}
343
344fn construct_forfeit_vtxo<G>(
345 input: &Vtxo<G>,
346 forfeit_tx: &Transaction,
347) -> ServerVtxo<Bare> {
348 ServerVtxo {
349 point: OutPoint::new(forfeit_tx.compute_txid(), 0),
350 policy: ServerVtxoPolicy::ServerOwned,
351 amount: input.amount,
352 anchor_point: input.anchor_point,
353 server_pubkey: input.server_pubkey,
354 expiry_height: input.expiry_height,
355 exit_delta: input.exit_delta,
356 genesis: Bare,
357 }
358}
359
360fn construct_connector_vtxo_single<G>(
364 input: &Vtxo<G>,
365 offboard_txid: Txid,
366) -> ServerVtxo<Bare> {
367 let point = OutPoint::new(offboard_txid, 1);
368 ServerVtxo {
369 anchor_point: point.clone(),
371 point: point,
372 policy: ServerVtxoPolicy::ServerOwned,
373 amount: P2TR_DUST,
374 server_pubkey: input.server_pubkey,
375 expiry_height: input.expiry_height + CONNECTOR_EXPIRY_DELTA as u32,
376 exit_delta: 0,
377 genesis: Bare,
378 }
379}
380
381fn construct_connector_vtxo_fanout_root(
386 offboard_txid: Txid,
387 max_expiry_height: BlockHeight,
388 server_pubkey: PublicKey,
389 nb_vtxos: usize,
390) -> ServerVtxo<Bare> {
391 let point = OutPoint::new(offboard_txid, 1);
392 ServerVtxo {
393 anchor_point: point.clone(),
395 point: point,
396 policy: ServerVtxoPolicy::ServerOwned,
397 amount: P2TR_DUST * nb_vtxos as u64,
398 server_pubkey: server_pubkey,
399 expiry_height: max_expiry_height + CONNECTOR_EXPIRY_DELTA as u32,
400 exit_delta: 0,
401 genesis: Bare,
402 }
403}
404
405fn construct_connector_vtxo_fanout_leaf<G>(
409 input: &Vtxo<G>,
410 input_idx: usize,
411 offboard_txid: Txid,
412 connector_txid: Txid,
413) -> ServerVtxo<Bare> {
414 ServerVtxo {
415 point: OutPoint::new(connector_txid, input_idx as u32),
416 anchor_point: OutPoint::new(offboard_txid, 1),
417 policy: ServerVtxoPolicy::ServerOwned,
418 amount: P2TR_DUST,
419 server_pubkey: input.server_pubkey,
420 expiry_height: input.expiry_height + CONNECTOR_EXPIRY_DELTA as u32,
421 exit_delta: 0,
422 genesis: Bare,
423 }
424}
425
426fn user_sign_vtxo_forfeit_input<G: Sync + Send>(
427 vtxo: &Vtxo<G>,
428 key: &Keypair,
429 connector: OutPoint,
430 connector_txout: &TxOut,
431 server_nonce: &musig::PublicNonce,
432) -> (musig::PublicNonce, musig::PartialSignature) {
433 let tx = create_offboard_forfeit_tx(vtxo, connector, None, None);
434 let mut shc = SighashCache::new(&tx);
435 let prevouts = [&vtxo.txout(), &connector_txout];
436 let sighash = shc.taproot_key_spend_signature_hash(
437 0, &Prevouts::All(&prevouts), TapSighashType::Default,
438 ).expect("provided all prevouts");
439 let tweak = vtxo.output_taproot().tap_tweak().to_byte_array();
440 let (pub_nonce, partial_sig) = musig::deterministic_partial_sign(
441 key,
442 [vtxo.server_pubkey()],
443 &[server_nonce],
444 sighash.to_byte_array(),
445 Some(tweak),
446 );
447 debug_assert!({
448 let (key_agg, _) = musig::tweaked_key_agg(
449 [vtxo.user_pubkey(), vtxo.server_pubkey()], tweak,
450 );
451 let agg_nonce = musig::nonce_agg(&[&pub_nonce, server_nonce]);
452 let ff_session = musig::Session::new(
453 &key_agg,
454 agg_nonce,
455 &sighash.to_byte_array(),
456 );
457 ff_session.partial_verify(
458 &key_agg,
459 &partial_sig,
460 &pub_nonce,
461 musig::pubkey_to(vtxo.user_pubkey()),
462 )
463 }, "invalid partial offboard forfeit signature");
464
465 (pub_nonce, partial_sig)
466}
467
468fn server_check_finalize_forfeit_tx<G: Sync + Send>(
472 vtxo: &Vtxo<G>,
473 server_key: &Keypair,
474 tweaked_connector_key: &Keypair,
475 connector: OutPoint,
476 connector_txout: &TxOut,
477 server_nonces: (&musig::PublicNonce, musig::SecretNonce),
478 user_nonce: &musig::PublicNonce,
479 user_partial_sig: &musig::PartialSignature,
480) -> Option<Transaction> {
481 let mut tx = create_offboard_forfeit_tx(vtxo, connector, None, None);
482 let mut shc = SighashCache::new(&tx);
483 let prevouts = [&vtxo.txout(), &connector_txout];
484 let vtxo_sig = {
485 let sighash = shc.taproot_key_spend_signature_hash(
486 0, &Prevouts::All(&prevouts), TapSighashType::Default,
487 ).expect("provided all prevouts");
488 let vtxo_taproot = vtxo.output_taproot();
489 let tweak = vtxo_taproot.tap_tweak().to_byte_array();
490 let agg_nonce = musig::nonce_agg(&[user_nonce, server_nonces.0]);
491
492 let (_our_part_sig, final_sig) = musig::partial_sign(
496 [vtxo.user_pubkey(), vtxo.server_pubkey()],
497 agg_nonce,
498 server_key,
499 server_nonces.1,
500 sighash.to_byte_array(),
501 Some(tweak),
502 Some(&[user_partial_sig]),
503 );
504 debug_assert!({
505 let (key_agg, _) = musig::tweaked_key_agg(
506 [vtxo.user_pubkey(), vtxo.server_pubkey()], tweak,
507 );
508 let ff_session = musig::Session::new(
509 &key_agg,
510 agg_nonce,
511 &sighash.to_byte_array(),
512 );
513 ff_session.partial_verify(
514 &key_agg,
515 &_our_part_sig,
516 server_nonces.0,
517 musig::pubkey_to(vtxo.server_pubkey()),
518 )
519 }, "invalid partial offboard forfeit signature");
520 let final_sig = final_sig.expect("we provided other sigs");
521 SECP.verify_schnorr(
522 &final_sig, &sighash.into(), vtxo_taproot.output_key().as_x_only_public_key(),
523 ).ok()?;
524 final_sig
525 };
526
527 let conn_sig = {
528 let sighash = shc.taproot_key_spend_signature_hash(
529 1, &Prevouts::All(&prevouts), TapSighashType::Default,
530 ).expect("provided all prevouts");
531 SECP.sign_schnorr_with_aux_rand(&sighash.into(), tweaked_connector_key, &rand::random())
532 };
533
534 tx.input[0].witness = Witness::from_slice(&[&vtxo_sig[..]]);
535 tx.input[1].witness = Witness::from_slice(&[&conn_sig[..]]);
536 debug_assert_eq!(tx,
537 create_offboard_forfeit_tx(vtxo, connector, Some(&vtxo_sig), Some(&conn_sig)),
538 );
539
540 #[cfg(test)]
541 {
542 let prevs = [vtxo.txout(), connector_txout.clone()];
543 if let Err(e) = crate::test_util::verify_tx(&prevs, 0, &tx) {
544 println!("forfeit tx for VTXO {} failed: {}", vtxo.id(), e);
545 panic!("forfeit tx for VTXO {} failed: {}", vtxo.id(), e);
546 }
547 }
548
549 Some(tx)
550}
551
552fn create_offboard_forfeit_tx<G: Sync + Send>(
553 vtxo: &Vtxo<G>,
554 connector: OutPoint,
555 vtxo_sig: Option<&schnorr::Signature>,
556 conn_sig: Option<&schnorr::Signature>,
557) -> Transaction {
558 Transaction {
559 version: bitcoin::transaction::Version(3),
560 lock_time: bitcoin::absolute::LockTime::ZERO,
561 input: vec![
562 TxIn {
563 previous_output: vtxo.point(),
564 sequence: Sequence::MAX,
565 script_sig: ScriptBuf::new(),
566 witness: vtxo_sig.map(|s| Witness::from_slice(&[&s[..]])).unwrap_or_default(),
567 },
568 TxIn {
569 previous_output: connector,
570 sequence: Sequence::MAX,
571 script_sig: ScriptBuf::new(),
572 witness: conn_sig.map(|s| Witness::from_slice(&[&s[..]])).unwrap_or_default(),
573 },
574 ],
575 output: vec![
576 TxOut {
577 value: vtxo.amount() + P2TR_DUST,
579 script_pubkey: ScriptBuf::new_p2tr(
580 &*SECP, vtxo.server_pubkey().x_only_public_key().0, None,
581 ),
582 },
583 fee::fee_anchor(),
584 ],
585 }
586}
587
588#[cfg(test)]
589mod test {
590 use std::str::FromStr;
591 use bitcoin::hex::FromHex;
592 use bitcoin::secp256k1::PublicKey;
593 use crate::test_util::dummy::{random_utxo, DummyTestVtxoSpec};
594 use super::*;
595
596 #[test]
597 fn test_offboard_forfeit() {
598 let server_key = Keypair::new(&*SECP, &mut bitcoin::secp256k1::rand::thread_rng());
599
600 let req_pk = PublicKey::from_str(
601 "02271fba79f590251099b07fa0393b4c55d5e50cd8fca2e2822b619f8aabf93b74",
602 ).unwrap();
603 let req = OffboardRequest {
604 script_pubkey: ScriptBuf::new_p2tr(&*SECP, req_pk.x_only_public_key().0, None),
605 net_amount: Amount::ONE_BTC,
606 deduct_fees_from_gross_amount: true,
607 fee_rate: FeeRate::from_sat_per_kwu(100),
608 };
609
610 let input1_key = Keypair::new(&*SECP, &mut bitcoin::secp256k1::rand::thread_rng());
611 let (_, input1) = DummyTestVtxoSpec {
612 user_keypair: input1_key,
613 server_keypair: server_key,
614 ..Default::default()
615 }.build();
616 let input2_key = Keypair::new(&*SECP, &mut bitcoin::secp256k1::rand::thread_rng());
617 let (_, input2) = DummyTestVtxoSpec {
618 user_keypair: input2_key,
619 server_keypair: server_key,
620 ..Default::default()
621 }.build();
622
623 let conn_key = Keypair::new(&*SECP, &mut bitcoin::secp256k1::rand::thread_rng());
624 let conn_spk = ScriptBuf::new_p2tr(
625 &*SECP, conn_key.public_key().x_only_public_key().0, None,
626 );
627
628 let change_amt = Amount::ONE_BTC * 2;
629 let offboard_tx = Transaction {
630 version: bitcoin::transaction::Version(3),
631 lock_time: bitcoin::absolute::LockTime::ZERO,
632 input: vec![
633 TxIn {
634 previous_output: random_utxo(),
635 sequence: Sequence::MAX,
636 script_sig: ScriptBuf::new(),
637 witness: Witness::new(),
638 },
639 ],
640 output: vec![
641 req.to_txout(),
643 TxOut {
645 script_pubkey: conn_spk.clone(),
646 value: P2TR_DUST * 2,
647 },
648 TxOut {
650 script_pubkey: ScriptBuf::from_bytes(Vec::<u8>::from_hex(
651 "512077243a077f583b197d36caac516b0c7e4319c7b6a2316c25972f44dfbf20fd09"
652 ).unwrap()),
653 value: change_amt,
654 },
655 ],
656 };
657
658 let inputs = [&input1, &input2];
659 let ctx = OffboardForfeitContext::new(&inputs, &offboard_tx);
660 ctx.validate_offboard_tx(&req).unwrap();
661
662 let (server_sec_nonces, server_pub_nonces) = (0..2).map(|_| {
663 musig::nonce_pair(&server_key)
664 }).collect::<(Vec<_>, Vec<_>)>();
665
666 let user_sigs = ctx.user_sign_forfeits(&[&input1_key, &input2_key], &server_pub_nonces);
667
668 ctx.finish(
669 &server_key,
670 &conn_key,
671 &server_pub_nonces,
672 server_sec_nonces,
673 &user_sigs.public_nonces,
674 &user_sigs.partial_signatures,
675 ).unwrap();
676 }
677}