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#[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#[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#[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 pub part_sig: musig::PartialSignature,
121}
122
123impl HashLockedForfeitBundle {
124 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 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 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 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
252const 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 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#[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#[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 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 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 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 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 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}