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#[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#[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#[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#[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 pub part_sig: musig::PartialSignature,
129}
130
131impl HashLockedForfeitBundle {
132 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 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 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 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
260const 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 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#[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#[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 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 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 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 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 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}