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