1use std::fmt;
2
3use bitcoin::secp256k1::{schnorr, PublicKey};
4use bitcoin::{Amount, OutPoint, Sequence, ScriptBuf, Transaction, TxIn, TxOut, Witness};
5use bitcoin::hashes::{sha256, Hash};
6use bitcoin::key::TweakedPublicKey;
7use bitcoin::sighash;
8use bitcoin::taproot::{self, TapLeafHash, LeafVersion, TapTweakHash};
9
10use bitcoin_ext::{fee, BlockDelta, BlockHeight, TaprootSpendInfoExt};
11
12use crate::SECP;
13use crate::musig;
14use crate::tree::signed::{
15 cosign_taproot, leaf_cosign_taproot, leaf_cosign_taproot_v0, unlock_clause, unlock_clause_v0,
16};
17use crate::vtxo::MaybePreimage;
18
19pub enum TransitionKind {
21 Cosigned,
22 HashLockedCosigned,
23 #[allow(non_camel_case_types)]
24 HashLockedCosigned_v0,
25 Arkoor,
26}
27
28impl TransitionKind {
29 pub const fn as_str(&self) -> &'static str {
30 match self {
31 Self::Cosigned => "cosigned",
32 Self::HashLockedCosigned => "hash-locked-cosigned-v1",
33 Self::HashLockedCosigned_v0 => "hash-locked-cosigned",
34 Self::Arkoor => "arkoor",
35 }
36 }
37}
38
39impl fmt::Display for TransitionKind {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 f.write_str(self.as_str())
42 }
43}
44
45impl fmt::Debug for TransitionKind {
46 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47 fmt::Display::fmt(self, f)
48 }
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct CosignedGenesis {
53 pub pubkeys: Vec<PublicKey>,
58 pub signature: Option<schnorr::Signature>,
59}
60
61impl CosignedGenesis {
62
63 pub fn input_taproot(
65 &self,
66 server_pubkey: PublicKey,
67 expiry_height: BlockHeight,
68 ) -> taproot::TaprootSpendInfo {
69 let agg_pk = musig::combine_keys(self.pubkeys.iter().copied())
70 .x_only_public_key().0;
71 cosign_taproot(agg_pk, server_pubkey, expiry_height)
72 }
73
74 pub fn input_txout(
75 &self,
76 amount: Amount,
77 server_pubkey: PublicKey,
78 expiry_height: BlockHeight,
79 ) -> TxOut {
80 TxOut {
81 value: amount,
82 script_pubkey: self.input_taproot(server_pubkey, expiry_height).script_pubkey(),
83 }
84 }
85
86 pub fn witness(&self) -> Witness {
87 match self.signature {
88 Some(ref sig) => Witness::from_slice(&[&sig[..]]),
89 None => Witness::new(),
90 }
91 }
92
93 pub fn has_all_witnesses(&self) -> bool {
95 self.signature.is_some()
96 }
97
98 pub fn validate_sigs(
99 &self,
100 tx: &Transaction,
101 input_idx: usize,
102 prev_txout: &TxOut,
103 server_pubkey: PublicKey,
104 expiry_height: BlockHeight,
105 ) -> Result<(), &'static str> {
106 let signature = match self.signature {
107 Some(sig) => sig,
108 None => return Err("missing cosigned signature"),
109 };
110
111 let mut shc = sighash::SighashCache::new(tx);
112
113 let tapsighash = shc.taproot_key_spend_signature_hash(
114 input_idx,
115 &sighash::Prevouts::All(&[prev_txout]),
116 sighash::TapSighashType::Default
117 ).expect("correct prevouts");
118
119 let pubkey = self.input_taproot(server_pubkey, expiry_height)
120 .output_key()
121 .to_x_only_public_key();
122
123 SECP.verify_schnorr(&signature, &tapsighash.into(), &pubkey)
124 .map_err(|_| "invalid signature")
125 }
126}
127
128
129#[derive(Debug, Clone, PartialEq, Eq)]
130pub struct HashLockedCosignedGenesis {
131 pub user_pubkey: PublicKey,
133 pub signature: Option<schnorr::Signature>,
135 pub unlock: MaybePreimage,
137}
138
139impl HashLockedCosignedGenesis {
140 pub fn input_taproot(
141 &self,
142 server_pubkey: PublicKey,
143 expiry_height: BlockHeight,
144 ) -> taproot::TaprootSpendInfo {
145 leaf_cosign_taproot(self.user_pubkey, server_pubkey, expiry_height, self.unlock.hash())
146 }
147
148 pub fn input_txout(
149 &self,
150 amount: Amount,
151 server_pubkey: PublicKey,
152 expiry_height: BlockHeight,
153 ) -> TxOut {
154 TxOut {
155 value: amount,
156 script_pubkey: self.input_taproot(server_pubkey, expiry_height).script_pubkey(),
157 }
158 }
159
160 pub fn witness(
161 &self,
162 server_pubkey: PublicKey,
163 expiry_height: BlockHeight,
164 ) -> Witness {
165 let preimage = match self.unlock {
167 MaybePreimage::Preimage(p) => p,
168 MaybePreimage::Hash(_) => return Witness::new(),
169 };
170
171 let sig = match self.signature {
172 Some(sig) => sig,
173 None => return Witness::new(),
174 };
175
176 let unlock_hash = sha256::Hash::hash(&preimage);
177 let taproot = leaf_cosign_taproot(
178 self.user_pubkey, server_pubkey, expiry_height, unlock_hash,
179 );
180
181 let clause = unlock_clause(taproot.internal_key(), unlock_hash);
182 let script_leaf = (clause, LeafVersion::TapScript);
183 let cb = taproot.control_block(&script_leaf)
184 .expect("unlock clause not found in hArk taproot");
185 Witness::from_slice(&[
186 &sig.serialize()[..],
187 &preimage[..],
188 &script_leaf.0.as_bytes(),
189 &cb.serialize()[..],
190 ])
191 }
192
193 pub fn has_all_witnesses(&self) -> bool {
195 match self.unlock {
196 MaybePreimage::Preimage(_) => {},
197 MaybePreimage::Hash(_) => return false,
198 };
199
200 match self.signature {
201 Some(_) => true,
202 None => false,
203 }
204 }
205
206 pub fn validate_sigs(
207 &self,
208 tx: &Transaction,
209 input_idx: usize,
210 prev_txout: &TxOut,
211 server_pubkey: PublicKey,
212 expiry_height: BlockHeight,
213 ) -> Result<(), &'static str> {
214 match self.unlock {
215 MaybePreimage::Preimage(_) => {},
216 MaybePreimage::Hash(_) => return Err("missing preimage")
217 };
218
219 let mut shc = sighash::SighashCache::new(tx);
220 let agg_pk = musig::combine_keys([self.user_pubkey, server_pubkey])
221 .x_only_public_key().0;
222 let script = unlock_clause(agg_pk, self.unlock.hash());
223 let leaf = TapLeafHash::from_script(&script, bitcoin::taproot::LeafVersion::TapScript);
224 let tapsighash = shc.taproot_script_spend_signature_hash(
225 input_idx, &sighash::Prevouts::All(&[prev_txout]), leaf, sighash::TapSighashType::Default,
226 ).expect("correct prevouts");
227
228 let pk = self.input_taproot(server_pubkey, expiry_height)
229 .internal_key();
230
231 match self.signature {
232 None => return Err("missing signature"),
233 Some(sig) => {
234 SECP.verify_schnorr(&sig, &tapsighash.into(), &pk)
235 .map_err(|_| "invalid signature")
236 }
237 }
238 }
239}
240
241
242#[derive(Debug, Clone, PartialEq, Eq)]
243#[allow(non_camel_case_types)]
244pub struct HashLockedCosignedGenesis_v0 {
245 pub user_pubkey: PublicKey,
247 pub signature: Option<schnorr::Signature>,
249 pub unlock: MaybePreimage,
251}
252
253impl HashLockedCosignedGenesis_v0 {
254 pub fn input_taproot(
255 &self,
256 server_pubkey: PublicKey,
257 expiry_height: BlockHeight,
258 ) -> taproot::TaprootSpendInfo {
259 leaf_cosign_taproot_v0(self.user_pubkey, server_pubkey, expiry_height, self.unlock.hash())
260 }
261
262 pub fn input_txout(
263 &self,
264 amount: Amount,
265 server_pubkey: PublicKey,
266 expiry_height: BlockHeight,
267 ) -> TxOut {
268 TxOut {
269 value: amount,
270 script_pubkey: self.input_taproot(server_pubkey, expiry_height).script_pubkey(),
271 }
272 }
273
274 pub fn witness(
275 &self,
276 server_pubkey: PublicKey,
277 expiry_height: BlockHeight,
278 ) -> Witness {
279 let preimage = match self.unlock {
281 MaybePreimage::Preimage(p) => p,
282 MaybePreimage::Hash(_) => return Witness::new(),
283 };
284
285 let sig = match self.signature {
286 Some(sig) => sig,
287 None => return Witness::new(),
288 };
289
290 let unlock_hash = sha256::Hash::hash(&preimage);
291 let taproot = leaf_cosign_taproot_v0(
292 self.user_pubkey, server_pubkey, expiry_height, unlock_hash,
293 );
294
295 let clause = unlock_clause_v0(taproot.internal_key(), unlock_hash);
296 let script_leaf = (clause, LeafVersion::TapScript);
297 let cb = taproot.control_block(&script_leaf)
298 .expect("unlock clause not found in hArk taproot");
299 Witness::from_slice(&[
300 &sig.serialize()[..],
301 &preimage[..],
302 &script_leaf.0.as_bytes(),
303 &cb.serialize()[..],
304 ])
305 }
306
307 pub fn has_all_witnesses(&self) -> bool {
309 match self.unlock {
310 MaybePreimage::Preimage(_) => {},
311 MaybePreimage::Hash(_) => return false,
312 };
313
314 match self.signature {
315 Some(_) => true,
316 None => false,
317 }
318 }
319
320 pub fn validate_sigs(
321 &self,
322 tx: &Transaction,
323 input_idx: usize,
324 prev_txout: &TxOut,
325 server_pubkey: PublicKey,
326 expiry_height: BlockHeight,
327 ) -> Result<(), &'static str> {
328 match self.unlock {
329 MaybePreimage::Preimage(_) => {},
330 MaybePreimage::Hash(_) => return Err("missing preimage")
331 };
332
333 let mut shc = sighash::SighashCache::new(tx);
334 let agg_pk = musig::combine_keys([self.user_pubkey, server_pubkey])
335 .x_only_public_key().0;
336 let script = unlock_clause_v0(agg_pk, self.unlock.hash());
337 let leaf = TapLeafHash::from_script(&script, bitcoin::taproot::LeafVersion::TapScript);
338 let tapsighash = shc.taproot_script_spend_signature_hash(
339 input_idx, &sighash::Prevouts::All(&[prev_txout]), leaf, sighash::TapSighashType::Default,
340 ).expect("correct prevouts");
341
342 let pk = self.input_taproot(server_pubkey, expiry_height)
343 .internal_key();
344
345 match self.signature {
346 None => return Err("missing signature"),
347 Some(sig) => {
348 SECP.verify_schnorr(&sig, &tapsighash.into(), &pk)
349 .map_err(|_| "invalid signature")
350 }
351 }
352 }
353}
354
355
356#[derive(Debug, Clone, PartialEq, Eq)]
357pub struct ArkoorGenesis {
358 pub client_cosigners: Vec<PublicKey>,
361 pub tap_tweak: taproot::TapTweakHash,
362 pub signature: Option<schnorr::Signature>,
363}
364
365impl ArkoorGenesis {
366 pub fn client_cosigners(&self) -> impl Iterator<Item = PublicKey> + '_ {
367 self.client_cosigners.iter().copied()
368 }
369
370 pub fn cosigners<'a>(&'a self, server_pubkey: PublicKey) -> impl Iterator<Item = PublicKey> + 'a {
371 self.client_cosigners.iter().cloned().chain([server_pubkey])
372 }
373
374 pub fn input_txout(&self, amount: Amount, server_pubkey: PublicKey) -> TxOut {
375 TxOut {
376 value: amount,
377 script_pubkey: ScriptBuf::new_p2tr_tweaked(self.output_key(server_pubkey))
378 }
379 }
380
381 pub fn output_key(&self, server_pubkey: PublicKey) -> TweakedPublicKey {
382 let (_, agg_pk) = musig::tweaked_key_agg(self.cosigners(server_pubkey), self.tap_tweak.to_byte_array());
383 TweakedPublicKey::dangerous_assume_tweaked(agg_pk.x_only_public_key().0)
384 }
385
386 pub fn witness(&self) -> Witness {
387 match self.signature {
388 Some(sig) => Witness::from_slice(&[&sig[..]]),
389 None => Witness::new(),
390 }
391 }
392
393 pub fn has_all_witnesses(&self) -> bool {
395 self.signature.is_some()
396 }
397
398 pub fn validate_sigs(
399 &self,
400 tx: &Transaction,
401 input_idx: usize,
402 prev_txout: &TxOut,
403 server_pubkey: PublicKey,
404 ) -> Result<(), &'static str> {
405 let signature = match self.signature {
406 Some(sig) => sig,
407 None => return Err("missing signature"),
408 };
409
410 let mut shc = sighash::SighashCache::new(tx);
411
412 let tapsighash = shc.taproot_key_spend_signature_hash(
413 input_idx,
414 &sighash::Prevouts::All(&[prev_txout]),
415 sighash::TapSighashType::Default
416 ).expect("correct prevouts");
417
418
419 SECP.verify_schnorr(
420 &signature,
421 &tapsighash.into(),
422 &self.output_key(server_pubkey).to_x_only_public_key(),
423 ).map_err(|_| "invalid signature")
424 }
425}
426
427#[derive(Debug, Clone, PartialEq, Eq)]
431pub enum GenesisTransition {
432 Cosigned(CosignedGenesis),
437 HashLockedCosigned(HashLockedCosignedGenesis),
446 #[allow(non_camel_case_types)]
451 HashLockedCosigned_v0(HashLockedCosignedGenesis_v0),
452 Arkoor(ArkoorGenesis),
454}
455
456impl GenesisTransition {
457 pub fn new_cosigned(pubkeys: Vec<PublicKey>, signature: Option<schnorr::Signature>) -> Self {
458 Self::Cosigned(CosignedGenesis { pubkeys, signature })
459 }
460
461 pub fn new_hash_locked_cosigned(
462 user_pubkey: PublicKey,
463 signature: Option<schnorr::Signature>,
464 unlock: MaybePreimage
465 ) -> Self {
466 Self::HashLockedCosigned(
467 HashLockedCosignedGenesis { user_pubkey, signature, unlock }
468 )
469 }
470
471
472 pub fn new_arkoor(
473 cosigners: Vec<PublicKey>,
474 tap_tweak: TapTweakHash,
475 signature: Option<schnorr::Signature>
476 ) -> Self {
477 Self::Arkoor(ArkoorGenesis { client_cosigners: cosigners, tap_tweak, signature })
478 }
479
480 pub fn input_txout(
482 &self,
483 amount: Amount,
484 server_pubkey: PublicKey,
485 expiry_height: BlockHeight,
486 _exit_delta: BlockDelta,
487 ) -> TxOut {
488 match self {
489 Self::Cosigned(inner) => inner.input_txout(amount, server_pubkey, expiry_height),
490 Self::HashLockedCosigned(inner) => inner.input_txout(amount, server_pubkey, expiry_height),
491 Self::HashLockedCosigned_v0(inner) => inner.input_txout(amount, server_pubkey, expiry_height),
492 Self::Arkoor(inner) => inner.input_txout(amount, server_pubkey),
493 }
494 }
495
496 pub fn witness(
498 &self,
499 server_pubkey: PublicKey,
500 expiry_height: BlockHeight,
501 ) -> Witness {
502 match self {
503 Self::Cosigned(inner) => inner.witness(),
504 Self::HashLockedCosigned(inner) => inner.witness(server_pubkey, expiry_height),
505 Self::HashLockedCosigned_v0(inner) => inner.witness(server_pubkey, expiry_height),
506 Self::Arkoor(inner) => inner.witness(),
507 }
508 }
509
510
511 pub fn has_all_witnesses(&self) -> bool {
513 match self {
514 Self::Cosigned(inner) => inner.has_all_witnesses(),
515 Self::HashLockedCosigned(inner) => inner.has_all_witnesses(),
516 Self::HashLockedCosigned_v0(inner) => inner.has_all_witnesses(),
517 Self::Arkoor(inner) => inner.has_all_witnesses(),
518 }
519 }
520
521 pub fn kind(&self) -> TransitionKind {
523 match self {
524 Self::Cosigned { .. } => TransitionKind::Cosigned,
525 Self::HashLockedCosigned { .. } => TransitionKind::HashLockedCosigned,
526 Self::HashLockedCosigned_v0 { .. } => TransitionKind::HashLockedCosigned_v0,
527 Self::Arkoor { .. } => TransitionKind::Arkoor,
528 }
529 }
530}
531
532#[derive(Debug, Clone, PartialEq, Eq)]
536pub struct GenesisItem {
537 pub transition: GenesisTransition,
539 pub output_idx: u8,
541 pub other_outputs: Vec<TxOut>,
543 pub fee_amount: Amount,
546}
547
548impl GenesisItem {
549 pub fn fee_anchor(&self) -> TxOut {
551 fee::fee_anchor_with_amount(self.fee_amount)
552 }
553
554 pub fn other_output_sum(&self) -> Option<Amount> {
556 let mut result = self.fee_amount;
557 for o in &self.other_outputs {
558 result = result.checked_add(o.value)?;
559 }
560 Some(result)
561 }
562
563 pub fn tx(&self,
565 prev: OutPoint,
566 next: TxOut,
567 server_pubkey: PublicKey,
568 expiry_height: BlockHeight,
569 ) -> Transaction {
570 Transaction {
571 version: bitcoin::transaction::Version(3),
572 lock_time: bitcoin::absolute::LockTime::ZERO,
573 input: vec![TxIn {
574 previous_output: prev,
575 script_sig: ScriptBuf::new(),
576 sequence: Sequence::ZERO,
577 witness: self.transition.witness(server_pubkey, expiry_height),
578 }],
579 output: {
580 let mut out = Vec::with_capacity(self.other_outputs.len().saturating_add(2));
581 out.extend(self.other_outputs.iter().take(self.output_idx as usize).cloned());
582 out.push(next);
583 out.extend(self.other_outputs.iter().skip(self.output_idx as usize).cloned());
584 out.push(self.fee_anchor());
585 out
586 },
587 }
588 }
589}