1use std::borrow::Borrow;
17
18use bitcoin::{
19 Amount, FeeRate, OutPoint, ScriptBuf, Sequence, TapSighashType, Transaction, TxIn, TxOut, Txid,
20 Witness,
21};
22use bitcoin::hashes::Hash;
23use bitcoin::hex::DisplayHex;
24use bitcoin::secp256k1::{schnorr, Keypair, PublicKey};
25use bitcoin::sighash::{Prevouts, SighashCache};
26
27use bitcoin_ext::{fee, BlockDelta, BlockHeight, KeypairExt, NonStandardOutput, TxOutExt, P2TR_DUST};
28
29use crate::{musig, ServerVtxo, ServerVtxoPolicy, Vtxo, VtxoId, SECP};
30use crate::connectors::construct_multi_connector_fanout_tx;
31use crate::vtxo::{Bare, Full};
32
33
34pub const OFFBOARD_TX_OFFBOARD_VOUT: usize = 0;
36pub const OFFBOARD_TX_CONNECTOR_VOUT: usize = 1;
38
39const CONNECTOR_EXPIRY_DELTA: BlockDelta = 144;
41
42
43#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
44#[error("invalid offboard request: {0}")]
45pub struct InvalidOffboardRequestError(String);
46
47impl From<NonStandardOutput> for InvalidOffboardRequestError {
48 fn from(err: NonStandardOutput) -> Self {
49 Self(format!("{:#}", err))
50 }
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
55pub struct OffboardRequest {
56 #[serde(with = "bitcoin_ext::serde::encodable")]
58 pub script_pubkey: ScriptBuf,
59 #[serde(rename = "amount_sat", with = "bitcoin::amount::serde::as_sat")]
61 pub net_amount: Amount,
62 pub deduct_fees_from_gross_amount: bool,
65 #[serde(rename = "fee_rate_kwu")]
67 pub fee_rate: FeeRate,
68}
69
70impl OffboardRequest {
71 pub fn validate(&self) -> Result<(), InvalidOffboardRequestError> {
73 Ok(self.to_txout().check_standard()?)
74 }
75
76 pub fn to_txout(&self) -> TxOut {
78 TxOut {
79 script_pubkey: self.script_pubkey.clone(),
80 value: self.net_amount,
81 }
82 }
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
86#[error("invalid offboard transaction: {0}")]
87pub struct InvalidOffboardTxError(String);
88
89impl<S: Into<String>> From<S> for InvalidOffboardTxError {
90 fn from(v: S) -> Self {
91 Self(v.into())
92 }
93}
94
95impl From<InvalidOffboardRequestError> for InvalidOffboardTxError {
96 fn from(e: InvalidOffboardRequestError) -> Self {
97 InvalidOffboardTxError(format!("invalid offboard request: {:#}", e))
98 }
99}
100
101#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
102#[error("invalid partial signature for VTXO {vtxo}")]
103pub struct InvalidUserPartialSignatureError {
104 pub vtxo: VtxoId,
105}
106
107#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
115pub enum OffboardForfeitError {
116 #[error("offboard has no input VTXOs")]
118 NoInputs,
119 #[error("wrong number of {vector}: expected {expected}, received {received}")]
121 WrongCount {
122 vector: &'static str,
124 expected: usize,
126 received: usize,
128 },
129 #[error("offboard tx has no connector output")]
133 MissingConnectorOutput,
134 #[error(transparent)]
135 InvalidUserPartialSignature(#[from] InvalidUserPartialSignatureError),
136}
137
138impl OffboardForfeitError {
139 pub fn check_count(
143 vector: &'static str,
144 expected: usize,
145 received: usize,
146 ) -> Result<(), OffboardForfeitError> {
147 if expected == received {
148 Ok(())
149 } else {
150 Err(OffboardForfeitError::WrongCount { vector, expected, received })
151 }
152 }
153}
154
155pub struct OffboardForfeitSignatures {
156 pub public_nonces: Vec<musig::PublicNonce>,
157 pub partial_signatures: Vec<musig::PartialSignature>,
158}
159
160pub struct OffboardForfeitResult {
161 pub forfeit_txs: Vec<Transaction>,
162 pub forfeit_vtxos: Vec<ServerVtxo>,
163 pub connector_tx: Option<Transaction>,
164 pub connector_vtxos: Vec<ServerVtxo>,
165}
166
167impl OffboardForfeitResult {
168 pub fn spend_info<'a>(
169 &'a self,
170 inputs: impl Iterator<Item = VtxoId> + 'a,
171 offboard_txid: Txid,
172 ) -> impl Iterator<Item = (VtxoId, Txid)> + 'a {
173 let vtxos_to_ff = inputs.zip(self.forfeit_txs.iter().map(|t| t.compute_txid()));
179
180 let connector = if let Some(ref conn_tx) = self.connector_tx {
181 Some((OutPoint::new(offboard_txid, 1).into(), conn_tx.compute_txid()))
182 } else {
183 None
184 };
185
186 vtxos_to_ff.chain(connector)
187 }
188}
189
190pub struct OffboardForfeitContext<'a, V> {
191 input_vtxos: &'a [V],
192 offboard_tx: &'a Transaction,
193}
194
195impl<'a, V> OffboardForfeitContext<'a, V> {
200 pub fn new(
206 input_vtxos: &'a [V],
207 offboard_tx: &'a Transaction,
208 ) -> Result<Self, OffboardForfeitError> {
209 if input_vtxos.is_empty() {
210 return Err(OffboardForfeitError::NoInputs);
211 }
212 Ok(Self { input_vtxos, offboard_tx })
213 }
214
215 pub fn validate_offboard_tx(
217 &self,
218 req: &OffboardRequest,
219 ) -> Result<(), InvalidOffboardTxError> {
220 let offb_txout = self.offboard_tx.output.get(OFFBOARD_TX_OFFBOARD_VOUT)
221 .ok_or("missing offboard output")?;
222 let exp_txout = req.to_txout();
223
224 if exp_txout.script_pubkey != offb_txout.script_pubkey {
225 return Err(format!(
226 "offboard output scriptPubkey doesn't match: got={}, expected={}",
227 offb_txout.script_pubkey.as_bytes().as_hex(),
228 exp_txout.script_pubkey.as_bytes().as_hex(),
229 ).into());
230 }
231 if exp_txout.value != offb_txout.value {
232 return Err(format!(
233 "offboard output amount doesn't match: got={}, expected={}",
234 offb_txout.value, exp_txout.value,
235 ).into());
236 }
237
238 let conn_txout = self.offboard_tx.output.get(OFFBOARD_TX_CONNECTOR_VOUT)
240 .ok_or("missing connector output")?;
241 let required_conn_value = P2TR_DUST * self.input_vtxos.len() as u64;
242 if conn_txout.value != required_conn_value {
243 return Err(format!(
244 "insufficient connector amount: got={}, need={}",
245 conn_txout.value, required_conn_value,
246 ).into());
247 }
248
249 Ok(())
250 }
251}
252
253impl<'a, V> OffboardForfeitContext<'a, V>
254where
255 V: AsRef<Vtxo<Full>>,
256{
257 pub fn user_sign_forfeits(
268 &self,
269 keys: &[impl Borrow<Keypair>],
270 server_nonces: &[musig::PublicNonce],
271 ) -> Result<OffboardForfeitSignatures, OffboardForfeitError> {
272 OffboardForfeitError::check_count("keys", self.input_vtxos.len(), keys.len())?;
273 OffboardForfeitError::check_count(
274 "forfeit cosign nonces", self.input_vtxos.len(), server_nonces.len(),
275 )?;
276
277 let mut pub_nonces = Vec::with_capacity(self.input_vtxos.len());
278 let mut part_sigs = Vec::with_capacity(self.input_vtxos.len());
279 let offboard_txid = self.offboard_tx.compute_txid();
280 let connector_fanout_prev = OutPoint::new(offboard_txid, OFFBOARD_TX_CONNECTOR_VOUT as u32);
281 let connector_fanout_txout = self.offboard_tx.output.get(OFFBOARD_TX_CONNECTOR_VOUT)
282 .ok_or(OffboardForfeitError::MissingConnectorOutput)?;
283
284 if self.input_vtxos.len() == 1 {
285 let (nonce, sig) = user_sign_vtxo_forfeit_input(
286 self.input_vtxos[0].as_ref(),
287 keys[0].borrow(),
288 connector_fanout_prev,
289 connector_fanout_txout,
290 &server_nonces[0],
291 );
292 pub_nonces.push(nonce);
293 part_sigs.push(sig);
294 } else {
295 let connector_tx = construct_multi_connector_fanout_tx(
299 connector_fanout_prev,
300 self.input_vtxos.len(),
301 &connector_fanout_txout.script_pubkey,
302 );
303 let connector_txid = connector_tx.compute_txid();
304
305 let connector_txout = TxOut {
310 script_pubkey: connector_fanout_txout.script_pubkey.clone(),
311 value: P2TR_DUST,
312 };
313 let iter = self.input_vtxos.iter().zip(keys).zip(server_nonces);
314 for (i, ((vtxo, key), server_nonce)) in iter.enumerate() {
315 let connector = OutPoint::new(connector_txid, u32::try_from(i).expect("connector index fits in u32"));
316 let (nonce, sig) = user_sign_vtxo_forfeit_input(
317 vtxo.as_ref(), key.borrow(), connector, &connector_txout, server_nonce,
318 );
319 pub_nonces.push(nonce);
320 part_sigs.push(sig);
321 }
322 }
323
324 Ok(OffboardForfeitSignatures {
325 public_nonces: pub_nonces,
326 partial_signatures: part_sigs,
327 })
328 }
329
330 pub fn finish(
339 &self,
340 server_key: &Keypair,
341 server_pub_nonces: &[musig::PublicNonce],
342 server_sec_nonces: Vec<musig::SecretNonce>,
343 user_pub_nonces: &[musig::PublicNonce],
344 user_partial_sigs: &[musig::PartialSignature],
345 ) -> Result<OffboardForfeitResult, OffboardForfeitError> {
346 let inputs = self.input_vtxos.len();
347 OffboardForfeitError::check_count("server public nonces", inputs, server_pub_nonces.len())?;
348 OffboardForfeitError::check_count("server secret nonces", inputs, server_sec_nonces.len())?;
349 OffboardForfeitError::check_count("user public nonces", inputs, user_pub_nonces.len())?;
350 OffboardForfeitError::check_count("user partial signatures", inputs, user_partial_sigs.len())?;
351
352 let offboard_txid = self.offboard_tx.compute_txid();
353 let connector_fanout_prev = OutPoint::new(offboard_txid, OFFBOARD_TX_CONNECTOR_VOUT as u32);
354 let connector_fanout_txout = self.offboard_tx.output.get(OFFBOARD_TX_CONNECTOR_VOUT)
355 .ok_or(OffboardForfeitError::MissingConnectorOutput)?;
356 let tweaked_connector_key = server_key.for_keyspend_only(&*SECP);
357
358 let mut ret = OffboardForfeitResult {
359 forfeit_txs: Vec::with_capacity(self.input_vtxos.len()),
360 forfeit_vtxos: Vec::with_capacity(self.input_vtxos.len()),
361 connector_tx: None,
362 connector_vtxos: Vec::new(),
363 };
364
365 if self.input_vtxos.len() == 1 {
366 let vtxo = self.input_vtxos[0].as_ref();
367 let tx = server_check_finalize_forfeit_tx(
368 vtxo,
369 server_key,
370 &tweaked_connector_key,
371 connector_fanout_prev,
372 connector_fanout_txout,
373 (&server_pub_nonces[0], server_sec_nonces.into_iter().next().unwrap()),
374 &user_pub_nonces[0],
375 &user_partial_sigs[0],
376 ).ok_or_else(|| InvalidUserPartialSignatureError { vtxo: vtxo.id() })?;
377 ret.forfeit_vtxos = vec![construct_forfeit_vtxo(vtxo, &tx)];
378 ret.forfeit_txs.push(tx);
379 ret.connector_vtxos = vec![construct_connector_vtxo_single(vtxo, offboard_txid)];
380 } else {
381 let connector_tx = {
385 let mut tx = construct_multi_connector_fanout_tx(
386 connector_fanout_prev,
387 self.input_vtxos.len(),
388 &connector_fanout_txout.script_pubkey,
389 );
390
391 let sighash = SighashCache::new(&tx).taproot_key_spend_signature_hash(
395 0, &Prevouts::All(&[connector_fanout_txout]), TapSighashType::Default,
396 ).expect("provided the connector prevout");
397 let sig = SECP.sign_schnorr_with_aux_rand(
398 &sighash.into(), &tweaked_connector_key, &rand::random(),
399 );
400 tx.input[0].witness = Witness::from_slice(&[&sig[..]]);
401
402 tx
403 };
404 let connector_txid = connector_tx.compute_txid();
405
406 ret.connector_tx = Some(connector_tx);
407 ret.connector_vtxos = Vec::with_capacity(self.input_vtxos.len().saturating_add(1));
408 ret.connector_vtxos.push(construct_connector_vtxo_fanout_root(
409 offboard_txid,
410 self.input_vtxos.iter().map(|v| v.as_ref().expiry_height()).max().unwrap(),
411 self.input_vtxos[0].as_ref().server_pubkey(), self.input_vtxos.len(),
413 ));
414
415 let connector_txout = TxOut {
420 script_pubkey: connector_fanout_txout.script_pubkey.clone(),
421 value: P2TR_DUST,
422 };
423 let iter = self.input_vtxos.iter()
424 .zip(server_pub_nonces)
425 .zip(server_sec_nonces)
426 .zip(user_pub_nonces)
427 .zip(user_partial_sigs);
428 for (i, ((((vtxo, server_pub), server_sec), user_pub), user_part)) in iter.enumerate() {
429 let vtxo = vtxo.as_ref();
430 let connector = OutPoint::new(connector_txid, u32::try_from(i).expect("connector index fits in u32"));
431 let tx = server_check_finalize_forfeit_tx(
432 vtxo,
433 server_key,
434 &tweaked_connector_key,
435 connector,
436 &connector_txout,
437 (server_pub, server_sec),
438 user_pub,
439 user_part,
440 ).ok_or_else(|| InvalidUserPartialSignatureError { vtxo: vtxo.as_ref().id() })?;
441
442 ret.forfeit_vtxos.push(construct_forfeit_vtxo(vtxo, &tx));
443 ret.forfeit_txs.push(tx);
444 ret.connector_vtxos.push(construct_connector_vtxo_fanout_leaf(
445 vtxo, i, offboard_txid, connector_txid,
446 ));
447 }
448 }
449
450 Ok(ret)
451 }
452}
453
454fn construct_forfeit_vtxo<G>(
455 input: &Vtxo<G>,
456 forfeit_tx: &Transaction,
457) -> ServerVtxo<Bare> {
458 ServerVtxo {
459 point: OutPoint::new(forfeit_tx.compute_txid(), 0),
460 policy: ServerVtxoPolicy::ServerOwned,
461 amount: forfeit_tx.output[0].value,
464 anchor_point: input.anchor_point,
465 server_pubkey: input.server_pubkey,
466 expiry_height: input.expiry_height,
467 exit_delta: input.exit_delta,
468 genesis: Bare,
469 }
470}
471
472fn construct_connector_vtxo_single<G>(
476 input: &Vtxo<G>,
477 offboard_txid: Txid,
478) -> ServerVtxo<Bare> {
479 let point = OutPoint::new(offboard_txid, 1);
480 ServerVtxo {
481 anchor_point: point.clone(),
483 point: point,
484 policy: ServerVtxoPolicy::ServerOwned,
485 amount: P2TR_DUST,
486 server_pubkey: input.server_pubkey,
487 expiry_height: input.expiry_height.checked_add(CONNECTOR_EXPIRY_DELTA as u32)
488 .expect("expiry_height + CONNECTOR_EXPIRY_DELTA fits in u32 by MAX_BLOCK_HEIGHT invariant"),
489 exit_delta: 0,
490 genesis: Bare,
491 }
492}
493
494fn construct_connector_vtxo_fanout_root(
499 offboard_txid: Txid,
500 max_expiry_height: BlockHeight,
501 server_pubkey: PublicKey,
502 nb_vtxos: usize,
503) -> ServerVtxo<Bare> {
504 let point = OutPoint::new(offboard_txid, 1);
505 ServerVtxo {
506 anchor_point: point.clone(),
508 point: point,
509 policy: ServerVtxoPolicy::ServerOwned,
510 amount: P2TR_DUST.checked_mul(nb_vtxos as u64)
511 .expect("P2TR_DUST * nb_vtxos fits in u64 by VTXO-count and dust bounds"),
512 server_pubkey: server_pubkey,
513 expiry_height: max_expiry_height.checked_add(CONNECTOR_EXPIRY_DELTA as u32)
514 .expect("max_expiry_height + CONNECTOR_EXPIRY_DELTA fits in u32 by MAX_BLOCK_HEIGHT invariant"),
515 exit_delta: 0,
516 genesis: Bare,
517 }
518}
519
520fn construct_connector_vtxo_fanout_leaf<G>(
524 input: &Vtxo<G>,
525 input_idx: usize,
526 offboard_txid: Txid,
527 connector_txid: Txid,
528) -> ServerVtxo<Bare> {
529 ServerVtxo {
530 point: OutPoint::new(connector_txid, u32::try_from(input_idx).expect("input index fits in u32")),
531 anchor_point: OutPoint::new(offboard_txid, 1),
532 policy: ServerVtxoPolicy::ServerOwned,
533 amount: P2TR_DUST,
534 server_pubkey: input.server_pubkey,
535 expiry_height: input.expiry_height.checked_add(CONNECTOR_EXPIRY_DELTA as u32)
536 .expect("expiry_height + CONNECTOR_EXPIRY_DELTA fits in u32 by MAX_BLOCK_HEIGHT invariant"),
537 exit_delta: 0,
538 genesis: Bare,
539 }
540}
541
542fn user_sign_vtxo_forfeit_input<G: Sync + Send>(
543 vtxo: &Vtxo<G>,
544 key: &Keypair,
545 connector: OutPoint,
546 connector_txout: &TxOut,
547 server_nonce: &musig::PublicNonce,
548) -> (musig::PublicNonce, musig::PartialSignature) {
549 let tx = create_offboard_forfeit_tx(vtxo, connector, None, None);
550 let mut shc = SighashCache::new(&tx);
551 let prevouts = [&vtxo.txout(), &connector_txout];
552 let sighash = shc.taproot_key_spend_signature_hash(
553 0, &Prevouts::All(&prevouts), TapSighashType::Default,
554 ).expect("provided all prevouts");
555 let tweak = vtxo.output_taproot().tap_tweak().to_byte_array();
556 let (pub_nonce, partial_sig) = musig::deterministic_partial_sign(
557 key,
558 [vtxo.server_pubkey()],
559 &[server_nonce],
560 sighash.to_byte_array(),
561 Some(tweak),
562 );
563 debug_assert!({
564 let (key_agg, _) = musig::tweaked_key_agg(
565 [vtxo.user_pubkey(), vtxo.server_pubkey()], tweak,
566 );
567 let agg_nonce = musig::nonce_agg(&[&pub_nonce, server_nonce]);
568 let ff_session = musig::Session::new(
569 &key_agg,
570 agg_nonce,
571 &sighash.to_byte_array(),
572 );
573 ff_session.partial_verify(
574 &key_agg,
575 &partial_sig,
576 &pub_nonce,
577 musig::pubkey_to(vtxo.user_pubkey()),
578 )
579 }, "invalid partial offboard forfeit signature");
580
581 (pub_nonce, partial_sig)
582}
583
584fn server_check_finalize_forfeit_tx<G: Sync + Send>(
588 vtxo: &Vtxo<G>,
589 server_key: &Keypair,
590 tweaked_connector_key: &Keypair,
591 connector: OutPoint,
592 connector_txout: &TxOut,
593 server_nonces: (&musig::PublicNonce, musig::SecretNonce),
594 user_nonce: &musig::PublicNonce,
595 user_partial_sig: &musig::PartialSignature,
596) -> Option<Transaction> {
597 let mut tx = create_offboard_forfeit_tx(vtxo, connector, None, None);
598 let mut shc = SighashCache::new(&tx);
599 let prevouts = [&vtxo.txout(), &connector_txout];
600 let vtxo_sig = {
601 let sighash = shc.taproot_key_spend_signature_hash(
602 0, &Prevouts::All(&prevouts), TapSighashType::Default,
603 ).expect("provided all prevouts");
604 let vtxo_taproot = vtxo.output_taproot();
605 let tweak = vtxo_taproot.tap_tweak().to_byte_array();
606 let agg_nonce = musig::nonce_agg(&[user_nonce, server_nonces.0]);
607
608 let (_our_part_sig, final_sig) = musig::partial_sign(
612 [vtxo.user_pubkey(), vtxo.server_pubkey()],
613 agg_nonce,
614 server_key,
615 server_nonces.1,
616 sighash.to_byte_array(),
617 Some(tweak),
618 Some(&[user_partial_sig]),
619 );
620 debug_assert!({
621 let (key_agg, _) = musig::tweaked_key_agg(
622 [vtxo.user_pubkey(), vtxo.server_pubkey()], tweak,
623 );
624 let ff_session = musig::Session::new(
625 &key_agg,
626 agg_nonce,
627 &sighash.to_byte_array(),
628 );
629 ff_session.partial_verify(
630 &key_agg,
631 &_our_part_sig,
632 server_nonces.0,
633 musig::pubkey_to(vtxo.server_pubkey()),
634 )
635 }, "invalid partial offboard forfeit signature");
636 let final_sig = final_sig.expect("we provided other sigs");
637 SECP.verify_schnorr(
638 &final_sig, &sighash.into(), vtxo_taproot.output_key().as_x_only_public_key(),
639 ).ok()?;
640 final_sig
641 };
642
643 let conn_sig = {
644 let sighash = shc.taproot_key_spend_signature_hash(
645 1, &Prevouts::All(&prevouts), TapSighashType::Default,
646 ).expect("provided all prevouts");
647 SECP.sign_schnorr_with_aux_rand(&sighash.into(), tweaked_connector_key, &rand::random())
648 };
649
650 tx.input[0].witness = Witness::from_slice(&[&vtxo_sig[..]]);
651 tx.input[1].witness = Witness::from_slice(&[&conn_sig[..]]);
652 debug_assert_eq!(tx,
653 create_offboard_forfeit_tx(vtxo, connector, Some(&vtxo_sig), Some(&conn_sig)),
654 );
655
656 #[cfg(test)]
657 {
658 let prevs = [vtxo.txout(), connector_txout.clone()];
659 if let Err(e) = crate::test_util::verify_tx(&prevs, 0, &tx) {
660 println!("forfeit tx for VTXO {} failed: {}", vtxo.id(), e);
661 panic!("forfeit tx for VTXO {} failed: {}", vtxo.id(), e);
662 }
663 }
664
665 Some(tx)
666}
667
668fn create_offboard_forfeit_tx<G: Sync + Send>(
669 vtxo: &Vtxo<G>,
670 connector: OutPoint,
671 vtxo_sig: Option<&schnorr::Signature>,
672 conn_sig: Option<&schnorr::Signature>,
673) -> Transaction {
674 Transaction {
675 version: bitcoin::transaction::Version(3),
676 lock_time: bitcoin::absolute::LockTime::ZERO,
677 input: vec![
678 TxIn {
679 previous_output: vtxo.point(),
680 sequence: Sequence::MAX,
681 script_sig: ScriptBuf::new(),
682 witness: vtxo_sig.map(|s| Witness::from_slice(&[&s[..]])).unwrap_or_default(),
683 },
684 TxIn {
685 previous_output: connector,
686 sequence: Sequence::MAX,
687 script_sig: ScriptBuf::new(),
688 witness: conn_sig.map(|s| Witness::from_slice(&[&s[..]])).unwrap_or_default(),
689 },
690 ],
691 output: vec![
692 TxOut {
693 value: vtxo.amount() + P2TR_DUST,
695 script_pubkey: ScriptBuf::new_p2tr(
696 &*SECP, vtxo.server_pubkey().x_only_public_key().0, None,
697 ),
698 },
699 fee::fee_anchor(),
700 ],
701 }
702}
703
704#[cfg(test)]
705mod test {
706 use std::str::FromStr;
707 use bitcoin::hex::FromHex;
708 use bitcoin::secp256k1::PublicKey;
709 use crate::test_util::dummy::{random_utxo, DummyTestVtxoSpec};
710 use super::*;
711
712 #[test]
713 fn test_offboard_forfeit() {
714 let server_key = Keypair::new(&*SECP, &mut bitcoin::secp256k1::rand::thread_rng());
715
716 let req_pk = PublicKey::from_str(
717 "02271fba79f590251099b07fa0393b4c55d5e50cd8fca2e2822b619f8aabf93b74",
718 ).unwrap();
719 let req = OffboardRequest {
720 script_pubkey: ScriptBuf::new_p2tr(&*SECP, req_pk.x_only_public_key().0, None),
721 net_amount: Amount::ONE_BTC,
722 deduct_fees_from_gross_amount: true,
723 fee_rate: FeeRate::from_sat_per_kwu(100),
724 };
725
726 let input1_key = Keypair::new(&*SECP, &mut bitcoin::secp256k1::rand::thread_rng());
727 let (_, input1) = DummyTestVtxoSpec {
728 user_keypair: input1_key,
729 server_keypair: server_key,
730 ..Default::default()
731 }.build();
732 let input2_key = Keypair::new(&*SECP, &mut bitcoin::secp256k1::rand::thread_rng());
733 let (_, input2) = DummyTestVtxoSpec {
734 user_keypair: input2_key,
735 server_keypair: server_key,
736 ..Default::default()
737 }.build();
738
739 let conn_spk = ScriptBuf::new_p2tr(
741 &*SECP, server_key.public_key().x_only_public_key().0, None,
742 );
743
744 let change_amt = Amount::ONE_BTC * 2;
745 let offboard_tx = Transaction {
746 version: bitcoin::transaction::Version(3),
747 lock_time: bitcoin::absolute::LockTime::ZERO,
748 input: vec![
749 TxIn {
750 previous_output: random_utxo(),
751 sequence: Sequence::MAX,
752 script_sig: ScriptBuf::new(),
753 witness: Witness::new(),
754 },
755 ],
756 output: vec![
757 req.to_txout(),
759 TxOut {
761 script_pubkey: conn_spk.clone(),
762 value: P2TR_DUST * 2,
763 },
764 TxOut {
766 script_pubkey: ScriptBuf::from_bytes(Vec::<u8>::from_hex(
767 "512077243a077f583b197d36caac516b0c7e4319c7b6a2316c25972f44dfbf20fd09"
768 ).unwrap()),
769 value: change_amt,
770 },
771 ],
772 };
773
774 let inputs = [&input1, &input2];
775 let ctx = OffboardForfeitContext::new(&inputs, &offboard_tx).unwrap();
776 ctx.validate_offboard_tx(&req).unwrap();
777
778 assert_eq!(
781 OffboardForfeitContext::new(&[] as &[&Vtxo<Full>], &offboard_tx).err(),
782 Some(OffboardForfeitError::NoInputs),
783 );
784
785 let (server_sec_nonces, server_pub_nonces) = (0..2).map(|_| {
786 musig::nonce_pair(&server_key)
787 }).collect::<(Vec<_>, Vec<_>)>();
788
789 let keys = [&input1_key, &input2_key];
790
791 assert_eq!(
794 ctx.user_sign_forfeits(&keys, &server_pub_nonces[..1]).err(),
795 Some(OffboardForfeitError::WrongCount {
796 vector: "forfeit cosign nonces", expected: 2, received: 1,
797 }),
798 );
799 assert_eq!(
800 ctx.user_sign_forfeits(&keys, &[]).err(),
801 Some(OffboardForfeitError::WrongCount {
802 vector: "forfeit cosign nonces", expected: 2, received: 0,
803 }),
804 );
805
806 let user_sigs = ctx.user_sign_forfeits(&keys, &server_pub_nonces).unwrap();
807
808 let (spare_sec_nonces, spare_pub_nonces) = (0..2).map(|_| {
811 musig::nonce_pair(&server_key)
812 }).collect::<(Vec<_>, Vec<_>)>();
813 assert_eq!(
814 ctx.finish(
815 &server_key,
816 &spare_pub_nonces,
817 spare_sec_nonces,
818 &user_sigs.public_nonces,
819 &user_sigs.partial_signatures[..1],
820 ).err(),
821 Some(OffboardForfeitError::WrongCount {
822 vector: "user partial signatures", expected: 2, received: 1,
823 }),
824 );
825
826 let result = ctx.finish(
827 &server_key,
828 &server_pub_nonces,
829 server_sec_nonces,
830 &user_sigs.public_nonces,
831 &user_sigs.partial_signatures,
832 ).unwrap();
833
834 let connector_tx = result.connector_tx.as_ref()
840 .expect("multi-input offboard must have a connector fanout tx");
841 let connector_txid = connector_tx.compute_txid();
842 for (i, (vtxo, forfeit_tx)) in inputs.iter().zip(&result.forfeit_txs).enumerate() {
843 assert_eq!(
844 forfeit_tx.input[1].previous_output,
845 OutPoint::new(connector_txid, i as u32),
846 "forfeit tx {} doesn't spend its fanout connector", i,
847 );
848 let real_prevouts = [vtxo.txout(), connector_tx.output[i].clone()];
849 crate::test_util::verify_tx(&real_prevouts, 0, forfeit_tx)
850 .expect(&format!("forfeit tx {} vtxo input invalid against real connector prevout", i));
851 crate::test_util::verify_tx(&real_prevouts, 1, forfeit_tx)
852 .expect(&format!("forfeit tx {} connector input invalid against real connector prevout", i));
853
854 assert_eq!(result.forfeit_vtxos[i].txout(), forfeit_tx.output[0],
857 "forfeit vtxo {} doesn't match its forfeit tx output", i,
858 );
859 }
860 }
861}