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 = 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.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
494/// Create the connector VTXO for the fanout output into multi connector tx
495///
496/// This connector is the fanout output on the offboard tx and is spent by the fanout
497/// tx that creates a connector for each input.
498fn 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		// NB they are the same here because this VTXO goes straight onchain
507		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
520/// Create the connector VTXO on the connector fanout tx
521///
522/// This connector is an output of the connector fanout tx.
523fn 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
584/// Check the user's partial signature, then finalize the forfeit tx
585///
586/// Returns `None` only if the user's partial signature is invalid.
587fn 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		// NB it is cheaper to check final schnorr signature than partial sig, so
609		// it is customary to do that insted
610
611		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				// also accumulate the connector dust
694				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		// connectors pay a plain keyspend of the server key
740		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				// the delivery goes first
758				req.to_txout(),
759				// then a connector
760				TxOut {
761					script_pubkey: conn_spk.clone(),
762					value: P2TR_DUST * 2,
763				},
764				// then maybe change
765				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		// An offboard with nothing to forfeit is refused at construction, so
779		// no other method has to cope with it.
780		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		// A server that sends the wrong number of nonces is rejected, not
792		// signed for: the caller must be able to survive a malformed response.
793		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		// Same in the other direction: a user sending the wrong number of
809		// partial signatures must not be able to take the server down.
810		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		// The forfeit txs must be valid against the prevouts that will actually
835		// exist on-chain: each spends an output of the connector fanout tx,
836		// which carries a single dust, not the combined fanout root output on
837		// the offboard tx. Taproot sighashes commit to all prevout amounts, so
838		// signing against the wrong value makes the forfeits consensus-invalid.
839		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			// The recorded forfeit vtxo must describe the actual forfeit tx
855			// output, or the watchman can never sweep it.
856			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}