Skip to main content

ark/
offboard.rs

1//!
2//! # Offboard mechanism using connector-swaps
3//!
4//!
5//! ## Connector VTXOs
6//!
7//! Connector outputs are plain key-spend p2tr outputs for the server key, so
8//! the watchman can sweep their dust once they are no longer needed. We create
9//! internal "ServerVtxo"s for them. Because they must not be swept before they
10//! are no longer required (i.e. when the input VTXO expires) — spending a
11//! connector invalidates the forfeit tx it belongs to, forgoing the full input
12//! VTXO amount, not just the dust — we use the expiry height on the VTXO to
13//! indicate when they can be swept.
14//!
15
16use 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
34/// The output index of the offboard output in the offboard tx
35pub const OFFBOARD_TX_OFFBOARD_VOUT: usize = 0;
36/// The output index of the connector output in the offboard tx
37pub const OFFBOARD_TX_CONNECTOR_VOUT: usize = 1;
38
39/// Additional number of blocks after the input VTXO expiry we wait to sweep connectors
40const CONNECTOR_EXPIRY_DELTA: BlockDelta = BlockDelta::new(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/// Contains information regarding an offboard that a client would like to perform.
54#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
55pub struct OffboardRequest {
56	/// The destination for the [TxOut].
57	#[serde(with = "bitcoin_ext::serde::encodable")]
58	pub script_pubkey: ScriptBuf,
59	/// The target amount in sats.
60	#[serde(rename = "amount_sat", with = "bitcoin::amount::serde::as_sat")]
61	pub net_amount: Amount,
62	/// Determines whether fees should be added onto the given amount or deducted from the gross
63	/// amount.
64	pub deduct_fees_from_gross_amount: bool,
65	/// What fee rate was used when calculating the fee for the offboard.
66	#[serde(rename = "fee_rate_kwu")]
67	pub fee_rate: FeeRate,
68}
69
70impl OffboardRequest {
71	/// Validate that the offboard has a valid script.
72	pub fn validate(&self) -> Result<(), InvalidOffboardRequestError> {
73		Ok(self.to_txout().check_standard()?)
74	}
75
76	/// Convert into a tx output.
77	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/// Something is wrong with what was handed to an [OffboardForfeitContext].
108///
109/// Every variant is a condition the counterparty can cause: a server picks
110/// how many cosign nonces it sends and what offboard tx it builds, a client
111/// picks how many nonces and partial signatures it sends. None of them may
112/// panic, because the side rejecting a malformed message would be the side
113/// that goes down.
114#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
115pub enum OffboardForfeitError {
116	/// There is nothing to forfeit. Every offboard spends at least one VTXO.
117	#[error("offboard has no input VTXOs")]
118	NoInputs,
119	/// A vector holding one entry per input VTXO has a different length.
120	#[error("wrong number of {vector}: expected {expected}, received {received}")]
121	WrongCount {
122		/// Names the vector that was the wrong length.
123		vector: &'static str,
124		/// How many entries were needed: one per input VTXO.
125		expected: usize,
126		/// How many entries were actually supplied.
127		received: usize,
128	},
129	/// The offboard tx has no connector output for the forfeits to spend.
130	/// [OffboardForfeitContext::validate_offboard_tx] reports this in more
131	/// detail; it shows up here only if that check was skipped.
132	#[error("offboard tx has no connector output")]
133	MissingConnectorOutput,
134	#[error(transparent)]
135	InvalidUserPartialSignature(#[from] InvalidUserPartialSignatureError),
136}
137
138impl OffboardForfeitError {
139	/// Returns [Self::WrongCount] unless `received` is exactly `expected`.
140	///
141	/// `vector` names what was counted, for the error message.
142	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		// We need:
174		// - each input vtxo spent by forfeit
175		// for not single:
176		// - connector root output spent by connector tx
177
178		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
195/// Construction and validation work with any VTXO representation: they
196/// only need the number of inputs, so the client can validate a prepared
197/// offboard tx with bare wallet vtxos. Only signing and finishing need
198/// the full form; those methods are bounded on `AsRef<Vtxo<Full>>` below.
199impl<'a, V> OffboardForfeitContext<'a, V> {
200	/// Create a new [OffboardForfeitContext] with given input VTXOs and offboard tx
201	///
202	/// Fails with [OffboardForfeitError::NoInputs] if there are no input
203	/// VTXOs. Every other method relies on that, so this is the only place
204	/// it has to be checked.
205	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	/// Validate offboard tx matches offboard request
216	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		// for the user we only need to check that there are enough connectors
239		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	/// Sign forfeit transactions for all input VTXOs
258	///
259	/// Provide the keys for the VTXO pubkeys in order of the input VTXOs.
260	///
261	/// `server_nonces` comes from the server, so a wrong count is an error and
262	/// not a panic: the caller can't keep a peer from sending a malformed
263	/// response, only reject it.
264	///
265	/// Call [Self::validate_offboard_tx] first; it reports a malformed
266	/// offboard tx in full detail, where this only names what it needed.
267	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			// here we will create a deterministic intermediate connector tx and
296			// sign forfeit txs with the outputs of that tx
297
298			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			// The forfeit txs spend the connector tx's outputs, which carry a
306			// single dust each; the offboard tx's connector output carries the
307			// combined sum. The sighash commits to the prevout values, so we
308			// must use the actual connector tx output here.
309			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	/// Check the user's partial signatures and finalize the forfeit txs
331	///
332	/// `user_pub_nonces` and `user_partial_sigs` come from the user, so wrong
333	/// counts are errors and not panics: the server has to be able to reject a
334	/// malformed request without going down.
335	///
336	/// Call [Self::validate_offboard_tx] first; it reports a malformed
337	/// offboard tx in full detail, where this only names what it needed.
338	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			// here we will create a deterministic intermediate connector tx and
382			// sign forfeit txs with the outputs of that tx
383
384			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				// The connector fanout tx spends the offboard's connector output, a
392				// key-path-only p2tr for the server key. Sign it; otherwise it would
393				// be stored/broadcast with an empty witness and rejected by the mempool.
394				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(), // should be the same, any will do
412				self.input_vtxos.len(),
413			));
414
415			// The forfeit txs spend the connector tx's outputs, which carry a
416			// single dust each; the offboard tx's connector output carries the
417			// combined sum. The sighash commits to the prevout values, so we
418			// must use the actual connector tx output here.
419			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		// the forfeit tx output accumulates the connector dust on
462		// top of the input amount
463		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
472/// Create the connector VTXO for the connector used to offboard a single VTXO
473///
474/// This connector is just a single 330 sat output on the offboard tx.
475fn 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		// NB they are the same here because this VTXO goes straight onchain
482		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 + CONNECTOR_EXPIRY_DELTA,
488		exit_delta: BlockDelta::ZERO,
489		genesis: Bare,
490	}
491}
492
493/// Create the connector VTXO for the fanout output into multi connector tx
494///
495/// This connector is the fanout output on the offboard tx and is spent by the fanout
496/// tx that creates a connector for each input.
497fn construct_connector_vtxo_fanout_root(
498	offboard_txid: Txid,
499	max_expiry_height: BlockHeight,
500	server_pubkey: PublicKey,
501	nb_vtxos: usize,
502) -> ServerVtxo<Bare> {
503	let point = OutPoint::new(offboard_txid, 1);
504	ServerVtxo {
505		// NB they are the same here because this VTXO goes straight onchain
506		anchor_point: point.clone(),
507		point: point,
508		policy: ServerVtxoPolicy::ServerOwned,
509		amount: P2TR_DUST.checked_mul(nb_vtxos as u64)
510			.expect("P2TR_DUST * nb_vtxos fits in u64 by VTXO-count and dust bounds"),
511		server_pubkey: server_pubkey,
512		expiry_height: max_expiry_height + CONNECTOR_EXPIRY_DELTA,
513		exit_delta: BlockDelta::ZERO,
514		genesis: Bare,
515	}
516}
517
518/// Create the connector VTXO on the connector fanout tx
519///
520/// This connector is an output of the connector fanout tx.
521fn construct_connector_vtxo_fanout_leaf<G>(
522	input: &Vtxo<G>,
523	input_idx: usize,
524	offboard_txid: Txid,
525	connector_txid: Txid,
526) -> ServerVtxo<Bare> {
527	ServerVtxo {
528		point: OutPoint::new(connector_txid, u32::try_from(input_idx).expect("input index fits in u32")),
529		anchor_point: OutPoint::new(offboard_txid, 1),
530		policy: ServerVtxoPolicy::ServerOwned,
531		amount: P2TR_DUST,
532		server_pubkey: input.server_pubkey,
533		expiry_height: input.expiry_height + CONNECTOR_EXPIRY_DELTA,
534		exit_delta: BlockDelta::ZERO,
535		genesis: Bare,
536	}
537}
538
539fn user_sign_vtxo_forfeit_input<G: Sync + Send>(
540	vtxo: &Vtxo<G>,
541	key: &Keypair,
542	connector: OutPoint,
543	connector_txout: &TxOut,
544	server_nonce: &musig::PublicNonce,
545) -> (musig::PublicNonce, musig::PartialSignature) {
546	let tx = create_offboard_forfeit_tx(vtxo, connector, None, None);
547	let mut shc = SighashCache::new(&tx);
548	let prevouts = [&vtxo.txout(), &connector_txout];
549	let sighash = shc.taproot_key_spend_signature_hash(
550		0, &Prevouts::All(&prevouts), TapSighashType::Default,
551	).expect("provided all prevouts");
552	let tweak = vtxo.output_taproot().tap_tweak().to_byte_array();
553	let (pub_nonce, partial_sig) = musig::deterministic_partial_sign(
554		key,
555		[vtxo.server_pubkey()],
556		&[server_nonce],
557		sighash.to_byte_array(),
558		Some(tweak),
559	);
560	debug_assert!({
561		let (key_agg, _) = musig::tweaked_key_agg(
562			[vtxo.user_pubkey(), vtxo.server_pubkey()], tweak,
563		);
564		let agg_nonce = musig::nonce_agg(&[&pub_nonce, server_nonce]);
565		let ff_session = musig::Session::new(
566			&key_agg,
567			agg_nonce,
568			&sighash.to_byte_array(),
569		);
570		ff_session.partial_verify(
571			&key_agg,
572			&partial_sig,
573			&pub_nonce,
574			musig::pubkey_to(vtxo.user_pubkey()),
575		)
576	}, "invalid partial offboard forfeit signature");
577
578	(pub_nonce, partial_sig)
579}
580
581/// Check the user's partial signature, then finalize the forfeit tx
582///
583/// Returns `None` only if the user's partial signature is invalid.
584fn server_check_finalize_forfeit_tx<G: Sync + Send>(
585	vtxo: &Vtxo<G>,
586	server_key: &Keypair,
587	tweaked_connector_key: &Keypair,
588	connector: OutPoint,
589	connector_txout: &TxOut,
590	server_nonces: (&musig::PublicNonce, musig::SecretNonce),
591	user_nonce: &musig::PublicNonce,
592	user_partial_sig: &musig::PartialSignature,
593) -> Option<Transaction> {
594	let mut tx = create_offboard_forfeit_tx(vtxo, connector, None, None);
595	let mut shc = SighashCache::new(&tx);
596	let prevouts = [&vtxo.txout(), &connector_txout];
597	let vtxo_sig = {
598		let sighash = shc.taproot_key_spend_signature_hash(
599			0, &Prevouts::All(&prevouts), TapSighashType::Default,
600		).expect("provided all prevouts");
601		let vtxo_taproot = vtxo.output_taproot();
602		let tweak = vtxo_taproot.tap_tweak().to_byte_array();
603		let agg_nonce = musig::nonce_agg(&[user_nonce, server_nonces.0]);
604
605		// NB it is cheaper to check final schnorr signature than partial sig, so
606		// it is customary to do that insted
607
608		let (_our_part_sig, final_sig) = musig::partial_sign(
609			[vtxo.user_pubkey(), vtxo.server_pubkey()],
610			agg_nonce,
611			server_key,
612			server_nonces.1,
613			sighash.to_byte_array(),
614			Some(tweak),
615			Some(&[user_partial_sig]),
616		);
617		debug_assert!({
618			let (key_agg, _) = musig::tweaked_key_agg(
619				[vtxo.user_pubkey(), vtxo.server_pubkey()], tweak,
620			);
621			let ff_session = musig::Session::new(
622				&key_agg,
623				agg_nonce,
624				&sighash.to_byte_array(),
625			);
626			ff_session.partial_verify(
627				&key_agg,
628				&_our_part_sig,
629				server_nonces.0,
630				musig::pubkey_to(vtxo.server_pubkey()),
631			)
632		}, "invalid partial offboard forfeit signature");
633		let final_sig = final_sig.expect("we provided other sigs");
634		SECP.verify_schnorr(
635			&final_sig, &sighash.into(), vtxo_taproot.output_key().as_x_only_public_key(),
636		).ok()?;
637		final_sig
638	};
639
640	let conn_sig = {
641		let sighash = shc.taproot_key_spend_signature_hash(
642			1, &Prevouts::All(&prevouts), TapSighashType::Default,
643		).expect("provided all prevouts");
644		SECP.sign_schnorr_with_aux_rand(&sighash.into(), tweaked_connector_key, &rand::random())
645	};
646
647	tx.input[0].witness = Witness::from_slice(&[&vtxo_sig[..]]);
648	tx.input[1].witness = Witness::from_slice(&[&conn_sig[..]]);
649	debug_assert_eq!(tx,
650		create_offboard_forfeit_tx(vtxo, connector, Some(&vtxo_sig), Some(&conn_sig)),
651	);
652
653	#[cfg(test)]
654	{
655		let prevs = [vtxo.txout(), connector_txout.clone()];
656		if let Err(e) = crate::test_util::verify_tx(&prevs, 0, &tx) {
657			println!("forfeit tx for VTXO {} failed: {}", vtxo.id(), e);
658			panic!("forfeit tx for VTXO {} failed: {}", vtxo.id(), e);
659		}
660	}
661
662	Some(tx)
663}
664
665fn create_offboard_forfeit_tx<G: Sync + Send>(
666	vtxo: &Vtxo<G>,
667	connector: OutPoint,
668	vtxo_sig: Option<&schnorr::Signature>,
669	conn_sig: Option<&schnorr::Signature>,
670) -> Transaction {
671	Transaction {
672		version: bitcoin::transaction::Version(3),
673		lock_time: bitcoin::absolute::LockTime::ZERO,
674		input: vec![
675			TxIn {
676				previous_output: vtxo.point(),
677				sequence: Sequence::MAX,
678				script_sig: ScriptBuf::new(),
679				witness: vtxo_sig.map(|s| Witness::from_slice(&[&s[..]])).unwrap_or_default(),
680			},
681			TxIn {
682				previous_output: connector,
683				sequence: Sequence::MAX,
684				script_sig: ScriptBuf::new(),
685				witness: conn_sig.map(|s| Witness::from_slice(&[&s[..]])).unwrap_or_default(),
686			},
687		],
688		output: vec![
689			TxOut {
690				// also accumulate the connector dust
691				value: vtxo.amount() + P2TR_DUST,
692				script_pubkey: ScriptBuf::new_p2tr(
693					&*SECP, vtxo.server_pubkey().x_only_public_key().0, None,
694				),
695			},
696			fee::fee_anchor(),
697		],
698	}
699}
700
701#[cfg(test)]
702mod test {
703	use std::str::FromStr;
704	use bitcoin::hex::FromHex;
705	use bitcoin::secp256k1::PublicKey;
706	use crate::test_util::dummy::{random_utxo, DummyTestVtxoSpec};
707	use super::*;
708
709	#[test]
710	fn test_offboard_forfeit() {
711		let server_key = Keypair::new(&*SECP, &mut bitcoin::secp256k1::rand::thread_rng());
712
713		let req_pk = PublicKey::from_str(
714			"02271fba79f590251099b07fa0393b4c55d5e50cd8fca2e2822b619f8aabf93b74",
715		).unwrap();
716		let req = OffboardRequest {
717			script_pubkey: ScriptBuf::new_p2tr(&*SECP, req_pk.x_only_public_key().0, None),
718			net_amount: Amount::ONE_BTC,
719			deduct_fees_from_gross_amount: true,
720			fee_rate: FeeRate::from_sat_per_kwu(100),
721		};
722
723		let input1_key = Keypair::new(&*SECP, &mut bitcoin::secp256k1::rand::thread_rng());
724		let (_, input1) = DummyTestVtxoSpec {
725			user_keypair: input1_key,
726			server_keypair: server_key,
727			..Default::default()
728		}.build();
729		let input2_key = Keypair::new(&*SECP, &mut bitcoin::secp256k1::rand::thread_rng());
730		let (_, input2) = DummyTestVtxoSpec {
731			user_keypair: input2_key,
732			server_keypair: server_key,
733			..Default::default()
734		}.build();
735
736		// connectors pay a plain keyspend of the server key
737		let conn_spk = ScriptBuf::new_p2tr(
738			&*SECP, server_key.public_key().x_only_public_key().0, None,
739		);
740
741		let change_amt = Amount::ONE_BTC * 2;
742		let offboard_tx = Transaction {
743			version: bitcoin::transaction::Version(3),
744			lock_time: bitcoin::absolute::LockTime::ZERO,
745			input: vec![
746				TxIn {
747					previous_output: random_utxo(),
748					sequence: Sequence::MAX,
749					script_sig: ScriptBuf::new(),
750					witness: Witness::new(),
751				},
752			],
753			output: vec![
754				// the delivery goes first
755				req.to_txout(),
756				// then a connector
757				TxOut {
758					script_pubkey: conn_spk.clone(),
759					value: P2TR_DUST * 2,
760				},
761				// then maybe change
762				TxOut {
763					script_pubkey: ScriptBuf::from_bytes(Vec::<u8>::from_hex(
764						"512077243a077f583b197d36caac516b0c7e4319c7b6a2316c25972f44dfbf20fd09"
765					).unwrap()),
766					value: change_amt,
767				},
768			],
769		};
770
771		let inputs = [&input1, &input2];
772		let ctx = OffboardForfeitContext::new(&inputs, &offboard_tx).unwrap();
773		ctx.validate_offboard_tx(&req).unwrap();
774
775		// An offboard with nothing to forfeit is refused at construction, so
776		// no other method has to cope with it.
777		assert_eq!(
778			OffboardForfeitContext::new(&[] as &[&Vtxo<Full>], &offboard_tx).err(),
779			Some(OffboardForfeitError::NoInputs),
780		);
781
782		let (server_sec_nonces, server_pub_nonces) = (0..2).map(|_| {
783			musig::nonce_pair(&server_key)
784		}).collect::<(Vec<_>, Vec<_>)>();
785
786		let keys = [&input1_key, &input2_key];
787
788		// A server that sends the wrong number of nonces is rejected, not
789		// signed for: the caller must be able to survive a malformed response.
790		assert_eq!(
791			ctx.user_sign_forfeits(&keys, &server_pub_nonces[..1]).err(),
792			Some(OffboardForfeitError::WrongCount {
793				vector: "forfeit cosign nonces", expected: 2, received: 1,
794			}),
795		);
796		assert_eq!(
797			ctx.user_sign_forfeits(&keys, &[]).err(),
798			Some(OffboardForfeitError::WrongCount {
799				vector: "forfeit cosign nonces", expected: 2, received: 0,
800			}),
801		);
802
803		let user_sigs = ctx.user_sign_forfeits(&keys, &server_pub_nonces).unwrap();
804
805		// Same in the other direction: a user sending the wrong number of
806		// partial signatures must not be able to take the server down.
807		let (spare_sec_nonces, spare_pub_nonces) = (0..2).map(|_| {
808			musig::nonce_pair(&server_key)
809		}).collect::<(Vec<_>, Vec<_>)>();
810		assert_eq!(
811			ctx.finish(
812				&server_key,
813				&spare_pub_nonces,
814				spare_sec_nonces,
815				&user_sigs.public_nonces,
816				&user_sigs.partial_signatures[..1],
817			).err(),
818			Some(OffboardForfeitError::WrongCount {
819				vector: "user partial signatures", expected: 2, received: 1,
820			}),
821		);
822
823		let result = ctx.finish(
824			&server_key,
825			&server_pub_nonces,
826			server_sec_nonces,
827			&user_sigs.public_nonces,
828			&user_sigs.partial_signatures,
829		).unwrap();
830
831		// The forfeit txs must be valid against the prevouts that will actually
832		// exist on-chain: each spends an output of the connector fanout tx,
833		// which carries a single dust, not the combined fanout root output on
834		// the offboard tx. Taproot sighashes commit to all prevout amounts, so
835		// signing against the wrong value makes the forfeits consensus-invalid.
836		let connector_tx = result.connector_tx.as_ref()
837			.expect("multi-input offboard must have a connector fanout tx");
838		let connector_txid = connector_tx.compute_txid();
839		for (i, (vtxo, forfeit_tx)) in inputs.iter().zip(&result.forfeit_txs).enumerate() {
840			assert_eq!(
841				forfeit_tx.input[1].previous_output,
842				OutPoint::new(connector_txid, i as u32),
843				"forfeit tx {} doesn't spend its fanout connector", i,
844			);
845			let real_prevouts = [vtxo.txout(), connector_tx.output[i].clone()];
846			crate::test_util::verify_tx(&real_prevouts, 0, forfeit_tx)
847				.expect(&format!("forfeit tx {} vtxo input invalid against real connector prevout", i));
848			crate::test_util::verify_tx(&real_prevouts, 1, forfeit_tx)
849				.expect(&format!("forfeit tx {} connector input invalid against real connector prevout", i));
850
851			// The recorded forfeit vtxo must describe the actual forfeit tx
852			// output, or the watchman can never sweep it.
853			assert_eq!(result.forfeit_vtxos[i].txout(), forfeit_tx.output[0],
854				"forfeit vtxo {} doesn't match its forfeit tx output", i,
855			);
856		}
857	}
858}