1use std::marker::PhantomData;
15
16use bitcoin::sighash::{self, SighashCache};
17use bitcoin::taproot::TaprootSpendInfo;
18use bitcoin::{Amount, OutPoint, ScriptBuf, TapSighash, Transaction, TxOut, Txid};
19use bitcoin::hashes::Hash;
20use bitcoin::secp256k1::{Keypair, PublicKey};
21
22use bitcoin_ext::{BlockDelta, BlockHeight, TaprootSpendInfoExt};
23
24use crate::error::IncorrectSigningKeyError;
25use crate::{musig, scripts, SECP};
26use crate::tree::signed::cosign_taproot;
27use crate::vtxo::{self, Full, Vtxo, VtxoId, VtxoPolicy, ServerVtxo, ServerVtxoPolicy, GenesisItem, GenesisTransition};
28
29use self::state::BuilderState;
30
31
32pub const BOARD_FUNDING_TX_VTXO_VOUT: u32 = 0;
34
35#[derive(Debug)]
37struct ExitData {
38 sighash: TapSighash,
39 funding_taproot: TaprootSpendInfo,
40 tx: Transaction,
41 txid: Txid,
42}
43
44fn compute_exit_data(
45 user_pubkey: PublicKey,
46 server_pubkey: PublicKey,
47 expiry_height: BlockHeight,
48 exit_delta: BlockDelta,
49 amount: Amount,
50 fee: Amount,
51 utxo: OutPoint,
52) -> ExitData {
53 let combined_pubkey = musig::combine_keys([user_pubkey, server_pubkey])
54 .x_only_public_key().0;
55 let funding_taproot = cosign_taproot(combined_pubkey, server_pubkey, expiry_height);
56 let funding_txout = TxOut {
57 value: amount,
58 script_pubkey: funding_taproot.script_pubkey(),
59 };
60
61 let exit_taproot = VtxoPolicy::new_pubkey(user_pubkey)
62 .taproot(server_pubkey, exit_delta, expiry_height);
63 let exit_txout = TxOut {
64 value: amount - fee,
65 script_pubkey: exit_taproot.script_pubkey(),
66 };
67
68 let tx = vtxo::create_exit_tx(utxo, exit_txout, None, fee);
69 let sighash = SighashCache::new(&tx).taproot_key_spend_signature_hash(
70 0, &sighash::Prevouts::All(&[funding_txout]), sighash::TapSighashType::Default,
71 ).expect("matching prevouts");
72
73 let txid = tx.compute_txid();
74 ExitData { sighash, funding_taproot, tx, txid }
75}
76
77#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
78pub enum BoardFundingError {
79 #[error("fee larger than amount: amount {amount}, fee {fee}")]
80 FeeHigherThanAmount {
81 amount: Amount,
82 fee: Amount,
83 },
84 #[error("amount is zero")]
85 ZeroAmount,
86 #[error("amount after fee is <= 0: amount {amount}, fee {fee}")]
87 ZeroAmountAfterFee {
88 amount: Amount,
89 fee: Amount,
90 },
91}
92
93#[derive(Debug, Clone, thiserror::Error)]
94pub enum BoardFromVtxoError {
95 #[error("funding txid mismatch: expected {expected}, got {got}")]
96 FundingTxMismatch {
97 expected: Txid,
98 got: Txid,
99 },
100 #[error("server pubkey mismatch: expected {expected}, got {got}")]
101 ServerPubkeyMismatch {
102 expected: PublicKey,
103 got: PublicKey,
104 },
105 #[error("vtxo id mismatch: expected {expected}, got {got}")]
106 VtxoIdMismatch {
107 expected: OutPoint,
108 got: OutPoint,
109 },
110 #[error("incorrect number of genesis items {genesis_count}, should be 1")]
111 IncorrectGenesisItemCount {
112 genesis_count: usize,
113 },
114 #[error("funding output mismatch: expected {expected:?}, got {got:?}")]
115 FundingOutputMismatch {
116 expected: TxOut,
117 got: Option<TxOut>,
118 },
119}
120
121#[derive(Debug)]
123pub struct BoardCosignResponse {
124 pub pub_nonce: musig::PublicNonce,
125 pub partial_signature: musig::PartialSignature,
126}
127
128pub mod state {
129 mod sealed {
130 pub trait Sealed {}
132 impl Sealed for super::Preparing {}
133 impl Sealed for super::CanGenerateNonces {}
134 impl Sealed for super::ServerCanCosign {}
135 impl Sealed for super::CanFinish {}
136 impl Sealed for super::ServerCanBuildVtxos {}
137 }
138
139 pub trait BuilderState: sealed::Sealed {}
141
142 pub struct Preparing;
144 impl BuilderState for Preparing {}
145
146 pub struct CanGenerateNonces;
149 impl BuilderState for CanGenerateNonces {}
150
151 pub struct ServerCanCosign;
153 impl BuilderState for ServerCanCosign {}
154
155 pub struct CanFinish;
158 impl BuilderState for CanFinish {}
159
160 pub struct ServerCanBuildVtxos;
162 impl BuilderState for ServerCanBuildVtxos {}
163
164 pub trait CanSign: BuilderState {}
167 impl CanSign for ServerCanCosign {}
168 impl CanSign for CanFinish {}
169
170 pub trait HasFundingDetails: BuilderState {}
172 impl HasFundingDetails for CanGenerateNonces {}
173 impl HasFundingDetails for ServerCanCosign {}
174 impl HasFundingDetails for CanFinish {}
175}
176
177#[derive(Debug)]
185pub struct BoardBuilder<S: BuilderState> {
186 pub user_pubkey: PublicKey,
187 pub expiry_height: BlockHeight,
188 pub server_pubkey: PublicKey,
189 pub exit_delta: BlockDelta,
190
191 amount: Option<Amount>,
192 fee: Option<Amount>,
193 utxo: Option<OutPoint>,
194
195 user_pub_nonce: Option<musig::PublicNonce>,
196 user_sec_nonce: Option<musig::SecretNonce>,
197
198 exit_data: Option<ExitData>,
200
201 signed_vtxo: Option<Vtxo<Full>>,
203
204 _state: PhantomData<S>,
205}
206
207impl<S: BuilderState> BoardBuilder<S> {
208 pub fn funding_script_pubkey(&self) -> ScriptBuf {
210 let combined_pubkey = musig::combine_keys([self.user_pubkey, self.server_pubkey])
211 .x_only_public_key().0;
212 cosign_taproot(combined_pubkey, self.server_pubkey, self.expiry_height).script_pubkey()
213 }
214
215 fn to_state<S2: BuilderState>(self) -> BoardBuilder<S2> {
216 BoardBuilder {
217 user_pubkey: self.user_pubkey,
218 expiry_height: self.expiry_height,
219 server_pubkey: self.server_pubkey,
220 exit_delta: self.exit_delta,
221 amount: self.amount,
222 utxo: self.utxo,
223 fee: self.fee,
224 user_pub_nonce: self.user_pub_nonce,
225 user_sec_nonce: self.user_sec_nonce,
226 exit_data: self.exit_data,
227 signed_vtxo: self.signed_vtxo,
228 _state: PhantomData,
229 }
230 }
231}
232
233impl BoardBuilder<state::Preparing> {
234 pub fn new(
238 user_pubkey: PublicKey,
239 expiry_height: BlockHeight,
240 server_pubkey: PublicKey,
241 exit_delta: BlockDelta,
242 ) -> BoardBuilder<state::Preparing> {
243 BoardBuilder {
244 user_pubkey, expiry_height, server_pubkey, exit_delta,
245 amount: None,
246 utxo: None,
247 fee: None,
248 user_pub_nonce: None,
249 user_sec_nonce: None,
250 exit_data: None,
251 signed_vtxo: None,
252 _state: PhantomData,
253 }
254 }
255
256 pub fn set_funding_details(
259 mut self,
260 amount: Amount,
261 fee: Amount,
262 utxo: OutPoint,
263 ) -> Result<BoardBuilder<state::CanGenerateNonces>, BoardFundingError> {
264 if amount == Amount::ZERO {
265 return Err(BoardFundingError::ZeroAmount);
266 } else if fee > amount {
267 return Err(BoardFundingError::FeeHigherThanAmount { amount, fee });
268 } else if amount - fee == Amount::ZERO {
269 return Err(BoardFundingError::ZeroAmountAfterFee { amount, fee });
270 }
271
272 let exit_data = compute_exit_data(
273 self.user_pubkey, self.server_pubkey, self.expiry_height,
274 self.exit_delta, amount, fee, utxo,
275 );
276
277 self.amount = Some(amount);
278 self.utxo = Some(utxo);
279 self.fee = Some(fee);
280 self.exit_data = Some(exit_data);
281
282 Ok(self.to_state())
283 }
284}
285
286impl BoardBuilder<state::CanGenerateNonces> {
287 pub fn generate_user_nonces(mut self) -> BoardBuilder<state::CanFinish> {
289 let exit_data = self.exit_data.as_ref().expect("state invariant");
290 let funding_taproot = &exit_data.funding_taproot;
291 let exit_sighash = exit_data.sighash;
292
293 let (agg, _) = musig::tweaked_key_agg(
294 [self.user_pubkey, self.server_pubkey],
295 funding_taproot.tap_tweak().to_byte_array(),
296 );
297 let (sec_nonce, pub_nonce) = agg.nonce_gen_with_uniform_randomness(
299 musig::SessionSecretRand::assume_uniformly_random(rand::random()),
300 musig::pubkey_to(self.user_pubkey),
301 &exit_sighash.to_byte_array(),
302 rand::random(),
303 );
304
305 self.user_pub_nonce = Some(pub_nonce);
306 self.user_sec_nonce = Some(sec_nonce);
307 self.to_state()
308 }
309
310 pub fn exit_tx(&self) -> &Transaction {
315 &self.exit_data.as_ref().expect("state invariant").tx
316 }
317
318 pub fn spend_info(&self) -> Vec<(VtxoId, Txid)> {
320 let exit_txid = self.exit_data.as_ref().expect("state invariant").txid;
321 vec![(self.utxo.expect("state invariant").into(), exit_txid)]
322 }
323}
324
325impl<S: state::CanSign> BoardBuilder<S> {
326 pub fn user_pub_nonce(&self) -> &musig::PublicNonce {
327 self.user_pub_nonce.as_ref().expect("state invariant")
328 }
329}
330
331impl BoardBuilder<state::ServerCanCosign> {
332 pub fn new_for_cosign(
335 user_pubkey: PublicKey,
336 expiry_height: BlockHeight,
337 server_pubkey: PublicKey,
338 exit_delta: BlockDelta,
339 amount: Amount,
340 fee: Amount,
341 utxo: OutPoint,
342 user_pub_nonce: musig::PublicNonce,
343 ) -> BoardBuilder<state::ServerCanCosign> {
344 let exit_data = compute_exit_data(
345 user_pubkey, server_pubkey, expiry_height, exit_delta, amount, fee, utxo,
346 );
347
348 BoardBuilder {
349 user_pubkey, expiry_height, server_pubkey, exit_delta,
350 amount: Some(amount),
351 fee: Some(fee),
352 utxo: Some(utxo),
353 user_pub_nonce: Some(user_pub_nonce),
354 user_sec_nonce: None,
355 exit_data: Some(exit_data),
356 signed_vtxo: None,
357 _state: PhantomData,
358 }
359 }
360
361 pub fn server_cosign(&self, key: &Keypair) -> BoardCosignResponse {
365 let exit_data = self.exit_data.as_ref().expect("state invariant");
366 let sighash = exit_data.sighash;
367 let taproot = &exit_data.funding_taproot;
368 let (pub_nonce, partial_signature) = musig::deterministic_partial_sign(
369 key,
370 [self.user_pubkey],
371 &[&self.user_pub_nonce()],
372 sighash.to_byte_array(),
373 Some(taproot.tap_tweak().to_byte_array()),
374 );
375 BoardCosignResponse { pub_nonce, partial_signature }
376 }
377}
378
379impl BoardBuilder<state::ServerCanBuildVtxos> {
380 pub fn new_from_vtxo(
389 vtxo: &Vtxo<Full>,
390 funding_tx: &Transaction,
391 server_pubkey: PublicKey,
392 ) -> Result<Self, BoardFromVtxoError> {
393 if vtxo.chain_anchor().txid != funding_tx.compute_txid() {
394 return Err(BoardFromVtxoError::FundingTxMismatch {
395 expected: vtxo.chain_anchor().txid,
396 got: funding_tx.compute_txid(),
397 })
398 }
399
400 if vtxo.server_pubkey() != server_pubkey {
401 return Err(BoardFromVtxoError::ServerPubkeyMismatch {
402 expected: server_pubkey,
403 got: vtxo.server_pubkey(),
404 })
405 }
406
407 if vtxo.genesis.items.len() != 1 {
408 return Err(BoardFromVtxoError::IncorrectGenesisItemCount {
409 genesis_count: vtxo.genesis.items.len(),
410 });
411 }
412
413 let fee = vtxo.genesis.items.first().unwrap().fee_amount;
414 let exit_data = compute_exit_data(
415 vtxo.user_pubkey(),
416 server_pubkey,
417 vtxo.expiry_height,
418 vtxo.exit_delta,
419 vtxo.amount() + fee,
420 fee,
421 vtxo.chain_anchor(),
422 );
423
424 let expected_funding_txout = TxOut {
427 value: vtxo.amount() + fee,
428 script_pubkey: exit_data.funding_taproot.script_pubkey(),
429 };
430 let funding_txout = funding_tx.output.get(vtxo.chain_anchor().vout as usize);
431 if funding_txout != Some(&expected_funding_txout) {
432 return Err(BoardFromVtxoError::FundingOutputMismatch {
433 expected: expected_funding_txout,
434 got: funding_txout.cloned(),
435 });
436 }
437
438 let expected_vtxo_id = OutPoint::new(exit_data.txid, BOARD_FUNDING_TX_VTXO_VOUT);
441 if vtxo.point() != expected_vtxo_id {
442 return Err(BoardFromVtxoError::VtxoIdMismatch {
443 expected: expected_vtxo_id,
444 got: vtxo.point(),
445 })
446 }
447
448 Ok(Self {
449 user_pub_nonce: None,
450 user_sec_nonce: None,
451 amount: Some(vtxo.amount() + fee),
452 fee: Some(fee),
453 user_pubkey: vtxo.user_pubkey(),
454 server_pubkey,
455 expiry_height: vtxo.expiry_height,
456 exit_delta: vtxo.exit_delta,
457 utxo: Some(vtxo.chain_anchor()),
458 exit_data: Some(exit_data),
459 signed_vtxo: Some(vtxo.clone()),
460 _state: PhantomData,
461 })
462 }
463
464 pub fn exit_txid(&self) -> Txid {
466 self.exit_data.as_ref().expect("state invariant").txid
467 }
468
469 pub fn build_server_vtxos(&self) -> Vec<ServerVtxo<Full>> {
475 let combined_pubkey = musig::combine_keys([self.user_pubkey, self.server_pubkey])
476 .x_only_public_key().0;
477
478 let vtxo = self.signed_vtxo.as_ref().expect("state invariant").clone();
479
480 vec![
481 Vtxo {
482 policy: ServerVtxoPolicy::new_expiry(combined_pubkey),
483 amount: self.amount.expect("state invariant"),
484 expiry_height: self.expiry_height,
485 server_pubkey: self.server_pubkey,
486 exit_delta: self.exit_delta,
487 anchor_point: self.utxo.expect("state invariant"),
488 genesis: Full { items: vec![] },
489 point: self.utxo.expect("state invariant"),
490 },
491 ServerVtxo::from(vtxo),
492 ]
493 }
494
495 pub fn spend_info(&self) -> Vec<(VtxoId, Txid)> {
497 let exit_txid = self.exit_data.as_ref().expect("state invariant").txid;
498 vec![(self.utxo.expect("state invariant").into(), exit_txid)]
499 }
500}
501
502impl BoardBuilder<state::CanFinish> {
503 pub fn verify_cosign_response(&self, server_cosign: &BoardCosignResponse) -> bool {
505 let exit_data = self.exit_data.as_ref().expect("state invariant");
506 let sighash = exit_data.sighash;
507 let taproot = &exit_data.funding_taproot;
508 scripts::verify_partial_sig(
509 sighash,
510 taproot.tap_tweak(),
511 (self.server_pubkey, &server_cosign.pub_nonce),
512 (self.user_pubkey, self.user_pub_nonce()),
513 &server_cosign.partial_signature
514 )
515 }
516
517 pub fn build_vtxo(
519 mut self,
520 server_cosign: &BoardCosignResponse,
521 user_key: &Keypair,
522 ) -> Result<Vtxo<Full>, IncorrectSigningKeyError> {
523 if user_key.public_key() != self.user_pubkey {
524 return Err(IncorrectSigningKeyError {
525 required: Some(self.user_pubkey),
526 provided: user_key.public_key(),
527 });
528 }
529
530 let exit_data = self.exit_data.as_ref().expect("state invariant");
531 let sighash = exit_data.sighash;
532 let taproot = &exit_data.funding_taproot;
533 let exit_txid = exit_data.txid;
534
535 let agg_nonce = musig::nonce_agg(&[&self.user_pub_nonce(), &server_cosign.pub_nonce]);
536 let (user_sig, final_sig) = musig::partial_sign(
537 [self.user_pubkey, self.server_pubkey],
538 agg_nonce,
539 user_key,
540 self.user_sec_nonce.take().expect("state invariant"),
541 sighash.to_byte_array(),
542 Some(taproot.tap_tweak().to_byte_array()),
543 Some(&[&server_cosign.partial_signature]),
544 );
545 debug_assert!(
546 scripts::verify_partial_sig(
547 sighash,
548 taproot.tap_tweak(),
549 (self.user_pubkey, self.user_pub_nonce()),
550 (self.server_pubkey, &server_cosign.pub_nonce),
551 &user_sig,
552 ),
553 "invalid board partial exit tx signature produced",
554 );
555
556 let final_sig = final_sig.expect("we provided the other sig");
557 debug_assert!(
558 SECP.verify_schnorr(
559 &final_sig, &sighash.into(), &taproot.output_key().to_x_only_public_key(),
560 ).is_ok(),
561 "invalid board exit tx signature produced",
562 );
563
564 let amount = self.amount.expect("state invariant");
565 let fee = self.fee.expect("state invariant");
566 let vtxo_amount = amount.checked_sub(fee).expect("fee cannot exceed amount");
567
568 Ok(Vtxo {
569 amount: vtxo_amount,
570 expiry_height: self.expiry_height,
571 server_pubkey: self.server_pubkey,
572 exit_delta: self.exit_delta,
573 anchor_point: self.utxo.expect("state invariant"),
574 genesis: Full {
575 items: vec![GenesisItem {
576 transition: GenesisTransition::new_cosigned(
577 vec![self.user_pubkey, self.server_pubkey],
578 Some(final_sig),
579 ),
580 output_idx: 0,
581 other_outputs: vec![],
582 fee_amount: fee,
583 }],
584 },
585 policy: VtxoPolicy::new_pubkey(self.user_pubkey),
586 point: OutPoint::new(exit_txid, BOARD_FUNDING_TX_VTXO_VOUT),
587 })
588 }
589}
590
591#[derive(Debug, Clone, thiserror::Error)]
592#[error("board funding tx validation error: {0}")]
593pub struct BoardFundingTxValidationError(String);
594
595
596#[cfg(test)]
597mod test {
598 use std::str::FromStr;
599
600 use bitcoin::{absolute, transaction, Amount};
601
602 use crate::test_util::encoding_roundtrip;
603
604 use super::*;
605
606 #[test]
607 fn test_board_builder() {
608 let user_key = Keypair::from_str("5255d132d6ec7d4fc2a41c8f0018bb14343489ddd0344025cc60c7aa2b3fda6a").unwrap();
612 let server_key = Keypair::from_str("1fb316e653eec61de11c6b794636d230379509389215df1ceb520b65313e5426").unwrap();
613
614 let amount = Amount::from_btc(1.5).unwrap();
616 let fee = Amount::from_btc(0.1).unwrap();
617 let expiry = BlockHeight::new(100_000);
618 let server_pubkey = server_key.public_key();
619 let exit_delta = BlockDelta::new(24);
620 let builder = BoardBuilder::new(
621 user_key.public_key(), expiry, server_pubkey, exit_delta,
622 );
623 let funding_tx = Transaction {
624 version: transaction::Version::TWO,
625 lock_time: absolute::LockTime::ZERO,
626 input: vec![],
627 output: vec![TxOut {
628 value: amount,
629 script_pubkey: builder.funding_script_pubkey(),
630 }],
631 };
632 let utxo = OutPoint::new(funding_tx.compute_txid(), 0);
633 assert_eq!(utxo.to_string(), "8c4b87af4ce8456bbd682859959ba64b95d5425d761a367f4f20b8ffccb1bde0:0");
634 let builder = builder.set_funding_details(amount, fee, utxo).unwrap().generate_user_nonces();
635
636 let cosign = {
638 let server_builder = BoardBuilder::new_for_cosign(
639 builder.user_pubkey, expiry, server_pubkey, exit_delta, amount, fee, utxo, *builder.user_pub_nonce(),
640 );
641 server_builder.server_cosign(&server_key)
642 };
643
644 assert!(builder.verify_cosign_response(&cosign));
646 let vtxo = builder.build_vtxo(&cosign, &user_key).unwrap();
647
648 encoding_roundtrip(&vtxo);
649
650 vtxo.validate(&funding_tx).unwrap();
651 }
652
653 fn create_board_vtxo() -> (Vtxo<Full>, Transaction, Keypair, Keypair) {
655 let user_key = Keypair::from_str("5255d132d6ec7d4fc2a41c8f0018bb14343489ddd0344025cc60c7aa2b3fda6a").unwrap();
656 let server_key = Keypair::from_str("1fb316e653eec61de11c6b794636d230379509389215df1ceb520b65313e5426").unwrap();
657
658 let amount = Amount::from_btc(1.5).unwrap();
659 let fee = Amount::from_btc(0.1).unwrap();
660 let expiry = BlockHeight::new(100_000);
661 let server_pubkey = server_key.public_key();
662 let exit_delta = BlockDelta::new(24);
663
664 let builder = BoardBuilder::new(
665 user_key.public_key(), expiry, server_pubkey, exit_delta,
666 );
667 let funding_tx = Transaction {
668 version: transaction::Version::TWO,
669 lock_time: absolute::LockTime::ZERO,
670 input: vec![],
671 output: vec![TxOut {
672 value: amount,
673 script_pubkey: builder.funding_script_pubkey(),
674 }],
675 };
676 let utxo = OutPoint::new(funding_tx.compute_txid(), 0);
677 let builder = builder.set_funding_details(amount, fee, utxo).unwrap().generate_user_nonces();
678
679 let cosign = {
680 let server_builder = BoardBuilder::new_for_cosign(
681 builder.user_pubkey, expiry, server_pubkey, exit_delta, amount, fee, utxo, *builder.user_pub_nonce(),
682 );
683 server_builder.server_cosign(&server_key)
684 };
685
686 let vtxo = builder.build_vtxo(&cosign, &user_key).unwrap();
687 (vtxo, funding_tx, user_key, server_key)
688 }
689
690 #[test]
691 fn test_new_from_vtxo_success() {
692 let (vtxo, funding_tx, _, server_key) = create_board_vtxo();
693
694 vtxo.validate(&funding_tx).unwrap();
695 println!("amount: {}", vtxo.amount());
696
697 let builder = BoardBuilder::new_from_vtxo(&vtxo, &funding_tx, server_key.public_key())
699 .expect("Is valid");
700
701 let server_vtxos = builder.build_server_vtxos();
702 assert_eq!(server_vtxos.len(), 2);
703 assert!(matches!(server_vtxos[0].policy(), ServerVtxoPolicy::Expiry(..)));
704 assert!(matches!(server_vtxos[1].policy(), ServerVtxoPolicy::User(VtxoPolicy::Pubkey {..})));
705 assert_eq!(server_vtxos[1].id(), vtxo.id());
706 assert_eq!(server_vtxos[1].txout(), vtxo.txout());
707 assert_eq!(server_vtxos[0].txout(), funding_tx.output[0]);
708 assert_eq!(
709 server_vtxos[1].transactions().nth(0).unwrap().tx.compute_txid(),
710 vtxo.transactions().nth(0).unwrap().tx.compute_txid(),
711 );
712 }
713
714 #[test]
715 fn test_new_from_vtxo_txid_mismatch() {
716 let (vtxo, funding_tx, _, server_key) = create_board_vtxo();
717
718 let wrong_funding_tx = Transaction {
720 version: transaction::Version::TWO,
721 lock_time: absolute::LockTime::ZERO,
722 input: vec![],
723 output: vec![TxOut {
724 value: Amount::from_btc(2.0).unwrap(), script_pubkey: funding_tx.output[0].script_pubkey.clone(),
726 }],
727 };
728
729 let result = BoardBuilder::new_from_vtxo(&vtxo, &wrong_funding_tx, server_key.public_key());
730 assert!(matches!(
731 result,
732 Err(BoardFromVtxoError::FundingTxMismatch { expected, got })
733 if expected == vtxo.chain_anchor().txid && got == wrong_funding_tx.compute_txid()
734 ));
735 }
736
737 #[test]
738 fn test_new_from_vtxo_server_pubkey_mismatch() {
739 let (vtxo, funding_tx, _, _) = create_board_vtxo();
740
741 let wrong_server_key = Keypair::from_str("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap();
743
744 let result = BoardBuilder::new_from_vtxo(&vtxo, &funding_tx, wrong_server_key.public_key());
745 assert!(matches!(
746 result,
747 Err(BoardFromVtxoError::ServerPubkeyMismatch { expected, got })
748 if expected == wrong_server_key.public_key() && got == vtxo.server_pubkey()
749 ));
750 }
751
752 #[test]
753 fn test_new_from_vtxo_vtxoid_mismatch() {
754 let (mut vtxo, funding_tx, _, server_key) = create_board_vtxo();
762
763 let original_point = vtxo.point;
765 vtxo.point = OutPoint::new(vtxo.point.txid, vtxo.point.vout + 1);
766
767 let result = BoardBuilder::new_from_vtxo(&vtxo, &funding_tx, server_key.public_key());
768 assert!(matches!(
769 result,
770 Err(BoardFromVtxoError::VtxoIdMismatch { expected, got })
771 if expected == original_point && got == vtxo.point
772 ));
773 }
774
775 #[test]
776 fn test_new_from_vtxo_funding_output_without_server_key() {
777 let (honest, _, user_key, server_key) = create_board_vtxo();
778 let attacker_key = Keypair::from_str("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").unwrap();
779
780 let fee = honest.genesis.items[0].fee_amount;
781 let amount = honest.amount() + fee;
782 let agg_pk = musig::combine_keys([user_key.public_key(), attacker_key.public_key()])
783 .x_only_public_key().0;
784 let funding_taproot = cosign_taproot(agg_pk, server_key.public_key(), honest.expiry_height);
785 let funding_txout = TxOut { value: amount, script_pubkey: funding_taproot.script_pubkey() };
786 let funding_tx = Transaction {
787 version: transaction::Version::TWO,
788 lock_time: absolute::LockTime::ZERO,
789 input: vec![],
790 output: vec![funding_txout.clone()],
791 };
792 let utxo = OutPoint::new(funding_tx.compute_txid(), 0);
793
794 let exit = compute_exit_data(
795 user_key.public_key(), server_key.public_key(), honest.expiry_height,
796 honest.exit_delta, amount, fee, utxo,
797 );
798 let sighash = SighashCache::new(&exit.tx).taproot_key_spend_signature_hash(
799 0, &sighash::Prevouts::All(&[funding_txout]), sighash::TapSighashType::Default,
800 ).unwrap();
801 let sig = musig::cosign_both(
802 &user_key, &attacker_key, sighash.to_byte_array(),
803 Some(funding_taproot.tap_tweak().to_byte_array()),
804 );
805
806 let mut vtxo = honest.clone();
807 vtxo.anchor_point = utxo;
808 vtxo.point = OutPoint::new(exit.txid, BOARD_FUNDING_TX_VTXO_VOUT);
809 vtxo.genesis.items[0].transition = GenesisTransition::new_cosigned(
810 vec![user_key.public_key(), attacker_key.public_key()], Some(sig),
811 );
812
813 vtxo.validate(&funding_tx).expect("signed by keys the attacker controls");
814 let result = BoardBuilder::new_from_vtxo(&vtxo, &funding_tx, server_key.public_key());
815 assert!(matches!(result, Err(BoardFromVtxoError::FundingOutputMismatch { .. })));
816 }
817
818 #[test]
819 fn test_board_funding_error() {
820 fn new_builder_with_funding_details(amount: Amount, fee: Amount) -> Result<BoardBuilder<state::CanGenerateNonces>, BoardFundingError> {
821 let user_key = Keypair::from_str("5255d132d6ec7d4fc2a41c8f0018bb14343489ddd0344025cc60c7aa2b3fda6a").unwrap();
822 let server_key = Keypair::from_str("1fb316e653eec61de11c6b794636d230379509389215df1ceb520b65313e5426").unwrap();
823 let expiry = BlockHeight::new(100_000);
824 let server_pubkey = server_key.public_key();
825 let exit_delta = BlockDelta::new(24);
826 let builder = BoardBuilder::new(
827 user_key.public_key(), expiry, server_pubkey, exit_delta,
828 );
829 let funding_tx = Transaction {
830 version: transaction::Version::TWO,
831 lock_time: absolute::LockTime::ZERO,
832 input: vec![],
833 output: vec![TxOut {
834 value: amount,
835 script_pubkey: builder.funding_script_pubkey(),
836 }],
837 };
838 let utxo = OutPoint::new(funding_tx.compute_txid(), 0);
839 builder.set_funding_details(amount, fee, utxo)
840 }
841
842 let fee = Amount::ONE_BTC;
843
844 let zero_amount_err = new_builder_with_funding_details(Amount::ZERO, fee).err();
845 assert_eq!(zero_amount_err, Some(BoardFundingError::ZeroAmount));
846
847 let fee_higher_err = new_builder_with_funding_details(Amount::ONE_SAT, fee).err();
848 assert_eq!(fee_higher_err, Some(BoardFundingError::FeeHigherThanAmount { amount: Amount::ONE_SAT, fee }));
849
850 let zero_amount_after_fee_err = new_builder_with_funding_details(fee, fee).err();
851 assert_eq!(zero_amount_after_fee_err, Some(BoardFundingError::ZeroAmountAfterFee { amount: fee, fee }));
852 }
853}
854