1
2use std::borrow::Borrow;
3
4use bitcoin::hashes::Hash;
5use bitcoin::{
6 Amount, FeeRate, OutPoint, Script, ScriptBuf, Sequence, TapSighashType, Transaction, TxIn,
7 TxOut, Weight, Witness,
8};
9use bitcoin::hex::DisplayHex;
10use bitcoin::secp256k1::{Keypair, schnorr};
11use bitcoin::sighash::{Prevouts, SighashCache};
12
13use bitcoin_ext::{fee, KeypairExt, TxOutExt, P2TR_DUST};
14
15use crate::connectors::construct_multi_connector_tx;
16use crate::{musig, Vtxo, VtxoId, SECP};
17
18
19pub const OFFBOARD_TX_OFFBOARD_VOUT: usize = 0;
21pub const OFFBOARD_TX_CONNECTOR_VOUT: usize = 1;
23
24
25#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
26#[error("invalid offboard request: {0}")]
27pub struct InvalidOffboardRequestError(&'static str);
28
29#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
30pub struct OffboardRequest {
31 #[serde(with = "bitcoin_ext::serde::encodable")]
32 pub script_pubkey: ScriptBuf,
33 #[serde(rename = "amount_sat", with = "bitcoin::amount::serde::as_sat")]
34 pub amount: Amount,
35}
36
37impl OffboardRequest {
38 pub fn calculate_fee(
48 script_pubkey: &Script,
49 fee_rate: FeeRate,
50 fixed_weight_charged: Weight,
51 ) -> Option<Amount> {
52 let total = Weight::from_vb(script_pubkey.len() as u64)?
53 .checked_add(fixed_weight_charged)?;
54 Some(fee_rate.checked_mul_by_weight(total)?)
55 }
56
57 pub fn validate(&self) -> Result<(), InvalidOffboardRequestError> {
59 if self.to_txout().is_standard() {
60 Ok(())
61 } else {
62 Err(InvalidOffboardRequestError("non-standard output"))
63 }
64 }
65
66 pub fn to_txout(&self) -> TxOut {
68 TxOut {
69 script_pubkey: self.script_pubkey.clone(),
70 value: self.amount,
71 }
72 }
73
74 pub fn fee(
84 &self,
85 fee_rate: FeeRate,
86 fixed_weight_charged: Weight,
87 ) -> Option<Amount> {
88 Self::calculate_fee(&self.script_pubkey, fee_rate, fixed_weight_charged)
89 }
90}
91
92#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
93#[error("invalid offboard transaction: {0}")]
94pub struct InvalidOffboardTxError(String);
95
96impl<S: Into<String>> From<S> for InvalidOffboardTxError {
97 fn from(v: S) -> Self {
98 Self(v.into())
99 }
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
103#[error("invalid partial signature for VTXO {vtxo}")]
104pub struct InvalidUserPartialSignatureError {
105 pub vtxo: VtxoId,
106}
107
108pub struct OffboardForfeitSignatures {
109 pub public_nonces: Vec<musig::PublicNonce>,
110 pub partial_signatures: Vec<musig::PartialSignature>,
111}
112
113pub struct OffboardForfeitContext<'a, V> {
114 input_vtxos: &'a [V],
115 offboard_tx: &'a Transaction,
116}
117
118impl<'a, V> OffboardForfeitContext<'a, V>
119where
120 V: AsRef<Vtxo>,
121{
122 pub fn new(input_vtxos: &'a [V], offboard_tx: &'a Transaction) -> Self {
126 assert_ne!(input_vtxos.len(), 0, "no input VTXOs");
127 Self { input_vtxos, offboard_tx }
128 }
129
130 pub fn validate_offboard_tx(
132 &self,
133 req: &OffboardRequest,
134 ) -> Result<(), InvalidOffboardTxError> {
135 let offb_txout = self.offboard_tx.output.get(OFFBOARD_TX_OFFBOARD_VOUT)
136 .ok_or("missing offboard output")?;
137 let exp_txout = req.to_txout();
138
139 if exp_txout.script_pubkey != offb_txout.script_pubkey {
140 return Err(format!(
141 "offboard output scriptPubkey doesn't match: got={}, expected={}",
142 offb_txout.script_pubkey.as_bytes().as_hex(),
143 exp_txout.script_pubkey.as_bytes().as_hex(),
144 ).into());
145 }
146 if exp_txout.value != offb_txout.value {
147 return Err(format!(
148 "offboard output amount doesn't match: got={}, expected={}",
149 offb_txout.value, exp_txout.value,
150 ).into());
151 }
152
153 let conn_txout = self.offboard_tx.output.get(OFFBOARD_TX_CONNECTOR_VOUT)
155 .ok_or("missing connector output")?;
156 let required_conn_value = P2TR_DUST * self.input_vtxos.len() as u64;
157 if conn_txout.value != required_conn_value {
158 return Err(format!(
159 "insufficient connector amount: got={}, need={}",
160 conn_txout.value, required_conn_value,
161 ).into());
162 }
163
164 Ok(())
165 }
166
167 pub fn user_sign_forfeits(
174 &self,
175 keys: &[impl Borrow<Keypair>],
176 server_nonces: &[musig::PublicNonce],
177 ) -> OffboardForfeitSignatures {
178 assert_eq!(self.input_vtxos.len(), keys.len(), "wrong number of keys");
179 assert_eq!(self.input_vtxos.len(), server_nonces.len(), "wrong number of nonces");
180 assert_ne!(self.input_vtxos.len(), 0, "no inputs");
181
182 let mut pub_nonces = Vec::with_capacity(self.input_vtxos.len());
183 let mut part_sigs = Vec::with_capacity(self.input_vtxos.len());
184 let offboard_txid = self.offboard_tx.compute_txid();
185 let connector_prev = OutPoint::new(offboard_txid, OFFBOARD_TX_CONNECTOR_VOUT as u32);
186 let connector_txout = self.offboard_tx.output.get(OFFBOARD_TX_CONNECTOR_VOUT)
187 .expect("invalid offboard tx");
188
189 if self.input_vtxos.len() == 1 {
190 let (nonce, sig) = user_sign_vtxo_forfeit_input(
191 self.input_vtxos[0].as_ref(),
192 keys[0].borrow(),
193 connector_prev,
194 connector_txout,
195 &server_nonces[0],
196 );
197 pub_nonces.push(nonce);
198 part_sigs.push(sig);
199 } else {
200 let connector_tx = construct_multi_connector_tx(
204 connector_prev, self.input_vtxos.len(), &connector_txout.script_pubkey,
205 );
206 let connector_txid = connector_tx.compute_txid();
207
208 let iter = self.input_vtxos.iter().zip(keys).zip(server_nonces);
210 for (i, ((vtxo, key), server_nonce)) in iter.enumerate() {
211 let connector = OutPoint::new(connector_txid, i as u32);
212 let (nonce, sig) = user_sign_vtxo_forfeit_input(
213 vtxo.as_ref(), key.borrow(), connector, connector_txout, server_nonce,
214 );
215 pub_nonces.push(nonce);
216 part_sigs.push(sig);
217 }
218 }
219
220 OffboardForfeitSignatures {
221 public_nonces: pub_nonces,
222 partial_signatures: part_sigs,
223 }
224 }
225
226 pub fn check_finalize_transactions(
231 &self,
232 server_key: &Keypair,
233 connector_key: &Keypair,
234 server_pub_nonces: &[musig::PublicNonce],
235 server_sec_nonces: Vec<musig::SecretNonce>,
236 user_pub_nonces: &[musig::PublicNonce],
237 user_partial_sigs: &[musig::PartialSignature],
238 ) -> Result<Vec<Transaction>, InvalidUserPartialSignatureError> {
239 assert_eq!(self.input_vtxos.len(), server_pub_nonces.len());
240 assert_eq!(self.input_vtxos.len(), server_sec_nonces.len());
241 assert_eq!(self.input_vtxos.len(), user_pub_nonces.len());
242 assert_eq!(self.input_vtxos.len(), user_partial_sigs.len());
243 assert_ne!(self.input_vtxos.len(), 0, "no inputs");
244
245 let offboard_txid = self.offboard_tx.compute_txid();
246 let connector_prev = OutPoint::new(offboard_txid, OFFBOARD_TX_CONNECTOR_VOUT as u32);
247 let connector_txout = self.offboard_tx.output.get(OFFBOARD_TX_CONNECTOR_VOUT)
248 .expect("invalid offboard tx");
249 let tweaked_connector_key = connector_key.for_keyspend(&*SECP);
250
251 let mut ret = Vec::with_capacity(self.input_vtxos.len());
252 if self.input_vtxos.len() == 1 {
253 let vtxo = self.input_vtxos[0].as_ref();
254 let tx = server_check_finalize_forfeit_tx(
255 vtxo,
256 server_key,
257 &tweaked_connector_key,
258 connector_prev,
259 connector_txout,
260 (&server_pub_nonces[0], server_sec_nonces.into_iter().next().unwrap()),
261 &user_pub_nonces[0],
262 &user_partial_sigs[0],
263 ).ok_or_else(|| InvalidUserPartialSignatureError { vtxo: vtxo.id() })?;
264 ret.push(tx);
265 } else {
266 let connector_tx = construct_multi_connector_tx(
270 connector_prev, self.input_vtxos.len(), &connector_txout.script_pubkey,
271 );
272 let connector_txid = connector_tx.compute_txid();
273
274 let iter = self.input_vtxos.iter()
276 .zip(server_pub_nonces)
277 .zip(server_sec_nonces)
278 .zip(user_pub_nonces)
279 .zip(user_partial_sigs);
280 for (i, ((((vtxo, server_pub), server_sec), user_pub), user_part)) in iter.enumerate() {
281 let connector = OutPoint::new(connector_txid, i as u32);
282 match server_check_finalize_forfeit_tx(
283 vtxo.as_ref(),
284 server_key,
285 &tweaked_connector_key,
286 connector,
287 connector_txout,
288 (server_pub, server_sec),
289 user_pub,
290 user_part,
291 ) {
292 Some(tx) => ret.push(tx),
293 None => return Err(InvalidUserPartialSignatureError {
294 vtxo: vtxo.as_ref().id(),
295 }),
296 }
297 }
298 }
299
300 Ok(ret)
301 }
302}
303
304fn user_sign_vtxo_forfeit_input(
305 vtxo: &Vtxo,
306 key: &Keypair,
307 connector: OutPoint,
308 connector_txout: &TxOut,
309 server_nonce: &musig::PublicNonce,
310) -> (musig::PublicNonce, musig::PartialSignature) {
311 let tx = create_offboard_forfeit_tx(vtxo, connector, None, None);
312 let mut shc = SighashCache::new(&tx);
313 let prevouts = [&vtxo.txout(), &connector_txout];
314 let sighash = shc.taproot_key_spend_signature_hash(
315 0, &Prevouts::All(&prevouts), TapSighashType::Default,
316 ).expect("provided all prevouts");
317 let tweak = vtxo.output_taproot().tap_tweak().to_byte_array();
318 let (pub_nonce, partial_sig) = musig::deterministic_partial_sign(
319 key,
320 [vtxo.server_pubkey()],
321 &[server_nonce],
322 sighash.to_byte_array(),
323 Some(tweak),
324 );
325 debug_assert!({
326 let (key_agg, _) = musig::tweaked_key_agg(
327 [vtxo.user_pubkey(), vtxo.server_pubkey()], tweak,
328 );
329 let agg_nonce = musig::nonce_agg(&[&pub_nonce, server_nonce]);
330 let ff_session = musig::Session::new(
331 &key_agg,
332 agg_nonce,
333 &sighash.to_byte_array(),
334 );
335 ff_session.partial_verify(
336 &key_agg,
337 &partial_sig,
338 &pub_nonce,
339 musig::pubkey_to(vtxo.user_pubkey()),
340 )
341 }, "invalid partial offboard forfeit signature");
342
343 (pub_nonce, partial_sig)
344}
345
346fn server_check_finalize_forfeit_tx(
350 vtxo: &Vtxo,
351 server_key: &Keypair,
352 tweaked_connector_key: &Keypair,
353 connector: OutPoint,
354 connector_txout: &TxOut,
355 server_nonces: (&musig::PublicNonce, musig::SecretNonce),
356 user_nonce: &musig::PublicNonce,
357 user_partial_sig: &musig::PartialSignature,
358) -> Option<Transaction> {
359 let mut tx = create_offboard_forfeit_tx(vtxo, connector, None, None);
360 let mut shc = SighashCache::new(&tx);
361 let prevouts = [&vtxo.txout(), &connector_txout];
362 let vtxo_sig = {
363 let sighash = shc.taproot_key_spend_signature_hash(
364 0, &Prevouts::All(&prevouts), TapSighashType::Default,
365 ).expect("provided all prevouts");
366 let vtxo_taproot = vtxo.output_taproot();
367 let tweak = vtxo_taproot.tap_tweak().to_byte_array();
368 let agg_nonce = musig::nonce_agg(&[user_nonce, server_nonces.0]);
369
370 let (_our_part_sig, final_sig) = musig::partial_sign(
374 [vtxo.user_pubkey(), vtxo.server_pubkey()],
375 agg_nonce,
376 server_key,
377 server_nonces.1,
378 sighash.to_byte_array(),
379 Some(tweak),
380 Some(&[user_partial_sig]),
381 );
382 debug_assert!({
383 let (key_agg, _) = musig::tweaked_key_agg(
384 [vtxo.user_pubkey(), vtxo.server_pubkey()], tweak,
385 );
386 let ff_session = musig::Session::new(
387 &key_agg,
388 agg_nonce,
389 &sighash.to_byte_array(),
390 );
391 ff_session.partial_verify(
392 &key_agg,
393 &_our_part_sig,
394 server_nonces.0,
395 musig::pubkey_to(vtxo.server_pubkey()),
396 )
397 }, "invalid partial offboard forfeit signature");
398 let final_sig = final_sig.expect("we provided other sigs");
399 SECP.verify_schnorr(
400 &final_sig, &sighash.into(), vtxo_taproot.output_key().as_x_only_public_key(),
401 ).ok()?;
402 final_sig
403 };
404
405 let conn_sig = {
406 let sighash = shc.taproot_key_spend_signature_hash(
407 1, &Prevouts::All(&prevouts), TapSighashType::Default,
408 ).expect("provided all prevouts");
409 SECP.sign_schnorr_with_aux_rand(&sighash.into(), tweaked_connector_key, &rand::random())
410 };
411
412 tx.input[0].witness = Witness::from_slice(&[&vtxo_sig[..]]);
413 tx.input[1].witness = Witness::from_slice(&[&conn_sig[..]]);
414 debug_assert_eq!(tx,
415 create_offboard_forfeit_tx(vtxo, connector, Some(&vtxo_sig), Some(&conn_sig)),
416 );
417
418 #[cfg(test)]
419 {
420 let prevs = [vtxo.txout(), connector_txout.clone()];
421 if let Err(e) = crate::test::verify_tx(&prevs, 0, &tx) {
422 println!("forfeit tx for VTXO {} failed: {}", vtxo.id(), e);
423 panic!("forfeit tx for VTXO {} failed: {}", vtxo.id(), e);
424 }
425 }
426
427 Some(tx)
428}
429
430fn create_offboard_forfeit_tx(
431 vtxo: &Vtxo,
432 connector: OutPoint,
433 vtxo_sig: Option<&schnorr::Signature>,
434 conn_sig: Option<&schnorr::Signature>,
435) -> Transaction {
436 Transaction {
437 version: bitcoin::transaction::Version(3),
438 lock_time: bitcoin::absolute::LockTime::ZERO,
439 input: vec![
440 TxIn {
441 previous_output: vtxo.point(),
442 sequence: Sequence::MAX,
443 script_sig: ScriptBuf::new(),
444 witness: vtxo_sig.map(|s| Witness::from_slice(&[&s[..]])).unwrap_or_default(),
445 },
446 TxIn {
447 previous_output: connector,
448 sequence: Sequence::MAX,
449 script_sig: ScriptBuf::new(),
450 witness: conn_sig.map(|s| Witness::from_slice(&[&s[..]])).unwrap_or_default(),
451 },
452 ],
453 output: vec![
454 TxOut {
455 value: vtxo.amount() + P2TR_DUST,
457 script_pubkey: ScriptBuf::new_p2tr(
458 &*SECP, vtxo.server_pubkey().x_only_public_key().0, None,
459 ),
460 },
461 fee::fee_anchor(),
462 ],
463 }
464}
465
466#[cfg(test)]
467mod test {
468 use std::str::FromStr;
469 use bitcoin::hex::FromHex;
470 use bitcoin::secp256k1::PublicKey;
471 use crate::test::dummy::{random_utxo, DummyTestVtxoSpec};
472 use super::*;
473
474 #[test]
475 fn test_offboard_forfeit() {
476 let server_key = Keypair::new(&*SECP, &mut bitcoin::secp256k1::rand::thread_rng());
477
478 let req_pk = PublicKey::from_str(
479 "02271fba79f590251099b07fa0393b4c55d5e50cd8fca2e2822b619f8aabf93b74",
480 ).unwrap();
481 let req = OffboardRequest {
482 script_pubkey: ScriptBuf::new_p2tr(&*SECP, req_pk.x_only_public_key().0, None),
483 amount: Amount::ONE_BTC,
484 };
485
486 let input1_key = Keypair::new(&*SECP, &mut bitcoin::secp256k1::rand::thread_rng());
487 let (_, input1) = DummyTestVtxoSpec {
488 user_keypair: input1_key,
489 server_keypair: server_key,
490 ..Default::default()
491 }.build();
492 let input2_key = Keypair::new(&*SECP, &mut bitcoin::secp256k1::rand::thread_rng());
493 let (_, input2) = DummyTestVtxoSpec {
494 user_keypair: input2_key,
495 server_keypair: server_key,
496 ..Default::default()
497 }.build();
498
499 let conn_key = Keypair::new(&*SECP, &mut bitcoin::secp256k1::rand::thread_rng());
500 let conn_spk = ScriptBuf::new_p2tr(
501 &*SECP, conn_key.public_key().x_only_public_key().0, None,
502 );
503
504 let change_amt = Amount::ONE_BTC * 2;
505 let offboard_tx = Transaction {
506 version: bitcoin::transaction::Version(3),
507 lock_time: bitcoin::absolute::LockTime::ZERO,
508 input: vec![
509 TxIn {
510 previous_output: random_utxo(),
511 sequence: Sequence::MAX,
512 script_sig: ScriptBuf::new(),
513 witness: Witness::new(),
514 },
515 ],
516 output: vec![
517 req.to_txout(),
519 TxOut {
521 script_pubkey: conn_spk.clone(),
522 value: P2TR_DUST * 2,
523 },
524 TxOut {
526 script_pubkey: ScriptBuf::from_bytes(Vec::<u8>::from_hex(
527 "512077243a077f583b197d36caac516b0c7e4319c7b6a2316c25972f44dfbf20fd09"
528 ).unwrap()),
529 value: change_amt,
530 },
531 ],
532 };
533
534 let inputs = [&input1, &input2];
535 let ctx = OffboardForfeitContext::new(&inputs, &offboard_tx);
536 ctx.validate_offboard_tx(&req).unwrap();
537
538 let (server_sec_nonces, server_pub_nonces) = (0..2).map(|_| {
539 musig::nonce_pair(&server_key)
540 }).collect::<(Vec<_>, Vec<_>)>();
541
542 let user_sigs = ctx.user_sign_forfeits(&[&input1_key, &input2_key], &server_pub_nonces);
543
544 ctx.check_finalize_transactions(
545 &server_key,
546 &conn_key,
547 &server_pub_nonces,
548 server_sec_nonces,
549 &user_sigs.public_nonces,
550 &user_sigs.partial_signatures,
551 ).unwrap();
552 }
553}