ark/
forfeit.rs

1
2
3use bitcoin::{OutPoint, ScriptBuf, Sequence, TapLeafHash, Transaction, TxIn, TxOut, Witness};
4use bitcoin::hashes::Hash;
5use bitcoin::secp256k1::{schnorr, Keypair, PublicKey};
6use bitcoin::sighash::{self, SighashCache, TapSighash, TapSighashType};
7use bitcoin::taproot::{self, LeafVersion, TaprootSpendInfo};
8
9use bitcoin_ext::{fee, TaprootSpendInfoExt, P2TR_DUST};
10
11use crate::{musig, Vtxo, VtxoId, SECP};
12use crate::connectors::ConnectorChain;
13use crate::encode::{ProtocolDecodingError, ProtocolEncoding, ReadExt, WriteExt};
14use crate::tree::signed::{unlock_clause, UnlockHash, UnlockPreimage};
15use crate::vtxo::exit_clause;
16
17
18/// The taproot for the policy of the output of the hArk forfeit tx
19///
20/// This policy allows the server to spend by revealing the unlock preimage,
21/// but still has a timeout to the user after exit delta.
22#[inline]
23pub fn hark_forfeit_claim_taproot(
24	vtxo: &Vtxo,
25	unlock_hash: UnlockHash,
26) -> TaprootSpendInfo {
27	let agg_pk = vtxo.forfeit_agg_pubkey();
28	taproot::TaprootBuilder::new()
29		.add_leaf(1, exit_clause(vtxo.user_pubkey(), vtxo.exit_delta())).unwrap()
30		.add_leaf(1, unlock_clause(agg_pk, unlock_hash)).unwrap()
31		.finalize(&SECP, agg_pk).unwrap()
32}
33
34/// Construct the first tx in the hArk forfeit protocol
35#[inline]
36pub fn create_hark_forfeit_tx(
37	vtxo: &Vtxo,
38	unlock_hash: UnlockHash,
39	signature: Option<&schnorr::Signature>,
40) -> Transaction {
41	let claim_taproot = hark_forfeit_claim_taproot(vtxo, unlock_hash);
42	Transaction {
43		version: bitcoin::transaction::Version(3),
44		lock_time: bitcoin::absolute::LockTime::ZERO,
45		input: vec![
46			TxIn {
47				previous_output: vtxo.point(),
48				sequence: Sequence::MAX,
49				script_sig: ScriptBuf::new(),
50				witness: signature.map(|s| Witness::from_slice(&[&s[..]])).unwrap_or_default(),
51			},
52		],
53		output: vec![
54			TxOut {
55				value: vtxo.amount(),
56				script_pubkey: claim_taproot.script_pubkey(),
57			},
58			fee::fee_anchor(),
59		],
60	}
61}
62
63/// Construct the second tx in the hArk forfeit protocol
64#[inline]
65pub fn create_hark_forfeit_claim_tx(
66	vtxo: &Vtxo,
67	forfeit_point: OutPoint,
68	unlock_hash: UnlockHash,
69	witness: Option<(&schnorr::Signature, UnlockPreimage)>,
70) -> Transaction {
71	Transaction {
72		version: bitcoin::transaction::Version(3),
73		lock_time: bitcoin::absolute::LockTime::ZERO,
74		input: vec![
75			TxIn {
76				previous_output: forfeit_point,
77				sequence: Sequence::MAX,
78				script_sig: ScriptBuf::new(),
79				witness: witness.map(|(signature, unlock_preimage)| {
80					let taproot = hark_forfeit_claim_taproot(vtxo, unlock_hash);
81					let agg_pk = taproot.internal_key();
82					debug_assert_eq!(agg_pk, vtxo.forfeit_agg_pubkey());
83					let clause = unlock_clause(agg_pk, unlock_hash);
84					let script_leaf = (clause, LeafVersion::TapScript);
85					let cb = taproot.control_block(&script_leaf)
86						.expect("unlock clause not found in hArk forfeit claim taproot");
87					Witness::from_slice(&[
88						&signature.serialize()[..],
89						&unlock_preimage[..],
90						&script_leaf.0.as_bytes(),
91						&cb.serialize()[..],
92					])
93				}).unwrap_or_default(),
94			},
95		],
96		output: vec![
97			TxOut {
98				value: vtxo.amount(),
99				script_pubkey: ScriptBuf::new_p2tr(&SECP, vtxo.server_pubkey().into(), None),
100			},
101			fee::fee_anchor(),
102		],
103	}
104}
105
106#[inline]
107pub fn hark_forfeit_sighash(
108	vtxo: &Vtxo,
109	unlock_hash: UnlockHash,
110) -> (TapSighash, Transaction) {
111	let exit_prevout = vtxo.txout();
112	let tx = create_hark_forfeit_tx(vtxo, unlock_hash, None);
113	let sighash = SighashCache::new(&tx).taproot_key_spend_signature_hash(
114		0, &sighash::Prevouts::All(&[exit_prevout]), TapSighashType::Default,
115	).expect("sighash error");
116	(sighash, tx)
117}
118
119#[inline]
120pub fn hark_forfeit_claim_sighash(
121	vtxo: &Vtxo,
122	forfeit_point: OutPoint,
123	unlock_hash: UnlockHash,
124) -> (TapSighash, Transaction) {
125	let claim_taproot = hark_forfeit_claim_taproot(vtxo, unlock_hash);
126	let claim_txout = TxOut {
127		script_pubkey: claim_taproot.script_pubkey(),
128		value: vtxo.amount(),
129	};
130	let tx = create_hark_forfeit_claim_tx(vtxo, forfeit_point, unlock_hash, None);
131	let agg_pk = claim_taproot.internal_key();
132	debug_assert_eq!(agg_pk, vtxo.forfeit_agg_pubkey());
133	let clause = unlock_clause(agg_pk, unlock_hash);
134	let leaf = TapLeafHash::from_script(&clause, LeafVersion::TapScript);
135	let sighash = SighashCache::new(&tx).taproot_script_spend_signature_hash(
136		0, &sighash::Prevouts::All(&[claim_txout]), leaf, TapSighashType::Default,
137	).expect("sighash error");
138	(sighash, tx)
139}
140
141/// Set of nonces for a hArk forfeit
142#[derive(Debug, Clone, PartialEq, Eq)]
143pub struct HashLockedForfeitNonces {
144	pub forfeit_tx_nonce: musig::PublicNonce,
145	pub forfeit_claim_tx_nonce: musig::PublicNonce,
146}
147
148impl ProtocolEncoding for HashLockedForfeitNonces {
149	fn encode<W: std::io::Write + ?Sized>(&self, w: &mut W) -> Result<(), std::io::Error> {
150		self.forfeit_tx_nonce.encode(w)?;
151		self.forfeit_claim_tx_nonce.encode(w)?;
152		Ok(())
153	}
154
155	fn decode<R: std::io::Read + ?Sized>(r: &mut R) -> Result<Self, ProtocolDecodingError> {
156		Ok(Self {
157			forfeit_tx_nonce: ProtocolEncoding::decode(r)?,
158			forfeit_claim_tx_nonce: ProtocolEncoding::decode(r)?,
159		})
160	}
161}
162
163/// A bundle of signatures that forfeits a user's VTXO
164/// conditional on the server revealing a secret preimage
165///
166/// In hArk, the forfeit protocol actually consists of two steps.
167/// First there is a tx that sends the money to an output that the
168/// server can claim if he provides the preimage, but that still
169/// has a timeout back to the user, to force the server to actually
170/// reveal the preimage before the new hArk VTXO expires.
171/// This output policy also has to contain the user's pubkey, so the
172/// user that forfeits will have to provide a partial signature for
173/// both the spend from his VTXO to the forfeit tx and on a tx that
174/// spends the forfeit tx to the server's wallet.
175#[derive(Debug, Clone, PartialEq, Eq)]
176pub struct HashLockedForfeitBundle {
177	pub vtxo_id: VtxoId,
178	pub unlock_hash: UnlockHash,
179	pub user_nonces: HashLockedForfeitNonces,
180	/// User's partial signature on the forfeit tx
181	pub forfeit_tx_part_sig: musig::PartialSignature,
182	/// User's partial signature on the forfeit claim tx
183	pub forfeit_claim_tx_part_sig: musig::PartialSignature,
184}
185
186impl HashLockedForfeitBundle {
187	/// Create a new [HashLockedForfeitBundle] for the given VTXO
188	pub fn forfeit_vtxo(
189		vtxo: &Vtxo,
190		unlock_hash: UnlockHash,
191		user_key: &Keypair,
192		server_nonces: &HashLockedForfeitNonces,
193	) -> Self {
194		let vtxo_exit_taproot = vtxo.output_taproot();
195		let (ff_sighash, ff_tx) = hark_forfeit_sighash(vtxo, unlock_hash);
196		let (ff_sec_nonce, ff_pub_nonce) = musig::nonce_pair_with_msg(
197			user_key, &ff_sighash.to_byte_array(),
198		);
199		let ff_agg_nonce = musig::nonce_agg(&[&ff_pub_nonce, &server_nonces.forfeit_tx_nonce]);
200		let ff_point = OutPoint::new(ff_tx.compute_txid(), 0);
201		let (ff_part_sig, _sig) = musig::partial_sign(
202			[vtxo.user_pubkey(), vtxo.server_pubkey()],
203			ff_agg_nonce,
204			user_key,
205			ff_sec_nonce,
206			ff_sighash.to_byte_array(),
207			Some(vtxo_exit_taproot.tap_tweak().to_byte_array()),
208			None,
209		);
210
211		let (claim_sighash, _tx) = hark_forfeit_claim_sighash(vtxo, ff_point, unlock_hash);
212		let (claim_sec_nonce, claim_pub_nonce) = musig::nonce_pair_with_msg(
213			user_key, &claim_sighash.to_byte_array(),
214		);
215		let claim_agg_nonce = musig::nonce_agg(
216			&[&claim_pub_nonce, &server_nonces.forfeit_claim_tx_nonce],
217		);
218		let (claim_part_sig, _sig) = musig::partial_sign(
219			[vtxo.user_pubkey(), vtxo.server_pubkey()],
220			claim_agg_nonce,
221			user_key,
222			claim_sec_nonce,
223			claim_sighash.to_byte_array(),
224			None,
225			None,
226		);
227
228		Self {
229			vtxo_id: vtxo.id(),
230			unlock_hash: unlock_hash,
231			user_nonces: HashLockedForfeitNonces {
232				forfeit_tx_nonce: ff_pub_nonce,
233				forfeit_claim_tx_nonce: claim_pub_nonce,
234			},
235			forfeit_tx_part_sig: ff_part_sig,
236			forfeit_claim_tx_part_sig: claim_part_sig,
237		}
238	}
239
240	/// Used by the server to verify if the partial signatures in the bundle are
241	/// valid
242	pub fn verify(
243		&self,
244		vtxo: &Vtxo,
245		server_nonces: &HashLockedForfeitNonces,
246	) -> Result<(), &'static str> {
247		if vtxo.id() != self.vtxo_id {
248			return Err("VTXO mismatch");
249		}
250
251		let ff_agg_nonce = musig::nonce_agg(
252			&[&self.user_nonces.forfeit_tx_nonce, &server_nonces.forfeit_tx_nonce],
253		);
254		let vtxo_exit_taproot = vtxo.output_taproot();
255		let (ff_sighash, ff_tx) = hark_forfeit_sighash(vtxo, self.unlock_hash);
256		let (ff_key_agg, _) = musig::tweaked_key_agg(
257			[vtxo.user_pubkey(), vtxo.server_pubkey()],
258			vtxo_exit_taproot.tap_tweak().to_byte_array(),
259		);
260		let ff_point = OutPoint::new(ff_tx.compute_txid(), 0);
261		let ff_session = musig::Session::new(
262			&ff_key_agg,
263			ff_agg_nonce,
264			&ff_sighash.to_byte_array(),
265		);
266		let success = ff_session.partial_verify(
267			&ff_key_agg,
268			&self.forfeit_tx_part_sig,
269			&self.user_nonces.forfeit_tx_nonce,
270			musig::pubkey_to(vtxo.user_pubkey()),
271		);
272		if !success {
273			return Err("invalid partial sig for forfeit tx");
274		}
275
276		let claim_agg_nonce = musig::nonce_agg(
277			&[&self.user_nonces.forfeit_claim_tx_nonce, &server_nonces.forfeit_claim_tx_nonce],
278		);
279		let (claim_sighash, _tx) = hark_forfeit_claim_sighash(vtxo, ff_point, self.unlock_hash);
280		let claim_key_agg = musig::key_agg([vtxo.user_pubkey(), vtxo.server_pubkey()]);
281		let claim_session = musig::Session::new(
282			&claim_key_agg,
283			claim_agg_nonce,
284			&claim_sighash.to_byte_array(),
285		);
286		let success = claim_session.partial_verify(
287			&claim_key_agg,
288			&self.forfeit_claim_tx_part_sig,
289			&self.user_nonces.forfeit_claim_tx_nonce,
290			musig::pubkey_to(vtxo.user_pubkey()),
291		);
292		if !success {
293			return Err("invalid partial sig for forfeit claim tx");
294		}
295		Ok(())
296	}
297
298	/// Used by the server to finish the forfeit signatures using its own
299	/// nonces.
300	///
301	/// NB users don't need to know these signatures.
302	pub fn finish(
303		self,
304		vtxo: &Vtxo,
305		server_pub_nonces: &HashLockedForfeitNonces,
306		[ff_sec_nonce, claim_sec_nonce]: [musig::SecretNonce; 2],
307		server_key: &Keypair,
308	) -> [schnorr::Signature; 2] {
309		assert_eq!(vtxo.id(), self.vtxo_id);
310
311		let ff_agg_nonce = musig::nonce_agg(
312			&[&self.user_nonces.forfeit_tx_nonce, &server_pub_nonces.forfeit_tx_nonce],
313		);
314		let vtxo_exit_taproot = vtxo.output_taproot();
315		let (ff_sighash, ff_tx) = hark_forfeit_sighash(vtxo, self.unlock_hash);
316		let ff_point = OutPoint::new(ff_tx.compute_txid(), 0);
317		let (_ff_part_sig, ff_sig) = musig::partial_sign(
318			[vtxo.user_pubkey(), vtxo.server_pubkey()],
319			ff_agg_nonce,
320			server_key,
321			ff_sec_nonce,
322			ff_sighash.to_byte_array(),
323			Some(vtxo_exit_taproot.tap_tweak().to_byte_array()),
324			Some(&[&self.forfeit_tx_part_sig]),
325		);
326		let ff_sig = ff_sig.expect("forfeit tx sig error");
327		debug_assert!({
328			let (ff_key_agg, _) = musig::tweaked_key_agg(
329				[vtxo.user_pubkey(), vtxo.server_pubkey()],
330				vtxo_exit_taproot.tap_tweak().to_byte_array(),
331			);
332			let ff_session = musig::Session::new(
333				&ff_key_agg,
334				ff_agg_nonce,
335				&ff_sighash.to_byte_array(),
336			);
337			ff_session.partial_verify(
338				&ff_key_agg,
339				&_ff_part_sig,
340				&server_pub_nonces.forfeit_tx_nonce,
341				musig::pubkey_to(vtxo.server_pubkey()),
342			)
343		});
344		debug_assert_eq!(Ok(()), SECP.verify_schnorr(
345			&ff_sig, &ff_sighash.into(), &vtxo_exit_taproot.output_key().to_x_only_public_key(),
346		));
347
348		let claim_agg_nonce = musig::nonce_agg(
349			&[&self.user_nonces.forfeit_claim_tx_nonce, &server_pub_nonces.forfeit_claim_tx_nonce],
350		);
351		let claim_taproot = hark_forfeit_claim_taproot(vtxo, self.unlock_hash);
352		let (claim_sighash, _tx) = hark_forfeit_claim_sighash(vtxo, ff_point, self.unlock_hash);
353		let (_claim_part_sig, claim_sig) = musig::partial_sign(
354			[vtxo.user_pubkey(), vtxo.server_pubkey()],
355			claim_agg_nonce,
356			server_key,
357			claim_sec_nonce,
358			claim_sighash.to_byte_array(),
359			None,
360			Some(&[&self.forfeit_claim_tx_part_sig]),
361		);
362		let claim_sig = claim_sig.expect("forfeit claim tx sig error");
363		debug_assert!({
364			let claim_key_agg = musig::key_agg([vtxo.user_pubkey(), vtxo.server_pubkey()]);
365			let claim_session = musig::Session::new(
366				&claim_key_agg,
367				claim_agg_nonce,
368				&claim_sighash.to_byte_array(),
369			);
370			claim_session.partial_verify(
371				&claim_key_agg,
372				&_claim_part_sig,
373				&server_pub_nonces.forfeit_claim_tx_nonce,
374				musig::pubkey_to(vtxo.server_pubkey()),
375			)
376		});
377		debug_assert_eq!(Ok(()), SECP.verify_schnorr(
378			&claim_sig, &claim_sighash.into(), &claim_taproot.internal_key(),
379		));
380
381		[ff_sig, claim_sig]
382	}
383}
384
385/// The serialization version of [HashLockedForfeitBundle].
386const HASH_LOCKED_FORFEIT_BUNDLE_VERSION: u8 = 0x00;
387
388impl ProtocolEncoding for HashLockedForfeitBundle {
389	fn encode<W: std::io::Write + ?Sized>(&self, w: &mut W) -> Result<(), std::io::Error> {
390		w.emit_u8(HASH_LOCKED_FORFEIT_BUNDLE_VERSION)?;
391		self.vtxo_id.encode(w)?;
392		self.unlock_hash.encode(w)?;
393		self.user_nonces.encode(w)?;
394		self.forfeit_tx_part_sig.encode(w)?;
395		self.forfeit_claim_tx_part_sig.encode(w)?;
396		Ok(())
397	}
398
399	fn decode<R: std::io::Read + ?Sized>(r: &mut R) -> Result<Self, ProtocolDecodingError> {
400		let ver = r.read_u8()?;
401		if ver != HASH_LOCKED_FORFEIT_BUNDLE_VERSION {
402			return Err(ProtocolDecodingError::invalid("unknown encoding version"));
403		}
404		Ok(Self {
405			vtxo_id: ProtocolEncoding::decode(r)?,
406			unlock_hash: ProtocolEncoding::decode(r)?,
407			user_nonces: ProtocolEncoding::decode(r)?,
408			forfeit_tx_part_sig: ProtocolEncoding::decode(r)?,
409			forfeit_claim_tx_part_sig: ProtocolEncoding::decode(r)?,
410		})
411	}
412}
413
414#[inline]
415pub fn create_connector_forfeit_tx(
416	vtxo: &Vtxo,
417	connector: OutPoint,
418	forfeit_sig: Option<&schnorr::Signature>,
419	connector_sig: Option<&schnorr::Signature>,
420) -> Transaction {
421	Transaction {
422		version: bitcoin::transaction::Version(3),
423		lock_time: bitcoin::absolute::LockTime::ZERO,
424		input: vec![
425			TxIn {
426				previous_output: vtxo.point(),
427				sequence: Sequence::ZERO,
428				script_sig: ScriptBuf::new(),
429				witness: forfeit_sig.map(|s| Witness::from_slice(&[&s[..]])).unwrap_or_default(),
430			},
431			TxIn {
432				previous_output: connector,
433				sequence: Sequence::ZERO,
434				script_sig: ScriptBuf::new(),
435				witness: connector_sig.map(|s| Witness::from_slice(&[&s[..]])).unwrap_or_default(),
436			},
437		],
438		output: vec![
439			TxOut {
440				value: vtxo.amount(),
441				script_pubkey: ScriptBuf::new_p2tr(&SECP, vtxo.server_pubkey().into(), None),
442			},
443			// We throw the connector dust value into the fee anchor
444			// because we can't have zero-value anchors and a non-zero fee.
445			fee::fee_anchor_with_amount(P2TR_DUST),
446		],
447	}
448}
449
450#[inline]
451fn connector_forfeit_input_sighash(
452	vtxo: &Vtxo,
453	connector: OutPoint,
454	connector_pk: PublicKey,
455	input_idx: usize,
456) -> (TapSighash, Transaction) {
457	let exit_prevout = vtxo.txout();
458	let connector_prevout = TxOut {
459		script_pubkey: ConnectorChain::output_script(connector_pk),
460		value: P2TR_DUST,
461	};
462	let tx = create_connector_forfeit_tx(vtxo, connector, None, None);
463	let sighash = SighashCache::new(&tx).taproot_key_spend_signature_hash(
464		input_idx,
465		&sighash::Prevouts::All(&[exit_prevout, connector_prevout]),
466		TapSighashType::Default,
467	).expect("sighash error");
468	(sighash, tx)
469}
470
471/// The sighash of the exit tx input of a forfeit tx.
472#[inline]
473pub fn connector_forfeit_sighash_exit(
474	vtxo: &Vtxo,
475	connector: OutPoint,
476	connector_pk: PublicKey,
477) -> (TapSighash, Transaction) {
478	connector_forfeit_input_sighash(vtxo, connector, connector_pk, 0)
479}
480
481/// The sighash of the connector input of a forfeit tx.
482#[inline]
483pub fn connector_forfeit_sighash_connector(
484	vtxo: &Vtxo,
485	connector: OutPoint,
486	connector_pk: PublicKey,
487) -> (TapSighash, Transaction) {
488	connector_forfeit_input_sighash(vtxo, connector, connector_pk, 1)
489}
490
491#[cfg(test)]
492mod test {
493	use bitcoin::hex::{DisplayHex, FromHex};
494	use crate::{test::verify_tx, vtxo::test::VTXO_VECTORS};
495	use super::*;
496
497	fn verify_hark_forfeits(
498		vtxo: &Vtxo,
499		unlock_preimage: UnlockPreimage,
500		server_sec_nonces: [musig::SecretNonce; 2],
501		server_pub_nonces: &HashLockedForfeitNonces,
502		bundle: HashLockedForfeitBundle,
503	) {
504		let unlock_hash = UnlockHash::hash(&unlock_preimage);
505		assert_eq!(Ok(()), bundle.verify(vtxo, server_pub_nonces));
506
507		// finish it which triggers debug asserts on partial sigs
508		let sigs = bundle.finish(vtxo, server_pub_nonces, server_sec_nonces, &VTXO_VECTORS.server_key);
509
510		let (ff_sighash, ff_tx) = hark_forfeit_sighash(vtxo, unlock_hash);
511		SECP.verify_schnorr(
512			&sigs[0],
513			&ff_sighash.into(),
514			&vtxo.output_taproot().output_key().to_x_only_public_key(),
515		).expect("forfeit tx sig check failed");
516		let ff_point = OutPoint::new(ff_tx.compute_txid(), 0);
517		let claim_taproot = hark_forfeit_claim_taproot(vtxo, unlock_hash);
518		let (claim_sighash, _tx) = hark_forfeit_claim_sighash(vtxo, ff_point, unlock_hash);
519		SECP.verify_schnorr(
520			&sigs[1],
521			&claim_sighash.into(),
522			&claim_taproot.internal_key(),
523		).expect("forfeit claim tx sig check failed");
524
525		// validate the actual txs
526		let ff_input = vtxo.txout();
527		let ff_tx = create_hark_forfeit_tx(vtxo, unlock_hash, Some(&sigs[0]));
528		verify_tx(&[ff_input], 0, &ff_tx).expect("forfeit tx error");
529		assert_eq!(ff_tx.compute_txid(), ff_point.txid);
530
531		let claim_input = ff_tx.output[0].clone();
532		let claim_tx = create_hark_forfeit_claim_tx(
533			vtxo, ff_point, unlock_hash, Some((&sigs[1], unlock_preimage)),
534		);
535		verify_tx(&[claim_input], 0, &claim_tx).expect("claim tx error");
536	}
537
538	#[test]
539	fn test_hark_forfeits() {
540		let server_ff_nonces = musig::nonce_pair(&VTXO_VECTORS.server_key);
541		let server_claim_nonces = musig::nonce_pair(&VTXO_VECTORS.server_key);
542		// we need to go through some hoops to print the secret nonces
543		let server_ff_sec_bytes = server_ff_nonces.0.dangerous_into_bytes();
544		let server_claim_sec_bytes = server_claim_nonces.0.dangerous_into_bytes();
545		println!("server ff sec nonce: {}", server_ff_sec_bytes.as_hex());
546		println!("server claim sec nonce: {}", server_claim_sec_bytes.as_hex());
547		let server_sec_nonces = [
548			musig::SecretNonce::dangerous_from_bytes(server_ff_sec_bytes),
549			musig::SecretNonce::dangerous_from_bytes(server_claim_sec_bytes),
550		];
551		let server_nonces = HashLockedForfeitNonces {
552			forfeit_tx_nonce: server_ff_nonces.1,
553			forfeit_claim_tx_nonce: server_claim_nonces.1,
554		};
555		println!("server pub nonces: {}", server_nonces.serialize_hex());
556
557		let vtxo = &VTXO_VECTORS.arkoor3_vtxo;
558		let unlock_preimage = UnlockPreimage::from_hex("c65f29e65dbc6cbad3e7f35c41986487c74ed513aeb37778354d42f3b0714645").unwrap();
559		let unlock_hash = UnlockHash::hash(&unlock_preimage);
560		let bundle = HashLockedForfeitBundle::forfeit_vtxo(
561			vtxo,
562			unlock_hash,
563			&VTXO_VECTORS.arkoor3_user_key,
564			&server_nonces,
565		);
566
567		// test encoding round trip
568		let encoded = bundle.serialize();
569		println!("bundle: {}", encoded.as_hex());
570		let decoded = HashLockedForfeitBundle::deserialize(&encoded).unwrap();
571		assert_eq!(bundle, decoded);
572		let bundle = decoded;
573
574		println!("verifying generated forfeits");
575		verify_hark_forfeits(
576			vtxo, unlock_preimage, server_sec_nonces, &server_nonces, bundle.clone(),
577		);
578
579		let (_sec, bad_nonce) = musig::nonce_pair(&VTXO_VECTORS.server_key);
580		assert_eq!(
581			bundle.verify(vtxo, &HashLockedForfeitNonces {
582				forfeit_tx_nonce: server_nonces.forfeit_tx_nonce,
583				forfeit_claim_tx_nonce: bad_nonce,
584			}),
585			Err("invalid partial sig for forfeit claim tx"),
586		);
587		assert_eq!(
588			bundle.verify(vtxo, &HashLockedForfeitNonces {
589				forfeit_tx_nonce: bad_nonce,
590				forfeit_claim_tx_nonce: server_nonces.forfeit_claim_tx_nonce,
591			}),
592			Err("invalid partial sig for forfeit tx"),
593		);
594
595
596		// verify a hard-coded example from a previous run of this test
597		let server_sec_nonces = [
598			musig::SecretNonce::dangerous_from_bytes(FromHex::from_hex("220edcf16a908aa082e1009ec7af0385c6027e39bf95024b8062e7cd4497b640c18690e8512756dc5d30f04c43ab7ebb7e77815119ab7a1113c932e80afc1d58ac701ea2622bf70a8243580d1879746ffe940588c5ad9d478d1b46e2bb9318743312a8657f684b47f963f7a0e95927b2c71005112d8edc5821a3f6f0f7bd6354947ff8ac").unwrap()),
599			musig::SecretNonce::dangerous_from_bytes(FromHex::from_hex("220edcf124c12726825b9615bd47f71c78603cb249752ff6b525cc00d460a9d31895411177c29b1052e2fecae8c791e32b0b5d13117973ab1d7d3fbb2d6ddf66b4a51241622bf70a8243580d1879746ffe940588c5ad9d478d1b46e2bb9318743312a8657f684b47f963f7a0e95927b2c71005112d8edc5821a3f6f0f7bd6354947ff8ac").unwrap()),
600		];
601		let server_nonces = HashLockedForfeitNonces::deserialize_hex("03e0e0644d80603dadedc1b2ee24f7b8b40e42bcb8e7bd59168eb17ba3afdd03c003ce6d038de9ae7a7a30b6847fb8f3e4b8b80c29f16ea34fb9ae21db87db7231ff020377a71d05a338b778163912ac99d3ebba6248aed0bf06d7b8964bfa8e3c04c802a50b14ad326f3aeccbe6f1ad752ad26d92ffa7e34a0aa67d7c1d449446292b77").unwrap();
602		let bundle = HashLockedForfeitBundle::deserialize_hex("00ff70cc93c752b2cdfa42fef244be8915b087a7e13d9cf6cb24b6443b6a8b87dc000000003d5491373df6a016f78b3f46d65a4fc6948824c43a59620404e8719cfee05d1a03e2f25e2b8414e8b0b313f9571165696c9aafbaca9e084cb7cb4fef7694641f8d035ced88a19e0718e72f12e01763f7e1cf525e1780cb436b5bc25cef17fd0f8a2502317251952a7a5bdf8c37515387e7f68a3343be642eda882b2279d869c3b2dd9d03511337c67552f9c4249ebc1ef225e26515adf9b54a508b93a3cf024d75b38389393bfe384fd0a91b6c8734ec9f37bee9937fb494fa0e5eb08f004208c658f54839454f8d29d0c41c641e9b8dba8d953f680ada737f73462b291281beaa5cd173").unwrap();
603
604		println!("verifying hard-coded forfeits");
605		verify_hark_forfeits(vtxo, unlock_preimage, server_sec_nonces, &server_nonces, bundle);
606	}
607}