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}
115
116#[derive(Debug)]
118pub struct BoardCosignResponse {
119 pub pub_nonce: musig::PublicNonce,
120 pub partial_signature: musig::PartialSignature,
121}
122
123pub mod state {
124 mod sealed {
125 pub trait Sealed {}
127 impl Sealed for super::Preparing {}
128 impl Sealed for super::CanGenerateNonces {}
129 impl Sealed for super::ServerCanCosign {}
130 impl Sealed for super::CanFinish {}
131 impl Sealed for super::ServerCanBuildVtxos {}
132 }
133
134 pub trait BuilderState: sealed::Sealed {}
136
137 pub struct Preparing;
139 impl BuilderState for Preparing {}
140
141 pub struct CanGenerateNonces;
144 impl BuilderState for CanGenerateNonces {}
145
146 pub struct ServerCanCosign;
148 impl BuilderState for ServerCanCosign {}
149
150 pub struct CanFinish;
153 impl BuilderState for CanFinish {}
154
155 pub struct ServerCanBuildVtxos;
157 impl BuilderState for ServerCanBuildVtxos {}
158
159 pub trait CanSign: BuilderState {}
162 impl CanSign for ServerCanCosign {}
163 impl CanSign for CanFinish {}
164
165 pub trait HasFundingDetails: BuilderState {}
167 impl HasFundingDetails for CanGenerateNonces {}
168 impl HasFundingDetails for ServerCanCosign {}
169 impl HasFundingDetails for CanFinish {}
170}
171
172#[derive(Debug)]
180pub struct BoardBuilder<S: BuilderState> {
181 pub user_pubkey: PublicKey,
182 pub expiry_height: BlockHeight,
183 pub server_pubkey: PublicKey,
184 pub exit_delta: BlockDelta,
185
186 amount: Option<Amount>,
187 fee: Option<Amount>,
188 utxo: Option<OutPoint>,
189
190 user_pub_nonce: Option<musig::PublicNonce>,
191 user_sec_nonce: Option<musig::SecretNonce>,
192
193 exit_data: Option<ExitData>,
195
196 signed_vtxo: Option<Vtxo<Full>>,
198
199 _state: PhantomData<S>,
200}
201
202impl<S: BuilderState> BoardBuilder<S> {
203 pub fn funding_script_pubkey(&self) -> ScriptBuf {
205 let combined_pubkey = musig::combine_keys([self.user_pubkey, self.server_pubkey])
206 .x_only_public_key().0;
207 cosign_taproot(combined_pubkey, self.server_pubkey, self.expiry_height).script_pubkey()
208 }
209
210 fn to_state<S2: BuilderState>(self) -> BoardBuilder<S2> {
211 BoardBuilder {
212 user_pubkey: self.user_pubkey,
213 expiry_height: self.expiry_height,
214 server_pubkey: self.server_pubkey,
215 exit_delta: self.exit_delta,
216 amount: self.amount,
217 utxo: self.utxo,
218 fee: self.fee,
219 user_pub_nonce: self.user_pub_nonce,
220 user_sec_nonce: self.user_sec_nonce,
221 exit_data: self.exit_data,
222 signed_vtxo: self.signed_vtxo,
223 _state: PhantomData,
224 }
225 }
226}
227
228impl BoardBuilder<state::Preparing> {
229 pub fn new(
233 user_pubkey: PublicKey,
234 expiry_height: BlockHeight,
235 server_pubkey: PublicKey,
236 exit_delta: BlockDelta,
237 ) -> BoardBuilder<state::Preparing> {
238 BoardBuilder {
239 user_pubkey, expiry_height, server_pubkey, exit_delta,
240 amount: None,
241 utxo: None,
242 fee: None,
243 user_pub_nonce: None,
244 user_sec_nonce: None,
245 exit_data: None,
246 signed_vtxo: None,
247 _state: PhantomData,
248 }
249 }
250
251 pub fn set_funding_details(
254 mut self,
255 amount: Amount,
256 fee: Amount,
257 utxo: OutPoint,
258 ) -> Result<BoardBuilder<state::CanGenerateNonces>, BoardFundingError> {
259 if amount == Amount::ZERO {
260 return Err(BoardFundingError::ZeroAmount);
261 } else if fee > amount {
262 return Err(BoardFundingError::FeeHigherThanAmount { amount, fee });
263 } else if amount - fee == Amount::ZERO {
264 return Err(BoardFundingError::ZeroAmountAfterFee { amount, fee });
265 }
266
267 let exit_data = compute_exit_data(
268 self.user_pubkey, self.server_pubkey, self.expiry_height,
269 self.exit_delta, amount, fee, utxo,
270 );
271
272 self.amount = Some(amount);
273 self.utxo = Some(utxo);
274 self.fee = Some(fee);
275 self.exit_data = Some(exit_data);
276
277 Ok(self.to_state())
278 }
279}
280
281impl BoardBuilder<state::CanGenerateNonces> {
282 pub fn generate_user_nonces(mut self) -> BoardBuilder<state::CanFinish> {
284 let exit_data = self.exit_data.as_ref().expect("state invariant");
285 let funding_taproot = &exit_data.funding_taproot;
286 let exit_sighash = exit_data.sighash;
287
288 let (agg, _) = musig::tweaked_key_agg(
289 [self.user_pubkey, self.server_pubkey],
290 funding_taproot.tap_tweak().to_byte_array(),
291 );
292 let (sec_nonce, pub_nonce) = agg.nonce_gen(
294 musig::SessionSecretRand::assume_unique_per_nonce_gen(rand::random()),
295 musig::pubkey_to(self.user_pubkey),
296 &exit_sighash.to_byte_array(),
297 None,
298 );
299
300 self.user_pub_nonce = Some(pub_nonce);
301 self.user_sec_nonce = Some(sec_nonce);
302 self.to_state()
303 }
304
305 pub fn exit_tx(&self) -> &Transaction {
310 &self.exit_data.as_ref().expect("state invariant").tx
311 }
312
313 pub fn spend_info(&self) -> Vec<(VtxoId, Txid)> {
315 let exit_txid = self.exit_data.as_ref().expect("state invariant").txid;
316 vec![(self.utxo.expect("state invariant").into(), exit_txid)]
317 }
318}
319
320impl<S: state::CanSign> BoardBuilder<S> {
321 pub fn user_pub_nonce(&self) -> &musig::PublicNonce {
322 self.user_pub_nonce.as_ref().expect("state invariant")
323 }
324}
325
326impl BoardBuilder<state::ServerCanCosign> {
327 pub fn new_for_cosign(
330 user_pubkey: PublicKey,
331 expiry_height: BlockHeight,
332 server_pubkey: PublicKey,
333 exit_delta: BlockDelta,
334 amount: Amount,
335 fee: Amount,
336 utxo: OutPoint,
337 user_pub_nonce: musig::PublicNonce,
338 ) -> BoardBuilder<state::ServerCanCosign> {
339 let exit_data = compute_exit_data(
340 user_pubkey, server_pubkey, expiry_height, exit_delta, amount, fee, utxo,
341 );
342
343 BoardBuilder {
344 user_pubkey, expiry_height, server_pubkey, exit_delta,
345 amount: Some(amount),
346 fee: Some(fee),
347 utxo: Some(utxo),
348 user_pub_nonce: Some(user_pub_nonce),
349 user_sec_nonce: None,
350 exit_data: Some(exit_data),
351 signed_vtxo: None,
352 _state: PhantomData,
353 }
354 }
355
356 pub fn server_cosign(&self, key: &Keypair) -> BoardCosignResponse {
360 let exit_data = self.exit_data.as_ref().expect("state invariant");
361 let sighash = exit_data.sighash;
362 let taproot = &exit_data.funding_taproot;
363 let (pub_nonce, partial_signature) = musig::deterministic_partial_sign(
364 key,
365 [self.user_pubkey],
366 &[&self.user_pub_nonce()],
367 sighash.to_byte_array(),
368 Some(taproot.tap_tweak().to_byte_array()),
369 );
370 BoardCosignResponse { pub_nonce, partial_signature }
371 }
372}
373
374impl BoardBuilder<state::ServerCanBuildVtxos> {
375 pub fn new_from_vtxo(
384 vtxo: &Vtxo<Full>,
385 funding_tx: &Transaction,
386 server_pubkey: PublicKey,
387 ) -> Result<Self, BoardFromVtxoError> {
388 if vtxo.chain_anchor().txid != funding_tx.compute_txid() {
389 return Err(BoardFromVtxoError::FundingTxMismatch {
390 expected: vtxo.chain_anchor().txid,
391 got: funding_tx.compute_txid(),
392 })
393 }
394
395 if vtxo.server_pubkey() != server_pubkey {
396 return Err(BoardFromVtxoError::ServerPubkeyMismatch {
397 expected: server_pubkey,
398 got: vtxo.server_pubkey(),
399 })
400 }
401
402 if vtxo.genesis.items.len() != 1 {
403 return Err(BoardFromVtxoError::IncorrectGenesisItemCount {
404 genesis_count: vtxo.genesis.items.len(),
405 });
406 }
407
408 let fee = vtxo.genesis.items.first().unwrap().fee_amount;
409 let exit_data = compute_exit_data(
410 vtxo.user_pubkey(),
411 server_pubkey,
412 vtxo.expiry_height,
413 vtxo.exit_delta,
414 vtxo.amount() + fee,
415 fee,
416 vtxo.chain_anchor(),
417 );
418
419 let expected_vtxo_id = OutPoint::new(exit_data.txid, BOARD_FUNDING_TX_VTXO_VOUT);
422 if vtxo.point() != expected_vtxo_id {
423 return Err(BoardFromVtxoError::VtxoIdMismatch {
424 expected: expected_vtxo_id,
425 got: vtxo.point(),
426 })
427 }
428
429 Ok(Self {
430 user_pub_nonce: None,
431 user_sec_nonce: None,
432 amount: Some(vtxo.amount() + fee),
433 fee: Some(fee),
434 user_pubkey: vtxo.user_pubkey(),
435 server_pubkey,
436 expiry_height: vtxo.expiry_height,
437 exit_delta: vtxo.exit_delta,
438 utxo: Some(vtxo.chain_anchor()),
439 exit_data: Some(exit_data),
440 signed_vtxo: Some(vtxo.clone()),
441 _state: PhantomData,
442 })
443 }
444
445 pub fn exit_txid(&self) -> Txid {
447 self.exit_data.as_ref().expect("state invariant").txid
448 }
449
450 pub fn build_server_vtxos(&self) -> Vec<ServerVtxo<Full>> {
456 let combined_pubkey = musig::combine_keys([self.user_pubkey, self.server_pubkey])
457 .x_only_public_key().0;
458
459 let vtxo = self.signed_vtxo.as_ref().expect("state invariant").clone();
460
461 vec![
462 Vtxo {
463 policy: ServerVtxoPolicy::new_expiry(combined_pubkey),
464 amount: self.amount.expect("state invariant"),
465 expiry_height: self.expiry_height,
466 server_pubkey: self.server_pubkey,
467 exit_delta: self.exit_delta,
468 anchor_point: self.utxo.expect("state invariant"),
469 genesis: Full { items: vec![] },
470 point: self.utxo.expect("state invariant"),
471 },
472 ServerVtxo::from(vtxo),
473 ]
474 }
475
476 pub fn spend_info(&self) -> Vec<(VtxoId, Txid)> {
478 let exit_txid = self.exit_data.as_ref().expect("state invariant").txid;
479 vec![(self.utxo.expect("state invariant").into(), exit_txid)]
480 }
481}
482
483impl BoardBuilder<state::CanFinish> {
484 pub fn verify_cosign_response(&self, server_cosign: &BoardCosignResponse) -> bool {
486 let exit_data = self.exit_data.as_ref().expect("state invariant");
487 let sighash = exit_data.sighash;
488 let taproot = &exit_data.funding_taproot;
489 scripts::verify_partial_sig(
490 sighash,
491 taproot.tap_tweak(),
492 (self.server_pubkey, &server_cosign.pub_nonce),
493 (self.user_pubkey, self.user_pub_nonce()),
494 &server_cosign.partial_signature
495 )
496 }
497
498 pub fn build_vtxo(
500 mut self,
501 server_cosign: &BoardCosignResponse,
502 user_key: &Keypair,
503 ) -> Result<Vtxo<Full>, IncorrectSigningKeyError> {
504 if user_key.public_key() != self.user_pubkey {
505 return Err(IncorrectSigningKeyError {
506 required: Some(self.user_pubkey),
507 provided: user_key.public_key(),
508 });
509 }
510
511 let exit_data = self.exit_data.as_ref().expect("state invariant");
512 let sighash = exit_data.sighash;
513 let taproot = &exit_data.funding_taproot;
514 let exit_txid = exit_data.txid;
515
516 let agg_nonce = musig::nonce_agg(&[&self.user_pub_nonce(), &server_cosign.pub_nonce]);
517 let (user_sig, final_sig) = musig::partial_sign(
518 [self.user_pubkey, self.server_pubkey],
519 agg_nonce,
520 user_key,
521 self.user_sec_nonce.take().expect("state invariant"),
522 sighash.to_byte_array(),
523 Some(taproot.tap_tweak().to_byte_array()),
524 Some(&[&server_cosign.partial_signature]),
525 );
526 debug_assert!(
527 scripts::verify_partial_sig(
528 sighash,
529 taproot.tap_tweak(),
530 (self.user_pubkey, self.user_pub_nonce()),
531 (self.server_pubkey, &server_cosign.pub_nonce),
532 &user_sig,
533 ),
534 "invalid board partial exit tx signature produced",
535 );
536
537 let final_sig = final_sig.expect("we provided the other sig");
538 debug_assert!(
539 SECP.verify_schnorr(
540 &final_sig, &sighash.into(), &taproot.output_key().to_x_only_public_key(),
541 ).is_ok(),
542 "invalid board exit tx signature produced",
543 );
544
545 let amount = self.amount.expect("state invariant");
546 let fee = self.fee.expect("state invariant");
547 let vtxo_amount = amount.checked_sub(fee).expect("fee cannot exceed amount");
548
549 Ok(Vtxo {
550 amount: vtxo_amount,
551 expiry_height: self.expiry_height,
552 server_pubkey: self.server_pubkey,
553 exit_delta: self.exit_delta,
554 anchor_point: self.utxo.expect("state invariant"),
555 genesis: Full {
556 items: vec![GenesisItem {
557 transition: GenesisTransition::new_cosigned(
558 vec![self.user_pubkey, self.server_pubkey],
559 Some(final_sig),
560 ),
561 output_idx: 0,
562 other_outputs: vec![],
563 fee_amount: fee,
564 }],
565 },
566 policy: VtxoPolicy::new_pubkey(self.user_pubkey),
567 point: OutPoint::new(exit_txid, BOARD_FUNDING_TX_VTXO_VOUT),
568 })
569 }
570}
571
572#[derive(Debug, Clone, thiserror::Error)]
573#[error("board funding tx validation error: {0}")]
574pub struct BoardFundingTxValidationError(String);
575
576
577#[cfg(test)]
578mod test {
579 use std::str::FromStr;
580
581 use bitcoin::{absolute, transaction, Amount};
582
583 use crate::test_util::encoding_roundtrip;
584
585 use super::*;
586
587 #[test]
588 fn test_board_builder() {
589 let user_key = Keypair::from_str("5255d132d6ec7d4fc2a41c8f0018bb14343489ddd0344025cc60c7aa2b3fda6a").unwrap();
593 let server_key = Keypair::from_str("1fb316e653eec61de11c6b794636d230379509389215df1ceb520b65313e5426").unwrap();
594
595 let amount = Amount::from_btc(1.5).unwrap();
597 let fee = Amount::from_btc(0.1).unwrap();
598 let expiry = 100_000;
599 let server_pubkey = server_key.public_key();
600 let exit_delta = 24;
601 let builder = BoardBuilder::new(
602 user_key.public_key(), expiry, server_pubkey, exit_delta,
603 );
604 let funding_tx = Transaction {
605 version: transaction::Version::TWO,
606 lock_time: absolute::LockTime::ZERO,
607 input: vec![],
608 output: vec![TxOut {
609 value: amount,
610 script_pubkey: builder.funding_script_pubkey(),
611 }],
612 };
613 let utxo = OutPoint::new(funding_tx.compute_txid(), 0);
614 assert_eq!(utxo.to_string(), "8c4b87af4ce8456bbd682859959ba64b95d5425d761a367f4f20b8ffccb1bde0:0");
615 let builder = builder.set_funding_details(amount, fee, utxo).unwrap().generate_user_nonces();
616
617 let cosign = {
619 let server_builder = BoardBuilder::new_for_cosign(
620 builder.user_pubkey, expiry, server_pubkey, exit_delta, amount, fee, utxo, *builder.user_pub_nonce(),
621 );
622 server_builder.server_cosign(&server_key)
623 };
624
625 assert!(builder.verify_cosign_response(&cosign));
627 let vtxo = builder.build_vtxo(&cosign, &user_key).unwrap();
628
629 encoding_roundtrip(&vtxo);
630
631 vtxo.validate(&funding_tx).unwrap();
632 }
633
634 fn create_board_vtxo() -> (Vtxo<Full>, Transaction, Keypair, Keypair) {
636 let user_key = Keypair::from_str("5255d132d6ec7d4fc2a41c8f0018bb14343489ddd0344025cc60c7aa2b3fda6a").unwrap();
637 let server_key = Keypair::from_str("1fb316e653eec61de11c6b794636d230379509389215df1ceb520b65313e5426").unwrap();
638
639 let amount = Amount::from_btc(1.5).unwrap();
640 let fee = Amount::from_btc(0.1).unwrap();
641 let expiry = 100_000;
642 let server_pubkey = server_key.public_key();
643 let exit_delta = 24;
644
645 let builder = BoardBuilder::new(
646 user_key.public_key(), expiry, server_pubkey, exit_delta,
647 );
648 let funding_tx = Transaction {
649 version: transaction::Version::TWO,
650 lock_time: absolute::LockTime::ZERO,
651 input: vec![],
652 output: vec![TxOut {
653 value: amount,
654 script_pubkey: builder.funding_script_pubkey(),
655 }],
656 };
657 let utxo = OutPoint::new(funding_tx.compute_txid(), 0);
658 let builder = builder.set_funding_details(amount, fee, utxo).unwrap().generate_user_nonces();
659
660 let cosign = {
661 let server_builder = BoardBuilder::new_for_cosign(
662 builder.user_pubkey, expiry, server_pubkey, exit_delta, amount, fee, utxo, *builder.user_pub_nonce(),
663 );
664 server_builder.server_cosign(&server_key)
665 };
666
667 let vtxo = builder.build_vtxo(&cosign, &user_key).unwrap();
668 (vtxo, funding_tx, user_key, server_key)
669 }
670
671 #[test]
672 fn test_new_from_vtxo_success() {
673 let (vtxo, funding_tx, _, server_key) = create_board_vtxo();
674
675 vtxo.validate(&funding_tx).unwrap();
676 println!("amount: {}", vtxo.amount());
677
678 let builder = BoardBuilder::new_from_vtxo(&vtxo, &funding_tx, server_key.public_key())
680 .expect("Is valid");
681
682 let server_vtxos = builder.build_server_vtxos();
683 assert_eq!(server_vtxos.len(), 2);
684 assert!(matches!(server_vtxos[0].policy(), ServerVtxoPolicy::Expiry(..)));
685 assert!(matches!(server_vtxos[1].policy(), ServerVtxoPolicy::User(VtxoPolicy::Pubkey {..})));
686 assert_eq!(server_vtxos[1].id(), vtxo.id());
687 assert_eq!(server_vtxos[1].txout(), vtxo.txout());
688 assert_eq!(server_vtxos[0].txout(), funding_tx.output[0]);
689 assert_eq!(
690 server_vtxos[1].transactions().nth(0).unwrap().tx.compute_txid(),
691 vtxo.transactions().nth(0).unwrap().tx.compute_txid(),
692 );
693 }
694
695 #[test]
696 fn test_new_from_vtxo_txid_mismatch() {
697 let (vtxo, funding_tx, _, server_key) = create_board_vtxo();
698
699 let wrong_funding_tx = Transaction {
701 version: transaction::Version::TWO,
702 lock_time: absolute::LockTime::ZERO,
703 input: vec![],
704 output: vec![TxOut {
705 value: Amount::from_btc(2.0).unwrap(), script_pubkey: funding_tx.output[0].script_pubkey.clone(),
707 }],
708 };
709
710 let result = BoardBuilder::new_from_vtxo(&vtxo, &wrong_funding_tx, server_key.public_key());
711 assert!(matches!(
712 result,
713 Err(BoardFromVtxoError::FundingTxMismatch { expected, got })
714 if expected == vtxo.chain_anchor().txid && got == wrong_funding_tx.compute_txid()
715 ));
716 }
717
718 #[test]
719 fn test_new_from_vtxo_server_pubkey_mismatch() {
720 let (vtxo, funding_tx, _, _) = create_board_vtxo();
721
722 let wrong_server_key = Keypair::from_str("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap();
724
725 let result = BoardBuilder::new_from_vtxo(&vtxo, &funding_tx, wrong_server_key.public_key());
726 assert!(matches!(
727 result,
728 Err(BoardFromVtxoError::ServerPubkeyMismatch { expected, got })
729 if expected == wrong_server_key.public_key() && got == vtxo.server_pubkey()
730 ));
731 }
732
733 #[test]
734 fn test_new_from_vtxo_vtxoid_mismatch() {
735 let (mut vtxo, funding_tx, _, server_key) = create_board_vtxo();
743
744 let original_point = vtxo.point;
746 vtxo.point = OutPoint::new(vtxo.point.txid, vtxo.point.vout + 1);
747
748 let result = BoardBuilder::new_from_vtxo(&vtxo, &funding_tx, server_key.public_key());
749 assert!(matches!(
750 result,
751 Err(BoardFromVtxoError::VtxoIdMismatch { expected, got })
752 if expected == original_point && got == vtxo.point
753 ));
754 }
755
756 #[test]
757 fn test_board_funding_error() {
758 fn new_builder_with_funding_details(amount: Amount, fee: Amount) -> Result<BoardBuilder<state::CanGenerateNonces>, BoardFundingError> {
759 let user_key = Keypair::from_str("5255d132d6ec7d4fc2a41c8f0018bb14343489ddd0344025cc60c7aa2b3fda6a").unwrap();
760 let server_key = Keypair::from_str("1fb316e653eec61de11c6b794636d230379509389215df1ceb520b65313e5426").unwrap();
761 let expiry = 100_000;
762 let server_pubkey = server_key.public_key();
763 let exit_delta = 24;
764 let builder = BoardBuilder::new(
765 user_key.public_key(), expiry, server_pubkey, exit_delta,
766 );
767 let funding_tx = Transaction {
768 version: transaction::Version::TWO,
769 lock_time: absolute::LockTime::ZERO,
770 input: vec![],
771 output: vec![TxOut {
772 value: amount,
773 script_pubkey: builder.funding_script_pubkey(),
774 }],
775 };
776 let utxo = OutPoint::new(funding_tx.compute_txid(), 0);
777 builder.set_funding_details(amount, fee, utxo)
778 }
779
780 let fee = Amount::ONE_BTC;
781
782 let zero_amount_err = new_builder_with_funding_details(Amount::ZERO, fee).err();
783 assert_eq!(zero_amount_err, Some(BoardFundingError::ZeroAmount));
784
785 let fee_higher_err = new_builder_with_funding_details(Amount::ONE_SAT, fee).err();
786 assert_eq!(fee_higher_err, Some(BoardFundingError::FeeHigherThanAmount { amount: Amount::ONE_SAT, fee }));
787
788 let zero_amount_after_fee_err = new_builder_with_funding_details(fee, fee).err();
789 assert_eq!(zero_amount_after_fee_err, Some(BoardFundingError::ZeroAmountAfterFee { amount: fee, fee }));
790 }
791}
792