ark/
offboard.rs

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
19/// The output index of the offboard output in the offboard tx
20pub const OFFBOARD_TX_OFFBOARD_VOUT: usize = 0;
21/// The output index of the connector output in the offboard tx
22pub 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	/// Calculate the fee we have to charge for adding an output
39	/// with the given scriptPubkey to a transaction.
40	///
41	/// Use the `fixed_weight_charged` argument to add the fixed fee part that the
42	/// server charges. This will be added to the offboard's output size
43	/// and multiplied with the given fee rate.
44	///
45	/// Returns `None` in the calculation overflows because of insane
46	/// scriptPubkey or fee rate.
47	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	/// Validate that the offboard has a valid script.
58	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	/// Convert into a tx output.
67	pub fn to_txout(&self) -> TxOut {
68		TxOut {
69			script_pubkey: self.script_pubkey.clone(),
70			value: self.amount,
71		}
72	}
73
74	/// Returns the fee charged for the user to make this offboard given the fee rate
75	///
76	///
77	/// Use the `fixed_weight_charged` argument to add the fixed fee part that the
78	/// server charges. This will be added to the offboard's output size
79	/// and multiplied with the given fee rate.
80	///
81	/// Returns `None` in the calculation overflows because of insane
82	/// scriptPubkey or fee rate.
83	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	/// Create a new [OffboardForfeitContext] with given input VTXOs and offboard tx
123	///
124	/// Number of input VTXOs must not be zero.
125	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	/// Validate offboard tx matches offboard request
131	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		// for the user we only need to check that there are enough connectors
154		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	/// Sign forfeit transactions for all input VTXOs
168	///
169	/// Provide the keys for the VTXO pubkeys in order of the input VTXOs.
170	///
171	/// Panics if wrong number of keys or nonces, or if [Self::validate_offboard_tx]
172	/// would have returned an error. The caller should call that method first.
173	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			// here we will create a deterministic intermediate connector tx and
201			// sign forfeit txs with the outputs of that tx
202
203			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			// NB all connector txouts are identical, we copy the one from the offboard tx
209			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	/// Check the user's partial signatures and finalize the forfeit txs
227	///
228	/// Panics if wrong number of secret nonces or partial signatures, or if [Self::validate_offboard_tx]
229	/// would have returned an error. The caller should call that method first.
230	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			// here we will create a deterministic intermediate connector tx and
267			// sign forfeit txs with the outputs of that tx
268
269			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			// NB all connector txouts are identical, we copy the one from the offboard tx
275			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
346/// Check the user's partial signature, then finalize the forfeit tx
347///
348/// Returns `None` only if the user's partial signature is invalid.
349fn 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		// NB it is cheaper to check final schnorr signature than partial sig, so
371		// it is customary to do that insted
372
373		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				// also accumulate the connector dust
456				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				// the delivery goes first
518				req.to_txout(),
519				// then a connector
520				TxOut {
521					script_pubkey: conn_spk.clone(),
522					value: P2TR_DUST * 2,
523				},
524				// then maybe change
525				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}