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