Skip to main content

ark/
forfeit.rs

1
2
3use bitcoin::{Amount, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, Witness};
4use bitcoin::hashes::Hash;
5use bitcoin::secp256k1::{schnorr, Keypair, PublicKey};
6use bitcoin::sighash::{self, SighashCache, TapSighash, TapSighashType};
7
8use bitcoin_ext::{fee, TaprootSpendInfoExt, P2TR_DUST};
9
10use crate::{musig, ServerVtxo, ServerVtxoPolicy, Vtxo, VtxoId, SECP};
11use crate::connectors::ConnectorChain;
12use crate::encode::{ProtocolDecodingError, ProtocolEncoding, ReadExt, WriteExt};
13use crate::tree::signed::UnlockHash;
14use crate::vtxo::{Full, GenesisItem, GenesisTransition};
15use crate::vtxo::policy::HarkForfeitVtxoPolicy;
16use crate::vtxo::genesis::ArkoorGenesis;
17
18
19/// Construct the forfeit tx in the hArk forfeit protocol
20#[inline]
21pub fn create_hark_forfeit_tx<G>(
22	vtxo: &Vtxo<G>,
23	unlock_hash: UnlockHash,
24	signature: Option<&schnorr::Signature>,
25) -> Transaction {
26	let claim_policy = HarkForfeitVtxoPolicy {
27		user_pubkey: vtxo.user_pubkey(),
28		unlock_hash: unlock_hash,
29	};
30	debug_assert_eq!(
31		musig::combine_keys([vtxo.user_pubkey(), vtxo.server_pubkey()]).x_only_public_key().0,
32		vtxo.output_taproot().internal_key(),
33	);
34
35	Transaction {
36		version: bitcoin::transaction::Version(3),
37		lock_time: bitcoin::absolute::LockTime::ZERO,
38		input: vec![
39			TxIn {
40				previous_output: vtxo.point(),
41				sequence: Sequence::MAX,
42				script_sig: ScriptBuf::new(),
43				witness: signature.map(|s| Witness::from_slice(&[&s[..]])).unwrap_or_default(),
44			},
45		],
46		output: vec![
47			TxOut {
48				value: vtxo.amount(),
49				script_pubkey: claim_policy
50					.taproot(vtxo.server_pubkey(), vtxo.exit_delta())
51					.script_pubkey(),
52			},
53			fee::fee_anchor(),
54		],
55	}
56}
57
58#[inline]
59fn hark_forfeit_sighash<G>(
60	vtxo: &Vtxo<G>,
61	unlock_hash: UnlockHash,
62) -> (TapSighash, Transaction) {
63	let exit_prevout = vtxo.txout();
64	let tx = create_hark_forfeit_tx(vtxo, unlock_hash, None);
65	let sighash = SighashCache::new(&tx).taproot_key_spend_signature_hash(
66		0, &sighash::Prevouts::All(&[exit_prevout]), TapSighashType::Default,
67	).expect("sighash error");
68	(sighash, tx)
69}
70
71/// Construct the internal VTXO that represents the forfeit output
72///
73/// The `forfeit_txid` argument is optional and will be calculated if not present.
74#[inline]
75fn build_internal_forfeit_vtxo(
76	vtxo: &Vtxo<Full>,
77	unlock_hash: UnlockHash,
78	forfeit_tx_sig: schnorr::Signature,
79	forfeit_txid: Option<Txid>,
80) -> ServerVtxo<Full> {
81	let ff_txid = forfeit_txid.unwrap_or_else(|| {
82		create_hark_forfeit_tx(vtxo, unlock_hash, None).compute_txid()
83	});
84	debug_assert_eq!(ff_txid, create_hark_forfeit_tx(vtxo, unlock_hash, None).compute_txid());
85
86	Vtxo {
87		point: OutPoint::new(ff_txid, 0),
88		policy: ServerVtxoPolicy::new_hark_forfeit(vtxo.user_pubkey(), unlock_hash),
89		genesis: Full {
90			items: vtxo.genesis.items.iter().cloned().chain([
91				GenesisItem {
92					transition: GenesisTransition::Arkoor(ArkoorGenesis {
93						client_cosigners: vec![vtxo.user_pubkey()],
94						tap_tweak: vtxo.output_taproot().tap_tweak(),
95						signature: Some(forfeit_tx_sig),
96					}),
97					output_idx: 0,
98					other_outputs: vec![],
99					fee_amount: Amount::ZERO,
100				}
101			]).collect(),
102		},
103
104		amount: vtxo.amount,
105		expiry_height: vtxo.expiry_height,
106		server_pubkey: vtxo.server_pubkey,
107		exit_delta: vtxo.exit_delta,
108		anchor_point: vtxo.anchor_point,
109	}
110}
111
112/// A bundle of a signature and metadata that forfeits a user's VTXO
113/// conditional on the server revealing a secret preimage
114#[derive(Debug, Clone, PartialEq, Eq)]
115pub struct HashLockedForfeitBundle {
116	pub vtxo_id: VtxoId,
117	pub unlock_hash: UnlockHash,
118	pub user_nonce: musig::PublicNonce,
119	/// User's partial signature on the forfeit tx
120	pub part_sig: musig::PartialSignature,
121}
122
123impl HashLockedForfeitBundle {
124	/// Create a new [HashLockedForfeitBundle] for the given VTXO
125	///
126	/// This is used to forfeit the VTXO to the server conditional on receiving
127	/// the unlock preimage corresponding to the given unlock hash.
128	pub fn new<G>(
129		vtxo: &Vtxo<G>,
130		unlock_hash: UnlockHash,
131		user_key: &Keypair,
132		server_nonce: &musig::PublicNonce,
133	) -> Self {
134		let vtxo_exit_taproot = vtxo.output_taproot();
135		let (ff_sighash, _) = hark_forfeit_sighash(vtxo, unlock_hash);
136		let (ff_sec_nonce, ff_pub_nonce) = musig::nonce_pair_with_msg(
137			user_key, &ff_sighash.to_byte_array(),
138		);
139		let ff_agg_nonce = musig::nonce_agg(&[&ff_pub_nonce, &server_nonce]);
140		let (ff_part_sig, _sig) = musig::partial_sign(
141			[vtxo.user_pubkey(), vtxo.server_pubkey()],
142			ff_agg_nonce,
143			user_key,
144			ff_sec_nonce,
145			ff_sighash.to_byte_array(),
146			Some(vtxo_exit_taproot.tap_tweak().to_byte_array()),
147			None,
148		);
149
150		Self {
151			vtxo_id: vtxo.id(),
152			unlock_hash: unlock_hash,
153			user_nonce: ff_pub_nonce,
154			part_sig: ff_part_sig,
155		}
156	}
157
158	/// Used by the server to verify if the partial signature in the bundle
159	/// is valid
160	pub fn verify<G>(
161		&self,
162		vtxo: &Vtxo<G>,
163		server_nonce: &musig::PublicNonce,
164	) -> Result<(), &'static str> {
165		if vtxo.id() != self.vtxo_id {
166			return Err("VTXO mismatch");
167		}
168
169		let ff_agg_nonce = musig::nonce_agg(
170			&[&self.user_nonce, &server_nonce],
171		);
172		let vtxo_exit_taproot = vtxo.output_taproot();
173		let (ff_sighash, _) = hark_forfeit_sighash(vtxo, self.unlock_hash);
174		let (ff_key_agg, _) = musig::tweaked_key_agg(
175			[vtxo.user_pubkey(), vtxo.server_pubkey()],
176			vtxo_exit_taproot.tap_tweak().to_byte_array(),
177		);
178		let ff_session = musig::Session::new(
179			&ff_key_agg,
180			ff_agg_nonce,
181			&ff_sighash.to_byte_array(),
182		);
183		let success = ff_session.partial_verify(
184			&ff_key_agg, &self.part_sig, &self.user_nonce, musig::pubkey_to(vtxo.user_pubkey()),
185		);
186		if !success {
187			return Err("invalid partial sig for forfeit tx");
188		}
189		Ok(())
190	}
191
192	/// Used by the server to finish the forfeit signature using its own
193	/// nonce
194	///
195	/// NB users don't need to know this signature
196	pub fn finish(
197		&self,
198		vtxo: &Vtxo<Full>,
199		server_pub_nonce: &musig::PublicNonce,
200		server_sec_nonce: musig::SecretNonce,
201		server_key: &Keypair,
202	) -> (schnorr::Signature, Transaction, ServerVtxo<Full>) {
203		assert_eq!(vtxo.id(), self.vtxo_id);
204
205		let ff_agg_nonce = musig::nonce_agg(
206			&[&self.user_nonce, &server_pub_nonce],
207		);
208		let vtxo_exit_taproot = vtxo.output_taproot();
209		let (ff_sighash, mut ff_tx) = hark_forfeit_sighash(vtxo, self.unlock_hash);
210		let (_ff_part_sig, ff_sig) = musig::partial_sign(
211			[vtxo.user_pubkey(), vtxo.server_pubkey()],
212			ff_agg_nonce,
213			server_key,
214			server_sec_nonce,
215			ff_sighash.to_byte_array(),
216			Some(vtxo_exit_taproot.tap_tweak().to_byte_array()),
217			Some(&[&self.part_sig]),
218		);
219		let ff_sig = ff_sig.expect("forfeit tx sig error");
220		debug_assert!({
221			let (ff_key_agg, _) = musig::tweaked_key_agg(
222				[vtxo.user_pubkey(), vtxo.server_pubkey()],
223				vtxo_exit_taproot.tap_tweak().to_byte_array(),
224			);
225			let ff_session = musig::Session::new(
226				&ff_key_agg,
227				ff_agg_nonce,
228				&ff_sighash.to_byte_array(),
229			);
230			ff_session.partial_verify(
231				&ff_key_agg,
232				&_ff_part_sig,
233				&server_pub_nonce,
234				musig::pubkey_to(vtxo.server_pubkey()),
235			)
236		});
237		debug_assert_eq!(Ok(()), SECP.verify_schnorr(
238			&ff_sig, &ff_sighash.into(), &vtxo_exit_taproot.output_key().to_x_only_public_key(),
239		));
240
241		// fill in the signature in the tx
242		ff_tx.input[0].witness = Witness::from_slice(&[&ff_sig[..]]);
243		debug_assert_eq!(ff_tx, create_hark_forfeit_tx(vtxo, self.unlock_hash, Some(&ff_sig)));
244
245		let ff_txid = ff_tx.compute_txid();
246		let ff_vtxo = build_internal_forfeit_vtxo(vtxo, self.unlock_hash, ff_sig, Some(ff_txid));
247
248		(ff_sig, ff_tx, ff_vtxo)
249	}
250}
251
252/// The serialization version of [HashLockedForfeitBundle].
253const HASH_LOCKED_FORFEIT_BUNDLE_VERSION: u8 = 0x01;
254
255impl ProtocolEncoding for HashLockedForfeitBundle {
256	fn encode<W: std::io::Write + ?Sized>(&self, w: &mut W) -> Result<(), std::io::Error> {
257		w.emit_u8(HASH_LOCKED_FORFEIT_BUNDLE_VERSION)?;
258		self.vtxo_id.encode(w)?;
259		self.unlock_hash.encode(w)?;
260		self.user_nonce.encode(w)?;
261		self.part_sig.encode(w)?;
262		Ok(())
263	}
264
265	fn decode<R: std::io::Read + ?Sized>(r: &mut R) -> Result<Self, ProtocolDecodingError> {
266		let ver = r.read_u8()?;
267		if ver != HASH_LOCKED_FORFEIT_BUNDLE_VERSION {
268			return Err(ProtocolDecodingError::invalid("unknown encoding version"));
269		}
270		Ok(Self {
271			vtxo_id: ProtocolEncoding::decode(r)?,
272			unlock_hash: ProtocolEncoding::decode(r)?,
273			user_nonce: ProtocolEncoding::decode(r)?,
274			part_sig: ProtocolEncoding::decode(r)?,
275		})
276	}
277}
278
279#[inline]
280pub fn create_connector_forfeit_tx<G>(
281	vtxo: &Vtxo<G>,
282	connector: OutPoint,
283	forfeit_sig: Option<&schnorr::Signature>,
284	connector_sig: Option<&schnorr::Signature>,
285) -> Transaction {
286	Transaction {
287		version: bitcoin::transaction::Version(3),
288		lock_time: bitcoin::absolute::LockTime::ZERO,
289		input: vec![
290			TxIn {
291				previous_output: vtxo.point(),
292				sequence: Sequence::ZERO,
293				script_sig: ScriptBuf::new(),
294				witness: forfeit_sig.map(|s| Witness::from_slice(&[&s[..]])).unwrap_or_default(),
295			},
296			TxIn {
297				previous_output: connector,
298				sequence: Sequence::ZERO,
299				script_sig: ScriptBuf::new(),
300				witness: connector_sig.map(|s| Witness::from_slice(&[&s[..]])).unwrap_or_default(),
301			},
302		],
303		output: vec![
304			TxOut {
305				value: vtxo.amount(),
306				script_pubkey: ScriptBuf::new_p2tr(&SECP, vtxo.server_pubkey().into(), None),
307			},
308			// We throw the connector dust value into the fee anchor
309			// because we can't have zero-value anchors and a non-zero fee.
310			fee::fee_anchor_with_amount(P2TR_DUST),
311		],
312	}
313}
314
315#[inline]
316fn connector_forfeit_input_sighash<G>(
317	vtxo: &Vtxo<G>,
318	connector: OutPoint,
319	connector_pk: PublicKey,
320	input_idx: usize,
321) -> (TapSighash, Transaction) {
322	let exit_prevout = vtxo.txout();
323	let connector_prevout = TxOut {
324		script_pubkey: ConnectorChain::output_script(connector_pk),
325		value: P2TR_DUST,
326	};
327	let tx = create_connector_forfeit_tx(vtxo, connector, None, None);
328	let sighash = SighashCache::new(&tx).taproot_key_spend_signature_hash(
329		input_idx,
330		&sighash::Prevouts::All(&[exit_prevout, connector_prevout]),
331		TapSighashType::Default,
332	).expect("sighash error");
333	(sighash, tx)
334}
335
336/// The sighash of the exit tx input of a forfeit tx.
337#[inline]
338pub fn connector_forfeit_sighash_exit<G>(
339	vtxo: &Vtxo<G>,
340	connector: OutPoint,
341	connector_pk: PublicKey,
342) -> (TapSighash, Transaction) {
343	connector_forfeit_input_sighash(vtxo, connector, connector_pk, 0)
344}
345
346/// The sighash of the connector input of a forfeit tx.
347#[inline]
348pub fn connector_forfeit_sighash_connector<G>(
349	vtxo: &Vtxo<G>,
350	connector: OutPoint,
351	connector_pk: PublicKey,
352) -> (TapSighash, Transaction) {
353	connector_forfeit_input_sighash(vtxo, connector, connector_pk, 1)
354}
355
356#[cfg(test)]
357mod test {
358	use std::str::FromStr;
359	use bitcoin::hex::{DisplayHex, FromHex};
360	use crate::test_util::{verify_tx, VTXO_VECTORS};
361	use crate::tree::signed::UnlockPreimage;
362	use super::*;
363
364	fn verify_hark_forfeits(
365		vtxo: &Vtxo<Full>,
366		unlock_preimage: UnlockPreimage,
367		server_sec_nonce: musig::SecretNonce,
368		server_pub_nonce: &musig::PublicNonce,
369		bundle: HashLockedForfeitBundle,
370	) {
371		let unlock_hash = UnlockHash::hash(&unlock_preimage);
372		assert_eq!(Ok(()), bundle.verify(vtxo, server_pub_nonce));
373
374		// finish it which triggers debug asserts on partial sigs
375		let (sig, tx, _vtxo) = bundle.finish(vtxo, server_pub_nonce, server_sec_nonce, &VTXO_VECTORS.server_key);
376
377		let (ff_sighash, ff_tx) = hark_forfeit_sighash(vtxo, unlock_hash);
378		SECP.verify_schnorr(
379			&sig,
380			&ff_sighash.into(),
381			&vtxo.output_taproot().output_key().to_x_only_public_key(),
382		).expect("forfeit tx sig check failed");
383		let ff_point = OutPoint::new(ff_tx.compute_txid(), 0);
384
385		// validate the actual txs
386		let ff_input = vtxo.txout();
387		let ff_tx_expected = create_hark_forfeit_tx(vtxo, unlock_hash, Some(&sig));
388		assert_eq!(ff_tx_expected, tx);
389		verify_tx(&[ff_input], 0, &ff_tx_expected).expect("forfeit tx error");
390		assert_eq!(ff_tx_expected.compute_txid(), ff_point.txid);
391	}
392
393	#[test]
394	fn test_hark_forfeits() {
395		let (server_sec_nonce, server_pub_nonce) = musig::nonce_pair(&VTXO_VECTORS.server_key);
396		// we need to go through some hoops to print the secret nonces
397		let server_sec_bytes = server_sec_nonce.dangerous_into_bytes();
398		println!("server ff sec nonce: {}", server_sec_bytes.as_hex());
399		let server_sec_nonce = musig::SecretNonce::dangerous_from_bytes(server_sec_bytes);
400		println!("server pub nonces: {}", server_pub_nonce.serialize_hex());
401
402		let vtxo = &VTXO_VECTORS.arkoor3_vtxo;
403		let unlock_preimage = UnlockPreimage::from_hex("c65f29e65dbc6cbad3e7f35c41986487c74ed513aeb37778354d42f3b0714645").unwrap();
404		let unlock_hash = UnlockHash::hash(&unlock_preimage);
405		let bundle = HashLockedForfeitBundle::new(
406			vtxo,
407			unlock_hash,
408			&VTXO_VECTORS.arkoor3_user_key,
409			&server_pub_nonce,
410		);
411
412		// test encoding round trip
413		let encoded = bundle.serialize();
414		println!("bundle: {}", encoded.as_hex());
415		let decoded = HashLockedForfeitBundle::deserialize(&encoded).unwrap();
416		assert_eq!(bundle, decoded);
417		let bundle = decoded;
418
419		println!("verifying generated forfeits");
420		verify_hark_forfeits(
421			vtxo, unlock_preimage, server_sec_nonce, &server_pub_nonce, bundle.clone(),
422		);
423
424		let (_sec, bad_nonce) = musig::nonce_pair(&VTXO_VECTORS.server_key);
425		assert_eq!(
426			bundle.verify(vtxo, &bad_nonce),
427			Err("invalid partial sig for forfeit tx"),
428		);
429
430
431		// verify a hard-coded example from a previous run of this test
432		let server_sec_nonce = musig::SecretNonce::dangerous_from_bytes(FromHex::from_hex(
433			"220edcf1e0025bafa93c541763eb869fb7325c235fea6f0b9e9b32ae7de3aa416a798bd0c1deecbd2b61bac43ca683c296631e1e555fec9fd200ce973e62f6f387a85e47622bf70a8243580d1879746ffe940588c5ad9d478d1b46e2bb9318743312a8657f684b47f963f7a0e95927b2c71005112d8edc5821a3f6f0f7bd6354947ff8ac",
434		).unwrap());
435		let server_pub_nonce = musig::PublicNonce::from_str("03b4d5711322b73c8c412d0d6a6bce0af1eb98662c6fac8faeab3d6979d77c7430034ff4c184887f764d00af812a1c155ca8be4d18a6c012fea0d6b4a819b451414e").unwrap();
436		let bundle = HashLockedForfeitBundle::deserialize_hex("0167754b5920d282ad276e8c80e1088f09238698bb674906fe47a0bfe2e9855912000000003d5491373df6a016f78b3f46d65a4fc6948824c43a59620404e8719cfee05d1a02afcb2341f8ff179d12fe0bf586653dc1ae91f97df02ab25958ecc37675b611bd0225697fa9181018d4a8b2130d4464df40beb48b80534dec43662b0115ac5dad95a102613bd646bceb9afbd895e8092ec504e5b4a419c027114445dff521569bf5").unwrap();
437
438		println!("verifying hard-coded forfeits");
439		verify_hark_forfeits(vtxo, unlock_preimage, server_sec_nonce, &server_pub_nonce, bundle);
440	}
441}